diff --git a/src/components/FlowEditor/editor/Automation2FlowEditor.module.css b/src/components/FlowEditor/editor/Automation2FlowEditor.module.css index 5f1e938..67a4261 100644 --- a/src/components/FlowEditor/editor/Automation2FlowEditor.module.css +++ b/src/components/FlowEditor/editor/Automation2FlowEditor.module.css @@ -256,6 +256,225 @@ background: var(--bg-primary, #fff); } +/* Toolbar: context (load + name) is fluid with ellipsis; actions stay right-aligned. */ +.canvasHeaderRow { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: center; + width: 100%; +} + +@media (max-width: 900px) { + .canvasHeaderRow { + grid-template-columns: 1fr; + } +} + +.canvasHeaderContext { + display: flex; + align-items: center; + gap: 0.5rem; + min-width: 0; + flex: 1; +} + +/* Closed setNameValue(e.target.value)} - onBlur={_commitNameEdit} - onKeyDown={(e) => { if (e.key === 'Enter') _commitNameEdit(); if (e.key === 'Escape') setEditingName(false); }} - style={{ padding: '0.25rem 0.4rem', fontSize: '0.95rem', fontWeight: 600, border: '1px solid var(--primary-color, #007bff)', borderRadius: 4, outline: 'none', minWidth: 140, maxWidth: 300 }} - /> - ) : ( -

- {currentWorkflow.label} -

- ) - ) : ( -

- {t('Neuer Workflow')} -

- )} - {onWorkflowSettings && ( - - - {newMenuOpen && ( -
- - {onNewFromTemplate && ( - - )} -
)} - - - {onAutoLayout && ( - - )} - - {/* Save as template */} - {currentWorkflowId && onSaveAsTemplate && ( -
- - {templateMenuOpen && ( -
- {(['user', 'instance', 'mandate'] as const).map((s) => ( +
+
+
+ + +
+ {newMenuOpen && ( +
+ + {onNewFromTemplate && ( - ))} + )}
)}
- )} - - - {onToggleChat && ( - - )} + + {onAutoLayout && ( + + )} + + {currentWorkflowId && onSaveAsTemplate && ( +
+ + {templateMenuOpen && ( +
+ {(['user', 'instance', 'mandate'] as const).map((s) => ( + + ))} +
+ )} +
+ )} + + + {onToggleChat && ( + + )} + {_isSysAdmin && onVerboseSchemaChange && ( + + )} +
- {/* Version Selector */} {currentWorkflowId && versions && versions.length > 0 && ( -
- {t('Version:')} +
+ {t('Version:')} + * + * The bound value is a plain `` string so backend adapters can keep + * using `featureInstanceId` lookups unchanged. Type stays + * `FeatureInstanceRef[]` on the parameter so DataPicker / RequiredAttributePicker + * filter correctly. + */ + +import React from 'react'; +import { useLanguage } from '../../../../providers/language/LanguageContext'; +import type { FieldRendererProps } from './index'; + +type FeatureInstanceOption = { id: string; label: string }; + +export const FeatureInstancePicker: React.FC = ({ + param, + value, + onChange, + instanceId, + request, +}) => { + const { t } = useLanguage(); + const featureCode = + (param.frontendOptions?.featureCode as string | undefined) || undefined; + const [instances, setInstances] = React.useState([]); + const [loading, setLoading] = React.useState(false); + const [loadError, setLoadError] = React.useState(null); + const autoSingleRef = React.useRef(false); + + React.useEffect(() => { + if (!instanceId || !request || !featureCode) return; + setLoading(true); + setLoadError(null); + request({ + url: `/api/workflows/${instanceId}/options/feature.instance?featureCode=${encodeURIComponent(featureCode)}`, + method: 'get', + }) + .then((res: unknown) => { + const data = res as { options?: Array<{ value: string; label: string }> }; + setInstances((data?.options || []).map((o) => ({ id: o.value, label: o.label }))); + }) + .catch((err: unknown) => { + console.error('FeatureInstancePicker: failed to load instances', err); + setInstances([]); + setLoadError(err instanceof Error ? err.message : String(err)); + }) + .finally(() => setLoading(false)); + }, [instanceId, request, featureCode]); + + React.useEffect(() => { + if (instances.length !== 1 || autoSingleRef.current) return; + if (value !== '' && value !== undefined && value !== null) return; + autoSingleRef.current = true; + onChange(instances[0].id); + }, [instances, value, onChange]); + + const strVal = typeof value === 'string' ? value : ''; + const codeLabel = featureCode ?? t('Feature'); + + return ( +
+ + {loading && ( +
{t('Lade…')}
+ )} + {!loading && instances.length === 0 && !loadError && ( +
+ {t('Keine {code}-Instanz im aktiven Mandanten — bitte in der Admin-Konsole anlegen.', { code: codeLabel })} +
+ )} + {!loading && instances.length === 1 && ( +
+ {instances[0].label} +
+ )} + {!loading && instances.length > 1 && ( + + )} + {loadError && ( +
+ {t('Mandanten-Liste konnte nicht geladen werden')} +
+ )} +
+ ); +}; + +export default FeatureInstancePicker; diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx index a910ca6..fb33a03 100644 --- a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx +++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx @@ -31,6 +31,7 @@ import { toApiGraph } from '../shared/graphUtils'; import { postUpstreamPaths } from '../../../../api/workflowApi'; import type { CanvasNode } from '../../editor/FlowCanvas'; import { DataRefRenderer } from './DataRefRenderer'; +import { FeatureInstancePicker } from './FeatureInstancePicker'; const TextInput: React.FC = ({ param, value, onChange }) => (
@@ -755,6 +756,7 @@ export const FRONTEND_TYPE_RENDERERS: Record = { hidden: HiddenInput, dataRef: DataRefRenderer, userConnection: ConnectionPicker, + featureInstance: FeatureInstancePicker, sharepointFolder: SharepointPathPicker, sharepointFile: SharepointPathPicker, clickupList: FolderPicker, diff --git a/src/components/FlowEditor/nodes/shared/DataPicker.tsx b/src/components/FlowEditor/nodes/shared/DataPicker.tsx index c1ea657..4edadc7 100644 --- a/src/components/FlowEditor/nodes/shared/DataPicker.tsx +++ b/src/components/FlowEditor/nodes/shared/DataPicker.tsx @@ -6,6 +6,7 @@ */ import React, { useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; import { createRef, createSystemVar, type DataRef, type SystemVarRef, isCompatible } from './dataRef'; import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext'; import type { GraphDefinedSchemaRef, NodeType, PortSchema } from '../../../../api/workflowApi'; @@ -254,33 +255,35 @@ export const DataPicker: React.FC = ({ open, onClose(); }; - return ( -
-
e.stopPropagation()}> + const _dialog = ( +
e.key === 'Escape' && onClose()} + role="presentation" + > +
e.stopPropagation()} + role="dialog" + aria-modal="true" + aria-labelledby="automation2DataPickerTitle" + >
-

+

{t('Datenquelle wählen')} {expectedParamType && ( {expectedParamType} )}

-
+
{expectedParamType && ( -