continued testing and improvement
This commit is contained in:
parent
b5084c028e
commit
60ff00802c
7 changed files with 340 additions and 297 deletions
|
|
@ -45,7 +45,7 @@ import { NodeSidebar } from './NodeSidebar';
|
|||
import { CanvasHeader } from './CanvasHeader';
|
||||
import { TemplatePicker } from './TemplatePicker';
|
||||
import { getCategoryIcon } from '../nodes/shared/utils';
|
||||
import { fromApiGraph, toApiGraph } from '../nodes/shared/graphUtils';
|
||||
import { fromApiGraph, toApiGraph, switchOutputCountFromCases, trimConnectionsForSwitchOutputs } from '../nodes/shared/graphUtils';
|
||||
import { buildNodeOutputsPreview, setPortTypeCatalog as setRegistryCatalog } from '../nodes/shared/outputPreviewRegistry';
|
||||
import { findGraphErrors } from '../nodes/shared/paramValidation';
|
||||
import { getLabel as getParamLabel } from '../nodes/shared/utils';
|
||||
|
|
@ -497,32 +497,40 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
}, [applyGraphWithSync, t]);
|
||||
|
||||
const handleNodeParametersChange = useCallback((nodeId: string, parameters: Record<string, unknown>) => {
|
||||
setCanvasNodes((prev) =>
|
||||
prev.map((n) => {
|
||||
setCanvasNodes((prev) => {
|
||||
const nextNodes = prev.map((n) => {
|
||||
if (n.id !== nodeId) return n;
|
||||
const next = { ...n, parameters };
|
||||
if (n.type === 'flow.switch' && 'cases' in parameters) {
|
||||
const cases = (parameters.cases as unknown[]) ?? [];
|
||||
next.outputs = Math.max(1, cases.length);
|
||||
const newCount = switchOutputCountFromCases(parameters.cases);
|
||||
next.outputs = newCount;
|
||||
setCanvasConnections((conns) =>
|
||||
trimConnectionsForSwitchOutputs(conns, nodeId, n.inputs, newCount)
|
||||
);
|
||||
}
|
||||
return next;
|
||||
})
|
||||
);
|
||||
});
|
||||
return nextNodes;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleMergeNodeParameters = useCallback((nodeId: string, patch: Record<string, unknown>) => {
|
||||
setCanvasNodes((prev) =>
|
||||
prev.map((n) => {
|
||||
setCanvasNodes((prev) => {
|
||||
const nextNodes = prev.map((n) => {
|
||||
if (n.id !== nodeId) return n;
|
||||
const merged = { ...(n.parameters ?? {}), ...patch };
|
||||
const next = { ...n, parameters: merged };
|
||||
if (n.type === 'flow.switch' && 'cases' in merged) {
|
||||
const cases = (merged.cases as unknown[]) ?? [];
|
||||
next.outputs = Math.max(1, cases.length);
|
||||
const newCount = switchOutputCountFromCases(merged.cases);
|
||||
next.outputs = newCount;
|
||||
setCanvasConnections((conns) =>
|
||||
trimConnectionsForSwitchOutputs(conns, nodeId, n.inputs, newCount)
|
||||
);
|
||||
}
|
||||
return next;
|
||||
})
|
||||
);
|
||||
});
|
||||
return nextNodes;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleNodeUpdate = useCallback(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import styles from './Automation2FlowEditor.module.css';
|
|||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
import { AiBadge } from '../nodes/shared/AiBadge';
|
||||
import { switchOutputLabel } from '../nodes/shared/graphUtils';
|
||||
|
||||
export interface CanvasNode {
|
||||
id: string;
|
||||
|
|
@ -1960,7 +1961,13 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
|
|||
(!selectedConnectionId ? wireTargetOk : true)) ||
|
||||
(!!selectedConnectionId && !isOutput && (!used || isCurrentTargetOfSelection));
|
||||
const nt = nodeTypeMap[node.type];
|
||||
const outputLabel = isOutput && nt?.outputLabels ? nt.outputLabels[index - node.inputs] : undefined;
|
||||
const outputIndex = index - node.inputs;
|
||||
const outputLabel =
|
||||
isOutput && node.type === 'flow.switch'
|
||||
? switchOutputLabel(node, outputIndex, t)
|
||||
: isOutput && nt?.outputLabels
|
||||
? nt.outputLabels[outputIndex]
|
||||
: undefined;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,274 @@
|
|||
/**
|
||||
* Backend-driven case list for flow.switch (depends on value dataRef).
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { FieldRendererProps } from './index';
|
||||
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||
import { isRef, type DataRef } from '../shared/dataRef';
|
||||
import { toApiGraph } from '../shared/graphUtils';
|
||||
import { fetchConditionMeta, type ConditionOperatorDef } from '../../../../api/workflowApi';
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
export interface SwitchCase {
|
||||
operator: string;
|
||||
value?: string | number | boolean;
|
||||
}
|
||||
|
||||
function normalizeCase(c: unknown): SwitchCase {
|
||||
if (c && typeof c === 'object' && 'operator' in (c as object)) {
|
||||
const o = c as SwitchCase;
|
||||
const v = o.value;
|
||||
const safeValue: string | number | boolean | undefined =
|
||||
typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' ? v : undefined;
|
||||
return { operator: o.operator ?? 'eq', value: safeValue };
|
||||
}
|
||||
const fallbackValue: string | number | boolean | undefined =
|
||||
typeof c === 'string' || typeof c === 'number' || typeof c === 'boolean' ? c : undefined;
|
||||
return { operator: 'eq', value: fallbackValue };
|
||||
}
|
||||
|
||||
function operatorsFromCatalog(
|
||||
catalog: Record<string, ConditionOperatorDef[]> | undefined,
|
||||
valueKind: string
|
||||
): ConditionOperatorDef[] {
|
||||
if (!catalog) return [];
|
||||
return catalog[valueKind] ?? catalog.unknown ?? [];
|
||||
}
|
||||
|
||||
function sanitizeCases(cases: SwitchCase[], operators: ConditionOperatorDef[]): SwitchCase[] {
|
||||
if (!operators.length) return cases;
|
||||
return cases.map((c) => {
|
||||
const op = operators.find((o) => o.id === c.operator) ?? operators[0];
|
||||
return {
|
||||
operator: op.id,
|
||||
value: op.needsValue ? c.value ?? '' : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function CaseValueInput({
|
||||
caseItem,
|
||||
opDef,
|
||||
valueKind,
|
||||
onChange,
|
||||
t,
|
||||
}: {
|
||||
caseItem: SwitchCase;
|
||||
opDef: ConditionOperatorDef | undefined;
|
||||
valueKind: string;
|
||||
onChange: (v: string | number) => void;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
const valueInput = opDef?.valueInput;
|
||||
const val = caseItem.value;
|
||||
|
||||
if (
|
||||
valueInput?.kind === 'select' ||
|
||||
valueInput?.kind === 'contentType' ||
|
||||
valueInput?.kind === 'outputMode' ||
|
||||
valueInput?.kind === 'language' ||
|
||||
valueInput?.kind === 'mime'
|
||||
) {
|
||||
return (
|
||||
<select
|
||||
value={String(val ?? '')}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
style={{ flex: 2, padding: '4px 6px', borderRadius: 4, border: '1px solid #ccc' }}
|
||||
>
|
||||
<option value="">{t('— wählen —')}</option>
|
||||
{(valueInput.options ?? []).map((opt) => (
|
||||
<option key={opt} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
type={
|
||||
valueInput?.kind === 'number' || valueKind === 'number'
|
||||
? 'number'
|
||||
: valueInput?.kind === 'date'
|
||||
? 'date'
|
||||
: 'text'
|
||||
}
|
||||
value={String(val ?? '')}
|
||||
onChange={(e) =>
|
||||
onChange(
|
||||
valueInput?.kind === 'number' || valueKind === 'number'
|
||||
? parseFloat(e.target.value) || 0
|
||||
: e.target.value
|
||||
)
|
||||
}
|
||||
placeholder={valueInput?.kind === 'regex' ? t('Regex-Muster') : t('Wert')}
|
||||
style={{ flex: 2, padding: '4px 6px', borderRadius: 4, border: '1px solid #ccc' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const CaseListEditor: React.FC<FieldRendererProps> = ({
|
||||
param,
|
||||
value,
|
||||
onChange,
|
||||
allParams,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
const dependsOn =
|
||||
param.frontendOptions && typeof param.frontendOptions === 'object'
|
||||
? String((param.frontendOptions as Record<string, unknown>).dependsOn ?? 'value')
|
||||
: 'value';
|
||||
|
||||
const valueParam = allParams?.[dependsOn];
|
||||
const ref: DataRef | null = isRef(valueParam) ? valueParam : null;
|
||||
|
||||
const rawCases = Array.isArray(value) ? value : [];
|
||||
const cases: SwitchCase[] = rawCases.map(normalizeCase);
|
||||
|
||||
const [operators, setOperators] = React.useState<ConditionOperatorDef[]>([]);
|
||||
const [valueKind, setValueKind] = React.useState('unknown');
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const catalog = dataFlow?.conditionOperatorCatalog;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!ref) {
|
||||
const ops = operatorsFromCatalog(catalog, 'unknown');
|
||||
setOperators(ops);
|
||||
setValueKind('unknown');
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const applyMeta = (vk: string, ops: ConditionOperatorDef[]) => {
|
||||
if (cancelled) return;
|
||||
setValueKind(vk);
|
||||
setOperators(ops);
|
||||
if (cases.length > 0) {
|
||||
const next = sanitizeCases(cases, ops);
|
||||
if (JSON.stringify(next) !== JSON.stringify(cases)) {
|
||||
onChange(next);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (dataFlow?.instanceId && dataFlow.request) {
|
||||
setLoading(true);
|
||||
fetchConditionMeta(dataFlow.request, dataFlow.instanceId, {
|
||||
graph: toApiGraph(dataFlow.nodes, dataFlow.connections),
|
||||
nodeId: dataFlow.currentNodeId,
|
||||
ref: { type: 'ref', nodeId: ref.nodeId, path: ref.path },
|
||||
})
|
||||
.then((meta) => applyMeta(meta.valueKind, meta.operators))
|
||||
.catch(() => applyMeta('unknown', operatorsFromCatalog(catalog, 'unknown')))
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
} else {
|
||||
applyMeta('unknown', operatorsFromCatalog(catalog, 'unknown'));
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ref?.nodeId, JSON.stringify(ref?.path), dataFlow?.currentNodeId, catalog]);
|
||||
|
||||
const setCases = (next: SwitchCase[]) => onChange(next);
|
||||
|
||||
const addCase = () => {
|
||||
const opDef = operators[0];
|
||||
setCases([
|
||||
...cases,
|
||||
{
|
||||
operator: opDef?.id ?? 'eq',
|
||||
value: opDef?.needsValue ? (valueKind === 'number' ? 0 : '') : undefined,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
if (!ref) {
|
||||
return (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 4 }}>
|
||||
{param.description || param.name}
|
||||
</label>
|
||||
<p style={{ fontSize: 12, color: 'var(--text-secondary)', margin: 0 }}>
|
||||
{t('Zuerst einen Wert im Data Picker wählen')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 4 }}>
|
||||
{param.description || param.name}
|
||||
</label>
|
||||
{loading && (
|
||||
<div style={{ fontSize: 11, color: '#888', marginBottom: 4 }}>
|
||||
{t('Lade Operatoren…')}
|
||||
</div>
|
||||
)}
|
||||
{cases.map((c, i) => {
|
||||
const opDef = operators.find((o) => o.id === c.operator) ?? operators[0];
|
||||
const needsValue = opDef?.needsValue ?? true;
|
||||
return (
|
||||
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4, alignItems: 'center' }}>
|
||||
<select
|
||||
value={c.operator}
|
||||
onChange={(e) => {
|
||||
const op = operators.find((o) => o.id === e.target.value);
|
||||
const next = [...cases];
|
||||
next[i] = {
|
||||
operator: e.target.value,
|
||||
value: op?.needsValue ? cases[i]?.value ?? '' : undefined,
|
||||
};
|
||||
setCases(next);
|
||||
}}
|
||||
disabled={loading || operators.length === 0}
|
||||
style={{ flex: 1, padding: '4px 6px', borderRadius: 4, border: '1px solid #ccc' }}
|
||||
>
|
||||
{operators.map((o) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{needsValue && (
|
||||
<CaseValueInput
|
||||
caseItem={c}
|
||||
opDef={opDef}
|
||||
valueKind={valueKind}
|
||||
t={t}
|
||||
onChange={(v) => {
|
||||
const next = [...cases];
|
||||
next[i] = { ...next[i], value: v };
|
||||
setCases(next);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCases(cases.filter((_, j) => j !== i))}
|
||||
style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={addCase}
|
||||
disabled={loading || operators.length === 0}
|
||||
style={{ padding: '4px 10px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}
|
||||
>
|
||||
{t('Fall hinzufügen')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -54,6 +54,7 @@ import { ContextAssignmentsEditor } from './ContextAssignmentsEditor';
|
|||
import { FeatureInstancePicker } from './FeatureInstancePicker';
|
||||
import { UserFileFolderPicker } from './UserFileFolderPicker';
|
||||
import { ConditionEditor } from './ConditionEditor';
|
||||
import { CaseListEditor } from './CaseListEditor';
|
||||
import { TemplateTextareaRenderer } from './TemplateTextareaRenderer';
|
||||
import { getApiBaseUrl } from '../../../../../config/config';
|
||||
|
||||
|
|
@ -639,37 +640,6 @@ const SharepointPathPicker: React.FC<FieldRendererProps> = ({ param, value, onCh
|
|||
);
|
||||
};
|
||||
|
||||
const CaseListEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||
const { t } = useLanguage();
|
||||
const cases = Array.isArray(value) ? value : [];
|
||||
const addCase = () => onChange([...cases, { operator: 'eq', value: '' }]);
|
||||
const removeCase = (idx: number) => onChange(cases.filter((_: unknown, i: number) => i !== idx));
|
||||
const updateCase = (idx: number, field: string, val: unknown) => {
|
||||
const next = [...cases];
|
||||
next[idx] = { ...(next[idx] as Record<string, unknown>), [field]: val };
|
||||
onChange(next);
|
||||
};
|
||||
return (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
||||
{cases.map((c: Record<string, unknown>, i: number) => (
|
||||
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
|
||||
<select value={String(c.operator || 'eq')} onChange={(e) => updateCase(i, 'operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
|
||||
<option value="eq">{t('ist gleich')}</option>
|
||||
<option value="neq">{t('ungleich')}</option>
|
||||
<option value="contains">{t('enthält')}</option>
|
||||
<option value="gt">{t('größer als')}</option>
|
||||
<option value="lt">{t('kleiner als')}</option>
|
||||
</select>
|
||||
<input type="text" value={String(c.value ?? '')} onChange={(e) => updateCase(i, 'value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<button onClick={() => removeCase(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={addCase} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>{t('Fall hinzufügen')}</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FieldBuilderEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||
const { t } = useLanguage();
|
||||
const ctx = useAutomation2DataFlow();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,39 @@ import type {
|
|||
} from '../../../../api/workflowApi';
|
||||
import type { CanvasNode, CanvasConnection } from '../../editor/FlowCanvas';
|
||||
|
||||
/** Switch: one output per case plus a default (``Sonst``) port. */
|
||||
export function switchOutputCountFromCases(cases: unknown): number {
|
||||
const n = Array.isArray(cases) ? cases.length : 0;
|
||||
return Math.max(1, n + 1);
|
||||
}
|
||||
|
||||
/** Drop edges from switch output ports that no longer exist after case removal. */
|
||||
export function trimConnectionsForSwitchOutputs(
|
||||
connections: CanvasConnection[],
|
||||
nodeId: string,
|
||||
nodeInputs: number,
|
||||
outputCount: number
|
||||
): CanvasConnection[] {
|
||||
return connections.filter((c) => {
|
||||
if (c.sourceId !== nodeId) return true;
|
||||
const outIdx = c.sourceHandle - nodeInputs;
|
||||
return outIdx >= 0 && outIdx < outputCount;
|
||||
});
|
||||
}
|
||||
|
||||
export function switchOutputLabel(
|
||||
node: CanvasNode,
|
||||
outputIndex: number,
|
||||
translate: (key: string) => string
|
||||
): string | undefined {
|
||||
if (node.type !== 'flow.switch') return undefined;
|
||||
const cases = (node.parameters?.cases as unknown[]) ?? [];
|
||||
const caseCount = Array.isArray(cases) ? cases.length : 0;
|
||||
if (outputIndex < caseCount) return `${translate('Fall')} ${outputIndex + 1}`;
|
||||
if (outputIndex === caseCount) return translate('Sonst');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function fromApiGraph(
|
||||
graph: Automation2Graph,
|
||||
nodeTypes: NodeType[]
|
||||
|
|
@ -26,7 +59,7 @@ export function fromApiGraph(
|
|||
let outputs = io.outputs;
|
||||
if (n.type === 'flow.switch') {
|
||||
const cases = (n.parameters?.cases as unknown[]) ?? [];
|
||||
outputs = Math.max(1, cases.length);
|
||||
outputs = switchOutputCountFromCases(cases);
|
||||
}
|
||||
const nt = nodeTypes.find((t) => t.id === n.type);
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,250 +0,0 @@
|
|||
/**
|
||||
* Switch node config - RefSourceSelect für Datenquelle, Fälle mit Operator + Wert.
|
||||
* Gleicher Kontext wie IfElse: typabhängige Operatoren (z.B. Alter < 19, = 30).
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { NodeConfigRendererProps } from '../shared/types';
|
||||
import { RefSourceSelect, getFieldType } from '../shared/RefSourceSelect';
|
||||
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||
import { isRef, createValue } from '../shared/dataRef';
|
||||
import { getMimeTypeOptionsFromUploadParams } from '../runtime/fileTypeMimeMapping';
|
||||
import { operatorsForType } from '../shared/conditionOperators';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
export interface SwitchCase {
|
||||
operator: string;
|
||||
value?: string | number | boolean;
|
||||
}
|
||||
|
||||
function normalizeCase(c: unknown): SwitchCase {
|
||||
if (c && typeof c === 'object' && 'operator' in (c as object)) {
|
||||
const o = c as SwitchCase;
|
||||
const v = o.value;
|
||||
const safeValue: string | number | boolean | undefined =
|
||||
typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' ? v : undefined;
|
||||
return { operator: o.operator ?? 'eq', value: safeValue };
|
||||
}
|
||||
const fallbackValue: string | number | boolean | undefined =
|
||||
typeof c === 'string' || typeof c === 'number' || typeof c === 'boolean' ? c : undefined;
|
||||
return { operator: 'eq', value: fallbackValue };
|
||||
}
|
||||
|
||||
export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
|
||||
const valueParam = params.value;
|
||||
const ref = isRef(valueParam) ? valueParam : null;
|
||||
let staticValue: string | number = '';
|
||||
if (!ref && valueParam != null) {
|
||||
if (typeof valueParam === 'object' && 'value' in valueParam) {
|
||||
const v = (valueParam as { value: unknown }).value;
|
||||
staticValue = v !== undefined && v !== null ? String(v) : '';
|
||||
} else if (typeof valueParam === 'string' || typeof valueParam === 'number') {
|
||||
staticValue = valueParam;
|
||||
}
|
||||
}
|
||||
const rawCases = (params.cases as unknown[]) ?? [];
|
||||
const cases: SwitchCase[] = rawCases.map(normalizeCase);
|
||||
|
||||
const fieldType = dataFlow ? getFieldType(ref, dataFlow.nodes, dataFlow.nodeOutputsPreview) : 'unknown';
|
||||
const operators = operatorsForType(fieldType);
|
||||
const isMimeTypeRef =
|
||||
ref && ref.path?.length >= 2 && ref.path[ref.path.length - 1] === 'mimeType';
|
||||
const sourceNode = ref && dataFlow
|
||||
? dataFlow.nodes.find((n: { id: string; type?: string; parameters?: Record<string, unknown> }) => n.id === ref.nodeId)
|
||||
: null;
|
||||
const mimeTypeOptions =
|
||||
isMimeTypeRef && sourceNode?.type === 'input.upload' && sourceNode.parameters
|
||||
? getMimeTypeOptionsFromUploadParams(sourceNode.parameters as Record<string, unknown>)
|
||||
: [];
|
||||
|
||||
const setValue = (val: unknown) => {
|
||||
updateParam('value', val);
|
||||
};
|
||||
|
||||
const setCases = (next: SwitchCase[]) => {
|
||||
updateParam('cases', next);
|
||||
};
|
||||
|
||||
const handleRefChange = (newRef: { type: 'ref'; nodeId: string; path: (string | number)[] } | null) => {
|
||||
if (newRef) {
|
||||
setValue(newRef);
|
||||
} else {
|
||||
setValue(createValue(staticValue));
|
||||
}
|
||||
};
|
||||
|
||||
const handleStaticValueChange = (v: string) => {
|
||||
setValue(createValue(fieldType === 'number' ? parseFloat(v) || 0 : v));
|
||||
};
|
||||
|
||||
const handleCaseOperatorChange = (index: number, op: string) => {
|
||||
const opDef = operators.find((o) => o.value === op);
|
||||
const next = [...cases];
|
||||
next[index] = {
|
||||
operator: op,
|
||||
value: opDef?.needsValue ? cases[index]?.value : undefined,
|
||||
};
|
||||
setCases(next);
|
||||
};
|
||||
|
||||
const handleCaseValueChange = (index: number, v: string | number | boolean) => {
|
||||
const next = [...cases];
|
||||
next[index] = {
|
||||
...next[index],
|
||||
value: fieldType === 'number' ? (typeof v === 'number' ? v : parseFloat(String(v)) || 0)
|
||||
: fieldType === 'boolean' ? (v === true || v === 'true')
|
||||
: String(v),
|
||||
};
|
||||
setCases(next);
|
||||
};
|
||||
|
||||
const renderCaseValueInput = (caseItem: SwitchCase, index: number) => {
|
||||
const val = caseItem.value;
|
||||
const valStr = String(val ?? '');
|
||||
|
||||
if (mimeTypeOptions.length > 0) {
|
||||
return (
|
||||
<select
|
||||
value={valStr}
|
||||
onChange={(e) => handleCaseValueChange(index, e.target.value)}
|
||||
className={styles.startsInput}
|
||||
>
|
||||
<option value="">{t('MIME-Typ wählen')}</option>
|
||||
{mimeTypeOptions.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label} ({o.value})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
if (fieldType === 'number') {
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
className={styles.startsInput}
|
||||
value={valStr}
|
||||
onChange={(e) => handleCaseValueChange(index, parseFloat(e.target.value) || 0)}
|
||||
placeholder="0"
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (fieldType === 'date') {
|
||||
return (
|
||||
<input
|
||||
type="date"
|
||||
className={styles.startsInput}
|
||||
value={valStr}
|
||||
onChange={(e) => handleCaseValueChange(index, e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (fieldType === 'boolean') {
|
||||
return (
|
||||
<select
|
||||
value={val === true ? 'true' : val === false ? 'false' : ''}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
handleCaseValueChange(index, v === 'true' ? true : v === 'false' ? false : '');
|
||||
}}
|
||||
className={styles.startsInput}
|
||||
>
|
||||
<option value="">{t('Wählen')}</option>
|
||||
<option value="true">{t('Ja (true)')}</option>
|
||||
<option value="false">{t('Nein (false)')}</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
className={styles.startsInput}
|
||||
value={valStr}
|
||||
onChange={(e) => handleCaseValueChange(index, e.target.value)}
|
||||
placeholder={isMimeTypeRef ? t('z.B. application/pdf') : t('Wert')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const addCase = () => {
|
||||
const opDef = operators[0];
|
||||
const defaultVal = opDef?.needsValue
|
||||
? (fieldType === 'number' ? 0 : fieldType === 'boolean' ? false : '')
|
||||
: undefined;
|
||||
setCases([
|
||||
...cases,
|
||||
{ operator: opDef?.value ?? 'eq', value: defaultVal },
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.ifElseConditionEditor}>
|
||||
<div className={styles.ifElseConditionRow}>
|
||||
<label>{t('Datenquelle')}</label>
|
||||
<RefSourceSelect
|
||||
value={ref}
|
||||
onChange={handleRefChange}
|
||||
placeholder={t('Feld zum Vergleich wählen')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!ref && (
|
||||
<div className={styles.ifElseConditionRow}>
|
||||
<label>{t('Fester Wert (ohne Referenz)')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={String(staticValue ?? '')}
|
||||
onChange={(e) => handleStaticValueChange(e.target.value)}
|
||||
placeholder={t('z. B. CH oder 42')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.ifElseConditionRow}>
|
||||
<label>{t('Fälle / Reihenfolge / Ausgabe')}</label>
|
||||
<div className={styles.formFieldsList}>
|
||||
{cases.map((c, i) => {
|
||||
const opDef = operators.find((o) => o.value === c.operator) ?? operators[0];
|
||||
const needsValue = opDef?.needsValue ?? true;
|
||||
return (
|
||||
<div key={i} className={styles.formFieldRow}>
|
||||
<select
|
||||
value={c.operator}
|
||||
onChange={(e) => handleCaseOperatorChange(i, e.target.value)}
|
||||
className={styles.startsInput}
|
||||
style={{ minWidth: 140 }}
|
||||
>
|
||||
{operators.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{needsValue && (
|
||||
<div style={{ flex: 1 }}>
|
||||
{renderCaseValueInput(c, i)}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.formFieldRemoveButton}
|
||||
onClick={() => setCases(cases.filter((_, j) => j !== i))}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button type="button" className={styles.startsAddBtn} onClick={addCase}>
|
||||
{t('+ Fall')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1 +1,2 @@
|
|||
export { SwitchNodeConfig } from './SwitchNodeConfig';
|
||||
export { CaseListEditor as SwitchNodeConfig } from '../frontendTypeRenderers/CaseListEditor';
|
||||
export type { SwitchCase } from '../frontendTypeRenderers/CaseListEditor';
|
||||
|
|
|
|||
Loading…
Reference in a new issue