Create Element
Workflow Element Utils
Learn how to create element utils and understand how they fit into the system.
Workflow Element Utils
Workflow changes are handled through utility functions, so we'll need to implement the utility functions for the new element in the following file:
// src/shared/kits/workflow/mvc/model/utils/element-utils/types/parallel.ts
import type { ParallelFlow, ElementFlow } from "../../../workflow";
import * as ElementUtils from "../element";
import type { DropArea } from "@/shared/kits/workflow/mvc/drop-area";
/**
* Sets the dragging position for the given element.
*
* @param element The element being dragged.
* @param position The current drag position.
* @returns The updated element with the dragging position set.
*/
export function drag(
element: ParallelFlow,
position: {
x: number;
y: number;
},
): ParallelFlow {
return { ...element, dragging: position };
}
/**
* Marks the element as no longer being dragged.
*
* @param element The element that has been dropped.
* @returns The updated element with dragging set to null.
*/
export function drop(element: ParallelFlow): ParallelFlow {
return { ...element, dragging: null };
}
/**
* Adds an element id to the 'next' drop area of the given element.
*
* @param element The element containing the 'next' drop area.
* @param enter The id of the element entering the 'next' drop area.
* @returns The updated element with the 'next' drop area including the given id.
*/
export function enterNextDropArea(
element: ParallelFlow,
enter: string,
): ParallelFlow {
const drop = new Set(element.dropNext);
drop.add(enter);
return { ...element, dropNext: drop };
}
/**
* Removes an element id from the 'next' drop area of the given element.
*
* @param element The element containing the 'next' drop area.
* @param leave The id of the element leaving the 'next' drop area.
* @returns The updated element with the 'next' drop area excluding the given id.
*/
export function leaveNextDropArea(
element: ParallelFlow,
leave: string,
): ParallelFlow {
const dropNext = new Set(element.dropNext);
dropNext.delete(leave);
return { ...element, dropNext };
}
/**
* Searches for an element with the specified id within the given element.
*
* @param element The element to start the search from.
* @param id The id of the element to find.
* @returns The element with the matching id, or `undefined` if not found.
*/
export function get(
element: ParallelFlow,
id: string,
): ElementFlow | undefined {
if (element.id === id) {
return element;
}
for (const branch of element.branches) {
for (const current of branch.then) {
const found = ElementUtils.get(current, id);
if (found) return found;
}
}
return undefined;
}
/**
* Adds an element immediately after the element with the specified id within the given element.
*
* @param element The element in which to perform the addition.
* @param id The id of the element after which the element should be added.
* @param add The element to add.
* @returns A tuple containing:
* - The updated element with the element added if the target id was found.
* - A boolean indicating whether the addition was successful.
*/
export function addNext(
element: ParallelFlow,
id: string,
add: ElementFlow,
): [ElementFlow, boolean] {
for (let i = 0; i < element.branches.length; i++) {
for (let j = 0; j < element.branches[i]!.then.length; j++) {
const current = element.branches[i]!.then[j]!;
if (current.id === id) {
const list = [...element.branches];
const elements = [...list[i]!.then];
elements.splice(j, 1, current, add);
list.splice(i, 1, { ...list[i]!, then: elements });
return [{ ...element, branches: list }, true];
}
const [updated, done] = ElementUtils.addNext(current, id, add);
if (done) {
const list = [...element.branches];
const elements = [...list[i]!.then];
elements.splice(j, 1, updated);
list.splice(i, 1, { ...list[i]!, then: elements });
return [{ ...element, branches: list }, true];
}
}
}
return [element, false];
}
/**
* Adds an element previously marked as being in a drop area into its correct position within the given element.
*
* @param element The element in which to perform the addition.
* @param add The element to add from the drop area.
* @returns A tuple containing:
* - The updated element with the added element in its correct position.
* - A boolean indicating whether the addition was successful.
*/
export function addDropAreaElement(
element: ParallelFlow,
add: ElementFlow,
): [ElementFlow, boolean] {
for (let i = 0; i < element.branches.length; i++) {
if (element.dropBranches[i]!.has(add.id)) {
const list = [...element.branches];
const elements = [add, ...list[i]!.then];
const dropBranches = [...element.dropBranches];
const dropBranch = new Set(dropBranches[i]!);
list.splice(i, 1, { ...list[i]!, then: elements });
dropBranch.delete(add.id);
dropBranches.splice(i, 1, dropBranch);
return [{ ...element, branches: list, dropBranches }, true];
}
}
for (let i = 0; i < element.branches.length; i++) {
for (let j = 0; j < element.branches[i]!.then.length; j++) {
const current = element.branches[i]!.then[j]!;
if (ElementUtils.inNextDropArea(current, add.id)) {
const updated = ElementUtils.leaveNextDropArea(current, add.id);
const list = [...element.branches];
const elements = [...list[i]!.then];
elements.splice(j, 1, updated, add);
list.splice(i, 1, { ...list[i]!, then: elements });
return [{ ...element, branches: list }, true];
}
const [updated, done] = ElementUtils.addDropAreaElement(current, add);
if (done) {
const list = [...element.branches];
const elements = [...list[i]!.then];
elements.splice(j, 1, updated);
list.splice(i, 1, { ...list[i]!, then: elements });
return [{ ...element, branches: list }, true];
}
}
}
return [element, false];
}
/**
* Removes the element with the specified id from the given element.
*
* @param element The element from which to remove the target element.
* @param id The id of the element to remove.
* @returns A tuple containing:
* - The updated element with the target element removed.
* - The removed element, or `undefined` if no matching element was found.
*/
export function remove(
element: ParallelFlow,
id: string,
): [ElementFlow, ElementFlow | undefined] {
for (let i = 0; i < element.branches.length; i++) {
for (let j = 0; j < element.branches[i]!.then.length; j++) {
const current = element.branches[i]!.then[j]!;
if (current.id === id) {
const list = [...element.branches];
const elements = [...list[i]!.then];
elements.splice(j, 1);
list.splice(i, 1, { ...list[i]!, then: elements });
return [{ ...element, branches: list }, current];
}
const [updatedElement, removed] = ElementUtils.remove(current, id);
if (removed) {
const list = [...element.branches];
const elements = [...list[i]!.then];
elements.splice(j, 1, updatedElement);
list.splice(i, 1, { ...list[i]!, then: elements });
return [{ ...element, branches: list }, removed];
}
}
}
return [element, undefined];
}
/**
* Applies a transformation function to the element with the specified id within the given element.
*
* @param element The element containing the target element to be modified.
* @param id The id of the element to modify.
* @param modify A function that takes the target element and returns the modified version.
* @returns A tuple containing:
* - The updated element with the target element modified.
* - A boolean indicating whether the modification was successful.
*/
export function modify(
element: ParallelFlow,
id: string,
modify: (element: ElementFlow) => ElementFlow,
): [ElementFlow, boolean] {
if (element.id === id) {
const modified = modify(element) as ParallelFlow;
return [modified, true];
}
for (let i = 0; i < element.branches.length; i++) {
for (let j = 0; j < element.branches[i]!.then.length; j++) {
const current = element.branches[i]!.then[j]!;
const [newElement, modified] = ElementUtils.modify(current, id, modify);
if (modified) {
const list = [...element.branches];
const elements = [...list[i]!.then];
elements.splice(j, 1, newElement);
list.splice(i, 1, { ...list[i]!, then: elements });
return [{ ...element, branches: list }, true];
}
}
}
return [element, false];
}
/**
* Checks whether the element with the specified id is currently in the 'next' drop area of the given element.
*
* @param element The element containing the 'next' drop area.
* @param id The id of the element to check.
* @returns A boolean indicating whether the element is in the 'next' drop area.
*/
export function inNextDropArea(element: ParallelFlow, id: string): boolean {
return element.dropNext.has(id);
}
/**
* Checks if the element with the specified id is currently inside one of the drop areas of the given element.
*
* @param element The element that contains the drop areas.
* @param id The id of the element to check.
* @returns A boolean indicating whether the element is inside a drop area.
*/
export function inDropArea(element: ParallelFlow, id: string): boolean {
if (
element.dropNext.has(id) ||
element.dropBranches.some((set) => set.has(id))
) {
return true;
}
for (const branch of element.branches) {
for (const current of branch.then) {
if (ElementUtils.inDropArea(current, id)) {
return true;
}
}
}
return false;
}
/**
* Retrieves the set of element ids that belong to the element with the specified id.
*
* @param element The element to start the search from.
* @param id The id of the element whose associated element ids should be retrieved.
* @returns A set of element ids that belong to the specified element, or `undefined` if the element was not found.
*/
export function belongToElement(
element: ParallelFlow,
id: string,
): Set<string> | undefined {
if (element.id === id) {
const elements = new Set([element.id]);
for (const branch of element.branches) {
for (const current of branch.then) {
const set = ElementUtils.belongToElement(current, current.id)!;
set.forEach((id) => elements.add(id));
}
}
return elements;
}
for (const branch of element.branches) {
for (const current of branch.then) {
const elements = ElementUtils.belongToElement(current, id);
if (elements) return elements;
}
}
return undefined;
}
/**
* Returns the drop area containing the element with the specified id,
* or `undefined` if the element is not currently inside any drop area.
*
* @param element The element to start the search from.
* @param id The id of the element whose drop area should be returned.
* @returns The drop area containing the element, or `undefined` if it isn't in any drop area.
*/
export function elementDropArea(
element: ParallelFlow,
id: string,
): DropArea | undefined {
if (element.dropNext.has(id)) {
return { type: "element", element, dropArea: "next" };
}
for (let i = 0; i < element.dropBranches.length; i++) {
if (element.dropBranches[i].has(id)) {
return { type: "element", element, dropArea: "branch", branch: i };
}
}
for (const branch of element.branches) {
for (const current of branch.then) {
const found = ElementUtils.elementDropArea(current, id);
if (found) return found;
}
}
return undefined;
}
/**
* Creates a new branch for the given parallel element.
*
* @param element The parallel element.
* @param label The label for the new branch.
* @returns The updated element with the new branch added.
*/
export function createBranch(
element: ParallelFlow,
label: string,
): ParallelFlow {
const list = [...element.branches];
list.push({ label, then: [] });
const dropBranches = [...element.dropBranches];
dropBranches.push(new Set());
return { ...element, branches: list, dropBranches };
}
/**
* Deletes a branch for the given parallel element.
*
* @param element The parallel element.
* @param branch The index of the branch.
* @returns The updated element with the branch deleted.
*/
export function deleteBranch(
element: ParallelFlow,
branch: number,
): ParallelFlow {
const list = [...element.branches];
list.splice(branch, 1);
const dropBranches = [...element.dropBranches];
dropBranches.splice(branch, 1);
return { ...element, branches: list, dropBranches };
}
/**
* Updates the label of the specified branch in the given parallel element.
*
* @param element The parallel element.
* @param branch The index of the branch.
* @param label The label for the branch.
* @returns The updated element with the updated branch label.
*/
export function setBranchInfo(
element: ParallelFlow,
branch: number,
label: string,
): ParallelFlow {
const parallelElement = element as ParallelFlow;
const list = [...parallelElement.branches];
list[branch] = { ...list[branch]!, label };
return { ...parallelElement, branches: list };
}
/**
* Adds an element id to the drop area of the specified branch in the given parallel element.
*
* @param element The parallel element.
* @param branch The index of the branch.
* @param enter The id of the element entering the drop area of the branch.
* @returns The updated element with the given id added to the branch's drop area.
*/
export function enterBranchDropArea(
element: ParallelFlow,
branch: number,
enter: string,
): ParallelFlow {
const set = new Set(element.dropBranches[branch]);
set.add(enter);
const dropBranches = [...element.dropBranches];
dropBranches.splice(branch, 1, set);
return { ...element, dropBranches };
}
/**
* Removes an element id from the drop area of the specified branch of the given parallel element.
*
* @param element The parallel element.
* @param branch The index of the branch.
* @param leave The id of the element leaving the drop area of the branch.
* @returns The updated element with the given id removed from the branch's drop area.
*/
export function leaveBranchDropArea(
element: ParallelFlow,
branch: number,
leave: string,
): ParallelFlow {
const set = new Set(element.dropBranches[branch]);
set.delete(leave);
const dropBranches = [...element.dropBranches];
dropBranches.splice(branch, 1, set);
return { ...element, dropBranches };
}
/**
* Adds an element at the top of the specified branch of the given parallel element.
*
* @param element The parallel element.
* @param branch The index of the branch.
* @param add The element to add.
* @returns The updated element with the element added at the top of the specified branch.
*/
export function addBranch(
element: ParallelFlow,
branch: number,
add: ElementFlow,
): ParallelFlow {
const list = [...element.branches];
const elements = [add, ...list[branch]!.then];
list.splice(branch, 1, { ...list[branch]!, then: elements });
return { ...element, branches: list };
}
One of these functions returns a value of type DropArea, and we'll need to update that union type to include an additional variant:
// src/shared/kits/workflow/mvc/drop-area.ts
import type {
ActionFlow,
ConditionFlow,
SwitchFlow,
LoopFlow,
ParallelFlow,
} from "./model/workflow";
export type DropArea = GlobalDropArea | ElementDropArea;
export type GlobalDropArea = {
type: "global";
};
export type ElementDropArea =
| ActionDropArea
| ConditionDropArea
| SwitchDropArea
| LoopDropArea
| ParallelDropArea;
export type ActionDropArea = ActionNextDropArea;
export type ActionNextDropArea = {
type: "element";
element: ActionFlow;
dropArea: "next";
};
export type ConditionDropArea =
| ConditionNextDropArea
| ConditionThenDropArea
| ConditionElseDropArea;
export type ConditionNextDropArea = {
type: "element";
element: ConditionFlow;
dropArea: "next";
};
export type ConditionThenDropArea = {
type: "element";
element: ConditionFlow;
dropArea: "then";
};
export type ConditionElseDropArea = {
type: "element";
element: ConditionFlow;
dropArea: "else";
};
export type SwitchDropArea =
| SwitchNextDropArea
| SwitchBranchDropArea
| SwitchDefaultDropArea;
export type SwitchNextDropArea = {
type: "element";
element: SwitchFlow;
dropArea: "next";
};
export type SwitchBranchDropArea = {
type: "element";
element: SwitchFlow;
dropArea: "branch";
branch: number;
};
export type SwitchDefaultDropArea = {
type: "element";
element: SwitchFlow;
dropArea: "default";
};
export type LoopDropArea = LoopNextDropArea | LoopIntoDropArea;
export type LoopNextDropArea = {
type: "element";
element: LoopFlow;
dropArea: "next";
};
export type LoopIntoDropArea = {
type: "element";
element: LoopFlow;
dropArea: "into";
};
export type ParallelDropArea = ParallelNextDropArea | ParallelBranchDropArea;
export type ParallelNextDropArea = {
type: "element";
element: ParallelFlow;
dropArea: "next";
};
export type ParallelBranchDropArea = {
type: "element";
element: ParallelFlow;
dropArea: "branch";
branch: number;
};
We'll also update the element utilities to include the new element's utils, as shown below:
// src/shared/kits/workflow/mvc/model/utils/element-utils/element.ts
import { zet } from "@/shared/lib/zet";
import type { ElementFlow } from "../../workflow/flow";
import * as ActionUtils from "./types/action";
import * as ConditionUtils from "./types/condition";
import * as SwitchUtils from "./types/switch";
import * as LoopUtils from "./types/loop";
import * as ParallelUtils from "./types/parallel";
import type { DropArea } from "@/shared/kits/workflow/mvc/drop-area";
interface Zet<T, U> {
object: ElementFlow;
nested: [];
filter: ["type"];
params: T;
return: U;
}
type Drag = Zet<[{ x: number; y: number }], ElementFlow>;
/**
* Sets the dragging position for the given element.
*
* @param element The element being dragged.
* @param position The current drag position.
* @returns The updated element with the dragging position set.
*/
export const drag = zet<Drag>([], ["type"], {
action: ActionUtils.drag,
condition: ConditionUtils.drag,
switch: SwitchUtils.drag,
loop: LoopUtils.drag,
parallel: ParallelUtils.drag,
});
type Drop = Zet<[], ElementFlow>;
/**
* Marks the element as no longer being dragged.
*
* @param element The element that has been dropped.
* @returns The updated element with dragging set to null.
*/
export const drop = zet<Drop>([], ["type"], {
action: ActionUtils.drop,
condition: ConditionUtils.drop,
switch: SwitchUtils.drop,
loop: LoopUtils.drop,
parallel: ParallelUtils.drop,
});
type EnterNextDropArea = Zet<[string], ElementFlow>;
/**
* Adds an element id to the 'next' drop area of the given element.
*
* @param element The element containing the 'next' drop area.
* @param enter The id of the element entering the 'next' drop area.
* @returns The updated element with the 'next' drop area including the given id.
*/
export const enterNextDropArea = zet<EnterNextDropArea>([], ["type"], {
action: ActionUtils.enterNextDropArea,
condition: ConditionUtils.enterNextDropArea,
switch: SwitchUtils.enterNextDropArea,
loop: LoopUtils.enterNextDropArea,
parallel: ParallelUtils.enterNextDropArea,
});
type LeaveNextDropArea = Zet<[string], ElementFlow>;
/**
* Removes an element id from the 'next' drop area of the given element.
*
* @param element The element containing the 'next' drop area.
* @param leave The id of the element leaving the 'next' drop area.
* @returns The updated element with the 'next' drop area excluding the given id.
*/
export const leaveNextDropArea = zet<LeaveNextDropArea>([], ["type"], {
action: ActionUtils.leaveNextDropArea,
condition: ConditionUtils.leaveNextDropArea,
switch: SwitchUtils.leaveNextDropArea,
loop: LoopUtils.leaveNextDropArea,
parallel: ParallelUtils.leaveNextDropArea,
});
type Get = Zet<[string], ElementFlow | undefined>;
/**
* Searches for an element with the specified id within the given element.
*
* @param element The element to start the search from.
* @param id The id of the element to find.
* @returns The element with the matching id, or `undefined` if not found.
*/
export const get = zet<Get>([], ["type"], {
action: ActionUtils.get,
condition: ConditionUtils.get,
switch: SwitchUtils.get,
loop: LoopUtils.get,
parallel: ParallelUtils.get,
});
type AddNext = Zet<[string, ElementFlow], [ElementFlow, boolean]>;
/**
* Adds an element immediately after the element with the specified id within the given element.
*
* @param element The element in which to perform the addition.
* @param id The id of the element after which the element should be added.
* @param add The element to add.
* @returns A tuple containing:
* - The updated element with the element added if the target id was found.
* - A boolean indicating whether the addition was successful.
*/
export const addNext = zet<AddNext>([], ["type"], {
action: ActionUtils.addNext,
condition: ConditionUtils.addNext,
switch: SwitchUtils.addNext,
loop: LoopUtils.addNext,
parallel: ParallelUtils.addNext,
});
type AddDropAreaElement = Zet<[ElementFlow], [ElementFlow, boolean]>;
/**
* Adds an element previously marked as being in a drop area into its correct position within the given element.
*
* @param element The element in which to perform the addition.
* @param add The element to add from the drop area.
* @returns A tuple containing:
* - The updated element with the added element in its correct position.
* - A boolean indicating whether the addition was successful.
*/
export const addDropAreaElement = zet<AddDropAreaElement>([], ["type"], {
action: ActionUtils.addDropAreaElement,
condition: ConditionUtils.addDropAreaElement,
switch: SwitchUtils.addDropAreaElement,
loop: LoopUtils.addDropAreaElement,
parallel: ParallelUtils.addDropAreaElement,
});
type Remove = Zet<[string], [ElementFlow, ElementFlow | undefined]>;
/**
* Removes the element with the specified id from the given element.
*
* @param element The element from which to remove the target element.
* @param id The id of the element to remove.
* @returns A tuple containing:
* - The updated element with the target element removed.
* - The removed element, or `undefined` if no matching element was found.
*/
export const remove = zet<Remove>([], ["type"], {
action: ActionUtils.remove,
condition: ConditionUtils.remove,
switch: SwitchUtils.remove,
loop: LoopUtils.remove,
parallel: ParallelUtils.remove,
});
type Modify = Zet<
[string, (element: ElementFlow) => ElementFlow],
[ElementFlow, boolean]
>;
/**
* Applies a transformation function to the element with the specified id within the given element.
*
* @param element The element containing the target element to be modified.
* @param id The id of the element to modify.
* @param modify A function that takes the target element and returns the modified version.
* @returns A tuple containing:
* - The updated element with the target element modified.
* - A boolean indicating whether the modification was successful.
*/
export const modify = zet<Modify>([], ["type"], {
action: ActionUtils.modify,
condition: ConditionUtils.modify,
switch: SwitchUtils.modify,
loop: LoopUtils.modify,
parallel: ParallelUtils.modify,
});
type InNextDropArea = Zet<[string], boolean>;
/**
* Checks whether the element with the specified id is currently in the 'next' drop area of the given element.
*
* @param element The element containing the 'next' drop area.
* @param id The id of the element to check.
* @returns A boolean indicating whether the element is in the 'next' drop area.
*/
export const inNextDropArea = zet<InNextDropArea>([], ["type"], {
action: ActionUtils.inNextDropArea,
condition: ConditionUtils.inNextDropArea,
switch: SwitchUtils.inNextDropArea,
loop: LoopUtils.inNextDropArea,
parallel: ParallelUtils.inNextDropArea,
});
type InDropArea = Zet<[string], boolean>;
/**
* Checks if the element with the specified id is currently inside one of the drop areas of the given element.
*
* @param element The element that contains the drop areas.
* @param id The id of the element to check.
* @returns A boolean indicating whether the element is inside a drop area.
*/
export const inDropArea = zet<InDropArea>([], ["type"], {
action: ActionUtils.inDropArea,
condition: ConditionUtils.inDropArea,
switch: SwitchUtils.inDropArea,
loop: LoopUtils.inDropArea,
parallel: ParallelUtils.inDropArea,
});
type BelongToElement = Zet<[string], Set<string> | undefined>;
/**
* Retrieves the set of element ids that belong to the element with the specified id.
*
* @param element The element to start the search from.
* @param id The id of the element whose associated element ids should be retrieved.
* @returns A set of element ids that belong to the specified element, or `undefined` if the element was not found.
*/
export const belongToElement = zet<BelongToElement>([], ["type"], {
action: ActionUtils.belongToElement,
condition: ConditionUtils.belongToElement,
switch: SwitchUtils.belongToElement,
loop: LoopUtils.belongToElement,
parallel: ParallelUtils.belongToElement,
});
type ElementDropArea = Zet<[string], DropArea | undefined>;
/**
* Returns the drop area containing the element with the specified id,
* or `undefined` if the element is not currently inside any drop area.
*
* @param element The element to start the search from.
* @param id The id of the element whose drop area should be returned.
* @returns The drop area containing the element, or `undefined` if it isn't in any drop area.
*/
export const elementDropArea = zet<ElementDropArea>([], ["type"], {
action: ActionUtils.elementDropArea,
condition: ConditionUtils.elementDropArea,
switch: SwitchUtils.elementDropArea,
loop: LoopUtils.elementDropArea,
parallel: ParallelUtils.elementDropArea,
});
We'll also update the following file to export the utilities we created:
// src/shared/kits/workflow/mvc/model/utils/element-utils/index.ts
export * as ElementUtils from "./element";
export * as ActionUtils from "./types/action";
export * as ConditionUtils from "./types/condition";
export * as SwitchUtils from "./types/switch";
export * as LoopUtils from "./types/loop";
export * as ParallelUtils from "./types/parallel";
We'll also update the following file to export the utilities we created:
// src/shared/kits/workflow/mvc/model/utils/index.ts
export * as WorkflowUtils from "./workflow";
export {
ElementUtils,
ActionUtils,
ConditionUtils,
SwitchUtils,
LoopUtils,
ParallelUtils,
} from "./element-utils";