mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-21 21:47:51 +00:00
feat(editor): 注册编辑器字段内置 typeMatch 校验规则并补充文档
- 新增 typeMatchRules.ts,为编辑器自定义字段注册内置 typeMatch 校验规则 - 抽取 event.ts 事件工具函数 - form typeMatch 适配 TDesign 校验器签名,错误消息补充实际值 - rules.md 新增「Editor 字段内置规则」章节,16 个字段文档补充校验说明
This commit is contained in:
parent
bbe73aae64
commit
1c67b5e77b
@ -27,6 +27,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:要求 `string`,有代码块 DSL 时须为已有 codeId。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -24,6 +24,10 @@ CodeSelect 组件支持:
|
||||
- 选择数据源方法
|
||||
- 配置代码块参数
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:做 `{ hookType: 'code', hookData }` 的浅层结构校验,`codeId` / 数据源方法存在性由内部单元格各自校验。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -41,6 +41,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:默认要求 `string`,`parse: true` 时跳过校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -27,6 +27,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为已知算子,能解析字段类型时按类型收窄。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -49,6 +49,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数据源路径 `string[]`,有 `fieldConfig` 且非路径值时跳过。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做 `name`/`type` 等浅层结构校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:要求 `string`,`${...}` 绑定须指向已有数据源 / 字段。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -27,6 +27,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为 `[dsId, methodName]`,方法须在该数据源可选方法集中。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做 `content`/`params` 等浅层结构校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做 `title`/`enable`/`data` 等浅层结构校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -38,6 +38,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:`value: 'id'` 时须为已有 ds id,否则为含 `isBindDataSource` + `dataSourceId` 的对象。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -27,6 +27,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做 `cond[].field`/`op` 浅层结构校验,算子合法性 / 字段路径存在性由内部单元格各自校验。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -28,6 +28,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为数组并做兼容新旧格式的浅层结构校验,事件名 / 联动动作是否属于可选项由内部单元格各自校验。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -29,6 +29,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为普通对象,`advanced` 且值为 `function` 时放行。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为 `string` 或 `number`,有节点树时须为已有页面片 id。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 值校验
|
||||
|
||||
安装 `@tmagic/editor` 后该字段会自动注册内置 `typeMatch` 校验规则:须为 `string` 或 `number`,有节点树时须为已有组件 id。服务数据未就绪时仅做基础形态校验。详见[表单校验 - Editor 字段内置规则](/form-config/rules.md#editor-字段内置规则)。
|
||||
|
||||
## Attributes
|
||||
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|
||||
@ -125,6 +125,29 @@ app.use(MagicForm, {
|
||||
});
|
||||
```
|
||||
|
||||
### Editor 字段内置规则
|
||||
|
||||
安装 `@tmagic/editor` 时会自动 `registerTypeMatchRules` 注册编辑器自定义字段规则。服务数据(数据源 / 代码块 / 节点树)未就绪时,只做基础形态校验,不做枚举或存在性失败。
|
||||
|
||||
| 字段 type | 期望值 |
|
||||
| --- | --- |
|
||||
| `key-value` / `style-setter` | 普通对象;`key-value` + `advanced` 且值为 `function` 时放行 |
|
||||
| `cond-op-select` | 已知算子;能解析字段类型时按类型收窄 |
|
||||
| `code-select-col` | `string`;有代码块 DSL 时须为已有 codeId |
|
||||
| `page-fragment-select` / `ui-select` | `string \| number`;有节点树时须为已有页面片 / 组件 id |
|
||||
| `data-source-input` | `string`;`${...}` 绑定须指向已有数据源/字段 |
|
||||
| `data-source-method-select` | `[dsId, methodName]`,方法须在该数据源可选方法集中 |
|
||||
| `data-source-field-select` | 数据源路径 `string[]`;有 `fieldConfig` 且非路径值时跳过 |
|
||||
| `data-source-select` | `value: 'id'` 为已有 ds id;否则为含 `isBindDataSource` + `dataSourceId` 的对象 |
|
||||
| `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` 等) |
|
||||
| `event-select` | 数组;兼容新旧格式的浅层结构校验(`name` / 联动组件 `method` 是否 ∈ 可选项由字段内 `eventNameConfig.rules`、`compActionConfig.rules` 单独校验,只标红对应 select) |
|
||||
| `display-conds` | 数组 + `cond[].field`/`op` 浅层结构校验(`op` 是否为已知算子、字段路径是否存在由内部 `cond-op-select`、`field` 单元格各自校验,只标红出错单元格) |
|
||||
|
||||
> 容器类字段(`event-select` / `code-select` / `display-conds`)遵循同一约定:容器级 typeMatch 只做结构校验,「枚举 / 存在性」下沉到内部单元格各自的 typeMatch/rules,避免单个子项非法导致整块表单标红。
|
||||
|
||||
业务仍可用 `registerTypeMatchRule` 覆盖上述任一 type。
|
||||
|
||||
## 示例
|
||||
|
||||
### select 选项匹配
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
"conventional-changelog-cli": "^5.0.0",
|
||||
"cosmiconfig": "^ 9.0.0 ",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"element-plus": "^2.11.8",
|
||||
"element-plus": "^2.14.3",
|
||||
"enquirer": "^2.4.1",
|
||||
"eslint": "^10.3.0",
|
||||
"execa": "^9.6.0",
|
||||
|
||||
@ -93,7 +93,8 @@ const codeConfig = computed<GroupListConfig>(() => ({
|
||||
{ value: HookCodeType.CODE, text: '代码块' },
|
||||
{ value: HookCodeType.DATA_SOURCE_METHOD, text: '数据源方法' },
|
||||
],
|
||||
defaultValue: 'code',
|
||||
rules: [{ typeMatch: true, trigger: 'blur' }],
|
||||
defaultValue: HookCodeType.CODE,
|
||||
onChange: (_mForm, v: HookCodeType, { setModel }) => {
|
||||
if (v === HookCodeType.DATA_SOURCE_METHOD) {
|
||||
setModel('codeId', []);
|
||||
@ -111,6 +112,7 @@ const codeConfig = computed<GroupListConfig>(() => ({
|
||||
labelWidth: 0,
|
||||
display: (_mForm, { model }) => model.codeType !== HookCodeType.DATA_SOURCE_METHOD,
|
||||
notEditable: () => !codeBlockService.getEditStatus(),
|
||||
rules: [{ typeMatch: true, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
type: 'data-source-method-select',
|
||||
@ -119,6 +121,7 @@ const codeConfig = computed<GroupListConfig>(() => ({
|
||||
labelWidth: 0,
|
||||
display: (_mForm, { model }) => model.codeType === HookCodeType.DATA_SOURCE_METHOD,
|
||||
notEditable: () => !dataSourceService.get('editable'),
|
||||
rules: [{ typeMatch: true }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -33,7 +33,7 @@ import { getDesignConfig, TMagicSelect } from '@tmagic/design';
|
||||
import type { CondOpSelectConfig, FieldProps } from '@tmagic/form';
|
||||
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import { arrayOptions, eqOptions, getFieldType, numberOptions } from '@editor/utils';
|
||||
import { getCondOpOptionsByFieldType, getFieldType } from '@editor/utils';
|
||||
|
||||
defineOptions({
|
||||
name: 'MFieldsCondOpSelect',
|
||||
@ -51,31 +51,9 @@ const optionComponent = getDesignConfig('components')?.option;
|
||||
|
||||
const options = computed(() => {
|
||||
const [id, ...fieldNames] = [...(props.config.parentFields || []), ...props.model.field];
|
||||
|
||||
const ds = dataSourceService.getDataSourceById(id);
|
||||
|
||||
const type = getFieldType(ds, fieldNames);
|
||||
|
||||
if (type === 'array') {
|
||||
return arrayOptions;
|
||||
}
|
||||
|
||||
if (type === 'boolean' || type === 'null') {
|
||||
return [
|
||||
{ text: '是', value: 'is' },
|
||||
{ text: '不是', value: 'not' },
|
||||
];
|
||||
}
|
||||
|
||||
if (type === 'number') {
|
||||
return [...eqOptions, ...numberOptions];
|
||||
}
|
||||
|
||||
if (type === 'string') {
|
||||
return [...arrayOptions, ...eqOptions];
|
||||
}
|
||||
|
||||
return [...arrayOptions, ...eqOptions, ...numberOptions];
|
||||
return getCondOpOptionsByFieldType(type);
|
||||
});
|
||||
|
||||
const fieldChangeHandler = (v: string) => {
|
||||
|
||||
@ -79,11 +79,11 @@ import { Coin } from '@element-plus/icons-vue';
|
||||
import type { DataSchema, DataSourceSchema } from '@tmagic/core';
|
||||
import { getDesignConfig, TMagicAutocomplete, TMagicInput, TMagicTag } from '@tmagic/design';
|
||||
import type { DataSourceInputConfig, FieldProps } from '@tmagic/form';
|
||||
import { getKeysArray, isNumber } from '@tmagic/utils';
|
||||
import { getKeysArray } from '@tmagic/utils';
|
||||
|
||||
import Icon from '@editor/components/Icon.vue';
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import { getDisplayField } from '@editor/utils/data-source';
|
||||
import { getDisplayField, resolveFieldByPath } from '@editor/utils/data-source';
|
||||
|
||||
defineOptions({
|
||||
name: 'MFieldsDataSourceInput',
|
||||
@ -251,34 +251,16 @@ const fieldQuerySearch = (
|
||||
return;
|
||||
}
|
||||
|
||||
let fields = ds.fields || [];
|
||||
|
||||
// 后面这些是字段
|
||||
let key = keys.shift();
|
||||
while (key) {
|
||||
if (isNumber(key)) {
|
||||
key = keys.shift();
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.name === key) {
|
||||
fields = field.fields || [];
|
||||
key = keys.shift();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const { fields } = resolveFieldByPath(ds.fields, keys, { skipNumberIndices: true });
|
||||
|
||||
if (curCharIsDot(dotIndex)) {
|
||||
// 当前输入的是.
|
||||
result = fields || [];
|
||||
result = fields;
|
||||
} else if (dotIndex > -1) {
|
||||
const queryName = queryString.substring(dotIndex + 1).toLowerCase();
|
||||
result =
|
||||
fields.filter(
|
||||
(field) => field.name?.toLowerCase().includes(queryName) || field.title?.toLowerCase().includes(queryName),
|
||||
) || [];
|
||||
result = fields.filter(
|
||||
(field) => field.name?.toLowerCase().includes(queryName) || field.title?.toLowerCase().includes(queryName),
|
||||
);
|
||||
}
|
||||
|
||||
cb(
|
||||
|
||||
@ -98,6 +98,10 @@ const config = computed<GroupListConfig>(() => ({
|
||||
label: '字段',
|
||||
checkStrictly: false,
|
||||
onChange: fieldOnChange,
|
||||
rules: [
|
||||
{ required: true, trigger: 'blur', message: '请选择字段' },
|
||||
{ typeMatch: true, trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
: {
|
||||
type: 'data-source-field-select',
|
||||
@ -107,6 +111,10 @@ const config = computed<GroupListConfig>(() => ({
|
||||
checkStrictly: false,
|
||||
dataSourceFieldType: ['string', 'number', 'boolean', 'any'],
|
||||
onChange: fieldOnChange,
|
||||
rules: [
|
||||
{ required: true, trigger: 'blur', message: '请选择字段' },
|
||||
{ typeMatch: true, trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'cond-op-select',
|
||||
@ -114,6 +122,10 @@ const config = computed<GroupListConfig>(() => ({
|
||||
label: '条件',
|
||||
width: 140,
|
||||
name: 'op',
|
||||
rules: [
|
||||
{ required: true, trigger: 'blur', message: '请选择条件' },
|
||||
{ typeMatch: true, trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '值',
|
||||
|
||||
@ -67,11 +67,9 @@ import { computed } from 'vue';
|
||||
import { Delete } from '@element-plus/icons-vue';
|
||||
import { has } from 'lodash-es';
|
||||
|
||||
import type { EventOption, MComponent, MContainer } from '@tmagic/core';
|
||||
import { ActionType } from '@tmagic/core';
|
||||
import { TMagicButton } from '@tmagic/design';
|
||||
import type {
|
||||
CascaderOption,
|
||||
CodeSelectColConfig,
|
||||
ContainerChangeEventData,
|
||||
DataSourceMethodSelectConfig,
|
||||
@ -84,10 +82,15 @@ import type {
|
||||
UISelectConfig,
|
||||
} from '@tmagic/form';
|
||||
import { defineFormItem, MContainer as MFormContainer, MPanel, MTable } from '@tmagic/form';
|
||||
import { DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX, traverseNode } from '@tmagic/utils';
|
||||
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import { getCascaderOptionsFromFields } from '@editor/utils';
|
||||
import {
|
||||
getCompActionAllowedValues,
|
||||
getCompActionOptions,
|
||||
getEventNameAllowedValues,
|
||||
getEventNameOptions,
|
||||
normalizeCompActionValue,
|
||||
} from '@editor/utils';
|
||||
|
||||
defineOptions({
|
||||
name: 'MFieldsEventSelect',
|
||||
@ -118,68 +121,56 @@ const eventNameConfig = computed(() => {
|
||||
labelWidth: '40px',
|
||||
checkStrictly: () => props.config.src !== 'component',
|
||||
valueSeparator: '.',
|
||||
options: (mForm: FormState, { formValue }: any) => {
|
||||
let events: EventOption[] | CascaderOption[] = [];
|
||||
|
||||
if (props.config.src === 'component') {
|
||||
events = eventsService.getEvent(formValue.type);
|
||||
|
||||
if (formValue.type === 'page-fragment-container' && formValue.pageFragmentId) {
|
||||
const pageFragment = editorService.get('root')?.items?.find((page) => page.id === formValue.pageFragmentId);
|
||||
if (pageFragment) {
|
||||
events = [
|
||||
{
|
||||
label: pageFragment.name || '页面片容器',
|
||||
value: pageFragment.id,
|
||||
children: events,
|
||||
},
|
||||
];
|
||||
pageFragment.items.forEach((node) => {
|
||||
traverseNode<MComponent | MContainer>(node, (node) => {
|
||||
const nodeEvents = (node.type && eventsService.getEvent(node.type)) || [];
|
||||
|
||||
events.push({
|
||||
label: `${node.name}_${node.id}`,
|
||||
value: `${node.id}`,
|
||||
children: nodeEvents,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return events;
|
||||
options: (_mForm: FormState, { formValue }: any) => getEventNameOptions(props.config.src, formValue),
|
||||
rules: [
|
||||
{
|
||||
validator: ({ value, callback }: any, { formValue }: any) => {
|
||||
const allowedNames = getEventNameAllowedValues(props.config as any, formValue);
|
||||
if (allowedNames && allowedNames.size > 0 && value && !allowedNames.has(value)) {
|
||||
return callback(`事件名(${value})不存在`);
|
||||
}
|
||||
}
|
||||
|
||||
return events.map((option) => ({
|
||||
text: option.label,
|
||||
value: option.value,
|
||||
}));
|
||||
}
|
||||
|
||||
if (props.config.src === 'datasource') {
|
||||
// 从数据源类型中获取到相关事件
|
||||
events = dataSourceService.getFormEvent(formValue.type);
|
||||
// 从数据源类型和实例中分别获取数据以追加数据变化的事件
|
||||
const dataSource = dataSourceService.getDataSourceById(formValue.id);
|
||||
const fields = dataSource?.fields || [];
|
||||
if (fields.length > 0) {
|
||||
return [
|
||||
...events,
|
||||
{
|
||||
label: '数据变化',
|
||||
value: DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX,
|
||||
children: getCascaderOptionsFromFields(fields),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
},
|
||||
callback();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
return { ...defaultEventNameConfig, ...props.config.eventNameConfig };
|
||||
});
|
||||
|
||||
const actionTypeOptions = computed(() => {
|
||||
const o: {
|
||||
text: string;
|
||||
label: string;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
}[] = [
|
||||
{
|
||||
text: '组件',
|
||||
label: '组件',
|
||||
value: ActionType.COMP,
|
||||
},
|
||||
];
|
||||
|
||||
if (!propsService.getDisabledCodeBlock()) {
|
||||
o.push({
|
||||
text: '代码',
|
||||
label: '代码',
|
||||
disabled: !Object.keys(codeBlockService.getCodeDsl() || {}).length,
|
||||
value: ActionType.CODE,
|
||||
});
|
||||
}
|
||||
|
||||
if (!propsService.getDisabledDataSource()) {
|
||||
o.push({
|
||||
text: '数据源',
|
||||
label: '数据源',
|
||||
value: ActionType.DATA_SOURCE,
|
||||
});
|
||||
}
|
||||
|
||||
return o;
|
||||
});
|
||||
|
||||
// 联动类型
|
||||
const actionTypeConfig = computed(() => {
|
||||
const defaultActionTypeConfig = {
|
||||
@ -187,39 +178,17 @@ const actionTypeConfig = computed(() => {
|
||||
text: '联动类型',
|
||||
type: 'select',
|
||||
defaultValue: ActionType.COMP,
|
||||
options: () => {
|
||||
const o: {
|
||||
text: string;
|
||||
label: string;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
}[] = [
|
||||
{
|
||||
text: '组件',
|
||||
label: '组件',
|
||||
value: ActionType.COMP,
|
||||
},
|
||||
];
|
||||
|
||||
if (!propsService.getDisabledCodeBlock()) {
|
||||
o.push({
|
||||
text: '代码',
|
||||
label: '代码',
|
||||
disabled: !Object.keys(codeBlockService.getCodeDsl() || {}).length,
|
||||
value: ActionType.CODE,
|
||||
});
|
||||
}
|
||||
|
||||
if (!propsService.getDisabledDataSource()) {
|
||||
o.push({
|
||||
text: '数据源',
|
||||
label: '数据源',
|
||||
value: ActionType.DATA_SOURCE,
|
||||
});
|
||||
}
|
||||
|
||||
return o;
|
||||
},
|
||||
options: actionTypeOptions.value,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '联动类型不能为空',
|
||||
},
|
||||
{
|
||||
typeMatch: true,
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
};
|
||||
return { ...defaultActionTypeConfig, ...props.config.actionTypeConfig };
|
||||
});
|
||||
@ -234,6 +203,12 @@ const targetCompConfig = computed(() => {
|
||||
onChange: (_MForm, _v, { setModel }) => {
|
||||
setModel('method', '');
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
typeMatch: true,
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
};
|
||||
return { ...defaultTargetCompConfig, ...props.config.targetCompConfig };
|
||||
});
|
||||
@ -254,41 +229,20 @@ const compActionConfig = computed(() => {
|
||||
},
|
||||
checkStrictly: () => props.config.src !== 'component',
|
||||
display: (mForm: FormState | undefined, { model }: any) => model.actionType === ActionType.COMP,
|
||||
options: (mForm: FormState, { model }: any) => {
|
||||
const node = editorService.getNodeById(model.to);
|
||||
if (!node?.type) return [];
|
||||
|
||||
let methods: EventOption[] | CascaderOption[] = [];
|
||||
|
||||
methods = eventsService.getMethod(node.type, model.to);
|
||||
|
||||
if (node.type === 'page-fragment-container' && node.pageFragmentId) {
|
||||
const pageFragment = editorService.get('root')?.items?.find((page) => page.id === node.pageFragmentId);
|
||||
if (pageFragment) {
|
||||
methods = [];
|
||||
pageFragment.items.forEach((node: MComponent | MContainer) => {
|
||||
traverseNode<MComponent | MContainer>(node, (node) => {
|
||||
const nodeMethods = (node.type && eventsService.getMethod(node.type, node.id)) || [];
|
||||
|
||||
if (nodeMethods.length) {
|
||||
methods.push({
|
||||
label: `${node.name}_${node.id}`,
|
||||
value: `${node.id}`,
|
||||
children: nodeMethods,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return methods;
|
||||
}
|
||||
}
|
||||
|
||||
return methods.map((method) => ({
|
||||
text: method.label,
|
||||
value: method.value,
|
||||
}));
|
||||
},
|
||||
options: (_mForm: FormState, { model }: any) => getCompActionOptions(model.to),
|
||||
rules: [
|
||||
{
|
||||
trigger: 'blur',
|
||||
validator: ({ value, callback }: any, { model }: any) => {
|
||||
const allowedMethods = getCompActionAllowedValues(props.config as any, model);
|
||||
const normalized = normalizeCompActionValue(value);
|
||||
if (allowedMethods && allowedMethods.size > 0 && normalized && !allowedMethods.has(normalized)) {
|
||||
return callback(`动作名(${normalized})不存在`);
|
||||
}
|
||||
callback();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
return { ...defaultCompActionConfig, ...props.config.compActionConfig };
|
||||
});
|
||||
|
||||
@ -21,7 +21,7 @@ import type { App } from 'vue';
|
||||
import type { DesignPluginOptions } from '@tmagic/design';
|
||||
import designPlugin from '@tmagic/design';
|
||||
import type { FormInstallOptions } from '@tmagic/form';
|
||||
import formPlugin from '@tmagic/form';
|
||||
import formPlugin, { registerTypeMatchRules } from '@tmagic/form';
|
||||
import tablePlugin from '@tmagic/table';
|
||||
|
||||
import Code from './fields/Code.vue';
|
||||
@ -44,6 +44,7 @@ import StyleSetter from './fields/StyleSetter/Index.vue';
|
||||
import uiSelect from './fields/UISelect.vue';
|
||||
import CodeEditor from './layouts/CodeEditor.vue';
|
||||
import { setEditorConfig } from './utils/config';
|
||||
import { editorTypeMatchRules } from './utils/typeMatchRules';
|
||||
import Editor from './Editor.vue';
|
||||
import type { EditorInstallOptions } from './type';
|
||||
|
||||
@ -64,6 +65,7 @@ export default {
|
||||
app.use(designPlugin, opt || {});
|
||||
app.use(formPlugin, opt || {});
|
||||
app.use(tablePlugin);
|
||||
registerTypeMatchRules(editorTypeMatchRules);
|
||||
|
||||
app.config.globalProperties.$TMAGIC_EDITOR = option;
|
||||
setEditorConfig(option);
|
||||
|
||||
@ -35,3 +35,11 @@
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
.el-form-item {
|
||||
&.is-error {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -235,19 +235,37 @@ export const getCascaderOptionsFromFields = (
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getFieldType = (ds: DataSourceSchema | undefined, fieldNames: string[]) => {
|
||||
let fields = ds?.fields;
|
||||
let type = '';
|
||||
/**
|
||||
* 按字段名路径下钻 DataSchema。
|
||||
* @param skipNumberIndices 为 true 时跳过数字段(模板路径中的数组下标,如 arr[0].x)
|
||||
*/
|
||||
export const resolveFieldByPath = (
|
||||
fields: DataSchema[] | undefined,
|
||||
fieldNames: string[],
|
||||
options: { skipNumberIndices?: boolean } = {},
|
||||
): { ok: boolean; field?: DataSchema; fields: DataSchema[] } => {
|
||||
let currentFields = fields || [];
|
||||
let field: DataSchema | undefined;
|
||||
|
||||
for (const fieldName of fieldNames) {
|
||||
if (!fields?.length) return '';
|
||||
|
||||
const field = fields.find((f) => f.name === fieldName);
|
||||
if (!field) return '';
|
||||
|
||||
type = field.type || '';
|
||||
fields = field.fields;
|
||||
for (const name of fieldNames) {
|
||||
if (options.skipNumberIndices && isNumber(name)) {
|
||||
continue;
|
||||
}
|
||||
if (!currentFields.length) {
|
||||
return { ok: false, fields: currentFields };
|
||||
}
|
||||
field = currentFields.find((item) => item.name === name);
|
||||
if (!field) {
|
||||
return { ok: false, fields: currentFields };
|
||||
}
|
||||
currentFields = field.fields || [];
|
||||
}
|
||||
|
||||
return type;
|
||||
return { field, ok: true, fields: currentFields };
|
||||
};
|
||||
|
||||
export const getFieldType = (ds: DataSourceSchema | undefined, fieldNames: string[]) => {
|
||||
const { ok, field } = resolveFieldByPath(ds?.fields, fieldNames);
|
||||
if (!ok) return '';
|
||||
return field?.type || '';
|
||||
};
|
||||
|
||||
224
packages/editor/src/utils/event.ts
Normal file
224
packages/editor/src/utils/event.ts
Normal file
@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { EventOption, Id, MComponent, MContainer } from '@tmagic/core';
|
||||
import type { CascaderOption } from '@tmagic/form';
|
||||
import { DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX, traverseNode } from '@tmagic/utils';
|
||||
|
||||
import dataSourceService from '@editor/services/dataSource';
|
||||
import editorService from '@editor/services/editor';
|
||||
import eventsService from '@editor/services/events';
|
||||
import { getCascaderOptionsFromFields } from '@editor/utils/data-source';
|
||||
|
||||
export const EVENT_NAME_VALUE_SEPARATOR = '.';
|
||||
|
||||
export type EventNameSelectOption = { text: string; value: string };
|
||||
export type EventNameOption = EventOption | CascaderOption | EventNameSelectOption;
|
||||
|
||||
/** 与 EventSelect 中 checkStrictly 一致:component 为 false,其余为 true */
|
||||
export const isEventNameCheckStrictly = (src?: string) => src !== 'component';
|
||||
|
||||
/** 将动作 method 值规范为与 collectEventNameOptionValues 一致的字符串(cascader 无 valueSeparator 时存数组) */
|
||||
export const normalizeCompActionValue = (value: unknown): string => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => `${item}`).join(EVENT_NAME_VALUE_SEPARATOR);
|
||||
}
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '';
|
||||
}
|
||||
return `${value}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 组装事件名称下拉/级联 options,与 EventSelect 默认 eventNameConfig.options 逻辑一致。
|
||||
*/
|
||||
export const getEventNameOptions = (
|
||||
src: string | undefined,
|
||||
formValue: Record<string, any> = {},
|
||||
): EventNameOption[] => {
|
||||
if (!formValue.type) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (src === 'component') {
|
||||
let events: EventOption[] | CascaderOption[] = eventsService.getEvent(formValue.type) || [];
|
||||
|
||||
if (formValue.type === 'page-fragment-container' && formValue.pageFragmentId) {
|
||||
const pageFragment = editorService.get('root')?.items?.find((page) => page.id === formValue.pageFragmentId);
|
||||
if (!pageFragment) {
|
||||
return [];
|
||||
}
|
||||
|
||||
events = [
|
||||
{
|
||||
label: pageFragment.name || '页面片容器',
|
||||
value: pageFragment.id,
|
||||
children: events,
|
||||
},
|
||||
];
|
||||
|
||||
(pageFragment.items || []).forEach((node) => {
|
||||
traverseNode<MComponent | MContainer>(node, (current) => {
|
||||
const nodeEvents = (current.type && eventsService.getEvent(current.type)) || [];
|
||||
(events as CascaderOption[]).push({
|
||||
label: `${current.name}_${current.id}`,
|
||||
value: `${current.id}`,
|
||||
children: nodeEvents,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
return (events as EventOption[]).map((option) => ({
|
||||
text: option.label,
|
||||
value: option.value,
|
||||
}));
|
||||
}
|
||||
|
||||
if (src === 'datasource') {
|
||||
const events: EventNameOption[] = dataSourceService.getFormEvent(formValue.type) || [];
|
||||
const dataSource = dataSourceService.getDataSourceById(formValue.id);
|
||||
const fields = dataSource?.fields || [];
|
||||
|
||||
if (fields.length > 0) {
|
||||
return [
|
||||
...events,
|
||||
{
|
||||
label: '数据变化',
|
||||
value: DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX,
|
||||
children: getCascaderOptionsFromFields(fields),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/** 将 select / cascader options 展平为最终写入 name 的字符串集合(cascader 用 `.` 拼接) */
|
||||
export const collectEventNameOptionValues = (
|
||||
options: EventNameOption[],
|
||||
checkStrictly: boolean,
|
||||
prefix: any[] = [],
|
||||
result: Set<string> = new Set(),
|
||||
): Set<string> => {
|
||||
options.forEach((option) => {
|
||||
if (typeof option?.value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = [...prefix, option.value];
|
||||
const joined = path.map((item) => `${item}`).join(EVENT_NAME_VALUE_SEPARATOR);
|
||||
const children = 'children' in option ? option.children : undefined;
|
||||
|
||||
if (Array.isArray(children) && children.length) {
|
||||
if (checkStrictly) {
|
||||
result.add(joined);
|
||||
}
|
||||
collectEventNameOptionValues(children as EventNameOption[], checkStrictly, path, result);
|
||||
return;
|
||||
}
|
||||
|
||||
result.add(joined);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析 event-select 允许的 name 集合。
|
||||
* 自定义 eventNameConfig.options 时返回 null(跳过枚举校验)。
|
||||
*/
|
||||
export const getEventNameAllowedValues = (
|
||||
config: { src?: string; eventNameConfig?: { options?: unknown } } = {},
|
||||
formValue: Record<string, any> = {},
|
||||
): Set<string> | null => {
|
||||
if (typeof config.eventNameConfig?.options !== 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (config.src !== 'component' && config.src !== 'datasource') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return collectEventNameOptionValues(getEventNameOptions(config.src, formValue), isEventNameCheckStrictly(config.src));
|
||||
};
|
||||
|
||||
/**
|
||||
* 组装联动组件动作下拉/级联 options,与 EventSelect 默认 compActionConfig.options 逻辑一致。
|
||||
*/
|
||||
export const getCompActionOptions = (toId?: Id): EventNameOption[] => {
|
||||
if (typeof toId === 'undefined' || toId === null || toId === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const node = editorService.getNodeById(toId);
|
||||
if (!node?.type) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let methods: EventOption[] | CascaderOption[] = eventsService.getMethod(node.type, toId) || [];
|
||||
|
||||
if (node.type === 'page-fragment-container' && node.pageFragmentId) {
|
||||
const pageFragment = editorService.get('root')?.items?.find((page) => page.id === node.pageFragmentId);
|
||||
if (!pageFragment) {
|
||||
return [];
|
||||
}
|
||||
|
||||
methods = [];
|
||||
(pageFragment.items || []).forEach((item: MComponent | MContainer) => {
|
||||
traverseNode<MComponent | MContainer>(item, (current) => {
|
||||
const nodeMethods = (current.type && eventsService.getMethod(current.type, current.id)) || [];
|
||||
|
||||
if (nodeMethods.length) {
|
||||
(methods as CascaderOption[]).push({
|
||||
label: `${current.name}_${current.id}`,
|
||||
value: `${current.id}`,
|
||||
children: nodeMethods,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return methods;
|
||||
}
|
||||
|
||||
return (methods as EventOption[]).map((method) => ({
|
||||
text: method.label,
|
||||
value: method.value,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析 event-select 联动组件动作允许的 method 集合。
|
||||
* 自定义 compActionConfig.options 时返回 null(跳过枚举校验)。
|
||||
*/
|
||||
export const getCompActionAllowedValues = (
|
||||
config: { src?: string; compActionConfig?: { options?: unknown } } = {},
|
||||
model: { to?: Id } = {},
|
||||
): Set<string> | null => {
|
||||
if (typeof config.compActionConfig?.options !== 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return collectEventNameOptionValues(getCompActionOptions(model.to), isEventNameCheckStrictly(config.src));
|
||||
};
|
||||
@ -30,4 +30,6 @@ export * from './undo-redo';
|
||||
export * from './indexed-db';
|
||||
export * from './history';
|
||||
export * from './const';
|
||||
export * from './typeMatchRules';
|
||||
export * from './event';
|
||||
export { default as loadMonaco } from './monaco-editor';
|
||||
|
||||
@ -45,6 +45,32 @@ export const numberOptions = [
|
||||
{ text: '不在范围内', value: 'not_between' },
|
||||
];
|
||||
|
||||
export const booleanOptions = [
|
||||
{ text: '是', value: 'is' },
|
||||
{ text: '不是', value: 'not' },
|
||||
];
|
||||
|
||||
/** 按字段类型返回条件运算符选项(UI 与 typeMatch 校验共用) */
|
||||
export const getCondOpOptionsByFieldType = (type: string) => {
|
||||
if (type === 'array') {
|
||||
return arrayOptions;
|
||||
}
|
||||
|
||||
if (type === 'boolean' || type === 'null') {
|
||||
return booleanOptions;
|
||||
}
|
||||
|
||||
if (type === 'number') {
|
||||
return [...eqOptions, ...numberOptions];
|
||||
}
|
||||
|
||||
if (type === 'string') {
|
||||
return [...arrayOptions, ...eqOptions];
|
||||
}
|
||||
|
||||
return [...arrayOptions, ...eqOptions, ...numberOptions];
|
||||
};
|
||||
|
||||
export const styleTabConfig: TabPaneConfig = {
|
||||
title: '样式',
|
||||
lazy: true,
|
||||
@ -127,6 +153,7 @@ export const eventTabConfig: TabPaneConfig = {
|
||||
src: 'component',
|
||||
labelWidth: '100px',
|
||||
type: 'event-select',
|
||||
rules: [{ typeMatch: true }],
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -155,6 +182,7 @@ export const advancedTabConfig: TabPaneConfig = {
|
||||
labelPosition: 'top',
|
||||
type: 'code-select',
|
||||
extra: '组件初始化时执行',
|
||||
rules: [{ typeMatch: true }],
|
||||
},
|
||||
{
|
||||
name: 'mounted',
|
||||
@ -162,6 +190,7 @@ export const advancedTabConfig: TabPaneConfig = {
|
||||
labelPosition: 'top',
|
||||
type: 'code-select',
|
||||
extra: '组件挂载到dom时执行',
|
||||
rules: [{ typeMatch: true }],
|
||||
},
|
||||
{
|
||||
name: 'display',
|
||||
@ -169,6 +198,7 @@ export const advancedTabConfig: TabPaneConfig = {
|
||||
extra: '控制组件是否渲染,关系的代码块返回值为false时不渲染',
|
||||
labelPosition: 'top',
|
||||
type: 'code-select',
|
||||
rules: [{ typeMatch: true }],
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -194,6 +224,7 @@ export const displayTabConfig: TabPaneConfig<DisplayCondsConfig> = {
|
||||
name: NODE_CONDS_KEY,
|
||||
titlePrefix: '条件组',
|
||||
defaultValue: [],
|
||||
rules: [{ typeMatch: true }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
545
packages/editor/src/utils/typeMatchRules.ts
Normal file
545
packages/editor/src/utils/typeMatchRules.ts
Normal file
@ -0,0 +1,545 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DataSourceFieldType, DataSourceSchema, Id } from '@tmagic/core';
|
||||
import { HookCodeType, HookType, NodeType } from '@tmagic/core';
|
||||
import type { TypeMatchValidateContext, TypeMatchValidator } from '@tmagic/form';
|
||||
import {
|
||||
DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX,
|
||||
DATA_SOURCE_SET_DATA_METHOD_NAME,
|
||||
dataSourceTemplateRegExp,
|
||||
getKeysArray,
|
||||
removeDataSourceFieldPrefix,
|
||||
} from '@tmagic/utils';
|
||||
|
||||
import codeBlockService from '@editor/services/codeBlock';
|
||||
import dataSourceService from '@editor/services/dataSource';
|
||||
import editorService from '@editor/services/editor';
|
||||
import { getFieldType, resolveFieldByPath } from '@editor/utils/data-source';
|
||||
import {
|
||||
arrayOptions,
|
||||
booleanOptions,
|
||||
eqOptions,
|
||||
getCondOpOptionsByFieldType,
|
||||
numberOptions,
|
||||
} from '@editor/utils/props';
|
||||
|
||||
const defaultMessage = (message: string | undefined, fallback: string) => message || fallback;
|
||||
|
||||
const isPlainObject = (value: any): value is Record<string, any> =>
|
||||
Object.prototype.toString.call(value) === '[object Object]';
|
||||
|
||||
const isId = (value: any): value is Id => typeof value === 'string' || typeof value === 'number';
|
||||
|
||||
const DATA_SOURCE_FIELD_TYPES = new Set<DataSourceFieldType>([
|
||||
'null',
|
||||
'boolean',
|
||||
'object',
|
||||
'array',
|
||||
'number',
|
||||
'string',
|
||||
'any',
|
||||
]);
|
||||
|
||||
export const ALL_COND_OPS = new Set<string>([
|
||||
...arrayOptions.map((item) => item.value),
|
||||
...eqOptions.map((item) => item.value),
|
||||
...numberOptions.map((item) => item.value),
|
||||
...booleanOptions.map((item) => item.value),
|
||||
]);
|
||||
|
||||
const getCondOpsByFieldType = (type: string): Set<string> =>
|
||||
new Set(getCondOpOptionsByFieldType(type).map((item) => item.value));
|
||||
|
||||
const getDataSources = (): DataSourceSchema[] => dataSourceService.get('dataSources') || [];
|
||||
|
||||
const findDataSource = (id: string): DataSourceSchema | undefined =>
|
||||
getDataSources().find((ds) => `${ds.id}` === `${id}`);
|
||||
|
||||
const getMethodNamesForDataSource = (ds: DataSourceSchema): Set<string> => {
|
||||
const names = new Set<string>([DATA_SOURCE_SET_DATA_METHOD_NAME]);
|
||||
(dataSourceService.getFormMethod(ds.type) || []).forEach((item) => {
|
||||
if (item?.value) names.add(item.value);
|
||||
});
|
||||
(ds.methods || []).forEach((method) => {
|
||||
if (method?.name) names.add(method.name);
|
||||
});
|
||||
return names;
|
||||
};
|
||||
|
||||
const validateDataSourceFieldPath = (
|
||||
path: string[],
|
||||
options: {
|
||||
dataSourceId?: string;
|
||||
valueMode?: 'key' | 'value';
|
||||
dataSourceFieldType?: DataSourceFieldType[];
|
||||
message?: string;
|
||||
} = {},
|
||||
): string | undefined => {
|
||||
const dataSources = getDataSources();
|
||||
if (!dataSources.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let dsId = options.dataSourceId;
|
||||
let fieldNames = path;
|
||||
|
||||
if (!dsId) {
|
||||
if (!path.length) {
|
||||
return defaultMessage(options.message, '值不在可选项中');
|
||||
}
|
||||
const rawDsId = path[0];
|
||||
dsId =
|
||||
options.valueMode === 'key' || !`${rawDsId}`.startsWith(DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX)
|
||||
? `${rawDsId}`
|
||||
: removeDataSourceFieldPrefix(`${rawDsId}`);
|
||||
fieldNames = path.slice(1);
|
||||
}
|
||||
|
||||
const ds = findDataSource(`${dsId}`);
|
||||
if (!ds) {
|
||||
return defaultMessage(options.message, '值不在可选项中');
|
||||
}
|
||||
|
||||
if (!fieldNames.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { field, ok } = resolveFieldByPath(ds.fields, fieldNames);
|
||||
if (!ok) {
|
||||
return defaultMessage(options.message, '值不在可选项中');
|
||||
}
|
||||
|
||||
const allowedTypes = options.dataSourceFieldType || ['any'];
|
||||
if (!allowedTypes.length || allowedTypes.includes('any')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const leafType = field?.type || 'any';
|
||||
if (leafType === 'any' || allowedTypes.includes(leafType)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return defaultMessage(options.message, '值不在可选项中');
|
||||
};
|
||||
|
||||
const validateDataSourceMethodTuple = (value: any, message?: string): string | undefined => {
|
||||
if (!Array.isArray(value) || value.length !== 2 || value.some((item) => typeof item !== 'string')) {
|
||||
return defaultMessage(message, `${value}类型应为长度为 2 的字符串数组`);
|
||||
}
|
||||
|
||||
const dataSources = getDataSources();
|
||||
if (!dataSources.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [dsId, methodName] = value;
|
||||
const ds = findDataSource(dsId);
|
||||
if (!ds) {
|
||||
return defaultMessage(message, `数据源(${dsId})不存在`);
|
||||
}
|
||||
|
||||
if (!getMethodNamesForDataSource(ds).has(methodName)) {
|
||||
return defaultMessage(message, `数据源方法(${methodName})不存在`);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateDataSourceTemplateBindings = (value: string, message?: string): string | undefined => {
|
||||
const dataSources = getDataSources();
|
||||
if (!dataSources.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const matches = value.matchAll(dataSourceTemplateRegExp);
|
||||
for (const match of matches) {
|
||||
const keys = getKeysArray(match[1]);
|
||||
if (!keys.length) {
|
||||
return defaultMessage(message, `数据源绑定(${value})不合法`);
|
||||
}
|
||||
|
||||
const [dsId, ...fieldNames] = keys;
|
||||
const ds = findDataSource(`${dsId}`);
|
||||
if (!ds) {
|
||||
return defaultMessage(message, `数据源绑定(${value})不合法`);
|
||||
}
|
||||
|
||||
const { ok } = resolveFieldByPath(ds.fields, fieldNames, { skipNumberIndices: true });
|
||||
if (!ok) {
|
||||
return defaultMessage(message, `数据源绑定(${value})不合法`);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateDataSourceFieldsItem = (item: any, message?: string): string | undefined => {
|
||||
if (!isPlainObject(item) || typeof item.name !== 'string') {
|
||||
return defaultMessage(message, '字段项结构不合法');
|
||||
}
|
||||
|
||||
if (typeof item.type !== 'undefined' && !DATA_SOURCE_FIELD_TYPES.has(item.type)) {
|
||||
return defaultMessage(message, '字段类型不合法');
|
||||
}
|
||||
|
||||
if (typeof item.fields !== 'undefined') {
|
||||
if (!Array.isArray(item.fields)) {
|
||||
return defaultMessage(message, '字段项结构不合法');
|
||||
}
|
||||
for (const child of item.fields) {
|
||||
const error = validateDataSourceFieldsItem(child, message);
|
||||
if (error) return error;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateKeyValue: TypeMatchValidator = (value, { message, props }) => {
|
||||
if (props.config?.advanced && typeof value === 'function') {
|
||||
return undefined;
|
||||
}
|
||||
if (!isPlainObject(value)) {
|
||||
return defaultMessage(message, '值类型应为对象');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateStyleSetter: TypeMatchValidator = (value, { message }) => {
|
||||
if (!isPlainObject(value)) {
|
||||
return defaultMessage(message, '值类型应为对象');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateCondOpSelect: TypeMatchValidator = (value, { message, props }) => {
|
||||
if (typeof value !== 'string') {
|
||||
return defaultMessage(message, '值类型应为字符串');
|
||||
}
|
||||
|
||||
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 fieldType = getFieldType(ds, fieldNames);
|
||||
const allowed = getCondOpsByFieldType(fieldType);
|
||||
|
||||
if (!allowed.has(value)) {
|
||||
return defaultMessage(message, `${value} 不在可选项中`);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateCodeSelectCol: TypeMatchValidator = (value, { message }) => {
|
||||
if (typeof value !== 'string') {
|
||||
return defaultMessage(message, `${value}类型应为字符串`);
|
||||
}
|
||||
|
||||
const codeDsl = codeBlockService.getCodeDsl();
|
||||
if (!codeDsl || !Object.keys(codeDsl).length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!(value in codeDsl)) {
|
||||
return defaultMessage(message, `代码块(${value})不存在`);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validatePageFragmentSelect: TypeMatchValidator = (value, { message }) => {
|
||||
if (!isId(value)) {
|
||||
return defaultMessage(message, `${value}类型应为字符串或数字`);
|
||||
}
|
||||
|
||||
const items = editorService.get('root')?.items;
|
||||
if (!items?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const exists = items.some((item) => item.type === NodeType.PAGE_FRAGMENT && `${item.id}` === `${value}`);
|
||||
if (!exists) {
|
||||
return defaultMessage(message, `页面片段(${value})不存在`);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateUiSelect: TypeMatchValidator = (value, { message }) => {
|
||||
if (!isId(value)) {
|
||||
return defaultMessage(message, `${value}类型应为字符串或数字`);
|
||||
}
|
||||
|
||||
const root = editorService.get('root');
|
||||
if (!root) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!editorService.getNodeById(value)) {
|
||||
return defaultMessage(message, `组件(${value})不存在`);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateDataSourceInput: TypeMatchValidator = (value, { message }) => {
|
||||
if (typeof value !== 'string') {
|
||||
return defaultMessage(message, `${value}类型应为字符串`);
|
||||
}
|
||||
return validateDataSourceTemplateBindings(value, message);
|
||||
};
|
||||
|
||||
const validateDataSourceMethodSelect: TypeMatchValidator = (value, { message }) =>
|
||||
validateDataSourceMethodTuple(value, message);
|
||||
|
||||
const isDataSourceFieldPathValue = (value: any, config: any): value is string[] => {
|
||||
if (!Array.isArray(value) || !value.length || value.some((item) => typeof item !== 'string')) {
|
||||
return false;
|
||||
}
|
||||
if (config.dataSourceId) {
|
||||
return true;
|
||||
}
|
||||
if (config.value === 'key') {
|
||||
return true;
|
||||
}
|
||||
return `${value[0]}`.startsWith(DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX);
|
||||
};
|
||||
|
||||
const validateDataSourceFieldSelect: TypeMatchValidator = (value, { message, props }) => {
|
||||
const config = props.config || {};
|
||||
|
||||
if (config.fieldConfig && !isDataSourceFieldPathValue(value, config)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
||||
return defaultMessage(message, `${value}类型应为字符串数组`);
|
||||
}
|
||||
|
||||
return validateDataSourceFieldPath(value, {
|
||||
dataSourceId: config.dataSourceId,
|
||||
valueMode: config.value,
|
||||
dataSourceFieldType: config.dataSourceFieldType,
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
const validateDataSourceSelect: TypeMatchValidator = (value, { message, props }) => {
|
||||
const config = props.config || {};
|
||||
const dataSources = getDataSources();
|
||||
const filtered = config.dataSourceType ? dataSources.filter((ds) => ds.type === config.dataSourceType) : dataSources;
|
||||
|
||||
if (config.value === 'id') {
|
||||
if (typeof value !== 'string') {
|
||||
return defaultMessage(message, `${value}类型应为字符串`);
|
||||
}
|
||||
if (!filtered.length) {
|
||||
return undefined;
|
||||
}
|
||||
if (!filtered.some((ds) => `${ds.id}` === `${value}`)) {
|
||||
return defaultMessage(message, `${value}不在可选项中`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!isPlainObject(value) || value.isBindDataSource !== true || typeof value.dataSourceId === 'undefined') {
|
||||
return defaultMessage(message, `${value}类型不合法`);
|
||||
}
|
||||
|
||||
if (!filtered.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ds = filtered.find((item) => `${item.id}` === `${value.dataSourceId}`);
|
||||
if (!ds) {
|
||||
return defaultMessage(message, `${value}不在可选项中`);
|
||||
}
|
||||
|
||||
if (typeof value.dataSourceType !== 'undefined' && value.dataSourceType !== ds.type) {
|
||||
return defaultMessage(message, `${value}不在可选项中`);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateCodeSelect: TypeMatchValidator = (value, { message }) => {
|
||||
if (!isPlainObject(value) || value.hookType !== HookType.CODE || !Array.isArray(value.hookData)) {
|
||||
return defaultMessage(message, `${value}类型不合法`);
|
||||
}
|
||||
|
||||
// 「codeId 是否存在 / 数据源方法是否存在」由 CodeSelect 内部 code-select-col、
|
||||
// data-source-method-select 单元格各自的 typeMatch 校验,只标红出错单元格。此处仅做结构校验。
|
||||
for (const item of value.hookData) {
|
||||
if (!isPlainObject(item)) {
|
||||
return defaultMessage(message, '钩子项结构不合法');
|
||||
}
|
||||
|
||||
if (item.codeType !== HookCodeType.CODE && item.codeType !== HookCodeType.DATA_SOURCE_METHOD) {
|
||||
return defaultMessage(message, '钩子项结构不合法');
|
||||
}
|
||||
|
||||
if (typeof item.params !== 'undefined' && !isPlainObject(item.params)) {
|
||||
return defaultMessage(message, '钩子项结构不合法');
|
||||
}
|
||||
|
||||
if (item.codeType === HookCodeType.CODE) {
|
||||
if (typeof item.codeId !== 'string') {
|
||||
return defaultMessage(message, '钩子项结构不合法');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// DATA_SOURCE_METHOD:仅校验元组形态,存在性交给单元格
|
||||
if (
|
||||
!Array.isArray(item.codeId) ||
|
||||
item.codeId.length !== 2 ||
|
||||
item.codeId.some((part: any) => typeof part !== 'string')
|
||||
) {
|
||||
return defaultMessage(message, '钩子项结构不合法');
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateDataSourceFields: TypeMatchValidator = (value, { message }) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return defaultMessage(message, `${value}类型应为数组`);
|
||||
}
|
||||
for (const item of value) {
|
||||
const error = validateDataSourceFieldsItem(item, message);
|
||||
if (error) return error;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateDataSourceMocks: TypeMatchValidator = (value, { message }) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return defaultMessage(message, `${value}类型应为数组`);
|
||||
}
|
||||
|
||||
for (const item of value) {
|
||||
if (
|
||||
!isPlainObject(item) ||
|
||||
typeof item.title !== 'string' ||
|
||||
typeof item.enable !== 'boolean' ||
|
||||
typeof item.useInEditor !== 'boolean' ||
|
||||
!isPlainObject(item.data)
|
||||
) {
|
||||
return defaultMessage(message, 'mock 项结构不合法');
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateDataSourceMethods: TypeMatchValidator = (value, { message }) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return defaultMessage(message, `${value}类型应为数组`);
|
||||
}
|
||||
|
||||
for (const item of value) {
|
||||
if (!isPlainObject(item) || typeof item.name !== 'string') {
|
||||
return defaultMessage(message, '方法项结构不合法');
|
||||
}
|
||||
if (typeof item.content !== 'undefined' && typeof item.content !== 'string' && typeof item.content !== 'function') {
|
||||
return defaultMessage(message, '方法项结构不合法');
|
||||
}
|
||||
if (typeof item.params !== 'undefined' && !Array.isArray(item.params)) {
|
||||
return defaultMessage(message, '方法项结构不合法');
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateEventSelect: TypeMatchValidator = (value, { message }) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return defaultMessage(message, `${value}类型应为数组`);
|
||||
}
|
||||
|
||||
// 「事件名 / 动作名是否在可选项中」由 EventSelect 的 eventNameConfig.rules、compActionConfig.rules 单独校验,
|
||||
// 以便只标红对应 select,而不是整张联动卡片。此处仅做结构校验。
|
||||
for (const item of value) {
|
||||
if (!isPlainObject(item) || typeof item.name !== 'string') {
|
||||
return defaultMessage(message, '事件项结构不合法');
|
||||
}
|
||||
|
||||
if (Array.isArray(item.actions)) {
|
||||
for (const action of item.actions) {
|
||||
if (!isPlainObject(action) || typeof action.actionType === 'undefined') {
|
||||
return defaultMessage(message, '事件项结构不合法');
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof item.to === 'undefined' || typeof item.method !== 'string') {
|
||||
return defaultMessage(message, '事件项结构不合法');
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const validateDisplayConds: TypeMatchValidator = (value, { message }) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return defaultMessage(message, `${value}类型应为数组`);
|
||||
}
|
||||
|
||||
// 「op 是否为合法算子 / 字段路径是否存在」由 DisplayConds 内部 cond-op-select、
|
||||
// field 单元格(data-source-field-select / cascader)各自的 typeMatch 校验,只标红出错单元格。
|
||||
// 此处仅做结构校验。
|
||||
for (const item of value) {
|
||||
if (!isPlainObject(item) || !Array.isArray(item.cond)) {
|
||||
return defaultMessage(message, '显示条件结构不合法');
|
||||
}
|
||||
|
||||
for (const cond of item.cond) {
|
||||
if (
|
||||
!isPlainObject(cond) ||
|
||||
!Array.isArray(cond.field) ||
|
||||
cond.field.some((part: any) => typeof part !== 'string') ||
|
||||
typeof cond.op !== 'string'
|
||||
) {
|
||||
return defaultMessage(message, '显示条件结构不合法');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const editorTypeMatchRules: Record<string, TypeMatchValidator> = {
|
||||
'key-value': validateKeyValue,
|
||||
'style-setter': validateStyleSetter,
|
||||
'cond-op-select': validateCondOpSelect,
|
||||
'code-select-col': validateCodeSelectCol,
|
||||
'page-fragment-select': validatePageFragmentSelect,
|
||||
'ui-select': validateUiSelect,
|
||||
'data-source-input': validateDataSourceInput,
|
||||
'data-source-method-select': validateDataSourceMethodSelect,
|
||||
'data-source-field-select': validateDataSourceFieldSelect,
|
||||
'data-source-select': validateDataSourceSelect,
|
||||
'code-select': validateCodeSelect,
|
||||
'data-source-fields': validateDataSourceFields,
|
||||
'data-source-mocks': validateDataSourceMocks,
|
||||
'data-source-methods': validateDataSourceMethods,
|
||||
'event-select': validateEventSelect,
|
||||
'display-conds': validateDisplayConds,
|
||||
};
|
||||
|
||||
export type { TypeMatchValidateContext };
|
||||
@ -137,6 +137,19 @@ describe('CodeSelect', () => {
|
||||
expect(dsCol.display(undefined, { model: { codeType: 'code' } })).toBe(false);
|
||||
});
|
||||
|
||||
test('codeId 单元格开启 typeMatch(存在性校验下沉到单元格)', () => {
|
||||
const wrapper = mount(CodeSelect, { props: baseProps() as any });
|
||||
const container = wrapper.findComponent({ name: 'MContainer' });
|
||||
const config = container.props('config') as any;
|
||||
const row = config.items[0];
|
||||
const codeIdCol = row.items[1];
|
||||
const dsCol = row.items[2];
|
||||
expect(codeIdCol.type).toBe('code-select-col');
|
||||
expect(codeIdCol.rules).toEqual([{ typeMatch: true, trigger: 'blur' }]);
|
||||
expect(dsCol.type).toBe('data-source-method-select');
|
||||
expect(dsCol.rules).toEqual([{ typeMatch: true }]);
|
||||
});
|
||||
|
||||
test('notEditable 调用各服务', () => {
|
||||
codeBlockService.getEditStatus.mockReturnValue(false);
|
||||
dataSourceService.get.mockReturnValue(false);
|
||||
|
||||
@ -23,9 +23,6 @@ vi.mock('@editor/utils', async () => {
|
||||
return {
|
||||
...actual,
|
||||
getFieldType: vi.fn(),
|
||||
arrayOptions: [{ text: 'in', value: 'in' }],
|
||||
eqOptions: [{ text: 'eq', value: 'eq' }],
|
||||
numberOptions: [{ text: 'gt', value: 'gt' }],
|
||||
};
|
||||
});
|
||||
|
||||
@ -61,7 +58,9 @@ describe('CondOpSelect', () => {
|
||||
test('array 类型展示 arrayOptions', () => {
|
||||
(getFieldType as any).mockReturnValue('array');
|
||||
const wrapper = mount(CondOpSelect, { props: baseProps() as any });
|
||||
expect(wrapper.findAll('option, .tmagic-design-option, [label]').length).toBeGreaterThan(0);
|
||||
const html = wrapper.html();
|
||||
expect(html).toContain('label="包含"');
|
||||
expect(html).toContain('label="不包含"');
|
||||
});
|
||||
|
||||
test('boolean/null 类型展示 是/不是', () => {
|
||||
@ -74,31 +73,31 @@ describe('CondOpSelect', () => {
|
||||
(getFieldType as any).mockReturnValue('number');
|
||||
const wrapper = mount(CondOpSelect, { props: baseProps() as any });
|
||||
const html = wrapper.html();
|
||||
expect(html).toContain('label="eq"');
|
||||
expect(html).toContain('label="gt"');
|
||||
expect(html).toContain('label="等于"');
|
||||
expect(html).toContain('label="大于"');
|
||||
});
|
||||
|
||||
test('string 类型 options 包含 array+eq', () => {
|
||||
(getFieldType as any).mockReturnValue('string');
|
||||
const wrapper = mount(CondOpSelect, { props: baseProps() as any });
|
||||
const html = wrapper.html();
|
||||
expect(html).toContain('label="in"');
|
||||
expect(html).toContain('label="eq"');
|
||||
expect(html).toContain('label="包含"');
|
||||
expect(html).toContain('label="等于"');
|
||||
});
|
||||
|
||||
test('其他类型展示 array+eq+number', () => {
|
||||
(getFieldType as any).mockReturnValue('any');
|
||||
const wrapper = mount(CondOpSelect, { props: baseProps() as any });
|
||||
const html = wrapper.html();
|
||||
expect(html).toContain('label="in"');
|
||||
expect(html).toContain('label="eq"');
|
||||
expect(html).toContain('label="gt"');
|
||||
expect(html).toContain('label="包含"');
|
||||
expect(html).toContain('label="等于"');
|
||||
expect(html).toContain('label="大于"');
|
||||
});
|
||||
|
||||
test('change 事件 emit', async () => {
|
||||
(getFieldType as any).mockReturnValue('string');
|
||||
const wrapper = mount(CondOpSelect, { props: baseProps() as any });
|
||||
await wrapper.findComponent({ name: 'TMagicSelect' }).vm.$emit('change', 'eq');
|
||||
expect(wrapper.emitted('change')?.[0]?.[0]).toBe('eq');
|
||||
await wrapper.findComponent({ name: 'TMagicSelect' }).vm.$emit('change', '=');
|
||||
expect(wrapper.emitted('change')?.[0]?.[0]).toBe('=');
|
||||
});
|
||||
});
|
||||
|
||||
@ -29,12 +29,16 @@ vi.mock('@editor/hooks/use-services', () => ({
|
||||
useServices: () => ({ dataSourceService, propsService }),
|
||||
}));
|
||||
|
||||
vi.mock('@editor/utils/data-source', () => ({
|
||||
getDisplayField: vi.fn((_dss: any, value: string) => {
|
||||
if (!value) return [];
|
||||
return [{ value, type: 'text' }];
|
||||
}),
|
||||
}));
|
||||
vi.mock('@editor/utils/data-source', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@editor/utils/data-source')>();
|
||||
return {
|
||||
...actual,
|
||||
getDisplayField: vi.fn((_dss: any, value: string) => {
|
||||
if (!value) return [];
|
||||
return [{ value, type: 'text' }];
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@editor/components/Icon.vue', () => ({
|
||||
default: defineComponent({ name: 'IconStub', setup: () => () => h('i') }),
|
||||
|
||||
@ -100,6 +100,35 @@ describe('DisplayConds', () => {
|
||||
expect(item.type).toBe('data-source-field-select');
|
||||
});
|
||||
|
||||
test('field / op 单元格开启 typeMatch(枚举校验下沉到单元格)', () => {
|
||||
mount(DisplayConds, {
|
||||
props: { config: { titlePrefix: 't', parentFields: ['ds1'] }, model: {}, name: 'conds' } as any,
|
||||
});
|
||||
const cascaderField = capturedConfig.items[0].items[0];
|
||||
const opItem = capturedConfig.items[0].items[1];
|
||||
expect(cascaderField.rules).toEqual([
|
||||
{ required: true, trigger: 'blur', message: '请选择字段' },
|
||||
{ typeMatch: true, trigger: 'blur' },
|
||||
]);
|
||||
expect(opItem.type).toBe('cond-op-select');
|
||||
expect(opItem.rules).toEqual([
|
||||
{ required: true, trigger: 'blur', message: '请选择条件' },
|
||||
{ typeMatch: true, trigger: 'blur' },
|
||||
]);
|
||||
|
||||
// parentFields 为空时 field 走 data-source-field-select,同样开启 typeMatch
|
||||
capturedConfig = null;
|
||||
mount(DisplayConds, {
|
||||
props: { config: { titlePrefix: 't', parentFields: [] }, model: {}, name: 'conds' } as any,
|
||||
});
|
||||
const dsField = capturedConfig.items[0].items[0];
|
||||
expect(dsField.type).toBe('data-source-field-select');
|
||||
expect(dsField.rules).toEqual([
|
||||
{ required: true, trigger: 'blur', message: '请选择字段' },
|
||||
{ typeMatch: true, trigger: 'blur' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('value 字段类型 - number', () => {
|
||||
mount(DisplayConds, {
|
||||
props: { config: { titlePrefix: 't', parentFields: ['ds1'] }, model: {}, name: 'conds' } as any,
|
||||
|
||||
@ -9,34 +9,42 @@ import { mount } from '@vue/test-utils';
|
||||
|
||||
import EventSelect from '@editor/fields/EventSelect.vue';
|
||||
|
||||
const editorService = {
|
||||
get: vi.fn(),
|
||||
getNodeById: vi.fn(),
|
||||
};
|
||||
const dataSourceService = {
|
||||
get: vi.fn(),
|
||||
getDataSourceById: vi.fn(),
|
||||
getFormEvent: vi.fn(() => []),
|
||||
};
|
||||
const eventsService = {
|
||||
getEvent: vi.fn(() => [{ label: 'click', value: 'click' }]),
|
||||
getMethod: vi.fn(() => [{ label: 'open', value: 'open' }]),
|
||||
};
|
||||
const codeBlockService = {
|
||||
getCodeDsl: vi.fn(() => ({ c1: {} })),
|
||||
getEditStatus: vi.fn(() => true),
|
||||
};
|
||||
const propsService = {
|
||||
getDisabledCodeBlock: vi.fn(() => false),
|
||||
getDisabledDataSource: vi.fn(() => false),
|
||||
};
|
||||
const { editorService, dataSourceService, eventsService, codeBlockService, propsService } = vi.hoisted(() => ({
|
||||
editorService: {
|
||||
get: vi.fn(),
|
||||
getNodeById: vi.fn(),
|
||||
},
|
||||
dataSourceService: {
|
||||
get: vi.fn(),
|
||||
getDataSourceById: vi.fn(),
|
||||
getFormEvent: vi.fn(() => []),
|
||||
},
|
||||
eventsService: {
|
||||
getEvent: vi.fn(() => [{ label: 'click', value: 'click' }]),
|
||||
getMethod: vi.fn(() => [{ label: 'open', value: 'open' }]),
|
||||
},
|
||||
codeBlockService: {
|
||||
getCodeDsl: vi.fn(() => ({ c1: {} })),
|
||||
getEditStatus: vi.fn(() => true),
|
||||
},
|
||||
propsService: {
|
||||
getDisabledCodeBlock: vi.fn(() => false),
|
||||
getDisabledDataSource: vi.fn(() => false),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@editor/hooks/use-services', () => ({
|
||||
useServices: () => ({ editorService, dataSourceService, eventsService, codeBlockService, propsService }),
|
||||
}));
|
||||
|
||||
vi.mock('@editor/utils', async () => {
|
||||
const actual = await vi.importActual<any>('@editor/utils');
|
||||
vi.mock('@editor/services/editor', () => ({ default: editorService }));
|
||||
vi.mock('@editor/services/dataSource', () => ({ default: dataSourceService }));
|
||||
vi.mock('@editor/services/events', () => ({ default: eventsService }));
|
||||
vi.mock('@editor/services/codeBlock', () => ({ default: codeBlockService }));
|
||||
vi.mock('@editor/services/props', () => ({ default: propsService }));
|
||||
|
||||
vi.mock('@editor/utils/data-source', async () => {
|
||||
const actual = await vi.importActual<any>('@editor/utils/data-source');
|
||||
return { ...actual, getCascaderOptionsFromFields: vi.fn(() => []) };
|
||||
});
|
||||
|
||||
@ -192,6 +200,44 @@ describe('EventSelect', () => {
|
||||
expect(opts[0]).toMatchObject({ text: 'click', value: 'click' });
|
||||
});
|
||||
|
||||
test('eventNameConfig.rules 仅校验事件名是否在可选项中', () => {
|
||||
const wrapper = mount(EventSelect, {
|
||||
props: baseProps({
|
||||
model: { events: [{ name: 'a', actions: [] }] },
|
||||
}) as any,
|
||||
});
|
||||
const cfg = wrapper.findComponent({ name: 'MFormContainer' }).props('config') as any;
|
||||
const [rule] = cfg.rules;
|
||||
|
||||
const okCb = vi.fn();
|
||||
rule.validator({ value: 'click', callback: okCb }, { formValue: { type: 'btn' } });
|
||||
expect(okCb).toHaveBeenCalledWith();
|
||||
|
||||
const errCb = vi.fn();
|
||||
rule.validator({ value: 'unknown', callback: errCb }, { formValue: { type: 'btn' } });
|
||||
expect(errCb).toHaveBeenCalledWith('事件名(unknown)不存在');
|
||||
|
||||
// 空值不做枚举校验
|
||||
const emptyCb = vi.fn();
|
||||
rule.validator({ value: '', callback: emptyCb }, { formValue: { type: 'btn' } });
|
||||
expect(emptyCb).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('eventNameConfig.rules 自定义 options 时跳过枚举', () => {
|
||||
const wrapper = mount(EventSelect, {
|
||||
props: baseProps({
|
||||
config: { type: 'event-select', src: 'component', eventNameConfig: { options: () => [{ value: 'x' }] } },
|
||||
model: { events: [{ name: 'a', actions: [] }] },
|
||||
}) as any,
|
||||
});
|
||||
const cfg = wrapper.findComponent({ name: 'MFormContainer' }).props('config') as any;
|
||||
const [rule] = cfg.rules;
|
||||
|
||||
const cb = vi.fn();
|
||||
rule.validator({ value: 'whatever', callback: cb }, { formValue: { type: 'btn' } });
|
||||
expect(cb).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('eventNameConfig type 当 page-fragment 且有 pageFragmentId 返回 cascader', () => {
|
||||
editorService.get.mockReturnValue({ items: [{ id: 'pf1', items: [] }] });
|
||||
const wrapper = mount(EventSelect, {
|
||||
@ -244,7 +290,7 @@ describe('EventSelect', () => {
|
||||
const panelCfg = wrapper.findComponent({ name: 'MPanel' }).props('config') as any;
|
||||
const groupItems = panelCfg.items[0].items;
|
||||
const actionType = groupItems[0];
|
||||
const opts = actionType.options();
|
||||
const opts = typeof actionType.options === 'function' ? actionType.options() : actionType.options;
|
||||
expect(opts.map((o: any) => o.value).sort()).toEqual(['code', 'comp', 'data-source'].sort());
|
||||
});
|
||||
|
||||
@ -258,7 +304,7 @@ describe('EventSelect', () => {
|
||||
});
|
||||
const panelCfg = wrapper.findComponent({ name: 'MPanel' }).props('config') as any;
|
||||
const actionType = panelCfg.items[0].items[0];
|
||||
const opts = actionType.options();
|
||||
const opts = typeof actionType.options === 'function' ? actionType.options() : actionType.options;
|
||||
expect(opts.map((o: any) => o.value)).toEqual(['comp']);
|
||||
propsService.getDisabledCodeBlock.mockReturnValue(false);
|
||||
propsService.getDisabledDataSource.mockReturnValue(false);
|
||||
@ -318,6 +364,74 @@ describe('EventSelect', () => {
|
||||
expect(compAction.options(undefined, { model: { to: 'unknown' } })).toEqual([]);
|
||||
});
|
||||
|
||||
test('compActionConfig.rules 仅校验动作名是否在可选项中', () => {
|
||||
editorService.getNodeById.mockReturnValue({ type: 'btn', id: '1' });
|
||||
eventsService.getMethod.mockReturnValue([{ label: 'open', value: 'open' }]);
|
||||
const wrapper = mount(EventSelect, {
|
||||
props: baseProps({
|
||||
model: { events: [{ name: 'a', actions: [] }] },
|
||||
}) as any,
|
||||
});
|
||||
const panelCfg = wrapper.findComponent({ name: 'MPanel' }).props('config') as any;
|
||||
const compAction = panelCfg.items[0].items[2];
|
||||
const [rule] = compAction.rules;
|
||||
|
||||
const okCb = vi.fn();
|
||||
rule.validator({ value: 'open', callback: okCb }, { model: { to: '1' } });
|
||||
expect(okCb).toHaveBeenCalledWith();
|
||||
|
||||
const errCb = vi.fn();
|
||||
rule.validator({ value: 'unknown', callback: errCb }, { model: { to: '1' } });
|
||||
expect(errCb).toHaveBeenCalledWith('动作名(unknown)不存在');
|
||||
|
||||
const emptyCb = vi.fn();
|
||||
rule.validator({ value: '', callback: emptyCb }, { model: { to: '1' } });
|
||||
expect(emptyCb).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('compActionConfig.rules 自定义 options 时跳过枚举', () => {
|
||||
editorService.getNodeById.mockReturnValue({ type: 'btn', id: '1' });
|
||||
const wrapper = mount(EventSelect, {
|
||||
props: baseProps({
|
||||
config: {
|
||||
type: 'event-select',
|
||||
src: 'component',
|
||||
compActionConfig: { options: () => [{ text: 'x', value: 'x' }] },
|
||||
},
|
||||
model: { events: [{ name: 'a', actions: [] }] },
|
||||
}) as any,
|
||||
});
|
||||
const panelCfg = wrapper.findComponent({ name: 'MPanel' }).props('config') as any;
|
||||
const compAction = panelCfg.items[0].items[2];
|
||||
const [rule] = compAction.rules;
|
||||
|
||||
const cb = vi.fn();
|
||||
rule.validator({ value: 'whatever', callback: cb }, { model: { to: '1' } });
|
||||
expect(cb).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('compActionConfig.rules 页面片 cascader 支持数组 method', () => {
|
||||
editorService.getNodeById.mockReturnValue({ type: 'page-fragment-container', id: '1', pageFragmentId: 'pf1' });
|
||||
editorService.get.mockReturnValue({ items: [{ id: 'pf1', items: [{ id: 'c1', type: 'btn', name: 'b' }] }] });
|
||||
eventsService.getMethod.mockReturnValue([{ label: 'open', value: 'open' }]);
|
||||
const wrapper = mount(EventSelect, {
|
||||
props: baseProps({
|
||||
model: { events: [{ name: 'a', actions: [] }] },
|
||||
}) as any,
|
||||
});
|
||||
const panelCfg = wrapper.findComponent({ name: 'MPanel' }).props('config') as any;
|
||||
const compAction = panelCfg.items[0].items[2];
|
||||
const [rule] = compAction.rules;
|
||||
|
||||
const okCb = vi.fn();
|
||||
rule.validator({ value: ['c1', 'open'], callback: okCb }, { model: { to: '1' } });
|
||||
expect(okCb).toHaveBeenCalledWith();
|
||||
|
||||
const errCb = vi.fn();
|
||||
rule.validator({ value: ['c1', 'missing'], callback: errCb }, { model: { to: '1' } });
|
||||
expect(errCb).toHaveBeenCalledWith('动作名(c1.missing)不存在');
|
||||
});
|
||||
|
||||
test('codeActionConfig display/notEditable', () => {
|
||||
codeBlockService.getEditStatus.mockReturnValue(false);
|
||||
const wrapper = mount(EventSelect, {
|
||||
|
||||
@ -10,9 +10,14 @@ import editorPlugin from '@editor/plugin';
|
||||
vi.mock('@tmagic/design', () => ({
|
||||
default: { install: vi.fn() },
|
||||
}));
|
||||
vi.mock('@tmagic/form', () => ({
|
||||
default: { install: vi.fn() },
|
||||
}));
|
||||
vi.mock('@tmagic/form', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@tmagic/form')>();
|
||||
return {
|
||||
...actual,
|
||||
default: { install: vi.fn() },
|
||||
registerTypeMatchRules: vi.fn(),
|
||||
};
|
||||
});
|
||||
vi.mock('@tmagic/table', () => ({
|
||||
default: { install: vi.fn() },
|
||||
}));
|
||||
@ -70,10 +75,13 @@ describe('plugin install', () => {
|
||||
};
|
||||
};
|
||||
|
||||
test('install 调用 design/form/table 插件并注册全局组件', () => {
|
||||
test('install 调用 design/form/table 插件并注册全局组件', async () => {
|
||||
const { registerTypeMatchRules } = await import('@tmagic/form');
|
||||
const { editorTypeMatchRules } = await import('@editor/utils/typeMatchRules');
|
||||
const { app, components } = buildApp();
|
||||
editorPlugin.install(app, { someOption: true } as any);
|
||||
expect(app.use).toHaveBeenCalledTimes(3);
|
||||
expect(registerTypeMatchRules).toHaveBeenCalledWith(editorTypeMatchRules);
|
||||
expect(Object.keys(components).length).toBeGreaterThan(10);
|
||||
expect(components.MEditor).toBeDefined();
|
||||
});
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
getFieldType,
|
||||
getFormConfig,
|
||||
getFormValue,
|
||||
resolveFieldByPath,
|
||||
} from '@editor/utils/data-source';
|
||||
|
||||
describe('data-source utils', () => {
|
||||
@ -84,6 +85,48 @@ describe('data-source utils', () => {
|
||||
expect(opts.map((o) => o.value)).toEqual(['a']);
|
||||
});
|
||||
|
||||
test('resolveFieldByPath 沿 path 下钻', () => {
|
||||
const fields: any = [
|
||||
{
|
||||
name: 'obj',
|
||||
type: 'object',
|
||||
fields: [{ name: 'name', type: 'string' }],
|
||||
},
|
||||
{
|
||||
name: 'arr',
|
||||
type: 'array',
|
||||
fields: [{ name: 'item', type: 'number' }],
|
||||
},
|
||||
];
|
||||
|
||||
expect(resolveFieldByPath(fields, ['obj', 'name'])).toMatchObject({
|
||||
ok: true,
|
||||
field: { name: 'name', type: 'string' },
|
||||
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, []).ok).toBe(true);
|
||||
});
|
||||
|
||||
test('resolveFieldByPath 可跳过数字下标', () => {
|
||||
const fields: any = [
|
||||
{
|
||||
name: 'arr',
|
||||
type: 'array',
|
||||
fields: [{ name: 'item', type: 'number', fields: [{ name: 'n', type: 'string' }] }],
|
||||
},
|
||||
];
|
||||
|
||||
const result = resolveFieldByPath(fields, ['arr', '0', 'item'], { skipNumberIndices: true });
|
||||
expect(result.ok).toBe(true);
|
||||
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);
|
||||
});
|
||||
|
||||
test('getFieldType 沿 path 取最终类型', () => {
|
||||
const ds: any = {
|
||||
fields: [{ name: 'obj', type: 'object', fields: [{ name: 'name', type: 'string' }] }],
|
||||
|
||||
107
packages/editor/tests/unit/utils/event.spec.ts
Normal file
107
packages/editor/tests/unit/utils/event.spec.ts
Normal file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent.
|
||||
*/
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
collectEventNameOptionValues,
|
||||
getCompActionAllowedValues,
|
||||
getCompActionOptions,
|
||||
getEventNameAllowedValues,
|
||||
getEventNameOptions,
|
||||
normalizeCompActionValue,
|
||||
} from '@editor/utils/event';
|
||||
|
||||
const { editorService, dataSourceService, eventsService } = vi.hoisted(() => ({
|
||||
editorService: {
|
||||
get: vi.fn(),
|
||||
getNodeById: vi.fn(),
|
||||
},
|
||||
dataSourceService: {
|
||||
getDataSourceById: vi.fn(),
|
||||
getFormEvent: vi.fn(() => []),
|
||||
},
|
||||
eventsService: {
|
||||
getEvent: vi.fn(() => [{ label: 'click', value: 'click' }]),
|
||||
getMethod: vi.fn(() => [{ label: 'open', value: 'open' }]),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@editor/services/editor', () => ({ default: editorService }));
|
||||
vi.mock('@editor/services/dataSource', () => ({ default: dataSourceService }));
|
||||
vi.mock('@editor/services/events', () => ({ default: eventsService }));
|
||||
vi.mock('@editor/utils/data-source', async () => {
|
||||
const actual = await vi.importActual<any>('@editor/utils/data-source');
|
||||
return { ...actual, getCascaderOptionsFromFields: vi.fn(() => [{ label: 'f1', value: 'f1' }]) };
|
||||
});
|
||||
|
||||
describe('event utils', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
eventsService.getEvent.mockReturnValue([{ label: 'click', value: 'click' }]);
|
||||
eventsService.getMethod.mockReturnValue([{ label: 'open', value: 'open' }]);
|
||||
});
|
||||
|
||||
test('normalizeCompActionValue 支持字符串与数组', () => {
|
||||
expect(normalizeCompActionValue('open')).toBe('open');
|
||||
expect(normalizeCompActionValue(['c1', 'open'])).toBe('c1.open');
|
||||
expect(normalizeCompActionValue('')).toBe('');
|
||||
expect(normalizeCompActionValue(undefined)).toBe('');
|
||||
});
|
||||
|
||||
test('getCompActionOptions 普通组件返回 select options', () => {
|
||||
editorService.getNodeById.mockReturnValue({ type: 'btn', id: '1' });
|
||||
expect(getCompActionOptions('1')).toEqual([{ text: 'open', value: 'open' }]);
|
||||
});
|
||||
|
||||
test('getCompActionOptions 无节点返回空', () => {
|
||||
editorService.getNodeById.mockReturnValue(null);
|
||||
expect(getCompActionOptions('missing')).toEqual([]);
|
||||
expect(getCompActionOptions(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
test('getCompActionOptions 页面片容器返回 cascader options', () => {
|
||||
editorService.getNodeById.mockReturnValue({ type: 'page-fragment-container', id: '1', pageFragmentId: 'pf1' });
|
||||
editorService.get.mockReturnValue({ items: [{ id: 'pf1', items: [{ id: 'c1', type: 'btn', name: 'b' }] }] });
|
||||
expect(getCompActionOptions('1')).toEqual([
|
||||
{
|
||||
label: 'b_c1',
|
||||
value: 'c1',
|
||||
children: [{ label: 'open', value: 'open' }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('getCompActionAllowedValues 枚举合法动作', () => {
|
||||
editorService.getNodeById.mockReturnValue({ type: 'btn', id: '1' });
|
||||
expect(getCompActionAllowedValues({ src: 'component' }, { to: '1' })).toEqual(new Set(['open']));
|
||||
});
|
||||
|
||||
test('getCompActionAllowedValues 自定义 options 返回 null', () => {
|
||||
expect(
|
||||
getCompActionAllowedValues({ src: 'component', compActionConfig: { options: () => [] } }, { to: '1' }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('getCompActionAllowedValues 页面片路径用 . 拼接', () => {
|
||||
editorService.getNodeById.mockReturnValue({ type: 'page-fragment-container', id: '1', pageFragmentId: 'pf1' });
|
||||
editorService.get.mockReturnValue({ items: [{ id: 'pf1', items: [{ id: 'c1', type: 'btn', name: 'b' }] }] });
|
||||
expect(getCompActionAllowedValues({ src: 'component' }, { to: '1' })).toEqual(new Set(['c1.open']));
|
||||
});
|
||||
|
||||
test('getEventNameAllowedValues / collectEventNameOptionValues', () => {
|
||||
expect(getEventNameAllowedValues({ src: 'component' }, { type: 'btn' })).toEqual(new Set(['click']));
|
||||
expect(
|
||||
getEventNameAllowedValues({ src: 'component', eventNameConfig: { options: [] } }, { type: 'btn' }),
|
||||
).toBeNull();
|
||||
expect(collectEventNameOptionValues([{ value: 'a', children: [{ value: 'b' }] }], false)).toEqual(new Set(['a.b']));
|
||||
});
|
||||
|
||||
test('getEventNameOptions datasource 追加数据变化', () => {
|
||||
dataSourceService.getDataSourceById.mockReturnValue({ fields: [{ name: 'f1' }] });
|
||||
const opts = getEventNameOptions('datasource', { type: 'ds', id: 'd1' });
|
||||
expect(opts.some((item: any) => item.value === 'ds-field-changed' || item.label === '数据变化')).toBe(true);
|
||||
});
|
||||
});
|
||||
@ -10,10 +10,12 @@ import { NODE_CONDS_RESULT_KEY } from '@tmagic/core';
|
||||
import {
|
||||
advancedTabConfig,
|
||||
arrayOptions,
|
||||
booleanOptions,
|
||||
displayTabConfig,
|
||||
eqOptions,
|
||||
eventTabConfig,
|
||||
fillConfig,
|
||||
getCondOpOptionsByFieldType,
|
||||
numberOptions,
|
||||
styleTabConfig,
|
||||
} from '@editor/utils/props';
|
||||
@ -26,10 +28,31 @@ vi.mock('@tmagic/design', () => ({
|
||||
}));
|
||||
|
||||
describe('props 选项常量', () => {
|
||||
test('eqOptions / arrayOptions / numberOptions 内容稳定', () => {
|
||||
test('eqOptions / arrayOptions / numberOptions / booleanOptions 内容稳定', () => {
|
||||
expect(eqOptions.map((o) => o.value)).toEqual(['=', '!=']);
|
||||
expect(arrayOptions.map((o) => o.value)).toEqual(['include', 'not_include']);
|
||||
expect(numberOptions.map((o) => o.value)).toEqual(['>', '>=', '<', '<=', 'between', 'not_between']);
|
||||
expect(booleanOptions.map((o) => o.value)).toEqual(['is', 'not']);
|
||||
});
|
||||
|
||||
test('getCondOpOptionsByFieldType 按类型返回选项', () => {
|
||||
expect(getCondOpOptionsByFieldType('array')).toEqual(arrayOptions);
|
||||
expect(getCondOpOptionsByFieldType('boolean')).toEqual(booleanOptions);
|
||||
expect(getCondOpOptionsByFieldType('null')).toEqual(booleanOptions);
|
||||
expect(getCondOpOptionsByFieldType('number').map((o) => o.value)).toEqual([
|
||||
...eqOptions.map((o) => o.value),
|
||||
...numberOptions.map((o) => o.value),
|
||||
]);
|
||||
expect(getCondOpOptionsByFieldType('string').map((o) => o.value)).toEqual([
|
||||
...arrayOptions.map((o) => o.value),
|
||||
...eqOptions.map((o) => o.value),
|
||||
]);
|
||||
expect(getCondOpOptionsByFieldType('any').map((o) => o.value)).toEqual([
|
||||
...arrayOptions.map((o) => o.value),
|
||||
...eqOptions.map((o) => o.value),
|
||||
...numberOptions.map((o) => o.value),
|
||||
]);
|
||||
expect(getCondOpOptionsByFieldType('').map((o) => o.value)).not.toContain('is');
|
||||
});
|
||||
|
||||
test('styleTabConfig / eventTabConfig / displayTabConfig 基础结构', () => {
|
||||
|
||||
340
packages/editor/tests/unit/utils/typeMatchRules.spec.ts
Normal file
340
packages/editor/tests/unit/utils/typeMatchRules.spec.ts
Normal file
@ -0,0 +1,340 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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 } from '@editor/utils/typeMatchRules';
|
||||
|
||||
const codeDslState = vi.hoisted(() => ({ value: null as Record<string, any> | null }));
|
||||
const dataSourcesState = vi.hoisted(() => ({ value: [] as any[] }));
|
||||
const formMethodsState = vi.hoisted(() => ({ value: [] as { label: string; value: string }[] }));
|
||||
const formEventsState = vi.hoisted(() => ({ value: [] as { label: string; value: string }[] }));
|
||||
const componentEventsState = vi.hoisted((): { value: Record<string, { label: string; value: string }[]> } => ({
|
||||
value: {},
|
||||
}));
|
||||
const rootState = vi.hoisted(() => ({ value: null as any }));
|
||||
const nodesState = vi.hoisted((): { value: Record<string, any> } => ({ value: {} }));
|
||||
|
||||
vi.mock('@editor/services/codeBlock', () => ({
|
||||
default: {
|
||||
getCodeDsl: () => codeDslState.value,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@editor/services/dataSource', () => ({
|
||||
default: {
|
||||
get: (key: string) => {
|
||||
if (key === 'dataSources') return dataSourcesState.value;
|
||||
if (key === 'events') return {};
|
||||
return undefined;
|
||||
},
|
||||
getFormMethod: () => formMethodsState.value,
|
||||
getFormEvent: () => formEventsState.value,
|
||||
getDataSourceById: (id: string) => dataSourcesState.value.find((ds) => `${ds.id}` === `${id}`),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@editor/services/editor', () => ({
|
||||
default: {
|
||||
get: (key: string) => (key === 'root' ? rootState.value : undefined),
|
||||
getNodeById: (id: string | number) => nodesState.value[`${id}`] || null,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@editor/services/events', () => ({
|
||||
default: {
|
||||
getEvent: (type: string) => componentEventsState.value[type] || [],
|
||||
},
|
||||
}));
|
||||
|
||||
const ctx = (config: Record<string, any> = {}, model: Record<string, any> = {}, message?: string) => ({
|
||||
fieldType: config.type || '',
|
||||
mForm: undefined,
|
||||
props: { config, model, prop: config.name || 'field' },
|
||||
message,
|
||||
});
|
||||
|
||||
const run = (
|
||||
type: string,
|
||||
value: any,
|
||||
config: Record<string, any> = {},
|
||||
model: Record<string, any> = {},
|
||||
message?: string,
|
||||
) => editorTypeMatchRules[type](value, ctx({ ...config, type }, model, message));
|
||||
|
||||
describe('editorTypeMatchRules', () => {
|
||||
beforeEach(() => {
|
||||
codeDslState.value = null;
|
||||
dataSourcesState.value = [];
|
||||
formMethodsState.value = [];
|
||||
formEventsState.value = [];
|
||||
componentEventsState.value = {};
|
||||
rootState.value = null;
|
||||
nodesState.value = {};
|
||||
});
|
||||
|
||||
test('key-value / style-setter 形态校验', () => {
|
||||
expect(run('key-value', { a: '1' })).toBeUndefined();
|
||||
expect(run('key-value', [])).toBe('值类型应为对象');
|
||||
expect(run('key-value', () => 1, { advanced: true })).toBeUndefined();
|
||||
expect(run('style-setter', { width: '1px' })).toBeUndefined();
|
||||
expect(run('style-setter', [])).toBe('值类型应为对象');
|
||||
});
|
||||
|
||||
test('cond-op-select 按字段类型收窄,未知类型用默认全集', () => {
|
||||
expect(run('cond-op-select', 1)).toBe('值类型应为字符串');
|
||||
expect(run('cond-op-select', '=')).toBeUndefined();
|
||||
expect(ALL_COND_OPS.has('between')).toBe(true);
|
||||
expect(ALL_COND_OPS.has('is')).toBe(true);
|
||||
|
||||
dataSourcesState.value = [
|
||||
{
|
||||
id: 'ds1',
|
||||
type: 'base',
|
||||
fields: [
|
||||
{ name: 'age', type: 'number' },
|
||||
{ name: 'flag', type: 'boolean' },
|
||||
],
|
||||
methods: [],
|
||||
},
|
||||
];
|
||||
expect(run('cond-op-select', '>', {}, { field: ['ds1', 'age'] })).toBeUndefined();
|
||||
expect(run('cond-op-select', 'include', {}, { field: ['ds1', 'age'] })).toBe('include 不在可选项中');
|
||||
expect(run('cond-op-select', 'is', {}, { field: ['ds1', 'flag'] })).toBeUndefined();
|
||||
// 未知类型与 UI 默认选项对齐:不含 boolean ops
|
||||
expect(run('cond-op-select', 'is', {}, { field: ['ds1', 'missing'] })).toBe('is 不在可选项中');
|
||||
expect(run('cond-op-select', 'include', {}, { field: ['ds1', 'missing'] })).toBeUndefined();
|
||||
});
|
||||
|
||||
test('code-select-col 有 DSL 时校验 key', () => {
|
||||
expect(run('code-select-col', 1)).toBe('1类型应为字符串');
|
||||
expect(run('code-select-col', 'code_1')).toBeUndefined();
|
||||
|
||||
codeDslState.value = { code_1: { name: 'A' } };
|
||||
expect(run('code-select-col', 'code_1')).toBeUndefined();
|
||||
expect(run('code-select-col', 'missing')).toBe('代码块(missing)不存在');
|
||||
});
|
||||
|
||||
test('page-fragment-select / ui-select 校验节点存在性', () => {
|
||||
expect(run('page-fragment-select', true)).toBe('true类型应为字符串或数字');
|
||||
expect(run('page-fragment-select', 'pf_1')).toBeUndefined();
|
||||
|
||||
rootState.value = {
|
||||
items: [
|
||||
{ id: 'pf_1', type: NodeType.PAGE_FRAGMENT },
|
||||
{ id: 'page_1', type: NodeType.PAGE },
|
||||
],
|
||||
};
|
||||
expect(run('page-fragment-select', 'pf_1')).toBeUndefined();
|
||||
expect(run('page-fragment-select', 'page_1')).toBe('页面片段(page_1)不存在');
|
||||
|
||||
rootState.value = null;
|
||||
expect(run('ui-select', 'node_1')).toBeUndefined();
|
||||
|
||||
rootState.value = { items: [] };
|
||||
nodesState.value = { node_1: { id: 'node_1' } };
|
||||
expect(run('ui-select', 'node_1')).toBeUndefined();
|
||||
expect(run('ui-select', 'missing')).toBe('组件(missing)不存在');
|
||||
});
|
||||
|
||||
test('data-source-input 校验模板绑定', () => {
|
||||
expect(run('data-source-input', 1)).toBe('1类型应为字符串');
|
||||
expect(run('data-source-input', 'plain')).toBeUndefined();
|
||||
expect(run('data-source-input', '${ds1.title}')).toBeUndefined();
|
||||
|
||||
dataSourcesState.value = [
|
||||
{
|
||||
id: 'ds1',
|
||||
type: 'base',
|
||||
fields: [
|
||||
{ name: 'title', type: 'string' },
|
||||
{ name: 'arr', type: 'array', fields: [{ name: 'item', type: 'string' }] },
|
||||
],
|
||||
methods: [],
|
||||
},
|
||||
];
|
||||
expect(run('data-source-input', 'hello ${ds1.title}')).toBeUndefined();
|
||||
expect(run('data-source-input', '${ds1.arr[0].item}')).toBeUndefined();
|
||||
expect(run('data-source-input', '${missing.title}')).toBe('数据源绑定(${missing.title})不合法');
|
||||
expect(run('data-source-input', '${ds1.missing}')).toBe('数据源绑定(${ds1.missing})不合法');
|
||||
});
|
||||
|
||||
test('data-source-method-select 校验方法元组', () => {
|
||||
expect(run('data-source-method-select', ['ds1'])).toBe('ds1类型应为长度为 2 的字符串数组');
|
||||
expect(run('data-source-method-select', ['ds1', 'foo'])).toBeUndefined();
|
||||
|
||||
dataSourcesState.value = [
|
||||
{
|
||||
id: 'ds1',
|
||||
type: 'base',
|
||||
fields: [],
|
||||
methods: [{ name: 'custom' }],
|
||||
},
|
||||
];
|
||||
formMethodsState.value = [{ label: '内置', value: 'builtin' }];
|
||||
|
||||
expect(run('data-source-method-select', ['ds1', DATA_SOURCE_SET_DATA_METHOD_NAME])).toBeUndefined();
|
||||
expect(run('data-source-method-select', ['ds1', 'builtin'])).toBeUndefined();
|
||||
expect(run('data-source-method-select', ['ds1', 'custom'])).toBeUndefined();
|
||||
expect(run('data-source-method-select', ['ds1', 'missing'])).toBe('数据源方法(missing)不存在');
|
||||
expect(run('data-source-method-select', ['missing', 'custom'])).toBe('数据源(missing)不存在');
|
||||
});
|
||||
|
||||
test('data-source-field-select 校验路径与 fieldConfig 跳过', () => {
|
||||
expect(run('data-source-field-select', 'x')).toBe('x类型应为字符串数组');
|
||||
expect(run('data-source-field-select', 'text-value', { fieldConfig: { type: 'text' } })).toBeUndefined();
|
||||
|
||||
dataSourcesState.value = [
|
||||
{
|
||||
id: 'ds1',
|
||||
type: 'base',
|
||||
fields: [
|
||||
{ name: 'title', type: 'string' },
|
||||
{ name: 'obj', type: 'object', fields: [{ name: 'a', type: 'number' }] },
|
||||
],
|
||||
methods: [],
|
||||
},
|
||||
];
|
||||
|
||||
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(run('data-source-field-select', ['ds1', 'missing'], { value: 'key' })).toBe('值不在可选项中');
|
||||
expect(
|
||||
run('data-source-field-select', ['title'], {
|
||||
dataSourceId: 'ds1',
|
||||
dataSourceFieldType: ['number'],
|
||||
}),
|
||||
).toBe('值不在可选项中');
|
||||
expect(
|
||||
run('data-source-field-select', ['obj', 'a'], {
|
||||
dataSourceId: 'ds1',
|
||||
dataSourceFieldType: ['number'],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('data-source-select id / 对象形态', () => {
|
||||
expect(run('data-source-select', { isBindDataSource: true, dataSourceId: 'ds1' })).toBeUndefined();
|
||||
expect(run('data-source-select', 'ds1', { value: 'id' })).toBeUndefined();
|
||||
|
||||
dataSourcesState.value = [
|
||||
{ id: 'ds1', type: 'base', fields: [], methods: [] },
|
||||
{ id: 'ds2', type: 'http', fields: [], methods: [] },
|
||||
];
|
||||
|
||||
expect(run('data-source-select', 'ds1', { value: 'id' })).toBeUndefined();
|
||||
expect(run('data-source-select', 'missing', { value: 'id' })).toBe('missing不在可选项中');
|
||||
expect(run('data-source-select', 'ds2', { value: 'id', dataSourceType: 'base' })).toBe('ds2不在可选项中');
|
||||
expect(
|
||||
run('data-source-select', { isBindDataSource: true, dataSourceId: 'ds1', dataSourceType: 'base' }),
|
||||
).toBeUndefined();
|
||||
expect(run('data-source-select', { isBindDataSource: true, dataSourceId: 'ds1', dataSourceType: 'http' })).toBe(
|
||||
'[object Object]不在可选项中',
|
||||
);
|
||||
expect(run('data-source-select', { isBindDataSource: false, dataSourceId: 'ds1' })).toBe(
|
||||
'[object Object]类型不合法',
|
||||
);
|
||||
});
|
||||
|
||||
test('code-select 校验 hookData', () => {
|
||||
expect(run('code-select', { hookType: HookType.CODE, hookData: [] })).toBeUndefined();
|
||||
expect(run('code-select', { hookType: 'x', hookData: [] })).toBe('[object Object]类型不合法');
|
||||
|
||||
codeDslState.value = { code_1: { name: 'A' } };
|
||||
dataSourcesState.value = [{ id: 'ds1', type: 'base', fields: [], methods: [{ name: 'custom' }] }];
|
||||
|
||||
expect(
|
||||
run('code-select', {
|
||||
hookType: HookType.CODE,
|
||||
hookData: [{ codeType: HookCodeType.CODE, codeId: 'code_1', params: {} }],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
// 存在性(codeId 是否在 DSL 中)已下沉到 code-select-col 单元格校验,容器级仅校验结构
|
||||
expect(
|
||||
run('code-select', {
|
||||
hookType: HookType.CODE,
|
||||
hookData: [{ codeType: HookCodeType.CODE, codeId: 'missing' }],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
run('code-select', {
|
||||
hookType: HookType.CODE,
|
||||
hookData: [{ codeType: HookCodeType.DATA_SOURCE_METHOD, codeId: ['ds1', 'custom'] }],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
// 数据源方法存在性已下沉到 data-source-method-select 单元格校验
|
||||
expect(
|
||||
run('code-select', {
|
||||
hookType: HookType.CODE,
|
||||
hookData: [{ codeType: HookCodeType.DATA_SOURCE_METHOD, codeId: ['ds1', 'missing'] }],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
run('code-select', {
|
||||
hookType: HookType.CODE,
|
||||
hookData: [{ codeType: 'bad', codeId: 'code_1' }],
|
||||
}),
|
||||
).toBe('钩子项结构不合法');
|
||||
// DATA_SOURCE_METHOD 的 codeId 元组形态仍在容器级校验
|
||||
expect(
|
||||
run('code-select', {
|
||||
hookType: HookType.CODE,
|
||||
hookData: [{ codeType: HookCodeType.DATA_SOURCE_METHOD, codeId: 'not-tuple' }],
|
||||
}),
|
||||
).toBe('钩子项结构不合法');
|
||||
});
|
||||
|
||||
test('容器类浅层结构校验', () => {
|
||||
expect(run('data-source-fields', [{ name: 'a', type: 'string' }])).toBeUndefined();
|
||||
expect(run('data-source-fields', [{ name: 'a', type: 'oops' }])).toBe('字段类型不合法');
|
||||
expect(
|
||||
run('data-source-fields', [{ name: 'obj', type: 'object', fields: [{ name: 'child', type: 'number' }] }]),
|
||||
).toBeUndefined();
|
||||
expect(run('data-source-fields', [{ name: 'obj', type: 'object', fields: 'bad' }])).toBe('字段项结构不合法');
|
||||
|
||||
expect(
|
||||
run('data-source-mocks', [{ title: 'm', enable: true, useInEditor: false, data: { a: 1 } }]),
|
||||
).toBeUndefined();
|
||||
expect(run('data-source-mocks', [{ title: 'm', enable: true, useInEditor: false, data: [] }])).toBe(
|
||||
'mock 项结构不合法',
|
||||
);
|
||||
|
||||
expect(run('data-source-methods', [{ name: 'fn', content: '() => {}', params: [] }])).toBeUndefined();
|
||||
expect(run('data-source-methods', [{ name: 'fn', content: 1 }])).toBe('方法项结构不合法');
|
||||
|
||||
expect(run('event-select', [{ name: 'click', actions: [{ actionType: 'comp' }] }])).toBeUndefined();
|
||||
expect(run('event-select', [{ name: 'click', to: 'node_1', method: 'show' }])).toBeUndefined();
|
||||
expect(run('event-select', [{ name: 'click' }])).toBe('事件项结构不合法');
|
||||
expect(run('event-select', [{ name: 'click', actions: [{}] }])).toBe('事件项结构不合法');
|
||||
|
||||
expect(run('display-conds', [{ cond: [{ field: ['ds1', 'title'], op: '=' }] }])).toBeUndefined();
|
||||
// op 是否合法、字段路径是否存在已下沉到 cond-op-select / field 单元格校验,容器级仅校验结构
|
||||
expect(run('display-conds', [{ cond: [{ field: ['ds1'], op: 'bad' }] }])).toBeUndefined();
|
||||
expect(run('display-conds', [{ cond: [{ field: ['ds1', 'missing'], op: '=' }] }])).toBeUndefined();
|
||||
expect(run('display-conds', [{ cond: 'bad' }])).toBe('显示条件结构不合法');
|
||||
expect(run('display-conds', [{ cond: [{ field: 'bad', op: '=' }] }])).toBe('显示条件结构不合法');
|
||||
expect(run('display-conds', [{ cond: [{ field: ['ds1'], op: 1 }] }])).toBe('显示条件结构不合法');
|
||||
});
|
||||
|
||||
test('自定义 message 优先', () => {
|
||||
expect(run('key-value', [], {}, {}, '自定义错误')).toBe('自定义错误');
|
||||
});
|
||||
});
|
||||
@ -7,7 +7,7 @@
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.el-table .m-form-item .el-form-item {
|
||||
.el-table .m-form-item .el-form-item:not(.is-error) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
.m-fields-table {
|
||||
.el-form-item {
|
||||
&.is-error {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.m-fields-table {
|
||||
width: 100%;
|
||||
|
||||
@ -62,7 +72,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
.el-form-item:not(.is-error) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { getDesignConfig } from '@tmagic/design';
|
||||
import { getValueByKeyPath } from '@tmagic/utils';
|
||||
|
||||
import type {
|
||||
@ -41,6 +42,56 @@ import type {
|
||||
|
||||
import { createTypeMatchValidator } from './typeMatch';
|
||||
|
||||
type AsyncValidatorFn = (rule: any, value: any, callback: Function, source?: any, options?: any) => any;
|
||||
|
||||
const isTDesignAdapter = () => getDesignConfig('adapterType') === 'tdesign-vue-next';
|
||||
|
||||
/**
|
||||
* 将 async-validator(Element Plus)风格的 validator 适配到当前 UI 库。
|
||||
* TDesign 调用签名为 `(val) => boolean | CustomValidateObj | Promise`,无 callback。
|
||||
*/
|
||||
export const adaptFormValidator = (validator: AsyncValidatorFn): AsyncValidatorFn => {
|
||||
return (arg1: any, arg2?: any, arg3?: any, arg4?: any, arg5?: any) => {
|
||||
if (!isTDesignAdapter()) {
|
||||
return validator(arg1, arg2, arg3, arg4, arg5);
|
||||
}
|
||||
|
||||
// TDesign: validator(val)
|
||||
const value = arg1;
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const callback = (error?: Error | string) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (error) {
|
||||
resolve({
|
||||
result: false,
|
||||
message: typeof error === 'string' ? error : error.message,
|
||||
});
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const result = validator(undefined, value, callback);
|
||||
if (result !== null && typeof (result as PromiseLike<unknown>).then === 'function') {
|
||||
Promise.resolve(result).then(
|
||||
() => {
|
||||
if (!settled) callback();
|
||||
},
|
||||
(err) => {
|
||||
callback(err instanceof Error ? err : new Error(String(err)));
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
callback(e instanceof Error ? e : new Error(String(e)));
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
interface DefaultItem {
|
||||
defaultValue: any;
|
||||
type: string;
|
||||
@ -264,32 +315,34 @@ export const getRules = function (mForm: FormState | undefined, rules: Rule[] |
|
||||
|
||||
return rules.map((item) => {
|
||||
if (item.typeMatch) {
|
||||
(item as any).validator = createTypeMatchValidator(mForm, props, item);
|
||||
(item as any).validator = adaptFormValidator(createTypeMatchValidator(mForm, props, item));
|
||||
return item;
|
||||
}
|
||||
|
||||
if (typeof item.validator === 'function') {
|
||||
const fnc = item.validator;
|
||||
|
||||
(item as any).validator = (rule: any, value: any, callback: Function, source: any, options: any) =>
|
||||
fnc(
|
||||
{
|
||||
rule,
|
||||
value: props.config.names ? props.model : value,
|
||||
callback,
|
||||
source,
|
||||
options,
|
||||
},
|
||||
{
|
||||
values: mForm?.initValues || {},
|
||||
model: props.model,
|
||||
parent: mForm?.parentValues || {},
|
||||
formValue: mForm?.values || props.model,
|
||||
prop: props.prop,
|
||||
config: props.config,
|
||||
},
|
||||
mForm,
|
||||
);
|
||||
(item as any).validator = adaptFormValidator(
|
||||
(rule: any, value: any, callback: Function, source: any, options: any) =>
|
||||
fnc(
|
||||
{
|
||||
rule,
|
||||
value: props.config.names ? props.model : value,
|
||||
callback,
|
||||
source,
|
||||
options,
|
||||
},
|
||||
{
|
||||
values: mForm?.initValues || {},
|
||||
model: props.model,
|
||||
parent: mForm?.parentValues || {},
|
||||
formValue: mForm?.values || props.model,
|
||||
prop: props.prop,
|
||||
config: props.config,
|
||||
},
|
||||
mForm,
|
||||
),
|
||||
);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
@ -239,29 +239,29 @@ const validateSelectValue = (
|
||||
if (config.allowCreate || config.remote) {
|
||||
if (config.multiple) {
|
||||
if (!Array.isArray(value)) {
|
||||
return defaultMessage(message, '值类型应为数组');
|
||||
return defaultMessage(message, `${value} 类型应为数组`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return defaultMessage(message, '值类型不合法');
|
||||
return defaultMessage(message, `${value} 类型不合法`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (config.multiple) {
|
||||
if (!Array.isArray(value)) {
|
||||
return defaultMessage(message, '值类型应为数组');
|
||||
return defaultMessage(message, `${value} 类型应为数组`);
|
||||
}
|
||||
if (value.some((item) => !includesOptionValue(optionValues, item))) {
|
||||
return defaultMessage(message, '值不在可选项中');
|
||||
return defaultMessage(message, `${value} 不在可选项中`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!includesOptionValue(optionValues, value)) {
|
||||
return defaultMessage(message, '值不在可选项中');
|
||||
return defaultMessage(message, `${value} 不在可选项中`);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@ -279,14 +279,14 @@ const validateCascaderValue = (
|
||||
|
||||
if (valueSeparator) {
|
||||
if (typeof value !== 'string' && !Array.isArray(value)) {
|
||||
return defaultMessage(message, '值类型应为字符串或数组');
|
||||
return defaultMessage(message, `${value} 类型应为字符串或数组`);
|
||||
}
|
||||
} else if (multiple) {
|
||||
if (!Array.isArray(value)) {
|
||||
return defaultMessage(message, '值类型应为数组');
|
||||
return defaultMessage(message, `${value} 类型应为数组`);
|
||||
}
|
||||
} else if (emitPath && !Array.isArray(value)) {
|
||||
return defaultMessage(message, '值类型应为数组');
|
||||
return defaultMessage(message, `${value} 类型应为数组`);
|
||||
}
|
||||
|
||||
if (config.remote) {
|
||||
@ -302,7 +302,7 @@ const validateCascaderValue = (
|
||||
|
||||
if (multiple) {
|
||||
if (!Array.isArray(normalizedValue)) {
|
||||
return defaultMessage(message, '值类型应为数组');
|
||||
return defaultMessage(message, `${value} 类型应为数组`);
|
||||
}
|
||||
|
||||
const invalid = normalizedValue.some((item) => {
|
||||
@ -313,20 +313,20 @@ const validateCascaderValue = (
|
||||
});
|
||||
|
||||
if (invalid) {
|
||||
return defaultMessage(message, '值不在可选项中');
|
||||
return defaultMessage(message, `${value} 不在可选项中`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (emitPath) {
|
||||
if (!Array.isArray(normalizedValue) || !isValidCascaderPath(options, normalizedValue)) {
|
||||
return defaultMessage(message, '值不在可选项中');
|
||||
return defaultMessage(message, `${value} 不在可选项中`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!includesOptionValue(collectCascaderLeafValues(options), normalizedValue)) {
|
||||
return defaultMessage(message, '值不在可选项中');
|
||||
return defaultMessage(message, `${value} 不在可选项中`);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@ -347,7 +347,7 @@ const validateBuiltinTypeMatch = (
|
||||
if (STRING_TYPES.has(fieldType)) {
|
||||
if (config.filter === 'number') {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) {
|
||||
return defaultMessage(message, '值类型应为数字');
|
||||
return defaultMessage(message, `${value} 类型应为数字`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@ -358,7 +358,7 @@ const validateBuiltinTypeMatch = (
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return defaultMessage(message, '值类型应为字符串');
|
||||
return defaultMessage(message, `${value} 类型应为字符串`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@ -373,7 +373,7 @@ const validateBuiltinTypeMatch = (
|
||||
|
||||
if (fieldType === 'number') {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) {
|
||||
return defaultMessage(message, '值类型应为数字');
|
||||
return defaultMessage(message, `${value} 类型应为数字`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@ -384,7 +384,7 @@ const validateBuiltinTypeMatch = (
|
||||
value.length !== 2 ||
|
||||
value.some((item) => typeof item !== 'number' || Number.isNaN(item))
|
||||
) {
|
||||
return defaultMessage(message, '值类型应为长度为 2 的数字数组');
|
||||
return defaultMessage(message, `${value} 类型应为长度为 2 的数字数组`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@ -392,7 +392,7 @@ const validateBuiltinTypeMatch = (
|
||||
if (fieldType === 'switch' || fieldType === 'checkbox') {
|
||||
const { activeValue, inactiveValue } = resolveToggleValues(config);
|
||||
if (!Object.is(value, activeValue) && !Object.is(value, inactiveValue)) {
|
||||
return defaultMessage(message, '值不在合法开关值中');
|
||||
return defaultMessage(message, `${value} 不在合法开关值中`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@ -405,18 +405,18 @@ const validateBuiltinTypeMatch = (
|
||||
if (fieldType === 'radio-group') {
|
||||
const optionValues = flattenSelectOptions(resolveOptions(mForm, props));
|
||||
if (!includesOptionValue(optionValues, value)) {
|
||||
return defaultMessage(message, '值不在可选项中');
|
||||
return defaultMessage(message, `${value} 不在可选项中`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (fieldType === 'checkbox-group') {
|
||||
if (!Array.isArray(value)) {
|
||||
return defaultMessage(message, '值类型应为数组');
|
||||
return defaultMessage(message, `${value} 类型应为数组`);
|
||||
}
|
||||
const optionValues = flattenSelectOptions(resolveOptions(mForm, props));
|
||||
if (value.some((item) => !includesOptionValue(optionValues, item))) {
|
||||
return defaultMessage(message, '值不在可选项中');
|
||||
return defaultMessage(message, `${value} 不在可选项中`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@ -438,8 +438,8 @@ const validateBuiltinTypeMatch = (
|
||||
return defaultMessage(
|
||||
message,
|
||||
isTimestampValueFormat(valueFormat)
|
||||
? '值类型应为长度为 2 的时间戳数字数组'
|
||||
: `值格式应为长度为 2 的 ${valueFormat} 数组`,
|
||||
? `${value} 类型应为长度为 2 的时间戳数字数组`
|
||||
: `${value} 格式应为长度为 2 的 ${valueFormat} 数组`,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
@ -447,7 +447,7 @@ const validateBuiltinTypeMatch = (
|
||||
|
||||
if (fieldType === 'table' || fieldType === 'group-list' || fieldType === 'grouplist') {
|
||||
if (!Array.isArray(value)) {
|
||||
return defaultMessage(message, '值类型应为数组');
|
||||
return defaultMessage(message, `${value} 类型应为数组`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -28,6 +28,8 @@ import {
|
||||
validateTypeMatch,
|
||||
} from '@form/utils/typeMatch';
|
||||
|
||||
import { setDesignConfig } from '@tmagic/design';
|
||||
|
||||
const mForm: FormState = {
|
||||
config: [],
|
||||
initValues: {},
|
||||
@ -74,15 +76,15 @@ describe('validateTypeMatch', () => {
|
||||
|
||||
test('text 期望 string', () => {
|
||||
expect(validateTypeMatch('ok', mForm, propsOf({ type: 'text' }))).toBeUndefined();
|
||||
expect(validateTypeMatch(1, mForm, propsOf({ type: 'text' }))).toBe('值类型应为字符串');
|
||||
expect(validateTypeMatch(1, mForm, propsOf({ type: 'text' }))).toBe('1 类型应为字符串');
|
||||
expect(validateTypeMatch(1, mForm, propsOf({ type: 'text' }), '自定义错误')).toBe('自定义错误');
|
||||
});
|
||||
|
||||
test('text filter=number 时期望 number', () => {
|
||||
expect(validateTypeMatch(1, mForm, propsOf({ type: 'text', filter: 'number' }))).toBeUndefined();
|
||||
expect(validateTypeMatch(0, mForm, propsOf({ type: 'text', filter: 'number' }))).toBeUndefined();
|
||||
expect(validateTypeMatch('1', mForm, propsOf({ type: 'text', filter: 'number' }))).toBe('值类型应为数字');
|
||||
expect(validateTypeMatch(NaN, mForm, propsOf({ type: 'textarea', filter: 'number' }))).toBe('值类型应为数字');
|
||||
expect(validateTypeMatch('1', mForm, propsOf({ type: 'text', filter: 'number' }))).toBe('1 类型应为数字');
|
||||
expect(validateTypeMatch(NaN, mForm, propsOf({ type: 'textarea', filter: 'number' }))).toBe('NaN 类型应为数字');
|
||||
});
|
||||
|
||||
test('text 自定义 filter 函数时跳过内置类型校验', () => {
|
||||
@ -91,8 +93,8 @@ describe('validateTypeMatch', () => {
|
||||
|
||||
test('number 期望 number', () => {
|
||||
expect(validateTypeMatch(1, mForm, propsOf({ type: 'number' }))).toBeUndefined();
|
||||
expect(validateTypeMatch('1', mForm, propsOf({ type: 'number' }))).toBe('值类型应为数字');
|
||||
expect(validateTypeMatch(NaN, mForm, propsOf({ type: 'number' }))).toBe('值类型应为数字');
|
||||
expect(validateTypeMatch('1', mForm, propsOf({ type: 'number' }))).toBe('1 类型应为数字');
|
||||
expect(validateTypeMatch(NaN, mForm, propsOf({ type: 'number' }))).toBe('NaN 类型应为数字');
|
||||
});
|
||||
|
||||
test('date / time / datetime 按 valueFormat(Day.js format)校验', () => {
|
||||
@ -124,20 +126,20 @@ describe('validateTypeMatch', () => {
|
||||
test('switch / checkbox 默认 true/false', () => {
|
||||
expect(validateTypeMatch(true, mForm, propsOf({ type: 'switch' }))).toBeUndefined();
|
||||
expect(validateTypeMatch(false, mForm, propsOf({ type: 'checkbox' }))).toBeUndefined();
|
||||
expect(validateTypeMatch(1, mForm, propsOf({ type: 'switch' }))).toBe('值不在合法开关值中');
|
||||
expect(validateTypeMatch(1, mForm, propsOf({ type: 'switch' }))).toBe('1 不在合法开关值中');
|
||||
});
|
||||
|
||||
test('switch / checkbox filter=number 时为 1/0', () => {
|
||||
expect(validateTypeMatch(1, mForm, propsOf({ type: 'switch', filter: 'number' }))).toBeUndefined();
|
||||
expect(validateTypeMatch(0, mForm, propsOf({ type: 'checkbox', filter: 'number' }))).toBeUndefined();
|
||||
expect(validateTypeMatch(true, mForm, propsOf({ type: 'switch', filter: 'number' }))).toBe('值不在合法开关值中');
|
||||
expect(validateTypeMatch(true, mForm, propsOf({ type: 'switch', filter: 'number' }))).toBe('true 不在合法开关值中');
|
||||
});
|
||||
|
||||
test('switch / checkbox 自定义 activeValue/inactiveValue', () => {
|
||||
const config = { type: 'switch', activeValue: 'on', inactiveValue: 'off' };
|
||||
expect(validateTypeMatch('on', mForm, propsOf(config))).toBeUndefined();
|
||||
expect(validateTypeMatch('off', mForm, propsOf(config))).toBeUndefined();
|
||||
expect(validateTypeMatch('maybe', mForm, propsOf(config))).toBe('值不在合法开关值中');
|
||||
expect(validateTypeMatch('maybe', mForm, propsOf(config))).toBe('maybe 不在合法开关值中');
|
||||
});
|
||||
|
||||
test('select 单选值必须在 options 中', () => {
|
||||
@ -149,7 +151,7 @@ describe('validateTypeMatch', () => {
|
||||
],
|
||||
};
|
||||
expect(validateTypeMatch(1, mForm, propsOf(config))).toBeUndefined();
|
||||
expect(validateTypeMatch(3, mForm, propsOf(config))).toBe('值不在可选项中');
|
||||
expect(validateTypeMatch(3, mForm, propsOf(config))).toBe('3 不在可选项中');
|
||||
});
|
||||
|
||||
test('select multiple 校验数组元素', () => {
|
||||
@ -162,8 +164,8 @@ describe('validateTypeMatch', () => {
|
||||
],
|
||||
};
|
||||
expect(validateTypeMatch(['a'], mForm, propsOf(config))).toBeUndefined();
|
||||
expect(validateTypeMatch(['a', 'c'], mForm, propsOf(config))).toBe('值不在可选项中');
|
||||
expect(validateTypeMatch('a', mForm, propsOf(config))).toBe('值类型应为数组');
|
||||
expect(validateTypeMatch(['a', 'c'], mForm, propsOf(config))).toBe('a,c 不在可选项中');
|
||||
expect(validateTypeMatch('a', mForm, propsOf(config))).toBe('a 类型应为数组');
|
||||
});
|
||||
|
||||
test('select options 为函数 / group', () => {
|
||||
@ -172,7 +174,7 @@ describe('validateTypeMatch', () => {
|
||||
options: () => [{ text: 'A', value: 1 }],
|
||||
};
|
||||
expect(validateTypeMatch(1, mForm, propsOf(fnConfig))).toBeUndefined();
|
||||
expect(validateTypeMatch(2, mForm, propsOf(fnConfig))).toBe('值不在可选项中');
|
||||
expect(validateTypeMatch(2, mForm, propsOf(fnConfig))).toBe('2 不在可选项中');
|
||||
|
||||
const groupConfig = {
|
||||
type: 'select',
|
||||
@ -186,15 +188,17 @@ describe('validateTypeMatch', () => {
|
||||
],
|
||||
};
|
||||
expect(validateTypeMatch('a', mForm, propsOf(groupConfig))).toBeUndefined();
|
||||
expect(validateTypeMatch('b', mForm, propsOf(groupConfig))).toBe('值不在可选项中');
|
||||
expect(validateTypeMatch('b', mForm, propsOf(groupConfig))).toBe('b 不在可选项中');
|
||||
});
|
||||
|
||||
test('select allowCreate / remote 不做枚举', () => {
|
||||
expect(validateTypeMatch('custom', mForm, propsOf({ type: 'select', allowCreate: true }))).toBeUndefined();
|
||||
expect(validateTypeMatch({ a: 1 }, mForm, propsOf({ type: 'select', allowCreate: true }))).toBe('值类型不合法');
|
||||
expect(validateTypeMatch({ a: 1 }, mForm, propsOf({ type: 'select', allowCreate: true }))).toBe(
|
||||
'[object Object] 类型不合法',
|
||||
);
|
||||
expect(validateTypeMatch(['x'], mForm, propsOf({ type: 'select', multiple: true, remote: true }))).toBeUndefined();
|
||||
expect(validateTypeMatch('x', mForm, propsOf({ type: 'select', multiple: true, remote: true }))).toBe(
|
||||
'值类型应为数组',
|
||||
'x 类型应为数组',
|
||||
);
|
||||
});
|
||||
|
||||
@ -207,7 +211,7 @@ describe('validateTypeMatch', () => {
|
||||
],
|
||||
};
|
||||
expect(validateTypeMatch(1, mForm, propsOf(radio))).toBeUndefined();
|
||||
expect(validateTypeMatch(3, mForm, propsOf(radio))).toBe('值不在可选项中');
|
||||
expect(validateTypeMatch(3, mForm, propsOf(radio))).toBe('3 不在可选项中');
|
||||
|
||||
const checkboxGroup = {
|
||||
type: 'checkbox-group',
|
||||
@ -217,7 +221,7 @@ describe('validateTypeMatch', () => {
|
||||
],
|
||||
};
|
||||
expect(validateTypeMatch(['a', 'b'], mForm, propsOf(checkboxGroup))).toBeUndefined();
|
||||
expect(validateTypeMatch(['c'], mForm, propsOf(checkboxGroup))).toBe('值不在可选项中');
|
||||
expect(validateTypeMatch(['c'], mForm, propsOf(checkboxGroup))).toBe('c 不在可选项中');
|
||||
});
|
||||
|
||||
test('cascader 静态路径与 valueSeparator', () => {
|
||||
@ -236,33 +240,33 @@ describe('validateTypeMatch', () => {
|
||||
|
||||
expect(validateTypeMatch(['zhejiang', 'hangzhou'], mForm, propsOf({ type: 'cascader', options }))).toBeUndefined();
|
||||
expect(validateTypeMatch(['zhejiang', 'ningbo'], mForm, propsOf({ type: 'cascader', options }))).toBe(
|
||||
'值不在可选项中',
|
||||
'zhejiang,ningbo 不在可选项中',
|
||||
);
|
||||
expect(
|
||||
validateTypeMatch('zhejiang/hangzhou', mForm, propsOf({ type: 'cascader', options, valueSeparator: '/' })),
|
||||
).toBeUndefined();
|
||||
expect(validateTypeMatch('bad', mForm, propsOf({ type: 'cascader', remote: true }))).toBe('值类型应为数组');
|
||||
expect(validateTypeMatch('bad', mForm, propsOf({ type: 'cascader', remote: true }))).toBe('bad 类型应为数组');
|
||||
expect(validateTypeMatch(['a'], mForm, propsOf({ type: 'cascader', remote: true }))).toBeUndefined();
|
||||
});
|
||||
|
||||
test('number-range / daterange / table', () => {
|
||||
expect(validateTypeMatch([1, 2], mForm, propsOf({ type: 'number-range' }))).toBeUndefined();
|
||||
expect(validateTypeMatch([1], mForm, propsOf({ type: 'number-range' }))).toBe('值类型应为长度为 2 的数字数组');
|
||||
expect(validateTypeMatch([1], mForm, propsOf({ type: 'number-range' }))).toBe('1 类型应为长度为 2 的数字数组');
|
||||
|
||||
expect(
|
||||
validateTypeMatch(['2020/01/01 00:00:00', '2020/01/02 00:00:00'], mForm, propsOf({ type: 'daterange' })),
|
||||
).toBeUndefined();
|
||||
expect(validateTypeMatch(['2020-01-01', '2020-01-02'], mForm, propsOf({ type: 'daterange' }))).toBe(
|
||||
'值格式应为长度为 2 的 YYYY/MM/DD HH:mm:ss 数组',
|
||||
'2020-01-01,2020-01-02 格式应为长度为 2 的 YYYY/MM/DD HH:mm:ss 数组',
|
||||
);
|
||||
expect(validateTypeMatch(['a'], mForm, propsOf({ type: 'daterange' }))).toBe(
|
||||
'值格式应为长度为 2 的 YYYY/MM/DD HH:mm:ss 数组',
|
||||
'a 格式应为长度为 2 的 YYYY/MM/DD HH:mm:ss 数组',
|
||||
);
|
||||
expect(validateTypeMatch([1, 2], mForm, propsOf({ type: 'daterange', valueFormat: 'timestamp' }))).toBeUndefined();
|
||||
expect(validateTypeMatch('x', mForm, propsOf({ type: 'daterange', names: ['a', 'b'] }))).toBeUndefined();
|
||||
|
||||
expect(validateTypeMatch([{ id: 1 }], mForm, propsOf({ type: 'table' }))).toBeUndefined();
|
||||
expect(validateTypeMatch({}, mForm, propsOf({ type: 'groupList' }))).toBe('值类型应为数组');
|
||||
expect(validateTypeMatch({}, mForm, propsOf({ type: 'groupList' }))).toBe('[object Object] 类型应为数组');
|
||||
});
|
||||
|
||||
test('容器类字段 no-op', () => {
|
||||
@ -272,25 +276,25 @@ describe('validateTypeMatch', () => {
|
||||
|
||||
test('无 type 默认按 text 校验', () => {
|
||||
expect(validateTypeMatch('ok', mForm, propsOf({}))).toBeUndefined();
|
||||
expect(validateTypeMatch(1, mForm, propsOf({}))).toBe('值类型应为字符串');
|
||||
expect(validateTypeMatch(1, mForm, propsOf({}))).toBe('1 类型应为字符串');
|
||||
});
|
||||
|
||||
test('textarea / color-picker / html 期望 string', () => {
|
||||
expect(validateTypeMatch('ok', mForm, propsOf({ type: 'textarea' }))).toBeUndefined();
|
||||
expect(validateTypeMatch('ok', mForm, propsOf({ type: 'color-picker' }))).toBeUndefined();
|
||||
expect(validateTypeMatch(1, mForm, propsOf({ type: 'html' }))).toBe('值类型应为字符串');
|
||||
expect(validateTypeMatch(1, mForm, propsOf({ type: 'html' }))).toBe('1 类型应为字符串');
|
||||
});
|
||||
|
||||
test('checkbox-group 非数组', () => {
|
||||
expect(
|
||||
validateTypeMatch('a', mForm, propsOf({ type: 'checkbox-group', options: [{ text: 'A', value: 'a' }] })),
|
||||
).toBe('值类型应为数组');
|
||||
).toBe('a 类型应为数组');
|
||||
});
|
||||
|
||||
test('timerange 按 valueFormat 校验', () => {
|
||||
expect(validateTypeMatch(['12:00:00', '13:00:00'], mForm, propsOf({ type: 'timerange' }))).toBeUndefined();
|
||||
expect(validateTypeMatch(['bad'], mForm, propsOf({ type: 'timerange' }))).toBe(
|
||||
'值格式应为长度为 2 的 HH:mm:ss 数组',
|
||||
'bad 格式应为长度为 2 的 HH:mm:ss 数组',
|
||||
);
|
||||
});
|
||||
|
||||
@ -306,7 +310,7 @@ describe('validateTypeMatch', () => {
|
||||
validateTypeMatch('hangzhou', mForm, propsOf({ type: 'cascader', options, emitPath: false })),
|
||||
).toBeUndefined();
|
||||
expect(validateTypeMatch('ningbo', mForm, propsOf({ type: 'cascader', options, emitPath: false }))).toBe(
|
||||
'值不在可选项中',
|
||||
'ningbo 不在可选项中',
|
||||
);
|
||||
});
|
||||
|
||||
@ -323,7 +327,7 @@ describe('validateTypeMatch', () => {
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
validateTypeMatch(['ningbo'], mForm, propsOf({ type: 'cascader', options, multiple: true, emitPath: false })),
|
||||
).toBe('值不在可选项中');
|
||||
).toBe('ningbo 不在可选项中');
|
||||
});
|
||||
|
||||
test('cascader valueSeparator 时数组值', () => {
|
||||
@ -340,7 +344,9 @@ describe('validateTypeMatch', () => {
|
||||
});
|
||||
|
||||
test('select allowCreate 对象值非法', () => {
|
||||
expect(validateTypeMatch({ a: 1 }, mForm, propsOf({ type: 'select', allowCreate: true }))).toBe('值类型不合法');
|
||||
expect(validateTypeMatch({ a: 1 }, mForm, propsOf({ type: 'select', allowCreate: true }))).toBe(
|
||||
'[object Object] 类型不合法',
|
||||
);
|
||||
});
|
||||
|
||||
test('动态 type 函数解析', () => {
|
||||
@ -394,6 +400,50 @@ describe('getRules typeMatch', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRules tdesign validator', () => {
|
||||
beforeEach(() => {
|
||||
setDesignConfig({ adapterType: 'tdesign-vue-next' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setDesignConfig({});
|
||||
});
|
||||
|
||||
test('typeMatch 校验失败时返回 CustomValidateObj', async () => {
|
||||
const rules: any = [{ typeMatch: true, message: '类型错误' }];
|
||||
const newRules: any = getRules(mForm, rules, propsOf({ type: 'text' }));
|
||||
// TDesign 调用签名:validator(val)
|
||||
await expect(newRules[0].validator(123)).resolves.toEqual({
|
||||
result: false,
|
||||
message: '类型错误',
|
||||
});
|
||||
});
|
||||
|
||||
test('typeMatch 校验通过时返回 true', async () => {
|
||||
const rules: any = [{ typeMatch: true }];
|
||||
const newRules: any = getRules(mForm, rules, propsOf({ type: 'text' }));
|
||||
await expect(newRules[0].validator('ok')).resolves.toBe(true);
|
||||
});
|
||||
|
||||
test('自定义 validator 通过 callback 报告错误', async () => {
|
||||
const rules: any = {
|
||||
validator: ({ value, callback }: any) => {
|
||||
if (value < 0) {
|
||||
callback(new Error('不能为负'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
};
|
||||
const newRules: any = getRules(mForm, rules, { config: {} });
|
||||
await expect(newRules[0].validator(-1)).resolves.toEqual({
|
||||
result: false,
|
||||
message: '不能为负',
|
||||
});
|
||||
await expect(newRules[0].validator(1)).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('typeMatch 扩展注册', () => {
|
||||
beforeEach(() => {
|
||||
clearTypeMatchRules();
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
"@tmagic/element-plus-adapter": "1.8.0-beta.11",
|
||||
"@tmagic/tdesign-vue-next-adapter": "1.8.0-beta.11",
|
||||
"@tmagic/tmagic-form-runtime": "1.1.3",
|
||||
"element-plus": "^2.11.8",
|
||||
"element-plus": "^2.14.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"serialize-javascript": "^7.0.0",
|
||||
|
||||
112
pnpm-lock.yaml
generated
112
pnpm-lock.yaml
generated
@ -60,8 +60,8 @@ importers:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0(@types/node@24.0.10)(typescript@6.0.3)
|
||||
element-plus:
|
||||
specifier: ^2.11.8
|
||||
version: 2.11.8(@vue/composition-api@1.7.2(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3))
|
||||
specifier: ^2.14.3
|
||||
version: 2.14.3(vue@3.5.38(typescript@6.0.3))
|
||||
enquirer:
|
||||
specifier: ^2.4.1
|
||||
version: 2.4.1
|
||||
@ -573,16 +573,16 @@ importers:
|
||||
version: 1.8.0-beta.11(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.11(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.11(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
|
||||
'@tmagic/element-plus-adapter':
|
||||
specifier: 1.8.0-beta.11
|
||||
version: 1.8.0-beta.11(@tmagic/design@1.8.0-beta.11(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3)))(element-plus@2.11.8(@vue/composition-api@1.7.2(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
|
||||
version: 1.8.0-beta.11(@tmagic/design@1.8.0-beta.11(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3)))(element-plus@2.14.3(vue@3.5.38(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
|
||||
'@tmagic/tdesign-vue-next-adapter':
|
||||
specifier: 1.8.0-beta.11
|
||||
version: 1.8.0-beta.11(@tmagic/design@1.8.0-beta.11(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3)))(tdesign-vue-next@1.17.3(vue@3.5.38(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
|
||||
'@tmagic/tmagic-form-runtime':
|
||||
specifier: 1.1.3
|
||||
version: 1.1.3(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(@tmagic/editor@1.8.0-beta.11(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.11(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.11(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3)))(element-plus@2.11.8(@vue/composition-api@1.7.2(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
|
||||
version: 1.1.3(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(@tmagic/editor@1.8.0-beta.11(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.11(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.11(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3)))(element-plus@2.14.3(vue@3.5.38(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
|
||||
element-plus:
|
||||
specifier: ^2.11.8
|
||||
version: 2.11.8(@vue/composition-api@1.7.2(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3))
|
||||
specifier: ^2.14.3
|
||||
version: 2.14.3(vue@3.5.38(typescript@6.0.3))
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
@ -923,7 +923,7 @@ importers:
|
||||
version: 1.8.0-beta.11(typescript@6.0.3)
|
||||
'@tmagic/react-runtime-help':
|
||||
specifier: 0.2.2
|
||||
version: 0.2.2(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.11(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(typescript@6.0.3))(lodash-es@4.17.21)(react@18.3.1)(typescript@6.0.3)
|
||||
version: 0.2.2(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.11(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(typescript@6.0.3))(lodash-es@4.18.1)(react@18.3.1)(typescript@6.0.3)
|
||||
'@tmagic/stage':
|
||||
specifier: 1.8.0-beta.11
|
||||
version: 1.8.0-beta.11(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(typescript@6.0.3)
|
||||
@ -2048,6 +2048,10 @@ packages:
|
||||
resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@ctrl/tinycolor@4.2.0':
|
||||
resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@daybrush/utils@1.13.0':
|
||||
resolution: {integrity: sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ==}
|
||||
|
||||
@ -2457,12 +2461,21 @@ packages:
|
||||
'@floating-ui/core@1.7.3':
|
||||
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
|
||||
|
||||
'@floating-ui/core@1.8.0':
|
||||
resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==}
|
||||
|
||||
'@floating-ui/dom@1.7.4':
|
||||
resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
|
||||
|
||||
'@floating-ui/dom@1.8.0':
|
||||
resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==}
|
||||
|
||||
'@floating-ui/utils@0.2.10':
|
||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
||||
|
||||
'@floating-ui/utils@0.2.12':
|
||||
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
|
||||
|
||||
'@humanfs/core@0.19.1':
|
||||
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
@ -2922,6 +2935,9 @@ packages:
|
||||
'@sxzz/popperjs-es@2.11.7':
|
||||
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
|
||||
|
||||
'@sxzz/popperjs-es@2.11.8':
|
||||
resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==}
|
||||
|
||||
'@tmagic/cli@1.8.0-beta.11':
|
||||
resolution: {integrity: sha512-j0yKe2+wVlSeSo/VuieaYh+WDQVEx2U5XA3FfGwusllgvInLB0DUNRcPyhdEtwckl4bDI7p+vvcmlMyVDeYXcg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -3348,6 +3364,9 @@ packages:
|
||||
'@types/lodash@4.17.20':
|
||||
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
|
||||
|
||||
'@types/lodash@4.17.24':
|
||||
resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==}
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
|
||||
|
||||
@ -4326,6 +4345,9 @@ packages:
|
||||
dayjs@1.11.19:
|
||||
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
|
||||
|
||||
dayjs@1.11.21:
|
||||
resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
|
||||
|
||||
debug@3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
@ -4447,6 +4469,11 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.2.0
|
||||
|
||||
element-plus@2.14.3:
|
||||
resolution: {integrity: sha512-pJcvxcpZjYruNzuJhAeVwnbYjfNgzBKnWHwSVEhwzM2/kcLI3brzmtIBxtPqd4hQWJfD1PRnjoc1WipLw2eBGg==}
|
||||
peerDependencies:
|
||||
vue: ^3.3.7
|
||||
|
||||
emmet-monaco-es@5.7.0:
|
||||
resolution: {integrity: sha512-q8jtgsEWWaXHnAJ6HAcwMB7WHraRtf032TWpeOpN6SGLtwxr7hd6RgF3Kbj2rPxb6Q19fYVVafAFODYRYUS8NQ==}
|
||||
peerDependencies:
|
||||
@ -5455,6 +5482,9 @@ packages:
|
||||
lodash-es@4.17.21:
|
||||
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||
|
||||
lodash-es@4.18.1:
|
||||
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
|
||||
|
||||
lodash-unified@1.0.3:
|
||||
resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}
|
||||
peerDependencies:
|
||||
@ -5498,6 +5528,9 @@ packages:
|
||||
lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
|
||||
lodash@4.18.1:
|
||||
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
||||
|
||||
log-symbols@4.1.0:
|
||||
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
|
||||
engines: {node: '>=10'}
|
||||
@ -6902,6 +6935,9 @@ packages:
|
||||
vue-component-type-helpers@2.2.12:
|
||||
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
|
||||
|
||||
vue-component-type-helpers@3.3.7:
|
||||
resolution: {integrity: sha512-Skkhw9agYSgsWqv7bxSOGJZa9SaiJbZVGdXuFWnrzKaQYHnw9qbjD630rw6RyMqDbp54nfLCLw5SZA55if7JLg==}
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
engines: {node: '>=12'}
|
||||
@ -8113,6 +8149,8 @@ snapshots:
|
||||
|
||||
'@ctrl/tinycolor@3.6.1': {}
|
||||
|
||||
'@ctrl/tinycolor@4.2.0': {}
|
||||
|
||||
'@daybrush/utils@1.13.0': {}
|
||||
|
||||
'@docsearch/css@4.6.3': {}
|
||||
@ -8379,13 +8417,24 @@ snapshots:
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/core@1.8.0':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.12
|
||||
|
||||
'@floating-ui/dom@1.7.4':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.3
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/dom@1.8.0':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.8.0
|
||||
'@floating-ui/utils': 0.2.12
|
||||
|
||||
'@floating-ui/utils@0.2.10': {}
|
||||
|
||||
'@floating-ui/utils@0.2.12': {}
|
||||
|
||||
'@humanfs/core@0.19.1': {}
|
||||
|
||||
'@humanfs/node@0.16.7':
|
||||
@ -8743,6 +8792,8 @@ snapshots:
|
||||
|
||||
'@sxzz/popperjs-es@2.11.7': {}
|
||||
|
||||
'@sxzz/popperjs-es@2.11.8': {}
|
||||
|
||||
'@tmagic/cli@1.8.0-beta.11(typescript@6.0.3)':
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
@ -8906,10 +8957,10 @@ snapshots:
|
||||
- '@tmagic/form-schema'
|
||||
- '@tmagic/schema'
|
||||
|
||||
'@tmagic/element-plus-adapter@1.8.0-beta.11(@tmagic/design@1.8.0-beta.11(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3)))(element-plus@2.11.8(@vue/composition-api@1.7.2(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))':
|
||||
'@tmagic/element-plus-adapter@1.8.0-beta.11(@tmagic/design@1.8.0-beta.11(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3)))(element-plus@2.14.3(vue@3.5.38(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))':
|
||||
dependencies:
|
||||
'@tmagic/design': 1.8.0-beta.11(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
|
||||
element-plus: 2.11.8(@vue/composition-api@1.7.2(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3))
|
||||
element-plus: 2.14.3(vue@3.5.38(typescript@6.0.3))
|
||||
vue: 3.5.38(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
@ -8954,9 +9005,9 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/react-runtime-help@0.2.2(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.11(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(typescript@6.0.3))(lodash-es@4.17.21)(react@18.3.1)(typescript@6.0.3)':
|
||||
'@tmagic/react-runtime-help@0.2.2(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.11(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(typescript@6.0.3))(lodash-es@4.18.1)(react@18.3.1)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
lodash-es: 4.17.21
|
||||
lodash-es: 4.18.1
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
'@tmagic/core': 1.8.0-beta.11(typescript@6.0.3)
|
||||
@ -9042,10 +9093,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/tmagic-form-runtime@1.1.3(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(@tmagic/editor@1.8.0-beta.11(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.11(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.11(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3)))(element-plus@2.11.8(@vue/composition-api@1.7.2(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))':
|
||||
'@tmagic/tmagic-form-runtime@1.1.3(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(@tmagic/editor@1.8.0-beta.11(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.11(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.11(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3)))(element-plus@2.14.3(vue@3.5.38(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))':
|
||||
dependencies:
|
||||
'@tmagic/editor': 1.8.0-beta.11(@tmagic/core@1.8.0-beta.11(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.11(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.11(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
|
||||
element-plus: 2.11.8(@vue/composition-api@1.7.2(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3))
|
||||
element-plus: 2.14.3(vue@3.5.38(typescript@6.0.3))
|
||||
vue: 3.5.38(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
'@tmagic/core': 1.8.0-beta.11(typescript@6.0.3)
|
||||
@ -9131,6 +9182,8 @@ snapshots:
|
||||
|
||||
'@types/lodash@4.17.20': {}
|
||||
|
||||
'@types/lodash@4.17.24': {}
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
dependencies:
|
||||
'@types/linkify-it': 5.0.0
|
||||
@ -10369,6 +10422,8 @@ snapshots:
|
||||
|
||||
dayjs@1.11.19: {}
|
||||
|
||||
dayjs@1.11.21: {}
|
||||
|
||||
debug@3.2.7:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@ -10477,6 +10532,25 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
||||
element-plus@2.14.3(vue@3.5.38(typescript@6.0.3)):
|
||||
dependencies:
|
||||
'@ctrl/tinycolor': 4.2.0
|
||||
'@element-plus/icons-vue': 2.3.2(vue@3.5.38(typescript@6.0.3))
|
||||
'@floating-ui/dom': 1.8.0
|
||||
'@popperjs/core': '@sxzz/popperjs-es@2.11.8'
|
||||
'@types/lodash': 4.17.24
|
||||
'@types/lodash-es': 4.17.12
|
||||
'@vueuse/core': 14.3.0(vue@3.5.38(typescript@6.0.3))
|
||||
async-validator: 4.2.5
|
||||
dayjs: 1.11.21
|
||||
lodash: 4.18.1
|
||||
lodash-es: 4.18.1
|
||||
lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1)
|
||||
memoize-one: 6.0.0
|
||||
normalize-wheel-es: 1.2.0
|
||||
vue: 3.5.38(typescript@6.0.3)
|
||||
vue-component-type-helpers: 3.3.7
|
||||
|
||||
emmet-monaco-es@5.7.0(monaco-editor@0.55.1):
|
||||
dependencies:
|
||||
emmet: 2.4.11
|
||||
@ -11658,12 +11732,20 @@ snapshots:
|
||||
|
||||
lodash-es@4.17.21: {}
|
||||
|
||||
lodash-es@4.18.1: {}
|
||||
|
||||
lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21):
|
||||
dependencies:
|
||||
'@types/lodash-es': 4.17.12
|
||||
lodash: 4.17.21
|
||||
lodash-es: 4.17.21
|
||||
|
||||
lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1):
|
||||
dependencies:
|
||||
'@types/lodash-es': 4.17.12
|
||||
lodash: 4.18.1
|
||||
lodash-es: 4.18.1
|
||||
|
||||
lodash.camelcase@4.3.0: {}
|
||||
|
||||
lodash.debounce@4.0.8: {}
|
||||
@ -11688,6 +11770,8 @@ snapshots:
|
||||
|
||||
lodash@4.17.21: {}
|
||||
|
||||
lodash@4.18.1: {}
|
||||
|
||||
log-symbols@4.1.0:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
@ -13140,6 +13224,8 @@ snapshots:
|
||||
|
||||
vue-component-type-helpers@2.2.12: {}
|
||||
|
||||
vue-component-type-helpers@3.3.7: {}
|
||||
|
||||
vue-demi@0.14.10(@vue/composition-api@1.7.2(vue@3.5.38(typescript@6.0.3)))(vue@3.5.38(typescript@6.0.3)):
|
||||
dependencies:
|
||||
vue: 3.5.38(typescript@6.0.3)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user