diff --git a/src/api/automation2Api.ts b/src/api/automation2Api.ts index 63d9df5..3568c46 100644 --- a/src/api/automation2Api.ts +++ b/src/api/automation2Api.ts @@ -289,3 +289,68 @@ export async function completeTask( 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 }; +} diff --git a/src/components/Automation2FlowEditor/Automation2FlowEditor.tsx b/src/components/Automation2FlowEditor/Automation2FlowEditor.tsx index 8dec46a..eef7a76 100644 --- a/src/components/Automation2FlowEditor/Automation2FlowEditor.tsx +++ b/src/components/Automation2FlowEditor/Automation2FlowEditor.tsx @@ -50,9 +50,8 @@ export const Automation2FlowEditor: React.FC = ({ const [error, setError] = useState(null); const [filter, setFilter] = useState(''); const [expandedCategories, setExpandedCategories] = useState>( - new Set(['trigger', 'input', 'flow', 'data']) + new Set(['trigger', 'input', 'flow', 'data', 'ai', 'email', 'sharepoint']) ); - const [expandedIoMethods, setExpandedIoMethods] = useState>(new Set()); const [canvasNodes, setCanvasNodes] = useState([]); const [canvasConnections, setCanvasConnections] = useState([]); const [executing, setExecuting] = useState(false); @@ -208,15 +207,6 @@ export const Automation2FlowEditor: React.FC = ({ }); }, []); - const toggleIoMethod = useCallback((method: string) => { - setExpandedIoMethods((prev) => { - const next = new Set(prev); - if (next.has(method)) next.delete(method); - else next.add(method); - return next; - }); - }, []); - const handleDropNodeType = useCallback( (nodeTypeId: string, x: number, y: number) => { const nt = nodeTypes.find((n) => n.id === nodeTypeId); @@ -280,9 +270,7 @@ export const Automation2FlowEditor: React.FC = ({ onFilterChange={setFilter} language={language} expandedCategories={expandedCategories} - expandedIoMethods={expandedIoMethods} onToggleCategory={toggleCategory} - onToggleIoMethod={toggleIoMethod} /> ); }; @@ -318,12 +306,17 @@ export const Automation2FlowEditor: React.FC = ({ onSelectionChange={setSelectedNode} /> - {selectedNode?.type?.startsWith('input.') && ( + {selectedNode && + ['input.', 'ai.', 'email.', 'sharepoint.'].some((p) => + selectedNode.type.startsWith(p) + ) && ( nt.id === selectedNode.type)} language={language} onParametersChange={handleNodeParametersChange} + instanceId={instanceId} + request={request} /> )} diff --git a/src/components/Automation2FlowEditor/NodeConfigPanel.tsx b/src/components/Automation2FlowEditor/NodeConfigPanel.tsx index fcd6ad0..36d7d1c 100644 --- a/src/components/Automation2FlowEditor/NodeConfigPanel.tsx +++ b/src/components/Automation2FlowEditor/NodeConfigPanel.tsx @@ -1,11 +1,12 @@ /** - * NodeConfigPanel - Configures parameters for input/human nodes. + * 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'; @@ -15,13 +16,19 @@ interface NodeConfigPanelProps { 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>({}); @@ -35,14 +42,15 @@ export const NodeConfigPanel: React.FC = ({ if (node) onParametersChange(node.id, next); }; - if (!node || !node.type.startsWith('input.')) return null; + 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}

-

Keine Konfiguration für {node.type}

+

No configuration for {node.type}

); } @@ -50,7 +58,13 @@ export const NodeConfigPanel: React.FC = ({ return (

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

- +
); }; diff --git a/src/components/Automation2FlowEditor/NodeSidebar.tsx b/src/components/Automation2FlowEditor/NodeSidebar.tsx index c10d8bd..80a768e 100644 --- a/src/components/Automation2FlowEditor/NodeSidebar.tsx +++ b/src/components/Automation2FlowEditor/NodeSidebar.tsx @@ -1,13 +1,13 @@ /** * NodeSidebar - Sidebar with searchable, collapsible node list. - * Groups node types by category; I/O nodes are sub-grouped by method. + * Groups node types by category (trigger, input, flow, data, ai, email, sharepoint). */ import React, { useMemo } from 'react'; import { FaChevronDown, FaChevronRight } from 'react-icons/fa'; import type { NodeType, NodeTypeCategory } from '../../api/automation2Api'; -import { IO_METHOD_ORDER, CATEGORY_ORDER } from './constants'; -import { getLabel, getIoMethodLabel } from './utils'; +import { CATEGORY_ORDER } from './constants'; +import { getLabel } from './utils'; import { NodeListItem } from './NodeListItem'; import styles from './Automation2FlowEditor.module.css'; @@ -18,9 +18,7 @@ interface NodeSidebarProps { onFilterChange: (value: string) => void; language: string; expandedCategories: Set; - expandedIoMethods: Set; onToggleCategory: (id: string) => void; - onToggleIoMethod: (method: string) => void; } export const NodeSidebar: React.FC = ({ @@ -30,9 +28,7 @@ export const NodeSidebar: React.FC = ({ onFilterChange, language, expandedCategories, - expandedIoMethods, onToggleCategory, - onToggleIoMethod, }) => { const filteredNodeTypes = useMemo(() => { if (!filter.trim()) return nodeTypes; @@ -55,25 +51,6 @@ export const NodeSidebar: React.FC = ({ return map; }, [filteredNodeTypes]); - const ioSubGroups = useMemo(() => { - const ioNodes = groupedByCategory['io'] || []; - const byMethod: Record = {}; - ioNodes.forEach((n) => { - const method = n.meta?.method ?? n.id.split('.')[1] ?? 'other'; - if (!byMethod[method]) byMethod[method] = []; - byMethod[method].push(n); - }); - const ordered: Array<{ method: string; nodes: NodeType[] }> = []; - const methodOrder = [...IO_METHOD_ORDER]; - methodOrder.forEach((m) => { - if (byMethod[m]?.length) ordered.push({ method: m, nodes: byMethod[m] }); - }); - Object.keys(byMethod).forEach((m) => { - if (!methodOrder.includes(m)) ordered.push({ method: m, nodes: byMethod[m] }); - }); - return ordered; - }, [groupedByCategory]); - const orderedCategories = useMemo(() => { const seen = new Set(); const result: string[] = []; @@ -109,44 +86,6 @@ export const NodeSidebar: React.FC = ({ const isExpanded = expandedCategories.has(catId); const catLabel = categories.find((c) => c.id === catId); const label = getLabel(catLabel?.label, language) || catId; - - if (catId === 'io' && ioSubGroups.length > 0) { - return ( - - {ioSubGroups.map(({ method, nodes }) => { - const methodLabel = getIoMethodLabel(method, language); - const isMethodExpanded = expandedIoMethods.has(method); - return ( -
- - {isMethodExpanded && - nodes.map((node) => ( - - ))} -
- ); - })} -
- ); - } - const items = groupedByCategory[catId] || []; return (
diff --git a/src/components/Automation2FlowEditor/categoryIcons.tsx b/src/components/Automation2FlowEditor/categoryIcons.tsx index 035e3eb..53c19e4 100644 --- a/src/components/Automation2FlowEditor/categoryIcons.tsx +++ b/src/components/Automation2FlowEditor/categoryIcons.tsx @@ -3,14 +3,16 @@ */ import React from 'react'; -import { FaPlay, FaCodeBranch, FaDatabase, FaPlug, FaUser } from 'react-icons/fa'; +import { FaPlay, FaCodeBranch, FaDatabase, FaPlug, FaUser, FaRobot, FaEnvelope, FaCloud } from 'react-icons/fa'; export const CATEGORY_ICONS: Record = { trigger: , input: , flow: , data: , - io: , + ai: , + email: , + sharepoint: , human: , }; diff --git a/src/components/Automation2FlowEditor/configs/AiNodeConfig.tsx b/src/components/Automation2FlowEditor/configs/AiNodeConfig.tsx new file mode 100644 index 0000000..cc7b76c --- /dev/null +++ b/src/components/Automation2FlowEditor/configs/AiNodeConfig.tsx @@ -0,0 +1,68 @@ +/** + * 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' ? ( +