fix: 防止开始节点重名参数被删除
- 校验开始节点参数名并保留冲突输入 - 在字段归一化前拦截重名并补充回归测试
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
import {useTinyflowStore} from '#store/stores.svelte';
|
||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||
import {
|
||||
isStartFormFieldKeyAvailable,
|
||||
renameStartFieldReferencesInNodes,
|
||||
removeStartFormField,
|
||||
START_NODE_TYPE,
|
||||
@@ -47,12 +48,15 @@
|
||||
}
|
||||
return param.formType ? [param.formType] : [];
|
||||
});
|
||||
let parameterNameDraft = $state<string | null>(null);
|
||||
let displayParamName = $derived.by(() => {
|
||||
if (isSystemStartParam) {
|
||||
return '用户问题';
|
||||
}
|
||||
return param.name;
|
||||
return parameterNameDraft ?? param.name;
|
||||
});
|
||||
let parameterNameError = $state('');
|
||||
let parameterNameErrorId = $derived(`parameter-name-error-${param.id || index}`);
|
||||
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
|
||||
@@ -146,7 +150,27 @@
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -191,10 +215,17 @@
|
||||
</script>
|
||||
|
||||
|
||||
<div class="input-item">
|
||||
<div class="input-item input-item-name">
|
||||
<Input style="width: 100%;" value={displayParamName} placeholder="请输入参数名称"
|
||||
disabled={param.nameDisabled === true}
|
||||
aria-invalid={parameterNameError ? 'true' : undefined}
|
||||
aria-describedby={parameterNameError ? parameterNameErrorId : undefined}
|
||||
oninput={updateName} />
|
||||
{#if parameterNameError}
|
||||
<div id={parameterNameErrorId} class="input-error" role="alert">
|
||||
{parameterNameError}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<Checkbox checked={param.required} disabled={param.requiredDisabled === true} onchange={updateRequired} />
|
||||
@@ -276,6 +307,22 @@
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
createInitialWorkflowData,
|
||||
ensureStartNodeParameters,
|
||||
FIELD_BINDING_META_KEY,
|
||||
isStartFormFieldKeyAvailable,
|
||||
normalizeStartNodeData,
|
||||
normalizeWorkflowStartNodes,
|
||||
renameStartFieldReferencesInNodes,
|
||||
@@ -148,6 +149,47 @@ describe('workflow node fields', () => {
|
||||
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', () => {
|
||||
const initialStartData = appendStartFormField(
|
||||
createInitialWorkflowData().nodes[0]?.data as Record<string, any>,
|
||||
|
||||
@@ -720,6 +720,36 @@ export function normalizeStartFormSchema(
|
||||
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(
|
||||
field?: Partial<StartFormFieldSchema> | null,
|
||||
existingKeys: string[] = [],
|
||||
@@ -773,6 +803,12 @@ export function updateStartFormField(
|
||||
? (currentData.parameters as Parameter[])
|
||||
: [];
|
||||
const schema = normalizeStartFormSchema(currentData.startFormSchema, currentParameters);
|
||||
if (
|
||||
patch.key !== undefined &&
|
||||
!isStartFormFieldKeyAvailable(currentData, currentKey, patch.key)
|
||||
) {
|
||||
return normalizeStartNodeData(currentData);
|
||||
}
|
||||
const nextSchema = schema.map((field) => {
|
||||
if (field.key !== currentKey) {
|
||||
return field;
|
||||
|
||||
Reference in New Issue
Block a user