feat(ui): multiple input domains & nameservers in ApplyNodeForm

This commit is contained in:
Fu Diwei 2024-12-27 16:42:07 +08:00
parent 75cf552e72
commit fb2d292cbf
11 changed files with 481 additions and 73 deletions

View File

@ -40,12 +40,7 @@ const AccessEditFormSSHConfig = ({ form, formName, disabled, initialValues, onVa
},
{ message: t("common.errmsg.host_invalid") }
),
port: z
.number()
.int()
.gte(1, t("common.errmsg.port_invalid"))
.lte(65535, t("common.errmsg.port_invalid"))
.transform((v) => +v),
port: z.number().int().gte(1, t("common.errmsg.port_invalid")).lte(65535, t("common.errmsg.port_invalid")),
username: z
.string()
.min(1, "access.form.ssh_username.placeholder")

View File

@ -27,7 +27,17 @@ const CertificateDetailDrawer = ({ data, loading, trigger, ...props }: Certifica
<>
{triggerDom}
<Drawer closable destroyOnClose open={open} loading={loading} placement="right" title={data?.id} width={640} onClose={() => setOpen(false)}>
<Drawer
afterOpenChange={setOpen}
closable
destroyOnClose
open={open}
loading={loading}
placement="right"
title={data?.id}
width={640}
onClose={() => setOpen(false)}
>
<Show when={!!data}>
<CertificateDetail data={data!} />
</Show>

View File

@ -0,0 +1,99 @@
import { useControllableValue } from "ahooks";
import { Button, Drawer, Form, Space, type DrawerProps, type FormProps, type ModalProps } from "antd";
import { useAntdForm, useTriggerElement } from "@/hooks";
export interface DrawerFormProps<T extends NonNullable<unknown> = any> extends Omit<FormProps<T>, "title" | "onFinish"> {
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
cancelButtonProps?: ModalProps["cancelButtonProps"];
cancelText?: ModalProps["cancelText"];
defaultOpen?: boolean;
drawerProps?: Omit<DrawerProps, "open" | "title" | "width">;
okButtonProps?: ModalProps["okButtonProps"];
okText?: ModalProps["okText"];
open?: boolean;
title?: React.ReactNode;
trigger?: React.ReactNode;
width?: string | number;
onOpenChange?: (open: boolean) => void;
onFinish?: (values: T) => void | Promise<unknown>;
}
const DrawerForm = <T extends NonNullable<unknown> = any>({
className,
style,
children,
form,
drawerProps,
title,
trigger,
width,
onFinish,
...props
}: DrawerFormProps<T>) => {
const [open, setOpen] = useControllableValue<boolean>(props, {
valuePropName: "open",
defaultValuePropName: "defaultOpen",
trigger: "onOpenChange",
});
const triggerDom = useTriggerElement(trigger, { onClick: () => setOpen(true) });
const {
form: formInst,
formPending,
formProps,
submit,
} = useAntdForm({
form,
onSubmit: async (values) => {
const ret = await onFinish?.(values);
if (ret != null && !ret) return false;
return true;
},
});
const mergedFormProps = { ...formProps, ...props };
const handleOkClick = async () => {
const ret = await submit();
if (ret != null && !ret) return;
setOpen(false);
};
const handleCancelClick = () => {
if (formPending) return;
setOpen(false);
};
return (
<>
{triggerDom}
<Drawer
footer={
<Space>
<Button onClick={handleCancelClick}>1</Button>
<Button type="primary" loading={formPending} onClick={handleOkClick}>
2
</Button>
</Space>
}
open={open}
title={title}
width={width}
{...drawerProps}
onClose={() => setOpen(false)}
>
<Form className={className} style={style} form={formInst} {...mergedFormProps}>
{children}
</Form>
</Drawer>
</>
);
};
export default DrawerForm;

View File

@ -0,0 +1,119 @@
import { useControllableValue } from "ahooks";
import { Form, Modal, type FormProps, type ModalProps } from "antd";
import { useAntdForm, useTriggerElement } from "@/hooks";
export interface ModalFormProps<T extends NonNullable<unknown> = any> extends Omit<FormProps<T>, "title" | "onFinish"> {
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
cancelButtonProps?: ModalProps["cancelButtonProps"];
cancelText?: ModalProps["cancelText"];
defaultOpen?: boolean;
modalProps?: Omit<
ModalProps,
| "cancelButtonProps"
| "cancelText"
| "confirmLoading"
| "forceRender"
| "okButtonProps"
| "okText"
| "okType"
| "open"
| "title"
| "width"
| "onOk"
| "onCancel"
>;
okButtonProps?: ModalProps["okButtonProps"];
okText?: ModalProps["okText"];
open?: boolean;
title?: ModalProps["title"];
trigger?: React.ReactNode;
width?: ModalProps["width"];
onOpenChange?: (open: boolean) => void;
onFinish?: (values: T) => void | Promise<unknown>;
}
const ModalForm = <T extends NonNullable<unknown> = any>({
className,
style,
children,
cancelButtonProps,
cancelText,
form,
modalProps,
okButtonProps,
okText,
title,
trigger,
width,
onFinish,
...props
}: ModalFormProps<T>) => {
const [open, setOpen] = useControllableValue<boolean>(props, {
valuePropName: "open",
defaultValuePropName: "defaultOpen",
trigger: "onOpenChange",
});
const triggerDom = useTriggerElement(trigger, { onClick: () => setOpen(true) });
const {
form: formInst,
formPending,
formProps,
submit,
} = useAntdForm({
form,
onSubmit: async (values) => {
const ret = await onFinish?.(values);
if (ret != null && !ret) return false;
return true;
},
});
const mergedFormProps = { ...formProps, ...props };
const handleOkClick = async () => {
const ret = await submit();
if (ret != null && !ret) return;
setOpen(false);
};
const handleCancelClick = () => {
if (formPending) return;
setOpen(false);
};
return (
<>
{triggerDom}
<Modal
cancelButtonProps={cancelButtonProps}
cancelText={cancelText}
confirmLoading={formPending}
forceRender={true}
okButtonProps={okButtonProps}
okText={okText}
okType="primary"
open={open}
title={title}
width={width}
{...modalProps}
onOk={handleOkClick}
onCancel={handleCancelClick}
>
<div className="pt-4 pb-2">
<Form className={className} style={style} form={formInst} {...mergedFormProps}>
{children}
</Form>
</div>
</Modal>
</>
);
};
export default ModalForm;

View File

@ -3,10 +3,10 @@ import { useTranslation } from "react-i18next";
import { useControllableValue } from "ahooks";
import { Button, Input, Space, type InputRef, type InputProps } from "antd";
import {
DownOutlined as DownOutlinedIcon,
ArrowDownOutlined as ArrowDownOutlinedIcon,
ArrowUpOutlined as ArrowUpOutlinedIcon,
MinusOutlined as MinusOutlinedIcon,
PlusOutlined as PlusOutlinedIcon,
UpOutlined as UpOutlinedIcon,
} from "@ant-design/icons";
import { produce } from "immer";
@ -17,11 +17,11 @@ export type MultipleInputProps = Omit<InputProps, "count" | "defaultValue" | "sh
minCount?: number;
showSortButton?: boolean;
value?: string[];
onChange?: (index: number, e: ChangeEvent<HTMLInputElement>) => void;
onCreate?: (index: number) => void;
onRemove?: (index: number) => void;
onSort?: (oldIndex: number, newIndex: number) => void;
onValueChange?: (value: string[]) => void;
onChange?: (value: string[]) => void;
onValueChange?: (index: number, element: string) => void;
onValueCreate?: (index: number) => void;
onValueRemove?: (index: number) => void;
onValueSort?: (oldIndex: number, newIndex: number) => void;
};
const MultipleInput = ({
@ -30,10 +30,10 @@ const MultipleInput = ({
maxCount,
minCount,
showSortButton = true,
onChange,
onCreate,
onSort,
onRemove,
onValueChange,
onValueCreate,
onValueSort,
onValueRemove,
...props
}: MultipleInputProps) => {
const { t } = useTranslation();
@ -44,7 +44,7 @@ const MultipleInput = ({
valuePropName: "value",
defaultValue: [],
defaultValuePropName: "defaultValue",
trigger: "onValueChange",
trigger: "onChange",
});
const handleCreate = () => {
@ -54,16 +54,16 @@ const MultipleInput = ({
setValue(newValue);
setTimeout(() => itemRefs.current[newValue.length - 1]?.focus(), 0);
onCreate?.(newValue.length - 1);
onValueCreate?.(newValue.length - 1);
};
const handleInputChange = (index: number, e: ChangeEvent<HTMLInputElement>) => {
const handleChange = (index: number, element: string) => {
const newValue = produce(value, (draft) => {
draft[index] = e.target.value;
draft[index] = element;
});
setValue(newValue);
onChange?.(index, e);
onValueChange?.(index, element);
};
const handleInputBlur = (index: number) => {
@ -87,7 +87,7 @@ const MultipleInput = ({
});
setValue(newValue);
onSort?.(index, index - 1);
onValueSort?.(index, index - 1);
};
const handleClickDown = (index: number) => {
@ -102,7 +102,7 @@ const MultipleInput = ({
});
setValue(newValue);
onSort?.(index, index + 1);
onValueSort?.(index, index + 1);
};
const handleClickAdd = (index: number) => {
@ -112,7 +112,7 @@ const MultipleInput = ({
setValue(newValue);
setTimeout(() => itemRefs.current[index + 1]?.focus(), 0);
onCreate?.(index + 1);
onValueCreate?.(index + 1);
};
const handleClickRemove = (index: number) => {
@ -121,7 +121,7 @@ const MultipleInput = ({
});
setValue(newValue);
onRemove?.(index);
onValueRemove?.(index);
};
return value == null || value.length === 0 ? (
@ -139,6 +139,7 @@ const MultipleInput = ({
return (
<MultipleInputItem
{...props}
key={index}
ref={(ref) => (itemRefs.current[index] = ref!)}
allowAdd={allowAdd}
allowClear={allowClear}
@ -150,12 +151,11 @@ const MultipleInput = ({
showSortButton={showSortButton}
value={element}
onBlur={() => handleInputBlur(index)}
onChange={(val) => handleInputChange(index, val)}
onChange={(val) => handleChange(index, val)}
onClickAdd={() => handleClickAdd(index)}
onClickDown={() => handleClickDown(index)}
onClickUp={() => handleClickUp(index)}
onClickRemove={() => handleClickRemove(index)}
onValueChange={undefined}
/>
);
})}
@ -165,7 +165,7 @@ const MultipleInput = ({
type MultipleInputItemProps = Omit<
MultipleInputProps,
"defaultValue" | "maxCount" | "minCount" | "preset" | "value" | "onChange" | "onCreate" | "onRemove" | "onSort" | "onValueChange"
"defaultValue" | "maxCount" | "minCount" | "preset" | "value" | "onChange" | "onValueCreate" | "onValueRemove" | "onValueSort" | "onValueChange"
> & {
allowAdd: boolean;
allowRemove: boolean;
@ -173,12 +173,11 @@ type MultipleInputItemProps = Omit<
allowDown: boolean;
defaultValue?: string;
value?: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
onChange?: (value: string) => void;
onClickAdd?: () => void;
onClickDown?: () => void;
onClickUp?: () => void;
onClickRemove?: () => void;
onValueChange?: (value: string) => void;
};
type MultipleInputItemInstance = {
@ -198,7 +197,6 @@ const MultipleInputItem = forwardRef<MultipleInputItemInstance, MultipleInputIte
disabled,
showSortButton,
size,
onChange,
onClickAdd,
onClickDown,
onClickUp,
@ -213,16 +211,16 @@ const MultipleInputItem = forwardRef<MultipleInputItemInstance, MultipleInputIte
valuePropName: "value",
defaultValue: "",
defaultValuePropName: "defaultValue",
trigger: "onValueChange",
trigger: "onChange",
});
const upBtn = useMemo(() => {
if (!showSortButton) return null;
return <Button icon={<UpOutlinedIcon />} color="default" disabled={disabled || !allowUp} type="text" onClick={onClickUp} />;
return <Button icon={<ArrowUpOutlinedIcon />} color="default" disabled={disabled || !allowUp} type="text" onClick={onClickUp} />;
}, [allowUp, disabled, showSortButton, onClickUp]);
const downBtn = useMemo(() => {
if (!showSortButton) return null;
return <Button icon={<DownOutlinedIcon />} color="default" disabled={disabled || !allowDown} type="text" onClick={onClickDown} />;
return <Button icon={<ArrowDownOutlinedIcon />} color="default" disabled={disabled || !allowDown} type="text" onClick={onClickDown} />;
}, [allowDown, disabled, showSortButton, onClickDown]);
const removeBtn = useMemo(() => {
return <Button icon={<MinusOutlinedIcon />} color="default" disabled={disabled || !allowRemove} type="text" onClick={onClickRemove} />;
@ -231,10 +229,8 @@ const MultipleInputItem = forwardRef<MultipleInputItemInstance, MultipleInputIte
return <Button icon={<PlusOutlinedIcon />} color="default" disabled={disabled || !allowAdd} type="text" onClick={onClickAdd} />;
}, [allowAdd, disabled, onClickAdd]);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
onChange?.(e);
};
useImperativeHandle(ref, () => ({
@ -260,7 +256,7 @@ const MultipleInputItem = forwardRef<MultipleInputItemInstance, MultipleInputIte
allowClear={allowClear}
defaultValue={undefined}
value={value}
onChange={handleChange}
onChange={handleInputChange}
/>
</div>
<Button.Group size={size}>

View File

@ -15,8 +15,7 @@ const NotifyChannelEditFormEmailFields = () => {
.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),
.lte(65535, t("common.errmsg.port_invalid")),
smtpTLS: z.boolean().nullish(),
username: z
.string({ message: t("settings.notification.channel.form.email_username.placeholder") })

View File

@ -1,13 +1,15 @@
import { memo, useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useControllableValue } from "ahooks";
import { AutoComplete, Button, Divider, Form, Input, Select, Switch, Tooltip, Typography, type AutoCompleteProps } from "antd";
import { AutoComplete, Button, Divider, Form, Input, Select, Space, Switch, Tooltip, Typography, type AutoCompleteProps } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
import { FormOutlined as FormOutlinedIcon, PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
import z from "zod";
import AccessEditModal from "@/components/access/AccessEditModal";
import AccessSelect from "@/components/access/AccessSelect";
import ModalForm from "@/components/core/ModalForm";
import MultipleInput from "@/components/core/MultipleInput";
import { usePanel } from "../PanelProvider";
import { useAntdForm, useZustandShallowSelector } from "@/hooks";
import { ACCESS_PROVIDER_USAGES, accessProvidersMap } from "@/domain/access";
@ -20,6 +22,8 @@ export type ApplyNodeFormProps = {
data: WorkflowNode;
};
const MULTIPLE_INPUT_DELIMITER = ";";
const initFormModel = (): WorkflowNodeConfig => {
return {
domain: "",
@ -38,10 +42,10 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
const { hidePanel } = usePanel();
const formSchema = z.object({
domain: z.string({ message: t("workflow.nodes.apply.form.domain.placeholder") }).refine(
(str) => {
return String(str)
.split(";")
domain: z.string({ message: t("workflow.nodes.apply.form.domains.placeholder") }).refine(
(v) => {
return String(v)
.split(MULTIPLE_INPUT_DELIMITER)
.every((e) => validDomainName(e, true));
},
{ message: t("common.errmsg.domain_invalid") }
@ -52,20 +56,20 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
nameservers: z
.string()
.refine(
(str) => {
if (!str) return true;
return String(str)
.split(";")
.every((e) => validDomainName(e) || validIPv4Address(e) || validIPv6Address(e));
(v) => {
if (!v) return true;
return String(v)
.split(MULTIPLE_INPUT_DELIMITER)
.every((e) => validIPv4Address(e) || validIPv6Address(e) || validDomainName(e));
},
{ message: t("common.errmsg.host_invalid") }
)
.nullish(),
timeout: z
.number()
.int()
.gte(1, t("workflow.nodes.apply.form.propagation_timeout.placeholder"))
.transform((v) => +v)
propagationTimeout: z
.union([
z.number().int().gte(1, t("workflow.nodes.apply.form.propagation_timeout.placeholder")),
z.string().refine((v) => !v || (parseInt(v) === +v && +v > 0), { message: t("workflow.nodes.apply.form.propagation_timeout.placeholder") }),
])
.nullish(),
disableFollowCNAME: z.boolean().nullish(),
});
@ -83,15 +87,50 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
},
});
const [fieldDomains, setFieldDomains] = useState(data?.config?.domain as string);
const [fieldNameservers, setFieldNameservers] = useState(data?.config?.nameservers as string);
const handleFieldDomainsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setFieldDomains(value);
formInst.setFieldValue("domain", value);
};
const handleFieldNameserversChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setFieldNameservers(value);
formInst.setFieldValue("nameservers", value);
};
return (
<Form {...formProps} form={formInst} disabled={formPending} layout="vertical">
<Form.Item
name="domain"
label={t("workflow.nodes.apply.form.domain.label")}
label={t("workflow.nodes.apply.form.domains.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.domain.tooltip") }}></span>}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.domains.tooltip") }}></span>}
>
<Input placeholder={t("workflow.nodes.apply.form.domain.placeholder")} />
<Space.Compact style={{ width: "100%" }}>
<Input
disabled={formPending}
value={fieldDomains}
placeholder={t("workflow.nodes.apply.form.domains.placeholder")}
onChange={handleFieldDomainsChange}
/>
<FormFieldDomainsModalForm
data={fieldDomains}
disabled={formPending}
trigger={
<Button>
<FormOutlinedIcon />
</Button>
}
onFinish={(v) => {
setFieldDomains(v);
formInst.setFieldValue("domain", v);
}}
/>
</Space.Compact>
</Form.Item>
<Form.Item
@ -100,12 +139,12 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.email.tooltip") }}></span>}
>
<ContactEmailSelect placeholder={t("workflow.nodes.apply.form.email.placeholder")} />
<FormFieldEmailSelect placeholder={t("workflow.nodes.apply.form.email.placeholder")} />
</Form.Item>
<Form.Item>
<label className="block mb-1">
<div className="flex items-center justify-between gap-4 w-full overflow-hidden">
<div className="flex items-center justify-between gap-4 w-full">
<div className="flex-grow max-w-full truncate">
<span>{t("workflow.nodes.apply.form.access.label")}</span>
<Tooltip title={t("workflow.nodes.apply.form.access.tooltip")}>
@ -166,7 +205,28 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.nameservers.tooltip") }}></span>}
>
<Input allowClear placeholder={t("workflow.nodes.apply.form.nameservers.placeholder")} />
<Space.Compact style={{ width: "100%" }}>
<Input
allowClear
disabled={formPending}
value={fieldNameservers}
placeholder={t("workflow.nodes.apply.form.nameservers.placeholder")}
onChange={handleFieldNameserversChange}
/>
<FormFieldNameserversModalForm
data={fieldNameservers}
disabled={formPending}
trigger={
<Button>
<FormOutlinedIcon />
</Button>
}
onFinish={(v) => {
setFieldNameservers(v);
formInst.setFieldValue("nameservers", v);
}}
/>
</Space.Compact>
</Form.Item>
<Form.Item
@ -203,7 +263,7 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
);
};
const ContactEmailSelect = ({
const FormFieldEmailSelect = ({
className,
style,
disabled,
@ -268,4 +328,126 @@ const ContactEmailSelect = ({
);
};
const FormFieldDomainsModalForm = ({
data,
disabled,
trigger,
onFinish,
}: {
data: string;
disabled?: boolean;
trigger?: React.ReactNode;
onFinish?: (data: string) => void;
}) => {
const { t } = useTranslation();
const formSchema = z.object({
domains: z.array(z.string()).refine(
(v) => {
return v.every((e) => !e?.trim() || validDomainName(e.trim(), true));
},
{ message: t("common.errmsg.domain_invalid") }
),
});
const formRule = createSchemaFieldRule(formSchema);
const [formInst] = Form.useForm<z.infer<typeof formSchema>>();
const [model, setModel] = useState<Partial<z.infer<typeof formSchema>>>({ domains: data?.split(MULTIPLE_INPUT_DELIMITER) });
useEffect(() => {
setModel({ domains: data?.split(MULTIPLE_INPUT_DELIMITER) });
}, [data]);
const handleFinish = useCallback(
(values: z.infer<typeof formSchema>) => {
onFinish?.(
values.domains
.map((e) => e.trim())
.filter((e) => !!e)
.join(MULTIPLE_INPUT_DELIMITER)
);
},
[onFinish]
);
return (
<ModalForm
disabled={disabled}
layout="vertical"
form={formInst}
initialValues={model}
modalProps={{ destroyOnClose: true }}
title={t("workflow.nodes.apply.form.domains.multiple_input_modal.title")}
trigger={trigger}
validateTrigger="onSubmit"
width={480}
onFinish={handleFinish}
>
<Form.Item name="domains" rules={[formRule]}>
<MultipleInput placeholder={t("workflow.nodes.apply.form.domains.multiple_input_modal.placeholder")} />
</Form.Item>
</ModalForm>
);
};
const FormFieldNameserversModalForm = ({
data,
disabled,
trigger,
onFinish,
}: {
data: string;
disabled?: boolean;
trigger?: React.ReactNode;
onFinish?: (data: string) => void;
}) => {
const { t } = useTranslation();
const formSchema = z.object({
nameservers: z.array(z.string()).refine(
(v) => {
return v.every((e) => !e?.trim() || validIPv4Address(e) || validIPv6Address(e) || validDomainName(e));
},
{ message: t("common.errmsg.domain_invalid") }
),
});
const formRule = createSchemaFieldRule(formSchema);
const [formInst] = Form.useForm<z.infer<typeof formSchema>>();
const [model, setModel] = useState<Partial<z.infer<typeof formSchema>>>({ nameservers: data?.split(MULTIPLE_INPUT_DELIMITER) });
useEffect(() => {
setModel({ nameservers: data?.split(MULTIPLE_INPUT_DELIMITER) });
}, [data]);
const handleFinish = useCallback(
(values: z.infer<typeof formSchema>) => {
onFinish?.(
values.nameservers
.map((e) => e.trim())
.filter((e) => !!e)
.join(MULTIPLE_INPUT_DELIMITER)
);
},
[onFinish]
);
return (
<ModalForm
disabled={disabled}
layout="vertical"
form={formInst}
initialValues={model}
modalProps={{ destroyOnClose: true }}
title={t("workflow.nodes.apply.form.nameservers.multiple_input_modal.title")}
trigger={trigger}
validateTrigger="onSubmit"
width={480}
onFinish={handleFinish}
>
<Form.Item name="nameservers" rules={[formRule]}>
<MultipleInput placeholder={t("workflow.nodes.apply.form.nameservers.multiple_input_modal.placeholder")} />
</Form.Item>
</ModalForm>
);
};
export default memo(ApplyNodeForm);

View File

@ -71,7 +71,7 @@ const NotifyNodeForm = ({ data }: NotifyNodeFormProps) => {
<Form.Item>
<label className="block mb-1">
<div className="flex items-center justify-between gap-4 w-full overflow-hidden">
<div className="flex items-center justify-between gap-4 w-full">
<div className="flex-grow max-w-full truncate">{t("workflow.nodes.notify.form.channel.label")}</div>
<div className="text-right">
<Link className="ant-typography" to="/settings/notification" target="_blank">

View File

@ -5,7 +5,7 @@ import { useDeepCompareEffect } from "ahooks";
export interface UseAntdFormOptions<T extends NonNullable<unknown> = any> {
form?: FormInstance<T>;
initialValues?: Partial<T> | (() => Partial<T> | Promise<Partial<T>>);
onSubmit?: (values: T) => void | Promise<void>;
onSubmit?: (values: T) => any;
}
export interface UseAntdFormReturns<T extends NonNullable<unknown> = any> {

View File

@ -40,9 +40,11 @@
"workflow.nodes.start.form.trigger_cron.tooltip": "Time zone is based on the server.",
"workflow.nodes.start.form.trigger_cron.extra": "Expected execution time for the last 5 times:",
"workflow.nodes.start.form.trigger_cron_alert.content": "Tips: If you have multiple workflows, it is recommended to set them to run at multiple times of the day instead of always running at specific times.<br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Lets Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Lets Encrypt (ACME) client run at a random time?</a>",
"workflow.nodes.apply.form.domain.label": "Domain",
"workflow.nodes.apply.form.domain.placeholder": "Please enter domain (separated by semicolons)",
"workflow.nodes.apply.form.domain.tooltip": "Wildcard domain: *.example.com",
"workflow.nodes.apply.form.domains.label": "Domains",
"workflow.nodes.apply.form.domains.placeholder": "Please enter domains (separated by semicolons)",
"workflow.nodes.apply.form.domains.tooltip": "Wildcard domain: *.example.com",
"workflow.nodes.apply.form.domains.multiple_input_modal.title": "Change Domains",
"workflow.nodes.apply.form.domains.multiple_input_modal.placeholder": "Please enter domain",
"workflow.nodes.apply.form.email.label": "Contact Email",
"workflow.nodes.apply.form.email.placeholder": "Please enter contact email",
"workflow.nodes.apply.form.email.tooltip": "Contact information required for SSL certificate application. Please pay attention to the <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">rate limits</a>.",
@ -56,6 +58,8 @@
"workflow.nodes.apply.form.nameservers.label": "DNS Recursive Nameservers",
"workflow.nodes.apply.form.nameservers.placeholder": "Please enter DNS recursive nameservers (separated by semicolons)",
"workflow.nodes.apply.form.nameservers.tooltip": "It determines whether to custom DNS recursive nameservers during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.",
"workflow.nodes.apply.form.nameservers.multiple_input_modal.title": "Change Nameservers",
"workflow.nodes.apply.form.nameservers.multiple_input_modal.placeholder": "Please enter nameserver",
"workflow.nodes.apply.form.propagation_timeout.label": "DNS Propagation Timeout",
"workflow.nodes.apply.form.propagation_timeout.placeholder": "Please enter DNS propagation timeout",
"workflow.nodes.apply.form.propagation_timeout.suffix": "Seconds",

View File

@ -40,9 +40,11 @@
"workflow.nodes.start.form.trigger_cron.tooltip": "时区以服务器设置为准。",
"workflow.nodes.start.form.trigger_cron.extra": "预计最近 5 次执行时间:",
"workflow.nodes.start.form.trigger_cron_alert.content": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Lets Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Lets Encrypt (ACME) 客户端启动时间应当随机?</a>",
"workflow.nodes.apply.form.domain.label": "域名",
"workflow.nodes.apply.form.domain.placeholder": "请输入域名(多个值请用半角分号隔开)",
"workflow.nodes.apply.form.domain.tooltip": "泛域名表示形式为:*.example.com",
"workflow.nodes.apply.form.domains.label": "域名",
"workflow.nodes.apply.form.domains.placeholder": "请输入域名(多个值请用半角分号隔开)",
"workflow.nodes.apply.form.domains.tooltip": "泛域名表示形式为:*.example.com",
"workflow.nodes.apply.form.domains.multiple_input_modal.title": "修改域名",
"workflow.nodes.apply.form.domains.multiple_input_modal.placeholder": "请输入域名",
"workflow.nodes.apply.form.email.label": "联系邮箱",
"workflow.nodes.apply.form.email.placeholder": "请输入联系邮箱",
"workflow.nodes.apply.form.email.tooltip": "申请签发 SSL 证书时所需的联系方式。请注意 Let's Encrypt 账户注册的<a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">速率限制(点此了解更多)</a>。",
@ -56,6 +58,8 @@
"workflow.nodes.apply.form.nameservers.label": "DNS 递归服务器",
"workflow.nodes.apply.form.nameservers.placeholder": "请输入 DNS 递归服务器(多个值请用半角分号隔开)",
"workflow.nodes.apply.form.nameservers.tooltip": "在 ACME DNS-01 认证时使用自定义的 DNS 递归服务器。如果你不了解该选项的用途,保持默认即可。",
"workflow.nodes.apply.form.nameservers.multiple_input_modal.title": "修改 DNS 递归服务器",
"workflow.nodes.apply.form.nameservers.multiple_input_modal.placeholder": "请输入 DNS 递归服务器",
"workflow.nodes.apply.form.propagation_timeout.label": "DNS 传播检查超时时间",
"workflow.nodes.apply.form.propagation_timeout.placeholder": "请输入 DNS 传播检查超时时间",
"workflow.nodes.apply.form.propagation_timeout.suffix": "秒",