diff --git a/CHANGELOG.md b/CHANGELOG.md index c2d34d7..39c6ad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,36 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.ht --- +## [2.38.0] — 2026-08-07 + +Gutenberg-Block „Datensatz-Karte" — keine IDs mehr abtippen. + +### ✨ Added +- **Block „Datensatz-Karte"** als Alternative zum Shortcode `[odw_dataset id="123"]`. Im Editor + über das Plus-Symbol einfügen und den Datensatz aus einer Liste wählen; zur Auswahl stehen nur + veröffentlichte Datensätze. Wer den Shortcode bevorzugt oder klassische Editoren nutzt, kann + ihn unverändert weiterverwenden. + +### ℹ️ Umsetzung +- **Dynamischer Block:** Gespeichert wird ausschließlich die Datensatz-ID, das Markup entsteht + beim Ausliefern über `ODW_Shortcode::render()`. Damit gibt es genau eine Quelle für die Karte — + Änderungen daran wirken auf Block und Shortcode gleichermaßen —, und ein später umbenannter + Datensatz erscheint mit seinem aktuellen Titel statt als eingefrorene Kopie im Beitrag. +- **Kein Build-Schritt.** `blocks/dataset-card/` enthält `block.json` und ein Editor-Skript in + schlichtem JavaScript statt JSX. Das Projekt hat keine JS-Build-Kette für den Admin-Bereich, + und eine allein für diesen Block einzuführen wäre viel Apparat für wenig Ertrag. +- **Auswahlliste über `wp_localize_script`,** nicht über den Core-Datenspeicher: Der Custom Post + Type ist bewusst nicht über die WP-REST-API exponiert (`show_in_rest => false`), dafür gibt es + die eigenen Endpunkte. Die Liste wird nur im Editor aufgebaut (`enqueue_block_editor_assets`) + und ist auf 200 Einträge begrenzt. +- **Platzhalter statt Live-Vorschau im Editor.** Eine serverseitig gerenderte Vorschau bekäme das + Frontend-Stylesheet im Editor-Rahmen nicht mit und sähe dort kaputt aus; die Platzhalterkarte + nutzt WordPress' eigene `Placeholder`-Komponente und braucht deshalb kein eigenes CSS. +- `bin/build-release.sh` kopiert `blocks/` mit ins Paket — ohne diese Ergänzung wäre der Block in + der Installation schlicht nicht vorhanden gewesen. + +--- + ## [2.37.0] — 2026-08-07 Alle 24 EU-Amtssprachen statt zwei. diff --git a/CLAUDE.md b/CLAUDE.md index b4e1e9b..4c81a6f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -147,6 +147,7 @@ Externe Harvester rufen /catalog, /datasets/, oder /delta ab | **ODW_Rdf** | JSON-LD → Turtle (dependency-frei) für RDF-Harvester | `to_turtle()` | | **ODW_Field_Reference** | Erzeugt `docs/FELD-REFERENZ.md` aus `config/field-catalog.php` | `build()`, `write()`, `js_map()` | | **ODW_Settings** | Plugin-Einstellungsseite | `get()`, `filter_catalog_title()` | +| **ODW_Block** | Gutenberg-Block „Datensatz-Karte“ (dynamisch, rendert über ODW_Shortcode) | `register()`, `localize()`, `render()` | | **ODW_Shortcode** | Frontend Download-Card: `[odw_dataset id="123"]` | `render()` | | **ODW_Batch_Import** | CSV/JSON Batch-Import: Parsing, Validierung, Bulk-Insert | `parse_file()`, `validate_row()`, `import_records()` | | **ODW_CLI** | WP-CLI Befehle (Qualitäts-Recalc, Cache-Clear) | `quality_recalculate()`, `cache_clear()` | @@ -730,7 +731,7 @@ Update **both** locations: - MINOR: New features (backward-compatible) - PATCH: Bug fixes only -Current: **v2.37.0** +Current: **v2.38.0** --- @@ -769,6 +770,10 @@ Neue technische Festlegungen gehören dorthin, nicht in README oder CLAUDE.md. - **Gebündelte Vokabulare vervollständigt (v2.37.0):** `access-right` (3 Stufen) und `language` (alle 24 EU-Amtssprachen) liegen unter `config/vocabularies/`. Die Sprachauswahl bot vorher nur Deutsch und Englisch, obwohl `odw_resolve_language_tag()` alle 24 Codes bereits umsetzen konnte. +- **Gutenberg-Block (v2.38.0):** „Datensatz-Karte“ als Alternative zum Shortcode. Dynamischer + Block ohne Build-Schritt — `blocks/dataset-card/` mit `block.json` und schlichtem JS; das + Rendern delegiert an `ODW_Shortcode::render()`. Die Auswahlliste kommt über + `wp_localize_script`, weil der CPT bewusst nicht über die WP-REST-API exponiert ist. - **Content Negotiation (v2.35.0):** Alle drei Endpunkte liefern `jsonld`/`json`/`turtle`; ohne `?format=` entscheidet der `Accept`-Header (q-Werte, `Vary: Accept`). Explizites `?format=` hat Vorrang. @@ -777,7 +782,6 @@ Neue technische Festlegungen gehören dorthin, nicht in README oder CLAUDE.md. - **Optional: RDF/XML** als weitere Serialisierung (Turtle und JSON-LD decken die gängigen Harvester bereits ab). -- **Gutenberg-Block** für die Download-Card (Alternative zum Shortcode). - **Mehrsprachigkeit der Oberfläche** (WPML/Polylang) — das Datenmodell unterstützt Mehrsprachigkeit bereits, die Integration fehlt. - **Optional/künftig:** Registry-getriebenes Formular-/JSON-LD-Rendering (Aufräumarbeit ohne @@ -872,6 +876,6 @@ Neue technische Festlegungen gehören dorthin, nicht in README oder CLAUDE.md. --- -**Zuletzt aktualisiert**: Version 2.37.0 (August 2026) +**Zuletzt aktualisiert**: Version 2.38.0 (August 2026) **Autor**: Open Data Wizard Team (nozilla) **License**: GPL-2.0-or-later diff --git a/README.md b/README.md index ad0231f..e32c832 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ![PHP Version](https://img.shields.io/badge/PHP-%3E%3D%208.1-8892BF?style=flat-square&logo=php&logoColor=white) ![WordPress](https://img.shields.io/badge/WordPress-compatible-21759B?style=flat-square&logo=wordpress&logoColor=white) ![DCAT-AP](https://img.shields.io/badge/DCAT--AP-3.0-brightgreen?style=flat-square) -![Version](https://img.shields.io/badge/Version-2.37.0-brightgreen?style=flat-square) +![Version](https://img.shields.io/badge/Version-2.38.0-brightgreen?style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-willkommen-brightgreen?style=flat-square) 📖 [Dokumentation](DOCUMENTATION.md) · 📋 [Feld-Referenz](docs/FELD-REFERENZ.md) · 📐 [Technische Spezifikation](TECHNICAL-SPEC.md) · 📝 [Changelog](CHANGELOG.md) · 🛡️ [Security](SECURITY.md) · ⚖️ [Lizenz](LICENSE) @@ -171,11 +171,27 @@ Automatische Metadaten-Qualitätsprüfung nach der [EU-MQA-Methodik](https://dat Berechnung nach jedem Speichern. Die **Listenspalte** zeigt Prozent + Stufe; die **Qualitäts-Meta-Box** ergänzt den MQA-Rohwert (z. B. „54 / 259 Punkte, von max. 405") als Detailzeile. -### 📥 Download-Card Shortcode +### 📥 Download-Card — Block oder Shortcode + +Zwei Wege zur selben Karte. **Empfohlen ist der Block:** + +Im Beitrags- oder Seiteneditor über das Plus-Symbol **„Datensatz-Karte"** einfügen und rechts in +der Seitenleiste den Datensatz aus einer Liste wählen — keine ID zum Abtippen. Zur Auswahl stehen +nur veröffentlichte Datensätze. + +Alternativ der Shortcode, etwa in klassischen Editoren oder Widgets: + ``` [odw_dataset id="123"] ``` -Rendert eine strukturierte Download-Card im Frontend: Titel, Thema-Badge, Lizenz, Schlagwörter als Tag-Pillen, Download-Button sowie einen **Metadaten-Download-Button (JSON-LD)**. CSS (`assets/css/frontend.css`) wird nur auf Seiten geladen, die den Shortcode enthalten. + +Beide rendern dieselbe strukturierte Download-Card im Frontend: Titel, Thema-Badge, Lizenz, +Schlagwörter als Tag-Pillen, Download-Button sowie einen **Metadaten-Download-Button (JSON-LD)**. +CSS (`assets/css/frontend.css`) wird nur auf Seiten geladen, die die Karte auch enthalten. + +Der Block speichert ausschließlich die Datensatz-ID und rendert beim Ausliefern — ein später +umbenannter Datensatz erscheint also mit seinem aktuellen Titel, statt eine eingefrorene Kopie im +Beitrag zu hinterlassen. ### 🔗 REST API Endpoints diff --git a/bin/build-release.sh b/bin/build-release.sh index 24dbc1f..bbacf94 100755 --- a/bin/build-release.sh +++ b/bin/build-release.sh @@ -3,7 +3,7 @@ # build-release.sh — erzeugt ein installationsfertiges WordPress-Plugin-ZIP. # # Enthält ausschließlich die zur Laufzeit benötigten Dateien: -# - Plugin-Bootstrap, Klassen, Assets, Sprachen, Beispiel-Dateien +# - Plugin-Bootstrap, Klassen, Assets, Blöcke, Sprachen, Beispiel-Dateien # - config/ (nur Datendateien; Dev-Configs und SHACL-Referenz entfallen) # - vendor/ mit ausschließlich der Produktionsabhängigkeit (Carbon Fields), # via `composer install --no-dev`, inkl. Bereinigung der Carbon-Fields-Dev-Dateien @@ -43,7 +43,7 @@ mkdir -p "$PLUG_DIR" # --- Runtime-Quelldateien kopieren (Allowlist) ----------------------------- cp "$ROOT_DIR/$SLUG.php" "$ROOT_DIR/uninstall.php" "$ROOT_DIR/README.md" "$ROOT_DIR/LICENSE" "$PLUG_DIR/" -cp -R "$ROOT_DIR/includes" "$ROOT_DIR/assets" "$ROOT_DIR/languages" "$ROOT_DIR/samples" "$PLUG_DIR/" +cp -R "$ROOT_DIR/includes" "$ROOT_DIR/assets" "$ROOT_DIR/blocks" "$ROOT_DIR/languages" "$ROOT_DIR/samples" "$PLUG_DIR/" # config/ vollständig kopieren, dann Dev-/Referenz-Anteile entfernen cp -R "$ROOT_DIR/config" "$PLUG_DIR/" diff --git a/blocks/dataset-card/block.json b/blocks/dataset-card/block.json new file mode 100644 index 0000000..5801f2a --- /dev/null +++ b/blocks/dataset-card/block.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "odw/dataset-card", + "title": "Datensatz-Karte", + "category": "widgets", + "icon": "database", + "description": "Zeigt einen veröffentlichten Datensatz als Download-Karte — die Alternative zum Shortcode [odw_dataset].", + "keywords": [ "Datensatz", "Open Data", "Download", "DCAT" ], + "textdomain": "open-data-wizard", + "supports": { + "html": false, + "align": [ "wide" ], + "spacing": { + "margin": true + } + }, + "attributes": { + "datasetId": { + "type": "number", + "default": 0 + } + }, + "editorScript": "odw-dataset-card-editor" +} diff --git a/blocks/dataset-card/editor.js b/blocks/dataset-card/editor.js new file mode 100644 index 0000000..9a096cf --- /dev/null +++ b/blocks/dataset-card/editor.js @@ -0,0 +1,124 @@ +/* global wp */ +/** + * Editor-Skript für den Block „Datensatz-Karte". + * + * Bewusst in schlichtem JavaScript statt JSX: Das Projekt hat keine + * JS-Build-Kette für den Admin-Bereich, und eine allein für diesen Block + * einzuführen wäre viel Apparat für wenig Ertrag. Der Preis ist das + * ausgeschriebene createElement() statt der gewohnten JSX-Schreibweise. + * + * Die Auswahlliste kommt aus window.odwDatasetBlock (siehe ODW_Block): Der + * Custom Post Type ist absichtlich nicht über die WP-REST-API exponiert — + * dafür gibt es die eigenen Endpunkte —, also lässt er sich im Editor nicht + * über den Core-Datenspeicher abfragen. + * + * Im Editor steht eine Platzhalterkarte statt einer Live-Vorschau. Eine + * serverseitig gerenderte Vorschau bekäme das Frontend-Stylesheet im + * Editor-Rahmen nicht mit und sähe dort kaputt aus — eine ehrliche + * Platzhalterkarte ist verlässlicher als eine halbe Vorschau. + */ +( function ( blocks, element, blockEditor, components ) { + 'use strict'; + + var el = element.createElement; + var cfg = window.odwDatasetBlock || { datasets: [], labels: {} }; + var datasets = cfg.datasets || []; + var labels = cfg.labels || {}; + + function findDataset( id ) { + for ( var i = 0; i < datasets.length; i++ ) { + if ( datasets[ i ].id === id ) { + return datasets[ i ]; + } + } + return null; + } + + function selectOptions() { + var options = [ { label: labels.choose || '', value: 0 } ]; + for ( var i = 0; i < datasets.length; i++ ) { + options.push( { label: datasets[ i ].title, value: datasets[ i ].id } ); + } + return options; + } + + /** + * Die Karte im Editor — kein Abbild des Frontends, sondern eine ruhige + * Zusammenfassung dessen, was veröffentlicht wird. + * + * @param {Object} dataset Ausgewählter Datensatz oder null. + * @return {Object} Element. + */ + function preview( dataset, onChange ) { + // Durchgehend die Placeholder-Komponente von WordPress: Sie bringt ihr + // Aussehen selbst mit. Eigenes CSS wäre im Beitragseditor wirkungslos — + // assets/css/admin.css lädt nur auf den Datensatz-Bildschirmen. + if ( ! datasets.length ) { + return el( + components.Placeholder, + { icon: 'database', label: labels.title }, + el( 'p', null, labels.none ) + ); + } + + if ( ! dataset ) { + return el( + components.Placeholder, + { icon: 'database', label: labels.title, instructions: labels.pick }, + el( components.SelectControl, { + value: 0, + options: selectOptions(), + onChange: onChange, + __nextHasNoMarginBottom: true, + } ) + ); + } + + return el( + components.Placeholder, + { icon: 'database', label: labels.title }, + el( 'p', null, el( 'strong', null, dataset.title ) ), + el( 'p', null, labels.rendered ) + ); + } + + blocks.registerBlockType( 'odw/dataset-card', { + edit: function ( props ) { + var blockProps = blockEditor.useBlockProps(); + var id = props.attributes.datasetId || 0; + var dataset = findDataset( id ); + + function choose( value ) { + props.setAttributes( { datasetId: parseInt( value, 10 ) || 0 } ); + } + + return el( + 'div', + blockProps, + el( + blockEditor.InspectorControls, + null, + el( + components.PanelBody, + { title: labels.panel, initialOpen: true }, + el( components.SelectControl, { + label: labels.field, + value: id, + options: selectOptions(), + help: datasets.length ? labels.help : labels.none, + onChange: choose, + __nextHasNoMarginBottom: true, + } ) + ) + ), + preview( dataset, choose ) + ); + }, + + // Dynamischer Block: Das Markup entsteht beim Ausliefern in PHP über + // dieselbe Funktion wie beim Shortcode, damit es nur eine Quelle gibt. + save: function () { + return null; + }, + } ); +}( window.wp.blocks, window.wp.element, window.wp.blockEditor, window.wp.components ) ); diff --git a/includes/class-block.php b/includes/class-block.php new file mode 100644 index 0000000..8950ebf --- /dev/null +++ b/includes/class-block.php @@ -0,0 +1,143 @@ + array( self::class, 'render' ) ) + ); + } + + /** + * Passes the list of published datasets and the editor labels to the script. + */ + public static function localize(): void { + $posts = get_posts( + array( + 'post_type' => 'odw_dataset', + 'post_status' => 'publish', + 'numberposts' => self::MAX_CHOICES, + 'orderby' => 'title', + 'order' => 'ASC', + 'suppress_filters' => false, + ) + ); + + $datasets = array(); + foreach ( $posts as $post ) { + $title = (string) $post->post_title; + + // Ein Datensatz ohne Titel wäre in der Auswahlliste unsichtbar — + // die ID macht ihn wenigstens auffindbar. + if ( '' === trim( $title ) ) { + $title = sprintf( + /* translators: %d: post ID of an untitled dataset */ + __( '(ohne Titel, ID %d)', 'open-data-wizard' ), + (int) $post->ID + ); + } + + $datasets[] = array( + 'id' => (int) $post->ID, + 'title' => $title, + ); + } + + wp_localize_script( + 'odw-dataset-card-editor', + 'odwDatasetBlock', + array( + 'datasets' => $datasets, + 'labels' => array( + 'title' => __( 'Datensatz-Karte', 'open-data-wizard' ), + 'panel' => __( 'Datensatz', 'open-data-wizard' ), + 'field' => __( 'Welcher Datensatz soll angezeigt werden?', 'open-data-wizard' ), + 'choose' => __( '— Bitte wählen —', 'open-data-wizard' ), + 'help' => __( 'Nur veröffentlichte Datensätze stehen zur Auswahl. Entwürfe erscheinen erst nach dem Veröffentlichen.', 'open-data-wizard' ), + 'none' => __( 'Es gibt noch keinen veröffentlichten Datensatz. Legen Sie zuerst einen an und veröffentlichen Sie ihn.', 'open-data-wizard' ), + 'pick' => __( 'Wählen Sie rechts in der Seitenleiste einen Datensatz aus.', 'open-data-wizard' ), + 'rendered' => __( 'wird als Download-Karte ausgegeben', 'open-data-wizard' ), + ), + ) + ); + } + + /** + * Renders the block on the front end. + * + * @param array $attributes Block attributes. + * @return string + */ + public static function render( array $attributes ): string { + $dataset_id = isset( $attributes['datasetId'] ) ? absint( $attributes['datasetId'] ) : 0; + + if ( $dataset_id <= 0 ) { + return ''; + } + + if ( ! class_exists( 'ODW_Shortcode' ) ) { + return ''; + } + + // Dieselbe Ausgabe wie beim Shortcode, inklusive dessen Prüfungen auf + // Beitragstyp und Veröffentlichungsstatus und dem Einbinden des + // Frontend-Stylesheets. + return ODW_Shortcode::render( array( 'id' => (string) $dataset_id ) ); + } +} diff --git a/languages/open-data-wizard-en_US.mo b/languages/open-data-wizard-en_US.mo index 4318bf4..bc2ed77 100644 Binary files a/languages/open-data-wizard-en_US.mo and b/languages/open-data-wizard-en_US.mo differ diff --git a/languages/open-data-wizard-en_US.po b/languages/open-data-wizard-en_US.po index 088409a..594da9a 100644 --- a/languages/open-data-wizard-en_US.po +++ b/languages/open-data-wizard-en_US.po @@ -1894,3 +1894,24 @@ msgstr "(automatic)" msgid "Davon steuert der Wizard %d Punkte selbst bei: Das Änderungsdatum wird bei jedem Speichern automatisch gesetzt. Deshalb steht auch ein noch leerer Datensatz nicht bei 0 %%." msgstr "Of these, the wizard contributes %d points itself: the modification date is set automatically every time you save. That is why even an empty dataset does not start at 0 %%." + +msgid "(ohne Titel, ID %d)" +msgstr "(untitled, ID %d)" + +msgid "Datensatz-Karte" +msgstr "Dataset card" + +msgid "Es gibt noch keinen veröffentlichten Datensatz. Legen Sie zuerst einen an und veröffentlichen Sie ihn." +msgstr "There is no published dataset yet. Create one first and publish it." + +msgid "Nur veröffentlichte Datensätze stehen zur Auswahl. Entwürfe erscheinen erst nach dem Veröffentlichen." +msgstr "Only published datasets can be selected. Drafts appear once they are published." + +msgid "Welcher Datensatz soll angezeigt werden?" +msgstr "Which dataset should be shown?" + +msgid "Wählen Sie rechts in der Seitenleiste einen Datensatz aus." +msgstr "Choose a dataset in the sidebar on the right." + +msgid "wird als Download-Karte ausgegeben" +msgstr "will be output as a download card" diff --git a/languages/open-data-wizard.pot b/languages/open-data-wizard.pot index ed9f4b4..b1514fb 100644 --- a/languages/open-data-wizard.pot +++ b/languages/open-data-wizard.pot @@ -1521,3 +1521,24 @@ msgstr "" msgid "Welchem Engagementfeld ist dieser Datensatz zuzuordnen?" msgstr "" + +msgid "(ohne Titel, ID %d)" +msgstr "" + +msgid "Datensatz-Karte" +msgstr "" + +msgid "Es gibt noch keinen veröffentlichten Datensatz. Legen Sie zuerst einen an und veröffentlichen Sie ihn." +msgstr "" + +msgid "Nur veröffentlichte Datensätze stehen zur Auswahl. Entwürfe erscheinen erst nach dem Veröffentlichen." +msgstr "" + +msgid "Welcher Datensatz soll angezeigt werden?" +msgstr "" + +msgid "Wählen Sie rechts in der Seitenleiste einen Datensatz aus." +msgstr "" + +msgid "wird als Download-Karte ausgegeben" +msgstr "" diff --git a/open-data-wizard.php b/open-data-wizard.php index 9b5bf38..5d50e7e 100644 --- a/open-data-wizard.php +++ b/open-data-wizard.php @@ -3,7 +3,7 @@ * Plugin Name: Open Data Wizard * Plugin URI: https://github.com/daimpad/OpenDataWizard * Description: DCAT-AP 3.0 konforme Open Data Metadatenverwaltung für WordPress. Bereitstellung als maschinenlesbarer JSON-LD-Endpoint für offene Daten. - * Version: 2.37.0 + * Version: 2.38.0 * Requires at least: 6.4 * Requires PHP: 8.1 * Author: nozilla @@ -26,7 +26,7 @@ exit; } -define( 'ODW_VERSION', '2.37.0' ); +define( 'ODW_VERSION', '2.38.0' ); define( 'ODW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'ODW_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'ODW_PLUGIN_FILE', __FILE__ ); @@ -147,6 +147,7 @@ function () use ( $e ): void { require_once ODW_PLUGIN_DIR . 'includes/class-admin.php'; require_once ODW_PLUGIN_DIR . 'includes/class-batch-import.php'; require_once ODW_PLUGIN_DIR . 'includes/class-shortcode.php'; + require_once ODW_PLUGIN_DIR . 'includes/class-block.php'; require_once ODW_PLUGIN_DIR . 'includes/class-cli.php'; ODW_Settings::init(); @@ -157,6 +158,7 @@ function () use ( $e ): void { ODW_Quality::init(); ODW_Admin::init(); ODW_Shortcode::init(); + ODW_Block::init(); ODW_Setup::init(); ODW_CLI::init(); }