发布 v1.10 #5
@@ -241,7 +241,9 @@ public class WorkflowExecutionAuditConsumer implements MQConsumerHandler {
|
|||||||
"iterationCount"))
|
"iterationCount"))
|
||||||
.intValue(),
|
.intValue(),
|
||||||
String.valueOf(
|
String.valueOf(
|
||||||
map.get("outputName")));
|
map.get("outputName")),
|
||||||
|
Boolean.TRUE.equals(
|
||||||
|
map.get("flattenAggregation")));
|
||||||
}
|
}
|
||||||
Map<Object, Object> restored =
|
Map<Object, Object> restored =
|
||||||
new LinkedHashMap<>();
|
new LinkedHashMap<>();
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ public class WorkflowCheckService {
|
|||||||
for (NodeView node : nodes) {
|
for (NodeView node : nodes) {
|
||||||
checkConfiguredLoopCount(node, issues, issueKeys);
|
checkConfiguredLoopCount(node, issues, issueKeys);
|
||||||
checkExplicitLoopInputs(node, issues, issueKeys);
|
checkExplicitLoopInputs(node, issues, issueKeys);
|
||||||
|
checkLoopOutputAggregations(node, issues, issueKeys);
|
||||||
if (StringUtils.hasText(node.parentId)) {
|
if (StringUtils.hasText(node.parentId)) {
|
||||||
NodeView parent = nodeMap.get(node.parentId);
|
NodeView parent = nodeMap.get(node.parentId);
|
||||||
if (parent != null && !TYPE_LOOP.equals(parent.type)) {
|
if (parent != null && !TYPE_LOOP.equals(parent.type)) {
|
||||||
@@ -605,6 +606,51 @@ public class WorkflowCheckService {
|
|||||||
true, 0, "Array<", 0, "Array<".length()));
|
true, 0, "Array<", 0, "Array<".length()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验循环输出的扁平聚合只作用于数组引用。
|
||||||
|
*
|
||||||
|
* @param node 循环节点
|
||||||
|
* @param issues 问题列表
|
||||||
|
* @param issueKeys 问题去重键
|
||||||
|
*/
|
||||||
|
private void checkLoopOutputAggregations(
|
||||||
|
NodeView node,
|
||||||
|
List<WorkflowCheckIssue> issues,
|
||||||
|
Set<String> issueKeys) {
|
||||||
|
if (!TYPE_LOOP.equals(node.type) || node.data == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONArray outputDefs = node.data.getJSONArray("outputDefs");
|
||||||
|
if (outputDefs == null || outputDefs.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < outputDefs.size(); index++) {
|
||||||
|
JSONObject outputDef = outputDefs.getJSONObject(index);
|
||||||
|
if (outputDef == null
|
||||||
|
|| !outputDef.getBooleanValue("flattenAggregation")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String refType = trimToNull(outputDef.getString("refType"));
|
||||||
|
String ref = trimToNull(outputDef.getString("ref"));
|
||||||
|
String dataType = trimToNull(outputDef.getString("dataType"));
|
||||||
|
if ("ref".equals(refType)
|
||||||
|
&& StringUtils.hasText(ref)
|
||||||
|
&& isArrayDataType(dataType)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String outputName = trimToNull(outputDef.getString("name"));
|
||||||
|
addIssue(
|
||||||
|
issues,
|
||||||
|
issueKeys,
|
||||||
|
"LOOP_OUTPUT_FLATTEN_TYPE_INVALID",
|
||||||
|
"循环输出参数[" + safe(outputName)
|
||||||
|
+ "]启用扁平聚合时必须引用数组变量",
|
||||||
|
node.id,
|
||||||
|
null,
|
||||||
|
node.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验 index 和 loopItem 仅在所属循环体内引用。
|
* 校验 index 和 loopItem 仅在所属循环体内引用。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -627,11 +627,11 @@ public class WorkflowExecutionAuditConsumerTest {
|
|||||||
loopRepository.append(
|
loopRepository.append(
|
||||||
resultId,
|
resultId,
|
||||||
0,
|
0,
|
||||||
Map.of("answer", "first"));
|
Map.of("answer", List.of("first", "second")));
|
||||||
loopRepository.append(
|
loopRepository.append(
|
||||||
resultId,
|
resultId,
|
||||||
1,
|
1,
|
||||||
Map.of("answer", "second"));
|
Map.of("answer", List.of("third")));
|
||||||
WorkflowExecutionAuditConsumer consumer =
|
WorkflowExecutionAuditConsumer consumer =
|
||||||
new WorkflowExecutionAuditConsumer(
|
new WorkflowExecutionAuditConsumer(
|
||||||
resultService,
|
resultService,
|
||||||
@@ -650,7 +650,8 @@ public class WorkflowExecutionAuditConsumerTest {
|
|||||||
new LoopResultReference(
|
new LoopResultReference(
|
||||||
resultId,
|
resultId,
|
||||||
2,
|
2,
|
||||||
"answer"));
|
"answer",
|
||||||
|
true));
|
||||||
WorkflowExecStep incomingStep =
|
WorkflowExecStep incomingStep =
|
||||||
new WorkflowExecStep();
|
new WorkflowExecStep();
|
||||||
incomingStep.setExecKey("step-loop");
|
incomingStep.setExecKey("step-loop");
|
||||||
@@ -700,10 +701,10 @@ public class WorkflowExecutionAuditConsumerTest {
|
|||||||
resultCaptor.getValue().getOutput(),
|
resultCaptor.getValue().getOutput(),
|
||||||
Map.class);
|
Map.class);
|
||||||
Assert.assertEquals(
|
Assert.assertEquals(
|
||||||
List.of("first", "second"),
|
List.of("first", "second", "third"),
|
||||||
stepOutput.get("answers"));
|
stepOutput.get("answers"));
|
||||||
Assert.assertEquals(
|
Assert.assertEquals(
|
||||||
List.of("first", "second"),
|
List.of("first", "second", "third"),
|
||||||
resultOutput.get("answers"));
|
resultOutput.get("answers"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -221,6 +221,55 @@ public class WorkflowCheckServiceTest {
|
|||||||
assertHasCode(result, "EXPLICIT_LOOP_ITEMS_TYPE_INVALID");
|
assertHasCode(result, "EXPLICIT_LOOP_ITEMS_TYPE_INVALID");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证循环数组输出允许启用扁平聚合。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testSaveShouldPassArrayLoopOutputFlattenAggregation() throws Exception {
|
||||||
|
WorkflowCheckService service = newService(new HashMap<>());
|
||||||
|
JSONObject loopData = loopData(
|
||||||
|
fixedParameter("count", "2", "Number"), null);
|
||||||
|
JSONObject output = refParameter(
|
||||||
|
"res",
|
||||||
|
"knowledge.documents.content",
|
||||||
|
"Array<String>");
|
||||||
|
output.put("flattenAggregation", true);
|
||||||
|
loopData.put("outputDefs", array(output));
|
||||||
|
String content = workflowJson(
|
||||||
|
array(node("loop-1", "loopNode", null, loopData)),
|
||||||
|
new JSONArray());
|
||||||
|
|
||||||
|
WorkflowCheckResult result = service.checkContent(
|
||||||
|
content, WorkflowCheckStage.SAVE, null);
|
||||||
|
|
||||||
|
Assert.assertTrue(result.isPassed());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证循环标量输出不能启用扁平聚合。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testSaveShouldBlockScalarLoopOutputFlattenAggregation() throws Exception {
|
||||||
|
WorkflowCheckService service = newService(new HashMap<>());
|
||||||
|
JSONObject loopData = loopData(
|
||||||
|
fixedParameter("count", "2", "Number"), null);
|
||||||
|
JSONObject output = refParameter(
|
||||||
|
"res",
|
||||||
|
"child.output",
|
||||||
|
"String");
|
||||||
|
output.put("flattenAggregation", true);
|
||||||
|
loopData.put("outputDefs", array(output));
|
||||||
|
String content = workflowJson(
|
||||||
|
array(node("loop-1", "loopNode", null, loopData)),
|
||||||
|
new JSONArray());
|
||||||
|
|
||||||
|
WorkflowCheckResult result = service.checkContent(
|
||||||
|
content, WorkflowCheckStage.SAVE, null);
|
||||||
|
|
||||||
|
Assert.assertFalse(result.isPassed());
|
||||||
|
assertHasCode(result, "LOOP_OUTPUT_FLATTEN_TYPE_INVALID");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证新旧循环输入不能同时提交。
|
* 验证新旧循环输入不能同时提交。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import FloatingTrigger from './floating-trigger.svelte';
|
|||||||
import Heading from './heading.svelte';
|
import Heading from './heading.svelte';
|
||||||
import MenuButton from './menu-button.svelte';
|
import MenuButton from './menu-button.svelte';
|
||||||
import MixedInput from './mixed-input.svelte';
|
import MixedInput from './mixed-input.svelte';
|
||||||
|
import InfoTooltip from './info-tooltip.svelte';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Button,
|
Button,
|
||||||
@@ -26,4 +27,5 @@ export {
|
|||||||
Heading,
|
Heading,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
MixedInput,
|
MixedInput,
|
||||||
|
InfoTooltip,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
const {
|
||||||
|
text,
|
||||||
|
label = '查看说明',
|
||||||
|
}: {
|
||||||
|
text: string;
|
||||||
|
label?: string;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tf-info-tooltip nopan nodrag"
|
||||||
|
aria-label={label}
|
||||||
|
data-help={text}
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
<path d="M12 10.75V16" />
|
||||||
|
<circle cx="12" cy="7.75" r="0.75" class="tf-info-tooltip-dot" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.tf-info-tooltip {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 16px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--tf-text-secondary);
|
||||||
|
cursor: help;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
overflow: visible;
|
||||||
|
fill: none;
|
||||||
|
stroke: currentcolor;
|
||||||
|
stroke-width: 1.8;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tf-info-tooltip-dot {
|
||||||
|
fill: currentcolor;
|
||||||
|
stroke: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: var(--tf-primary-color);
|
||||||
|
background: var(--tf-primary-soft-bg);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 2px var(--tf-primary-soft-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 8px);
|
||||||
|
left: 0;
|
||||||
|
z-index: 120;
|
||||||
|
width: 280px;
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--tf-tip-text);
|
||||||
|
text-align: left;
|
||||||
|
white-space: normal;
|
||||||
|
pointer-events: none;
|
||||||
|
visibility: hidden;
|
||||||
|
content: attr(data-help);
|
||||||
|
background: var(--tf-tip-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--tf-shadow-medium);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::after,
|
||||||
|
&:focus::after {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
placeholder,
|
placeholder,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
showSelectedType = true,
|
showSelectedType = true,
|
||||||
|
selectedType,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
items: SelectItem[],
|
items: SelectItem[],
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
placeholder?: string
|
placeholder?: string
|
||||||
variant?: 'default' | 'reference' | 'model'
|
variant?: 'default' | 'reference' | 'model'
|
||||||
showSelectedType?: boolean
|
showSelectedType?: boolean
|
||||||
|
selectedType?: string
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
@@ -184,10 +186,10 @@
|
|||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="tf-parameter-name">{item.displayLabel || item.label}</span>
|
<span class="tf-parameter-name" title={String(item.displayLabel || item.label)}>{item.displayLabel || item.label}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if item.dataType}
|
{#if item.dataType}
|
||||||
<span class="tf-parameter-type">{item.dataType}</span>
|
<span class="tf-parameter-type" title={item.dataType}>{item.dataType}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if item.itemTypeLabel}
|
{#if item.itemTypeLabel}
|
||||||
<span class="tf-parameter-meta">{item.itemTypeLabel}</span>
|
<span class="tf-parameter-meta">{item.itemTypeLabel}</span>
|
||||||
@@ -230,9 +232,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="tf-parameter-name">{item.displayLabel || item.label}</span>
|
<span class="tf-parameter-name" title={String(item.displayLabel || item.label)}>{item.displayLabel || item.label}</span>
|
||||||
{#if variant === 'reference' && showSelectedType && item.dataType}
|
{#if variant === 'reference' && showSelectedType && (selectedType ?? item.dataType)}
|
||||||
<span class="tf-parameter-type">{item.dataType}</span>
|
<span class="tf-parameter-type" title={selectedType ?? item.dataType}>{selectedType ?? item.dataType}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -251,9 +253,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="tf-parameter-name">{item.displayLabel || item.label}</span>
|
<span class="tf-parameter-name" title={String(item.displayLabel || item.label)}>{item.displayLabel || item.label}</span>
|
||||||
{#if variant === 'reference' && showSelectedType && item.dataType}
|
{#if variant === 'reference' && showSelectedType && (selectedType ?? item.dataType)}
|
||||||
<span class="tf-parameter-type">{item.dataType}</span>
|
<span class="tf-parameter-type" title={selectedType ?? item.dataType}>{selectedType ?? item.dataType}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if index < activeItemsState.length - 1}
|
{#if index < activeItemsState.length - 1}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Input, MenuButton, Textarea} from '../base';
|
import {Checkbox, InfoTooltip, Input, MenuButton, Textarea} from '../base';
|
||||||
import {Button, FloatingTrigger, Select} from '../base/index.js';
|
import {Button, FloatingTrigger, Select} from '../base/index.js';
|
||||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||||
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
|
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
|
||||||
@@ -7,6 +7,10 @@
|
|||||||
import {useRefOptions} from '../utils/useRefOptions.svelte';
|
import {useRefOptions} from '../utils/useRefOptions.svelte';
|
||||||
import {onMount} from 'svelte';
|
import {onMount} from 'svelte';
|
||||||
import type {Parameter} from '#types';
|
import type {Parameter} from '#types';
|
||||||
|
import {
|
||||||
|
isArrayDataType,
|
||||||
|
resolveLoopOutputDataType,
|
||||||
|
} from '../../utils/loopScope';
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!param.refType) {
|
if (!param.refType) {
|
||||||
@@ -21,7 +25,8 @@
|
|||||||
useChildrenOnly,
|
useChildrenOnly,
|
||||||
showContentType = false,
|
showContentType = false,
|
||||||
fixedNumberMin,
|
fixedNumberMin,
|
||||||
fixedNumberMax
|
fixedNumberMax,
|
||||||
|
loopOutputAggregation = false
|
||||||
}: {
|
}: {
|
||||||
parameter: Parameter,
|
parameter: Parameter,
|
||||||
index: number,
|
index: number,
|
||||||
@@ -30,6 +35,7 @@
|
|||||||
showContentType?: boolean,
|
showContentType?: boolean,
|
||||||
fixedNumberMin?: number,
|
fixedNumberMin?: number,
|
||||||
fixedNumberMax?: number,
|
fixedNumberMax?: number,
|
||||||
|
loopOutputAggregation?: boolean,
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
|
||||||
@@ -44,12 +50,12 @@
|
|||||||
|
|
||||||
const { updateNodeData } = useSvelteFlow();
|
const { updateNodeData } = useSvelteFlow();
|
||||||
|
|
||||||
const updateParam = (key: string, value: any) => {
|
const updateParams = (patch: Partial<Parameter>) => {
|
||||||
updateNodeData(currentNodeId, (node) => {
|
updateNodeData(currentNodeId, (node) => {
|
||||||
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
|
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
|
||||||
parameters[index] = {
|
parameters[index] = {
|
||||||
...parameters[index],
|
...parameters[index],
|
||||||
[key]: value
|
...patch
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
[dataKeyName]: [...parameters]
|
[dataKeyName]: [...parameters]
|
||||||
@@ -57,6 +63,9 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateParam = (key: string, value: any) => {
|
||||||
|
updateParams({[key]: value});
|
||||||
|
};
|
||||||
|
|
||||||
const updateParamByEvent = (name: string, event: Event) => {
|
const updateParamByEvent = (name: string, event: Event) => {
|
||||||
const newValue = (event.target as any).value;
|
const newValue = (event.target as any).value;
|
||||||
@@ -99,13 +108,35 @@
|
|||||||
|
|
||||||
const updateRef = (item: any) => {
|
const updateRef = (item: any) => {
|
||||||
const newValue = item.value;
|
const newValue = item.value;
|
||||||
|
if (!loopOutputAggregation) {
|
||||||
updateParam('ref', newValue);
|
updateParam('ref', newValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dataType = item.dataType || 'String';
|
||||||
|
updateParams({
|
||||||
|
ref: newValue,
|
||||||
|
dataType,
|
||||||
|
flattenAggregation:
|
||||||
|
isArrayDataType(dataType)
|
||||||
|
? Boolean(param.flattenAggregation)
|
||||||
|
: false
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const updateRefType = (item: any) => {
|
const updateRefType = (item: any) => {
|
||||||
const newValue = item.value;
|
const newValue = item.value;
|
||||||
|
if (!loopOutputAggregation) {
|
||||||
updateParam('refType', newValue);
|
updateParam('refType', newValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateParams({
|
||||||
|
refType: newValue,
|
||||||
|
flattenAggregation:
|
||||||
|
newValue !== 'ref'
|
||||||
|
? false
|
||||||
|
: Boolean(param.flattenAggregation)
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateContentType = (item: any) => {
|
const updateContentType = (item: any) => {
|
||||||
@@ -125,7 +156,29 @@
|
|||||||
});
|
});
|
||||||
triggerObject?.hide();
|
triggerObject?.hide();
|
||||||
};
|
};
|
||||||
let selectItems = useRefOptions(() => useChildrenOnly === true);
|
let selectItems = useRefOptions(
|
||||||
|
() => useChildrenOnly === true,
|
||||||
|
() => param.ref || ''
|
||||||
|
);
|
||||||
|
let sourceDataType = $derived.by(() => {
|
||||||
|
return selectItems.selected?.dataType || param.dataType || 'String';
|
||||||
|
});
|
||||||
|
let canFlattenAggregation = $derived.by(() => {
|
||||||
|
return isArrayDataType(sourceDataType);
|
||||||
|
});
|
||||||
|
let loopOutputDataType = $derived.by(() => {
|
||||||
|
return resolveLoopOutputDataType(
|
||||||
|
sourceDataType,
|
||||||
|
canFlattenAggregation && Boolean(param.flattenAggregation)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const updateFlattenAggregation = (event: Event) => {
|
||||||
|
const checked = (event.target as HTMLInputElement).checked;
|
||||||
|
updateParams({
|
||||||
|
dataType: sourceDataType,
|
||||||
|
flattenAggregation: canFlattenAggregation && checked
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@@ -153,11 +206,25 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if (param.refType !== 'input')}
|
{:else if (param.refType !== 'input')}
|
||||||
<Select items={selectItems.current} style="width: 100%" defaultValue={["ref"]} value={[param.ref]} variant="reference"
|
<Select items={selectItems.current} style="width: 100%" defaultValue={["ref"]} value={[param.ref]} variant="reference"
|
||||||
|
selectedType={loopOutputAggregation ? loopOutputDataType : undefined}
|
||||||
expandAll
|
expandAll
|
||||||
onSelect={updateRef} />
|
onSelect={updateRef} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="input-item">
|
<div class="input-item input-actions">
|
||||||
|
{#if loopOutputAggregation && param.refType === 'ref' && param.ref && canFlattenAggregation}
|
||||||
|
<label class="flatten-aggregation-control">
|
||||||
|
<Checkbox
|
||||||
|
checked={Boolean(param.flattenAggregation)}
|
||||||
|
onchange={updateFlattenAggregation}
|
||||||
|
/>
|
||||||
|
<span>扁平聚合</span>
|
||||||
|
</label>
|
||||||
|
<InfoTooltip
|
||||||
|
label="查看扁平聚合说明"
|
||||||
|
text="关闭时按轮次保留数组;开启后按循环顺序合并,只扁平一层。例如 [[A, B], [C]] 会变为 [A, B, C],轮次分组信息将丢失。"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<FloatingTrigger placement="bottom" bind:this={triggerObject}>
|
<FloatingTrigger placement="bottom" bind:this={triggerObject}>
|
||||||
<MenuButton />
|
<MenuButton />
|
||||||
{#snippet floating()}
|
{#snippet floating()}
|
||||||
@@ -209,6 +276,12 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-actions {
|
||||||
|
gap: 5px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.fixed-value {
|
.fixed-value {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -243,4 +316,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flatten-aggregation-control {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--tf-text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
:global(.tf-checkbox:focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--tf-primary-soft-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
useChildrenOnly,
|
useChildrenOnly,
|
||||||
showContentType = false,
|
showContentType = false,
|
||||||
fixedNumberMin,
|
fixedNumberMin,
|
||||||
fixedNumberMax
|
fixedNumberMax,
|
||||||
|
loopOutputAggregation = false
|
||||||
}: {
|
}: {
|
||||||
noneParameterText?: string;
|
noneParameterText?: string;
|
||||||
dataKeyName?: string;
|
dataKeyName?: string;
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
showContentType?: boolean,
|
showContentType?: boolean,
|
||||||
fixedNumberMin?: number,
|
fixedNumberMin?: number,
|
||||||
fixedNumberMax?: number,
|
fixedNumberMax?: number,
|
||||||
|
loopOutputAggregation?: boolean,
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let currentNodeId = getCurrentNodeId();
|
let currentNodeId = getCurrentNodeId();
|
||||||
@@ -28,7 +30,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div class="input-container">
|
<div class:loop-output-aggregation={loopOutputAggregation} class="input-container">
|
||||||
{#if (parameters.length !== 0)}
|
{#if (parameters.length !== 0)}
|
||||||
<div class="input-header">参数名称</div>
|
<div class="input-header">参数名称</div>
|
||||||
<div class="input-header">参数值</div>
|
<div class="input-header">参数值</div>
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
{showContentType}
|
{showContentType}
|
||||||
{fixedNumberMin}
|
{fixedNumberMin}
|
||||||
{fixedNumberMax}
|
{fixedNumberMax}
|
||||||
|
{loopOutputAggregation}
|
||||||
/>
|
/>
|
||||||
{:else }
|
{:else }
|
||||||
<div class="none-params">{noneParameterText}</div>
|
<div class="none-params">{noneParameterText}</div>
|
||||||
@@ -61,6 +64,10 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&.loop-output-aggregation {
|
||||||
|
grid-template-columns: 124px minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
.none-params {
|
.none-params {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background: var(--tf-bg-muted);
|
background: var(--tf-bg-muted);
|
||||||
|
|||||||
@@ -50,7 +50,12 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<RefParameterList noneParameterText="无输出参数" dataKeyName="outputDefs" useChildrenOnly={true} />
|
<RefParameterList
|
||||||
|
noneParameterText="无输出参数"
|
||||||
|
dataKeyName="outputDefs"
|
||||||
|
useChildrenOnly={true}
|
||||||
|
loopOutputAggregation={true}
|
||||||
|
/>
|
||||||
|
|
||||||
</NodeWrapper>
|
</NodeWrapper>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
buildLoopReferenceParameters,
|
buildLoopReferenceParameters,
|
||||||
buildLoopScopeParameters,
|
buildLoopScopeParameters,
|
||||||
isArrayDataType,
|
isArrayDataType,
|
||||||
|
projectParameterDataType,
|
||||||
} from '../../utils/loopScope';
|
} from '../../utils/loopScope';
|
||||||
|
|
||||||
const fillRefNodeIds = (
|
const fillRefNodeIds = (
|
||||||
@@ -25,9 +26,9 @@ const fillRefNodeIds = (
|
|||||||
const getChildren = (
|
const getChildren = (
|
||||||
params: any,
|
params: any,
|
||||||
parentId: string,
|
parentId: string,
|
||||||
nodeIsChildren: boolean,
|
|
||||||
nodeType: string,
|
nodeType: string,
|
||||||
parentPathLabel = '',
|
parentPathLabel = '',
|
||||||
|
ancestorCollectionDepth = 0,
|
||||||
parentIsCollection = false,
|
parentIsCollection = false,
|
||||||
) => {
|
) => {
|
||||||
if (!params || params.length === 0) return [];
|
if (!params || params.length === 0) return [];
|
||||||
@@ -43,25 +44,29 @@ const getChildren = (
|
|||||||
: parentIsCollection
|
: parentIsCollection
|
||||||
? `${parentPathLabel}.[].${normalizedChildLabel}`
|
? `${parentPathLabel}.[].${normalizedChildLabel}`
|
||||||
: `${parentPathLabel}.${normalizedChildLabel}`;
|
: `${parentPathLabel}.${normalizedChildLabel}`;
|
||||||
const dataType = nodeIsChildren
|
const dataType = projectParameterDataType(
|
||||||
? `Array<${param.dataType || 'String'}>`
|
param,
|
||||||
: param.dataType || 'String';
|
ancestorCollectionDepth,
|
||||||
|
);
|
||||||
|
const nextCollectionDepth =
|
||||||
|
ancestorCollectionDepth + (isCollection ? 1 : 0);
|
||||||
return {
|
return {
|
||||||
label: pathLabel,
|
label: pathLabel,
|
||||||
dataType: dataType,
|
dataType,
|
||||||
value: parentId + '.' + param.name,
|
value: parentId + '.' + param.name,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
nodeType: nodeType,
|
nodeType: nodeType,
|
||||||
displayLabel: pathLabel,
|
displayLabel: pathLabel,
|
||||||
pathLabel,
|
pathLabel,
|
||||||
itemTypeLabel: parentIsCollection ? '数组项字段' : undefined,
|
itemTypeLabel:
|
||||||
|
ancestorCollectionDepth > 0 ? '数组项字段' : undefined,
|
||||||
isCollection,
|
isCollection,
|
||||||
children: getChildren(
|
children: getChildren(
|
||||||
param.children,
|
param.children,
|
||||||
parentId + '.' + param.name,
|
parentId + '.' + param.name,
|
||||||
nodeIsChildren,
|
|
||||||
nodeType,
|
nodeType,
|
||||||
pathLabel,
|
pathLabel,
|
||||||
|
nextCollectionDepth,
|
||||||
isCollection,
|
isCollection,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@@ -70,7 +75,6 @@ const getChildren = (
|
|||||||
|
|
||||||
const nodeToOptions = (
|
const nodeToOptions = (
|
||||||
node: Node,
|
node: Node,
|
||||||
nodeIsChildren: boolean,
|
|
||||||
currentNode: Node,
|
currentNode: Node,
|
||||||
nodes: Node[],
|
nodes: Node[],
|
||||||
) => {
|
) => {
|
||||||
@@ -94,13 +98,10 @@ const nodeToOptions = (
|
|||||||
const children = [];
|
const children = [];
|
||||||
if (parameters)
|
if (parameters)
|
||||||
for (const parameter of parameters) {
|
for (const parameter of parameters) {
|
||||||
const dataType = nodeIsChildren
|
|
||||||
? `Array<${parameter.dataType || 'String'}>`
|
|
||||||
: parameter.dataType || 'String';
|
|
||||||
const label = getStartNodeParameterLabel(parameter);
|
const label = getStartNodeParameterLabel(parameter);
|
||||||
children.push({
|
children.push({
|
||||||
label,
|
label,
|
||||||
dataType: dataType,
|
dataType: projectParameterDataType(parameter),
|
||||||
value: node.id + '.' + parameter.name,
|
value: node.id + '.' + parameter.name,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
nodeType: nodeType,
|
nodeType: nodeType,
|
||||||
@@ -132,7 +133,6 @@ const nodeToOptions = (
|
|||||||
children: getChildren(
|
children: getChildren(
|
||||||
referenceParameters,
|
referenceParameters,
|
||||||
node.id,
|
node.id,
|
||||||
false,
|
|
||||||
nodeType,
|
nodeType,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@@ -145,7 +145,7 @@ const nodeToOptions = (
|
|||||||
value: node.id,
|
value: node.id,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
nodeType: nodeType,
|
nodeType: nodeType,
|
||||||
children: getChildren(outputDefs, node.id, nodeIsChildren, nodeType),
|
children: getChildren(outputDefs, node.id, nodeType),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,6 +153,7 @@ const nodeToOptions = (
|
|||||||
|
|
||||||
export const useRefOptions: any = (
|
export const useRefOptions: any = (
|
||||||
useChildrenOnly: boolean | (() => boolean) = false,
|
useChildrenOnly: boolean | (() => boolean) = false,
|
||||||
|
currentRef: string | (() => string) = '',
|
||||||
) => {
|
) => {
|
||||||
const currentNodeId = getCurrentNodeId();
|
const currentNodeId = getCurrentNodeId();
|
||||||
const currentNode = useNodesData(currentNodeId);
|
const currentNode = useNodesData(currentNodeId);
|
||||||
@@ -161,11 +162,16 @@ export const useRefOptions: any = (
|
|||||||
typeof useChildrenOnly === 'function'
|
typeof useChildrenOnly === 'function'
|
||||||
? useChildrenOnly()
|
? useChildrenOnly()
|
||||||
: useChildrenOnly;
|
: useChildrenOnly;
|
||||||
|
const getCurrentRef = () =>
|
||||||
|
typeof currentRef === 'function' ? currentRef() : currentRef;
|
||||||
|
|
||||||
let selectItems = $derived.by(() => {
|
let selectItems = $derived.by(() => {
|
||||||
const resultOptions = [];
|
const resultOptions = [];
|
||||||
if (!currentNode.current) {
|
if (!currentNode.current) {
|
||||||
return [];
|
return {
|
||||||
|
items: [],
|
||||||
|
selected: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//通过 nodeLookup.get 才会得到有 parentId 的 node
|
//通过 nodeLookup.get 才会得到有 parentId 的 node
|
||||||
@@ -173,11 +179,9 @@ export const useRefOptions: any = (
|
|||||||
|
|
||||||
if (isChildrenOnly()) {
|
if (isChildrenOnly()) {
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
const nodeIsChildren = node.parentId === currentNode.current.id;
|
if (node.parentId === currentNode.current.id) {
|
||||||
if (nodeIsChildren) {
|
|
||||||
const nodeOptions = nodeToOptions(
|
const nodeOptions = nodeToOptions(
|
||||||
node,
|
node,
|
||||||
nodeIsChildren,
|
|
||||||
cNode,
|
cNode,
|
||||||
nodes,
|
nodes,
|
||||||
);
|
);
|
||||||
@@ -193,10 +197,8 @@ export const useRefOptions: any = (
|
|||||||
node.type === 'loopNode' &&
|
node.type === 'loopNode' &&
|
||||||
buildLoopScopeParameters(node, cNode, nodes).length > 0;
|
buildLoopScopeParameters(node, cNode, nodes).length > 0;
|
||||||
if (refNodeIds.includes(node.id) || isScopedLoop) {
|
if (refNodeIds.includes(node.id) || isScopedLoop) {
|
||||||
const nodeIsChildren = node.parentId === currentNode.current.id;
|
|
||||||
const nodeOptions = nodeToOptions(
|
const nodeOptions = nodeToOptions(
|
||||||
node,
|
node,
|
||||||
nodeIsChildren,
|
|
||||||
cNode,
|
cNode,
|
||||||
nodes,
|
nodes,
|
||||||
);
|
);
|
||||||
@@ -205,12 +207,30 @@ export const useRefOptions: any = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultOptions;
|
const items = resultOptions;
|
||||||
|
const stack = [...items];
|
||||||
|
let selected;
|
||||||
|
const currentValue = getCurrentRef();
|
||||||
|
// 单次深度优先扫描定位当前引用,避免为每个参数额外构建全量索引。
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const item = stack.pop();
|
||||||
|
if (item?.value === currentValue) {
|
||||||
|
selected = item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (item?.children?.length) {
|
||||||
|
stack.push(...item.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { items, selected };
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get current() {
|
get current() {
|
||||||
return selectItems;
|
return selectItems.items;
|
||||||
|
},
|
||||||
|
get selected() {
|
||||||
|
return selectItems.selected;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -170,4 +170,5 @@ export type Parameter = {
|
|||||||
requiredDisabled?: boolean;
|
requiredDisabled?: boolean;
|
||||||
systemReserved?: boolean;
|
systemReserved?: boolean;
|
||||||
autoManaged?: boolean;
|
autoManaged?: boolean;
|
||||||
|
flattenAggregation?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
buildLoopItemParameter,
|
buildLoopItemParameter,
|
||||||
buildLoopReferenceParameters,
|
buildLoopReferenceParameters,
|
||||||
buildLoopScopeParameters,
|
buildLoopScopeParameters,
|
||||||
|
findReferenceParameter,
|
||||||
|
resolveLoopOutputDataType,
|
||||||
resolveLoopInputs,
|
resolveLoopInputs,
|
||||||
} from './loopScope';
|
} from './loopScope';
|
||||||
|
|
||||||
@@ -227,7 +229,7 @@ describe('loopScope', () => {
|
|||||||
'records',
|
'records',
|
||||||
]);
|
]);
|
||||||
expect(downstreamParameters[0]?.dataType).toBe('Array<String>');
|
expect(downstreamParameters[0]?.dataType).toBe('Array<String>');
|
||||||
expect(downstreamParameters[1]?.dataType).toBe('Array');
|
expect(downstreamParameters[1]?.dataType).toBe('Array<Object>');
|
||||||
expect(downstreamParameters[1]?.children?.[0]?.name).toBe('summary');
|
expect(downstreamParameters[1]?.children?.[0]?.name).toBe('summary');
|
||||||
|
|
||||||
downstreamParameters[1]!.children![0]!.name = 'changed';
|
downstreamParameters[1]!.children![0]!.name = 'changed';
|
||||||
@@ -235,4 +237,64 @@ describe('loopScope', () => {
|
|||||||
(childNode.data.outputDefs as Array<any>)[1].children[0].name,
|
(childNode.data.outputDefs as Array<any>)[1].children[0].name,
|
||||||
).toBe('summary');
|
).toBe('summary');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('集合子字段按实际层级推导循环输出类型', () => {
|
||||||
|
const loopNode = {
|
||||||
|
id: 'loop',
|
||||||
|
type: 'loopNode',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {
|
||||||
|
loopInputs: {
|
||||||
|
count: {
|
||||||
|
name: 'count',
|
||||||
|
refType: 'fixed',
|
||||||
|
value: '2',
|
||||||
|
dataType: 'Number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outputDefs: [
|
||||||
|
{
|
||||||
|
name: 'grouped',
|
||||||
|
refType: 'ref',
|
||||||
|
ref: 'knowledge.documents.content',
|
||||||
|
dataType: 'Array<String>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'flattened',
|
||||||
|
refType: 'ref',
|
||||||
|
ref: 'knowledge.documents.content',
|
||||||
|
dataType: 'Array<String>',
|
||||||
|
flattenAggregation: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} satisfies Node;
|
||||||
|
const downstreamNode = {
|
||||||
|
id: 'downstream',
|
||||||
|
type: 'endNode',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {},
|
||||||
|
} satisfies Node;
|
||||||
|
const nodes = [knowledgeNode, loopNode, downstreamNode];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
findReferenceParameter(
|
||||||
|
nodes,
|
||||||
|
'knowledge.documents.content',
|
||||||
|
)?.dataType,
|
||||||
|
).toBe('Array<String>');
|
||||||
|
expect(
|
||||||
|
buildLoopReferenceParameters(loopNode, downstreamNode, nodes)
|
||||||
|
.map((parameter) => parameter.dataType),
|
||||||
|
).toEqual(['Array<Array<String>>', 'Array<String>']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('扁平聚合只移除循环新增的一层数组', () => {
|
||||||
|
expect(resolveLoopOutputDataType('String', false))
|
||||||
|
.toBe('Array<String>');
|
||||||
|
expect(resolveLoopOutputDataType('Array<String>', false))
|
||||||
|
.toBe('Array<Array<String>>');
|
||||||
|
expect(resolveLoopOutputDataType('Array<String>', true))
|
||||||
|
.toBe('Array<String>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,53 @@ export function isArrayDataType(dataType?: string | null) {
|
|||||||
return normalized === 'array' || normalized.startsWith('array<');
|
return normalized === 'array' || normalized.startsWith('array<');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDataType(
|
||||||
|
dataType?: string | null,
|
||||||
|
hasChildren = false,
|
||||||
|
) {
|
||||||
|
const normalized = asString(dataType);
|
||||||
|
if (!normalized) {
|
||||||
|
return hasChildren ? 'Object' : 'String';
|
||||||
|
}
|
||||||
|
if (normalized.toLowerCase() === 'array') {
|
||||||
|
return `Array<${hasChildren ? 'Object' : 'Any'}>`;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算参数穿过上级集合后的实际引用类型。
|
||||||
|
*/
|
||||||
|
export function projectParameterDataType(
|
||||||
|
parameter: Parameter,
|
||||||
|
ancestorCollectionDepth = 0,
|
||||||
|
) {
|
||||||
|
const dataType = normalizeDataType(
|
||||||
|
parameter.dataType,
|
||||||
|
Boolean(parameter.children?.length),
|
||||||
|
);
|
||||||
|
if (ancestorCollectionDepth <= 0) {
|
||||||
|
return dataType;
|
||||||
|
}
|
||||||
|
const prefix = 'Array<'.repeat(ancestorCollectionDepth);
|
||||||
|
const suffix = '>'.repeat(ancestorCollectionDepth);
|
||||||
|
return `${prefix}${dataType}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算循环完成后对外暴露的输出类型。
|
||||||
|
*/
|
||||||
|
export function resolveLoopOutputDataType(
|
||||||
|
sourceDataType?: string | null,
|
||||||
|
flattenAggregation = false,
|
||||||
|
) {
|
||||||
|
const normalized = normalizeDataType(sourceDataType);
|
||||||
|
if (flattenAggregation && isArrayDataType(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return `Array<${normalized}>`;
|
||||||
|
}
|
||||||
|
|
||||||
function getNodeParameters(node: Node): Parameter[] {
|
function getNodeParameters(node: Node): Parameter[] {
|
||||||
if (node.type === 'startNode') {
|
if (node.type === 'startNode') {
|
||||||
return Array.isArray(node.data?.parameters)
|
return Array.isArray(node.data?.parameters)
|
||||||
@@ -52,16 +99,32 @@ export function findReferenceParameter(
|
|||||||
const path = normalizedReference.slice(node.id.length + 1).split('.');
|
const path = normalizedReference.slice(node.id.length + 1).split('.');
|
||||||
let parameters = getNodeParameters(node);
|
let parameters = getNodeParameters(node);
|
||||||
let current: Parameter | undefined;
|
let current: Parameter | undefined;
|
||||||
for (const segment of path) {
|
let ancestorCollectionDepth = 0;
|
||||||
|
for (let index = 0; index < path.length; index++) {
|
||||||
|
const segment = path[index];
|
||||||
current = parameters.find(
|
current = parameters.find(
|
||||||
(parameter) => asString(parameter.name) === segment,
|
(parameter) => asString(parameter.name) === segment,
|
||||||
);
|
);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
index < path.length - 1 &&
|
||||||
|
isArrayDataType(current.dataType)
|
||||||
|
) {
|
||||||
|
ancestorCollectionDepth++;
|
||||||
|
}
|
||||||
parameters = current.children || [];
|
parameters = current.children || [];
|
||||||
}
|
}
|
||||||
return current ? cloneParameter(current) : undefined;
|
if (!current) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const projected = cloneParameter(current);
|
||||||
|
projected.dataType = projectParameterDataType(
|
||||||
|
current,
|
||||||
|
ancestorCollectionDepth,
|
||||||
|
);
|
||||||
|
return projected;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveLoopInputs(
|
export function resolveLoopInputs(
|
||||||
@@ -219,17 +282,19 @@ export function buildLoopReferenceParameters(
|
|||||||
nodes,
|
nodes,
|
||||||
outputParameter.ref,
|
outputParameter.ref,
|
||||||
);
|
);
|
||||||
if (!sourceParameter) {
|
const sourceDataType =
|
||||||
return outputParameter;
|
asString(
|
||||||
}
|
sourceParameter?.dataType || outputParameter.dataType,
|
||||||
const sourceDataType = asString(sourceParameter.dataType) || 'String';
|
) || 'String';
|
||||||
const sourceChildren = sourceParameter.children?.map(cloneParameter);
|
const sourceChildren = (
|
||||||
|
sourceParameter?.children || outputParameter.children
|
||||||
|
)?.map(cloneParameter);
|
||||||
return {
|
return {
|
||||||
...outputParameter,
|
...outputParameter,
|
||||||
dataType:
|
dataType: resolveLoopOutputDataType(
|
||||||
sourceChildren?.length && !isArrayDataType(sourceDataType)
|
sourceDataType,
|
||||||
? 'Array'
|
Boolean(outputParameter.flattenAggregation),
|
||||||
: `Array<${sourceDataType}>`,
|
),
|
||||||
children: sourceChildren,
|
children: sourceChildren,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user