fix: 防止开始节点重名参数被删除
- 校验开始节点参数名并保留冲突输入 - 在字段归一化前拦截重名并补充回归测试
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
import {useTinyflowStore} from '#store/stores.svelte';
|
import {useTinyflowStore} from '#store/stores.svelte';
|
||||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||||
import {
|
import {
|
||||||
|
isStartFormFieldKeyAvailable,
|
||||||
renameStartFieldReferencesInNodes,
|
renameStartFieldReferencesInNodes,
|
||||||
removeStartFormField,
|
removeStartFormField,
|
||||||
START_NODE_TYPE,
|
START_NODE_TYPE,
|
||||||
@@ -47,12 +48,15 @@
|
|||||||
}
|
}
|
||||||
return param.formType ? [param.formType] : [];
|
return param.formType ? [param.formType] : [];
|
||||||
});
|
});
|
||||||
|
let parameterNameDraft = $state<string | null>(null);
|
||||||
let displayParamName = $derived.by(() => {
|
let displayParamName = $derived.by(() => {
|
||||||
if (isSystemStartParam) {
|
if (isSystemStartParam) {
|
||||||
return '用户问题';
|
return '用户问题';
|
||||||
}
|
}
|
||||||
return param.name;
|
return parameterNameDraft ?? param.name;
|
||||||
});
|
});
|
||||||
|
let parameterNameError = $state('');
|
||||||
|
let parameterNameErrorId = $derived(`parameter-name-error-${param.id || index}`);
|
||||||
|
|
||||||
const { updateNodeData } = useSvelteFlow();
|
const { updateNodeData } = useSvelteFlow();
|
||||||
|
|
||||||
@@ -146,7 +150,27 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateName = (event: Event) => {
|
const updateName = (event: Event) => {
|
||||||
const newValue = (event.target as any).value;
|
const input = event.target as HTMLInputElement;
|
||||||
|
const newValue = input.value;
|
||||||
|
if (isStartNodeInputParam) {
|
||||||
|
const normalizedValue = trimString(newValue);
|
||||||
|
if (!normalizedValue) {
|
||||||
|
parameterNameError = '参数名不能为空';
|
||||||
|
parameterNameDraft = newValue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isStartFormFieldKeyAvailable(
|
||||||
|
node?.current?.data as Record<string, any>,
|
||||||
|
param.name || '',
|
||||||
|
normalizedValue
|
||||||
|
)) {
|
||||||
|
parameterNameError = '参数名已存在';
|
||||||
|
parameterNameDraft = newValue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parameterNameDraft = null;
|
||||||
|
parameterNameError = '';
|
||||||
updateParameter('name', newValue);
|
updateParameter('name', newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -191,10 +215,17 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div class="input-item">
|
<div class="input-item input-item-name">
|
||||||
<Input style="width: 100%;" value={displayParamName} placeholder="请输入参数名称"
|
<Input style="width: 100%;" value={displayParamName} placeholder="请输入参数名称"
|
||||||
disabled={param.nameDisabled === true}
|
disabled={param.nameDisabled === true}
|
||||||
|
aria-invalid={parameterNameError ? 'true' : undefined}
|
||||||
|
aria-describedby={parameterNameError ? parameterNameErrorId : undefined}
|
||||||
oninput={updateName} />
|
oninput={updateName} />
|
||||||
|
{#if parameterNameError}
|
||||||
|
<div id={parameterNameErrorId} class="input-error" role="alert">
|
||||||
|
{parameterNameError}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="input-item">
|
<div class="input-item">
|
||||||
<Checkbox checked={param.required} disabled={param.requiredDisabled === true} onchange={updateRequired} />
|
<Checkbox checked={param.required} disabled={param.requiredDisabled === true} onchange={updateRequired} />
|
||||||
@@ -276,6 +307,22 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-item-name {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-error {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--tf-danger-soft-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.tf-input[aria-invalid='true']) {
|
||||||
|
border-color: var(--tf-danger-soft-border);
|
||||||
|
}
|
||||||
|
|
||||||
.input-more-setting {
|
.input-more-setting {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
createInitialWorkflowData,
|
createInitialWorkflowData,
|
||||||
ensureStartNodeParameters,
|
ensureStartNodeParameters,
|
||||||
FIELD_BINDING_META_KEY,
|
FIELD_BINDING_META_KEY,
|
||||||
|
isStartFormFieldKeyAvailable,
|
||||||
normalizeStartNodeData,
|
normalizeStartNodeData,
|
||||||
normalizeWorkflowStartNodes,
|
normalizeWorkflowStartNodes,
|
||||||
renameStartFieldReferencesInNodes,
|
renameStartFieldReferencesInNodes,
|
||||||
@@ -148,6 +149,47 @@ describe('workflow node fields', () => {
|
|||||||
expect(nextParameter?.id).toBe(previousParameter?.id);
|
expect(nextParameter?.id).toBe(previousParameter?.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps start fields unchanged when renaming to an existing key', () => {
|
||||||
|
const initial = createInitialWorkflowData();
|
||||||
|
const withTopic = appendStartFormField(
|
||||||
|
initial.nodes[0]?.data as Record<string, any>,
|
||||||
|
{
|
||||||
|
key: 'topic',
|
||||||
|
label: '主题',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const withDetails = appendStartFormField(withTopic, {
|
||||||
|
key: 'details',
|
||||||
|
label: '详情',
|
||||||
|
type: 'textarea',
|
||||||
|
});
|
||||||
|
const previousFields = withDetails.startFormSchema;
|
||||||
|
const previousParameters = withDetails.parameters;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
isStartFormFieldKeyAvailable(withDetails, 'details', 'summary'),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isStartFormFieldKeyAvailable(withDetails, 'details', 'topic'),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isStartFormFieldKeyAvailable(withDetails, 'details', 'user_input'),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
const duplicatedCustomKey = updateStartFormField(withDetails, 'details', {
|
||||||
|
key: 'topic',
|
||||||
|
});
|
||||||
|
const duplicatedSystemKey = updateStartFormField(withDetails, 'details', {
|
||||||
|
key: 'user_input',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(duplicatedCustomKey.startFormSchema).toEqual(previousFields);
|
||||||
|
expect(duplicatedCustomKey.parameters).toEqual(previousParameters);
|
||||||
|
expect(duplicatedSystemKey.startFormSchema).toEqual(previousFields);
|
||||||
|
expect(duplicatedSystemKey.parameters).toEqual(previousParameters);
|
||||||
|
});
|
||||||
|
|
||||||
it('renames downstream token and managed references when start field key changes', () => {
|
it('renames downstream token and managed references when start field key changes', () => {
|
||||||
const initialStartData = appendStartFormField(
|
const initialStartData = appendStartFormField(
|
||||||
createInitialWorkflowData().nodes[0]?.data as Record<string, any>,
|
createInitialWorkflowData().nodes[0]?.data as Record<string, any>,
|
||||||
|
|||||||
@@ -720,6 +720,36 @@ export function normalizeStartFormSchema(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isStartFormFieldKeyAvailable(
|
||||||
|
data: Record<string, any> | null | undefined,
|
||||||
|
currentKey: string,
|
||||||
|
candidateKey: string,
|
||||||
|
) {
|
||||||
|
const normalizedCurrentKey = trimString(currentKey);
|
||||||
|
const normalizedCandidateKey = trimString(candidateKey);
|
||||||
|
if (!normalizedCurrentKey || !normalizedCandidateKey) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentData = (data || {}) as Record<string, any>;
|
||||||
|
const currentParameters = Array.isArray(currentData.parameters)
|
||||||
|
? (currentData.parameters as Parameter[])
|
||||||
|
: [];
|
||||||
|
const schema = normalizeStartFormSchema(
|
||||||
|
currentData.startFormSchema,
|
||||||
|
currentParameters,
|
||||||
|
);
|
||||||
|
if (!schema.some((field) => field.key === normalizedCurrentKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !schema.some(
|
||||||
|
(field) =>
|
||||||
|
field.key === normalizedCandidateKey &&
|
||||||
|
field.key !== normalizedCurrentKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function createCustomStartFormField(
|
export function createCustomStartFormField(
|
||||||
field?: Partial<StartFormFieldSchema> | null,
|
field?: Partial<StartFormFieldSchema> | null,
|
||||||
existingKeys: string[] = [],
|
existingKeys: string[] = [],
|
||||||
@@ -773,6 +803,12 @@ export function updateStartFormField(
|
|||||||
? (currentData.parameters as Parameter[])
|
? (currentData.parameters as Parameter[])
|
||||||
: [];
|
: [];
|
||||||
const schema = normalizeStartFormSchema(currentData.startFormSchema, currentParameters);
|
const schema = normalizeStartFormSchema(currentData.startFormSchema, currentParameters);
|
||||||
|
if (
|
||||||
|
patch.key !== undefined &&
|
||||||
|
!isStartFormFieldKeyAvailable(currentData, currentKey, patch.key)
|
||||||
|
) {
|
||||||
|
return normalizeStartNodeData(currentData);
|
||||||
|
}
|
||||||
const nextSchema = schema.map((field) => {
|
const nextSchema = schema.map((field) => {
|
||||||
if (field.key !== currentKey) {
|
if (field.key !== currentKey) {
|
||||||
return field;
|
return field;
|
||||||
|
|||||||
Reference in New Issue
Block a user