diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/ContextAssignmentsEditor.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/ContextAssignmentsEditor.tsx new file mode 100644 index 0000000..da2c0ca --- /dev/null +++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/ContextAssignmentsEditor.tsx @@ -0,0 +1,372 @@ +/** + * One place to configure context.setContext rows: target key, then either + * upstream picker, a fixed literal, or a human task. + */ + +import React from 'react'; +import { useLanguage } from '../../../../providers/language/LanguageContext'; +import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext'; +import { DataPicker } from '../shared/DataPicker'; +import { isRef, isSystemVar, type DataRef, type SystemVarRef } from '../shared/dataRef'; +import type { FieldRendererProps } from './index'; + +type ValueSource = 'pickUpstream' | 'literal' | 'humanTask'; + +export interface ContextAssignmentRow { + contextKey: string; + valueSource: ValueSource; + /** Single resolved ref (server resolves { type: ref } to a value). */ + upstreamRef?: DataRef | SystemVarRef | null; + /** Optional dotted path under the picked value, or under the wire payload (expert). */ + sourcePath?: string; + literal?: string; + taskTitle?: string; + taskDescription?: string; + mode?: 'set' | 'setIfEmpty' | 'append' | 'increment'; + valueType?: string; +} + +function defaultRow(): ContextAssignmentRow { + return { + contextKey: '', + valueSource: 'literal', + literal: '', + mode: 'set', + valueType: 'str', + }; +} + +function legacyEntryToRow( + e: Record, + globalPick: unknown, +): ContextAssignmentRow { + const am = String(e.assignmentMode || 'direct'); + let valueSource: ValueSource = 'literal'; + if (am === 'fromUpstream') valueSource = 'pickUpstream'; + else if (am === 'humanTask') valueSource = 'humanTask'; + + const sourcePathStr = typeof e.sourcePath === 'string' ? e.sourcePath : ''; + let upstream: DataRef | SystemVarRef | undefined; + if (isRef(e.upstreamRef) || isSystemVar(e.upstreamRef)) { + upstream = e.upstreamRef as DataRef | SystemVarRef; + } else if ( + am === 'fromUpstream' && + !sourcePathStr.trim() && + (isRef(globalPick) || isSystemVar(globalPick)) + ) { + upstream = globalPick as DataRef | SystemVarRef; + } + + return { + contextKey: typeof e.contextKey === 'string' ? e.contextKey : typeof e.key === 'string' ? e.key : '', + valueSource, + upstreamRef: upstream, + sourcePath: sourcePathStr, + literal: e.literal != null ? String(e.literal) : e.value != null ? String(e.value) : '', + taskTitle: typeof e.taskTitle === 'string' ? e.taskTitle : '', + taskDescription: typeof e.taskDescription === 'string' ? e.taskDescription : '', + mode: (e.mode as ContextAssignmentRow['mode']) || 'set', + valueType: typeof e.valueType === 'string' ? e.valueType : typeof e.type === 'string' ? e.type : 'str', + }; +} + +function normalizeRows(raw: unknown, allParams?: Record): ContextAssignmentRow[] { + if (Array.isArray(raw) && raw.length > 0) { + return raw.map((r) => { + if (!r || typeof r !== 'object') return defaultRow(); + const o = r as Record; + let valueSource = o.valueSource as ValueSource | undefined; + if (!valueSource && o.assignmentMode === 'fromUpstream') valueSource = 'pickUpstream'; + else if (!valueSource && o.assignmentMode === 'humanTask') valueSource = 'humanTask'; + else if (!valueSource) valueSource = 'literal'; + return { + contextKey: typeof o.contextKey === 'string' ? o.contextKey : typeof o.key === 'string' ? o.key : '', + valueSource, + upstreamRef: (isRef(o.upstreamRef) || isSystemVar(o.upstreamRef) ? o.upstreamRef : undefined) as + | DataRef + | SystemVarRef + | undefined, + sourcePath: typeof o.sourcePath === 'string' ? o.sourcePath : '', + literal: o.literal != null ? String(o.literal) : o.value != null ? String(o.value) : '', + taskTitle: typeof o.taskTitle === 'string' ? o.taskTitle : '', + taskDescription: typeof o.taskDescription === 'string' ? o.taskDescription : '', + mode: (o.mode as ContextAssignmentRow['mode']) || 'set', + valueType: typeof o.valueType === 'string' ? o.valueType : typeof o.type === 'string' ? o.type : 'str', + }; + }); + } + + const g = allParams; + if (g && Array.isArray(g.entries) && g.entries.length > 0) { + const globalPick = g.upstreamPick; + return (g.entries as Record[]).map((e) => legacyEntryToRow(e, globalPick)); + } + if (g) { + const tk = String(g.targetKey || '').trim(); + const globalPick = g.upstreamPick; + if ( + tk && + globalPick !== undefined && + globalPick !== null && + !(typeof globalPick === 'string' && !globalPick.trim()) && + !(typeof globalPick === 'object' && globalPick !== null && Object.keys(globalPick).length === 0) + ) { + const ups = + isRef(globalPick) || isSystemVar(globalPick) ? (globalPick as DataRef | SystemVarRef) : undefined; + return [ + { + contextKey: tk, + valueSource: 'pickUpstream' as const, + upstreamRef: ups, + sourcePath: '', + literal: '', + taskTitle: '', + taskDescription: '', + mode: 'set', + valueType: 'str', + }, + ]; + } + } + + return [defaultRow()]; +} + +const MODES: Array<{ id: NonNullable; labelDe: string }> = [ + { id: 'set', labelDe: 'setzen' }, + { id: 'setIfEmpty', labelDe: 'setzen wenn leer' }, + { id: 'append', labelDe: 'anhängen' }, + { id: 'increment', labelDe: 'addieren' }, +]; + +const TYPES = ['str', 'int', 'float', 'bool', 'object', 'list'] as const; + +const ROW_BOX: React.CSSProperties = { + border: '1px solid #ddd', + borderRadius: 6, + padding: 8, + marginBottom: 8, + background: '#fafafa', +}; + +const CHIP_STYLE: React.CSSProperties = { + display: 'flex', + alignItems: 'center', + gap: 6, + padding: '4px 8px', + background: '#eaf6e8', + border: '1px solid #5cb85c', + borderRadius: 4, + fontSize: 12, + marginTop: 4, +}; + +const REMOVE_BTN: React.CSSProperties = { + padding: '0 6px', + border: '1px solid #5cb85c', + borderRadius: 3, + background: '#fff', + color: '#3c763d', + cursor: 'pointer', + fontSize: 11, + marginLeft: 'auto', +}; + +export const ContextAssignmentsEditor: React.FC = ({ param, value, onChange, allParams }) => { + const { t } = useLanguage(); + const dataFlow = useAutomation2DataFlow(); + const rows = normalizeRows(value, allParams); + const [pickerRow, setPickerRow] = React.useState(null); + + const sourceIds = dataFlow?.getAvailableSourceIds() ?? []; + const hasSources = sourceIds.some((id) => { + const n = dataFlow?.nodes.find((x) => x.id === id); + return n?.type !== 'trigger.manual'; + }); + + const setRows = (next: ContextAssignmentRow[]) => { + onChange(next.length ? next : [defaultRow()]); + }; + + const setRow = (idx: number, patch: Partial) => { + const next = [...rows]; + next[idx] = { ...next[idx], ...patch }; + setRows(next); + }; + + const addRow = () => setRows([...rows, defaultRow()]); + + const removeRow = (idx: number) => { + if (rows.length <= 1) { + onChange([defaultRow()]); + return; + } + setRows(rows.filter((_, i) => i !== idx)); + }; + + const labelForRef = (ref: DataRef | SystemVarRef): string => { + if (isSystemVar(ref)) { + return t('System') + `: ${ref.variable}`; + } + const nodeLabel = + dataFlow?.getNodeLabel( + dataFlow.nodes.find((n) => n.id === ref.nodeId) ?? { id: ref.nodeId }, + ) ?? ref.nodeId; + const pathStr = ref.path.length > 0 ? ref.path.map(String).join('.') : null; + return pathStr ? `${nodeLabel} → ${pathStr}` : nodeLabel; + }; + + const onPickRef = (idx: number, picked: DataRef | SystemVarRef) => { + if (!isRef(picked) && !isSystemVar(picked)) return; + setRow(idx, { upstreamRef: picked }); + setPickerRow(null); + }; + + return ( +
+ + + {rows.map((row, idx) => ( +
+
+ setRow(idx, { contextKey: e.target.value })} + style={{ flex: '2 1 140px', minWidth: 120, padding: '4px 6px', borderRadius: 4, border: '1px solid #ccc' }} + /> + + + + +
+ + {row.valueSource === 'pickUpstream' && ( +
+ {row.upstreamRef && (isRef(row.upstreamRef) || isSystemVar(row.upstreamRef)) && ( +
+ {labelForRef(row.upstreamRef)} + +
+ )} + + setRow(idx, { sourcePath: e.target.value })} + style={{ width: '100%', marginTop: 6, padding: '4px 6px', borderRadius: 4, border: '1px dashed #aaa', fontSize: 11 }} + /> +
+ )} + + {row.valueSource === 'literal' && ( + setRow(idx, { literal: e.target.value })} + style={{ width: '100%', padding: '4px 6px', borderRadius: 4, border: '1px solid #ccc' }} + /> + )} + + {row.valueSource === 'humanTask' && ( +
+ setRow(idx, { taskTitle: e.target.value })} + style={{ width: '100%', padding: '4px 6px', borderRadius: 4, border: '1px solid #ccc' }} + /> +