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,143 +55,195 @@ 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 }) => (
<div className="pt-2"> <FormItem>
<div> <FormLabel>{t('Slug')}</FormLabel>
{t('Accept characters')}: <Text code>a-z</Text>{' '} <FormControl>
<Text code>0-9</Text> <Text code>-</Text> <AntdInput
</div> {...field}
<div> addonBefore={
{t('No consecutive dashes')} <Text code>--</Text> width < 280 ? '/status/' : `${window.origin}/status/`
</div> }
</div> />
} </FormControl>
rules={[ <FormDescription>
{ <div className="pt-2">
required: true, <div>
}, {t('Accept characters')}: <Text code>a-z</Text>{' '}
{ <Text code>0-9</Text> <Text code>-</Text>
validator: urlSlugValidator,
},
]}
>
<Input addonBefore={`${window.origin}/status/`} />
</Form.Item>
<Form.Item label={t('Description')} name="description">
<MarkdownEditorFormItem />
</Form.Item>
<Form.Item
label={t('Custom Domain')}
name="domain"
extra={
<div>
{t(
'You can config your status page in your own domain, for example: status.example.com'
)}
</div>
}
rules={[
{
validator: domainValidator,
},
]}
>
<Input />
</Form.Item>
<Form.List name="monitorList">
{(fields, { add, remove }, { errors }) => {
return (
<>
<Form.Item label={t('Monitors')}>
<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">
<Form.Item
name={[field.name, 'id']}
rules={[
{
required: true,
message: t('Please select monitor'),
},
]}
noStyle={true}
>
<MonitorPicker />
</Form.Item>
<div className="item-center flex">
<div className="flex flex-1 items-center">
<Form.Item
name={[field.name, 'showCurrent']}
valuePropName="checked"
noStyle={true}
>
<Switch size="small" />
</Form.Item>
<span className="ml-1 align-middle text-sm">
{t('Show Latest Value')}
</span>
</div>
<LuMinusCircle
className="mt-1.5 cursor-pointer text-lg"
onClick={() => remove(field.name)}
/>
</div>
</div>
</>
))}
</div> </div>
<div>
{t('No consecutive dashes')} <Text code>--</Text>
</div>
</div>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button {/* Description */}
variant="dashed" <FormField
onClick={() => add()} control={form.control}
style={{ width: '60%' }} name="description"
Icon={LuPlus} render={({ field }) => (
> <FormItem>
{t('Add Monitor')} <FormLabel>{t('Description')}</FormLabel>
</Button> <FormControl>
<MarkdownEditorFormItem {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Form.ErrorList errors={errors} /> {/* Custom Domain */}
</Form.Item> <FormField
</> control={form.control}
); name="domain"
}} render={({ field }) => (
</Form.List> <FormItem>
<FormLabel>{t('Custom Domain')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
<div>
{t(
'You can config your status page in your own domain, for example: status.example.com'
)}
</div>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex gap-4"> {/* MonitorList */}
<Button type="submit" loading={props.isLoading}> <FormField
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`);
return (
<>
{i !== 0 && <Divider className="my-0.5" />}
<div key={field.key} className="mb-2 flex flex-col gap-1">
<Controller
control={form.control}
name={`monitorList.${i}.id`}
render={({ field }) => <MonitorPicker {...field} />}
/>
<div className="flex flex-1 items-center">
<Controller
control={form.control}
name={`monitorList.${i}.showCurrent`}
render={({ field }) => (
<Switch
{...showCurrentProps}
checked={field.value}
onCheckedChange={field.onChange}
/>
)}
/>
<span className="ml-1 flex-1 align-middle text-sm">
{t('Show Latest Value')}
</span>
<LuMinusCircle
className="cursor-pointer text-lg"
onClick={() => remove(i)}
/>
</div>
</div>
</>
);
})}
<FormMessage />
<Button
variant="dashed"
onClick={() =>
append({
id: '',
showCurrent: false,
})
}
style={{ width: '60%' }}
Icon={LuPlus}
>
{t('Add Monitor')}
</Button>
</FormItem>
)}
/>
<div className="!mt-8 flex gap-2">
<Button type="submit" loading={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>
</div> </Form>
); );
}); });
MonitorStatusPageEditForm.displayName = 'MonitorStatusPageEditForm'; MonitorStatusPageEditForm.displayName = 'MonitorStatusPageEditForm';