Create ElementElement View

Create Element

Element View

Learn how to create the element view and understand how it fits into the system.


Element Icons

With the model layer updated, we can now move on to updating the view layer, starting with creating the icons for the new element:

// src/shared/kits/workflow/icons/parallel.tsx

import type { ComponentPropsWithoutRef } from "react";

export function ParallelIcon(props: ComponentPropsWithoutRef<"svg">) {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <path
        d="M2.66666 12V11.8667C2.66666 10.7466 2.66666 10.1865 2.88465 9.75869C3.0764 9.38236 3.38236 9.0764 3.75868 8.88466C4.1865 8.66667 4.74656 8.66667 5.86666 8.66667H10.1333C11.2534 8.66667 11.8135 8.66667 12.2413 8.88466C12.6176 9.0764 12.9236 9.38236 13.1153 9.75869C13.3333 10.1865 13.3333 10.7466 13.3333 11.8667V12M2.66666 12C1.93028 12 1.33333 12.597 1.33333 13.3333C1.33333 14.0697 1.93028 14.6667 2.66666 14.6667C3.40304 14.6667 3.99999 14.0697 3.99999 13.3333C3.99999 12.597 3.40304 12 2.66666 12ZM13.3333 12C12.5969 12 12 12.597 12 13.3333C12 14.0697 12.5969 14.6667 13.3333 14.6667C14.0697 14.6667 14.6667 14.0697 14.6667 13.3333C14.6667 12.597 14.0697 12 13.3333 12ZM8 12C7.26362 12 6.66666 12.597 6.66666 13.3333C6.66666 14.0697 7.26362 14.6667 8 14.6667C8.73637 14.6667 9.33333 14.0697 9.33333 13.3333C9.33333 12.597 8.73637 12 8 12ZM8 12V5.33334M4 5.33334H12C12.6213 5.33334 12.9319 5.33334 13.1769 5.23184C13.5036 5.09652 13.7632 4.83695 13.8985 4.51025C14 4.26522 14 3.95459 14 3.33334C14 2.71208 14 2.40145 13.8985 2.15642C13.7632 1.82972 13.5036 1.57016 13.1769 1.43483C12.9319 1.33334 12.6213 1.33334 12 1.33334H3.99999C3.37874 1.33334 3.06811 1.33334 2.82308 1.43483C2.49638 1.57016 2.23681 1.82972 2.10149 2.15642C1.99999 2.40145 1.99999 2.71208 1.99999 3.33334C1.99999 3.95459 1.99999 4.26522 2.10149 4.51025C2.23681 4.83695 2.49638 5.09652 2.82308 5.23184C3.06811 5.33334 3.37874 5.33334 4 5.33334Z"
        stroke="inherit"
        strokeWidth="1.33333"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

export function ParallelBranchIcon(props: ComponentPropsWithoutRef<"svg">) {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <path
        d="M10.6667 8C10.6667 9.47276 9.47276 10.6667 8 10.6667C6.52724 10.6667 5.33334 9.47276 5.33334 8M10.6667 8C10.6667 6.52724 9.47276 5.33334 8 5.33334C6.52724 5.33334 5.33334 6.52724 5.33334 8M10.6667 8H14.6667M5.33334 8H1.33347"
        stroke="inherit"
        strokeWidth="1.33333"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

export function ParallelCreateBranchIcon(
  props: ComponentPropsWithoutRef<"svg">,
) {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <path
        d="M8.00001 3.33334V12.6667M3.33334 8H12.6667"
        stroke="inherit"
        strokeWidth="1.33333"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

Element Nodes

We'll create the nodes for the new element, starting by adding the files shown below.

The Block component:

// src/shared/kits/workflow/mvc/view/entities/entities/nodes/element-type/parallel/flow/block.tsx

import { Handle, Position, NodeProps, Node } from "@xyflow/react";

import { ParallelIcon } from "@/shared/kits/workflow/icons/parallel";

import Base from "@/shared/kits/workflow/mvc/view/entities/components/flow/block";

export interface BlockEntity {
  type: "elementType/parallel/flow/block";
  meta: {
    type: "elementType";
    elementType: "parallel";
    mode: "flow";
    item: "block";
    id: string;
    dropArea: false;
  };
  data: {
    drag: boolean;
    onRemove: () => void;
  };
}

export default function Block({
  data,
  selected,
}: NodeProps<Node<BlockEntity["data"], BlockEntity["type"]>>) {
  return (
    <>
      <Handle type="target" position={Position.Top} />
      <Base
        drag={data.drag}
        slot={<Base.Slot size="sm" />}
        flow={
          <Base.Root selected={selected} failure={null}>
            <Base.Header>
              <Base.IconName>
                <Base.Icon icon={ParallelIcon} />
                <Base.Name>Parallel</Base.Name>
              </Base.IconName>
              <Base.Delete onRemove={data.onRemove} />
            </Base.Header>
          </Base.Root>
        }
      />
      <Handle type="source" position={Position.Bottom} />
    </>
  );
}

The AddNext component:

// src/shared/kits/workflow/mvc/view/entities/entities/nodes/element-type/parallel/flow/add-next.tsx

import { Handle, Position, NodeProps, Node } from "@xyflow/react";

import Base from "@/shared/kits/workflow/mvc/view/entities/components/flow/add";

export interface AddNextEntity {
  type: "elementType/parallel/flow/addNext";
  meta: {
    type: "elementType";
    elementType: "parallel";
    mode: "flow";
    item: "addNext";
    id: string;
    dropArea: true;
  };
  data: {
    drag: boolean;
    drop: boolean;
  };
}

export default function AddNext({
  data,
  selected,
}: NodeProps<Node<AddNextEntity["data"], AddNextEntity["type"]>>) {
  return (
    <>
      <Handle type="target" position={Position.Top} />
      <Base
        drag={data.drag}
        slot={<Base.Slot />}
        flow={<Base.Root drop={data.drop} selected={selected} failure={null} />}
      />
      <Handle type="source" position={Position.Bottom} />
    </>
  );
}

The Container component:

// src/shared/kits/workflow/mvc/view/entities/entities/nodes/element-type/parallel/flow/container.tsx

import { Handle, Position, NodeProps, Node } from "@xyflow/react";

import Base from "@/shared/kits/workflow/mvc/view/entities/components/flow/container";

export interface ContainerEntity {
  type: "elementType/parallel/flow/container";
  meta: {
    type: "elementType";
    elementType: "parallel";
    mode: "flow";
    item: "container";
    id: string;
    dropArea: false;
  };
  data: {
    drag: boolean;
    size: {
      w: number;
      h: number;
    };
  };
}

export default function Container({
  data,
}: NodeProps<Node<ContainerEntity["data"], ContainerEntity["type"]>>) {
  return (
    <>
      <Handle type="target" position={Position.Top} />
      <Base
        drag={data.drag}
        slot={<Base.Slot size={data.size} />}
        flow={<Base.Root size={data.size} />}
      />
      <Handle type="source" position={Position.Bottom} />
    </>
  );
}

The BlockBranch component:

// src/shared/kits/workflow/mvc/view/entities/entities/nodes/element-type/parallel/flow/block-branch.tsx

import { Handle, Position, NodeProps, Node } from "@xyflow/react";

import { ParallelBranchIcon } from "@/shared/kits/workflow/icons/parallel";

import Base from "@/shared/kits/workflow/mvc/view/entities/components/flow/block";

export interface BlockBranchEntity {
  type: "elementType/parallel/flow/blockBranch";
  meta: {
    type: "elementType";
    elementType: "parallel";
    mode: "flow";
    item: "blockBranch";
    id: string;
    branch: number;
    dropArea: false;
  };
  data: {
    drag: boolean;
    text: string;
    failure: string | null;
    onRemove: () => void;
  };
}

export default function BlockBranch({
  data,
  selected,
}: NodeProps<Node<BlockBranchEntity["data"], BlockBranchEntity["type"]>>) {
  return (
    <>
      <Handle type="target" position={Position.Top} />
      <Base
        drag={data.drag}
        slot={<Base.Slot size="md" />}
        flow={
          <Base.Root selected={selected} failure={data.failure}>
            <Base.Header>
              <Base.IconName>
                <Base.Icon icon={ParallelBranchIcon} />
                <Base.Name>Branch</Base.Name>
              </Base.IconName>
              <Base.Delete onRemove={data.onRemove} />
            </Base.Header>
            <Base.Content>{data.text}</Base.Content>
          </Base.Root>
        }
      />
      <Handle type="source" position={Position.Bottom} />
    </>
  );
}

The AddBranch component:

// src/shared/kits/workflow/mvc/view/entities/entities/nodes/element-type/parallel/flow/add-branch.tsx

import { Handle, Position, NodeProps, Node } from "@xyflow/react";

import Base from "@/shared/kits/workflow/mvc/view/entities/components/flow/add";

export interface AddBranchEntity {
  type: "elementType/parallel/flow/addBranch";
  meta: {
    type: "elementType";
    elementType: "parallel";
    mode: "flow";
    item: "addBranch";
    id: string;
    branch: number;
    dropArea: true;
  };
  data: {
    drag: boolean;
    drop: boolean;
  };
}

export default function AddBranch({
  data,
  selected,
}: NodeProps<Node<AddBranchEntity["data"], AddBranchEntity["type"]>>) {
  return (
    <>
      <Handle type="target" position={Position.Top} />
      <Base
        drag={data.drag}
        slot={<Base.Slot />}
        flow={<Base.Root drop={data.drop} selected={selected} failure={null} />}
      />
      <Handle type="source" position={Position.Bottom} />
    </>
  );
}

The CreateBranch component:

// src/shared/kits/workflow/mvc/view/entities/entities/nodes/element-type/parallel/flow/create-branch.tsx

import { Handle, Position, NodeProps, Node } from "@xyflow/react";

import { ParallelCreateBranchIcon } from "@/shared/kits/workflow/icons/parallel";

import Base from "@/shared/kits/workflow/mvc/view/entities/components/flow/create";

export interface CreateBranchEntity {
  type: "elementType/parallel/flow/createBranch";
  meta: {
    type: "elementType";
    elementType: "parallel";
    mode: "flow";
    item: "createBranch";
    id: string;
    dropArea: false;
  };
  data: {
    drag: boolean;
  };
}

export default function CreateBranch({
  data,
  selected,
}: NodeProps<Node<CreateBranchEntity["data"], CreateBranchEntity["type"]>>) {
  return (
    <>
      <Handle type="target" position={Position.Top} />
      <Base
        drag={data.drag}
        slot={<Base.Slot />}
        flow={
          <Base.Root selected={selected}>
            <Base.Icon icon={ParallelCreateBranchIcon} />
          </Base.Root>
        }
      />
      <Handle type="source" position={Position.Bottom} />
    </>
  );
}

After creating the nodes, we'll add the following file, which exports the FlowEntity union type and the flowNodeTypes object:

// src/shared/kits/workflow/mvc/view/entities/entities/nodes/element-type/parallel/flow/index.ts

import type { NodeTypes } from "@xyflow/react";

import Block, { BlockEntity } from "./block";
import AddNext, { AddNextEntity } from "./add-next";
import Container, { ContainerEntity } from "./container";
import BlockBranch, { BlockBranchEntity } from "./block-branch";
import AddBranch, { AddBranchEntity } from "./add-branch";
import CreateBranch, { CreateBranchEntity } from "./create-branch";

export type FlowEntity =
  | BlockEntity
  | AddNextEntity
  | ContainerEntity
  | BlockBranchEntity
  | AddBranchEntity
  | CreateBranchEntity;

export const flowNodeTypes: NodeTypes = {
  "elementType/parallel/flow/block": Block,
  "elementType/parallel/flow/addNext": AddNext,
  "elementType/parallel/flow/contaienr": Container,
  "elementType/parallel/flow/blockBranch": BlockBranch,
  "elementType/parallel/flow/addBranch": AddBranch,
  "elementType/parallel/flow/createBranch": CreateBranch,
};

We'll also add the file shown below, which exports the ParallelEntity union type along with the parallelNodeTypes object

// src/shared/kits/workflow/mvc/view/entities/entities/nodes/element-type/parallel/index.ts

import type { NodeTypes } from "@xyflow/react";

import { flowNodeTypes, type FlowEntity } from "./flow";

export type ParallelEntity = FlowEntity;

export const parallelNodeTypes: NodeTypes = {
  ...flowNodeTypes,
};

Finally, we'll also update the following file to add the new type to ElementTypeEntity and extend the elementTypeNodeTypes object:

// src/shared/kits/workflow/mvc/view/entities/entities/nodes/element-type/index.ts

import type { NodeTypes } from "@xyflow/react";

import { actionNodeTypes, ActionEntity } from "./action";
import { conditionNodeTypes, ConditionEntity } from "./condition";
import { switchNodeTypes, SwitchEntity } from "./switch";
import { loopNodeTypes, LoopEntity } from "./loop";
import { parallelNodeTypes, ParallelEntity } from "./parallel";

export type ElementTypeEntity =
  | ActionEntity
  | ConditionEntity
  | SwitchEntity
  | LoopEntity
  | ParallelEntity;

export const elementTypeNodeTypes: NodeTypes = {
  ...actionNodeTypes,
  ...conditionNodeTypes,
  ...switchNodeTypes,
  ...loopNodeTypes,
  ...parallelNodeTypes,
};

Element View

Now that we've created the nodes, we need to implement the function responsible for generating the visual representation of the element. This function must return both the top and bottom nodes, and its implementation is shown below:

// src/shared/kits/workflow/mvc/view/view/flow-view/element/parallel.ts

import { constants } from "@/constants";

import type { ParallelFlow } from "@/shared/kits/workflow/mvc/model/workflow";

import type {
  TypedFlowNode,
  TypedFlowEdge,
  TypedFlowComponent,
  TypedFlowContainer,
} from "@/shared/kits/workflow/mvc/view/types/flows";

import { connect } from "@/shared/lib/flows";

import { elementView } from ".";

import type { Options } from "../types";

import { isDragging, isSelected } from "../utils";

export function parallelView(
  element: ParallelFlow,
  options: Options,
): [TypedFlowNode, TypedFlowNode] {
  const dragging = isDragging(element, options.dragging);

  const block: TypedFlowComponent = {
    id: `${element.id}/flow/block`,
    type: "component",
    entity: {
      type: "elementType/parallel/flow/block",
      meta: {
        type: "elementType",
        elementType: "parallel",
        mode: "flow",
        item: "block",
        id: element.id,
        dropArea: false,
      },
      data: {
        drag: dragging,
        onRemove: () => {
          options.onWorkflowChange([
            {
              type: "element",
              change: "remove",
              id: element.id,
            },
          ]);
        },
      },
    },
    selected: isSelected(`${element.id}/flow/block`, options.active),
    position: {
      x: 0,
      y: 0,
    },
    positionAbsolute: {
      x: 0,
      y: 0,
    },
    size: {
      w: constants.workflow.node.block.w,
      h: constants.workflow.node.block.h.sm,
    },
    gaps: {
      next: constants.workflow.gaps.next,
    },
    next: [],
    prev: [],
    parent: options.parent,
  };

  options.map.set(block.id, block);

  const container: TypedFlowContainer = {
    id: `${element.id}/flow/container`,
    type: "container",
    entity: {
      type: "elementType/loop/flow/container",
      meta: {
        type: "elementType",
        elementType: "loop",
        mode: "flow",
        item: "container",
        id: element.id,
        dropArea: false,
      },
      data: {
        drag: dragging,
        size: {
          w: 0,
          h: 0,
        },
      },
    },
    selected: isSelected(`${element.id}/flow/container`, options.active),
    position: {
      x: 0,
      y: 0,
    },
    positionAbsolute: {
      x: 0,
      y: 0,
    },
    size: {
      w: 0,
      h: 0,
    },
    room: {
      x: constants.workflow.room.x,
      y: constants.workflow.room.y,
    },
    gaps: {
      next: 0,
      into: constants.workflow.gaps.into,
    },
    into: [],
    next: [],
    prev: [],
    parent: options.parent,
  };

  options.map.set(container.id, container);

  const blockToContainer: TypedFlowEdge = {
    id: `${block.id}-${container.id}`,
    entity: { type: "flow/edge", data: { drag: dragging } },
    length: constants.workflow.edge.sm,
    source: block,
    target: container,
  };

  block.next.push({ edge: blockToContainer, node: container });
  container.prev.push({ edge: blockToContainer, node: block });

  for (let i = 0; i < element.branches.length; i++) {
    const branch = element.branches[i]!;

    const blockBranch: TypedFlowComponent = {
      id: `${element.id}/flow/blockBranch/${i}`,
      type: "component",
      entity: {
        type: "elementType/parallel/flow/blockBranch",
        meta: {
          type: "elementType",
          elementType: "parallel",
          mode: "flow",
          item: "blockBranch",
          id: element.id,
          branch: i,
          dropArea: false,
        },
        data: {
          drag: dragging,
          text: branch.label,
          failure: parallelFailure(element, i, options),
          onRemove: () => {
            options.onWorkflowChange([
              {
                type: "elementType",
                elementType: "parallel",
                change: "deleteBranch",
                id: element.id,
                branch: i,
              },
            ]);
          },
        },
      },
      selected: isSelected(
        `${element.id}/flow/blockBranch/${i}`,
        options.active,
      ),
      position: {
        x: 0,
        y: 0,
      },
      positionAbsolute: {
        x: 0,
        y: 0,
      },
      size: {
        w: constants.workflow.node.block.w,
        h: constants.workflow.node.block.h.md,
      },
      gaps: {
        next: 0,
      },
      next: [],
      prev: [],
      parent: container,
    };

    options.map.set(blockBranch.id, blockBranch);

    const addBranch: TypedFlowComponent = {
      id: `${element.id}/flow/addBranch/${i}`,
      type: "component",
      entity: {
        type: "elementType/parallel/flow/addBranch",
        meta: {
          type: "elementType",
          elementType: "parallel",
          mode: "flow",
          item: "addBranch",
          id: element.id,
          branch: i,
          dropArea: true,
        },
        data: {
          drag: dragging,
          drop: element.dropBranches[i]!.size > 0,
        },
      },
      selected: isSelected(`${element.id}/flow/addBranch/${i}`, options.active),
      position: {
        x: 0,
        y: 0,
      },
      positionAbsolute: {
        x: 0,
        y: 0,
      },
      size: {
        w: constants.workflow.node.add.size.w,
        h: constants.workflow.node.add.size.h,
      },
      gaps: {
        next: 0,
      },
      next: [],
      prev: [],
      parent: container,
    };

    options.map.set(addBranch.id, addBranch);

    const blockBranchToAddBranch: TypedFlowEdge = {
      id: `${blockBranch.id}-${addBranch.id}`,
      entity: { type: "flow/edge", data: { drag: dragging } },
      length: constants.workflow.edge.sm,
      source: blockBranch,
      target: addBranch,
    };

    blockBranch.next.push({ edge: blockBranchToAddBranch, node: addBranch });
    addBranch.prev.push({ edge: blockBranchToAddBranch, node: blockBranch });

    const nodesBranch = branch.then.map((n) => {
      return elementView(n, {
        ...options,
        dragging: dragging,
        parent: container,
      });
    });

    connect(
      [addBranch, ...nodesBranch],
      (top, bottom): TypedFlowEdge => ({
        id: `${top.id}-${bottom.id}`,
        entity: { type: "flow/edge", data: { drag: dragging } },
        length: constants.workflow.edge.sm,
        source: top,
        target: bottom,
      }),
    );

    container.into.push(blockBranch);
  }

  const createBranch: TypedFlowComponent = {
    id: `${element.id}/flow/createBranch`,
    type: "component",
    entity: {
      type: "elementType/parallel/flow/createBranch",
      meta: {
        type: "elementType",
        elementType: "parallel",
        mode: "flow",
        item: "createBranch",
        id: element.id,
        dropArea: false,
      },
      data: {
        drag: dragging,
      },
    },
    selected: isSelected(`${element.id}/flow/createBranch`, options.active),
    position: {
      x: 0,
      y: 0,
    },
    positionAbsolute: {
      x: 0,
      y: 0,
    },
    size: {
      w: constants.workflow.node.create.w,
      h: constants.workflow.node.create.h,
    },
    gaps: {
      next: 0,
    },
    next: [],
    prev: [],
    parent: container,
  };

  options.map.set(createBranch.id, createBranch);

  container.into.push(createBranch);

  const addNext: TypedFlowComponent = {
    id: `${element.id}/flow/addNext`,
    type: "component",
    entity: {
      type: "elementType/parallel/flow/addNext",
      meta: {
        type: "elementType",
        elementType: "parallel",
        mode: "flow",
        item: "addNext",
        id: element.id,
        dropArea: true,
      },
      data: {
        drag: dragging,
        drop: element.dropNext.size > 0,
      },
    },
    selected: isSelected(`${element.id}/flow/addNext`, options.active),
    position: {
      x: 0,
      y: 0,
    },
    positionAbsolute: {
      x: 0,
      y: 0,
    },
    size: {
      w: constants.workflow.node.add.size.w,
      h: constants.workflow.node.add.size.h,
    },
    gaps: {
      next: 0,
    },
    next: [],
    prev: [],
    parent: options.parent,
  };

  options.map.set(addNext.id, addNext);

  const containerToAddNext: TypedFlowEdge = {
    id: `${container.id}-${addNext.id}`,
    entity: { type: "flow/edge", data: { drag: dragging } },
    length: constants.workflow.edge.sm,
    source: container,
    target: addNext,
  };

  container.next.push({ edge: containerToAddNext, node: addNext });
  addNext.prev.push({ edge: containerToAddNext, node: container });

  return [block, addNext];
}

function parallelFailure(
  element: ParallelFlow,
  branch: number,
  options: Options,
): string | null {
  if (options.failure) {
    if (options.failure.type === "element") {
      if (options.failure.element === "parallel") {
        if (options.failure.id === element.id) {
          if (options.failure.branch === branch) {
            return options.failure.message;
          }
        }
      }
    }
  }
  return null;
}

Finally, we need to update the file below to add the function we just created:

// src/shared/kits/workflow/mvc/view/view/flow-view/element/index.ts

import { zet } from "@/shared/lib/zet";

import type { ElementFlow } from "@/shared/kits/workflow/mvc/model/workflow";
import type { TypedFlowNode } from "@/shared/kits/workflow/mvc/view/types/flows";

import type { Options } from "../types";

import { actionView } from "./action";
import { conditionView } from "./condition";
import { switchView } from "./switch";
import { loopView } from "./loop";
import { parallelView } from "./parallel";

interface Zet {
  object: ElementFlow;
  nested: [];
  filter: ["type"];
  params: [Options];
  return: [TypedFlowNode, TypedFlowNode];
}

const dispatch = zet<Zet>([], ["type"], {
  action: actionView,
  condition: conditionView,
  switch: switchView,
  loop: loopView,
  parallel: parallelView,
});

export function elementView(
  element: ElementFlow,
  options: Options,
): [TypedFlowNode, TypedFlowNode] {
  return dispatch(element, options);
}