mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-14 08:29:50 +00:00
feat(ui): multiple input domains & nameservers in ApplyNodeForm
This commit is contained in:
parent
75cf552e72
commit
fb2d292cbf
@ -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")
|
||||
|
@ -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>
|
||||
|
99
ui/src/components/core/DrawerForm.tsx
Normal file
99
ui/src/components/core/DrawerForm.tsx
Normal 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;
|
119
ui/src/components/core/ModalForm.tsx
Normal file
119
ui/src/components/core/ModalForm.tsx
Normal 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;
|
@ -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}>
|
||||
|
@ -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") })
|
||||
|
@ -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);
|
||||
|
@ -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">
|
||||
|
@ -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> {
|
||||
|
@ -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\">Let’s 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 Let’s 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",
|
||||
|
@ -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\">Let’s 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\">为什么我的 Let’s 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": "秒",
|
||||
|
Loading…
x
Reference in New Issue
Block a user