ui-nyla/src/components/FlowEditor/nodes/shared/LoopItemsSelect.tsx
ValueOn AG a13a158c67
Some checks failed
Deploy Nyla Frontend to Integration / deploy (push) Failing after 51s
cleaned servicebag and removed servicehub
2026-06-08 23:35:38 +02:00

215 lines
6.5 KiB
TypeScript

/**
* Loop node - Datenquelle für Iteration mit benutzerfreundlichen Labels.
* Zeigt nur iterierbare Quellen: Arrays und Objekte (Formularfelder → {name, value}).
*/
import React from 'react';
import { createRef, isRef, type DataRef } from './dataRef';
import { refToOptionValue, optionValueToRef } from './RefSourceSelect';
import { useWorkflowDataFlow } from '../../context/WorkflowDataFlowContext';
import styles from '../../editor/WorkflowFlowEditor.module.css';
import { useLanguage } from '../../../../providers/language/LanguageContext';
interface LoopOption {
ref: DataRef;
label: string;
}
function getValueAtPath(obj: unknown, path: (string | number)[]): unknown {
let current: unknown = obj;
for (const seg of path) {
if (current == null) return undefined;
const key = typeof seg === 'number' ? String(seg) : seg;
if (Array.isArray(current) && /^\d+$/.test(key)) {
current = current[parseInt(key, 10)];
} else if (typeof current === 'object' && key in (current as object)) {
current = (current as Record<string, unknown>)[key];
} else return undefined;
}
return current;
}
/** Build iterable options with friendly labels for Loop node */
function buildLoopOptions(
sourceIds: string[],
nodes: Array<{ id: string; type?: string; title?: string; parameters?: Record<string, unknown> }>,
nodeOutputsPreview: Record<string, unknown>,
getNodeLabel: (n: { id: string; type?: string; title?: string }) => string,
translate: (key: string) => string
): LoopOption[] {
const options: LoopOption[] = [];
for (const nodeId of sourceIds) {
const node = nodes.find((n) => n.id === nodeId);
if (node?.type === 'trigger.manual') continue;
const nodeLabel = getNodeLabel(node ?? { id: nodeId });
const preview = nodeOutputsPreview[nodeId];
// Special cases with friendly labels
if (node?.type === 'trigger.form') {
options.push({
ref: createRef(nodeId, ['payload']),
label: `${translate('Alle Formularfelder')} (${nodeLabel})`,
});
const filesVal = getValueAtPath(preview, ['files']);
if (Array.isArray(filesVal)) {
options.push({
ref: createRef(nodeId, ['files']),
label: `${translate('Alle Dateien aus Formular')} (${nodeLabel})`,
});
}
continue;
}
if (node?.type === 'input.form') {
options.push({
ref: createRef(nodeId, []),
label: `${translate('Alle Formularfelder')} (${nodeLabel})`,
});
continue;
}
if (node?.type === 'input.upload') {
options.push({
ref: createRef(nodeId, ['files']),
label: `${translate('Alle hochgeladenen Dateien')} (${nodeLabel})`,
});
options.push({
ref: createRef(nodeId, ['fileIds']),
label: `${translate('Alle Datei-IDs')} (${nodeLabel})`,
});
continue;
}
if (node?.type === 'flow.loop') {
options.push({
ref: createRef(nodeId, ['items']),
label: `${translate('Alle Elemente aus Schleife')} (${nodeLabel})`,
});
continue;
}
if (node?.type === 'email.searchEmail') {
options.push({
ref: createRef(nodeId, ['data', 'searchResults', 'results']),
label: `${translate('Alle gefundenen E-Mails')} (${nodeLabel})`,
});
continue;
}
if (node?.type === 'email.checkEmail') {
options.push({
ref: createRef(nodeId, ['data', 'emails', 'emails']),
label: `${translate('Alle E-Mails')} (${nodeLabel})`,
});
continue;
}
if (node?.type === 'sharepoint.listFiles') {
options.push({
ref: createRef(nodeId, ['files']),
label: `${translate('Alle Dateien')} (${nodeLabel})`,
});
continue;
}
// Generic: find top-level arrays and root object in preview
if (preview != null && typeof preview === 'object') {
for (const [k, v] of Object.entries(preview as Record<string, unknown>)) {
const path: (string | number)[] = [k];
const pathStr = path.join('.');
if (Array.isArray(v)) {
options.push({
ref: createRef(nodeId, path),
label: `${nodeLabel}.${pathStr}`,
});
} else if (v != null && typeof v === 'object' && !Array.isArray(v)) {
const inner = v as Record<string, unknown>;
for (const [k2, v2] of Object.entries(inner)) {
if (Array.isArray(v2)) {
options.push({
ref: createRef(nodeId, [k, k2]),
label: `${nodeLabel}.${k}.${k2}`,
});
}
}
}
}
}
}
// Deduplicate by ref (path might repeat from different collection)
const seen = new Set<string>();
return options.filter((o) => {
const key = refToOptionValue(o.ref);
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
interface LoopItemsSelectProps {
value: DataRef | { type: 'value'; value: unknown } | null;
onChange: (ref: DataRef | null) => void;
placeholder?: string;
}
export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
onChange,
placeholder,
}) => {
const { t } = useLanguage();
const dataFlow = useWorkflowDataFlow();
if (!dataFlow) return null;
const sourceIds = dataFlow.getAvailableSourceIds();
if (sourceIds.length === 0) {
return (
<p className={styles.dynamicValueEmptyHint}>
{t('Keine vorherigen Nodes verbunden. Verbinden Sie zuerst Nodes mit der Schleife.')}
</p>
);
}
const options = buildLoopOptions(
sourceIds,
dataFlow.nodes,
dataFlow.nodeOutputsPreview,
dataFlow.getNodeLabel,
t
);
const ref = isRef(value) ? value : null;
const currentValue = ref ? refToOptionValue(ref) : '';
return (
<div className={styles.ifElseConditionRow}>
<label>{t('Datenquelle für Iteration')}</label>
<select
value={currentValue}
onChange={(e) => {
const v = e.target.value;
if (!v) {
onChange(null);
return;
}
const r = optionValueToRef(v);
if (r) onChange(r);
}}
className={styles.startsInput}
>
<option value="">{placeholder ?? t('Über was soll iteriert werden?')}</option>
{options.map((o) => (
<option key={refToOptionValue(o.ref)} value={refToOptionValue(o.ref)}>
{o.label}
</option>
))}
</select>
<p className={styles.nodeConfigNameHint}>
{t('Z.B. für jedes Formularfeld, jede Datei aus Upload, jede E-Mail aus Suche.')}
</p>
</div>
);
};