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 { 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<typeof zForm>;
|
||||
|
||||
export default function AddGroup() {
|
||||
const { projectId } = useAppParams();
|
||||
@@ -23,6 +24,7 @@ export default function AddGroup() {
|
||||
const trpc = useTRPC();
|
||||
|
||||
const { register, handleSubmit, control, formState } = useForm<IForm>({
|
||||
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 });
|
||||
})}
|
||||
>
|
||||
<InputWithLabel label="ID" placeholder="acme-corp" {...register('id', { required: true })} autoFocus />
|
||||
<InputWithLabel label="Name" placeholder="Acme Corp" {...register('name', { required: true })} />
|
||||
<InputWithLabel label="Type" placeholder="company" {...register('type', { required: true })} />
|
||||
<InputWithLabel
|
||||
label="ID"
|
||||
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 items-center justify-between">
|
||||
<span className="text-sm font-medium">Properties</span>
|
||||
<span className="font-medium text-sm">Properties</span>
|
||||
<Button
|
||||
onClick={() => append({ key: '', value: '' })}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => append({ key: '', value: '' })}
|
||||
>
|
||||
<PlusIcon className="mr-1 size-3" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex gap-2">
|
||||
<div className="flex gap-2" key={field.id}>
|
||||
<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"
|
||||
placeholder="key"
|
||||
@@ -92,11 +110,11 @@ export default function AddGroup() {
|
||||
{...register(`properties.${index}.value`)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0"
|
||||
onClick={() => remove(index)}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<Trash2Icon className="size-4" />
|
||||
</Button>
|
||||
@@ -105,10 +123,13 @@ export default function AddGroup() {
|
||||
</div>
|
||||
|
||||
<ButtonContainer>
|
||||
<Button type="button" variant="outline" onClick={() => popModal()}>
|
||||
<Button onClick={() => popModal()} type="button" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={!formState.isDirty || mutation.isPending}>
|
||||
<Button
|
||||
disabled={!formState.isDirty || mutation.isPending}
|
||||
type="submit"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
|
||||
@@ -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<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 trpc = useTRPC();
|
||||
|
||||
const { register, handleSubmit, control, formState } = useForm<IForm>({
|
||||
resolver: zodResolver(zForm),
|
||||
defaultValues: {
|
||||
type,
|
||||
name,
|
||||
properties: Object.entries(properties as Record<string, string>).map(([key, value]) => ({
|
||||
key,
|
||||
value: String(value),
|
||||
})),
|
||||
properties: Object.entries(properties as Record<string, string>).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 });
|
||||
})}
|
||||
>
|
||||
<InputWithLabel label="Name" {...register('name', { required: true })} />
|
||||
<InputWithLabel label="Type" {...register('type', { required: true })} />
|
||||
<InputWithLabel
|
||||
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 items-center justify-between">
|
||||
<span className="text-sm font-medium">Properties</span>
|
||||
<span className="font-medium text-sm">Properties</span>
|
||||
<Button
|
||||
onClick={() => append({ key: '', value: '' })}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => append({ key: '', value: '' })}
|
||||
>
|
||||
<PlusIcon className="mr-1 size-3" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex gap-2">
|
||||
<div className="flex gap-2" key={field.id}>
|
||||
<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"
|
||||
placeholder="key"
|
||||
@@ -94,11 +118,11 @@ export default function EditGroup({ id, projectId, name, type, properties }: Edi
|
||||
{...register(`properties.${index}.value`)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0"
|
||||
onClick={() => remove(index)}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<Trash2Icon className="size-4" />
|
||||
</Button>
|
||||
@@ -107,10 +131,13 @@ export default function EditGroup({ id, projectId, name, type, properties }: Edi
|
||||
</div>
|
||||
|
||||
<ButtonContainer>
|
||||
<Button type="button" variant="outline" onClick={() => popModal()}>
|
||||
<Button onClick={() => popModal()} type="button" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={!formState.isDirty || mutation.isPending}>
|
||||
<Button
|
||||
disabled={!formState.isDirty || mutation.isPending}
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
|
||||
@@ -540,6 +540,32 @@ export const zCheckout = z.object({
|
||||
});
|
||||
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({
|
||||
id: z.string().min(2),
|
||||
name: z.string().min(2),
|
||||
|
||||
Reference in New Issue
Block a user