mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-01 05:28:14 +08:00
feat(editor): support custom width and initial diff mode for history diff dialog
为历史差异/回滚确认弹窗新增可配置宽度(dialogWidth / width 透传至 TMagicDialog), 并支持通过 payload.mode 指定打开时的初始对比模式,不可用时回退自动推断。
This commit is contained in:
parent
4330738b9f
commit
83f4e52845
@ -102,6 +102,7 @@ onCodeBlockDiff(id, index);
|
||||
| --- | --- | --- |
|
||||
| `appContext` | 否 | 父级应用上下文,用于让动态挂载的差异确认弹窗继承全局组件 / 指令 / provide / 插件(Element Plus、`@tmagic/form` 字段组件等)。在组件 `setup` 中调用时会自动取当前组件的 `appContext`,无需手动传;仅当在组件 setup 之外调用时才需显式传入(如 `editorApp._context`)。 |
|
||||
| `extendState` | 否 | 透传给差异确认弹窗的 `extendState`(同 Editor 的 [`extendFormState`](#自定义对比判断)),使对比表单中依赖业务上下文的 `display` / `disabled` 等 `filterFunction` 正常工作。 |
|
||||
| `dialogWidth` | 否 | 内置页面 / 数据源 / 代码块的差异 / 回滚确认弹窗默认宽度(透传给 `TMagicDialog` 的 `width`),如 `'1200px'` / `'80%'`。缺省时使用弹窗内置默认宽度(`900px`)。业务自有历史可在 `viewDiff` / `confirmAndRevert` 调用时通过各自入参的 `width` 单独覆盖。 |
|
||||
|
||||
> 若只需要无确认、无校验的静默回滚,直接用上面的 `editorService.revertPageStep` 等即可,无需 `useHistoryRevert`。
|
||||
|
||||
|
||||
@ -162,11 +162,19 @@ const hasCurrent = computed(() => payload.value?.currentValue !== undefined && p
|
||||
/** 是否存在该步「修改后的值」:不存在(如仅删除)时「与修改前对比」无意义,置灰禁用。 */
|
||||
const hasValue = computed(() => payload.value?.value !== undefined && payload.value?.value !== null);
|
||||
|
||||
/** 指定模式当前是否可用:before 依赖「修改后的值」,current 依赖「当前值」。 */
|
||||
const isModeAvailable = (m: DiffMode): boolean => (m === 'current' ? hasCurrent.value : hasValue.value);
|
||||
|
||||
/**
|
||||
* 计算 open 时的初始对比模式:
|
||||
* 没有「修改后的值」但有当前值时(「与修改前对比」不可用),默认进入「与当前对比」;否则默认「与修改前对比」。
|
||||
* - 调用方通过 payload.mode 指定且该模式可用时,优先使用指定模式;
|
||||
* - 否则没有「修改后的值」但有当前值时(「与修改前对比」不可用),默认进入「与当前对比」;
|
||||
* - 其余情况默认「与修改前对比」。
|
||||
*/
|
||||
const resolveInitialMode = (): DiffMode => (!hasValue.value && hasCurrent.value ? 'current' : 'before');
|
||||
const resolveInitialMode = (specified?: DiffMode): DiffMode => {
|
||||
if (specified && isModeAvailable(specified)) return specified;
|
||||
return !hasValue.value && hasCurrent.value ? 'current' : 'before';
|
||||
};
|
||||
|
||||
/** 左侧(旧/参照)值 */
|
||||
const leftValue = computed<Record<string, any>>(() => {
|
||||
@ -222,8 +230,8 @@ const targetText = computed(() => {
|
||||
const open = (p: DiffDialogPayload) => {
|
||||
payload.value = p;
|
||||
// 每次打开按 payload 重置对比模式(hasValue / hasCurrent 依赖 payload,需先赋值):
|
||||
// 无「修改后的值」但有当前值时默认「与当前对比」,否则默认「与修改前对比」
|
||||
mode.value = resolveInitialMode();
|
||||
// 优先使用 payload.mode 指定的模式;不可用或未指定时按数据自动推断。
|
||||
mode.value = resolveInitialMode(p.mode);
|
||||
// 默认回到表单对比形态,避免残留上一次选择的源码模式
|
||||
viewMode.value = 'form';
|
||||
visible.value = true;
|
||||
|
||||
@ -94,6 +94,7 @@ const mountHistoryDiffDialog = async (
|
||||
loadConfig: options.loadConfig,
|
||||
selfDiffFieldTypes: options.selfDiffFieldTypes,
|
||||
compareFormState: options.compareFormState,
|
||||
width: options.width,
|
||||
onClose: options.onClose,
|
||||
});
|
||||
if (options.appContext) {
|
||||
@ -192,7 +193,7 @@ const viewHistoryDiffDialog = async (
|
||||
export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services?: Services) => {
|
||||
// 自动捕获调用方所在组件的 appContext(在 setup 中调用时),业务方亦可显式覆盖。
|
||||
const appContext = options.appContext ?? getCurrentInstance()?.appContext ?? null;
|
||||
const { extendState, getPropsPanelFormState } = options;
|
||||
const { extendState, getPropsPanelFormState, dialogWidth } = options;
|
||||
|
||||
/** 目标数据已被删除、无法回滚时的统一提示。 */
|
||||
const showRevertTargetMissing = () => {
|
||||
@ -216,7 +217,13 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
|
||||
*/
|
||||
const runRevert = (payload: DiffDialogPayload | null, extra?: CustomDiffFormOptions): Promise<boolean> => {
|
||||
if (payload) {
|
||||
return confirmRevertWithDiffDialog(payload, { appContext, extendState, services, ...extra });
|
||||
return confirmRevertWithDiffDialog(payload, {
|
||||
appContext,
|
||||
extendState,
|
||||
services,
|
||||
...extra,
|
||||
width: extra?.width ?? dialogWidth,
|
||||
});
|
||||
}
|
||||
return confirmRevert();
|
||||
};
|
||||
@ -334,6 +341,7 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
|
||||
appContext,
|
||||
extendState,
|
||||
services,
|
||||
width: dialogWidth,
|
||||
compareFormState: getPropsPanelFormState?.(),
|
||||
});
|
||||
}
|
||||
@ -341,12 +349,12 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
|
||||
|
||||
const onDataSourceDiff = (id: string | number, index: number): Promise<void> | void => {
|
||||
const payload = buildDataSourceDiffPayload(id, index);
|
||||
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services });
|
||||
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, width: dialogWidth });
|
||||
};
|
||||
|
||||
const onCodeBlockDiff = (id: string | number, index: number): Promise<void> | void => {
|
||||
const payload = buildCodeBlockDiffPayload(id, index);
|
||||
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services });
|
||||
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, width: dialogWidth });
|
||||
};
|
||||
|
||||
/**
|
||||
@ -373,6 +381,7 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
|
||||
const confirmed = await runRevert(revertOptions.diffPayload ?? null, {
|
||||
loadConfig: revertOptions.loadConfig,
|
||||
selfDiffFieldTypes: revertOptions.selfDiffFieldTypes,
|
||||
width: revertOptions.width,
|
||||
});
|
||||
if (!confirmed) return null;
|
||||
return await revertOptions.revert();
|
||||
@ -383,7 +392,14 @@ export const useHistoryRevert = (options: UseHistoryRevertOptions = {}, services
|
||||
* 可透传 `loadConfig` / `selfDiffFieldTypes`。payload 为 null(不可对比)时静默返回。
|
||||
*/
|
||||
const viewDiff = (payload: DiffDialogPayload | null, extra?: CustomDiffFormOptions): Promise<void> | void => {
|
||||
if (payload) return viewHistoryDiffDialog(payload, { appContext, extendState, services, ...extra });
|
||||
if (payload)
|
||||
return viewHistoryDiffDialog(payload, {
|
||||
appContext,
|
||||
extendState,
|
||||
services,
|
||||
...extra,
|
||||
width: extra?.width ?? dialogWidth,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1352,6 +1352,14 @@ export interface DiffDialogPayload {
|
||||
targetLabel?: string;
|
||||
/** 用于标题展示的目标 id */
|
||||
id?: string | number;
|
||||
/**
|
||||
* 指定打开时的初始对比模式:
|
||||
* - before:该步骤修改前 vs 修改后
|
||||
* - current:该步骤修改后 vs 当前最新值
|
||||
* 不传时按是否存在「修改后的值」/「当前值」自动推断。
|
||||
* 若指定的模式当前不可用(对应数据缺失),将回退到自动推断结果。
|
||||
*/
|
||||
mode?: 'before' | 'current';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1408,6 +1416,12 @@ export interface UseHistoryRevertOptions {
|
||||
* 以保证两处 filterFunction 读取到一致的运行态上下文。
|
||||
*/
|
||||
getPropsPanelFormState?: () => FormState | undefined;
|
||||
/**
|
||||
* 内置页面 / 数据源 / 代码块的差异 / 回滚确认弹窗默认宽度(透传给 TMagicDialog 的 `width`),
|
||||
* 如 `'1200px'` / `'80%'`。缺省时使用弹窗内置默认宽度(900px)。
|
||||
* 业务自有历史(`viewDiff` / `confirmAndRevert`)可在调用时通过各自入参的 `width` 单独覆盖。
|
||||
*/
|
||||
dialogWidth?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1428,6 +1442,11 @@ export interface CustomDiffFormOptions {
|
||||
* 对比弹窗会用它覆盖 CompareForm 中同名扩展字段,避免上下文不一致。
|
||||
*/
|
||||
compareFormState?: FormState;
|
||||
/**
|
||||
* 差异 / 确认回滚弹窗宽度(透传给 HistoryDiffDialog 的 TMagicDialog `width`),
|
||||
* 如 `'1200px'` / `'80%'`。缺省时使用弹窗内置默认宽度(900px)。
|
||||
*/
|
||||
width?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -24,6 +24,36 @@ vi.mock('@editor/layouts/history-list/composables', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
// 捕获动态挂载 HistoryDiffDialog 时传入 createApp 的 props(用于断言 width 透传),
|
||||
// 并桩掉弹窗实例的 open / confirm,避免真正渲染组件。
|
||||
const dialogInstance = {
|
||||
open: vi.fn(),
|
||||
confirm: vi.fn(async () => true),
|
||||
};
|
||||
const appMock = {
|
||||
_context: {},
|
||||
mount: vi.fn(() => dialogInstance),
|
||||
unmount: vi.fn(),
|
||||
};
|
||||
const createAppMock = vi.fn((..._args: any[]) => appMock);
|
||||
|
||||
vi.mock('vue', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue')>('vue');
|
||||
return {
|
||||
...actual,
|
||||
createApp: (...args: any[]) => createAppMock(...args),
|
||||
// setup 之外调用,强制返回 null,避免依赖宿主组件上下文
|
||||
getCurrentInstance: () => null,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@editor/layouts/history-list/HistoryDiffDialog.vue', () => ({
|
||||
default: { name: 'MEditorHistoryDiffDialog' },
|
||||
}));
|
||||
|
||||
/** 读取最近一次挂载弹窗时传入的 props(createApp 第二个参数)。 */
|
||||
const lastDialogProps = () => createAppMock.mock.calls.at(-1)?.[1] as unknown as Record<string, any>;
|
||||
|
||||
const createServices = () =>
|
||||
({
|
||||
editorService: {
|
||||
@ -45,6 +75,19 @@ const createServices = () =>
|
||||
},
|
||||
}) as any;
|
||||
|
||||
/** 构造一个可差异对比(单实体 update,前后值都在)的历史分组,用于触发差异弹窗。 */
|
||||
const diffableGroups = (id: string | number = 'p1') => [
|
||||
{
|
||||
id,
|
||||
steps: [
|
||||
{
|
||||
index: 0,
|
||||
step: { diff: [{ oldSchema: { id: 'n1', name: 'A' }, newSchema: { id: 'n1', name: 'B' } }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
@ -105,4 +148,82 @@ describe('useHistoryRevert', () => {
|
||||
expect(tMagicMessage.error).toHaveBeenCalledWith('回滚失败:该记录对应的数据已被删除');
|
||||
expect(services.dataSourceService.revert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('弹窗宽度透传', () => {
|
||||
test('onPageDiff 使用 options.dialogWidth 作为弹窗宽度并打开弹窗', async () => {
|
||||
const services = createServices();
|
||||
services.historyService.getHistoryGroups.mockReturnValue(diffableGroups());
|
||||
|
||||
const { onPageDiff } = useHistoryRevert({ dialogWidth: '1200px' }, services);
|
||||
await onPageDiff(0);
|
||||
|
||||
expect(createAppMock).toHaveBeenCalledTimes(1);
|
||||
expect(lastDialogProps().width).toBe('1200px');
|
||||
expect(lastDialogProps().isConfirm).toBe(false);
|
||||
expect(dialogInstance.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('未配置 dialogWidth 时不强制 width(由弹窗内置默认值兜底)', async () => {
|
||||
const services = createServices();
|
||||
services.historyService.getHistoryGroups.mockReturnValue(diffableGroups());
|
||||
|
||||
const { onPageDiff } = useHistoryRevert({}, services);
|
||||
await onPageDiff(0);
|
||||
|
||||
expect(createAppMock).toHaveBeenCalledTimes(1);
|
||||
expect(lastDialogProps().width).toBeUndefined();
|
||||
});
|
||||
|
||||
test('onPageRevert 在可差异步骤上走差异确认弹窗并透传 dialogWidth', async () => {
|
||||
const services = createServices();
|
||||
services.historyService.getStepList.mockReturnValue([
|
||||
{ step: { opType: 'update', diff: [{ newSchema: { id: 'n1' }, oldSchema: { id: 'n1' } }] } },
|
||||
]);
|
||||
services.editorService.getNodeById.mockReturnValue({ id: 'n1' });
|
||||
services.historyService.getHistoryGroups.mockReturnValue(diffableGroups());
|
||||
|
||||
const { onPageRevert } = useHistoryRevert({ dialogWidth: '80%' }, services);
|
||||
await onPageRevert(0);
|
||||
|
||||
expect(lastDialogProps().width).toBe('80%');
|
||||
expect(lastDialogProps().isConfirm).toBe(true);
|
||||
expect(dialogInstance.confirm).toHaveBeenCalled();
|
||||
expect(services.editorService.revertPageStep).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
test('viewDiff 的逐次 width 入参可覆盖 dialogWidth 默认值', async () => {
|
||||
const services = createServices();
|
||||
const { viewDiff } = useHistoryRevert({ dialogWidth: '1000px' }, services);
|
||||
|
||||
await viewDiff({ category: 'module', lastValue: { a: 1 }, value: { a: 2 } } as any, { width: '600px' });
|
||||
|
||||
expect(lastDialogProps().width).toBe('600px');
|
||||
});
|
||||
|
||||
test('viewDiff 未传 width 时回退到 dialogWidth 默认值', async () => {
|
||||
const services = createServices();
|
||||
const { viewDiff } = useHistoryRevert({ dialogWidth: '1000px' }, services);
|
||||
|
||||
await viewDiff({ category: 'module', lastValue: { a: 1 }, value: { a: 2 } } as any);
|
||||
|
||||
expect(lastDialogProps().width).toBe('1000px');
|
||||
});
|
||||
|
||||
test('confirmAndRevert 透传 width 至确认弹窗并在确认后执行 revert', async () => {
|
||||
const services = createServices();
|
||||
const revert = vi.fn(async () => 'done');
|
||||
const { confirmAndRevert } = useHistoryRevert({ dialogWidth: '1000px' }, services);
|
||||
|
||||
const result = await confirmAndRevert({
|
||||
diffPayload: { category: 'module', lastValue: { a: 1 }, value: { a: 2 } } as any,
|
||||
width: '720px',
|
||||
revert,
|
||||
});
|
||||
|
||||
expect(lastDialogProps().width).toBe('720px');
|
||||
expect(lastDialogProps().isConfirm).toBe(true);
|
||||
expect(revert).toHaveBeenCalled();
|
||||
expect(result).toBe('done');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user