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 { DataRefRenderer } from './DataRefRenderer';
|
||||
import { ContextBuilderRenderer } from './ContextBuilderRenderer';
|
||||
import { ContextAssignmentsEditor } from './ContextAssignmentsEditor';
|
||||
import { FeatureInstancePicker } from './FeatureInstancePicker';
|
||||
import { UserFileFolderPicker } from './UserFileFolderPicker';
|
||||
import { TemplateTextareaRenderer } from './TemplateTextareaRenderer';
|
||||
|
|
@ -914,6 +915,7 @@ export const FRONTEND_TYPE_RENDERERS: Record<string, FieldRendererComponent> = {
|
|||
hidden: HiddenInput,
|
||||
dataRef: DataRefRenderer,
|
||||
contextBuilder: ContextBuilderRenderer,
|
||||
contextAssignments: ContextAssignmentsEditor,
|
||||
userConnection: ConnectionPicker,
|
||||
featureInstance: FeatureInstancePicker,
|
||||
sharepointFolder: SharepointPathPicker,
|
||||
|
|
|
|||
Loading…
Reference in a new issue