node handover standartisiert, kein hardcoden mehr, inhalt extraktion node verbessert, output ports vereinheitlicht mit user im blick
This commit is contained in:
parent
0941b9e0ad
commit
5ff75a63e3
5 changed files with 198 additions and 75 deletions
|
|
@ -32,6 +32,10 @@ export interface PortField {
|
||||||
enumValues?: string[] | null;
|
enumValues?: string[] | null;
|
||||||
/** When true, surface at the top of the DataPicker as the most common/recommended pick. */
|
/** When true, surface at the top of the DataPicker as the most common/recommended pick. */
|
||||||
recommended?: boolean;
|
recommended?: boolean;
|
||||||
|
/** Human label from portTypeCatalog (backend). Preferred over technical path in DataPicker. */
|
||||||
|
pickerLabel?: string | null;
|
||||||
|
/** Backend: segment for one list element (between List field and nested field). */
|
||||||
|
pickerItemLabel?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PortSchema {
|
export interface PortSchema {
|
||||||
|
|
@ -39,6 +43,20 @@ export interface PortSchema {
|
||||||
fields: PortField[];
|
fields: PortField[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One pickable binding — defined on ``outputPorts[n].dataPickOptions`` (authoritative list from gateway). */
|
||||||
|
export interface DataPickOption {
|
||||||
|
path: (string | number)[];
|
||||||
|
pickerLabel: string;
|
||||||
|
detail?: string;
|
||||||
|
recommended?: boolean;
|
||||||
|
iterable?: boolean;
|
||||||
|
/** For display and optional strict compatibility (e.g. str, Any). */
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Prefer ``outputPorts[].dataPickOptions``; kept for older payloads. */
|
||||||
|
export type OutputPickHint = DataPickOption;
|
||||||
|
|
||||||
export interface InputPortDef {
|
export interface InputPortDef {
|
||||||
accepts: string[];
|
accepts: string[];
|
||||||
}
|
}
|
||||||
|
|
@ -53,6 +71,11 @@ export interface OutputPortDef {
|
||||||
schema: string | GraphDefinedSchemaRef;
|
schema: string | GraphDefinedSchemaRef;
|
||||||
dynamic?: boolean;
|
dynamic?: boolean;
|
||||||
deriveFrom?: string;
|
deriveFrom?: string;
|
||||||
|
/**
|
||||||
|
* When set, DataPicker uses **only** this list for that port (no portTypeCatalog expansion).
|
||||||
|
* Authoritative, like `parameters` for node configuration.
|
||||||
|
*/
|
||||||
|
dataPickOptions?: DataPickOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NodeType {
|
export interface NodeType {
|
||||||
|
|
@ -76,7 +99,6 @@ export interface NodeType {
|
||||||
action?: string;
|
action?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NodeTypeCategory {
|
export interface NodeTypeCategory {
|
||||||
id: string;
|
id: string;
|
||||||
label: Record<string, string> | string;
|
label: Record<string, string> | string;
|
||||||
|
|
|
||||||
|
|
@ -1771,6 +1771,39 @@
|
||||||
border-color: var(--primary-color, #007bff);
|
border-color: var(--primary-color, #007bff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Curated picker: disclose technical / rare paths behind a single quiet control. */
|
||||||
|
.dataPickerCuratedToggle {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
padding: 0.38rem 0.55rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary, #5c6370);
|
||||||
|
background: var(--bg-primary, #fff);
|
||||||
|
border: 1px dashed var(--border-color, #cfd4dc);
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
transition: background 0.12s, color 0.12s, border-color 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataPickerCuratedToggle:hover {
|
||||||
|
color: var(--text-primary, #333);
|
||||||
|
background: var(--bg-secondary, #f4f6f8);
|
||||||
|
border-color: var(--border-color, #b8c0cc);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataPickerCuratedDivider {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-secondary, #8a9199);
|
||||||
|
margin: 0.75rem 0 0.35rem 0;
|
||||||
|
padding-left: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dynamic Value Field */
|
/* Dynamic Value Field */
|
||||||
.dynamicValueField {
|
.dynamicValueField {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -749,9 +749,20 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
|
||||||
|
|
||||||
const configurableSelected =
|
const configurableSelected =
|
||||||
selectedNode &&
|
selectedNode &&
|
||||||
['input.', 'ai.', 'email.', 'sharepoint.', 'clickup.', 'trigger.', 'flow.', 'file.', 'trustee.'].some((p) =>
|
[
|
||||||
selectedNode.type.startsWith(p)
|
'input.',
|
||||||
);
|
'ai.',
|
||||||
|
'email.',
|
||||||
|
'sharepoint.',
|
||||||
|
'clickup.',
|
||||||
|
'trigger.',
|
||||||
|
'flow.',
|
||||||
|
'file.',
|
||||||
|
'trustee.',
|
||||||
|
'context.',
|
||||||
|
'data.',
|
||||||
|
'redmine.',
|
||||||
|
].some((p) => selectedNode.type.startsWith(p));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
|
|
||||||
|
|
@ -123,11 +123,11 @@ function _renderPicker(props?: { expectedParamType?: string; onPick?: (r: DataRe
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
describe('DataPicker — generic-object drill-down (T8)', () => {
|
describe('DataPicker — generic-object drill-down (T8)', () => {
|
||||||
it('renders the wildcard "documents → * → name" path when drilling into List[UdmDocument]', async () => {
|
it('renders the wildcard "documents › * › name" path when drilling into List[UdmDocument]', async () => {
|
||||||
_renderPicker();
|
_renderPicker();
|
||||||
await userEvent.click(screen.getByText(/^up$/));
|
await userEvent.click(screen.getByText(/^up$/));
|
||||||
expect(screen.getByText(/documents → \* → name/)).toBeInTheDocument();
|
expect(screen.getByText(/documents › \* › name/)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/documents → \* → mimeType/)).toBeInTheDocument();
|
expect(screen.getByText(/documents › \* › mimeType/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('lists the wholeOutput, top-level fields, and drilled fields together', async () => {
|
it('lists the wholeOutput, top-level fields, and drilled fields together', async () => {
|
||||||
|
|
@ -137,7 +137,7 @@ describe('DataPicker — generic-object drill-down (T8)', () => {
|
||||||
expect(screen.getByText('count')).toBeInTheDocument();
|
expect(screen.getByText('count')).toBeInTheDocument();
|
||||||
expect(screen.getByText('meta')).toBeInTheDocument();
|
expect(screen.getByText('meta')).toBeInTheDocument();
|
||||||
// Multiple drilled candidates exist (name, mimeType, sizeBytes, _success, _error).
|
// Multiple drilled candidates exist (name, mimeType, sizeBytes, _success, _error).
|
||||||
expect(screen.getAllByText(/documents → \*/).length).toBeGreaterThanOrEqual(2);
|
expect(screen.getAllByText(/documents › \*/).length).toBeGreaterThanOrEqual(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -150,14 +150,14 @@ describe('DataPicker — strict type filtering (T7)', () => {
|
||||||
_renderPicker({ expectedParamType: 'str' });
|
_renderPicker({ expectedParamType: 'str' });
|
||||||
expect(screen.getByLabelText(/Nur kompatible/i)).toBeChecked();
|
expect(screen.getByLabelText(/Nur kompatible/i)).toBeChecked();
|
||||||
await userEvent.click(screen.getByText(/^up$/));
|
await userEvent.click(screen.getByText(/^up$/));
|
||||||
// documents (List[UdmDocument]) is a hard mismatch → must be hidden.
|
// documents (List[UdmDocument]) is a hard mismatch → shown with warning (not removed).
|
||||||
expect(screen.queryByText('documents')).not.toBeInTheDocument();
|
expect(screen.getByText('documents')).toBeInTheDocument();
|
||||||
// meta (str) is exact match → kept.
|
// meta (str) is exact match → kept.
|
||||||
expect(screen.getByText('meta')).toBeInTheDocument();
|
expect(screen.getByText('meta')).toBeInTheDocument();
|
||||||
// count (int) is "coerce" against str → kept (coerce is allowed in strict mode).
|
// count (int) is "coerce" against str → kept (coerce is allowed in strict mode).
|
||||||
expect(screen.getByText('count')).toBeInTheDocument();
|
expect(screen.getByText('count')).toBeInTheDocument();
|
||||||
// Drilled wildcard candidates of type str (name, mimeType) remain.
|
// Drilled wildcard candidates of type str (name, mimeType) remain.
|
||||||
expect(screen.getByText(/documents → \* → name/)).toBeInTheDocument();
|
expect(screen.getByText(/documents › \* › name/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows all fields after the user disables the strict toggle', async () => {
|
it('shows all fields after the user disables the strict toggle', async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Automation2 Flow Editor - Schema-based Data Picker.
|
* Automation2 Flow Editor - Schema-based Data Picker.
|
||||||
* Builds pickable paths from portTypeCatalog + node outputPorts.
|
* Builds pickable paths from portTypeCatalog + node outputPorts, or from
|
||||||
|
* outputPorts[n].dataPickOptions when the backend defines an explicit list (authoritative).
|
||||||
* Resolves Transit chains to show the real upstream schema.
|
* Resolves Transit chains to show the real upstream schema.
|
||||||
* Includes a System Variables section.
|
* Includes a System Variables section.
|
||||||
*/
|
*/
|
||||||
|
|
@ -9,7 +10,7 @@ import React, { useMemo, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { createRef, createSystemVar, type DataRef, type SystemVarRef, isCompatible } from './dataRef';
|
import { createRef, createSystemVar, type DataRef, type SystemVarRef, isCompatible } from './dataRef';
|
||||||
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||||
import type { GraphDefinedSchemaRef, NodeType, PortSchema } from '../../../../api/workflowApi';
|
import type { DataPickOption, GraphDefinedSchemaRef, NodeType, PortField, PortSchema } from '../../../../api/workflowApi';
|
||||||
import { findLoopAncestorIds } from './scopeHelpers';
|
import { findLoopAncestorIds } from './scopeHelpers';
|
||||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||||
|
|
||||||
|
|
@ -39,14 +40,28 @@ interface PickablePath {
|
||||||
typeMismatch?: boolean;
|
typeMismatch?: boolean;
|
||||||
/** Surfaced at the top of the list as the most common / recommended pick. */
|
/** Surfaced at the top of the list as the most common / recommended pick. */
|
||||||
recommended?: boolean;
|
recommended?: boolean;
|
||||||
|
/** Tooltip (Katalog oder Backend-Hinweistext). */
|
||||||
|
detail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _LIST_INNER_RE = /^List\[(.+)\]$/;
|
const _LIST_INNER_RE = /^List\[(.+)\]$/;
|
||||||
|
|
||||||
|
function _fieldSegHuman(field: PortField): string {
|
||||||
|
const picker = field.pickerLabel;
|
||||||
|
if (typeof picker === 'string' && picker.trim()) return picker.trim();
|
||||||
|
return field.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _detailFromField(description: unknown): string | undefined {
|
||||||
|
if (typeof description === 'string' && description.trim()) return description.trim();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function _buildPathsFromSchema(
|
function _buildPathsFromSchema(
|
||||||
schema: PortSchema | undefined,
|
schema: PortSchema | undefined,
|
||||||
catalog: Record<string, PortSchema>,
|
catalog: Record<string, PortSchema>,
|
||||||
basePath: (string | number)[] = [],
|
basePath: (string | number)[] = [],
|
||||||
|
baseSegments: string[] = [],
|
||||||
depth = 0,
|
depth = 0,
|
||||||
): PickablePath[] {
|
): PickablePath[] {
|
||||||
if (!schema || !schema.fields || depth > 8) return [];
|
if (!schema || !schema.fields || depth > 8) return [];
|
||||||
|
|
@ -64,21 +79,43 @@ function _buildPathsFromSchema(
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const field of schema.fields) {
|
for (const field of schema.fields) {
|
||||||
|
const segHuman = _fieldSegHuman(field);
|
||||||
const fieldPath = [...basePath, field.name];
|
const fieldPath = [...basePath, field.name];
|
||||||
const label = fieldPath.map(String).join(' → ');
|
const label =
|
||||||
result.push({ path: fieldPath, label, type: field.type, recommended: field.recommended ?? false });
|
baseSegments.length > 0
|
||||||
|
? `${baseSegments.join(' › ')} › ${segHuman}`
|
||||||
|
: segHuman;
|
||||||
|
const detail = _detailFromField(field.description);
|
||||||
|
result.push({
|
||||||
|
path: fieldPath,
|
||||||
|
label,
|
||||||
|
type: field.type,
|
||||||
|
recommended: field.recommended ?? false,
|
||||||
|
detail,
|
||||||
|
});
|
||||||
const m = typeof field.type === 'string' ? field.type.match(_LIST_INNER_RE) : null;
|
const m = typeof field.type === 'string' ? field.type.match(_LIST_INNER_RE) : null;
|
||||||
const inner = m?.[1]?.trim();
|
const inner = m?.[1]?.trim();
|
||||||
if (inner && catalog[inner]) {
|
if (inner && catalog[inner]) {
|
||||||
// Generic List drill-down: use '*' wildcard so the engine maps each item.
|
const pil = typeof field.pickerItemLabel === 'string' ? field.pickerItemLabel.trim() : '';
|
||||||
result.push(..._buildPathsFromSchema(catalog[inner], catalog, [...fieldPath, '*'], depth + 1));
|
const itemBridge = pil || '*';
|
||||||
|
const nextSegments = [...baseSegments, segHuman, itemBridge];
|
||||||
|
result.push(..._buildPathsFromSchema(catalog[inner], catalog, [...fieldPath, '*'], nextSegments, depth + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.push({ path: [...basePath, '_success'], label: [...basePath, '_success'].map(String).join(' → '), type: 'bool' });
|
result.push({
|
||||||
result.push({ path: [...basePath, '_error'], label: [...basePath, '_error'].map(String).join(' → '), type: 'str' });
|
path: [...basePath, '_success'],
|
||||||
|
label:
|
||||||
|
baseSegments.length > 0 ? `${baseSegments.join(' › ')} › Erfolgskennzeichen` : '_success',
|
||||||
|
type: 'bool',
|
||||||
|
});
|
||||||
|
result.push({
|
||||||
|
path: [...basePath, '_error'],
|
||||||
|
label:
|
||||||
|
baseSegments.length > 0 ? `${baseSegments.join(' › ')} › Fehlermeldung` : '_error',
|
||||||
|
type: 'str',
|
||||||
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Annotate each candidate with `iterable=true` if it is `List[X]` and the
|
/** Annotate each candidate with `iterable=true` if it is `List[X]` and the
|
||||||
* consumer expects `X`. Used to render a "Iterieren als Loop"-Vorschlag. */
|
* consumer expects `X`. Used to render a "Iterieren als Loop"-Vorschlag. */
|
||||||
function _markIterableCandidates(paths: PickablePath[], expectedParamType?: string): PickablePath[] {
|
function _markIterableCandidates(paths: PickablePath[], expectedParamType?: string): PickablePath[] {
|
||||||
|
|
@ -162,6 +199,18 @@ function _buildPathsFromPreview(
|
||||||
return [{ path: [...basePath], label: pathLabel }];
|
return [{ path: [...basePath], label: pathLabel }];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gateway ``outputPorts[n].dataPickOptions`` — authoritative; no client-side catalog merge. */
|
||||||
|
function _pathsFromDataPickOptions(options: DataPickOption[]): PickablePath[] {
|
||||||
|
return options.map((o) => ({
|
||||||
|
path: [...o.path],
|
||||||
|
label: o.pickerLabel,
|
||||||
|
type: o.type,
|
||||||
|
recommended: Boolean(o.recommended),
|
||||||
|
iterable: Boolean(o.iterable),
|
||||||
|
detail: typeof o.detail === 'string' ? o.detail.trim() : undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
function _resolveSchemaForNode(
|
function _resolveSchemaForNode(
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
nodes: Array<{ id: string; type?: string; parameters?: Record<string, unknown> }>,
|
nodes: Array<{ id: string; type?: string; parameters?: Record<string, unknown> }>,
|
||||||
|
|
@ -332,7 +381,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
|
||||||
const loopLabel = loopNode ? getNodeLabel(loopNode as { id: string; title?: string }) : loopId;
|
const loopLabel = loopNode ? getNodeLabel(loopNode as { id: string; title?: string }) : loopId;
|
||||||
const loopSchema = catalog.LoopItem;
|
const loopSchema = catalog.LoopItem;
|
||||||
const loopPaths = loopSchema
|
const loopPaths = loopSchema
|
||||||
? _buildPathsFromSchema(loopSchema, catalog, [], 0).filter((p) => !String(p.path[p.path.length - 1]).startsWith('_'))
|
? _buildPathsFromSchema(loopSchema, catalog, [], [], 0).filter((p) => !String(p.path[p.path.length - 1]).startsWith('_'))
|
||||||
: [
|
: [
|
||||||
{ path: ['currentItem'], label: 'currentItem', type: 'Any' },
|
{ path: ['currentItem'], label: 'currentItem', type: 'Any' },
|
||||||
{ path: ['currentIndex'], label: 'currentIndex', type: 'int' },
|
{ path: ['currentIndex'], label: 'currentIndex', type: 'int' },
|
||||||
|
|
@ -423,24 +472,33 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
|
||||||
const typeLabel = nodeTypeDef?.label ?? node?.type ?? '';
|
const typeLabel = nodeTypeDef?.label ?? node?.type ?? '';
|
||||||
const isExpanded = expandedNodes.has(nodeId);
|
const isExpanded = expandedNodes.has(nodeId);
|
||||||
|
|
||||||
const resolvedSchema = _resolveSchemaForNode(
|
const port0Def = nodeTypeDef?.outputPorts?.[0];
|
||||||
nodeId,
|
const backendPick =
|
||||||
nodes,
|
port0Def?.dataPickOptions &&
|
||||||
nodeTypes,
|
Array.isArray(port0Def.dataPickOptions) &&
|
||||||
connections,
|
port0Def.dataPickOptions.length > 0;
|
||||||
catalog,
|
|
||||||
new Set(),
|
let schemaPaths: PickablePath[];
|
||||||
formTypeToPort,
|
if (backendPick) {
|
||||||
);
|
schemaPaths = _pathsFromDataPickOptions(port0Def!.dataPickOptions!);
|
||||||
const schemaPaths = _buildPathsFromSchema(resolvedSchema, catalog);
|
} else {
|
||||||
|
const resolvedSchema = _resolveSchemaForNode(
|
||||||
|
nodeId,
|
||||||
|
nodes,
|
||||||
|
nodeTypes,
|
||||||
|
connections,
|
||||||
|
catalog,
|
||||||
|
new Set(),
|
||||||
|
formTypeToPort,
|
||||||
|
);
|
||||||
|
schemaPaths = _buildPathsFromSchema(resolvedSchema, catalog);
|
||||||
|
}
|
||||||
const annotated = _markIterableCandidates(
|
const annotated = _markIterableCandidates(
|
||||||
schemaPaths.length > 0
|
schemaPaths.length > 0
|
||||||
? schemaPaths
|
? schemaPaths
|
||||||
: _buildPathsFromPreview(nodeOutputsPreview[nodeId], [], t('(ganze Ausgabe)')),
|
: _buildPathsFromPreview(nodeOutputsPreview[nodeId], [], t('(ganze Ausgabe)')),
|
||||||
expectedParamType,
|
expectedParamType,
|
||||||
);
|
);
|
||||||
// Always show all paths; mark mismatches as a visual warning instead of hiding them.
|
|
||||||
// Recommended entries bubble to the top.
|
|
||||||
const markedPaths = annotated.map((p) => ({
|
const markedPaths = annotated.map((p) => ({
|
||||||
...p,
|
...p,
|
||||||
typeMismatch:
|
typeMismatch:
|
||||||
|
|
@ -450,7 +508,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
|
||||||
!p.iterable &&
|
!p.iterable &&
|
||||||
isCompatible(p.type!, expectedParamType!) === 'mismatch',
|
isCompatible(p.type!, expectedParamType!) === 'mismatch',
|
||||||
}));
|
}));
|
||||||
const paths = [
|
const orderedPaths = [
|
||||||
...markedPaths.filter((p) => p.recommended),
|
...markedPaths.filter((p) => p.recommended),
|
||||||
...markedPaths.filter((p) => !p.recommended),
|
...markedPaths.filter((p) => !p.recommended),
|
||||||
];
|
];
|
||||||
|
|
@ -472,56 +530,55 @@ export const DataPicker: React.FC<DataPickerProps> = ({ open,
|
||||||
</button>
|
</button>
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className={styles.dataPickerTree}>
|
<div className={styles.dataPickerTree}>
|
||||||
{paths.length === 0 && (
|
{orderedPaths.length === 0 && (
|
||||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)', padding: '4px 8px' }}>
|
<div style={{ fontSize: 11, color: 'var(--text-secondary)', padding: '4px 8px' }}>
|
||||||
{t('(keine Felder verfügbar)')}
|
{t('(keine Felder verfügbar)')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{paths.map((p, i) => {
|
{orderedPaths.map((p, i) => (
|
||||||
return (
|
<div
|
||||||
<div
|
key={`${p.path.join('.')}-${i}`}
|
||||||
key={`${p.path.join('.')}-${i}`}
|
style={{ display: 'flex', alignItems: 'center', gap: 4 }}
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: 4 }}
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${styles.dataPickerLeaf}${p.recommended ? ` ${styles.dataPickerLeafRecommended}` : ''}`}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
onClick={() => handlePick(nodeId, p.path, p.type)}
|
||||||
|
title={p.detail || p.label}
|
||||||
>
|
>
|
||||||
|
{p.label}
|
||||||
|
{p.recommended && (
|
||||||
|
<span className={styles.dataPickerRecommendedPill}>
|
||||||
|
{t('Empfohlen')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{p.type && (
|
||||||
|
<span className={styles.dataPickerLeafType}>
|
||||||
|
({p.type})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{p.typeMismatch && (
|
||||||
|
<span
|
||||||
|
className={styles.dataPickerMismatchBadge}
|
||||||
|
title={t('Typ weicht ab — wird beim Ausführen konvertiert')}
|
||||||
|
>
|
||||||
|
⚠
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{p.iterable && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`${styles.dataPickerLeaf}${p.recommended ? ` ${styles.dataPickerLeafRecommended}` : ''}`}
|
className={`${styles.dataPickerLeaf} ${styles.dataPickerIterateBtn}`}
|
||||||
style={{ flex: 1 }}
|
onClick={() => handlePickIterate(nodeId, p.path, expectedParamType)}
|
||||||
onClick={() => handlePick(nodeId, p.path, p.type)}
|
title={t('Pro Element der Liste iterieren (Loop)')}
|
||||||
>
|
>
|
||||||
{p.label}
|
{t('iterieren')}
|
||||||
{p.recommended && (
|
|
||||||
<span className={styles.dataPickerRecommendedPill}>
|
|
||||||
{t('Empfohlen')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{p.type && (
|
|
||||||
<span className={styles.dataPickerLeafType}>
|
|
||||||
({p.type})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{p.typeMismatch && (
|
|
||||||
<span
|
|
||||||
className={styles.dataPickerMismatchBadge}
|
|
||||||
title={t('Typ weicht ab — wird beim Ausführen konvertiert')}
|
|
||||||
>
|
|
||||||
⚠
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
{p.iterable && (
|
)}
|
||||||
<button
|
</div>
|
||||||
type="button"
|
))}
|
||||||
className={`${styles.dataPickerLeaf} ${styles.dataPickerIterateBtn}`}
|
|
||||||
onClick={() => handlePickIterate(nodeId, p.path, expectedParamType)}
|
|
||||||
title={t('Pro Element der Liste iterieren (Loop)')}
|
|
||||||
>
|
|
||||||
{t('iterieren')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue