neue context nodes hinzugefügt, muss noch debuggt werden
This commit is contained in:
parent
5ff75a63e3
commit
7d716bc205
2 changed files with 374 additions and 0 deletions
|
|
@ -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<string, unknown>,
|
||||||
|
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<string, unknown>): ContextAssignmentRow[] {
|
||||||
|
if (Array.isArray(raw) && raw.length > 0) {
|
||||||
|
return raw.map((r) => {
|
||||||
|
if (!r || typeof r !== 'object') return defaultRow();
|
||||||
|
const o = r as Record<string, unknown>;
|
||||||
|
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<string, unknown>[]).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<ContextAssignmentRow['mode']>; 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<FieldRendererProps> = ({ param, value, onChange, allParams }) => {
|
||||||
|
const { t } = useLanguage();
|
||||||
|
const dataFlow = useAutomation2DataFlow();
|
||||||
|
const rows = normalizeRows(value, allParams);
|
||||||
|
const [pickerRow, setPickerRow] = React.useState<number | null>(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<ContextAssignmentRow>) => {
|
||||||
|
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 (
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<label style={{ display: 'block', fontSize: 12, marginBottom: 6, fontWeight: 600 }}>
|
||||||
|
{param.description || param.name}
|
||||||
|
{param.required && <span style={{ color: '#d9534f', marginLeft: 4 }}>*</span>}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{rows.map((row, idx) => (
|
||||||
|
<div key={idx} style={ROW_BOX}>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', marginBottom: 6 }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={t('Ziel-Schlüssel im Kontext')}
|
||||||
|
value={row.contextKey}
|
||||||
|
onChange={(e) => setRow(idx, { contextKey: e.target.value })}
|
||||||
|
style={{ flex: '2 1 140px', minWidth: 120, padding: '4px 6px', borderRadius: 4, border: '1px solid #ccc' }}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={row.valueSource}
|
||||||
|
onChange={(e) => {
|
||||||
|
const vs = e.target.value as ValueSource;
|
||||||
|
const patch: Partial<ContextAssignmentRow> = { valueSource: vs };
|
||||||
|
if (vs === 'literal') patch.upstreamRef = undefined;
|
||||||
|
if (vs === 'pickUpstream') patch.literal = '';
|
||||||
|
setRow(idx, patch);
|
||||||
|
}}
|
||||||
|
style={{ flex: '1 1 160px', padding: '4px 6px', borderRadius: 4, border: '1px solid #ccc' }}
|
||||||
|
>
|
||||||
|
<option value="pickUpstream">{t('Wert aus Daten-Picker')}</option>
|
||||||
|
<option value="literal">{t('Fester Wert')}</option>
|
||||||
|
<option value="humanTask">{t('Benutzer setzt Wert (Task)')}</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={row.mode || 'set'}
|
||||||
|
onChange={(e) => setRow(idx, { mode: e.target.value as ContextAssignmentRow['mode'] })}
|
||||||
|
style={{ flex: '1 1 120px', padding: '4px 6px', borderRadius: 4, border: '1px solid #ccc' }}
|
||||||
|
>
|
||||||
|
{MODES.map((m) => (
|
||||||
|
<option key={m.id} value={m.id}>
|
||||||
|
{m.labelDe}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={row.valueType || 'str'}
|
||||||
|
onChange={(e) => setRow(idx, { valueType: e.target.value })}
|
||||||
|
style={{ flex: '0 1 90px', padding: '4px 6px', borderRadius: 4, border: '1px solid #ccc' }}
|
||||||
|
>
|
||||||
|
{TYPES.map((tp) => (
|
||||||
|
<option key={tp} value={tp}>
|
||||||
|
{tp}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button type="button" style={{ padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }} onClick={() => removeRow(idx)}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{row.valueSource === 'pickUpstream' && (
|
||||||
|
<div>
|
||||||
|
{row.upstreamRef && (isRef(row.upstreamRef) || isSystemVar(row.upstreamRef)) && (
|
||||||
|
<div style={CHIP_STYLE}>
|
||||||
|
<span style={{ flex: 1, color: '#2d6a2d' }}>{labelForRef(row.upstreamRef)}</span>
|
||||||
|
<button type="button" style={REMOVE_BTN} onClick={() => setRow(idx, { upstreamRef: undefined })} title={t('Entfernen')}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPickerRow(idx)}
|
||||||
|
disabled={!hasSources}
|
||||||
|
style={{
|
||||||
|
marginTop: 4,
|
||||||
|
width: '100%',
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: 4,
|
||||||
|
border: '1px solid #1c5fb5',
|
||||||
|
background: hasSources ? '#fff' : '#f5f5f5',
|
||||||
|
color: hasSources ? '#1c5fb5' : '#999',
|
||||||
|
cursor: hasSources ? 'pointer' : 'not-allowed',
|
||||||
|
fontSize: 12,
|
||||||
|
textAlign: 'left',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasSources ? t('Datenquelle wählen …') : t('Keine vorherigen Nodes verfügbar')}
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={t('Optional: Zusatz-Pfad (z. B. payload.status)')}
|
||||||
|
value={row.sourcePath || ''}
|
||||||
|
onChange={(e) => setRow(idx, { sourcePath: e.target.value })}
|
||||||
|
style={{ width: '100%', marginTop: 6, padding: '4px 6px', borderRadius: 4, border: '1px dashed #aaa', fontSize: 11 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{row.valueSource === 'literal' && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={t('Wert (oder JSON für object/list)')}
|
||||||
|
value={row.literal ?? ''}
|
||||||
|
onChange={(e) => setRow(idx, { literal: e.target.value })}
|
||||||
|
style={{ width: '100%', padding: '4px 6px', borderRadius: 4, border: '1px solid #ccc' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{row.valueSource === 'humanTask' && (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={t('Titel der Aufgabe (optional)')}
|
||||||
|
value={row.taskTitle || ''}
|
||||||
|
onChange={(e) => setRow(idx, { taskTitle: e.target.value })}
|
||||||
|
style={{ width: '100%', padding: '4px 6px', borderRadius: 4, border: '1px solid #ccc' }}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
placeholder={t('Beschreibung für den Bearbeiter (optional)')}
|
||||||
|
value={row.taskDescription || ''}
|
||||||
|
onChange={(e) => setRow(idx, { taskDescription: e.target.value })}
|
||||||
|
rows={2}
|
||||||
|
style={{ width: '100%', padding: '4px 6px', borderRadius: 4, border: '1px solid #ccc', fontSize: 12 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button type="button" onClick={addRow} style={{ padding: '4px 10px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>
|
||||||
|
{t('Zuweisung hinzufügen')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{dataFlow && pickerRow != null && (
|
||||||
|
<DataPicker
|
||||||
|
open
|
||||||
|
onClose={() => setPickerRow(null)}
|
||||||
|
onPick={(picked) => onPickRef(pickerRow, picked)}
|
||||||
|
availableSourceIds={sourceIds}
|
||||||
|
nodes={dataFlow.nodes}
|
||||||
|
nodeOutputsPreview={dataFlow.nodeOutputsPreview}
|
||||||
|
getNodeLabel={dataFlow.getNodeLabel}
|
||||||
|
expectedParamType="Any"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -33,6 +33,7 @@ import { postUpstreamPaths } from '../../../../api/workflowApi';
|
||||||
import type { CanvasNode } from '../../editor/FlowCanvas';
|
import type { CanvasNode } from '../../editor/FlowCanvas';
|
||||||
import { DataRefRenderer } from './DataRefRenderer';
|
import { DataRefRenderer } from './DataRefRenderer';
|
||||||
import { ContextBuilderRenderer } from './ContextBuilderRenderer';
|
import { ContextBuilderRenderer } from './ContextBuilderRenderer';
|
||||||
|
import { ContextAssignmentsEditor } from './ContextAssignmentsEditor';
|
||||||
import { FeatureInstancePicker } from './FeatureInstancePicker';
|
import { FeatureInstancePicker } from './FeatureInstancePicker';
|
||||||
import { UserFileFolderPicker } from './UserFileFolderPicker';
|
import { UserFileFolderPicker } from './UserFileFolderPicker';
|
||||||
import { TemplateTextareaRenderer } from './TemplateTextareaRenderer';
|
import { TemplateTextareaRenderer } from './TemplateTextareaRenderer';
|
||||||
|
|
@ -914,6 +915,7 @@ export const FRONTEND_TYPE_RENDERERS: Record<string, FieldRendererComponent> = {
|
||||||
hidden: HiddenInput,
|
hidden: HiddenInput,
|
||||||
dataRef: DataRefRenderer,
|
dataRef: DataRefRenderer,
|
||||||
contextBuilder: ContextBuilderRenderer,
|
contextBuilder: ContextBuilderRenderer,
|
||||||
|
contextAssignments: ContextAssignmentsEditor,
|
||||||
userConnection: ConnectionPicker,
|
userConnection: ConnectionPicker,
|
||||||
featureInstance: FeatureInstancePicker,
|
featureInstance: FeatureInstancePicker,
|
||||||
sharepointFolder: SharepointPathPicker,
|
sharepointFolder: SharepointPathPicker,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue