refactor: refactor server status edit form with react-hook-form
This commit is contained in:
parent
ef30750802
commit
6160d7bcb9
@ -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';
|
||||||
|
Loading…
Reference in New Issue
Block a user