mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-09-25 13:26:22 +00:00
feat(editor): 数据源字段路径支持数组下标,并细化 field-select 与展示条件的类型校验
This commit is contained in:
parent
86fcd5d593
commit
a577d7cee3
@ -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` 等) |
|
||||||
|
|||||||
@ -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('或')}的字段`);
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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 || '';
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 函数行为', () => {
|
||||||
|
|||||||
@ -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]类型不合法',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user