feat(web): add run inputs options on dashboard
This commit is contained in:
		
							parent
							
								
									cdbf4f3dd5
								
							
						
					
					
						commit
						592cc2abcd
					
				
							
								
								
									
										
											BIN
										
									
								
								web/bun.lockb
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/bun.lockb
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							@ -25,15 +25,21 @@
 | 
			
		||||
    "@radix-ui/react-dialog": "^1.0.5",
 | 
			
		||||
    "@radix-ui/react-dropdown-menu": "^2.0.6",
 | 
			
		||||
    "@radix-ui/react-label": "^2.0.2",
 | 
			
		||||
    "@radix-ui/react-popover": "^1.0.7",
 | 
			
		||||
    "@radix-ui/react-radio-group": "^1.1.3",
 | 
			
		||||
    "@radix-ui/react-select": "^2.0.0",
 | 
			
		||||
    "@radix-ui/react-separator": "^1.0.3",
 | 
			
		||||
    "@radix-ui/react-slot": "^1.0.2",
 | 
			
		||||
    "@radix-ui/react-switch": "^1.0.3",
 | 
			
		||||
    "@radix-ui/react-tabs": "^1.0.4",
 | 
			
		||||
    "@radix-ui/react-toggle": "^1.0.3",
 | 
			
		||||
    "@radix-ui/react-tooltip": "^1.0.7",
 | 
			
		||||
    "@tanstack/react-table": "^8.10.7",
 | 
			
		||||
    "@types/jsonwebtoken": "^9.0.5",
 | 
			
		||||
    "@types/uuid": "^9.0.7",
 | 
			
		||||
    "class-variance-authority": "^0.7.0",
 | 
			
		||||
    "clsx": "^2.0.0",
 | 
			
		||||
    "date-fns": "^3.0.5",
 | 
			
		||||
    "dayjs": "^1.11.10",
 | 
			
		||||
    "drizzle-orm": "^0.29.1",
 | 
			
		||||
    "jsonwebtoken": "^9.0.2",
 | 
			
		||||
@ -43,6 +49,7 @@
 | 
			
		||||
    "next-plausible": "^3.12.0",
 | 
			
		||||
    "next-usequerystate": "^1.13.2",
 | 
			
		||||
    "react": "^18",
 | 
			
		||||
    "react-day-picker": "^8.9.1",
 | 
			
		||||
    "react-dom": "^18",
 | 
			
		||||
    "react-hook-form": "^7.48.2",
 | 
			
		||||
    "react-use-websocket": "^4.5.0",
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,3 @@
 | 
			
		||||
import { OutputRender } from "./OutputRender";
 | 
			
		||||
import { CodeBlock } from "@/components/CodeBlock";
 | 
			
		||||
import {
 | 
			
		||||
  Table,
 | 
			
		||||
  TableBody,
 | 
			
		||||
@ -8,8 +6,7 @@ import {
 | 
			
		||||
  TableHeader,
 | 
			
		||||
  TableRow,
 | 
			
		||||
} from "@/components/ui/table";
 | 
			
		||||
import { findAllRuns } from "@/server/findAllRuns";
 | 
			
		||||
import { getRunsOutput } from "@/server/getRunsOutput";
 | 
			
		||||
import type { findAllRuns } from "@/server/findAllRuns";
 | 
			
		||||
 | 
			
		||||
export async function RunInputs({
 | 
			
		||||
  run,
 | 
			
		||||
@ -30,16 +27,28 @@ export async function RunInputs({
 | 
			
		||||
            {Object.entries(run.workflow_inputs).map(([key, data]) => {
 | 
			
		||||
              let imageUrl;
 | 
			
		||||
              try {
 | 
			
		||||
                const url = new URL(data);
 | 
			
		||||
                if (url.pathname.endsWith('.png')) {
 | 
			
		||||
                if (data.startsWith("data:image/")) {
 | 
			
		||||
                  imageUrl = data;
 | 
			
		||||
                } else {
 | 
			
		||||
                  const url = new URL(data);
 | 
			
		||||
                  if (url.pathname.endsWith(".png")) {
 | 
			
		||||
                    imageUrl = data;
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              } catch (_) {
 | 
			
		||||
              }
 | 
			
		||||
              } catch (_) {}
 | 
			
		||||
              return (
 | 
			
		||||
                <TableRow key={key}>
 | 
			
		||||
                  <TableCell>{key}</TableCell>
 | 
			
		||||
                  {imageUrl ? <TableCell><img className="w-[200px] aspect-square object-contain" src={imageUrl}></img></TableCell> : <TableCell>{data}</TableCell>}
 | 
			
		||||
                  {imageUrl ? (
 | 
			
		||||
                    <TableCell>
 | 
			
		||||
                      <img
 | 
			
		||||
                        className="w-[200px] aspect-square object-contain"
 | 
			
		||||
                        src={imageUrl}
 | 
			
		||||
                      />
 | 
			
		||||
                    </TableCell>
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <TableCell>{data}</TableCell>
 | 
			
		||||
                  )}
 | 
			
		||||
                </TableRow>
 | 
			
		||||
              );
 | 
			
		||||
            })}
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,16 @@
 | 
			
		||||
 | 
			
		||||
import { callServerPromise } from "./callServerPromise";
 | 
			
		||||
import { LoadingIcon } from "@/components/LoadingIcon";
 | 
			
		||||
import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import {
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogDescription,
 | 
			
		||||
  DialogHeader,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  DialogTrigger,
 | 
			
		||||
} from "@/components/ui/dialog";
 | 
			
		||||
import {
 | 
			
		||||
  DropdownMenu,
 | 
			
		||||
  DropdownMenuContent,
 | 
			
		||||
@ -18,14 +27,16 @@ import {
 | 
			
		||||
  SelectTrigger,
 | 
			
		||||
  SelectValue,
 | 
			
		||||
} from "@/components/ui/select";
 | 
			
		||||
import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
 | 
			
		||||
import { createRun } from "@/server/createRun";
 | 
			
		||||
import { createDeployments } from "@/server/curdDeploments";
 | 
			
		||||
import type { getMachines } from "@/server/curdMachine";
 | 
			
		||||
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
 | 
			
		||||
import { Copy, MoreVertical, Play } from "lucide-react";
 | 
			
		||||
import { parseAsInteger, useQueryState } from "next-usequerystate";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import { useMemo, useState } from "react";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
import { z } from "zod";
 | 
			
		||||
 | 
			
		||||
export function VersionSelect({
 | 
			
		||||
  workflow,
 | 
			
		||||
@ -106,31 +117,95 @@ export function RunWorkflowButton({
 | 
			
		||||
    defaultValue: machines[0].id ?? "",
 | 
			
		||||
  });
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
  return (
 | 
			
		||||
    <Button
 | 
			
		||||
      className="gap-2"
 | 
			
		||||
      disabled={isLoading}
 | 
			
		||||
      onClick={async () => {
 | 
			
		||||
        const workflow_version_id = workflow?.versions.find(
 | 
			
		||||
          (x) => x.version === version
 | 
			
		||||
        )?.id;
 | 
			
		||||
        if (!workflow_version_id) return;
 | 
			
		||||
 | 
			
		||||
        setIsLoading(true);
 | 
			
		||||
        try {
 | 
			
		||||
          const origin = window.location.origin;
 | 
			
		||||
          await callServerPromise(
 | 
			
		||||
            createRun(origin, workflow_version_id, machine, undefined, true)
 | 
			
		||||
          );
 | 
			
		||||
          // console.log(res.json());
 | 
			
		||||
          setIsLoading(false);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          setIsLoading(false);
 | 
			
		||||
        }
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      Run {isLoading ? <LoadingIcon /> : <Play size={14} />}
 | 
			
		||||
    </Button>
 | 
			
		||||
  const [values, setValues] = useState<Record<string, string>>({});
 | 
			
		||||
  const [open, setOpen] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const schema = useMemo(() => {
 | 
			
		||||
    const workflow_version = getWorkflowVersionFromVersionIndex(
 | 
			
		||||
      workflow,
 | 
			
		||||
      version
 | 
			
		||||
    );
 | 
			
		||||
    const inputs = getInputsFromWorkflow(workflow_version);
 | 
			
		||||
 | 
			
		||||
    if (!inputs) return null;
 | 
			
		||||
 | 
			
		||||
    return z.object({
 | 
			
		||||
      ...Object.fromEntries(
 | 
			
		||||
        inputs?.map((x) => {
 | 
			
		||||
          return [x?.input_id, z.string().optional()];
 | 
			
		||||
        })
 | 
			
		||||
      ),
 | 
			
		||||
    });
 | 
			
		||||
  }, [version]);
 | 
			
		||||
 | 
			
		||||
  const runWorkflow = async () => {
 | 
			
		||||
    console.log(values);
 | 
			
		||||
 | 
			
		||||
    const val = Object.keys(values).length > 0 ? values : undefined;
 | 
			
		||||
 | 
			
		||||
    const workflow_version_id = workflow?.versions.find(
 | 
			
		||||
      (x) => x.version === version
 | 
			
		||||
    )?.id;
 | 
			
		||||
    console.log(workflow_version_id);
 | 
			
		||||
    if (!workflow_version_id) return;
 | 
			
		||||
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
    try {
 | 
			
		||||
      const origin = window.location.origin;
 | 
			
		||||
      await callServerPromise(
 | 
			
		||||
        createRun(origin, workflow_version_id, machine, val, true)
 | 
			
		||||
      );
 | 
			
		||||
      // console.log(res.json());
 | 
			
		||||
      setIsLoading(false);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      setIsLoading(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setOpen(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog open={open} onOpenChange={setOpen}>
 | 
			
		||||
      <DialogTrigger asChild className="appearance-none hover:cursor-pointer">
 | 
			
		||||
        <Button className="gap-2" disabled={isLoading}>
 | 
			
		||||
          Run {isLoading ? <LoadingIcon /> : <Play size={14} />}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </DialogTrigger>
 | 
			
		||||
      <DialogContent className="max-w-xl">
 | 
			
		||||
        <DialogHeader>
 | 
			
		||||
          <DialogTitle>Run outputs</DialogTitle>
 | 
			
		||||
          <DialogDescription>
 | 
			
		||||
            You can view your run's outputs here
 | 
			
		||||
          </DialogDescription>
 | 
			
		||||
        </DialogHeader>
 | 
			
		||||
        {/* <div className="max-h-96 overflow-y-scroll"> */}
 | 
			
		||||
        {schema && (
 | 
			
		||||
          <AutoForm
 | 
			
		||||
            formSchema={schema}
 | 
			
		||||
            values={values}
 | 
			
		||||
            onValuesChange={setValues}
 | 
			
		||||
            onSubmit={runWorkflow}
 | 
			
		||||
          >
 | 
			
		||||
            <div className="flex justify-end">
 | 
			
		||||
              <AutoFormSubmit>
 | 
			
		||||
                Run
 | 
			
		||||
                <span className="ml-2">
 | 
			
		||||
                  {isLoading ? <LoadingIcon /> : <Play size={14} />}
 | 
			
		||||
                </span>
 | 
			
		||||
              </AutoFormSubmit>
 | 
			
		||||
            </div>
 | 
			
		||||
          </AutoForm>
 | 
			
		||||
        )}
 | 
			
		||||
        {!schema && (
 | 
			
		||||
          <Button className="gap-2" disabled={isLoading} onClick={runWorkflow}>
 | 
			
		||||
            Confirm {isLoading ? <LoadingIcon /> : <Play size={14} />}
 | 
			
		||||
          </Button>
 | 
			
		||||
        )}
 | 
			
		||||
        {/* </div> */}
 | 
			
		||||
        {/* <div className="max-h-96 overflow-y-scroll">{view}</div> */}
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
export const customInputNodes: Record<string, string> = {
 | 
			
		||||
  ComfyUIDeployExternalText: "string",
 | 
			
		||||
  ComfyUIDeployExternalImage: "string - (public image url)",
 | 
			
		||||
  ComfyUIDeployExternalImageAlpha: "string - (public image url)",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,11 @@
 | 
			
		||||
"use client"
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
 | 
			
		||||
import { ChevronDown } from "lucide-react"
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
 | 
			
		||||
import { ChevronDown } from "lucide-react";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const Accordion = AccordionPrimitive.Root
 | 
			
		||||
const Accordion = AccordionPrimitive.Root;
 | 
			
		||||
 | 
			
		||||
const AccordionItem = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AccordionPrimitive.Item>,
 | 
			
		||||
@ -17,8 +16,8 @@ const AccordionItem = React.forwardRef<
 | 
			
		||||
    className={cn("border-b", className)}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
AccordionItem.displayName = "AccordionItem"
 | 
			
		||||
));
 | 
			
		||||
AccordionItem.displayName = "AccordionItem";
 | 
			
		||||
 | 
			
		||||
const AccordionTrigger = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AccordionPrimitive.Trigger>,
 | 
			
		||||
@ -37,8 +36,8 @@ const AccordionTrigger = React.forwardRef<
 | 
			
		||||
      <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
 | 
			
		||||
    </AccordionPrimitive.Trigger>
 | 
			
		||||
  </AccordionPrimitive.Header>
 | 
			
		||||
))
 | 
			
		||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
 | 
			
		||||
));
 | 
			
		||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
 | 
			
		||||
 | 
			
		||||
const AccordionContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof AccordionPrimitive.Content>,
 | 
			
		||||
@ -51,8 +50,8 @@ const AccordionContent = React.forwardRef<
 | 
			
		||||
  >
 | 
			
		||||
    <div className={cn("pb-4 pt-0", className)}>{children}</div>
 | 
			
		||||
  </AccordionPrimitive.Content>
 | 
			
		||||
))
 | 
			
		||||
));
 | 
			
		||||
 | 
			
		||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
 | 
			
		||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
 | 
			
		||||
 | 
			
		||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
 | 
			
		||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										33
									
								
								web/src/components/ui/auto-form/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								web/src/components/ui/auto-form/config.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
import AutoFormCheckbox from "./fields/checkbox";
 | 
			
		||||
import AutoFormDate from "./fields/date";
 | 
			
		||||
import AutoFormEnum from "./fields/enum";
 | 
			
		||||
import AutoFormInput from "./fields/input";
 | 
			
		||||
import AutoFormNumber from "./fields/number";
 | 
			
		||||
import AutoFormRadioGroup from "./fields/radio-group";
 | 
			
		||||
import AutoFormSwitch from "./fields/switch";
 | 
			
		||||
import AutoFormTextarea from "./fields/textarea";
 | 
			
		||||
 | 
			
		||||
export const INPUT_COMPONENTS = {
 | 
			
		||||
  checkbox: AutoFormCheckbox,
 | 
			
		||||
  date: AutoFormDate,
 | 
			
		||||
  select: AutoFormEnum,
 | 
			
		||||
  radio: AutoFormRadioGroup,
 | 
			
		||||
  switch: AutoFormSwitch,
 | 
			
		||||
  textarea: AutoFormTextarea,
 | 
			
		||||
  number: AutoFormNumber,
 | 
			
		||||
  fallback: AutoFormInput,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Define handlers for specific Zod types.
 | 
			
		||||
 * You can expand this object to support more types.
 | 
			
		||||
 */
 | 
			
		||||
export const DEFAULT_ZOD_HANDLERS: {
 | 
			
		||||
  [key: string]: keyof typeof INPUT_COMPONENTS;
 | 
			
		||||
} = {
 | 
			
		||||
  ZodBoolean: "checkbox",
 | 
			
		||||
  ZodDate: "date",
 | 
			
		||||
  ZodEnum: "select",
 | 
			
		||||
  ZodNativeEnum: "select",
 | 
			
		||||
  ZodNumber: "number",
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										67
									
								
								web/src/components/ui/auto-form/fields/array.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								web/src/components/ui/auto-form/fields/array.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,67 @@
 | 
			
		||||
import type { useForm , useForm } from "react-hook-form";
 | 
			
		||||
import { useFieldArray } from "react-hook-form";
 | 
			
		||||
import { Button } from "../../button";
 | 
			
		||||
import { Separator } from "../../separator";
 | 
			
		||||
import { beautifyObjectName } from "../utils";
 | 
			
		||||
import AutoFormObject from "./object";
 | 
			
		||||
import { Plus, Trash } from "lucide-react";
 | 
			
		||||
import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
 | 
			
		||||
import type { z } from "zod";
 | 
			
		||||
 | 
			
		||||
export default function AutoFormArray({
 | 
			
		||||
  name,
 | 
			
		||||
  item,
 | 
			
		||||
  form,
 | 
			
		||||
  path = [],
 | 
			
		||||
  fieldConfig,
 | 
			
		||||
}: {
 | 
			
		||||
  name: string;
 | 
			
		||||
  item: z.ZodArray<any>;
 | 
			
		||||
  form: ReturnType<typeof useForm>;
 | 
			
		||||
  path?: string[];
 | 
			
		||||
  fieldConfig?: any;
 | 
			
		||||
}) {
 | 
			
		||||
  const { fields, append, remove } = useFieldArray({
 | 
			
		||||
    control: form.control,
 | 
			
		||||
    name,
 | 
			
		||||
  });
 | 
			
		||||
  const title = item._def.description ?? beautifyObjectName(name);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AccordionItem value={name}>
 | 
			
		||||
      <AccordionTrigger>{title}</AccordionTrigger>
 | 
			
		||||
      <AccordionContent className="border-l p-3 pl-6">
 | 
			
		||||
        {fields.map((_field, index) => {
 | 
			
		||||
          const key = [...path, index.toString()].join(".");
 | 
			
		||||
          return (
 | 
			
		||||
            <div className="mb-4 grid gap-6" key={`${key}`}>
 | 
			
		||||
              <AutoFormObject
 | 
			
		||||
                schema={item._def.type as z.ZodObject<any, any>}
 | 
			
		||||
                form={form}
 | 
			
		||||
                fieldConfig={fieldConfig}
 | 
			
		||||
                path={[...path, index.toString()]}
 | 
			
		||||
              />
 | 
			
		||||
              <Button
 | 
			
		||||
                variant="secondary"
 | 
			
		||||
                size="icon"
 | 
			
		||||
                type="button"
 | 
			
		||||
                onClick={() => remove(index)}
 | 
			
		||||
              >
 | 
			
		||||
                <Trash className="h-4 w-4" />
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Separator />
 | 
			
		||||
            </div>
 | 
			
		||||
          );
 | 
			
		||||
        })}
 | 
			
		||||
        <Button
 | 
			
		||||
          type="button"
 | 
			
		||||
          onClick={() => append({})}
 | 
			
		||||
          className="flex items-center"
 | 
			
		||||
        >
 | 
			
		||||
          <Plus className="mr-2" size={16} />
 | 
			
		||||
          Add
 | 
			
		||||
        </Button>
 | 
			
		||||
      </AccordionContent>
 | 
			
		||||
    </AccordionItem>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								web/src/components/ui/auto-form/fields/checkbox.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								web/src/components/ui/auto-form/fields/checkbox.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
import { Checkbox } from "../../checkbox";
 | 
			
		||||
import { FormControl, FormDescription, FormItem, FormLabel } from "../../form";
 | 
			
		||||
import { AutoFormInputComponentProps } from "../types";
 | 
			
		||||
 | 
			
		||||
export default function AutoFormCheckbox({
 | 
			
		||||
  label,
 | 
			
		||||
  isRequired,
 | 
			
		||||
  field,
 | 
			
		||||
  fieldConfigItem,
 | 
			
		||||
  fieldProps,
 | 
			
		||||
}: AutoFormInputComponentProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
 | 
			
		||||
      <FormControl>
 | 
			
		||||
        <Checkbox
 | 
			
		||||
          checked={field.value}
 | 
			
		||||
          onCheckedChange={field.onChange}
 | 
			
		||||
          {...fieldProps}
 | 
			
		||||
        />
 | 
			
		||||
      </FormControl>
 | 
			
		||||
      <div className="space-y-1 leading-none">
 | 
			
		||||
        <FormLabel>
 | 
			
		||||
          {label}
 | 
			
		||||
          {isRequired && <span className="text-destructive"> *</span>}
 | 
			
		||||
        </FormLabel>
 | 
			
		||||
        {fieldConfigItem.description && (
 | 
			
		||||
          <FormDescription>{fieldConfigItem.description}</FormDescription>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </FormItem>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								web/src/components/ui/auto-form/fields/date.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/src/components/ui/auto-form/fields/date.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
import { DatePicker } from "../../date-picker";
 | 
			
		||||
import {
 | 
			
		||||
  FormControl,
 | 
			
		||||
  FormDescription,
 | 
			
		||||
  FormItem,
 | 
			
		||||
  FormLabel,
 | 
			
		||||
  FormMessage,
 | 
			
		||||
} from "../../form";
 | 
			
		||||
import type { AutoFormInputComponentProps } from "../types";
 | 
			
		||||
 | 
			
		||||
export default function AutoFormDate({
 | 
			
		||||
  label,
 | 
			
		||||
  isRequired,
 | 
			
		||||
  field,
 | 
			
		||||
  fieldConfigItem,
 | 
			
		||||
  fieldProps,
 | 
			
		||||
}: AutoFormInputComponentProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <FormItem>
 | 
			
		||||
      <FormLabel>
 | 
			
		||||
        {label}
 | 
			
		||||
        {isRequired && <span className="text-destructive"> *</span>}
 | 
			
		||||
      </FormLabel>
 | 
			
		||||
      <FormControl>
 | 
			
		||||
        <DatePicker
 | 
			
		||||
          date={field.value}
 | 
			
		||||
          setDate={field.onChange}
 | 
			
		||||
          {...fieldProps}
 | 
			
		||||
        />
 | 
			
		||||
      </FormControl>
 | 
			
		||||
      {fieldConfigItem.description && (
 | 
			
		||||
        <FormDescription>{fieldConfigItem.description}</FormDescription>
 | 
			
		||||
      )}
 | 
			
		||||
      <FormMessage />
 | 
			
		||||
    </FormItem>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										71
									
								
								web/src/components/ui/auto-form/fields/enum.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								web/src/components/ui/auto-form/fields/enum.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
			
		||||
import {
 | 
			
		||||
  FormControl,
 | 
			
		||||
  FormDescription,
 | 
			
		||||
  FormItem,
 | 
			
		||||
  FormLabel,
 | 
			
		||||
  FormMessage,
 | 
			
		||||
} from "../../form";
 | 
			
		||||
import {
 | 
			
		||||
  Select,
 | 
			
		||||
  SelectContent,
 | 
			
		||||
  SelectItem,
 | 
			
		||||
  SelectTrigger,
 | 
			
		||||
  SelectValue,
 | 
			
		||||
} from "../../select";
 | 
			
		||||
import { AutoFormInputComponentProps } from "../types";
 | 
			
		||||
import { getBaseSchema } from "../utils";
 | 
			
		||||
import * as z from "zod";
 | 
			
		||||
 | 
			
		||||
export default function AutoFormEnum({
 | 
			
		||||
  label,
 | 
			
		||||
  isRequired,
 | 
			
		||||
  field,
 | 
			
		||||
  fieldConfigItem,
 | 
			
		||||
  zodItem,
 | 
			
		||||
}: AutoFormInputComponentProps) {
 | 
			
		||||
  const baseValues = (getBaseSchema(zodItem) as unknown as z.ZodEnum<any>)._def
 | 
			
		||||
    .values;
 | 
			
		||||
 | 
			
		||||
  let values: [string, string][] = [];
 | 
			
		||||
  if (!Array.isArray(baseValues)) {
 | 
			
		||||
    values = Object.entries(baseValues);
 | 
			
		||||
  } else {
 | 
			
		||||
    values = baseValues.map((value) => [value, value]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function findItem(value: any) {
 | 
			
		||||
    return values.find((item) => item[0] === value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <FormItem>
 | 
			
		||||
      <FormLabel>
 | 
			
		||||
        {label}
 | 
			
		||||
        {isRequired && <span className="text-destructive"> *</span>}
 | 
			
		||||
      </FormLabel>
 | 
			
		||||
      <FormControl>
 | 
			
		||||
        <Select onValueChange={field.onChange} defaultValue={field.value}>
 | 
			
		||||
          <SelectTrigger>
 | 
			
		||||
            <SelectValue
 | 
			
		||||
              className="w-full"
 | 
			
		||||
              placeholder={fieldConfigItem.inputProps?.placeholder}
 | 
			
		||||
            >
 | 
			
		||||
              {field.value ? findItem(field.value)?.[1] : "Select an option"}
 | 
			
		||||
            </SelectValue>
 | 
			
		||||
          </SelectTrigger>
 | 
			
		||||
          <SelectContent>
 | 
			
		||||
            {values.map(([value, label]) => (
 | 
			
		||||
              <SelectItem value={label} key={value}>
 | 
			
		||||
                {label}
 | 
			
		||||
              </SelectItem>
 | 
			
		||||
            ))}
 | 
			
		||||
          </SelectContent>
 | 
			
		||||
        </Select>
 | 
			
		||||
      </FormControl>
 | 
			
		||||
      {fieldConfigItem.description && (
 | 
			
		||||
        <FormDescription>{fieldConfigItem.description}</FormDescription>
 | 
			
		||||
      )}
 | 
			
		||||
      <FormMessage />
 | 
			
		||||
    </FormItem>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								web/src/components/ui/auto-form/fields/input.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								web/src/components/ui/auto-form/fields/input.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
			
		||||
import {
 | 
			
		||||
  FormControl,
 | 
			
		||||
  FormDescription,
 | 
			
		||||
  FormItem,
 | 
			
		||||
  FormLabel,
 | 
			
		||||
  FormMessage,
 | 
			
		||||
} from "../../form";
 | 
			
		||||
import { Input } from "../../input";
 | 
			
		||||
import { AutoFormInputComponentProps } from "../types";
 | 
			
		||||
 | 
			
		||||
export default function AutoFormInput({
 | 
			
		||||
  label,
 | 
			
		||||
  isRequired,
 | 
			
		||||
  fieldConfigItem,
 | 
			
		||||
  fieldProps,
 | 
			
		||||
}: AutoFormInputComponentProps) {
 | 
			
		||||
  const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
 | 
			
		||||
  const showLabel = _showLabel === undefined ? true : _showLabel;
 | 
			
		||||
  return (
 | 
			
		||||
    <FormItem>
 | 
			
		||||
      {showLabel && (
 | 
			
		||||
        <FormLabel>
 | 
			
		||||
          {label}
 | 
			
		||||
          {isRequired && <span className="text-destructive"> *</span>}
 | 
			
		||||
        </FormLabel>
 | 
			
		||||
      )}
 | 
			
		||||
      <FormControl>
 | 
			
		||||
        <Input type="text" {...fieldPropsWithoutShowLabel} />
 | 
			
		||||
      </FormControl>
 | 
			
		||||
      {fieldConfigItem.description && (
 | 
			
		||||
        <FormDescription>{fieldConfigItem.description}</FormDescription>
 | 
			
		||||
      )}
 | 
			
		||||
      <FormMessage />
 | 
			
		||||
    </FormItem>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								web/src/components/ui/auto-form/fields/number.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								web/src/components/ui/auto-form/fields/number.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
import { AutoFormInputComponentProps } from "../types";
 | 
			
		||||
import AutoFormInput from "./input";
 | 
			
		||||
 | 
			
		||||
export default function AutoFormNumber({
 | 
			
		||||
  fieldProps,
 | 
			
		||||
  ...props
 | 
			
		||||
}: AutoFormInputComponentProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <AutoFormInput
 | 
			
		||||
      fieldProps={{
 | 
			
		||||
        type: "number",
 | 
			
		||||
        ...fieldProps,
 | 
			
		||||
      }}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										130
									
								
								web/src/components/ui/auto-form/fields/object.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								web/src/components/ui/auto-form/fields/object.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,130 @@
 | 
			
		||||
import * as z from "zod";
 | 
			
		||||
import { useForm } from "react-hook-form";
 | 
			
		||||
import { FieldConfig, FieldConfigItem } from "../types";
 | 
			
		||||
import {
 | 
			
		||||
  Accordion,
 | 
			
		||||
  AccordionContent,
 | 
			
		||||
  AccordionItem,
 | 
			
		||||
  AccordionTrigger,
 | 
			
		||||
} from "../../accordion";
 | 
			
		||||
import {
 | 
			
		||||
  beautifyObjectName,
 | 
			
		||||
  getBaseSchema,
 | 
			
		||||
  getBaseType,
 | 
			
		||||
  zodToHtmlInputProps,
 | 
			
		||||
} from "../utils";
 | 
			
		||||
import { FormField } from "../../form";
 | 
			
		||||
import { DEFAULT_ZOD_HANDLERS, INPUT_COMPONENTS } from "../config";
 | 
			
		||||
import AutoFormArray from "./array";
 | 
			
		||||
 | 
			
		||||
function DefaultParent({ children }: { children: React.ReactNode }) {
 | 
			
		||||
  return <>{children}</>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function AutoFormObject<
 | 
			
		||||
  SchemaType extends z.ZodObject<any, any>,
 | 
			
		||||
>({
 | 
			
		||||
  schema,
 | 
			
		||||
  form,
 | 
			
		||||
  fieldConfig,
 | 
			
		||||
  path = [],
 | 
			
		||||
}: {
 | 
			
		||||
  schema: SchemaType | z.ZodEffects<SchemaType>;
 | 
			
		||||
  form: ReturnType<typeof useForm>;
 | 
			
		||||
  fieldConfig?: FieldConfig<z.infer<SchemaType>>;
 | 
			
		||||
  path?: string[];
 | 
			
		||||
}) {
 | 
			
		||||
  const { shape } = getBaseSchema<SchemaType>(schema);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Accordion type="multiple" className="space-y-5">
 | 
			
		||||
      {Object.keys(shape).map((name) => {
 | 
			
		||||
        const item = shape[name] as z.ZodAny;
 | 
			
		||||
        const zodBaseType = getBaseType(item);
 | 
			
		||||
        const itemName = item._def.description ?? beautifyObjectName(name);
 | 
			
		||||
        const key = [...path, name].join(".");
 | 
			
		||||
 | 
			
		||||
        if (zodBaseType === "ZodObject") {
 | 
			
		||||
          return (
 | 
			
		||||
            <AccordionItem value={name} key={key}>
 | 
			
		||||
              <AccordionTrigger>{itemName}</AccordionTrigger>
 | 
			
		||||
              <AccordionContent className="p-2">
 | 
			
		||||
                <AutoFormObject
 | 
			
		||||
                  schema={item as unknown as z.ZodObject<any, any>}
 | 
			
		||||
                  form={form}
 | 
			
		||||
                  fieldConfig={
 | 
			
		||||
                    (fieldConfig?.[name] ?? {}) as FieldConfig<
 | 
			
		||||
                      z.infer<typeof item>
 | 
			
		||||
                    >
 | 
			
		||||
                  }
 | 
			
		||||
                  path={[...path, name]}
 | 
			
		||||
                />
 | 
			
		||||
              </AccordionContent>
 | 
			
		||||
            </AccordionItem>
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        if (zodBaseType === "ZodArray") {
 | 
			
		||||
          return (
 | 
			
		||||
            <AutoFormArray
 | 
			
		||||
              key={key}
 | 
			
		||||
              name={name}
 | 
			
		||||
              item={item as unknown as z.ZodArray<any>}
 | 
			
		||||
              form={form}
 | 
			
		||||
              fieldConfig={fieldConfig?.[name] ?? {}}
 | 
			
		||||
              path={[...path, name]}
 | 
			
		||||
            />
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const fieldConfigItem: FieldConfigItem = fieldConfig?.[name] ?? {};
 | 
			
		||||
        const zodInputProps = zodToHtmlInputProps(item);
 | 
			
		||||
        const isRequired =
 | 
			
		||||
          zodInputProps.required ||
 | 
			
		||||
          fieldConfigItem.inputProps?.required ||
 | 
			
		||||
          false;
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
          <FormField
 | 
			
		||||
            control={form.control}
 | 
			
		||||
            name={key}
 | 
			
		||||
            key={key}
 | 
			
		||||
            render={({ field }) => {
 | 
			
		||||
              const inputType =
 | 
			
		||||
                fieldConfigItem.fieldType ??
 | 
			
		||||
                DEFAULT_ZOD_HANDLERS[zodBaseType] ??
 | 
			
		||||
                "fallback";
 | 
			
		||||
 | 
			
		||||
              const InputComponent =
 | 
			
		||||
                typeof inputType === "function"
 | 
			
		||||
                  ? inputType
 | 
			
		||||
                  : INPUT_COMPONENTS[inputType];
 | 
			
		||||
              const ParentElement =
 | 
			
		||||
                fieldConfigItem.renderParent ?? DefaultParent;
 | 
			
		||||
 | 
			
		||||
              return (
 | 
			
		||||
                <ParentElement key={`${key}.parent`}>
 | 
			
		||||
                  <InputComponent
 | 
			
		||||
                    zodInputProps={zodInputProps}
 | 
			
		||||
                    field={field}
 | 
			
		||||
                    fieldConfigItem={fieldConfigItem}
 | 
			
		||||
                    label={itemName}
 | 
			
		||||
                    isRequired={isRequired}
 | 
			
		||||
                    zodItem={item}
 | 
			
		||||
                    fieldProps={{
 | 
			
		||||
                      ...zodInputProps,
 | 
			
		||||
                      ...field,
 | 
			
		||||
                      ...fieldConfigItem.inputProps,
 | 
			
		||||
                      value: !fieldConfigItem.inputProps?.defaultValue
 | 
			
		||||
                        ? field.value ?? ""
 | 
			
		||||
                        : undefined,
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                </ParentElement>
 | 
			
		||||
              );
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
      })}
 | 
			
		||||
    </Accordion>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								web/src/components/ui/auto-form/fields/radio-group.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								web/src/components/ui/auto-form/fields/radio-group.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
			
		||||
import * as z from "zod";
 | 
			
		||||
import { AutoFormInputComponentProps } from "../types";
 | 
			
		||||
import { FormControl, FormItem, FormLabel, FormMessage } from "../../form";
 | 
			
		||||
import { RadioGroup, RadioGroupItem } from "../../radio-group";
 | 
			
		||||
 | 
			
		||||
export default function AutoFormRadioGroup({
 | 
			
		||||
  label,
 | 
			
		||||
  isRequired,
 | 
			
		||||
  field,
 | 
			
		||||
  zodItem,
 | 
			
		||||
  fieldProps,
 | 
			
		||||
}: AutoFormInputComponentProps) {
 | 
			
		||||
  const values = (zodItem as unknown as z.ZodEnum<any>)._def.values;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <FormItem className="space-y-3">
 | 
			
		||||
      <FormLabel>
 | 
			
		||||
        {label}
 | 
			
		||||
        {isRequired && <span className="text-destructive"> *</span>}
 | 
			
		||||
      </FormLabel>
 | 
			
		||||
      <FormControl>
 | 
			
		||||
        <RadioGroup
 | 
			
		||||
          onValueChange={field.onChange}
 | 
			
		||||
          defaultValue={field.value}
 | 
			
		||||
          className="flex flex-col space-y-1"
 | 
			
		||||
          {...fieldProps}
 | 
			
		||||
        >
 | 
			
		||||
          {values.map((value: any) => (
 | 
			
		||||
            <FormItem
 | 
			
		||||
              className="flex items-center space-x-3 space-y-0"
 | 
			
		||||
              key={value}
 | 
			
		||||
            >
 | 
			
		||||
              <FormControl>
 | 
			
		||||
                <RadioGroupItem value={value} />
 | 
			
		||||
              </FormControl>
 | 
			
		||||
              <FormLabel className="font-normal">{value}</FormLabel>
 | 
			
		||||
            </FormItem>
 | 
			
		||||
          ))}
 | 
			
		||||
        </RadioGroup>
 | 
			
		||||
      </FormControl>
 | 
			
		||||
      <FormMessage />
 | 
			
		||||
    </FormItem>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								web/src/components/ui/auto-form/fields/switch.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								web/src/components/ui/auto-form/fields/switch.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
import { FormControl, FormDescription, FormItem, FormLabel } from "../../form";
 | 
			
		||||
import { Switch } from "../../switch";
 | 
			
		||||
import { AutoFormInputComponentProps } from "../types";
 | 
			
		||||
 | 
			
		||||
export default function AutoFormSwitch({
 | 
			
		||||
  label,
 | 
			
		||||
  isRequired,
 | 
			
		||||
  field,
 | 
			
		||||
  fieldConfigItem,
 | 
			
		||||
  fieldProps,
 | 
			
		||||
}: AutoFormInputComponentProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
 | 
			
		||||
      <FormControl>
 | 
			
		||||
        <Switch
 | 
			
		||||
          checked={field.value}
 | 
			
		||||
          onCheckedChange={field.onChange}
 | 
			
		||||
          {...fieldProps}
 | 
			
		||||
        />
 | 
			
		||||
      </FormControl>
 | 
			
		||||
      <div className="space-y-1 leading-none">
 | 
			
		||||
        <FormLabel>
 | 
			
		||||
          {label}
 | 
			
		||||
          {isRequired && <span className="text-destructive"> *</span>}
 | 
			
		||||
        </FormLabel>
 | 
			
		||||
        {fieldConfigItem.description && (
 | 
			
		||||
          <FormDescription>{fieldConfigItem.description}</FormDescription>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </FormItem>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								web/src/components/ui/auto-form/fields/textarea.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								web/src/components/ui/auto-form/fields/textarea.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
			
		||||
import {
 | 
			
		||||
  FormControl,
 | 
			
		||||
  FormDescription,
 | 
			
		||||
  FormItem,
 | 
			
		||||
  FormLabel,
 | 
			
		||||
  FormMessage,
 | 
			
		||||
} from "../../form";
 | 
			
		||||
import { Textarea } from "../../textarea";
 | 
			
		||||
import { AutoFormInputComponentProps } from "../types";
 | 
			
		||||
 | 
			
		||||
export default function AutoFormTextarea({
 | 
			
		||||
  label,
 | 
			
		||||
  isRequired,
 | 
			
		||||
  fieldConfigItem,
 | 
			
		||||
  fieldProps,
 | 
			
		||||
}: AutoFormInputComponentProps) {
 | 
			
		||||
  const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
 | 
			
		||||
  const showLabel = _showLabel === undefined ? true : _showLabel;
 | 
			
		||||
  return (
 | 
			
		||||
    <FormItem>
 | 
			
		||||
      {showLabel && (
 | 
			
		||||
        <FormLabel>
 | 
			
		||||
          {label}
 | 
			
		||||
          {isRequired && <span className="text-destructive"> *</span>}
 | 
			
		||||
        </FormLabel>
 | 
			
		||||
      )}
 | 
			
		||||
      <FormControl>
 | 
			
		||||
        <Textarea {...fieldPropsWithoutShowLabel} />
 | 
			
		||||
      </FormControl>
 | 
			
		||||
      {fieldConfigItem.description && (
 | 
			
		||||
        <FormDescription>{fieldConfigItem.description}</FormDescription>
 | 
			
		||||
      )}
 | 
			
		||||
      <FormMessage />
 | 
			
		||||
    </FormItem>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										87
									
								
								web/src/components/ui/auto-form/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								web/src/components/ui/auto-form/index.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,87 @@
 | 
			
		||||
"use client";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { z } from "zod";
 | 
			
		||||
import { Form } from "../form";
 | 
			
		||||
import { DefaultValues, useForm } from "react-hook-form";
 | 
			
		||||
 | 
			
		||||
import { zodResolver } from "@hookform/resolvers/zod";
 | 
			
		||||
import { Button } from "../button";
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
 | 
			
		||||
import { FieldConfig } from "./types";
 | 
			
		||||
import {
 | 
			
		||||
  ZodObjectOrWrapped,
 | 
			
		||||
  getDefaultValues,
 | 
			
		||||
  getObjectFormSchema,
 | 
			
		||||
} from "./utils";
 | 
			
		||||
import AutoFormObject from "./fields/object";
 | 
			
		||||
 | 
			
		||||
export function AutoFormSubmit({ children }: { children?: React.ReactNode }) {
 | 
			
		||||
  return <Button type="submit">{children ?? "Submit"}</Button>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function AutoForm<SchemaType extends ZodObjectOrWrapped>({
 | 
			
		||||
  formSchema,
 | 
			
		||||
  values: valuesProp,
 | 
			
		||||
  onValuesChange: onValuesChangeProp,
 | 
			
		||||
  onParsedValuesChange,
 | 
			
		||||
  onSubmit: onSubmitProp,
 | 
			
		||||
  fieldConfig,
 | 
			
		||||
  children,
 | 
			
		||||
  className,
 | 
			
		||||
}: {
 | 
			
		||||
  formSchema: SchemaType;
 | 
			
		||||
  values?: Partial<z.infer<SchemaType>>;
 | 
			
		||||
  onValuesChange?: (values: Partial<z.infer<SchemaType>>) => void;
 | 
			
		||||
  onParsedValuesChange?: (values: Partial<z.infer<SchemaType>>) => void;
 | 
			
		||||
  onSubmit?: (values: z.infer<SchemaType>) => void;
 | 
			
		||||
  fieldConfig?: FieldConfig<z.infer<SchemaType>>;
 | 
			
		||||
  children?: React.ReactNode;
 | 
			
		||||
  className?: string;
 | 
			
		||||
}) {
 | 
			
		||||
  const objectFormSchema = getObjectFormSchema(formSchema);
 | 
			
		||||
  const defaultValues: DefaultValues<z.infer<typeof objectFormSchema>> =
 | 
			
		||||
    getDefaultValues(objectFormSchema);
 | 
			
		||||
 | 
			
		||||
  const form = useForm<z.infer<typeof objectFormSchema>>({
 | 
			
		||||
    resolver: zodResolver(formSchema),
 | 
			
		||||
    defaultValues,
 | 
			
		||||
    values: valuesProp,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  function onSubmit(values: z.infer<typeof formSchema>) {
 | 
			
		||||
    const parsedValues = formSchema.safeParse(values);
 | 
			
		||||
    if (parsedValues.success) {
 | 
			
		||||
      onSubmitProp?.(parsedValues.data);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Form {...form}>
 | 
			
		||||
      <form
 | 
			
		||||
        onSubmit={(e) => {
 | 
			
		||||
          form.handleSubmit(onSubmit)(e);
 | 
			
		||||
        }}
 | 
			
		||||
        onChange={() => {
 | 
			
		||||
          const values = form.getValues();
 | 
			
		||||
          onValuesChangeProp?.(values);
 | 
			
		||||
          const parsedValues = formSchema.safeParse(values);
 | 
			
		||||
          if (parsedValues.success) {
 | 
			
		||||
            onParsedValuesChange?.(parsedValues.data);
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
        className={cn("space-y-5", className)}
 | 
			
		||||
      >
 | 
			
		||||
        <AutoFormObject
 | 
			
		||||
          schema={objectFormSchema}
 | 
			
		||||
          form={form}
 | 
			
		||||
          fieldConfig={fieldConfig}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        {children}
 | 
			
		||||
      </form>
 | 
			
		||||
    </Form>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default AutoForm;
 | 
			
		||||
							
								
								
									
										37
									
								
								web/src/components/ui/auto-form/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/src/components/ui/auto-form/types.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
import { ControllerRenderProps, FieldValues } from "react-hook-form";
 | 
			
		||||
import * as z from "zod";
 | 
			
		||||
import { INPUT_COMPONENTS } from "./config";
 | 
			
		||||
 | 
			
		||||
export type FieldConfigItem = {
 | 
			
		||||
  description?: React.ReactNode;
 | 
			
		||||
  inputProps?: React.InputHTMLAttributes<HTMLInputElement> & {
 | 
			
		||||
    showLabel?: boolean;
 | 
			
		||||
  };
 | 
			
		||||
  fieldType?:
 | 
			
		||||
    | keyof typeof INPUT_COMPONENTS
 | 
			
		||||
    | React.FC<AutoFormInputComponentProps>;
 | 
			
		||||
 | 
			
		||||
  renderParent?: (props: {
 | 
			
		||||
    children: React.ReactNode;
 | 
			
		||||
  }) => React.ReactElement | null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type FieldConfig<SchemaType extends z.infer<z.ZodObject<any, any>>> = {
 | 
			
		||||
  // If SchemaType.key is an object, create a nested FieldConfig, otherwise FieldConfigItem
 | 
			
		||||
  [Key in keyof SchemaType]?: SchemaType[Key] extends object
 | 
			
		||||
    ? FieldConfig<z.infer<SchemaType[Key]>>
 | 
			
		||||
    : FieldConfigItem;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A FormInput component can handle a specific Zod type (e.g. "ZodBoolean")
 | 
			
		||||
 */
 | 
			
		||||
export type AutoFormInputComponentProps = {
 | 
			
		||||
  zodInputProps: React.InputHTMLAttributes<HTMLInputElement>;
 | 
			
		||||
  field: ControllerRenderProps<FieldValues, any>;
 | 
			
		||||
  fieldConfigItem: FieldConfigItem;
 | 
			
		||||
  label: string;
 | 
			
		||||
  isRequired: boolean;
 | 
			
		||||
  fieldProps: any;
 | 
			
		||||
  zodItem: z.ZodAny;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										159
									
								
								web/src/components/ui/auto-form/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								web/src/components/ui/auto-form/utils.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,159 @@
 | 
			
		||||
import { DefaultValues } from "react-hook-form";
 | 
			
		||||
import { z } from "zod";
 | 
			
		||||
 | 
			
		||||
// TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions.
 | 
			
		||||
export type ZodObjectOrWrapped =
 | 
			
		||||
  | z.ZodObject<any, any>
 | 
			
		||||
  | z.ZodEffects<z.ZodObject<any, any>>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Beautify a camelCase string.
 | 
			
		||||
 * e.g. "myString" -> "My String"
 | 
			
		||||
 */
 | 
			
		||||
export function beautifyObjectName(string: string) {
 | 
			
		||||
  let output = string.replace(/([A-Z])/g, " $1");
 | 
			
		||||
  output = output.charAt(0).toUpperCase() + output.slice(1);
 | 
			
		||||
  return output;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get the lowest level Zod type.
 | 
			
		||||
 * This will unpack optionals, refinements, etc.
 | 
			
		||||
 */
 | 
			
		||||
export function getBaseSchema<
 | 
			
		||||
  ChildType extends z.ZodAny | z.AnyZodObject = z.ZodAny,
 | 
			
		||||
>(schema: ChildType | z.ZodEffects<ChildType>): ChildType {
 | 
			
		||||
  if ("innerType" in schema._def) {
 | 
			
		||||
    return getBaseSchema(schema._def.innerType as ChildType);
 | 
			
		||||
  }
 | 
			
		||||
  if ("schema" in schema._def) {
 | 
			
		||||
    return getBaseSchema(schema._def.schema as ChildType);
 | 
			
		||||
  }
 | 
			
		||||
  return schema as ChildType;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get the type name of the lowest level Zod type.
 | 
			
		||||
 * This will unpack optionals, refinements, etc.
 | 
			
		||||
 */
 | 
			
		||||
export function getBaseType(schema: z.ZodAny): string {
 | 
			
		||||
  return getBaseSchema(schema)._def.typeName;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Search for a "ZodDefult" in the Zod stack and return its value.
 | 
			
		||||
 */
 | 
			
		||||
export function getDefaultValueInZodStack(schema: z.ZodAny): any {
 | 
			
		||||
  const typedSchema = schema as unknown as z.ZodDefault<
 | 
			
		||||
    z.ZodNumber | z.ZodString
 | 
			
		||||
  >;
 | 
			
		||||
 | 
			
		||||
  if (typedSchema._def.typeName === "ZodDefault") {
 | 
			
		||||
    return typedSchema._def.defaultValue();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if ("innerType" in typedSchema._def) {
 | 
			
		||||
    return getDefaultValueInZodStack(
 | 
			
		||||
      typedSchema._def.innerType as unknown as z.ZodAny,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  if ("schema" in typedSchema._def) {
 | 
			
		||||
    return getDefaultValueInZodStack(
 | 
			
		||||
      (typedSchema._def as any).schema as z.ZodAny,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get all default values from a Zod schema.
 | 
			
		||||
 */
 | 
			
		||||
export function getDefaultValues<Schema extends z.ZodObject<any, any>>(
 | 
			
		||||
  schema: Schema,
 | 
			
		||||
) {
 | 
			
		||||
  const { shape } = schema;
 | 
			
		||||
  type DefaultValuesType = DefaultValues<Partial<z.infer<Schema>>>;
 | 
			
		||||
  const defaultValues = {} as DefaultValuesType;
 | 
			
		||||
 | 
			
		||||
  for (const key of Object.keys(shape)) {
 | 
			
		||||
    const item = shape[key] as z.ZodAny;
 | 
			
		||||
 | 
			
		||||
    if (getBaseType(item) === "ZodObject") {
 | 
			
		||||
      const defaultItems = getDefaultValues(
 | 
			
		||||
        getBaseSchema(item) as unknown as z.ZodObject<any, any>,
 | 
			
		||||
      );
 | 
			
		||||
      for (const defaultItemKey of Object.keys(defaultItems)) {
 | 
			
		||||
        const pathKey = `${key}.${defaultItemKey}` as keyof DefaultValuesType;
 | 
			
		||||
        defaultValues[pathKey] = defaultItems[defaultItemKey];
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      const defaultValue = getDefaultValueInZodStack(item);
 | 
			
		||||
      if (defaultValue !== undefined) {
 | 
			
		||||
        defaultValues[key as keyof DefaultValuesType] = defaultValue;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return defaultValues;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getObjectFormSchema(
 | 
			
		||||
  schema: ZodObjectOrWrapped,
 | 
			
		||||
): z.ZodObject<any, any> {
 | 
			
		||||
  if (schema._def.typeName === "ZodEffects") {
 | 
			
		||||
    const typedSchema = schema as z.ZodEffects<z.ZodObject<any, any>>;
 | 
			
		||||
    return getObjectFormSchema(typedSchema._def.schema);
 | 
			
		||||
  }
 | 
			
		||||
  return schema as z.ZodObject<any, any>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Convert a Zod schema to HTML input props to give direct feedback to the user.
 | 
			
		||||
 * Once submitted, the schema will be validated completely.
 | 
			
		||||
 */
 | 
			
		||||
export function zodToHtmlInputProps(
 | 
			
		||||
  schema:
 | 
			
		||||
    | z.ZodNumber
 | 
			
		||||
    | z.ZodString
 | 
			
		||||
    | z.ZodOptional<z.ZodNumber | z.ZodString>
 | 
			
		||||
    | any,
 | 
			
		||||
): React.InputHTMLAttributes<HTMLInputElement> {
 | 
			
		||||
  if (["ZodOptional", "ZodNullable"].includes(schema._def.typeName)) {
 | 
			
		||||
    const typedSchema = schema as z.ZodOptional<z.ZodNumber | z.ZodString>;
 | 
			
		||||
    return {
 | 
			
		||||
      ...zodToHtmlInputProps(typedSchema._def.innerType),
 | 
			
		||||
      required: false,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const typedSchema = schema as z.ZodNumber | z.ZodString;
 | 
			
		||||
 | 
			
		||||
  if (!("checks" in typedSchema._def)) return {
 | 
			
		||||
    required: true
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const { checks } = typedSchema._def;
 | 
			
		||||
  const inputProps: React.InputHTMLAttributes<HTMLInputElement> = {
 | 
			
		||||
    required: true,
 | 
			
		||||
  };
 | 
			
		||||
  const type = getBaseType(schema);
 | 
			
		||||
 | 
			
		||||
  for (const check of checks) {
 | 
			
		||||
    if (check.kind === "min") {
 | 
			
		||||
      if (type === "ZodString") {
 | 
			
		||||
        inputProps.minLength = check.value;
 | 
			
		||||
      } else {
 | 
			
		||||
        inputProps.min = check.value;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (check.kind === "max") {
 | 
			
		||||
      if (type === "ZodString") {
 | 
			
		||||
        inputProps.maxLength = check.value;
 | 
			
		||||
      } else {
 | 
			
		||||
        inputProps.max = check.value;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return inputProps;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								web/src/components/ui/calendar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								web/src/components/ui/calendar.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,66 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import { ChevronLeft, ChevronRight } from "lucide-react"
 | 
			
		||||
import { DayPicker } from "react-day-picker"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
import { buttonVariants } from "@/components/ui/button"
 | 
			
		||||
 | 
			
		||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>
 | 
			
		||||
 | 
			
		||||
function Calendar({
 | 
			
		||||
  className,
 | 
			
		||||
  classNames,
 | 
			
		||||
  showOutsideDays = true,
 | 
			
		||||
  ...props
 | 
			
		||||
}: CalendarProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <DayPicker
 | 
			
		||||
      showOutsideDays={showOutsideDays}
 | 
			
		||||
      className={cn("p-3", className)}
 | 
			
		||||
      classNames={{
 | 
			
		||||
        months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
 | 
			
		||||
        month: "space-y-4",
 | 
			
		||||
        caption: "flex justify-center pt-1 relative items-center",
 | 
			
		||||
        caption_label: "text-sm font-medium",
 | 
			
		||||
        nav: "space-x-1 flex items-center",
 | 
			
		||||
        nav_button: cn(
 | 
			
		||||
          buttonVariants({ variant: "outline" }),
 | 
			
		||||
          "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
 | 
			
		||||
        ),
 | 
			
		||||
        nav_button_previous: "absolute left-1",
 | 
			
		||||
        nav_button_next: "absolute right-1",
 | 
			
		||||
        table: "w-full border-collapse space-y-1",
 | 
			
		||||
        head_row: "flex",
 | 
			
		||||
        head_cell:
 | 
			
		||||
          "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
 | 
			
		||||
        row: "flex w-full mt-2",
 | 
			
		||||
        cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
 | 
			
		||||
        day: cn(
 | 
			
		||||
          buttonVariants({ variant: "ghost" }),
 | 
			
		||||
          "h-9 w-9 p-0 font-normal aria-selected:opacity-100"
 | 
			
		||||
        ),
 | 
			
		||||
        day_range_end: "day-range-end",
 | 
			
		||||
        day_selected:
 | 
			
		||||
          "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
 | 
			
		||||
        day_today: "bg-accent text-accent-foreground",
 | 
			
		||||
        day_outside:
 | 
			
		||||
          "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
 | 
			
		||||
        day_disabled: "text-muted-foreground opacity-50",
 | 
			
		||||
        day_range_middle:
 | 
			
		||||
          "aria-selected:bg-accent aria-selected:text-accent-foreground",
 | 
			
		||||
        day_hidden: "invisible",
 | 
			
		||||
        ...classNames,
 | 
			
		||||
      }}
 | 
			
		||||
      components={{
 | 
			
		||||
        IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
 | 
			
		||||
        IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
 | 
			
		||||
      }}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
Calendar.displayName = "Calendar"
 | 
			
		||||
 | 
			
		||||
export { Calendar }
 | 
			
		||||
							
								
								
									
										46
									
								
								web/src/components/ui/date-picker.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								web/src/components/ui/date-picker.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import { Calendar } from "@/components/ui/calendar";
 | 
			
		||||
import {
 | 
			
		||||
  Popover,
 | 
			
		||||
  PopoverContent,
 | 
			
		||||
  PopoverTrigger,
 | 
			
		||||
} from "@/components/ui/popover";
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import { format } from "date-fns";
 | 
			
		||||
import { Calendar as CalendarIcon } from "lucide-react";
 | 
			
		||||
import { forwardRef } from "react";
 | 
			
		||||
 | 
			
		||||
export const DatePicker = forwardRef<
 | 
			
		||||
  HTMLDivElement,
 | 
			
		||||
  {
 | 
			
		||||
    date?: Date;
 | 
			
		||||
    setDate: (date?: Date) => void;
 | 
			
		||||
  }
 | 
			
		||||
>(function DatePickerCmp({ date, setDate }, ref) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Popover>
 | 
			
		||||
      <PopoverTrigger asChild>
 | 
			
		||||
        <Button
 | 
			
		||||
          variant="outline"
 | 
			
		||||
          className={cn(
 | 
			
		||||
            "w-full justify-start text-left font-normal",
 | 
			
		||||
            !date && "text-muted-foreground"
 | 
			
		||||
          )}
 | 
			
		||||
        >
 | 
			
		||||
          <CalendarIcon className="mr-2 h-4 w-4" />
 | 
			
		||||
          {date ? format(date, "PPP") : <span>Pick a date</span>}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </PopoverTrigger>
 | 
			
		||||
      <PopoverContent className="w-auto p-0" ref={ref}>
 | 
			
		||||
        <Calendar
 | 
			
		||||
          mode="single"
 | 
			
		||||
          selected={date}
 | 
			
		||||
          onSelect={setDate}
 | 
			
		||||
          initialFocus
 | 
			
		||||
        />
 | 
			
		||||
      </PopoverContent>
 | 
			
		||||
    </Popover>
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
@ -1,30 +1,23 @@
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as LabelPrimitive from "@radix-ui/react-label"
 | 
			
		||||
import { Slot } from "@radix-ui/react-slot"
 | 
			
		||||
import {
 | 
			
		||||
  Controller,
 | 
			
		||||
  ControllerProps,
 | 
			
		||||
  FieldPath,
 | 
			
		||||
  FieldValues,
 | 
			
		||||
  FormProvider,
 | 
			
		||||
  useFormContext,
 | 
			
		||||
} from "react-hook-form"
 | 
			
		||||
import { Label } from "@/components/ui/label";
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import type * as LabelPrimitive from "@radix-ui/react-label";
 | 
			
		||||
import { Slot } from "@radix-ui/react-slot";
 | 
			
		||||
import * as React from "react";
 | 
			
		||||
import type { ControllerProps, FieldPath, FieldValues } from "react-hook-form";
 | 
			
		||||
import { Controller, FormProvider, useFormContext } from "react-hook-form";
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
import { Label } from "@/components/ui/label"
 | 
			
		||||
 | 
			
		||||
const Form = FormProvider
 | 
			
		||||
const Form = FormProvider;
 | 
			
		||||
 | 
			
		||||
type FormFieldContextValue<
 | 
			
		||||
  TFieldValues extends FieldValues = FieldValues,
 | 
			
		||||
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
 | 
			
		||||
> = {
 | 
			
		||||
  name: TName
 | 
			
		||||
}
 | 
			
		||||
  name: TName;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
 | 
			
		||||
  {} as FormFieldContextValue
 | 
			
		||||
)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const FormField = <
 | 
			
		||||
  TFieldValues extends FieldValues = FieldValues,
 | 
			
		||||
@ -36,21 +29,21 @@ const FormField = <
 | 
			
		||||
    <FormFieldContext.Provider value={{ name: props.name }}>
 | 
			
		||||
      <Controller {...props} />
 | 
			
		||||
    </FormFieldContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const useFormField = () => {
 | 
			
		||||
  const fieldContext = React.useContext(FormFieldContext)
 | 
			
		||||
  const itemContext = React.useContext(FormItemContext)
 | 
			
		||||
  const { getFieldState, formState } = useFormContext()
 | 
			
		||||
  const fieldContext = React.useContext(FormFieldContext);
 | 
			
		||||
  const itemContext = React.useContext(FormItemContext);
 | 
			
		||||
  const { getFieldState, formState } = useFormContext();
 | 
			
		||||
 | 
			
		||||
  const fieldState = getFieldState(fieldContext.name, formState)
 | 
			
		||||
  const fieldState = getFieldState(fieldContext.name, formState);
 | 
			
		||||
 | 
			
		||||
  if (!fieldContext) {
 | 
			
		||||
    throw new Error("useFormField should be used within <FormField>")
 | 
			
		||||
    throw new Error("useFormField should be used within <FormField>");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const { id } = itemContext
 | 
			
		||||
  const { id } = itemContext;
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    id,
 | 
			
		||||
@ -59,36 +52,36 @@ const useFormField = () => {
 | 
			
		||||
    formDescriptionId: `${id}-form-item-description`,
 | 
			
		||||
    formMessageId: `${id}-form-item-message`,
 | 
			
		||||
    ...fieldState,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type FormItemContextValue = {
 | 
			
		||||
  id: string
 | 
			
		||||
}
 | 
			
		||||
  id: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const FormItemContext = React.createContext<FormItemContextValue>(
 | 
			
		||||
  {} as FormItemContextValue
 | 
			
		||||
)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const FormItem = React.forwardRef<
 | 
			
		||||
  HTMLDivElement,
 | 
			
		||||
  React.HTMLAttributes<HTMLDivElement>
 | 
			
		||||
>(({ className, ...props }, ref) => {
 | 
			
		||||
  const id = React.useId()
 | 
			
		||||
  const id = React.useId();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <FormItemContext.Provider value={{ id }}>
 | 
			
		||||
      <div ref={ref} className={cn("space-y-2", className)} {...props} />
 | 
			
		||||
    </FormItemContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
FormItem.displayName = "FormItem"
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
FormItem.displayName = "FormItem";
 | 
			
		||||
 | 
			
		||||
const FormLabel = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof LabelPrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
 | 
			
		||||
>(({ className, ...props }, ref) => {
 | 
			
		||||
  const { error, formItemId } = useFormField()
 | 
			
		||||
  const { error, formItemId } = useFormField();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Label
 | 
			
		||||
@ -97,15 +90,16 @@ const FormLabel = React.forwardRef<
 | 
			
		||||
      htmlFor={formItemId}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
FormLabel.displayName = "FormLabel"
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
FormLabel.displayName = "FormLabel";
 | 
			
		||||
 | 
			
		||||
const FormControl = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof Slot>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof Slot>
 | 
			
		||||
>(({ ...props }, ref) => {
 | 
			
		||||
  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
 | 
			
		||||
  const { error, formItemId, formDescriptionId, formMessageId } =
 | 
			
		||||
    useFormField();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Slot
 | 
			
		||||
@ -119,15 +113,15 @@ const FormControl = React.forwardRef<
 | 
			
		||||
      aria-invalid={!!error}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
FormControl.displayName = "FormControl"
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
FormControl.displayName = "FormControl";
 | 
			
		||||
 | 
			
		||||
const FormDescription = React.forwardRef<
 | 
			
		||||
  HTMLParagraphElement,
 | 
			
		||||
  React.HTMLAttributes<HTMLParagraphElement>
 | 
			
		||||
>(({ className, ...props }, ref) => {
 | 
			
		||||
  const { formDescriptionId } = useFormField()
 | 
			
		||||
  const { formDescriptionId } = useFormField();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <p
 | 
			
		||||
@ -136,19 +130,19 @@ const FormDescription = React.forwardRef<
 | 
			
		||||
      className={cn("text-sm text-muted-foreground", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
FormDescription.displayName = "FormDescription"
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
FormDescription.displayName = "FormDescription";
 | 
			
		||||
 | 
			
		||||
const FormMessage = React.forwardRef<
 | 
			
		||||
  HTMLParagraphElement,
 | 
			
		||||
  React.HTMLAttributes<HTMLParagraphElement>
 | 
			
		||||
>(({ className, children, ...props }, ref) => {
 | 
			
		||||
  const { error, formMessageId } = useFormField()
 | 
			
		||||
  const body = error ? String(error?.message) : children
 | 
			
		||||
  const { error, formMessageId } = useFormField();
 | 
			
		||||
  const body = error ? String(error?.message) : children;
 | 
			
		||||
 | 
			
		||||
  if (!body) {
 | 
			
		||||
    return null
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
@ -160,9 +154,9 @@ const FormMessage = React.forwardRef<
 | 
			
		||||
    >
 | 
			
		||||
      {body}
 | 
			
		||||
    </p>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
FormMessage.displayName = "FormMessage"
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
FormMessage.displayName = "FormMessage";
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  useFormField,
 | 
			
		||||
@ -173,4 +167,4 @@ export {
 | 
			
		||||
  FormDescription,
 | 
			
		||||
  FormMessage,
 | 
			
		||||
  FormField,
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										31
									
								
								web/src/components/ui/popover.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								web/src/components/ui/popover.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const Popover = PopoverPrimitive.Root
 | 
			
		||||
 | 
			
		||||
const PopoverTrigger = PopoverPrimitive.Trigger
 | 
			
		||||
 | 
			
		||||
const PopoverContent = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof PopoverPrimitive.Content>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
 | 
			
		||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
 | 
			
		||||
  <PopoverPrimitive.Portal>
 | 
			
		||||
    <PopoverPrimitive.Content
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      align={align}
 | 
			
		||||
      sideOffset={sideOffset}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  </PopoverPrimitive.Portal>
 | 
			
		||||
))
 | 
			
		||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
 | 
			
		||||
 | 
			
		||||
export { Popover, PopoverTrigger, PopoverContent }
 | 
			
		||||
							
								
								
									
										44
									
								
								web/src/components/ui/radio-group.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								web/src/components/ui/radio-group.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
 | 
			
		||||
import { Circle } from "lucide-react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const RadioGroup = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof RadioGroupPrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
 | 
			
		||||
>(({ className, ...props }, ref) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <RadioGroupPrimitive.Root
 | 
			
		||||
      className={cn("grid gap-2", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
      ref={ref}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
 | 
			
		||||
 | 
			
		||||
const RadioGroupItem = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof RadioGroupPrimitive.Item>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
 | 
			
		||||
>(({ className, ...props }, ref) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <RadioGroupPrimitive.Item
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
 | 
			
		||||
        <Circle className="h-2.5 w-2.5 fill-current text-current" />
 | 
			
		||||
      </RadioGroupPrimitive.Indicator>
 | 
			
		||||
    </RadioGroupPrimitive.Item>
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
 | 
			
		||||
 | 
			
		||||
export { RadioGroup, RadioGroupItem }
 | 
			
		||||
							
								
								
									
										31
									
								
								web/src/components/ui/separator.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								web/src/components/ui/separator.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const Separator = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SeparatorPrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
 | 
			
		||||
>(
 | 
			
		||||
  (
 | 
			
		||||
    { className, orientation = "horizontal", decorative = true, ...props },
 | 
			
		||||
    ref
 | 
			
		||||
  ) => (
 | 
			
		||||
    <SeparatorPrimitive.Root
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      decorative={decorative}
 | 
			
		||||
      orientation={orientation}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "shrink-0 bg-border",
 | 
			
		||||
        orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
)
 | 
			
		||||
Separator.displayName = SeparatorPrimitive.Root.displayName
 | 
			
		||||
 | 
			
		||||
export { Separator }
 | 
			
		||||
							
								
								
									
										29
									
								
								web/src/components/ui/switch.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								web/src/components/ui/switch.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const Switch = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof SwitchPrimitives.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
  <SwitchPrimitives.Root
 | 
			
		||||
    className={cn(
 | 
			
		||||
      "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
 | 
			
		||||
      className
 | 
			
		||||
    )}
 | 
			
		||||
    {...props}
 | 
			
		||||
    ref={ref}
 | 
			
		||||
  >
 | 
			
		||||
    <SwitchPrimitives.Thumb
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
 | 
			
		||||
      )}
 | 
			
		||||
    />
 | 
			
		||||
  </SwitchPrimitives.Root>
 | 
			
		||||
))
 | 
			
		||||
Switch.displayName = SwitchPrimitives.Root.displayName
 | 
			
		||||
 | 
			
		||||
export { Switch }
 | 
			
		||||
							
								
								
									
										24
									
								
								web/src/components/ui/textarea.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								web/src/components/ui/textarea.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
export interface TextareaProps
 | 
			
		||||
  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
 | 
			
		||||
 | 
			
		||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
 | 
			
		||||
  ({ className, ...props }, ref) => {
 | 
			
		||||
    return (
 | 
			
		||||
      <textarea
 | 
			
		||||
        className={cn(
 | 
			
		||||
          "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
 | 
			
		||||
          className
 | 
			
		||||
        )}
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        {...props}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
Textarea.displayName = "Textarea"
 | 
			
		||||
 | 
			
		||||
export { Textarea }
 | 
			
		||||
							
								
								
									
										45
									
								
								web/src/components/ui/toggle.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								web/src/components/ui/toggle.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const toggleVariants = cva(
 | 
			
		||||
  "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
        default: "bg-transparent",
 | 
			
		||||
        outline:
 | 
			
		||||
          "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
 | 
			
		||||
      },
 | 
			
		||||
      size: {
 | 
			
		||||
        default: "h-10 px-3",
 | 
			
		||||
        sm: "h-9 px-2.5",
 | 
			
		||||
        lg: "h-11 px-5",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      variant: "default",
 | 
			
		||||
      size: "default",
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const Toggle = React.forwardRef<
 | 
			
		||||
  React.ElementRef<typeof TogglePrimitive.Root>,
 | 
			
		||||
  React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
 | 
			
		||||
    VariantProps<typeof toggleVariants>
 | 
			
		||||
>(({ className, variant, size, ...props }, ref) => (
 | 
			
		||||
  <TogglePrimitive.Root
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    className={cn(toggleVariants({ variant, size, className }))}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))
 | 
			
		||||
 | 
			
		||||
Toggle.displayName = TogglePrimitive.Root.displayName
 | 
			
		||||
 | 
			
		||||
export { Toggle, toggleVariants }
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user