feat(ui): new SettingsNotification using antd
This commit is contained in:
97
ui/src/components/notification/NotifyChannelEditForm.tsx
Normal file
97
ui/src/components/notification/NotifyChannelEditForm.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
|
||||
import { useCreation, useDeepCompareEffect } from "ahooks";
|
||||
import { Form } from "antd";
|
||||
|
||||
import { type NotifyChannelsSettingsContent } from "@/domain/settings";
|
||||
import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields";
|
||||
import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields";
|
||||
import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields";
|
||||
import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields";
|
||||
import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields";
|
||||
import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields";
|
||||
import NotifyChannelEditFormWebhookFields from "./NotifyChannelEditFormWebhookFields";
|
||||
|
||||
type NotifyChannelEditFormModelType = NotifyChannelsSettingsContent[keyof NotifyChannelsSettingsContent];
|
||||
|
||||
export type NotifyChannelEditFormProps = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
channel: keyof NotifyChannelsSettingsContent;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
model?: NotifyChannelEditFormModelType;
|
||||
onModelChange?: (model: NotifyChannelEditFormModelType) => void;
|
||||
};
|
||||
|
||||
export type NotifyChannelEditFormInstance = {
|
||||
getFieldsValue: () => NotifyChannelEditFormModelType;
|
||||
resetFields: () => void;
|
||||
validateFields: () => Promise<NotifyChannelEditFormModelType>;
|
||||
};
|
||||
|
||||
const NotifyChannelEditForm = forwardRef<NotifyChannelEditFormInstance, NotifyChannelEditFormProps>(
|
||||
({ className, style, channel, disabled, loading, model, onModelChange }, ref) => {
|
||||
const [form] = Form.useForm();
|
||||
const formName = useCreation(() => `notifyChannelEditForm_${Math.random().toString(36).substring(2, 10)}${new Date().getTime()}`, []);
|
||||
const formFieldsComponent = useMemo(() => {
|
||||
/*
|
||||
注意:如果追加新的子组件,请保持以 ASCII 排序。
|
||||
NOTICE: If you add new child component, please keep ASCII order.
|
||||
*/
|
||||
switch (channel) {
|
||||
case "bark":
|
||||
return <NotifyChannelEditFormBarkFields />;
|
||||
case "dingtalk":
|
||||
return <NotifyChannelEditFormDingTalkFields />;
|
||||
case "email":
|
||||
return <NotifyChannelEditFormEmailFields />;
|
||||
case "lark":
|
||||
return <NotifyChannelEditFormLarkFields />;
|
||||
case "serverchan":
|
||||
return <NotifyChannelEditFormServerChanFields />;
|
||||
case "telegram":
|
||||
return <NotifyChannelEditFormTelegramFields />;
|
||||
case "webhook":
|
||||
return <NotifyChannelEditFormWebhookFields />;
|
||||
}
|
||||
}, [channel]);
|
||||
|
||||
const [initialValues, setInitialValues] = useState(model);
|
||||
useDeepCompareEffect(() => {
|
||||
setInitialValues(model);
|
||||
}, [model]);
|
||||
|
||||
const handleFormChange = (_: unknown, fields: NotifyChannelEditFormModelType) => {
|
||||
onModelChange?.(fields);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getFieldsValue: () => {
|
||||
return form.getFieldsValue(true);
|
||||
},
|
||||
resetFields: () => {
|
||||
return form.resetFields();
|
||||
},
|
||||
validateFields: () => {
|
||||
return form.validateFields();
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<Form
|
||||
className={className}
|
||||
style={style}
|
||||
form={form}
|
||||
disabled={loading || disabled}
|
||||
initialValues={initialValues}
|
||||
layout="vertical"
|
||||
name={formName}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
{formFieldsComponent}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default NotifyChannelEditForm;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const NotifyChannelEditFormBarkFields = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
serverUrl: z
|
||||
.string({ message: t("settings.notification.channel.form.bark_server_url.placeholder") })
|
||||
.url({ message: t("common.errmsg.url_invalid") })
|
||||
.nullish(),
|
||||
deviceKey: z
|
||||
.string({ message: t("settings.notification.channel.form.bark_device_key.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.bark_device_key.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 })),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="serverUrl"
|
||||
label={t("settings.notification.channel.form.bark_server_url.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.bark_server_url.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("settings.notification.channel.form.bark_server_url.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="deviceKey"
|
||||
label={t("settings.notification.channel.form.bark_device_key.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.bark_device_key.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("settings.notification.channel.form.bark_device_key.placeholder")} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifyChannelEditFormBarkFields;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const NotifyChannelEditFormDingTalkFields = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
accessToken: z
|
||||
.string({ message: t("settings.notification.channel.form.dingtalk_access_token.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.dingtalk_access_token.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 })),
|
||||
secret: z
|
||||
.string({ message: t("settings.notification.channel.form.dingtalk_secret.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.dingtalk_secret.placeholder"))
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 })),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="accessToken"
|
||||
label={t("settings.notification.channel.form.dingtalk_access_token.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.dingtalk_access_token.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("settings.notification.channel.form.dingtalk_access_token.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="secret"
|
||||
label={t("settings.notification.channel.form.dingtalk_secret.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.dingtalk_secret.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("settings.notification.channel.form.dingtalk_secret.placeholder")} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifyChannelEditFormDingTalkFields;
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Input, InputNumber, Switch } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const NotifyChannelEditFormEmailFields = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
smtpHost: z
|
||||
.string({ message: t("settings.notification.channel.form.email_smtp_host.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.email_smtp_host.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 })),
|
||||
smtpPort: z
|
||||
.number({ message: t("settings.notification.channel.form.email_smtp_port.placeholder") })
|
||||
.int()
|
||||
.gte(1, t("common.errmsg.port_invalid"))
|
||||
.lte(65535, t("common.errmsg.port_invalid"))
|
||||
.transform((v) => +v),
|
||||
smtpTLS: z.boolean().nullish(),
|
||||
username: z
|
||||
.string({ message: t("settings.notification.channel.form.email_username.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.email_username.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 })),
|
||||
password: z
|
||||
.string({ message: t("settings.notification.channel.form.email_password.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.email_password.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 })),
|
||||
senderAddress: z
|
||||
.string({ message: t("settings.notification.channel.form.email_sender_address.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.email_sender_address.placeholder"))
|
||||
.email({ message: t("common.errmsg.email_invalid") }),
|
||||
receiverAddress: z
|
||||
.string({ message: t("settings.notification.channel.form.email_receiver_address.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.email_receiver_address.placeholder"))
|
||||
.email({ message: t("common.errmsg.email_invalid") }),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
const handleTLSSwitchChange = (checked: boolean) => {
|
||||
const oldPort = form.getFieldValue("smtpPort");
|
||||
const newPort = checked && (oldPort == null || oldPort === 25) ? 465 : !checked && (oldPort == null || oldPort === 465) ? 25 : oldPort;
|
||||
if (newPort !== oldPort) {
|
||||
form.setFieldValue("smtpPort", newPort);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex space-x-2">
|
||||
<div className="w-2/5">
|
||||
<Form.Item name="smtpHost" label={t("settings.notification.channel.form.email_smtp_host.label")} rules={[formRule]}>
|
||||
<Input placeholder={t("settings.notification.channel.form.email_smtp_host.placeholder")} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className="w-2/5">
|
||||
<Form.Item name="smtpPort" label={t("settings.notification.channel.form.email_smtp_port.label")} rules={[formRule]} initialValue={465}>
|
||||
<InputNumber className="w-full" placeholder={t("settings.notification.channel.form.email_smtp_port.placeholder")} min={1} max={65535} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className="w-1/5">
|
||||
<Form.Item name="smtpTLS" label={t("settings.notification.channel.form.email_smtp_tls.label")} rules={[formRule]} initialValue={true}>
|
||||
<Switch onChange={handleTLSSwitchChange} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<div className="w-1/2">
|
||||
<Form.Item name="username" label={t("settings.notification.channel.form.email_username.label")} rules={[formRule]}>
|
||||
<Input placeholder={t("settings.notification.channel.form.email_username.placeholder")} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className="w-1/2">
|
||||
<Form.Item name="password" label={t("settings.notification.channel.form.email_password.label")} rules={[formRule]}>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("settings.notification.channel.form.email_password.placeholder")} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.Item name="senderAddress" label={t("settings.notification.channel.form.email_sender_address.label")} rules={[formRule]}>
|
||||
<Input type="email" placeholder={t("settings.notification.channel.form.email_sender_address.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="receiverAddress" label={t("settings.notification.channel.form.email_receiver_address.label")} rules={[formRule]}>
|
||||
<Input type="email" placeholder={t("settings.notification.channel.form.email_receiver_address.placeholder")} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifyChannelEditFormEmailFields;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const NotifyChannelEditFormLarkFields = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
webhookUrl: z
|
||||
.string({ message: t("settings.notification.channel.form.lark_webhook_url.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.lark_webhook_url.placeholder"))
|
||||
.url({ message: t("common.errmsg.url_invalid") }),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="webhookUrl"
|
||||
label={t("settings.notification.channel.form.lark_webhook_url.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.lark_webhook_url.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("settings.notification.channel.form.lark_webhook_url.placeholder")} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifyChannelEditFormLarkFields;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const NotifyChannelEditFormServerChanFields = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
url: z
|
||||
.string({ message: t("settings.notification.channel.form.serverchan_url.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.serverchan_url.placeholder"))
|
||||
.url({ message: t("common.errmsg.url_invalid") }),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="url"
|
||||
label={t("settings.notification.channel.form.serverchan_url.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.serverchan_url.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("settings.notification.channel.form.serverchan_url.placeholder")} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifyChannelEditFormServerChanFields;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const NotifyChannelEditFormTelegramFields = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
apiToken: z
|
||||
.string({ message: t("settings.notification.channel.form.telegram_api_token.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.telegram_api_token.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 })),
|
||||
chatId: z
|
||||
.string({ message: t("settings.notification.channel.form.telegram_chat_id.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.telegram_chat_id.placeholder"))
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 })),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="apiToken"
|
||||
label={t("settings.notification.channel.form.telegram_api_token.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.telegram_api_token.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("settings.notification.channel.form.telegram_api_token.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="chatId"
|
||||
label={t("settings.notification.channel.form.telegram_chat_id.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.telegram_chat_id.tooltip") }}></span>}
|
||||
>
|
||||
<Input type="number" placeholder={t("settings.notification.channel.form.telegram_chat_id.placeholder")} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifyChannelEditFormTelegramFields;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const NotifyChannelEditFormWebhookFields = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
url: z
|
||||
.string({ message: t("settings.notification.channel.form.webhook_url.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.webhook_url.placeholder"))
|
||||
.url({ message: t("common.errmsg.url_invalid") }),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Item name="url" label={t("settings.notification.channel.form.webhook_url.label")} rules={[formRule]}>
|
||||
<Input placeholder={t("settings.notification.channel.form.webhook_url.placeholder")} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifyChannelEditFormWebhookFields;
|
||||
110
ui/src/components/notification/NotifyChannels.tsx
Normal file
110
ui/src/components/notification/NotifyChannels.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDeepCompareMemo } from "@ant-design/pro-components";
|
||||
import { Button, Collapse, message, notification, Skeleton, Space, Switch, Tooltip, type CollapseProps } from "antd";
|
||||
|
||||
import NotifyChannelEditForm, { type NotifyChannelEditFormInstance } from "./NotifyChannelEditForm";
|
||||
import NotifyTestButton from "./NotifyTestButton";
|
||||
import { notifyChannelsMap } from "@/domain/settings";
|
||||
import { useNotifyChannelStore } from "@/stores/notify";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
type NotifyChannelsSemanticDOM = "collapse" | "form";
|
||||
|
||||
export type NotifyChannelsProps = {
|
||||
className?: string;
|
||||
classNames?: Partial<Record<NotifyChannelsSemanticDOM, string>>;
|
||||
style?: React.CSSProperties;
|
||||
styles?: Partial<Record<NotifyChannelsSemanticDOM, React.CSSProperties>>;
|
||||
};
|
||||
|
||||
const NotifyChannels = ({ className, classNames, style, styles }: NotifyChannelsProps) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const [messageApi, MessageContextHolder] = message.useMessage();
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
|
||||
const { initialized, channels, setChannel, fetchChannels } = useNotifyChannelStore();
|
||||
useEffect(() => {
|
||||
fetchChannels();
|
||||
}, [fetchChannels]);
|
||||
|
||||
const channelFormRefs = useRef<Array<NotifyChannelEditFormInstance | null>>([]);
|
||||
const channelCollapseItems: CollapseProps["items"] = useDeepCompareMemo(
|
||||
() =>
|
||||
Array.from(notifyChannelsMap.values()).map((channel, index) => {
|
||||
return {
|
||||
key: `channel-${channel.type}`,
|
||||
label: <>{t(channel.name)}</>,
|
||||
children: (
|
||||
<div className={classNames?.form} style={styles?.form}>
|
||||
<NotifyChannelEditForm ref={(ref) => (channelFormRefs.current[index] = ref)} model={channels[channel.type]} channel={channel.type} />
|
||||
|
||||
<Space>
|
||||
<Button type="primary" onClick={() => handleClickSubmit(channel.type, index)}>
|
||||
{t("common.button.save")}
|
||||
</Button>
|
||||
|
||||
{channels[channel.type] ? (
|
||||
<Tooltip title={t("settings.notification.push_test.tooltip")}>
|
||||
<>
|
||||
<NotifyTestButton channel={channel.type} />
|
||||
</>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
extra: (
|
||||
<div onClick={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} onMouseUp={(e) => e.stopPropagation()}>
|
||||
<Switch
|
||||
defaultChecked={channels[channel.type]?.enabled as boolean}
|
||||
disabled={channels[channel.type] == null}
|
||||
checkedChildren={t("settings.notification.channel.enabled.on")}
|
||||
unCheckedChildren={t("settings.notification.channel.enabled.off")}
|
||||
onChange={(checked) => handleSwitchChange(channel.type, checked)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
forceRender: true,
|
||||
};
|
||||
}),
|
||||
[i18n.language, channels]
|
||||
);
|
||||
|
||||
const handleSwitchChange = (channel: string, enabled: boolean) => {
|
||||
setChannel(channel, { enabled });
|
||||
};
|
||||
|
||||
const handleClickSubmit = async (channel: string, index: number) => {
|
||||
const form = channelFormRefs.current[index];
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
await form.validateFields();
|
||||
|
||||
try {
|
||||
setChannel(channel, form.getFieldsValue());
|
||||
|
||||
messageApi.success(t("common.text.operation_succeeded"));
|
||||
} catch (err) {
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
{MessageContextHolder}
|
||||
{NotificationContextHolder}
|
||||
|
||||
{!initialized ? (
|
||||
<Skeleton active />
|
||||
) : (
|
||||
<Collapse className={classNames?.collapse} style={styles?.collapse} accordion={true} bordered={false} items={channelCollapseItems} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifyChannels;
|
||||
128
ui/src/components/notification/NotifyTemplate.tsx
Normal file
128
ui/src/components/notification/NotifyTemplate.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRequest } from "ahooks";
|
||||
import { Button, Form, Input, message, notification, Skeleton } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import { defaultNotifyTemplate, SETTINGS_NAMES, type NotifyTemplatesSettingsContent } from "@/domain/settings";
|
||||
import { get as getSettings, save as saveSettings } from "@/repository/settings";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
export type NotifyTemplateFormProps = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
const NotifyTemplateForm = ({ className, style }: NotifyTemplateFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [messageApi, MessageContextHolder] = message.useMessage();
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
|
||||
const formSchema = z.object({
|
||||
subject: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, t("settings.notification.template.form.subject.placeholder"))
|
||||
.max(1000, t("common.errmsg.string_max", { max: 1000 })),
|
||||
message: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, t("settings.notification.template.form.message.placeholder"))
|
||||
.max(1000, t("common.errmsg.string_max", { max: 1000 })),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
const [form] = Form.useForm<z.infer<typeof formSchema>>();
|
||||
const [formPending, setFormPending] = useState(false);
|
||||
|
||||
const [initialValues, setInitialValues] = useState<Partial<z.infer<typeof formSchema>>>();
|
||||
const [initialChanged, setInitialChanged] = useState(false);
|
||||
|
||||
const { loading } = useRequest(
|
||||
() => {
|
||||
return getSettings<NotifyTemplatesSettingsContent>(SETTINGS_NAMES.NOTIFY_TEMPLATES);
|
||||
},
|
||||
{
|
||||
onError: (err) => {
|
||||
if (err instanceof ClientResponseError && err.isAbort) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
},
|
||||
onFinally: (_, resp) => {
|
||||
const template = resp?.content?.notifyTemplates?.[0] ?? defaultNotifyTemplate;
|
||||
setInitialValues({ ...template });
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const handleInputChange = () => {
|
||||
setInitialChanged(true);
|
||||
};
|
||||
|
||||
const handleFormFinish = async (fields: z.infer<typeof formSchema>) => {
|
||||
setFormPending(true);
|
||||
|
||||
try {
|
||||
const settings = await getSettings<NotifyTemplatesSettingsContent>(SETTINGS_NAMES.NOTIFY_TEMPLATES);
|
||||
await saveSettings<NotifyTemplatesSettingsContent>({
|
||||
...settings,
|
||||
content: {
|
||||
notifyTemplates: [fields],
|
||||
},
|
||||
});
|
||||
|
||||
messageApi.success(t("common.text.operation_succeeded"));
|
||||
} catch (err) {
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
} finally {
|
||||
setFormPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
{MessageContextHolder}
|
||||
{NotificationContextHolder}
|
||||
|
||||
{loading ? (
|
||||
<Skeleton active />
|
||||
) : (
|
||||
<Form form={form} disabled={formPending} initialValues={initialValues} layout="vertical" onFinish={handleFormFinish}>
|
||||
<Form.Item
|
||||
name="subject"
|
||||
label={t("settings.notification.template.form.subject.label")}
|
||||
extra={t("settings.notification.template.form.subject.tooltip")}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Input placeholder={t("settings.notification.template.form.subject.placeholder")} onChange={handleInputChange} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="message"
|
||||
label={t("settings.notification.template.form.message.label")}
|
||||
extra={t("settings.notification.template.form.message.tooltip")}
|
||||
rules={[formRule]}
|
||||
>
|
||||
<Input.TextArea
|
||||
autoSize={{ minRows: 3, maxRows: 5 }}
|
||||
placeholder={t("settings.notification.template.form.message.placeholder")}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" disabled={!initialChanged} loading={formPending}>
|
||||
{t("common.button.save")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifyTemplateForm;
|
||||
54
ui/src/components/notification/NotifyTestButton.tsx
Normal file
54
ui/src/components/notification/NotifyTestButton.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useRequest } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, message, notification, type ButtonProps } from "antd";
|
||||
|
||||
import { notifyTest } from "@/api/notify";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
export type NotifyTestButtonProps = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
channel: string;
|
||||
disabled?: boolean;
|
||||
size?: ButtonProps["size"];
|
||||
};
|
||||
|
||||
const NotifyTestButton = ({ className, style, channel, disabled, size }: NotifyTestButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [messageApi, MessageContextHolder] = message.useMessage();
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
|
||||
const { loading, run: executeNotifyTest } = useRequest(
|
||||
() => {
|
||||
return notifyTest(channel);
|
||||
},
|
||||
{
|
||||
refreshDeps: [channel],
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
messageApi.success(t("settings.notification.push_test.pushed"));
|
||||
},
|
||||
onError: (err) => {
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
executeNotifyTest();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{MessageContextHolder}
|
||||
{NotificationContextHolder}
|
||||
|
||||
<Button className={className} style={style} disabled={disabled} loading={loading} size={size} onClick={handleClick}>
|
||||
{t("settings.notification.push_test.button")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifyTestButton;
|
||||
Reference in New Issue
Block a user