commits
tags
import { Handle, Position, type NodeProps } from "@xyflow/react";
import type { Branch, Step } from "../lib/api";
import type { Schema } from "../lib/schema";
import { fieldLabel } from "../lib/schema";
import type { Lang, T } from "../lib/i18n";
import { newBranch, retype } from "../lib/stepops";
import { handleLabel, nodeControl, outputHandles } from "../lib/handles";
import { Fields } from "./Fields";
import { ConditionEditor } from "./ConditionEditor";
import { RowFieldsEditor } from "./RowFieldsEditor";
import { CallFlowFields } from "./CallFlowFields";
export interface StepNodeData {
step: Step;
schema: Schema;
lang: Lang;
t: T;
onChange: (step: Step) => void;
onDelete: () => void;
subflowTargets?: { id: string; name: string }[];
externalFlows?: string[];
onOpenSubflow?: (id: string) => void;
onOpenExternal?: (name: string) => void;
[k: string]: unknown;
}
export function StepNode({ data }: NodeProps) {
const d = data as StepNodeData;
const { step, schema, lang, t } = d;
const spec = schema.step_types.find((s) => s.type === step.type);
const ctrl = nodeControl(schema, step.type);
const outs = outputHandles(schema, step);
const patch = (p: Record<string, unknown>) => {
const next: Step = { ...step };
for (const [k, v] of Object.entries(p)) {
if (v === undefined || v === "") delete (next as Record<string, unknown>)[k];
else (next as Record<string, unknown>)[k] = v;
}
d.onChange(next);
};
const branches: Branch[] = step.branches ?? [newBranch()];
const setBranch = (i: number, b: Branch) =>
d.onChange({ ...step, branches: branches.map((x, j) => (j === i ? b : x)) });
return (
<div className={"step-node cat-" + (spec?.category ?? "x") + (step.enabled === false ? " disabled" : "")}>
<Handle type="target" position={Position.Top} id="in" />
<div className="node-head">
<input
type="checkbox"
className="nodrag"
checked={step.enabled !== false}
title={t("enabled_title")}
onChange={(e) => d.onChange({ ...step, enabled: e.target.checked ? undefined : false })}
/>
<select
className="nodrag type-select"
value={step.type}
onChange={(e) => d.onChange(retype(schema, step, e.target.value))}
>
{schema.step_types.map((s) => (
<option key={s.type} value={s.type}>
{fieldLabel(s, lang)}
</option>
))}
</select>
<span className="spacer" />
<button className="danger nodrag" onClick={d.onDelete}>
✕
</button>
</div>
<div className="node-subhead">
<input
type="text"
className="nodrag label-input"
placeholder={t("label_ph")}
value={step.label ?? ""}
onChange={(e) => d.onChange({ ...step, label: e.target.value || undefined })}
/>
<select
className="nodrag"
value={step.on_error === "continue" ? "continue" : "stop"}
onChange={(e) => d.onChange({ ...step, on_error: e.target.value === "continue" ? "continue" : undefined })}
>
<option value="stop">{t("on_error_stop")}</option>
<option value="continue">{t("on_error_continue")}</option>
</select>
</div>
<div className="node-body nodrag">
{!spec && <div className="hint">{t("unknown_step")}: {step.type}</div>}
{spec && (
<Fields
values={step}
fields={spec.fields}
lang={lang}
showScope={ctrl === "loop" || step.type === "extract_row"}
onChange={patch}
/>
)}
{step.type === "extract_row" && (
<RowFieldsEditor
fields={step.fields ?? []}
lang={lang}
t={t}
onChange={(fields) => d.onChange({ ...step, fields })}
/>
)}
{step.type === "call_flow" && (
<CallFlowFields
step={step}
t={t}
subflowTargets={d.subflowTargets ?? []}
externalFlows={d.externalFlows ?? []}
onChange={d.onChange}
onOpenSubflow={d.onOpenSubflow ?? (() => {})}
onOpenExternal={d.onOpenExternal ?? (() => {})}
/>
)}
{ctrl === "if" && (
<div className="branches nodrag">
{branches.map((b, i) => (
<div className="branch" key={i}>
<div className="branch-head">
<strong>{t("branch_case", { n: i + 1 })}</strong>
{branches.length > 1 && (
<button
className="danger tiny"
onClick={() => d.onChange({ ...step, branches: branches.filter((_, j) => j !== i) })}
>
{t("remove_branch")}
</button>
)}
</div>
<ConditionEditor
condition={b.condition}
schema={schema}
lang={lang}
t={t}
showScope
onChange={(condition) => setBranch(i, { condition })}
/>
</div>
))}
<button className="secondary tiny" onClick={() => d.onChange({ ...step, branches: [...branches, newBranch()] })}>
{t("add_branch")}
</button>
</div>
)}
</div>
{/* utgångshandtag – längs nodens underkant */}
<div className="node-outs">
{outs.map((h, i) => (
<div className="out-col" key={h}>
<span className="out-label">{handleLabel(h) || t("out_next")}</span>
<Handle
type="source"
position={Position.Bottom}
id={h}
style={{ left: `${((i + 0.5) / outs.length) * 100}%` }}
/>
</div>
))}
</div>
</div>
);
}