发布 v1.10 #5
@@ -832,11 +832,36 @@ public class WorkflowCheckService {
|
||||
continue;
|
||||
}
|
||||
if (workflowDatacenterContentService.isLlmNode(node.type)) {
|
||||
checkLlmUserPrompt(node, issues, issueKeys);
|
||||
checkLlmQueryContext(node, parsed, issues, issueKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验大模型节点的用户提示词,避免空提示词进入运行时解析。
|
||||
*
|
||||
* @param node 大模型节点
|
||||
* @param issues 问题列表
|
||||
* @param issueKeys 问题去重键
|
||||
*/
|
||||
private void checkLlmUserPrompt(
|
||||
NodeView node,
|
||||
List<WorkflowCheckIssue> issues,
|
||||
Set<String> issueKeys) {
|
||||
String userPrompt = node.data == null ? null : node.data.getString("userPrompt");
|
||||
if (!StringUtils.hasText(userPrompt)) {
|
||||
addIssue(
|
||||
issues,
|
||||
issueKeys,
|
||||
"LLM_USER_PROMPT_EMPTY",
|
||||
"大模型节点的用户提示词不能为空",
|
||||
node.id,
|
||||
null,
|
||||
node.name);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkMakeFileNode(NodeView node,
|
||||
List<WorkflowCheckIssue> issues,
|
||||
Set<String> issueKeys) {
|
||||
|
||||
@@ -436,6 +436,41 @@ public class WorkflowCheckServiceTest {
|
||||
assertHasCode(result, "SEARCH_DATASET_INVALID");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证保存阶段拒绝空白的大模型用户提示词。
|
||||
*/
|
||||
@Test
|
||||
public void testSaveShouldBlockBlankLlmUserPrompt() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
JSONObject llmData = data("大模型");
|
||||
llmData.put("userPrompt", " ");
|
||||
String content = workflowJson(
|
||||
array(node("llm-1", "llmNode", null, llmData)),
|
||||
new JSONArray()
|
||||
);
|
||||
|
||||
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.SAVE, null);
|
||||
Assert.assertFalse(result.isPassed());
|
||||
assertHasCode(result, "LLM_USER_PROMPT_EMPTY");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证保存阶段接受有效的大模型用户提示词。
|
||||
*/
|
||||
@Test
|
||||
public void testSaveShouldPassNonBlankLlmUserPrompt() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
JSONObject llmData = data("大模型");
|
||||
llmData.put("userPrompt", "{{start-1.user_input}}");
|
||||
String content = workflowJson(
|
||||
array(node("llm-1", "llmNode", null, llmData)),
|
||||
new JSONArray()
|
||||
);
|
||||
|
||||
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.SAVE, null);
|
||||
Assert.assertTrue(result.isPassed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreExecuteShouldBlockMissingStartOrEnd() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
@@ -453,6 +488,34 @@ public class WorkflowCheckServiceTest {
|
||||
assertHasCode(result, "END_NODE_MISSING");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证执行前校验拒绝空的大模型用户提示词。
|
||||
*/
|
||||
@Test
|
||||
public void testPreExecuteShouldBlockEmptyLlmUserPrompt() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
JSONObject llmData = data("大模型");
|
||||
llmData.put("userPrompt", "");
|
||||
String content = workflowJson(
|
||||
array(
|
||||
node("s1", "startNode", null, data("开始")),
|
||||
node("llm-1", "llmNode", null, llmData),
|
||||
node("e1", "endNode", null, data("结束"))
|
||||
),
|
||||
array(
|
||||
edge("edge-1", "s1", "llm-1"),
|
||||
edge("edge-2", "llm-1", "e1")
|
||||
)
|
||||
);
|
||||
|
||||
WorkflowCheckResult result = service.checkContent(
|
||||
content,
|
||||
WorkflowCheckStage.PRE_EXECUTE,
|
||||
BigInteger.ONE);
|
||||
Assert.assertFalse(result.isPassed());
|
||||
assertHasCode(result, "LLM_USER_PROMPT_EMPTY");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreExecuteShouldPassForSourceOnlySearchDatasetNode() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
|
||||
@@ -52,9 +52,10 @@
|
||||
return param.formType ? [param.formType] : [];
|
||||
});
|
||||
let parameterNameDraft = $state<string | null>(null);
|
||||
let systemLabelDraft = $state<string | null>(null);
|
||||
let displayParamName = $derived.by(() => {
|
||||
if (isSystemStartParam) {
|
||||
return '用户问题';
|
||||
return systemLabelDraft ?? trimString(param.formLabel);
|
||||
}
|
||||
return parameterNameDraft ?? param.name;
|
||||
});
|
||||
@@ -76,6 +77,8 @@
|
||||
|
||||
const applyStartFieldPatch = (fieldKey: string, patch: Record<string, any>) => {
|
||||
const currentFieldId = trimString(param.id);
|
||||
const shouldSyncReferences = Object.prototype.hasOwnProperty.call(patch, 'key')
|
||||
|| Object.prototype.hasOwnProperty.call(patch, 'label');
|
||||
store.updateNodes((nodes) => {
|
||||
const edges = store.getEdges();
|
||||
let nextFieldKey = fieldKey;
|
||||
@@ -98,6 +101,9 @@
|
||||
}
|
||||
};
|
||||
});
|
||||
if (!shouldSyncReferences) {
|
||||
return nextNodes;
|
||||
}
|
||||
return renameStartFieldReferencesInNodes(
|
||||
nextNodes,
|
||||
edges,
|
||||
@@ -172,11 +178,11 @@
|
||||
const updateName = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const newValue = input.value;
|
||||
parameterNameDraft = newValue;
|
||||
if (isStartNodeInputParam) {
|
||||
const normalizedValue = trimString(newValue);
|
||||
if (!normalizedValue) {
|
||||
parameterNameError = '参数名不能为空';
|
||||
parameterNameDraft = newValue;
|
||||
return;
|
||||
}
|
||||
if (!isStartFormFieldKeyAvailable(
|
||||
@@ -185,13 +191,66 @@
|
||||
normalizedValue
|
||||
)) {
|
||||
parameterNameError = '参数名已存在';
|
||||
parameterNameDraft = newValue;
|
||||
return;
|
||||
}
|
||||
}
|
||||
parameterNameError = '';
|
||||
};
|
||||
|
||||
const updateVisibleName = (event: Event) => {
|
||||
if (isSystemStartParam) {
|
||||
systemLabelDraft = (event.target as HTMLInputElement).value;
|
||||
return;
|
||||
}
|
||||
updateName(event);
|
||||
};
|
||||
|
||||
const commitName = () => {
|
||||
if (parameterNameDraft === null) {
|
||||
return;
|
||||
}
|
||||
const draftValue = parameterNameDraft;
|
||||
if (isStartNodeInputParam) {
|
||||
const normalizedValue = trimString(draftValue);
|
||||
if (!normalizedValue) {
|
||||
parameterNameError = '参数名不能为空';
|
||||
return;
|
||||
}
|
||||
if (!isStartFormFieldKeyAvailable(
|
||||
node?.current?.data as Record<string, any>,
|
||||
param.name || '',
|
||||
normalizedValue
|
||||
)) {
|
||||
parameterNameError = '参数名已存在';
|
||||
return;
|
||||
}
|
||||
parameterNameDraft = null;
|
||||
parameterNameError = '';
|
||||
updateParameter('name', newValue);
|
||||
if (normalizedValue !== trimString(param.name)) {
|
||||
updateParameter('name', normalizedValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
parameterNameDraft = null;
|
||||
parameterNameError = '';
|
||||
if (draftValue !== (param.name || '')) {
|
||||
updateParameter('name', draftValue);
|
||||
}
|
||||
};
|
||||
|
||||
const commitVisibleName = () => {
|
||||
if (!isSystemStartParam) {
|
||||
commitName();
|
||||
return;
|
||||
}
|
||||
if (systemLabelDraft === null) {
|
||||
return;
|
||||
}
|
||||
const nextLabel = trimString(systemLabelDraft);
|
||||
systemLabelDraft = null;
|
||||
if (nextLabel !== trimString(param.formLabel)) {
|
||||
updateParameter('formLabel', nextLabel);
|
||||
}
|
||||
};
|
||||
|
||||
const updateRequired = (event: Event) => {
|
||||
@@ -236,11 +295,13 @@
|
||||
|
||||
|
||||
<div class="input-item input-item-name">
|
||||
<Input style="width: 100%;" value={displayParamName} placeholder="请输入参数名称"
|
||||
disabled={param.nameDisabled === true}
|
||||
<Input style="width: 100%;" value={displayParamName}
|
||||
placeholder={isSystemStartParam ? '用户问题' : '请输入参数名称'}
|
||||
disabled={isSystemStartParam ? false : param.nameDisabled === true}
|
||||
aria-invalid={parameterNameError ? 'true' : undefined}
|
||||
aria-describedby={parameterNameError ? parameterNameErrorId : undefined}
|
||||
oninput={updateName} />
|
||||
oninput={updateVisibleName}
|
||||
onchange={commitVisibleName} />
|
||||
{#if parameterNameError}
|
||||
<div id={parameterNameErrorId} class="input-error" role="alert">
|
||||
{parameterNameError}
|
||||
@@ -296,7 +357,7 @@
|
||||
数据标题:
|
||||
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
|
||||
updateParamByEvent('formLabel', event)
|
||||
}} value={param.formLabel} />
|
||||
}} value={param.formLabel} placeholder={isSystemStartParam ? '用户问题' : undefined} />
|
||||
</div>
|
||||
|
||||
<div class="input-more-item">
|
||||
|
||||
@@ -34,18 +34,57 @@ describe('workflow node fields', () => {
|
||||
expect(parameters[0]?.name).toBe('user_input');
|
||||
expect(parameters[0]?.systemReserved).toBe(true);
|
||||
expect(parameters[0]?.required).toBe(true);
|
||||
expect(parameters[0]?.formLabel).toBe('');
|
||||
expect(parameters[0]?.displayName).toBe('流程开始 > 用户问题');
|
||||
expect(initial.nodes[0]?.data?.startFormMeta).toMatchObject({
|
||||
title: '开始问答',
|
||||
submitText: '开始',
|
||||
});
|
||||
expect(initial.nodes[0]?.data?.startFormSchema?.[0]).toMatchObject({
|
||||
key: 'user_input',
|
||||
label: '',
|
||||
type: 'textarea',
|
||||
systemReserved: true,
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves an empty system start title for the default placeholder', () => {
|
||||
const initial = createInitialWorkflowData();
|
||||
const startData = initial.nodes[0]?.data as Record<string, any>;
|
||||
const customized = updateStartFormField(startData, 'user_input', {
|
||||
label: '反洗钱',
|
||||
});
|
||||
const cleared = updateStartFormField(customized, 'user_input', {
|
||||
label: '',
|
||||
});
|
||||
|
||||
expect(customized.startFormSchema?.[0]?.label).toBe('反洗钱');
|
||||
expect(cleared.startFormSchema?.[0]?.label).toBe('');
|
||||
expect((cleared.parameters as any[])?.[0]?.formLabel).toBe('');
|
||||
expect((cleared.parameters as any[])?.[0]?.displayName).toBe(
|
||||
'流程开始 > 用户问题',
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves an explicitly entered default or prefixed system title', () => {
|
||||
const initial = createInitialWorkflowData();
|
||||
const startData = initial.nodes[0]?.data as Record<string, any>;
|
||||
const defaultTitle = updateStartFormField(startData, 'user_input', {
|
||||
label: '用户问题',
|
||||
});
|
||||
const prefixedTitle = updateStartFormField(startData, 'user_input', {
|
||||
label: '用户问题说明',
|
||||
});
|
||||
|
||||
expect(defaultTitle.startFormSchema?.[0]?.label).toBe('用户问题');
|
||||
expect((defaultTitle.parameters as any[])?.[0]?.formLabel).toBe('用户问题');
|
||||
expect(prefixedTitle.startFormSchema?.[0]?.label).toBe('用户问题说明');
|
||||
expect((prefixedTitle.parameters as any[])?.[0]?.formLabel).toBe(
|
||||
'用户问题说明',
|
||||
);
|
||||
});
|
||||
|
||||
it('appends custom start form field into schema source of truth', () => {
|
||||
const initial = createInitialWorkflowData();
|
||||
const startNode = initial.nodes[0]!;
|
||||
@@ -326,6 +365,129 @@ describe('workflow node fields', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('refreshes downstream reference labels when the system title changes', () => {
|
||||
const initial = createInitialWorkflowData();
|
||||
const customStartData = updateStartFormField(
|
||||
initial.nodes[0]?.data as Record<string, any>,
|
||||
'user_input',
|
||||
{ label: '反洗钱' },
|
||||
);
|
||||
const clearedStartData = updateStartFormField(
|
||||
customStartData,
|
||||
'user_input',
|
||||
{ label: '' },
|
||||
);
|
||||
const startNode: Node = {
|
||||
id: 'start_1',
|
||||
type: 'startNode',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
title: '开始节点',
|
||||
...clearedStartData,
|
||||
},
|
||||
};
|
||||
const llmNode: Node = {
|
||||
id: 'llm_1',
|
||||
type: 'llmNode',
|
||||
position: { x: 120, y: 0 },
|
||||
data: {
|
||||
title: '大模型',
|
||||
userPrompt: '',
|
||||
parameters: [
|
||||
{
|
||||
id: 'param_1',
|
||||
name: 'start_1.user_input',
|
||||
ref: 'start_1.user_input',
|
||||
refType: 'ref',
|
||||
autoManaged: true,
|
||||
formLabel: '开始节点 > 反洗钱',
|
||||
displayName: '开始节点 > 反洗钱',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const endNode: Node = {
|
||||
id: 'end_1',
|
||||
type: 'endNode',
|
||||
position: { x: 240, y: 0 },
|
||||
data: {
|
||||
title: '结束节点',
|
||||
parameters: [
|
||||
{
|
||||
id: 'param_2',
|
||||
name: 'start_1.user_input',
|
||||
ref: 'start_1.user_input',
|
||||
refType: 'ref',
|
||||
formLabel: '开始节点 > 反洗钱',
|
||||
displayName: '开始节点 > 反洗钱',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const edges: Edge[] = [
|
||||
{ id: 'edge_1', source: 'start_1', target: 'llm_1' } as Edge,
|
||||
{ id: 'edge_2', source: 'llm_1', target: 'end_1' } as Edge,
|
||||
];
|
||||
|
||||
const nextNodes = renameStartFieldReferencesInNodes(
|
||||
[startNode, llmNode, endNode],
|
||||
edges,
|
||||
'start_1',
|
||||
'user_input',
|
||||
'user_input',
|
||||
);
|
||||
const nextLlmNode = nextNodes.find((node) => node.id === 'llm_1')!;
|
||||
const nextParameter = (nextLlmNode.data?.parameters as any[])?.[0];
|
||||
const nextEndParameter = (
|
||||
nextNodes.find((node) => node.id === 'end_1')?.data?.parameters as any[]
|
||||
)?.[0];
|
||||
|
||||
expect(nextLlmNode.data?.userPrompt).toBe('');
|
||||
expect(nextParameter?.name).toBe('start_1.user_input');
|
||||
expect(nextParameter?.ref).toBe('start_1.user_input');
|
||||
expect(nextParameter?.displayName).toBe('开始节点 > 用户问题');
|
||||
expect(nextParameter?.formLabel).toBe('开始节点 > 用户问题');
|
||||
expect(nextEndParameter?.displayName).toBe('开始节点 > 用户问题');
|
||||
expect(nextEndParameter?.formLabel).toBe('开始节点 > 用户问题');
|
||||
expect((nextLlmNode.data as any)?.[FIELD_BINDING_META_KEY]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves node identities when a title sync has nothing to update', () => {
|
||||
const initial = createInitialWorkflowData();
|
||||
const startNode: Node = {
|
||||
...initial.nodes[0]!,
|
||||
id: 'start_1',
|
||||
};
|
||||
const untouchedNode: Node = {
|
||||
id: 'code_1',
|
||||
type: 'codeNode',
|
||||
position: { x: 120, y: 0 },
|
||||
data: {
|
||||
title: '代码节点',
|
||||
parameters: [
|
||||
{
|
||||
id: 'param_1',
|
||||
name: 'manual_value',
|
||||
displayName: '手工参数',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const nodes = [startNode, untouchedNode];
|
||||
|
||||
const nextNodes = renameStartFieldReferencesInNodes(
|
||||
nodes,
|
||||
[],
|
||||
'start_1',
|
||||
'user_input',
|
||||
'user_input',
|
||||
);
|
||||
|
||||
expect(nextNodes).toBe(nodes);
|
||||
expect(nextNodes[0]).toBe(startNode);
|
||||
expect(nextNodes[1]).toBe(untouchedNode);
|
||||
});
|
||||
|
||||
it('preserves resource content type independently from input type', () => {
|
||||
for (const contentType of ['image', 'video', 'audio', 'other'] as const) {
|
||||
const key = `preview_${contentType}`;
|
||||
|
||||
@@ -184,6 +184,9 @@ function getNodeTitle(node?: Node | null) {
|
||||
}
|
||||
|
||||
function getParameterLabel(parameter?: Parameter | null) {
|
||||
if (isSystemStartParameter(parameter)) {
|
||||
return trimString(parameter?.formLabel) || SYSTEM_START_PARAM_LABEL;
|
||||
}
|
||||
return (
|
||||
asString(parameter?.formLabel).trim() ||
|
||||
asString(parameter?.displayName).trim() ||
|
||||
@@ -415,6 +418,14 @@ function normalizeSystemStartFormType(value: unknown): 'input' | 'textarea' {
|
||||
return trimString(value) === 'input' ? 'input' : 'textarea';
|
||||
}
|
||||
|
||||
function normalizeSystemStartFormLabel(value: unknown) {
|
||||
return trimString(value);
|
||||
}
|
||||
|
||||
function getSystemStartDisplayLabel(value: unknown) {
|
||||
return normalizeSystemStartFormLabel(value) || SYSTEM_START_PARAM_LABEL;
|
||||
}
|
||||
|
||||
function normalizeStartFormFieldUiType(
|
||||
value: unknown,
|
||||
fallback: StartFormFieldType = 'text',
|
||||
@@ -519,8 +530,8 @@ export function normalizeStartFormMeta(
|
||||
function normalizeSystemStartParameter(
|
||||
parameter?: Parameter | null,
|
||||
): Parameter {
|
||||
const fallbackLabel = SYSTEM_START_PARAM_LABEL;
|
||||
const formLabel = trimString(parameter?.formLabel) || fallbackLabel;
|
||||
const formLabel = normalizeSystemStartFormLabel(parameter?.formLabel);
|
||||
const displayLabel = getSystemStartDisplayLabel(formLabel);
|
||||
const formType = normalizeSystemStartFormType(parameter?.formType);
|
||||
return ensureParameterId({
|
||||
...cloneParameter(parameter || {}),
|
||||
@@ -534,7 +545,7 @@ function normalizeSystemStartParameter(
|
||||
formDescription: asString(parameter?.formDescription),
|
||||
formPlaceholder: trimString(parameter?.formPlaceholder) || '请输入用户问题',
|
||||
defaultValue: asString(parameter?.defaultValue),
|
||||
displayName: `流程开始 > ${formLabel}`,
|
||||
displayName: `流程开始 > ${displayLabel}`,
|
||||
nameDisabled: true,
|
||||
dataTypeDisabled: true,
|
||||
deleteDisabled: true,
|
||||
@@ -637,8 +648,15 @@ function normalizeStartFormField(
|
||||
const options = isOptionFieldType(type)
|
||||
? ensureStringArray(field?.options || existingParameter?.enums)
|
||||
: [];
|
||||
const requestedLabel =
|
||||
trimString(field?.label) ||
|
||||
const hasFieldLabel = Object.prototype.hasOwnProperty.call(
|
||||
field || {},
|
||||
'label',
|
||||
);
|
||||
const requestedLabel = isSystemField
|
||||
? normalizeSystemStartFormLabel(
|
||||
hasFieldLabel ? field?.label : existingParameter?.formLabel,
|
||||
)
|
||||
: trimString(field?.label) ||
|
||||
trimString(existingParameter?.formLabel) ||
|
||||
trimString(existingParameter?.displayName) ||
|
||||
key;
|
||||
@@ -796,8 +814,7 @@ export function normalizeStartFormSchema(
|
||||
{
|
||||
id: trimString(systemParameter?.id),
|
||||
key: SYSTEM_START_PARAM_NAME,
|
||||
label:
|
||||
trimString(systemParameter?.formLabel) || SYSTEM_START_PARAM_LABEL,
|
||||
label: normalizeSystemStartFormLabel(systemParameter?.formLabel),
|
||||
type:
|
||||
normalizeSystemStartFormType(systemParameter?.formType) === 'input'
|
||||
? 'text'
|
||||
@@ -933,6 +950,7 @@ export function updateStartFormField(
|
||||
!trimString(patch.label) &&
|
||||
patch.type != null &&
|
||||
isDefaultStartFormFieldLabel(field.label);
|
||||
const hasLabelPatch = Object.prototype.hasOwnProperty.call(patch, 'label');
|
||||
return {
|
||||
...field,
|
||||
...patch,
|
||||
@@ -947,7 +965,9 @@ export function updateStartFormField(
|
||||
)
|
||||
: field.key),
|
||||
label:
|
||||
trimString(patch.label) ||
|
||||
field.systemReserved && hasLabelPatch
|
||||
? normalizeSystemStartFormLabel(patch.label)
|
||||
: trimString(patch.label) ||
|
||||
(shouldAutoRenameLabel
|
||||
? getDefaultStartFormFieldLabel(nextType)
|
||||
: field.label),
|
||||
@@ -1450,28 +1470,115 @@ function replaceStartFieldReferenceValue(
|
||||
|
||||
export function collectDownstreamNodeIds(rootNodeId: string, edges: Edge[]) {
|
||||
const nodeIds = new Set<string>();
|
||||
const adjacency = new Map<string, string[]>();
|
||||
for (const edge of edges) {
|
||||
if (!edge.source || !edge.target || edge.sourceHandle === 'loop_handle') {
|
||||
continue;
|
||||
}
|
||||
const targets = adjacency.get(edge.source);
|
||||
if (targets) {
|
||||
targets.push(edge.target);
|
||||
} else {
|
||||
adjacency.set(edge.source, [edge.target]);
|
||||
}
|
||||
}
|
||||
|
||||
const visit = (nodeId: string) => {
|
||||
const pendingNodeIds = [rootNodeId];
|
||||
while (pendingNodeIds.length > 0) {
|
||||
const nodeId = pendingNodeIds.pop();
|
||||
if (!nodeId || nodeIds.has(nodeId)) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
nodeIds.add(nodeId);
|
||||
edges
|
||||
.filter(
|
||||
(edge) => edge.source === nodeId && edge.sourceHandle !== 'loop_handle',
|
||||
)
|
||||
.forEach((edge) => {
|
||||
if (edge.target) {
|
||||
visit(edge.target);
|
||||
const targets = adjacency.get(nodeId) || [];
|
||||
for (let index = targets.length - 1; index >= 0; index -= 1) {
|
||||
pendingNodeIds.push(targets[index]!);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
visit(rootNodeId);
|
||||
nodeIds.delete(rootNodeId);
|
||||
return Array.from(nodeIds);
|
||||
}
|
||||
|
||||
function syncReferenceParameterLabels(
|
||||
parameters: Parameter[],
|
||||
refPath: string,
|
||||
displayName: string,
|
||||
): Parameter[] {
|
||||
let changed = false;
|
||||
const nextParameters = parameters.map((parameter) => {
|
||||
const currentChildren = Array.isArray(parameter.children)
|
||||
? parameter.children
|
||||
: [];
|
||||
const nextChildren = currentChildren.length
|
||||
? syncReferenceParameterLabels(currentChildren, refPath, displayName)
|
||||
: currentChildren;
|
||||
const matchesReference =
|
||||
(parameter.autoManaged === true || parameter.refType === 'ref') &&
|
||||
(trimString(parameter.name) === refPath ||
|
||||
trimString(parameter.ref) === refPath);
|
||||
const labelChanged =
|
||||
matchesReference &&
|
||||
(parameter.displayName !== displayName ||
|
||||
parameter.formLabel !== displayName);
|
||||
const childrenChanged = nextChildren !== currentChildren;
|
||||
if (!labelChanged && !childrenChanged) {
|
||||
return parameter;
|
||||
}
|
||||
changed = true;
|
||||
return {
|
||||
...parameter,
|
||||
...(labelChanged ? { displayName, formLabel: displayName } : {}),
|
||||
...(childrenChanged ? { children: nextChildren } : {}),
|
||||
};
|
||||
});
|
||||
return changed ? nextParameters : parameters;
|
||||
}
|
||||
|
||||
function syncStartFieldReferenceLabelsInNodes(
|
||||
nodes: Node[],
|
||||
startNodeId: string,
|
||||
fieldKey: string,
|
||||
) {
|
||||
const startNode = nodes.find((node) => node.id === startNodeId);
|
||||
const startParameters = Array.isArray(startNode?.data?.parameters)
|
||||
? (startNode.data.parameters as Parameter[])
|
||||
: [];
|
||||
const sourceParameter = findParameterByName(startParameters, fieldKey);
|
||||
if (!startNode || !sourceParameter) {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
const refPath = `${startNodeId}.${fieldKey}`;
|
||||
const displayName = `${getNodeTitle(startNode)} > ${getReferenceParameterLabel(sourceParameter)}`;
|
||||
let changed = false;
|
||||
const nextNodes = nodes.map((node) => {
|
||||
const parameters = Array.isArray(node.data?.parameters)
|
||||
? (node.data.parameters as Parameter[])
|
||||
: [];
|
||||
if (parameters.length === 0) {
|
||||
return node;
|
||||
}
|
||||
const nextParameters = syncReferenceParameterLabels(
|
||||
parameters,
|
||||
refPath,
|
||||
displayName,
|
||||
);
|
||||
if (nextParameters === parameters) {
|
||||
return node;
|
||||
}
|
||||
changed = true;
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...((node.data || {}) as Record<string, any>),
|
||||
parameters: nextParameters,
|
||||
},
|
||||
};
|
||||
});
|
||||
return changed ? nextNodes : nodes;
|
||||
}
|
||||
|
||||
export function renameStartFieldReferencesInNodes(
|
||||
nodes: Node[],
|
||||
edges: Edge[],
|
||||
@@ -1482,19 +1589,16 @@ export function renameStartFieldReferencesInNodes(
|
||||
const normalizedStartNodeId = trimString(startNodeId);
|
||||
const normalizedCurrentKey = trimString(currentKey);
|
||||
const normalizedNextKey = trimString(nextKey);
|
||||
if (
|
||||
!normalizedStartNodeId ||
|
||||
!normalizedCurrentKey ||
|
||||
!normalizedNextKey ||
|
||||
normalizedCurrentKey === normalizedNextKey
|
||||
) {
|
||||
if (!normalizedStartNodeId || !normalizedCurrentKey || !normalizedNextKey) {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
const oldRefPath = `${normalizedStartNodeId}.${normalizedCurrentKey}`;
|
||||
const newRefPath = `${normalizedStartNodeId}.${normalizedNextKey}`;
|
||||
|
||||
const nextNodes: Node[] = nodes.map((node) => {
|
||||
const keyChanged = normalizedCurrentKey !== normalizedNextKey;
|
||||
const renamedNodes: Node[] = keyChanged
|
||||
? nodes.map((node) => {
|
||||
if (node.id === normalizedStartNodeId) {
|
||||
return node;
|
||||
}
|
||||
@@ -1510,7 +1614,16 @@ export function renameStartFieldReferencesInNodes(
|
||||
...node,
|
||||
data: nextData ?? {},
|
||||
};
|
||||
});
|
||||
})
|
||||
: nodes;
|
||||
const nextNodes = syncStartFieldReferenceLabelsInNodes(
|
||||
renamedNodes,
|
||||
normalizedStartNodeId,
|
||||
normalizedNextKey,
|
||||
);
|
||||
if (!keyChanged) {
|
||||
return nextNodes;
|
||||
}
|
||||
|
||||
const affectedNodeIds = collectDownstreamNodeIds(
|
||||
normalizedStartNodeId,
|
||||
|
||||
Reference in New Issue
Block a user