发布 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 {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;

View File

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

View File

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