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:

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

import type { ReactNode } from "react";

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: 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,
): ReactNode {
  return dispatch(widget, workflow, onWorkflowChange);
}

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

The ParallelCreateBranchView component:

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

import { useCallback, type ReactNode } from "react";
import {
  Formity,
  type Schema,
  type Form,
  type Return,
  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,
  SidebarTop,
  SidebarHeading,
  SidebarClose,
  SidebarContent,
  SidebarDivider,
} from "@/shared/ui/sidebar";

import {
  SidebarForm,
  Field,
  Label,
  Textarea,
} from "@/shared/kits/sidebar-form";

type Values = [Form<{ label: string }>, Return<{ label: string }>];

type Inputs = {
  label: string;
};

const schema: Schema<Values, Inputs> = [
  {
    form: {
      values: ({ label }) => ({
        label: [label, []],
      }),
      render: ({ values, onNext }) => (
        <SidebarForm
          defaultValues={values}
          resolver={zodResolver(
            z.object({
              label: z.string(),
            }),
          )}
          onSubmit={onNext}
        >
          <Field name="label">
            <Label>Label</Label>
            <Textarea maxLength={160} placeholder="Enter the label" />
          </Field>
          <Button type="submit">Save</Button>
        </SidebarForm>
      ),
    },
  },
  {
    return: ({ label }) => ({
      label: label,
    }),
  },
];

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

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

  return (
    <Sidebar className="absolute inset-y-0 right-0 overflow-auto border-l border-l-neutral-800">
      <SidebarTop>
        <SidebarHeading>Create branch</SidebarHeading>
        <SidebarClose
          onClick={() => {
            onWorkflowChange([{ type: "global", change: "removeWidget" }]);
          }}
        />
      </SidebarTop>
      <SidebarContent>
        <Formity<Values, Inputs>
          schema={schema}
          inputs={{
            label: "",
          }}
          onReturn={onSubmit}
        />
      </SidebarContent>
      <SidebarDivider />
    </Sidebar>
  );
}

The ParallelBranchInfoView component:

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

import { useCallback, type ReactNode } from "react";
import {
  Formity,
  type Schema,
  type Form,
  type Return,
  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,
  SidebarTop,
  SidebarHeading,
  SidebarClose,
  SidebarContent,
  SidebarDivider,
} from "@/shared/ui/sidebar";

import {
  SidebarForm,
  Field,
  Label,
  Textarea,
} from "@/shared/kits/sidebar-form";

type Values = [Form<{ label: string }>, Return<{ label: string }>];

type Inputs = {
  label: string;
};

const schema: Schema<Values, Inputs> = [
  {
    form: {
      values: ({ label }) => ({
        label: [label, []],
      }),
      render: ({ values, onNext }) => (
        <SidebarForm
          defaultValues={values}
          resolver={zodResolver(
            z.object({
              label: z.string(),
            }),
          )}
          onSubmit={onNext}
        >
          <Field name="label">
            <Label>Label</Label>
            <Textarea maxLength={160} placeholder="Enter the label" />
          </Field>
          <Button type="submit">Save</Button>
        </SidebarForm>
      ),
    },
  },
  {
    return: ({ label }) => ({
      label: label,
    }),
  },
];

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

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

  return (
    <Sidebar className="absolute inset-y-0 right-0 overflow-auto border-l border-l-neutral-800">
      <SidebarTop>
        <SidebarHeading>Branch</SidebarHeading>
        <SidebarClose
          onClick={() => {
            onWorkflowChange([{ type: "global", change: "removeWidget" }]);
          }}
        />
      </SidebarTop>
      <SidebarContent>
        <Formity<Values, Inputs>
          schema={schema}
          inputs={{
            label: widget.element.branches[widget.branch]!.label,
          }}
          onReturn={onSubmit}
        />
      </SidebarContent>
      <SidebarDivider />
    </Sidebar>
  );
}

The ParallelAddBranchView component:

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

import type { ReactNode } from "react";

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): 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:

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

import type { ReactNode } from "react";

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: 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,
): 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:

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

import type { ComponentPropsWithoutRef, ComponentType, ReactNode } from "react";

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

import {
  Sidebar,
  SidebarTop,
  SidebarHeading,
  SidebarClose,
  SidebarContent,
} from "@/shared/ui/sidebar";

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): ReactNode {
  return (
    <Sidebar className="absolute inset-y-0 right-0 overflow-auto border-l border-l-neutral-800">
      <SidebarTop>
        <SidebarHeading>Add element</SidebarHeading>
        <SidebarClose onClick={onClose} />
      </SidebarTop>
      <SidebarContent>
        <Option
          onClick={() =>
            onAdd({
              type: "action",
              id: crypto.randomUUID(),
              message: "",
              dragging: null,
              dropNext: new Set(),
            })
          }
        >
          <Icon icon={ActionIcon} />
          <Text>Action</Text>
        </Option>
        <Option
          onClick={() =>
            onAdd({
              type: "condition",
              id: crypto.randomUUID(),
              if: "",
              then: [],
              else: [],
              dragging: null,
              dropNext: new Set(),
              dropThen: new Set(),
              dropElse: new Set(),
            })
          }
        >
          <Icon icon={ConditionIcon} />
          <Text>Condition</Text>
        </Option>
        <Option
          onClick={() =>
            onAdd({
              type: "switch",
              id: crypto.randomUUID(),
              branches: [],
              default: [],
              dragging: null,
              dropNext: new Set(),
              dropBranches: [],
              dropDefault: new Set(),
            })
          }
        >
          <Icon icon={SwitchIcon} />
          <Text>Switch</Text>
        </Option>
        <Option
          onClick={() =>
            onAdd({
              type: "loop",
              id: crypto.randomUUID(),
              while: "",
              into: [],
              dragging: null,
              dropNext: new Set(),
              dropInto: new Set(),
            })
          }
        >
          <Icon icon={LoopIcon} />
          <Text>Loop</Text>
        </Option>
        <Option
          onClick={() =>
            onAdd({
              type: "parallel",
              id: crypto.randomUUID(),
              branches: [],
              dragging: null,
              dropNext: new Set(),
              dropBranches: [],
            })
          }
        >
          <Icon icon={ParallelIcon} />
          <Text>Parallel</Text>
        </Option>
      </SidebarContent>
    </Sidebar>
  );
}

function Option({
  className,
  ...props
}: ComponentPropsWithoutRef<"button">): ReactNode {
  return (
    <button
      className={cn(
        "flex h-10 w-full items-center gap-2 rounded-lg border border-neutral-800 bg-neutral-900 px-3 transition-colors hover:bg-neutral-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white",
        className,
      )}
      {...props}
    />
  );
}

interface IconProps extends ComponentPropsWithoutRef<"div"> {
  icon: ComponentType<ComponentPropsWithoutRef<"svg">>;
}

function Icon({ icon: Component, className, ...props }: IconProps): ReactNode {
  return (
    <div
      className={cn("flex size-4 items-center justify-center", className)}
      {...props}
    >
      <Component className="size-full stroke-white opacity-75" />
    </div>
  );
}

function Text({
  className,
  ...props
}: ComponentPropsWithoutRef<"p">): ReactNode {
  return (
    <p
      className={cn("font-sans text-sm font-normal text-white", className)}
      {...props}
    />
  );
}