126 lines
4.6 KiB
TypeScript
126 lines
4.6 KiB
TypeScript
/**
|
||
* Automation2 Flow Editor - Data Picker for selecting node output references.
|
||
*/
|
||
|
||
import React, { useState } from 'react';
|
||
import { createRef, type DataRef } from './dataRef';
|
||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||
|
||
interface DataPickerProps {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
onPick: (ref: DataRef) => void;
|
||
availableSourceIds: string[];
|
||
nodes: Array<{ id: string; title?: string; type?: string }>;
|
||
nodeOutputsPreview: Record<string, unknown>;
|
||
getNodeLabel: (node: { id: string; title?: string }) => string;
|
||
}
|
||
|
||
/** Collect all pickable paths (each leads to a value the user can reference) */
|
||
function buildPickablePaths(obj: unknown, basePath: (string | number)[] = []): Array<{ path: (string | number)[]; label: string }> {
|
||
const pathLabel = basePath.length ? basePath.map(String).join(' → ') : '(ganze Ausgabe)';
|
||
if (obj == null || typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
|
||
return [{ path: [...basePath], label: pathLabel }];
|
||
}
|
||
if (Array.isArray(obj)) {
|
||
const result: Array<{ path: (string | number)[]; label: string }> = [{ path: [...basePath], label: pathLabel }];
|
||
for (let i = 0; i < Math.min(obj.length, 10); i++) {
|
||
result.push(...buildPickablePaths(obj[i], [...basePath, i]));
|
||
}
|
||
return result;
|
||
}
|
||
if (typeof obj === 'object') {
|
||
const result: Array<{ path: (string | number)[]; label: string }> = [{ path: [...basePath], label: pathLabel }];
|
||
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
||
result.push(...buildPickablePaths(v, [...basePath, k]));
|
||
}
|
||
return result;
|
||
}
|
||
return [{ path: [...basePath], label: pathLabel }];
|
||
}
|
||
|
||
export const DataPicker: React.FC<DataPickerProps> = ({
|
||
open,
|
||
onClose,
|
||
onPick,
|
||
availableSourceIds,
|
||
nodes,
|
||
nodeOutputsPreview,
|
||
getNodeLabel,
|
||
}) => {
|
||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
||
|
||
if (!open) return null;
|
||
|
||
const toggleExpand = (nodeId: string) => {
|
||
setExpandedNodes((prev) => {
|
||
const next = new Set(prev);
|
||
if (next.has(nodeId)) next.delete(nodeId);
|
||
else next.add(nodeId);
|
||
return next;
|
||
});
|
||
};
|
||
|
||
const handlePick = (nodeId: string, path: (string | number)[]) => {
|
||
onPick(createRef(nodeId, path));
|
||
onClose();
|
||
};
|
||
|
||
return (
|
||
<div className={styles.dataPickerOverlay} onClick={onClose}>
|
||
<div className={styles.dataPickerModal} onClick={(e) => e.stopPropagation()}>
|
||
<div className={styles.dataPickerHeader}>
|
||
<h4 className={styles.dataPickerTitle}>Datenquelle wählen</h4>
|
||
<button type="button" className={styles.dataPickerClose} onClick={onClose} aria-label="Schließen">
|
||
×
|
||
</button>
|
||
</div>
|
||
<div className={styles.dataPickerBody}>
|
||
{(() => {
|
||
const filteredIds = availableSourceIds.filter((nodeId) => {
|
||
const node = nodes.find((n) => n.id === nodeId);
|
||
return node?.type !== 'trigger.manual';
|
||
});
|
||
if (filteredIds.length === 0) {
|
||
return <p className={styles.dataPickerEmpty}>Keine vorherigen Nodes verfügbar.</p>;
|
||
}
|
||
return filteredIds.map((nodeId) => {
|
||
const node = nodes.find((n) => n.id === nodeId);
|
||
const preview = nodeOutputsPreview[nodeId];
|
||
const label = node ? getNodeLabel(node) : nodeId;
|
||
const paths = buildPickablePaths(preview);
|
||
const isExpanded = expandedNodes.has(nodeId);
|
||
|
||
return (
|
||
<div key={nodeId} className={styles.dataPickerNodeSection}>
|
||
<button
|
||
type="button"
|
||
className={styles.dataPickerNodeHeader}
|
||
onClick={() => toggleExpand(nodeId)}
|
||
>
|
||
<span className={styles.dataPickerExpandIcon}>{isExpanded ? '▼' : '▶'}</span>
|
||
<span className={styles.dataPickerNodeLabel}>{label}</span>
|
||
</button>
|
||
{isExpanded && (
|
||
<div className={styles.dataPickerTree}>
|
||
{paths.map((p, i) => (
|
||
<button
|
||
key={`${p.path.join('.')}-${i}`}
|
||
type="button"
|
||
className={styles.dataPickerLeaf}
|
||
onClick={() => handlePick(nodeId, p.path)}
|
||
>
|
||
{p.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
});
|
||
})()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|