From a22c39dd367a79d3654d90ae0c2a0d2a74e248ff Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 16:03:58 +0000 Subject: [PATCH] feat(ux/5b): Flow-Metadaten editierbar (id, url-key, name, title, icon) Neuer FlowMetadataDialog (ueber DialogBase) ersetzt den reinen Namens-Dialog und macht id, url-key, name, title (de/en) und icon editierbar. id/url-key werden als slug-artige Identifier validiert (utils/flowMetadata.ts, unit-getestet), mit Ableitung aus dem Namen. Vorher leitete der Dialog id/url-key/title still aus dem Namen ab und das Icon war nicht editierbar. related_pages + Layout-Vertrag in der technischen Doku dokumentiert. Flow-description bewusst weggelassen (nicht im ListingFlow-Schema von portal). Toten WorkflowNameDialog entfernt. --- CLAUDE.md | 2 +- docs/technical_documentation.md | 37 ++++- plans/ux-overhaul.md | 2 +- src/App.tsx | 65 ++++---- .../FlowMetadataDialog/FlowMetadataDialog.tsx | 152 ++++++++++++++++++ src/components/Navigation/Navigation.tsx | 2 +- .../WorkflowNameDialog/WorkflowNameDialog.tsx | 64 -------- src/utils/flowMetadata.test.ts | 44 +++++ src/utils/flowMetadata.ts | 36 +++++ 9 files changed, 297 insertions(+), 107 deletions(-) create mode 100644 src/components/FlowMetadataDialog/FlowMetadataDialog.tsx delete mode 100644 src/components/WorkflowNameDialog/WorkflowNameDialog.tsx create mode 100644 src/utils/flowMetadata.test.ts create mode 100644 src/utils/flowMetadata.ts diff --git a/CLAUDE.md b/CLAUDE.md index 8e7f24e..1f14a26 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,7 @@ Stack: React 19, TypeScript 4.9, Material-UI (MUI) 7, styled-components, react-d | **Element-Editoren** | `src/components/PropertyEditor/editors/*` (Text, Boolean, String, SingleSelection, Number, Date, File, Group, Array, ChipGroup, Custom, Visibility) | | **Palette** | `src/components/ElementPalette/ElementPalette.tsx` (ziehbare Element-Typen) | | **Seiten** | `src/components/PageNavigator/PageNavigator.tsx`, `EditPageDialog.tsx` | -| **Flow-Metadaten** | `src/App.tsx` (hält Flow-State + leeres Template), `src/components/Navigation/Navigation.tsx`, `WorkflowNameDialog/` | +| **Flow-Metadaten** | `src/App.tsx` (hält Flow-State + leeres Template), `src/components/Navigation/Navigation.tsx`, `FlowMetadataDialog/` (id/url-key/name/title/icon, Validierung in `utils/flowMetadata.ts`) | | **Export/UUID** | `src/utils/uuidUtils.ts` (Export-Transform — strippt `uuid`) | | **Utilities** | `SubflowManager.ts`, `normalizeUtils.ts`, `visibilityUtils.ts`, `deepCloneUtils.ts`, `pathUtils.ts` | diff --git a/docs/technical_documentation.md b/docs/technical_documentation.md index 5a7898b..382c19a 100644 --- a/docs/technical_documentation.md +++ b/docs/technical_documentation.md @@ -50,8 +50,8 @@ interface ListingFlow { interface Page { pattern_type: string; // "Page" oder "CustomUIElement" id: string; - layout?: string; // z.B. "2_COL_RIGHT_FILL" für Edit-Seiten - related_pages?: RelatedPage[]; // Verknüpfung zu korrespondierenden Seiten + layout?: string; // siehe "Seiten-Layout" unten + related_pages?: RelatedPage[]; // Verknüpfung Edit-/View-Seite, siehe unten short_title?: TranslatableString; // Kurztitel für kompakte Anzeige title?: TranslatableString; // Vollständiger Seitentitel icon?: string; // Material Design Icon-Name @@ -59,6 +59,39 @@ interface Page { } ``` +##### Seiten-Layout (`layout`) + +Das `layout`-Feld steuert, wie portal die rechte Spalte einer Seite rendert. **portal +unterstützt genau drei Werte** (Quelle: `portal-applications` → `d-fc-page-default.vue`); jeder +andere Wert läuft dort in den „unbekannter Typ"-Zweig: + +| Wert | Bedeutung | +|---|---| +| _(nicht gesetzt)_ | **Standard** — rechte Spalte zentriert | +| `2_COL_RIGHT_WIDER` | zwei Spalten, rechte Spalte breiter zentriert | +| `2_COL_RIGHT_FILL` | zwei Spalten, genau ein rechtes Element füllt die Spalte | + +Single Source of Truth im Editor: `src/components/PageNavigator/pageLayouts.ts`. Der Editor bietet +nur diese Werte an (mit Live-Vorschau via `LayoutPreview`); ein abwesendes Layout wird als +„Standard" dargestellt. Frühere Optionen `2_COL_LEFT_WIDER`/`1_COL` waren Schema-Drift und wurden +entfernt. + +##### Korrespondierende Seiten (`related_pages`) + +```typescript +interface RelatedPage { + viewing_context: 'VIEW' | 'EDIT'; // Kontext der referenzierten Seite + page_id: string; // ID der referenzierten Seite +} +``` + +Jeder Flow hält Seiten zweimal: `pages_edit` (Eingabe) und `pages_view` (Anzeige). `related_pages` +verknüpft eine Edit-Seite mit ihrer View-Seite und umgekehrt — `viewing_context` gibt an, in welchem +Kontext die referenzierte Seite steht. Der Editor pflegt diese Verknüpfung automatisch: Beim +Speichern einer Edit-Seite mit aktivem „Im View-Modus anzeigen" wird die zugehörige View-Seite +erzeugt/aktualisiert und über `related_pages` (bzw. die ID-Konvention `edit-…` ↔ `view-…`) +zugeordnet (siehe `findCorrespondingViewPage()`). + #### TranslatableString ```typescript interface TranslatableString { diff --git a/plans/ux-overhaul.md b/plans/ux-overhaul.md index efdc49c..ebc8fbc 100644 --- a/plans/ux-overhaul.md +++ b/plans/ux-overhaul.md @@ -59,7 +59,7 @@ Editor-Factory-Muster, `AccordionSection`, `TabbedTranslatableFields`, `ElementP ## Phase 5 — Seiten & Flow-Metadaten - [x] **Layout-Vorschau + Layout-Wahl bei Seitenanlage** *(PR Phase 5a)*: Zentrale `pageLayouts.ts` (Single Source of Truth, unit-getestet) + SVG-`LayoutPreview`. Schema-Abgleich mit portal (`d-fc-page-default.vue`): nur `null`/Standard, `2_COL_RIGHT_WIDER`, `2_COL_RIGHT_FILL` werden gerendert — der Editor bot zusätzlich `2_COL_LEFT_WIDER`/`1_COL` an (**Schema-Drift, entfernt**). Layout jetzt mit Vorschau in `EditPageDialog` **und** schon im Neue-Seite-Dialog wählbar; abwesendes Layout = „Standard"; nicht unterstützte Alt-Werte werden als deaktivierter „⚠ Nicht unterstützt"-Eintrag sichtbar gemacht. **`pattern_type`-Auswahl bewusst verworfen**: portal verzweigt nicht über den Page-`pattern_type` (alle Top-Level-Seiten laufen durch `d-fc-page-default`), ein Selektor wäre wirkungslos und driftgefährdet. -- [ ] Flow-Metadaten editierbar (id/url-key mit Validierung, Flow-Icon, Title de/en); `related_pages` dokumentieren. *(Phase 5b — Flow-`description` entfällt: nicht im `ListingFlow`-Schema von portal.)* +- [x] **Flow-Metadaten editierbar** *(PR Phase 5b)*: Neuer `FlowMetadataDialog` (über `DialogBase`) ersetzt den reinen Namens-Dialog und macht `id`, `url-key`, `name`, `title` (de/en) und `icon` editierbar; `id`/`url-key` werden als slug-artige Identifier validiert (`utils/flowMetadata.ts`, unit-getestet), mit „aus Name"-Ableitung. Befund: vorher leitete der Dialog `id`/`url-key`/`title` still aus dem Namen ab und das Icon war gar nicht editierbar. `related_pages` + Layout-Vertrag in `docs/technical_documentation.md` dokumentiert. Flow-`description` bewusst weggelassen — nicht im `ListingFlow`-Schema von portal. --- diff --git a/src/App.tsx b/src/App.tsx index 9561045..6fad20c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid'; import { tokens } from './theme/tokens'; import { generateUUID, transformFlowForExport } from './utils/uuidUtils'; import styled from 'styled-components'; -import WorkflowNameDialog from './components/WorkflowNameDialog/WorkflowNameDialog'; +import FlowMetadataDialog, { FlowMetadata } from './components/FlowMetadataDialog/FlowMetadataDialog'; import ModuleManagerDialog from './components/ModuleManager/ModuleManagerDialog'; import Navigation from './components/Navigation/Navigation'; @@ -1638,7 +1638,7 @@ const createElement = (type: string): PatternLibraryElement => { const AppContent: React.FC = () => { const { state, dispatch } = useEditor(); const [selectedElementPath, setSelectedElementPath] = useState([]); - const [showWorkflowNameDialog, setShowWorkflowNameDialog] = useState(false); + const [showFlowMetadataDialog, setShowFlowMetadataDialog] = useState(false); const [showModuleManager, setShowModuleManager] = useState(false); const [showShortcuts, setShowShortcuts] = useState(false); // Erstkontakt-Onboarding: beim ersten Start automatisch zeigen (einmalig via localStorage). @@ -2218,42 +2218,25 @@ const AppContent: React.FC = () => { } }; - // Handler für das Bearbeiten des Workflow-Namens + // Handler für das Bearbeiten der Flow-Metadaten const handleEditWorkflowName = () => { - setShowWorkflowNameDialog(true); + setShowFlowMetadataDialog(true); }; - // Handler für das Speichern des Workflow-Namens - const handleSaveWorkflowName = (name: string) => { - if (!name) return; - - const newFlow = { - ...emptyFlow, - id: name.toLowerCase().replace(/\s+/g, '-'), - 'url-key': name.toLowerCase().replace(/\s+/g, '-'), - name: name, - title: { - de: name, - en: name - } + // Handler für das Speichern der Flow-Metadaten (id, url-key, name, title, icon) + const handleSaveFlowMetadata = (meta: FlowMetadata) => { + const fields = { + id: meta.id, + 'url-key': meta.urlKey, + name: meta.name, + title: { de: meta.titleDe, en: meta.titleEn || meta.titleDe }, + icon: meta.icon, }; - // Wenn es bereits einen Flow gibt, aktualisieren wir nur den Namen if (state.currentFlow) { - const updatedFlow = { - ...state.currentFlow, - id: name.toLowerCase().replace(/\s+/g, '-'), - 'url-key': name.toLowerCase().replace(/\s+/g, '-'), - name: name, - title: { - de: name, - en: name - } - }; - dispatch({ type: 'UPDATE_FLOW', flow: updatedFlow }); + dispatch({ type: 'UPDATE_FLOW', flow: { ...state.currentFlow, ...fields } }); } else { - // Ansonsten erstellen wir einen neuen Flow - dispatch({ type: 'SET_FLOW', flow: newFlow }); + dispatch({ type: 'SET_FLOW', flow: { ...emptyFlow, ...fields } }); } }; @@ -2402,13 +2385,19 @@ const AppContent: React.FC = () => { setShowShortcuts(false)} /> - {/* Workflow-Namen-Dialog */} - setShowWorkflowNameDialog(false)} - onSave={handleSaveWorkflowName} - isFirstTime={false} + {/* Flow-Metadaten-Dialog */} + setShowFlowMetadataDialog(false)} + onSave={handleSaveFlowMetadata} + initial={{ + name: state.currentFlow?.name ?? emptyFlow.name, + id: state.currentFlow?.id ?? emptyFlow.id, + urlKey: state.currentFlow?.['url-key'] ?? emptyFlow['url-key'], + titleDe: state.currentFlow?.title?.de ?? '', + titleEn: state.currentFlow?.title?.en ?? '', + icon: state.currentFlow?.icon ?? emptyFlow.icon, + }} /> {/* Modul-Katalog-Manager */} diff --git a/src/components/FlowMetadataDialog/FlowMetadataDialog.tsx b/src/components/FlowMetadataDialog/FlowMetadataDialog.tsx new file mode 100644 index 0000000..98ba119 --- /dev/null +++ b/src/components/FlowMetadataDialog/FlowMetadataDialog.tsx @@ -0,0 +1,152 @@ +import React, { useEffect, useState } from 'react'; +import { Box, Button, TextField, Typography } from '@mui/material'; +import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'; +import DialogBase from '../common/DialogBase'; +import IconField from '../PropertyEditor/common/IconField'; +import { slugify, validateSlug, validateRequired } from '../../utils/flowMetadata'; + +export interface FlowMetadata { + name: string; + id: string; + urlKey: string; + titleDe: string; + titleEn: string; + icon: string; +} + +interface FlowMetadataDialogProps { + open: boolean; + initial: FlowMetadata; + onClose: () => void; + onSave: (meta: FlowMetadata) => void; +} + +/** + * Editor für die Flow-Metadaten (`id`, `url-key`, `name`, `title` de/en, `icon`). + * Löst den früheren reinen Namens-Dialog ab, der `id`/`url-key`/`title` still aus dem + * Namen ableitete und das Icon gar nicht editierbar machte. `id`/`url-key` werden als + * slug-artige Identifier validiert. + */ +const FlowMetadataDialog: React.FC = ({ open, initial, onClose, onSave }) => { + const [name, setName] = useState(initial.name); + const [id, setId] = useState(initial.id); + const [urlKey, setUrlKey] = useState(initial.urlKey); + const [titleDe, setTitleDe] = useState(initial.titleDe); + const [titleEn, setTitleEn] = useState(initial.titleEn); + const [icon, setIcon] = useState(initial.icon); + + // Felder beim Öffnen auf den aktuellen Flow-Zustand zurücksetzen. + useEffect(() => { + if (open) { + setName(initial.name); + setId(initial.id); + setUrlKey(initial.urlKey); + setTitleDe(initial.titleDe); + setTitleEn(initial.titleEn); + setIcon(initial.icon); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]); + + const nameError = validateRequired(name, 'Name'); + const idError = validateSlug(id, 'ID'); + const urlKeyError = validateSlug(urlKey, 'URL-Key'); + const titleDeError = validateRequired(titleDe, 'Titel (Deutsch)'); + const hasError = !!(nameError || idError || urlKeyError || titleDeError); + + const handleConfirm = () => { + if (hasError) return; + onSave({ + name: name.trim(), + id: id.trim(), + urlKey: urlKey.trim(), + titleDe: titleDe.trim(), + titleEn: titleEn.trim(), + icon, + }); + onClose(); + }; + + return ( + + + setName(e.target.value)} + error={!!nameError} + helperText={nameError || 'Interner Anzeigename des Flows.'} + /> + + + setId(e.target.value)} + error={!!idError} + helperText={idError || 'Technischer Identifier (z. B. doorbit_esg).'} + /> + + + + + setUrlKey(e.target.value)} + error={!!urlKeyError} + helperText={urlKeyError || 'Identifier in der URL (z. B. new-flow).'} + /> + + + + + Titel (nutzersichtbar) + + setTitleDe(e.target.value)} + error={!!titleDeError} + helperText={titleDeError || undefined} + /> + setTitleEn(e.target.value)} + /> + + + + + ); +}; + +export default FlowMetadataDialog; diff --git a/src/components/Navigation/Navigation.tsx b/src/components/Navigation/Navigation.tsx index 351d27c..c3861ce 100644 --- a/src/components/Navigation/Navigation.tsx +++ b/src/components/Navigation/Navigation.tsx @@ -118,7 +118,7 @@ const Navigation: React.FC = ({ Speichern - +