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-dialog": "^1.0.5",
 | 
				
			||||||
    "@radix-ui/react-dropdown-menu": "^2.0.6",
 | 
					    "@radix-ui/react-dropdown-menu": "^2.0.6",
 | 
				
			||||||
    "@radix-ui/react-label": "^2.0.2",
 | 
					    "@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-select": "^2.0.0",
 | 
				
			||||||
 | 
					    "@radix-ui/react-separator": "^1.0.3",
 | 
				
			||||||
    "@radix-ui/react-slot": "^1.0.2",
 | 
					    "@radix-ui/react-slot": "^1.0.2",
 | 
				
			||||||
 | 
					    "@radix-ui/react-switch": "^1.0.3",
 | 
				
			||||||
    "@radix-ui/react-tabs": "^1.0.4",
 | 
					    "@radix-ui/react-tabs": "^1.0.4",
 | 
				
			||||||
 | 
					    "@radix-ui/react-toggle": "^1.0.3",
 | 
				
			||||||
    "@radix-ui/react-tooltip": "^1.0.7",
 | 
					    "@radix-ui/react-tooltip": "^1.0.7",
 | 
				
			||||||
    "@tanstack/react-table": "^8.10.7",
 | 
					    "@tanstack/react-table": "^8.10.7",
 | 
				
			||||||
    "@types/jsonwebtoken": "^9.0.5",
 | 
					    "@types/jsonwebtoken": "^9.0.5",
 | 
				
			||||||
    "@types/uuid": "^9.0.7",
 | 
					    "@types/uuid": "^9.0.7",
 | 
				
			||||||
    "class-variance-authority": "^0.7.0",
 | 
					    "class-variance-authority": "^0.7.0",
 | 
				
			||||||
    "clsx": "^2.0.0",
 | 
					    "clsx": "^2.0.0",
 | 
				
			||||||
 | 
					    "date-fns": "^3.0.5",
 | 
				
			||||||
    "dayjs": "^1.11.10",
 | 
					    "dayjs": "^1.11.10",
 | 
				
			||||||
    "drizzle-orm": "^0.29.1",
 | 
					    "drizzle-orm": "^0.29.1",
 | 
				
			||||||
    "jsonwebtoken": "^9.0.2",
 | 
					    "jsonwebtoken": "^9.0.2",
 | 
				
			||||||
@ -43,6 +49,7 @@
 | 
				
			|||||||
    "next-plausible": "^3.12.0",
 | 
					    "next-plausible": "^3.12.0",
 | 
				
			||||||
    "next-usequerystate": "^1.13.2",
 | 
					    "next-usequerystate": "^1.13.2",
 | 
				
			||||||
    "react": "^18",
 | 
					    "react": "^18",
 | 
				
			||||||
 | 
					    "react-day-picker": "^8.9.1",
 | 
				
			||||||
    "react-dom": "^18",
 | 
					    "react-dom": "^18",
 | 
				
			||||||
    "react-hook-form": "^7.48.2",
 | 
					    "react-hook-form": "^7.48.2",
 | 
				
			||||||
    "react-use-websocket": "^4.5.0",
 | 
					    "react-use-websocket": "^4.5.0",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,3 @@
 | 
				
			|||||||
import { OutputRender } from "./OutputRender";
 | 
					 | 
				
			||||||
import { CodeBlock } from "@/components/CodeBlock";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Table,
 | 
					  Table,
 | 
				
			||||||
  TableBody,
 | 
					  TableBody,
 | 
				
			||||||
@ -8,8 +6,7 @@ import {
 | 
				
			|||||||
  TableHeader,
 | 
					  TableHeader,
 | 
				
			||||||
  TableRow,
 | 
					  TableRow,
 | 
				
			||||||
} from "@/components/ui/table";
 | 
					} from "@/components/ui/table";
 | 
				
			||||||
import { findAllRuns } from "@/server/findAllRuns";
 | 
					import type { findAllRuns } from "@/server/findAllRuns";
 | 
				
			||||||
import { getRunsOutput } from "@/server/getRunsOutput";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function RunInputs({
 | 
					export async function RunInputs({
 | 
				
			||||||
  run,
 | 
					  run,
 | 
				
			||||||
@ -30,16 +27,28 @@ export async function RunInputs({
 | 
				
			|||||||
            {Object.entries(run.workflow_inputs).map(([key, data]) => {
 | 
					            {Object.entries(run.workflow_inputs).map(([key, data]) => {
 | 
				
			||||||
              let imageUrl;
 | 
					              let imageUrl;
 | 
				
			||||||
              try {
 | 
					              try {
 | 
				
			||||||
                const url = new URL(data);
 | 
					                if (data.startsWith("data:image/")) {
 | 
				
			||||||
                if (url.pathname.endsWith('.png')) {
 | 
					 | 
				
			||||||
                  imageUrl = data;
 | 
					                  imageUrl = data;
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                  const url = new URL(data);
 | 
				
			||||||
 | 
					                  if (url.pathname.endsWith(".png")) {
 | 
				
			||||||
 | 
					                    imageUrl = data;
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              } catch (_) {
 | 
					              } catch (_) {}
 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
              return (
 | 
					              return (
 | 
				
			||||||
                <TableRow key={key}>
 | 
					                <TableRow key={key}>
 | 
				
			||||||
                  <TableCell>{key}</TableCell>
 | 
					                  <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>
 | 
					                </TableRow>
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
            })}
 | 
					            })}
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,16 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { callServerPromise } from "./callServerPromise";
 | 
					import { callServerPromise } from "./callServerPromise";
 | 
				
			||||||
import { LoadingIcon } from "@/components/LoadingIcon";
 | 
					import { LoadingIcon } from "@/components/LoadingIcon";
 | 
				
			||||||
 | 
					import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form";
 | 
				
			||||||
import { Button } from "@/components/ui/button";
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Dialog,
 | 
				
			||||||
 | 
					  DialogContent,
 | 
				
			||||||
 | 
					  DialogDescription,
 | 
				
			||||||
 | 
					  DialogHeader,
 | 
				
			||||||
 | 
					  DialogTitle,
 | 
				
			||||||
 | 
					  DialogTrigger,
 | 
				
			||||||
 | 
					} from "@/components/ui/dialog";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  DropdownMenu,
 | 
					  DropdownMenu,
 | 
				
			||||||
  DropdownMenuContent,
 | 
					  DropdownMenuContent,
 | 
				
			||||||
@ -18,14 +27,16 @@ import {
 | 
				
			|||||||
  SelectTrigger,
 | 
					  SelectTrigger,
 | 
				
			||||||
  SelectValue,
 | 
					  SelectValue,
 | 
				
			||||||
} from "@/components/ui/select";
 | 
					} from "@/components/ui/select";
 | 
				
			||||||
 | 
					import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
 | 
				
			||||||
import { createRun } from "@/server/createRun";
 | 
					import { createRun } from "@/server/createRun";
 | 
				
			||||||
import { createDeployments } from "@/server/curdDeploments";
 | 
					import { createDeployments } from "@/server/curdDeploments";
 | 
				
			||||||
import type { getMachines } from "@/server/curdMachine";
 | 
					import type { getMachines } from "@/server/curdMachine";
 | 
				
			||||||
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
 | 
					import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
 | 
				
			||||||
import { Copy, MoreVertical, Play } from "lucide-react";
 | 
					import { Copy, MoreVertical, Play } from "lucide-react";
 | 
				
			||||||
import { parseAsInteger, useQueryState } from "next-usequerystate";
 | 
					import { parseAsInteger, useQueryState } from "next-usequerystate";
 | 
				
			||||||
import { useState } from "react";
 | 
					import { useMemo, useState } from "react";
 | 
				
			||||||
import { toast } from "sonner";
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					import { z } from "zod";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function VersionSelect({
 | 
					export function VersionSelect({
 | 
				
			||||||
  workflow,
 | 
					  workflow,
 | 
				
			||||||
@ -106,31 +117,95 @@ export function RunWorkflowButton({
 | 
				
			|||||||
    defaultValue: machines[0].id ?? "",
 | 
					    defaultValue: machines[0].id ?? "",
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  const [isLoading, setIsLoading] = useState(false);
 | 
					  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);
 | 
					  const [values, setValues] = useState<Record<string, string>>({});
 | 
				
			||||||
        try {
 | 
					  const [open, setOpen] = useState(false);
 | 
				
			||||||
          const origin = window.location.origin;
 | 
					
 | 
				
			||||||
          await callServerPromise(
 | 
					  const schema = useMemo(() => {
 | 
				
			||||||
            createRun(origin, workflow_version_id, machine, undefined, true)
 | 
					    const workflow_version = getWorkflowVersionFromVersionIndex(
 | 
				
			||||||
          );
 | 
					      workflow,
 | 
				
			||||||
          // console.log(res.json());
 | 
					      version
 | 
				
			||||||
          setIsLoading(false);
 | 
					    );
 | 
				
			||||||
        } catch (error) {
 | 
					    const inputs = getInputsFromWorkflow(workflow_version);
 | 
				
			||||||
          setIsLoading(false);
 | 
					
 | 
				
			||||||
        }
 | 
					    if (!inputs) return null;
 | 
				
			||||||
      }}
 | 
					
 | 
				
			||||||
    >
 | 
					    return z.object({
 | 
				
			||||||
      Run {isLoading ? <LoadingIcon /> : <Play size={14} />}
 | 
					      ...Object.fromEntries(
 | 
				
			||||||
    </Button>
 | 
					        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> = {
 | 
					export const customInputNodes: Record<string, string> = {
 | 
				
			||||||
  ComfyUIDeployExternalText: "string",
 | 
					  ComfyUIDeployExternalText: "string",
 | 
				
			||||||
  ComfyUIDeployExternalImage: "string - (public image url)",
 | 
					  ComfyUIDeployExternalImage: "string - (public image url)",
 | 
				
			||||||
 | 
					  ComfyUIDeployExternalImageAlpha: "string - (public image url)",
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,11 @@
 | 
				
			|||||||
"use client"
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import * as React from "react"
 | 
					import { cn } from "@/lib/utils";
 | 
				
			||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
 | 
					import * as AccordionPrimitive from "@radix-ui/react-accordion";
 | 
				
			||||||
import { ChevronDown } from "lucide-react"
 | 
					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<
 | 
					const AccordionItem = React.forwardRef<
 | 
				
			||||||
  React.ElementRef<typeof AccordionPrimitive.Item>,
 | 
					  React.ElementRef<typeof AccordionPrimitive.Item>,
 | 
				
			||||||
@ -17,8 +16,8 @@ const AccordionItem = React.forwardRef<
 | 
				
			|||||||
    className={cn("border-b", className)}
 | 
					    className={cn("border-b", className)}
 | 
				
			||||||
    {...props}
 | 
					    {...props}
 | 
				
			||||||
  />
 | 
					  />
 | 
				
			||||||
))
 | 
					));
 | 
				
			||||||
AccordionItem.displayName = "AccordionItem"
 | 
					AccordionItem.displayName = "AccordionItem";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AccordionTrigger = React.forwardRef<
 | 
					const AccordionTrigger = React.forwardRef<
 | 
				
			||||||
  React.ElementRef<typeof AccordionPrimitive.Trigger>,
 | 
					  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" />
 | 
					      <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
 | 
				
			||||||
    </AccordionPrimitive.Trigger>
 | 
					    </AccordionPrimitive.Trigger>
 | 
				
			||||||
  </AccordionPrimitive.Header>
 | 
					  </AccordionPrimitive.Header>
 | 
				
			||||||
))
 | 
					));
 | 
				
			||||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
 | 
					AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AccordionContent = React.forwardRef<
 | 
					const AccordionContent = React.forwardRef<
 | 
				
			||||||
  React.ElementRef<typeof AccordionPrimitive.Content>,
 | 
					  React.ElementRef<typeof AccordionPrimitive.Content>,
 | 
				
			||||||
@ -51,8 +50,8 @@ const AccordionContent = React.forwardRef<
 | 
				
			|||||||
  >
 | 
					  >
 | 
				
			||||||
    <div className={cn("pb-4 pt-0", className)}>{children}</div>
 | 
					    <div className={cn("pb-4 pt-0", className)}>{children}</div>
 | 
				
			||||||
  </AccordionPrimitive.Content>
 | 
					  </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 { Label } from "@/components/ui/label";
 | 
				
			||||||
import * as LabelPrimitive from "@radix-ui/react-label"
 | 
					import { cn } from "@/lib/utils";
 | 
				
			||||||
import { Slot } from "@radix-ui/react-slot"
 | 
					import type * as LabelPrimitive from "@radix-ui/react-label";
 | 
				
			||||||
import {
 | 
					import { Slot } from "@radix-ui/react-slot";
 | 
				
			||||||
  Controller,
 | 
					import * as React from "react";
 | 
				
			||||||
  ControllerProps,
 | 
					import type { ControllerProps, FieldPath, FieldValues } from "react-hook-form";
 | 
				
			||||||
  FieldPath,
 | 
					import { Controller, FormProvider, useFormContext } from "react-hook-form";
 | 
				
			||||||
  FieldValues,
 | 
					 | 
				
			||||||
  FormProvider,
 | 
					 | 
				
			||||||
  useFormContext,
 | 
					 | 
				
			||||||
} from "react-hook-form"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { cn } from "@/lib/utils"
 | 
					const Form = FormProvider;
 | 
				
			||||||
import { Label } from "@/components/ui/label"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const Form = FormProvider
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
type FormFieldContextValue<
 | 
					type FormFieldContextValue<
 | 
				
			||||||
  TFieldValues extends FieldValues = FieldValues,
 | 
					  TFieldValues extends FieldValues = FieldValues,
 | 
				
			||||||
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
 | 
					  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
 | 
				
			||||||
> = {
 | 
					> = {
 | 
				
			||||||
  name: TName
 | 
					  name: TName;
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
 | 
					const FormFieldContext = React.createContext<FormFieldContextValue>(
 | 
				
			||||||
  {} as FormFieldContextValue
 | 
					  {} as FormFieldContextValue
 | 
				
			||||||
)
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const FormField = <
 | 
					const FormField = <
 | 
				
			||||||
  TFieldValues extends FieldValues = FieldValues,
 | 
					  TFieldValues extends FieldValues = FieldValues,
 | 
				
			||||||
@ -36,21 +29,21 @@ const FormField = <
 | 
				
			|||||||
    <FormFieldContext.Provider value={{ name: props.name }}>
 | 
					    <FormFieldContext.Provider value={{ name: props.name }}>
 | 
				
			||||||
      <Controller {...props} />
 | 
					      <Controller {...props} />
 | 
				
			||||||
    </FormFieldContext.Provider>
 | 
					    </FormFieldContext.Provider>
 | 
				
			||||||
  )
 | 
					  );
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const useFormField = () => {
 | 
					const useFormField = () => {
 | 
				
			||||||
  const fieldContext = React.useContext(FormFieldContext)
 | 
					  const fieldContext = React.useContext(FormFieldContext);
 | 
				
			||||||
  const itemContext = React.useContext(FormItemContext)
 | 
					  const itemContext = React.useContext(FormItemContext);
 | 
				
			||||||
  const { getFieldState, formState } = useFormContext()
 | 
					  const { getFieldState, formState } = useFormContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const fieldState = getFieldState(fieldContext.name, formState)
 | 
					  const fieldState = getFieldState(fieldContext.name, formState);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!fieldContext) {
 | 
					  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 {
 | 
					  return {
 | 
				
			||||||
    id,
 | 
					    id,
 | 
				
			||||||
@ -59,36 +52,36 @@ const useFormField = () => {
 | 
				
			|||||||
    formDescriptionId: `${id}-form-item-description`,
 | 
					    formDescriptionId: `${id}-form-item-description`,
 | 
				
			||||||
    formMessageId: `${id}-form-item-message`,
 | 
					    formMessageId: `${id}-form-item-message`,
 | 
				
			||||||
    ...fieldState,
 | 
					    ...fieldState,
 | 
				
			||||||
  }
 | 
					  };
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type FormItemContextValue = {
 | 
					type FormItemContextValue = {
 | 
				
			||||||
  id: string
 | 
					  id: string;
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const FormItemContext = React.createContext<FormItemContextValue>(
 | 
					const FormItemContext = React.createContext<FormItemContextValue>(
 | 
				
			||||||
  {} as FormItemContextValue
 | 
					  {} as FormItemContextValue
 | 
				
			||||||
)
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const FormItem = React.forwardRef<
 | 
					const FormItem = React.forwardRef<
 | 
				
			||||||
  HTMLDivElement,
 | 
					  HTMLDivElement,
 | 
				
			||||||
  React.HTMLAttributes<HTMLDivElement>
 | 
					  React.HTMLAttributes<HTMLDivElement>
 | 
				
			||||||
>(({ className, ...props }, ref) => {
 | 
					>(({ className, ...props }, ref) => {
 | 
				
			||||||
  const id = React.useId()
 | 
					  const id = React.useId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <FormItemContext.Provider value={{ id }}>
 | 
					    <FormItemContext.Provider value={{ id }}>
 | 
				
			||||||
      <div ref={ref} className={cn("space-y-2", className)} {...props} />
 | 
					      <div ref={ref} className={cn("space-y-2", className)} {...props} />
 | 
				
			||||||
    </FormItemContext.Provider>
 | 
					    </FormItemContext.Provider>
 | 
				
			||||||
  )
 | 
					  );
 | 
				
			||||||
})
 | 
					});
 | 
				
			||||||
FormItem.displayName = "FormItem"
 | 
					FormItem.displayName = "FormItem";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const FormLabel = React.forwardRef<
 | 
					const FormLabel = React.forwardRef<
 | 
				
			||||||
  React.ElementRef<typeof LabelPrimitive.Root>,
 | 
					  React.ElementRef<typeof LabelPrimitive.Root>,
 | 
				
			||||||
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
 | 
					  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
 | 
				
			||||||
>(({ className, ...props }, ref) => {
 | 
					>(({ className, ...props }, ref) => {
 | 
				
			||||||
  const { error, formItemId } = useFormField()
 | 
					  const { error, formItemId } = useFormField();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Label
 | 
					    <Label
 | 
				
			||||||
@ -97,15 +90,16 @@ const FormLabel = React.forwardRef<
 | 
				
			|||||||
      htmlFor={formItemId}
 | 
					      htmlFor={formItemId}
 | 
				
			||||||
      {...props}
 | 
					      {...props}
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
  )
 | 
					  );
 | 
				
			||||||
})
 | 
					});
 | 
				
			||||||
FormLabel.displayName = "FormLabel"
 | 
					FormLabel.displayName = "FormLabel";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const FormControl = React.forwardRef<
 | 
					const FormControl = React.forwardRef<
 | 
				
			||||||
  React.ElementRef<typeof Slot>,
 | 
					  React.ElementRef<typeof Slot>,
 | 
				
			||||||
  React.ComponentPropsWithoutRef<typeof Slot>
 | 
					  React.ComponentPropsWithoutRef<typeof Slot>
 | 
				
			||||||
>(({ ...props }, ref) => {
 | 
					>(({ ...props }, ref) => {
 | 
				
			||||||
  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
 | 
					  const { error, formItemId, formDescriptionId, formMessageId } =
 | 
				
			||||||
 | 
					    useFormField();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Slot
 | 
					    <Slot
 | 
				
			||||||
@ -119,15 +113,15 @@ const FormControl = React.forwardRef<
 | 
				
			|||||||
      aria-invalid={!!error}
 | 
					      aria-invalid={!!error}
 | 
				
			||||||
      {...props}
 | 
					      {...props}
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
  )
 | 
					  );
 | 
				
			||||||
})
 | 
					});
 | 
				
			||||||
FormControl.displayName = "FormControl"
 | 
					FormControl.displayName = "FormControl";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const FormDescription = React.forwardRef<
 | 
					const FormDescription = React.forwardRef<
 | 
				
			||||||
  HTMLParagraphElement,
 | 
					  HTMLParagraphElement,
 | 
				
			||||||
  React.HTMLAttributes<HTMLParagraphElement>
 | 
					  React.HTMLAttributes<HTMLParagraphElement>
 | 
				
			||||||
>(({ className, ...props }, ref) => {
 | 
					>(({ className, ...props }, ref) => {
 | 
				
			||||||
  const { formDescriptionId } = useFormField()
 | 
					  const { formDescriptionId } = useFormField();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <p
 | 
					    <p
 | 
				
			||||||
@ -136,19 +130,19 @@ const FormDescription = React.forwardRef<
 | 
				
			|||||||
      className={cn("text-sm text-muted-foreground", className)}
 | 
					      className={cn("text-sm text-muted-foreground", className)}
 | 
				
			||||||
      {...props}
 | 
					      {...props}
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
  )
 | 
					  );
 | 
				
			||||||
})
 | 
					});
 | 
				
			||||||
FormDescription.displayName = "FormDescription"
 | 
					FormDescription.displayName = "FormDescription";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const FormMessage = React.forwardRef<
 | 
					const FormMessage = React.forwardRef<
 | 
				
			||||||
  HTMLParagraphElement,
 | 
					  HTMLParagraphElement,
 | 
				
			||||||
  React.HTMLAttributes<HTMLParagraphElement>
 | 
					  React.HTMLAttributes<HTMLParagraphElement>
 | 
				
			||||||
>(({ className, children, ...props }, ref) => {
 | 
					>(({ className, children, ...props }, ref) => {
 | 
				
			||||||
  const { error, formMessageId } = useFormField()
 | 
					  const { error, formMessageId } = useFormField();
 | 
				
			||||||
  const body = error ? String(error?.message) : children
 | 
					  const body = error ? String(error?.message) : children;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!body) {
 | 
					  if (!body) {
 | 
				
			||||||
    return null
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@ -160,9 +154,9 @@ const FormMessage = React.forwardRef<
 | 
				
			|||||||
    >
 | 
					    >
 | 
				
			||||||
      {body}
 | 
					      {body}
 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
  )
 | 
					  );
 | 
				
			||||||
})
 | 
					});
 | 
				
			||||||
FormMessage.displayName = "FormMessage"
 | 
					FormMessage.displayName = "FormMessage";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export {
 | 
					export {
 | 
				
			||||||
  useFormField,
 | 
					  useFormField,
 | 
				
			||||||
@ -173,4 +167,4 @@ export {
 | 
				
			|||||||
  FormDescription,
 | 
					  FormDescription,
 | 
				
			||||||
  FormMessage,
 | 
					  FormMessage,
 | 
				
			||||||
  FormField,
 | 
					  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