From cc8d9444f48a9af52c802f8eb0fa502927daecf9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 16:27:23 +0000 Subject: [PATCH] fix(schema): Schema-Drift-Audit + Behebung der HIGH/MITTEL-Drift Audit-Report unter plans/schema-drift-audit.md. Behobene Drift: - NumberUIElement: Modell/Editor/Normalisierung auf den portal-Vertrag (minimum/maximum/default) umgestellt. Vorher min/max/step/default_value - portal-unbekannt; default ging beim Round-Trip verloren. normalizeUtils migriert Alt-Exporte beim Import. Test: numberNormalization.test.ts. - SingleSelection: Enhanced-Editor schrieb ungueltiges BUTTONGROUP statt BUTTON_GROUP. - KeyValueList.type um COUNTER_BAR erweitert. - FieldText.align/unit und Table.hide_index_column ergaenzt (Round-Trip-Erhalt). - Page.layout-Kommentar auf pageLayouts.ts als SSoT verwiesen. Aufgeschobene/portal-seitige Befunde sind im Audit-Report dokumentiert. --- plans/schema-drift-audit.md | 57 +++++++++++++ .../PropertyEditor/PropertyEditor.tsx | 4 +- .../editors/NumberElementEditor.tsx | 44 ++++------- .../SingleSelectionElementEditorEnhanced.tsx | 2 +- src/models/listingFlow.ts | 2 +- src/models/uiElements.ts | 16 ++-- src/utils/normalizeUtils.ts | 22 +++++- src/utils/numberNormalization.test.ts | 79 +++++++++++++++++++ 8 files changed, 184 insertions(+), 42 deletions(-) create mode 100644 plans/schema-drift-audit.md create mode 100644 src/utils/numberNormalization.test.ts diff --git a/plans/schema-drift-audit.md b/plans/schema-drift-audit.md new file mode 100644 index 0000000..d42c81b --- /dev/null +++ b/plans/schema-drift-audit.md @@ -0,0 +1,57 @@ +# Schema-Drift-Audit: flow-ui-toolkit ↔ portal-applications + +**Stand:** 2026-06-15 · **Autoritativ:** `portal-applications/api/rest/listing-flow-api.yaml` (REST/JSON = Vertrag, +snake_case), gestützt durch `api/graphql/schema/flow-config.graphqls` (BFF, camelCase) und +`customers/schema/readme.md`. + +Geprüft: `src/models/uiElements.ts`, `src/models/listingFlow.ts`, `src/components/PropertyEditor/editors/*`, +`src/utils/normalizeUtils.ts`. Ziel: Felder/Enums, die zwischen Toolkit und portal auseinanderlaufen +(„Schema-Drift ist ein Bug", siehe CLAUDE.md). + +Severity: **HOCH** = bricht/verliert Daten beim Round-Trip oder erzeugt ungültiges JSON · +**MITTEL** = fehlende Editierbarkeit/Enum-Werte, aber Round-Trip-safe · **NIEDRIG** = kosmetisch/Doku. + +--- + +## ✅ In diesem PR behoben + +| Befund | Sev | Fix | +|---|---|---| +| **NumberUIElement verliert `default`/Grenzen** — Modell/Editor nutzten `min`/`max`/`step`/`default_value`; portal kennt nur `minimum`/`maximum`/`default`. `normalizeUtils` wandelte importiertes `default` → `default_value` ohne Rück-Mapping → Default/Grenzen gingen beim Export verloren. | HOCH | Modell auf `minimum`/`maximum`/`default` umgestellt (`step`/`default_value` entfernt), Editor angepasst (Schrittweite entfernt — portal kennt keine), `normalizeUtils` migriert Alt-Exporte beim Import auf die Schema-Felder. Test: `numberNormalization.test.ts`. | +| **SingleSelection schreibt `BUTTONGROUP`** statt `BUTTON_GROUP` — der aktive Enhanced-Editor erzeugte einen ungültigen Enum-Wert (Schema/Modell: `BUTTON_GROUP`). | HOCH | MenuItem-Wert in `SingleSelectionElementEditorEnhanced.tsx` auf `BUTTON_GROUP` korrigiert. | +| **KeyValueList `type`** kannte nur `TABLE`, Schema hat `COUNTER_BAR` + `TABLE`. | MITTEL | Union auf `'COUNTER_BAR' \| 'TABLE'` erweitert. | +| **FieldText** ohne `align`/`unit` (Schema: `align: LEFT\|CENTER`, `unit`; GraphQL `horizontalAlignment`). | MITTEL | `align?`/`unit?` ergänzt (Round-Trip-Erhalt). | +| **Table** ohne `hide_index_column` (Schema/GraphQL: `hideIndexColumn`). | MITTEL | `hide_index_column?` ergänzt. | +| `Page.layout`-Kommentar irreführend. | NIEDRIG | Kommentar verweist nun auf `pageLayouts.ts` (Standard/`2_COL_RIGHT_WIDER`/`2_COL_RIGHT_FILL`). | + +> Hinweis: `Page.layout`-Drift (`1_COL`/`2_COL_LEFT_WIDER`) wurde bereits in PR #21 (Phase 5a) behoben. + +--- + +## ⏭️ Bewusst aufgeschoben (Begründung) + +| Befund | Sev | Warum nicht jetzt | +|---|---|---| +| **BooleanUIElement `type` (SWITCH/CHECKBOX/…) + `options.{true/false_label}`** sind toolkit-erfunden — portal kennt sie nicht (Boolean ist immer ein Switch). | HOCH* | **Feature-Entfernung** mit sichtbarer Editor-UI → braucht Produkt-Sign-off. Daten gehen nicht verloren (portal ignoriert die Felder); irreführend ist nur, dass der Autor glaubt, eine Darstellung zu wählen. Empfehlung: `type`/`options` aus Boolean-Typ + Editor entfernen. | +| **StringUIElement `default_value`/`placeholder`/`multiline`/`pattern`** nicht im Schema. | MITTEL | Ebenfalls Feature-Entfernung; Round-Trip-safe. Entweder entfernen oder bewusst als portal-Erweiterung abstimmen. | +| **RCO-Context `LIST`** — nur GraphQL kennt `LIST` zusätzlich zu CREATE/EDIT/VIEW/BACK_OFFICE; REST-YAML (autoritativ fürs JSON) nicht. | MITTEL | Unklar, ob `LIST` im JSON-Vertrag vorkommt. Erst mit portal verifizieren, dann ggf. `RelationalContextOperator.context` + VisibilityConditionEditor ergänzen. | +| **CustomUIElement `PROJECT_CONFIGURATOR`** als Typ-Option in Editor/Palette. | NIEDRIG | Gehört thematisch in die Listen-/Editor-Vereinheitlichung; `type: string` lässt den Wert ohnehin zu (kein Bruch). | +| **Module `name`/`description` required** im Toolkit, optional in YAML. | NIEDRIG | Strenger als der Vertrag (kein Round-Trip-Bruch). Optional-Machen würde TS-Zugriffe in `ModuleManagerDialog` berühren → separat. | +| Erfundene, harmlose Zusatzfelder: `Group.field_id`, `Date.default_value`, `ImageGallery.field_id`, `Table.type`, `ListingFlow.version`. | NIEDRIG | Round-Trip-safe; Aufräumen ohne Dringlichkeit. | + +\* Severity-Einstufung des Audits; da kein Datenverlust, eher „irreführende UI". + +--- + +## 🔎 Unklar / manuell mit portal verifizieren + +- **FileUIElement `id_field_*`**: YAML `required` nennt `id_field_name`, definiert aber `id_field_id` (YAML-interner Widerspruch). Toolkit nutzt `id_field_id` (konsistent mit readme.md + GraphQL `idField`) → vermutlich korrekt, YAML-`required` ist der Bug. Außerdem prüfen, ob der Export das toolkit-interne `FileUIElement.field_id` strippt. +- **`caption_field_id` vs `name_element`**: REST nutzt `caption_field_id`, GraphQL `nameElement: StringUIElement` — klären, welche Form das JSON erwartet. +- **`preferred_position` vs `display_position`** bei View-Elementen (Text/Table/KeyValueList/FieldText): mögliche Namens-Drift — klären, welches Feld portal im JSON liest. + +--- + +## ℹ️ Portal-seitig (kein Toolkit-Fix — YAML hinkt GraphQL/readme hinterher) + +- `SubFlow.type`-Enum im YAML listet nur 5 Werte; readme.md hat die Vollmenge (die der Toolkit korrekt führt). YAML-Enum nachziehen. +- `ImageGallery.preferred_size` im YAML nur `[M, L]`; GraphQL/Toolkit kennen `S, M, M_NARROW, L`. YAML nachziehen. diff --git a/src/components/PropertyEditor/PropertyEditor.tsx b/src/components/PropertyEditor/PropertyEditor.tsx index 8a91187..dd8e780 100644 --- a/src/components/PropertyEditor/PropertyEditor.tsx +++ b/src/components/PropertyEditor/PropertyEditor.tsx @@ -641,7 +641,7 @@ const PropertyEditor: React.FC = ({ element, onUpdate }) => size="small" fullWidth type="number" - value={numberElement.default_value || ''} + value={numberElement.default ?? ''} onChange={(e) => { const updatedCustomElement = { ...element.element as CustomUIElement }; @@ -652,7 +652,7 @@ const PropertyEditor: React.FC = ({ element, onUpdate }) => elementPath, { ...numberElement, - default_value: parseFloat(e.target.value) + default: parseFloat(e.target.value) } ); diff --git a/src/components/PropertyEditor/editors/NumberElementEditor.tsx b/src/components/PropertyEditor/editors/NumberElementEditor.tsx index 8f23a19..b4cf4fc 100644 --- a/src/components/PropertyEditor/editors/NumberElementEditor.tsx +++ b/src/components/PropertyEditor/editors/NumberElementEditor.tsx @@ -74,7 +74,7 @@ const NumberElementEditor: React.FC = ({ element, onUp // Konvertiere Werte zu Zahlen, wenn sie numerisch sind const value = event.target.value; - if (field === 'min' || field === 'max' || field === 'step' || field === 'default_value') { + if (field === 'minimum' || field === 'maximum' || field === 'default') { elementAny[field] = value === '' ? '' : Number(value); } else { elementAny[field] = value; @@ -101,12 +101,10 @@ const NumberElementEditor: React.FC = ({ element, onUp }; // Berechne den Slider-Bereich - const min = numberElement.min !== undefined ? numberElement.min : 0; - const max = numberElement.max !== undefined ? numberElement.max : 100; - const step = numberElement.step || 1; - const defaultValue = numberElement.default_value !== undefined - ? numberElement.default_value - : (numberElement.default !== undefined ? numberElement.default : min); + const min = numberElement.minimum !== undefined ? numberElement.minimum : 0; + const max = numberElement.maximum !== undefined ? numberElement.maximum : 100; + const step = 1; // portal kennt keine Schrittweite — nur für die Slider-Vorschau + const defaultValue = numberElement.default !== undefined ? numberElement.default : min; return ( <> @@ -215,10 +213,8 @@ const NumberElementEditor: React.FC = ({ element, onUp = ({ element, onUp - - - {numberElement.min !== undefined && numberElement.max !== undefined && ( + {numberElement.minimum !== undefined && numberElement.maximum !== undefined && ( Wertebereich-Vorschau = ({ element, onUp {getUnitDisplayLabel(numberElement.unit)} ) : null, inputProps: { - min: numberElement.min, - max: numberElement.max, - step: numberElement.step || 1 + min: numberElement.minimum, + max: numberElement.maximum, + step: 1 } }} disabled diff --git a/src/components/PropertyEditor/editors/SingleSelectionElementEditorEnhanced.tsx b/src/components/PropertyEditor/editors/SingleSelectionElementEditorEnhanced.tsx index 13ac014..45c889b 100644 --- a/src/components/PropertyEditor/editors/SingleSelectionElementEditorEnhanced.tsx +++ b/src/components/PropertyEditor/editors/SingleSelectionElementEditorEnhanced.tsx @@ -267,7 +267,7 @@ export const SingleSelectionElementEditorEnhanced: React.FC handleTypeChange(e as any)} > Dropdown - Button Gruppe + Button Gruppe diff --git a/src/models/listingFlow.ts b/src/models/listingFlow.ts index 82385b1..79402c5 100644 --- a/src/models/listingFlow.ts +++ b/src/models/listingFlow.ts @@ -45,7 +45,7 @@ export interface Module { export interface Page { pattern_type: string; // Im alten Schema "Page", im doorbit_esg.json Schema "CustomUIElement" id: string; - layout?: string; // z.B. "2_COL_RIGHT_FILL" für Edit-Seiten, "2_COL_RIGHT_WIDER" für View-Seiten + layout?: string; // unterstützt (portal): (absent)=Standard, 2_COL_RIGHT_WIDER, 2_COL_RIGHT_FILL — siehe pageLayouts.ts related_pages?: RelatedPage[]; // Verknüpfung zu korrespondierenden View- oder Edit-Seiten short_title?: TranslatableString; title?: TranslatableString; diff --git a/src/models/uiElements.ts b/src/models/uiElements.ts index 0a95808..95c0088 100644 --- a/src/models/uiElements.ts +++ b/src/models/uiElements.ts @@ -80,11 +80,12 @@ export interface NumberUIElement extends UIElementEdit { type?: 'INTEGER' | 'DOUBLE'; field_id: FieldId; unit?: string; - min?: number; - max?: number; - step?: number; + // Schema-Vertrag (portal): minimum/maximum/default. Früher nutzte der Toolkit + // min/max/step/default_value — diese sind kein portal-Feld und wurden entfernt; + // normalizeUtils migriert Alt-Exporte beim Import auf diese Namen. + minimum?: number; + maximum?: number; default?: number; - default_value?: number; } export interface DateUIElement extends UIElementEdit { @@ -163,7 +164,7 @@ export interface ChipGroupUIElement extends UIElementEdit { export interface KeyValueListUIElement extends UIElementEdit { pattern_type: 'KeyValueListUIElement'; - type: 'TABLE'; + type: 'COUNTER_BAR' | 'TABLE'; items: KeyValueListItem[]; } @@ -197,6 +198,9 @@ export interface FieldTextUIElement extends UIElementEdit { pattern_type: 'FieldTextUIElement'; type: 'HEADING' | 'PARAGRAPH'; field_value: { field_id: FieldId }; + // Textausrichtung (portal: align; GraphQL: horizontalAlignment). + align?: 'LEFT' | 'CENTER'; + unit?: string; } /** @@ -206,6 +210,8 @@ export interface FieldTextUIElement extends UIElementEdit { export interface TableUIElement extends UIElementEdit { pattern_type: 'TableUIElement'; type?: 'TABLE'; + // Blendet die Index-/Laufnummern-Spalte aus (portal: hide_index_column). + hide_index_column?: boolean; columns: TableColumn[]; } diff --git a/src/utils/normalizeUtils.ts b/src/utils/normalizeUtils.ts index df5a239..4fcd684 100644 --- a/src/utils/normalizeUtils.ts +++ b/src/utils/normalizeUtils.ts @@ -302,10 +302,24 @@ const normalizeElement = (element: any, ensureFieldId: boolean = true): any => { delete element.items; } - // Normalisiere NumberUIElement default zu default_value - if (patternType === 'NumberUIElement' && element.default !== undefined && element.default_value === undefined) { - element.default_value = element.default; - delete element.default; + // Normalisiere NumberUIElement auf die Schema-Feldnamen (minimum/maximum/default). + // portal kennt nur minimum/maximum/default; frühere Toolkit-Exporte nutzten + // min/max/step/default_value. Beim Import migrieren, damit Default-/Grenzwerte nicht + // verloren gehen (und kein vom Schema unbekanntes Feld zurück-exportiert wird). + if (patternType === 'NumberUIElement') { + if (element.min !== undefined && element.minimum === undefined) { + element.minimum = element.min; + } + if (element.max !== undefined && element.maximum === undefined) { + element.maximum = element.max; + } + if (element.default_value !== undefined && element.default === undefined) { + element.default = element.default_value; + } + delete element.min; + delete element.max; + delete element.step; + delete element.default_value; } // Normalisiere visibility_condition diff --git a/src/utils/numberNormalization.test.ts b/src/utils/numberNormalization.test.ts new file mode 100644 index 0000000..baa1a94 --- /dev/null +++ b/src/utils/numberNormalization.test.ts @@ -0,0 +1,79 @@ +import { normalizeElementTypes } from './normalizeUtils'; +import { ListingFlow } from '../models/listingFlow'; + +const flowWith = (numberElement: any): any => ({ + id: 'test-flow', + 'url-key': 'test', + name: 'Test Flow', + title: { de: 'Test', en: 'Test' }, + icon: 'test', + pages_edit: [ + { + pattern_type: 'Page', + id: 'page1', + elements: [{ element: numberElement }], + }, + ], + pages_view: [], +}); + +const numberOf = (result: ListingFlow): any => + (result.pages_edit[0].elements[0] as any).element; + +describe('NumberUIElement-Normalisierung (Schema-Drift-Fix)', () => { + it('migriert Alt-Felder min/max/step/default_value auf minimum/maximum/default', () => { + const result = normalizeElementTypes( + flowWith({ + pattern_type: 'NumberUIElement', + field_id: { field_name: 'n' }, + min: 1, + max: 9, + step: 2, + default_value: 5, + }) as ListingFlow + ); + const el = numberOf(result); + expect(el.minimum).toBe(1); + expect(el.maximum).toBe(9); + expect(el.default).toBe(5); + // Alt-Felder werden entfernt, damit sie nicht zurück-exportiert werden. + expect(el.min).toBeUndefined(); + expect(el.max).toBeUndefined(); + expect(el.step).toBeUndefined(); + expect(el.default_value).toBeUndefined(); + }); + + it('lässt portal-konforme Felder minimum/maximum/default unverändert', () => { + const result = normalizeElementTypes( + flowWith({ + pattern_type: 'NumberUIElement', + field_id: { field_name: 'n' }, + minimum: 0, + maximum: 100, + default: 42, + }) as ListingFlow + ); + const el = numberOf(result); + expect(el.minimum).toBe(0); + expect(el.maximum).toBe(100); + expect(el.default).toBe(42); + }); + + it('überschreibt vorhandene Schema-Felder nicht mit Alt-Werten', () => { + const result = normalizeElementTypes( + flowWith({ + pattern_type: 'NumberUIElement', + field_id: { field_name: 'n' }, + minimum: 3, + min: 999, + default: 7, + default_value: 999, + }) as ListingFlow + ); + const el = numberOf(result); + expect(el.minimum).toBe(3); + expect(el.default).toBe(7); + expect(el.min).toBeUndefined(); + expect(el.default_value).toBeUndefined(); + }); +});