From 3bf79e1ae53e8e4f45f762d11582e4992d57966c Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Tue, 7 Apr 2026 00:49:12 +0200 Subject: [PATCH] automation unification implemented --- src/App.tsx | 4 +- src/api/automation2Api.ts | 532 -------- src/api/automationApi.ts | 385 ------ src/api/workflowApi.ts | 1155 ++++++++++------- .../ActionsPanel/ActionsPanel.module.css | 289 ----- src/components/ActionsPanel/ActionsPanel.tsx | 216 --- src/components/ActionsPanel/index.ts | 2 - .../editor/CanvasHeader.tsx | 130 -- .../AutomationEditor.module.css | 946 -------------- .../AutomationEditor/AutomationEditor.tsx | 1072 --------------- src/components/AutomationEditor/index.ts | 2 - src/components/Chat/ChatInput.tsx | 102 ++ src/components/Chat/ChatMessageList.tsx | 89 ++ src/components/Chat/index.ts | 3 + .../context/Automation2DataFlowContext.tsx | 2 +- .../editor/Automation2FlowEditor.module.css | 0 .../editor/Automation2FlowEditor.tsx | 148 ++- .../FlowEditor/editor/CanvasHeader.tsx | 250 ++++ .../FlowEditor/editor/EditorChatPanel.tsx | 145 +++ .../editor/FlowCanvas.tsx | 2 +- .../editor/NodeConfigPanel.tsx | 4 +- .../editor/NodeListItem.tsx | 2 +- .../editor/NodeSidebar.tsx | 2 +- .../FlowEditor/editor/RunTracingPanel.tsx | 116 ++ .../editor/WorkflowConfigurationModal.tsx | 2 +- .../index.ts | 2 +- .../nodes/configs/AiNodeConfig.tsx | 0 .../nodes/configs/ApprovalNodeConfig.tsx | 0 .../nodes/configs/ClickUpNodeConfig.tsx | 2 +- .../nodes/configs/CommentNodeConfig.tsx | 0 .../nodes/configs/ConfirmationNodeConfig.tsx | 0 .../nodes/configs/EmailNodeConfig.tsx | 2 +- .../nodes/configs/FileCreateNodeConfig.tsx | 0 .../nodes/configs/ReviewNodeConfig.tsx | 0 .../nodes/configs/SelectionNodeConfig.tsx | 0 .../nodes/configs/SharePointNodeConfig.tsx | 2 +- .../nodes/configs/UploadNodeConfig.tsx | 0 .../nodes/configs/index.ts | 0 .../nodes/configs/types.ts | 0 .../nodes/form/FormNodeConfig.tsx | 2 +- .../nodes/form/index.ts | 0 .../nodes/ifElse/IfElseNodeConfig.tsx | 0 .../nodes/ifElse/index.ts | 0 .../nodes/loop/LoopNodeConfig.tsx | 0 .../nodes/loop/index.ts | 0 .../nodes/runtime/fileTypeMimeMapping.ts | 0 .../nodes/runtime/scheduleCron.ts | 0 .../nodes/runtime/workflowStartSync.ts | 4 +- .../nodes/shared/DataPicker.tsx | 0 .../nodes/shared/DynamicValueField.tsx | 0 .../nodes/shared/HybridStaticRefField.tsx | 0 .../nodes/shared/LoopItemsSelect.tsx | 0 .../nodes/shared/RefSourceSelect.tsx | 0 .../nodes/shared/categoryIcons.tsx | 0 .../nodes/shared/clickupFormSync.ts | 0 .../nodes/shared/conditionOperators.ts | 0 .../nodes/shared/constants.ts | 0 .../nodes/shared/dataFlowGraph.ts | 0 .../nodes/shared/dataRef.ts | 0 .../nodes/shared/graphUtils.ts | 2 +- .../nodes/shared/outputPreviewRegistry.ts | 0 .../nodes/shared/types.ts | 2 +- .../nodes/shared/utils.ts | 0 .../nodes/start/FormStartNodeConfig.tsx | 0 .../nodes/start/ScheduleStartNodeConfig.tsx | 0 .../nodes/start/StartNodeConfig.tsx | 0 .../nodes/start/index.ts | 0 .../nodes/switch/SwitchNodeConfig.tsx | 0 .../nodes/switch/index.ts | 0 .../TreeNavigation/TreeNavigation.module.css | 7 +- .../TreeNavigation/TreeNavigation.tsx | 12 +- src/config/pageRegistry.tsx | 11 +- src/hooks/useAutomations.ts | 618 --------- src/layouts/MainLayout.tsx | 6 +- src/pages/FeatureView.tsx | 39 +- src/pages/Store.tsx | 4 +- src/pages/admin/AdminAutomationEventsPage.tsx | 243 ---- src/pages/admin/AdminAutomationLogsPage.tsx | 223 ---- src/pages/admin/AdminFeatureAccessPage.tsx | 21 - .../admin/AdminFeatureInstanceUsersPage.tsx | 20 - src/pages/admin/AdminFeatureRolesPage.tsx | 19 - src/pages/admin/AdminInvitationsPage.tsx | 19 - src/pages/admin/AdminMandateRolesPage.tsx | 23 - src/pages/admin/AdminMandatesPage.tsx | 25 +- src/pages/admin/AdminUserMandatesPage.tsx | 20 - src/pages/admin/AdminUsersPage.tsx | 25 +- src/pages/admin/index.ts | 2 - src/pages/basedata/ConnectionsPage.tsx | 47 +- src/pages/basedata/FilesPage.tsx | 26 +- .../automation/AutomationDefinitionsView.tsx | 623 --------- .../automation/AutomationTemplatesView.tsx | 198 --- src/pages/views/automation/index.ts | 6 - .../views/automation2/Automation2Page.tsx | 38 - .../Automation2WorkflowsTasks.module.css | 0 .../GraphicalEditorDashboardPage.tsx | 256 ++++ .../GraphicalEditorKeepAlive.tsx | 58 + .../graphicalEditor/GraphicalEditorPage.tsx | 81 ++ .../GraphicalEditorTemplatesPage.tsx | 269 ++++ .../GraphicalEditorWorkflowsPage.tsx} | 28 +- .../GraphicalEditorWorkflowsTasksPage.tsx} | 16 +- src/types/mandate.ts | 17 +- 101 files changed, 2321 insertions(+), 6297 deletions(-) delete mode 100644 src/api/automation2Api.ts delete mode 100644 src/api/automationApi.ts delete mode 100644 src/components/ActionsPanel/ActionsPanel.module.css delete mode 100644 src/components/ActionsPanel/ActionsPanel.tsx delete mode 100644 src/components/ActionsPanel/index.ts delete mode 100644 src/components/Automation2FlowEditor/editor/CanvasHeader.tsx delete mode 100644 src/components/AutomationEditor/AutomationEditor.module.css delete mode 100644 src/components/AutomationEditor/AutomationEditor.tsx delete mode 100644 src/components/AutomationEditor/index.ts create mode 100644 src/components/Chat/ChatInput.tsx create mode 100644 src/components/Chat/ChatMessageList.tsx create mode 100644 src/components/Chat/index.ts rename src/components/{Automation2FlowEditor => FlowEditor}/context/Automation2DataFlowContext.tsx (97%) rename src/components/{Automation2FlowEditor => FlowEditor}/editor/Automation2FlowEditor.module.css (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/editor/Automation2FlowEditor.tsx (74%) create mode 100644 src/components/FlowEditor/editor/CanvasHeader.tsx create mode 100644 src/components/FlowEditor/editor/EditorChatPanel.tsx rename src/components/{Automation2FlowEditor => FlowEditor}/editor/FlowCanvas.tsx (99%) rename src/components/{Automation2FlowEditor => FlowEditor}/editor/NodeConfigPanel.tsx (96%) rename src/components/{Automation2FlowEditor => FlowEditor}/editor/NodeListItem.tsx (95%) rename src/components/{Automation2FlowEditor => FlowEditor}/editor/NodeSidebar.tsx (98%) create mode 100644 src/components/FlowEditor/editor/RunTracingPanel.tsx rename src/components/{Automation2FlowEditor => FlowEditor}/editor/WorkflowConfigurationModal.tsx (98%) rename src/components/{Automation2FlowEditor => FlowEditor}/index.ts (83%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/configs/AiNodeConfig.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/configs/ApprovalNodeConfig.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/configs/ClickUpNodeConfig.tsx (99%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/configs/CommentNodeConfig.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/configs/ConfirmationNodeConfig.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/configs/EmailNodeConfig.tsx (99%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/configs/FileCreateNodeConfig.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/configs/ReviewNodeConfig.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/configs/SelectionNodeConfig.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/configs/SharePointNodeConfig.tsx (99%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/configs/UploadNodeConfig.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/configs/index.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/configs/types.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/form/FormNodeConfig.tsx (99%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/form/index.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/ifElse/IfElseNodeConfig.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/ifElse/index.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/loop/LoopNodeConfig.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/loop/index.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/runtime/fileTypeMimeMapping.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/runtime/scheduleCron.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/runtime/workflowStartSync.ts (98%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/DataPicker.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/DynamicValueField.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/HybridStaticRefField.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/LoopItemsSelect.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/RefSourceSelect.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/categoryIcons.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/clickupFormSync.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/conditionOperators.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/constants.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/dataFlowGraph.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/dataRef.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/graphUtils.ts (98%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/outputPreviewRegistry.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/types.ts (92%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/shared/utils.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/start/FormStartNodeConfig.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/start/ScheduleStartNodeConfig.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/start/StartNodeConfig.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/start/index.ts (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/switch/SwitchNodeConfig.tsx (100%) rename src/components/{Automation2FlowEditor => FlowEditor}/nodes/switch/index.ts (100%) delete mode 100644 src/hooks/useAutomations.ts delete mode 100644 src/pages/admin/AdminAutomationEventsPage.tsx delete mode 100644 src/pages/admin/AdminAutomationLogsPage.tsx delete mode 100644 src/pages/views/automation/AutomationDefinitionsView.tsx delete mode 100644 src/pages/views/automation/AutomationTemplatesView.tsx delete mode 100644 src/pages/views/automation/index.ts delete mode 100644 src/pages/views/automation2/Automation2Page.tsx rename src/pages/views/{automation2 => graphicalEditor}/Automation2WorkflowsTasks.module.css (100%) create mode 100644 src/pages/views/graphicalEditor/GraphicalEditorDashboardPage.tsx create mode 100644 src/pages/views/graphicalEditor/GraphicalEditorKeepAlive.tsx create mode 100644 src/pages/views/graphicalEditor/GraphicalEditorPage.tsx create mode 100644 src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx rename src/pages/views/{automation2/Automation2WorkflowsPage.tsx => graphicalEditor/GraphicalEditorWorkflowsPage.tsx} (90%) rename src/pages/views/{automation2/Automation2WorkflowsTasksPage.tsx => graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx} (98%) diff --git a/src/App.tsx b/src/App.tsx index 62adda3..9c69204 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,7 +38,7 @@ import { SettingsPage } from './pages/Settings'; import { GDPRPage } from './pages/GDPR'; import StorePage from './pages/Store'; import { FeatureViewPage } from './pages/FeatureView'; -import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminAutomationEventsPage, AdminAutomationLogsPage, AdminLogsPage } from './pages/admin'; +import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminLogsPage } from './pages/admin'; import { AdminMandateWizardPage, AdminInvitationWizardPage } from './pages/admin/wizards'; import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata'; import { BillingDataView, BillingAdmin, BillingMandateView, AdminSubscriptionsPage } from './pages/billing'; @@ -195,8 +195,6 @@ function App() { } /> } /> - } /> - } /> } /> } /> } /> diff --git a/src/api/automation2Api.ts b/src/api/automation2Api.ts deleted file mode 100644 index d7d7641..0000000 --- a/src/api/automation2Api.ts +++ /dev/null @@ -1,532 +0,0 @@ -/** - * Automation2 API - * Node types and graph execution for n8n-style flows. - */ - -import type { ApiRequestOptions } from '../hooks/useApi'; - -const LOG = '[Automation2]'; - -// ============================================================================ -// TYPES -// ============================================================================ - -export interface NodeTypeParameter { - name: string; - type: string; - required?: boolean; - description?: string; - default?: unknown; -} - -export interface NodeType { - id: string; - category: string; - label: string; - description: string; - parameters: NodeTypeParameter[]; - inputs: number; - outputs: number; - /** Labels per output (e.g. ["Ja", "Nein"] for flow.ifElse) */ - outputLabels?: string[]; - executor: string; - meta?: { - icon?: string; - color?: string; - method?: string; - action?: string; - }; -} - -export interface NodeTypeCategory { - id: string; - label: Record | string; -} - -export interface NodeTypesResponse { - nodeTypes: NodeType[]; - categories: NodeTypeCategory[]; -} - -export interface Automation2GraphNode { - id: string; - type: string; - parameters?: Record; -} - -export interface Automation2Connection { - source: string; - target: string; - sourceOutput?: number; - targetInput?: number; -} - -export interface Automation2Graph { - nodes: Automation2GraphNode[]; - connections: Automation2Connection[]; -} - -export interface ExecuteGraphResponse { - success: boolean; - nodeOutputs?: Record; - error?: string; - stopped?: boolean; - failedNode?: string; - paused?: boolean; - taskId?: string; - runId?: string; - nodeId?: string; -} - -/** Entry point / start configured outside the canvas (manual, form, schedule, …) */ -export interface WorkflowEntryPoint { - id: string; - kind: string; - category: 'on_demand' | 'always_on'; - enabled: boolean; - title: Record | string; - description?: Record; - config: Record; -} - -export interface Automation2Workflow { - id: string; - label: string; - graph: Automation2Graph; - active?: boolean; - /** Entry points (Starts) — how this workflow may be invoked */ - invocations?: WorkflowEntryPoint[]; - /** Enriched: run count */ - runCount?: number; - /** Enriched: has active (running/paused) run */ - isRunning?: boolean; - /** Enriched: status of active run */ - runStatus?: string; - /** Enriched: nodeId where workflow is stuck (paused) */ - stuckAtNodeId?: string; - /** Enriched: human-readable label for stuck node */ - stuckAtNodeLabel?: string; - /** Enriched: created timestamp (seconds) */ - createdAt?: number; - /** Enriched: last run started timestamp (seconds) */ - lastStartedAt?: number; -} - -// ============================================================================ -// API FUNCTIONS -// ============================================================================ - -export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; - -/** - * Fetch node types for the flow builder (backend-driven). - * GET /api/automation2/{instanceId}/node-types?language=de - */ -export async function fetchNodeTypes( - request: ApiRequestFunction, - instanceId: string, - language = 'de' -): Promise { - console.log(`${LOG} fetchNodeTypes: instanceId=${instanceId} language=${language}`); - const data = await request({ - url: `/api/automation2/${instanceId}/node-types`, - method: 'get', - params: { language }, - }); - const nodeTypes = data?.nodeTypes ?? []; - const categories = data?.categories ?? []; - console.log(`${LOG} fetchNodeTypes response: ${nodeTypes.length} nodeTypes, ${categories.length} categories`); - return { nodeTypes, categories }; -} - -/** - * Execute an automation2 graph. - * POST /api/automation2/{instanceId}/execute - */ -export interface ExecuteGraphOptions { - /** Use a configured start on the saved workflow */ - entryPointId?: string; - /** Full run envelope (overrides entry point mapping) */ - runEnvelope?: Record; - /** Merged into envelope.payload */ - payload?: Record; -} - -export async function executeGraph( - request: ApiRequestFunction, - instanceId: string, - graph: Automation2Graph, - workflowId?: string, - options?: ExecuteGraphOptions -): Promise { - console.log( - `${LOG} executeGraph request: instanceId=${instanceId} workflowId=${workflowId} nodes=${graph.nodes.length} connections=${graph.connections.length}`, - { nodes: graph.nodes, connections: graph.connections, options } - ); - const start = performance.now(); - try { - const data: Record = { graph, workflowId }; - if (options?.entryPointId) data.entryPointId = options.entryPointId; - if (options?.runEnvelope) data.runEnvelope = options.runEnvelope; - if (options?.payload && Object.keys(options.payload).length > 0) data.payload = options.payload; - const result = await request({ - url: `/api/automation2/${instanceId}/execute`, - method: 'post', - data, - }); - const ms = Math.round(performance.now() - start); - console.log( - `${LOG} executeGraph response (${ms}ms): success=${result?.success} error=${result?.error ?? 'none'} nodeOutputs_keys=${Object.keys(result?.nodeOutputs ?? {}).join(',')} failedNode=${result?.failedNode ?? '-'}`, - result - ); - return result; - } catch (err) { - const ms = Math.round(performance.now() - start); - console.error( - `${LOG} executeGraph FAILED (${ms}ms): instanceId=${instanceId}`, - err - ); - throw err; - } -} - -// ------------------------------------------------------------------------- -// Workflows CRUD -// ------------------------------------------------------------------------- - -export async function fetchWorkflows( - request: ApiRequestFunction, - instanceId: string, - params?: { active?: boolean } -): Promise { - const data = await request({ - url: `/api/automation2/${instanceId}/workflows`, - method: 'get', - params: params?.active !== undefined ? { active: params.active } : undefined, - }); - return data?.workflows ?? []; -} - -export async function fetchWorkflow( - request: ApiRequestFunction, - instanceId: string, - workflowId: string -): Promise { - return await request({ - url: `/api/automation2/${instanceId}/workflows/${workflowId}`, - method: 'get', - }); -} - -export async function createWorkflow( - request: ApiRequestFunction, - instanceId: string, - body: { label: string; graph: Automation2Graph; invocations?: WorkflowEntryPoint[] } -): Promise { - return await request({ - url: `/api/automation2/${instanceId}/workflows`, - method: 'post', - data: body, - }); -} - -export async function updateWorkflow( - request: ApiRequestFunction, - instanceId: string, - workflowId: string, - body: { - label?: string; - graph?: Automation2Graph; - invocations?: WorkflowEntryPoint[]; - active?: boolean; - } -): Promise { - return await request({ - url: `/api/automation2/${instanceId}/workflows/${workflowId}`, - method: 'put', - data: body, - }); -} - -export async function deleteWorkflow( - request: ApiRequestFunction, - instanceId: string, - workflowId: string -): Promise { - await request({ - url: `/api/automation2/${instanceId}/workflows/${workflowId}`, - method: 'delete', - }); -} - -export interface Automation2Run { - id: string; - workflowId: string; - status: string; - nodeOutputs?: Record; - currentNodeId?: string; -} - -export async function fetchWorkflowRuns( - request: ApiRequestFunction, - instanceId: string, - workflowId: string -): Promise { - const data = await request({ - url: `/api/automation2/${instanceId}/workflows/${workflowId}/runs`, - method: 'get', - }); - return data?.runs ?? []; -} - -export interface CompletedRun extends Automation2Run { - workflowLabel?: string; - sysModifiedAt?: number; - sysCreatedAt?: number; -} - -export async function fetchCompletedRuns( - request: ApiRequestFunction, - instanceId: string, - limit = 20 -): Promise { - const data = await request({ - url: `/api/automation2/${instanceId}/runs/completed`, - method: 'get', - params: { limit }, - }); - return data?.runs ?? []; -} - -// ------------------------------------------------------------------------- -// Tasks -// ------------------------------------------------------------------------- - -export interface Automation2Task { - id: string; - runId: string; - workflowId: string; - nodeId: string; - nodeType: string; - config: Record; - status: string; - result?: Record; - /** Workflow label (enriched by API) */ - workflowLabel?: string; - /** Unix timestamp ms (from sysCreatedAt) */ - createdAt?: number; - /** Optional due date - configurable in future */ - dueAt?: number; -} - -export async function fetchTasks( - request: ApiRequestFunction, - instanceId: string, - params?: { workflowId?: string; status?: string } -): Promise { - const data = await request({ - url: `/api/automation2/${instanceId}/tasks`, - method: 'get', - params, - }); - return data?.tasks ?? []; -} - -export async function completeTask( - request: ApiRequestFunction, - instanceId: string, - taskId: string, - result: Record -): Promise { - return await request({ - url: `/api/automation2/${instanceId}/tasks/${taskId}/complete`, - method: 'post', - data: { result }, - }); -} - -// ------------------------------------------------------------------------- -// Connections and Browse (for Email/SharePoint node config) -// ------------------------------------------------------------------------- - -export interface UserConnection { - id: string; - authority: string; - externalUsername?: string; - externalEmail?: string; - status: string; -} - -export async function fetchConnections( - request: ApiRequestFunction, - instanceId: string -): Promise { - const data = await request({ - url: `/api/automation2/${instanceId}/connections`, - method: 'get', - }); - return data?.connections ?? []; -} - -export interface ConnectionService { - service: string; - label: string; - icon: string; -} - -export async function fetchConnectionServices( - request: ApiRequestFunction, - instanceId: string, - connectionId: string -): Promise { - const data = await request({ - url: `/api/automation2/${instanceId}/connections/${connectionId}/services`, - method: 'get', - }); - return data?.services ?? []; -} - -export interface BrowseEntry { - name: string; - path: string; - isFolder: boolean; - size?: number; - mimeType?: string; - metadata?: Record; -} - -export async function fetchBrowse( - request: ApiRequestFunction, - instanceId: string, - connectionId: string, - service: string, - path = '/' -): Promise<{ items: BrowseEntry[]; path: string; service: string }> { - const data = await request({ - url: `/api/automation2/${instanceId}/connections/${connectionId}/browse`, - method: 'get', - params: { service, path }, - }); - return { items: data?.items ?? [], path: data?.path ?? path, service: data?.service ?? service }; -} - -/** ClickUp GET /task/{taskId} — list.id for resolving list-scoped fields when only task id is known. */ -export async function fetchClickupTask( - request: ApiRequestFunction, - connectionId: string, - taskId: string -): Promise> { - const data = await request({ - url: `/api/clickup/${connectionId}/tasks/${encodeURIComponent(taskId)}`, - method: 'get', - }); - return data && typeof data === 'object' ? (data as Record) : {}; -} - -/** ClickUp list metadata (statuses, etc.) — GET /api/clickup/{connectionId}/lists/{listId}. */ -export async function fetchClickupList( - request: ApiRequestFunction, - connectionId: string, - listId: string -): Promise> { - const data = await request({ - url: `/api/clickup/${connectionId}/lists/${listId}`, - method: 'get', - }); - return data && typeof data === 'object' ? (data as Record) : {}; -} - -/** ClickUp workspace/team (members for assignees) — GET /api/clickup/{connectionId}/teams/{teamId}. */ -export async function fetchClickupTeam( - request: ApiRequestFunction, - connectionId: string, - teamId: string -): Promise> { - const data = await request({ - url: `/api/clickup/${connectionId}/teams/${teamId}`, - method: 'get', - }); - return data && typeof data === 'object' ? (data as Record) : {}; -} - -/** ClickUp list custom fields (GET /api/clickup/{connectionId}/lists/{listId}/fields). */ -export async function fetchClickupListFields( - request: ApiRequestFunction, - connectionId: string, - listId: string -): Promise<{ fields?: unknown[] } & Record> { - const data = await request({ - url: `/api/clickup/${connectionId}/lists/${listId}/fields`, - method: 'get', - }); - return (data && typeof data === 'object' ? data : {}) as { fields?: unknown[] } & Record; -} - -/** ClickUp GET /list/{id}/task page (tasks in a list for relationship dropdowns). */ -export interface ClickupListTaskItem { - id?: string; - name?: string; -} - -export async function fetchClickupListTasks( - request: ApiRequestFunction, - connectionId: string, - listId: string, - options?: { page?: number; includeClosed?: boolean } -): Promise< - { tasks?: ClickupListTaskItem[]; last_page?: boolean } & Record -> { - const data = await request({ - url: `/api/clickup/${connectionId}/lists/${listId}/tasks`, - method: 'get', - params: { - page: options?.page ?? 0, - include_closed: options?.includeClosed ?? false, - }, - }); - return (data && typeof data === 'object' ? data : {}) as { - tasks?: ClickupListTaskItem[]; - last_page?: boolean; - } & Record; -} - -/** Paginated tasks in a list — for ClickUp relationship dropdowns and input.form „ClickUp-Aufgabe“. */ -export async function loadClickupListTasksForDropdown( - request: ApiRequestFunction, - connectionId: string, - listId: string -): Promise> { - const acc: Array<{ id: string; name: string }> = []; - const seen = new Set(); - const maxPages = 12; - const pageSizeHint = 100; - for (let page = 0; page < maxPages; page++) { - const data = await fetchClickupListTasks(request, connectionId, listId, { - page, - includeClosed: false, - }); - if (data && typeof data === 'object' && 'error' in data && (data as { error?: unknown }).error) { - const err = (data as { error?: unknown }).error; - const body = (data as { body?: string }).body; - throw new Error( - typeof err === 'string' ? err + (body ? `: ${body.slice(0, 200)}` : '') : 'ClickUp API error' - ); - } - const tasks = Array.isArray(data.tasks) ? data.tasks : []; - for (const t of tasks) { - const id = t?.id != null ? String(t.id) : ''; - if (!id || seen.has(id)) continue; - seen.add(id); - acc.push({ id, name: String(t.name ?? id) }); - } - const rawLast = (data as Record).last_page; - const last = - rawLast === true || - rawLast === 'true' || - tasks.length === 0 || - tasks.length < pageSizeHint; - if (last) break; - } - acc.sort((a, b) => a.name.localeCompare(b.name, 'de')); - return acc; -} diff --git a/src/api/automationApi.ts b/src/api/automationApi.ts deleted file mode 100644 index 955ada7..0000000 --- a/src/api/automationApi.ts +++ /dev/null @@ -1,385 +0,0 @@ -import { ApiRequestOptions } from '../hooks/useApi'; - -// ============================================================================ -// TYPES & INTERFACES -// ============================================================================ - -export interface Automation { - id: string; - mandateId: string; - featureInstanceId: string; - label: string; - template: string | object; - placeholders: Record; - schedule: string; - active: boolean; - status?: string; - lastExecution?: number; - nextExecution?: number; - executionLogs?: AutomationLog[]; - allowedProviders?: string[]; - sysCreatedAt?: number; - _updatedAt?: number; - sysCreatedByUserName?: string; - mandateName?: string; - featureInstanceName?: string; - [key: string]: any; -} - -export interface AutomationLog { - id: string; - timestamp: number; - status: string; - workflowId?: string; - messages?: string[]; -} - -// Multilingual text type (matches backend TextMultilingual) -export interface TextMultilingual { - en: string; - ge?: string; - fr?: string; - it?: string; -} - -// AutomationTemplate from DB -export interface AutomationTemplate { - id: string; - label: TextMultilingual; - overview?: TextMultilingual; - template: string; // JSON string with {{KEY:...}} placeholders - sysCreatedAt?: number; - sysCreatedBy?: string; - sysCreatedByUserName?: string; -} - -// Workflow 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; - execResultLabel: string; - }; -} - -export interface WorkflowActionParameter { - name: string; - type: string; - frontendType: string; - required: boolean; - default?: any; - description: string; - frontendOptions?: string | string[]; -} - -export interface CreateAutomationRequest { - label: string; - template: string; - placeholders?: Record; - schedule?: string; - active?: boolean; - mandateId?: string; - featureInstanceId?: string; -} - -export interface UpdateAutomationRequest { - label?: string; - template?: string; - placeholders?: Record; - schedule?: string; - active?: boolean; -} - -export interface ExecuteAutomationResponse { - id: string; - status: string; - workflowId?: string; - [key: string]: any; -} - -// Type for the request function passed to API functions -export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; - -// ============================================================================ -// API REQUEST FUNCTIONS -// ============================================================================ - -/** - * Fetch all automations for the current mandate - * Endpoint: GET /api/automations - */ -export async function fetchAutomations(request: ApiRequestFunction): Promise { - console.log('📤 fetchAutomations: Making API request to /api/automations'); - - try { - const data = await request({ - url: '/api/automations', - method: 'get' - }); - - console.log('📥 fetchAutomations: API response:', data); - - // Handle different response formats - let automations: Automation[] = []; - - if (Array.isArray(data)) { - automations = data; - } else if (data && typeof data === 'object') { - if (Array.isArray(data.automations)) { - automations = data.automations; - } else if (Array.isArray(data.items)) { - automations = data.items; - } else if (Array.isArray(data.data)) { - automations = data.data; - } - } - - console.log(`✅ fetchAutomations: Returning ${automations.length} automations`); - return automations; - } catch (error) { - console.error('❌ fetchAutomations: Error fetching automations:', error); - throw error; - } -} - -/** - * Fetch a single automation by ID - * Endpoint: GET /api/automations/{automationId} - */ -export async function fetchAutomation( - request: ApiRequestFunction, - automationId: string -): Promise { - return await request({ - url: `/api/automations/${automationId}`, - method: 'get' - }); -} - -/** - * Create a new automation - * Endpoint: POST /api/automations - */ -export async function createAutomationApi( - request: ApiRequestFunction, - automationData: CreateAutomationRequest -): Promise { - return await request({ - url: '/api/automations', - method: 'post', - data: automationData - }); -} - -/** - * Update an existing automation - * Endpoint: PUT /api/automations/{automationId} - */ -export async function updateAutomationApi( - request: ApiRequestFunction, - automationId: string, - updateData: UpdateAutomationRequest -): Promise { - return await request({ - url: `/api/automations/${automationId}`, - method: 'put', - data: updateData - }); -} - -/** - * Delete an automation - * Endpoint: DELETE /api/automations/{automationId} - */ -export async function deleteAutomationApi( - request: ApiRequestFunction, - automationId: string -): Promise { - await request({ - url: `/api/automations/${automationId}`, - method: 'delete' - }); -} - -/** - * Execute an automation (test mode) - * Endpoint: POST /api/automations/{automationId}/execute - */ -export async function executeAutomationApi( - request: ApiRequestFunction, - automationId: string -): Promise { - return await request({ - url: `/api/automations/${automationId}/execute`, - method: 'post' - }); -} - -/** - * Fetch automation attributes for dynamic form generation - * Endpoint: GET /api/attributes/AutomationDefinition - */ -export async function fetchAutomationAttributes( - request: ApiRequestFunction -): Promise { - const data = await request({ - url: '/api/attributes/AutomationDefinition', - method: 'get' - }); - - if (data?.attributes && Array.isArray(data.attributes)) { - return data.attributes; - } - - if (Array.isArray(data)) { - return data; - } - - return []; -} - -// ============================================================================ -// AUTOMATION TEMPLATES API -// ============================================================================ - -/** - * Fetch all automation templates (RBAC-filtered: own templates) - * Endpoint: GET /api/automation-templates - */ -export async function fetchAutomationTemplates( - request: ApiRequestFunction, - params?: any -): Promise { - const requestParams: Record = {}; - if (params && typeof params === 'object') { - const paginationObj: any = {}; - if (params.page !== undefined) paginationObj.page = params.page; - if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize; - if (params.sort) paginationObj.sort = params.sort; - if (params.filters) paginationObj.filters = params.filters; - if (params.search) paginationObj.search = params.search; - if (Object.keys(paginationObj).length > 0) { - requestParams.pagination = JSON.stringify(paginationObj); - } - } - return await request({ - url: '/api/automation-templates', - method: 'get', - params: requestParams, - }); -} - -/** - * Fetch single automation template by ID - * Endpoint: GET /api/automation-templates/{templateId} - */ -export async function fetchAutomationTemplateById( - request: ApiRequestFunction, - templateId: string -): Promise { - 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 -): Promise { - return await request({ - url: '/api/automation-templates', - method: 'post', - data: templateData - }); -} - -/** - * Update automation template - * Endpoint: PUT /api/automation-templates/{templateId} - */ -export async function updateAutomationTemplateApi( - request: ApiRequestFunction, - templateId: string, - templateData: Partial -): Promise { - return await request({ - url: `/api/automation-templates/${templateId}`, - method: 'put', - data: templateData - }); -} - -/** - * Delete automation template - * Endpoint: DELETE /api/automation-templates/{templateId} - */ -export async function deleteAutomationTemplateApi( - request: ApiRequestFunction, - templateId: string -): Promise { - await request({ - url: `/api/automation-templates/${templateId}`, - method: 'delete' - }); -} - -/** - * Fetch automation template attributes for dynamic form generation - * Endpoint: GET /api/automation-templates/attributes - */ -export async function fetchAutomationTemplateAttributes( - request: ApiRequestFunction -): Promise { - const data = await request({ - url: '/api/automation-templates/attributes', - method: 'get' - }); - - // Backend returns: { attributes: { model: "...", attributes: [...] } } - if (data?.attributes?.attributes && Array.isArray(data.attributes.attributes)) { - return data.attributes.attributes; - } - - // Fallback: direct attributes array - if (data?.attributes && Array.isArray(data.attributes)) { - return data.attributes; - } - - return Array.isArray(data) ? data : []; -} - -// ============================================================================ -// WORKFLOW ACTIONS API -// ============================================================================ - -/** - * Fetch available workflow actions (RBAC-filtered) - * Endpoint: GET /api/automations/actions - */ -export async function fetchWorkflowActions( - request: ApiRequestFunction -): Promise { - const data = await request({ - url: '/api/automations/actions', - method: 'get' - }); - - return data?.actions || []; -} diff --git a/src/api/workflowApi.ts b/src/api/workflowApi.ts index fc890d5..c97b9f8 100644 --- a/src/api/workflowApi.ts +++ b/src/api/workflowApi.ts @@ -1,507 +1,778 @@ -import { ApiRequestOptions } from '../hooks/useApi'; +/** + * Workflow API (GraphicalEditor) + * Node types and graph execution for n8n-style flows. + */ + +import type { ApiRequestOptions } from '../hooks/useApi'; + +const LOG = '[Workflow]'; // ============================================================================ -// TYPES & INTERFACES +// TYPES // ============================================================================ -// Workflow interfaces -export interface Workflow { +export interface NodeTypeParameter { + name: string; + type: string; + required?: boolean; + description?: string; + default?: unknown; +} + +export interface NodeType { + id: string; + category: string; + label: string; + description: string; + parameters: NodeTypeParameter[]; + inputs: number; + outputs: number; + /** Labels per output (e.g. ["Ja", "Nein"] for flow.ifElse) */ + outputLabels?: string[]; + executor: string; + meta?: { + icon?: string; + color?: string; + method?: string; + action?: string; + }; +} + +export interface NodeTypeCategory { + id: string; + label: Record | string; +} + +export interface NodeTypesResponse { + nodeTypes: NodeType[]; + categories: NodeTypeCategory[]; +} + +export interface Automation2GraphNode { + id: string; + type: string; + parameters?: Record; +} + +export interface Automation2Connection { + source: string; + target: string; + sourceOutput?: number; + targetInput?: number; +} + +export interface Automation2Graph { + nodes: Automation2GraphNode[]; + connections: Automation2Connection[]; +} + +export interface ExecuteGraphResponse { + success: boolean; + nodeOutputs?: Record; + error?: string; + stopped?: boolean; + failedNode?: string; + paused?: boolean; + taskId?: string; + runId?: string; + nodeId?: string; +} + +/** Entry point / start configured outside the canvas (manual, form, schedule, …) */ +export interface WorkflowEntryPoint { + id: string; + kind: string; + category: 'on_demand' | 'always_on'; + enabled: boolean; + title: Record | string; + description?: Record; + config: Record; +} + +export interface Automation2Workflow { + id: string; + label: string; + graph: Automation2Graph; + active?: boolean; + /** Entry points (Starts) — how this workflow may be invoked */ + invocations?: WorkflowEntryPoint[]; + /** Enriched: run count */ + runCount?: number; + /** Enriched: has active (running/paused) run */ + isRunning?: boolean; + /** Enriched: status of active run */ + runStatus?: string; + /** Enriched: nodeId where workflow is stuck (paused) */ + stuckAtNodeId?: string; + /** Enriched: human-readable label for stuck node */ + stuckAtNodeLabel?: string; + /** Enriched: created timestamp (seconds) */ + createdAt?: number; + /** Enriched: last run started timestamp (seconds) */ + lastStartedAt?: number; +} + +// ============================================================================ +// AUTO-PREFIX TYPES (Greenfield) +// ============================================================================ + +export type AutoWorkflowStatus = 'draft' | 'published' | 'archived'; +export type AutoRunStatus = 'running' | 'paused' | 'completed' | 'failed' | 'cancelled'; +export type AutoStepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped'; +export type AutoTaskStatus = 'pending' | 'completed' | 'cancelled' | 'expired'; +export type AutoTemplateScope = 'user' | 'instance' | 'mandate' | 'system'; + +export interface AutoVersion { + id: string; + workflowId: string; + versionNumber: number; + status: AutoWorkflowStatus; + graph: Automation2Graph; + invocations?: WorkflowEntryPoint[]; + publishedAt?: number; + publishedBy?: string; +} + +export interface AutoRun { + id: string; + workflowId: string; + versionId?: string; + status: AutoRunStatus; + trigger?: Record; + startedAt?: number; + completedAt?: number; + nodeOutputs?: Record; + currentNodeId?: string; + resumeContext?: Record; + error?: string; + costTokens?: number; + costCredits?: number; +} + +export interface AutoWorkflow { id: string; mandateId: string; - status: string; - name?: string; - workflowMode?: string; - [key: string]: any; // Allow additional properties + featureInstanceId: string; + label: string; + description?: string; + tags?: string[]; + isTemplate: boolean; + templateSourceId?: string; + templateScope?: AutoTemplateScope; + sharedReadOnly?: boolean; + currentVersionId?: string; + active: boolean; + eventId?: string; + notifyOnFailure?: boolean; + graph: Automation2Graph; + invocations?: WorkflowEntryPoint[]; + sysCreatedBy?: string; + sysCreatedAt?: number; + sysModifiedBy?: string; + sysModifiedAt?: number; } -export interface WorkflowMessage { - [key: string]: any; -} - -export interface WorkflowStats { - [key: string]: any; -} - -export interface WorkflowDocument { - [key: string]: any; -} - -export interface WorkflowLog { - [key: string]: any; -} - -// Request/Response interfaces based on API documentation -export interface FileAttachment { +export interface AutoTask { id: string; - name: string; + runId: string; + workflowId: string; + nodeId: string; + nodeType: string; + config: Record; + assigneeId?: string; + status: AutoTaskStatus; + result?: Record; + expiresAt?: number; + sysCreatedAt?: number; } -export interface UserInputRequest { - input: string; - files?: FileAttachment[]; // optional file attachments - array of {id, name} objects - metadata?: Record; // optional metadata +export interface AutoStepLog { + id: string; + runId: string; + nodeId: string; + nodeType: string; + status: AutoStepStatus; + inputSnapshot?: Record; + output?: Record; + error?: string; + startedAt?: number; + completedAt?: number; + durationMs?: number; + tokensUsed?: number; + retryCount?: number; } -export interface StartWorkflowRequest { - prompt: string; - listFileId?: string[]; // Array of file ID strings (files must be uploaded first via /api/files/upload) - userLanguage?: string; // Optional, defaults to "en" - metadata?: Record; - allowedProviders?: string[]; // Optional: Restrict AI calls to these providers (empty = all RBAC-permitted) -} +// ============================================================================ +// API FUNCTIONS +// ============================================================================ -export interface StartWorkflowResponse extends Workflow { - // Workflow object returned from start endpoint -} - -export interface ChatDataResponse { - messages: WorkflowMessage[]; - logs: WorkflowLog[]; - documents: WorkflowDocument[]; - workflowCost: number; -} - -// Type for the request function passed to API functions export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; -// ============================================================================ -// API REQUEST FUNCTIONS -// ============================================================================ - -function _requireApiBaseUrl(apiBaseUrl: string | undefined, caller: string): string { - if (!apiBaseUrl) { - throw new Error(`${caller}: apiBaseUrl is required (instanceId/featureCode missing)`); - } - return apiBaseUrl; +/** + * Fetch node types for the flow builder (backend-driven). + * GET /api/workflows/{instanceId}/node-types?language=de + */ +export async function fetchNodeTypes( + request: ApiRequestFunction, + instanceId: string, + language = 'de' +): Promise { + console.log(`${LOG} fetchNodeTypes: instanceId=${instanceId} language=${language}`); + const data = await request({ + url: `/api/workflows/${instanceId}/node-types`, + method: 'get', + params: { language }, + }); + const nodeTypes = data?.nodeTypes ?? []; + const categories = data?.categories ?? []; + console.log(`${LOG} fetchNodeTypes response: ${nodeTypes.length} nodeTypes, ${categories.length} categories`); + return { nodeTypes, categories }; } /** - * Fetch workflows - feature-scoped: GET {apiBaseUrl}/workflows + * Execute an automation2 graph. + * POST /api/workflows/{instanceId}/execute */ +export interface ExecuteGraphOptions { + /** Use a configured start on the saved workflow */ + entryPointId?: string; + /** Full run envelope (overrides entry point mapping) */ + runEnvelope?: Record; + /** Merged into envelope.payload */ + payload?: Record; +} + +export async function executeGraph( + request: ApiRequestFunction, + instanceId: string, + graph: Automation2Graph, + workflowId?: string, + options?: ExecuteGraphOptions +): Promise { + console.log( + `${LOG} executeGraph request: instanceId=${instanceId} workflowId=${workflowId} nodes=${graph.nodes.length} connections=${graph.connections.length}`, + { nodes: graph.nodes, connections: graph.connections, options } + ); + const start = performance.now(); + try { + const data: Record = { graph, workflowId }; + if (options?.entryPointId) data.entryPointId = options.entryPointId; + if (options?.runEnvelope) data.runEnvelope = options.runEnvelope; + if (options?.payload && Object.keys(options.payload).length > 0) data.payload = options.payload; + const result = await request({ + url: `/api/workflows/${instanceId}/execute`, + method: 'post', + data, + }); + const ms = Math.round(performance.now() - start); + console.log( + `${LOG} executeGraph response (${ms}ms): success=${result?.success} error=${result?.error ?? 'none'} nodeOutputs_keys=${Object.keys(result?.nodeOutputs ?? {}).join(',')} failedNode=${result?.failedNode ?? '-'}`, + result + ); + return result; + } catch (err) { + const ms = Math.round(performance.now() - start); + console.error( + `${LOG} executeGraph FAILED (${ms}ms): instanceId=${instanceId}`, + err + ); + throw err; + } +} + +// ------------------------------------------------------------------------- +// Workflows CRUD +// ------------------------------------------------------------------------- + export async function fetchWorkflows( request: ApiRequestFunction, - params?: { pagination?: string }, - apiBaseUrl?: string -): Promise { - const base = _requireApiBaseUrl(apiBaseUrl, 'fetchWorkflows'); - const url = `${base}/workflows`; - console.log('📤 fetchWorkflows: Making API request to', url); - - try { - const requestConfig: any = { url, method: 'get' as const }; - if (params?.pagination) { - requestConfig.params = { pagination: params.pagination }; - } - const data = await request(requestConfig); - - console.log('📥 fetchWorkflows: API response:', data); - - // Handle different response formats - let workflows: Workflow[] = []; - - if (Array.isArray(data)) { - // Direct array response - workflows = data; - } else if (data && typeof data === 'object') { - // Check for common wrapper properties - if (Array.isArray(data.workflows)) { - workflows = data.workflows; - } else if (Array.isArray(data.data)) { - workflows = data.data; - } else if (Array.isArray(data.items)) { - workflows = data.items; - } else if (Array.isArray(data.results)) { - workflows = data.results; - } else { - // Try to find any array property - const keys = Object.keys(data); - for (const key of keys) { - if (Array.isArray(data[key])) { - workflows = data[key]; - console.log(`ℹ️ fetchWorkflows: Found workflows array in property '${key}'`); - break; - } - } - } - } - - // Validate that we have workflow objects with id property - const validWorkflows = workflows.filter((w: any) => w && typeof w === 'object' && w.id); - - if (validWorkflows.length !== workflows.length) { - console.warn(`⚠️ fetchWorkflows: Filtered out ${workflows.length - validWorkflows.length} invalid workflows`); - } - - console.log(`✅ fetchWorkflows: Returning ${validWorkflows.length} valid workflows`); - return validWorkflows; - } catch (error) { - console.error('❌ fetchWorkflows: Error fetching workflows:', error); - throw error; - } + instanceId: string, + params?: { active?: boolean; pagination?: any } +): Promise { + const queryParams: Record = {}; + if (params?.active !== undefined) queryParams.active = params.active; + if (params?.pagination) queryParams.pagination = JSON.stringify(params.pagination); + const data = await request({ + url: `/api/workflows/${instanceId}/workflows`, + method: 'get', + params: Object.keys(queryParams).length > 0 ? queryParams : undefined, + }); + if (data?.items && data?.pagination) return data; + return data?.workflows ?? []; } -/** - * Fetch a single workflow by ID: GET {apiBaseUrl}/workflows/{workflowId} - */ export async function fetchWorkflow( request: ApiRequestFunction, - workflowId: string, - apiBaseUrl?: string -): Promise { - const base = _requireApiBaseUrl(apiBaseUrl, 'fetchWorkflow'); - const url = `${base}/workflows/${workflowId}`; - return await request({ url, method: 'get' }); + instanceId: string, + workflowId: string +): Promise { + return await request({ + url: `/api/workflows/${instanceId}/workflows/${workflowId}`, + method: 'get', + }); } -/** - * Fetch workflow status (lightweight status check): GET {apiBaseUrl}/workflows/{workflowId}/status - */ -export async function fetchWorkflowStatus( +export async function createWorkflow( request: ApiRequestFunction, - workflowId: string, - apiBaseUrl?: string -): Promise { - const base = _requireApiBaseUrl(apiBaseUrl, 'fetchWorkflowStatus'); - const url = `${base}/workflows/${workflowId}/status`; - const data = await request({ url, method: 'get' }); - - if (data && typeof data === 'object') { - if (data.status) { - return { status: data.status }; - } - return data; - } - - return null; + instanceId: string, + body: { label: string; graph: Automation2Graph; invocations?: WorkflowEntryPoint[] } +): Promise { + return await request({ + url: `/api/workflows/${instanceId}/workflows`, + method: 'post', + data: body, + }); } -/** - * Fetch workflow messages: GET {apiBaseUrl}/workflows/{workflowId}/messages - */ -export async function fetchWorkflowMessages( - request: ApiRequestFunction, - workflowId: string, - messageId?: string, - apiBaseUrl?: string -): Promise { - const params = messageId ? { messageId } : undefined; - const base = _requireApiBaseUrl(apiBaseUrl, 'fetchWorkflowMessages'); - const url = `${base}/workflows/${workflowId}/messages`; - const data = await request({ url, method: 'get', params }); - - if (Array.isArray(data)) { - return data; - } - - if (data && typeof data === 'object') { - if (Array.isArray(data.messages)) { - return data.messages; - } - if (Array.isArray(data.data)) { - return data.data; - } - } - - return []; -} - -/** - * Fetch workflow logs: GET {apiBaseUrl}/workflows/{workflowId}/logs - */ -export async function fetchWorkflowLogs( - request: ApiRequestFunction, - workflowId: string, - logId?: string, - apiBaseUrl?: string -): Promise { - const params = logId ? { logId } : undefined; - const base = _requireApiBaseUrl(apiBaseUrl, 'fetchWorkflowLogs'); - const url = `${base}/workflows/${workflowId}/logs`; - const data = await request({ url, method: 'get', params }); - - if (Array.isArray(data)) { - return data; - } - - if (data && typeof data === 'object') { - if (Array.isArray(data.logs)) { - return data.logs; - } - if (Array.isArray(data.data)) { - return data.data; - } - } - - return []; -} - -/** - * Fetch unified chat data (messages, logs, stats, documents) - * Endpoint: GET /api/automations/{instanceId}/workflows/{workflowId}/chatData - * Query params: afterTimestamp (optional) - fetch only data created after this time - */ -export async function fetchChatData( +export async function updateWorkflow( request: ApiRequestFunction, instanceId: string, workflowId: string, - afterTimestamp?: number -): Promise { - const params = afterTimestamp ? { afterTimestamp: afterTimestamp.toString() } : undefined; - const requestConfig = { - url: `/api/automations/${instanceId}/workflows/${workflowId}/chatData`, - method: 'get' as const, - params - }; - - console.log('📤 fetchChatData request:', requestConfig); - - const data = await request(requestConfig); - - console.log('📥 fetchChatData response:', data); - - const workflowCost: number = data.workflowCost ?? 0; - - if (data.items && Array.isArray(data.items)) { - const messages: WorkflowMessage[] = []; - const logs: WorkflowLog[] = []; - const documents: WorkflowDocument[] = []; - - data.items.forEach((item: any) => { - if (item.type === 'message') { - const messageData = item.item || item; - if (messageData && (messageData.id || messageData.message)) { - messages.push(messageData); - } - } else if (item.type === 'log') { - const logData = item.item || item; - if (logData) { - logs.push(logData); - } - } else if (item.type === 'document') { - const docData = item.item || item; - if (docData) { - documents.push(docData); - } - } - }); - - return { - messages, - logs, - documents: documents.length > 0 ? documents : (Array.isArray(data.documents) ? data.documents : []), - workflowCost - }; + body: { + label?: string; + graph?: Automation2Graph; + invocations?: WorkflowEntryPoint[]; + active?: boolean; + notifyOnFailure?: boolean; } - - return { - messages: Array.isArray(data.messages) ? data.messages : [], - logs: Array.isArray(data.logs) ? data.logs : [], - documents: Array.isArray(data.documents) ? data.documents : [], - workflowCost - }; +): Promise { + return await request({ + url: `/api/workflows/${instanceId}/workflows/${workflowId}`, + method: 'put', + data: body, + }); } -/** - * Start a new workflow or continue an existing one - * Endpoint: POST /api/automations/{instanceId}/start - * Query params: workflowId (optional), workflowMode (default: "Dynamic") - */ -export async function startWorkflowApi( - request: ApiRequestFunction, - instanceId: string, - workflowData: StartWorkflowRequest, - options?: { workflowId?: string; workflowMode?: 'Dynamic' | 'Automation' } -): Promise { - const params: Record = {}; - - // workflowMode is REQUIRED according to API spec - if (options?.workflowMode) { - params.workflowMode = options.workflowMode; - } else { - params.workflowMode = 'Dynamic'; - } - - if (options?.workflowId) { - params.workflowId = options.workflowId; - } - - const requestBody: any = { - prompt: workflowData.prompt, - ...(workflowData.listFileId && workflowData.listFileId.length > 0 && { listFileId: workflowData.listFileId }), - ...(workflowData.userLanguage && { userLanguage: workflowData.userLanguage }), - ...(workflowData.metadata && { metadata: workflowData.metadata }), - ...(workflowData.allowedProviders && workflowData.allowedProviders.length > 0 && { allowedProviders: workflowData.allowedProviders }) - }; - - const requestConfig = { - url: `/api/automations/${instanceId}/start`, - method: 'post' as const, - data: requestBody, - params: params - }; - - // Log full request details - console.log('📤 Full startWorkflow request details:'); - console.log(' URL:', requestConfig.url); - console.log(' Method:', requestConfig.method); - console.log(' Query Parameters:', params); - console.log(' Request Body:', JSON.stringify(requestBody, null, 2)); - console.log(' Full Request Config:', JSON.stringify(requestConfig, null, 2)); - - const response = await request(requestConfig); - - console.log('📥 startWorkflow response:', response); - - return response; -} - -/** - * Stop a running workflow - * Endpoint: POST /api/automations/{instanceId}/workflows/{workflowId}/stop - */ -export async function stopWorkflowApi( +export async function deleteWorkflow( request: ApiRequestFunction, instanceId: string, workflowId: string ): Promise { await request({ - url: `/api/automations/${instanceId}/workflows/${workflowId}/stop`, - method: 'post' + url: `/api/workflows/${instanceId}/workflows/${workflowId}`, + method: 'delete', }); } -/** - * Update workflow properties: PUT {apiBaseUrl}/workflows/{workflowId} - */ -export async function updateWorkflowApi( +export interface Automation2Run { + id: string; + workflowId: string; + status: string; + nodeOutputs?: Record; + currentNodeId?: string; +} + +export async function fetchWorkflowRuns( request: ApiRequestFunction, + instanceId: string, + workflowId: string +): Promise { + const data = await request({ + url: `/api/workflows/${instanceId}/workflows/${workflowId}/runs`, + method: 'get', + }); + return data?.runs ?? []; +} + +export interface CompletedRun extends Automation2Run { + workflowLabel?: string; + sysModifiedAt?: number; + sysCreatedAt?: number; +} + +export async function fetchCompletedRuns( + request: ApiRequestFunction, + instanceId: string, + limit = 20 +): Promise { + const data = await request({ + url: `/api/workflows/${instanceId}/runs/completed`, + method: 'get', + params: { limit }, + }); + return data?.runs ?? []; +} + +// ------------------------------------------------------------------------- +// Tasks +// ------------------------------------------------------------------------- + +export interface Automation2Task { + id: string; + runId: string; + workflowId: string; + nodeId: string; + nodeType: string; + config: Record; + status: string; + result?: Record; + /** Workflow label (enriched by API) */ + workflowLabel?: string; + /** Unix timestamp ms (from sysCreatedAt) */ + createdAt?: number; + /** Optional due date - configurable in future */ + dueAt?: number; +} + +export async function fetchTasks( + request: ApiRequestFunction, + instanceId: string, + params?: { workflowId?: string; status?: string } +): Promise { + const data = await request({ + url: `/api/workflows/${instanceId}/tasks`, + method: 'get', + params, + }); + return data?.tasks ?? []; +} + +export async function completeTask( + request: ApiRequestFunction, + instanceId: string, + taskId: string, + result: Record +): Promise { + return await request({ + url: `/api/workflows/${instanceId}/tasks/${taskId}/complete`, + method: 'post', + data: { result }, + }); +} + +// ------------------------------------------------------------------------- +// Versions (AutoVersion Lifecycle) +// ------------------------------------------------------------------------- + +export async function fetchVersions( + request: ApiRequestFunction, + instanceId: string, + workflowId: string +): Promise { + const data = await request({ + url: `/api/workflows/${instanceId}/workflows/${workflowId}/versions`, + method: 'get', + }); + return data?.versions ?? []; +} + +export async function createDraftVersion( + request: ApiRequestFunction, + instanceId: string, + workflowId: string +): Promise { + return await request({ + url: `/api/workflows/${instanceId}/workflows/${workflowId}/versions/draft`, + method: 'post', + }); +} + +export async function publishVersion( + request: ApiRequestFunction, + instanceId: string, + versionId: string +): Promise { + return await request({ + url: `/api/workflows/${instanceId}/versions/${versionId}/publish`, + method: 'post', + }); +} + +export async function unpublishVersion( + request: ApiRequestFunction, + instanceId: string, + versionId: string +): Promise { + return await request({ + url: `/api/workflows/${instanceId}/versions/${versionId}/unpublish`, + method: 'post', + }); +} + +export async function archiveVersion( + request: ApiRequestFunction, + instanceId: string, + versionId: string +): Promise { + return await request({ + url: `/api/workflows/${instanceId}/versions/${versionId}/archive`, + method: 'post', + }); +} + +// ------------------------------------------------------------------------- +// Templates +// ------------------------------------------------------------------------- + +export interface AutoWorkflowTemplate extends Automation2Workflow { + isTemplate: boolean; + templateScope?: AutoTemplateScope; + templateSourceId?: string; + sharedReadOnly?: boolean; +} + +export async function fetchTemplates( + request: ApiRequestFunction, + instanceId: string, + scope?: AutoTemplateScope, + pagination?: any +): Promise { + const queryParams: Record = {}; + if (scope) queryParams.scope = scope; + if (pagination) queryParams.pagination = JSON.stringify(pagination); + const data = await request({ + url: `/api/workflows/${instanceId}/templates`, + method: 'get', + params: Object.keys(queryParams).length > 0 ? queryParams : undefined, + }); + if (data?.items && data?.pagination) return data; + return data?.templates ?? []; +} + +export async function createTemplateFromWorkflow( + request: ApiRequestFunction, + instanceId: string, workflowId: string, - updateData: Partial<{ name: string; description?: string; tags?: string[] }>, - apiBaseUrl?: string -): Promise { - const base = _requireApiBaseUrl(apiBaseUrl, 'updateWorkflowApi'); - const url = `${base}/workflows/${workflowId}`; - return await request({ url, method: 'put', data: updateData }); + scope: AutoTemplateScope = 'user' +): Promise { + return await request({ + url: `/api/workflows/${instanceId}/templates/from-workflow`, + method: 'post', + data: { workflowId, scope }, + }); } -/** - * Delete a workflow and all associated data: DELETE {apiBaseUrl}/workflows/{workflowId} - */ -export async function deleteWorkflowApi( +export async function copyTemplate( request: ApiRequestFunction, - workflowId: string, - apiBaseUrl?: string -): Promise { - const base = _requireApiBaseUrl(apiBaseUrl, 'deleteWorkflowApi'); - const url = `${base}/workflows/${workflowId}`; - await request({ url, method: 'delete' }); + instanceId: string, + templateId: string +): Promise { + return await request({ + url: `/api/workflows/${instanceId}/templates/${templateId}/copy`, + method: 'post', + }); } -/** - * Delete multiple workflows: DELETE {apiBaseUrl}/workflows/{workflowId} (per workflow) - */ -export async function deleteWorkflowsApi( +export async function shareTemplate( request: ApiRequestFunction, - workflowIds: string[], - apiBaseUrl?: string -): Promise { - const base = `${_requireApiBaseUrl(apiBaseUrl, 'deleteWorkflowsApi')}/workflows`; - const deletePromises = workflowIds.map(workflowId => - request({ - url: `${base}/${workflowId}`, - method: 'delete' - }).catch(error => { - console.error(`Failed to delete workflow ${workflowId}:`, error); - throw error; - }) - ); - - await Promise.all(deletePromises); + instanceId: string, + templateId: string, + scope: AutoTemplateScope +): Promise { + return await request({ + url: `/api/workflows/${instanceId}/templates/${templateId}/share`, + method: 'post', + data: { scope }, + }); } -/** - * Delete a message from a workflow: DELETE {apiBaseUrl}/workflows/{workflowId}/messages/{messageId} - */ -export async function deleteMessageApi( +// ------------------------------------------------------------------------- +// Connections and Browse (for Email/SharePoint node config) +// ------------------------------------------------------------------------- + +export interface UserConnection { + id: string; + authority: string; + externalUsername?: string; + externalEmail?: string; + status: string; +} + +export async function fetchConnections( request: ApiRequestFunction, - workflowId: string, - messageId: string, - apiBaseUrl?: string -): Promise { - const base = _requireApiBaseUrl(apiBaseUrl, 'deleteMessageApi'); - const url = `${base}/workflows/${workflowId}/messages/${messageId}`; - await request({ url, method: 'delete' }); + instanceId: string +): Promise { + const data = await request({ + url: `/api/workflows/${instanceId}/connections`, + method: 'get', + }); + return data?.connections ?? []; } -/** - * Delete a file reference from a message: DELETE {apiBaseUrl}/workflows/{workflowId}/messages/{messageId}/files/{fileId} - */ -export async function deleteFileFromMessageApi( - request: ApiRequestFunction, - workflowId: string, - messageId: string, - fileId: string, - apiBaseUrl?: string -): Promise { - const base = _requireApiBaseUrl(apiBaseUrl, 'deleteFileFromMessageApi'); - const url = `${base}/workflows/${workflowId}/messages/${messageId}/files/${fileId}`; - await request({ url, method: 'delete' }); -} - -/** - * Fetch attributes for a workflow type - * Endpoint: GET /api/attributes/{entityType} - */ -export interface AttributeDefinition { - name: string; - type: 'text' | 'email' | 'date' | 'checkbox' | 'select' | 'multiselect' | 'number' | 'textarea'; +export interface ConnectionService { + service: string; label: string; - description?: string; - required?: boolean; - default?: any; - options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string; - validation?: any; - ui?: any; - readonly?: boolean; - editable?: boolean; - visible?: boolean; - order?: number; - placeholder?: string; - sortable?: boolean; - filterable?: boolean; - searchable?: boolean; - width?: number; - minWidth?: number; - maxWidth?: number; - filterOptions?: string[]; + icon: string; } -export async function fetchAttributes( +export async function fetchConnectionServices( request: ApiRequestFunction, - entityType: string = 'ChatWorkflow' -): Promise { - const data = await request({ - url: `/api/attributes/${entityType}`, - method: 'get' + instanceId: string, + connectionId: string +): Promise { + const data = await request({ + url: `/api/workflows/${instanceId}/connections/${connectionId}/services`, + method: 'get', }); - - // Extract attributes from response - check if response.data.attributes exists, otherwise check if response.data is an array - let attrs: AttributeDefinition[] = []; - if (data?.attributes && Array.isArray(data.attributes)) { - attrs = data.attributes; - } else if (Array.isArray(data)) { - attrs = data; - } else if (data && typeof data === 'object') { - // Try to find any array property in the response - const keys = Object.keys(data); - for (const key of keys) { - if (Array.isArray(data[key])) { - attrs = data[key]; - break; - } - } - } - - return attrs; + return data?.services ?? []; } +export interface BrowseEntry { + name: string; + path: string; + isFolder: boolean; + size?: number; + mimeType?: string; + metadata?: Record; +} + +export async function fetchBrowse( + request: ApiRequestFunction, + instanceId: string, + connectionId: string, + service: string, + path = '/' +): Promise<{ items: BrowseEntry[]; path: string; service: string }> { + const data = await request({ + url: `/api/workflows/${instanceId}/connections/${connectionId}/browse`, + method: 'get', + params: { service, path }, + }); + return { items: data?.items ?? [], path: data?.path ?? path, service: data?.service ?? service }; +} + +/** ClickUp GET /task/{taskId} — list.id for resolving list-scoped fields when only task id is known. */ +export async function fetchClickupTask( + request: ApiRequestFunction, + connectionId: string, + taskId: string +): Promise> { + const data = await request({ + url: `/api/clickup/${connectionId}/tasks/${encodeURIComponent(taskId)}`, + method: 'get', + }); + return data && typeof data === 'object' ? (data as Record) : {}; +} + +/** ClickUp list metadata (statuses, etc.) — GET /api/clickup/{connectionId}/lists/{listId}. */ +export async function fetchClickupList( + request: ApiRequestFunction, + connectionId: string, + listId: string +): Promise> { + const data = await request({ + url: `/api/clickup/${connectionId}/lists/${listId}`, + method: 'get', + }); + return data && typeof data === 'object' ? (data as Record) : {}; +} + +/** ClickUp workspace/team (members for assignees) — GET /api/clickup/{connectionId}/teams/{teamId}. */ +export async function fetchClickupTeam( + request: ApiRequestFunction, + connectionId: string, + teamId: string +): Promise> { + const data = await request({ + url: `/api/clickup/${connectionId}/teams/${teamId}`, + method: 'get', + }); + return data && typeof data === 'object' ? (data as Record) : {}; +} + +/** ClickUp list custom fields (GET /api/clickup/{connectionId}/lists/{listId}/fields). */ +export async function fetchClickupListFields( + request: ApiRequestFunction, + connectionId: string, + listId: string +): Promise<{ fields?: unknown[] } & Record> { + const data = await request({ + url: `/api/clickup/${connectionId}/lists/${listId}/fields`, + method: 'get', + }); + return (data && typeof data === 'object' ? data : {}) as { fields?: unknown[] } & Record; +} + +/** ClickUp GET /list/{id}/task page (tasks in a list for relationship dropdowns). */ +export interface ClickupListTaskItem { + id?: string; + name?: string; +} + +export async function fetchClickupListTasks( + request: ApiRequestFunction, + connectionId: string, + listId: string, + options?: { page?: number; includeClosed?: boolean } +): Promise< + { tasks?: ClickupListTaskItem[]; last_page?: boolean } & Record +> { + const data = await request({ + url: `/api/clickup/${connectionId}/lists/${listId}/tasks`, + method: 'get', + params: { + page: options?.page ?? 0, + include_closed: options?.includeClosed ?? false, + }, + }); + return (data && typeof data === 'object' ? data : {}) as { + tasks?: ClickupListTaskItem[]; + last_page?: boolean; + } & Record; +} + +// ------------------------------------------------------------------------- +// Monitoring / Metrics +// ------------------------------------------------------------------------- + +export interface WorkflowMetrics { + workflowCount: number; + activeWorkflows: number; + totalRuns: number; + runsByStatus: Record; + totalTasks: number; + tasksByStatus: Record; + totalTokens: number; + totalCredits: number; +} + +export async function fetchMetrics( + request: ApiRequestFunction, + instanceId: string +): Promise { + return await request({ + url: `/api/workflows/${instanceId}/metrics`, + method: 'get', + }); +} + +/** Paginated tasks in a list — for ClickUp relationship dropdowns and input.form „ClickUp-Aufgabe“. */ +export async function loadClickupListTasksForDropdown( + request: ApiRequestFunction, + connectionId: string, + listId: string +): Promise> { + const acc: Array<{ id: string; name: string }> = []; + const seen = new Set(); + const maxPages = 12; + const pageSizeHint = 100; + for (let page = 0; page < maxPages; page++) { + const data = await fetchClickupListTasks(request, connectionId, listId, { + page, + includeClosed: false, + }); + if (data && typeof data === 'object' && 'error' in data && (data as { error?: unknown }).error) { + const err = (data as { error?: unknown }).error; + const body = (data as { body?: string }).body; + throw new Error( + typeof err === 'string' ? err + (body ? `: ${body.slice(0, 200)}` : '') : 'ClickUp API error' + ); + } + const tasks = Array.isArray(data.tasks) ? data.tasks : []; + for (const t of tasks) { + const id = t?.id != null ? String(t.id) : ''; + if (!id || seen.has(id)) continue; + seen.add(id); + acc.push({ id, name: String(t.name ?? id) }); + } + const rawLast = (data as Record).last_page; + const last = + rawLast === true || + rawLast === 'true' || + tasks.length === 0 || + tasks.length < pageSizeHint; + if (last) break; + } + acc.sort((a, b) => a.name.localeCompare(b.name, 'de')); + return acc; +} diff --git a/src/components/ActionsPanel/ActionsPanel.module.css b/src/components/ActionsPanel/ActionsPanel.module.css deleted file mode 100644 index 0aa9d92..0000000 --- a/src/components/ActionsPanel/ActionsPanel.module.css +++ /dev/null @@ -1,289 +0,0 @@ -/* ActionsPanel Styles */ - -.panel { - display: flex; - flex-direction: column; - height: 100%; - background: var(--bg-secondary, #f5f5f5); - border-radius: 8px; - overflow: hidden; -} - -.header { - padding: 1rem; - border-bottom: 1px solid var(--border-color, #e0e0e0); - background: var(--bg-primary, #ffffff); -} - -.title { - margin: 0 0 0.75rem 0; - font-size: 1rem; - font-weight: 600; - color: var(--text-primary, #333); -} - -.searchBox { - display: flex; - align-items: center; - background: var(--bg-secondary, #f5f5f5); - border-radius: 6px; - padding: 0.5rem 0.75rem; -} - -.searchIcon { - color: var(--text-secondary, #666); - margin-right: 0.5rem; - font-size: 0.875rem; -} - -.searchInput { - flex: 1; - border: none; - background: transparent; - font-size: 0.875rem; - color: var(--text-primary, #333); - outline: none; -} - -.searchInput::placeholder { - color: var(--text-tertiary, #999); -} - -.actionsList { - flex: 1; - overflow-y: auto; - padding: 0.5rem; -} - -.loading, -.error, -.empty { - padding: 2rem; - text-align: center; - color: var(--text-secondary, #666); -} - -.error { - color: var(--error-color, #dc3545); -} - -.retryButton { - margin-top: 1rem; - padding: 0.5rem 1rem; - background: var(--primary-color, #007bff); - color: white; - border: none; - border-radius: 4px; - cursor: pointer; -} - -.retryButton:hover { - background: var(--primary-hover, #0056b3); -} - -/* Method Groups */ -.methodGroup { - margin-bottom: 0.5rem; - background: var(--bg-primary, #ffffff); - border-radius: 6px; - overflow: hidden; -} - -.methodHeader { - display: flex; - align-items: center; - width: 100%; - padding: 0.75rem 1rem; - background: transparent; - border: none; - cursor: pointer; - text-align: left; - font-size: 0.875rem; - font-weight: 600; - color: var(--text-primary, #333); - transition: background 0.2s; -} - -.methodHeader:hover { - background: var(--bg-hover, #f0f0f0); -} - -.methodHeader svg { - margin-right: 0.5rem; - font-size: 0.75rem; - color: var(--text-secondary, #666); -} - -.methodName { - flex: 1; - text-transform: capitalize; -} - -.methodCount { - background: var(--primary-color, #007bff); - color: white; - padding: 0.125rem 0.5rem; - border-radius: 10px; - font-size: 0.75rem; - font-weight: 500; -} - -/* Method Actions */ -.methodActions { - border-top: 1px solid var(--border-color, #e0e0e0); -} - -.actionItem { - border-bottom: 1px solid var(--border-light, #f0f0f0); -} - -.actionItem:last-child { - border-bottom: none; -} - -.actionHeader { - display: flex; - align-items: center; - padding: 0.75rem 1rem; - cursor: pointer; - transition: background 0.2s; -} - -.actionHeader:hover { - background: var(--bg-hover, #f5f5f5); -} - -.actionInfo { - flex: 1; - min-width: 0; -} - -.actionName { - display: block; - font-weight: 500; - font-size: 0.875rem; - color: var(--text-primary, #333); -} - -.actionDesc { - display: block; - font-size: 0.75rem; - color: var(--text-secondary, #666); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.copyButton { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - background: var(--bg-secondary, #f5f5f5); - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 4px; - cursor: pointer; - color: var(--text-secondary, #666); - transition: all 0.2s; -} - -.copyButton:hover { - background: var(--primary-color, #007bff); - border-color: var(--primary-color, #007bff); - color: white; -} - -/* Action Details */ -.actionDetails { - padding: 0.75rem 1rem; - background: var(--bg-secondary, #f8f9fa); - border-top: 1px solid var(--border-light, #f0f0f0); -} - -.actionDetails h5 { - margin: 0 0 0.5rem 0; - font-size: 0.75rem; - font-weight: 600; - color: var(--text-secondary, #666); - text-transform: uppercase; -} - -/* Parameters */ -.parameters { - margin-bottom: 1rem; -} - -.parameters ul { - margin: 0; - padding: 0; - list-style: none; -} - -.param { - display: flex; - flex-wrap: wrap; - align-items: baseline; - gap: 0.5rem; - padding: 0.25rem 0; - font-size: 0.8125rem; -} - -.paramName { - font-weight: 500; - color: var(--text-primary, #333); -} - -.required { - color: var(--error-color, #dc3545); - margin-left: 2px; -} - -.paramType { - font-family: monospace; - font-size: 0.75rem; - background: var(--bg-code, #e9ecef); - padding: 0.125rem 0.375rem; - border-radius: 3px; - color: var(--text-secondary, #666); -} - -.paramDesc { - width: 100%; - font-size: 0.75rem; - color: var(--text-tertiary, #888); -} - -/* Example JSON */ -.exampleJson { - margin-bottom: 1rem; -} - -.exampleJson pre { - margin: 0; - padding: 0.75rem; - background: var(--bg-code, #1e1e1e); - color: var(--text-code, #d4d4d4); - border-radius: 4px; - font-size: 0.75rem; - overflow-x: auto; - white-space: pre-wrap; - word-break: break-all; -} - -.insertButton { - width: 100%; - padding: 0.5rem 1rem; - background: var(--primary-color, #007bff); - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 0.875rem; - font-weight: 500; - transition: background 0.2s; -} - -.insertButton:hover { - background: var(--primary-hover, #0056b3); -} diff --git a/src/components/ActionsPanel/ActionsPanel.tsx b/src/components/ActionsPanel/ActionsPanel.tsx deleted file mode 100644 index 58c2b04..0000000 --- a/src/components/ActionsPanel/ActionsPanel.tsx +++ /dev/null @@ -1,216 +0,0 @@ -/** - * ActionsPanel - * - * Displays available workflow actions for copy/paste into templates. - * Groups actions by method and shows parameters + example JSON. - */ - -import React, { useState, useMemo, useEffect } from 'react'; -import { useWorkflowActions, type WorkflowAction } from '../../hooks/useAutomations'; -import { FaSearch, FaCopy, FaChevronDown, FaChevronRight, FaCheck } from 'react-icons/fa'; -import { useToast } from '../../contexts/ToastContext'; -import styles from './ActionsPanel.module.css'; - -interface ActionsPanelProps { - /** Callback when action JSON is inserted (optional) */ - onInsert?: (actionJson: string) => void; - /** Callback when action JSON is copied (optional) */ - onCopy?: (actionJson: string) => void; -} - -export const ActionsPanel: React.FC = ({ onInsert, onCopy }) => { - const { actions, loading, error, fetchActions } = useWorkflowActions(); - const { showSuccess } = useToast(); - - const [filter, setFilter] = useState(''); - const [expandedMethods, setExpandedMethods] = useState>(new Set()); - const [expandedAction, setExpandedAction] = useState(null); - const [copiedAction, setCopiedAction] = useState(null); - - useEffect(() => { - fetchActions(); - }, [fetchActions]); - - // Filter actions by search term - 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) || - a.actionId.toLowerCase().includes(lower) - ); - }, [actions, filter]); - - // Group actions by method - const groupedActions = useMemo(() => { - const groups: Record = {}; - filteredActions.forEach(action => { - if (!groups[action.method]) { - groups[action.method] = []; - } - groups[action.method].push(action); - }); - return groups; - }, [filteredActions]); - - // Toggle method expansion - const toggleMethod = (method: string) => { - setExpandedMethods(prev => { - const newSet = new Set(prev); - if (newSet.has(method)) { - newSet.delete(method); - } else { - newSet.add(method); - } - return newSet; - }); - }; - - // Toggle action details - const toggleAction = (actionId: string) => { - setExpandedAction(prev => prev === actionId ? null : actionId); - }; - - // Copy action JSON to clipboard - const handleCopy = async (action: WorkflowAction) => { - const json = JSON.stringify(action.exampleJson, null, 2); - try { - await navigator.clipboard.writeText(json); - setCopiedAction(action.actionId); - setTimeout(() => setCopiedAction(null), 2000); - showSuccess('JSON kopiert'); - onCopy?.(json); - } catch (err) { - console.error('Failed to copy:', err); - } - }; - - // Insert action JSON - const handleInsert = (action: WorkflowAction) => { - const json = JSON.stringify(action.exampleJson, null, 2); - onInsert?.(json); - }; - - if (loading) { - return ( -
-
Lade Actions...
-
- ); - } - - if (error) { - return ( -
-
Fehler: {error}
- -
- ); - } - - return ( -
-
-

Verfügbare Actions

-
- - setFilter(e.target.value)} - className={styles.searchInput} - /> -
-
- -
- {Object.keys(groupedActions).length === 0 ? ( -
Keine Actions gefunden
- ) : ( - Object.entries(groupedActions).map(([method, methodActions]) => ( -
- - - {expandedMethods.has(method) && ( -
- {methodActions.map(action => ( -
-
toggleAction(action.actionId)} - > -
- {action.action} - {action.description} -
- -
- - {expandedAction === action.actionId && ( -
- {action.parameters.length > 0 && ( -
-
Parameter:
-
    - {action.parameters.map(param => ( -
  • - - {param.name} - {param.required && *} - - {param.type} - {param.description && ( - {param.description} - )} -
  • - ))} -
-
- )} - -
-
Beispiel JSON:
-
{JSON.stringify(action.exampleJson, null, 2)}
-
- - {onInsert && ( - - )} -
- )} -
- ))} -
- )} -
- )) - )} -
-
- ); -}; - -export default ActionsPanel; diff --git a/src/components/ActionsPanel/index.ts b/src/components/ActionsPanel/index.ts deleted file mode 100644 index 6db9c3b..0000000 --- a/src/components/ActionsPanel/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ActionsPanel } from './ActionsPanel'; -export { default } from './ActionsPanel'; diff --git a/src/components/Automation2FlowEditor/editor/CanvasHeader.tsx b/src/components/Automation2FlowEditor/editor/CanvasHeader.tsx deleted file mode 100644 index b399d12..0000000 --- a/src/components/Automation2FlowEditor/editor/CanvasHeader.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/** - * CanvasHeader - Workflow controls (Neu, Speichern, laden, Ausführen) and execute result. - */ - -import React from 'react'; -import { FaCog, FaPlay, FaSpinner } from 'react-icons/fa'; -import type { Automation2Workflow, ExecuteGraphResponse } from '../../../api/automation2Api'; -import styles from './Automation2FlowEditor.module.css'; - -interface CanvasHeaderProps { - workflows: Automation2Workflow[]; - currentWorkflowId: string | null; - onWorkflowSelect: (workflowId: string | null) => void; - onNew: () => void; - onSave: () => void; - onExecute: () => void; - onWorkflowSettings?: () => void; - saving: boolean; - executing: boolean; - hasNodes: boolean; - executeResult: ExecuteGraphResponse | null; -} - -export const CanvasHeader: React.FC = ({ - workflows, - currentWorkflowId, - onWorkflowSelect, - onNew, - onSave, - onExecute, - onWorkflowSettings, - saving, - executing, - hasNodes, - executeResult, -}) => ( -
-
-

- Workflow-Editor -

- {onWorkflowSettings && ( - - )} - - - - -
- {executeResult && ( -
- {executeResult.success ? ( - <>✓ Ausführung abgeschlossen. - ) : (executeResult as { paused?: boolean }).paused ? ( - <> - ⏸ Workflow pausiert. Öffne Workflows & Tasks in der Sidebar, um den - Task zu bearbeiten. - - ) : ( - <>✗ {executeResult.error ?? 'Unbekannter Fehler'} - )} -
- )} -
-); diff --git a/src/components/AutomationEditor/AutomationEditor.module.css b/src/components/AutomationEditor/AutomationEditor.module.css deleted file mode 100644 index 9143aaa..0000000 --- a/src/components/AutomationEditor/AutomationEditor.module.css +++ /dev/null @@ -1,946 +0,0 @@ -/** - * AutomationEditor Styles - * - * Full-screen editor with form on left and actions panel on right - */ - -/* Used when AutomationEditor had custom overlay - kept for reference, Popup is used now */ -.editorOverlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.6); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - padding: 1rem; -} - -/* Popup customisation for fullscreen editor - fill content area */ -.editorPopup :global([class*="content"]) { - padding: 0; - display: flex; - flex-direction: column; - min-height: 0; -} - -.editorContainer { - background: var(--surface-color, #ffffff); - border-radius: 12px; - width: 100%; - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - overflow: hidden; -} - -/* Header */ -.editorHeader { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 1.5rem; - border-bottom: 1px solid var(--border-color, #e0e0e0); - background: var(--bg-primary, #ffffff); - flex-shrink: 0; -} - -.headerLeft { - display: flex; - align-items: center; - gap: 1rem; -} - -.editorTitle { - font-size: 1.25rem; - font-weight: 600; - color: var(--text-primary, #333); - margin: 0; -} - -.modeBadge { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.75rem; - border-radius: 12px; - font-size: 0.75rem; - font-weight: 500; - text-transform: uppercase; -} - -.modeBadge.template { - background: var(--info-bg, #e3f2fd); - color: var(--info-color, #1976d2); -} - -.modeBadge.definition { - background: var(--success-bg, #e8f5e9); - color: var(--success-color, #388e3c); -} - -.headerActions { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.closeButton { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - background: transparent; - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 8px; - cursor: pointer; - color: var(--text-secondary, #666); - font-size: 1.125rem; - transition: all 0.2s; -} - -.closeButton:hover { - background: var(--bg-secondary, #f5f5f5); - color: var(--text-primary, #333); -} - -/* Main Content Area */ -.editorContent { - display: flex; - flex: 1; - min-height: 0; - overflow: hidden; -} - -/* Form Panel (Left Side) */ -.formPanel { - flex: 1; - display: flex; - flex-direction: column; - min-width: 0; - border-right: 1px solid var(--border-color, #e0e0e0); -} - -.formPanelHeader { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1.5rem; - background: var(--bg-secondary, #f5f5f5); - border-bottom: 1px solid var(--border-color, #e0e0e0); - flex-shrink: 0; -} - -.formPanelTitle { - font-size: 0.875rem; - font-weight: 600; - color: var(--text-secondary, #666); - margin: 0; -} - -.formPanelContent { - flex: 1; - overflow-y: auto; - padding: 1.5rem; -} - -/* Actions Panel (Right Side) */ -.actionsPanel { - width: 400px; - flex-shrink: 0; - display: flex; - flex-direction: column; - background: var(--bg-secondary, #f8f9fa); -} - -.actionsPanelCollapsed { - width: 48px; -} - -.actionsPanelToggle { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - padding: 0.75rem; - background: var(--bg-secondary, #f5f5f5); - border: none; - border-bottom: 1px solid var(--border-color, #e0e0e0); - cursor: pointer; - color: var(--text-secondary, #666); - font-size: 0.875rem; - font-weight: 500; - transition: all 0.2s; - flex-shrink: 0; -} - -.actionsPanelToggle:hover { - background: var(--bg-hover, #e8e8e8); - color: var(--text-primary, #333); -} - -.actionsPanelToggle svg { - margin-right: 0.5rem; -} - -.actionsPanelCollapsed .actionsPanelToggle { - writing-mode: vertical-rl; - text-orientation: mixed; - padding: 1rem 0.75rem; - height: 100%; -} - -.actionsPanelCollapsed .actionsPanelToggle svg { - margin-right: 0; - margin-bottom: 0.5rem; - transform: rotate(90deg); -} - -.actionsPanelContainer { - flex: 1; - overflow: hidden; -} - -/* Footer */ -.editorFooter { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 1.5rem; - border-top: 1px solid var(--border-color, #e0e0e0); - background: var(--bg-primary, #ffffff); - flex-shrink: 0; -} - -.footerLeft { - display: flex; - align-items: center; - gap: 1rem; -} - -.footerRight { - display: flex; - align-items: center; - gap: 0.75rem; -} - -/* Buttons */ -.primaryButton { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.625rem 1.25rem; - background: var(--primary-color, #f25843); - color: white; - border: none; - border-radius: 6px; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - transition: background 0.2s, transform 0.1s; -} - -.primaryButton:hover:not(:disabled) { - background: var(--primary-dark, #d94d3a); -} - -.primaryButton:active:not(:disabled) { - transform: scale(0.98); -} - -.primaryButton:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.secondaryButton { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.625rem 1.25rem; - background: var(--surface-color, #ffffff); - color: var(--text-primary, #333); - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 6px; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - transition: background 0.2s, border-color 0.2s; -} - -.secondaryButton:hover:not(:disabled) { - background: var(--bg-secondary, #f5f5f5); - border-color: var(--text-secondary, #666); -} - -.secondaryButton:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.dangerButton { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.625rem 1.25rem; - background: var(--danger-color, #dc3545); - color: white; - border: none; - border-radius: 6px; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - transition: background 0.2s; -} - -.dangerButton:hover:not(:disabled) { - background: var(--danger-dark, #c82333); -} - -/* JSON Editor Section */ -.jsonEditorSection { - margin-top: 1.5rem; - padding-top: 1.5rem; - border-top: 1px solid var(--border-color, #e0e0e0); -} - -.jsonEditorHeader { - display: flex; - flex-direction: column; - gap: 0.5rem; - margin-bottom: 0.75rem; -} - -.jsonEditorLabelRow { - display: flex; - justify-content: space-between; - align-items: center; -} - -.jsonEditorLabel { - display: flex; - align-items: center; - font-size: 0.875rem; - font-weight: 500; - color: var(--text-primary, #333); -} - -.jsonEditorHint { - font-size: 0.75rem; - color: var(--text-tertiary, #999); -} - -.formatButton { - display: flex; - align-items: center; - gap: 0.375rem; - padding: 0.375rem 0.75rem; - background: var(--bg-secondary, #f5f5f5); - color: var(--text-secondary, #666); - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 4px; - font-size: 0.75rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s; -} - -.formatButton:hover { - background: var(--primary-color, #f25843); - color: white; - border-color: var(--primary-color, #f25843); -} - -.jsonTextarea { - width: 100%; - min-height: 300px; - padding: 1rem; - font-family: 'Fira Code', 'Monaco', 'Consolas', monospace; - font-size: 0.8125rem; - line-height: 1.5; - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 8px; - background: var(--bg-code, #1e1e1e); - color: var(--text-code, #d4d4d4); - resize: vertical; - tab-size: 2; -} - -.jsonTextarea:focus { - outline: none; - border-color: var(--primary-color, #f25843); - box-shadow: 0 0 0 3px rgba(242, 88, 67, 0.1); -} - -.jsonTextarea.error { - border-color: var(--danger-color, #dc3545); -} - -.jsonError { - margin-top: 0.5rem; - padding: 0.5rem 0.75rem; - background: var(--danger-bg, #fef2f2); - color: var(--danger-color, #dc3545); - border-radius: 4px; - font-size: 0.8125rem; -} - -/* Placeholders Section */ -.placeholdersSection { - margin-top: 1.5rem; - padding-top: 1.5rem; - border-top: 1px solid var(--border-color, #e0e0e0); -} - -.placeholdersHeader { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.75rem; -} - -.placeholdersTitle { - font-size: 0.875rem; - font-weight: 500; - color: var(--text-primary, #333); -} - -.placeholdersHint { - font-size: 0.75rem; - color: var(--text-tertiary, #999); -} - -.placeholdersList { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.placeholderItem { - display: flex; - flex-direction: column; - gap: 0.5rem; - padding: 0.75rem; - background: var(--bg-secondary, #f5f5f5); - border-radius: 6px; -} - -.placeholderKeyRow { - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; -} - -.placeholderKey { - padding: 0.375rem 0.625rem; - background: var(--bg-code, #e9ecef); - border-radius: 4px; - font-family: monospace; - font-size: 0.8125rem; - font-weight: 500; - color: var(--text-primary, #333); -} - -.placeholderDescription { - font-size: 0.75rem; - color: var(--text-secondary, #666); - flex: 1; -} - -.placeholderType { - padding: 0.25rem 0.5rem; - background: var(--info-bg, #e3f2fd); - color: var(--info-color, #1976d2); - border-radius: 4px; - font-size: 0.6875rem; - font-weight: 500; - text-transform: uppercase; -} - -.placeholderError { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem; - background: var(--error-bg, #ffebee); - color: var(--error-color, #c62828); - border: 1px solid var(--error-border, #ef9a9a); - border-radius: 6px; - font-size: 0.8125rem; -} - -.placeholderError svg { - flex-shrink: 0; -} - -.sharepointFolderInput { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.sharepointFolderHint { - font-size: 0.75rem; - color: var(--text-secondary, #666); - font-style: italic; -} - -/* SharePoint Folder Picker */ -.sharepointFolderPicker { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.sharepointFolderHeader { - display: flex; - gap: 0.5rem; - align-items: center; -} - -.sharepointFolderHeader .placeholderInput { - flex: 1; -} - -.sharepointBrowseButton { - display: flex; - align-items: center; - gap: 0.375rem; - padding: 0.5rem 0.75rem; - background: var(--secondary-button-bg, #f0f0f0); - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 6px; - cursor: pointer; - font-size: 0.875rem; - color: var(--text-primary, #333); - white-space: nowrap; - transition: background-color 0.15s; -} - -.sharepointBrowseButton:hover { - background: var(--secondary-button-hover-bg, #e0e0e0); -} - -.sharepointFolderBrowser { - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 8px; - padding: 1rem; - background: var(--bg-secondary, #fafafa); -} - -.sharepointError { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - background: var(--danger-bg, #fff0f0); - color: var(--danger-color, #d32f2f); - border-radius: 4px; - margin-bottom: 0.75rem; - font-size: 0.875rem; -} - -.sharepointSection { - margin-bottom: 1rem; -} - -.sharepointSection:last-child { - margin-bottom: 0; -} - -.sharepointSection label { - display: block; - font-size: 0.75rem; - font-weight: 600; - color: var(--text-secondary, #666); - margin-bottom: 0.25rem; - text-transform: uppercase; - letter-spacing: 0.025em; -} - -.sharepointLoading { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem; - color: var(--text-secondary, #666); - font-size: 0.875rem; -} - -.spinner { - animation: spin 1s linear infinite; -} - -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -.sharepointSelect { - width: 100%; - padding: 0.5rem 0.75rem; - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 6px; - font-size: 0.875rem; - background: var(--bg-primary, white); - cursor: pointer; -} - -.sharepointSelect:focus { - outline: none; - border-color: var(--primary-color, #1976d2); -} - -.sharepointBreadcrumb { - font-size: 0.75rem; - color: var(--text-secondary, #666); - margin-bottom: 0.5rem; - padding: 0.25rem 0; - border-bottom: 1px solid var(--border-color, #e0e0e0); -} - -.sharepointFolderList { - max-height: 200px; - overflow-y: auto; - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 4px; - background: var(--bg-primary, white); -} - -.sharepointFolderItem { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - cursor: pointer; - font-size: 0.875rem; - border-bottom: 1px solid var(--border-light, #f0f0f0); - transition: background-color 0.1s; -} - -.sharepointFolderItem:last-child { - border-bottom: none; -} - -.sharepointFolderItem:hover { - background: var(--bg-hover, #f5f5f5); -} - -.sharepointFolderItem .folderName { - flex: 1; - cursor: pointer; -} - -.sharepointFolderItem .folderName:hover { - text-decoration: underline; -} - -.selectFolderButton { - padding: 0.25rem 0.5rem; - font-size: 0.75rem; - background: var(--primary-color, #1976d2); - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - opacity: 0; - transition: opacity 0.15s; -} - -.sharepointFolderItem:hover .selectFolderButton { - opacity: 1; -} - -.selectFolderButton:hover { - background: var(--primary-hover, #1565c0); -} - -.sharepointEmpty { - padding: 1rem; - text-align: center; - color: var(--text-secondary, #666); - font-size: 0.875rem; - font-style: italic; -} - -.selectCurrentFolderButton { - width: 100%; - margin-top: 0.75rem; - padding: 0.5rem 0.75rem; - background: var(--success-color, #2e7d32); - color: white; - border: none; - border-radius: 6px; - cursor: pointer; - font-size: 0.875rem; - transition: background-color 0.15s; -} - -.selectCurrentFolderButton:hover { - background: var(--success-hover, #1b5e20); -} - -.placeholderInput { - width: 100%; - padding: 0.5rem 0.75rem; - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 6px; - font-size: 0.875rem; - background: var(--bg-primary, #ffffff); - color: var(--text-primary, #333); -} - -.placeholderInput:focus { - outline: none; - border-color: var(--primary-color, #f25843); -} - -.placeholderSelect { - width: 100%; - padding: 0.5rem 0.75rem; - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 6px; - font-size: 0.875rem; - background: var(--bg-primary, #ffffff); - color: var(--text-primary, #333); - cursor: pointer; -} - -.placeholderSelect:focus { - outline: none; - border-color: var(--primary-color, #f25843); -} - -.placeholderSelect:disabled { - background: var(--bg-secondary, #f5f5f5); - cursor: not-allowed; -} - -.placeholderTextarea { - width: 100%; - padding: 0.5rem 0.75rem; - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 6px; - font-size: 0.875rem; - background: var(--bg-primary, #ffffff); - color: var(--text-primary, #333); - resize: vertical; - min-height: 60px; -} - -.placeholderTextarea:focus { - outline: none; - border-color: var(--primary-color, #f25843); -} - -.placeholderCheckbox { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.875rem; - color: var(--text-primary, #333); - cursor: pointer; -} - -.placeholderCheckbox input[type="checkbox"] { - width: 18px; - height: 18px; - accent-color: var(--primary-color, #f25843); - cursor: pointer; -} - -.noPlaceholders { - padding: 1rem; - text-align: center; - color: var(--text-tertiary, #999); - font-size: 0.875rem; - background: var(--bg-secondary, #f5f5f5); - border-radius: 6px; -} - -/* Form Fields */ -.formFields { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.formGroup { - display: flex; - flex-direction: column; - gap: 0.375rem; -} - -.formLabel { - font-size: 0.875rem; - font-weight: 500; - color: var(--text-primary, #333); -} - -.formLabel .required { - color: var(--danger-color, #dc3545); -} - -.formInput { - padding: 0.625rem 0.75rem; - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 6px; - font-size: 0.875rem; - background: var(--bg-primary, #ffffff); - color: var(--text-primary, #333); - transition: border-color 0.2s, box-shadow 0.2s; -} - -.formInput:focus { - outline: none; - border-color: var(--primary-color, #f25843); - box-shadow: 0 0 0 3px rgba(242, 88, 67, 0.1); -} - -.formTextarea { - padding: 0.625rem 0.75rem; - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 6px; - font-size: 0.875rem; - background: var(--bg-primary, #ffffff); - color: var(--text-primary, #333); - resize: vertical; - min-height: 80px; - transition: border-color 0.2s, box-shadow 0.2s; -} - -.formTextarea:focus { - outline: none; - border-color: var(--primary-color, #f25843); - box-shadow: 0 0 0 3px rgba(242, 88, 67, 0.1); -} - -.formHint { - font-size: 0.75rem; - color: var(--text-tertiary, #999); - margin: 0; -} - -.checkboxLabel { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.875rem; - font-weight: 500; - color: var(--text-primary, #333); - cursor: pointer; -} - -.checkboxLabel input[type="checkbox"] { - width: 18px; - height: 18px; - accent-color: var(--primary-color, #f25843); - cursor: pointer; -} - -/* Language Tabs */ -.languageTabs { - display: flex; - gap: 0.25rem; - margin-bottom: 1rem; - border-bottom: 1px solid var(--border-color, #e0e0e0); - padding-bottom: 0.5rem; -} - -.languageTab { - padding: 0.5rem 1rem; - background: transparent; - border: 1px solid transparent; - border-bottom: none; - border-radius: 6px 6px 0 0; - font-size: 0.8125rem; - font-weight: 500; - color: var(--text-secondary, #666); - cursor: pointer; - transition: all 0.2s; -} - -.languageTab:hover { - background: var(--bg-secondary, #f5f5f5); - color: var(--text-primary, #333); -} - -.languageTab.active { - background: var(--bg-primary, #ffffff); - border-color: var(--border-color, #e0e0e0); - color: var(--primary-color, #f25843); - border-bottom: 2px solid var(--primary-color, #f25843); - margin-bottom: -1px; -} - -/* Loading State */ -.loadingState { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 3rem; - color: var(--text-secondary, #666); -} - -.spinner { - width: 32px; - height: 32px; - border: 3px solid var(--border-color, #e0e0e0); - border-top-color: var(--primary-color, #f25843); - border-radius: 50%; - animation: spin 1s linear infinite; - margin-bottom: 1rem; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -/* Responsive */ -@media (max-width: 1200px) { - .actionsPanel { - width: 350px; - } -} - -@media (max-width: 900px) { - .editorContent { - flex-direction: column; - } - - .formPanel { - border-right: none; - border-bottom: 1px solid var(--border-color, #e0e0e0); - } - - .actionsPanel { - width: 100%; - height: 300px; - } - - .actionsPanelCollapsed { - width: 100%; - height: 48px; - } - - .actionsPanelCollapsed .actionsPanelToggle { - writing-mode: horizontal-tb; - text-orientation: mixed; - padding: 0.75rem; - height: auto; - } - - .actionsPanelCollapsed .actionsPanelToggle svg { - margin-bottom: 0; - margin-right: 0.5rem; - transform: none; - } -} diff --git a/src/components/AutomationEditor/AutomationEditor.tsx b/src/components/AutomationEditor/AutomationEditor.tsx deleted file mode 100644 index aa81bc0..0000000 --- a/src/components/AutomationEditor/AutomationEditor.tsx +++ /dev/null @@ -1,1072 +0,0 @@ -/** - * AutomationEditor - * - * Full-screen editor for AutomationDefinitions and AutomationTemplates. - * Features: - * - Mode toggle: 'definition' vs 'template' - * - Custom form fields for definition/template properties - * - JSON template editor with syntax highlighting - * - Integrated ActionsPanel for action discovery and copy/paste - * - Placeholder extraction and editing - */ - -import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'; -import { FaSave, FaChevronLeft, FaChevronRight, FaCode, FaExclamationTriangle, FaMagic, FaFolder, FaFolderOpen, FaArrowUp, FaSpinner } from 'react-icons/fa'; -import { Popup } from '../UiComponents/Popup'; -import { ActionsPanel } from '../ActionsPanel'; -import { ProviderMultiSelect, _defaultProviderSelection, _migrateFromLegacy, _toBackendProviders } from '../ProviderSelector'; -import type { ProviderSelection } from '../ProviderSelector'; -import { useBilling } from '../../hooks/useBilling'; -import { useToast } from '../../contexts/ToastContext'; -import { useLanguage } from '../../providers/language/LanguageContext'; -import { useWorkflowActions } from '../../hooks/useAutomations'; -import { useApiRequest } from '../../hooks/useApi'; -import api from '../../api'; -import type { Automation, AutomationTemplate } from '../../hooks/useAutomations'; -import styles from './AutomationEditor.module.css'; - -// SharePoint folder picker types -interface SiteOption { - value: string; - label: string; - siteId: string; - siteName: string; - webUrl: string; - path: string; -} - -interface FolderOption { - value: string; - label: string; - siteId: string; - folderName: string; - path: string; -} - -// SharePoint Folder Picker Component -interface SharepointFolderPickerProps { - connectionReference: string; - value: string; - onChange: (path: string) => void; -} - -const SharepointFolderPicker: React.FC = ({ connectionReference, value, onChange }) => { - const [siteOptions, setSiteOptions] = useState([]); - const [folderOptions, setFolderOptions] = useState([]); - const [selectedSite, setSelectedSite] = useState(null); - const [currentPath, setCurrentPath] = useState(''); - const [isLoadingSites, setIsLoadingSites] = useState(false); - const [isLoadingFolders, setIsLoadingFolders] = useState(false); - const [error, setError] = useState(null); - const [isExpanded, setIsExpanded] = useState(false); - - // Load sites when connection reference changes - useEffect(() => { - if (!connectionReference || !isExpanded) return; - - const loadSites = async () => { - setIsLoadingSites(true); - setError(null); - try { - const params = new URLSearchParams({ connectionReference }); - const response = await api.get(`/api/sharepoint/folder-options?${params}`); - setSiteOptions(response.data || []); - } catch (err: any) { - console.error('Failed to load sites:', err); - setError(err.response?.data?.detail || 'Fehler beim Laden der SharePoint-Sites'); - setSiteOptions([]); - } finally { - setIsLoadingSites(false); - } - }; - - loadSites(); - }, [connectionReference, isExpanded]); - - // Load folders when site or path changes - useEffect(() => { - if (!selectedSite || !connectionReference) return; - - const loadFolders = async () => { - setIsLoadingFolders(true); - setError(null); - try { - const params = new URLSearchParams({ - connectionReference, - siteId: selectedSite.siteId - }); - if (currentPath) params.append('path', currentPath); - - const response = await api.get(`/api/sharepoint/folder-options?${params}`); - setFolderOptions(response.data || []); - } catch (err: any) { - console.error('Failed to load folders:', err); - setError(err.response?.data?.detail || 'Fehler beim Laden der Ordner'); - setFolderOptions([]); - } finally { - setIsLoadingFolders(false); - } - }; - - loadFolders(); - }, [selectedSite, currentPath, connectionReference]); - - const handleSiteSelect = (siteId: string) => { - const site = siteOptions.find(s => s.siteId === siteId); - setSelectedSite(site || null); - setCurrentPath(''); - setFolderOptions([]); - }; - - const handleFolderNavigate = (folder: FolderOption) => { - setCurrentPath(folder.path); - }; - - const handleFolderSelect = (folder: FolderOption) => { - const fullPath = `${selectedSite?.path || ''}/${folder.path}`; - onChange(fullPath); - setIsExpanded(false); - }; - - const handleGoUp = () => { - if (!currentPath) return; - const parts = currentPath.split('/'); - parts.pop(); - setCurrentPath(parts.join('/')); - }; - - const handleSelectCurrentFolder = () => { - if (!selectedSite) return; - const fullPath = currentPath - ? `${selectedSite.path}/${currentPath}` - : selectedSite.path; - onChange(fullPath); - setIsExpanded(false); - }; - - if (!connectionReference) { - return ( -
- onChange(e.target.value)} - placeholder="/sites/SiteName/Ordner/Pfad" - /> - - Keine Verbindung ausgewählt - manueller Pfad - -
- ); - } - - return ( -
-
- onChange(e.target.value)} - placeholder="/sites/SiteName/Ordner/Pfad" - /> - -
- - setIsExpanded(false)} - size="large" - closable={true} - > - {error && ( -
- {error} -
- )} - - {/* Site Selection */} -
- - {isLoadingSites ? ( -
- Lade Sites... -
- ) : ( - - )} -
- - {/* Folder Browser */} - {selectedSite && ( -
-
- {selectedSite.siteName} - {currentPath && / {currentPath}} -
- -
- {currentPath && ( -
- .. -
- )} - - {isLoadingFolders ? ( -
- Lade Ordner... -
- ) : folderOptions.length === 0 ? ( -
- Keine Unterordner -
- ) : ( - folderOptions.map(folder => ( -
- - handleFolderNavigate(folder)} - > - {folder.folderName} - - -
- )) - )} -
- - -
- )} -
-
- ); -}; - -// Local TextMultilingual type that supports both 'ge' and 'de' for German -interface LocalTextMultilingual { - en: string; - ge?: string; - de?: string; - fr?: string; - it?: string; -} - -// Placeholder type info extracted from actions -interface PlaceholderTypeInfo { - name: string; - frontendType: string; - frontendOptions?: string | string[]; - description?: string; - required?: boolean; - connectionPlaceholder?: string; // For sharepointFolder: which placeholder holds the connection reference -} - -// Editor mode type -export type EditorMode = 'definition' | 'template'; - -// Props interface -export interface AutomationEditorProps { - /** Editor mode: 'definition' for AutomationDefinition, 'template' for AutomationTemplate */ - mode: EditorMode; - /** Initial data for editing (null for create) */ - initialData?: Automation | AutomationTemplate | null; - /** Callback when saving */ - onSave: (data: any) => Promise; - /** Callback when canceling */ - onCancel: () => void; - /** Whether the editor is in a saving state */ - saving?: boolean; - /** Custom title override */ - title?: string; -} - -// Available languages for multilingual fields -const LANGUAGES = [ - { code: 'en', label: 'English' }, - { code: 'de', label: 'Deutsch' }, - { code: 'fr', label: 'Français' }, -]; - -/** - * Extract {{KEY:name}} placeholders from JSON string - */ -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; -} - -/** - * Validate JSON string - */ -function _validateJson(jsonString: string): { valid: boolean; error?: string } { - if (!jsonString || jsonString.trim() === '') { - return { valid: false, error: 'Template JSON darf nicht leer sein' }; - } - try { - JSON.parse(jsonString); - return { valid: true }; - } catch (e: any) { - return { valid: false, error: `Ungültiges JSON: ${e.message}` }; - } -} - -export const AutomationEditor: React.FC = ({ - mode, - initialData, - onSave, - onCancel, - saving = false, - title -}) => { - const { showError, showSuccess } = useToast(); - const { currentLanguage } = useLanguage(); - const jsonTextareaRef = useRef(null); - - // State - const [showActionsPanel, setShowActionsPanel] = useState(true); - const [activeLanguageTab, setActiveLanguageTab] = useState<'en' | 'de' | 'fr'>((currentLanguage as 'en' | 'de' | 'fr') || 'de'); - - // Definition fields - const [label, setLabel] = useState(''); - const [schedule, setSchedule] = useState('0 22 * * *'); - const [active, setActive] = useState(false); - const [providerSelection, setProviderSelection] = useState(_defaultProviderSelection()); - const { allowedProviders: billingProviders } = useBilling(); - - // Template multilingual fields - const [labelMulti, setLabelMulti] = useState({ en: '', de: '' }); - const [overviewMulti, setOverviewMulti] = useState({ en: '', de: '' }); - - // Common fields - const [templateJson, setTemplateJson] = useState(''); - const [jsonError, setJsonError] = useState(null); - const [placeholders, setPlaceholders] = useState>({}); - const [isSaving, setIsSaving] = useState(false); - - // Actions and placeholder type info - const { actions, fetchActions } = useWorkflowActions(); - const { request } = useApiRequest(); - const [connectionOptions, setConnectionOptions] = useState>([]); - const [loadingConnections, setLoadingConnections] = useState(false); - - // Load actions on mount - useEffect(() => { - fetchActions(); - }, [fetchActions]); - - // Load user connections for userConnection type placeholders - useEffect(() => { - const loadConnections = async () => { - setLoadingConnections(true); - try { - const response = await request({ - url: '/api/connections/', - method: 'get' - }); - // Response is PaginatedResponse with items array - // Backend provides connectionReference and displayLabel computed fields - const connections = response?.items || response || []; - if (Array.isArray(connections)) { - const options = connections - .filter((conn: any) => conn.connectionReference && conn.displayLabel) - .map((conn: any) => ({ - value: conn.connectionReference, - label: conn.displayLabel - })); - setConnectionOptions(options); - } - } catch (err) { - console.error('Error loading connections:', err); - } finally { - setLoadingConnections(false); - } - }; - loadConnections(); - }, [request]); - - // Helper function to extract placeholder names from a value (string or array) - // Returns array of { name, isArrayElement } objects - const extractPlaceholders = (value: any, isArrayElement = false): { name: string; isArrayElement: boolean }[] => { - const placeholders: { name: string; isArrayElement: boolean }[] = []; - const regex = /\{\{KEY:(\w+)\}\}/g; - - if (typeof value === 'string') { - let match; - while ((match = regex.exec(value)) !== null) { - placeholders.push({ name: match[1], isArrayElement }); - } - } else if (Array.isArray(value)) { - for (const item of value) { - // Mark items inside arrays as array elements - placeholders.push(...extractPlaceholders(item, true)); - } - } - - return placeholders; - }; - - // Build a map of placeholder names to their type info from actions - const placeholderTypeMap = useMemo(() => { - const typeMap: Record = {}; - - // Parse the template JSON to find which actions are used - try { - const parsed = JSON.parse(templateJson); - const tasks = parsed?.tasks || []; - - for (const task of tasks) { - // Support both 'actions' and 'actionList' field names - const taskActions = task?.actionList || task?.actions || []; - for (const action of taskActions) { - const execMethod = action?.execMethod; - const execAction = action?.execAction; - const execParams = action?.execParameters || {}; - - // Find matching action definition - const actionDef = actions.find( - a => a.method === execMethod && a.action === execAction - ); - - if (actionDef) { - // First pass: Find connectionReference placeholder for this action - let connectionPlaceholder: string | undefined; - const connectionRefValue = execParams['connectionReference']; - const connPlaceholders = extractPlaceholders(connectionRefValue); - if (connPlaceholders.length > 0) { - connectionPlaceholder = connPlaceholders[0].name; - } - - // Second pass: Check each parameter for {{KEY:xxx}} placeholders - for (const [paramName, paramValue] of Object.entries(execParams)) { - const foundPlaceholders = extractPlaceholders(paramValue); - for (const placeholder of foundPlaceholders) { - const placeholderName = placeholder.name; - // Find parameter definition - const paramDef = actionDef.parameters.find(p => p.name === paramName); - if (paramDef && !typeMap[placeholderName]) { - // If placeholder is inside an array, always use text field - const effectiveFrontendType = placeholder.isArrayElement - ? 'text' - : (paramDef.frontendType || 'text'); - - typeMap[placeholderName] = { - name: placeholderName, - frontendType: effectiveFrontendType, - frontendOptions: paramDef.frontendOptions, - description: paramDef.description, - required: paramDef.required, - // For sharepointFolder types, store the associated connection placeholder - connectionPlaceholder: effectiveFrontendType === 'sharepointFolder' ? connectionPlaceholder : undefined - }; - } - } - } - } - } - } - } catch { - // Ignore parse errors - } - - return typeMap; - }, [templateJson, actions]); - - // Initialize data from initialData - useEffect(() => { - if (initialData) { - if (mode === 'template') { - const tmpl = initialData as AutomationTemplate; - // Convert TextMultilingual (with 'ge') to LocalTextMultilingual (with 'de') - const tmplLabel = tmpl.label || { en: '' }; - const tmplOverview = tmpl.overview || { en: '' }; - setLabelMulti({ - en: tmplLabel.en || '', - de: (tmplLabel as any).de || (tmplLabel as any).ge || '', - fr: tmplLabel.fr || '' - }); - setOverviewMulti({ - en: tmplOverview.en || '', - de: (tmplOverview as any).de || (tmplOverview as any).ge || '', - fr: tmplOverview.fr || '' - }); - } else { - const def = initialData as Automation; - setLabel(def.label || ''); - setSchedule(def.schedule || '0 22 * * *'); - setActive(def.active ?? false); - setProviderSelection(_migrateFromLegacy(def.allowedProviders || [])); - } - - // Extract template JSON - const template = initialData.template; - if (template) { - const jsonStr = typeof template === 'string' - ? template - : JSON.stringify(template, null, 2); - setTemplateJson(jsonStr); - - // Extract placeholders - const keys = _extractPlaceholdersFromJson(jsonStr); - const existingPlaceholders = (initialData as Automation).placeholders || {}; - const newPlaceholders: Record = {}; - keys.forEach(key => { - newPlaceholders[key] = existingPlaceholders[key] || ''; - }); - setPlaceholders(newPlaceholders); - } - } else { - // New item - set defaults - if (mode === 'template') { - setLabelMulti({ en: '', de: '' }); - setOverviewMulti({ en: '', de: '' }); - } else { - setLabel(''); - setSchedule('0 22 * * *'); - setActive(false); - } - setTemplateJson('{\n "overview": "Beschreibung des Workflows",\n "tasks": [\n {\n "objective": "Aufgabe beschreiben",\n "actions": []\n }\n ]\n}'); - setPlaceholders({}); - } - }, [initialData, mode]); - - // Update placeholders when JSON changes - const handleJsonChange = useCallback((newJson: string) => { - setTemplateJson(newJson); - - // Validate JSON - const validation = _validateJson(newJson); - setJsonError(validation.error || null); - - // Extract and update placeholders - if (validation.valid) { - const keys = _extractPlaceholdersFromJson(newJson); - setPlaceholders(prev => { - const newPlaceholders: Record = {}; - keys.forEach(key => { - newPlaceholders[key] = prev[key] || ''; - }); - return newPlaceholders; - }); - } - }, []); - - // Format JSON (prettify) - const handleFormatJson = useCallback(() => { - try { - const parsed = JSON.parse(templateJson); - const formatted = JSON.stringify(parsed, null, 2); - setTemplateJson(formatted); - setJsonError(null); - showSuccess('JSON formatiert'); - } catch (e: any) { - showError('JSON kann nicht formatiert werden: ' + e.message); - } - }, [templateJson, showSuccess, showError]); - - // Handle placeholder value change - const handlePlaceholderChange = useCallback((key: string, value: string) => { - setPlaceholders(prev => ({ - ...prev, - [key]: value - })); - }, []); - - // Handle multilingual field change - const handleMultilingualChange = useCallback(( - setter: React.Dispatch>, - lang: string, - value: string - ) => { - setter(prev => ({ - ...prev, - [lang]: value - })); - }, []); - - // Insert action JSON from ActionsPanel - const handleInsertAction = useCallback((actionJson: string) => { - if (jsonTextareaRef.current) { - const textarea = jsonTextareaRef.current; - const start = textarea.selectionStart; - const end = textarea.selectionEnd; - const currentValue = textarea.value; - - // Insert at cursor position - const newValue = currentValue.substring(0, start) + actionJson + currentValue.substring(end); - setTemplateJson(newValue); - - // Validate - const validation = _validateJson(newValue); - setJsonError(validation.error || null); - - // Focus and set cursor after inserted text - setTimeout(() => { - textarea.focus(); - textarea.selectionStart = start + actionJson.length; - textarea.selectionEnd = start + actionJson.length; - }, 0); - - showSuccess('Action eingefügt'); - } - }, [showSuccess]); - - // Handle save - const handleSave = useCallback(async () => { - // Validate JSON - const validation = _validateJson(templateJson); - if (!validation.valid) { - showError(validation.error || 'Ungültiges JSON'); - return; - } - - // Validate required fields - if (mode === 'template') { - if (!labelMulti?.en && !labelMulti?.de) { - showError('Label (mindestens eine Sprache) ist erforderlich'); - return; - } - } else { - if (!label) { - showError('Label ist erforderlich'); - return; - } - } - - setIsSaving(true); - try { - // Build save data based on mode - let saveData: Record; - - if (mode === 'template') { - saveData = { - label: labelMulti, - overview: overviewMulti, - template: templateJson - }; - } else { - saveData = { - label, - schedule, - active, - template: templateJson, - placeholders, - allowedProviders: _toBackendProviders(providerSelection, billingProviders), - }; - } - - // Preserve existing fields from initialData - if (initialData) { - if ('mandateId' in initialData) saveData.mandateId = initialData.mandateId; - if ('featureInstanceId' in initialData) saveData.featureInstanceId = (initialData as Automation).featureInstanceId; - } - - await onSave(saveData); - } catch (err: any) { - showError(`Fehler beim Speichern: ${err.message}`); - } finally { - setIsSaving(false); - } - }, [label, schedule, active, providerSelection, billingProviders, labelMulti, overviewMulti, templateJson, placeholders, mode, initialData, onSave, showError]); - - // Computed values - const editorTitle = title || (mode === 'template' - ? (initialData ? 'Vorlage bearbeiten' : 'Neue Vorlage') - : (initialData ? 'Automatisierung bearbeiten' : 'Neue Automatisierung')); - - const placeholderKeys = Object.keys(placeholders); - const isFormSaving = saving || isSaving; - - return ( - -
- {jsonError && ( - - - JSON enthält Fehler - - )} -
-
- - -
- - } - > -
- {/* Content */} -
- {/* Form Panel (Left) */} -
-
-

Einstellungen

-
-
- {/* Form Fields based on mode */} - {mode === 'template' ? ( - /* Template Mode: Multilingual fields */ -
- {/* Language Tabs */} -
- {LANGUAGES.map(lang => ( - - ))} -
- - {/* Label Field */} -
- - handleMultilingualChange(setLabelMulti, activeLanguageTab, e.target.value)} - placeholder={`Label (${activeLanguageTab.toUpperCase()})`} - /> -

Name der Vorlage

-
- - {/* Overview Field */} -
- -