import { SwimLane, Tag, Workflow, WorkflowAction, WorkflowStage } from 'verben-workflow-ui/src/lib/models'; import { Connection, ConnectionPoint, Node, SwimlaneItem, ToolType } from './workflow-designer.types'; import * as i0 from "@angular/core"; /** * The designer's single source of truth — and, in practice, its domain-logic * god object. Holds the entire editing model plus most of the behaviour that * operates on it: node placement, connection creation and routing, connection * rules, swimlane management, and the whole save-time transform. * * ## How mutation works here * * This is a `providedIn: 'root'` singleton injected **by reference** into every * component that touches the model. Components mutate its arrays directly * (`state.connections.push(...)`) — there is no store, reducer, or action * indirection. Change detection works because templates iterate * `state.swimlanes` / `state.connections` with `@for`. * * The one deliberate exception is {@link moveNodeToSwimlane}, which reassigns * arrays immutably so `@for` re-renders **both** the source and target lanes. * In-place mutation is fine everywhere else, but not across two lanes at once. * * ## Before you change this class * * A parallel implementation exists under `services/` (`NodeManagementService`, * `ConnectionService`, `TransformerService`, ...) that duplicates much of this * logic. It is only partially wired in, and the copies have **drifted** — the * two disagree on node placement offsets, connection endpoint offsets, and * which node types may connect. This class is the live path; the services are * mostly not. Change this one. * * @see ./docs/01-architecture.md — the split-brain problem in full * @see ./docs/02-coordinate-system.md — read before touching any coordinate math * @see ./docs/06-known-issues.md — the catalogued divergences */ export declare class WorkflowDesignerState { /** Ordered lanes. Each owns its own nodes; there is no flat node list. */ swimlanes: SwimlaneItem[]; /** Id of the workflow's entry-point node. */ startNodeId: string | null; /** All edges, flat across lanes. */ connections: Connection[]; draggingConnectionData: { sourcePoint?: ConnectionPoint; sourceSwimlaneIndex?: number; startX?: number; startY?: number; currentX?: number; currentY?: number; }; workflowFormId: string | null; workflowFormName: string | null; workflowId: string | null; /** * The workflow exactly as loaded from the API. * * This and the three `*Record` maps below are kept so that save can tell new * objects from changed ones and emit the correct `ObjectState`. Losing them * makes every entity look new on the next save. */ workflow: Workflow | null; /** Originally-loaded lanes, keyed by id. */ swimlaneRecord: Record; /** Originally-loaded stages, keyed by id. */ stageRecord: Record; /** Originally-loaded actions, keyed by id. */ actionRecord: Record; setWorkflowId(id: string): void; laneIdToIndexMap: Map; /** * Which node types each source type may connect to. Consulted by * {@link canConnect} and {@link getAllowedTargetNodeTypes}. * * Forms are terminal sinks — they accept incoming edges but have no outgoing * ones, which is why `form` maps to an empty list. * * ⚠️ `ConnectionService` holds a **different** copy of this table that also * allows `stage → form`. The two have drifted; this one is the live path. * See `./docs/06-known-issues.md` section A. */ private readonly connectionRules; setWorkflowForm(formId: string | null, formName: string | null): void; addSwimlane(name: string, tags: Tag[]): void; registerLaneMapping(laneId: string, swimlaneIndex: number): void; /** * Builds the attachment points for a node, laid out to match its rendered * shape. Returned coordinates are **relative to the node's own origin**. * * Layout per type: * - **stage** — points every 50px along all four edges. * - **decision** — one point at each of the diamond's four edge midpoints. * - **form** — `left` (input) points only; forms are terminal sinks. * - **subflow** — top, bottom, and two points on each of left and right. * * Regenerate these whenever a node's size or type changes, or connections * will reference point ids that no longer exist. */ generateConnectionPoints(node: Node): ConnectionPoint[]; /** * Creates a node in a swimlane and returns it (or `null` if the lane or type * is invalid). * * ## Coordinate contract — callers must obey both halves * * 1. `y` must be an **absolute canvas Y**, not lane-relative. This method * converts it internally (`y - swimlaneIndex * 263`). * 2. `swimlaneIndex` must be the **drop-target** lane — the lane the node is * landing in — not the lane a drag started from. * * Violating either half double-subtracts the lane offset and the node renders * roughly one lane too high, attached to the wrong swimlane. That was the * connection-drop bug; `designer-canvas.connection-drop.spec.ts` pins it. * * The canonical correct caller is toolbar placement in * `workflow-designer.component.ts`: * * ```ts * const swimlaneIndex = Math.floor(event.y / 263); // target lane from drop Y * this.state.addNode(swimlaneIndex, 'stage', event.x, event.y /* absolute *\/); * ``` * * The first node created in an empty workflow is automatically marked as the * start node. * * ⚠️ `NodeManagementService.addNode` applies an extra `- 40` that this method * and the render template do not. See `./docs/06-known-issues.md` section A. * * @param swimlaneIndex Target lane index (the lane the node lands in). * @param type Node type; `'swimlane'` and `'action'` are rejected. * @param x Canvas X. * @param y **Absolute** canvas Y. * @param useExistingId Reuse `stageData.Code` as the node id instead of * generating one — used when rebuilding nodes from a loaded workflow so * they keep their backend identity. */ addNode(swimlaneIndex: number, type: ToolType, x: number, y: number, stageData?: Partial, workflowData?: { id: string; name: string; }, useExistingId?: boolean): Node | null; getNodeCount(): number; findNodeById(id: string): { node: Node; swimlaneIndex: number; } | null; findActionById(connectionId: string): Connection | null; startConnectionDrag(point: ConnectionPoint, swimlaneIndex: number, globalX: number, globalY: number): void; updateConnectionDrag(currentX: number, currentY: number): void; endConnectionDrag(): void; isConnectionDragging(): boolean; /** * Endpoints for the **in-progress** connection preview line: from the source * point (resolved to absolute canvas space) to the current cursor position. * * Returns `null` when no drag is active. For a *saved* connection's * endpoints, use {@link getConnectionData} instead. */ getConnectionPathData(): { startX: number; startY: number; endX: number; endY: number; sourceSwimlaneIndex: number; } | null; /** * Completes the in-progress drag by pushing a `Connection` from the recorded * source point to the given target, and returns it. * * Reads the source from {@link draggingConnectionData}, so it is only valid * during a drag — returns `null` if no source was recorded. * * Note this does **not** check {@link canConnect}; callers are expected to * have validated the pairing before calling. The new connection has an empty * label and no action — those are filled in afterwards via the action dialog. */ createConnection(targetNodeId: string, targetPointId: string, targetSwimlaneIndex: number): Connection | null; /** * Resolves a saved connection's endpoints into **absolute canvas * coordinates** for rendering: * * ``` * startY = sourceNode.y + sourcePoint.y + connection.sourceSwimlaneIndex * 263 * endY = targetNode.y + targetPoint.y + connection.targetSwimlaneIndex * 263 * ``` * * Both offsets are needed because `node.y` is lane-relative and `point.y` is * node-relative. * * Returns `null` if either endpoint's node no longer exists — callers should * treat that as "skip rendering this edge", not as an error. * * ⚠️ Relies on the connection's denormalised lane indices being current. A * node moved between lanes without its connections updated renders detached. * * ⚠️ `ConnectionService.getConnectionData` adds a further `+ 40` here. The two * disagree; this is the live path. See `./docs/06-known-issues.md` section A. */ getConnectionData(connection: Connection): { startX: number; startY: number; endX: number; endY: number; sourceSwimlaneIndex: number; targetSwimlaneIndex: number; } | null; /** * Whether an edge from `sourceNodeType` to `targetNodeType` is permitted by * {@link connectionRules}. Unknown types return `false`. */ canConnect(sourceNodeType: string, targetNodeType: string): boolean; /** * Node types the in-progress drag is allowed to land on. Drives the choices * offered in the node-type popup when a connection is dropped on empty * canvas. Empty when no drag is active. */ getAllowedTargetNodeTypes(): string[]; /** * Deletes a swimlane. Only succeeds when the lane has no nodes — stages must * be moved to another lane first. Returns false when the lane still has nodes * or the index is out of range. * * Keeps everything aligned after removal: * - Re-sequences `order` on the remaining lanes (immutable reassignment so * Angular's @for blocks re-render). * - Decrements the swimlane indices stored on connections whose endpoints * live below the removed lane, since those lanes shift up by one. * * The lane's original record is left in `swimlaneRecord`, so a previously * loaded lane is emitted with `ObjectState.Removed` by * `transformToWorkflowModel`; a never-saved lane simply disappears. */ deleteSwimlane(index: number): boolean; updateSwimlane(index: number, name: string, tags: Tag[]): void; /** * Rebuilds the full `Workflow` API model from the current editing state — * the save-side counterpart to `parseWorkflowData` in * `workflow-designer.component.ts`. * * Mapping: * - swimlanes → `Lanes` * - stage **and subflow** nodes → `Stages` (subflows carry `IsSubProcess: true`) * - connections → `Actions` * * Each entity's `DataState` is derived by looking it up in `swimlaneRecord` / * `stageRecord` / `actionRecord`: found means `Changed`, absent means `New`. * Clearing those maps therefore makes everything save as new. * * ⚠️ Decision and form nodes are **not** emitted as stages — only `stage` and * `subflow` are. Decision semantics survive through the conditions on their * connections. * * ⚠️ When no workflow was loaded, the header is hard-coded (`Name: 'New * Workflow'`, empty description, default assignment and status) because the * designer has no UI for editing it. See `./docs/06-known-issues.md` section G. * * This only builds the payload; `WorkflowDataService.saveWorkflows` sends it. * Deletions are not part of this — they are issued eagerly at the point of * user action. See `./docs/08-api-reference.md` section 3. */ transformToWorkflowModel(): Workflow; getConnectionPointType(pointId: string): 'top' | 'right' | 'bottom' | 'left' | undefined; loadedObjectIds: { [key: string]: string; }; registerLoadedObject(id: string, code: string): void; wasLoadedFromApi(id: string): boolean; getCodeForObject(id: string): string; /** * Moves a node from one swimlane to another and updates all related state: * - Node position (swimlane-relative x/y) * - Swimlane arrays (removes from old, adds to new) * - Connection swimlane indices for this node * - Node's stageData.Tags and SwimLane to match the destination swimlane */ moveNodeToSwimlane(nodeId: string, fromSwimlaneIndex: number, toSwimlaneIndex: number, newX: number, absoluteY: number): Node | null; /** Swimlane height constant used for coordinate conversions. */ readonly swimlaneHeight = 263; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵprov: i0.ɵɵInjectableDeclaration; }