mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-09-12 15:09:43 +00:00
fix(editor): 统一数据源字段路径解析并优化 typeMatch 校验
修复 ds-field:: 前缀解析、checkStrictly 数据源取值及字段类型不匹配时的校验提示。
This commit is contained in:
parent
050814f3f8
commit
05fbdb17b5
@ -31,6 +31,7 @@ import { computed } from 'vue';
|
||||
|
||||
import { getDesignConfig, TMagicSelect } from '@tmagic/design';
|
||||
import type { CondOpSelectConfig, FieldProps } from '@tmagic/form';
|
||||
import { removeDataSourceFieldPrefix } from '@tmagic/utils';
|
||||
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import { getCondOpOptionsByFieldType, getFieldType } from '@editor/utils';
|
||||
@ -50,8 +51,8 @@ const props = defineProps<FieldProps<CondOpSelectConfig>>();
|
||||
const optionComponent = getDesignConfig('components')?.option;
|
||||
|
||||
const options = computed(() => {
|
||||
const [id, ...fieldNames] = [...(props.config.parentFields || []), ...props.model.field];
|
||||
const ds = dataSourceService.getDataSourceById(id);
|
||||
const [id, ...fieldNames] = [...(props.config.parentFields || []), ...(props.model.field || [])];
|
||||
const ds = id ? dataSourceService.getDataSourceById(removeDataSourceFieldPrefix(id)) : undefined;
|
||||
const type = getFieldType(ds, fieldNames);
|
||||
return getCondOpOptionsByFieldType(type);
|
||||
});
|
||||
|
||||
@ -143,7 +143,10 @@ const checkStrictly = computed(() => {
|
||||
if (typeof props.config.checkStrictly !== 'function') {
|
||||
value = props.config.checkStrictly;
|
||||
} else {
|
||||
const dsId = removeDataSourceFieldPrefix(props.model[0]);
|
||||
const fieldValue = props.model[props.name];
|
||||
// 指定了 dataSourceId 时取值里只有字段名,数据源 id 只能取自 config
|
||||
const rawDsId = props.config.dataSourceId ?? (Array.isArray(fieldValue) ? fieldValue[0] : fieldValue);
|
||||
const dsId = removeDataSourceFieldPrefix(`${rawDsId}`);
|
||||
const dataSource = dataSources.value.find((ds) => ds.id === dsId);
|
||||
|
||||
value = props.config.checkStrictly(mForm, {
|
||||
|
||||
@ -25,6 +25,7 @@ import {
|
||||
type GroupListConfig,
|
||||
MGroupList,
|
||||
} from '@tmagic/form';
|
||||
import { removeDataSourceFieldPrefix } from '@tmagic/utils';
|
||||
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import { getCascaderOptionsFromFields, getFieldType } from '@editor/utils';
|
||||
@ -46,9 +47,14 @@ const mForm = inject<FormState | undefined>('mForm');
|
||||
|
||||
const parentFields = computed(() => filterFunction<string[]>(mForm, props.config.parentFields, props) || []);
|
||||
|
||||
const resolveFieldPath = (path: string[]) => {
|
||||
const [id, ...fieldNames] = path;
|
||||
const ds = id ? dataSourceService.getDataSourceById(removeDataSourceFieldPrefix(`${id}`)) : undefined;
|
||||
return { ds, fieldNames };
|
||||
};
|
||||
|
||||
const fieldOnChange = (_formState: FormState | undefined, v: string[], { model }: { model: Record<string, any> }) => {
|
||||
const [id, ...fieldNames] = [...parentFields.value, ...v];
|
||||
const ds = dataSourceService.getDataSourceById(id);
|
||||
const { ds, fieldNames } = resolveFieldPath([...parentFields.value, ...v]);
|
||||
const type = getFieldType(ds, fieldNames);
|
||||
if (type === 'number') {
|
||||
model.value = Number(model.value);
|
||||
@ -82,14 +88,13 @@ const config = computed<GroupListConfig>(() => ({
|
||||
? {
|
||||
type: 'cascader',
|
||||
options: () => {
|
||||
const [dsId, ...keys] = parentFields.value;
|
||||
const ds = dataSourceService.getDataSourceById(dsId);
|
||||
const { ds, fieldNames } = resolveFieldPath(parentFields.value);
|
||||
if (!ds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let fields = ds.fields || [];
|
||||
keys.forEach((key) => {
|
||||
fieldNames.forEach((key) => {
|
||||
const field = fields.find((f) => f.name === key);
|
||||
fields = field?.fields || [];
|
||||
});
|
||||
@ -139,8 +144,7 @@ const config = computed<GroupListConfig>(() => ({
|
||||
{
|
||||
name: 'value',
|
||||
type: (_mForm, { model }) => {
|
||||
const [id, ...fieldNames] = [...parentFields.value, ...model.field];
|
||||
const ds = dataSourceService.getDataSourceById(id);
|
||||
const { ds, fieldNames } = resolveFieldPath([...parentFields.value, ...(model.field || [])]);
|
||||
const type = getFieldType(ds, fieldNames);
|
||||
|
||||
if (type === 'number') {
|
||||
|
||||
@ -21,7 +21,7 @@ import { appendValidateSuggestion } from '@tmagic/design';
|
||||
import { type ContainerChangeEventData, defineFormConfig, MContainer } from '@tmagic/form';
|
||||
import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
import { validateDataSourceFieldSelectValue } from '@editor/utils/type-match-rules';
|
||||
import { validateDataSourceFieldSelect } from '@editor/utils/type-match-rules';
|
||||
|
||||
import { AlignCenter, AlignLeft, AlignRight } from '../icons/text-align';
|
||||
|
||||
@ -99,7 +99,7 @@ const formConfig = defineFormConfig([
|
||||
return callback();
|
||||
}
|
||||
|
||||
const result = validateDataSourceFieldSelectValue(
|
||||
const result = validateDataSourceFieldSelect(
|
||||
value,
|
||||
{
|
||||
fieldType: 'data-source-field-select',
|
||||
|
||||
@ -209,7 +209,6 @@ const validateDataSourceFieldPath = (
|
||||
path: string[],
|
||||
options: {
|
||||
dataSourceId?: string;
|
||||
valueMode?: 'key' | 'value';
|
||||
dataSourceFieldType?: DataSourceFieldType[];
|
||||
message?: string;
|
||||
} = {},
|
||||
@ -219,19 +218,11 @@ const validateDataSourceFieldPath = (
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let dsId = options.dataSourceId;
|
||||
let fieldNames = path;
|
||||
|
||||
const dsId = options.dataSourceId;
|
||||
if (!dsId) {
|
||||
if (!path.length) {
|
||||
return defaultMessage(options.message, '值不在可选项中', dataSourceIdSuggestion());
|
||||
}
|
||||
const rawDsId = path[0];
|
||||
dsId =
|
||||
options.valueMode === 'key' || !`${rawDsId}`.startsWith(DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX)
|
||||
? `${rawDsId}`
|
||||
: removeDataSourceFieldPrefix(`${rawDsId}`);
|
||||
fieldNames = path.slice(1);
|
||||
// 仅当取值为空数组或首项为空串时命中。表单校验管道里 validateTypeMatch 对空数组会短路,
|
||||
// 因此这里只可能来自直接调用 validateDataSourceFieldSelect 的自定义 validator
|
||||
return defaultMessage(options.message, '数据源不存在', dataSourceIdSuggestion());
|
||||
}
|
||||
|
||||
const ds = findDataSource(`${dsId}`);
|
||||
@ -239,11 +230,11 @@ const validateDataSourceFieldPath = (
|
||||
return defaultMessage(options.message, `数据源(${dsId})不存在`, dataSourceIdSuggestion());
|
||||
}
|
||||
|
||||
if (!fieldNames.length) {
|
||||
if (!path.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { field, ok, fields, failedName } = resolveFieldByPath(ds.fields, fieldNames);
|
||||
const { field, ok, fields, failedName } = resolveFieldByPath(ds.fields, path);
|
||||
if (!ok) {
|
||||
return defaultMessage(
|
||||
options.message,
|
||||
@ -258,11 +249,15 @@ const validateDataSourceFieldPath = (
|
||||
}
|
||||
|
||||
const leafType = field?.type || 'any';
|
||||
if (leafType === 'any' || allowedTypes.includes(leafType)) {
|
||||
return undefined;
|
||||
}
|
||||
if (leafType !== 'any' && !allowedTypes.includes(leafType)) {
|
||||
const fieldName = field?.name || path[path.length - 1];
|
||||
|
||||
return defaultMessage(options.message, '值不在可选项中', dataSourceIdSuggestion());
|
||||
// 文案已点明当前字段类型与要求类型,无需再列同级可选字段
|
||||
return defaultMessage(
|
||||
options.message,
|
||||
`请选择类型为${allowedTypes.join('或')}的字段,字段(${fieldName})的类型为${leafType}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const validateDataSourceMethodTuple = (value: any, message?: string): string | undefined => {
|
||||
@ -367,7 +362,7 @@ const validateCondOpSelect: TypeMatchValidator = (value, { message, props }) =>
|
||||
const parentFields = props.config?.parentFields || [];
|
||||
const fieldPath = Array.isArray(props.model?.field) ? props.model.field : [];
|
||||
const [id, ...fieldNames] = [...parentFields, ...fieldPath];
|
||||
const ds = id ? findDataSource(`${id}`) : undefined;
|
||||
const ds = id ? findDataSource(removeDataSourceFieldPrefix(`${id}`)) : undefined;
|
||||
const fieldType = getFieldType(ds, fieldNames);
|
||||
const allowed = getCondOpsByFieldType(fieldType);
|
||||
|
||||
@ -464,7 +459,7 @@ export type ValidateDataSourceFieldSelectOptions = {
|
||||
/**
|
||||
* data-source-field-select 的 typeMatch 校验逻辑,可供自定义 rules.validator 复用。
|
||||
*/
|
||||
export const validateDataSourceFieldSelectValue = (
|
||||
export const validateDataSourceFieldSelect = (
|
||||
value: any,
|
||||
context: TypeMatchValidateContext,
|
||||
options?: ValidateDataSourceFieldSelectOptions,
|
||||
@ -492,17 +487,22 @@ export const validateDataSourceFieldSelectValue = (
|
||||
return defaultMessage(message, `${value}类型应为字符串数组`, dataSourceFieldPathSuggestion());
|
||||
}
|
||||
|
||||
return validateDataSourceFieldPath(value, {
|
||||
dataSourceId: config.dataSourceId,
|
||||
valueMode: config.value,
|
||||
// 未指定 dataSourceId 时,路径首项为数据源 id,其余为字段名。
|
||||
// value 模式的首项带 ds-field:: 前缀、key 模式不带,removeDataSourceFieldPrefix 对后者是幂等的,无需分支。
|
||||
let dataSourceId = config.dataSourceId ? `${config.dataSourceId}` : undefined;
|
||||
let fieldNames = value;
|
||||
if (!dataSourceId) {
|
||||
dataSourceId = value.length ? removeDataSourceFieldPrefix(value[0]) : undefined;
|
||||
fieldNames = value.slice(1);
|
||||
}
|
||||
|
||||
return validateDataSourceFieldPath(fieldNames, {
|
||||
dataSourceId,
|
||||
dataSourceFieldType: config.dataSourceFieldType,
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
const validateDataSourceFieldSelect: TypeMatchValidator = (value, context) =>
|
||||
validateDataSourceFieldSelectValue(value, context);
|
||||
|
||||
const validateDataSourceSelect: TypeMatchValidator = (value, { message, props }) => {
|
||||
const config = props.config || {};
|
||||
const dataSources = getDataSources();
|
||||
@ -678,7 +678,8 @@ export const editorTypeMatchRules: Record<string, TypeMatchValidator> = {
|
||||
'ui-select': validateUiSelect,
|
||||
'data-source-input': validateDataSourceInput,
|
||||
'data-source-method-select': validateDataSourceMethodSelect,
|
||||
'data-source-field-select': validateDataSourceFieldSelect,
|
||||
/** 注册进 typeMatch 规则表的入口,收窄为 TypeMatchValidator,避免调用方误传第三个参数 */
|
||||
'data-source-field-select': (value, context) => validateDataSourceFieldSelect(value, context),
|
||||
'data-source-select': validateDataSourceSelect,
|
||||
'code-select': validateCodeSelect,
|
||||
'data-source-fields': validateDataSourceFields,
|
||||
|
||||
@ -100,4 +100,14 @@ describe('CondOpSelect', () => {
|
||||
await wrapper.findComponent({ name: 'TMagicSelect' }).vm.$emit('change', '=');
|
||||
expect(wrapper.emitted('change')?.[0]?.[0]).toBe('=');
|
||||
});
|
||||
|
||||
test('字段路径带 ds-field:: 前缀时仍按数据源 id 查询', () => {
|
||||
(getFieldType as any).mockReturnValue('number');
|
||||
mount(CondOpSelect, {
|
||||
props: baseProps({
|
||||
model: { field: ['ds-field::ds1', 'a'], op: '' },
|
||||
}) as any,
|
||||
});
|
||||
expect(dataSourceService.getDataSourceById).toHaveBeenCalledWith('ds1');
|
||||
});
|
||||
});
|
||||
|
||||
@ -324,4 +324,17 @@ describe('DataSourceFieldSelect Index', () => {
|
||||
await toggleBtn.trigger('click');
|
||||
expect(wrapper.findAll('.fake-cascader').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('checkStrictly 为函数时从当前字段值解析数据源', () => {
|
||||
const checkStrictly = vi.fn(() => true);
|
||||
mount(DSFSIndex, {
|
||||
props: {
|
||||
config: { checkStrictly },
|
||||
model: { v: ['ds-ds1', 'a'] },
|
||||
name: 'v',
|
||||
} as any,
|
||||
});
|
||||
expect(checkStrictly).toHaveBeenCalled();
|
||||
expect(checkStrictly.mock.calls[0][1].dataSource?.id).toBe('ds1');
|
||||
});
|
||||
});
|
||||
|
||||
@ -21,7 +21,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { HookCodeType, HookType, NodeType } from '@tmagic/core';
|
||||
import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, DATA_SOURCE_SET_DATA_METHOD_NAME } from '@tmagic/utils';
|
||||
|
||||
import { ALL_COND_OPS, editorTypeMatchRules, validateDataSourceFieldSelectValue } from '@editor/utils/type-match-rules';
|
||||
import { ALL_COND_OPS, editorTypeMatchRules, validateDataSourceFieldSelect } from '@editor/utils/type-match-rules';
|
||||
|
||||
const codeDslState = vi.hoisted(() => ({ value: null as Record<string, any> | null }));
|
||||
const dataSourcesState = vi.hoisted(() => ({ value: [] as any[] }));
|
||||
@ -126,6 +126,9 @@ describe('editorTypeMatchRules', () => {
|
||||
},
|
||||
];
|
||||
expect(run('cond-op-select', '>', {}, { field: ['ds1', 'age'] })).toBeUndefined();
|
||||
expect(
|
||||
run('cond-op-select', '>', {}, { field: [`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}ds1`, 'age'] }),
|
||||
).toBeUndefined();
|
||||
expect(firstLine(run('cond-op-select', 'include', {}, { field: ['ds1', 'age'] }))).toBe('include 不在可选项中');
|
||||
expect(run('cond-op-select', 'is', {}, { field: ['ds1', 'flag'] })).toBeUndefined();
|
||||
// 未知类型与 UI 默认选项对齐:不含 boolean ops
|
||||
@ -234,6 +237,11 @@ 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();
|
||||
// 未指定 dataSourceId 且路径为空:缺少数据源 id
|
||||
expect(firstLine(run('data-source-field-select', [], { value: 'key' }))).toBe('数据源不存在');
|
||||
// 已有 dataSourceId 时,空字段路径视为只选了数据源,不报错
|
||||
expect(run('data-source-field-select', [], { dataSourceId: 'ds1' })).toBeUndefined();
|
||||
expect(run('data-source-field-select', ['ds1'], { value: 'key' })).toBeUndefined();
|
||||
// 数据源不存在时直出具体的数据源 id
|
||||
expect(firstLine(run('data-source-field-select', ['missing', 'title'], { value: 'key' }))).toBe(
|
||||
'数据源(missing)不存在',
|
||||
@ -254,23 +262,55 @@ describe('editorTypeMatchRules', () => {
|
||||
);
|
||||
// 父字段无子字段时无建议
|
||||
expect(run('data-source-field-select', ['ds1', 'title', 'x'], { value: 'key' })).toBe('数据源字段(x)不存在');
|
||||
// 类型不匹配时,文案直出当前字段与其类型,不追加建议
|
||||
expect(
|
||||
firstLine(
|
||||
run('data-source-field-select', ['title'], {
|
||||
dataSourceId: 'ds1',
|
||||
dataSourceFieldType: ['number'],
|
||||
}),
|
||||
),
|
||||
).toBe('值不在可选项中');
|
||||
run('data-source-field-select', ['title'], {
|
||||
dataSourceId: 'ds1',
|
||||
dataSourceFieldType: ['number'],
|
||||
}),
|
||||
).toBe('请选择类型为number的字段,字段(title)的类型为string');
|
||||
expect(
|
||||
run('data-source-field-select', ['obj', 'a'], {
|
||||
dataSourceId: 'ds1',
|
||||
dataSourceFieldType: ['number'],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
run('data-source-field-select', ['obj', 'a'], {
|
||||
dataSourceId: 'ds1',
|
||||
dataSourceFieldType: ['string'],
|
||||
}),
|
||||
).toBe('请选择类型为string的字段,字段(a)的类型为number');
|
||||
|
||||
// 有 fieldConfig、未声明 value/dataSourceId 时,仍按取值中的 ds-field:: 前缀解析数据源路径
|
||||
dataSourcesState.value = [
|
||||
{
|
||||
id: 'ds_4554243a',
|
||||
type: 'base',
|
||||
fields: [{ name: 'marqueeModId', type: 'string' }],
|
||||
methods: [],
|
||||
},
|
||||
];
|
||||
expect(
|
||||
run('data-source-field-select', [`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}ds_4554243a`, 'marqueeModId'], {
|
||||
name: 'modId',
|
||||
checkStrictly: false,
|
||||
dataSourceFieldType: ['string'],
|
||||
fieldConfig: { type: 'mod-select', modType: 100119 },
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
firstLine(
|
||||
run('data-source-field-select', [`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}ds_4554243a`, 'missing'], {
|
||||
name: 'modId',
|
||||
dataSourceFieldType: ['string'],
|
||||
fieldConfig: { type: 'mod-select', modType: 100119 },
|
||||
}),
|
||||
),
|
||||
).toBe('数据源字段(missing)不存在');
|
||||
});
|
||||
|
||||
test('validateDataSourceFieldSelectValue 支持自定义 plain 值校验并保留数据源路径校验', () => {
|
||||
test('validateDataSourceFieldSelect 支持自定义 plain 值校验并保留数据源路径校验', () => {
|
||||
const config = {
|
||||
name: 'fontWeight',
|
||||
type: 'data-source-field-select',
|
||||
@ -287,9 +327,9 @@ describe('editorTypeMatchRules', () => {
|
||||
},
|
||||
};
|
||||
|
||||
expect(validateDataSourceFieldSelectValue(700, context, allowStringOrNumber)).toBeUndefined();
|
||||
expect(validateDataSourceFieldSelectValue('bold', context, allowStringOrNumber)).toBeUndefined();
|
||||
expect(firstLine(validateDataSourceFieldSelectValue({ a: 1 }, context, allowStringOrNumber))).toBe(
|
||||
expect(validateDataSourceFieldSelect(700, context, allowStringOrNumber)).toBeUndefined();
|
||||
expect(validateDataSourceFieldSelect('bold', context, allowStringOrNumber)).toBeUndefined();
|
||||
expect(firstLine(validateDataSourceFieldSelect({ a: 1 }, context, allowStringOrNumber))).toBe(
|
||||
'fontWeight 类型应为字符串或数字',
|
||||
);
|
||||
|
||||
@ -306,7 +346,7 @@ describe('editorTypeMatchRules', () => {
|
||||
];
|
||||
|
||||
expect(
|
||||
validateDataSourceFieldSelectValue(
|
||||
validateDataSourceFieldSelect(
|
||||
[`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}ds1`, 'weight'],
|
||||
context,
|
||||
allowStringOrNumber,
|
||||
@ -314,7 +354,7 @@ describe('editorTypeMatchRules', () => {
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
firstLine(
|
||||
validateDataSourceFieldSelectValue(
|
||||
validateDataSourceFieldSelect(
|
||||
[`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}ds1`, 'missing'],
|
||||
context,
|
||||
allowStringOrNumber,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user