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:
Ida 2026-05-03 15:07:25 +02:00
parent 992c0472c6
commit 1d2d247273
8 changed files with 74 additions and 25 deletions

View file

@ -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 {

View file

@ -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}>

View file

@ -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}
>

View file

@ -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}>

View file

@ -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

View file

@ -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(

View file

@ -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';

View file

@ -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