diff --git a/plans/ux-overhaul.md b/plans/ux-overhaul.md index f791e64..6a0999f 100644 --- a/plans/ux-overhaul.md +++ b/plans/ux-overhaul.md @@ -54,7 +54,8 @@ Editor-Factory-Muster, `AccordionSection`, `TabbedTranslatableFields`, `ElementP ## Phase 4 — Editor-Konsistenz & Wartbarkeit - [ ] `useElementUpdate`-Hook (dedupliziert Handler-Boilerplate über ~12 Editoren). - [ ] Listen-Editoren vereinheitlichen (ChipGroup-Dialog vs. SingleSelection-Tabelle). -- [~] **Feld-ID-Prominenz** *(PR Phase 4)*: gemeinsame `common/FieldIdField` (Pflicht-Marker, Hilfetext, Leer-Warnung) **zentral** im `EnhancedElementEditorFactory` für alle wertführenden Typen (Boolean/String/Number/Date/SingleSelection). Befund: Feld-ID war zuvor uneinheitlich — bei Number/Date/Boolean/SingleSelection gar nicht editierbar, bei String als String statt `{field_name}`. Jetzt überall sichtbar + einheitlich als `{field_name}` geschrieben. Offen: Duplikat-Optionen verhindern, breitere Pflichtfeld-Markierungen, `useElementUpdate`-Hook. +- [~] **Feld-ID-Prominenz** *(PR Phase 4)*: gemeinsame `common/FieldIdField` (Pflicht-Marker, Hilfetext, Leer-Warnung) **zentral** im `EnhancedElementEditorFactory` für alle wertführenden Typen (Boolean/String/Number/Date/SingleSelection). Befund: Feld-ID war zuvor uneinheitlich — bei Number/Date/Boolean/SingleSelection gar nicht editierbar, bei String als String statt `{field_name}`. Jetzt überall sichtbar + einheitlich als `{field_name}` geschrieben. Offen: breitere Pflichtfeld-Markierungen, `useElementUpdate`-Hook. +- [x] **Duplikat-/Leer-Optionen verhindern** *(PR Phase 4)*: Auswahlfeld-Optionen-Schlüssel werden auf Eindeutigkeit und Nicht-Leere geprüft (`getOptionKeyError`, Unit-getestet). Befund: `handleUpdateOptionKey` erlaubte beliebige Keys ohne Prüfung — doppelte/leere `key`s sind vertragskritisch (portal speichert den Wert über `key`). Jetzt: Fehlerstatus + Hilfetext am Key-Feld, Sammel-Warnung über der Optionen-Tabelle, kollisionssichere Auto-Key-Generierung beim Hinzufügen. ## Phase 5 — Seiten & Flow-Metadaten - [ ] `pattern_type`-Auswahl bei Seitenanlage; Layout-Vorschau (`PageNavigator.tsx`, `EditPageDialog.tsx`). diff --git a/src/components/PropertyEditor/editors/SingleSelectionElementEditorEnhanced.test.ts b/src/components/PropertyEditor/editors/SingleSelectionElementEditorEnhanced.test.ts new file mode 100644 index 0000000..e12d1d5 --- /dev/null +++ b/src/components/PropertyEditor/editors/SingleSelectionElementEditorEnhanced.test.ts @@ -0,0 +1,33 @@ +import { getOptionKeyError } from './SingleSelectionElementEditorEnhanced'; + +describe('getOptionKeyError', () => { + it('akzeptiert eindeutige, nicht-leere Schlüssel', () => { + const options = [{ key: 'a' }, { key: 'b' }, { key: 'c' }]; + expect(getOptionKeyError(options, 0)).toBeNull(); + expect(getOptionKeyError(options, 1)).toBeNull(); + expect(getOptionKeyError(options, 2)).toBeNull(); + }); + + it('meldet einen leeren Schlüssel', () => { + const options = [{ key: '' }, { key: 'b' }]; + expect(getOptionKeyError(options, 0)).toBe('Schlüssel darf nicht leer sein'); + }); + + it('meldet einen Schlüssel, der nur aus Leerzeichen besteht', () => { + const options = [{ key: ' ' }, { key: 'b' }]; + expect(getOptionKeyError(options, 0)).toBe('Schlüssel darf nicht leer sein'); + }); + + it('markiert beide Optionen eines doppelten Schlüssels', () => { + const options = [{ key: 'dup' }, { key: 'dup' }, { key: 'unique' }]; + expect(getOptionKeyError(options, 0)).toBe('Schlüssel muss eindeutig sein'); + expect(getOptionKeyError(options, 1)).toBe('Schlüssel muss eindeutig sein'); + expect(getOptionKeyError(options, 2)).toBeNull(); + }); + + it('behandelt Schlüssel mit umgebenden Leerzeichen als Duplikat', () => { + const options = [{ key: 'x' }, { key: ' x ' }]; + expect(getOptionKeyError(options, 0)).toBe('Schlüssel muss eindeutig sein'); + expect(getOptionKeyError(options, 1)).toBe('Schlüssel muss eindeutig sein'); + }); +}); diff --git a/src/components/PropertyEditor/editors/SingleSelectionElementEditorEnhanced.tsx b/src/components/PropertyEditor/editors/SingleSelectionElementEditorEnhanced.tsx index be38f43..13ac014 100644 --- a/src/components/PropertyEditor/editors/SingleSelectionElementEditorEnhanced.tsx +++ b/src/components/PropertyEditor/editors/SingleSelectionElementEditorEnhanced.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { + Alert, Box, Button, FormControl, @@ -44,6 +45,25 @@ interface SingleSelectionElementEditorEnhancedProps { onChange: (updatedElement: SingleSelectionUIElement) => void; } +/** + * Validiert den Schlüssel einer Option an Position `index`. + * Der Schlüssel ist vertragskritisch: portal speichert den ausgewählten Wert + * über diesen `key`. Leere oder doppelte Schlüssel erzeugen fehlerhafte Flows. + * @returns Fehlertext oder `null`, wenn der Schlüssel gültig ist. + */ +export function getOptionKeyError( + options: { key: string }[], + index: number +): string | null { + const key = (options[index]?.key || '').trim(); + if (!key) return 'Schlüssel darf nicht leer sein'; + const duplicate = options.some( + (option, i) => i !== index && (option.key || '').trim() === key + ); + if (duplicate) return 'Schlüssel muss eindeutig sein'; + return null; +} + /** * Verbesserte Editor-Komponente für SingleSelectionUIElement. * Ermöglicht die Bearbeitung von Auswahlfeldern mit Optionen. @@ -65,8 +85,12 @@ export const SingleSelectionElementEditorEnhanced: React.FC { if (!newOption.trim()) return; - // Erstelle eine eindeutige ID für die neue Option - const key = uuidv4().substring(0, 8); + // Erstelle eine eindeutige ID für die neue Option (Kollisionen vermeiden) + const existingKeys = new Set((element.options || []).map((o) => o.key)); + let key = uuidv4().substring(0, 8); + while (existingKeys.has(key)) { + key = uuidv4().substring(0, 8); + } const newOptions = [ ...(element.options || []), @@ -275,6 +299,13 @@ export const SingleSelectionElementEditorEnhanced: React.FC + {(element.options || []).some((_, i) => getOptionKeyError(element.options || [], i)) && ( + + Jede Option braucht einen eindeutigen, nicht-leeren Schlüssel. Doppelte oder + leere Schlüssel führen zu fehlerhaften Flows. + + )} + @@ -302,6 +333,8 @@ export const SingleSelectionElementEditorEnhanced: React.FC handleUpdateOptionKey(index, e.target.value)} />