tmagic-editor/packages/editor/src/fields/headless-validation.ts
roymondchen f2ee7ae7b5 feat(form): 统一字段值 effect 机制并将 nested 重命名为 innerConfig
将字段挂载时的 model 写入迁移至 registerField effect,
渲染与无渲染校验共用 applyMountValueEffects;
nested 重命名为 innerConfig 以区分配置派生与值写入。
2026-09-01 12:05:28 +08:00

178 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 DisplayCondsConfig,
type EventSelectConfig,
type FieldInnerConfig,
type FieldMountValueEffect,
filterFunction,
type FormItemConfig,
type HeadlessFieldOptions,
} from '@tmagic/form/headless';
import type { StyleSchema } from '@tmagic/schema';
import { editorTypeMatchRules, validateDataSourceFieldSelect } from '@editor/utils/type-match-rules';
import { createCodeSelectConfig, normalizeCodeSelectValue } from './configs/codeSelect';
import { createDisplayCondsConfig } from './configs/displayConds';
import { createEventSelectConfig, isLegacyEventValue } from './configs/eventSelect';
import { createStyleSetterConfig } from './StyleSetter/configs';
const getName = (config: FormItemConfig): string => `${(config as any).name ?? ''}`;
/** `code-select` 旧数据兼容:空值改写成 `{ hookType, hookData }`。 */
const codeSelectEffect: FieldMountValueEffect = ({ config, model }) => {
normalizeCodeSelectValue(model, getName(config));
};
/** `event-select`:表单 init 对未声明 defaultValue 的自定义 type 会写成空串,统一收成数组。 */
const eventSelectEffect: FieldMountValueEffect = ({ config, model }) => {
const name = getName(config);
if (model && !Array.isArray(model[name])) {
model[name] = [];
}
};
/**
* `code-select` 的内部配置。
*
* 对应 `fields/CodeSelect.vue` 内部
* `<MContainer :config="codeConfig" :model="model[name]" :prop="prop">`。
*
* @param ctx - innerConfig 回调入参
* @returns 内部容器的 config / model / prop
*/
const codeSelectInnerConfig: FieldInnerConfig = ({ config, model, prop }) => {
const name = getName(config);
return {
config: createCodeSelectConfig(config as any),
model: model?.[name],
prop,
};
};
/**
* `display-conds` 的内部配置。
*
* 对应 `fields/DisplayConds.vue` 内部把同一份 groupList 配置交给 `MGroupList`。
*
* @param ctx - innerConfig 回调入参
* @returns 内部 group-list 配置prop 基准为 parentProp避免 name 被拼两次
*/
const displayCondsInnerConfig: FieldInnerConfig = ({ config, model, prop, parentProp, mForm }) => {
const name = getName(config);
const parentFields =
filterFunction<string[]>(mForm, (config as DisplayCondsConfig).parentFields, {
model,
config,
prop,
}) || [];
return {
config: createDisplayCondsConfig(config as DisplayCondsConfig, name, parentFields),
// 内部 group-list 复用了字段自身的 nameprop 基准要退回父级,否则 name 会被拼两次
prop: parentProp,
};
};
/**
* `event-select` 的内部配置。
*
* 对应 `fields/EventSelect.vue`:列表走 group-list事件名渲染在 title slot
* 无渲染校验仍把事件名当作列表项字段,路径为 `<prop>.<index>.name`。
*
* @param ctx - innerConfig 回调入参
* @returns 合成的 group-list 配置;旧数据格式返回 null不参与校验
*/
const eventSelectInnerConfig: FieldInnerConfig = ({ config, model, parentProp }) => {
const name = getName(config);
const events = model?.[name];
// 旧数据格式走的是另一套表格配置,其中不含任何 rules不参与校验
if (!Array.isArray(events) || isLegacyEventValue(events)) return null;
return {
config: createEventSelectConfig(config as EventSelectConfig, name, { includeEventName: true }),
prop: parentProp,
};
};
/**
* `style-setter` 的内部配置。
*
* 对应 `fields/StyleSetter/Index.vue`6 个面板共用 `:values="model[name]"`、`:prop="prop || name"`。
* `theme` 按 `useTheme` 缺省空串传入,只改 flexWrap 的 UI childType不影响校验字段。
*
* @param ctx - innerConfig 回调入参
* @returns style 面板配置及 styleModel
*/
const styleSetterInnerConfig: FieldInnerConfig = ({ config, model, prop }) => {
const name = getName(config);
const styleModel = (model?.[name] ?? {}) as Partial<StyleSchema>;
return {
config: createStyleSetterConfig(styleModel, '', validateDataSourceFieldSelect),
model: styleModel,
prop,
};
};
/**
* 编辑器字段的无渲染登记表(不含 Vue 组件)。
*
* Node 里 `registerFields(editorFields)` 即可配合 `validateForm` / `submitForm`。
* 安装编辑器时由 plugin 把 `component` 叠上去再传给 `@tmagic/form`。
*
* - 叶子:内部只渲染叶子 UI或把子表单渲染在独立的 MForm / MFormBox 实例里
* - innerConfig内部再渲染 MContainer / MPanel / MGroupList把运行期配置交出来
* - typeMatch该 type 自身的类型匹配校验
*
* innerConfig 返回的 config / model / prop 与组件模板里传给内部容器的那一组保持一一对应,
* 且配置本身与组件共用同一份工厂(`fields/configs/`)。
*/
export const editorFields: Record<string, HeadlessFieldOptions> = {
'vs-code': {},
'code-link': {},
'ui-select': { typeMatch: editorTypeMatchRules['ui-select'] },
'cond-op-select': { typeMatch: editorTypeMatchRules['cond-op-select'] },
'page-fragment-select': { typeMatch: editorTypeMatchRules['page-fragment-select'] },
'data-source-select': { typeMatch: editorTypeMatchRules['data-source-select'] },
'data-source-input': { typeMatch: editorTypeMatchRules['data-source-input'] },
'key-value': { typeMatch: editorTypeMatchRules['key-value'] },
'code-select-col': { typeMatch: editorTypeMatchRules['code-select-col'] },
'data-source-fields': { typeMatch: editorTypeMatchRules['data-source-fields'] },
'data-source-mocks': { typeMatch: editorTypeMatchRules['data-source-mocks'] },
'data-source-methods': { typeMatch: editorTypeMatchRules['data-source-methods'] },
'data-source-method-select': { typeMatch: editorTypeMatchRules['data-source-method-select'] },
'data-source-field-select': { typeMatch: editorTypeMatchRules['data-source-field-select'] },
'code-select': {
effect: codeSelectEffect,
innerConfig: codeSelectInnerConfig,
typeMatch: editorTypeMatchRules['code-select'],
},
'display-conds': { innerConfig: displayCondsInnerConfig, typeMatch: editorTypeMatchRules['display-conds'] },
'event-select': {
effect: eventSelectEffect,
innerConfig: eventSelectInnerConfig,
typeMatch: editorTypeMatchRules['event-select'],
},
'style-setter': { innerConfig: styleSetterInnerConfig, typeMatch: editorTypeMatchRules['style-setter'] },
};