foxygit / RPA-Remisser Log in
commits tags

/webui/frontend/src/components/StepNode.tsx · 5.88 KB

raw
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>
  );
}