Create ElementElement Drag View

Create Element

Element Drag View

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


Element Drag Nodes

When the element is dragged, it will use different nodes than the ones defined earlier, so we'll create the following files to support this behavior.

The Block component:

// src/shared/kits/workflow/mvc/view/entities/entities/nodes/element-type/parallel/drag/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/drag/block";

export interface BlockEntity {
  type: "elementType/parallel/drag/block";
  meta: {
    type: "elementType";
    elementType: "parallel";
    mode: "drag";
    item: "block";
    id: string;
  };
  data: {
    root: boolean;
  };
}

export default function Block({
  data,
}: NodeProps<Node<BlockEntity["data"], BlockEntity["type"]>>) {
  return (
    <>
      <Handle type="target" position={Position.Top} />
      <Base root={data.root}>
        <Base.Header>
          <Base.IconName>
            <Base.Icon icon={ParallelIcon} />
            <Base.Name>Parallel</Base.Name>
          </Base.IconName>
          <Base.Delete />
        </Base.Header>
      </Base>
      <Handle type="source" position={Position.Bottom} />
    </>
  );
}

The AddNext component:

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

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

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

export interface AddNextEntity {
  type: "elementType/parallel/drag/addNext";
  meta: {
    type: "elementType";
    elementType: "parallel";
    mode: "drag";
    item: "addNext";
    id: string;
  };
  data: Record<string, never>;
}

export default function AddNext() {
  return (
    <>
      <Handle type="target" position={Position.Top} />
      <Base />
      <Handle type="source" position={Position.Bottom} />
    </>
  );
}

The Container component:

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

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

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

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

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

The BlockBranch component:

// src/shared/kits/workflow/mvc/view/entities/entities/nodes/element-type/parallel/drag/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/drag/block";

export interface BlockBranchEntity {
  type: "elementType/parallel/drag/blockBranch";
  meta: {
    type: "elementType";
    elementType: "parallel";
    mode: "drag";
    item: "blockBranch";
    id: string;
    branch: number;
  };
  data: {
    text: string;
  };
}

export default function BlockBranch({
  data,
}: NodeProps<Node<BlockBranchEntity["data"], BlockBranchEntity["type"]>>) {
  return (
    <>
      <Handle type="target" position={Position.Top} />
      <Base root={false}>
        <Base.Header>
          <Base.IconName>
            <Base.Icon icon={ParallelBranchIcon} />
            <Base.Name>Branch</Base.Name>
          </Base.IconName>
          <Base.Delete />
        </Base.Header>
        <Base.Content>{data.text}</Base.Content>
      </Base>
      <Handle type="source" position={Position.Bottom} />
    </>
  );
}

The AddBranch component:

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

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

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

export interface AddBranchEntity {
  type: "elementType/parallel/drag/addBranch";
  meta: {
    type: "elementType";
    elementType: "parallel";
    mode: "drag";
    item: "addBranch";
    id: string;
    branch: number;
  };
  data: Record<string, never>;
}

export default function AddBranch() {
  return (
    <>
      <Handle type="target" position={Position.Top} />
      <Base />
      <Handle type="source" position={Position.Bottom} />
    </>
  );
}

The CreateBranch component:

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

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

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

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

export interface CreateBranchEntity {
  type: "elementType/parallel/drag/createBranch";
  meta: {
    type: "elementType";
    elementType: "parallel";
    mode: "drag";
    item: "createBranch";
    id: string;
  };
  data: Record<string, never>;
}

export default function CreateBranch() {
  return (
    <>
      <Handle type="target" position={Position.Top} />
      <Base>
        <Base.Icon icon={ParallelCreateBranchIcon} />
      </Base>
      <Handle type="source" position={Position.Bottom} />
    </>
  );
}

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

// src/shared/kits/workflow/mvc/view/entities/entities/nodes/element-type/parallel/drag/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 DragEntity =
  | BlockEntity
  | AddNextEntity
  | ContainerEntity
  | BlockBranchEntity
  | AddBranchEntity
  | CreateBranchEntity;

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

W'll also update the file shown below to add the new type to ParallelEntity and extend 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";
import { dragNodeTypes, type DragEntity } from "./drag";

export type ParallelEntity = FlowEntity | DragEntity;

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

Element Drag View

Now that we've created the nodes, we need to implement a function that receives the new element and generates the visual representation of the dragged flow - either for the element itself when dragged, or for one of its nested elements when that element is dragged:

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

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

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

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

import { elementFlowView } from "../element-flow-view";
import { elementDragView } from ".";

export function parallelView(
  element: ParallelFlow,
  options: DragOptions,
): TypedFlowNode | null {
  if (element.dragging) {
    const [node] = elementFlowView(element, {
      map: options.map,
      root: true,
      parent: null,
      position: element.dragging,
    });
    autoLayout(node);
    return node;
  }
  for (const branch of element.branches) {
    for (const item of branch.then) {
      const view = elementDragView(item, { map: options.map });
      if (view) return view;
    }
  }
  return null;
}

We also need to update the file below to add the function we just created:

// src/shared/kits/workflow/mvc/view/view/drag-view/element-drag-view/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 { DragOptions } 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: [DragOptions];
  return: TypedFlowNode | null;
}

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

/**
 * Builds the visual representation of the dragged flow - for the element itself
 * if it's being dragged, or for one of its nested elements if that element is being dragged.
 * Returns the root node, or `null` if no dragged flow exists.
 */
export function elementDragView(
  element: ElementFlow,
  options: DragOptions,
): TypedFlowNode | null {
  return dispatch(element, options);
}

Besides the previous function, we must also implement the one shown below, which returns the visual representation of the element's dragged flow:

// src/shared/kits/workflow/mvc/view/view/drag-view/element-flow-view/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 type { FlowOptions } from "../types";

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

import { elementFlowView } from ".";

export function parallelView(
  element: ParallelFlow,
  options: FlowOptions,
): [TypedFlowNode, TypedFlowNode] {
  const block: TypedFlowComponent = {
    id: `${element.id}/drag/block`,
    type: "component",
    entity: {
      type: "elementType/parallel/drag/block",
      meta: {
        type: "elementType",
        elementType: "parallel",
        mode: "drag",
        item: "block",
        id: element.id,
      },
      data: {
        root: options.root,
      },
    },
    selected: false,
    position: { ...options.position },
    positionAbsolute: { ...options.position },
    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}/drag/container`,
    type: "container",
    entity: {
      type: "elementType/parallel/drag/container",
      meta: {
        type: "elementType",
        elementType: "parallel",
        mode: "drag",
        item: "container",
        id: element.id,
      },
      data: {
        size: {
          w: 0,
          h: 0,
        },
      },
    },
    selected: false,
    position: { ...options.position },
    positionAbsolute: { ...options.position },
    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: "drag/edge", data: {} },
    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}/drag/blockBranch/${i}`,
      type: "component",
      entity: {
        type: "elementType/parallel/drag/blockBranch",
        meta: {
          type: "elementType",
          elementType: "parallel",
          mode: "drag",
          item: "blockBranch",
          id: element.id,
          branch: i,
        },
        data: {
          text: branch.label,
        },
      },
      selected: false,
      position: { ...options.position },
      positionAbsolute: { ...options.position },
      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}/drag/addBranch/${i}`,
      type: "component",
      entity: {
        type: "elementType/parallel/drag/addBranch",
        meta: {
          type: "elementType",
          elementType: "parallel",
          mode: "drag",
          item: "addBranch",
          id: element.id,
          branch: i,
        },
        data: {},
      },
      selected: false,
      position: { ...options.position },
      positionAbsolute: { ...options.position },
      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: "drag/edge", data: {} },
      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 elementFlowView(n, {
        ...options,
        root: false,
        parent: container,
      });
    });

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

    container.into.push(blockBranch);
  }

  const createBranch: TypedFlowComponent = {
    id: `${element.id}/drag/createBranch`,
    type: "component",
    entity: {
      type: "elementType/parallel/drag/createBranch",
      meta: {
        type: "elementType",
        elementType: "parallel",
        mode: "drag",
        item: "createBranch",
        id: element.id,
      },
      data: {},
    },
    selected: false,
    position: { ...options.position },
    positionAbsolute: { ...options.position },
    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}/drag/addNext`,
    type: "component",
    entity: {
      type: "elementType/parallel/drag/addNext",
      meta: {
        type: "elementType",
        elementType: "parallel",
        mode: "drag",
        item: "addNext",
        id: element.id,
      },
      data: {},
    },
    selected: false,
    position: { ...options.position },
    positionAbsolute: { ...options.position },
    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: "drag/edge", data: {} },
    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];
}

We also need to update the file below to add the function we just created:

// src/shared/kits/workflow/mvc/view/view/drag-view/element-flow-view/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 { actionView } from "./action";
import { conditionView } from "./condition";
import { switchView } from "./switch";
import { loopView } from "./loop";
import { parallelView } from "./parallel";

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

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

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

/**
 * Builds the visual representation of the element's dragged flow
 * and returns its top and bottom nodes.
 */
export function elementFlowView(
  element: ElementFlow,
  options: FlowOptions,
): [TypedFlowNode, TypedFlowNode] {
  return dispatch(element, options);
}