222 lines
7.5 KiB
TypeScript
222 lines
7.5 KiB
TypeScript
/**
|
|
* Single canonical start node on the canvas — id and type follow workflow primary entry kind.
|
|
*/
|
|
|
|
import type { CanvasConnection, CanvasNode } from '../../editor/FlowCanvas';
|
|
import type { NodeType } from '../../../../api/workflowApi';
|
|
import type { WorkflowEntryPoint } from '../../../../api/workflowApi';
|
|
import { getLabel } from '../shared/utils';
|
|
|
|
export const CANVAS_START_NODE_ID = 'start';
|
|
|
|
/** Primary entry is always the first invocation (gear configures index 0). */
|
|
export function getPrimaryEntry(invocations: WorkflowEntryPoint[] | undefined): WorkflowEntryPoint | undefined {
|
|
return invocations?.[0];
|
|
}
|
|
|
|
/** Kind of the primary entry (drives canvas node type) */
|
|
export function getPrimaryStartKind(invocations: WorkflowEntryPoint[] | undefined): string {
|
|
return getPrimaryEntry(invocations)?.kind ?? 'manual';
|
|
}
|
|
|
|
function entryTitle(entry: WorkflowEntryPoint | undefined, language: string): string {
|
|
if (!entry?.title) return '';
|
|
const t = entry.title;
|
|
if (typeof t === 'string') return t.trim();
|
|
const s = t[language] || t.de || t.en || Object.values(t)[0];
|
|
return (s != null ? String(s) : '').trim();
|
|
}
|
|
|
|
export function mapKindToNodeType(kind: string): string {
|
|
if (kind === 'form') return 'trigger.form';
|
|
if (kind === 'schedule') return 'trigger.schedule';
|
|
// Immer aktiv: zunächst Standard-Start; Listener (E-Mail, Webhook, …) folgt separat
|
|
if (kind === 'always_on') return 'trigger.manual';
|
|
return 'trigger.manual';
|
|
}
|
|
|
|
function categoryForKind(kind: string): 'on_demand' | 'always_on' {
|
|
if (kind === 'manual' || kind === 'form') return 'on_demand';
|
|
return 'always_on';
|
|
}
|
|
|
|
function titleForStartNode(
|
|
kind: string,
|
|
invocations: WorkflowEntryPoint[],
|
|
nodeTypes: NodeType[],
|
|
language: string
|
|
): string {
|
|
const custom = entryTitle(getPrimaryEntry(invocations), language);
|
|
if (custom) return custom;
|
|
const nt = nodeTypes.find((n) => n.id === mapKindToNodeType(kind));
|
|
if (nt) return getLabel(nt.label, language);
|
|
return 'Start';
|
|
}
|
|
|
|
/** Rewire connections when replacing node ids */
|
|
function rewireConnections(
|
|
connections: CanvasConnection[],
|
|
fromId: string,
|
|
toId: string
|
|
): CanvasConnection[] {
|
|
if (fromId === toId) return connections;
|
|
return connections.map((c) => ({
|
|
...c,
|
|
sourceId: c.sourceId === fromId ? toId : c.sourceId,
|
|
targetId: c.targetId === fromId ? toId : c.targetId,
|
|
}));
|
|
}
|
|
|
|
/** Deep-rewrite ref.nodeId in parameters (e.g. flow.ifElse condition.ref) */
|
|
function rewireRefInParams(params: unknown, fromIds: Set<string>, toId: string): unknown {
|
|
if (params == null) return params;
|
|
if (typeof params === 'object' && params !== null && 'type' in params && 'nodeId' in params) {
|
|
const obj = params as { type?: string; nodeId?: string; path?: unknown };
|
|
if (obj.type === 'ref' && typeof obj.nodeId === 'string' && fromIds.has(obj.nodeId)) {
|
|
return { ...obj, nodeId: toId };
|
|
}
|
|
}
|
|
if (Array.isArray(params)) {
|
|
return params.map((item) => rewireRefInParams(item, fromIds, toId));
|
|
}
|
|
if (typeof params === 'object' && params !== null) {
|
|
const out: Record<string, unknown> = {};
|
|
for (const [k, v] of Object.entries(params)) {
|
|
out[k] = rewireRefInParams(v, fromIds, toId);
|
|
}
|
|
return out;
|
|
}
|
|
return params;
|
|
}
|
|
|
|
/** Rewrite refs in all nodes' parameters when trigger id changes */
|
|
function rewireRefsInNodes(
|
|
nodes: CanvasNode[],
|
|
fromIds: Set<string>,
|
|
toId: string
|
|
): CanvasNode[] {
|
|
if (fromIds.size === 0) return nodes;
|
|
return nodes.map((n) => {
|
|
const p = n.parameters;
|
|
if (!p || typeof p !== 'object') return n;
|
|
const next = rewireRefInParams(p, fromIds, toId);
|
|
if (next === p) return n;
|
|
return { ...n, parameters: next as Record<string, unknown> };
|
|
});
|
|
}
|
|
|
|
/** Remove duplicate trigger nodes; keep first, merge connections onto it */
|
|
function dedupeTriggers(
|
|
nodes: CanvasNode[],
|
|
connections: CanvasConnection[]
|
|
): { nodes: CanvasNode[]; connections: CanvasConnection[] } {
|
|
const triggers = nodes.filter((n) => n.type.startsWith('trigger.'));
|
|
if (triggers.length <= 1) return { nodes, connections };
|
|
|
|
const keep = triggers[0];
|
|
const removeIds = new Set(triggers.slice(1).map((n) => n.id));
|
|
let nextConn = connections;
|
|
for (const rid of removeIds) {
|
|
nextConn = rewireConnections(nextConn, rid, keep.id);
|
|
}
|
|
const newNodes = nodes.filter((n) => !removeIds.has(n.id));
|
|
return { nodes: newNodes, connections: nextConn };
|
|
}
|
|
|
|
/** Normalize canonical id `start` and update type/labels from primary kind */
|
|
export function syncCanvasStartNode(
|
|
nodes: CanvasNode[],
|
|
connections: CanvasConnection[],
|
|
invocations: WorkflowEntryPoint[],
|
|
nodeTypes: NodeType[],
|
|
language: string
|
|
): { nodes: CanvasNode[]; connections: CanvasConnection[] } {
|
|
const kind = getPrimaryStartKind(invocations);
|
|
const targetType = mapKindToNodeType(kind);
|
|
const title = titleForStartNode(kind, invocations, nodeTypes, language);
|
|
const nt = nodeTypes.find((n) => n.id === targetType);
|
|
const inputs = nt?.inputs ?? 0;
|
|
const outputs = nt?.outputs ?? 1;
|
|
|
|
const triggerIdsBeforeDedupe = new Set(nodes.filter((n) => n.type.startsWith('trigger.')).map((n) => n.id));
|
|
let { nodes: ns, connections: cs } = dedupeTriggers(nodes, connections);
|
|
|
|
let startIdx = ns.findIndex((n) => n.type.startsWith('trigger.'));
|
|
if (startIdx === -1) {
|
|
const newNode: CanvasNode = {
|
|
id: CANVAS_START_NODE_ID,
|
|
type: targetType,
|
|
x: 100,
|
|
y: 120,
|
|
title,
|
|
label: title,
|
|
inputs,
|
|
outputs,
|
|
color: nt?.meta?.color as string | undefined,
|
|
parameters: {},
|
|
};
|
|
ns = rewireRefsInNodes([newNode, ...ns], triggerIdsBeforeDedupe, CANVAS_START_NODE_ID);
|
|
return { nodes: ns, connections: cs };
|
|
}
|
|
|
|
const current = ns[startIdx];
|
|
const oldId = current.id;
|
|
let nextConn = cs;
|
|
if (oldId !== CANVAS_START_NODE_ID) {
|
|
nextConn = rewireConnections(nextConn, oldId, CANVAS_START_NODE_ID);
|
|
}
|
|
ns = rewireRefsInNodes(ns, triggerIdsBeforeDedupe, CANVAS_START_NODE_ID);
|
|
|
|
const updated: CanvasNode = {
|
|
...current,
|
|
id: CANVAS_START_NODE_ID,
|
|
type: targetType,
|
|
title,
|
|
label: title,
|
|
inputs,
|
|
outputs,
|
|
color: nt?.meta?.color as string | undefined,
|
|
parameters:
|
|
targetType === current.type ? current.parameters ?? {} : preserveParametersForTypeSwitch(current, targetType),
|
|
};
|
|
|
|
const nextNodes = [...ns];
|
|
nextNodes[startIdx] = updated;
|
|
return { nodes: nextNodes, connections: nextConn };
|
|
}
|
|
|
|
function preserveParametersForTypeSwitch(node: CanvasNode, newType: string): Record<string, unknown> {
|
|
const p = node.parameters ?? {};
|
|
if (newType === 'trigger.form' && p.formFields) return { formFields: p.formFields };
|
|
if (newType === 'trigger.schedule' && (p.cron || p.schedule)) {
|
|
const out: Record<string, unknown> = {};
|
|
if (p.cron != null) out.cron = p.cron;
|
|
if (p.schedule != null) out.schedule = p.schedule;
|
|
return out;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
/** Build invocations: replace primary (index 0), keep further entries (e.g. listener config later). */
|
|
export function buildInvocationsForPrimaryKind(
|
|
kind: string,
|
|
existing: WorkflowEntryPoint[] | undefined,
|
|
titleDe: string
|
|
): WorkflowEntryPoint[] {
|
|
const list = existing ?? [];
|
|
const primaryId =
|
|
list[0]?.id ??
|
|
(typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `ep-${Date.now()}`);
|
|
const category = categoryForKind(kind);
|
|
const primary: WorkflowEntryPoint = {
|
|
id: primaryId,
|
|
kind,
|
|
category,
|
|
enabled: true,
|
|
title: { de: titleDe, en: titleDe, fr: titleDe },
|
|
description: {},
|
|
config: {},
|
|
};
|
|
const rest = list.slice(1).filter((x) => x.id !== primaryId);
|
|
return [primary, ...rest];
|
|
}
|