Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
119 changes: 75 additions & 44 deletions assets/js/odw-admin-fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = '<span aria-hidden="true">i</span>';

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 ) {
Expand All @@ -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 <em>).
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 = '<span aria-hidden="true">i</span>';

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' );
}
}
} );
} );
}
}

// -------------------------------------------------------------------------
Expand Down
6 changes: 3 additions & 3 deletions config/dcat-ap-fields.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand Down
1 change: 1 addition & 0 deletions includes/class-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ),
Expand Down
7 changes: 7 additions & 0 deletions includes/class-batch-import.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 12 additions & 2 deletions includes/class-quality.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ) );
Expand Down Expand Up @@ -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 );
}
Expand Down
Binary file modified languages/open-data-wizard-en_US.mo
Binary file not shown.
6 changes: 6 additions & 0 deletions languages/open-data-wizard-en_US.po
Original file line number Diff line number Diff line change
Expand Up @@ -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."
6 changes: 6 additions & 0 deletions languages/open-data-wizard.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
4 changes: 2 additions & 2 deletions open-data-wizard.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__ );
Expand Down
19 changes: 19 additions & 0 deletions tests/test-registry-schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -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}."
);
}
}
}
Loading