fix: handover fix, if/else node extended comparison mode
This commit is contained in:
parent
919ad061e1
commit
c61ccf4330
8 changed files with 302 additions and 187 deletions
|
|
@ -116,10 +116,19 @@ export interface FormFieldType {
|
||||||
portType: string;
|
portType: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConditionOperatorDef {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
labelKey?: string;
|
||||||
|
needsValue: boolean;
|
||||||
|
valueInput?: { kind: string; options?: string[] };
|
||||||
|
}
|
||||||
|
|
||||||
export interface NodeTypesResponse {
|
export interface NodeTypesResponse {
|
||||||
nodeTypes: NodeType[];
|
nodeTypes: NodeType[];
|
||||||
categories: NodeTypeCategory[];
|
categories: NodeTypeCategory[];
|
||||||
portTypeCatalog?: Record<string, PortSchema>;
|
portTypeCatalog?: Record<string, PortSchema>;
|
||||||
|
conditionOperatorCatalog?: Record<string, ConditionOperatorDef[]>;
|
||||||
systemVariables?: Record<string, SystemVariable>;
|
systemVariables?: Record<string, SystemVariable>;
|
||||||
formFieldTypes?: FormFieldType[];
|
formFieldTypes?: FormFieldType[];
|
||||||
}
|
}
|
||||||
|
|
@ -310,15 +319,17 @@ export async function fetchNodeTypes(
|
||||||
const nodeTypes = data?.nodeTypes ?? [];
|
const nodeTypes = data?.nodeTypes ?? [];
|
||||||
const categories = data?.categories ?? [];
|
const categories = data?.categories ?? [];
|
||||||
const portTypeCatalog = data?.portTypeCatalog ?? undefined;
|
const portTypeCatalog = data?.portTypeCatalog ?? undefined;
|
||||||
|
const conditionOperatorCatalog = data?.conditionOperatorCatalog ?? undefined;
|
||||||
const systemVariables = data?.systemVariables ?? undefined;
|
const systemVariables = data?.systemVariables ?? undefined;
|
||||||
const formFieldTypes = data?.formFieldTypes ?? undefined;
|
const formFieldTypes = data?.formFieldTypes ?? undefined;
|
||||||
console.log(
|
console.log(
|
||||||
`${LOG} fetchNodeTypes response: ${nodeTypes.length} nodeTypes, ${categories.length} categories, ` +
|
`${LOG} fetchNodeTypes response: ${nodeTypes.length} nodeTypes, ${categories.length} categories, ` +
|
||||||
`${portTypeCatalog ? Object.keys(portTypeCatalog).length : 0} portTypes, ` +
|
`${portTypeCatalog ? Object.keys(portTypeCatalog).length : 0} portTypes, ` +
|
||||||
|
`${conditionOperatorCatalog ? Object.keys(conditionOperatorCatalog).length : 0} conditionKinds, ` +
|
||||||
`${systemVariables ? Object.keys(systemVariables).length : 0} sysVars, ` +
|
`${systemVariables ? Object.keys(systemVariables).length : 0} sysVars, ` +
|
||||||
`${formFieldTypes ? formFieldTypes.length : 0} formFieldTypes`
|
`${formFieldTypes ? formFieldTypes.length : 0} formFieldTypes`
|
||||||
);
|
);
|
||||||
return { nodeTypes, categories, portTypeCatalog, systemVariables, formFieldTypes };
|
return { nodeTypes, categories, portTypeCatalog, conditionOperatorCatalog, systemVariables, formFieldTypes };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpstreamPathEntry {
|
export interface UpstreamPathEntry {
|
||||||
|
|
@ -328,6 +339,39 @@ export interface UpstreamPathEntry {
|
||||||
type: string;
|
type: string;
|
||||||
label: string;
|
label: string;
|
||||||
scopeOrigin: 'data' | 'loop' | 'system';
|
scopeOrigin: 'data' | 'loop' | 'system';
|
||||||
|
valueKind?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConditionMetaResponse {
|
||||||
|
valueKind: string;
|
||||||
|
operators: ConditionOperatorDef[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConditionMetaRequest {
|
||||||
|
graph: Automation2Graph;
|
||||||
|
nodeId?: string;
|
||||||
|
ref: { type: 'ref'; nodeId: string; path: (string | number)[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/workflows/{instanceId}/condition-meta — operators for a DataRef (If/Else).
|
||||||
|
*/
|
||||||
|
export async function fetchConditionMeta(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
instanceId: string,
|
||||||
|
body: ConditionMetaRequest,
|
||||||
|
language = 'de'
|
||||||
|
): Promise<ConditionMetaResponse> {
|
||||||
|
const data = await request({
|
||||||
|
url: `/api/workflows/${instanceId}/condition-meta`,
|
||||||
|
method: 'post',
|
||||||
|
params: { language },
|
||||||
|
data: body,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
valueKind: String(data?.valueKind ?? 'unknown'),
|
||||||
|
operators: (data?.operators ?? []) as ConditionOperatorDef[],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
import React, { createContext, useContext, useMemo } from 'react';
|
import React, { createContext, useContext, useMemo } from 'react';
|
||||||
import type { CanvasNode, CanvasConnection } from '../editor/FlowCanvas';
|
import type { CanvasNode, CanvasConnection } from '../editor/FlowCanvas';
|
||||||
import { getAvailableSources } from '../nodes/shared/dataFlowGraph';
|
import { getAvailableSources } from '../nodes/shared/dataFlowGraph';
|
||||||
import type { ApiRequestFunction, FormFieldType, NodeType, PortField, PortSchema, SystemVariable } from '../../../api/workflowApi';
|
import type { ApiRequestFunction, ConditionOperatorDef, FormFieldType, NodeType, PortField, PortSchema, SystemVariable } from '../../../api/workflowApi';
|
||||||
|
|
||||||
export interface Automation2DataFlowContextValue {
|
export interface Automation2DataFlowContextValue {
|
||||||
currentNodeId: string;
|
currentNodeId: string;
|
||||||
|
|
@ -19,6 +19,8 @@ export interface Automation2DataFlowContextValue {
|
||||||
systemVariables: Record<string, SystemVariable>;
|
systemVariables: Record<string, SystemVariable>;
|
||||||
/** Canonical form field types from the API — maps UI type id to portType primitive. */
|
/** Canonical form field types from the API — maps UI type id to portType primitive. */
|
||||||
formFieldTypes: FormFieldType[];
|
formFieldTypes: FormFieldType[];
|
||||||
|
/** Backend-driven condition operators per valueKind (flow.ifElse). */
|
||||||
|
conditionOperatorCatalog: Record<string, ConditionOperatorDef[]>;
|
||||||
getNodeLabel: (node: { id: string; title?: string; label?: string; type?: string }) => string;
|
getNodeLabel: (node: { id: string; title?: string; label?: string; type?: string }) => string;
|
||||||
getAvailableSourceIds: () => string[];
|
getAvailableSourceIds: () => string[];
|
||||||
/** Present when rendered inside the flow editor (ConnectionPicker / tools). */
|
/** Present when rendered inside the flow editor (ConnectionPicker / tools). */
|
||||||
|
|
@ -44,6 +46,7 @@ interface Automation2DataFlowProviderProps {
|
||||||
portTypeCatalog?: Record<string, PortSchema>;
|
portTypeCatalog?: Record<string, PortSchema>;
|
||||||
systemVariables?: Record<string, SystemVariable>;
|
systemVariables?: Record<string, SystemVariable>;
|
||||||
formFieldTypes?: FormFieldType[];
|
formFieldTypes?: FormFieldType[];
|
||||||
|
conditionOperatorCatalog?: Record<string, ConditionOperatorDef[]>;
|
||||||
instanceId?: string;
|
instanceId?: string;
|
||||||
request?: ApiRequestFunction;
|
request?: ApiRequestFunction;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
@ -59,6 +62,7 @@ export const Automation2DataFlowProvider: React.FC<Automation2DataFlowProviderPr
|
||||||
portTypeCatalog = {},
|
portTypeCatalog = {},
|
||||||
systemVariables = {},
|
systemVariables = {},
|
||||||
formFieldTypes = [],
|
formFieldTypes = [],
|
||||||
|
conditionOperatorCatalog = {},
|
||||||
instanceId,
|
instanceId,
|
||||||
request,
|
request,
|
||||||
children,
|
children,
|
||||||
|
|
@ -120,6 +124,7 @@ export const Automation2DataFlowProvider: React.FC<Automation2DataFlowProviderPr
|
||||||
portTypeCatalog,
|
portTypeCatalog,
|
||||||
systemVariables,
|
systemVariables,
|
||||||
formFieldTypes,
|
formFieldTypes,
|
||||||
|
conditionOperatorCatalog,
|
||||||
getNodeLabel: (n: { id: string; title?: string; label?: string; type?: string }) =>
|
getNodeLabel: (n: { id: string; title?: string; label?: string; type?: string }) =>
|
||||||
n.title ?? n.label ?? n.type ?? n.id,
|
n.title ?? n.label ?? n.type ?? n.id,
|
||||||
getAvailableSourceIds: () => getAvailableSources(node.id, nodes, connections),
|
getAvailableSourceIds: () => getAvailableSources(node.id, nodes, connections),
|
||||||
|
|
@ -127,7 +132,7 @@ export const Automation2DataFlowProvider: React.FC<Automation2DataFlowProviderPr
|
||||||
request,
|
request,
|
||||||
parseGraphDefinedSchema,
|
parseGraphDefinedSchema,
|
||||||
};
|
};
|
||||||
}, [node, nodes, connections, nodeOutputsPreview, nodeTypes, language, portTypeCatalog, systemVariables, formFieldTypes, instanceId, request]);
|
}, [node, nodes, connections, nodeOutputsPreview, nodeTypes, language, portTypeCatalog, systemVariables, formFieldTypes, conditionOperatorCatalog, instanceId, request]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Automation2DataFlowContext.Provider value={value}>
|
<Automation2DataFlowContext.Provider value={value}>
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,9 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
const [portTypeCatalog, setPortTypeCatalog] = useState<Record<string, unknown>>({});
|
const [portTypeCatalog, setPortTypeCatalog] = useState<Record<string, unknown>>({});
|
||||||
const [systemVariables, setSystemVariables] = useState<Record<string, unknown>>({});
|
const [systemVariables, setSystemVariables] = useState<Record<string, unknown>>({});
|
||||||
const [formFieldTypes, setFormFieldTypes] = useState<import('../../../api/workflowApi').FormFieldType[]>([]);
|
const [formFieldTypes, setFormFieldTypes] = useState<import('../../../api/workflowApi').FormFieldType[]>([]);
|
||||||
|
const [conditionOperatorCatalog, setConditionOperatorCatalog] = useState<
|
||||||
|
Record<string, import('../../../api/workflowApi').ConditionOperatorDef[]>
|
||||||
|
>({});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
|
|
@ -545,6 +548,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
}
|
}
|
||||||
if (data.systemVariables) setSystemVariables(data.systemVariables);
|
if (data.systemVariables) setSystemVariables(data.systemVariables);
|
||||||
if (data.formFieldTypes) setFormFieldTypes(data.formFieldTypes);
|
if (data.formFieldTypes) setFormFieldTypes(data.formFieldTypes);
|
||||||
|
if (data.conditionOperatorCatalog) setConditionOperatorCatalog(data.conditionOperatorCatalog);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
setNodeTypes([]);
|
setNodeTypes([]);
|
||||||
|
|
@ -1024,6 +1028,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
portTypeCatalog={portTypeCatalog as Record<string, never>}
|
portTypeCatalog={portTypeCatalog as Record<string, never>}
|
||||||
systemVariables={systemVariables as Record<string, never>}
|
systemVariables={systemVariables as Record<string, never>}
|
||||||
formFieldTypes={formFieldTypes}
|
formFieldTypes={formFieldTypes}
|
||||||
|
conditionOperatorCatalog={conditionOperatorCatalog}
|
||||||
instanceId={instanceId}
|
instanceId={instanceId}
|
||||||
request={request}
|
request={request}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
/**
|
||||||
|
* Backend-driven condition editor for flow.ifElse (depends on Item 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 StructuredCondition {
|
||||||
|
type: 'condition';
|
||||||
|
operator: string;
|
||||||
|
value?: string | number;
|
||||||
|
/** Legacy — ignored when Item is set */
|
||||||
|
ref?: DataRef | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCondition(v: unknown): StructuredCondition {
|
||||||
|
if (v && typeof v === 'object' && (v as StructuredCondition).type === 'condition') {
|
||||||
|
const c = v as StructuredCondition;
|
||||||
|
return { type: 'condition', operator: c.operator ?? 'eq', value: c.value };
|
||||||
|
}
|
||||||
|
return { type: 'condition', operator: 'eq', value: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function operatorsFromCatalog(
|
||||||
|
catalog: Record<string, ConditionOperatorDef[]> | undefined,
|
||||||
|
valueKind: string
|
||||||
|
): ConditionOperatorDef[] {
|
||||||
|
if (!catalog) return [];
|
||||||
|
return catalog[valueKind] ?? catalog.unknown ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConditionEditor: 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 ?? 'Item')
|
||||||
|
: 'Item';
|
||||||
|
|
||||||
|
const itemRef = allParams?.[dependsOn];
|
||||||
|
const ref: DataRef | null = isRef(itemRef) ? itemRef : null;
|
||||||
|
|
||||||
|
const cond = parseCondition(value);
|
||||||
|
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) {
|
||||||
|
setOperators([]);
|
||||||
|
setValueKind('unknown');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const applyMeta = (vk: string, ops: ConditionOperatorDef[]) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setValueKind(vk);
|
||||||
|
setOperators(ops);
|
||||||
|
const valid = ops.some((o) => o.id === cond.operator);
|
||||||
|
if (!valid && ops.length > 0) {
|
||||||
|
const first = ops[0];
|
||||||
|
onChange({
|
||||||
|
type: 'condition',
|
||||||
|
operator: first.id,
|
||||||
|
value: first.needsValue ? cond.value ?? '' : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
const ops = operatorsFromCatalog(catalog, 'unknown');
|
||||||
|
applyMeta('unknown', ops);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setLoading(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const ops = operatorsFromCatalog(catalog, 'unknown');
|
||||||
|
applyMeta('unknown', ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- reset operators when Item ref changes
|
||||||
|
}, [ref?.nodeId, JSON.stringify(ref?.path), dataFlow?.currentNodeId, catalog]);
|
||||||
|
|
||||||
|
const currentOp = operators.find((o) => o.id === cond.operator) ?? operators[0];
|
||||||
|
const needsValue = currentOp?.needsValue ?? true;
|
||||||
|
const valueInput = currentOp?.valueInput;
|
||||||
|
|
||||||
|
const setCondition = (next: StructuredCondition) => {
|
||||||
|
onChange(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 ein Item im Data Picker wählen')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOperatorChange = (opId: string) => {
|
||||||
|
const opDef = operators.find((o) => o.id === opId);
|
||||||
|
setCondition({
|
||||||
|
type: 'condition',
|
||||||
|
operator: opId,
|
||||||
|
value: opDef?.needsValue ? cond.value ?? '' : undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValueChange = (v: string | number) => {
|
||||||
|
const kind = valueInput?.kind;
|
||||||
|
const parsed =
|
||||||
|
kind === 'number' || valueKind === 'number' ? parseFloat(String(v)) || 0 : String(v);
|
||||||
|
setCondition({ type: 'condition', operator: cond.operator, value: parsed });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<label style={{ display: 'block', fontSize: 12, marginBottom: 4 }}>
|
||||||
|
{param.description || param.name}
|
||||||
|
</label>
|
||||||
|
<ConditionRow>
|
||||||
|
<label>{t('Vergleich')}</label>
|
||||||
|
<select
|
||||||
|
value={cond.operator}
|
||||||
|
onChange={(e) => handleOperatorChange(e.target.value)}
|
||||||
|
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>
|
||||||
|
</ConditionRow>
|
||||||
|
{loading && (
|
||||||
|
<div style={{ fontSize: 11, color: '#888', marginBottom: 4 }}>{t('Lade Operatoren…')}</div>
|
||||||
|
)}
|
||||||
|
{needsValue && (
|
||||||
|
<ConditionRow>
|
||||||
|
<label>{t('Wert')}</label>
|
||||||
|
{valueInput?.kind === 'select' ||
|
||||||
|
valueInput?.kind === 'contentType' ||
|
||||||
|
valueInput?.kind === 'outputMode' ||
|
||||||
|
valueInput?.kind === 'language' ||
|
||||||
|
valueInput?.kind === 'mime' ? (
|
||||||
|
<select
|
||||||
|
value={String(cond.value ?? '')}
|
||||||
|
onChange={(e) => handleValueChange(e.target.value)}
|
||||||
|
style={{ flex: 1, 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>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type={
|
||||||
|
valueInput?.kind === 'number'
|
||||||
|
? 'number'
|
||||||
|
: valueInput?.kind === 'date'
|
||||||
|
? 'date'
|
||||||
|
: 'text'
|
||||||
|
}
|
||||||
|
value={String(cond.value ?? '')}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleValueChange(
|
||||||
|
valueInput?.kind === 'number' ? parseFloat(e.target.value) || 0 : e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
valueInput?.kind === 'regex' ? t('Regex-Muster') : t('Wert eingeben')
|
||||||
|
}
|
||||||
|
style={{ flex: 1, padding: '4px 6px', borderRadius: 4, border: '1px solid #ccc' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ConditionRow>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConditionRow: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||||
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 6, fontSize: 12 }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
@ -53,6 +53,7 @@ import { ContextBuilderRenderer } from './ContextBuilderRenderer';
|
||||||
import { ContextAssignmentsEditor } from './ContextAssignmentsEditor';
|
import { ContextAssignmentsEditor } from './ContextAssignmentsEditor';
|
||||||
import { FeatureInstancePicker } from './FeatureInstancePicker';
|
import { FeatureInstancePicker } from './FeatureInstancePicker';
|
||||||
import { UserFileFolderPicker } from './UserFileFolderPicker';
|
import { UserFileFolderPicker } from './UserFileFolderPicker';
|
||||||
|
import { ConditionEditor } from './ConditionEditor';
|
||||||
import { TemplateTextareaRenderer } from './TemplateTextareaRenderer';
|
import { TemplateTextareaRenderer } from './TemplateTextareaRenderer';
|
||||||
import { getApiBaseUrl } from '../../../../../config/config';
|
import { getApiBaseUrl } from '../../../../../config/config';
|
||||||
|
|
||||||
|
|
@ -907,30 +908,7 @@ const CronBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange, all
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ConditionBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
const ConditionBuilder = ConditionEditor;
|
||||||
const { t } = useLanguage();
|
|
||||||
const cond = (typeof value === 'object' && value !== null) ? value as Record<string, unknown> : {};
|
|
||||||
const update = (field: string, val: unknown) => onChange({ ...cond, type: 'condition', [field]: val });
|
|
||||||
return (
|
|
||||||
<div style={{ marginBottom: 8 }}>
|
|
||||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
|
||||||
<div style={{ display: 'flex', gap: 4 }}>
|
|
||||||
<select value={String(cond.operator ?? 'eq')} onChange={(e) => update('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="gt">{t('größer als')}</option>
|
|
||||||
<option value="lt">{t('kleiner als')}</option>
|
|
||||||
<option value="contains">{t('enthält')}</option>
|
|
||||||
<option value="empty">{t('ist leer')}</option>
|
|
||||||
<option value="not_empty">{t('ist nicht leer')}</option>
|
|
||||||
<option value="is_true">{t('ist wahr')}</option>
|
|
||||||
<option value="is_false">{t('ist falsch')}</option>
|
|
||||||
</select>
|
|
||||||
<input type="text" placeholder={t('Wert')} value={String(cond.value ?? '')} onChange={(e) => update('value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MappingTableEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
const MappingTableEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
/**
|
|
||||||
* If/Else node config - inline UI: source dropdown, operator (type-dependent), value.
|
|
||||||
* Kein Popup, alles in einer Zeile.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import type { NodeConfigRendererProps } from '../shared/types';
|
|
||||||
import { RefSourceSelect, getFieldType } from '../shared/RefSourceSelect';
|
|
||||||
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
|
||||||
import { isRef } 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 StructuredCondition {
|
|
||||||
type: 'condition';
|
|
||||||
ref: { type: 'ref'; nodeId: string; path: (string | number)[] } | null;
|
|
||||||
operator: string;
|
|
||||||
value?: string | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseCondition(v: unknown): StructuredCondition | null {
|
|
||||||
if (v && typeof v === 'object' && (v as StructuredCondition).type === 'condition') {
|
|
||||||
const c = v as StructuredCondition;
|
|
||||||
if (c.ref === null || isRef(c.ref)) return c;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
|
||||||
const { t } = useLanguage();
|
|
||||||
const dataFlow = useAutomation2DataFlow();
|
|
||||||
|
|
||||||
const cond = parseCondition(params.condition);
|
|
||||||
const ref = cond?.ref ?? null;
|
|
||||||
const operator = cond?.operator ?? 'eq';
|
|
||||||
const value = cond?.value ?? '';
|
|
||||||
|
|
||||||
const fieldType = dataFlow ? getFieldType(ref, dataFlow.nodes, dataFlow.nodeOutputsPreview) : 'unknown';
|
|
||||||
const operators = operatorsForType(fieldType);
|
|
||||||
const currentOp = operators.find((o) => o.value === operator) ?? operators[0];
|
|
||||||
const needsValue = currentOp?.needsValue ?? true;
|
|
||||||
|
|
||||||
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 setCondition = (next: StructuredCondition) => {
|
|
||||||
updateParam('condition', next);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefChange = (newRef: { type: 'ref'; nodeId: string; path: (string | number)[] } | null) => {
|
|
||||||
if (!newRef) {
|
|
||||||
setCondition({
|
|
||||||
type: 'condition',
|
|
||||||
ref: null,
|
|
||||||
operator: 'eq',
|
|
||||||
value: '',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newType = dataFlow ? getFieldType(newRef, dataFlow.nodes, dataFlow.nodeOutputsPreview) : 'unknown';
|
|
||||||
const newOps = operatorsForType(newType);
|
|
||||||
setCondition({
|
|
||||||
type: 'condition',
|
|
||||||
ref: newRef,
|
|
||||||
operator: newOps[0]?.value ?? 'eq',
|
|
||||||
value: cond?.value ?? '',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOperatorChange = (op: string) => {
|
|
||||||
const opDef = operators.find((o) => o.value === op);
|
|
||||||
setCondition({
|
|
||||||
type: 'condition',
|
|
||||||
ref: cond?.ref ?? null,
|
|
||||||
operator: op,
|
|
||||||
value: opDef?.needsValue ? value : undefined,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleValueChange = (v: string | number) => {
|
|
||||||
setCondition({
|
|
||||||
type: 'condition',
|
|
||||||
ref: cond?.ref ?? null,
|
|
||||||
operator,
|
|
||||||
value: fieldType === 'number' ? (parseFloat(String(v)) || 0) : String(v),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.ifElseConditionEditor}>
|
|
||||||
<div className={styles.ifElseConditionRow}>
|
|
||||||
<label>{t('Datenquelle')}</label>
|
|
||||||
<RefSourceSelect value={ref} onChange={handleRefChange} placeholder={t('Formularfeld wählen')} />
|
|
||||||
</div>
|
|
||||||
<div className={styles.ifElseConditionRow}>
|
|
||||||
<label>Vergleich</label>
|
|
||||||
<select value={operator} onChange={(e) => handleOperatorChange(e.target.value)}>
|
|
||||||
{operators.map((o) => (
|
|
||||||
<option key={o.value} value={o.value}>
|
|
||||||
{o.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{needsValue && (
|
|
||||||
<div className={styles.ifElseConditionRow}>
|
|
||||||
<label>{t('Wert')}</label>
|
|
||||||
{mimeTypeOptions.length > 0 ? (
|
|
||||||
<select
|
|
||||||
value={String(value ?? '')}
|
|
||||||
onChange={(e) => handleValueChange(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="">{t('MIME-Typ wählen')}</option>
|
|
||||||
{mimeTypeOptions.map((o) => (
|
|
||||||
<option key={o.value} value={o.value}>
|
|
||||||
{o.label} ({o.value})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
type={fieldType === 'number' ? 'number' : fieldType === 'date' ? 'date' : 'text'}
|
|
||||||
value={String(value ?? '')}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleValueChange(
|
|
||||||
fieldType === 'number' ? parseFloat(e.target.value) || 0 : e.target.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
placeholder={
|
|
||||||
fieldType === 'number'
|
|
||||||
? '0'
|
|
||||||
: fieldType === 'date'
|
|
||||||
? 'TT.MM.JJJJ'
|
|
||||||
: isMimeTypeRef
|
|
||||||
? t('z.B. application/pdf')
|
|
||||||
: t('z.B. ch')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export { IfElseNodeConfig } from './IfElseNodeConfig';
|
export { ConditionEditor as IfElseNodeConfig } from '../frontendTypeRenderers/ConditionEditor';
|
||||||
|
export type { StructuredCondition } from '../frontendTypeRenderers/ConditionEditor';
|
||||||
|
|
|
||||||
|
|
@ -1177,6 +1177,13 @@ const _FileLinkList: React.FC<{ files: Array<{ id: string; fileName?: string }>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _INTERNAL_EXTRACT_FILENAME_SUBSTR = 'extracted_content_transient';
|
||||||
|
|
||||||
|
/** Hide persisted transient extract JSON from user-facing Workspace file lists */
|
||||||
|
function _isHiddenWorkflowArtifactFile(f: { fileName?: string }): boolean {
|
||||||
|
return (f.fileName ?? '').toLowerCase().includes(_INTERNAL_EXTRACT_FILENAME_SUBSTR);
|
||||||
|
}
|
||||||
|
|
||||||
const _ProducedFilesSection: React.FC<{
|
const _ProducedFilesSection: React.FC<{
|
||||||
steps: Array<{ outputFiles?: Array<{ id: string; fileName?: string }> }>;
|
steps: Array<{ outputFiles?: Array<{ id: string; fileName?: string }> }>;
|
||||||
unassignedFiles?: Array<{ id: string; fileName?: string }>;
|
unassignedFiles?: Array<{ id: string; fileName?: string }>;
|
||||||
|
|
@ -1186,10 +1193,12 @@ const _ProducedFilesSection: React.FC<{
|
||||||
const allFiles: Array<{ id: string; fileName?: string }> = [];
|
const allFiles: Array<{ id: string; fileName?: string }> = [];
|
||||||
for (const step of steps) {
|
for (const step of steps) {
|
||||||
for (const f of step.outputFiles ?? []) {
|
for (const f of step.outputFiles ?? []) {
|
||||||
|
if (_isHiddenWorkflowArtifactFile(f)) continue;
|
||||||
if (!seen.has(f.id)) { seen.add(f.id); allFiles.push(f); }
|
if (!seen.has(f.id)) { seen.add(f.id); allFiles.push(f); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const f of unassignedFiles ?? []) {
|
for (const f of unassignedFiles ?? []) {
|
||||||
|
if (_isHiddenWorkflowArtifactFile(f)) continue;
|
||||||
if (!seen.has(f.id)) { seen.add(f.id); allFiles.push(f); }
|
if (!seen.has(f.id)) { seen.add(f.id); allFiles.push(f); }
|
||||||
}
|
}
|
||||||
if (!allFiles.length) return null;
|
if (!allFiles.length) return null;
|
||||||
|
|
@ -1312,8 +1321,8 @@ const _WorkspaceTab: React.FC<_WorkspaceTabProps> = ({ runId, onBack }) => {
|
||||||
{steps.map((step) => {
|
{steps.map((step) => {
|
||||||
const inputData = _stripFileRefKeys(step.inputSnapshot ?? {});
|
const inputData = _stripFileRefKeys(step.inputSnapshot ?? {});
|
||||||
const outputData = _stripFileRefKeys(step.output ?? {});
|
const outputData = _stripFileRefKeys(step.output ?? {});
|
||||||
const inputFiles = step.inputFiles ?? [];
|
const inputFiles = (step.inputFiles ?? []).filter((f) => !_isHiddenWorkflowArtifactFile(f));
|
||||||
const outputFiles = step.outputFiles ?? [];
|
const outputFiles = (step.outputFiles ?? []).filter((f) => !_isHiddenWorkflowArtifactFile(f));
|
||||||
const hasInput = inputData !== undefined || inputFiles.length > 0;
|
const hasInput = inputData !== undefined || inputFiles.length > 0;
|
||||||
const hasOutput = outputData !== undefined || outputFiles.length > 0;
|
const hasOutput = outputData !== undefined || outputFiles.length > 0;
|
||||||
return (
|
return (
|
||||||
|
|
@ -1374,12 +1383,16 @@ const _WorkspaceTab: React.FC<_WorkspaceTabProps> = ({ runId, onBack }) => {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{unassignedFiles && unassignedFiles.length > 0 && (
|
{(() => {
|
||||||
|
const visibleUnassigned = (unassignedFiles ?? []).filter((f) => !_isHiddenWorkflowArtifactFile(f));
|
||||||
|
if (!visibleUnassigned.length) return null;
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<h4 style={{ margin: '1rem 0 0.5rem' }}>{t('Sonstige Dokumente')}</h4>
|
<h4 style={{ margin: '1rem 0 0.5rem' }}>{t('Sonstige Dokumente')}</h4>
|
||||||
<_FileLinkList files={unassignedFiles} />
|
<_FileLinkList files={visibleUnassigned} />
|
||||||
</>
|
</>
|
||||||
)}
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue