split table into measurements and records, add units to value display
This commit is contained in:
@@ -4,7 +4,7 @@ import React, {
|
||||
import {
|
||||
ReactFlow, Background, Controls, MiniMap,
|
||||
useNodesState, useEdgesState, addEdge, useReactFlow,
|
||||
ReactFlowProvider, getNodesBounds, getViewportForBounds,
|
||||
ReactFlowProvider, getViewportForBounds,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
|
||||
@@ -18,20 +18,28 @@ import { serializeWorkflowState } from './workflowSerialization';
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────────
|
||||
|
||||
const DATA_TYPES = new Set(['DATA_FIELD', 'IMAGE', 'LINE', 'TABLE', 'COORD', 'STATS_SOURCE']);
|
||||
const DATA_TYPES = new Set([
|
||||
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE', 'ANY_TABLE',
|
||||
'COORD', 'STATS_SOURCE', 'VALUE_SOURCE',
|
||||
]);
|
||||
|
||||
const SOCKET_COMPATIBILITY = {
|
||||
STATS_SOURCE: new Set(['DATA_FIELD', 'IMAGE', 'LINE', 'TABLE']),
|
||||
STATS_SOURCE: new Set(['DATA_FIELD', 'IMAGE', 'LINE', 'RECORD_TABLE']),
|
||||
ANY_TABLE: new Set(['MEASURE_TABLE', 'RECORD_TABLE']),
|
||||
VALUE_SOURCE: new Set(['FLOAT', 'MEASURE_TABLE']),
|
||||
};
|
||||
|
||||
const TYPE_COLORS = {
|
||||
DATA_FIELD: '#ff002f',
|
||||
IMAGE: '#00ff08a0',
|
||||
LINE: '#ffbe5c',
|
||||
TABLE: '#35e2fd',
|
||||
MEASURE_TABLE:'#35e2fd',
|
||||
RECORD_TABLE:'#fbbf24',
|
||||
ANY_TABLE: '#67e8f9',
|
||||
COORD: '#e91ed1',
|
||||
FLOAT: '#7dd3fc',
|
||||
STATS_SOURCE:'#c084fc',
|
||||
VALUE_SOURCE:'#60a5fa',
|
||||
};
|
||||
|
||||
const NODE_TYPES = { custom: CustomNode };
|
||||
@@ -56,6 +64,46 @@ function socketTypesCompatible(sourceType, targetType) {
|
||||
return !!accepted?.has(sourceType);
|
||||
}
|
||||
|
||||
function getRenderedNodeBounds(nodes) {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
let found = false;
|
||||
|
||||
for (const node of nodes) {
|
||||
const selectorId = typeof CSS !== 'undefined' && typeof CSS.escape === 'function'
|
||||
? CSS.escape(String(node.id))
|
||||
: String(node.id);
|
||||
const el = document.querySelector(`.react-flow__node[data-id="${selectorId}"]`);
|
||||
const width = el?.offsetWidth || node.measured?.width || node.width || 0;
|
||||
const height = el?.offsetHeight || node.measured?.height || node.height || 0;
|
||||
const x = node.positionAbsolute?.x ?? node.position?.x ?? 0;
|
||||
const y = node.positionAbsolute?.y ?? node.position?.y ?? 0;
|
||||
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x + width);
|
||||
maxY = Math.max(maxY, y + height);
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: Math.max(1, maxX - minX),
|
||||
height: Math.max(1, maxY - minY),
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForImageElement(img) {
|
||||
if (img.complete && img.naturalWidth > 0) return;
|
||||
if (typeof img.decode === 'function') {
|
||||
@@ -463,7 +511,12 @@ function Flow() {
|
||||
updateNodeData(msg.data.node_id, { tableRows: msg.data.rows });
|
||||
break;
|
||||
case 'scalar':
|
||||
updateNodeData(msg.data.node_id, { scalarValue: msg.data.value });
|
||||
updateNodeData(msg.data.node_id, {
|
||||
scalarValue: {
|
||||
value: msg.data.value,
|
||||
unit: typeof msg.data.unit === 'string' ? msg.data.unit : '',
|
||||
},
|
||||
});
|
||||
break;
|
||||
case 'mesh3d':
|
||||
updateNodeData(msg.data.node_id, { meshData: msg.data.mesh });
|
||||
@@ -797,7 +850,10 @@ function Flow() {
|
||||
const allNodes = reactFlow.getNodes();
|
||||
if (allNodes.length === 0) throw new Error('No nodes to capture');
|
||||
|
||||
const bounds = getNodesBounds(allNodes);
|
||||
const bounds = getRenderedNodeBounds(allNodes);
|
||||
if (!bounds) {
|
||||
throw new Error('Could not determine rendered node bounds');
|
||||
}
|
||||
const pad = 0.1; // 10% margin on each side
|
||||
const imageWidth = Math.ceil(bounds.width * (1 + pad * 2));
|
||||
const imageHeight = Math.ceil(bounds.height * (1 + pad * 2));
|
||||
|
||||
@@ -8,17 +8,23 @@ const CropBoxOverlay = lazy(() => import('./CropBoxOverlay'));
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────────
|
||||
|
||||
const DATA_TYPES = new Set(['DATA_FIELD', 'IMAGE', 'LINE', 'TABLE', 'COORD', 'STATS_SOURCE']);
|
||||
const DATA_TYPES = new Set([
|
||||
'DATA_FIELD', 'IMAGE', 'LINE', 'MEASURE_TABLE', 'RECORD_TABLE', 'ANY_TABLE',
|
||||
'COORD', 'STATS_SOURCE', 'VALUE_SOURCE',
|
||||
]);
|
||||
const SOCKET_WIDGET_TYPES = new Set(['FLOAT']);
|
||||
|
||||
const TYPE_COLORS = {
|
||||
DATA_FIELD: '#3a7abf',
|
||||
IMAGE: '#4caf50',
|
||||
LINE: '#ff9800',
|
||||
TABLE: '#fdd835',
|
||||
MEASURE_TABLE:'#35e2fd',
|
||||
RECORD_TABLE:'#fbbf24',
|
||||
ANY_TABLE: '#67e8f9',
|
||||
COORD: '#e91e63',
|
||||
FLOAT: '#7dd3fc',
|
||||
STATS_SOURCE:'#c084fc',
|
||||
VALUE_SOURCE:'#60a5fa',
|
||||
};
|
||||
|
||||
const CAT_COLORS = {
|
||||
@@ -183,7 +189,42 @@ function getTableColumns(rows) {
|
||||
return columns;
|
||||
}
|
||||
|
||||
function formatTableCell(value) {
|
||||
function getMeasurementChoices(rows) {
|
||||
const names = [];
|
||||
for (const row of rows || []) {
|
||||
const quantity = row?.quantity;
|
||||
if (typeof quantity === 'string' && quantity && !names.includes(quantity)) {
|
||||
names.push(quantity);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
const SI_PREFIXES = [
|
||||
{ exp: -24, prefix: 'y' },
|
||||
{ exp: -21, prefix: 'z' },
|
||||
{ exp: -18, prefix: 'a' },
|
||||
{ exp: -15, prefix: 'f' },
|
||||
{ exp: -12, prefix: 'p' },
|
||||
{ exp: -9, prefix: 'n' },
|
||||
{ exp: -6, prefix: 'u' },
|
||||
{ exp: -3, prefix: 'm' },
|
||||
{ exp: 0, prefix: '' },
|
||||
{ exp: 3, prefix: 'k' },
|
||||
{ exp: 6, prefix: 'M' },
|
||||
{ exp: 9, prefix: 'G' },
|
||||
{ exp: 12, prefix: 'T' },
|
||||
{ exp: 15, prefix: 'P' },
|
||||
{ exp: 18, prefix: 'E' },
|
||||
{ exp: 21, prefix: 'Z' },
|
||||
{ exp: 24, prefix: 'Y' },
|
||||
];
|
||||
|
||||
const PREFIXABLE_UNITS = new Set([
|
||||
'm', 's', 'A', 'V', 'W', 'Hz', 'F', 'C', 'J', 'N', 'Pa', 'T', 'H', 'S', 'g', 'K', 'Ohm', 'ohm', 'Ω',
|
||||
]);
|
||||
|
||||
function formatNumericCell(value) {
|
||||
if (value == null) return '';
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isFinite(value)) return String(value);
|
||||
@@ -196,6 +237,48 @@ function formatTableCell(value) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function applySIPrefix(value, unit) {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||
return { valueText: formatNumericCell(value), unitText: unit };
|
||||
}
|
||||
if (typeof unit !== 'string' || !PREFIXABLE_UNITS.has(unit)) {
|
||||
return { valueText: formatNumericCell(value), unitText: unit };
|
||||
}
|
||||
if (value === 0) {
|
||||
return { valueText: '0', unitText: unit };
|
||||
}
|
||||
|
||||
const abs = Math.abs(value);
|
||||
let exp = Math.floor(Math.log10(abs) / 3) * 3;
|
||||
exp = Math.max(-24, Math.min(24, exp));
|
||||
|
||||
let scaled = value / (10 ** exp);
|
||||
if (Math.abs(scaled) >= 999.5 && exp < 24) {
|
||||
exp += 3;
|
||||
scaled = value / (10 ** exp);
|
||||
}
|
||||
|
||||
const prefix = SI_PREFIXES.find((entry) => entry.exp === exp)?.prefix ?? '';
|
||||
return {
|
||||
valueText: formatNumericCell(scaled),
|
||||
unitText: `${prefix}${unit}`,
|
||||
};
|
||||
}
|
||||
|
||||
function formatTableCell(value) {
|
||||
return formatNumericCell(value);
|
||||
}
|
||||
|
||||
function formatTableRowCell(row, column) {
|
||||
if (column === 'value' && typeof row?.unit === 'string') {
|
||||
return applySIPrefix(row?.value, row.unit).valueText;
|
||||
}
|
||||
if (column === 'unit' && typeof row?.unit === 'string') {
|
||||
return applySIPrefix(row?.value, row.unit).unitText;
|
||||
}
|
||||
return formatTableCell(row?.[column]);
|
||||
}
|
||||
|
||||
function formatScalarValue(value) {
|
||||
if (value == null || Number.isNaN(Number(value))) return '—';
|
||||
const numeric = Number(value);
|
||||
@@ -206,6 +289,43 @@ function formatScalarValue(value) {
|
||||
return numeric.toFixed(abs >= 100 ? 2 : 4).replace(/\.?0+$/, '');
|
||||
}
|
||||
|
||||
function getScalarPayload(scalarValue) {
|
||||
if (typeof scalarValue === 'number') {
|
||||
return Number.isFinite(scalarValue) ? { value: scalarValue, unit: '' } : null;
|
||||
}
|
||||
if (!scalarValue || typeof scalarValue !== 'object') return null;
|
||||
const numeric = Number(scalarValue.value);
|
||||
if (!Number.isFinite(numeric)) return null;
|
||||
return {
|
||||
value: numeric,
|
||||
unit: typeof scalarValue.unit === 'string' ? scalarValue.unit : '',
|
||||
};
|
||||
}
|
||||
|
||||
function formatScalarDisplay(scalarValue) {
|
||||
const payload = getScalarPayload(scalarValue);
|
||||
if (!payload) return null;
|
||||
|
||||
if (payload.unit) {
|
||||
if (PREFIXABLE_UNITS.has(payload.unit)) {
|
||||
const prefixed = applySIPrefix(payload.value, payload.unit);
|
||||
return {
|
||||
valueText: prefixed.valueText,
|
||||
unitText: prefixed.unitText,
|
||||
};
|
||||
}
|
||||
return {
|
||||
valueText: formatScalarValue(payload.value),
|
||||
unitText: payload.unit,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valueText: formatScalarValue(payload.value),
|
||||
unitText: '',
|
||||
};
|
||||
}
|
||||
|
||||
function getSourceTypeForInput(store, nodeId, inputName) {
|
||||
const targetHandle = `input::${inputName}::`;
|
||||
const edge = store.edges?.find((e) => e.target === nodeId && e.targetHandle?.startsWith(targetHandle));
|
||||
@@ -221,6 +341,13 @@ function getSourceNodeForInput(store, nodeId, inputName) {
|
||||
return store.nodeLookup?.get(edge.source) || store.nodes?.find((n) => n.id === edge.source) || null;
|
||||
}
|
||||
|
||||
function getWidgetSourceInputName(opts) {
|
||||
return opts?.source_type_input
|
||||
|| opts?.choices_from_table_input
|
||||
|| opts?.choices_from_measure_input
|
||||
|| Object.keys(opts?.show_when_source_type || {})[0];
|
||||
}
|
||||
|
||||
function widgetVisibleForSourceType(widget, sourceType) {
|
||||
const rules = widget?.opts?.show_when_source_type;
|
||||
if (!rules || typeof rules !== 'object') return true;
|
||||
@@ -233,15 +360,37 @@ function widgetVisibleForSourceType(widget, sourceType) {
|
||||
function NodeTable({ rows }) {
|
||||
const columns = getTableColumns(rows);
|
||||
if (columns.length === 0) return null;
|
||||
const lowerColumns = columns.map((column) => String(column).toLowerCase());
|
||||
const hasMeasurementLayout = (
|
||||
lowerColumns.length === 3
|
||||
&& lowerColumns[0] === 'quantity'
|
||||
&& lowerColumns[1] === 'value'
|
||||
&& lowerColumns[2] === 'unit'
|
||||
);
|
||||
|
||||
const getColumnClass = (column) => {
|
||||
const lower = String(column).toLowerCase();
|
||||
if (lower === 'value') return 'node-table-col-value';
|
||||
if (lower === 'unit') return 'node-table-col-unit';
|
||||
if (lower === 'quantity') return 'node-table-col-quantity';
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="node-table-wrap">
|
||||
<div className="node-table-scroll">
|
||||
<table className="node-table-grid">
|
||||
{hasMeasurementLayout && (
|
||||
<colgroup>
|
||||
<col className="node-table-col-quantity" />
|
||||
<col className="node-table-col-value" />
|
||||
<col className="node-table-col-unit" />
|
||||
</colgroup>
|
||||
)}
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
<th key={column} scope="col">{column}</th>
|
||||
<th key={column} scope="col" className={getColumnClass(column)}>{column}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -250,13 +399,17 @@ function NodeTable({ rows }) {
|
||||
<tr key={row.id ?? row.quantity ?? rowIndex}>
|
||||
{columns.map((column) => {
|
||||
const value = row?.[column];
|
||||
const displayValue = formatTableRowCell(row, column);
|
||||
return (
|
||||
<td
|
||||
key={`${rowIndex}-${column}`}
|
||||
className={typeof value === 'number' ? 'node-table-num' : ''}
|
||||
title={formatTableCell(value)}
|
||||
className={[
|
||||
getColumnClass(column),
|
||||
(typeof value === 'number' || (column === 'value' && typeof row?.value === 'number')) ? 'node-table-num' : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
title={displayValue}
|
||||
>
|
||||
{formatTableCell(value)}
|
||||
{displayValue}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
@@ -274,6 +427,7 @@ function NodeTable({ rows }) {
|
||||
function CustomNode({ id, data }) {
|
||||
const ctx = useContext(NodeContext);
|
||||
const def = data.definition;
|
||||
const scalarDisplay = formatScalarDisplay(data.scalarValue);
|
||||
|
||||
// Parse inputs into data handles and widgets
|
||||
const required = def.input.required || {};
|
||||
@@ -418,15 +572,20 @@ function CustomNode({ id, data }) {
|
||||
<div className="node-warning">{data.warning}</div>
|
||||
)}
|
||||
|
||||
{typeof data.scalarValue === 'number' && (
|
||||
{scalarDisplay && (
|
||||
<div className="node-value-display">
|
||||
<div className="node-value-label">Value</div>
|
||||
<div className="node-value-box">{formatScalarValue(data.scalarValue)}</div>
|
||||
<div className="node-value-box">
|
||||
<span className="node-value-box-number">{scalarDisplay.valueText}</span>
|
||||
{scalarDisplay.unitText && (
|
||||
<span className="node-value-box-unit">{scalarDisplay.unitText}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Widget rows */}
|
||||
{widgets.filter((w) => widgetVisibleForSourceType(w, connectedSourceTypes?.[w.opts?.source_type_input || w.opts?.choices_from_table_input || Object.keys(w.opts?.show_when_source_type || {})[0]])).map((w) => (
|
||||
{widgets.filter((w) => widgetVisibleForSourceType(w, connectedSourceTypes?.[getWidgetSourceInputName(w.opts)])).map((w) => (
|
||||
<div className={`widget-row${w.socketType ? ' widget-row-socket' : ''}`} key={w.name}>
|
||||
{w.socketType && (
|
||||
<Handle
|
||||
@@ -553,9 +712,7 @@ function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFile
|
||||
const dynamicSourceType = useStore(
|
||||
useCallback(
|
||||
(s) => {
|
||||
const inputName = opts?.source_type_input
|
||||
|| opts?.choices_from_table_input
|
||||
|| Object.keys(opts?.show_when_source_type || {})[0];
|
||||
const inputName = getWidgetSourceInputName(opts);
|
||||
if (!inputName) return null;
|
||||
return getSourceTypeForInput(s, nodeId, inputName);
|
||||
},
|
||||
@@ -568,7 +725,7 @@ function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFile
|
||||
const tableInputName = opts?.choices_from_table_input;
|
||||
if (!tableInputName) return [];
|
||||
const sourceType = getSourceTypeForInput(s, nodeId, tableInputName);
|
||||
if (sourceType !== 'TABLE') return [];
|
||||
if (sourceType !== 'RECORD_TABLE') return [];
|
||||
const sourceNode = getSourceNodeForInput(s, nodeId, tableInputName);
|
||||
const rows = sourceNode?.data?.tableRows;
|
||||
return Array.isArray(rows) ? getTableColumns(rows) : [];
|
||||
@@ -576,6 +733,20 @@ function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFile
|
||||
[nodeId, opts?.choices_from_table_input],
|
||||
),
|
||||
);
|
||||
const dynamicMeasurementChoices = useStore(
|
||||
useCallback(
|
||||
(s) => {
|
||||
const measurementInputName = opts?.choices_from_measure_input;
|
||||
if (!measurementInputName) return [];
|
||||
const sourceType = getSourceTypeForInput(s, nodeId, measurementInputName);
|
||||
if (sourceType !== 'MEASURE_TABLE') return [];
|
||||
const sourceNode = getSourceNodeForInput(s, nodeId, measurementInputName);
|
||||
const rows = sourceNode?.data?.tableRows;
|
||||
return Array.isArray(rows) ? getMeasurementChoices(rows) : [];
|
||||
},
|
||||
[nodeId, opts?.choices_from_measure_input],
|
||||
),
|
||||
);
|
||||
const dynamicTypeChoices = (() => {
|
||||
const byType = opts?.choices_by_source_type;
|
||||
if (!byType) return [];
|
||||
@@ -600,6 +771,13 @@ function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFile
|
||||
if (preferred != null) onChange(nodeId, name, preferred);
|
||||
}, [dynamicTableColumns, name, nodeId, onChange, opts?.choices_from_table_input, val]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!opts?.choices_from_measure_input || dynamicMeasurementChoices.length === 0) return;
|
||||
const current = String(val ?? '');
|
||||
if (dynamicMeasurementChoices.includes(current)) return;
|
||||
if (dynamicMeasurementChoices[0] != null) onChange(nodeId, name, dynamicMeasurementChoices[0]);
|
||||
}, [dynamicMeasurementChoices, name, nodeId, onChange, opts?.choices_from_measure_input, val]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dynamicTypeChoices.length === 0) return;
|
||||
const current = String(val ?? '');
|
||||
@@ -661,6 +839,24 @@ function WidgetControl({ widget, nodeId, value, widgetValues, onChange, openFile
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'STRING' && opts?.choices_from_measure_input && dynamicMeasurementChoices.length > 0) {
|
||||
const selected = dynamicMeasurementChoices.includes(String(val)) ? String(val) : dynamicMeasurementChoices[0];
|
||||
return (
|
||||
<>
|
||||
<label>{name}</label>
|
||||
<select
|
||||
className="nodrag"
|
||||
value={selected}
|
||||
onChange={(e) => onChange(nodeId, name, e.target.value)}
|
||||
>
|
||||
{dynamicMeasurementChoices.map((choice) => (
|
||||
<option key={choice} value={choice}>{choice}</option>
|
||||
))}
|
||||
</select>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'FILE_PICKER') {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -194,6 +194,20 @@ html, body, #root {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.node-value-box-number {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.node-value-box-unit {
|
||||
display: inline-block;
|
||||
margin-left: 0.35em;
|
||||
font-size: 0.58em;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
color: rgba(224, 242, 254, 0.82);
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* ── I/O rows ──────────────────────────────────────────────────────── */
|
||||
.io-row {
|
||||
display: flex;
|
||||
@@ -564,6 +578,8 @@ html, body, #root {
|
||||
font-family: "SF Mono", "Fira Code", monospace;
|
||||
font-size: 10px;
|
||||
color: #cbd5e1;
|
||||
table-layout: auto;
|
||||
font-variant-numeric: tabular-nums lining-nums;
|
||||
}
|
||||
|
||||
.node-table-grid th,
|
||||
@@ -594,6 +610,20 @@ html, body, #root {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.node-table-col-quantity {
|
||||
width: 46%;
|
||||
}
|
||||
|
||||
.node-table-col-value {
|
||||
width: 32%;
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.node-table-col-unit {
|
||||
width: 22%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.node-table-num {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,43 @@ function mergeDefinition(nodeData, defs) {
|
||||
};
|
||||
}
|
||||
|
||||
function getSocketType(inputDef) {
|
||||
if (!inputDef) return null;
|
||||
const [type] = Array.isArray(inputDef) ? inputDef : [inputDef];
|
||||
return Array.isArray(type) ? type[0] : type;
|
||||
}
|
||||
|
||||
function getInputType(definition, inputName) {
|
||||
const required = definition?.input?.required || {};
|
||||
const optional = definition?.input?.optional || {};
|
||||
return getSocketType(required[inputName] ?? optional[inputName]);
|
||||
}
|
||||
|
||||
function remapLegacyHandle(handleId, kind, nodeData) {
|
||||
if (typeof handleId !== 'string') return handleId;
|
||||
|
||||
const parts = handleId.split('::');
|
||||
if (parts.length !== 3 || parts[2] !== 'TABLE') return handleId;
|
||||
|
||||
if (kind === 'source' && parts[0] === 'output') {
|
||||
const outputSlot = Number.parseInt(parts[1], 10);
|
||||
const outputType = nodeData?.definition?.output?.[outputSlot];
|
||||
if (typeof outputType === 'string' && outputType !== 'TABLE') {
|
||||
return `output::${outputSlot}::${outputType}`;
|
||||
}
|
||||
return handleId;
|
||||
}
|
||||
|
||||
if (kind === 'target' && parts[0] === 'input') {
|
||||
const inputType = getInputType(nodeData?.definition, parts[1]);
|
||||
if (typeof inputType === 'string' && inputType !== 'TABLE') {
|
||||
return `input::${parts[1]}::${inputType}`;
|
||||
}
|
||||
}
|
||||
|
||||
return handleId;
|
||||
}
|
||||
|
||||
export function hydrateWorkflowState(data, defs = {}) {
|
||||
const loadedNodes = Array.isArray(data?.nodes) ? data.nodes : [];
|
||||
const loadedEdges = Array.isArray(data?.edges) ? data.edges : [];
|
||||
@@ -43,11 +80,19 @@ export function hydrateWorkflowState(data, defs = {}) {
|
||||
},
|
||||
}));
|
||||
|
||||
const nodeById = new Map(nodes.map((node) => [String(node.id), node.data]));
|
||||
|
||||
const edges = loadedEdges.map((edge) => ({
|
||||
...edge,
|
||||
sourceHandle: remapLegacyHandle(edge.sourceHandle, 'source', nodeById.get(String(edge.source))),
|
||||
targetHandle: remapLegacyHandle(edge.targetHandle, 'target', nodeById.get(String(edge.target))),
|
||||
}));
|
||||
|
||||
const nextNodeId = Math.max(0, ...loadedNodes.map((node) => parseInt(node.id, 10) || 0)) + 1;
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges: loadedEdges,
|
||||
edges,
|
||||
nextNodeId,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user