diff --git a/packages/editor/src/utils/data-source/index.ts b/packages/editor/src/utils/data-source/index.ts index 34e193df..78b0a983 100644 --- a/packages/editor/src/utils/data-source/index.ts +++ b/packages/editor/src/utils/data-source/index.ts @@ -243,7 +243,7 @@ export const resolveFieldByPath = ( fields: DataSchema[] | undefined, fieldNames: string[], options: { skipNumberIndices?: boolean } = {}, -): { ok: boolean; field?: DataSchema; fields: DataSchema[] } => { +): { ok: boolean; field?: DataSchema; fields: DataSchema[]; failedName?: string } => { let currentFields = fields || []; let field: DataSchema | undefined; @@ -252,11 +252,11 @@ export const resolveFieldByPath = ( continue; } if (!currentFields.length) { - return { ok: false, fields: currentFields }; + return { ok: false, fields: currentFields, failedName: name }; } field = currentFields.find((item) => item.name === name); if (!field) { - return { ok: false, fields: currentFields }; + return { ok: false, fields: currentFields, failedName: name }; } currentFields = field.fields || []; } diff --git a/packages/editor/src/utils/type-match-rules.ts b/packages/editor/src/utils/type-match-rules.ts index ec24a5fc..112bede8 100644 --- a/packages/editor/src/utils/type-match-rules.ts +++ b/packages/editor/src/utils/type-match-rules.ts @@ -20,6 +20,7 @@ import type { DataSourceFieldType, DataSourceSchema, Id } from '@tmagic/core'; import { NodeType } from '@tmagic/core'; import { appendValidateSuggestion } from '@tmagic/design'; import type { TypeMatchValidateContext, TypeMatchValidator } from '@tmagic/form'; +import { validateTypeMatch } from '@tmagic/form'; import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, DATA_SOURCE_SET_DATA_METHOD_NAME, @@ -261,16 +262,20 @@ const validateDataSourceFieldPath = ( const ds = findDataSource(`${dsId}`); if (!ds) { - return defaultMessage(options.message, '值不在可选项中', dataSourceIdSuggestion()); + return defaultMessage(options.message, `数据源(${dsId})不存在`, dataSourceIdSuggestion()); } if (!fieldNames.length) { return undefined; } - const { field, ok } = resolveFieldByPath(ds.fields, fieldNames); + const { field, ok, fields, failedName } = resolveFieldByPath(ds.fields, fieldNames); if (!ok) { - return defaultMessage(options.message, '值不在可选项中', dataSourceIdSuggestion()); + return defaultMessage( + options.message, + `数据源字段(${failedName})不存在`, + listSuggestion(fields.map((item) => item.name)), + ); } const allowedTypes = options.dataSourceFieldType || ['any']; @@ -471,11 +476,12 @@ const isDataSourceFieldPathValue = (value: any, config: any): value is string[] return `${value[0]}`.startsWith(DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX); }; -const validateDataSourceFieldSelect: TypeMatchValidator = (value, { message, props }) => { +const validateDataSourceFieldSelect: TypeMatchValidator = (value, { mForm, message, props }) => { const config = props.config || {}; if (config.fieldConfig && !isDataSourceFieldPathValue(value, config)) { - return undefined; + // 值不是数据源字段路径时,按 fieldConfig 的类型校验(与表单项自身 typeMatch 行为一致) + return validateTypeMatch(value, mForm, { ...props, config: { name: config.name, ...config.fieldConfig } }, message); } if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) { diff --git a/packages/editor/tests/unit/utils/data-source.spec.ts b/packages/editor/tests/unit/utils/data-source.spec.ts index f77eef57..561552fc 100644 --- a/packages/editor/tests/unit/utils/data-source.spec.ts +++ b/packages/editor/tests/unit/utils/data-source.spec.ts @@ -105,8 +105,8 @@ describe('data-source utils', () => { fields: [], }); expect(resolveFieldByPath(fields, ['obj']).field?.name).toBe('obj'); - expect(resolveFieldByPath(fields, ['unknown'])).toEqual({ ok: false, fields }); - expect(resolveFieldByPath(undefined, ['x'])).toEqual({ ok: false, fields: [] }); + expect(resolveFieldByPath(fields, ['unknown'])).toEqual({ ok: false, fields, failedName: 'unknown' }); + expect(resolveFieldByPath(undefined, ['x'])).toEqual({ ok: false, fields: [], failedName: 'x' }); expect(resolveFieldByPath(fields, []).ok).toBe(true); }); @@ -124,7 +124,9 @@ describe('data-source utils', () => { expect(result.field?.name).toBe('item'); expect(result.fields).toEqual([{ name: 'n', type: 'string' }]); - expect(resolveFieldByPath(fields, ['arr', '0', 'missing'], { skipNumberIndices: true }).ok).toBe(false); + const failed = resolveFieldByPath(fields, ['arr', '0', 'missing'], { skipNumberIndices: true }); + expect(failed.ok).toBe(false); + expect(failed.failedName).toBe('missing'); }); test('getFieldType 沿 path 取最终类型', () => { diff --git a/packages/editor/tests/unit/utils/typeMatchRules.spec.ts b/packages/editor/tests/unit/utils/typeMatchRules.spec.ts index ba45e649..aec5c1de 100644 --- a/packages/editor/tests/unit/utils/typeMatchRules.spec.ts +++ b/packages/editor/tests/unit/utils/typeMatchRules.spec.ts @@ -207,9 +207,18 @@ describe('editorTypeMatchRules', () => { expect(firstLine(run('data-source-method-select', ['missing', 'custom']))).toBe('数据源(missing)不存在'); }); - test('data-source-field-select 校验路径与 fieldConfig 跳过', () => { + test('data-source-field-select 校验路径与 fieldConfig 类型校验', () => { expect(firstLine(run('data-source-field-select', 'x'))).toBe('x类型应为字符串数组'); - expect(run('data-source-field-select', 'text-value', { fieldConfig: { type: 'text' } })).toBeUndefined(); + + // 有 fieldConfig 且值不是数据源字段路径时,按 fieldConfig 的类型校验(text 允许数字) + expect(run('data-source-field-select', 'text-value', { name: 'f', fieldConfig: { type: 'text' } })).toBeUndefined(); + expect(run('data-source-field-select', 123, { name: 'f', fieldConfig: { type: 'text' } })).toBeUndefined(); + expect(firstLine(run('data-source-field-select', { a: 1 }, { name: 'f', fieldConfig: { type: 'text' } }))).toBe( + '[object Object] 类型应为字符串', + ); + expect(run('data-source-field-select', 123, { name: 'f', fieldConfig: { type: 'number' } })).toBeUndefined(); + // fieldConfig 未声明 type 时不做类型校验 + expect(run('data-source-field-select', 123, { name: 'f', fieldConfig: {} })).toBeUndefined(); dataSourcesState.value = [ { @@ -225,7 +234,26 @@ describe('editorTypeMatchRules', () => { expect(run('data-source-field-select', ['ds1', 'title'], { value: 'key' })).toBeUndefined(); expect(run('data-source-field-select', [`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}ds1`, 'title'])).toBeUndefined(); - expect(firstLine(run('data-source-field-select', ['ds1', 'missing'], { value: 'key' }))).toBe('值不在可选项中'); + // 数据源不存在时直出具体的数据源 id + expect(firstLine(run('data-source-field-select', ['missing', 'title'], { value: 'key' }))).toBe( + '数据源(missing)不存在', + ); + expect(firstLine(run('data-source-field-select', ['title'], { dataSourceId: 'missing' }))).toBe( + '数据源(missing)不存在', + ); + // 字段路径解析失败时直出具体失败的字段名 + expect(firstLine(run('data-source-field-select', ['ds1', 'missing'], { value: 'key' }))).toBe( + '数据源字段(missing)不存在', + ); + // 字段路径解析失败时,建议列出失败层级的可用字段名而非数据源 id + expect(run('data-source-field-select', ['ds1', 'missing'], { value: 'key' })).toBe( + '数据源字段(missing)不存在\n\n请使用以下某一个值:"title";"obj"', + ); + expect(run('data-source-field-select', ['ds1', 'obj', 'missing'], { value: 'key' })).toBe( + '数据源字段(missing)不存在\n\n请使用以下某一个值:"a"', + ); + // 父字段无子字段时无建议 + expect(run('data-source-field-select', ['ds1', 'title', 'x'], { value: 'key' })).toBe('数据源字段(x)不存在'); expect( firstLine( run('data-source-field-select', ['title'], {