From 587dad5cf92a090ef5e6328e949e5f725c9a78a7 Mon Sep 17 00:00:00 2001 From: Ida Date: Thu, 14 May 2026 13:06:31 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20extract=20content=20node=20angepasst=20?= =?UTF-8?q?f=C3=BCr=20mehr=20optionen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FlowEditor/editor/NodeConfigPanel.tsx | 392 ++++++++++++++---- .../nodes/frontendTypeRenderers/index.tsx | 120 +++++- 2 files changed, 429 insertions(+), 83 deletions(-) diff --git a/src/components/FlowEditor/editor/NodeConfigPanel.tsx b/src/components/FlowEditor/editor/NodeConfigPanel.tsx index ebd30fb..c60600b 100644 --- a/src/components/FlowEditor/editor/NodeConfigPanel.tsx +++ b/src/components/FlowEditor/editor/NodeConfigPanel.tsx @@ -15,6 +15,82 @@ import { useAutomation2DataFlow } from '../context/Automation2DataFlowContext'; import styles from './Automation2FlowEditor.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; +import { AccordionList } from '../../UiComponents/AccordionList'; +import type { AccordionListItem } from '../../UiComponents/AccordionList'; + +const CONTEXT_EXTRACT_CONTENT_NODE_TYPE = 'context.extractContent'; +const CONTEXT_EXTRACT_CHUNK_PARAM_NAMES = ['chunkSizeUnit', 'chunkSize', 'chunkOverlap'] as const; +const CONTEXT_EXTRACT_CHUNK_SET = new Set(CONTEXT_EXTRACT_CHUNK_PARAM_NAMES); + +/** Optional params use stored value only (unset ⇒ no chip). Required uses schema default as fallback. */ +export function workflowParamUiValue(stored: Record, param: NodeTypeParameter): unknown { + const raw = stored[param.name]; + if (param.required) { + return raw !== undefined && raw !== null ? raw : param.default; + } + return raw; +} + +function effectiveSchemaParamString(name: string, currentParams: Record, nt: NodeType): string { + const raw = currentParams[name]; + const s = raw !== undefined && raw !== null ? String(raw) : ''; + if (s !== '') return s; + const meta = nt.parameters?.find((p) => p.name === name); + const d = meta?.default; + return d !== undefined && d !== null ? String(d) : ''; +} + +function accordionExtractParamTitle(param: NodeTypeParameter, t: (key: string) => string): React.ReactNode { + return ( + + {param.required ? ( + + * + + ) : null} + {param.name} + + ); +} + +function verboseSchemaTypeBadge( + verboseSchema: boolean, + param: NodeTypeParameter, + t: (key: string) => string, +): React.ReactElement | null { + if (!verboseSchema || !param.type) return null; + return ( +
+ + {param.type} + +
+ ); +} interface NodeConfigPanelProps { node: CanvasNode | null; @@ -30,6 +106,35 @@ interface NodeConfigPanelProps { verboseSchema?: boolean; } +/** When ``frontendOptions.dependsOn`` and ``frontendOptions.showWhen`` are set + * (same convention as trustee / gateway nodeAdapter ``visibleWhen``), hide the + * parameter unless the referenced parameter's effective value matches. + */ +export function parameterVisibleForFrontendOptions( + param: NodeTypeParameter, + params: Record, + nodeType: NodeType, +): boolean { + const fo = param.frontendOptions; + if (!fo || typeof fo !== 'object') return true; + const dependsOnRaw = fo.dependsOn as unknown; + const showWhenRaw = fo.showWhen as unknown; + if (typeof dependsOnRaw !== 'string' || dependsOnRaw.length === 0 || showWhenRaw === undefined || showWhenRaw === null) { + return true; + } + const depMeta = nodeType.parameters?.find((p) => p.name === dependsOnRaw); + const rawSibling = params[dependsOnRaw]; + const siblingValue = + rawSibling !== undefined && rawSibling !== null ? String(rawSibling) : ''; + const fallback = + depMeta?.default !== undefined && depMeta?.default !== null ? String(depMeta.default) : ''; + const effective = siblingValue !== '' ? siblingValue : fallback; + const allowed: string[] = Array.isArray(showWhenRaw) + ? showWhenRaw.map((x) => String(x)) + : [String(showWhenRaw)]; + return allowed.includes(effective); +} + export const NodeConfigPanel: React.FC = ({ node, nodeType, language, @@ -62,7 +167,12 @@ export const NodeConfigPanel: React.FC = ({ node, const updateParam = useCallback( (key: string, value: unknown) => { setParams((prev) => { - const next = { ...prev, [key]: value }; + const next = { ...prev }; + if (value === undefined) { + delete next[key]; + } else { + next[key] = value; + } const id = nodeIdRef.current; if (id) { if (notifyParentTimeoutRef.current != null) { @@ -135,6 +245,139 @@ export const NodeConfigPanel: React.FC = ({ node, .join('\n'); }, [requiredErrors, nodeType, language]); + const extractContentAccordionItems = useMemo((): AccordionListItem[] | null => { + if (!node || !nodeType || node.type !== CONTEXT_EXTRACT_CONTENT_NODE_TYPE) return null; + + const byName = new Map((nodeType.parameters ?? []).map((p) => [p.name, p])); + const out: AccordionListItem[] = []; + + for (const param of sortedParameters) { + if (param.frontendType === 'hidden') continue; + if (CONTEXT_EXTRACT_CHUNK_SET.has(param.name)) continue; + if (!parameterVisibleForFrontendOptions(param, params, nodeType)) continue; + + const usePicker = _shouldUseRequiredPicker(param); + if (usePicker) { + out.push({ + id: param.name, + title: accordionExtractParamTitle(param, t), + children: ( +
+ {verboseSchemaTypeBadge(verboseSchema, param, t)} + updateParam(param.name, val)} + /> +
+ ), + }); + continue; + } + + const frontendType = param.frontendType || 'text'; + const Renderer = FRONTEND_TYPE_RENDERERS[frontendType] ?? FRONTEND_TYPE_RENDERERS.text; + + if (param.name === 'outputMode') { + const chunksNested = effectiveSchemaParamString('outputMode', params, nodeType) === 'chunks'; + out.push({ + id: param.name, + title: accordionExtractParamTitle(param, t), + children: ( +
+ {verboseSchemaTypeBadge(verboseSchema, param, t)} + updateParam(param.name, val)} + allParams={params} + instanceId={instanceId} + request={request} + nodeType={node.type} + onPatchParams={patchParams} + hideAccordionTitle + /> + {chunksNested ? ( +
+ + key={`extract-chunks-${node.id}`} + defaultOpenId={null} + items={CONTEXT_EXTRACT_CHUNK_PARAM_NAMES.map((chunkName): AccordionListItem => { + const cp = byName.get(chunkName); + if (!cp) { + return { id: chunkName, title: chunkName, children: <> }; + } + const ft = cp.frontendType || 'text'; + const ChunkRenderer = FRONTEND_TYPE_RENDERERS[ft] ?? FRONTEND_TYPE_RENDERERS.text; + return { + id: chunkName, + title: accordionExtractParamTitle(cp, t), + children: ( +
+ {verboseSchemaTypeBadge(verboseSchema, cp, t)} + updateParam(cp.name, val)} + allParams={params} + instanceId={instanceId} + request={request} + nodeType={node.type} + onPatchParams={patchParams} + hideAccordionTitle + /> +
+ ), + }; + })} + /> +
+ ) : null} +
+ ), + }); + continue; + } + + out.push({ + id: param.name, + title: accordionExtractParamTitle(param, t), + children: ( +
+ {verboseSchemaTypeBadge(verboseSchema, param, t)} + updateParam(param.name, val)} + allParams={params} + instanceId={instanceId} + request={request} + nodeType={node.type} + onPatchParams={patchParams} + hideAccordionTitle + /> +
+ ), + }); + } + + return out; + }, [ + sortedParameters, + params, + nodeType, + language, + node?.id, + node?.type, + verboseSchema, + instanceId, + request, + patchParams, + updateParam, + t, + ]); + if (!node || !nodeType) return null; const isTrigger = node.type.startsWith('trigger.'); @@ -239,79 +482,88 @@ export const NodeConfigPanel: React.FC = ({ node, {requiredErrors.map((e) => e.paramLabel).join(', ')} )} - {parameters.map((param: NodeTypeParameter) => { - // Safety net: hidden params have no UI footprint at all — no row, - // no required-mark, no type-badge. Their value is system-set. - if (param.frontendType === 'hidden') return null; - const useRequiredPicker = _shouldUseRequiredPicker(param); - if (useRequiredPicker) { + {extractContentAccordionItems !== null ? ( + + key={`${node.id}-extract-accordion`} + defaultOpenId={null} + items={extractContentAccordionItems} + /> + ) : ( + parameters.map((param: NodeTypeParameter) => { + // Safety net: hidden params have no UI footprint at all — no row, + // no required-mark, no type-badge. Their value is system-set. + if (param.frontendType === 'hidden') return null; + if (!parameterVisibleForFrontendOptions(param, params, nodeType)) return null; + const useRequiredPicker = _shouldUseRequiredPicker(param); + if (useRequiredPicker) { + return ( +
+ updateParam(param.name, val)} + /> +
+ ); + } + const frontendType = param.frontendType || 'text'; + const Renderer = FRONTEND_TYPE_RENDERERS[frontendType] ?? FRONTEND_TYPE_RENDERERS.text; return ( -
- updateParam(param.name, val)} +
+
+ {param.required && ( + + * + + )} + {verboseSchema && param.type && ( + + {param.type} + + )} +
+ updateParam(param.name, val)} + allParams={params} + instanceId={instanceId} + request={request} + nodeType={node.type} + onPatchParams={patchParams} />
); - } - const frontendType = param.frontendType || 'text'; - const Renderer = FRONTEND_TYPE_RENDERERS[frontendType] ?? FRONTEND_TYPE_RENDERERS.text; - return ( -
-
- {param.required && ( - - * - - )} - {verboseSchema && param.type && ( - - {param.type} - - )} -
- updateParam(param.name, val)} - allParams={params} - instanceId={instanceId} - request={request} - nodeType={node.type} - onPatchParams={patchParams} - /> -
- ); - })} + }) + )}
); }; diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx index 3c5e36f..0b20625 100644 --- a/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx +++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/index.tsx @@ -25,6 +25,8 @@ export interface FieldRendererProps { nodeType?: string; /** Atomically merge several parameter keys (e.g. cron + schedule). */ onPatchParams?: (patch: Record) => void; + /** Hide the prominent ``param.name`` line (e.g. Accordion header already shows it). */ + hideAccordionTitle?: boolean; } export type FieldRendererComponent = ComponentType; @@ -135,25 +137,117 @@ function _normalizedSelectOptions(raw: unknown): Array<{ value: string; label: s return out; } -const SelectInput: React.FC = ({ param, value, onChange }) => { +const SelectInput: React.FC = ({ param, value, onChange, hideAccordionTitle }) => { + const { t } = useLanguage(); const options = _normalizedSelectOptions( param.frontendOptions?.options ?? param.options ?? [] ); + const allowClear = !param.required; + const current = value === undefined || value === null || value === '' ? '' : String(value); + const groupId = `select-segment-${param.name.replace(/[^a-zA-Z0-9_-]/g, '_')}`; + const titleId = `${groupId}-title`; + const descId = `${groupId}-desc`; + const showNameLine = !hideAccordionTitle; + const labelledBy = showNameLine + ? param.description + ? `${titleId} ${descId}` + : titleId + : param.description + ? descId + : undefined; return (
- - + {options.map((opt) => { + const selected = current === opt.value; + return ( + + ); + })} +
); };