Create ElementWidget View

Create Element

Widget View

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


Widget View

We'll now implement the function that receives the element's widget and returns the correct component, and for that we'll create the file shown below:

TSX
// src/shared/kits/workflow/mvc/view/widget/widget/element-type/parallel/index.tsx

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

import type {
  Workflow,
  ParallelWidget,
} from "@/shared/kits/workflow/mvc/model/workflow";
import type { OnWorkflowChange } from "@/shared/kits/workflow/mvc/model/on-workflow-change";

import { ParallelCreateBranchView } from "./create-branch";
import { ParallelBranchInfoView } from "./branch-info";
import { ParallelAddBranchView } from "./add-branch";

interface Zet {
  object: ParallelWidget;
  nested: [];
  filter: ["widget"];
  params: [Workflow, OnWorkflowChange];
  return: React.ReactNode;
}

const dispatch = zet<Zet>([], ["widget"], {
  createBranch: (widget, workflow, onWorkflowChange) => (
    <ParallelCreateBranchView
      key={workflow.active}
      widget={widget}
      workflow={workflow}
      onWorkflowChange={onWorkflowChange}
    />
  ),
  branchInfo: (widget, workflow, onWorkflowChange) => (
    <ParallelBranchInfoView
      key={workflow.active}
      widget={widget}
      workflow={workflow}
      onWorkflowChange={onWorkflowChange}
    />
  ),
  addBranch: (widget, workflow, onWorkflowChange) => (
    <ParallelAddBranchView
      key={workflow.active}
      widget={widget}
      workflow={workflow}
      onWorkflowChange={onWorkflowChange}
    />
  ),
});

export function parallelView(
  widget: ParallelWidget,
  workflow: Workflow,
  onWorkflowChange: OnWorkflowChange,
): React.ReactNode {
  return dispatch(widget, workflow, onWorkflowChange);
}

We'll also need to create the following components that are imported.

The ParallelCreateBranchView component:

TSX
// src/shared/kits/workflow/mvc/view/widget/widget/element-type/parallel/create-branch.tsx

import { useCallback } from "react";
import { Formity, type s, type Flow, type ReturnOutput } from "@formity/react";

import type {
  Workflow,
  ParallelCreateBranchWidget,
} from "@/shared/kits/workflow/mvc/model/workflow";

import type { OnWorkflowChange } from "@/shared/kits/workflow/mvc/model/on-workflow-change";

import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";

import { Button } from "@/shared/ui/button";

import { Sidebar } from "@/shared/ui/sidebar";

import { Editor, LongText } from "@/shared/kits/editor";

type Schema = {
  render: React.ReactNode;
  struct: [s.Form<{ label: string }>, s.Return<{ label: string }>];
  inputs: {
    label: string;
  };
  params: Record<never, never>;
};

const flow: Flow<Schema> = [
  {
    form: {
      fields: ({ label }) => ({
        label: [label, []],
      }),
      render: ({ fields, next }) => (
        <Editor
          defaultValues={fields}
          resolver={zodResolver(
            z.object({
              label: z.string(),
            }),
          )}
          onSubmit={next}
        >
          <LongText name="label">
            <LongText.Label>Label</LongText.Label>
            <LongText.Input maxLength={160} placeholder="Enter the label" />
          </LongText>
          <Button type="submit">Save</Button>
        </Editor>
      ),
    },
  },
  {
    return: ({ label }) => ({
      label: label,
    }),
  },
];

interface ParallelCreateBranchViewProps {
  widget: ParallelCreateBranchWidget;
  workflow: Workflow;
  onWorkflowChange: OnWorkflowChange;
}

export function ParallelCreateBranchView({
  widget,
  onWorkflowChange,
}: ParallelCreateBranchViewProps): React.ReactNode {
  const onSubmit = useCallback(
    (output: ReturnOutput<Schema>) => {
      onWorkflowChange([
        {
          type: "elementType",
          elementType: "parallel",
          change: "createBranch",
          id: widget.element.id,
          label: output.label,
        },
      ]);
    },
    [widget.element.id, onWorkflowChange],
  );

  return (
    <Sidebar className="absolute inset-y-0 right-0 overflow-auto border-l border-l-gray-200">
      <Sidebar.Top>
        <Sidebar.Heading>Create branch</Sidebar.Heading>
        <Sidebar.Close
          onClick={() => {
            onWorkflowChange([{ type: "global", change: "removeWidget" }]);
          }}
        />
      </Sidebar.Top>
      <Sidebar.Content>
        <Formity<Schema>
          flow={flow}
          inputs={{
            label: "",
          }}
          onReturn={onSubmit}
        />
      </Sidebar.Content>
      <Sidebar.Divider />
    </Sidebar>
  );
}

The ParallelBranchInfoView component:

TSX
// src/shared/kits/workflow/mvc/view/widget/widget/element-type/parallel/branch-info.tsx

import { useCallback } from "react";
import { Formity, type s, type Flow, type ReturnOutput } from "@formity/react";

import type {
  Workflow,
  ParallelBranchInfoWidget,
} from "@/shared/kits/workflow/mvc/model/workflow";

import type { OnWorkflowChange } from "@/shared/kits/workflow/mvc/model/on-workflow-change";

import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";

import { Button } from "@/shared/ui/button";

import { Sidebar } from "@/shared/ui/sidebar";

import { Editor, LongText } from "@/shared/kits/editor";

type Schema = {
  render: React.ReactNode;
  struct: [s.Form<{ label: string }>, s.Return<{ label: string }>];
  inputs: {
    label: string;
  };
  params: Record<never, never>;
};

const flow: Flow<Schema> = [
  {
    form: {
      fields: ({ label }) => ({
        label: [label, []],
      }),
      render: ({ fields, next }) => (
        <Editor
          defaultValues={fields}
          resolver={zodResolver(
            z.object({
              label: z.string(),
            }),
          )}
          onSubmit={next}
        >
          <LongText name="label">
            <LongText.Label>Label</LongText.Label>
            <LongText.Input maxLength={160} placeholder="Enter the label" />
          </LongText>
          <Button type="submit">Save</Button>
        </Editor>
      ),
    },
  },
  {
    return: ({ label }) => ({
      label: label,
    }),
  },
];

interface ParallelBranchInfoViewProps {
  widget: ParallelBranchInfoWidget;
  workflow: Workflow;
  onWorkflowChange: OnWorkflowChange;
}

export function ParallelBranchInfoView({
  widget,
  onWorkflowChange,
}: ParallelBranchInfoViewProps): React.ReactNode {
  const onSubmit = useCallback(
    (output: ReturnOutput<Schema>) => {
      onWorkflowChange([
        {
          type: "elementType",
          elementType: "parallel",
          change: "branchInfo",
          id: widget.element.id,
          branch: widget.branch,
          label: output.label,
        },
      ]);
    },
    [widget.element.id, widget.branch, onWorkflowChange],
  );

  return (
    <Sidebar className="absolute inset-y-0 right-0 overflow-auto border-l border-l-gray-200">
      <Sidebar.Top>
        <Sidebar.Heading>Branch</Sidebar.Heading>
        <Sidebar.Close
          onClick={() => {
            onWorkflowChange([{ type: "global", change: "removeWidget" }]);
          }}
        />
      </Sidebar.Top>
      <Sidebar.Content>
        <Formity<Schema>
          flow={flow}
          inputs={{
            label: widget.element.branches[widget.branch]!.label,
          }}
          onReturn={onSubmit}
        />
      </Sidebar.Content>
      <Sidebar.Divider />
    </Sidebar>
  );
}

The ParallelAddBranchView component:

TSX
// src/shared/kits/workflow/mvc/view/widget/widget/element-type/parallel/add-branch.tsx

import type {
  Workflow,
  ParallelAddBranchWidget,
} from "@/shared/kits/workflow/mvc/model/workflow";

import type { OnWorkflowChange } from "@/shared/kits/workflow/mvc/model/on-workflow-change";

import { AddElement } from "@/shared/kits/workflow/mvc/view/widget/widget-components/add-element";

interface ParallelAddBranchViewProps {
  widget: ParallelAddBranchWidget;
  workflow: Workflow;
  onWorkflowChange: OnWorkflowChange;
}

export function ParallelAddBranchView({
  widget,
  onWorkflowChange,
}: ParallelAddBranchViewProps): React.ReactNode {
  return (
    <AddElement
      onClose={() => {
        onWorkflowChange([{ type: "global", change: "removeWidget" }]);
      }}
      onAdd={(element) => {
        onWorkflowChange([
          {
            type: "elementType",
            elementType: "parallel",
            change: "addBranch",
            id: widget.element.id,
            branch: widget.branch,
            element,
          },
        ]);
      }}
    />
  );
}

Finally, we'll update the following file to include the new function we created:

TSX
// src/shared/kits/workflow/mvc/view/widget/widget/element-type/index.tsx

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

import type {
  Workflow,
  ElementTypeWidget,
} from "@/shared/kits/workflow/mvc/model/workflow";

import type { OnWorkflowChange } from "@/shared/kits/workflow/mvc/model/on-workflow-change";

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

interface Zet {
  object: ElementTypeWidget;
  nested: [];
  filter: ["elementType"];
  params: [Workflow, OnWorkflowChange];
  return: React.ReactNode;
}

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

export function elementTypeView(
  widget: ElementTypeWidget,
  workflow: Workflow,
  onWorkflowChange: OnWorkflowChange,
): React.ReactNode {
  return dispatch(widget, workflow, onWorkflowChange);
}

Add Element Widget

We also need to add the new element to the component responsible for adding elements to the workflow, as shown in the following file:

TSX
// src/shared/kits/workflow/mvc/view/widget/widget-components/add-element.tsx

import { Sidebar } from "@/shared/ui/sidebar";
import { Button } from "@/shared/ui/button";

import { ActionIcon } from "@/shared/kits/workflow/icons/action";
import { ConditionIcon } from "@/shared/kits/workflow/icons/condition";
import { SwitchIcon } from "@/shared/kits/workflow/icons/switch";
import { LoopIcon } from "@/shared/kits/workflow/icons/loop";
import { ParallelIcon } from "@/shared/kits/workflow/icons/parallel";

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

interface AddElementProps {
  onClose: () => void;
  onAdd: (element: ElementFlow) => void;
}

export function AddElement({
  onClose,
  onAdd,
}: AddElementProps): React.ReactNode {
  return (
    <Sidebar className="absolute inset-y-0 right-0 overflow-auto border-l border-l-gray-200">
      <Sidebar.Top>
        <Sidebar.Heading>Add element</Sidebar.Heading>
        <Sidebar.Close onClick={onClose} />
      </Sidebar.Top>
      <Sidebar.Content>
        <Button
          variant="light"
          className="w-full justify-start gap-2 px-3 [&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:stroke-gray-500"
          onClick={() =>
            onAdd({
              type: "action",
              id: crypto.randomUUID(),
              message: "",
              dragging: null,
              dropNext: new Set(),
            })
          }
        >
          <ActionIcon />
          Action
        </Button>
        <Button
          variant="light"
          className="w-full justify-start gap-2 px-3 [&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:stroke-gray-500"
          onClick={() =>
            onAdd({
              type: "condition",
              id: crypto.randomUUID(),
              if: "",
              then: [],
              else: [],
              dragging: null,
              dropNext: new Set(),
              dropThen: new Set(),
              dropElse: new Set(),
            })
          }
        >
          <ConditionIcon />
          Condition
        </Button>
        <Button
          variant="light"
          className="w-full justify-start gap-2 px-3 [&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:stroke-gray-500"
          onClick={() =>
            onAdd({
              type: "switch",
              id: crypto.randomUUID(),
              branches: [],
              default: [],
              dragging: null,
              dropNext: new Set(),
              dropBranches: [],
              dropDefault: new Set(),
            })
          }
        >
          <SwitchIcon />
          Switch
        </Button>
        <Button
          variant="light"
          className="w-full justify-start gap-2 px-3 [&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:stroke-gray-500"
          onClick={() =>
            onAdd({
              type: "loop",
              id: crypto.randomUUID(),
              while: "",
              into: [],
              dragging: null,
              dropNext: new Set(),
              dropInto: new Set(),
            })
          }
        >
          <LoopIcon />
          Loop
        </Button>
        <Button
          variant="light"
          className="w-full justify-start gap-2 px-3 [&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:stroke-gray-500"
          onClick={() =>
            onAdd({
              type: "parallel",
              id: crypto.randomUUID(),
              branches: [],
              dragging: null,
              dropNext: new Set(),
              dropBranches: [],
            })
          }
        >
          <ParallelIcon />
          Parallel
        </Button>
      </Sidebar.Content>
    </Sidebar>
  );
}