mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-06-29 02:31:59 +00:00
feat(editor): 优化历史记录对比与回滚流程
This commit is contained in:
parent
1c936ff439
commit
ce65b18dbb
@ -135,7 +135,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { provide } from 'vue';
|
||||
import { provide, ref } from 'vue';
|
||||
|
||||
import type { MApp } from '@tmagic/core';
|
||||
|
||||
@ -227,6 +227,8 @@ const stageOptions: StageOptions = {
|
||||
|
||||
stageOverlayService.set('stageOptions', stageOptions);
|
||||
|
||||
const propsPanelRef = ref<InstanceType<typeof FormPanel> | null>(null);
|
||||
|
||||
provide('services', services);
|
||||
|
||||
provide('codeOptions', props.codeOptions);
|
||||
@ -237,6 +239,11 @@ provide('stageOptions', stageOptions);
|
||||
* 与 PropsPanel 通过 `:extend-state` 显式传入的方式保持等价。
|
||||
*/
|
||||
provide('extendFormState', props.extendFormState);
|
||||
/**
|
||||
* 提供 PropsPanel 主属性表单的 formState getter,供历史差异弹窗复用,
|
||||
* 让 CompareForm 与 PropsPanel 的 filterFunction 上下文保持一致。
|
||||
*/
|
||||
provide('getPropsPanelFormState', () => propsPanelRef.value?.configForm?.formState);
|
||||
|
||||
/**
|
||||
* 把历史记录面板的自定义扩展 tab 提供给深层的 HistoryListPanel(它挂在 NavMenu 中,
|
||||
@ -248,9 +255,11 @@ provide('historyListExtraTabs', props.historyListExtraTabs);
|
||||
provide<EventBus>('eventBus', new EventEmitter());
|
||||
|
||||
const propsPanelMountedHandler = (e: InstanceType<typeof FormPanel>) => {
|
||||
propsPanelRef.value = e;
|
||||
emit('props-panel-mounted', e);
|
||||
};
|
||||
const propsPanelUnmountedHandler = () => {
|
||||
propsPanelRef.value = null;
|
||||
emit('props-panel-unmounted');
|
||||
};
|
||||
|
||||
|
||||
@ -10,8 +10,9 @@
|
||||
:is-compare="true"
|
||||
:disabled="true"
|
||||
:label-width="labelWidth"
|
||||
:extend-state="extendState"
|
||||
:extend-state="mergedExtendState"
|
||||
:show-diff="showDiff"
|
||||
:self-diff-field-types="selfDiffFieldTypes"
|
||||
></MForm>
|
||||
</div>
|
||||
</template>
|
||||
@ -61,6 +62,13 @@ const props = withDefaults(
|
||||
* 因此在差异对比场景下也需要透传,避免出现 `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[];
|
||||
/**
|
||||
* 自定义 FormConfig 加载逻辑。传入后将接管内置的按 `category`(node/data-source/code-block)
|
||||
* 取配置逻辑,调用方可根据业务自行返回(或异步返回)表单配置。可通过
|
||||
@ -74,6 +82,7 @@ const props = withDefaults(
|
||||
category: 'node',
|
||||
labelWidth: '120px',
|
||||
services: () => useServices(),
|
||||
extendState: (state: FormState) => state,
|
||||
},
|
||||
);
|
||||
|
||||
@ -174,6 +183,10 @@ const removeStyleDisplayConfig = (formConfig: FormConfig): FormConfig =>
|
||||
};
|
||||
});
|
||||
|
||||
const mergedExtendState = (state: FormState) => {
|
||||
return props.extendState(props.baseFormState || state);
|
||||
};
|
||||
|
||||
/**
|
||||
* 内置的默认 FormConfig 加载逻辑:按 `category` 从对应 service / 工具取配置。
|
||||
* 作为 ctx.defaultLoadConfig 透传给自定义 `loadConfig`,方便复用与二次加工。
|
||||
|
||||
@ -30,10 +30,10 @@
|
||||
<template #suffix>
|
||||
<Icon :icon="Coin" />
|
||||
</template>
|
||||
<template #default="{ item }">
|
||||
<template #default="slotProps">
|
||||
<div style="display: flex; flex-direction: column; line-height: 1.2em">
|
||||
<div>{{ item.text }}</div>
|
||||
<span style="font-size: 10px; color: rgba(0, 0, 0, 0.6)">{{ item.value }}</span>
|
||||
<div>{{ slotProps?.item?.text || '' }}</div>
|
||||
<span style="font-size: 10px; color: rgba(0, 0, 0, 0.6)">{{ slotProps?.item?.value || '' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</component>
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
:data-source-type="payload.dataSourceType"
|
||||
:value="rightValue"
|
||||
:last-value="leftValue"
|
||||
:base-form-state="compareFormState"
|
||||
:extend-state="extendState"
|
||||
:load-config="loadConfig"
|
||||
:self-diff-field-types="selfDiffFieldTypes"
|
||||
@ -107,6 +108,7 @@ const props = withDefaults(
|
||||
isConfirm?: boolean;
|
||||
onConfirm?: () => void;
|
||||
selfDiffFieldTypes?: string[];
|
||||
compareFormState?: FormState;
|
||||
}>(),
|
||||
{
|
||||
services: () => useServices(),
|
||||
|
||||
@ -18,7 +18,9 @@
|
||||
<TMagicTabs v-model="activeTab" class="m-editor-history-list-tabs">
|
||||
<component
|
||||
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
||||
v-bind="tabPaneComponent?.props({ name: 'page', label: `${pageName} (${pageGroups.length})` }) || {}"
|
||||
v-bind="
|
||||
tabPaneComponent?.props({ name: 'page', label: `${pageName} (${pageGroups.length})`, lazy: true }) || {}
|
||||
"
|
||||
>
|
||||
<PageTab
|
||||
:list="pageGroupsDisplay"
|
||||
@ -38,8 +40,11 @@
|
||||
v-if="!disabledDataSource"
|
||||
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
||||
v-bind="
|
||||
tabPaneComponent?.props({ name: 'data-source', label: `${dataSourceName} (${dataSourceGroups.length})` }) ||
|
||||
{}
|
||||
tabPaneComponent?.props({
|
||||
name: 'data-source',
|
||||
label: `${dataSourceName} (${dataSourceGroups.length})`,
|
||||
lazy: true,
|
||||
}) || {}
|
||||
"
|
||||
>
|
||||
<BucketTab
|
||||
@ -59,7 +64,11 @@
|
||||
v-if="!disabledCodeBlock"
|
||||
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
||||
v-bind="
|
||||
tabPaneComponent?.props({ name: 'code-block', label: `${codeBlockName} (${codeBlockGroups.length})` }) || {}
|
||||
tabPaneComponent?.props({
|
||||
name: 'code-block',
|
||||
label: `${codeBlockName} (${codeBlockGroups.length})`,
|
||||
lazy: true,
|
||||
}) || {}
|
||||
"
|
||||
>
|
||||
<BucketTab
|
||||
@ -79,7 +88,7 @@
|
||||
v-for="tab in extraTabs"
|
||||
:key="tab.name"
|
||||
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
||||
v-bind="tabPaneComponent?.props({ name: tab.name, label: resolveTabLabel(tab) }) || {}"
|
||||
v-bind="tabPaneComponent?.props({ name: tab.name, label: resolveTabLabel(tab), lazy: true }) || {}"
|
||||
>
|
||||
<component :is="tab.component" v-bind="tab.props || {}" v-on="tab.listeners || {}" />
|
||||
</component>
|
||||
@ -193,6 +202,7 @@ const extendFormState = inject<((_state: FormState) => Record<string, any> | Pro
|
||||
'extendFormState',
|
||||
undefined,
|
||||
);
|
||||
const getPropsPanelFormState = inject<(() => FormState | undefined) | undefined>('getPropsPanelFormState', undefined);
|
||||
|
||||
const {
|
||||
expanded,
|
||||
@ -306,7 +316,7 @@ const onCodeBlockGotoInitial = (id: string | number) => {
|
||||
* 业务方亦可直接 import useHistoryRevert(services) 调用,无需自行挂载任何弹窗。
|
||||
*/
|
||||
const { onPageRevert, onDataSourceRevert, onCodeBlockRevert, onPageDiff, onDataSourceDiff, onCodeBlockDiff } =
|
||||
useHistoryRevert(services, { extendState: extendFormState });
|
||||
useHistoryRevert(services, { extendState: extendFormState, getPropsPanelFormState });
|
||||
|
||||
/**
|
||||
* 把内存中(已清空对应类别后的)历史状态重新写回 IndexedDB,
|
||||
|
||||
@ -59,6 +59,7 @@ const buildDiffPayload = (source: DiffPayloadSource, index: number, id?: string
|
||||
};
|
||||
|
||||
interface MountedDiffDialog {
|
||||
app: ReturnType<typeof createApp>;
|
||||
instance: {
|
||||
open: (_payload: DiffDialogPayload) => void;
|
||||
confirm: (_payload: DiffDialogPayload) => Promise<boolean>;
|
||||
@ -75,7 +76,7 @@ interface MountedDiffDialog {
|
||||
const mountHistoryDiffDialog = async (
|
||||
options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
|
||||
CustomDiffFormOptions & {
|
||||
services: Pick<Services, 'editorService' | 'dataSourceService' | 'codeBlockService' | 'historyService'>;
|
||||
services: Services;
|
||||
isConfirm?: boolean;
|
||||
onClose?: () => void;
|
||||
},
|
||||
@ -92,6 +93,7 @@ const mountHistoryDiffDialog = async (
|
||||
extendState: options.extendState,
|
||||
loadConfig: options.loadConfig,
|
||||
selfDiffFieldTypes: options.selfDiffFieldTypes,
|
||||
compareFormState: options.compareFormState,
|
||||
onClose: options.onClose,
|
||||
});
|
||||
if (options.appContext) {
|
||||
@ -111,7 +113,7 @@ const mountHistoryDiffDialog = async (
|
||||
}, 300);
|
||||
};
|
||||
|
||||
return { instance, destroy };
|
||||
return { app, instance, destroy };
|
||||
};
|
||||
|
||||
/**
|
||||
@ -123,7 +125,7 @@ const confirmRevertWithDiffDialog = async (
|
||||
payload: DiffDialogPayload,
|
||||
options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
|
||||
CustomDiffFormOptions & {
|
||||
services: Pick<Services, 'editorService' | 'dataSourceService' | 'codeBlockService' | 'historyService'>;
|
||||
services: Services;
|
||||
},
|
||||
): Promise<boolean> => {
|
||||
const { instance, destroy } = await mountHistoryDiffDialog({
|
||||
@ -145,7 +147,7 @@ const viewHistoryDiffDialog = async (
|
||||
payload: DiffDialogPayload,
|
||||
options: Pick<UseHistoryRevertOptions, 'appContext' | 'extendState'> &
|
||||
CustomDiffFormOptions & {
|
||||
services: Pick<Services, 'editorService' | 'dataSourceService' | 'codeBlockService' | 'historyService'>;
|
||||
services: Services;
|
||||
},
|
||||
): Promise<void> => {
|
||||
// onClose 在用户关闭弹窗时才触发,此时 handle.destroy 早已赋值。
|
||||
@ -187,14 +189,11 @@ const viewHistoryDiffDialog = async (
|
||||
* onPageDiff(index); // 弹出只读差异弹窗查看前后值差异
|
||||
* ```
|
||||
*/
|
||||
export const useHistoryRevert = (
|
||||
services: Pick<Services, 'editorService' | 'dataSourceService' | 'codeBlockService' | 'historyService'>,
|
||||
options: UseHistoryRevertOptions = {},
|
||||
) => {
|
||||
export const useHistoryRevert = (services: Services, options: UseHistoryRevertOptions = {}) => {
|
||||
const { editorService, dataSourceService, codeBlockService, historyService } = services;
|
||||
// 自动捕获调用方所在组件的 appContext(在 setup 中调用时),业务方亦可显式覆盖。
|
||||
const appContext = options.appContext ?? getCurrentInstance()?.appContext ?? null;
|
||||
const { extendState } = options;
|
||||
const { extendState, getPropsPanelFormState } = options;
|
||||
|
||||
/** 目标数据已被删除、无法回滚时的统一提示。 */
|
||||
const showRevertTargetMissing = () => {
|
||||
@ -300,7 +299,7 @@ export const useHistoryRevert = (
|
||||
showRevertTargetMissing();
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return runRevert(buildPageDiffPayload(index)).then((result) =>
|
||||
return runRevert(buildPageDiffPayload(index), { compareFormState: getPropsPanelFormState?.() }).then((result) =>
|
||||
result ? editorService.revertPageStep(index) : null,
|
||||
);
|
||||
};
|
||||
@ -331,7 +330,14 @@ export const useHistoryRevert = (
|
||||
*/
|
||||
const onPageDiff = (index: number): Promise<void> | void => {
|
||||
const payload = buildPageDiffPayload(index);
|
||||
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services });
|
||||
if (payload) {
|
||||
return viewHistoryDiffDialog(payload, {
|
||||
appContext,
|
||||
extendState,
|
||||
services,
|
||||
compareFormState: getPropsPanelFormState?.(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDataSourceDiff = (id: string | number, index: number): Promise<void> | void => {
|
||||
|
||||
@ -1367,6 +1367,12 @@ export interface UseHistoryRevertOptions {
|
||||
* 使对比表单中依赖业务上下文的 `display` / `disabled` 等 filterFunction 正常工作。
|
||||
*/
|
||||
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
||||
/**
|
||||
* 返回 PropsPanel 主属性表单(FormPanel -> MForm)的 formState。
|
||||
* 仅页面历史「查看差异 / 回滚确认」场景会使用该 formState 覆盖 CompareForm 中同名扩展字段,
|
||||
* 以保证两处 filterFunction 读取到一致的运行态上下文。
|
||||
*/
|
||||
getPropsPanelFormState?: () => FormState | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1382,6 +1388,11 @@ export interface CustomDiffFormOptions {
|
||||
loadConfig?: CompareFormLoadConfig;
|
||||
/** 需要走 self diff 的字段类型(如模块的 mod-cond)。 */
|
||||
selfDiffFieldTypes?: string[];
|
||||
/**
|
||||
* 可选:外部提供的 formState(通常来自 PropsPanel 主表单),
|
||||
* 对比弹窗会用它覆盖 CompareForm 中同名扩展字段,避免上下文不一致。
|
||||
*/
|
||||
compareFormState?: FormState;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -69,6 +69,8 @@ vi.mock('@tmagic/design', async () => {
|
||||
class: 'select-field-btn',
|
||||
onClick: () => emit('select', { value: 'a', type: 'field' }),
|
||||
}),
|
||||
// 模拟部分实现会以 undefined 触发默认插槽参数
|
||||
slots.default?.(),
|
||||
slots.suffix?.(),
|
||||
]);
|
||||
};
|
||||
@ -164,6 +166,10 @@ describe('DataSourceInput', () => {
|
||||
expect(wrapper.find('.fake-autocomplete').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('autocomplete 默认插槽参数为 undefined 时不应报错', () => {
|
||||
expect(() => mountIt('text-value', true)).not.toThrow();
|
||||
});
|
||||
|
||||
test('未 focus 时显示文本视图', () => {
|
||||
const wrapper = mountIt('hello');
|
||||
expect(wrapper.find('.tmagic-data-source-input-text').exists()).toBe(true);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user