feat: 优化内容模板变量引用与换行展示
- 内容模板直接引用上游参数并自动维护运行参数 - 支持 Enjoy 变量标签编辑及中文变量显示 - 保留试运行与聊天运行页的模板输出换行
This commit is contained in:
@@ -20,7 +20,9 @@
|
||||
findBackspaceTokenRange,
|
||||
findTokenRangeAtCursor,
|
||||
flattenParameterCandidates,
|
||||
formatParamToken,
|
||||
getTokenRanges,
|
||||
type TokenSyntax,
|
||||
} from '../utils/paramToken';
|
||||
|
||||
const {
|
||||
@@ -30,6 +32,7 @@
|
||||
rows = 3,
|
||||
placeholder = '',
|
||||
disabled = false,
|
||||
tokenSyntax = 'mustache',
|
||||
class: className = '',
|
||||
style = '',
|
||||
...rest
|
||||
@@ -40,6 +43,7 @@
|
||||
rows?: number;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
tokenSyntax?: TokenSyntax;
|
||||
class?: string;
|
||||
style?: string;
|
||||
[key: string]: any;
|
||||
@@ -169,10 +173,13 @@
|
||||
});
|
||||
}
|
||||
|
||||
function createTokenDecorations(metaMap: Map<string, TokenMeta>): Extension {
|
||||
function createTokenDecorations(
|
||||
metaMap: Map<string, TokenMeta>,
|
||||
syntax: TokenSyntax
|
||||
): Extension {
|
||||
const buildDecorations = (source: string) => {
|
||||
const decorations = [];
|
||||
for (const range of getTokenRanges(source)) {
|
||||
for (const range of getTokenRanges(source, syntax)) {
|
||||
const tokenMeta = metaMap.get(range.key);
|
||||
const displayName =
|
||||
!tokenMeta || tokenMeta.variant === 'invalid'
|
||||
@@ -299,12 +306,15 @@
|
||||
if (!editorView || disabled) {
|
||||
return;
|
||||
}
|
||||
insertTextAtSelection(`{{${paramName}}}`);
|
||||
insertTextAtSelection(formatParamToken(paramName, tokenSyntax));
|
||||
emitChange(editorView.state.doc.toString());
|
||||
triggerObject?.hide?.();
|
||||
}
|
||||
|
||||
function moveCursorAcrossToken(direction: 'left' | 'right') {
|
||||
function moveCursorAcrossToken(
|
||||
direction: 'left' | 'right',
|
||||
syntax: TokenSyntax
|
||||
) {
|
||||
return (view: EditorView) => {
|
||||
const selection = view.state.selection.main;
|
||||
if (!selection.empty) {
|
||||
@@ -317,11 +327,13 @@
|
||||
direction === 'left'
|
||||
? findTokenRangeAtCursor(source, cursor, {
|
||||
includeStart: false,
|
||||
includeEnd: true
|
||||
includeEnd: true,
|
||||
syntax
|
||||
})
|
||||
: findTokenRangeAtCursor(source, cursor, {
|
||||
includeStart: true,
|
||||
includeEnd: false
|
||||
includeEnd: false,
|
||||
syntax
|
||||
});
|
||||
|
||||
if (!range) {
|
||||
@@ -341,13 +353,17 @@
|
||||
};
|
||||
}
|
||||
|
||||
function deleteTokenBackward(view: EditorView) {
|
||||
function deleteTokenBackward(view: EditorView, syntax: TokenSyntax) {
|
||||
const selection = view.state.selection.main;
|
||||
if (!selection.empty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const range = findBackspaceTokenRange(view.state.doc.toString(), selection.head);
|
||||
const range = findBackspaceTokenRange(
|
||||
view.state.doc.toString(),
|
||||
selection.head,
|
||||
syntax
|
||||
);
|
||||
if (!range) {
|
||||
return false;
|
||||
}
|
||||
@@ -366,16 +382,21 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function deleteTokenForward(view: EditorView) {
|
||||
function deleteTokenForward(view: EditorView, syntax: TokenSyntax) {
|
||||
const selection = view.state.selection.main;
|
||||
if (!selection.empty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const range = findTokenRangeAtCursor(view.state.doc.toString(), selection.head, {
|
||||
includeStart: true,
|
||||
includeEnd: false
|
||||
});
|
||||
const range = findTokenRangeAtCursor(
|
||||
view.state.doc.toString(),
|
||||
selection.head,
|
||||
{
|
||||
includeStart: true,
|
||||
includeEnd: false,
|
||||
syntax
|
||||
}
|
||||
);
|
||||
if (!range) {
|
||||
return false;
|
||||
}
|
||||
@@ -398,23 +419,26 @@
|
||||
return text.replace(/\r\n|\r|\n/g, ' ');
|
||||
}
|
||||
|
||||
function createEditorKeymap(singleLine: boolean) {
|
||||
function createEditorKeymap(
|
||||
singleLine: boolean,
|
||||
syntax: TokenSyntax
|
||||
) {
|
||||
const entries = [
|
||||
{
|
||||
key: 'ArrowLeft',
|
||||
run: moveCursorAcrossToken('left')
|
||||
run: moveCursorAcrossToken('left', syntax)
|
||||
},
|
||||
{
|
||||
key: 'ArrowRight',
|
||||
run: moveCursorAcrossToken('right')
|
||||
run: moveCursorAcrossToken('right', syntax)
|
||||
},
|
||||
{
|
||||
key: 'Backspace',
|
||||
run: deleteTokenBackward
|
||||
run: (view: EditorView) => deleteTokenBackward(view, syntax)
|
||||
},
|
||||
{
|
||||
key: 'Delete',
|
||||
run: deleteTokenForward
|
||||
run: (view: EditorView) => deleteTokenForward(view, syntax)
|
||||
},
|
||||
...defaultKeymap,
|
||||
...historyKeymap
|
||||
@@ -528,11 +552,11 @@
|
||||
}
|
||||
}),
|
||||
eventCompartment.of(createDomEventHandlers(isSingleLine)),
|
||||
keymapCompartment.of(createEditorKeymap(isSingleLine)),
|
||||
keymapCompartment.of(createEditorKeymap(isSingleLine, tokenSyntax)),
|
||||
editableCompartment.of([EditorView.editable.of(!disabled), EditorState.readOnly.of(disabled)]),
|
||||
placeholderCompartment.of(placeholder ? cmPlaceholder(placeholder) : []),
|
||||
lineModeCompartment.of(createLineModeExtension(isSingleLine)),
|
||||
tokenCompartment.of(createTokenDecorations(tokenMetaMap)),
|
||||
tokenCompartment.of(createTokenDecorations(tokenMetaMap, tokenSyntax)),
|
||||
themeCompartment.of(createEditorTheme(isSingleLine))
|
||||
]
|
||||
})
|
||||
@@ -585,7 +609,9 @@
|
||||
return;
|
||||
}
|
||||
editorView.dispatch({
|
||||
effects: keymapCompartment.reconfigure(createEditorKeymap(isSingleLine))
|
||||
effects: keymapCompartment.reconfigure(
|
||||
createEditorKeymap(isSingleLine, tokenSyntax)
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -603,7 +629,9 @@
|
||||
return;
|
||||
}
|
||||
editorView.dispatch({
|
||||
effects: tokenCompartment.reconfigure(createTokenDecorations(tokenMetaMap))
|
||||
effects: tokenCompartment.reconfigure(
|
||||
createTokenDecorations(tokenMetaMap, tokenSyntax)
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -170,4 +170,43 @@ describe('ParamTokenEditor', () => {
|
||||
|
||||
await destroy();
|
||||
});
|
||||
|
||||
it('should render and insert Enjoy parameter tokens', async () => {
|
||||
const onchange = vi.fn();
|
||||
const { host, view, destroy } = await renderEditor({
|
||||
tokenSyntax: 'enjoy',
|
||||
value: '申请人:#(申请人)',
|
||||
parameters: [
|
||||
{
|
||||
name: '申请人',
|
||||
refType: 'ref',
|
||||
ref: 'llm.output',
|
||||
},
|
||||
],
|
||||
onchange,
|
||||
});
|
||||
|
||||
expect(host.querySelector('.param-token-chip-text')?.textContent).toBe(
|
||||
'申请人',
|
||||
);
|
||||
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: view.state.doc.length,
|
||||
insert: '',
|
||||
},
|
||||
});
|
||||
(host.querySelector('.param-token-item') as HTMLButtonElement).click();
|
||||
flushSync();
|
||||
|
||||
expect(view.state.doc.toString()).toBe('#(申请人)');
|
||||
expect(onchange).toHaveBeenCalledWith({
|
||||
target: {
|
||||
value: '#(申请人)',
|
||||
},
|
||||
});
|
||||
|
||||
await destroy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
|
||||
<script lang="ts">
|
||||
import NodeWrapper from '../core/NodeWrapper.svelte';
|
||||
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
|
||||
import {Button, Heading} from '../base';
|
||||
import {Textarea} from '../base/index.js';
|
||||
import RefParameterList from '../core/RefParameterList.svelte';
|
||||
import {type NodeProps, useNodesData, useStore, useSvelteFlow} from '@xyflow/svelte';
|
||||
import {Heading} from '../base';
|
||||
import ParamTokenEditor from '../core/ParamTokenEditor.svelte';
|
||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||
import {useAddParameter} from '../utils/useAddParameter.svelte';
|
||||
import OutputDefList from '../core/OutputDefList.svelte';
|
||||
import type {TinyflowNodeData} from '#types';
|
||||
import type {Parameter, TinyflowNodeData} from '#types';
|
||||
import {
|
||||
buildEnjoyEditorReferenceParameters,
|
||||
syncManagedParametersForFields,
|
||||
} from '../../utils/workflowNodeFields';
|
||||
|
||||
const { data, ...rest }: {
|
||||
data: TinyflowNodeData,
|
||||
@@ -17,10 +20,30 @@
|
||||
} = $props();
|
||||
|
||||
const currentNodeId = getCurrentNodeId();
|
||||
let currentNode = useNodesData(currentNodeId);
|
||||
const { addParameter } = useAddParameter();
|
||||
|
||||
|
||||
const { nodes, edges } = $derived(useStore());
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
const editorParameters = $derived.by(() => {
|
||||
return buildEnjoyEditorReferenceParameters(
|
||||
currentNodeId,
|
||||
nodes || [],
|
||||
edges || [],
|
||||
(((currentNode?.current?.data?.parameters as Array<Parameter>) || data.parameters || []) as Array<Parameter>)
|
||||
);
|
||||
});
|
||||
const syncTemplateValue = (nextValue: string) => {
|
||||
const currentData = ((currentNode?.current?.data as Record<string, any>) || data || {}) as Record<string, any>;
|
||||
updateNodeData(currentNodeId, {
|
||||
template: nextValue,
|
||||
parameters: syncManagedParametersForFields(
|
||||
(currentData.parameters as Array<Parameter>) || [],
|
||||
editorParameters,
|
||||
{ template: nextValue },
|
||||
'enjoy'
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
if (!data.outputDefs || data.outputDefs.length === 0) {
|
||||
@@ -44,28 +67,21 @@
|
||||
</svg>
|
||||
{/snippet}
|
||||
|
||||
<div class="heading">
|
||||
<Heading level={3}>输入参数</Heading>
|
||||
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
|
||||
addParameter(currentNodeId)
|
||||
}}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
<RefParameterList />
|
||||
|
||||
<Heading level={3} mt="10px" mb="10px">模板内容</Heading>
|
||||
<Heading level={3} mb="10px">模板内容</Heading>
|
||||
|
||||
<div class="setting-item">
|
||||
<Textarea rows={10} placeholder="请输入模板内容" style="width: 100%" onchange={(e:any)=>{
|
||||
updateNodeData(currentNodeId, ()=>{
|
||||
return {
|
||||
template: e.target.value
|
||||
}
|
||||
})
|
||||
}} value={String(data.template || '')} />
|
||||
<ParamTokenEditor
|
||||
mode="textarea"
|
||||
rows={10}
|
||||
tokenSyntax="enjoy"
|
||||
placeholder="请输入模板内容"
|
||||
style="width: 100%"
|
||||
parameters={editorParameters}
|
||||
value={String(data.template || '')}
|
||||
oninput={(e:any)=>{
|
||||
syncTemplateValue(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -91,4 +107,3 @@
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
findBackspaceTokenRange,
|
||||
findTokenRangeAtCursor,
|
||||
flattenParameterNames,
|
||||
formatParamToken,
|
||||
getTokenRanges,
|
||||
insertTextAtCursor,
|
||||
parseTokenParts,
|
||||
splitTokenDisplay,
|
||||
@@ -64,18 +66,21 @@ describe('paramToken utils', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
dataType: undefined,
|
||||
disconnected: false,
|
||||
displayName: 'input',
|
||||
name: 'input',
|
||||
resolved: false,
|
||||
},
|
||||
{
|
||||
dataType: undefined,
|
||||
disconnected: false,
|
||||
displayName: 'docs',
|
||||
name: 'docs',
|
||||
resolved: true,
|
||||
},
|
||||
{
|
||||
dataType: undefined,
|
||||
disconnected: false,
|
||||
displayName: 'runtimeInput',
|
||||
name: 'runtimeInput',
|
||||
resolved: true,
|
||||
@@ -178,4 +183,20 @@ describe('paramToken utils', () => {
|
||||
expect(rightBoundary?.key).toBe('docs');
|
||||
expect(leftBoundary?.key).toBe('docs');
|
||||
});
|
||||
|
||||
it('should format and parse Enjoy parameter tokens', () => {
|
||||
expect(formatParamToken('申请人', 'enjoy')).toBe('#(申请人)');
|
||||
expect(getTokenRanges('申请:#(申请人)', 'enjoy')).toEqual([
|
||||
{
|
||||
start: 3,
|
||||
end: 9,
|
||||
text: '#(申请人)',
|
||||
key: '申请人',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should keep complex Enjoy expressions as editable text', () => {
|
||||
expect(getTokenRanges('#(申请人 ?? 被申请人)', 'enjoy')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,22 @@ export interface ParameterCandidate {
|
||||
dataType?: string;
|
||||
}
|
||||
|
||||
const TOKEN_PATTERN = /\{\{\s*([^{}]+?)\s*}}/g;
|
||||
export type TokenSyntax = 'enjoy' | 'mustache';
|
||||
|
||||
const MUSTACHE_TOKEN_PATTERN = /\{\{\s*([^{}]+?)\s*}}/g;
|
||||
const ENJOY_TOKEN_PATTERN =
|
||||
/#\(\s*([A-Za-z_$\u4E00-\u9FA5][A-Za-z0-9_$\u4E00-\u9FA5]*(?:\.[A-Za-z_$\u4E00-\u9FA5][A-Za-z0-9_$\u4E00-\u9FA5]*)*)\s*\)/g;
|
||||
|
||||
function getTokenPattern(syntax: TokenSyntax): RegExp {
|
||||
return syntax === 'enjoy' ? ENJOY_TOKEN_PATTERN : MUSTACHE_TOKEN_PATTERN;
|
||||
}
|
||||
|
||||
export function formatParamToken(
|
||||
tokenKey: string,
|
||||
syntax: TokenSyntax = 'mustache',
|
||||
): string {
|
||||
return syntax === 'enjoy' ? `#(${tokenKey})` : `{{${tokenKey}}}`;
|
||||
}
|
||||
|
||||
export function normalizeTokenKey(tokenKey: string): string {
|
||||
return tokenKey.trim();
|
||||
@@ -156,14 +171,16 @@ export function flattenParameterCandidates(
|
||||
export function parseTokenParts(
|
||||
content: string,
|
||||
validParams: string[] = [],
|
||||
syntax: TokenSyntax = 'mustache',
|
||||
): TokenPart[] {
|
||||
const source = content ?? '';
|
||||
const validSet = new Set(validParams.map(normalizeTokenKey));
|
||||
const parts: TokenPart[] = [];
|
||||
const tokenPattern = getTokenPattern(syntax);
|
||||
|
||||
let lastIndex = 0;
|
||||
TOKEN_PATTERN.lastIndex = 0;
|
||||
let match: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
|
||||
tokenPattern.lastIndex = 0;
|
||||
let match: RegExpExecArray | null = tokenPattern.exec(source);
|
||||
|
||||
while (match) {
|
||||
if (match.index > lastIndex) {
|
||||
@@ -183,7 +200,7 @@ export function parseTokenParts(
|
||||
});
|
||||
|
||||
lastIndex = match.index + rawToken.length;
|
||||
match = TOKEN_PATTERN.exec(source);
|
||||
match = tokenPattern.exec(source);
|
||||
}
|
||||
|
||||
if (lastIndex < source.length) {
|
||||
@@ -203,11 +220,15 @@ export function parseTokenParts(
|
||||
return parts;
|
||||
}
|
||||
|
||||
export function getTokenRanges(content: string): TokenRange[] {
|
||||
export function getTokenRanges(
|
||||
content: string,
|
||||
syntax: TokenSyntax = 'mustache',
|
||||
): TokenRange[] {
|
||||
const source = content ?? '';
|
||||
const ranges: TokenRange[] = [];
|
||||
TOKEN_PATTERN.lastIndex = 0;
|
||||
let match: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
|
||||
const tokenPattern = getTokenPattern(syntax);
|
||||
tokenPattern.lastIndex = 0;
|
||||
let match: RegExpExecArray | null = tokenPattern.exec(source);
|
||||
|
||||
while (match) {
|
||||
const rawToken = match[0];
|
||||
@@ -217,7 +238,7 @@ export function getTokenRanges(content: string): TokenRange[] {
|
||||
text: rawToken,
|
||||
key: normalizeTokenKey(match[1] || ''),
|
||||
});
|
||||
match = TOKEN_PATTERN.exec(source);
|
||||
match = tokenPattern.exec(source);
|
||||
}
|
||||
|
||||
return ranges;
|
||||
@@ -229,6 +250,7 @@ export function findTokenRangeAtCursor(
|
||||
options?: {
|
||||
includeStart?: boolean;
|
||||
includeEnd?: boolean;
|
||||
syntax?: TokenSyntax;
|
||||
},
|
||||
): TokenRange | null {
|
||||
if (!Number.isInteger(cursor) || cursor < 0) {
|
||||
@@ -237,7 +259,7 @@ export function findTokenRangeAtCursor(
|
||||
|
||||
const includeStart = options?.includeStart ?? false;
|
||||
const includeEnd = options?.includeEnd ?? false;
|
||||
const ranges = getTokenRanges(content);
|
||||
const ranges = getTokenRanges(content, options?.syntax);
|
||||
for (const range of ranges) {
|
||||
const leftValid = includeStart
|
||||
? cursor >= range.start
|
||||
@@ -254,10 +276,12 @@ export function findTokenRangeAtCursor(
|
||||
export function findBackspaceTokenRange(
|
||||
content: string,
|
||||
cursor: number,
|
||||
syntax: TokenSyntax = 'mustache',
|
||||
): TokenRange | null {
|
||||
return findTokenRangeAtCursor(content, cursor, {
|
||||
includeStart: false,
|
||||
includeEnd: true,
|
||||
syntax,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
buildSequentialFieldBindingPatches,
|
||||
buildFieldBindingPatch,
|
||||
buildEditorReferenceParameters,
|
||||
buildEnjoyEditorReferenceParameters,
|
||||
buildSingleRunModel,
|
||||
buildSingleRunParameters,
|
||||
createInitialWorkflowData,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
normalizeWorkflowStartNodes,
|
||||
renameStartFieldReferencesInNodes,
|
||||
removeStartFormField,
|
||||
syncManagedParametersForFields,
|
||||
updateStartFormField,
|
||||
} from './workflowNodeFields';
|
||||
|
||||
@@ -666,6 +668,74 @@ describe('workflow node fields', () => {
|
||||
).toBe('流程开始 > 用户问题');
|
||||
});
|
||||
|
||||
it('builds stable Enjoy aliases and syncs direct upstream references', () => {
|
||||
const llmNode: Node = {
|
||||
id: 'node_llm',
|
||||
type: 'llmNode',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
title: '大模型',
|
||||
outputDefs: [{ name: 'output', dataType: 'String' }],
|
||||
},
|
||||
};
|
||||
const templateNode: Node = {
|
||||
id: 'node_template',
|
||||
type: 'templateNode',
|
||||
position: { x: 120, y: 0 },
|
||||
data: {
|
||||
title: '内容模板',
|
||||
parameters: [],
|
||||
},
|
||||
};
|
||||
const edges: Edge[] = [
|
||||
{
|
||||
id: 'edge_1',
|
||||
source: 'node_llm',
|
||||
target: 'node_template',
|
||||
} as Edge,
|
||||
];
|
||||
|
||||
const candidates = buildEnjoyEditorReferenceParameters(
|
||||
'node_template',
|
||||
[llmNode, templateNode],
|
||||
edges,
|
||||
[],
|
||||
);
|
||||
const candidate = candidates.find(
|
||||
(parameter) => parameter.ref === 'node_llm.output',
|
||||
);
|
||||
|
||||
expect(candidate?.name).toMatch(/^ref_[A-Za-z0-9_]+$/);
|
||||
expect(candidate?.displayName).toBe('大模型 > output');
|
||||
|
||||
const parameters = syncManagedParametersForFields(
|
||||
[],
|
||||
candidates,
|
||||
{ template: `#(${candidate?.name})` },
|
||||
'enjoy',
|
||||
);
|
||||
|
||||
expect(parameters).toHaveLength(1);
|
||||
expect(parameters[0]).toMatchObject({
|
||||
name: candidate?.name,
|
||||
ref: 'node_llm.output',
|
||||
refType: 'ref',
|
||||
autoManaged: true,
|
||||
});
|
||||
|
||||
const rebuiltCandidates = buildEnjoyEditorReferenceParameters(
|
||||
'node_template',
|
||||
[llmNode, templateNode],
|
||||
edges,
|
||||
parameters,
|
||||
);
|
||||
expect(
|
||||
rebuiltCandidates.find(
|
||||
(parameter) => parameter.ref === 'node_llm.output',
|
||||
)?.name,
|
||||
).toBe(candidate?.name);
|
||||
});
|
||||
|
||||
it('uses output parameter name for reference display', () => {
|
||||
const knowledgeNode: Node = {
|
||||
id: 'knowledge_1',
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { Edge, Node } from '@xyflow/svelte';
|
||||
|
||||
import type { Parameter } from '../types';
|
||||
import { getTokenRanges } from '../components/utils/paramToken';
|
||||
import {
|
||||
getTokenRanges,
|
||||
type TokenSyntax,
|
||||
} from '../components/utils/paramToken';
|
||||
import { genShortId } from '../components/utils/IdGen';
|
||||
import {
|
||||
buildLoopReferenceParameters,
|
||||
@@ -1250,10 +1253,103 @@ export function buildEditorReferenceParameters(
|
||||
return uniqueParameters([...disconnectedParameters, ...upstreamParameters]);
|
||||
}
|
||||
|
||||
function toEnjoyParameterName(refPath: string) {
|
||||
let encoded = '';
|
||||
for (const char of refPath) {
|
||||
if (/^[A-Za-z0-9]$/.test(char)) {
|
||||
encoded += char;
|
||||
} else if (char === '_') {
|
||||
encoded += '__';
|
||||
} else {
|
||||
encoded += `_${char.codePointAt(0)?.toString(16)}_`;
|
||||
}
|
||||
}
|
||||
return `ref_${encoded}`;
|
||||
}
|
||||
|
||||
export function buildEnjoyEditorReferenceParameters(
|
||||
currentNodeId: string,
|
||||
nodes: Node[],
|
||||
edges: Edge[],
|
||||
existingParameters?: Parameter[],
|
||||
) {
|
||||
const currentParameters = Array.isArray(existingParameters)
|
||||
? existingParameters.map(ensureParameterId)
|
||||
: [];
|
||||
const upstreamParameters = buildEditorReferenceParameters(
|
||||
currentNodeId,
|
||||
nodes,
|
||||
edges,
|
||||
[],
|
||||
);
|
||||
const upstreamRefSet = new Set(
|
||||
upstreamParameters
|
||||
.map((parameter) => trimString(parameter.ref || parameter.name))
|
||||
.filter(Boolean),
|
||||
);
|
||||
const existingManagedByRef = new Map(
|
||||
currentParameters
|
||||
.filter((parameter) => parameter.autoManaged === true)
|
||||
.map((parameter) => [trimString(parameter.ref), parameter] as const)
|
||||
.filter(([refPath]) => refPath.length > 0),
|
||||
);
|
||||
|
||||
const directParameters = upstreamParameters.map((parameter) => {
|
||||
const refPath = trimString(parameter.ref || parameter.name);
|
||||
const existingParameter = existingManagedByRef.get(refPath);
|
||||
return ensureParameterId({
|
||||
...parameter,
|
||||
name:
|
||||
trimString(existingParameter?.name) ||
|
||||
toEnjoyParameterName(refPath),
|
||||
ref: refPath,
|
||||
});
|
||||
});
|
||||
const disconnectedParameters = currentParameters
|
||||
.filter(
|
||||
(parameter) =>
|
||||
parameter.autoManaged === true &&
|
||||
trimString(parameter.ref) &&
|
||||
!upstreamRefSet.has(trimString(parameter.ref)),
|
||||
)
|
||||
.map((parameter) =>
|
||||
ensureParameterId({
|
||||
...parameter,
|
||||
resolved: false,
|
||||
disconnected: true,
|
||||
displayName: buildDisconnectedDisplayName(parameter),
|
||||
formLabel: buildDisconnectedDisplayName(parameter),
|
||||
} as Parameter & { resolved: boolean; disconnected: boolean }),
|
||||
);
|
||||
const legacyParameters = currentParameters
|
||||
.filter((parameter) => parameter.autoManaged !== true)
|
||||
.map((parameter) => {
|
||||
const refPath = trimString(parameter.ref);
|
||||
const disconnected =
|
||||
parameter.refType === 'ref' &&
|
||||
refPath.length > 0 &&
|
||||
!upstreamRefSet.has(refPath);
|
||||
return ensureParameterId({
|
||||
...parameter,
|
||||
resolved: !disconnected,
|
||||
disconnected: disconnected || undefined,
|
||||
displayName:
|
||||
parameter.displayName || parameter.formLabel || parameter.name,
|
||||
} as Parameter & { resolved: boolean; disconnected?: boolean });
|
||||
});
|
||||
|
||||
return uniqueParameters([
|
||||
...legacyParameters,
|
||||
...disconnectedParameters,
|
||||
...directParameters,
|
||||
]);
|
||||
}
|
||||
|
||||
export function syncManagedParametersForFields(
|
||||
existingParameters: Parameter[] | undefined,
|
||||
candidateParameters: Parameter[],
|
||||
fieldValues: Record<string, string | undefined>,
|
||||
tokenSyntax: TokenSyntax = 'mustache',
|
||||
) {
|
||||
const currentParameters = Array.isArray(existingParameters)
|
||||
? existingParameters.map(ensureParameterId)
|
||||
@@ -1262,7 +1358,9 @@ export function syncManagedParametersForFields(
|
||||
new Set(
|
||||
Object.values(fieldValues)
|
||||
.flatMap((value) =>
|
||||
getTokenRanges(asString(value)).map((token) => token.key),
|
||||
getTokenRanges(asString(value), tokenSyntax).map(
|
||||
(token) => token.key,
|
||||
),
|
||||
)
|
||||
.filter((value) => value.trim().length > 0),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user