From 0f791a53fbf5f02131d6607bac06497fe94727a0 Mon Sep 17 00:00:00 2001 From: idittrich-valueon Date: Wed, 25 Mar 2026 09:39:19 +0100 Subject: [PATCH] next version of visual workflow editor with Clickup Connector --- Untitled | 1 + src/api/automation2Api.ts | 188 +- src/api/connectionApi.ts | 6 +- .../Automation2FlowEditor.module.css | 499 ---- .../Automation2FlowEditor/NodeConfigPanel.tsx | 70 - .../configs/AiNodeConfig.tsx | 68 - .../configs/FormNodeConfig.tsx | 126 - .../configs/ReviewNodeConfig.tsx | 17 - .../configs/SharePointNodeConfig.tsx | 245 -- .../configs/UploadNodeConfig.tsx | 37 - .../Automation2FlowEditor/configs/types.ts | 16 - .../Automation2FlowEditor/constants.ts | 38 - .../context/Automation2DataFlowContext.tsx | 66 + .../editor/Automation2FlowEditor.module.css | 1453 +++++++++++ .../{ => editor}/Automation2FlowEditor.tsx | 228 +- .../{ => editor}/CanvasHeader.tsx | 17 +- .../{ => editor}/FlowCanvas.tsx | 443 +++- .../editor/NodeConfigPanel.tsx | 120 + .../{ => editor}/NodeListItem.tsx | 6 +- .../{ => editor}/NodeSidebar.tsx | 15 +- .../editor/WorkflowConfigurationModal.tsx | 115 + src/components/Automation2FlowEditor/index.ts | 20 +- .../nodes/configs/AiNodeConfig.tsx | 90 + .../configs/ApprovalNodeConfig.tsx | 0 .../nodes/configs/ClickUpNodeConfig.tsx | 2221 +++++++++++++++++ .../{ => nodes}/configs/CommentNodeConfig.tsx | 0 .../configs/ConfirmationNodeConfig.tsx | 0 .../{ => nodes}/configs/EmailNodeConfig.tsx | 2 +- .../nodes/configs/FileCreateNodeConfig.tsx | 121 + .../nodes/configs/ReviewNodeConfig.tsx | 18 + .../configs/SelectionNodeConfig.tsx | 0 .../nodes/configs/SharePointNodeConfig.tsx | 340 +++ .../nodes/configs/UploadNodeConfig.tsx | 80 + .../{ => nodes}/configs/index.ts | 25 +- .../nodes/configs/types.ts | 1 + .../nodes/form/FormNodeConfig.tsx | 228 ++ .../Automation2FlowEditor/nodes/form/index.ts | 1 + .../nodes/ifElse/IfElseNodeConfig.tsx | 151 ++ .../nodes/ifElse/index.ts | 1 + .../nodes/loop/LoopNodeConfig.tsx | 26 + .../Automation2FlowEditor/nodes/loop/index.ts | 1 + .../nodes/runtime/fileTypeMimeMapping.ts | 98 + .../nodes/runtime/scheduleCron.ts | 296 +++ .../nodes/runtime/workflowStartSync.ts | 222 ++ .../nodes/shared/DataPicker.tsx | 126 + .../nodes/shared/DynamicValueField.tsx | 114 + .../nodes/shared/HybridStaticRefField.tsx | 109 + .../nodes/shared/LoopItemsSelect.tsx | 211 ++ .../nodes/shared/RefSourceSelect.tsx | 405 +++ .../{ => nodes/shared}/categoryIcons.tsx | 4 +- .../nodes/shared/clickupFormSync.ts | 277 ++ .../nodes/shared/conditionOperators.ts | 66 + .../nodes/shared/constants.ts | 20 + .../nodes/shared/dataFlowGraph.ts | 97 + .../nodes/shared/dataRef.ts | 91 + .../{ => nodes/shared}/graphUtils.ts | 21 +- .../nodes/shared/outputPreviewRegistry.ts | 153 ++ .../nodes/shared/types.ts | 28 + .../{ => nodes/shared}/utils.ts | 0 .../nodes/start/FormStartNodeConfig.tsx | 122 + .../nodes/start/ScheduleStartNodeConfig.tsx | 435 ++++ .../nodes/start/StartNodeConfig.tsx | 40 + .../nodes/start/index.ts | 3 + .../nodes/switch/SwitchNodeConfig.tsx | 247 ++ .../nodes/switch/index.ts | 1 + .../FolderTree/SharepointBrowseTree.tsx | 41 +- src/hooks/useConnections.ts | 96 +- src/hooks/useFiles.ts | 7 +- src/pages/admin/Admin.module.css | 32 +- src/pages/basedata/ConnectionsPage.tsx | 41 +- .../automation2/Automation2WorkflowsPage.tsx | 90 +- .../Automation2WorkflowsTasks.module.css | 189 +- .../Automation2WorkflowsTasksPage.tsx | 523 +++- src/pages/views/workspace/DataSourcePanel.tsx | 1 + src/pages/views/workspace/FilePreview.tsx | 14 +- 75 files changed, 9926 insertions(+), 1394 deletions(-) create mode 100644 Untitled delete mode 100644 src/components/Automation2FlowEditor/Automation2FlowEditor.module.css delete mode 100644 src/components/Automation2FlowEditor/NodeConfigPanel.tsx delete mode 100644 src/components/Automation2FlowEditor/configs/AiNodeConfig.tsx delete mode 100644 src/components/Automation2FlowEditor/configs/FormNodeConfig.tsx delete mode 100644 src/components/Automation2FlowEditor/configs/ReviewNodeConfig.tsx delete mode 100644 src/components/Automation2FlowEditor/configs/SharePointNodeConfig.tsx delete mode 100644 src/components/Automation2FlowEditor/configs/UploadNodeConfig.tsx delete mode 100644 src/components/Automation2FlowEditor/configs/types.ts delete mode 100644 src/components/Automation2FlowEditor/constants.ts create mode 100644 src/components/Automation2FlowEditor/context/Automation2DataFlowContext.tsx create mode 100644 src/components/Automation2FlowEditor/editor/Automation2FlowEditor.module.css rename src/components/Automation2FlowEditor/{ => editor}/Automation2FlowEditor.tsx (58%) rename src/components/Automation2FlowEditor/{ => editor}/CanvasHeader.tsx (87%) rename src/components/Automation2FlowEditor/{ => editor}/FlowCanvas.tsx (57%) create mode 100644 src/components/Automation2FlowEditor/editor/NodeConfigPanel.tsx rename src/components/Automation2FlowEditor/{ => editor}/NodeListItem.tsx (87%) rename src/components/Automation2FlowEditor/{ => editor}/NodeSidebar.tsx (87%) create mode 100644 src/components/Automation2FlowEditor/editor/WorkflowConfigurationModal.tsx create mode 100644 src/components/Automation2FlowEditor/nodes/configs/AiNodeConfig.tsx rename src/components/Automation2FlowEditor/{ => nodes}/configs/ApprovalNodeConfig.tsx (100%) create mode 100644 src/components/Automation2FlowEditor/nodes/configs/ClickUpNodeConfig.tsx rename src/components/Automation2FlowEditor/{ => nodes}/configs/CommentNodeConfig.tsx (100%) rename src/components/Automation2FlowEditor/{ => nodes}/configs/ConfirmationNodeConfig.tsx (100%) rename src/components/Automation2FlowEditor/{ => nodes}/configs/EmailNodeConfig.tsx (99%) create mode 100644 src/components/Automation2FlowEditor/nodes/configs/FileCreateNodeConfig.tsx create mode 100644 src/components/Automation2FlowEditor/nodes/configs/ReviewNodeConfig.tsx rename src/components/Automation2FlowEditor/{ => nodes}/configs/SelectionNodeConfig.tsx (100%) create mode 100644 src/components/Automation2FlowEditor/nodes/configs/SharePointNodeConfig.tsx create mode 100644 src/components/Automation2FlowEditor/nodes/configs/UploadNodeConfig.tsx rename src/components/Automation2FlowEditor/{ => nodes}/configs/index.ts (61%) create mode 100644 src/components/Automation2FlowEditor/nodes/configs/types.ts create mode 100644 src/components/Automation2FlowEditor/nodes/form/FormNodeConfig.tsx create mode 100644 src/components/Automation2FlowEditor/nodes/form/index.ts create mode 100644 src/components/Automation2FlowEditor/nodes/ifElse/IfElseNodeConfig.tsx create mode 100644 src/components/Automation2FlowEditor/nodes/ifElse/index.ts create mode 100644 src/components/Automation2FlowEditor/nodes/loop/LoopNodeConfig.tsx create mode 100644 src/components/Automation2FlowEditor/nodes/loop/index.ts create mode 100644 src/components/Automation2FlowEditor/nodes/runtime/fileTypeMimeMapping.ts create mode 100644 src/components/Automation2FlowEditor/nodes/runtime/scheduleCron.ts create mode 100644 src/components/Automation2FlowEditor/nodes/runtime/workflowStartSync.ts create mode 100644 src/components/Automation2FlowEditor/nodes/shared/DataPicker.tsx create mode 100644 src/components/Automation2FlowEditor/nodes/shared/DynamicValueField.tsx create mode 100644 src/components/Automation2FlowEditor/nodes/shared/HybridStaticRefField.tsx create mode 100644 src/components/Automation2FlowEditor/nodes/shared/LoopItemsSelect.tsx create mode 100644 src/components/Automation2FlowEditor/nodes/shared/RefSourceSelect.tsx rename src/components/Automation2FlowEditor/{ => nodes/shared}/categoryIcons.tsx (79%) create mode 100644 src/components/Automation2FlowEditor/nodes/shared/clickupFormSync.ts create mode 100644 src/components/Automation2FlowEditor/nodes/shared/conditionOperators.ts create mode 100644 src/components/Automation2FlowEditor/nodes/shared/constants.ts create mode 100644 src/components/Automation2FlowEditor/nodes/shared/dataFlowGraph.ts create mode 100644 src/components/Automation2FlowEditor/nodes/shared/dataRef.ts rename src/components/Automation2FlowEditor/{ => nodes/shared}/graphUtils.ts (80%) create mode 100644 src/components/Automation2FlowEditor/nodes/shared/outputPreviewRegistry.ts create mode 100644 src/components/Automation2FlowEditor/nodes/shared/types.ts rename src/components/Automation2FlowEditor/{ => nodes/shared}/utils.ts (100%) create mode 100644 src/components/Automation2FlowEditor/nodes/start/FormStartNodeConfig.tsx create mode 100644 src/components/Automation2FlowEditor/nodes/start/ScheduleStartNodeConfig.tsx create mode 100644 src/components/Automation2FlowEditor/nodes/start/StartNodeConfig.tsx create mode 100644 src/components/Automation2FlowEditor/nodes/start/index.ts create mode 100644 src/components/Automation2FlowEditor/nodes/switch/SwitchNodeConfig.tsx create mode 100644 src/components/Automation2FlowEditor/nodes/switch/index.ts diff --git a/Untitled b/Untitled new file mode 100644 index 0000000..2f259b7 --- /dev/null +++ b/Untitled @@ -0,0 +1 @@ +s \ No newline at end of file diff --git a/src/api/automation2Api.ts b/src/api/automation2Api.ts index f215f7e..976c7d8 100644 --- a/src/api/automation2Api.ts +++ b/src/api/automation2Api.ts @@ -27,6 +27,8 @@ export interface NodeType { parameters: NodeTypeParameter[]; inputs: number; outputs: number; + /** Labels per output (e.g. ["Ja", "Nein"] for flow.ifElse) */ + outputLabels?: string[]; executor: string; meta?: { icon?: string; @@ -76,11 +78,24 @@ export interface ExecuteGraphResponse { 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 */ @@ -128,22 +143,36 @@ export async function fetchNodeTypes( * 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 + 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 } + { 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: { graph, workflowId }, + data, }); const ms = Math.round(performance.now() - start); console.log( @@ -167,11 +196,13 @@ export async function executeGraph( export async function fetchWorkflows( request: ApiRequestFunction, - instanceId: string + 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 ?? []; } @@ -190,7 +221,7 @@ export async function fetchWorkflow( export async function createWorkflow( request: ApiRequestFunction, instanceId: string, - body: { label: string; graph: Automation2Graph } + body: { label: string; graph: Automation2Graph; invocations?: WorkflowEntryPoint[] } ): Promise { return await request({ url: `/api/automation2/${instanceId}/workflows`, @@ -203,7 +234,12 @@ export async function updateWorkflow( request: ApiRequestFunction, instanceId: string, workflowId: string, - body: { label?: string; graph?: Automation2Graph } + body: { + label?: string; + graph?: Automation2Graph; + invocations?: WorkflowEntryPoint[]; + active?: boolean; + } ): Promise { return await request({ url: `/api/automation2/${instanceId}/workflows/${workflowId}`, @@ -243,6 +279,25 @@ export async function fetchWorkflowRuns( return data?.runs ?? []; } +export interface CompletedRun extends Automation2Run { + workflowLabel?: string; + _modifiedAt?: number; + _createdAt?: 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 // ------------------------------------------------------------------------- @@ -354,3 +409,124 @@ export async function fetchBrowse( }); 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/connectionApi.ts b/src/api/connectionApi.ts index cb1b83e..7263e95 100644 --- a/src/api/connectionApi.ts +++ b/src/api/connectionApi.ts @@ -7,7 +7,7 @@ import { ApiRequestOptions } from '../hooks/useApi'; export interface Connection { id: string; userId: string; - authority: 'local' | 'google' | 'msft'; + authority: 'local' | 'google' | 'msft' | 'clickup'; externalId: string; externalUsername: string; externalEmail?: string; @@ -52,8 +52,8 @@ export interface PaginatedResponse { export interface CreateConnectionData { id?: string; userId?: string; - authority?: 'msft' | 'google'; - type?: 'msft' | 'google'; // Backend expects this field + authority?: 'msft' | 'google' | 'clickup'; + type?: 'msft' | 'google' | 'clickup'; // Backend maps type → authority externalId?: string; externalUsername?: string; externalEmail?: string; diff --git a/src/components/Automation2FlowEditor/Automation2FlowEditor.module.css b/src/components/Automation2FlowEditor/Automation2FlowEditor.module.css deleted file mode 100644 index 828453e..0000000 --- a/src/components/Automation2FlowEditor/Automation2FlowEditor.module.css +++ /dev/null @@ -1,499 +0,0 @@ -/** - * Automation2 Flow Editor Styles - * Sidebar with node list + canvas area. - */ - -.container { - display: flex; - flex: 1; - min-height: 0; - overflow: hidden; -} - -/* ============================================================================= - SIDEBAR - Node List - ============================================================================= */ - -.sidebar { - flex-shrink: 0; - width: 280px; - display: flex; - flex-direction: column; - background: var(--bg-secondary, #f8f9fa); - border-right: 1px solid var(--border-color, #e0e0e0); - overflow: hidden; -} - -.sidebarHeader { - padding: 1rem; - border-bottom: 1px solid var(--border-color, #e0e0e0); - background: var(--bg-primary, #fff); -} - -.sidebarTitle { - margin: 0; - font-size: 1rem; - font-weight: 600; - color: var(--text-primary, #1a1a1a); -} - -.sidebarSearch { - margin-top: 0.75rem; - 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, #fff); - color: var(--text-primary, #333); -} - -.sidebarSearch::placeholder { - color: var(--text-tertiary, #999); -} - -.sidebarSearch:focus { - outline: none; - border-color: var(--primary-color, #007bff); - box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.15); -} - -.nodeList { - flex: 1; - overflow-y: auto; - padding: 0.5rem; -} - -/* Category Groups */ -.categoryGroup { - margin-bottom: 1rem; -} - - -.categoryHeader { - display: flex; - align-items: center; - padding: 0.5rem 0.75rem; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-secondary, #666); -} - -.categoryIcon { - margin-right: 0.5rem; - font-size: 0.875rem; -} - -.categoryLabel { - flex: 1; -} - -.categoryCount { - background: var(--bg-tertiary, #e9ecef); - color: var(--text-secondary, #666); - padding: 0.125rem 0.5rem; - border-radius: 10px; - font-size: 0.7rem; -} - -/* Node Items */ -.nodeItem { - display: flex; - align-items: center; - padding: 0.5rem 0.75rem; - margin-bottom: 0.25rem; - border-radius: 6px; - cursor: grab; - transition: background 0.15s; - border: 1px solid transparent; -} - -.nodeItem:hover { - background: var(--bg-hover, #e9ecef); -} - -.nodeItem:active { - cursor: grabbing; -} - -.nodeItemIcon { - flex-shrink: 0; - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 6px; - margin-right: 0.75rem; - font-size: 0.875rem; -} - -.nodeItemInfo { - flex: 1; - min-width: 0; -} - -.nodeItemLabel { - display: block; - font-size: 0.875rem; - font-weight: 500; - color: var(--text-primary, #333); -} - -.nodeItemDesc { - display: block; - font-size: 0.75rem; - color: var(--text-secondary, #666); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* Loading / Error */ -.loading, -.error { - padding: 2rem; - text-align: center; - color: var(--text-secondary, #666); -} - -.error { - color: var(--danger-color, #dc3545); -} - -.retryButton { - margin-top: 0.75rem; - padding: 0.5rem 1rem; - background: var(--primary-color, #007bff); - color: white; - border: none; - border-radius: 6px; - cursor: pointer; - font-size: 0.875rem; -} - -.retryButton:hover { - background: var(--primary-hover, #0056b3); -} - -.spinner { - animation: spin 1s linear infinite; -} - -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -/* ============================================================================= - CANVAS - ============================================================================= */ - -.canvas { - flex: 1; - display: flex; - flex-direction: column; - min-width: 0; - background: var(--canvas-bg, #fafafa); -} - -.canvasHeader { - flex-shrink: 0; - padding: 0.75rem 1rem; - border-bottom: 1px solid var(--border-color, #e0e0e0); - background: var(--bg-primary, #fff); -} - -.canvasTitle { - margin: 0; - font-size: 0.875rem; - font-weight: 500; - color: var(--text-secondary, #666); -} - -.canvasArea { - flex: 1; - padding: 2rem; - min-height: 400px; - overflow: hidden; -} - -.canvasDropZone { - position: relative; - min-height: 100%; - height: 100%; - overflow: hidden; - border-radius: 8px; - /* Infinite grid: on viewport, moves with pan/zoom via inline style */ - background-image: radial-gradient(circle, var(--border-color, #e0e0e0) 1px, transparent 1px); - background-repeat: repeat; -} - -.canvasContent { - position: absolute; - left: 0; - top: 0; - will-change: transform; - background: transparent; -} - -.canvasGrab { - cursor: grab; -} - -.canvasPanning { - cursor: grabbing; - user-select: none; -} - -.canvasPlaceholder { - position: absolute; - left: 2rem; - top: 2rem; - text-align: center; - color: var(--text-tertiary, #999); - border: 2px dashed var(--border-color, #dee2e6); - border-radius: 8px; - padding: 2rem 3rem; -} - -.canvasPlaceholder p { - margin: 0.25rem 0; - font-size: 0.875rem; -} - -/* Canvas Nodes */ -.canvasNode { - position: absolute; - border-radius: 8px; - border: 2px solid; - cursor: grab; - overflow: visible; -} - -.canvasNode:active { - cursor: grabbing; -} - -.canvasNodeSelected { - box-shadow: 0 0 0 2px var(--primary-color, #007bff); -} - -.canvasNodeContent { - display: flex; - align-items: flex-start; - gap: 0.5rem; - padding: 0.4rem 0.6rem; - height: 100%; - box-sizing: border-box; -} - -.canvasNodeIcon { - flex-shrink: 0; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 6px; - font-size: 0.9rem; -} - -.canvasNodeText { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 0.15rem; -} - -.canvasNodeTitle { - font-size: 0.875rem; - font-weight: 500; - color: var(--text-primary, #333); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - cursor: text; -} - -.canvasNodeTitle:hover { - text-decoration: underline; -} - -.canvasNodeComment { - font-size: 0.7rem; - color: var(--text-tertiary, #999); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - cursor: text; - min-height: 1em; -} - -.canvasNodeComment:hover { - text-decoration: underline; -} - -.canvasNodeInput { - width: 100%; - padding: 0.15rem 0.25rem; - font-size: 0.875rem; - border: 1px solid var(--primary-color, #007bff); - border-radius: 4px; - outline: none; -} - -/* Connection Handles */ -.handle { - position: absolute; - border-radius: 50%; - background: var(--bg-primary, #fff); - border: 2px solid var(--border-color, #666); - cursor: crosshair; - z-index: 2; -} - -.handle:hover, -.handleConnectable { - border-color: var(--primary-color, #007bff); - background: var(--primary-color, #007bff); -} - -.handleInput { - cursor: copy; -} - -/* Node Config Panel */ -.nodeConfigPanel { - padding: 1rem; - background: var(--bg-primary, #fff); - border-left: 1px solid var(--border-color, #e0e0e0); - width: 280px; - flex-shrink: 0; - overflow-y: auto; - min-width: 0; -} - -.nodeConfigPanel h4 { - margin: 0 0 0.75rem 0; - font-size: 0.9rem; -} - -.nodeConfigPanel label { - display: block; - font-size: 0.75rem; - color: var(--text-secondary, #666); - margin-top: 0.5rem; - margin-bottom: 0.25rem; -} - -.nodeConfigPanel input[type='text'], -.nodeConfigPanel input[type='number'], -.nodeConfigPanel select, -.nodeConfigPanel textarea { - width: 100%; - padding: 0.4rem; - font-size: 0.875rem; - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 4px; -} - -.nodeConfigPanel textarea { - min-height: 60px; -} - -.nodeConfigPanel button { - margin-top: 0.5rem; - padding: 0.4rem 0.75rem; - font-size: 0.8rem; - background: var(--primary-color, #007bff); - color: white; - border: none; - border-radius: 4px; - cursor: pointer; -} - -/* Form fields editor (input.form) */ -.formFieldsList { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.formFieldRow { - display: flex; - flex-direction: column; - gap: 0.35rem; - padding: 0.5rem; - background: var(--bg-secondary, #f8f9fa); - border-radius: 6px; - border: 1px solid var(--border-color, #e0e0e0); -} - -.formFieldRowHeader { - display: flex; - align-items: flex-start; - gap: 0.35rem; -} - -.formFieldDragHandle { - flex-shrink: 0; - padding: 0.25rem; - cursor: grab; - color: var(--text-tertiary, #999); - align-self: stretch; - display: flex; - align-items: center; -} - -.formFieldDragHandle:active { - cursor: grabbing; -} - -.formFieldDragHandle:hover { - color: var(--text-secondary, #666); -} - -.formFieldInputs { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 0.35rem; -} - -.formFieldRowFooter { - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; -} - -.formFieldRequiredLabel { - display: inline-flex; - align-items: center; - gap: 0.35rem; - font-size: 0.75rem; - color: var(--text-secondary, #666); - cursor: pointer; -} - -.formFieldRemoveButton { - margin-left: auto; - padding: 0.25rem 0.4rem; - border: none; - background: transparent; - color: var(--text-tertiary, #999); - cursor: pointer; - border-radius: 4px; - display: flex; - align-items: center; -} - -.formFieldRemoveButton:hover { - color: var(--danger-color, #dc3545); - background: rgba(220, 53, 69, 0.1); -} diff --git a/src/components/Automation2FlowEditor/NodeConfigPanel.tsx b/src/components/Automation2FlowEditor/NodeConfigPanel.tsx deleted file mode 100644 index 36d7d1c..0000000 --- a/src/components/Automation2FlowEditor/NodeConfigPanel.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/** - * NodeConfigPanel - Configures parameters for input, ai, email, sharepoint nodes. - * Delegates to config components from configs/. - */ - -import React, { useState, useEffect } from 'react'; -import type { CanvasNode } from './FlowCanvas'; -import type { NodeType } from '../../api/automation2Api'; -import type { ApiRequestFunction } from '../../api/automation2Api'; -import { getLabel } from './utils'; -import { NODE_CONFIG_REGISTRY } from './configs'; -import styles from './Automation2FlowEditor.module.css'; - -interface NodeConfigPanelProps { - node: CanvasNode | null; - nodeType: NodeType | undefined; - language: string; - onParametersChange: (nodeId: string, parameters: Record) => void; - instanceId?: string; - request?: ApiRequestFunction; -} - -const CONFIGURABLE_PREFIXES = ['input.', 'ai.', 'email.', 'sharepoint.']; - -export const NodeConfigPanel: React.FC = ({ - node, - nodeType, - language, - onParametersChange, - instanceId, - request, -}) => { - const [params, setParams] = useState>({}); - - useEffect(() => { - setParams(node?.parameters ?? {}); - }, [node?.id, node?.parameters]); - - const updateParam = (key: string, value: unknown) => { - const next = { ...params, [key]: value }; - setParams(next); - if (node) onParametersChange(node.id, next); - }; - - const isConfigurable = node && CONFIGURABLE_PREFIXES.some((p) => node.type.startsWith(p)); - if (!node || !isConfigurable) return null; - - const ConfigRenderer = NODE_CONFIG_REGISTRY[node.type]; - if (!ConfigRenderer) { - return ( -
-

{getLabel(nodeType?.label, language) || node.type}

-

No configuration for {node.type}

-
- ); - } - - return ( -
-

{getLabel(nodeType?.label, language) || node.type}

- -
- ); -}; diff --git a/src/components/Automation2FlowEditor/configs/AiNodeConfig.tsx b/src/components/Automation2FlowEditor/configs/AiNodeConfig.tsx deleted file mode 100644 index cc7b76c..0000000 --- a/src/components/Automation2FlowEditor/configs/AiNodeConfig.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/** - * AI node config - prompt, query, document options per node type. - */ - -import React from 'react'; -import type { NodeConfigRendererProps } from './types'; - -const AI_FIELD_CONFIG: Record = { - 'ai.prompt': [ - { label: 'Prompt', key: 'prompt', type: 'textarea' }, - { label: 'Output format', key: 'resultType', type: 'select', options: ['txt', 'json', 'md', 'html', 'csv'] }, - ], - 'ai.webResearch': [{ label: 'Query', key: 'query', type: 'textarea' }], - 'ai.summarizeDocument': [ - { label: 'Summary length', key: 'summaryLength', type: 'select', options: ['short', 'medium', 'long'] }, - ], - 'ai.translateDocument': [{ label: 'Target language', key: 'targetLanguage', type: 'input' }], - 'ai.convertDocument': [ - { label: 'Target format', key: 'targetFormat', type: 'select', options: ['pdf', 'docx', 'txt', 'md'] }, - ], - 'ai.generateDocument': [ - { label: 'Prompt', key: 'prompt', type: 'textarea' }, - { label: 'Format', key: 'format', type: 'select', options: ['docx', 'txt', 'md'] }, - ], - 'ai.generateCode': [ - { label: 'Prompt', key: 'prompt', type: 'textarea' }, - { label: 'Language', key: 'language', type: 'select', options: ['python', 'javascript', 'typescript', 'sql'] }, - ], -}; - -export const AiNodeConfig: React.FC = ({ params, updateParam, nodeType = 'ai.prompt' }) => { - const fields = AI_FIELD_CONFIG[nodeType] ?? AI_FIELD_CONFIG['ai.prompt']; - - return ( - <> - {fields.map((f) => ( -
- - {f.type === 'textarea' ? ( -