mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor(organization): migrate to react-hook-form with zod validation
This commit is contained in:
@@ -9,18 +9,39 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PenBoxIcon, Plus } from "lucide-react";
|
import { PenBoxIcon, Plus } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const organizationSchema = z.object({
|
||||||
|
name: z.string().min(1, {
|
||||||
|
message: "Organization name is required",
|
||||||
|
}),
|
||||||
|
logo: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type OrganizationFormValues = z.infer<typeof organizationSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AddOrganization({ organizationId }: Props) {
|
export function AddOrganization({ organizationId }: Props) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data: organization } = api.organization.one.useQuery(
|
const { data: organization } = api.organization.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -33,24 +54,37 @@ export function AddOrganization({ organizationId }: Props) {
|
|||||||
const { mutateAsync, isLoading } = organizationId
|
const { mutateAsync, isLoading } = organizationId
|
||||||
? api.organization.update.useMutation()
|
? api.organization.update.useMutation()
|
||||||
: api.organization.create.useMutation();
|
: api.organization.create.useMutation();
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [name, setName] = useState("");
|
const form = useForm<OrganizationFormValues>({
|
||||||
const [logo, setLogo] = useState("");
|
resolver: zodResolver(organizationSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
logo: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (organization) {
|
if (organization) {
|
||||||
setName(organization.name);
|
form.reset({
|
||||||
setLogo(organization.logo || "");
|
name: organization.name,
|
||||||
|
logo: organization.logo || "",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [organization]);
|
}, [organization, form]);
|
||||||
const handleSubmit = async () => {
|
|
||||||
await mutateAsync({ name, logo, organizationId: organizationId ?? "" })
|
const onSubmit = async (values: OrganizationFormValues) => {
|
||||||
|
await mutateAsync({
|
||||||
|
name: values.name,
|
||||||
|
logo: values.logo,
|
||||||
|
organizationId: organizationId ?? "",
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setOpen(false);
|
form.reset();
|
||||||
toast.success(
|
toast.success(
|
||||||
`Organization ${organizationId ? "updated" : "created"} successfully`,
|
`Organization ${organizationId ? "updated" : "created"} successfully`,
|
||||||
);
|
);
|
||||||
utils.organization.all.invalidate();
|
utils.organization.all.invalidate();
|
||||||
|
setOpen(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -59,6 +93,7 @@ export function AddOrganization({ organizationId }: Props) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -67,14 +102,11 @@ export function AddOrganization({ organizationId }: Props) {
|
|||||||
className="group cursor-pointer hover:bg-blue-500/10"
|
className="group cursor-pointer hover:bg-blue-500/10"
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : (
|
) : (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="gap-2 p-2"
|
className="gap-2 p-2"
|
||||||
onClick={() => {
|
|
||||||
setOpen(true);
|
|
||||||
}}
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
|
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
|
||||||
@@ -97,36 +129,53 @@ export function AddOrganization({ organizationId }: Props) {
|
|||||||
: "Create a new organization to manage your projects."}
|
: "Create a new organization to manage your projects."}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4 py-4">
|
<Form {...form}>
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
<form
|
||||||
<Label htmlFor="name" className="text-right">
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
Name
|
className="grid gap-4 py-4"
|
||||||
</Label>
|
>
|
||||||
<Input
|
<FormField
|
||||||
id="name"
|
control={form.control}
|
||||||
value={name}
|
name="name"
|
||||||
onChange={(e) => setName(e.target.value)}
|
render={({ field }) => (
|
||||||
className="col-span-3"
|
<FormItem className="tems-center gap-4">
|
||||||
|
<FormLabel className="text-right">Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Organization name"
|
||||||
|
{...field}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage className="" />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<FormField
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
control={form.control}
|
||||||
<Label htmlFor="logo" className="text-right">
|
name="logo"
|
||||||
Logo URL
|
render={({ field }) => (
|
||||||
</Label>
|
<FormItem className=" gap-4">
|
||||||
<Input
|
<FormLabel className="text-right">Logo URL</FormLabel>
|
||||||
id="logo"
|
<FormControl>
|
||||||
value={logo}
|
<Input
|
||||||
onChange={(e) => setLogo(e.target.value)}
|
placeholder="https://example.com/logo.png"
|
||||||
placeholder="https://example.com/logo.png"
|
{...field}
|
||||||
className="col-span-3"
|
value={field.value || ""}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage className="col-span-3 col-start-2" />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<DialogFooter className="mt-4">
|
||||||
</div>
|
<Button type="submit" isLoading={isLoading}>
|
||||||
<DialogFooter>
|
{organizationId ? "Update organization" : "Create organization"}
|
||||||
<Button type="submit" onClick={handleSubmit} isLoading={isLoading}>
|
</Button>
|
||||||
{organizationId ? "Update organization" : "Create organization"}
|
</DialogFooter>
|
||||||
</Button>
|
</form>
|
||||||
</DialogFooter>
|
</Form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ function SidebarLogo() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Organization Logo and Selector */}
|
{/* Organization Logo and Selector */}
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem className={"w-full"}>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
@@ -562,7 +562,7 @@ function SidebarLogo() {
|
|||||||
"transition-all",
|
"transition-all",
|
||||||
state === "collapsed" ? "size-4" : "size-5",
|
state === "collapsed" ? "size-4" : "size-5",
|
||||||
)}
|
)}
|
||||||
logoUrl={activeOrganization?.logo}
|
logoUrl={activeOrganization?.logo || undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
@@ -9,8 +11,7 @@ export const Logo = ({ className = "size-14", logoUrl }: Props) => {
|
|||||||
<img
|
<img
|
||||||
src={logoUrl}
|
src={logoUrl}
|
||||||
alt="Organization Logo"
|
alt="Organization Logo"
|
||||||
className={className}
|
className={cn(className, "object-contain rounded-sm")}
|
||||||
style={{ objectFit: "contain" }}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user