refactor: workflow condition node
refactor: workflow condition node
This commit is contained in:
@@ -35,7 +35,14 @@ const AddNode = ({ node, disabled }: AddNodeProps) => {
|
||||
[WorkflowNodeType.ExecuteResultBranch, "workflow_node.execute_result_branch.label", <SisternodeOutlinedIcon />],
|
||||
]
|
||||
.filter(([type]) => {
|
||||
if (node.type !== WorkflowNodeType.Apply && node.type !== WorkflowNodeType.Deploy && node.type !== WorkflowNodeType.Notify) {
|
||||
const hasExecuteResult = [
|
||||
WorkflowNodeType.Apply,
|
||||
WorkflowNodeType.Upload,
|
||||
WorkflowNodeType.Monitor,
|
||||
WorkflowNodeType.Deploy,
|
||||
WorkflowNodeType.Notify,
|
||||
].includes(node.type);
|
||||
if (!hasExecuteResult) {
|
||||
return type !== WorkflowNodeType.ExecuteResultBranch;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ const ApplyNode = ({ node, disabled }: ApplyNodeProps) => {
|
||||
|
||||
const formRef = useRef<ApplyNodeConfigFormInstance>(null);
|
||||
const [formPending, setFormPending] = useState(false);
|
||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForApply;
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForApply;
|
||||
|
||||
const handleDrawerConfirm = async () => {
|
||||
setFormPending(true);
|
||||
@@ -74,12 +74,12 @@ const ApplyNode = ({ node, disabled }: ApplyNodeProps) => {
|
||||
</SharedNode.Block>
|
||||
|
||||
<SharedNode.ConfigDrawer
|
||||
getConfigNewValues={getFormValues}
|
||||
node={node}
|
||||
open={drawerOpen}
|
||||
pending={formPending}
|
||||
onConfirm={handleDrawerConfirm}
|
||||
onOpenChange={(open) => setDrawerOpen(open)}
|
||||
getFormValues={() => formRef.current!.getFieldsValue()}
|
||||
>
|
||||
<ApplyNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
|
||||
</SharedNode.ConfigDrawer>
|
||||
|
||||
@@ -56,7 +56,7 @@ export type ApplyNodeConfigFormInstance = {
|
||||
validateFields: FormInstance<ApplyNodeConfigFormFieldValues>["validateFields"];
|
||||
};
|
||||
|
||||
const MULTIPLE_INPUT_DELIMITER = ";";
|
||||
const MULTIPLE_INPUT_SEPARATOR = ";";
|
||||
|
||||
const initFormModel = (): ApplyNodeConfigFormFieldValues => {
|
||||
return {
|
||||
@@ -76,7 +76,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
domains: z.string({ message: t("workflow_node.apply.form.domains.placeholder") }).refine((v) => {
|
||||
if (!v) return false;
|
||||
return String(v)
|
||||
.split(MULTIPLE_INPUT_DELIMITER)
|
||||
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||
.every((e) => validDomainName(e, { allowWildcard: true }));
|
||||
}, t("common.errmsg.domain_invalid")),
|
||||
contactEmail: z.string({ message: t("workflow_node.apply.form.contact_email.placeholder") }).email(t("common.errmsg.email_invalid")),
|
||||
@@ -106,7 +106,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
.refine((v) => {
|
||||
if (!v) return true;
|
||||
return String(v)
|
||||
.split(MULTIPLE_INPUT_DELIMITER)
|
||||
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||
.every((e) => validIPv4Address(e) || validIPv6Address(e) || validDomainName(e));
|
||||
}, t("common.errmsg.host_invalid")),
|
||||
dnsPropagationWait: z.preprocess(
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { memo, useRef, useState } from "react";
|
||||
import { MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons";
|
||||
import { FilterFilled as FilterFilledIcon, FilterOutlined as FilterOutlinedIcon, MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons";
|
||||
import { Button, Card, Popover } from "antd";
|
||||
import { produce } from "immer";
|
||||
|
||||
import type { Expr, WorkflowNodeIoValueType } from "@/domain/workflow";
|
||||
import { ExprType } from "@/domain/workflow";
|
||||
import { useZustandShallowSelector } from "@/hooks";
|
||||
import { useWorkflowStore } from "@/stores/workflow";
|
||||
|
||||
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
|
||||
import AddNode from "./AddNode";
|
||||
import type { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm";
|
||||
import ConditionNodeConfigForm from "./ConditionNodeConfigForm";
|
||||
import ConditionNodeConfigForm, { type ConditionNodeConfigFormFieldValues, type ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm";
|
||||
|
||||
export type ConditionNodeProps = SharedNodeProps & {
|
||||
branchId: string;
|
||||
@@ -23,55 +20,9 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
||||
|
||||
const [formPending, setFormPending] = useState(false);
|
||||
const formRef = useRef<ConditionNodeConfigFormInstance>(null);
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
const getFormValues = () => formRef.current!.getFieldsValue() as ConditionNodeConfigFormFieldValues;
|
||||
|
||||
// 将表单值转换为表达式结构
|
||||
const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => {
|
||||
// 创建单个条件的表达式
|
||||
const createComparisonExpr = (condition: ConditionItem): Expr => {
|
||||
const selectors = condition.leftSelector.split("#");
|
||||
const t = selectors[2] as WorkflowNodeIoValueType;
|
||||
const left: Expr = {
|
||||
type: ExprType.Var,
|
||||
selector: {
|
||||
id: selectors[0],
|
||||
name: selectors[1],
|
||||
type: t,
|
||||
},
|
||||
};
|
||||
|
||||
const right: Expr = { type: ExprType.Const, value: condition.rightValue, valueType: t };
|
||||
|
||||
return {
|
||||
type: ExprType.Compare,
|
||||
op: condition.operator,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
};
|
||||
|
||||
// 如果只有一个条件,直接返回比较表达式
|
||||
if (values.conditions.length === 1) {
|
||||
return createComparisonExpr(values.conditions[0]);
|
||||
}
|
||||
|
||||
// 多个条件,通过逻辑运算符连接
|
||||
let expr: Expr = createComparisonExpr(values.conditions[0]);
|
||||
|
||||
for (let i = 1; i < values.conditions.length; i++) {
|
||||
expr = {
|
||||
type: ExprType.Logical,
|
||||
op: values.logicalOperator,
|
||||
left: expr,
|
||||
right: createComparisonExpr(values.conditions[i]),
|
||||
};
|
||||
}
|
||||
|
||||
return expr;
|
||||
};
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
const handleDrawerConfirm = async () => {
|
||||
setFormPending(true);
|
||||
@@ -84,10 +35,9 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
||||
|
||||
try {
|
||||
const newValues = getFormValues();
|
||||
const expression = formToExpression(newValues);
|
||||
const newNode = produce(node, (draft) => {
|
||||
draft.config = {
|
||||
expression,
|
||||
...newValues,
|
||||
};
|
||||
draft.validated = true;
|
||||
});
|
||||
@@ -100,7 +50,7 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
classNames={{ root: "shadow-md" }}
|
||||
classNames={{ root: "mt-20 shadow-md" }}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
arrow={false}
|
||||
content={
|
||||
@@ -116,26 +66,35 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP
|
||||
>
|
||||
<Card className="relative z-[1] mt-10 w-[256px] shadow-md" styles={{ body: { padding: 0 } }} hoverable onClick={() => setDrawerOpen(true)}>
|
||||
<div className="flex h-[48px] flex-col items-center justify-center truncate px-4 py-2">
|
||||
<SharedNode.Title
|
||||
className="focus:bg-background focus:text-foreground overflow-hidden outline-slate-200 focus:rounded-sm"
|
||||
node={node}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className="relative w-full overflow-hidden" onClick={(e) => e.stopPropagation()}>
|
||||
<SharedNode.Title
|
||||
className="focus:bg-background focus:text-foreground overflow-hidden outline-slate-200 focus:rounded-sm"
|
||||
node={node}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2" onClick={() => setDrawerOpen(true)}>
|
||||
{node.config?.expression ? (
|
||||
<Button color="primary" icon={<FilterFilledIcon />} variant="link" />
|
||||
) : (
|
||||
<Button color="default" icon={<FilterOutlinedIcon />} variant="link" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<SharedNode.ConfigDrawer
|
||||
node={node}
|
||||
open={drawerOpen}
|
||||
pending={formPending}
|
||||
onConfirm={handleDrawerConfirm}
|
||||
onOpenChange={(open) => setDrawerOpen(open)}
|
||||
getFormValues={() => formRef.current!.getFieldsValue()}
|
||||
>
|
||||
<ConditionNodeConfigForm nodeId={node.id} ref={formRef} disabled={disabled} initialValues={node.config} />
|
||||
</SharedNode.ConfigDrawer>
|
||||
</Popover>
|
||||
|
||||
<SharedNode.ConfigDrawer
|
||||
getConfigNewValues={getFormValues}
|
||||
node={node}
|
||||
open={drawerOpen}
|
||||
pending={formPending}
|
||||
onConfirm={handleDrawerConfirm}
|
||||
onOpenChange={(open) => setDrawerOpen(open)}
|
||||
>
|
||||
<ConditionNodeConfigForm nodeId={node.id} ref={formRef} disabled={disabled} initialValues={node.config} />
|
||||
</SharedNode.ConfigDrawer>
|
||||
|
||||
<AddNode node={node} disabled={disabled} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,36 +1,16 @@
|
||||
import { forwardRef, memo, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { Button, Card, Form, Input, Select, Radio } from "antd";
|
||||
import { PlusOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||
import i18n from "@/i18n";
|
||||
|
||||
import {
|
||||
WorkflowNodeConfigForCondition,
|
||||
Expr,
|
||||
WorkflowNodeIOValueSelector,
|
||||
ComparisonOperator,
|
||||
LogicalOperator,
|
||||
isConstExpr,
|
||||
isVarExpr,
|
||||
WorkflowNode,
|
||||
workflowNodeIOOptions,
|
||||
WorkflowNodeIoValueType,
|
||||
ExprType,
|
||||
} from "@/domain/workflow";
|
||||
import { FormInstance } from "antd";
|
||||
import { useZustandShallowSelector } from "@/hooks";
|
||||
import { useWorkflowStore } from "@/stores/workflow";
|
||||
import { forwardRef, memo, useImperativeHandle, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, type FormInstance } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
// 表单内部使用的扁平结构 - 修改后只保留必要字段
|
||||
export interface ConditionItem {
|
||||
leftSelector: string;
|
||||
operator: ComparisonOperator;
|
||||
rightValue: string;
|
||||
}
|
||||
import { type Expr, type WorkflowNodeConfigForCondition } from "@/domain/workflow";
|
||||
import { useAntdForm } from "@/hooks";
|
||||
|
||||
import ConditionNodeConfigFormExpressionEditor, { type ConditionNodeConfigFormExpressionEditorInstance } from "./ConditionNodeConfigFormExpressionEditor";
|
||||
|
||||
export type ConditionNodeConfigFormFieldValues = {
|
||||
conditions: ConditionItem[];
|
||||
logicalOperator: LogicalOperator;
|
||||
expression?: Expr | undefined;
|
||||
};
|
||||
|
||||
export type ConditionNodeConfigFormProps = {
|
||||
@@ -38,9 +18,8 @@ export type ConditionNodeConfigFormProps = {
|
||||
style?: React.CSSProperties;
|
||||
disabled?: boolean;
|
||||
initialValues?: Partial<WorkflowNodeConfigForCondition>;
|
||||
onValuesChange?: (values: WorkflowNodeConfigForCondition) => void;
|
||||
availableSelectors?: WorkflowNodeIOValueSelector[];
|
||||
nodeId: string;
|
||||
onValuesChange?: (values: WorkflowNodeConfigForCondition) => void;
|
||||
};
|
||||
|
||||
export type ConditionNodeConfigFormInstance = {
|
||||
@@ -49,298 +28,49 @@ export type ConditionNodeConfigFormInstance = {
|
||||
validateFields: FormInstance<ConditionNodeConfigFormFieldValues>["validateFields"];
|
||||
};
|
||||
|
||||
// 初始表单值
|
||||
const initFormModel = (): ConditionNodeConfigFormFieldValues => {
|
||||
return {
|
||||
conditions: [
|
||||
{
|
||||
leftSelector: "",
|
||||
operator: "==",
|
||||
rightValue: "",
|
||||
},
|
||||
],
|
||||
logicalOperator: LogicalOperator.And,
|
||||
};
|
||||
};
|
||||
|
||||
// 递归提取表达式中的条件项
|
||||
const expressionToForm = (expr?: Expr): ConditionNodeConfigFormFieldValues => {
|
||||
if (!expr) return initFormModel();
|
||||
|
||||
const conditions: ConditionItem[] = [];
|
||||
let logicalOp: LogicalOperator = LogicalOperator.And;
|
||||
|
||||
const extractComparisons = (expr: Expr): void => {
|
||||
if (expr.type === ExprType.Compare) {
|
||||
// 确保左侧是变量,右侧是常量
|
||||
if (isVarExpr(expr.left) && isConstExpr(expr.right)) {
|
||||
conditions.push({
|
||||
leftSelector: `${expr.left.selector.id}#${expr.left.selector.name}#${expr.left.selector.type}`,
|
||||
operator: expr.op,
|
||||
rightValue: String(expr.right.value),
|
||||
});
|
||||
}
|
||||
} else if (expr.type === ExprType.Logical) {
|
||||
logicalOp = expr.op;
|
||||
extractComparisons(expr.left);
|
||||
extractComparisons(expr.right);
|
||||
}
|
||||
};
|
||||
|
||||
extractComparisons(expr);
|
||||
|
||||
return {
|
||||
conditions: conditions.length > 0 ? conditions : initFormModel().conditions,
|
||||
logicalOperator: logicalOp,
|
||||
};
|
||||
};
|
||||
|
||||
// 根据变量类型获取适当的操作符选项
|
||||
const getOperatorsByType = (type: string): { value: ComparisonOperator; label: string }[] => {
|
||||
switch (type) {
|
||||
case "number":
|
||||
case "string":
|
||||
return [
|
||||
{ value: "==", label: i18n.t("workflow_node.condition.form.comparison.equal") },
|
||||
{ value: "!=", label: i18n.t("workflow_node.condition.form.comparison.not_equal") },
|
||||
{ value: ">", label: i18n.t("workflow_node.condition.form.comparison.greater_than") },
|
||||
{ value: ">=", label: i18n.t("workflow_node.condition.form.comparison.greater_than_or_equal") },
|
||||
{ value: "<", label: i18n.t("workflow_node.condition.form.comparison.less_than") },
|
||||
{ value: "<=", label: i18n.t("workflow_node.condition.form.comparison.less_than_or_equal") },
|
||||
];
|
||||
case "boolean":
|
||||
return [{ value: "is", label: i18n.t("workflow_node.condition.form.comparison.is") }];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 从选择器字符串中提取变量类型
|
||||
const getVariableTypeFromSelector = (selector: string): string => {
|
||||
if (!selector) return "string";
|
||||
|
||||
// 假设选择器格式为 "id#name#type"
|
||||
const parts = selector.split("#");
|
||||
if (parts.length >= 3) {
|
||||
return parts[2].toLowerCase() || "string";
|
||||
}
|
||||
return "string";
|
||||
return {};
|
||||
};
|
||||
|
||||
const ConditionNodeConfigForm = forwardRef<ConditionNodeConfigFormInstance, ConditionNodeConfigFormProps>(
|
||||
({ className, style, disabled, initialValues, onValuesChange, nodeId }, ref) => {
|
||||
({ className, style, disabled, initialValues, nodeId, onValuesChange }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const prefix = "workflow_node.condition.form";
|
||||
|
||||
const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"]));
|
||||
const formSchema = z.object({
|
||||
expression: z.any().nullish(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
const { form: formInst, formProps } = useAntdForm({
|
||||
name: "workflowNodeConditionConfigForm",
|
||||
initialValues: initialValues ?? initFormModel(),
|
||||
});
|
||||
|
||||
const [form] = Form.useForm<ConditionNodeConfigFormFieldValues>();
|
||||
const [formModel, setFormModel] = useState<ConditionNodeConfigFormFieldValues>(initFormModel());
|
||||
const editorRef = useRef<ConditionNodeConfigFormExpressionEditorInstance>(null);
|
||||
|
||||
const [previousNodes, setPreviousNodes] = useState<WorkflowNode[]>([]);
|
||||
useEffect(() => {
|
||||
const previousNodes = getWorkflowOuptutBeforeId(nodeId);
|
||||
setPreviousNodes(previousNodes);
|
||||
}, [nodeId]);
|
||||
|
||||
// 初始化表单值
|
||||
useEffect(() => {
|
||||
if (initialValues?.expression) {
|
||||
const formValues = expressionToForm(initialValues.expression);
|
||||
form.setFieldsValue(formValues);
|
||||
setFormModel(formValues);
|
||||
}
|
||||
}, [form, initialValues]);
|
||||
|
||||
// 公开表单方法
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getFieldsValue: form.getFieldsValue,
|
||||
resetFields: form.resetFields,
|
||||
validateFields: form.validateFields,
|
||||
}),
|
||||
[form]
|
||||
);
|
||||
|
||||
// 表单值变更处理
|
||||
const handleFormChange = (_: undefined, values: ConditionNodeConfigFormFieldValues) => {
|
||||
setFormModel(values);
|
||||
|
||||
if (onValuesChange) {
|
||||
// 将表单值转换为表达式
|
||||
const expression = formToExpression(values);
|
||||
onValuesChange({ expression });
|
||||
}
|
||||
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
|
||||
onValuesChange?.(values);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
getFieldsValue: formInst.getFieldsValue,
|
||||
resetFields: formInst.resetFields,
|
||||
validateFields: (nameList, config) => {
|
||||
const t1 = formInst.validateFields(nameList, config);
|
||||
const t2 = editorRef.current!.validate();
|
||||
return Promise.all([t1, t2]).then(() => t1);
|
||||
},
|
||||
} as ConditionNodeConfigFormInstance;
|
||||
});
|
||||
|
||||
return (
|
||||
<Form form={form} className={className} style={style} layout="vertical" disabled={disabled} initialValues={formModel} onValuesChange={handleFormChange}>
|
||||
<Form.List name="conditions">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Card
|
||||
key={key}
|
||||
size="small"
|
||||
className="mb-3"
|
||||
extra={fields.length > 1 ? <Button icon={<DeleteOutlined />} danger type="text" onClick={() => remove(name)} /> : null}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 左侧变量选择器 */}
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, "leftSelector"]}
|
||||
className="mb-0 flex-1"
|
||||
rules={[{ required: true, message: t(`${prefix}.variable.errmsg`) }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={t(`${prefix}.variable.placeholder`)}
|
||||
options={previousNodes.map((item) => {
|
||||
return workflowNodeIOOptions(item);
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 操作符 - 动态根据变量类型改变选项 */}
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => {
|
||||
return prevValues.conditions?.[name]?.leftSelector !== currentValues.conditions?.[name]?.leftSelector;
|
||||
}}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const leftSelector = getFieldValue(["conditions", name, "leftSelector"]);
|
||||
const varType = getVariableTypeFromSelector(leftSelector);
|
||||
const operators = getOperatorsByType(varType);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, "operator"]}
|
||||
className="mb-0 w-32"
|
||||
rules={[{ required: true, message: t(`${prefix}.operator.errmsg`) }]}
|
||||
>
|
||||
<Select options={operators} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* 右侧输入控件 - 根据变量类型使用不同的控件 */}
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => {
|
||||
return prevValues.conditions?.[name]?.leftSelector !== currentValues.conditions?.[name]?.leftSelector;
|
||||
}}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const leftSelector = getFieldValue(["conditions", name, "leftSelector"]);
|
||||
const varType = getVariableTypeFromSelector(leftSelector);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, "rightValue"]}
|
||||
className="mb-0 flex-1"
|
||||
rules={[{ required: true, message: t(`${prefix}.value.errmsg`) }]}
|
||||
>
|
||||
{varType === "boolean" ? (
|
||||
<Select placeholder={t(`${prefix}.value.boolean.placeholder`)}>
|
||||
<Select.Option value="true">{t(`${prefix}.value.boolean.true`)}</Select.Option>
|
||||
<Select.Option value="false">{t(`${prefix}.value.boolean.false`)}</Select.Option>
|
||||
</Select>
|
||||
) : varType === "number" ? (
|
||||
<Input type="number" placeholder={t(`${prefix}.value.number.placeholder`)} />
|
||||
) : (
|
||||
<Input placeholder={t(`${prefix}.value.string.placeholder`)} />
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{/* 添加条件按钮 */}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() =>
|
||||
add({
|
||||
leftSelector: "",
|
||||
operator: "==",
|
||||
rightValue: "",
|
||||
})
|
||||
}
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
{t(`${prefix}.add_condition.button`)}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
{formModel.conditions && formModel.conditions.length > 1 && (
|
||||
<Form.Item name="logicalOperator" label={t(`${prefix}.logical_operator.label`)}>
|
||||
<Radio.Group buttonStyle="solid">
|
||||
<Radio.Button value="and">{t(`${prefix}.logical_operator.and`)}</Radio.Button>
|
||||
<Radio.Button value="or">{t(`${prefix}.logical_operator.or`)}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
|
||||
<Form.Item name="expression" label={t("workflow_node.condition.form.expression.label")} rules={[formRule]}>
|
||||
<ConditionNodeConfigFormExpressionEditor ref={editorRef} nodeId={nodeId} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// 表单值转换为表达式结构 (需要添加)
|
||||
const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => {
|
||||
const createComparisonExpr = (condition: ConditionItem): Expr => {
|
||||
const [id, name, typeStr] = condition.leftSelector.split("#");
|
||||
|
||||
const type = typeStr as WorkflowNodeIoValueType;
|
||||
|
||||
const left: Expr = {
|
||||
type: ExprType.Var,
|
||||
selector: { id, name, type },
|
||||
};
|
||||
|
||||
const right: Expr = {
|
||||
type: ExprType.Const,
|
||||
value: condition.rightValue,
|
||||
valueType: type,
|
||||
};
|
||||
|
||||
return {
|
||||
type: ExprType.Compare,
|
||||
op: condition.operator,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
};
|
||||
|
||||
// 如果只有一个条件,直接返回比较表达式
|
||||
if (values.conditions.length === 1) {
|
||||
return createComparisonExpr(values.conditions[0]);
|
||||
}
|
||||
|
||||
// 多个条件,通过逻辑运算符连接
|
||||
let expr: Expr = createComparisonExpr(values.conditions[0]);
|
||||
|
||||
for (let i = 1; i < values.conditions.length; i++) {
|
||||
expr = {
|
||||
type: ExprType.Logical,
|
||||
op: values.logicalOperator,
|
||||
left: expr,
|
||||
right: createComparisonExpr(values.conditions[i]),
|
||||
};
|
||||
}
|
||||
|
||||
return expr;
|
||||
};
|
||||
|
||||
export default memo(ConditionNodeConfigForm);
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CloseOutlined as CloseOutlinedIcon, PlusOutlined } from "@ant-design/icons";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { Button, Form, Input, Radio, Select, theme } from "antd";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import type { Expr, ExprComparisonOperator, ExprLogicalOperator, ExprValue, ExprValueSelector, ExprValueType } from "@/domain/workflow";
|
||||
import { ExprType } from "@/domain/workflow";
|
||||
import { useAntdFormName, useZustandShallowSelector } from "@/hooks";
|
||||
import { useWorkflowStore } from "@/stores/workflow";
|
||||
|
||||
export type ConditionNodeConfigFormExpressionEditorProps = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
defaultValue?: Expr;
|
||||
disabled?: boolean;
|
||||
nodeId: string;
|
||||
value?: Expr;
|
||||
onChange?: (value: Expr) => void;
|
||||
};
|
||||
|
||||
export type ConditionNodeConfigFormExpressionEditorInstance = {
|
||||
validate: () => Promise<void>;
|
||||
};
|
||||
|
||||
// 表单内部使用的扁平结构
|
||||
type ConditionItem = {
|
||||
// 选择器,格式为 "${nodeId}#${outputName}#${valueType}"
|
||||
// 将 [ExprValueSelector] 转为字符串形式,以便于结构化存储。
|
||||
leftSelector?: string;
|
||||
// 比较运算符。
|
||||
operator?: ExprComparisonOperator;
|
||||
// 值。
|
||||
// 将 [ExprValue] 转为字符串形式,以便于结构化存储。
|
||||
rightValue?: string;
|
||||
};
|
||||
|
||||
type ConditionFormValues = {
|
||||
conditions: ConditionItem[];
|
||||
logicalOperator: ExprLogicalOperator;
|
||||
};
|
||||
|
||||
const initFormModel = (): ConditionFormValues => {
|
||||
return {
|
||||
conditions: [{}],
|
||||
logicalOperator: "and",
|
||||
};
|
||||
};
|
||||
|
||||
const exprToFormValues = (expr?: Expr): ConditionFormValues => {
|
||||
if (!expr) return initFormModel();
|
||||
|
||||
const conditions: ConditionItem[] = [];
|
||||
let logicalOp: ExprLogicalOperator = "and";
|
||||
|
||||
const extractExpr = (expr: Expr): void => {
|
||||
if (expr.type === ExprType.Comparison) {
|
||||
if (expr.left.type == ExprType.Variant && expr.right.type == ExprType.Constant) {
|
||||
conditions.push({
|
||||
leftSelector: expr.left.selector?.id != null ? `${expr.left.selector.id}#${expr.left.selector.name}#${expr.left.selector.type}` : undefined,
|
||||
operator: expr.operator != null ? expr.operator : undefined,
|
||||
rightValue: expr.right?.value != null ? String(expr.right.value) : undefined,
|
||||
});
|
||||
} else {
|
||||
console.warn("[certimate] invalid comparison expression: left must be a variant and right must be a constant", expr);
|
||||
}
|
||||
} else if (expr.type === ExprType.Logical) {
|
||||
logicalOp = expr.operator || "and";
|
||||
extractExpr(expr.left);
|
||||
extractExpr(expr.right);
|
||||
}
|
||||
};
|
||||
|
||||
extractExpr(expr);
|
||||
|
||||
return {
|
||||
conditions: conditions,
|
||||
logicalOperator: logicalOp,
|
||||
};
|
||||
};
|
||||
|
||||
const formValuesToExpr = (values: ConditionFormValues): Expr | undefined => {
|
||||
const wrapExpr = (condition: ConditionItem): Expr => {
|
||||
const [id, name, type] = (condition.leftSelector?.split("#") ?? ["", "", ""]) as [string, string, ExprValueType];
|
||||
const valid = !!id && !!name && !!type;
|
||||
|
||||
const left: Expr = {
|
||||
type: ExprType.Variant,
|
||||
selector: valid
|
||||
? {
|
||||
id: id,
|
||||
name: name,
|
||||
type: type,
|
||||
}
|
||||
: ({} as ExprValueSelector),
|
||||
};
|
||||
|
||||
const right: Expr = {
|
||||
type: ExprType.Constant,
|
||||
value: condition.rightValue!,
|
||||
valueType: type,
|
||||
};
|
||||
|
||||
return {
|
||||
type: ExprType.Comparison,
|
||||
operator: condition.operator!,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
};
|
||||
|
||||
if (values.conditions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 只有一个条件时,直接返回比较表达式
|
||||
if (values.conditions.length === 1) {
|
||||
const { leftSelector, operator, rightValue } = values.conditions[0];
|
||||
if (!leftSelector || !operator || !rightValue) {
|
||||
return undefined;
|
||||
}
|
||||
return wrapExpr(values.conditions[0]);
|
||||
}
|
||||
|
||||
// 多个条件时,通过逻辑运算符连接
|
||||
let expr: Expr = wrapExpr(values.conditions[0]);
|
||||
for (let i = 1; i < values.conditions.length; i++) {
|
||||
expr = {
|
||||
type: ExprType.Logical,
|
||||
operator: values.logicalOperator,
|
||||
left: expr,
|
||||
right: wrapExpr(values.conditions[i]),
|
||||
};
|
||||
}
|
||||
return expr;
|
||||
};
|
||||
|
||||
const ConditionNodeConfigFormExpressionEditor = forwardRef<ConditionNodeConfigFormExpressionEditorInstance, ConditionNodeConfigFormExpressionEditorProps>(
|
||||
({ className, style, disabled, nodeId, ...props }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"]));
|
||||
|
||||
const [value, setValue] = useControllableValue<Expr | undefined>(props, {
|
||||
valuePropName: "value",
|
||||
defaultValuePropName: "defaultValue",
|
||||
trigger: "onChange",
|
||||
});
|
||||
|
||||
const [formInst] = Form.useForm<ConditionFormValues>();
|
||||
const formName = useAntdFormName({ form: formInst, name: "workflowNodeConditionConfigFormExpressionEditorForm" });
|
||||
const [formModel, setFormModel] = useState<ConditionFormValues>(initFormModel());
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
const formValues = exprToFormValues(value);
|
||||
formInst.setFieldsValue(formValues);
|
||||
setFormModel(formValues);
|
||||
} else {
|
||||
formInst.resetFields();
|
||||
setFormModel(initFormModel());
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const ciSelectorCandidates = useMemo(() => {
|
||||
const previousNodes = getWorkflowOuptutBeforeId(nodeId);
|
||||
return previousNodes
|
||||
.map((node) => {
|
||||
const group = {
|
||||
label: node.name,
|
||||
options: Array<{ label: string; value: string }>(),
|
||||
};
|
||||
|
||||
for (const output of node.outputs ?? []) {
|
||||
switch (output.type) {
|
||||
case "certificate":
|
||||
group.options.push({
|
||||
label: `${output.label} - ${t("workflow.variables.selector.validity.label")}`,
|
||||
value: `${node.id}#${output.name}.validity#boolean`,
|
||||
});
|
||||
group.options.push({
|
||||
label: `${output.label} - ${t("workflow.variables.selector.days_left.label")}`,
|
||||
value: `${node.id}#${output.name}.daysLeft#number`,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
group.options.push({
|
||||
label: `${output.label}`,
|
||||
value: `${node.id}#${output.name}#${output.type}`,
|
||||
});
|
||||
console.warn("[certimate] invalid workflow output type in condition expressions", output);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return group;
|
||||
})
|
||||
.filter((item) => item.options.length > 0);
|
||||
}, [nodeId]);
|
||||
|
||||
const getValueTypeBySelector = (selector: string): ExprValueType | undefined => {
|
||||
if (!selector) return;
|
||||
|
||||
const parts = selector.split("#");
|
||||
if (parts.length >= 3) {
|
||||
return parts[2].toLowerCase() as ExprValueType;
|
||||
}
|
||||
};
|
||||
|
||||
const getOperatorsBySelector = (selector: string): { value: ExprComparisonOperator; label: string }[] => {
|
||||
const valueType = getValueTypeBySelector(selector);
|
||||
return getOperatorsByValueType(valueType!);
|
||||
};
|
||||
|
||||
const getOperatorsByValueType = (valueType: ExprValue): { value: ExprComparisonOperator; label: string }[] => {
|
||||
switch (valueType) {
|
||||
case "number":
|
||||
return [
|
||||
{ value: "eq", label: t("workflow_node.condition.form.expression.operator.option.eq.label") },
|
||||
{ value: "neq", label: t("workflow_node.condition.form.expression.operator.option.neq.label") },
|
||||
{ value: "gt", label: t("workflow_node.condition.form.expression.operator.option.gt.label") },
|
||||
{ value: "gte", label: t("workflow_node.condition.form.expression.operator.option.gte.label") },
|
||||
{ value: "lt", label: t("workflow_node.condition.form.expression.operator.option.lt.label") },
|
||||
{ value: "lte", label: t("workflow_node.condition.form.expression.operator.option.lte.label") },
|
||||
];
|
||||
|
||||
case "string":
|
||||
return [
|
||||
{ value: "eq", label: t("workflow_node.condition.form.expression.operator.option.eq.label") },
|
||||
{ value: "neq", label: t("workflow_node.condition.form.expression.operator.option.neq.label") },
|
||||
];
|
||||
|
||||
case "boolean":
|
||||
return [
|
||||
{ value: "eq", label: t("workflow_node.condition.form.expression.operator.option.eq.alias_is_label") },
|
||||
{ value: "neq", label: t("workflow_node.condition.form.expression.operator.option.neq.alias_not_label") },
|
||||
];
|
||||
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormChange = (_: undefined, values: ConditionFormValues) => {
|
||||
setValue(formValuesToExpr(values));
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
validate: async () => {
|
||||
await formInst.validateFields();
|
||||
},
|
||||
} as ConditionNodeConfigFormExpressionEditorInstance;
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
className={className}
|
||||
style={style}
|
||||
form={formInst}
|
||||
disabled={disabled}
|
||||
initialValues={formModel}
|
||||
layout="vertical"
|
||||
name={formName}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
<Show when={formModel.conditions?.length > 1}>
|
||||
<Form.Item
|
||||
className="mb-2"
|
||||
name="logicalOperator"
|
||||
rules={[{ required: true, message: t("workflow_node.condition.form.expression.logical_operator.errmsg") }]}
|
||||
>
|
||||
<Radio.Group block>
|
||||
<Radio.Button value="and">{t("workflow_node.condition.form.expression.logical_operator.option.and.label")}</Radio.Button>
|
||||
<Radio.Button value="or">{t("workflow_node.condition.form.expression.logical_operator.option.or.label")}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Show>
|
||||
|
||||
<Form.List name="conditions">
|
||||
{(fields, { add, remove }) => (
|
||||
<div className="flex flex-col gap-2">
|
||||
{fields.map(({ key, name: index, ...rest }) => (
|
||||
<div key={key} className="flex gap-2">
|
||||
{/* 左:变量选择器 */}
|
||||
<Form.Item
|
||||
className="mb-0 flex-1"
|
||||
name={[index, "leftSelector"]}
|
||||
rules={[{ required: true, message: t("workflow_node.condition.form.expression.variable.errmsg") }]}
|
||||
{...rest}
|
||||
>
|
||||
<Select
|
||||
labelRender={({ label, value }) => {
|
||||
if (value != null) {
|
||||
const group = ciSelectorCandidates.find((group) => group.options.some((option) => option.value === value));
|
||||
return `${group?.label} - ${label}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<span style={{ color: themeToken.colorTextPlaceholder }}>{t("workflow_node.condition.form.expression.variable.placeholder")}</span>
|
||||
);
|
||||
}}
|
||||
options={ciSelectorCandidates}
|
||||
placeholder={t("workflow_node.condition.form.expression.variable.placeholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 中:运算符选择器,根据变量类型决定选项 */}
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => {
|
||||
return prevValues.conditions?.[index]?.leftSelector !== currentValues.conditions?.[index]?.leftSelector;
|
||||
}}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const leftSelector = getFieldValue(["conditions", index, "leftSelector"]);
|
||||
const operators = getOperatorsBySelector(leftSelector);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
className="mb-0 w-36"
|
||||
name={[index, "operator"]}
|
||||
rules={[{ required: true, message: t("workflow_node.condition.form.expression.operator.errmsg") }]}
|
||||
{...rest}
|
||||
>
|
||||
<Select
|
||||
open={operators.length === 0 ? false : undefined}
|
||||
options={operators}
|
||||
placeholder={t("workflow_node.condition.form.expression.operator.placeholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* 右:输入控件,根据变量类型决定组件 */}
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => {
|
||||
return prevValues.conditions?.[index]?.leftSelector !== currentValues.conditions?.[index]?.leftSelector;
|
||||
}}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const leftSelector = getFieldValue(["conditions", index, "leftSelector"]);
|
||||
const valueType = getValueTypeBySelector(leftSelector);
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
className="mb-0 w-36"
|
||||
name={[index, "rightValue"]}
|
||||
rules={[{ required: true, message: t("workflow_node.condition.form.expression.value.errmsg") }]}
|
||||
{...rest}
|
||||
>
|
||||
{valueType === "string" ? (
|
||||
<Input placeholder={t("workflow_node.condition.form.expression.value.placeholder")} />
|
||||
) : valueType === "number" ? (
|
||||
<Input type="number" placeholder={t("workflow_node.condition.form.expression.value.placeholder")} />
|
||||
) : valueType === "boolean" ? (
|
||||
<Select placeholder={t("workflow_node.condition.form.expression.value.placeholder")}>
|
||||
<Select.Option value="true">{t("workflow_node.condition.form.expression.value.option.true.label")}</Select.Option>
|
||||
<Select.Option value="false">{t("workflow_node.condition.form.expression.value.option.false.label")}</Select.Option>
|
||||
</Select>
|
||||
) : (
|
||||
<Input readOnly placeholder={t("workflow_node.condition.form.expression.value.placeholder")} />
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
className="my-1"
|
||||
color="default"
|
||||
disabled={disabled}
|
||||
icon={<CloseOutlinedIcon />}
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={() => remove(index)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Form.Item>
|
||||
<Button type="dashed" block icon={<PlusOutlined />} onClick={() => add({})}>
|
||||
{t("workflow_node.condition.form.expression.add_condition.button")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ConditionNodeConfigFormExpressionEditor;
|
||||
@@ -24,10 +24,10 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => {
|
||||
|
||||
const formRef = useRef<DeployNodeConfigFormInstance>(null);
|
||||
const [formPending, setFormPending] = useState(false);
|
||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForDeploy;
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [drawerFooterShow, setDrawerFooterShow] = useState(true);
|
||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForDeploy;
|
||||
|
||||
useEffect(() => {
|
||||
setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider);
|
||||
@@ -86,8 +86,9 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => {
|
||||
</SharedNode.Block>
|
||||
|
||||
<SharedNode.ConfigDrawer
|
||||
node={node}
|
||||
footer={drawerFooterShow}
|
||||
getConfigNewValues={getFormValues}
|
||||
node={node}
|
||||
open={drawerOpen}
|
||||
pending={formPending}
|
||||
onConfirm={handleDrawerConfirm}
|
||||
@@ -95,7 +96,6 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => {
|
||||
setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider);
|
||||
setDrawerOpen(open);
|
||||
}}
|
||||
getFormValues={() => formRef.current!.getFieldsValue()}
|
||||
>
|
||||
<DeployNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} nodeId={node.id} onValuesChange={handleFormValuesChange} />
|
||||
</SharedNode.ConfigDrawer>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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, Flex, Form, type FormInstance, Select, Switch, Tooltip, Typography } from "antd";
|
||||
import { Button, Divider, Flex, Form, type FormInstance, Select, Switch, Tooltip, Typography, theme } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -11,7 +11,7 @@ import DeploymentProviderPicker from "@/components/provider/DeploymentProviderPi
|
||||
import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect.tsx";
|
||||
import Show from "@/components/Show";
|
||||
import { ACCESS_USAGES, DEPLOYMENT_PROVIDERS, accessProvidersMap, deploymentProvidersMap } from "@/domain/provider";
|
||||
import { type WorkflowNode, type WorkflowNodeConfigForDeploy } from "@/domain/workflow";
|
||||
import { type WorkflowNodeConfigForDeploy, WorkflowNodeType } from "@/domain/workflow";
|
||||
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
|
||||
import { useWorkflowStore } from "@/stores/workflow";
|
||||
|
||||
@@ -125,14 +125,9 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
||||
({ className, style, disabled, initialValues, nodeId, onValuesChange }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"]));
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
// TODO: 优化此处逻辑
|
||||
const [previousNodes, setPreviousNodes] = useState<WorkflowNode[]>([]);
|
||||
useEffect(() => {
|
||||
const previousNodes = getWorkflowOuptutBeforeId(nodeId, "certificate");
|
||||
setPreviousNodes(previousNodes);
|
||||
}, [nodeId]);
|
||||
const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"]));
|
||||
|
||||
const formSchema = z.object({
|
||||
certificate: z
|
||||
@@ -170,6 +165,24 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
||||
}
|
||||
}, [fieldProvider]);
|
||||
|
||||
const certificateCandidates = useMemo(() => {
|
||||
const previousNodes = getWorkflowOuptutBeforeId(nodeId, "certificate");
|
||||
return previousNodes
|
||||
.filter((node) => node.type === WorkflowNodeType.Apply || node.type === WorkflowNodeType.Upload)
|
||||
.map((item) => {
|
||||
return {
|
||||
label: item.name,
|
||||
options: (item.outputs ?? [])?.map((output) => {
|
||||
return {
|
||||
label: output.label,
|
||||
value: `${item.id}#${output.name}`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
})
|
||||
.filter((group) => group.options.length > 0);
|
||||
}, [nodeId]);
|
||||
|
||||
const [nestedFormInst] = Form.useForm();
|
||||
const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeDeployConfigFormProviderConfigForm" });
|
||||
const nestedFormEl = useMemo(() => {
|
||||
@@ -487,17 +500,15 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
||||
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}`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
})}
|
||||
labelRender={({ label, value }) => {
|
||||
if (value != null) {
|
||||
const group = certificateCandidates.find((group) => group.options.some((option) => option.value === value));
|
||||
return `${group?.label} - ${label}`;
|
||||
}
|
||||
|
||||
return <span style={{ color: themeToken.colorTextPlaceholder }}>{t("workflow_node.deploy.form.certificate.placeholder")}</span>;
|
||||
}}
|
||||
options={certificateCandidates}
|
||||
placeholder={t("workflow_node.deploy.form.certificate.placeholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -19,7 +19,7 @@ export type DeployNodeConfigFormAliyunCASDeployConfigProps = {
|
||||
onValuesChange?: (values: DeployNodeConfigFormAliyunCASDeployConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const MULTIPLE_INPUT_DELIMITER = ";";
|
||||
const MULTIPLE_INPUT_SEPARATOR = ";";
|
||||
|
||||
const initFormModel = (): DeployNodeConfigFormAliyunCASDeployConfigFieldValues => {
|
||||
return {};
|
||||
@@ -42,7 +42,7 @@ const DeployNodeConfigFormAliyunCASDeployConfig = ({
|
||||
resourceIds: z.string({ message: t("workflow_node.deploy.form.aliyun_cas_deploy_resource_ids.placeholder") }).refine((v) => {
|
||||
if (!v) return false;
|
||||
return String(v)
|
||||
.split(MULTIPLE_INPUT_DELIMITER)
|
||||
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||
.every((e) => /^[1-9]\d*$/.test(e));
|
||||
}, t("workflow_node.deploy.form.aliyun_cas_deploy_resource_ids.errmsg.invalid")),
|
||||
contactIds: z
|
||||
@@ -51,7 +51,7 @@ const DeployNodeConfigFormAliyunCASDeployConfig = ({
|
||||
.refine((v) => {
|
||||
if (!v) return true;
|
||||
return String(v)
|
||||
.split(MULTIPLE_INPUT_DELIMITER)
|
||||
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||
.every((e) => /^[1-9]\d*$/.test(e));
|
||||
}, t("workflow_node.deploy.form.aliyun_cas_deploy_contact_ids.errmsg.invalid")),
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ export type DeployNodeConfigFormBaotaPanelSiteConfigProps = {
|
||||
const SITE_TYPE_PHP = "php";
|
||||
const SITE_TYPE_OTHER = "other";
|
||||
|
||||
const MULTIPLE_INPUT_DELIMITER = ";";
|
||||
const MULTIPLE_INPUT_SEPARATOR = ";";
|
||||
|
||||
const initFormModel = (): DeployNodeConfigFormBaotaPanelSiteConfigFieldValues => {
|
||||
return {
|
||||
@@ -60,7 +60,7 @@ const DeployNodeConfigFormBaotaPanelSiteConfig = ({
|
||||
if (fieldSiteType !== SITE_TYPE_OTHER) return true;
|
||||
if (!v) return false;
|
||||
return String(v)
|
||||
.split(MULTIPLE_INPUT_DELIMITER)
|
||||
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||
.every((e) => !!e.trim());
|
||||
}, t("workflow_node.deploy.form.baotapanel_site_names.placeholder")),
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ export type DeployNodeConfigFormTencentCloudSSLDeployConfigProps = {
|
||||
onValuesChange?: (values: DeployNodeConfigFormTencentCloudSSLDeployConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const MULTIPLE_INPUT_DELIMITER = ";";
|
||||
const MULTIPLE_INPUT_SEPARATOR = ";";
|
||||
|
||||
const initFormModel = (): DeployNodeConfigFormTencentCloudSSLDeployConfigFieldValues => {
|
||||
return {};
|
||||
@@ -46,7 +46,7 @@ const DeployNodeConfigFormTencentCloudSSLDeployConfig = ({
|
||||
resourceIds: z.string({ message: t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.placeholder") }).refine((v) => {
|
||||
if (!v) return false;
|
||||
return String(v)
|
||||
.split(MULTIPLE_INPUT_DELIMITER)
|
||||
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||
.every((e) => /^[A-Za-z0-9*._-|]+$/.test(e));
|
||||
}, t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.errmsg.invalid")),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, type FormInstance, Input, Select } from "antd";
|
||||
import { Alert, Form, type FormInstance, Input, Select } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -56,6 +56,10 @@ const DeployNodeConfigFormUniCloudWebHostConfig = ({
|
||||
name={formName}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
<Form.Item>
|
||||
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.unicloud_webhost.guide") }}></span>} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="spaceProvider" label={t("workflow_node.deploy.form.unicloud_webhost_space_provider.label")} rules={[formRule]}>
|
||||
<Select
|
||||
options={["aliyun", "tencent"].map((s) => ({
|
||||
|
||||
@@ -18,7 +18,7 @@ export type DeployNodeConfigFormWangsuCDNConfigProps = {
|
||||
onValuesChange?: (values: DeployNodeConfigFormWangsuCDNConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const MULTIPLE_INPUT_DELIMITER = ";";
|
||||
const MULTIPLE_INPUT_SEPARATOR = ";";
|
||||
|
||||
const initFormModel = (): DeployNodeConfigFormWangsuCDNConfigFieldValues => {
|
||||
return {
|
||||
@@ -42,7 +42,7 @@ const DeployNodeConfigFormWangsuCDNConfig = ({
|
||||
.refine((v) => {
|
||||
if (!v) return false;
|
||||
return String(v)
|
||||
.split(MULTIPLE_INPUT_DELIMITER)
|
||||
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||
.every((e) => validDomainName(e));
|
||||
}, t("workflow_node.deploy.form.wangsu_cdn_domains.placeholder")),
|
||||
});
|
||||
|
||||
@@ -23,9 +23,9 @@ const MonitorNode = ({ node, disabled }: MonitorNodeProps) => {
|
||||
|
||||
const formRef = useRef<MonitorNodeConfigFormInstance>(null);
|
||||
const [formPending, setFormPending] = useState(false);
|
||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForMonitor;
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForMonitor;
|
||||
|
||||
const wrappedEl = useMemo(() => {
|
||||
if (node.type !== WorkflowNodeType.Monitor) {
|
||||
@@ -74,12 +74,12 @@ const MonitorNode = ({ node, disabled }: MonitorNodeProps) => {
|
||||
</SharedNode.Block>
|
||||
|
||||
<SharedNode.ConfigDrawer
|
||||
getConfigNewValues={getFormValues}
|
||||
node={node}
|
||||
open={drawerOpen}
|
||||
pending={formPending}
|
||||
onConfirm={handleDrawerConfirm}
|
||||
onOpenChange={(open) => setDrawerOpen(open)}
|
||||
getFormValues={() => formRef.current!.getFieldsValue()}
|
||||
>
|
||||
<MonitorNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
|
||||
</SharedNode.ConfigDrawer>
|
||||
|
||||
@@ -25,9 +25,9 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => {
|
||||
|
||||
const formRef = useRef<NotifyNodeConfigFormInstance>(null);
|
||||
const [formPending, setFormPending] = useState(false);
|
||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForNotify;
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForNotify;
|
||||
|
||||
const wrappedEl = useMemo(() => {
|
||||
if (node.type !== WorkflowNodeType.Notify) {
|
||||
@@ -82,12 +82,12 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => {
|
||||
</SharedNode.Block>
|
||||
|
||||
<SharedNode.ConfigDrawer
|
||||
getConfigNewValues={getFormValues}
|
||||
node={node}
|
||||
open={drawerOpen}
|
||||
pending={formPending}
|
||||
onConfirm={handleDrawerConfirm}
|
||||
onOpenChange={(open) => setDrawerOpen(open)}
|
||||
getFormValues={() => formRef.current!.getFieldsValue()}
|
||||
>
|
||||
<NotifyNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
|
||||
</SharedNode.ConfigDrawer>
|
||||
|
||||
@@ -23,9 +23,9 @@ const StartNode = ({ node, disabled }: StartNodeProps) => {
|
||||
|
||||
const formRef = useRef<StartNodeConfigFormInstance>(null);
|
||||
const [formPending, setFormPending] = useState(false);
|
||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForStart;
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForStart;
|
||||
|
||||
const wrappedEl = useMemo(() => {
|
||||
if (node.type !== WorkflowNodeType.Start) {
|
||||
@@ -83,12 +83,12 @@ const StartNode = ({ node, disabled }: StartNodeProps) => {
|
||||
</SharedNode.Block>
|
||||
|
||||
<SharedNode.ConfigDrawer
|
||||
getConfigNewValues={getFormValues}
|
||||
node={node}
|
||||
open={drawerOpen}
|
||||
pending={formPending}
|
||||
onConfirm={handleDrawerConfirm}
|
||||
onOpenChange={(open) => setDrawerOpen(open)}
|
||||
getFormValues={() => formRef.current!.getFieldsValue()}
|
||||
>
|
||||
<StartNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
|
||||
</SharedNode.ConfigDrawer>
|
||||
|
||||
@@ -23,9 +23,9 @@ const UploadNode = ({ node, disabled }: UploadNodeProps) => {
|
||||
|
||||
const formRef = useRef<UploadNodeConfigFormInstance>(null);
|
||||
const [formPending, setFormPending] = useState(false);
|
||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForUpload;
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForUpload;
|
||||
|
||||
const wrappedEl = useMemo(() => {
|
||||
if (node.type !== WorkflowNodeType.Upload) {
|
||||
@@ -74,12 +74,12 @@ const UploadNode = ({ node, disabled }: UploadNodeProps) => {
|
||||
</SharedNode.Block>
|
||||
|
||||
<SharedNode.ConfigDrawer
|
||||
getConfigNewValues={getFormValues}
|
||||
node={node}
|
||||
open={drawerOpen}
|
||||
pending={formPending}
|
||||
onConfirm={handleDrawerConfirm}
|
||||
onOpenChange={(open) => setDrawerOpen(open)}
|
||||
getFormValues={() => formRef.current!.getFieldsValue()}
|
||||
>
|
||||
<UploadNodeConfigForm ref={formRef} disabled={disabled} initialValues={node.config} />
|
||||
</SharedNode.ConfigDrawer>
|
||||
|
||||
@@ -33,7 +33,7 @@ const SharedNodeTitle = ({ className, style, node, disabled }: SharedNodeTitlePr
|
||||
|
||||
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||
const oldName = node.name;
|
||||
const newName = e.target.innerText.trim().substring(0, 64) || oldName;
|
||||
const newName = e.target.innerText.replaceAll("\r", "").replaceAll("\n", "").trim().substring(0, 64) || oldName;
|
||||
if (oldName === newName) {
|
||||
return;
|
||||
}
|
||||
@@ -45,9 +45,16 @@ const SharedNodeTitle = ({ className, style, node, disabled }: SharedNodeTitlePr
|
||||
);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.code === "Enter") {
|
||||
e.preventDefault();
|
||||
e.currentTarget.blur();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full cursor-text overflow-hidden text-center">
|
||||
<div className={className} style={style} contentEditable={!disabled} suppressContentEditableWarning onBlur={handleBlur}>
|
||||
<div className={className} style={style} contentEditable={!disabled} suppressContentEditableWarning onBlur={handleBlur} onKeyDown={handleKeyDown}>
|
||||
{node.name}
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,7 +98,7 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex,
|
||||
|
||||
const handleRenameConfirm = async () => {
|
||||
const oldName = node.name;
|
||||
const newName = nameRef.current?.trim()?.substring(0, 64) || oldName;
|
||||
const newName = nameRef.current?.replaceAll("\r", "")?.replaceAll("\n", "").trim()?.substring(0, 64) || oldName;
|
||||
if (oldName === newName) {
|
||||
return;
|
||||
}
|
||||
@@ -195,7 +202,7 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex,
|
||||
};
|
||||
// #endregion
|
||||
|
||||
// #region Wrapper
|
||||
// #region Block
|
||||
type SharedNodeBlockProps = SharedNodeProps & {
|
||||
children: React.ReactNode;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
@@ -245,7 +252,7 @@ type SharedNodeEditDrawerProps = SharedNodeProps & {
|
||||
pending?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
onConfirm: () => void | Promise<unknown>;
|
||||
getFormValues: () => NonNullable<unknown>;
|
||||
getConfigNewValues: () => NonNullable<unknown>; // 用于获取节点配置的新值,以便在抽屉关闭前进行对比,决定是否提示保存
|
||||
};
|
||||
|
||||
const SharedNodeConfigDrawer = ({
|
||||
@@ -256,7 +263,7 @@ const SharedNodeConfigDrawer = ({
|
||||
loading,
|
||||
pending,
|
||||
onConfirm,
|
||||
getFormValues,
|
||||
getConfigNewValues,
|
||||
...props
|
||||
}: SharedNodeEditDrawerProps) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -284,7 +291,7 @@ const SharedNodeConfigDrawer = ({
|
||||
if (pending) return;
|
||||
|
||||
const oldValues = JSON.parse(JSON.stringify(node.config ?? {}));
|
||||
const newValues = JSON.parse(JSON.stringify(getFormValues()));
|
||||
const newValues = JSON.parse(JSON.stringify(getConfigNewValues()));
|
||||
const changed = !isEqual(oldValues, {}) && !isEqual(oldValues, newValues);
|
||||
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
|
||||
Reference in New Issue
Block a user