fix: alle Node definitionen korrigiert und im backend gesetzt - keine mapping layer sonder saubere quelldaten, fehlende dataRef parameter hinzugefügt, damit jede node kontext nutzen kann
This commit is contained in:
parent
992c0472c6
commit
1d2d247273
8 changed files with 74 additions and 25 deletions
|
|
@ -85,11 +85,19 @@ export interface SystemVariable {
|
|||
description: string;
|
||||
}
|
||||
|
||||
/** Single form field type with its canonical port primitive. Delivered by GET /node-types. */
|
||||
export interface FormFieldType {
|
||||
id: string;
|
||||
label: string;
|
||||
portType: string;
|
||||
}
|
||||
|
||||
export interface NodeTypesResponse {
|
||||
nodeTypes: NodeType[];
|
||||
categories: NodeTypeCategory[];
|
||||
portTypeCatalog?: Record<string, PortSchema>;
|
||||
systemVariables?: Record<string, SystemVariable>;
|
||||
formFieldTypes?: FormFieldType[];
|
||||
}
|
||||
|
||||
export interface Automation2GraphNode {
|
||||
|
|
@ -279,12 +287,14 @@ export async function fetchNodeTypes(
|
|||
const categories = data?.categories ?? [];
|
||||
const portTypeCatalog = data?.portTypeCatalog ?? undefined;
|
||||
const systemVariables = data?.systemVariables ?? undefined;
|
||||
const formFieldTypes = data?.formFieldTypes ?? undefined;
|
||||
console.log(
|
||||
`${LOG} fetchNodeTypes response: ${nodeTypes.length} nodeTypes, ${categories.length} categories, ` +
|
||||
`${portTypeCatalog ? Object.keys(portTypeCatalog).length : 0} portTypes, ` +
|
||||
`${systemVariables ? Object.keys(systemVariables).length : 0} sysVars`
|
||||
`${systemVariables ? Object.keys(systemVariables).length : 0} sysVars, ` +
|
||||
`${formFieldTypes ? formFieldTypes.length : 0} formFieldTypes`
|
||||
);
|
||||
return { nodeTypes, categories, portTypeCatalog, systemVariables };
|
||||
return { nodeTypes, categories, portTypeCatalog, systemVariables, formFieldTypes };
|
||||
}
|
||||
|
||||
export interface UpstreamPathEntry {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import type { CanvasNode, CanvasConnection } from '../editor/FlowCanvas';
|
||||
import { getAvailableSources } from '../nodes/shared/dataFlowGraph';
|
||||
import type { ApiRequestFunction, NodeType, PortField, PortSchema, SystemVariable } from '../../../api/workflowApi';
|
||||
import type { ApiRequestFunction, FormFieldType, NodeType, PortField, PortSchema, SystemVariable } from '../../../api/workflowApi';
|
||||
|
||||
export interface Automation2DataFlowContextValue {
|
||||
currentNodeId: string;
|
||||
|
|
@ -17,6 +17,8 @@ export interface Automation2DataFlowContextValue {
|
|||
language: string;
|
||||
portTypeCatalog: Record<string, PortSchema>;
|
||||
systemVariables: Record<string, SystemVariable>;
|
||||
/** Canonical form field types from the API — maps UI type id to portType primitive. */
|
||||
formFieldTypes: FormFieldType[];
|
||||
getNodeLabel: (node: { id: string; title?: string; label?: string; type?: string }) => string;
|
||||
getAvailableSourceIds: () => string[];
|
||||
/** Present when rendered inside the flow editor (ConnectionPicker / tools). */
|
||||
|
|
@ -41,6 +43,7 @@ interface Automation2DataFlowProviderProps {
|
|||
language: string;
|
||||
portTypeCatalog?: Record<string, PortSchema>;
|
||||
systemVariables?: Record<string, SystemVariable>;
|
||||
formFieldTypes?: FormFieldType[];
|
||||
instanceId?: string;
|
||||
request?: ApiRequestFunction;
|
||||
children: React.ReactNode;
|
||||
|
|
@ -55,12 +58,18 @@ export const Automation2DataFlowProvider: React.FC<Automation2DataFlowProviderPr
|
|||
language,
|
||||
portTypeCatalog = {},
|
||||
systemVariables = {},
|
||||
formFieldTypes = [],
|
||||
instanceId,
|
||||
request,
|
||||
children,
|
||||
}) => {
|
||||
const value = useMemo((): Automation2DataFlowContextValue | null => {
|
||||
if (!node) return null;
|
||||
const formTypeToPort: Record<string, string> = Object.fromEntries(
|
||||
formFieldTypes.map((f) => [f.id, f.portType])
|
||||
);
|
||||
const resolvePortType = (rawType: string): string => formTypeToPort[rawType] ?? rawType;
|
||||
|
||||
const parseGraphDefinedSchema = (parameterKey: string): PortSchema | null => {
|
||||
const raw = node.parameters?.[parameterKey];
|
||||
if (!Array.isArray(raw)) return null;
|
||||
|
|
@ -72,8 +81,8 @@ export const Automation2DataFlowProvider: React.FC<Automation2DataFlowProviderPr
|
|||
const lab = rec.label;
|
||||
const desc =
|
||||
typeof lab === 'string' ? lab : typeof lab === 'object' && lab !== null ? String((lab as Record<string, string>).de ?? '') : '';
|
||||
const ftype = typeof rec.type === 'string' ? rec.type : 'str';
|
||||
if (ftype === 'group' && Array.isArray(rec.fields)) {
|
||||
const rawType = typeof rec.type === 'string' ? rec.type : 'str';
|
||||
if (rawType === 'group' && Array.isArray(rec.fields)) {
|
||||
for (const sub of rec.fields as Record<string, unknown>[]) {
|
||||
if (!sub || typeof sub.name !== 'string') continue;
|
||||
const sl = sub.label;
|
||||
|
|
@ -85,7 +94,7 @@ export const Automation2DataFlowProvider: React.FC<Automation2DataFlowProviderPr
|
|||
: '';
|
||||
fields.push({
|
||||
name: `${rec.name}.${sub.name}`,
|
||||
type: typeof sub.type === 'string' ? sub.type : 'str',
|
||||
type: resolvePortType(typeof sub.type === 'string' ? sub.type : 'str'),
|
||||
description: (sdesc && sdesc.trim()) || `${rec.name}.${sub.name}`,
|
||||
required: Boolean(sub.required),
|
||||
});
|
||||
|
|
@ -94,7 +103,7 @@ export const Automation2DataFlowProvider: React.FC<Automation2DataFlowProviderPr
|
|||
}
|
||||
fields.push({
|
||||
name: rec.name,
|
||||
type: ftype,
|
||||
type: resolvePortType(rawType),
|
||||
description: (desc && desc.trim()) || rec.name,
|
||||
required: Boolean(rec.required),
|
||||
});
|
||||
|
|
@ -110,6 +119,7 @@ export const Automation2DataFlowProvider: React.FC<Automation2DataFlowProviderPr
|
|||
language,
|
||||
portTypeCatalog,
|
||||
systemVariables,
|
||||
formFieldTypes,
|
||||
getNodeLabel: (n: { id: string; title?: string; label?: string; type?: string }) =>
|
||||
n.title ?? n.label ?? n.type ?? n.id,
|
||||
getAvailableSourceIds: () => getAvailableSources(node.id, nodes, connections),
|
||||
|
|
@ -117,7 +127,7 @@ export const Automation2DataFlowProvider: React.FC<Automation2DataFlowProviderPr
|
|||
request,
|
||||
parseGraphDefinedSchema,
|
||||
};
|
||||
}, [node, nodes, connections, nodeOutputsPreview, nodeTypes, language, portTypeCatalog, systemVariables, instanceId, request]);
|
||||
}, [node, nodes, connections, nodeOutputsPreview, nodeTypes, language, portTypeCatalog, systemVariables, formFieldTypes, instanceId, request]);
|
||||
|
||||
return (
|
||||
<Automation2DataFlowContext.Provider value={value}>
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
const [categories, setCategories] = useState<NodeTypeCategory[]>([]);
|
||||
const [portTypeCatalog, setPortTypeCatalog] = useState<Record<string, unknown>>({});
|
||||
const [systemVariables, setSystemVariables] = useState<Record<string, unknown>>({});
|
||||
const [formFieldTypes, setFormFieldTypes] = useState<import('../../../api/workflowApi').FormFieldType[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [filter, setFilter] = useState('');
|
||||
|
|
@ -459,6 +460,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
setRegistryCatalog(data.portTypeCatalog as never);
|
||||
}
|
||||
if (data.systemVariables) setSystemVariables(data.systemVariables);
|
||||
if (data.formFieldTypes) setFormFieldTypes(data.formFieldTypes);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
setNodeTypes([]);
|
||||
|
|
@ -904,6 +906,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
|||
language={language}
|
||||
portTypeCatalog={portTypeCatalog as Record<string, never>}
|
||||
systemVariables={systemVariables as Record<string, never>}
|
||||
formFieldTypes={formFieldTypes}
|
||||
instanceId={instanceId}
|
||||
request={request}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -7,11 +7,16 @@ import { FaGripVertical, FaTimes } from 'react-icons/fa';
|
|||
import type { FormField, NodeConfigRendererProps } from '../shared/types';
|
||||
import { FORM_FIELD_TYPES, FORM_FIELD_TYPE_LABELS } from '../../../../utils/attributeTypeMapper';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
||||
const { t } = useLanguage();
|
||||
const ctx = useAutomation2DataFlow();
|
||||
const fieldTypeOptions = ctx?.formFieldTypes?.length
|
||||
? ctx.formFieldTypes
|
||||
: FORM_FIELD_TYPES.map((ft) => ({ id: ft, label: FORM_FIELD_TYPE_LABELS[ft] ?? ft, portType: 'str' }));
|
||||
const fields = (params.fields as FormField[]) ?? [];
|
||||
|
||||
const moveField = (fromIndex: number, toIndex: number) => {
|
||||
|
|
@ -88,8 +93,8 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, upda
|
|||
}}
|
||||
style={{ width: 'auto', minWidth: 90 }}
|
||||
>
|
||||
{FORM_FIELD_TYPES.map(ft => (
|
||||
<option key={ft} value={ft}>{t(FORM_FIELD_TYPE_LABELS[ft])}</option>
|
||||
{fieldTypeOptions.map((ft) => (
|
||||
<option key={ft.id} value={ft.id}>{t(ft.label)}</option>
|
||||
))}
|
||||
</select>
|
||||
<label className={styles.formFieldRequiredLabel}>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type { ComponentType } from 'react';
|
|||
import type { NodeTypeParameter } from '../../../../api/workflowApi';
|
||||
import type { ApiRequestFunction } from '../../../../api/workflowApi';
|
||||
import { FORM_FIELD_TYPES, FORM_FIELD_TYPE_LABELS } from '../../../../utils/attributeTypeMapper';
|
||||
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||
|
||||
export interface FieldRendererProps {
|
||||
param: NodeTypeParameter;
|
||||
|
|
@ -27,7 +28,6 @@ export type FieldRendererComponent = ComponentType<FieldRendererProps>;
|
|||
import React from 'react';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||
import { toApiGraph } from '../shared/graphUtils';
|
||||
import { postUpstreamPaths } from '../../../../api/workflowApi';
|
||||
import type { CanvasNode } from '../../editor/FlowCanvas';
|
||||
|
|
@ -535,6 +535,10 @@ const CaseListEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }
|
|||
|
||||
const FieldBuilderEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||
const { t } = useLanguage();
|
||||
const ctx = useAutomation2DataFlow();
|
||||
const fieldTypeOptions = ctx?.formFieldTypes?.length
|
||||
? ctx.formFieldTypes
|
||||
: FORM_FIELD_TYPES.map((ft) => ({ id: ft, label: FORM_FIELD_TYPE_LABELS[ft] ?? ft, portType: 'str' }));
|
||||
const fields = Array.isArray(value) ? value : [];
|
||||
const addField = () => onChange([...fields, { name: '', type: 'text', label: '', required: false }]);
|
||||
const removeField = (idx: number) => onChange(fields.filter((_: unknown, i: number) => i !== idx));
|
||||
|
|
@ -550,8 +554,8 @@ const FieldBuilderEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
|
|||
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4, alignItems: 'center' }}>
|
||||
<input type="text" placeholder={t('Name')} value={String(f.name ?? '')} onChange={(e) => updateField(i, 'name', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<select value={String(f.type ?? 'text')} onChange={(e) => updateField(i, 'type', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
|
||||
{FORM_FIELD_TYPES.map(ft => (
|
||||
<option key={ft} value={ft}>{t(FORM_FIELD_TYPE_LABELS[ft])}</option>
|
||||
{fieldTypeOptions.map((ft) => (
|
||||
<option key={ft.id} value={ft.id}>{t(ft.label)}</option>
|
||||
))}
|
||||
<option value="group">{t('Gruppe')}</option>
|
||||
</select>
|
||||
|
|
@ -585,8 +589,8 @@ const FieldBuilderEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
|
|||
}}
|
||||
style={{ flex: 1, minWidth: 80, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}
|
||||
>
|
||||
{FORM_FIELD_TYPES.map(ft => (
|
||||
<option key={ft} value={ft}>{t(FORM_FIELD_TYPE_LABELS[ft])}</option>
|
||||
{fieldTypeOptions.map((ft) => (
|
||||
<option key={ft.id} value={ft.id}>{t(ft.label)}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -78,7 +78,9 @@ function _markIterableCandidates(paths: PickablePath[], expectedParamType?: stri
|
|||
function _deriveFormPortSchemaFromParams(
|
||||
node: { parameters?: Record<string, unknown> },
|
||||
paramKey: string,
|
||||
formTypeToPort: Record<string, string> = {},
|
||||
): PortSchema | undefined {
|
||||
const resolvePortType = (rawType: string) => formTypeToPort[rawType] ?? rawType;
|
||||
const raw = node.parameters?.[paramKey];
|
||||
if (!Array.isArray(raw)) return undefined;
|
||||
const fields: Array<{ name: string; type: string; description: string | Record<string, string>; required: boolean }> = [];
|
||||
|
|
@ -90,8 +92,8 @@ function _deriveFormPortSchemaFromParams(
|
|||
let description: string | Record<string, string> = rec.name;
|
||||
if (typeof lab === 'string') description = lab;
|
||||
else if (lab && typeof lab === 'object') description = lab as Record<string, string>;
|
||||
const ftype = typeof rec.type === 'string' ? rec.type : 'str';
|
||||
if (ftype === 'group' && Array.isArray(rec.fields)) {
|
||||
const rawType = typeof rec.type === 'string' ? rec.type : 'str';
|
||||
if (rawType === 'group' && Array.isArray(rec.fields)) {
|
||||
for (const sub of rec.fields as Record<string, unknown>[]) {
|
||||
if (!sub || typeof sub.name !== 'string') continue;
|
||||
const sl = sub.label;
|
||||
|
|
@ -100,7 +102,7 @@ function _deriveFormPortSchemaFromParams(
|
|||
else if (sl && typeof sl === 'object') sdesc = sl as Record<string, string>;
|
||||
fields.push({
|
||||
name: `${rec.name}.${sub.name}`,
|
||||
type: typeof sub.type === 'string' ? sub.type : 'str',
|
||||
type: resolvePortType(typeof sub.type === 'string' ? sub.type : 'str'),
|
||||
description: sdesc,
|
||||
required: Boolean(sub.required),
|
||||
});
|
||||
|
|
@ -109,7 +111,7 @@ function _deriveFormPortSchemaFromParams(
|
|||
}
|
||||
fields.push({
|
||||
name: rec.name,
|
||||
type: ftype,
|
||||
type: resolvePortType(rawType),
|
||||
description,
|
||||
required: Boolean(rec.required),
|
||||
});
|
||||
|
|
@ -151,6 +153,7 @@ function _resolveSchemaForNode(
|
|||
connections: Array<{ source: string; target: string; sourceOutput?: number }>,
|
||||
catalog: Record<string, PortSchema>,
|
||||
visited: Set<string> = new Set(),
|
||||
formTypeToPort: Record<string, string> = {},
|
||||
): PortSchema | undefined {
|
||||
if (visited.has(nodeId)) return undefined;
|
||||
visited.add(nodeId);
|
||||
|
|
@ -170,10 +173,10 @@ function _resolveSchemaForNode(
|
|||
const schemaSpec = port0.schema;
|
||||
if (typeof schemaSpec === 'object' && schemaSpec !== null && schemaSpec.kind === 'fromGraph') {
|
||||
const paramKey = schemaSpec.parameter ?? 'fields';
|
||||
return _deriveFormPortSchemaFromParams(node, paramKey);
|
||||
return _deriveFormPortSchemaFromParams(node, paramKey, formTypeToPort);
|
||||
}
|
||||
if (port0.dynamic && port0.deriveFrom) {
|
||||
return _deriveFormPortSchemaFromParams(node, port0.deriveFrom);
|
||||
return _deriveFormPortSchemaFromParams(node, port0.deriveFrom, formTypeToPort);
|
||||
}
|
||||
if (typeof schemaSpec === 'string' && schemaSpec !== 'Transit') {
|
||||
return catalog[schemaSpec];
|
||||
|
|
@ -182,7 +185,7 @@ function _resolveSchemaForNode(
|
|||
// Transit: follow the incoming connection to find the real producer
|
||||
const incoming = connections.find((c) => c.target === nodeId);
|
||||
if (!incoming) return undefined;
|
||||
return _resolveSchemaForNode(incoming.source, nodes, nodeTypes, connections, catalog, visited);
|
||||
return _resolveSchemaForNode(incoming.source, nodes, nodeTypes, connections, catalog, visited, formTypeToPort);
|
||||
}
|
||||
|
||||
export const DataPicker: React.FC<DataPickerProps> = ({ open,
|
||||
|
|
@ -228,6 +231,9 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
|
|||
const catalog = ctx?.portTypeCatalog ?? {};
|
||||
const systemVars = ctx?.systemVariables ?? {};
|
||||
const nodeTypes = ctx?.nodeTypes ?? [];
|
||||
const formTypeToPort: Record<string, string> = Object.fromEntries(
|
||||
(ctx?.formFieldTypes ?? []).map((f) => [f.id, f.portType])
|
||||
);
|
||||
|
||||
const toggleExpand = (nodeId: string) => {
|
||||
setExpandedNodes((prev) => {
|
||||
|
|
@ -395,6 +401,8 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
|
|||
nodeTypes,
|
||||
connections,
|
||||
catalog,
|
||||
new Set(),
|
||||
formTypeToPort,
|
||||
);
|
||||
const schemaPaths = _buildPathsFromSchema(resolvedSchema, catalog);
|
||||
const annotated = _markIterableCandidates(
|
||||
|
|
|
|||
|
|
@ -69,7 +69,11 @@ export function createRef(nodeId: string, path: (string | number)[] = [], expect
|
|||
return { type: 'ref', nodeId, path, ...(expectedType ? { expectedType } : {}) };
|
||||
}
|
||||
|
||||
/** Structural type compatibility (best-effort; same as gateway soft rules). */
|
||||
/**
|
||||
* Structural type compatibility using the canonical type vocabulary: str / int / float / bool / Any.
|
||||
* All node parameters and form field schemas must use these types (no `string`, `number`, `boolean`
|
||||
* aliases) so no alias-mapping is needed here.
|
||||
*/
|
||||
export function isCompatible(producedType: string, expectedType: string): 'ok' | 'coerce' | 'mismatch' {
|
||||
if (!expectedType || !producedType) return 'ok';
|
||||
if (producedType === expectedType) return 'ok';
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type { NodeConfigRendererProps } from '../shared/types';
|
|||
import type { FormField } from '../shared/types';
|
||||
import { FORM_FIELD_TYPES, FORM_FIELD_TYPE_LABELS } from '../../../../utils/attributeTypeMapper';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
|
|
@ -28,6 +29,10 @@ function _parseFields(params: Record<string, unknown>, t: (key: string) => strin
|
|||
|
||||
export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
||||
const { t } = useLanguage();
|
||||
const ctx = useAutomation2DataFlow();
|
||||
const fieldTypeOptions = ctx?.formFieldTypes?.length
|
||||
? ctx.formFieldTypes
|
||||
: FORM_FIELD_TYPES.map((ft) => ({ id: ft, label: FORM_FIELD_TYPE_LABELS[ft] ?? ft, portType: 'str' }));
|
||||
const fields = useMemo(() => _parseFields(params, t), [params, t]);
|
||||
|
||||
const setFields = (next: FormField[]) => {
|
||||
|
|
@ -73,8 +78,8 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
setFields(next);
|
||||
}}
|
||||
>
|
||||
{FORM_FIELD_TYPES.map(ft => (
|
||||
<option key={ft} value={ft}>{t(FORM_FIELD_TYPE_LABELS[ft])}</option>
|
||||
{fieldTypeOptions.map((ft) => (
|
||||
<option key={ft.id} value={ft.id}>{t(ft.label)}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
|
|
|
|||
Loading…
Reference in a new issue