From 8a816ba44f2866bc6a9195323700dff0f920e096 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 26 Dec 2024 03:06:15 +0800 Subject: [PATCH] feat(ui): new WorkflowApplyNodeForm using antd --- migrations/1735151867_updated_access.go | 110 ++++++ ui/src/components/certimate/EmailsEdit.tsx | 114 ------ ui/src/components/certimate/StringList.tsx | 217 ----------- ui/src/components/workflow/ApplyForm.tsx | 356 ------------------ ui/src/components/workflow/PanelBody.tsx | 4 +- .../workflow/node/ApplyNodeForm.tsx | 239 ++++++++++++ .../workflow/node/NotifyNodeForm.tsx | 67 ++-- .../workflow/node/StartNodeForm.tsx | 49 +-- ui/src/i18n/locales/en/nls.workflow.json | 19 + ui/src/i18n/locales/zh/nls.workflow.json | 19 + 10 files changed, 437 insertions(+), 757 deletions(-) create mode 100644 migrations/1735151867_updated_access.go delete mode 100644 ui/src/components/certimate/EmailsEdit.tsx delete mode 100644 ui/src/components/certimate/StringList.tsx delete mode 100644 ui/src/components/workflow/ApplyForm.tsx create mode 100644 ui/src/components/workflow/node/ApplyNodeForm.tsx diff --git a/migrations/1735151867_updated_access.go b/migrations/1735151867_updated_access.go new file mode 100644 index 00000000..4dfdef1a --- /dev/null +++ b/migrations/1735151867_updated_access.go @@ -0,0 +1,110 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models/schema" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e") + if err != nil { + return err + } + + // update + edit_configType := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "hwy7m03o", + "name": "configType", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "acmehttpreq", + "aliyun", + "aws", + "baiducloud", + "byteplus", + "cloudflare", + "dogecloud", + "godaddy", + "huaweicloud", + "k8s", + "local", + "namedotcom", + "namesilo", + "powerdns", + "qiniu", + "ssh", + "tencentcloud", + "volcengine", + "webhook" + ] + } + }`), edit_configType); err != nil { + return err + } + collection.Schema.AddField(edit_configType) + + return dao.SaveCollection(collection) + }, func(db dbx.Builder) error { + dao := daos.New(db); + + collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e") + if err != nil { + return err + } + + // update + edit_configType := &schema.SchemaField{} + if err := json.Unmarshal([]byte(`{ + "system": false, + "id": "hwy7m03o", + "name": "configType", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "aliyun", + "tencent", + "huaweicloud", + "qiniu", + "aws", + "cloudflare", + "namesilo", + "godaddy", + "pdns", + "httpreq", + "local", + "ssh", + "webhook", + "k8s", + "baiducloud", + "dogecloud", + "volcengine", + "byteplus", + "namedotcom" + ] + } + }`), edit_configType); err != nil { + return err + } + collection.Schema.AddField(edit_configType) + + return dao.SaveCollection(collection) + }) +} diff --git a/ui/src/components/certimate/EmailsEdit.tsx b/ui/src/components/certimate/EmailsEdit.tsx deleted file mode 100644 index 49bad862..00000000 --- a/ui/src/components/certimate/EmailsEdit.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { ClientResponseError } from "pocketbase"; - -import { cn } from "@/components/ui/utils"; -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { type PbErrorData } from "@/domain/base"; -import { useContactStore } from "@/stores/contact"; - -type EmailsEditProps = { - className?: string; - trigger: React.ReactNode; -}; - -const EmailsEdit = ({ className, trigger }: EmailsEditProps) => { - const { emails, setEmails, fetchEmails } = useContactStore(); - - const [open, setOpen] = useState(false); - const { t } = useTranslation(); - - const formSchema = z.object({ - email: z.string().email("common.errmsg.email_invalid"), - }); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - email: "", - }, - }); - - useEffect(() => { - fetchEmails(); - }, []); - - const onSubmit = async (data: z.infer) => { - if (emails.includes(data.email)) { - form.setError("email", { - message: "common.errmsg.email_duplicate", - }); - return; - } - - try { - await setEmails([...emails, data.email]); - - form.reset(); - form.clearErrors(); - - setOpen(false); - } catch (e) { - const err = e as ClientResponseError; - - Object.entries(err.response.data as PbErrorData).forEach(([key, value]) => { - form.setError(key as keyof z.infer, { - type: "manual", - message: value.message, - }); - }); - } - }; - - return ( - - - {trigger} - - - - {t("domain.application.form.email.add")} - - -
-
- { - e.stopPropagation(); - form.handleSubmit(onSubmit)(e); - }} - className="space-y-8" - > - ( - - {t("domain.application.form.email.label")} - - - - - - - )} - /> - -
- -
- - -
-
-
- ); -}; - -export default EmailsEdit; diff --git a/ui/src/components/certimate/StringList.tsx b/ui/src/components/certimate/StringList.tsx deleted file mode 100644 index 8c8293d6..00000000 --- a/ui/src/components/certimate/StringList.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { z } from "zod"; -import { useTranslation } from "react-i18next"; -import { Edit, Plus, Trash2 } from "lucide-react"; - -import { cn } from "@/components/ui/utils"; -import Show from "@/components/Show"; -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; -import { FormControl, FormItem, FormLabel } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; - -type StringListProps = { - className?: string; - value: string; - valueType?: ValueType; - onValueChange: (value: string) => void; -}; - -const titles: Record = { - domain: "common.text.domain", - ip: "common.text.ip", - dns: "common.text.dns", -}; - -const StringList = ({ value, className, onValueChange, valueType = "domain" }: StringListProps) => { - const [list, setList] = useState([]); - - const { t } = useTranslation(); - - useMemo(() => { - if (value) { - setList(value.split(";")); - } - }, [value]); - - useEffect(() => { - const changeList = () => { - onValueChange(list.join(";")); - }; - changeList(); - }, [list]); - - const addVal = (val: string) => { - if (list.includes(val)) { - return; - } - setList([...list, val]); - }; - - const editVal = (index: number, val: string) => { - const newList = [...list]; - newList[index] = val; - setList(newList); - }; - - const onRemoveClick = (index: number) => { - const newList = [...list]; - newList.splice(index, 1); - setList(newList); - }; - - return ( - <> -
- - -
{t(titles[valueType])}
- - 0}> - { - addVal(val); - }} - valueType={valueType} - value={""} - trigger={ -
- - -
{t("common.button.add")}
-
- } - /> -
-
- - 0} - fallback={ -
-
{t("common.text." + valueType + ".empty")}
- - -
- } - > -
- {list.map((item, index) => ( -
-
{item}
-
- } - value={item} - onValueChange={(val: string) => { - editVal(index, val); - }} - /> - { - onRemoveClick(index); - }} - /> -
-
- ))} -
-
-
-
-
- - ); -}; - -export default StringList; - -type ValueType = "domain" | "dns" | "host"; - -type StringEditProps = { - value: string; - trigger: React.ReactNode; - onValueChange: (value: string) => void; - valueType: ValueType; - op?: "add" | "edit"; -}; - -const StringEdit = ({ trigger, value, onValueChange, op = "add", valueType }: StringEditProps) => { - const [currentValue, setCurrentValue] = useState(""); - const [open, setOpen] = useState(false); - const [error, setError] = useState(""); - const { t } = useTranslation(); - - useEffect(() => { - setCurrentValue(value); - }, [value]); - - const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }); - - const ipSchema = z.string().ip({ message: t("common.errmsg.ip_invalid") }); - - const schedules: Record = { - domain: domainSchema, - dns: ipSchema, - host: ipSchema, - }; - - const onSaveClick = useCallback(() => { - const schema = schedules[valueType]; - - const resp = schema.safeParse(currentValue); - if (!resp.success) { - setError(JSON.parse(resp.error.message)[0].message); - return; - } - - setCurrentValue(""); - setOpen(false); - setError(""); - - onValueChange(currentValue); - }, [currentValue]); - - return ( - { - setOpen(open); - }} - > - {trigger} - - - {t(titles[valueType])} - - { - setCurrentValue(e.target.value); - }} - /> - 0}> -
{error}
-
- - - - -
-
- ); -}; diff --git a/ui/src/components/workflow/ApplyForm.tsx b/ui/src/components/workflow/ApplyForm.tsx deleted file mode 100644 index fd3ec882..00000000 --- a/ui/src/components/workflow/ApplyForm.tsx +++ /dev/null @@ -1,356 +0,0 @@ -import { memo, useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { Collapse, Divider, Switch, Tooltip, Typography } from "antd"; -import z from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { ChevronsUpDown as ChevronsUpDownIcon, Plus as PlusIcon, CircleHelp as CircleHelpIcon } from "lucide-react"; - -import { Button } from "@/components/ui/button"; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select"; -import AccessEditModal from "@/components/access/AccessEditModal"; -import EmailsEdit from "@/components/certimate/EmailsEdit"; -import StringList from "@/components/certimate/StringList"; -import { accessProvidersMap } from "@/domain/access"; -import { useZustandShallowSelector } from "@/hooks"; -import { useAccessStore } from "@/stores/access"; -import { useContactStore } from "@/stores/contact"; -import { WorkflowNode, WorkflowNodeConfig } from "@/domain/workflow"; -import { useWorkflowStore } from "@/stores/workflow"; -import { usePanel } from "./PanelProvider"; - -type ApplyFormProps = { - data: WorkflowNode; -}; - -const ApplyForm = ({ data }: ApplyFormProps) => { - const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); - - const { accesses } = useAccessStore(); - const { emails, fetchEmails } = useContactStore(); - - useEffect(() => { - fetchEmails(); - }, []); - - const { t } = useTranslation(); - - const { hidePanel } = usePanel(); - - const formSchema = z.object({ - domain: z.string().min(1, { - message: "common.errmsg.domain_invalid", - }), - email: z.string().email("common.errmsg.email_invalid").optional(), - access: z.string().regex(/^[a-zA-Z0-9]+$/, { - message: "domain.application.form.access.placeholder", - }), - keyAlgorithm: z.string().optional(), - nameservers: z.string().optional(), - timeout: z.number().optional(), - disableFollowCNAME: z.boolean().optional(), - }); - - let config: WorkflowNodeConfig = { - domain: "", - email: "", - access: "", - keyAlgorithm: "RSA2048", - nameservers: "", - timeout: 60, - disableFollowCNAME: true, - }; - if (data) config = data.config ?? config; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - domain: config.domain as string, - email: config.email as string, - access: config.access as string, - keyAlgorithm: config.keyAlgorithm as string, - nameservers: config.nameservers as string, - timeout: config.timeout as number, - disableFollowCNAME: config.disableFollowCNAME as boolean, - }, - }); - - const onSubmit = async (config: z.infer) => { - updateNode({ ...data, config, validated: true }); - hidePanel(); - }; - - return ( - <> -
- - {/* 域名 */} - ( - - <> - { - form.setValue("domain", domain); - }} - /> - - - - )} - /> - - {/* 邮箱 */} - ( - - -
{t("domain.application.form.email.label") + " " + t("domain.application.form.email.tips")}
- - - {t("common.button.add")} - - } - /> -
- - - - - -
- )} - /> - - {/* DNS 服务商授权 */} - ( - - -
{t("domain.application.form.access.label")}
- - - {t("common.button.add")} - - } - /> -
- - - - - -
- )} - /> - - - - {t("domain.application.form.advanced_settings.label")}, - children: ( -
- {/* 证书算法 */} - ( - - {t("domain.application.form.key_algorithm.label")} - - - )} - /> - - {/* DNS */} - ( - - { - form.setValue("nameservers", val); - }} - valueType="dns" - > - - - - )} - /> - - {/* DNS 超时时间 */} - ( - - {t("domain.application.form.timeout.label")} - - { - form.setValue("timeout", parseInt(e.target.value)); - }} - /> - - - - - )} - /> - - {/* 禁用 CNAME 跟随 */} - ( - - -
- {t("domain.application.form.disable_follow_cname.label")} - - {t("domain.application.form.disable_follow_cname.tips")} - - {t("domain.application.form.disable_follow_cname.tips_link")} - -

- } - > - -
-
-
- -
- { - form.setValue(field.name, value); - }} - /> -
-
- -
- )} - /> -
- ), - extra: , - forceRender: true, - showArrow: false, - }, - ]} - /> - -
- -
- - - - ); -}; - -export default memo(ApplyForm); diff --git a/ui/src/components/workflow/PanelBody.tsx b/ui/src/components/workflow/PanelBody.tsx index 50029ff2..fcb6a388 100644 --- a/ui/src/components/workflow/PanelBody.tsx +++ b/ui/src/components/workflow/PanelBody.tsx @@ -1,7 +1,7 @@ import { WorkflowNode, WorkflowNodeType } from "@/domain/workflow"; import StartNodeForm from "./node/StartNodeForm"; import DeployPanelBody from "./DeployPanelBody"; -import ApplyForm from "./ApplyForm"; +import ApplyNodeForm from "./node/ApplyNodeForm"; import NotifyNodeForm from "./node/NotifyNodeForm"; type PanelBodyProps = { @@ -13,7 +13,7 @@ const PanelBody = ({ data }: PanelBodyProps) => { case WorkflowNodeType.Start: return ; case WorkflowNodeType.Apply: - return ; + return ; case WorkflowNodeType.Deploy: return ; case WorkflowNodeType.Notify: diff --git a/ui/src/components/workflow/node/ApplyNodeForm.tsx b/ui/src/components/workflow/node/ApplyNodeForm.tsx new file mode 100644 index 00000000..0fb19b54 --- /dev/null +++ b/ui/src/components/workflow/node/ApplyNodeForm.tsx @@ -0,0 +1,239 @@ +import { memo, useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useControllableValue } from "ahooks"; +import { AutoComplete, Button, Divider, Form, Input, InputNumber, Select, Switch, Typography, type AutoCompleteProps } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import z from "zod"; +import { Plus as PlusIcon } from "lucide-react"; + +import AccessEditModal from "@/components/access/AccessEditModal"; +import AccessSelect from "@/components/access/AccessSelect"; +import { usePanel } from "../PanelProvider"; +import { useAntdForm, useZustandShallowSelector } from "@/hooks"; +import { ACCESS_PROVIDER_USAGES, accessProvidersMap } from "@/domain/access"; +import { type WorkflowNode, type WorkflowNodeConfig } from "@/domain/workflow"; +import { useContactStore } from "@/stores/contact"; +import { useWorkflowStore } from "@/stores/workflow"; +import { validDomainName, validIPv4Address, validIPv6Address } from "@/utils/validators"; + +export type ApplyNodeFormProps = { + data: WorkflowNode; +}; + +const initFormModel = (): WorkflowNodeConfig => { + return { + domain: "", + keyAlgorithm: "RSA2048", + timeout: 60, + disableFollowCNAME: true, + }; +}; + +const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => { + const { t } = useTranslation(); + + const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); + const { hidePanel } = usePanel(); + + const formSchema = z.object({ + domain: z.string({ message: t("workflow.nodes.apply.form.domain.placeholder") }).refine( + (str) => { + return String(str) + .split(";") + .every((e) => validDomainName(e, true)); + }, + { message: t("common.errmsg.domain_invalid") } + ), + email: z.string({ message: t("workflow.nodes.apply.form.email.placeholder") }).email("common.errmsg.email_invalid"), + access: z.string({ message: t("workflow.nodes.apply.form.access.placeholder") }).min(1, t("workflow.nodes.apply.form.access.placeholder")), + keyAlgorithm: z.string().nullish(), + nameservers: z + .string() + .refine( + (str) => { + if (!str) return true; + return String(str) + .split(";") + .every((e) => validDomainName(e) || validIPv4Address(e) || validIPv6Address(e)); + }, + { message: t("common.errmsg.host_invalid") } + ) + .nullish(), + timeout: z.number().gte(1, t("workflow.nodes.apply.form.timeout.placeholder")).nullish(), + disableFollowCNAME: z.boolean().nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + const { + form: formInst, + formPending, + formProps, + } = useAntdForm>({ + initialValues: data?.config ?? initFormModel(), + onSubmit: async (values) => { + await updateNode({ ...data, config: { ...values }, validated: true }); + hidePanel(); + }, + }); + + return ( +
+ + + + + + + + + + + + { + const provider = accessProvidersMap.get(record.configType); + return ACCESS_PROVIDER_USAGES.ALL === provider?.usage || ACCESS_PROVIDER_USAGES.APPLY === provider?.usage; + }} + /> + + + + + + {t("workflow.nodes.apply.form.advanced_settings.label")} + + + + + + + + } + > + + + + } + > + + + + + + +
+ ); +}; + +const ContactEmailSelect = ({ + className, + style, + disabled, + placeholder, + ...props +}: { + className?: string; + style?: React.CSSProperties; + defaultValue?: string; + disabled?: boolean; + placeholder?: string; + value?: string; + onChange?: (value: string) => void; +}) => { + const { emails, fetchEmails } = useContactStore(); + const emailsToOptions = useCallback(() => emails.map((email) => ({ label: email, value: email })), [emails]); + useEffect(() => { + fetchEmails(); + }, [fetchEmails]); + + const [value, setValue] = useControllableValue(props, { + valuePropName: "value", + defaultValuePropName: "defaultValue", + trigger: "onChange", + }); + + const [options, setOptions] = useState([]); + useEffect(() => { + setOptions(emailsToOptions()); + }, [emails, emailsToOptions]); + + const handleChange = (value: string) => { + setValue(value); + }; + + const handleSearch = (text: string) => { + const temp = emailsToOptions(); + if (text) { + if (temp.every((option) => option.label !== text)) { + temp.unshift({ label: text, value: text }); + } + } + + setOptions(temp); + }; + + return ( + + ); +}; + +export default memo(ApplyNodeForm); diff --git a/ui/src/components/workflow/node/NotifyNodeForm.tsx b/ui/src/components/workflow/node/NotifyNodeForm.tsx index 9d4961cf..62321999 100644 --- a/ui/src/components/workflow/node/NotifyNodeForm.tsx +++ b/ui/src/components/workflow/node/NotifyNodeForm.tsx @@ -1,14 +1,13 @@ -import { memo, useEffect, useState } from "react"; +import { memo, useEffect } from "react"; import { Link } from "react-router"; import { useTranslation } from "react-i18next"; -import { useDeepCompareEffect } from "ahooks"; import { Button, Form, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; import { ChevronRight as ChevronRightIcon } from "lucide-react"; import { usePanel } from "../PanelProvider"; -import { useZustandShallowSelector } from "@/hooks"; +import { useAntdForm, useZustandShallowSelector } from "@/hooks"; import { notifyChannelsMap } from "@/domain/settings"; import { type WorkflowNode, type WorkflowNodeConfig } from "@/domain/workflow"; import { useNotifyChannelStore } from "@/stores/notify"; @@ -20,8 +19,8 @@ export type NotifyNodeFormProps = { const initFormModel = (): WorkflowNodeConfig => { return { - subject: "", - message: "", + subject: "Completed!", + message: "Your workflow has been completed on Certimate.", }; }; @@ -48,40 +47,30 @@ const NotifyNodeForm = ({ data }: NotifyNodeFormProps) => { channel: z.string({ message: t("workflow.nodes.notify.form.channel.placeholder") }).min(1, t("workflow.nodes.notify.form.channel.placeholder")), }); const formRule = createSchemaFieldRule(formSchema); - const [formInst] = Form.useForm>(); - const [formPending, setFormPending] = useState(false); - - const [initialValues, setInitialValues] = useState>>( - (data?.config as Partial>) ?? initFormModel() - ); - useDeepCompareEffect(() => { - setInitialValues((data?.config as Partial>) ?? initFormModel()); - }, [data?.config]); - - const handleFormFinish = async (values: z.infer) => { - setFormPending(true); - - try { + const { + form: formInst, + formPending, + formProps, + } = useAntdForm>({ + initialValues: data?.config ?? initFormModel(), + onSubmit: async (values) => { await updateNode({ ...data, config: { ...values }, validated: true }); - hidePanel(); - } finally { - setFormPending(false); - } - }; + }, + }); return ( -
+ - + - - diff --git a/ui/src/components/workflow/node/StartNodeForm.tsx b/ui/src/components/workflow/node/StartNodeForm.tsx index 51b97e01..0b1f30a2 100644 --- a/ui/src/components/workflow/node/StartNodeForm.tsx +++ b/ui/src/components/workflow/node/StartNodeForm.tsx @@ -1,13 +1,12 @@ import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useDeepCompareEffect } from "ahooks"; import { Alert, Button, Form, Input, Radio } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import dayjs from "dayjs"; import { z } from "zod"; import { usePanel } from "../PanelProvider"; -import { useZustandShallowSelector } from "@/hooks"; +import { useAntdForm, useZustandShallowSelector } from "@/hooks"; import { type WorkflowNode, type WorkflowNodeConfig } from "@/domain/workflow"; import { useWorkflowStore } from "@/stores/workflow"; import { validCronExpression, getNextCronExecutions } from "@/utils/cron"; @@ -48,15 +47,17 @@ const StartNodeForm = ({ data }: StartNodeFormProps) => { } }); const formRule = createSchemaFieldRule(formSchema); - const [formInst] = Form.useForm>(); - const [formPending, setFormPending] = useState(false); - - const [initialValues, setInitialValues] = useState>>( - (data?.config as Partial>) ?? initFormModel() - ); - useDeepCompareEffect(() => { - setInitialValues((data?.config as Partial>) ?? initFormModel()); - }, [data?.config]); + const { + form: formInst, + formPending, + formProps, + } = useAntdForm>({ + initialValues: data?.config ?? initFormModel(), + onSubmit: async (values) => { + await updateNode({ ...data, config: { ...values }, validated: true }); + hidePanel(); + }, + }); const [triggerType, setTriggerType] = useState(data?.config?.executionMethod); const [triggerCronLastExecutions, setTriggerCronExecutions] = useState([]); @@ -77,20 +78,8 @@ const StartNodeForm = ({ data }: StartNodeFormProps) => { setTriggerCronExecutions(getNextCronExecutions(value, 5)); }; - const handleFormFinish = async (values: z.infer) => { - setFormPending(true); - - try { - await updateNode({ ...data, config: { ...values }, validated: true }); - - hidePanel(); - } finally { - setFormPending(false); - } - }; - return ( - + { tooltip={} extra={ triggerCronLastExecutions.length > 0 ? ( - +
{t("workflow.nodes.start.form.trigger_cron.extra")}
- {triggerCronLastExecutions.map((d) => ( - <> - {dayjs(d).format("YYYY-MM-DD HH:mm:ss")} + {triggerCronLastExecutions.map((date, index) => ( + + {dayjs(date).format("YYYY-MM-DD HH:mm:ss")}
- +
))} - +
) : ( <> ) diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index 6fe4c2bb..970682de 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -40,6 +40,25 @@ "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.

Reference links:
1. Let’s Encrypt rate limits
2. Why should my Let’s Encrypt (ACME) client run at a random time?", + "workflow.nodes.apply.form.domain.label": "Domain (wildcard domain is supported)", + "workflow.nodes.apply.form.domain.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.access.label": "DNS Provider Authorization", + "workflow.nodes.apply.form.access.placeholder": "Please select an authorization of DNS provider", + "workflow.nodes.apply.form.access.button": "Create", + "workflow.nodes.apply.form.advanced_settings.label": "Advanced Settings", + "workflow.nodes.apply.form.key_algorithm.label": "Certificate Key Algorithm", + "workflow.nodes.apply.form.key_algorithm.placeholder": "Please select certificate key algorithm", + "workflow.nodes.apply.form.nameservers.label": "DNS Recursive Nameservers", + "workflow.nodes.apply.form.nameservers.placeholder": "Please enter DNS recursive nameservers", + "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.timeout.label": "DNS Propagation Timeout", + "workflow.nodes.apply.form.timeout.placeholder": "Please enter DNS propagation timeout", + "workflow.nodes.apply.form.timeout.suffix": "Seconds", + "workflow.nodes.apply.form.timeout.tooltip": "It determines the maximum waiting time for DNS propagation checks during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.", + "workflow.nodes.apply.form.disable_follow_cname.label": "Disable CNAME following", + "workflow.nodes.apply.form.disable_follow_cname.tooltip": "It determines whether to disable CNAME following during ACME DNS-01 authentication. If you don't understand this option, just keep it by default.
Learn more.", "workflow.nodes.notify.form.subject.label": "Subject", "workflow.nodes.notify.form.subject.placeholder": "Please enter subject", "workflow.nodes.notify.form.message.label": "Message", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 5848840c..11f9c0d1 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -40,6 +40,25 @@ "workflow.nodes.start.form.trigger_cron.tooltip": "时区以服务器设置为准。", "workflow.nodes.start.form.trigger_cron.extra": "预计最近 5 次执行时间:", "workflow.nodes.start.form.trigger_cron_alert.content": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。

参考链接:
1. Let’s Encrypt 速率限制
2. 为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?", + "workflow.nodes.apply.form.domain.label": "域名(支持泛域名)", + "workflow.nodes.apply.form.domain.placeholder": "请输入域名", + "workflow.nodes.apply.form.email.label": "联系邮箱", + "workflow.nodes.apply.form.email.placeholder": "请输入联系邮箱", + "workflow.nodes.apply.form.access.label": "DNS 提供商授权", + "workflow.nodes.apply.form.access.placeholder": "请选择 DNS 提供商授权", + "workflow.nodes.apply.form.access.button": "新建", + "workflow.nodes.apply.form.advanced_settings.label": "高级设置", + "workflow.nodes.apply.form.key_algorithm.label": "数字证书算法", + "workflow.nodes.apply.form.key_algorithm.placeholder": "请选择数字证书算法", + "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.timeout.label": "DNS 传播检查超时时间", + "workflow.nodes.apply.form.timeout.placeholder": "请输入 DNS 传播检查超时时间", + "workflow.nodes.apply.form.timeout.suffix": "秒", + "workflow.nodes.apply.form.timeout.tooltip": "在 ACME DNS-01 认证时等待 DNS 传播检查的最长时间。如果你不了解此选项的用途,保持默认即可。", + "workflow.nodes.apply.form.disable_follow_cname.label": "禁止 CNAME 跟随", + "workflow.nodes.apply.form.disable_follow_cname.tooltip": "在 ACME DNS-01 认证时是否禁止 CNAME 跟随。如果你不了解该选项的用途,保持默认即可。
点此了解更多。", "workflow.nodes.notify.form.subject.label": "通知主题", "workflow.nodes.notify.form.subject.placeholder": "请输入通知主题", "workflow.nodes.notify.form.message.label": "通知内容",