wiki/z-archive/implementation/doc_automation_templates_impl_frontend.md

30 KiB
Raw Blame History

Automation Templates Frontend Implementation Concept

Basis: doc_automation_templates_db_and_editor_concept.md


STATUS: IMPLEMENTIERT (2026-02-03)

Backend ist vollständig implementiert:

  • API: GET/POST/PUT/DELETE /api/automation-templates
  • Model: AutomationTemplate mit TextMultilingual Feldern
  • Navigation: /workflows/automation-templates in mainSystem.py
  • API: GET /api/automations/actions - Actions-Katalog

Frontend implementiert:

Komponente Status
automationApi.ts - Typen + API-Funktionen Implementiert
useAutomations.ts - Hooks (useWorkflowActions) Implementiert
AutomationTemplatesPage.tsx Implementiert
AutomationEditor.tsx NEU - Full-screen Editor
ActionsPanel.tsx Implementiert
App.tsx - Route Implementiert
pageRegistry.tsx - Icon Implementiert

Neue Dateien:

  • src/components/AutomationEditor/AutomationEditor.tsx
  • src/components/AutomationEditor/AutomationEditor.module.css
  • src/components/AutomationEditor/index.ts

WICHTIGE HINWEISE

FormGenerator: Automatische Multilingual-Unterstützung

Komponente TextMultilingual-Handling
FormGeneratorTable Automatisch - erkennt TextMultilingual und rendert in User-Sprache
FormGeneratorForm Automatisch - mit type: 'multilingual' in Attribut-Definition

Kein manueller Code nötig! Einfach Daten mit TextMultilingual-Feldern übergeben.

Bestehende vs. Neue API-Typen

Typ Quelle Verwendung
AutomationTemplate subAutomationTemplates.py (Legacy) Alte hardcoded Templates
AutomationTemplateDB DB (neu) Neue datenbankbasierte Templates

Die bestehende fetchAutomationTemplates() Funktion lädt Legacy-Templates. Für die neuen DB-Templates wird fetchAutomationTemplatesDB() verwendet.


1. Übersicht der Komponenten

Komponente Zweck Status
automationApi.ts API-Funktionen für Templates + Actions Erweitern
useAutomations.ts Hooks für Templates + Actions Erweitern
AutomationsPage.tsx Liste der Definitions; öffnet Editor Erweitern
AutomationTemplatesPage.tsx Liste der Templates (CRUD) Neu
AutomationEditor.tsx Editor mit Modus-Flag (Definition/Template) Neu
ActionsPanel.tsx Actions-Katalog mit Copy/Paste Neu

2. API-Erweiterungen (automationApi.ts)

2.1 Neuer Typ: AutomationTemplate (DB-Version, MultiLanguage)

// Multilingual text type (matches backend TextMultilingual)
export interface TextMultilingual {
    en: string;
    ge?: string;
    fr?: string;
    it?: string;
}

// NEW: AutomationTemplate from DB (not the old in-memory structure)
export interface AutomationTemplateDB {
    id: string;
    label: TextMultilingual;
    overview?: TextMultilingual;
    template: string;  // JSON string with {{KEY:...}} placeholders
    _createdAt?: number;
    _createdBy?: string;
    _createdByUserName?: string;
}

// Action definition from backend
export interface WorkflowAction {
    method: string;
    action: string;
    actionId: string;
    description: string;
    category?: string;
    parameters: WorkflowActionParameter[];
    exampleJson: {
        execMethod: string;
        execAction: string;
        execParameters: Record<string, any>;
        execResultLabel: string;
    };
}

export interface WorkflowActionParameter {
    name: string;
    type: string;
    frontendType: string;
    required: boolean;
    default?: any;
    description: string;
    frontendOptions?: string | string[];
}

2.2 Neue API-Funktionen

// ============================================================================
// AUTOMATION TEMPLATES (DB) API
// ============================================================================

/**
 * Fetch all automation templates (RBAC-filtered: own templates)
 * Endpoint: GET /api/automation-templates
 */
export async function fetchAutomationTemplatesDB(
    request: ApiRequestFunction
): Promise<AutomationTemplateDB[]> {
    const data = await request({
        url: '/api/automation-templates',
        method: 'get'
    });
    
    if (data?.items && Array.isArray(data.items)) {
        return data.items;
    }
    return Array.isArray(data) ? data : [];
}

/**
 * Fetch single template by ID
 * Endpoint: GET /api/automation-templates/{id}
 */
export async function fetchAutomationTemplateById(
    request: ApiRequestFunction,
    templateId: string
): Promise<AutomationTemplateDB | null> {
    try {
        return await request({
            url: `/api/automation-templates/${templateId}`,
            method: 'get'
        });
    } catch (error) {
        console.error('Error fetching template:', error);
        return null;
    }
}

/**
 * Create new automation template
 * Endpoint: POST /api/automation-templates
 */
export async function createAutomationTemplateApi(
    request: ApiRequestFunction,
    templateData: Omit<AutomationTemplateDB, 'id' | '_createdAt' | '_createdBy'>
): Promise<AutomationTemplateDB> {
    return await request({
        url: '/api/automation-templates',
        method: 'post',
        data: templateData
    });
}

/**
 * Update automation template
 * Endpoint: PUT /api/automation-templates/{id}
 */
export async function updateAutomationTemplateApi(
    request: ApiRequestFunction,
    templateId: string,
    templateData: Partial<AutomationTemplateDB>
): Promise<AutomationTemplateDB> {
    return await request({
        url: `/api/automation-templates/${templateId}`,
        method: 'put',
        data: templateData
    });
}

/**
 * Delete automation template
 * Endpoint: DELETE /api/automation-templates/{id}
 */
export async function deleteAutomationTemplateApi(
    request: ApiRequestFunction,
    templateId: string
): Promise<void> {
    await request({
        url: `/api/automation-templates/${templateId}`,
        method: 'delete'
    });
}

// ============================================================================
// WORKFLOW ACTIONS API
// ============================================================================

/**
 * Fetch available workflow actions (RBAC-filtered)
 * Endpoint: GET /api/automations/actions
 */
export async function fetchWorkflowActions(
    request: ApiRequestFunction
): Promise<WorkflowAction[]> {
    const data = await request({
        url: '/api/automations/actions',
        method: 'get'
    });
    
    return data?.actions || [];
}

3. Hooks-Erweiterungen (useAutomations.ts)

3.1 Neuer Hook: useAutomationTemplates

/**
 * Hook for managing AutomationTemplates (DB)
 */
export function useAutomationTemplates() {
    const [templates, setTemplates] = useState<AutomationTemplateDB[]>([]);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState<string | null>(null);
    const { request } = useApiRequest();
    const { checkPermission } = usePermissions();
    const [permissions, setPermissions] = useState<UserPermissions | null>(null);

    const fetchTemplates = useCallback(async () => {
        setLoading(true);
        try {
            const data = await fetchAutomationTemplatesDB(request);
            setTemplates(data);
        } catch (e: any) {
            setError(e.message);
            setTemplates([]);
        } finally {
            setLoading(false);
        }
    }, [request]);

    const fetchPermissions = useCallback(async () => {
        const perms = await checkPermission('DATA', 'AutomationTemplate');
        setPermissions(perms);
        return perms;
    }, [checkPermission]);

    const createTemplate = useCallback(async (data: Omit<AutomationTemplateDB, 'id'>) => {
        return await createAutomationTemplateApi(request, data);
    }, [request]);

    const updateTemplate = useCallback(async (id: string, data: Partial<AutomationTemplateDB>) => {
        return await updateAutomationTemplateApi(request, id, data);
    }, [request]);

    const deleteTemplate = useCallback(async (id: string) => {
        await deleteAutomationTemplateApi(request, id);
    }, [request]);

    return {
        templates,
        loading,
        error,
        permissions,
        fetchTemplates,
        fetchPermissions,
        createTemplate,
        updateTemplate,
        deleteTemplate,
    };
}

3.2 Neuer Hook: useWorkflowActions

/**
 * Hook for fetching available workflow actions (RBAC-filtered)
 */
export function useWorkflowActions() {
    const [actions, setActions] = useState<WorkflowAction[]>([]);
    const [loading, setLoading] = useState(false);
    const { request } = useApiRequest();

    const fetchActions = useCallback(async () => {
        setLoading(true);
        try {
            const data = await fetchWorkflowActions(request);
            setActions(data);
        } catch (e) {
            console.error('Error fetching actions:', e);
            setActions([]);
        } finally {
            setLoading(false);
        }
    }, [request]);

    return { actions, loading, fetchActions };
}

4. Neue Seite: AutomationTemplatesPage.tsx

4.1 Imports

import React, { useState, useMemo, useEffect } from 'react';
import { FaSync, FaPlus, FaFileAlt } from 'react-icons/fa';
import { useLanguage } from '../../providers/language/LanguageContext';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { useAutomationTemplates, AutomationTemplateDB, TextMultilingual } from '../../hooks/useAutomations';
import { AutomationEditor } from '../../components/AutomationEditor';  // Neu
import styles from '../admin/Admin.module.css';

4.2 Struktur

/**
 * AutomationTemplatesPage
 * 
 * CRUD für AutomationTemplates (eigene Templates verwalten).
 * Nutzt FormGeneratorTable wie AutomationsPage.
 */
export const AutomationTemplatesPage: React.FC = () => {
    const {
        templates,
        loading,
        permissions,
        fetchTemplates,
        fetchPermissions,
        createTemplate,
        updateTemplate,
        deleteTemplate,
    } = useAutomationTemplates();

    const [showEditor, setShowEditor] = useState(false);
    const [editingTemplate, setEditingTemplate] = useState<AutomationTemplateDB | null>(null);

    useEffect(() => {
        fetchTemplates();
        fetchPermissions();
    }, []);

    // Check permissions
    const canCreate = permissions?.create !== 'n';
    const canUpdate = permissions?.update !== 'n';
    const canDelete = permissions?.delete !== 'n';

    // Open Editor in "Template mode"
    const handleEdit = (template: AutomationTemplateDB) => {
        setEditingTemplate(template);
        setShowEditor(true);
    };

    const handleCreate = () => {
        setEditingTemplate(null);  // New template
        setShowEditor(true);
    };

    const handleSave = async (data: Partial<AutomationTemplateDB>) => {
        if (editingTemplate) {
            await updateTemplate(editingTemplate.id, data);
        } else {
            await createTemplate(data as any);
        }
        setShowEditor(false);
        fetchTemplates();
    };

    // Columns - FormGeneratorTable rendert TextMultilingual automatisch in User-Sprache!
    const columns = useMemo(() => [
        { key: 'label', label: 'Label', type: 'string' as const, sortable: true, searchable: true },
        { key: 'overview', label: 'Beschreibung', type: 'string' as const, width: 300 },
        { key: '_createdByUserName', label: 'Erstellt von', type: 'string' as const },
    ], []);

    // Handle delete
    const handleDelete = async (template: AutomationTemplateDB) => {
        await deleteTemplate(template.id);
        fetchTemplates();
    };

    return (
        <div className={styles.adminPage}>
            <div className={styles.pageHeader}>
                <div>
                    <h1 className={styles.pageTitle}>Automation-Vorlagen</h1>
                    <p className={styles.pageSubtitle}>Verwalten Sie Ihre Workflow-Vorlagen</p>
                </div>
                <div className={styles.headerActions}>
                    <button className={styles.secondaryButton} onClick={() => fetchTemplates()}>
                        <FaSync /> Aktualisieren
                    </button>
                    {canCreate && (
                        <button className={styles.primaryButton} onClick={handleCreate}>
                            <FaPlus /> Neue Vorlage
                        </button>
                    )}
                </div>
            </div>

            <div className={styles.tableContainer}>
                <FormGeneratorTable
                    data={templates as any[]}
                    columns={columns}
                    loading={loading}
                    pagination={true}
                    pageSize={25}
                    searchable={true}
                    sortable={true}
                    actionButtons={[
                        ...(canUpdate ? [{
                            type: 'edit' as const,
                            onAction: handleEdit,
                            title: 'Bearbeiten',
                        }] : []),
                        ...(canDelete ? [{
                            type: 'delete' as const,
                            title: 'Löschen',
                        }] : []),
                    ]}
                    onDelete={handleDelete}
                    emptyMessage="Keine Vorlagen vorhanden"
                />
            </div>

            {showEditor && (
                <AutomationEditor
                    mode="template"
                    initialData={editingTemplate}
                    onSave={handleSave}
                    onCancel={() => setShowEditor(false)}
                />
            )}
        </div>
    );
};

5. Neue Komponente: AutomationEditor.tsx

5.1 Konzept: Saubere Lösung via FormGeneratorForm

Kernidee: FormGeneratorForm nutzen für ALLE Felder inkl. Multilingual!

/**
 * Attribute-Definitionen je nach Modus
 * FormGeneratorForm rendert type:'multilingual' automatisch mit Sprach-Tabs
 */
function getEditorAttributes(mode: 'definition' | 'template'): AttributeDefinition[] {
    const baseAttributes: AttributeDefinition[] = [
        {
            name: 'template',
            label: 'Template JSON',
            type: 'textarea',
            required: true,
            minRows: 15,
            maxRows: 30,
            description: 'Workflow-Definition als JSON'
        }
    ];

    if (mode === 'template') {
        return [
            { name: 'label', label: 'Label', type: 'multilingual', required: true },
            { name: 'overview', label: 'Beschreibung', type: 'multilingual' },
            ...baseAttributes
        ];
    } else {
        return [
            { name: 'label', label: 'Label', type: 'text', required: true },
            { name: 'schedule', label: 'Schedule (Cron)', type: 'text' },
            { name: 'active', label: 'Aktiv', type: 'checkbox' },
            ...baseAttributes,
            { name: 'placeholders', label: 'Platzhalter', type: 'textarea', description: 'JSON-Objekt' }
        ];
    }
}

5.2 Komponente

interface AutomationEditorProps {
    mode: 'definition' | 'template';
    initialData?: AutomationTemplateDB | Automation | null;
    onSave: (data: any) => Promise<void>;
    onCancel: () => void;
}

export const AutomationEditor: React.FC<AutomationEditorProps> = ({
    mode,
    initialData,
    onSave,
    onCancel,
}) => {
    const [showActionsPanel, setShowActionsPanel] = useState(false);
    
    // Daten für FormGeneratorForm vorbereiten
    const formData = useMemo(() => {
        if (!initialData) return {};
        return {
            ...initialData,
            template: typeof initialData.template === 'string' 
                ? initialData.template 
                : JSON.stringify(initialData.template, null, 2)
        };
    }, [initialData]);

    return (
        <div className={styles.editorOverlay}>
            <div className={styles.editorContainer}>
                <header>
                    <h2>{mode === 'template' ? 'Template bearbeiten' : 'Automation bearbeiten'}</h2>
                    <button onClick={onCancel}><FaTimes /></button>
                </header>

                <div className={styles.editorContent}>
                    {/* Main Form - FormGeneratorForm macht alles! */}
                    <div className={styles.editorMain}>
                        <FormGeneratorForm
                            attributes={getEditorAttributes(mode)}
                            data={formData}
                            mode={initialData ? 'edit' : 'create'}
                            onSubmit={onSave}
                            onCancel={onCancel}
                            submitButtonText="Speichern"
                            cancelButtonText="Abbrechen"
                        />
                    </div>

                    {/* Actions Panel (optional, für Copy/Paste) */}
                    <div className={styles.actionsPanel}>
                        <button onClick={() => setShowActionsPanel(!showActionsPanel)}>
                            {showActionsPanel ? 'Actions ausblenden' : 'Actions anzeigen'}
                        </button>
                        {showActionsPanel && <ActionsPanel />}
                    </div>
                </div>
            </div>
        </div>
    );
};

Vorteile dieser Lösung:

  • Kein manueller Code für Multilingual-Inputs
  • FormGeneratorForm validiert automatisch
  • Konsistent mit dem Rest der Anwendung
  • Weniger Code, weniger Bugs

6. Neue Komponente: ActionsPanel.tsx

6.1 Struktur

interface ActionsPanelProps {
    onInsert: (actionJson: string) => void;
    onCopy?: (actionJson: string) => void;
}

export const ActionsPanel: React.FC<ActionsPanelProps> = ({ onInsert, onCopy }) => {
    const { actions, loading, fetchActions } = useWorkflowActions();
    const [filter, setFilter] = useState('');
    const [expandedAction, setExpandedAction] = useState<string | null>(null);

    useEffect(() => {
        fetchActions();
    }, []);

    const filteredActions = useMemo(() => {
        if (!filter) return actions;
        const lower = filter.toLowerCase();
        return actions.filter(a => 
            a.method.toLowerCase().includes(lower) ||
            a.action.toLowerCase().includes(lower) ||
            a.description.toLowerCase().includes(lower)
        );
    }, [actions, filter]);

    // Group by method
    const groupedActions = useMemo(() => {
        const groups: Record<string, WorkflowAction[]> = {};
        filteredActions.forEach(action => {
            if (!groups[action.method]) {
                groups[action.method] = [];
            }
            groups[action.method].push(action);
        });
        return groups;
    }, [filteredActions]);

    const handleCopy = (action: WorkflowAction) => {
        const json = JSON.stringify(action.exampleJson, null, 2);
        navigator.clipboard.writeText(json);
        onCopy?.(json);
    };

    const handleInsert = (action: WorkflowAction) => {
        const json = JSON.stringify(action.exampleJson, null, 2);
        onInsert(json);
    };

    return (
        <div className={styles.actionsPanel}>
            <h3>Verfügbare Actions</h3>
            
            <input
                type="text"
                placeholder="Suchen..."
                value={filter}
                onChange={(e) => setFilter(e.target.value)}
            />

            {loading ? (
                <div>Lade Actions...</div>
            ) : (
                <div className={styles.actionsList}>
                    {Object.entries(groupedActions).map(([method, methodActions]) => (
                        <div key={method} className={styles.methodGroup}>
                            <h4>{method}</h4>
                            {methodActions.map(action => (
                                <div key={action.actionId} className={styles.actionItem}>
                                    <div 
                                        className={styles.actionHeader}
                                        onClick={() => setExpandedAction(
                                            expandedAction === action.actionId ? null : action.actionId
                                        )}
                                    >
                                        <span className={styles.actionName}>
                                            {action.action}
                                        </span>
                                        <span className={styles.actionDesc}>
                                            {action.description}
                                        </span>
                                    </div>
                                    
                                    {expandedAction === action.actionId && (
                                        <div className={styles.actionDetails}>
                                            <h5>Parameter:</h5>
                                            <ul>
                                                {action.parameters.map(p => (
                                                    <li key={p.name}>
                                                        <strong>{p.name}</strong>
                                                        {p.required && <span className={styles.required}>*</span>}
                                                        : {p.description}
                                                    </li>
                                                ))}
                                            </ul>
                                            
                                            <h5>Beispiel JSON:</h5>
                                            <pre>{JSON.stringify(action.exampleJson, null, 2)}</pre>
                                            
                                            <div className={styles.actionButtons}>
                                                <button onClick={() => handleCopy(action)}>
                                                    <FaCopy /> Kopieren
                                                </button>
                                                <button onClick={() => handleInsert(action)}>
                                                    <FaPlus /> Einfügen
                                                </button>
                                            </div>
                                        </div>
                                    )}
                                </div>
                            ))}
                        </div>
                    ))}
                </div>
            )}
        </div>
    );
};

7. Anpassungen in AutomationsPage.tsx

7.1 Editor-Integration

Bestehende handleEditClick und Template-Auswahl anpassen:

// State for editor
const [showEditor, setShowEditor] = useState(false);
const [editorMode, setEditorMode] = useState<'definition' | 'template'>('definition');
const [editorInitialData, setEditorInitialData] = useState<Automation | AutomationTemplateDB | null>(null);

// Edit existing definition
const handleEditClick = async (automation: Automation) => {
    const fullAutomation = await fetchAutomationById(automation.id);
    setEditorInitialData(fullAutomation);
    setEditorMode('definition');
    setShowEditor(true);
};

// Create from template
const handleCreateFromTemplate = async (template: AutomationTemplateDB) => {
    // Extract label in user's language
    const userLang = getUserLanguage(); // e.g., 'de' or 'en'
    const label = template.label[userLang] || template.label.en || 'New Automation';
    
    // Pre-fill new definition
    const newDefinition: Partial<Automation> = {
        label: label,
        template: template.template,
        placeholders: {},
        schedule: '0 22 * * *',
        active: false,
        mandateId: mandateId,
        featureInstanceId: featureInstanceId,
    };
    
    setEditorInitialData(newDefinition as Automation);
    setEditorMode('definition');
    setShowEditor(true);
};

// Save handler from editor
const handleEditorSave = async (data: any) => {
    if (editorMode === 'definition') {
        if (editorInitialData?.id) {
            await handleAutomationUpdate(editorInitialData.id, data);
        } else {
            await handleAutomationCreate(data);
        }
    }
    // Template save is handled in AutomationTemplatesPage
    setShowEditor(false);
    refetch();
};

7.2 Template-Auswahl Modal anpassen

Bisherige Template-Auswahl nutzt jetzt AutomationTemplateDB statt altes Format:

const [templatesDB, setTemplatesDB] = useState<AutomationTemplateDB[]>([]);

const loadTemplates = async () => {
    setLoadingTemplates(true);
    const data = await fetchAutomationTemplatesDB(request);
    setTemplatesDB(data);
    setLoadingTemplates(false);
};

// In Template-Modal: Display label in current language
// Nutze inline-Zugriff oder FormGeneratorTable für Liste
const { currentLanguage } = useLanguage();
const getLang = (ml: TextMultilingual | undefined) => 
    ml?.[currentLanguage as keyof TextMultilingual] || ml?.en || '';

{templatesDB.map(template => (
    <div key={template.id} onClick={() => handleCreateFromTemplate(template)}>
        <h4>{getLang(template.label)}</h4>
        <p>{getLang(template.overview)}</p>
    </div>
))}

8. Navigation / Routing

Architektur-Hinweis: ObjectKey-Formate

Kontext Format Beispiel Verwendung
Backend Navigation ui.system.xxx ui.system.automations NAVIGATION_SECTIONS in mainSystem.py
Frontend Icons page.system.xxx page.system.automations pageRegistry.tsx PAGE_ICONS
RBAC DATA data.automation.xxx data.automation.AutomationTemplate Tabellen-Zugriffsregeln
RBAC UI ui.feature.automation.xxx ui.feature.automation.templates Feature-UI-Objekte

Die Sidebar-Navigation kommt vom Backend (/api/navigation), nicht aus pageRegistry. PageRegistry liefert nur die Icons zu den vom Backend gelieferten Items.


8.1 Backend: mainSystem.py (NAVIGATION_SECTIONS)

WICHTIG: Die Navigation wird vom Backend gesteuert! In gateway/modules/system/mainSystem.py unter NAVIGATION_SECTIONS im Abschnitt "workflows" hinzufügen:

{
    "id": "automation-templates",
    "objectKey": "ui.system.automation-templates",
    "label": {"en": "Templates", "de": "Vorlagen", "fr": "Modèles"},
    "icon": "FaFileAlt",  # Oder FaCopy für Vorlagen
    "path": "/workflows/automation-templates",
    "order": 35,  # Nach automations (30)
    "public": True,
},

8.2 Frontend: App.tsx

Route innerhalb des <Route path="workflows"> Blocks hinzufügen:

// In App.tsx, im workflows-Block:
<Route path="workflows">
    <Route path="playground" element={<PlaygroundPage />} />
    <Route path="list" element={<WorkflowsPage />} />
    <Route path="automations" element={<AutomationsPage />} />
    <Route path="automation-templates" element={<AutomationTemplatesPage />} />  {/* NEU */}
</Route>

Import hinzufügen:

import { AutomationTemplatesPage } from './pages/workflows/AutomationTemplatesPage';

8.3 Frontend: pageRegistry.tsx

WICHTIG: Format ist page.system.xxx, nicht ui.system.xxx!

// Import hinzufügen (FaFileAlt ist bereits importiert):
// import { ..., FaFileAlt, ... } from 'react-icons/fa';

// In PAGE_ICONS hinzufügen:
'page.system.automation-templates': <FaFileAlt />,

8.4 Frontend: pages/workflows/index.ts

Export hinzufügen:

export { PlaygroundPage } from './PlaygroundPage';
export { WorkflowsPage } from './WorkflowsPage';
export { AutomationsPage } from './AutomationsPage';
export { AutomationTemplatesPage } from './AutomationTemplatesPage';  // NEU

8.5 Locales (de.ts, en.ts, fr.ts)

Übersetzungen hinzufügen:

de.ts:

'nav.automation-templates': 'Vorlagen',
'automation-templates.title': 'Automation-Vorlagen',
'automation-templates.create': 'Neue Vorlage',
'automation-templates.edit': 'Vorlage bearbeiten',
'automation-templates.empty': 'Keine Vorlagen vorhanden',

en.ts:

'nav.automation-templates': 'Templates',
'automation-templates.title': 'Automation Templates',
'automation-templates.create': 'New Template',
'automation-templates.edit': 'Edit Template',
'automation-templates.empty': 'No templates available',

9. Hilfsfunktionen

Hinweis: getMultilingualText() ist NICHT nötig - FormGeneratorTable rendert TextMultilingual automatisch in der User-Sprache via formatTextMultilingual() intern.

9.1 extractPlaceholdersFromJson

/**
 * Extract {{KEY:name}} placeholders from JSON string
 */
export function extractPlaceholdersFromJson(jsonString: string): string[] {
    const regex = /\{\{KEY:(\w+)\}\}/g;
    const keys: string[] = [];
    let match;
    while ((match = regex.exec(jsonString)) !== null) {
        if (!keys.includes(match[1])) {
            keys.push(match[1]);
        }
    }
    return keys;
}

10. Zusammenfassung der Dateien

Datei Änderung
api/automationApi.ts Neue Typen + API-Funktionen für Templates (DB) und Actions
hooks/useAutomations.ts Neue Hooks: useAutomationTemplates, useWorkflowActions
pages/workflows/AutomationsPage.tsx Editor-Integration, Template-Auswahl mit DB-Templates
pages/workflows/AutomationTemplatesPage.tsx Neu: CRUD für eigene Templates
components/AutomationEditor/AutomationEditor.tsx Neu: Editor mit Modus-Flag
components/AutomationEditor/ActionsPanel.tsx Neu: Actions-Katalog
App.tsx Neue Route für AutomationTemplatesPage
config/pageRegistry.tsx Icon für neue Seite

11. Implementierungsreihenfolge

  1. API-Typen und Funktionen (automationApi.ts)
  2. Hooks (useAutomations.ts erweitern)
  3. ActionsPanel (unabhängig testbar)
  4. AutomationEditor (Kern-Komponente)
  5. AutomationTemplatesPage (CRUD für Templates)
  6. AutomationsPage anpassen (Editor-Integration)
  7. Routing (App.tsx, Navigation)