refactor: refactor server status edit form with react-hook-form

This commit is contained in:
moonrailgun 2024-09-15 23:36:34 +08:00
parent ef30750802
commit 6160d7bcb9

View File

@ -1,26 +1,53 @@
import { Switch, Divider, Form, Input, Typography } from 'antd';
import React from 'react'; import React from 'react';
import { MonitorPicker } from '../MonitorPicker'; import { MonitorPicker } from '../MonitorPicker';
import { domainValidator, urlSlugValidator } from '../../../utils/validator';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { LuMinusCircle, LuPlus } from 'react-icons/lu'; import { LuMinus, LuMinusCircle, LuPlus } from 'react-icons/lu';
import { MarkdownEditor } from '@/components/MarkdownEditor'; import { MarkdownEditor } from '@/components/MarkdownEditor';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useEventWithLoading } from '@/hooks/useEvent';
import { Input as AntdInput, Divider, Typography } from 'antd';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { domainRegex, slugRegex } from '@tianji/shared';
import { useElementSize } from '@/hooks/useResizeObserver';
import { Switch } from '@/components/ui/switch';
const { Text } = Typography; const Text = Typography.Text;
export interface MonitorStatusPageEditFormValues { const editFormSchema = z.object({
title: string; title: z.string(),
slug: string; slug: z.string().regex(slugRegex),
description: string; description: z.string(),
monitorList: PrismaJson.MonitorStatusPageList; domain: z
domain: string; .string()
} .regex(domainRegex, 'Invalid domain')
.or(z.literal(''))
.optional(),
monitorList: z.array(
z.object({
id: z.string(),
showCurrent: z.boolean().default(false).optional(),
})
),
});
export type MonitorStatusPageEditFormValues = z.infer<typeof editFormSchema>;
interface MonitorStatusPageEditFormProps { interface MonitorStatusPageEditFormProps {
isLoading?: boolean; isLoading?: boolean;
initialValues?: Partial<MonitorStatusPageEditFormValues>; initialValues?: Partial<MonitorStatusPageEditFormValues>;
onFinish: (values: MonitorStatusPageEditFormValues) => void; onFinish: (values: MonitorStatusPageEditFormValues) => Promise<void>;
onCancel?: () => void; onCancel?: () => void;
saveButtonLabel?: string; saveButtonLabel?: string;
} }
@ -28,30 +55,70 @@ interface MonitorStatusPageEditFormProps {
export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps> = export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps> =
React.memo((props) => { React.memo((props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { ref, width } = useElementSize();
const form = useForm<MonitorStatusPageEditFormValues>({
resolver: zodResolver(editFormSchema),
defaultValues: props.initialValues ?? {
title: '',
slug: '',
description: '',
domain: '',
monitorList: [],
},
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: 'monitorList',
keyName: 'key',
});
const [handleSubmit, isLoading] = useEventWithLoading(
async (values: MonitorStatusPageEditFormValues) => {
await props.onFinish(values);
form.reset();
}
);
return ( return (
<div> <Form {...form}>
<Form<MonitorStatusPageEditFormValues> <form
layout="vertical" ref={ref}
initialValues={props.initialValues} onSubmit={form.handleSubmit(handleSubmit)}
onFinish={props.onFinish} className="flex flex-col space-y-2"
> >
<Form.Item {/* Title */}
label={t('Title')} <FormField
control={form.control}
name="title" name="title"
rules={[ render={({ field }) => (
{ <FormItem>
required: true, <FormLabel>{t('Title')}</FormLabel>
}, <FormControl>
]} <Input {...field} />
> </FormControl>
<Input /> <FormMessage />
</Form.Item> </FormItem>
)}
/>
<Form.Item {/* Slug */}
label="Slug" <FormField
control={form.control}
name="slug" name="slug"
extra={ render={({ field }) => (
<FormItem>
<FormLabel>{t('Slug')}</FormLabel>
<FormControl>
<AntdInput
{...field}
addonBefore={
width < 280 ? '/status/' : `${window.origin}/status/`
}
/>
</FormControl>
<FormDescription>
<div className="pt-2"> <div className="pt-2">
<div> <div>
{t('Accept characters')}: <Text code>a-z</Text>{' '} {t('Accept characters')}: <Text code>a-z</Text>{' '}
@ -61,110 +128,122 @@ export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps>
{t('No consecutive dashes')} <Text code>--</Text> {t('No consecutive dashes')} <Text code>--</Text>
</div> </div>
</div> </div>
} </FormDescription>
rules={[ <FormMessage />
{ </FormItem>
required: true, )}
}, />
{
validator: urlSlugValidator,
},
]}
>
<Input addonBefore={`${window.origin}/status/`} />
</Form.Item>
<Form.Item label={t('Description')} name="description"> {/* Description */}
<MarkdownEditorFormItem /> <FormField
</Form.Item> control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>{t('Description')}</FormLabel>
<FormControl>
<MarkdownEditorFormItem {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Form.Item {/* Custom Domain */}
label={t('Custom Domain')} <FormField
control={form.control}
name="domain" name="domain"
extra={ render={({ field }) => (
<FormItem>
<FormLabel>{t('Custom Domain')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
<div> <div>
{t( {t(
'You can config your status page in your own domain, for example: status.example.com' 'You can config your status page in your own domain, for example: status.example.com'
)} )}
</div> </div>
} </FormDescription>
rules={[ <FormMessage />
{ </FormItem>
validator: domainValidator, )}
}, />
]}
> {/* MonitorList */}
<Input /> <FormField
</Form.Item> control={form.control}
name="monitorList"
render={() => (
<FormItem>
<FormLabel>{t('Monitor List')}</FormLabel>
{fields.map((field, i) => {
const { onChange: onMonitorChange, ...monitorProps } =
form.register(`monitorList.${i}.id`, {
onChange: (e) => console.log(e.target.value),
});
const { onChange: onShowCurrentChange, ...showCurrentProps } =
form.register(`monitorList.${i}.showCurrent`);
<Form.List name="monitorList">
{(fields, { add, remove }, { errors }) => {
return ( return (
<> <>
<Form.Item label={t('Monitors')}> {i !== 0 && <Divider className="my-0.5" />}
<div className="mb-2 flex flex-col gap-2">
{fields.map((field, index) => (
// monitor item
<>
{index !== 0 && <Divider className="my-0.5" />}
<div key={field.key} className="flex flex-col gap-1"> <div key={field.key} className="mb-2 flex flex-col gap-1">
<Form.Item <Controller
name={[field.name, 'id']} control={form.control}
rules={[ name={`monitorList.${i}.id`}
{ render={({ field }) => <MonitorPicker {...field} />}
required: true, />
message: t('Please select monitor'),
},
]}
noStyle={true}
>
<MonitorPicker />
</Form.Item>
<div className="item-center flex">
<div className="flex flex-1 items-center"> <div className="flex flex-1 items-center">
<Form.Item <Controller
name={[field.name, 'showCurrent']} control={form.control}
valuePropName="checked" name={`monitorList.${i}.showCurrent`}
noStyle={true} render={({ field }) => (
> <Switch
<Switch size="small" /> {...showCurrentProps}
</Form.Item> checked={field.value}
onCheckedChange={field.onChange}
/>
)}
/>
<span className="ml-1 align-middle text-sm"> <span className="ml-1 flex-1 align-middle text-sm">
{t('Show Latest Value')} {t('Show Latest Value')}
</span> </span>
</div>
<LuMinusCircle <LuMinusCircle
className="mt-1.5 cursor-pointer text-lg" className="cursor-pointer text-lg"
onClick={() => remove(field.name)} onClick={() => remove(i)}
/> />
</div> </div>
</div> </div>
</> </>
))} );
</div> })}
<FormMessage />
<Button <Button
variant="dashed" variant="dashed"
onClick={() => add()} onClick={() =>
append({
id: '',
showCurrent: false,
})
}
style={{ width: '60%' }} style={{ width: '60%' }}
Icon={LuPlus} Icon={LuPlus}
> >
{t('Add Monitor')} {t('Add Monitor')}
</Button> </Button>
</FormItem>
)}
/>
<Form.ErrorList errors={errors} /> <div className="!mt-8 flex gap-2">
</Form.Item> <Button type="submit" loading={isLoading}>
</>
);
}}
</Form.List>
<div className="flex gap-4">
<Button type="submit" loading={props.isLoading}>
{props.saveButtonLabel ?? t('Save')} {props.saveButtonLabel ?? t('Save')}
</Button> </Button>
@ -174,8 +253,8 @@ export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps>
</Button> </Button>
)} )}
</div> </div>
</form>
</Form> </Form>
</div>
); );
}); });
MonitorStatusPageEditForm.displayName = 'MonitorStatusPageEditForm'; MonitorStatusPageEditForm.displayName = 'MonitorStatusPageEditForm';