feat(editor): 数据源字段路径支持数组下标,并细化 field-select 与展示条件的类型校验

This commit is contained in:
roymondchen 2026-09-23 11:43:36 +08:00
parent 86fcd5d593
commit a577d7cee3
7 changed files with 351 additions and 47 deletions

View File

@ -165,7 +165,7 @@ app.use(MagicForm, {
| `page-fragment-select` / `ui-select` | `string \| number`;有节点树时须为已有页面片 / 组件 id | | `page-fragment-select` / `ui-select` | `string \| number`;有节点树时须为已有页面片 / 组件 id |
| `data-source-input` | `string`;`${...}` 绑定须指向已有数据源/字段 | | `data-source-input` | `string`;`${...}` 绑定须指向已有数据源/字段 |
| `data-source-method-select` | `[dsId, methodName]`,方法须在该数据源可选方法集中 | | `data-source-method-select` | `[dsId, methodName]`,方法须在该数据源可选方法集中 |
| `data-source-field-select` | 数据源路径 `string[]`;有 `fieldConfig` 且非路径值时跳过 | | `data-source-field-select` | 数据源路径 `Array<string \| number>`。`number` 只能作数组下标;数字字符串先按字段名匹配,没有同名字段时才作下标。元素结构为空的下标,在限定了具体字段类型时不通过。有 `fieldConfig` 且非路径值时按 `fieldConfig` 校验 |
| `data-source-select` | `value: 'id'` 为已有 ds id;否则为含 `isBindDataSource` + `dataSourceId` 的对象 | | `data-source-select` | `value: 'id'` 为已有 ds id;否则为含 `isBindDataSource` + `dataSourceId` 的对象 |
| `code-select` | `{ hookType: 'code', hookData }` 的浅层结构校验(`codeId` 存在性 / 数据源方法存在性由内部 `code-select-col`、`data-source-method-select` 单元格各自校验,只标红出错单元格) | | `code-select` | `{ hookType: 'code', hookData }` 的浅层结构校验(`codeId` 存在性 / 数据源方法存在性由内部 `code-select-col`、`data-source-method-select` 单元格各自校验,只标红出错单元格) |
| `data-source-fields` / `data-source-mocks` / `data-source-methods` | 数组 + 浅层结构(`name`/`type`、`title`/`enable`/`data`、`content`/`params` 等) | | `data-source-fields` / `data-source-mocks` / `data-source-methods` | 数组 + 浅层结构(`name`/`type`、`title`/`enable`/`data`、`content`/`params` 等) |

View File

@ -51,7 +51,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, ref, resolveComponent, watch } from 'vue'; import { computed, inject, ref, resolveComponent, watch } from 'vue';
import { DataSchema } from '@tmagic/core';
import { TMagicButton, tMagicMessage, TMagicTooltip } from '@tmagic/design'; import { TMagicButton, tMagicMessage, TMagicTooltip } from '@tmagic/design';
import { import {
type ContainerChangeEventData, type ContainerChangeEventData,
@ -64,6 +63,7 @@ import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, removeDataSourceFieldPrefix } f
import MIcon from '@editor/components/Icon.vue'; import MIcon from '@editor/components/Icon.vue';
import { useServices } from '@editor/hooks/use-services'; import { useServices } from '@editor/hooks/use-services';
import { resolveFieldByPath } from '@editor/utils/data-source';
import dataSourceIcon from '../../icons/DatasourceIcon.vue'; import dataSourceIcon from '../../icons/DatasourceIcon.vue';
@ -175,11 +175,8 @@ const onChangeHandler = (value: string[], eventData?: ContainerChangeEventData)
return; return;
} }
let fields = dataSource.fields || []; const { field, ok, untypedArrayElement } = resolveFieldByPath(dataSource.fields, keys, {
let field: DataSchema | undefined; allowArrayIndex: true,
(keys || []).forEach((key) => {
field = fields.find((f) => f.name === key);
fields = field?.fields || [];
}); });
const dataSourceFieldType = props.config.dataSourceFieldType || ['any']; const dataSourceFieldType = props.config.dataSourceFieldType || ['any'];
@ -187,11 +184,14 @@ const onChangeHandler = (value: string[], eventData?: ContainerChangeEventData)
dataSourceFieldType.push('any'); dataSourceFieldType.push('any');
} }
if ( // 未声明元素结构的下标不是显式 any,限定了具体类型时不放行
!keys.length || const typeMatches =
(field?.type && ok &&
(field.type === 'any' || dataSourceFieldType.includes('any') || dataSourceFieldType.includes(field.type))) !!field?.type &&
) { (dataSourceFieldType.includes('any') ||
(!untypedArrayElement && (field.type === 'any' || dataSourceFieldType.includes(field.type))));
if (!keys.length || typeMatches) {
emit('change', value, eventData); emit('change', value, eventData);
} else { } else {
tMagicMessage.error(`请选择类型为${dataSourceFieldType.join('或')}的字段`); tMagicMessage.error(`请选择类型为${dataSourceFieldType.join('或')}的字段`);

View File

@ -20,7 +20,7 @@ import type { DisplayCondsConfig, FormState, GroupListConfig } from '@tmagic/for
import { removeDataSourceFieldPrefix } from '@tmagic/utils'; import { removeDataSourceFieldPrefix } from '@tmagic/utils';
import dataSourceService from '@editor/services/dataSource'; import dataSourceService from '@editor/services/dataSource';
import { getCascaderOptionsFromFields, getFieldType } from '@editor/utils/data-source'; import { getCascaderOptionsFromFields, getFieldType, resolveFieldByPath } from '@editor/utils/data-source';
import { stickyAddButton } from './stickyAddButton'; import { stickyAddButton } from './stickyAddButton';
@ -38,7 +38,7 @@ export const createDisplayCondsConfig = (
name: string, name: string,
parentFields: string[], parentFields: string[],
): GroupListConfig => { ): GroupListConfig => {
const resolveFieldPath = (path: string[]) => { const resolveFieldPath = (path: Array<string | number>) => {
const [id, ...fieldNames] = path; const [id, ...fieldNames] = path;
const ds = id ? dataSourceService.getDataSourceById(removeDataSourceFieldPrefix(`${id}`)) : undefined; const ds = id ? dataSourceService.getDataSourceById(removeDataSourceFieldPrefix(`${id}`)) : undefined;
return { ds, fieldNames }; return { ds, fieldNames };
@ -69,13 +69,13 @@ export const createDisplayCondsConfig = (
return []; return [];
} }
let fields = ds.fields || []; const resolved = resolveFieldByPath(ds.fields, fieldNames, { allowArrayIndex: true });
fieldNames.forEach((key) => { return getCascaderOptionsFromFields(resolved.ok ? resolved.fields : [], [
const field = fields.find((f) => f.name === key); 'string',
fields = field?.fields || []; 'number',
}); 'boolean',
'any',
return getCascaderOptionsFromFields(fields, ['string', 'number', 'boolean', 'any']); ]);
}, },
name: 'field', name: 'field',
value: 'key', value: 'key',

View File

@ -1,6 +1,6 @@
import type { DataSchema, DataSourceFieldType, DataSourceSchema } from '@tmagic/core'; import type { DataSchema, DataSourceFieldType, DataSourceSchema } from '@tmagic/core';
import { type CascaderOption, type FormConfig, type TabConfig } from '@tmagic/form'; import { type CascaderOption, type FormConfig, type TabConfig } from '@tmagic/form';
import { dataSourceTemplateRegExp, getKeysArray, isNumber } from '@tmagic/utils'; import { dataSourceTemplateRegExp, getKeysArray, isArrayIndex, isNumber } from '@tmagic/utils';
import BaseFormConfig from './formConfigs/base'; import BaseFormConfig from './formConfigs/base';
import HttpFormConfig from './formConfigs/http'; import HttpFormConfig from './formConfigs/http';
@ -239,34 +239,86 @@ export const getCascaderOptionsFromFields = (
/** /**
* 按字段名路径下钻 DataSchema。 * 按字段名路径下钻 DataSchema。
* @param skipNumberIndices 为 true 时跳过数字段(模板路径中的数组下标,如 arr[0].x) * @param skipNumberIndices 为 true 时跳过数字段(模板路径中的数组下标,如 arr[0].x)
* @param allowArrayIndex 为 true 时识别数组下标。`number` 只能是下标;数字字符串先按当前层字段名匹配,没有同名字段时才当中下标
*/ */
export const resolveFieldByPath = ( export const resolveFieldByPath = (
fields: DataSchema[] | undefined, fields: DataSchema[] | undefined,
fieldNames: string[], fieldNames: Array<string | number>,
options: { skipNumberIndices?: boolean } = {}, options: { skipNumberIndices?: boolean; allowArrayIndex?: boolean } = {},
): { ok: boolean; field?: DataSchema; fields: DataSchema[]; failedName?: string } => { ): {
ok: boolean;
field?: DataSchema;
fields: DataSchema[];
failedName?: string;
/** number 下标没有紧跟在数组字段后面 */
invalidArrayIndex?: boolean;
/** 路径停在数组下标上,当前值是数组元素 */
arrayElement?: boolean;
/** 数组没有子字段,元素类型未知 */
untypedArrayElement?: boolean;
} => {
let currentFields = fields || []; let currentFields = fields || [];
let field: DataSchema | undefined; let field: DataSchema | undefined;
/** 刚消费了数组下标,路径若在此结束则当前值是数组元素 */
let atArrayElement = false;
let arrayIndexName = '';
for (const name of fieldNames) { for (const name of fieldNames) {
if (options.skipNumberIndices && isNumber(name)) { if (options.skipNumberIndices && isNumber(name)) {
continue; continue;
} }
if (!currentFields.length) {
return { ok: false, fields: currentFields, failedName: name }; const segment = `${name}`;
// number 不能当字段名。数字字符串仅在当前层没有同名字段时才当中下标,避免盖住名为 "0" 的子字段。
if (options.allowArrayIndex && isArrayIndex(name) && field?.type === 'array' && !atArrayElement) {
const namedField = typeof name === 'string' ? currentFields.find((item) => item.name === segment) : undefined;
if (!namedField) {
atArrayElement = true;
arrayIndexName = segment;
continue;
} }
field = currentFields.find((item) => item.name === name); }
if (options.allowArrayIndex && typeof name === 'number') {
return { ok: false, fields: currentFields, failedName: segment, invalidArrayIndex: true };
}
atArrayElement = false;
if (!currentFields.length) {
return { ok: false, fields: currentFields, failedName: segment };
}
field = currentFields.find((item) => item.name === segment);
if (!field) { if (!field) {
return { ok: false, fields: currentFields, failedName: name }; return { ok: false, fields: currentFields, failedName: segment };
} }
currentFields = field.fields || []; currentFields = field.fields || [];
} }
if (atArrayElement && field?.type === 'array') {
const elementFields = field.fields || [];
const untypedArrayElement = elementFields.length === 0;
return {
ok: true,
arrayElement: true,
untypedArrayElement,
field: {
name: arrayIndexName,
// 有子字段时元素是这些字段组成的对象;没有子字段时类型未知,不能当成显式 any
type: untypedArrayElement ? 'any' : 'object',
fields: elementFields,
},
fields: elementFields,
};
}
return { field, ok: true, fields: currentFields }; return { field, ok: true, fields: currentFields };
}; };
export const getFieldType = (ds: DataSourceSchema | undefined, fieldNames: string[]) => { export const getFieldType = (ds: DataSourceSchema | undefined, fieldNames: Array<string | number>) => {
const { ok, field } = resolveFieldByPath(ds?.fields, fieldNames); const { ok, field, untypedArrayElement } = resolveFieldByPath(ds?.fields, fieldNames, {
if (!ok) return ''; allowArrayIndex: true,
});
if (!ok || untypedArrayElement) return '';
return field?.type || ''; return field?.type || '';
}; };

View File

@ -31,6 +31,7 @@ import {
DATA_SOURCE_SET_DATA_METHOD_NAME, DATA_SOURCE_SET_DATA_METHOD_NAME,
dataSourceTemplateRegExp, dataSourceTemplateRegExp,
getKeysArray, getKeysArray,
isArrayIndex,
removeDataSourceFieldPrefix, removeDataSourceFieldPrefix,
} from '@tmagic/utils'; } from '@tmagic/utils';
@ -211,7 +212,7 @@ const displayCondSuggestion = (): string => {
}; };
const validateDataSourceFieldPath = ( const validateDataSourceFieldPath = (
path: string[], path: Array<string | number>,
options: { options: {
dataSourceId?: string; dataSourceId?: string;
dataSourceFieldType?: DataSourceFieldType[]; dataSourceFieldType?: DataSourceFieldType[];
@ -239,8 +240,17 @@ const validateDataSourceFieldPath = (
return undefined; return undefined;
} }
const { field, ok, fields, failedName } = resolveFieldByPath(ds.fields, path); const { field, ok, fields, failedName, invalidArrayIndex, arrayElement, untypedArrayElement } = resolveFieldByPath(
ds.fields,
path,
{
allowArrayIndex: true,
},
);
if (!ok) { if (!ok) {
if (invalidArrayIndex) {
return defaultMessage(options.message, `数组下标(${failedName})只能接在数组字段后面`);
}
return defaultMessage( return defaultMessage(
options.message, options.message,
`数据源字段(${failedName})不存在`, `数据源字段(${failedName})不存在`,
@ -253,6 +263,13 @@ const validateDataSourceFieldPath = (
return undefined; return undefined;
} }
if (untypedArrayElement) {
return defaultMessage(
options.message,
`数组下标(${field?.name ?? path[path.length - 1]})的元素类型未定义,请选择具体字段`,
);
}
const leafType = field?.type || 'any'; const leafType = field?.type || 'any';
if (leafType !== 'any' && !allowedTypes.includes(leafType)) { if (leafType !== 'any' && !allowedTypes.includes(leafType)) {
const fieldName = field?.name || path[path.length - 1]; const fieldName = field?.name || path[path.length - 1];
@ -260,7 +277,9 @@ const validateDataSourceFieldPath = (
// 文案已点明当前字段类型与要求类型,无需再列同级可选字段 // 文案已点明当前字段类型与要求类型,无需再列同级可选字段
return defaultMessage( return defaultMessage(
options.message, options.message,
`请选择类型为${allowedTypes.join('或')}的字段,字段(${fieldName})的类型为${leafType}`, arrayElement
? `请选择类型为${allowedTypes.join('或')}的字段,数组元素的类型为${leafType}`
: `请选择类型为${allowedTypes.join('或')}的字段,字段(${fieldName})的类型为${leafType}`,
); );
} }
}; };
@ -437,8 +456,12 @@ const validateDataSourceInput: TypeMatchValidator = (value, { message }) => {
const validateDataSourceMethodSelect: TypeMatchValidator = (value, { message }) => const validateDataSourceMethodSelect: TypeMatchValidator = (value, { message }) =>
validateDataSourceMethodTuple(value, message); validateDataSourceMethodTuple(value, message);
const isDataSourceFieldPathValue = (value: any, config: any): value is string[] => { /** 字段名,或非负整数下标(number)。数字字符串本身是 string,走字段名分支。 */
if (!Array.isArray(value) || !value.length || value.some((item) => typeof item !== 'string')) { const isDataSourceFieldPathSegment = (item: unknown): item is string | number =>
typeof item === 'string' || (typeof item === 'number' && isArrayIndex(item));
const isDataSourceFieldPathValue = (value: any, config: any): value is Array<string | number> => {
if (!Array.isArray(value) || !value.length || value.some((item) => !isDataSourceFieldPathSegment(item))) {
return false; return false;
} }
if (config.dataSourceId) { if (config.dataSourceId) {
@ -488,16 +511,29 @@ export const validateDataSourceFieldSelect = (
} }
} }
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) { if (!Array.isArray(value) || value.some((item) => !isDataSourceFieldPathSegment(item))) {
return defaultMessage(message, `${value}类型应为字符串数组`, dataSourceFieldPathSuggestion()); return defaultMessage(
message,
`${value}类型应为字符串数组,数组字段后可接数字下标`,
dataSourceFieldPathSuggestion(),
);
} }
// 未指定 dataSourceId 时,路径首项为数据源 id,其余为字段名。 // 未指定 dataSourceId 时,路径首项为数据源 id,其余为字段名。
// value 模式的首项带 ds-field:: 前缀、key 模式不带,removeDataSourceFieldPrefix 对后者是幂等的,无需分支。 // value 模式的首项带 ds-field:: 前缀、key 模式不带,removeDataSourceFieldPrefix 对后者是幂等的,无需分支。
// 数组下标只出现在字段段,数据源 id 仍必须是字符串。
let dataSourceId = config.dataSourceId ? `${config.dataSourceId}` : undefined; let dataSourceId = config.dataSourceId ? `${config.dataSourceId}` : undefined;
let fieldNames = value; let fieldNames: Array<string | number> = value;
if (!dataSourceId) { if (!dataSourceId) {
dataSourceId = value.length ? removeDataSourceFieldPrefix(value[0]) : undefined; const dsId = value[0];
if (value.length && typeof dsId !== 'string') {
return defaultMessage(
message,
`${value}类型应为字符串数组,数组字段后可接数字下标`,
dataSourceFieldPathSuggestion(),
);
}
dataSourceId = value.length ? removeDataSourceFieldPrefix(dsId) : undefined;
fieldNames = value.slice(1); fieldNames = value.slice(1);
} }

View File

@ -105,8 +105,16 @@ describe('data-source utils', () => {
fields: [], fields: [],
}); });
expect(resolveFieldByPath(fields, ['obj']).field?.name).toBe('obj'); expect(resolveFieldByPath(fields, ['obj']).field?.name).toBe('obj');
expect(resolveFieldByPath(fields, ['unknown'])).toEqual({ ok: false, fields, failedName: 'unknown' }); expect(resolveFieldByPath(fields, ['unknown'])).toEqual({
expect(resolveFieldByPath(undefined, ['x'])).toEqual({ ok: false, fields: [], failedName: 'x' }); ok: false,
fields,
failedName: 'unknown',
});
expect(resolveFieldByPath(undefined, ['x'])).toEqual({
ok: false,
fields: [],
failedName: 'x',
});
expect(resolveFieldByPath(fields, []).ok).toBe(true); expect(resolveFieldByPath(fields, []).ok).toBe(true);
}); });
@ -129,14 +137,123 @@ describe('data-source utils', () => {
expect(failed.failedName).toBe('missing'); expect(failed.failedName).toBe('missing');
}); });
test('resolveFieldByPath 数组字段后可接下标,下标可以是 number', () => {
const fields: any = [
{
name: 'arr',
type: 'array',
fields: [{ name: 'item', type: 'string' }],
},
{
name: 'title',
type: 'string',
},
{
name: 'matrix',
type: 'array',
fields: [{ name: 'row', type: 'array', fields: [{ name: 'cell', type: 'number' }] }],
},
];
const byNumber = resolveFieldByPath(fields, ['arr', 0, 'item'], { allowArrayIndex: true });
expect(byNumber.ok).toBe(true);
expect(byNumber.field).toMatchObject({ name: 'item', type: 'string' });
const byNumericString = resolveFieldByPath(fields, ['arr', '0', 'item'], {
allowArrayIndex: true,
});
expect(byNumericString.ok).toBe(true);
expect(byNumericString.field?.name).toBe('item');
const element = resolveFieldByPath(fields, ['arr', 0], { allowArrayIndex: true });
expect(element.ok).toBe(true);
expect(element.field).toMatchObject({ name: '0', type: 'object' });
const nested = resolveFieldByPath(fields, ['matrix', 0, 'row', 1, 'cell'], {
allowArrayIndex: true,
});
expect(nested.ok).toBe(true);
expect(nested.field).toMatchObject({ name: 'cell', type: 'number' });
const invalid = resolveFieldByPath(fields, ['title', 0], { allowArrayIndex: true });
expect(invalid).toMatchObject({ ok: false, invalidArrayIndex: true, failedName: '0' });
const untyped = resolveFieldByPath([{ name: 'ids', type: 'array' }] as any, ['ids', 0], {
allowArrayIndex: true,
});
expect(untyped.ok).toBe(true);
expect(untyped.untypedArrayElement).toBe(true);
expect(untyped.field).toMatchObject({ name: '0', type: 'any' });
// 数字字符串优先匹配同名字段;number 仍是下标,再继续找元素上的字段
const numericName = resolveFieldByPath(
[
{
name: 'arr',
type: 'array',
fields: [{ name: '0', type: 'object', fields: [{ name: 'x', type: 'string' }] }],
},
] as any,
['arr', '0', 'x'],
{ allowArrayIndex: true },
);
expect(numericName.ok).toBe(true);
expect(numericName.field).toMatchObject({ name: 'x', type: 'string' });
const indexThenProp = resolveFieldByPath(
[
{
name: 'arr',
type: 'array',
fields: [
{ name: '0', type: 'number' },
{ name: 'item', type: 'string' },
],
},
] as any,
['arr', 0, 'item'],
{ allowArrayIndex: true },
);
expect(indexThenProp.ok).toBe(true);
expect(indexThenProp.field).toMatchObject({ name: 'item', type: 'string' });
const namedZero = resolveFieldByPath(
[
{
name: 'arr',
type: 'array',
fields: [
{ name: '0', type: 'number' },
{ name: 'item', type: 'string' },
],
},
] as any,
['arr', '0'],
{ allowArrayIndex: true },
);
expect(namedZero.arrayElement).toBeUndefined();
expect(namedZero.field).toMatchObject({ name: '0', type: 'number' });
});
test('getFieldType 沿 path 取最终类型', () => { test('getFieldType 沿 path 取最终类型', () => {
const ds: any = { const ds: any = {
fields: [{ name: 'obj', type: 'object', fields: [{ name: 'name', type: 'string' }] }], fields: [
{ name: 'obj', type: 'object', fields: [{ name: 'name', type: 'string' }] },
{ name: 'arr', type: 'array', fields: [{ name: 'item', type: 'string' }] },
{ name: 'ids', type: 'array' },
{ name: 'keyed', type: 'array', fields: [{ name: '0', type: 'number' }] },
],
}; };
expect(getFieldType(ds, ['obj', 'name'])).toBe('string'); expect(getFieldType(ds, ['obj', 'name'])).toBe('string');
expect(getFieldType(ds, ['obj'])).toBe('object'); expect(getFieldType(ds, ['obj'])).toBe('object');
expect(getFieldType(ds, ['unknown'])).toBe(''); expect(getFieldType(ds, ['unknown'])).toBe('');
expect(getFieldType(undefined, ['x'])).toBe(''); expect(getFieldType(undefined, ['x'])).toBe('');
expect(getFieldType(ds, ['arr', 0, 'item'])).toBe('string');
expect(getFieldType(ds, ['arr', '0', 'item'])).toBe('string');
expect(getFieldType(ds, ['arr', 0])).toBe('object');
expect(getFieldType(ds, ['ids', 0])).toBe('');
expect(getFieldType(ds, ['keyed', '0'])).toBe('number');
expect(getFieldType(ds, ['keyed', 0, '0'])).toBe('number');
}); });
test('getFormConfig - 内部 tab 配置 defaultValue/display 函数行为', () => { test('getFormConfig - 内部 tab 配置 defaultValue/display 函数行为', () => {

View File

@ -211,7 +211,7 @@ describe('editorTypeMatchRules', () => {
}); });
test('data-source-field-select 校验路径与 fieldConfig 类型校验', () => { test('data-source-field-select 校验路径与 fieldConfig 类型校验', () => {
expect(firstLine(run('data-source-field-select', 'x'))).toBe('x类型应为字符串数组'); expect(firstLine(run('data-source-field-select', 'x'))).toBe('x类型应为字符串数组,数组字段后可接数字下标');
// 有 fieldConfig 且值不是数据源字段路径时,按 fieldConfig 的类型校验(text 允许数字) // 有 fieldConfig 且值不是数据源字段路径时,按 fieldConfig 的类型校验(text 允许数字)
expect(run('data-source-field-select', 'text-value', { name: 'f', fieldConfig: { type: 'text' } })).toBeUndefined(); expect(run('data-source-field-select', 'text-value', { name: 'f', fieldConfig: { type: 'text' } })).toBeUndefined();
@ -282,6 +282,95 @@ describe('editorTypeMatchRules', () => {
}), }),
).toBe('请选择类型为string的字段,字段(a)的类型为number'); ).toBe('请选择类型为string的字段,字段(a)的类型为number');
dataSourcesState.value = [
{
id: 'ds1',
type: 'base',
fields: [
{ name: 'title', type: 'string' },
{ name: 'arr', type: 'array', fields: [{ name: 'item', type: 'string' }] },
{
name: 'matrix',
type: 'array',
fields: [{ name: 'row', type: 'array', fields: [{ name: 'cell', type: 'number' }] }],
},
],
methods: [],
},
];
// 数组字段后可接下标:number 与数字字符串都合法,并继续解析元素字段
expect(run('data-source-field-select', ['ds1', 'arr', 0, 'item'], { value: 'key' })).toBeUndefined();
expect(run('data-source-field-select', ['ds1', 'arr', '0', 'item'], { value: 'key' })).toBeUndefined();
expect(
run('data-source-field-select', [`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}ds1`, 'arr', 0, 'item'], {
fieldConfig: { type: 'text' },
dataSourceFieldType: ['string'],
}),
).toBeUndefined();
expect(run('data-source-field-select', ['arr', 0, 'item'], { dataSourceId: 'ds1' })).toBeUndefined();
expect(run('data-source-field-select', ['ds1', 'matrix', 0, 'row', 1, 'cell'], { value: 'key' })).toBeUndefined();
expect(
run('data-source-field-select', ['ds1', 'arr', 0], {
value: 'key',
dataSourceFieldType: ['object'],
}),
).toBe(undefined);
expect(
firstLine(
run('data-source-field-select', ['ds1', 'arr', 0], {
value: 'key',
dataSourceFieldType: ['string'],
}),
),
).toBe('请选择类型为string的字段,数组元素的类型为object');
expect(firstLine(run('data-source-field-select', ['ds1', 'title', 0], { value: 'key' }))).toBe(
'数组下标(0)只能接在数组字段后面',
);
expect(firstLine(run('data-source-field-select', ['ds1', 'arr', 1.5], { value: 'key' }))).toBe(
'ds1,arr,1.5类型应为字符串数组,数组字段后可接数字下标',
);
expect(firstLine(run('data-source-field-select', [0, 'item'], { value: 'key' }))).toBe(
'0,item类型应为字符串数组,数组字段后可接数字下标',
);
dataSourcesState.value = [
{
id: 'ds1',
type: 'base',
fields: [
{
name: 'arr',
type: 'array',
fields: [
{ name: '0', type: 'number' },
{ name: 'item', type: 'string' },
],
},
{ name: 'ids', type: 'array' },
],
methods: [],
},
];
// 数字字符串与子字段同名时按字段名,number 下标再取元素上的字段
expect(
run('data-source-field-select', ['ds1', 'arr', '0'], {
value: 'key',
dataSourceFieldType: ['number'],
}),
).toBeUndefined();
expect(run('data-source-field-select', ['ds1', 'arr', 0, 'item'], { value: 'key' })).toBeUndefined();
expect(firstLine(run('data-source-field-select', ['ds1', 'arr', '0', 'item'], { value: 'key' }))).toBe(
'数据源字段(item)不存在',
);
expect(
firstLine(
run('data-source-field-select', ['ds1', 'ids', 0], {
value: 'key',
dataSourceFieldType: ['string'],
}),
),
).toBe('数组下标(0)的元素类型未定义,请选择具体字段');
// 有 fieldConfig、未声明 value/dataSourceId 时,仍按取值中的 ds-field:: 前缀解析数据源路径 // 有 fieldConfig、未声明 value/dataSourceId 时,仍按取值中的 ds-field:: 前缀解析数据源路径
dataSourcesState.value = [ dataSourcesState.value = [
{ {
@ -378,10 +467,20 @@ describe('editorTypeMatchRules', () => {
'ds2不在可选项中', 'ds2不在可选项中',
); );
expect( expect(
run('data-source-select', { isBindDataSource: true, dataSourceId: 'ds1', dataSourceType: 'base' }), run('data-source-select', {
isBindDataSource: true,
dataSourceId: 'ds1',
dataSourceType: 'base',
}),
).toBeUndefined(); ).toBeUndefined();
expect( expect(
firstLine(run('data-source-select', { isBindDataSource: true, dataSourceId: 'ds1', dataSourceType: 'http' })), firstLine(
run('data-source-select', {
isBindDataSource: true,
dataSourceId: 'ds1',
dataSourceType: 'http',
}),
),
).toBe('[object Object]不在可选项中'); ).toBe('[object Object]不在可选项中');
expect(firstLine(run('data-source-select', { isBindDataSource: false, dataSourceId: 'ds1' }))).toBe( expect(firstLine(run('data-source-select', { isBindDataSource: false, dataSourceId: 'ds1' }))).toBe(
'[object Object]类型不合法', '[object Object]类型不合法',