feat: enhance condition handling with enum value options and metadata in workflow components
This commit is contained in:
@@ -292,6 +292,8 @@ function ConditionNodePanel({
|
||||
{branches.map((branch, index) => {
|
||||
const summary = summariesByBranchID.get(branch.id)
|
||||
const condition = branch.condition ?? {}
|
||||
const selectedVariable = findConditionVariable(availableVariables, condition.left)
|
||||
const operatorOptions = getConditionOperatorOptions(selectedVariable)
|
||||
const conditionRight = condition.right === undefined || condition.right === null
|
||||
? ""
|
||||
: String(condition.right)
|
||||
@@ -344,7 +346,7 @@ function ConditionNodePanel({
|
||||
<Label className="text-xs">判断方式</Label>
|
||||
<OptionCombobox
|
||||
value={condition.operator ?? "eq"}
|
||||
options={conditionOperators}
|
||||
options={operatorOptions}
|
||||
placeholder="选择判断方式"
|
||||
searchPlaceholder="搜索判断方式"
|
||||
emptyText="没有可用判断方式"
|
||||
@@ -354,19 +356,13 @@ function ConditionNodePanel({
|
||||
/>
|
||||
</div>
|
||||
{!conditionOperatorWithoutRight(condition.operator ?? "eq") ? (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">比较值</Label>
|
||||
<Input
|
||||
value={conditionRight}
|
||||
onChange={(event) => commitBranch(branch.id, {
|
||||
condition: {
|
||||
...condition,
|
||||
right: normalizeConditionRight(event.target.value),
|
||||
},
|
||||
})}
|
||||
placeholder="请输入比较值"
|
||||
/>
|
||||
</div>
|
||||
<ConditionRightControl
|
||||
value={conditionRight}
|
||||
variable={selectedVariable}
|
||||
onChange={(right) => commitBranch(branch.id, {
|
||||
condition: { ...condition, right },
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
@@ -422,25 +418,126 @@ const conditionOperators = [
|
||||
{ value: "exists", label: "存在" },
|
||||
{ value: "not_exists", label: "不存在" },
|
||||
{ value: "truthy", label: "为真" },
|
||||
{ value: "is_true", label: "为真" },
|
||||
{ value: "falsy", label: "为假" },
|
||||
{ value: "is_false", label: "为假" },
|
||||
{ value: "gt", label: "大于" },
|
||||
{ value: "gte", label: "大于等于" },
|
||||
{ value: "lt", label: "小于" },
|
||||
{ value: "lte", label: "小于等于" },
|
||||
]
|
||||
|
||||
function conditionOperatorWithoutRight(operator: string) {
|
||||
return ["exists", "not_exists", "truthy", "falsy"].includes(operator)
|
||||
function ConditionRightControl({
|
||||
value,
|
||||
variable,
|
||||
onChange,
|
||||
}: {
|
||||
value: string
|
||||
variable?: WorkflowVariableRef
|
||||
onChange: (value: unknown) => void
|
||||
}) {
|
||||
const valueOptions = getConditionValueOptions(variable)
|
||||
if (valueOptions.length > 0) {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">比较值</Label>
|
||||
<OptionCombobox
|
||||
value={value}
|
||||
options={valueOptions}
|
||||
placeholder="选择比较值"
|
||||
searchPlaceholder="搜索比较值"
|
||||
emptyText="当前变量没有可选值"
|
||||
onChange={(nextValue) => onChange(decodeConditionRight(nextValue, variable))}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">比较值</Label>
|
||||
<Input
|
||||
type={variable?.type === "number" || variable?.type === "integer" ? "number" : "text"}
|
||||
value={value}
|
||||
onChange={(event) => onChange(normalizeConditionRight(event.target.value, variable))}
|
||||
placeholder={variable ? `请输入${variable.label || variable.field}的比较值` : "请输入比较值"}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeConditionRight(value: string) {
|
||||
function conditionOperatorWithoutRight(operator: string) {
|
||||
return ["exists", "not_exists", "truthy", "is_true", "falsy", "is_false"].includes(operator)
|
||||
}
|
||||
|
||||
function normalizeConditionRight(value: string, variable?: WorkflowVariableRef) {
|
||||
const trimmed = value.trim()
|
||||
if (variable?.type === "boolean") {
|
||||
return trimmed === "true"
|
||||
}
|
||||
if (variable?.type === "number" || variable?.type === "integer") {
|
||||
return trimmed === "" ? "" : Number(trimmed)
|
||||
}
|
||||
if (variable?.type === "string") {
|
||||
return trimmed
|
||||
}
|
||||
if (trimmed === "true") return true
|
||||
if (trimmed === "false") return false
|
||||
if (trimmed !== "" && !Number.isNaN(Number(trimmed))) return Number(trimmed)
|
||||
return trimmed
|
||||
}
|
||||
|
||||
function findConditionVariable(
|
||||
variables: WorkflowVariableRef[],
|
||||
selector?: WorkflowVariableSelector
|
||||
): WorkflowVariableRef | undefined {
|
||||
if (!selector?.nodeId || !selector.field) {
|
||||
return undefined
|
||||
}
|
||||
return variables.find((item) => item.nodeId === selector.nodeId && item.field === selector.field)
|
||||
}
|
||||
|
||||
function getConditionOperatorOptions(variable?: WorkflowVariableRef) {
|
||||
if (!variable?.operators?.length) {
|
||||
return conditionOperators
|
||||
}
|
||||
const allowed = new Set(variable.operators)
|
||||
return conditionOperators.filter((item) => allowed.has(item.value))
|
||||
}
|
||||
|
||||
function getConditionValueOptions(variable?: WorkflowVariableRef) {
|
||||
if (variable?.valueOptions?.length) {
|
||||
return variable.valueOptions.map((item) => ({
|
||||
value: encodeConditionRight(item.value),
|
||||
label: item.label,
|
||||
}))
|
||||
}
|
||||
if (variable?.type === "boolean") {
|
||||
return [
|
||||
{ value: "true", label: "是" },
|
||||
{ value: "false", label: "否" },
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function encodeConditionRight(value: unknown) {
|
||||
if (typeof value === "string") return value
|
||||
if (typeof value === "number" || typeof value === "boolean") return String(value)
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
function decodeConditionRight(value: string, variable?: WorkflowVariableRef) {
|
||||
if (variable?.type === "boolean") {
|
||||
return value === "true"
|
||||
}
|
||||
if (variable?.type === "number" || variable?.type === "integer") {
|
||||
return Number(value)
|
||||
}
|
||||
const option = variable?.valueOptions?.find((item) => encodeConditionRight(item.value) === value)
|
||||
return option ? option.value : value
|
||||
}
|
||||
|
||||
function normalizeBranch(branch: WorkflowConditionBranch): WorkflowConditionBranch {
|
||||
if (branch.default) {
|
||||
const { condition: _condition, ...rest } = branch
|
||||
|
||||
@@ -15,7 +15,7 @@ export function VariableSelector({
|
||||
}) {
|
||||
const options = variables.map((item) => ({
|
||||
value: `${item.nodeId}.${item.field}`,
|
||||
label: `${item.nodeName}.${item.field} · ${item.type}`,
|
||||
label: `${item.nodeName}.${item.label || item.field} · ${item.type}`,
|
||||
}))
|
||||
const selectedValue = value?.nodeId && value.field ? `${value.nodeId}.${value.field}` : ""
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
type WorkflowHistory,
|
||||
type WorkflowHelperLine,
|
||||
type WorkflowNodeConfig,
|
||||
type WorkflowVariableSpec,
|
||||
} from "./workflow-utils"
|
||||
import { NodeConfigPanel, type WorkflowBranchSummary } from "./node-config-panel"
|
||||
import type { WorkflowBranchTargetOption } from "./node-config-panel"
|
||||
@@ -250,8 +251,8 @@ export function WorkflowEditor({
|
||||
[draft, nodeSpecs, propertyPanelNode]
|
||||
)
|
||||
const propertyPanelBranchSummaries = useMemo(
|
||||
() => (propertyPanelNode ? getBranchSummaries(nodes, propertyPanelNode.id) : []),
|
||||
[nodes, propertyPanelNode]
|
||||
() => (propertyPanelNode ? getBranchSummaries(nodes, propertyPanelNode.id, nodeSpecs) : []),
|
||||
[nodeSpecs, nodes, propertyPanelNode]
|
||||
)
|
||||
const propertyPanelBranchTargetOptions = useMemo(
|
||||
() => (propertyPanelNode ? getBranchTargetOptions(nodes, edges, propertyPanelNode.id) : []),
|
||||
@@ -1012,7 +1013,9 @@ const conditionOperators = [
|
||||
{ value: "exists", label: "存在" },
|
||||
{ value: "not_exists", label: "不存在" },
|
||||
{ value: "truthy", label: "为真" },
|
||||
{ value: "is_true", label: "为真" },
|
||||
{ value: "falsy", label: "为假" },
|
||||
{ value: "is_false", label: "为假" },
|
||||
{ value: "gt", label: "大于" },
|
||||
{ value: "gte", label: "大于等于" },
|
||||
{ value: "lt", label: "小于" },
|
||||
@@ -1021,7 +1024,8 @@ const conditionOperators = [
|
||||
|
||||
function getBranchSummaries(
|
||||
nodes: WorkflowFlowNode[],
|
||||
nodeId: string
|
||||
nodeId: string,
|
||||
nodeSpecs: AIWorkflowNodeSpec[]
|
||||
): WorkflowBranchSummary[] {
|
||||
const node = nodes.find((item) => item.id === nodeId)
|
||||
const branches = node?.data.config?.branches ?? []
|
||||
@@ -1032,7 +1036,7 @@ function getBranchSummaries(
|
||||
branchId: branch.id,
|
||||
targetNodeId: branch.targetNodeId,
|
||||
targetName: target?.data.name ?? target?.data.title ?? branch.targetNodeId,
|
||||
conditionLabel: branch.condition ? formatConditionLabel(branch.condition) : "无条件匹配",
|
||||
conditionLabel: branch.condition ? formatConditionLabel(branch.condition, nodes, nodeSpecs) : "无条件匹配",
|
||||
isDefault: Boolean(branch.default),
|
||||
}
|
||||
})
|
||||
@@ -1054,31 +1058,62 @@ function getBranchTargetOptions(
|
||||
})
|
||||
}
|
||||
|
||||
function formatConditionLabel(condition: WorkflowCondition) {
|
||||
const left = condition.left?.nodeId && condition.left.field
|
||||
? `${condition.left.nodeId}.${condition.left.field}`
|
||||
: "未选择变量"
|
||||
function formatConditionLabel(
|
||||
condition: WorkflowCondition,
|
||||
nodes: WorkflowFlowNode[],
|
||||
nodeSpecs: AIWorkflowNodeSpec[]
|
||||
) {
|
||||
const variable = findConditionOutputSpec(condition.left, nodes, nodeSpecs)
|
||||
const left = variable?.label
|
||||
?? (condition.left?.nodeId && condition.left.field ? `${condition.left.nodeId}.${condition.left.field}` : "未选择变量")
|
||||
const operator = conditionOperators.find((item) => item.value === condition.operator)?.label
|
||||
?? condition.operator
|
||||
?? "未选择判断方式"
|
||||
|
||||
if (["exists", "not_exists", "truthy", "falsy"].includes(condition.operator ?? "")) {
|
||||
if (["exists", "not_exists", "truthy", "is_true", "falsy", "is_false"].includes(condition.operator ?? "")) {
|
||||
return `${left} ${operator}`
|
||||
}
|
||||
|
||||
return `${left} ${operator} ${formatConditionRight(condition.right)}`
|
||||
return `${left} ${operator} ${formatConditionRight(condition.right, variable)}`
|
||||
}
|
||||
|
||||
function formatConditionRight(value: unknown) {
|
||||
function formatConditionRight(value: unknown, variable?: WorkflowVariableSpec) {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return "未填写比较值"
|
||||
}
|
||||
const option = variable?.valueOptions?.find((item) => conditionValueEquals(item.value, value))
|
||||
if (option) {
|
||||
return option.label
|
||||
}
|
||||
if (variable?.type === "boolean") {
|
||||
return value === true ? "是" : "否"
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function findConditionOutputSpec(
|
||||
selector: WorkflowCondition["left"],
|
||||
nodes: WorkflowFlowNode[],
|
||||
nodeSpecs: AIWorkflowNodeSpec[]
|
||||
): WorkflowVariableSpec | undefined {
|
||||
if (!selector?.nodeId || !selector.field) {
|
||||
return undefined
|
||||
}
|
||||
const sourceNode = nodes.find((item) => item.id === selector.nodeId)
|
||||
if (!sourceNode) {
|
||||
return undefined
|
||||
}
|
||||
const spec = getNodeSpec(nodeSpecs, sourceNode.data.nodeType ?? "")
|
||||
return spec?.outputSchema?.find((item) => item.name === selector.field)
|
||||
}
|
||||
|
||||
function conditionValueEquals(left: unknown, right: unknown) {
|
||||
return JSON.stringify(left) === JSON.stringify(right)
|
||||
}
|
||||
|
||||
function getEventClientPoint(event: MouseEvent | TouchEvent) {
|
||||
if ("changedTouches" in event) {
|
||||
const touch = event.changedTouches[0] ?? event.touches[0]
|
||||
|
||||
@@ -317,6 +317,49 @@ describe("getAvailableVariables", () => {
|
||||
|
||||
assert.deepEqual(plain(variables), [])
|
||||
})
|
||||
|
||||
it("preserves condition editor metadata from output specs", async () => {
|
||||
const { getAvailableVariables } = await loadModule()
|
||||
|
||||
const variables = getAvailableVariables(
|
||||
{
|
||||
nodes: [
|
||||
{ id: "policy_1", type: "workflowNode", position: { x: 0, y: 0 }, data: { nodeType: "reply_policy", name: "回复策略" } },
|
||||
{ id: "condition_1", type: "workflowNode", position: { x: 200, y: 0 }, data: { nodeType: "condition" } },
|
||||
],
|
||||
edges: [{ id: "e1", source: "policy_1", target: "condition_1" }],
|
||||
},
|
||||
"condition_1",
|
||||
[
|
||||
{
|
||||
type: "reply_policy",
|
||||
outputSchema: [
|
||||
{
|
||||
name: "action",
|
||||
label: "处理策略",
|
||||
type: "string",
|
||||
description: "Selected policy action.",
|
||||
operators: ["eq", "neq"],
|
||||
valueOptions: [{ value: "direct_reply", label: "直接回复客户" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
assert.deepEqual(plain(variables), [
|
||||
{
|
||||
nodeId: "policy_1",
|
||||
nodeName: "回复策略",
|
||||
field: "action",
|
||||
label: "处理策略",
|
||||
type: "string",
|
||||
description: "Selected policy action.",
|
||||
operators: ["eq", "neq"],
|
||||
valueOptions: [{ value: "direct_reply", label: "直接回复客户" }],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("toApiDefinition", () => {
|
||||
|
||||
@@ -108,11 +108,20 @@ export type WorkflowVariableSelector = {
|
||||
field: string
|
||||
}
|
||||
|
||||
export type WorkflowVariableValueOption = {
|
||||
value: unknown
|
||||
label: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type WorkflowVariableSpec = {
|
||||
name: string
|
||||
label?: string
|
||||
type: WorkflowVariableType
|
||||
required?: boolean
|
||||
description?: string
|
||||
operators?: string[]
|
||||
valueOptions?: WorkflowVariableValueOption[]
|
||||
}
|
||||
|
||||
export type WorkflowNodeSpec = {
|
||||
@@ -128,8 +137,11 @@ export type WorkflowVariableRef = {
|
||||
nodeId: string
|
||||
nodeName: string
|
||||
field: string
|
||||
label?: string
|
||||
type: string
|
||||
description: string
|
||||
operators?: string[]
|
||||
valueOptions?: WorkflowVariableValueOption[]
|
||||
}
|
||||
|
||||
export type WorkflowDraftValidation = {
|
||||
@@ -647,8 +659,11 @@ export function getAvailableVariables(
|
||||
nodeId: sourceNode.id,
|
||||
nodeName: sourceNode.data?.name ?? spec?.title ?? sourceNode.id,
|
||||
field: output.name,
|
||||
label: output.label,
|
||||
type: output.type,
|
||||
description: output.description ?? "",
|
||||
operators: output.operators,
|
||||
valueOptions: output.valueOptions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,9 +301,16 @@ export type AIWorkflowVariableSelector = {
|
||||
|
||||
export type AIWorkflowVariableSpec = {
|
||||
name: string
|
||||
label?: string
|
||||
type: AIWorkflowVariableType
|
||||
required?: boolean
|
||||
description: string
|
||||
operators?: string[]
|
||||
valueOptions?: {
|
||||
value: unknown
|
||||
label: string
|
||||
description?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export type AIWorkflowDefinition = {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { request } from "@/lib/api/client"
|
||||
|
||||
export type PublicConfig = {
|
||||
language: string
|
||||
wxworkEnabled: boolean
|
||||
oidcEnabled: boolean
|
||||
}
|
||||
|
||||
let publicConfigPromise: Promise<PublicConfig> | null = null
|
||||
|
||||
export function fetchPublicConfig() {
|
||||
publicConfigPromise ??= request<PublicConfig>("/api/config", {
|
||||
skipAuth: true,
|
||||
}).catch((error) => {
|
||||
publicConfigPromise = null
|
||||
throw error
|
||||
})
|
||||
return publicConfigPromise
|
||||
}
|
||||
Reference in New Issue
Block a user