group validation
This commit is contained in:
@@ -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 { ButtonContainer } from '@/components/button-container';
|
||||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
import { InputWithLabel } from '@/components/forms/input-with-label';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAppParams } from '@/hooks/use-app-params';
|
import { useAppParams } from '@/hooks/use-app-params';
|
||||||
import { handleError, useTRPC } from '@/integrations/trpc/react';
|
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 {
|
const zForm = zCreateGroup.omit({ projectId: true, properties: true }).extend({
|
||||||
id: string;
|
properties: z.array(z.object({ key: z.string(), value: z.string() })),
|
||||||
type: string;
|
});
|
||||||
name: string;
|
type IForm = z.infer<typeof zForm>;
|
||||||
properties: { key: string; value: string }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AddGroup() {
|
export default function AddGroup() {
|
||||||
const { projectId } = useAppParams();
|
const { projectId } = useAppParams();
|
||||||
@@ -23,6 +24,7 @@ export default function AddGroup() {
|
|||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
|
|
||||||
const { register, handleSubmit, control, formState } = useForm<IForm>({
|
const { register, handleSubmit, control, formState } = useForm<IForm>({
|
||||||
|
resolver: zodResolver(zForm),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: '',
|
id: '',
|
||||||
type: '',
|
type: '',
|
||||||
@@ -45,7 +47,7 @@ export default function AddGroup() {
|
|||||||
popModal();
|
popModal();
|
||||||
},
|
},
|
||||||
onError: handleError,
|
onError: handleError,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,30 +59,46 @@ export default function AddGroup() {
|
|||||||
const props = Object.fromEntries(
|
const props = Object.fromEntries(
|
||||||
properties
|
properties
|
||||||
.filter((p) => p.key.trim() !== '')
|
.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 });
|
mutation.mutate({ projectId, ...values, properties: props });
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<InputWithLabel label="ID" placeholder="acme-corp" {...register('id', { required: true })} autoFocus />
|
<InputWithLabel
|
||||||
<InputWithLabel label="Name" placeholder="Acme Corp" {...register('name', { required: true })} />
|
label="ID"
|
||||||
<InputWithLabel label="Type" placeholder="company" {...register('type', { required: true })} />
|
placeholder="acme-corp"
|
||||||
|
{...register('id')}
|
||||||
|
autoFocus
|
||||||
|
error={formState.errors.id?.message}
|
||||||
|
/>
|
||||||
|
<InputWithLabel
|
||||||
|
label="Name"
|
||||||
|
placeholder="Acme Corp"
|
||||||
|
{...register('name')}
|
||||||
|
error={formState.errors.name?.message}
|
||||||
|
/>
|
||||||
|
<InputWithLabel
|
||||||
|
label="Type"
|
||||||
|
placeholder="company"
|
||||||
|
{...register('type')}
|
||||||
|
error={formState.errors.type?.message}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-medium">Properties</span>
|
<span className="font-medium text-sm">Properties</span>
|
||||||
<Button
|
<Button
|
||||||
|
onClick={() => append({ key: '', value: '' })}
|
||||||
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
|
||||||
onClick={() => append({ key: '', value: '' })}
|
|
||||||
>
|
>
|
||||||
<PlusIcon className="mr-1 size-3" />
|
<PlusIcon className="mr-1 size-3" />
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<div key={field.id} className="flex gap-2">
|
<div className="flex gap-2" key={field.id}>
|
||||||
<input
|
<input
|
||||||
className="h-9 flex-1 rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
className="h-9 flex-1 rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||||
placeholder="key"
|
placeholder="key"
|
||||||
@@ -92,11 +110,11 @@ export default function AddGroup() {
|
|||||||
{...register(`properties.${index}.value`)}
|
{...register(`properties.${index}.value`)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
onClick={() => remove(index)}
|
onClick={() => remove(index)}
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<Trash2Icon className="size-4" />
|
<Trash2Icon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -105,10 +123,13 @@ export default function AddGroup() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<Button type="button" variant="outline" onClick={() => popModal()}>
|
<Button onClick={() => popModal()} type="button" variant="outline">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={!formState.isDirty || mutation.isPending}>
|
<Button
|
||||||
|
disabled={!formState.isDirty || mutation.isPending}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
|
|||||||
@@ -1,35 +1,51 @@
|
|||||||
import { ButtonContainer } from '@/components/button-container';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { InputWithLabel } from '@/components/forms/input-with-label';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { handleError, useTRPC } from '@/integrations/trpc/react';
|
|
||||||
import type { IServiceGroup } from '@openpanel/db';
|
import type { IServiceGroup } from '@openpanel/db';
|
||||||
|
import { zUpdateGroup } from '@openpanel/validation';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { PlusIcon, Trash2Icon } from 'lucide-react';
|
import { PlusIcon, Trash2Icon } from 'lucide-react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { z } from 'zod';
|
||||||
import { popModal } from '.';
|
import { popModal } from '.';
|
||||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
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 {
|
const zForm = zUpdateGroup
|
||||||
type: string;
|
.omit({ id: true, projectId: true, properties: true })
|
||||||
name: string;
|
.extend({
|
||||||
properties: { key: string; value: string }[];
|
properties: z.array(z.object({ key: z.string(), value: z.string() })),
|
||||||
}
|
});
|
||||||
|
type IForm = z.infer<typeof zForm>;
|
||||||
|
|
||||||
type EditGroupProps = Pick<IServiceGroup, 'id' | 'projectId' | 'name' | 'type' | 'properties'>;
|
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 queryClient = useQueryClient();
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC();
|
||||||
|
|
||||||
const { register, handleSubmit, control, formState } = useForm<IForm>({
|
const { register, handleSubmit, control, formState } = useForm<IForm>({
|
||||||
|
resolver: zodResolver(zForm),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type,
|
type,
|
||||||
name,
|
name,
|
||||||
properties: Object.entries(properties as Record<string, string>).map(([key, value]) => ({
|
properties: Object.entries(properties as Record<string, string>).map(
|
||||||
key,
|
([key, value]) => ({
|
||||||
value: String(value),
|
key,
|
||||||
})),
|
value: String(value),
|
||||||
|
})
|
||||||
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,7 +64,7 @@ export default function EditGroup({ id, projectId, name, type, properties }: Edi
|
|||||||
popModal();
|
popModal();
|
||||||
},
|
},
|
||||||
onError: handleError,
|
onError: handleError,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -60,29 +76,37 @@ export default function EditGroup({ id, projectId, name, type, properties }: Edi
|
|||||||
const props = Object.fromEntries(
|
const props = Object.fromEntries(
|
||||||
formProps
|
formProps
|
||||||
.filter((p) => p.key.trim() !== '')
|
.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 });
|
mutation.mutate({ id, projectId, ...values, properties: props });
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<InputWithLabel label="Name" {...register('name', { required: true })} />
|
<InputWithLabel
|
||||||
<InputWithLabel label="Type" {...register('type', { required: true })} />
|
label="Name"
|
||||||
|
{...register('name')}
|
||||||
|
error={formState.errors.name?.message}
|
||||||
|
/>
|
||||||
|
<InputWithLabel
|
||||||
|
label="Type"
|
||||||
|
{...register('type')}
|
||||||
|
error={formState.errors.type?.message}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-medium">Properties</span>
|
<span className="font-medium text-sm">Properties</span>
|
||||||
<Button
|
<Button
|
||||||
|
onClick={() => append({ key: '', value: '' })}
|
||||||
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
|
||||||
onClick={() => append({ key: '', value: '' })}
|
|
||||||
>
|
>
|
||||||
<PlusIcon className="mr-1 size-3" />
|
<PlusIcon className="mr-1 size-3" />
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<div key={field.id} className="flex gap-2">
|
<div className="flex gap-2" key={field.id}>
|
||||||
<input
|
<input
|
||||||
className="h-9 flex-1 rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
className="h-9 flex-1 rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||||
placeholder="key"
|
placeholder="key"
|
||||||
@@ -94,11 +118,11 @@ export default function EditGroup({ id, projectId, name, type, properties }: Edi
|
|||||||
{...register(`properties.${index}.value`)}
|
{...register(`properties.${index}.value`)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
onClick={() => remove(index)}
|
onClick={() => remove(index)}
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<Trash2Icon className="size-4" />
|
<Trash2Icon className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -107,10 +131,13 @@ export default function EditGroup({ id, projectId, name, type, properties }: Edi
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<Button type="button" variant="outline" onClick={() => popModal()}>
|
<Button onClick={() => popModal()} type="button" variant="outline">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={!formState.isDirty || mutation.isPending}>
|
<Button
|
||||||
|
disabled={!formState.isDirty || mutation.isPending}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
toNullIfDefaultMinDate,
|
toNullIfDefaultMinDate,
|
||||||
updateGroup,
|
updateGroup,
|
||||||
} from '@openpanel/db';
|
} from '@openpanel/db';
|
||||||
|
import { zCreateGroup, zUpdateGroup } from '@openpanel/validation';
|
||||||
import sqlstring from 'sqlstring';
|
import sqlstring from 'sqlstring';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { createTRPCRouter, protectedProcedure } from '../trpc';
|
import { createTRPCRouter, protectedProcedure } from '../trpc';
|
||||||
@@ -55,29 +56,13 @@ export const groupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(
|
.input(zCreateGroup)
|
||||||
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({}),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(({ input }) => {
|
.mutation(({ input }) => {
|
||||||
return createGroup(input);
|
return createGroup(input);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(
|
.input(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(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(({ input: { id, projectId, ...data } }) => {
|
.mutation(({ input: { id, projectId, ...data } }) => {
|
||||||
return updateGroup(id, projectId, data);
|
return updateGroup(id, projectId, data);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -540,6 +540,32 @@ export const zCheckout = z.object({
|
|||||||
});
|
});
|
||||||
export type ICheckout = z.infer<typeof zCheckout>;
|
export type ICheckout = z.infer<typeof zCheckout>;
|
||||||
|
|
||||||
|
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<typeof zCreateGroup>;
|
||||||
|
|
||||||
|
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<typeof zUpdateGroup>;
|
||||||
|
|
||||||
export const zEditOrganization = z.object({
|
export const zEditOrganization = z.object({
|
||||||
id: z.string().min(2),
|
id: z.string().min(2),
|
||||||
name: z.string().min(2),
|
name: z.string().min(2),
|
||||||
|
|||||||
Reference in New Issue
Block a user