933 lines
30 KiB
Markdown
933 lines
30 KiB
Markdown
# 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)
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// ============================================================================
|
||
// 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
|
||
|
||
```typescript
|
||
/**
|
||
* 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
|
||
|
||
```typescript
|
||
/**
|
||
* 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
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
/**
|
||
* 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!
|
||
|
||
```typescript
|
||
/**
|
||
* 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
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
// 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:
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```python
|
||
{
|
||
"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:
|
||
|
||
```typescript
|
||
// 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:**
|
||
```typescript
|
||
import { AutomationTemplatesPage } from './pages/workflows/AutomationTemplatesPage';
|
||
```
|
||
|
||
### 8.3 Frontend: pageRegistry.tsx
|
||
|
||
**WICHTIG:** Format ist `page.system.xxx`, nicht `ui.system.xxx`!
|
||
|
||
```typescript
|
||
// 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:
|
||
```typescript
|
||
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:**
|
||
```typescript
|
||
'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:**
|
||
```typescript
|
||
'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
|
||
|
||
```typescript
|
||
/**
|
||
* 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)
|