mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-23 06:28:17 +00:00
feat(editor): 抽离表单视图与对比逻辑
This commit is contained in:
parent
6cdf54a4a6
commit
6f3f321736
@ -19,107 +19,36 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, provide, type Ref, ref, type ShallowRef, useTemplateRef, watch, watchEffect } from 'vue';
|
||||
import { computed, type Ref, type ShallowRef } from 'vue';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import { type CodeBlockContent, type DataSourceSchema, HookType, type MNode } from '@tmagic/core';
|
||||
import { type FieldSize } from '@tmagic/design';
|
||||
import { type FormConfig, type FormState, type FormValue, MForm } from '@tmagic/form';
|
||||
import { type CodeBlockContent, HookType } from '@tmagic/core';
|
||||
import { type FormConfig, type FormValue, MForm } from '@tmagic/form';
|
||||
|
||||
import type { CompareCategory, CompareFormLoadConfig, Services } from '@editor/type';
|
||||
import { getCodeBlockFormConfig } from '@editor/utils/code-block';
|
||||
import { useCompareForm } from '@editor/hooks/use-compare-form';
|
||||
import type { CompareFormBaseProps } from '@editor/type';
|
||||
|
||||
defineOptions({
|
||||
name: 'MEditorCompareForm',
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 当前值(修改后的值) */
|
||||
value: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
|
||||
/** 用于对比的旧值(修改前的值) */
|
||||
lastValue?: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
|
||||
/**
|
||||
* 类型说明:
|
||||
* - `category` 为 `node` 时,`type` 为节点组件的类型,例如 'text'、'button'、'page'、'container' 等
|
||||
* - `category` 为 `data-source` 时,`type` 为数据源类型,例如 'base'、'http'
|
||||
* - `category` 为 `code-block` 时,`type` 可不传
|
||||
*/
|
||||
type?: string;
|
||||
/** 表单配置类别,决定从哪里取 FormConfig */
|
||||
category?: CompareCategory;
|
||||
/** 数据源代码块场景下的数据源类型(base/http),用于代码块表单中"执行时机"展示 */
|
||||
dataSourceType?: string;
|
||||
labelWidth?: string;
|
||||
/**
|
||||
* 外层容器高度。设置后表单内容超出时会在 CompareForm 内部出现滚动条,
|
||||
* 避免 dialog / 面板使用方需要自行处理滚动。可传任意 CSS 长度,例如 `60vh` / `400px` / `100%`。
|
||||
*/
|
||||
height?: string;
|
||||
/**
|
||||
* 用户自定义注入到 MForm.formState 的扩展字段,与 Editor 顶层的 `extendFormState`、
|
||||
* PropsPanel 的 `extend-state` 语义一致。表单 item 的 `display` / `disabled` 等
|
||||
* filterFunction 经常依赖这里注入的字段(如 stage、自定义业务上下文等),
|
||||
* 因此在差异对比场景下也需要透传,避免出现 `formState.xxx is undefined` 的运行时错误。
|
||||
*/
|
||||
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
||||
/**
|
||||
* 外部透传的基础 formState(通常来自 PropsPanel 主属性表单)。
|
||||
* CompareForm 会提取其中的扩展字段覆盖到自己的 formState,保证 filterFunction 上下文一致。
|
||||
*/
|
||||
baseFormState?: FormState;
|
||||
/** 需要走 self diff 的字段类型(例如 mod-cond)。 */
|
||||
selfDiffFieldTypes?: string[];
|
||||
/**
|
||||
* 表单内组件的尺寸(透传给 MForm 的 `size`),可选 'large' | 'default' | 'small'。
|
||||
* 缺省时使用 MForm 内置默认尺寸。
|
||||
*/
|
||||
size?: FieldSize;
|
||||
/**
|
||||
* 自定义 FormConfig 加载逻辑。传入后将接管内置的按 `category`(node/data-source/code-block)
|
||||
* 取配置逻辑,调用方可根据业务自行返回(或异步返回)表单配置。可通过
|
||||
* `ctx.defaultLoadConfig()` 复用默认结果再做二次加工。返回的 config 直接用于对比展示。
|
||||
*/
|
||||
loadConfig?: CompareFormLoadConfig;
|
||||
/** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
|
||||
services?: Services;
|
||||
}>(),
|
||||
defineProps<
|
||||
CompareFormBaseProps & {
|
||||
/** 用于对比的旧值(修改前的值) */
|
||||
lastValue?: CompareFormBaseProps['value'];
|
||||
/** 需要走 self diff 的字段类型(例如 mod-cond)。 */
|
||||
selfDiffFieldTypes?: string[];
|
||||
}
|
||||
>(),
|
||||
{
|
||||
category: 'node',
|
||||
labelWidth: '120px',
|
||||
extendState: (state: FormState) => state,
|
||||
},
|
||||
);
|
||||
|
||||
provide('services', props.services);
|
||||
|
||||
const config = ref<FormConfig>([]);
|
||||
|
||||
/** vs-code 编辑器的 monaco 配置项,沿用 Editor 顶层 provide('codeOptions', ...) 的注入。 */
|
||||
const codeOptions = inject<Record<string, any>>('codeOptions', {});
|
||||
|
||||
/** 将代码块的 content 字段统一成字符串,便于在表单/对比中展示 */
|
||||
const normalizeCodeBlockValue = (
|
||||
v: Partial<CodeBlockContent> | Record<string, any> | undefined,
|
||||
): Record<string, any> => {
|
||||
if (!v) return {};
|
||||
const next: Record<string, any> = { ...v };
|
||||
if (next.content && typeof next.content !== 'string') {
|
||||
try {
|
||||
next.content = next.content.toString();
|
||||
} catch {
|
||||
next.content = '';
|
||||
}
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
const currentValues = computed<FormValue>(() => {
|
||||
if (props.category === 'code-block') {
|
||||
return normalizeCodeBlockValue(props.value as Partial<CodeBlockContent>);
|
||||
}
|
||||
return (props.value || {}) as FormValue;
|
||||
});
|
||||
const { config, currentValues, wrapperStyle, mergedExtendState, loadConfig, formRef, normalizeCodeBlockValue } =
|
||||
useCompareForm(props);
|
||||
|
||||
const lastValuesProcessed = computed<FormValue>(() => {
|
||||
if (props.category === 'code-block') {
|
||||
@ -128,18 +57,6 @@ const lastValuesProcessed = computed<FormValue>(() => {
|
||||
return (props.lastValue || {}) as FormValue;
|
||||
});
|
||||
|
||||
/**
|
||||
* 外层包裹层的样式:当传入 `height` 时启用固定高度 + 内部滚动,
|
||||
* 这样滚动条会出现在 CompareForm 内部,避免父容器(如 Dialog)自身也产生滚动。
|
||||
*/
|
||||
const wrapperStyle = computed(() => {
|
||||
if (!props.height) return undefined;
|
||||
return {
|
||||
height: props.height,
|
||||
overflow: 'auto',
|
||||
} as Record<string, string>;
|
||||
});
|
||||
|
||||
/**
|
||||
* `code-select` 字段在历史数据中存在两种"语义为空"的形态:
|
||||
* - 字符串 `''`(旧数据 / 用户从未配置过钩子);
|
||||
@ -168,110 +85,6 @@ const showDiff = ({ curValue, lastValue, config }: { curValue: any; lastValue: a
|
||||
return !isEqual(curValue, lastValue);
|
||||
};
|
||||
|
||||
const removeStyleDisplayConfig = (formConfig: FormConfig): FormConfig =>
|
||||
formConfig.map((item) => {
|
||||
if (!('type' in item)) return item;
|
||||
if (item?.type !== 'tab' || !Array.isArray(item.items)) return item;
|
||||
|
||||
return {
|
||||
...item,
|
||||
items: item.items.map((tabPane) => {
|
||||
if (tabPane?.title !== '样式' || !Array.isArray(tabPane.items)) return tabPane;
|
||||
|
||||
return {
|
||||
...tabPane,
|
||||
display: true,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const mergedExtendState = (state: FormState) => {
|
||||
return props.extendState(props.baseFormState || state);
|
||||
};
|
||||
|
||||
/**
|
||||
* 内置的默认 FormConfig 加载逻辑:按 `category` 从对应 service / 工具取配置。
|
||||
* 作为 ctx.defaultLoadConfig 透传给自定义 `loadConfig`,方便复用与二次加工。
|
||||
*/
|
||||
const defaultLoadConfig = async (): Promise<FormConfig> => {
|
||||
if (!props.services) {
|
||||
return [];
|
||||
}
|
||||
|
||||
switch (props.category) {
|
||||
case 'node': {
|
||||
if (!props.type) {
|
||||
return [];
|
||||
}
|
||||
return removeStyleDisplayConfig(
|
||||
await props.services.propsService.getPropsConfig(props.type, { node: props.value as unknown as MNode }),
|
||||
);
|
||||
}
|
||||
case 'data-source': {
|
||||
const config = props.services.dataSourceService.getFormConfig(props.type || 'base');
|
||||
// 数据源表单外层 tab 的「数据定义」项 status 为 'fields',tab-pane name 随之为 'fields'。
|
||||
// 未显式设置 active 时,Tabs 默认取 '0',与 'fields' 不匹配会导致打开弹窗时无默认激活项,
|
||||
// 这里与 DataSourceConfigPanel 保持一致,默认激活「数据定义」tab。
|
||||
return config.map((item) => ('type' in item && item.type === 'tab' ? { ...item, active: 'fields' } : item));
|
||||
}
|
||||
case 'code-block': {
|
||||
return getCodeBlockFormConfig({
|
||||
paramColConfig: props.services.codeBlockService.getParamsColConfig(),
|
||||
// 通过传入 dataSourceType 间接表达"是数据源代码块"——在对比场景下 props.dataSourceType
|
||||
// 由调用方按 step 上下文显式传入,未传则视为普通代码块,「执行时机」字段隐藏。
|
||||
isDataSource: () => Boolean(props.dataSourceType),
|
||||
dataSourceType: () => props.dataSourceType,
|
||||
codeOptions,
|
||||
// 对比模式只读,不需要校验/语法检查
|
||||
editable: false,
|
||||
});
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const loadConfig = async () => {
|
||||
if (props.loadConfig) {
|
||||
config.value = await props.loadConfig({
|
||||
category: props.category,
|
||||
type: props.type,
|
||||
dataSourceType: props.dataSourceType,
|
||||
defaultLoadConfig,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
config.value = await defaultLoadConfig();
|
||||
};
|
||||
|
||||
watch(
|
||||
[() => props.category, () => props.type, () => props.dataSourceType, () => props.loadConfig],
|
||||
() => {
|
||||
loadConfig();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const formRef = useTemplateRef<InstanceType<typeof MForm>>('form');
|
||||
|
||||
/**
|
||||
* 把 services / stage 注入 MForm 的 formState,避免 propsService 注入的表单配置中
|
||||
* 形如 `display: ({ services }) => services.uiService.get(...)` 的 filterFunction
|
||||
* 在执行时拿不到 `formState.services` 而报错。
|
||||
*
|
||||
* 与 props-panel/FormPanel.vue 中的注入方式保持一致:
|
||||
* - services:整个 useServices() 返回的服务集合;
|
||||
* - stage:当前 editorService.get('stage') 的最新值。
|
||||
*/
|
||||
watchEffect(() => {
|
||||
if (formRef.value && props.services) {
|
||||
formRef.value.formState.stage = props.services.editorService.get('stage');
|
||||
formRef.value.formState.services = props.services;
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose<{
|
||||
form: ShallowRef<InstanceType<typeof MForm> | null>;
|
||||
config: Ref<FormConfig>;
|
||||
|
||||
55
packages/editor/src/components/ViewForm.vue
Normal file
55
packages/editor/src/components/ViewForm.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="m-editor-view-form-wrapper" :style="wrapperStyle">
|
||||
<MForm
|
||||
v-if="config.length"
|
||||
ref="form"
|
||||
class="m-editor-view-form"
|
||||
:config="config"
|
||||
:init-values="currentValues"
|
||||
:disabled="disabled"
|
||||
:label-width="labelWidth"
|
||||
:extend-state="mergedExtendState"
|
||||
:size="size"
|
||||
></MForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type Ref, type ShallowRef } from 'vue';
|
||||
|
||||
import { type FormConfig, MForm } from '@tmagic/form';
|
||||
|
||||
import { useCompareForm } from '@editor/hooks/use-compare-form';
|
||||
import type { CompareFormBaseProps } from '@editor/type';
|
||||
|
||||
defineOptions({
|
||||
name: 'MEditorViewForm',
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
CompareFormBaseProps & {
|
||||
/** 是否禁用表单(默认只读展示)。 */
|
||||
disabled?: boolean;
|
||||
}
|
||||
>(),
|
||||
{
|
||||
category: 'node',
|
||||
labelWidth: '120px',
|
||||
disabled: true,
|
||||
// extendState 的默认值由 useCompareForm 内部兜底(props.extendState ?? ...),此处无需重复提供
|
||||
},
|
||||
);
|
||||
|
||||
const { config, currentValues, wrapperStyle, mergedExtendState, loadConfig, formRef } = useCompareForm(props);
|
||||
|
||||
defineExpose<{
|
||||
form: ShallowRef<InstanceType<typeof MForm> | null>;
|
||||
config: Ref<FormConfig>;
|
||||
reload: () => Promise<void>;
|
||||
}>({
|
||||
form: formRef,
|
||||
config,
|
||||
reload: loadConfig,
|
||||
});
|
||||
</script>
|
||||
@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
export * from './use-code-block-edit';
|
||||
export * from './use-compare-form';
|
||||
export * from './use-stage';
|
||||
export * from './use-float-box';
|
||||
export * from './use-window-rect';
|
||||
|
||||
187
packages/editor/src/hooks/use-compare-form.ts
Normal file
187
packages/editor/src/hooks/use-compare-form.ts
Normal file
@ -0,0 +1,187 @@
|
||||
/*
|
||||
* 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 { computed, type ComputedRef, inject, provide, type Ref, ref, useTemplateRef, watch, watchEffect } from 'vue';
|
||||
|
||||
import type { CodeBlockContent, MNode } from '@tmagic/core';
|
||||
import { type FormConfig, type FormState, type FormValue, MForm } from '@tmagic/form';
|
||||
|
||||
import type { CompareFormBaseProps } from '@editor/type';
|
||||
import { getCodeBlockFormConfig } from '@editor/utils/code-block';
|
||||
import { removeStyleDisplayConfig } from '@editor/utils/props';
|
||||
|
||||
export interface UseCompareFormReturn {
|
||||
config: Ref<FormConfig>;
|
||||
currentValues: ComputedRef<FormValue>;
|
||||
wrapperStyle: ComputedRef<Record<string, string> | undefined>;
|
||||
mergedExtendState: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
||||
loadConfig: () => Promise<void>;
|
||||
formRef: Readonly<Ref<InstanceType<typeof MForm> | null>>;
|
||||
normalizeCodeBlockValue: (v: Partial<CodeBlockContent> | Record<string, any> | undefined) => Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* CompareForm / ViewForm 共用逻辑:
|
||||
* - 按 `category`(node / data-source / code-block) 加载 FormConfig(支持自定义 `loadConfig`);
|
||||
* - 代码块 `content` 归一化为字符串;
|
||||
* - 外层容器固定高度 + 内部滚动的 `wrapperStyle`;
|
||||
* - 将 services / stage 注入 MForm.formState,保证 filterFunction 上下文一致。
|
||||
*
|
||||
* 两个组件的差异仅在于是否做新旧值对比,这部分逻辑保留在各自组件中。
|
||||
*/
|
||||
export const useCompareForm = (props: CompareFormBaseProps): UseCompareFormReturn => {
|
||||
provide('services', props.services);
|
||||
|
||||
const config = ref<FormConfig>([]);
|
||||
|
||||
/** vs-code 编辑器的 monaco 配置项,沿用 Editor 顶层 provide('codeOptions', ...) 的注入。 */
|
||||
const codeOptions = inject<Record<string, any>>('codeOptions', {});
|
||||
|
||||
/** 将代码块的 content 字段统一成字符串,便于在表单 / 对比中展示 */
|
||||
const normalizeCodeBlockValue = (
|
||||
v: Partial<CodeBlockContent> | Record<string, any> | undefined,
|
||||
): Record<string, any> => {
|
||||
if (!v) return {};
|
||||
const next: Record<string, any> = { ...v };
|
||||
if (next.content && typeof next.content !== 'string') {
|
||||
try {
|
||||
next.content = next.content.toString();
|
||||
} catch {
|
||||
next.content = '';
|
||||
}
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
const currentValues = computed<FormValue>(() => {
|
||||
if (props.category === 'code-block') {
|
||||
return normalizeCodeBlockValue(props.value as Partial<CodeBlockContent>);
|
||||
}
|
||||
return (props.value || {}) as FormValue;
|
||||
});
|
||||
|
||||
/**
|
||||
* 外层包裹层的样式:当传入 `height` 时启用固定高度 + 内部滚动,
|
||||
* 这样滚动条会出现在组件内部,避免父容器(如 Dialog)自身也产生滚动。
|
||||
*/
|
||||
const wrapperStyle = computed(() => {
|
||||
if (!props.height) return undefined;
|
||||
const style: Record<string, string> = {
|
||||
height: props.height,
|
||||
overflow: 'auto',
|
||||
};
|
||||
return style;
|
||||
});
|
||||
|
||||
const mergedExtendState = (state: FormState) => {
|
||||
const extendState = props.extendState ?? ((s: FormState) => s);
|
||||
return extendState(props.baseFormState || state);
|
||||
};
|
||||
|
||||
/**
|
||||
* 内置的默认 FormConfig 加载逻辑:按 `category` 从对应 service / 工具取配置。
|
||||
* 作为 ctx.defaultLoadConfig 透传给自定义 `loadConfig`,方便复用与二次加工。
|
||||
*/
|
||||
const defaultLoadConfig = async (): Promise<FormConfig> => {
|
||||
if (!props.services) {
|
||||
return [];
|
||||
}
|
||||
|
||||
switch (props.category) {
|
||||
case 'node': {
|
||||
if (!props.type) {
|
||||
return [];
|
||||
}
|
||||
return removeStyleDisplayConfig(
|
||||
await props.services.propsService.getPropsConfig(props.type, { node: props.value as unknown as MNode }),
|
||||
);
|
||||
}
|
||||
case 'data-source': {
|
||||
const config = props.services.dataSourceService.getFormConfig(props.type || 'base');
|
||||
// 数据源表单外层 tab 的「数据定义」项 status 为 'fields',tab-pane name 随之为 'fields'。
|
||||
// 未显式设置 active 时,Tabs 默认取 '0',与 'fields' 不匹配会导致打开弹窗时无默认激活项,
|
||||
// 这里与 DataSourceConfigPanel 保持一致,默认激活「数据定义」tab。
|
||||
return config.map((item) => ('type' in item && item.type === 'tab' ? { ...item, active: 'fields' } : item));
|
||||
}
|
||||
case 'code-block': {
|
||||
return getCodeBlockFormConfig({
|
||||
paramColConfig: props.services.codeBlockService.getParamsColConfig(),
|
||||
// 通过传入 dataSourceType 间接表达"是数据源代码块"——在对比 / 展示场景下 props.dataSourceType
|
||||
// 由调用方按 step 上下文显式传入,未传则视为普通代码块,「执行时机」字段隐藏。
|
||||
isDataSource: () => Boolean(props.dataSourceType),
|
||||
dataSourceType: () => props.dataSourceType,
|
||||
codeOptions,
|
||||
// 对比 / 展示模式只读,不需要校验/语法检查
|
||||
editable: false,
|
||||
});
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const loadConfig = async () => {
|
||||
if (props.loadConfig) {
|
||||
config.value = await props.loadConfig({
|
||||
category: props.category as string,
|
||||
type: props.type,
|
||||
dataSourceType: props.dataSourceType,
|
||||
defaultLoadConfig,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
config.value = await defaultLoadConfig();
|
||||
};
|
||||
|
||||
watch(
|
||||
[() => props.category, () => props.type, () => props.dataSourceType, () => props.loadConfig],
|
||||
() => {
|
||||
loadConfig();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const formRef = useTemplateRef<InstanceType<typeof MForm>>('form');
|
||||
|
||||
/**
|
||||
* 把 services / stage 注入 MForm 的 formState,避免 propsService 注入的表单配置中
|
||||
* 形如 `display: ({ services }) => services.uiService.get(...)` 的 filterFunction
|
||||
* 在执行时拿不到 `formState.services` 而报错。
|
||||
*
|
||||
* 与 props-panel/FormPanel.vue 中的注入方式保持一致:
|
||||
* - services:整个 useServices() 返回的服务集合;
|
||||
* - stage:当前 editorService.get('stage') 的最新值。
|
||||
*/
|
||||
watchEffect(() => {
|
||||
if (formRef.value && props.services) {
|
||||
formRef.value.formState.stage = props.services.editorService.get('stage');
|
||||
formRef.value.formState.services = props.services;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
config,
|
||||
currentValues,
|
||||
wrapperStyle,
|
||||
mergedExtendState,
|
||||
loadConfig,
|
||||
formRef,
|
||||
normalizeCodeBlockValue,
|
||||
};
|
||||
};
|
||||
@ -74,6 +74,7 @@ export { default as SplitView } from './components/SplitView.vue';
|
||||
export { default as Resizer } from './components/Resizer.vue';
|
||||
export { default as CodeBlockEditor } from './components/CodeBlockEditor.vue';
|
||||
export { default as CompareForm } from './components/CompareForm.vue';
|
||||
export { default as ViewForm } from './components/ViewForm.vue';
|
||||
export { default as HistoryListBucket } from './layouts/history-list/Bucket.vue';
|
||||
export { default as HistoryListBucketTab } from './layouts/history-list/BucketTab.vue';
|
||||
export { default as HistoryDiffDialog } from './layouts/history-list/HistoryDiffDialog.vue';
|
||||
|
||||
@ -566,6 +566,58 @@ export interface CompareFormLoadConfigContext {
|
||||
* 可通过 `ctx.defaultLoadConfig()` 复用默认结果再做二次加工。
|
||||
*/
|
||||
export type CompareFormLoadConfig = (ctx: CompareFormLoadConfigContext) => FormConfig | Promise<FormConfig>;
|
||||
|
||||
/**
|
||||
* CompareForm / ViewForm 共用的基础 props。
|
||||
* 两者都基于同一套「按 category 加载 FormConfig + 注入 services/stage」的逻辑(见 useCompareForm),
|
||||
* 差异仅在于是否做新旧值对比。这里抽出公共字段避免重复定义。
|
||||
*/
|
||||
export interface CompareFormBaseProps {
|
||||
/** 当前值(对比场景下为修改后的值) */
|
||||
value: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
|
||||
/**
|
||||
* 类型说明:
|
||||
* - `category` 为 `node` 时,`type` 为节点组件的类型,例如 'text'、'button'、'page'、'container' 等
|
||||
* - `category` 为 `data-source` 时,`type` 为数据源类型,例如 'base'、'http'
|
||||
* - `category` 为 `code-block` 时,`type` 可不传
|
||||
*/
|
||||
type?: string;
|
||||
/** 表单配置类别,决定从哪里取 FormConfig */
|
||||
category?: CompareCategory;
|
||||
/** 数据源代码块场景下的数据源类型(base/http),用于代码块表单中"执行时机"展示 */
|
||||
dataSourceType?: string;
|
||||
labelWidth?: string;
|
||||
/**
|
||||
* 外层容器高度。设置后表单内容超出时会在组件内部出现滚动条,
|
||||
* 避免 dialog / 面板使用方需要自行处理滚动。可传任意 CSS 长度,例如 `60vh` / `400px` / `100%`。
|
||||
*/
|
||||
height?: string;
|
||||
/**
|
||||
* 用户自定义注入到 MForm.formState 的扩展字段,与 Editor 顶层的 `extendFormState`、
|
||||
* PropsPanel 的 `extend-state` 语义一致。表单 item 的 `display` / `disabled` 等
|
||||
* filterFunction 经常依赖这里注入的字段(如 stage、自定义业务上下文等),
|
||||
* 因此在对比 / 展示场景下也需要透传,避免出现 `formState.xxx is undefined` 的运行时错误。
|
||||
*/
|
||||
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
||||
/**
|
||||
* 外部透传的基础 formState(通常来自 PropsPanel 主属性表单)。
|
||||
* 组件会提取其中的扩展字段覆盖到自己的 formState,保证 filterFunction 上下文一致。
|
||||
*/
|
||||
baseFormState?: FormState;
|
||||
/**
|
||||
* 表单内组件的尺寸(透传给 MForm 的 `size`),可选 'large' | 'default' | 'small'。
|
||||
* 缺省时使用 MForm 内置默认尺寸。
|
||||
*/
|
||||
size?: FieldSize;
|
||||
/**
|
||||
* 自定义 FormConfig 加载逻辑。传入后将接管内置的按 `category`(node/data-source/code-block)
|
||||
* 取配置逻辑,调用方可根据业务自行返回(或异步返回)表单配置。可通过
|
||||
* `ctx.defaultLoadConfig()` 复用默认结果再做二次加工。
|
||||
*/
|
||||
loadConfig?: CompareFormLoadConfig;
|
||||
/** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
|
||||
services?: Services;
|
||||
}
|
||||
// #endregion CompareForm
|
||||
|
||||
// #region SideItemKey
|
||||
|
||||
@ -362,6 +362,35 @@ export const fillConfig = (
|
||||
return [tabConfig];
|
||||
};
|
||||
|
||||
/**
|
||||
* 将属性表单配置中「样式」tab-pane 的 `display` 强制置为 `true`。
|
||||
*
|
||||
* `propsService.getPropsConfig` 返回的样式 tab 默认带有
|
||||
* `display: ({ services }) => !(services?.uiService?.get('showStylePanel') ?? true)`,
|
||||
* 在对比 / 只读展示场景(CompareForm / ViewForm)下并不需要跟随 uiService 状态隐藏,
|
||||
* 这里统一放开,保证样式 tab 始终可见。
|
||||
*
|
||||
* @param formConfig 组件属性表单配置
|
||||
* @returns 处理后的表单配置(不修改入参,返回浅拷贝)
|
||||
*/
|
||||
export const removeStyleDisplayConfig = (formConfig: FormConfig): FormConfig =>
|
||||
formConfig.map((item) => {
|
||||
if (!('type' in item)) return item;
|
||||
if (item?.type !== 'tab' || !Array.isArray(item.items)) return item;
|
||||
|
||||
return {
|
||||
...item,
|
||||
items: item.items.map((tabPane) => {
|
||||
if (tabPane?.title !== '样式' || !Array.isArray(tabPane.items)) return tabPane;
|
||||
|
||||
return {
|
||||
...tabPane,
|
||||
display: true,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// #region ValidatePropsFormOptions
|
||||
/**
|
||||
* validatePropsForm 参数
|
||||
|
||||
172
packages/editor/tests/unit/components/ViewForm.spec.ts
Normal file
172
packages/editor/tests/unit/components/ViewForm.spec.ts
Normal file
@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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 { defineComponent, h, nextTick } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import ViewForm from '@editor/components/ViewForm.vue';
|
||||
|
||||
const propsService = {
|
||||
getPropsConfig: vi.fn(async () => [{ type: 'text', name: 'name' }]),
|
||||
};
|
||||
const dataSourceService = {
|
||||
getFormConfig: vi.fn(() => [{ type: 'text', name: 'title' }]),
|
||||
};
|
||||
const codeBlockService = {
|
||||
getParamsColConfig: vi.fn(() => null),
|
||||
};
|
||||
const editorService = {
|
||||
get: vi.fn(() => ({ select: vi.fn() })),
|
||||
};
|
||||
|
||||
const services = {
|
||||
propsService,
|
||||
dataSourceService,
|
||||
codeBlockService,
|
||||
editorService,
|
||||
} as any;
|
||||
|
||||
let capturedFormProps: Record<string, any> = {};
|
||||
|
||||
vi.mock('@editor/utils/code-block', () => ({
|
||||
getCodeBlockFormConfig: vi.fn(() => [{ type: 'text', name: 'content' }]),
|
||||
}));
|
||||
|
||||
vi.mock('@tmagic/form', () => ({
|
||||
MForm: defineComponent({
|
||||
name: 'MForm',
|
||||
props: ['config', 'initValues', 'disabled', 'labelWidth', 'extendState', 'size'],
|
||||
setup(props, { expose }) {
|
||||
capturedFormProps = props as Record<string, any>;
|
||||
expose({ formState: {} });
|
||||
return () => h('div', { class: 'fake-mform' });
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
capturedFormProps = {};
|
||||
});
|
||||
|
||||
describe('ViewForm.vue', () => {
|
||||
test('node 类别按 type 加载配置并渲染 MForm', async () => {
|
||||
const wrapper = mount(ViewForm, {
|
||||
props: {
|
||||
category: 'node',
|
||||
type: 'text',
|
||||
value: { id: 'n1', name: 'a' },
|
||||
services,
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(propsService.getPropsConfig).toHaveBeenCalledWith('text', { node: { id: 'n1', name: 'a' } });
|
||||
expect(wrapper.find('.fake-mform').exists()).toBe(true);
|
||||
expect(capturedFormProps.initValues).toEqual({ id: 'n1', name: 'a' });
|
||||
});
|
||||
|
||||
test('默认 disabled 为 true', async () => {
|
||||
mount(ViewForm, {
|
||||
props: {
|
||||
category: 'node',
|
||||
type: 'text',
|
||||
value: { id: 'n1' },
|
||||
services,
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(capturedFormProps.disabled).toBe(true);
|
||||
});
|
||||
|
||||
test('可通过 disabled=false 覆盖为可编辑', async () => {
|
||||
mount(ViewForm, {
|
||||
props: {
|
||||
category: 'node',
|
||||
type: 'text',
|
||||
value: { id: 'n1' },
|
||||
disabled: false,
|
||||
services,
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(capturedFormProps.disabled).toBe(false);
|
||||
});
|
||||
|
||||
test('size 透传给 MForm', async () => {
|
||||
mount(ViewForm, {
|
||||
props: {
|
||||
category: 'node',
|
||||
type: 'text',
|
||||
value: { id: 'n1' },
|
||||
size: 'small',
|
||||
services,
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(capturedFormProps.size).toBe('small');
|
||||
});
|
||||
|
||||
test('code-block 类别会把 content 非字符串值 normalize 成字符串', async () => {
|
||||
mount(ViewForm, {
|
||||
props: {
|
||||
category: 'code-block',
|
||||
value: { id: 'cb_1', content: { toString: () => 'fn-body' } },
|
||||
services,
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(capturedFormProps.initValues.content).toBe('fn-body');
|
||||
});
|
||||
|
||||
test('传入 height 时外层容器启用内部滚动样式', () => {
|
||||
const wrapper = mount(ViewForm, {
|
||||
props: {
|
||||
category: 'node',
|
||||
type: 'text',
|
||||
value: { id: 'n1' },
|
||||
height: '400px',
|
||||
services,
|
||||
},
|
||||
});
|
||||
const style = wrapper.find('.m-editor-view-form-wrapper').attributes('style') || '';
|
||||
expect(style).toContain('height: 400px');
|
||||
expect(style).toContain('overflow: auto');
|
||||
});
|
||||
|
||||
test('node 类别缺少 type 时不渲染 MForm', async () => {
|
||||
const wrapper = mount(ViewForm, {
|
||||
props: {
|
||||
category: 'node',
|
||||
value: { id: 'n1' },
|
||||
services,
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(wrapper.find('.fake-mform').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('reload 暴露方法会重新加载配置', async () => {
|
||||
const wrapper = mount(ViewForm, {
|
||||
props: {
|
||||
category: 'data-source',
|
||||
type: 'base',
|
||||
value: { id: 'ds_1' },
|
||||
services,
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
dataSourceService.getFormConfig.mockClear();
|
||||
await (wrapper.vm as any).reload();
|
||||
expect(dataSourceService.getFormConfig).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
222
packages/editor/tests/unit/hooks/use-compare-form.spec.ts
Normal file
222
packages/editor/tests/unit/hooks/use-compare-form.spec.ts
Normal file
@ -0,0 +1,222 @@
|
||||
/*
|
||||
* 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 { defineComponent, h, nextTick } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import { MForm } from '@tmagic/form';
|
||||
|
||||
import { useCompareForm } from '@editor/hooks/use-compare-form';
|
||||
|
||||
const propsService = {
|
||||
getPropsConfig: vi.fn(async () => [
|
||||
{
|
||||
type: 'tab',
|
||||
items: [{ title: '样式', items: [{ type: 'text', name: 'color', display: false }] }],
|
||||
},
|
||||
{ type: 'text', name: 'name' },
|
||||
]),
|
||||
};
|
||||
const dataSourceService = {
|
||||
getFormConfig: vi.fn(() => [{ type: 'tab', items: [{ status: 'fields', items: [] }] }]),
|
||||
};
|
||||
const codeBlockService = {
|
||||
getParamsColConfig: vi.fn(() => null),
|
||||
};
|
||||
const editorService = {
|
||||
get: vi.fn(() => ({ select: vi.fn() })),
|
||||
};
|
||||
|
||||
const services = {
|
||||
propsService,
|
||||
dataSourceService,
|
||||
codeBlockService,
|
||||
editorService,
|
||||
} as any;
|
||||
|
||||
let capturedGetCodeBlockArgs: any;
|
||||
|
||||
vi.mock('@editor/utils/code-block', () => ({
|
||||
getCodeBlockFormConfig: vi.fn((args: any) => {
|
||||
capturedGetCodeBlockArgs = args;
|
||||
return [{ type: 'text', name: 'content' }];
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@tmagic/form', () => ({
|
||||
MForm: defineComponent({
|
||||
name: 'MForm',
|
||||
setup(_, { expose }) {
|
||||
expose({ formState: {} });
|
||||
return () => h('div', { class: 'fake-mform' });
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mountHook = (props: any, provideOptions?: Record<string, any>) => {
|
||||
let captured: any;
|
||||
const comp = defineComponent({
|
||||
setup() {
|
||||
captured = useCompareForm(props);
|
||||
return () => h(MForm as any, { ref: 'form' });
|
||||
},
|
||||
});
|
||||
const wrapper = mount(comp, { global: { provide: provideOptions } });
|
||||
return { captured, wrapper };
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
capturedGetCodeBlockArgs = undefined;
|
||||
});
|
||||
|
||||
describe('useCompareForm', () => {
|
||||
test('无 services 时 config 为空数组', async () => {
|
||||
const { captured } = mountHook({ category: 'node', type: 'text', value: {} });
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(captured.config.value).toEqual([]);
|
||||
});
|
||||
|
||||
test('node 类别加载 props 配置并把「样式」tab display 置为 true', async () => {
|
||||
const { captured } = mountHook({ category: 'node', type: 'text', value: { id: 'n1' }, services });
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(propsService.getPropsConfig).toHaveBeenCalledWith('text', { node: { id: 'n1' } });
|
||||
const tab = captured.config.value.find((i: any) => i.type === 'tab');
|
||||
const stylePane = tab.items.find((p: any) => p.title === '样式');
|
||||
expect(stylePane.display).toBe(true);
|
||||
});
|
||||
|
||||
test('node 类别缺少 type 时返回空配置', async () => {
|
||||
const { captured } = mountHook({ category: 'node', value: { id: 'n1' }, services });
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(propsService.getPropsConfig).not.toHaveBeenCalled();
|
||||
expect(captured.config.value).toEqual([]);
|
||||
});
|
||||
|
||||
test('data-source 类别默认激活 fields tab', async () => {
|
||||
const { captured } = mountHook({ category: 'data-source', type: 'http', value: {}, services });
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(dataSourceService.getFormConfig).toHaveBeenCalledWith('http');
|
||||
const tab = captured.config.value.find((i: any) => i.type === 'tab');
|
||||
expect(tab.active).toBe('fields');
|
||||
});
|
||||
|
||||
test('data-source 未传 type 时默认使用 base', async () => {
|
||||
mountHook({ category: 'data-source', value: {}, services });
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(dataSourceService.getFormConfig).toHaveBeenCalledWith('base');
|
||||
});
|
||||
|
||||
test('code-block 类别归一化 content 并按 dataSourceType 判定是否数据源代码块', async () => {
|
||||
const { captured } = mountHook(
|
||||
{ category: 'code-block', dataSourceType: 'http', value: { content: { toString: () => 'body' } }, services },
|
||||
{ codeOptions: { theme: 'vs-dark' } },
|
||||
);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(captured.currentValues.value.content).toBe('body');
|
||||
expect(capturedGetCodeBlockArgs.editable).toBe(false);
|
||||
expect(capturedGetCodeBlockArgs.isDataSource()).toBe(true);
|
||||
expect(capturedGetCodeBlockArgs.dataSourceType()).toBe('http');
|
||||
expect(capturedGetCodeBlockArgs.codeOptions).toEqual({ theme: 'vs-dark' });
|
||||
});
|
||||
|
||||
test('未传 dataSourceType 时 isDataSource 为 false', async () => {
|
||||
mountHook({ category: 'code-block', value: { content: 'x' }, services });
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(capturedGetCodeBlockArgs.isDataSource()).toBe(false);
|
||||
});
|
||||
|
||||
test('normalizeCodeBlockValue 处理各种输入', () => {
|
||||
const { captured } = mountHook({ category: 'node', type: 'text', value: {}, services });
|
||||
expect(captured.normalizeCodeBlockValue(undefined)).toEqual({});
|
||||
expect(captured.normalizeCodeBlockValue({ content: 'x' })).toEqual({ content: 'x' });
|
||||
expect(captured.normalizeCodeBlockValue({ content: { toString: () => 'y' } }).content).toBe('y');
|
||||
const bad = {
|
||||
content: {
|
||||
toString: () => {
|
||||
throw new Error('e');
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(captured.normalizeCodeBlockValue(bad).content).toBe('');
|
||||
});
|
||||
|
||||
test('currentValues 非 code-block 场景直接返回 value', async () => {
|
||||
const { captured } = mountHook({ category: 'node', type: 'text', value: { id: 'n1', name: 'a' }, services });
|
||||
await nextTick();
|
||||
expect(captured.currentValues.value).toEqual({ id: 'n1', name: 'a' });
|
||||
});
|
||||
|
||||
test('wrapperStyle 根据 height 生成', () => {
|
||||
const { captured } = mountHook({ category: 'node', type: 'text', value: {}, height: '60vh', services });
|
||||
expect(captured.wrapperStyle.value).toEqual({ height: '60vh', overflow: 'auto' });
|
||||
const { captured: c2 } = mountHook({ category: 'node', type: 'text', value: {}, services });
|
||||
expect(c2.wrapperStyle.value).toBeUndefined();
|
||||
});
|
||||
|
||||
test('mergedExtendState 优先使用 baseFormState 并调用 extendState', () => {
|
||||
const extendState = vi.fn((s: any) => ({ ...s, x: 1 }));
|
||||
const base = { a: 1 } as any;
|
||||
const { captured } = mountHook({
|
||||
category: 'node',
|
||||
type: 'text',
|
||||
value: {},
|
||||
services,
|
||||
extendState,
|
||||
baseFormState: base,
|
||||
});
|
||||
const result = captured.mergedExtendState({ b: 2 });
|
||||
expect(extendState).toHaveBeenCalledWith(base);
|
||||
expect(result).toEqual({ a: 1, x: 1 });
|
||||
});
|
||||
|
||||
test('mergedExtendState 无 extendState 时原样返回 state', () => {
|
||||
const { captured } = mountHook({ category: 'node', type: 'text', value: {}, services });
|
||||
const state = { a: 1 } as any;
|
||||
expect(captured.mergedExtendState(state)).toBe(state);
|
||||
});
|
||||
|
||||
test('自定义 loadConfig 可接管配置加载并复用 defaultLoadConfig', async () => {
|
||||
const loadConfig = vi.fn(async ({ defaultLoadConfig }: any) => {
|
||||
await defaultLoadConfig();
|
||||
return [{ type: 'text', name: 'custom' }];
|
||||
});
|
||||
const { captured } = mountHook({ category: 'node', type: 'text', value: { id: 'n1' }, services, loadConfig });
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(loadConfig).toHaveBeenCalled();
|
||||
expect(propsService.getPropsConfig).toHaveBeenCalled();
|
||||
expect(captured.config.value).toEqual([{ type: 'text', name: 'custom' }]);
|
||||
});
|
||||
|
||||
test('reload 会重新加载配置', async () => {
|
||||
const { captured } = mountHook({ category: 'data-source', type: 'base', value: {}, services });
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
dataSourceService.getFormConfig.mockClear();
|
||||
await captured.loadConfig();
|
||||
expect(dataSourceService.getFormConfig).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('formRef.formState 注入 stage / services', async () => {
|
||||
const stage = { select: vi.fn() };
|
||||
editorService.get.mockReturnValue(stage);
|
||||
const { captured } = mountHook({ category: 'node', type: 'text', value: {}, services });
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(editorService.get).toHaveBeenCalledWith('stage');
|
||||
expect(captured.formRef.value.formState.services).toBe(services);
|
||||
expect(captured.formRef.value.formState.stage).toBe(stage);
|
||||
});
|
||||
});
|
||||
@ -18,6 +18,7 @@ import {
|
||||
fillConfig,
|
||||
getCondOpOptionsByFieldType,
|
||||
numberOptions,
|
||||
removeStyleDisplayConfig,
|
||||
styleTabConfig,
|
||||
validatePropsForm,
|
||||
} from '@editor/utils/props';
|
||||
@ -223,3 +224,51 @@ describe('validatePropsForm', () => {
|
||||
expect(state).toEqual({ stage, services });
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeStyleDisplayConfig', () => {
|
||||
test('将 tab 内「样式」pane 的 display 置为 true', () => {
|
||||
const config = [
|
||||
{
|
||||
type: 'tab',
|
||||
items: [
|
||||
{ title: '属性', items: [{ name: 'a' }] },
|
||||
{ title: '样式', display: false, items: [{ name: 'color' }] },
|
||||
],
|
||||
},
|
||||
] as any;
|
||||
|
||||
const result = removeStyleDisplayConfig(config) as any;
|
||||
const stylePane = result[0].items.find((i: any) => i.title === '样式');
|
||||
expect(stylePane.display).toBe(true);
|
||||
// 非样式 pane 保持不变
|
||||
expect(result[0].items.find((i: any) => i.title === '属性').display).toBeUndefined();
|
||||
});
|
||||
|
||||
test('不修改入参,返回浅拷贝', () => {
|
||||
const stylePane = { title: '样式', display: false, items: [{ name: 'color' }] };
|
||||
const config = [{ type: 'tab', items: [stylePane] }] as any;
|
||||
|
||||
const result = removeStyleDisplayConfig(config) as any;
|
||||
expect(result).not.toBe(config);
|
||||
expect(result[0]).not.toBe(config[0]);
|
||||
expect(stylePane.display).toBe(false);
|
||||
});
|
||||
|
||||
test('非 tab 项 / 无 items 的项原样返回', () => {
|
||||
const plain = { name: 'x', type: 'text' };
|
||||
const tabWithoutItems = { type: 'tab' };
|
||||
const noType = { foo: 'bar' };
|
||||
const config = [plain, tabWithoutItems, noType] as any;
|
||||
|
||||
const result = removeStyleDisplayConfig(config) as any;
|
||||
expect(result[0]).toBe(plain);
|
||||
expect(result[1]).toBe(tabWithoutItems);
|
||||
expect(result[2]).toBe(noType);
|
||||
});
|
||||
|
||||
test('样式 pane 无 items 数组时不处理', () => {
|
||||
const config = [{ type: 'tab', items: [{ title: '样式' }] }] as any;
|
||||
const result = removeStyleDisplayConfig(config) as any;
|
||||
expect(result[0].items[0].display).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user