发布 v1.10 #5

Merged
czm merged 147 commits from develop into main 2026-08-20 11:36:27 +08:00
3 changed files with 128 additions and 3 deletions
Showing only changes of commit 9959f95b94 - Show all commits

View File

@@ -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;

View File

@@ -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>,

View File

@@ -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;