diff --git a/CHANGELOG.md b/CHANGELOG.md index 62a8e5d..6eedee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.ht --- +## [2.11.3] — 2026-06-28 + +UX-/Korrektur-Fixes aus Praxis-Feedback bei der Live-Installation. + +### 🐛 Fixed +- **Hilfe-Tooltip (ⓘ) sprang in die nächste Zeile:** Das ⓘ-Symbol wird jetzt **innerhalb des Feld-Labels** verankert und sitzt damit in derselben Zeile wie die Formularfrage (zuvor als Geschwister-Element unter dem block-level Label umgebrochen). +- **CESSDA-Themenklassifikation ohne Erklärung:** Da das eigentliche CF-Feld (mit Hilfetext) ausgeblendet ist, hatte das JS-gebaute Eingabe-Widget keinen Tooltip. Es erhält nun denselben ⓘ-Tooltip mit Erklärung **(dct:subject)** inkl. Handlungsaufforderung („Thema eintippen oder auswählen"). Die URI wird weiterhin automatisch verknüpft. +- **DCAT-AP-Namen in der Qualitätsprüfung ergänzt:** Die Indikatoren *Beschreibung*, *Herausgeber* und *Lizenz* trugen in der Registry (`config/dcat-ap-fields.php`) noch die reine Formularfrage als Label; sie heißen nun einheitlich `Beschreibung (dct:description)`, `Herausgeber (dct:publisher)` und `Lizenz (dct:license)` — konsistent mit den übrigen Indikatoren (wirkt auch auf Validierungsmeldungen). +- **Qualitäts-Score wird beim ersten Veröffentlichen/Speichern korrekt berechnet:** Der Neuberechnungs-Hook lief auf `save_post_odw_dataset`, das WordPress **vor** dem generischen `save_post` feuert — genau dort speichert Carbon Fields aber erst seine Meta-Werte (Priorität 10). Dadurch wurde der Score aus veralteten Daten berechnet und stimmte erst nach dem **zweiten** Speichern. Der Hook läuft nun auf `save_post` (Priorität 30, nach dem CF-Save) mit Post-Type-Guard. Batch-Importe berechnen die Qualität nun ebenfalls explizit nach dem Schreiben der Metadaten. + +### ✅ Tests +- Neuer Registry-Test stellt sicher, dass jedes Label seinen DCAT-AP-Property-Namen trägt. + +--- + ## [2.11.2] — 2026-06-28 ### 🔧 Changed diff --git a/README.md b/README.md index 42b55d7..e6c56d2 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.11.2-brightgreen?style=flat-square) +![Version](https://img.shields.io/badge/Version-2.11.3-brightgreen?style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-willkommen-brightgreen?style=flat-square) 📖 [Dokumentation](DOCUMENTATION.md) · 📐 [Technische Spezifikation](TECHNICAL-SPEC.md) · 📝 [Changelog](CHANGELOG.md) · 🛡️ [Security](SECURITY.md) · ⚖️ [Lizenz](LICENSE) diff --git a/assets/js/odw-admin-fields.js b/assets/js/odw-admin-fields.js index 67c2c9c..d0069e7 100644 --- a/assets/js/odw-admin-fields.js +++ b/assets/js/odw-admin-fields.js @@ -115,6 +115,13 @@ lbl.className = 'odw-cessda-label'; lbl.textContent = w.label || ''; + // The real CF field (with its help text) is hidden, so attach the same + // ⓘ tooltip here to explain what the CESSDA classification is. + if ( w.help ) { + var tipLabel = ( data.helpTip && data.helpTip.label ) || ''; + lbl.appendChild( buildHelpTip( helpTextNode( w.help ), tipLabel ) ); + } + var input = document.createElement( 'input' ); input.type = 'text'; input.className = 'odw-cessda-input'; @@ -415,6 +422,66 @@ // label one hover/click away. Progressive enhancement: without JS the help // text simply stays inline. // ------------------------------------------------------------------------- + // One document-level handler closes any open click-tooltip on outside tap. + function ensureTipDocHandler() { + if ( document.body.dataset.odwTipDocBound ) { + return; + } + document.body.dataset.odwTipDocBound = '1'; + document.addEventListener( 'click', function ( e ) { + document.querySelectorAll( '.odw-help-tip-wrap.is-open' ).forEach( function ( w ) { + if ( ! w.contains( e.target ) ) { + w.classList.remove( 'is-open' ); + var b = w.querySelector( '.odw-help-tip' ); + if ( b ) { + b.setAttribute( 'aria-expanded', 'false' ); + } + } + } ); + } ); + } + + // Build an ⓘ tooltip wrapper around a content node. Reused by the field help + // tooltips and the JS-built widgets (e.g. CESSDA) so they look identical. + function buildHelpTip( contentNode, tipLabel ) { + ensureTipDocHandler(); + + var wrap = document.createElement( 'span' ); + wrap.className = 'odw-help-tip-wrap'; + + var btn = document.createElement( 'button' ); + btn.type = 'button'; + btn.className = 'odw-help-tip'; + btn.setAttribute( 'aria-label', tipLabel || 'Hilfe' ); + btn.setAttribute( 'aria-expanded', 'false' ); + btn.innerHTML = ''; + + var pop = document.createElement( 'span' ); + pop.className = 'odw-help-pop'; + pop.setAttribute( 'role', 'tooltip' ); + pop.appendChild( contentNode ); + + wrap.appendChild( btn ); + wrap.appendChild( pop ); + + // Click toggles for touch/keyboard; hover/focus is handled in CSS. + btn.addEventListener( 'click', function ( e ) { + e.preventDefault(); + var open = wrap.classList.toggle( 'is-open' ); + btn.setAttribute( 'aria-expanded', open ? 'true' : 'false' ); + } ); + + return wrap; + } + + // Build a popup content node from a plain (multi-line) help string. + function helpTextNode( text ) { + var span = document.createElement( 'span' ); + span.className = 'odw-help-pop__content'; + span.textContent = text; // CSS white-space:pre-line preserves line breaks. + return span; + } + function initHelpTooltips() { var tip = data.helpTip || {}; document.querySelectorAll( '.cf-field' ).forEach( function ( field ) { @@ -427,62 +494,26 @@ if ( ! help || ! ( help.textContent || '' ).trim() ) { return; } - var head = field.querySelector( ':scope > .cf-field__head' ) || field.querySelector( ':scope > label' ); - if ( ! head ) { + // Anchor the ⓘ inside the label so it sits inline with the question + // text (the head/label is block-level, so a sibling would wrap below). + var anchor = field.querySelector( ':scope > .cf-field__head > .cf-field__label' ) || + field.querySelector( ':scope > .cf-field__head' ) || + field.querySelector( ':scope > label' ); + if ( ! anchor ) { return; } // The attribute both guards re-processing and drives the CSS rule that // hides the inline help (robust even if React re-renders the ). field.dataset.odwTipInit = '1'; - var wrap = document.createElement( 'span' ); - wrap.className = 'odw-help-tip-wrap'; - - var btn = document.createElement( 'button' ); - btn.type = 'button'; - btn.className = 'odw-help-tip'; - btn.setAttribute( 'aria-label', tip.label || 'Hilfe' ); - btn.setAttribute( 'aria-expanded', 'false' ); - btn.innerHTML = ''; - - var pop = document.createElement( 'span' ); - pop.className = 'odw-help-pop'; - pop.setAttribute( 'role', 'tooltip' ); - // Clone (not move) the React-owned help node into the popup so its // exact content/formatting is preserved while React keeps owning the - // original, which the CSS rule above hides from the inline flow. + // original, which the CSS rule hides from the inline flow. var content = help.cloneNode( true ); content.className = 'odw-help-pop__content'; - pop.appendChild( content ); - - wrap.appendChild( btn ); - wrap.appendChild( pop ); - head.appendChild( wrap ); - // Click toggles for touch/keyboard; hover/focus is handled in CSS. - btn.addEventListener( 'click', function ( e ) { - e.preventDefault(); - var open = wrap.classList.toggle( 'is-open' ); - btn.setAttribute( 'aria-expanded', open ? 'true' : 'false' ); - } ); + anchor.appendChild( buildHelpTip( content, tip.label ) ); } ); - - // One document-level handler closes any open click-tooltip on outside tap. - if ( ! document.body.dataset.odwTipDocBound ) { - document.body.dataset.odwTipDocBound = '1'; - document.addEventListener( 'click', function ( e ) { - document.querySelectorAll( '.odw-help-tip-wrap.is-open' ).forEach( function ( w ) { - if ( ! w.contains( e.target ) ) { - w.classList.remove( 'is-open' ); - var b = w.querySelector( '.odw-help-tip' ); - if ( b ) { - b.setAttribute( 'aria-expanded', 'false' ); - } - } - } ); - } ); - } } // ------------------------------------------------------------------------- diff --git a/config/dcat-ap-fields.php b/config/dcat-ap-fields.php index 7c802ea..5d2d3cf 100644 --- a/config/dcat-ap-fields.php +++ b/config/dcat-ap-fields.php @@ -59,7 +59,7 @@ 'key' => 'description', 'meta_key' => '_odw_description', 'dcat_prop' => 'dct:description', - 'label' => __( 'Worum geht es in diesem Datensatz?', 'open-data-wizard' ), + 'label' => __( 'Beschreibung (dct:description)', 'open-data-wizard' ), 'points' => 10, 'required' => true, 'profile' => 'ap', @@ -73,7 +73,7 @@ 'key' => 'publisher', 'meta_key' => '_odw_publisher', 'dcat_prop' => 'dct:publisher', - 'label' => __( 'Wer gibt diese Daten heraus?', 'open-data-wizard' ), + 'label' => __( 'Herausgeber (dct:publisher)', 'open-data-wizard' ), 'points' => 10, 'required' => true, 'profile' => 'ap', @@ -87,7 +87,7 @@ 'key' => 'license', 'meta_key' => '', 'dcat_prop' => 'dct:license', - 'label' => __( 'Unter welcher Lizenz sind diese Daten verfügbar?', 'open-data-wizard' ), + 'label' => __( 'Lizenz (dct:license)', 'open-data-wizard' ), 'points' => 10, 'required' => true, 'profile' => 'ap', diff --git a/includes/class-admin.php b/includes/class-admin.php index be781fe..0f502d0 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -378,6 +378,7 @@ public static function enqueue_assets( string $hook ): void { 'label' => __( 'CESSDA Themenklassifikation', 'open-data-wizard' ), 'placeholder' => __( 'Thema eintippen oder auswählen…', 'open-data-wizard' ), 'linkLabel' => __( 'Verknüpfte URI:', 'open-data-wizard' ), + 'help' => __( 'CESSDA-THEMENKLASSIFIKATION (dct:subject)', 'open-data-wizard' ) . "\n\n" . __( 'Fachliche Einordnung Ihres Datensatzes aus dem CESSDA Controlled Vocabulary (Version 4.2.3, Deutsch). Tippen Sie ein Thema ein oder wählen Sie eines aus der Liste — die passende URI wird automatisch verknüpft. Beispiel: Volkszählungen, Migration, Wirtschaftspolitik.', 'open-data-wizard' ), ), 'helpTip' => array( 'label' => __( 'Hilfe anzeigen', 'open-data-wizard' ), diff --git a/includes/class-batch-import.php b/includes/class-batch-import.php index ec86d6b..385e14d 100644 --- a/includes/class-batch-import.php +++ b/includes/class-batch-import.php @@ -489,6 +489,13 @@ private static function create_dataset_from_record( array $record, int $row_inde // Set import tracking. update_post_meta( $post_id, '_odw_imported', current_time( 'mysql' ) ); + // Recalculate quality now that all meta is written. wp_insert_post() above + // fired save_post before these meta values existed, so the score stored then + // would be stale; recompute explicitly to reflect the imported metadata. + if ( class_exists( 'ODW_Quality' ) ) { + ODW_Quality::store( $post_id, ODW_Quality::calculate( $post_id ) ); + } + return array( 'success' => true, 'post_id' => $post_id, diff --git a/includes/class-quality.php b/includes/class-quality.php index 2fb87d8..2dac924 100644 --- a/includes/class-quality.php +++ b/includes/class-quality.php @@ -39,8 +39,13 @@ class ODW_Quality { * Registers WordPress hooks. */ public static function init(): void { - // Qualität nach jedem echten Speichern neu berechnen (nach CF-Save und set_modified_date). - add_action( 'save_post_odw_dataset', array( self::class, 'recalculate_on_save' ), 30 ); + // Qualität nach jedem echten Speichern neu berechnen. Wichtig: das generische + // save_post (Priorität 30) statt save_post_odw_dataset verwenden — WordPress + // feuert save_post_{post_type} VOR dem generischen save_post, an dem Carbon + // Fields seine Meta-Werte erst speichert (Priorität 10). Auf save_post_odw_dataset + // liefen wir vor dem CF-Save und lasen veraltete Meta (Score erst beim 2. Speichern + // korrekt). Priorität 30 garantiert, dass CF (10) bereits geschrieben hat. + add_action( 'save_post', array( self::class, 'recalculate_on_save' ), 30 ); // Meta-Box auf dem Edit-Screen registrieren. add_action( 'add_meta_boxes', array( self::class, 'register_meta_box' ) ); @@ -332,6 +337,11 @@ public static function recalculate_on_save( int $post_id ): void { return; } + // Now hooked on the generic save_post, so guard the post type explicitly. + if ( 'odw_dataset' !== get_post_type( $post_id ) ) { + return; + } + $result = self::calculate( $post_id ); self::store( $post_id, $result ); } diff --git a/languages/open-data-wizard-en_US.mo b/languages/open-data-wizard-en_US.mo index 53bbdaf..da61890 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 8384f60..738f069 100644 --- a/languages/open-data-wizard-en_US.po +++ b/languages/open-data-wizard-en_US.po @@ -1235,3 +1235,9 @@ msgstr "The JSON-LD view below shows the last saved result." msgid "Speichern Sie den Datensatz, um sie zu aktualisieren." msgstr "Save the dataset to update it." + +msgid "CESSDA-THEMENKLASSIFIKATION (dct:subject)" +msgstr "CESSDA TOPIC CLASSIFICATION (dct:subject)" + +msgid "Fachliche Einordnung Ihres Datensatzes aus dem CESSDA Controlled Vocabulary (Version 4.2.3, Deutsch). Tippen Sie ein Thema ein oder wählen Sie eines aus der Liste — die passende URI wird automatisch verknüpft. Beispiel: Volkszählungen, Migration, Wirtschaftspolitik." +msgstr "A subject classification of your dataset from the CESSDA Controlled Vocabulary (version 4.2.3, German). Type a topic or pick one from the list — the matching URI is linked automatically. Example: censuses, migration, economic policy." diff --git a/languages/open-data-wizard.pot b/languages/open-data-wizard.pot index 2c54c2f..db45c79 100644 --- a/languages/open-data-wizard.pot +++ b/languages/open-data-wizard.pot @@ -1232,3 +1232,9 @@ msgstr "" msgid "Speichern Sie den Datensatz, um sie zu aktualisieren." msgstr "" + +msgid "CESSDA-THEMENKLASSIFIKATION (dct:subject)" +msgstr "" + +msgid "Fachliche Einordnung Ihres Datensatzes aus dem CESSDA Controlled Vocabulary (Version 4.2.3, Deutsch). Tippen Sie ein Thema ein oder wählen Sie eines aus der Liste — die passende URI wird automatisch verknüpft. Beispiel: Volkszählungen, Migration, Wirtschaftspolitik." +msgstr "" diff --git a/open-data-wizard.php b/open-data-wizard.php index b485cf5..825e383 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.11.2 + * Version: 2.11.3 * Requires at least: 6.4 * Requires PHP: 8.1 * Author: nozilla @@ -26,7 +26,7 @@ exit; } -define( 'ODW_VERSION', '2.11.2' ); +define( 'ODW_VERSION', '2.11.3' ); define( 'ODW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'ODW_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'ODW_PLUGIN_FILE', __FILE__ ); diff --git a/tests/test-registry-schema.php b/tests/test-registry-schema.php index 6ea2e21..fc778a1 100644 --- a/tests/test-registry-schema.php +++ b/tests/test-registry-schema.php @@ -109,4 +109,23 @@ public function test_keys_are_unique(): void { $keys = array_column( $this->defs, 'key' ); $this->assertSame( count( $keys ), count( array_unique( $keys ) ) ); } + + /** + * Every label carries its DCAT-AP property name so the quality report and + * validation messages stay consistent (regression: description/publisher/ + * license previously used the bare form question without the dct/dcat name). + */ + public function test_labels_include_dcat_property_name(): void { + foreach ( $this->defs as $entry ) { + $prop = (string) ( $entry['dcat_prop'] ?? '' ); + if ( '' === $prop ) { + continue; + } + $this->assertStringContainsString( + '(' . $prop . ')', + (string) $entry['label'], + "Label for '{$entry['key']}' must include its DCAT-AP property name {$prop}." + ); + } + } }