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}
/>
);
}