fix(ui): antd nested form bugs

This commit is contained in:
Fu Diwei
2025-01-06 19:10:29 +08:00
parent 4ba7237326
commit 87e1749553
60 changed files with 1694 additions and 1358 deletions

View File

@@ -0,0 +1,290 @@
import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
import { Button, Divider, Form, type FormInstance, Select, Tooltip, Typography } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import AccessEditModal from "@/components/access/AccessEditModal";
import AccessSelect from "@/components/access/AccessSelect";
import DeployProviderPicker from "@/components/provider/DeployProviderPicker";
import DeployProviderSelect from "@/components/provider/DeployProviderSelect";
import { ACCESS_USAGES, DEPLOY_PROVIDERS, accessProvidersMap, deployProvidersMap } from "@/domain/provider";
import { type WorkflowNode, type WorkflowNodeConfigForDeploy } from "@/domain/workflow";
import { useAntdForm, useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow";
import DeployNodeConfigFormAliyunALBFields from "./DeployNodeConfigFormAliyunALBFields";
import DeployNodeConfigFormAliyunCDNFields from "./DeployNodeConfigFormAliyunCDNFields";
import DeployNodeConfigFormAliyunCLBFields from "./DeployNodeConfigFormAliyunCLBFields";
import DeployNodeConfigFormAliyunDCDNFields from "./DeployNodeConfigFormAliyunDCDNFields";
import DeployNodeConfigFormAliyunNLBFields from "./DeployNodeConfigFormAliyunNLBFields";
import DeployNodeConfigFormAliyunOSSFields from "./DeployNodeConfigFormAliyunOSSFields";
import DeployNodeConfigFormBaiduCloudCDNFields from "./DeployNodeConfigFormBaiduCloudCDNFields";
import DeployNodeConfigFormBytePlusCDNFields from "./DeployNodeConfigFormBytePlusCDNFields";
import DeployNodeConfigFormDogeCloudCDNFields from "./DeployNodeConfigFormDogeCloudCDNFields";
import DeployNodeConfigFormHuaweiCloudCDNFields from "./DeployNodeConfigFormHuaweiCloudCDNFields";
import DeployNodeConfigFormHuaweiCloudELBFields from "./DeployNodeConfigFormHuaweiCloudELBFields";
import DeployNodeConfigFormKubernetesSecretFields from "./DeployNodeConfigFormKubernetesSecretFields";
import DeployNodeConfigFormLocalFields from "./DeployNodeConfigFormLocalFields";
import DeployNodeConfigFormQiniuCDNFields from "./DeployNodeConfigFormQiniuCDNFields";
import DeployNodeConfigFormSSHFields from "./DeployNodeConfigFormSSHFields";
import DeployNodeConfigFormTencentCloudCDNFields from "./DeployNodeConfigFormTencentCloudCDNFields";
import DeployNodeConfigFormTencentCloudCLBFields from "./DeployNodeConfigFormTencentCloudCLBFields";
import DeployNodeConfigFormTencentCloudCOSFields from "./DeployNodeConfigFormTencentCloudCOSFields";
import DeployNodeConfigFormTencentCloudECDNFields from "./DeployNodeConfigFormTencentCloudECDNFields";
import DeployNodeConfigFormTencentCloudEOFields from "./DeployNodeConfigFormTencentCloudEOFields";
import DeployNodeConfigFormVolcEngineCDNFields from "./DeployNodeConfigFormVolcEngineCDNFields";
import DeployNodeConfigFormVolcEngineLiveFields from "./DeployNodeConfigFormVolcEngineLiveFields";
import DeployNodeConfigFormWebhookFields from "./DeployNodeConfigFormWebhookFields";
type DeployNodeConfigFormFieldValues = Partial<WorkflowNodeConfigForDeploy>;
export type DeployNodeConfigFormProps = {
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
initialValues?: DeployNodeConfigFormFieldValues;
nodeId: string;
onValuesChange?: (values: DeployNodeConfigFormFieldValues) => void;
};
export type DeployNodeConfigFormInstance = {
getFieldsValue: () => ReturnType<FormInstance<DeployNodeConfigFormFieldValues>["getFieldsValue"]>;
resetFields: FormInstance<DeployNodeConfigFormFieldValues>["resetFields"];
validateFields: FormInstance<DeployNodeConfigFormFieldValues>["validateFields"];
};
const initFormModel = (): DeployNodeConfigFormFieldValues => {
return {};
};
const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNodeConfigFormProps>(
({ className, style, disabled, initialValues, nodeId, onValuesChange }, ref) => {
const { t } = useTranslation();
const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"]));
// TODO: 优化此处逻辑
const [previousNodes, setPreviousNodes] = useState<WorkflowNode[]>([]);
useEffect(() => {
const previousNodes = getWorkflowOuptutBeforeId(nodeId, "certificate");
setPreviousNodes(previousNodes);
}, [nodeId]);
const formSchema = z.object({
provider: z.string({ message: t("workflow_node.deploy.form.provider.placeholder") }).nonempty(t("workflow_node.deploy.form.provider.placeholder")),
providerAccessId: z
.string({ message: t("workflow_node.deploy.form.provider_access.placeholder") })
.nonempty(t("workflow_node.deploy.form.provider_access.placeholder")),
certificate: z
.string({ message: t("workflow_node.deploy.form.certificate.placeholder") })
.nonempty(t("workflow_node.deploy.form.certificate.placeholder")),
});
const formRule = createSchemaFieldRule(formSchema);
const { form: formInst, formProps } = useAntdForm({
initialValues: initialValues ?? initFormModel(),
});
const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true });
const formFieldsEl = useMemo(() => {
/*
注意:如果追加新的子组件,请保持以 ASCII 排序。
NOTICE: If you add new child component, please keep ASCII order.
*/
switch (fieldProvider) {
case DEPLOY_PROVIDERS.ALIYUN_ALB:
return <DeployNodeConfigFormAliyunALBFields />;
case DEPLOY_PROVIDERS.ALIYUN_CLB:
return <DeployNodeConfigFormAliyunCLBFields />;
case DEPLOY_PROVIDERS.ALIYUN_CDN:
return <DeployNodeConfigFormAliyunCDNFields />;
case DEPLOY_PROVIDERS.ALIYUN_DCDN:
return <DeployNodeConfigFormAliyunDCDNFields />;
case DEPLOY_PROVIDERS.ALIYUN_NLB:
return <DeployNodeConfigFormAliyunNLBFields />;
case DEPLOY_PROVIDERS.ALIYUN_OSS:
return <DeployNodeConfigFormAliyunOSSFields />;
case DEPLOY_PROVIDERS.BAIDUCLOUD_CDN:
return <DeployNodeConfigFormBaiduCloudCDNFields />;
case DEPLOY_PROVIDERS.BYTEPLUS_CDN:
return <DeployNodeConfigFormBytePlusCDNFields />;
case DEPLOY_PROVIDERS.DOGECLOUD_CDN:
return <DeployNodeConfigFormDogeCloudCDNFields />;
case DEPLOY_PROVIDERS.HUAWEICLOUD_CDN:
return <DeployNodeConfigFormHuaweiCloudCDNFields />;
case DEPLOY_PROVIDERS.HUAWEICLOUD_ELB:
return <DeployNodeConfigFormHuaweiCloudELBFields />;
case DEPLOY_PROVIDERS.KUBERNETES_SECRET:
return <DeployNodeConfigFormKubernetesSecretFields />;
case DEPLOY_PROVIDERS.LOCAL:
return <DeployNodeConfigFormLocalFields />;
case DEPLOY_PROVIDERS.QINIU_CDN:
return <DeployNodeConfigFormQiniuCDNFields />;
case DEPLOY_PROVIDERS.SSH:
return <DeployNodeConfigFormSSHFields />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_CDN:
return <DeployNodeConfigFormTencentCloudCDNFields />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_CLB:
return <DeployNodeConfigFormTencentCloudCLBFields />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_COS:
return <DeployNodeConfigFormTencentCloudCOSFields />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_ECDN:
return <DeployNodeConfigFormTencentCloudECDNFields />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_EO:
return <DeployNodeConfigFormTencentCloudEOFields />;
case DEPLOY_PROVIDERS.VOLCENGINE_CDN:
return <DeployNodeConfigFormVolcEngineCDNFields />;
case DEPLOY_PROVIDERS.VOLCENGINE_LIVE:
return <DeployNodeConfigFormVolcEngineLiveFields />;
case DEPLOY_PROVIDERS.WEBHOOK:
return <DeployNodeConfigFormWebhookFields />;
}
}, [fieldProvider]);
const handleProviderPick = (value: string) => {
formInst.setFieldValue("provider", value);
onValuesChange?.(formInst.getFieldsValue(true));
};
const handleProviderSelect = (value: string) => {
if (fieldProvider === value) return;
// TODO: 暂时不支持切换部署目标,需后端调整,否则之前若存在部署结果输出就不会再部署
// 切换部署目标时重置表单,避免其他部署目标的配置字段影响当前部署目标
if (initialValues?.provider === value) {
formInst.resetFields();
} else {
const oldValues = formInst.getFieldsValue();
const newValues: Record<string, unknown> = {};
for (const key in oldValues) {
if (key === "provider" || key === "providerAccessId" || key === "certificate") {
newValues[key] = oldValues[key];
} else {
newValues[key] = undefined;
}
}
(formInst as FormInstance).setFieldsValue(newValues);
if (deployProvidersMap.get(fieldProvider)?.provider !== deployProvidersMap.get(value)?.provider) {
formInst.setFieldValue("providerAccessId", undefined);
onValuesChange?.(formInst.getFieldsValue(true));
}
}
};
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values as DeployNodeConfigFormFieldValues);
};
useImperativeHandle(ref, () => {
return {
getFieldsValue: () => {
return formInst.getFieldsValue(true);
},
resetFields: (fields) => {
return formInst.resetFields(fields);
},
validateFields: (nameList, config) => {
return formInst.validateFields(nameList, config);
},
} as DeployNodeConfigFormInstance;
});
return (
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
<Show when={!!fieldProvider} fallback={<DeployProviderPicker onSelect={handleProviderPick} />}>
<Form.Item name="provider" label={t("workflow_node.deploy.form.provider.label")} rules={[formRule]}>
<DeployProviderSelect
allowClear
disabled
placeholder={t("workflow_node.deploy.form.provider.placeholder")}
showSearch
onSelect={handleProviderSelect}
/>
</Form.Item>
<Form.Item className="mb-0">
<label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate">
<span>{t("workflow_node.deploy.form.provider_access.label")}</span>
<Tooltip title={t("workflow_node.deploy.form.provider_access.tooltip")}>
<Typography.Text className="ms-1" type="secondary">
<QuestionCircleOutlinedIcon />
</Typography.Text>
</Tooltip>
</div>
<div className="text-right">
<AccessEditModal
data={{ provider: deployProvidersMap.get(fieldProvider!)?.provider }}
preset="add"
trigger={
<Button size="small" type="link">
<PlusOutlinedIcon />
{t("workflow_node.deploy.form.provider_access.button")}
</Button>
}
afterSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider);
if (ACCESS_USAGES.ALL === provider?.usage || ACCESS_USAGES.DEPLOY === provider?.usage) {
formInst.setFieldValue("providerAccessId", record.id);
}
}}
/>
</div>
</div>
</label>
<Form.Item name="providerAccessId" rules={[formRule]}>
<AccessSelect
placeholder={t("workflow_node.deploy.form.provider_access.placeholder")}
filter={(record) => {
if (fieldProvider) {
return deployProvidersMap.get(fieldProvider)?.provider === record.provider;
}
const provider = accessProvidersMap.get(record.provider);
return ACCESS_USAGES.ALL === provider?.usage || ACCESS_USAGES.APPLY === provider?.usage;
}}
/>
</Form.Item>
</Form.Item>
<Form.Item
name="certificate"
label={t("workflow_node.deploy.form.certificate.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.certificate.tooltip") }}></span>}
>
<Select
options={previousNodes.map((item) => {
return {
label: item.name,
options: item.outputs?.map((output) => {
return {
label: `${item.name} - ${output.label}`,
value: `${item.id}#${output.name}`,
};
}),
};
})}
placeholder={t("workflow_node.deploy.form.certificate.placeholder")}
/>
</Form.Item>
<Divider className="my-1">
<Typography.Text className="text-xs font-normal" type="secondary">
{t("workflow_node.deploy.form.params_config.label")}
</Typography.Text>
</Divider>
{formFieldsEl}
</Show>
</Form>
);
}
);
export default memo(DeployNodeConfigForm);