diff --git a/apps/start/src/modals/add-group.tsx b/apps/start/src/modals/add-group.tsx index d6cc0baf..05192da2 100644 --- a/apps/start/src/modals/add-group.tsx +++ b/apps/start/src/modals/add-group.tsx @@ -1,21 +1,22 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { zCreateGroup } from '@openpanel/validation'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { PlusIcon, Trash2Icon } from 'lucide-react'; +import { useFieldArray, useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { z } from 'zod'; +import { popModal } from '.'; +import { ModalContent, ModalHeader } from './Modal/Container'; import { ButtonContainer } from '@/components/button-container'; import { InputWithLabel } from '@/components/forms/input-with-label'; import { Button } from '@/components/ui/button'; import { useAppParams } from '@/hooks/use-app-params'; import { handleError, useTRPC } from '@/integrations/trpc/react'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { PlusIcon, Trash2Icon } from 'lucide-react'; -import { useFieldArray, useForm } from 'react-hook-form'; -import { toast } from 'sonner'; -import { popModal } from '.'; -import { ModalContent, ModalHeader } from './Modal/Container'; -interface IForm { - id: string; - type: string; - name: string; - properties: { key: string; value: string }[]; -} +const zForm = zCreateGroup.omit({ projectId: true, properties: true }).extend({ + properties: z.array(z.object({ key: z.string(), value: z.string() })), +}); +type IForm = z.infer; export default function AddGroup() { const { projectId } = useAppParams(); @@ -23,6 +24,7 @@ export default function AddGroup() { const trpc = useTRPC(); const { register, handleSubmit, control, formState } = useForm({ + resolver: zodResolver(zForm), defaultValues: { id: '', type: '', @@ -45,7 +47,7 @@ export default function AddGroup() { popModal(); }, onError: handleError, - }), + }) ); return ( @@ -57,30 +59,46 @@ export default function AddGroup() { const props = Object.fromEntries( properties .filter((p) => p.key.trim() !== '') - .map((p) => [p.key.trim(), String(p.value)]), + .map((p) => [p.key.trim(), String(p.value)]) ); mutation.mutate({ projectId, ...values, properties: props }); })} > - - - + + +
- Properties + Properties
{fields.map((field, index) => ( -
+
@@ -105,10 +123,13 @@ export default function AddGroup() {
- - diff --git a/apps/start/src/modals/edit-group.tsx b/apps/start/src/modals/edit-group.tsx index 7940fad7..851a3a3d 100644 --- a/apps/start/src/modals/edit-group.tsx +++ b/apps/start/src/modals/edit-group.tsx @@ -1,35 +1,51 @@ -import { ButtonContainer } from '@/components/button-container'; -import { InputWithLabel } from '@/components/forms/input-with-label'; -import { Button } from '@/components/ui/button'; -import { handleError, useTRPC } from '@/integrations/trpc/react'; +import { zodResolver } from '@hookform/resolvers/zod'; import type { IServiceGroup } from '@openpanel/db'; +import { zUpdateGroup } from '@openpanel/validation'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { PlusIcon, Trash2Icon } from 'lucide-react'; import { useFieldArray, useForm } from 'react-hook-form'; import { toast } from 'sonner'; +import { z } from 'zod'; import { popModal } from '.'; import { ModalContent, ModalHeader } from './Modal/Container'; +import { ButtonContainer } from '@/components/button-container'; +import { InputWithLabel } from '@/components/forms/input-with-label'; +import { Button } from '@/components/ui/button'; +import { handleError, useTRPC } from '@/integrations/trpc/react'; -interface IForm { - type: string; - name: string; - properties: { key: string; value: string }[]; -} +const zForm = zUpdateGroup + .omit({ id: true, projectId: true, properties: true }) + .extend({ + properties: z.array(z.object({ key: z.string(), value: z.string() })), + }); +type IForm = z.infer; -type EditGroupProps = Pick; +type EditGroupProps = Pick< + IServiceGroup, + 'id' | 'projectId' | 'name' | 'type' | 'properties' +>; -export default function EditGroup({ id, projectId, name, type, properties }: EditGroupProps) { +export default function EditGroup({ + id, + projectId, + name, + type, + properties, +}: EditGroupProps) { const queryClient = useQueryClient(); const trpc = useTRPC(); const { register, handleSubmit, control, formState } = useForm({ + resolver: zodResolver(zForm), defaultValues: { type, name, - properties: Object.entries(properties as Record).map(([key, value]) => ({ - key, - value: String(value), - })), + properties: Object.entries(properties as Record).map( + ([key, value]) => ({ + key, + value: String(value), + }) + ), }, }); @@ -48,7 +64,7 @@ export default function EditGroup({ id, projectId, name, type, properties }: Edi popModal(); }, onError: handleError, - }), + }) ); return ( @@ -60,29 +76,37 @@ export default function EditGroup({ id, projectId, name, type, properties }: Edi const props = Object.fromEntries( formProps .filter((p) => p.key.trim() !== '') - .map((p) => [p.key.trim(), String(p.value)]), + .map((p) => [p.key.trim(), String(p.value)]) ); mutation.mutate({ id, projectId, ...values, properties: props }); })} > - - + +
- Properties + Properties
{fields.map((field, index) => ( -
+
@@ -107,10 +131,13 @@ export default function EditGroup({ id, projectId, name, type, properties }: Edi
- - diff --git a/packages/trpc/src/routers/group.ts b/packages/trpc/src/routers/group.ts index 695bd474..adf43605 100644 --- a/packages/trpc/src/routers/group.ts +++ b/packages/trpc/src/routers/group.ts @@ -14,6 +14,7 @@ import { toNullIfDefaultMinDate, updateGroup, } from '@openpanel/db'; +import { zCreateGroup, zUpdateGroup } from '@openpanel/validation'; import sqlstring from 'sqlstring'; import { z } from 'zod'; import { createTRPCRouter, protectedProcedure } from '../trpc'; @@ -55,29 +56,13 @@ export const groupRouter = createTRPCRouter({ }), create: protectedProcedure - .input( - z.object({ - id: z.string().min(1), - projectId: z.string(), - type: z.string().min(1), - name: z.string().min(1), - properties: z.record(z.string()).default({}), - }) - ) + .input(zCreateGroup) .mutation(({ input }) => { return createGroup(input); }), update: protectedProcedure - .input( - z.object({ - id: z.string().min(1), - projectId: z.string(), - type: z.string().min(1).optional(), - name: z.string().min(1).optional(), - properties: z.record(z.string()).optional(), - }) - ) + .input(zUpdateGroup) .mutation(({ input: { id, projectId, ...data } }) => { return updateGroup(id, projectId, data); }), diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index b7d11000..2b27593e 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -540,6 +540,32 @@ export const zCheckout = z.object({ }); export type ICheckout = z.infer; +export const zGroupId = z + .string() + .min(1) + .regex( + /^[a-z0-9_-]+$/, + 'ID must only contain lowercase letters, digits, hyphens, or underscores', + ); + +export const zCreateGroup = z.object({ + id: zGroupId, + projectId: z.string(), + type: z.string().min(1), + name: z.string().min(1), + properties: z.record(z.string()).default({}), +}); +export type ICreateGroup = z.infer; + +export const zUpdateGroup = z.object({ + id: z.string().min(1), + projectId: z.string(), + type: z.string().min(1).optional(), + name: z.string().min(1).optional(), + properties: z.record(z.string()).optional(), +}); +export type IUpdateGroup = z.infer; + export const zEditOrganization = z.object({ id: z.string().min(2), name: z.string().min(2),