feat(editor): 历史记录面板支持自定义扩展 tab 并开放 Bucket/goto 配置

新增 historyListExtraTabs 配置,可在内置页面/数据源/代码块 tab 后追加业务自定义历史 tab。
导出 HistoryListBucket 供复用,GroupRow 支持配置是否允许跳转,Bucket 支持配置是否展示初始项。
This commit is contained in:
roymondchen 2026-06-01 19:21:36 +08:00
parent 818b41f07f
commit 8612311db1
10 changed files with 291 additions and 57 deletions

View File

@ -1508,6 +1508,55 @@ const extendFormState = async (state) => {
``` ```
::: :::
## historyListExtraTabs
- **详情:**
[历史记录面板](/guide/advanced/history-list.md) 的自定义扩展 tab。
业务方可借此在历史记录面板内置的「页面 / 数据源 / 代码块」三个 tab 之后追加自定义模块的历史 tab例如某个自定义模块维护自己的操作历史时可在面板中增加一个独立的 tab 来展示与回滚。
- **默认值:** `[]`
- **类型:** `HistoryListExtraTab[]`
::: details 查看 HistoryListExtraTab 类型定义
<<< @/../packages/editor/src/type.ts#HistoryListExtraTab{ts}
:::
- **示例:**
```html
<template>
<m-editor :menu="menu" :history-list-extra-tabs="historyListExtraTabs"></m-editor>
</template>
<script setup>
import { markRaw } from 'vue';
import MyModuleHistoryTab from './MyModuleHistoryTab.vue';
const historyListExtraTabs = [
{
name: 'my-module',
// label 支持字符串或函数,函数形式便于展示动态数量
label: () => '我的模块',
component: markRaw(MyModuleHistoryTab),
// 传入内容组件的 props
props: { foo: 'bar' },
// 内容组件的事件监听
listeners: {
goto: (cursor) => console.log(cursor),
},
},
];
</script>
```
::: tip
内容组件内部可自行通过 `useServices()` 获取 `historyService` 等服务来读取与回滚自定义模块的历史。
:::
## pageBarSortOptions ## pageBarSortOptions
- **详情:** - **详情:**

View File

@ -76,6 +76,47 @@ const menu = ref({
表单对比依赖 `@tmagic/form` 的对比模式(`isCompare` / `lastValues`)。对于 `event-select``code-select``code-select-col` 等由列表或嵌套子表单组成的复合字段,表单会逐项展示新增 / 删除 / 修改的高亮差异,并在对比模式下隐藏「添加 / 删除 / 编辑」等写操作按钮,仅保留只读展示。 表单对比依赖 `@tmagic/form` 的对比模式(`isCompare` / `lastValues`)。对于 `event-select``code-select``code-select-col` 等由列表或嵌套子表单组成的复合字段,表单会逐项展示新增 / 删除 / 修改的高亮差异,并在对比模式下隐藏「添加 / 删除 / 编辑」等写操作按钮,仅保留只读展示。
::: :::
## 扩展自定义 tab
内置的三个 tab 之外,业务方可以通过 Editor 的 [`historyListExtraTabs`](/api/editor/props.html#historylistextratabs) 在面板中追加自定义的历史 tab追加在「页面 / 数据源 / 代码块」之后。适用于某个自定义模块维护自己的操作历史,需要在历史记录面板中独立展示与回滚的场景。
```html
<template>
<m-editor :menu="menu" :history-list-extra-tabs="historyListExtraTabs"></m-editor>
</template>
<script setup>
import { markRaw } from 'vue';
import MyModuleHistoryTab from './MyModuleHistoryTab.vue';
const historyListExtraTabs = [
{
name: 'my-module',
// label 支持字符串或函数,函数形式便于展示动态数量
label: () => `我的模块 (${getMyModuleHistory().length})`,
component: markRaw(MyModuleHistoryTab),
props: { foo: 'bar' },
listeners: {
goto: (cursor) => console.log(cursor),
},
},
];
</script>
```
每个扩展 tab 的字段说明:
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `name` | 是 | tab 唯一标识,作为内部 `TMagicTabs``name` |
| `label` | 是 | tab 显示文案,支持字符串或返回字符串的函数(便于展示动态数量) |
| `component` | 是 | tab 内容区渲染的组件 |
| `props` | 否 | 传入内容组件的 props |
| `listeners` | 否 | 内容组件的事件监听 |
> 内容组件内部可自行通过 `useServices()` 拿到 `historyService` 等服务,读取并回滚自定义模块自己维护的历史。
## 自定义对比判断 ## 自定义对比判断
差异对话框中的「表单对比」最终透传到 `MForm`,你可以通过 Editor 顶层注入的 `extendFormState` 让对比表单拿到完整业务上下文,从而让依赖上下文的 `display` / `disabled``filterFunction` 正常工作。 差异对话框中的「表单对比」最终透传到 `MForm`,你可以通过 Editor 顶层注入的 `extendFormState` 让对比表单拿到完整业务上下文,从而让依赖上下文的 `display` / `disabled``filterFunction` 正常工作。

View File

@ -237,6 +237,13 @@ provide('stageOptions', stageOptions);
*/ */
provide('extendFormState', props.extendFormState); provide('extendFormState', props.extendFormState);
/**
* 把历史记录面板的自定义扩展 tab 提供给深层的 HistoryListPanel它挂在 NavMenu
* markRaw component 形式渲染无法直接通过 props 透传业务方可借此在历史记录
* 面板内追加自定义模块的历史 tab
*/
provide('historyListExtraTabs', props.historyListExtraTabs);
provide<EventBus>('eventBus', new EventEmitter()); provide<EventBus>('eventBus', new EventEmitter());
const propsPanelMountedHandler = (e: InstanceType<typeof FormPanel>) => { const propsPanelMountedHandler = (e: InstanceType<typeof FormPanel>) => {

View File

@ -15,6 +15,7 @@ import type {
ComponentGroup, ComponentGroup,
CustomContentMenuFunction, CustomContentMenuFunction,
DatasourceTypeOption, DatasourceTypeOption,
HistoryListExtraTab,
IsExpandableFunction, IsExpandableFunction,
MenuBarData, MenuBarData,
MenuButton, MenuButton,
@ -125,6 +126,8 @@ export interface EditorProps {
/** 组件树节点双击前的钩子函数,返回 false 则阻止默认的双击行为 */ /** 组件树节点双击前的钩子函数,返回 false 则阻止默认的双击行为 */
beforeLayerNodeDblclick?: (event: MouseEvent, data: TreeNodeData) => Promise<boolean | void> | boolean | void; beforeLayerNodeDblclick?: (event: MouseEvent, data: TreeNodeData) => Promise<boolean | void> | boolean | void;
extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>; extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/** 历史记录面板的自定义扩展 tab追加在内置的页面/数据源/代码块 tab 之后 */
historyListExtraTabs?: HistoryListExtraTab[];
/** 页面顺序拖拽配置参数 */ /** 页面顺序拖拽配置参数 */
pageBarSortOptions?: PageBarSortOptions; pageBarSortOptions?: PageBarSortOptions;
/** 页面搜索函数 */ /** 页面搜索函数 */
@ -145,6 +148,7 @@ export const defaultEditorProps = {
disabledCodeBlock: false, disabledCodeBlock: false,
componentGroupList: () => [], componentGroupList: () => [],
datasourceList: () => [], datasourceList: () => [],
historyListExtraTabs: () => [],
menu: () => ({ left: [], right: [] }), menu: () => ({ left: [], right: [] }),
layerContentMenu: () => [], layerContentMenu: () => [],
stageContentMenu: () => [], stageContentMenu: () => [],

View File

@ -71,6 +71,7 @@ export { default as SplitView } from './components/SplitView.vue';
export { default as Resizer } from './components/Resizer.vue'; export { default as Resizer } from './components/Resizer.vue';
export { default as CodeBlockEditor } from './components/CodeBlockEditor.vue'; export { default as CodeBlockEditor } from './components/CodeBlockEditor.vue';
export { default as CompareForm } from './components/CompareForm.vue'; export { default as CompareForm } from './components/CompareForm.vue';
export { default as HistoryListBucket } from './layouts/history-list/Bucket.vue';
export { default as FloatingBox } from './components/FloatingBox.vue'; export { default as FloatingBox } from './components/FloatingBox.vue';
export { default as Tree } from './components/Tree.vue'; export { default as Tree } from './components/Tree.vue';
export { default as TreeNode } from './components/TreeNode.vue'; export { default as TreeNode } from './components/TreeNode.vue';

View File

@ -36,8 +36,13 @@
<!-- <!--
初始状态项永远位于该 bucket 列表底部同样按倒序展示最底部 = 最早状态 初始状态项永远位于该 bucket 列表底部同样按倒序展示最底部 = 最早状态
bucket 内所有 group 都未 applied 时即为当前位置 bucket 内所有 group 都未 applied 时即为当前位置
showInitial=false 时不展示用于没有"撤销到初始状态"语义的自定义历史如业务模块历史
--> -->
<InitialRow :is-current="isInitial" @goto-initial="$emit('goto-initial', bucketId)" /> <InitialRow
v-if="showInitial !== false"
:is-current="isInitial"
@goto-initial="$emit('goto-initial', bucketId)"
/>
</ul> </ul>
</div> </div>
</template> </template>
@ -54,13 +59,19 @@ defineOptions({
name: 'MEditorHistoryListBucket', name: 'MEditorHistoryListBucket',
}); });
const props = defineProps<{ const props = withDefaults(
defineProps<{
/** Bucket 标题,例如 "数据源" / "代码块",渲染在 bucket 头部。 */ /** Bucket 标题,例如 "数据源" / "代码块",渲染在 bucket 头部。 */
title: string; title: string;
/** 当前 bucket 对应的目标 iddataSource.id 或 codeBlock.id同时用于组装子项的 key。 */ /** 当前 bucket 对应的目标 iddataSource.id 或 codeBlock.id同时用于组装子项的 key。 */
bucketId: string | number; bucketId: string | number;
/** 子项 key 的命名空间前缀:`ds` 表示数据源,`cb` 表示代码块。与上层折叠状态 key 保持一致。 */ /**
prefix: 'ds' | 'cb'; * 子项 key 的命名空间前缀内置 `ds` 表示数据源`cb` 表示代码块
* 业务方复用 Bucket 时可传入自定义前缀 `mod`与上层折叠状态 key 保持一致
*/
prefix: string;
/** 是否展示底部「回到初始状态」入口,默认 true。无 undo cursor 语义的自定义历史可传 false 关闭。 */
showInitial?: boolean;
/** 当前 bucket 下的所有历史分组,按时间倒序展示(最近的操作在前)。 */ /** 当前 bucket 下的所有历史分组,按时间倒序展示(最近的操作在前)。 */
groups: { groups: {
applied: boolean; applied: boolean;
@ -76,7 +87,11 @@ const props = defineProps<{
isStepDiffable?: (_step: any) => boolean; isStepDiffable?: (_step: any) => boolean;
/** 共享的折叠状态表key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */ /** 共享的折叠状态表key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */
expanded: Record<string, boolean>; expanded: Record<string, boolean>;
}>(); }>(),
{
showInitial: true,
},
);
defineEmits<{ defineEmits<{
/** 透传子组件 GroupRow 的 toggle由上层 panel 更新 expanded。 */ /** 透传子组件 GroupRow 的 toggle由上层 panel 更新 expanded。 */

View File

@ -35,8 +35,8 @@
<li <li
v-for="s in subStepsDisplay" v-for="s in subStepsDisplay"
:key="s.index" :key="s.index"
:class="{ 'is-undone': !s.applied, 'is-current': s.isCurrent, 'is-clickable': !s.isCurrent }" :class="{ 'is-undone': !s.applied, 'is-current': s.isCurrent, 'is-clickable': gotoEnabled && !s.isCurrent }"
:title="s.isCurrent ? '当前所在记录' : '点击跳转到该记录'" :title="subStepTitle(s)"
@click="onSubStepClick(s)" @click="onSubStepClick(s)"
> >
<span class="m-editor-history-list-item-index">#{{ s.index + 1 }}</span> <span class="m-editor-history-list-item-index">#{{ s.index + 1 }}</span>
@ -72,7 +72,8 @@ defineOptions({
name: 'MEditorHistoryListGroupRow', name: 'MEditorHistoryListGroupRow',
}); });
const props = defineProps<{ const props = withDefaults(
defineProps<{
/** 唯一标识当前组的 key作为 toggle 事件的 payload 回传给上层。形如 `pg-${idx}` / `ds-${id}-${idx}` / `cb-${id}-${idx}`。 */ /** 唯一标识当前组的 key作为 toggle 事件的 payload 回传给上层。形如 `pg-${idx}` / `ds-${id}-${idx}` / `cb-${id}-${idx}`。 */
groupKey: string; groupKey: string;
/** 该组当前是否处于已应用状态false 表示已被 undo 撤销UI 会显示为灰态)。 */ /** 该组当前是否处于已应用状态false 表示已被 undo 撤销UI 会显示为灰态)。 */
@ -99,7 +100,18 @@ const props = defineProps<{
expanded: boolean; expanded: boolean;
/** 是否为当前所在的分组包含栈中最近一次已应用步骤的那一组UI 高亮展示。 */ /** 是否为当前所在的分组包含栈中最近一次已应用步骤的那一组UI 高亮展示。 */
isCurrent?: boolean; isCurrent?: boolean;
}>(); /**
* 是否支持跳转到该记录(goto)默认 true
* false 单步组头部与子步条目都不再可点击跳转也不会触发 goto 事件
* 仅保留合并组头部的展开 / 收起能力以及查看差异回滚等其它入口
*/
gotoEnabled?: boolean;
}>(),
{
isCurrent: false,
gotoEnabled: true,
},
);
const emit = defineEmits<{ const emit = defineEmits<{
/** /**
@ -129,15 +141,19 @@ const emit = defineEmits<{
(_e: 'revert-step', _index: number): void; (_e: 'revert-step', _index: number): void;
}>(); }>();
/** 单步组:头部可点击 goto合并组头部可点击切换展开。当前组isCurrent的单步组头部不可点击。 */ /**
* 单步组头部可点击 goto gotoEnabled合并组头部可点击切换展开
* 当前组isCurrent或禁用 goto 单步组头部不可点击
*/
const isHeadClickable = computed(() => { const isHeadClickable = computed(() => {
if (props.merged) return true; if (props.merged) return true;
return !props.isCurrent; return props.gotoEnabled && !props.isCurrent;
}); });
const headTitle = computed(() => { const headTitle = computed(() => {
if (props.merged) return props.expanded ? '点击收起子步' : '点击展开子步'; if (props.merged) return props.expanded ? '点击收起子步' : '点击展开子步';
if (props.isCurrent) return '当前所在记录'; if (props.isCurrent) return '当前所在记录';
if (!props.gotoEnabled) return '';
return '点击跳转到该记录'; return '点击跳转到该记录';
}); });
@ -152,15 +168,23 @@ const onHeadClick = () => {
return; return;
} }
if (props.isCurrent) return; if (props.isCurrent) return;
if (!props.gotoEnabled) return;
if (!props.subSteps.length) return; if (!props.subSteps.length) return;
emit('goto', props.subSteps[0].index); emit('goto', props.subSteps[0].index);
}; };
const onSubStepClick = (s: { index: number; isCurrent?: boolean }) => { const onSubStepClick = (s: { index: number; isCurrent?: boolean }) => {
if (s.isCurrent) return; if (s.isCurrent) return;
if (!props.gotoEnabled) return;
emit('goto', s.index); emit('goto', s.index);
}; };
const subStepTitle = (s: { isCurrent?: boolean }) => {
if (s.isCurrent) return '当前所在记录';
if (!props.gotoEnabled) return '';
return '点击跳转到该记录';
};
/** 单步组头部是否展示"查看差异"入口:要求该唯一子步本身可对比。 */ /** 单步组头部是否展示"查看差异"入口:要求该唯一子步本身可对比。 */
const headDiffable = computed(() => !props.merged && Boolean(props.subSteps[0]?.diffable)); const headDiffable = computed(() => !props.merged && Boolean(props.subSteps[0]?.diffable));

View File

@ -32,6 +32,7 @@
</component> </component>
<component <component
v-if="!disabledDataSource"
:is="tabPaneComponent?.component || 'el-tab-pane'" :is="tabPaneComponent?.component || 'el-tab-pane'"
v-bind="tabPaneComponent?.props({ name: 'data-source', label: `数据源 (${dataSourceGroups.length})` }) || {}" v-bind="tabPaneComponent?.props({ name: 'data-source', label: `数据源 (${dataSourceGroups.length})` }) || {}"
> >
@ -47,6 +48,7 @@
</component> </component>
<component <component
v-if="!disabledCodeBlock"
:is="tabPaneComponent?.component || 'el-tab-pane'" :is="tabPaneComponent?.component || 'el-tab-pane'"
v-bind="tabPaneComponent?.props({ name: 'code-block', label: `代码块 (${codeBlockGroups.length})` }) || {}" v-bind="tabPaneComponent?.props({ name: 'code-block', label: `代码块 (${codeBlockGroups.length})` }) || {}"
> >
@ -60,6 +62,15 @@
@revert-step="onCodeBlockRevert" @revert-step="onCodeBlockRevert"
/> />
</component> </component>
<component
v-for="tab in extraTabs"
:key="tab.name"
:is="tabPaneComponent?.component || 'el-tab-pane'"
v-bind="tabPaneComponent?.props({ name: tab.name, label: resolveTabLabel(tab) }) || {}"
>
<component :is="tab.component" v-bind="tab.props || {}" v-on="tab.listeners || {}" />
</component>
</TMagicTabs> </TMagicTabs>
</div> </div>
@ -98,7 +109,7 @@
* tab 的内容拆分为独立的 SFCPageTab / DataSourceTab / CodeBlockTab * tab 的内容拆分为独立的 SFCPageTab / DataSourceTab / CodeBlockTab
* 共享的描述生成与折叠状态在 composables.ts 中维护 * 共享的描述生成与折叠状态在 composables.ts 中维护
*/ */
import { inject, markRaw, ref, useTemplateRef } from 'vue'; import { computed, inject, markRaw, ref, useTemplateRef, watch } from 'vue';
import { Clock, Close } from '@element-plus/icons-vue'; import { Clock, Close } from '@element-plus/icons-vue';
import { getDesignConfig, TMagicButton, TMagicPopover, TMagicTabs, TMagicTooltip } from '@tmagic/design'; import { getDesignConfig, TMagicButton, TMagicPopover, TMagicTabs, TMagicTooltip } from '@tmagic/design';
@ -106,6 +117,7 @@ import type { FormState } from '@tmagic/form';
import MIcon from '@editor/components/Icon.vue'; import MIcon from '@editor/components/Icon.vue';
import { useServices } from '@editor/hooks/use-services'; import { useServices } from '@editor/hooks/use-services';
import type { HistoryListExtraTab } from '@editor/type';
import CodeBlockTab from './CodeBlockTab.vue'; import CodeBlockTab from './CodeBlockTab.vue';
import { useHistoryList } from './composables'; import { useHistoryList } from './composables';
@ -119,14 +131,36 @@ defineOptions({
const ClockIcon = markRaw(Clock); const ClockIcon = markRaw(Clock);
const CloseIcon = markRaw(Close); const CloseIcon = markRaw(Close);
const activeTab = ref<'page' | 'data-source' | 'code-block'>('page'); const activeTab = ref<string>('page');
/** 面板显隐受控reference 图标点击切换,右上角关闭按钮置为 false。 */ /** 面板显隐受控reference 图标点击切换,右上角关闭按钮置为 false。 */
const visible = ref(false); const visible = ref(false);
const tabPaneComponent = getDesignConfig('components')?.tabPane; const tabPaneComponent = getDesignConfig('components')?.tabPane;
const { editorService, dataSourceService, codeBlockService, historyService } = useServices(); /**
* 业务方自定义的扩展 tab Editor 顶层通过 `historyListExtraTabs` 注入
* 追加在内置页面 / 数据源 / 代码块三个 tab 之后未提供时为空数组
*/
const extraTabs = inject<HistoryListExtraTab[]>('historyListExtraTabs', []);
/** label 支持字符串或函数,函数形式便于展示动态数量等内容。 */
const resolveTabLabel = (tab: HistoryListExtraTab) => (typeof tab.label === 'function' ? tab.label() : tab.label);
const { editorService, dataSourceService, codeBlockService, historyService, propsService } = useServices();
/**
* 数据源 / 代码块功能可被业务方通过 `disabledDataSource` / `disabledCodeBlock` 禁用
* 禁用后对应的历史记录 tab 不再展示若当前激活的 tab 恰好被禁用则回退到页面tab
*/
const disabledDataSource = computed(() => propsService.getDisabledDataSource());
const disabledCodeBlock = computed(() => propsService.getDisabledCodeBlock());
watch([disabledDataSource, disabledCodeBlock], ([dsDisabled, cbDisabled]) => {
if ((activeTab.value === 'data-source' && dsDisabled) || (activeTab.value === 'code-block' && cbDisabled)) {
activeTab.value = 'page';
}
});
/** /**
* 通过 inject 拿到 Editor 顶层注入的 `extendFormState`转交给 HistoryDiffDialog * 通过 inject 拿到 Editor 顶层注入的 `extendFormState`转交给 HistoryDiffDialog

View File

@ -475,6 +475,29 @@ export interface SideComponent extends MenuComponent {
} }
// #endregion SideComponent // #endregion SideComponent
// #region HistoryListExtraTab
/**
* HistoryListPanel tab
*
* Editor `historyListExtraTabs` tab
*
* tab / / tab
* tab
*/
export interface HistoryListExtraTab {
/** tab 唯一标识,作为 TMagicTabs 的 name */
name: string;
/** tab 显示文案,支持传入函数以展示动态内容(如记录数量) */
label: string | (() => string);
/** tab 内容区渲染的组件Vue 组件或字符串标签) */
component: any;
/** 传入内容组件的 props */
props?: Record<string, any>;
/** 内容组件的事件监听 */
listeners?: Record<string, (..._args: any[]) => any>;
}
// #endregion HistoryListExtraTab
// #region SideItemKey // #region SideItemKey
export enum SideItemKey { export enum SideItemKey {
COMPONENT_LIST = 'component-list', COMPONENT_LIST = 'component-list',

View File

@ -12,9 +12,13 @@ import historyService from '@editor/services/history';
const editorService = { gotoPageStep: vi.fn(async () => 0) }; const editorService = { gotoPageStep: vi.fn(async () => 0) };
const dataSourceService = { goto: vi.fn(() => 0) }; const dataSourceService = { goto: vi.fn(() => 0) };
const codeBlockService = { goto: vi.fn(async () => 0) }; const codeBlockService = { goto: vi.fn(async () => 0) };
const propsService = {
getDisabledDataSource: vi.fn(() => false),
getDisabledCodeBlock: vi.fn(() => false),
};
vi.mock('@editor/hooks/use-services', () => ({ vi.mock('@editor/hooks/use-services', () => ({
useServices: () => ({ historyService, editorService, dataSourceService, codeBlockService }), useServices: () => ({ historyService, editorService, dataSourceService, codeBlockService, propsService }),
})); }));
vi.mock('@tmagic/design', () => ({ vi.mock('@tmagic/design', () => ({
@ -251,6 +255,38 @@ describe('HistoryListPanel.vue', () => {
expect(editorService.gotoPageStep).toHaveBeenCalledWith(0); expect(editorService.gotoPageStep).toHaveBeenCalledWith(0);
}); });
test('注入 historyListExtraTabs 时追加渲染自定义 tab 内容组件', async () => {
const { default: historyListPanel } = await import('@editor/layouts/history-list/HistoryListPanel.vue');
const customTab = defineComponent({
name: 'CustomHistoryTab',
props: ['title'],
setup(p) {
return () => h('div', { class: 'custom-history-tab' }, p.title);
},
});
const wrapper = mount(historyListPanel, {
attachTo: document.body,
global: {
provide: {
historyListExtraTabs: [
{
name: 'custom-module',
label: () => '自定义模块 (1)',
component: customTab,
props: { title: 'hello-custom' },
},
],
},
},
});
await nextTick();
const custom = wrapper.find('.custom-history-tab');
expect(custom.exists()).toBe(true);
expect(custom.text()).toBe('hello-custom');
});
test('点击数据源/代码块初始项调用对应 service.goto(id, 0)', async () => { test('点击数据源/代码块初始项调用对应 service.goto(id, 0)', async () => {
historyService.pushDataSource('ds_x', { historyService.pushDataSource('ds_x', {
oldSchema: null, oldSchema: null,