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
3 changes: 2 additions & 1 deletion plans/ux-overhaul.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import {
Alert,
Box,
Button,
FormControl,
Expand Down Expand Up @@ -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.
Expand All @@ -65,8 +85,12 @@ export const SingleSelectionElementEditorEnhanced: React.FC<SingleSelectionEleme
const handleAddOption = () => {
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 || []),
Expand Down Expand Up @@ -275,6 +299,13 @@ export const SingleSelectionElementEditorEnhanced: React.FC<SingleSelectionEleme
Optionen
</Typography>

{(element.options || []).some((_, i) => getOptionKeyError(element.options || [], i)) && (
<Alert severity="warning" sx={{ mb: 1 }}>
Jede Option braucht einen eindeutigen, nicht-leeren Schlüssel. Doppelte oder
leere Schlüssel führen zu fehlerhaften Flows.
</Alert>
)}

<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead>
Expand Down Expand Up @@ -302,6 +333,8 @@ export const SingleSelectionElementEditorEnhanced: React.FC<SingleSelectionEleme
fullWidth
size="small"
value={option.key}
error={!!getOptionKeyError(element.options || [], index)}
helperText={getOptionKeyError(element.options || [], index) || undefined}
onChange={(e) => handleUpdateOptionKey(index, e.target.value)}
/>
</TableCell>
Expand Down
Loading