mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-09-25 13:26:22 +00:00
feat(editor): enablePropsFormValidate 改为响应式注入并接管 typeMatchValid / validateOnInit
ENABLE_PROPS_FORM_VALIDATE 原先 provide 的是布尔快照,父组件动态切换 enablePropsFormValidate 后子孙组件拿不到新值。改为与 isLargeStageContainer 一致用 computed 注入,注入键类型变为 InjectionKey<ComputedRef<boolean>>, PropsPanel / FormPanel 侧统一按 .value 读取。 同时 FormPanel 内 MForm 的 type-match-valid / validate-on-init 由硬编码 true 改为跟随 enablePropsFormValidate,未开启时不再做 typeMatch 校验与 初始化校验。
This commit is contained in:
parent
80197be543
commit
a100462a52
@ -1311,6 +1311,8 @@ const guidesOptions = {
|
||||
|
||||
关闭时保持原行为:属性 / 样式表单校验失败则丢弃本次改动,不写入节点。
|
||||
|
||||
该开关同时决定属性 / 样式表单内部 `MForm` 的 `typeMatchValid`(按字段 type 校验值形态)与 `validateOnInit`(初始化后立即校验一次):开启时才一并开启,关闭时两者均为 `false`。
|
||||
|
||||
:::tip
|
||||
校验错误以「来源」为维度分别记录 —— 属性表单来源记为 `props`,样式表单来源记为 `style`;两者指向同一节点,互不覆盖。节点只要任一来源存在错误即视为出错。
|
||||
|
||||
|
||||
@ -10,6 +10,8 @@
|
||||
|
||||
> 该能力默认关闭,需通过 `<m-editor :enable-props-form-validate="true">` 显式开启。
|
||||
|
||||
开启后,属性 / 样式表单内部 `MForm` 的 `typeMatchValid`(按字段 type 校验值形态)与 `validateOnInit`(初始化后立即校验一次)也会一并开启;关闭时两者同样关闭。开关可动态变化,表单会同步生效。
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 错误来源维度
|
||||
|
||||
@ -236,7 +236,12 @@ provide('services', services);
|
||||
provide('codeOptions', props.codeOptions);
|
||||
provide('stageOptions', stageOptions);
|
||||
/** 是否启用「属性配置表单校验」联动能力,供 PropsPanel / FormPanel 判断校验失败时是否仍更新节点并记录错误 */
|
||||
provide(ENABLE_PROPS_FORM_VALIDATE, props.enablePropsFormValidate ?? false);
|
||||
// 同 isLargeStageContainer:用 computed 包一层,否则传下去的是 provide 那一刻的值快照,
|
||||
// props.enablePropsFormValidate 后续变化不会同步到子孙。
|
||||
provide(
|
||||
ENABLE_PROPS_FORM_VALIDATE,
|
||||
computed(() => props.enablePropsFormValidate ?? false),
|
||||
);
|
||||
/**
|
||||
* 编辑器注入给整棵表单树的业务上下文(`services` / `stage`),属性面板、对比表单、
|
||||
* 侧边栏里的嵌套 MForm 都通过 inject 自动继承。
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { InjectionKey } from 'vue';
|
||||
import type { ComputedRef, InjectionKey } from 'vue';
|
||||
|
||||
import type { DataSourceSchema, EventOption, Id, MApp, MNode, MPage, MPageFragment } from '@tmagic/core';
|
||||
import type { FormConfig } from '@tmagic/form';
|
||||
@ -31,8 +31,11 @@ import type {
|
||||
/**
|
||||
* 「属性配置表单校验」联动能力的 provide/inject 注入键。
|
||||
* 使用 Symbol 避免与其它字符串键冲突,供 PropsPanel / FormPanel 注入判断校验失败时是否仍更新节点并记录错误。
|
||||
*
|
||||
* 注入值是 ComputedRef:Editor 用 `computed(() => props.enablePropsFormValidate ?? false)` 提供,
|
||||
* 保证外部动态切换该 prop 时子孙组件能同步拿到最新值;子孙侧用 `.value` 读取。
|
||||
*/
|
||||
export const ENABLE_PROPS_FORM_VALIDATE: InjectionKey<boolean> = Symbol('enablePropsFormValidate');
|
||||
export const ENABLE_PROPS_FORM_VALIDATE: InjectionKey<ComputedRef<boolean>> = Symbol('enablePropsFormValidate');
|
||||
|
||||
export interface EditorProps {
|
||||
/** 是否是大屏模拟器容器,大屏容器时,左侧属性面板会扩展为两列(正常3列),颜色表单不扩展两列等 */
|
||||
|
||||
@ -12,9 +12,9 @@
|
||||
:size="propsPanelSize"
|
||||
:init-values="values"
|
||||
:config="config"
|
||||
:type-match-valid="true"
|
||||
:type-match-valid="enablePropsFormValidate"
|
||||
:context="formContext"
|
||||
:validate-on-init="true"
|
||||
:validate-on-init="enablePropsFormValidate"
|
||||
@change="submit"
|
||||
@error="errorHandler"
|
||||
></MForm>
|
||||
@ -86,7 +86,10 @@ const emit = defineEmits<{
|
||||
unmounted: [];
|
||||
}>();
|
||||
|
||||
const enablePropsFormValidate = inject(ENABLE_PROPS_FORM_VALIDATE, false);
|
||||
const enablePropsFormValidate = inject(
|
||||
ENABLE_PROPS_FORM_VALIDATE,
|
||||
computed(() => false),
|
||||
);
|
||||
|
||||
const services = useServices();
|
||||
const { uiService } = services;
|
||||
@ -116,7 +119,7 @@ const submit = async (v: FormValue, eventData: ContainerChangeEventData) => {
|
||||
// 校验成功:正常更新节点(第三个参数 error 为空,表示清除该来源的错误记录)
|
||||
emit('submit', values, eventData);
|
||||
} catch (e: any) {
|
||||
if (enablePropsFormValidate) {
|
||||
if (enablePropsFormValidate.value) {
|
||||
// 启用校验联动:校验失败时仍以当前表单值更新节点,并把错误信息一并抛给上层记录
|
||||
emit('submit', v, eventData, e);
|
||||
} else {
|
||||
@ -146,7 +149,7 @@ const formatValidateErrorHtml = (error: string): string =>
|
||||
const saveCode = async (values: any) => {
|
||||
const newValues = props.codeValueKey ? { [props.codeValueKey]: values } : values;
|
||||
|
||||
if (!enablePropsFormValidate) {
|
||||
if (!enablePropsFormValidate.value) {
|
||||
// 未启用校验联动:保持原行为,直接提交源码保存的值
|
||||
emit('submit', newValues);
|
||||
return;
|
||||
|
||||
@ -93,7 +93,10 @@ const emit = defineEmits<{
|
||||
|
||||
const { editorService, uiService, propsService, storageService } = useServices();
|
||||
|
||||
const enablePropsFormValidate = inject(ENABLE_PROPS_FORM_VALIDATE, false);
|
||||
const enablePropsFormValidate = inject(
|
||||
ENABLE_PROPS_FORM_VALIDATE,
|
||||
computed(() => false),
|
||||
);
|
||||
|
||||
const values = ref<FormValue>({});
|
||||
// ts类型应该是FormConfig, 但是打包时会出错,所以暂时用any
|
||||
@ -205,7 +208,9 @@ const submit = async (
|
||||
replace,
|
||||
// 启用校验联动时,仅校验失败(error 存在)才把错误信息随更新传入 editorService 记录;
|
||||
// 其余情况(含表单校验成功、CodeEditor 源码保存)不携带 invalidInfo,由 editorService 在执行 update 时统一清除该节点错误。
|
||||
...(enablePropsFormValidate && error ? { invalidInfo: { id: newValue.id, source, error: error?.message } } : {}),
|
||||
...(enablePropsFormValidate.value && error
|
||||
? { invalidInfo: { id: newValue.id, source, error: error?.message } }
|
||||
: {}),
|
||||
});
|
||||
} catch (e: any) {
|
||||
emit('submit-error', e);
|
||||
|
||||
@ -10,8 +10,10 @@ import { mount } from '@vue/test-utils';
|
||||
import { FORM_CONTEXT_KEY, type FormContext } from '@tmagic/form';
|
||||
|
||||
import Editor from '@editor/Editor.vue';
|
||||
import { ENABLE_PROPS_FORM_VALIDATE } from '@editor/editorProps';
|
||||
|
||||
let injectedFormContext: ComputedRef<FormContext> | undefined;
|
||||
let injectedEnablePropsFormValidate: ComputedRef<boolean> | undefined;
|
||||
|
||||
const { initServiceEventsMock, initServiceStateMock } = vi.hoisted(() => ({
|
||||
initServiceEventsMock: vi.fn(),
|
||||
@ -107,6 +109,10 @@ vi.mock('@editor/layouts/props-panel/PropsPanel.vue', () => ({
|
||||
emits: ['mounted', 'unmounted', 'submit-error', 'form-error'],
|
||||
setup(_p, { emit }) {
|
||||
injectedFormContext = inject(FORM_CONTEXT_KEY, undefined);
|
||||
injectedEnablePropsFormValidate = inject(
|
||||
ENABLE_PROPS_FORM_VALIDATE,
|
||||
computed(() => false),
|
||||
);
|
||||
return () =>
|
||||
h('div', { class: 'fake-props-panel' }, [
|
||||
h('button', { class: 'mounted-btn', onClick: () => emit('mounted', { proxy: true }) }),
|
||||
@ -125,6 +131,7 @@ vi.mock('@editor/layouts/props-panel/FormPanel.vue', () => ({
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
injectedFormContext = undefined;
|
||||
injectedEnablePropsFormValidate = undefined;
|
||||
});
|
||||
|
||||
describe('Editor', () => {
|
||||
@ -246,6 +253,24 @@ describe('Editor', () => {
|
||||
editorServiceMod.default.__setStage(stageStub);
|
||||
});
|
||||
|
||||
test('ENABLE_PROPS_FORM_VALIDATE 默认注入 false', async () => {
|
||||
mount(Editor, { props: {} as any });
|
||||
await nextTick();
|
||||
expect(injectedEnablePropsFormValidate?.value).toBe(false);
|
||||
});
|
||||
|
||||
test('ENABLE_PROPS_FORM_VALIDATE 走 computed 读时求值,prop 变化后子孙能读到最新值', async () => {
|
||||
const wrapper = mount(Editor, { props: { enablePropsFormValidate: false } as any });
|
||||
await nextTick();
|
||||
expect(injectedEnablePropsFormValidate?.value).toBe(false);
|
||||
|
||||
await wrapper.setProps({ enablePropsFormValidate: true });
|
||||
expect(injectedEnablePropsFormValidate?.value).toBe(true);
|
||||
|
||||
await wrapper.setProps({ enablePropsFormValidate: false });
|
||||
expect(injectedEnablePropsFormValidate?.value).toBe(false);
|
||||
});
|
||||
|
||||
test('expose services', () => {
|
||||
const wrapper = mount(Editor, { props: {} as any });
|
||||
expect((wrapper.vm as any).editorService).toBeDefined();
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* Copyright (C) 2025 Tencent.
|
||||
*/
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { computed, defineComponent, h, nextTick } from 'vue';
|
||||
import { computed, defineComponent, h, nextTick, ref } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import { FORM_CONTEXT_KEY } from '@tmagic/form';
|
||||
@ -78,7 +78,7 @@ vi.mock('@tmagic/form', async () => {
|
||||
validateForm: vi.fn((options?: any) => validateFormImpl(options)),
|
||||
MForm: defineComponent({
|
||||
name: 'MForm',
|
||||
props: ['config', 'initValues', 'context'],
|
||||
props: ['config', 'initValues', 'context', 'typeMatchValid', 'validateOnInit'],
|
||||
emits: ['change', 'error'],
|
||||
setup(_p, { expose, emit }) {
|
||||
const formState = { stage: null as any, services: null as any };
|
||||
@ -171,7 +171,7 @@ describe('FormPanel', () => {
|
||||
};
|
||||
const wrapper = mount(FormPanel, {
|
||||
props: { config: [], values: {} } as any,
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: true } },
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: computed(() => true) } },
|
||||
});
|
||||
await wrapper.find('.change-btn').trigger('click');
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
@ -188,7 +188,7 @@ describe('FormPanel', () => {
|
||||
test('校验成功时 emit submit 且不携带 error', async () => {
|
||||
const wrapper = mount(FormPanel, {
|
||||
props: { config: [], values: {} } as any,
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: true } },
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: computed(() => true) } },
|
||||
});
|
||||
await wrapper.find('.change-btn').trigger('click');
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
@ -245,7 +245,7 @@ describe('FormPanel', () => {
|
||||
validateFormImpl = validateSpy;
|
||||
const wrapper = mount(FormPanel, {
|
||||
props: { config: [], values: {}, codeValueKey: 'style' } as any,
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: true } },
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: computed(() => true) } },
|
||||
});
|
||||
await wrapper.find('.fake-btn').trigger('click');
|
||||
await wrapper.find('.fake-code-editor').trigger('click');
|
||||
@ -261,7 +261,7 @@ describe('FormPanel', () => {
|
||||
validateFormImpl = validateSpy;
|
||||
const wrapper = mount(FormPanel, {
|
||||
props: { config: [], values: {} } as any,
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: true } },
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: computed(() => true) } },
|
||||
});
|
||||
await wrapper.find('.fake-btn').trigger('click');
|
||||
await wrapper.find('.fake-code-editor').trigger('click');
|
||||
@ -276,7 +276,7 @@ describe('FormPanel', () => {
|
||||
validateFormImpl = async () => '';
|
||||
const wrapper = mount(FormPanel, {
|
||||
props: { config: [], values: {} } as any,
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: true } },
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: computed(() => true) } },
|
||||
});
|
||||
await wrapper.find('.fake-btn').trigger('click');
|
||||
await wrapper.find('.fake-code-editor').trigger('click');
|
||||
@ -293,7 +293,7 @@ describe('FormPanel', () => {
|
||||
validateFormImpl = async () => '字段A -> 必填';
|
||||
const wrapper = mount(FormPanel, {
|
||||
props: { config: [], values: {} } as any,
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: true } },
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: computed(() => true) } },
|
||||
});
|
||||
await wrapper.find('.fake-btn').trigger('click');
|
||||
await wrapper.find('.fake-code-editor').trigger('click');
|
||||
@ -314,7 +314,7 @@ describe('FormPanel', () => {
|
||||
};
|
||||
const wrapper = mount(FormPanel, {
|
||||
props: { config: [], values: {} } as any,
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: true } },
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: computed(() => true) } },
|
||||
});
|
||||
await wrapper.find('.fake-btn').trigger('click');
|
||||
await wrapper.find('.fake-code-editor').trigger('click');
|
||||
@ -329,4 +329,41 @@ describe('FormPanel', () => {
|
||||
expect(tMagicMessage).not.toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('未启用 enablePropsFormValidate 时 MForm 不开启 typeMatchValid / validateOnInit', () => {
|
||||
const wrapper = mount(FormPanel, { props: { config: [], values: {} } as any });
|
||||
|
||||
const mForm = wrapper.findComponent({ name: 'MForm' });
|
||||
expect(mForm.props('typeMatchValid')).toBe(false);
|
||||
expect(mForm.props('validateOnInit')).toBe(false);
|
||||
});
|
||||
|
||||
test('启用 enablePropsFormValidate 时 MForm 开启 typeMatchValid / validateOnInit', () => {
|
||||
const wrapper = mount(FormPanel, {
|
||||
props: { config: [], values: {} } as any,
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: computed(() => true) } },
|
||||
});
|
||||
|
||||
const mForm = wrapper.findComponent({ name: 'MForm' });
|
||||
expect(mForm.props('typeMatchValid')).toBe(true);
|
||||
expect(mForm.props('validateOnInit')).toBe(true);
|
||||
});
|
||||
|
||||
test('enablePropsFormValidate 变化时 MForm 的 typeMatchValid / validateOnInit 同步更新', async () => {
|
||||
const enabled = ref(false);
|
||||
const wrapper = mount(FormPanel, {
|
||||
props: { config: [], values: {} } as any,
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: computed(() => enabled.value) } },
|
||||
});
|
||||
|
||||
const mForm = wrapper.findComponent({ name: 'MForm' });
|
||||
expect(mForm.props('typeMatchValid')).toBe(false);
|
||||
expect(mForm.props('validateOnInit')).toBe(false);
|
||||
|
||||
enabled.value = true;
|
||||
await nextTick();
|
||||
|
||||
expect(mForm.props('typeMatchValid')).toBe(true);
|
||||
expect(mForm.props('validateOnInit')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* Copyright (C) 2025 Tencent.
|
||||
*/
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { defineComponent, h, nextTick, ref } from 'vue';
|
||||
import { computed, defineComponent, h, nextTick, ref } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import { ENABLE_PROPS_FORM_VALIDATE } from '@editor/editorProps';
|
||||
@ -193,7 +193,7 @@ describe('PropsPanel', () => {
|
||||
test('启用 enablePropsFormValidate 时属性表单校验失败携带 invalidInfo(source=props)', async () => {
|
||||
const wrapper = mount(PropsPanel, {
|
||||
props: {} as any,
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: true } },
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: computed(() => true) } },
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
await wrapper.find('.submit-with-err-btn').trigger('click');
|
||||
@ -205,7 +205,7 @@ describe('PropsPanel', () => {
|
||||
test('启用 enablePropsFormValidate 且校验成功时不携带 invalidInfo(保持错误状态不变)', async () => {
|
||||
const wrapper = mount(PropsPanel, {
|
||||
props: {} as any,
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: true } },
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: computed(() => true) } },
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
await wrapper.find('.submit-btn').trigger('click');
|
||||
@ -218,7 +218,7 @@ describe('PropsPanel', () => {
|
||||
showStylePanel.value = true;
|
||||
const wrapper = mount(PropsPanel, {
|
||||
props: {} as any,
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: true } },
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: computed(() => true) } },
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
// 第二个 FormPanel 为样式面板
|
||||
@ -233,7 +233,7 @@ describe('PropsPanel', () => {
|
||||
test('CodeEditor 源码保存不携带 invalidInfo(未经表单校验,不应改动错误状态)', async () => {
|
||||
const wrapper = mount(PropsPanel, {
|
||||
props: {} as any,
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: true } },
|
||||
global: { provide: { [ENABLE_PROPS_FORM_VALIDATE]: computed(() => true) } },
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
await wrapper.find('.code-save-btn').trigger('click');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user