fix(editor): 修复历史记录面板分组与展示逻辑

统一历史记录列表在不同维度下的分组与展示行为,避免对比信息与交互状态不一致,并补齐对应单测覆盖。
This commit is contained in:
roymondchen 2026-06-17 17:14:27 +08:00
parent 24b9b34f65
commit bbf79fd6df
10 changed files with 133 additions and 467 deletions

View File

@ -135,6 +135,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 { import type {
BaseStepValue,
CodeBlockStepValue, CodeBlockStepValue,
DataSourceStepValue, DataSourceStepValue,
DiffDialogPayload, DiffDialogPayload,
@ -143,15 +144,7 @@ import type {
} from '@editor/type'; } from '@editor/type';
import BucketTab from './BucketTab.vue'; import BucketTab from './BucketTab.vue';
import { import { describeStep, isSingleDiffStepRevertable, useHistoryList } from './composables';
describeCodeBlockGroup,
describeCodeBlockStep,
describeDataSourceGroup,
describeDataSourceStep,
isCodeBlockStepRevertable,
isDataSourceStepRevertable,
useHistoryList,
} from './composables';
import HistoryDiffDialog from './HistoryDiffDialog.vue'; import HistoryDiffDialog from './HistoryDiffDialog.vue';
import PageTab from './PageTab.vue'; import PageTab from './PageTab.vue';
@ -223,34 +216,28 @@ const {
*/ */
const pageMarker = computed(() => historyService.getPageMarker()); const pageMarker = computed(() => historyService.getPageMarker());
/** 数据源 step 仅 update前后 schema 都存在)时可查看差异。 */
const isDataSourceStepDiffable = (step: DataSourceStepValue) =>
Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
/** 代码块 step 仅 update前后 content 都存在)时可查看差异。 */ /** 代码块 step 仅 update前后 content 都存在)时可查看差异。 */
const isCodeBlockStepDiffable = (step: CodeBlockStepValue) => const isStepDiffable = (step: BaseStepValue) => Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
/** /**
* 数据源 / 代码块两类 bucket 历史的整体渲染配置 title / prefix 与各自的描述 * 数据源 / 代码块两类 bucket 历史的整体渲染配置 title / prefix 与各自的描述
* 可差异可回滚判定收敛为单一对象整体注入 BucketTab组件内部按需读取 * 可差异可回滚判定收敛为单一对象整体注入 BucketTab组件内部按需读取
*/ */
// / describeGroup toRowGroup 退 describeStep
const dataSourceConfig: HistoryBucketConfig<DataSourceStepValue> = { const dataSourceConfig: HistoryBucketConfig<DataSourceStepValue> = {
title: '数据源', title: '数据源',
prefix: 'ds', prefix: 'ds',
describeGroup: describeDataSourceGroup, describeStep: (step: DataSourceStepValue): string => describeStep(step, (schema) => schema?.title, '数据源'),
describeStep: describeDataSourceStep, isStepDiffable,
isStepDiffable: isDataSourceStepDiffable, isStepRevertable: isSingleDiffStepRevertable,
isStepRevertable: isDataSourceStepRevertable,
}; };
const codeBlockConfig: HistoryBucketConfig<CodeBlockStepValue> = { const codeBlockConfig: HistoryBucketConfig<CodeBlockStepValue> = {
title: '代码块', title: '代码块',
prefix: 'cb', prefix: 'cb',
describeGroup: describeCodeBlockGroup, describeStep: (step: CodeBlockStepValue): string => describeStep(step, (content) => content?.name, '代码块'),
describeStep: describeCodeBlockStep, isStepDiffable,
isStepDiffable: isCodeBlockStepDiffable, isStepRevertable: isSingleDiffStepRevertable,
isStepRevertable: isCodeBlockStepRevertable,
}; };
/** 把"目标 step 索引"翻译成"目标 cursor"(已应用步骤数量)。 */ /** 把"目标 step 索引"翻译成"目标 cursor"(已应用步骤数量)。 */

View File

@ -5,10 +5,6 @@ import { datetimeFormatter } from '@tmagic/form';
import { useServices } from '@editor/hooks/use-services'; import { useServices } from '@editor/hooks/use-services';
import type { import type {
BaseStepValue, BaseStepValue,
CodeBlockHistoryGroup,
CodeBlockStepValue,
DataSourceHistoryGroup,
DataSourceStepValue,
HistoryOpSource, HistoryOpSource,
HistoryOpType, HistoryOpType,
HistoryRowDescriptor, HistoryRowDescriptor,
@ -227,12 +223,14 @@ export const toRowGroup = <T extends BaseStepValue = BaseStepValue>(
): HistoryRowGroup => { ): HistoryRowGroup => {
const { describeGroup, describeStep, isStepDiffable, isStepRevertable } = descriptor; const { describeGroup, describeStep, isStepDiffable, isStepRevertable } = descriptor;
const timestamp = groupTimestamp(group); const timestamp = groupTimestamp(group);
// 无 describeGroup 时回退到组内最后一步的 describeStep数据源/代码块不做相邻合并,每组恒为单步,二者等价。
const lastStep = group.steps[group.steps.length - 1]?.step;
return { return {
key, key,
applied: group.applied, applied: group.applied,
isCurrent: Boolean(group.isCurrent), isCurrent: Boolean(group.isCurrent),
opType: group.opType, opType: group.opType,
desc: describeGroup(group), desc: describeGroup ? describeGroup(group) : describeStep(lastStep),
source: groupSource(group), source: groupSource(group),
time: formatHistoryTime(timestamp), time: formatHistoryTime(timestamp),
timeTitle: formatHistoryFullTime(timestamp), timeTitle: formatHistoryFullTime(timestamp),
@ -273,30 +271,43 @@ const pickLastDescription = (descs: (string | undefined)[]): string | undefined
return undefined; return undefined;
}; };
export const describePageStep = (step: StepValue) => { /**
* / /
*
* - / label退N X
* - label · propPath diff X退N X
* / / op
* id schema.id historyDescription
*/
export const describeStep = <T>(
step: BaseStepValue<T>,
getLabel: (_schema?: T) => string | number | undefined,
unit: string,
): string => {
if (step.historyDescription) return step.historyDescription; if (step.historyDescription) return step.historyDescription;
const { opType } = step;
const items = step.diff ?? []; const items = step.diff ?? [];
if (opType === 'add') { const label = (schema?: T) => labelWithId(getLabel(schema), (schema as { id?: string | number } | undefined)?.id);
const count = items.length;
if (step.opType === 'add') {
const node = items[0]?.newSchema; const node = items[0]?.newSchema;
return `新增 ${count} 个节点${count === 1 && node ? `${labelWithId(nameOf(node), node.id)}` : ''}`; return items.length === 1 && node ? label(node) : `${items.length}${unit}`;
} }
if (opType === 'remove') { if (step.opType === 'remove') {
const count = items.length;
const node = items[0]?.oldSchema; const node = items[0]?.oldSchema;
return `删除 ${count} 个节点${count === 1 && node ? `${labelWithId(nameOf(node), node.id)}` : ''}`; return items.length === 1 && node ? label(node) : `${items.length}${unit}`;
} }
if (!items.length) return '修改节点'; if (!items.length) return unit;
if (items.length === 1) { if (items.length === 1) {
const { newSchema, changeRecords } = items[0]; const { newSchema, oldSchema, changeRecords } = items[0];
const propPath = changeRecords?.[0]?.propPath; const propPath = changeRecords?.map((changeRecord) => changeRecord.propPath).join(',');
const target = labelWithId(nameOf(newSchema), newSchema?.id); const target = label(newSchema ?? oldSchema);
return `修改 ${target}${propPath ? ` · ${propPath}` : ''}`; return propPath ? `${target} · ${propPath}` : target;
} }
return `修改 ${items.length}节点`; return `${items.length}${unit}`;
}; };
export const describePageStep = (step: StepValue): string => describeStep(step, (node) => nameOf(node), '节点');
/** /**
* *
* - historyDescription historyDescription * - historyDescription historyDescription
@ -307,68 +318,8 @@ export const describePageGroup = (group: PageHistoryGroup) => {
const lastDesc = pickLastDescription(group.steps.map((s) => s.step.historyDescription)); const lastDesc = pickLastDescription(group.steps.map((s) => s.step.historyDescription));
if (lastDesc) return lastDesc; if (lastDesc) return lastDesc;
if (group.steps.length === 1) return describePageStep(group.steps[0].step); if (group.steps.length === 1) return describePageStep(group.steps[0].step);
const paths = new Set<string>();
group.steps.forEach((s) => {
s.step.diff?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
});
const pathList = Array.from(paths).slice(0, 3).join(', ');
const target = labelWithId(
group.targetName ?? (group.targetId !== undefined ? `${group.targetId}` : '节点'),
group.targetId,
);
return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
};
export const describeDataSourceStep = (step: DataSourceStepValue) => { return labelWithId(group.targetName ?? (group.targetId !== undefined ? `${group.targetId}` : '节点'), group.targetId);
if (step.historyDescription) return step.historyDescription;
const { oldSchema: oldSchema, newSchema: newSchema, changeRecords } = step.diff?.[0] ?? {};
if (!oldSchema && newSchema) return `创建 ${labelWithId(newSchema.title, newSchema.id ?? step.id)}`;
if (!newSchema && oldSchema) return `删除 ${labelWithId(oldSchema.title, oldSchema.id ?? step.id)}`;
const propPath = changeRecords?.[0]?.propPath;
const title = labelWithId(newSchema?.title || oldSchema?.title, step.id);
return propPath ? `修改 ${title} · ${propPath}` : `修改 ${title}`;
};
export const describeDataSourceGroup = (group: DataSourceHistoryGroup) => {
const lastDesc = pickLastDescription(group.steps.map((s) => s.step.historyDescription));
if (lastDesc) return lastDesc;
if (group.steps.length === 1) return describeDataSourceStep(group.steps[0].step);
const paths = new Set<string>();
group.steps.forEach((s) => {
s.step.diff?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
});
const pathList = Array.from(paths).slice(0, 3).join(', ');
const rawTitle =
group.steps[group.steps.length - 1].step.diff?.[0]?.newSchema?.title ||
group.steps[0].step.diff?.[0]?.oldSchema?.title;
const target = labelWithId(rawTitle, group.id);
return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
};
export const describeCodeBlockStep = (step: CodeBlockStepValue) => {
if (step.historyDescription) return step.historyDescription;
const { oldSchema: oldContent, newSchema: newContent, changeRecords } = step.diff?.[0] ?? {};
if (!oldContent && newContent) return `创建 ${labelWithId(newContent.name, newContent.id ?? step.id)}`;
if (!newContent && oldContent) return `删除 ${labelWithId(oldContent.name, oldContent.id ?? step.id)}`;
const propPath = changeRecords?.[0]?.propPath;
const title = labelWithId(newContent?.name || oldContent?.name, step.id);
return propPath ? `修改 ${title} · ${propPath}` : `修改 ${title}`;
};
export const describeCodeBlockGroup = (group: CodeBlockHistoryGroup) => {
const lastDesc = pickLastDescription(group.steps.map((s) => s.step.historyDescription));
if (lastDesc) return lastDesc;
if (group.steps.length === 1) return describeCodeBlockStep(group.steps[0].step);
const paths = new Set<string>();
group.steps.forEach((s) => {
s.step.diff?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
});
const pathList = Array.from(paths).slice(0, 3).join(', ');
const rawName =
group.steps[group.steps.length - 1].step.diff?.[0]?.newSchema?.name ||
group.steps[0].step.diff?.[0]?.oldSchema?.name;
const target = labelWithId(rawName, group.id);
return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
}; };
/** /**
@ -385,22 +336,11 @@ export const isPageStepRevertable = (step: StepValue): boolean => {
}; };
/** /**
* step * diff /
* - oldSchema/ newSchema changeRecords * - oldSchema/ newSchema changeRecords
* - schema changeRecords patch * - changeRecords patch
*/ */
export const isDataSourceStepRevertable = (step: DataSourceStepValue): boolean => { export const isSingleDiffStepRevertable = (step: BaseStepValue): boolean => {
const item = step.diff?.[0];
if (!item?.oldSchema || !item?.newSchema) return true;
return Boolean(item.changeRecords?.length);
};
/**
* step
* - oldSchema/ newSchema changeRecords
* - content changeRecords patch
*/
export const isCodeBlockStepRevertable = (step: CodeBlockStepValue): boolean => {
const item = step.diff?.[0]; const item = step.diff?.[0];
if (!item?.oldSchema || !item?.newSchema) return true; if (!item?.oldSchema || !item?.newSchema) return true;
return Boolean(item.changeRecords?.length); return Boolean(item.changeRecords?.length);

View File

@ -23,9 +23,7 @@ import type { ChangeRecord } from '@tmagic/form';
import { guid } from '@tmagic/utils'; import { guid } from '@tmagic/utils';
import type { import type {
CodeBlockHistoryGroup,
CodeBlockStepValue, CodeBlockStepValue,
DataSourceHistoryGroup,
DataSourceStepValue, DataSourceStepValue,
HistoryOpSource, HistoryOpSource,
HistoryPersistOptions, HistoryPersistOptions,
@ -33,6 +31,7 @@ import type {
PageHistoryGroup, PageHistoryGroup,
PageHistoryStepEntry, PageHistoryStepEntry,
PersistedHistoryState, PersistedHistoryState,
StackHistoryGroup,
StepValue, StepValue,
} from '@editor/type'; } from '@editor/type';
import { getEditorConfig } from '@editor/utils/config'; import { getEditorConfig } from '@editor/utils/config';
@ -527,8 +526,8 @@ class History extends BaseService {
* codeBlockId * codeBlockId
* update * update
*/ */
public getCodeBlockHistoryGroups(): CodeBlockHistoryGroup[] { public getCodeBlockHistoryGroups(): StackHistoryGroup<CodeBlockStepValue, 'code-block'>[] {
const groups: CodeBlockHistoryGroup[] = []; const groups: StackHistoryGroup<CodeBlockStepValue, 'code-block'>[] = [];
Object.entries(this.state.codeBlockState).forEach(([id, undoRedo]) => { Object.entries(this.state.codeBlockState).forEach(([id, undoRedo]) => {
if (!undoRedo) return; if (!undoRedo) return;
const list = undoRedo.getElementList(); const list = undoRedo.getElementList();
@ -619,8 +618,8 @@ class History extends BaseService {
/** /**
* dataSourceId * dataSourceId
*/ */
public getDataSourceHistoryGroups(): DataSourceHistoryGroup[] { public getDataSourceHistoryGroups(): StackHistoryGroup<DataSourceStepValue, 'data-source'>[] {
const groups: DataSourceHistoryGroup[] = []; const groups: StackHistoryGroup<DataSourceStepValue, 'data-source'>[] = [];
Object.entries(this.state.dataSourceState).forEach(([id, undoRedo]) => { Object.entries(this.state.dataSourceState).forEach(([id, undoRedo]) => {
if (!undoRedo) return; if (!undoRedo) return;
const list = undoRedo.getElementList(); const list = undoRedo.getElementList();

View File

@ -962,32 +962,26 @@ export interface PageHistoryGroup {
} }
/** /**
* * / id
* - codeBlockId 'update' group * `kind` step
* - 'add' / 'remove' * - `StackHistoryGroup<DataSourceStepValue, 'data-source'>`
* - `StackHistoryGroup<CodeBlockStepValue, 'code-block'>`
*
* {@link PageHistoryGroup} `steps`
*/ */
export interface CodeBlockHistoryGroup { export interface StackHistoryGroup<
kind: 'code-block'; T extends BaseStepValue = BaseStepValue,
/** 关联的 codeBlock id */ K extends 'code-block' | 'data-source' = 'code-block' | 'data-source',
> {
/** 区分代码块 / 数据源。 */
kind: K;
/** 关联的代码块 / 数据源 id。 */
id: Id; id: Id;
/** 该分组的操作类型 */ /** 该分组的操作类型 */
opType: HistoryOpType; opType: HistoryOpType;
/** 组内所有步骤,按时间正序 */ /** 组内所有步骤,按时间正序(不做相邻合并,恒为单元素)。 */
steps: { step: CodeBlockStepValue; index: number; applied: boolean; isCurrent?: boolean }[]; steps: { step: T; index: number; applied: boolean; isCurrent?: boolean }[];
/** 组内最后一步是否已应用,用于整组的状态展示 */ /** 组内最后一步是否已应用,用于整组的状态展示。 */
applied: boolean;
/** 是否为当前所在的分组(包含该栈最近一次已应用步骤的那一组)。 */
isCurrent?: boolean;
}
/**
* CodeBlockHistoryGroup
*/
export interface DataSourceHistoryGroup {
kind: 'data-source';
id: Id;
opType: HistoryOpType;
steps: { step: DataSourceStepValue; index: number; applied: boolean; isCurrent?: boolean }[];
applied: boolean; applied: boolean;
/** 是否为当前所在的分组(包含该栈最近一次已应用步骤的那一组)。 */ /** 是否为当前所在的分组(包含该栈最近一次已应用步骤的那一组)。 */
isCurrent?: boolean; isCurrent?: boolean;
@ -1296,8 +1290,11 @@ export interface DiffDialogPayload {
* describe* / isStep* props * describe* / isStep* props
*/ */
export interface HistoryRowDescriptor<T extends BaseStepValue = BaseStepValue> { export interface HistoryRowDescriptor<T extends BaseStepValue = BaseStepValue> {
/** 组级描述文案生成器,接收一个 group返回展示文本。 */ /**
describeGroup: (_group: any) => string; * group
* 退 {@link describeStep}/
*/
describeGroup?: (_group: any) => string;
/** 单步描述文案生成器,接收一个 step返回展示文本合并组展开后的子步列表用。 */ /** 单步描述文案生成器,接收一个 step返回展示文本合并组展开后的子步列表用。 */
describeStep: (_step: T) => string; describeStep: (_step: T) => string;
/** 判断某个 step 是否可查看差异(前后值都存在)。不传则一律不展示差异入口。 */ /** 判断某个 step 是否可查看差异(前后值都存在)。不传则一律不展示差异入口。 */

View File

@ -26,9 +26,9 @@ import { guid } from '@tmagic/utils';
import type { import type {
BaseStepValue, BaseStepValue,
HistoryOpSource, HistoryOpSource,
HistoryOpType,
PageHistoryGroup, PageHistoryGroup,
PageHistoryStepEntry, PageHistoryStepEntry,
StackHistoryGroup,
StepDiffItem, StepDiffItem,
StepValue, StepValue,
} from '@editor/type'; } from '@editor/type';
@ -131,14 +131,7 @@ export const mergeStackSteps = <S extends BaseStepValue, K extends 'code-block'
id: Id, id: Id,
list: S[], list: S[],
cursor: number, cursor: number,
): { ): StackHistoryGroup<S, K>[] => {
kind: K;
id: Id;
opType: HistoryOpType;
steps: { step: S; index: number; applied: boolean; isCurrent?: boolean }[];
applied: boolean;
isCurrent?: boolean;
}[] => {
const currentIndex = cursor - 1; const currentIndex = cursor - 1;
return list.map((step, index) => { return list.map((step, index) => {
const applied = index < cursor; const applied = index < cursor;

View File

@ -8,8 +8,8 @@ import { defineComponent, h } from 'vue';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import BucketTab from '@editor/layouts/history-list/BucketTab.vue'; import BucketTab from '@editor/layouts/history-list/BucketTab.vue';
import { describeCodeBlockGroup, describeCodeBlockStep } from '@editor/layouts/history-list/composables'; import { describeStep } from '@editor/layouts/history-list/composables';
import type { CodeBlockHistoryGroup, CodeBlockStepValue } from '@editor/type'; import type { CodeBlockStepValue, HistoryBucketConfig, StackHistoryGroup } from '@editor/type';
vi.mock('@tmagic/design', () => ({ vi.mock('@tmagic/design', () => ({
TMagicScrollbar: defineComponent({ TMagicScrollbar: defineComponent({
@ -40,7 +40,7 @@ const buildGroup = (
steps: any[], steps: any[],
applied = true, applied = true,
startIndex = 0, startIndex = 0,
): CodeBlockHistoryGroup => ({ ): StackHistoryGroup<CodeBlockStepValue, 'code-block'> => ({
kind: 'code-block', kind: 'code-block',
id, id,
opType, opType,
@ -49,16 +49,17 @@ const buildGroup = (
}); });
/** 代码块 tab 复用通用 BucketTab固定注入代码块的 configtitle/prefix/describe/isStepDiffable。 */ /** 代码块 tab 复用通用 BucketTab固定注入代码块的 configtitle/prefix/describe/isStepDiffable。 */
const codeBlockConfig: HistoryBucketConfig<any> = {
title: '代码块',
prefix: 'cb',
describeStep: (step: CodeBlockStepValue): string => describeStep(step, (content) => content?.name, '代码块'),
isStepDiffable: (step: CodeBlockStepValue) => Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema),
};
const mountCodeBlockTab = (props: { buckets: any[]; expanded: Record<string, boolean> }) => const mountCodeBlockTab = (props: { buckets: any[]; expanded: Record<string, boolean> }) =>
mount(BucketTab, { mount(BucketTab, {
props: { props: {
config: { config: codeBlockConfig,
title: '代码块',
prefix: 'cb',
describeGroup: describeCodeBlockGroup,
describeStep: describeCodeBlockStep,
isStepDiffable: (step: CodeBlockStepValue) => Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema),
},
...props, ...props,
}, },
}); });
@ -83,7 +84,7 @@ describe('CodeBlockTab.vue', () => {
expect(wrapper.find('.m-editor-history-list-bucket-title code').text()).toBe('code_1'); expect(wrapper.find('.m-editor-history-list-bucket-title code').text()).toBe('code_1');
const desc = wrapper.find('.m-editor-history-list-item-desc').text(); const desc = wrapper.find('.m-editor-history-list-item-desc').text();
expect(desc).toBe('创建 fn (id: code_1)'); expect(desc).toBe('fn (id: code_1)');
}); });
test('toggle 透传key 形如 cb-${id}-${idx}', async () => { test('toggle 透传key 形如 cb-${id}-${idx}', async () => {
@ -178,7 +179,7 @@ describe('CodeBlockTab.vue', () => {
const items = wrapper.findAll('.m-editor-history-list-substeps li'); const items = wrapper.findAll('.m-editor-history-list-substeps li');
expect(items).toHaveLength(2); expect(items).toHaveLength(2);
// 子步倒序渲染最新在上params 在前content 在后 // 子步倒序渲染最新在上params 在前content 在后
expect(items[0].text()).toContain('修改 fn (id: code_1) · params'); expect(items[0].text()).toContain('fn (id: code_1) · params');
expect(items[1].text()).toContain('修改 fn (id: code_1) · content'); expect(items[1].text()).toContain('fn (id: code_1) · content');
}); });
}); });

View File

@ -8,8 +8,8 @@ import { defineComponent, h } from 'vue';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import BucketTab from '@editor/layouts/history-list/BucketTab.vue'; import BucketTab from '@editor/layouts/history-list/BucketTab.vue';
import { describeDataSourceGroup, describeDataSourceStep } from '@editor/layouts/history-list/composables'; import { describeStep } from '@editor/layouts/history-list/composables';
import type { DataSourceHistoryGroup, DataSourceStepValue } from '@editor/type'; import type { DataSourceStepValue, HistoryBucketConfig, StackHistoryGroup } from '@editor/type';
vi.mock('@tmagic/design', () => ({ vi.mock('@tmagic/design', () => ({
TMagicScrollbar: defineComponent({ TMagicScrollbar: defineComponent({
@ -40,7 +40,7 @@ const buildGroup = (
steps: any[], steps: any[],
applied = true, applied = true,
startIndex = 0, startIndex = 0,
): DataSourceHistoryGroup => ({ ): StackHistoryGroup<DataSourceStepValue, 'data-source'> => ({
kind: 'data-source', kind: 'data-source',
id, id,
opType, opType,
@ -49,16 +49,17 @@ const buildGroup = (
}); });
/** 数据源 tab 复用通用 BucketTab固定注入数据源的 configtitle/prefix/describe/isStepDiffable。 */ /** 数据源 tab 复用通用 BucketTab固定注入数据源的 configtitle/prefix/describe/isStepDiffable。 */
const dataSourceConfig: HistoryBucketConfig<any> = {
title: '数据源',
prefix: 'ds',
describeStep: (step: DataSourceStepValue): string => describeStep(step, (schema) => schema?.title, '数据源'),
isStepDiffable: (step: DataSourceStepValue) => Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema),
};
const mountDataSourceTab = (props: { buckets: any[]; expanded: Record<string, boolean> }) => const mountDataSourceTab = (props: { buckets: any[]; expanded: Record<string, boolean> }) =>
mount(BucketTab, { mount(BucketTab, {
props: { props: {
config: { config: dataSourceConfig,
title: '数据源',
prefix: 'ds',
describeGroup: describeDataSourceGroup,
describeStep: describeDataSourceStep,
isStepDiffable: (step: DataSourceStepValue) => Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema),
},
...props, ...props,
}, },
}); });
@ -92,9 +93,9 @@ describe('DataSourceTab.vue', () => {
const rows = wrapper.findAll('.m-editor-history-list-group'); const rows = wrapper.findAll('.m-editor-history-list-group');
expect(rows).toHaveLength(2); expect(rows).toHaveLength(2);
expect(rows[0].find('.m-editor-history-list-item-op').text()).toBe('新增'); expect(rows[0].find('.m-editor-history-list-item-op').text()).toBe('新增');
expect(rows[0].find('.m-editor-history-list-item-desc').text()).toBe('创建 A (id: ds_1)'); expect(rows[0].find('.m-editor-history-list-item-desc').text()).toBe('A (id: ds_1)');
expect(rows[1].find('.m-editor-history-list-item-op').text()).toBe('删除'); expect(rows[1].find('.m-editor-history-list-item-op').text()).toBe('删除');
expect(rows[1].find('.m-editor-history-list-item-desc').text()).toBe('删除 B (id: ds_2)'); expect(rows[1].find('.m-editor-history-list-item-desc').text()).toBe('B (id: ds_2)');
}); });
test('toggle 透传key 形如 ds-${id}-${idx}', async () => { test('toggle 透传key 形如 ds-${id}-${idx}', async () => {

View File

@ -158,9 +158,9 @@ describe('HistoryListPanel.vue', () => {
expect(rows.length).toBe(3); expect(rows.length).toBe(3);
const descs = rows.map((r) => r.find('.m-editor-history-list-item-desc').text()); const descs = rows.map((r) => r.find('.m-editor-history-list-item-desc').text());
expect(descs.some((t) => t.includes('新增 1 个节点'))).toBe(true); expect(descs.some((t) => t === 'A (id: n1)')).toBe(true);
expect(descs.some((t) => t === '创建 DS (id: ds_1)')).toBe(true); expect(descs.some((t) => t === 'DS (id: ds_1)')).toBe(true);
expect(descs.some((t) => t === '创建 CB (id: code_1)')).toBe(true); expect(descs.some((t) => t === 'CB (id: code_1)')).toBe(true);
}); });
test('点击合并组头部能切换 expanded 状态(不触发 goto', async () => { test('点击合并组头部能切换 expanded 状态(不触发 goto', async () => {
@ -288,7 +288,7 @@ describe('HistoryListPanel.vue', () => {
const heads = wrapper.findAll('.m-editor-history-list-group-head'); const heads = wrapper.findAll('.m-editor-history-list-group-head');
// 找到数据源 tab 那一组 // 找到数据源 tab 那一组
const dsHead = heads.find((h) => h.text().includes('创建 DS')); const dsHead = heads.find((h) => h.text().includes('DS (id: ds_1)'));
expect(dsHead).toBeTruthy(); expect(dsHead).toBeTruthy();
await dsHead!.find('.m-editor-history-list-item-goto').trigger('click'); await dsHead!.find('.m-editor-history-list-item-goto').trigger('click');
expect(dataSourceService.goto).toHaveBeenCalledWith('ds_1', 1); expect(dataSourceService.goto).toHaveBeenCalledWith('ds_1', 1);
@ -307,7 +307,7 @@ describe('HistoryListPanel.vue', () => {
await nextTick(); await nextTick();
const heads = wrapper.findAll('.m-editor-history-list-group-head'); const heads = wrapper.findAll('.m-editor-history-list-group-head');
const cbHead = heads.find((h) => h.text().includes('创建 CB')); const cbHead = heads.find((h) => h.text().includes('CB (id: code_1)'));
expect(cbHead).toBeTruthy(); expect(cbHead).toBeTruthy();
await cbHead!.find('.m-editor-history-list-item-goto').trigger('click'); await cbHead!.find('.m-editor-history-list-item-goto').trigger('click');
expect(codeBlockService.goto).toHaveBeenCalledWith('code_1', 1); expect(codeBlockService.goto).toHaveBeenCalledWith('code_1', 1);

View File

@ -71,10 +71,10 @@ describe('PageTab.vue', () => {
expect(rows).toHaveLength(2); expect(rows).toHaveLength(2);
// 第一组 add // 第一组 add
expect(rows[0].find('.m-editor-history-list-item-op').text()).toBe('新增'); expect(rows[0].find('.m-editor-history-list-item-op').text()).toBe('新增');
expect(rows[0].find('.m-editor-history-list-item-desc').text()).toContain('新增 1 个节点'); expect(rows[0].find('.m-editor-history-list-item-desc').text()).toBe('A (id: n1)');
// 第二组 update // 第二组 update
expect(rows[1].find('.m-editor-history-list-item-op').text()).toBe('修改'); expect(rows[1].find('.m-editor-history-list-item-op').text()).toBe('修改');
expect(rows[1].find('.m-editor-history-list-item-desc').text()).toBe('修改 按钮 (id: btn) · style.color'); expect(rows[1].find('.m-editor-history-list-item-desc').text()).toBe('按钮 (id: btn) · style.color');
}); });
test('step 含 timestamp 时渲染时间元素', () => { test('step 含 timestamp 时渲染时间元素', () => {

View File

@ -8,31 +8,18 @@ import { defineComponent, h } from 'vue';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { import {
describeCodeBlockGroup,
describeCodeBlockStep,
describeDataSourceGroup,
describeDataSourceStep,
describePageGroup, describePageGroup,
describePageStep, describePageStep,
formatHistoryFullTime, formatHistoryFullTime,
formatHistoryTime, formatHistoryTime,
groupTimestamp, groupTimestamp,
isCodeBlockStepRevertable,
isDataSourceStepRevertable,
isPageStepRevertable, isPageStepRevertable,
isSingleDiffStepRevertable,
opLabel, opLabel,
useHistoryList, useHistoryList,
} from '@editor/layouts/history-list/composables'; } from '@editor/layouts/history-list/composables';
import historyService from '@editor/services/history'; import historyService from '@editor/services/history';
import type { import type { PageHistoryGroup, PageHistoryStepEntry, StepValue } from '@editor/type';
CodeBlockHistoryGroup,
CodeBlockStepValue,
DataSourceHistoryGroup,
DataSourceStepValue,
PageHistoryGroup,
PageHistoryStepEntry,
StepValue,
} from '@editor/type';
afterEach(() => { afterEach(() => {
historyService.reset(); historyService.reset();
@ -111,7 +98,7 @@ describe('describePageStep', () => {
opType: 'add', opType: 'add',
diff: [{ newSchema: { id: 'btn_1', type: 'button', name: '主按钮' } }], diff: [{ newSchema: { id: 'btn_1', type: 'button', name: '主按钮' } }],
} as unknown as StepValue; } as unknown as StepValue;
expect(describePageStep(step)).toBe('新增 1 个节点(主按钮 (id: btn_1)'); expect(describePageStep(step)).toBe('主按钮 (id: btn_1)');
}); });
test('add 节点无 name 但有 type使用 type 作为名称', () => { test('add 节点无 name 但有 type使用 type 作为名称', () => {
@ -119,7 +106,7 @@ describe('describePageStep', () => {
opType: 'add', opType: 'add',
diff: [{ newSchema: { id: 'n1', type: 'text' } }], diff: [{ newSchema: { id: 'n1', type: 'text' } }],
} as unknown as StepValue; } as unknown as StepValue;
expect(describePageStep(step)).toBe('新增 1 个节点(text (id: n1)'); expect(describePageStep(step)).toBe('text (id: n1)');
}); });
test('add 节点 name 与 id 相同:仅显示 id', () => { test('add 节点 name 与 id 相同:仅显示 id', () => {
@ -127,7 +114,7 @@ describe('describePageStep', () => {
opType: 'add', opType: 'add',
diff: [{ newSchema: { id: 'n1', name: 'n1' } }], diff: [{ newSchema: { id: 'n1', name: 'n1' } }],
} as unknown as StepValue; } as unknown as StepValue;
expect(describePageStep(step)).toBe('新增 1 个节点(n1'); expect(describePageStep(step)).toBe('n1');
}); });
test('add 多个节点:仅给出数量', () => { test('add 多个节点:仅给出数量', () => {
@ -135,12 +122,12 @@ describe('describePageStep', () => {
opType: 'add', opType: 'add',
diff: [{ newSchema: { id: 'a' } }, { newSchema: { id: 'b' } }], diff: [{ newSchema: { id: 'a' } }, { newSchema: { id: 'b' } }],
} as unknown as StepValue; } as unknown as StepValue;
expect(describePageStep(step)).toBe('新增 2 个节点'); expect(describePageStep(step)).toBe('2 个节点');
}); });
test('add 无 nodescount 为 0 且不附名称', () => { test('add 无 nodescount 为 0 且不附名称', () => {
const step = { opType: 'add' } as unknown as StepValue; const step = { opType: 'add' } as unknown as StepValue;
expect(describePageStep(step)).toBe('新增 0 个节点'); expect(describePageStep(step)).toBe('0 个节点');
}); });
test('remove 单个节点:含名称与 id', () => { test('remove 单个节点:含名称与 id', () => {
@ -148,7 +135,7 @@ describe('describePageStep', () => {
opType: 'remove', opType: 'remove',
diff: [{ oldSchema: { id: 'btn_1', name: '主按钮' } }], diff: [{ oldSchema: { id: 'btn_1', name: '主按钮' } }],
} as unknown as StepValue; } as unknown as StepValue;
expect(describePageStep(step)).toBe('删除 1 个节点(主按钮 (id: btn_1)'); expect(describePageStep(step)).toBe('主按钮 (id: btn_1)');
}); });
test('remove 多个节点', () => { test('remove 多个节点', () => {
@ -156,7 +143,7 @@ describe('describePageStep', () => {
opType: 'remove', opType: 'remove',
diff: [{ oldSchema: { id: 'a' } }, { oldSchema: { id: 'b' } }], diff: [{ oldSchema: { id: 'a' } }, { oldSchema: { id: 'b' } }],
} as unknown as StepValue; } as unknown as StepValue;
expect(describePageStep(step)).toBe('删除 2 个节点'); expect(describePageStep(step)).toBe('2 个节点');
}); });
test('update 单节点:附 propPath 与 id', () => { test('update 单节点:附 propPath 与 id', () => {
@ -170,7 +157,7 @@ describe('describePageStep', () => {
}, },
], ],
} as unknown as StepValue; } as unknown as StepValue;
expect(describePageStep(step)).toBe('修改 按钮 (id: btn_1) · style.color'); expect(describePageStep(step)).toBe('按钮 (id: btn_1) · style.color');
}); });
test('update 单节点无 propPath仅展示节点', () => { test('update 单节点无 propPath仅展示节点', () => {
@ -178,7 +165,7 @@ describe('describePageStep', () => {
opType: 'update', opType: 'update',
diff: [{ newSchema: { id: 'btn_1', name: '按钮' }, oldSchema: { id: 'btn_1' } }], diff: [{ newSchema: { id: 'btn_1', name: '按钮' }, oldSchema: { id: 'btn_1' } }],
} as unknown as StepValue; } as unknown as StepValue;
expect(describePageStep(step)).toBe('修改 按钮 (id: btn_1)'); expect(describePageStep(step)).toBe('按钮 (id: btn_1)');
}); });
test('update 多节点:返回数量', () => { test('update 多节点:返回数量', () => {
@ -189,12 +176,12 @@ describe('describePageStep', () => {
{ newSchema: { id: 'b' }, oldSchema: { id: 'b' } }, { newSchema: { id: 'b' }, oldSchema: { id: 'b' } },
], ],
} as unknown as StepValue; } as unknown as StepValue;
expect(describePageStep(step)).toBe('修改 2 个节点'); expect(describePageStep(step)).toBe('2 个节点');
}); });
test('update diff 缺省:兜底为「修改节点」', () => { test('update diff 缺省:兜底为「节点」', () => {
const step = { opType: 'update' } as unknown as StepValue; const step = { opType: 'update' } as unknown as StepValue;
expect(describePageStep(step)).toBe('修改节点'); expect(describePageStep(step)).toBe('节点');
}); });
}); });
@ -230,10 +217,10 @@ describe('describePageGroup', () => {
applied: true, applied: true,
steps: [buildPageEntry(step)], steps: [buildPageEntry(step)],
}; };
expect(describePageGroup(group)).toBe('修改 A (id: a)'); expect(describePageGroup(group)).toBe('A (id: a)');
}); });
test('多步合并组:聚合 propPath 列表', () => { test('多步合并组:展示目标名称与 id', () => {
const mkStep = (path: string) => const mkStep = (path: string) =>
({ ({
opType: 'update', opType: 'update',
@ -255,10 +242,10 @@ describe('describePageGroup', () => {
applied: true, applied: true,
steps: [buildPageEntry(mkStep('style.color'), 0), buildPageEntry(mkStep('style.fontSize'), 1)], steps: [buildPageEntry(mkStep('style.color'), 0), buildPageEntry(mkStep('style.fontSize'), 1)],
}; };
expect(describePageGroup(group)).toBe('修改 按钮 (id: btn_1) · style.color, style.fontSize'); expect(describePageGroup(group)).toBe('按钮 (id: btn_1)');
}); });
test('多步合并组:超过 3 个 propPath 时截断并加省略号', () => { test('多步合并组:多步时仍仅展示目标', () => {
const mkStep = (path: string) => const mkStep = (path: string) =>
({ ({
opType: 'update', opType: 'update',
@ -285,9 +272,7 @@ describe('describePageGroup', () => {
buildPageEntry(mkStep('d'), 3), buildPageEntry(mkStep('d'), 3),
], ],
}; };
const desc = describePageGroup(group); expect(describePageGroup(group)).toBe('按钮 (id: btn_1)');
expect(desc).toContain('修改 按钮 (id: btn_1) · a, b, c');
expect(desc.endsWith('…')).toBe(true);
}); });
test('多步合并组无 propPath 时仅展示目标', () => { test('多步合并组无 propPath 时仅展示目标', () => {
@ -306,7 +291,7 @@ describe('describePageGroup', () => {
applied: true, applied: true,
steps: [buildPageEntry(mkStep(), 0), buildPageEntry(mkStep(), 1)], steps: [buildPageEntry(mkStep(), 0), buildPageEntry(mkStep(), 1)],
}; };
expect(describePageGroup(group)).toBe('修改 按钮 (id: btn_1)'); expect(describePageGroup(group)).toBe('按钮 (id: btn_1)');
}); });
test('多步组 targetName 缺省时使用 targetId 兜底', () => { test('多步组 targetName 缺省时使用 targetId 兜底', () => {
@ -322,226 +307,7 @@ describe('describePageGroup', () => {
], ],
}; };
// targetName 为 undefinedlabelWithId 看 label === id 时只展示 id // targetName 为 undefinedlabelWithId 看 label === id 时只展示 id
expect(describePageGroup(group)).toBe('修改 btn_1'); expect(describePageGroup(group)).toBe('btn_1');
});
});
describe('describeDataSourceStep', () => {
test('historyDescription 优先', () => {
const step = {
id: 'ds_1',
opType: 'update',
diff: [{}],
historyDescription: '自定义',
} as unknown as DataSourceStepValue;
expect(describeDataSourceStep(step)).toBe('自定义');
});
test('新增oldSchema=null展示 title 与 id', () => {
const step = {
id: 'ds_1',
opType: 'add',
diff: [{ newSchema: { id: 'ds_1', title: '用户列表' } }],
} as unknown as DataSourceStepValue;
expect(describeDataSourceStep(step)).toBe('创建 用户列表 (id: ds_1)');
});
test('删除newSchema=null展示 title 与 id', () => {
const step = {
id: 'ds_1',
opType: 'remove',
diff: [{ oldSchema: { id: 'ds_1', title: '用户列表' } }],
} as unknown as DataSourceStepValue;
expect(describeDataSourceStep(step)).toBe('删除 用户列表 (id: ds_1)');
});
test('修改:展示 propPath', () => {
const step = {
id: 'ds_1',
opType: 'update',
diff: [
{
oldSchema: { id: 'ds_1', title: '用户列表' },
newSchema: { id: 'ds_1', title: '用户列表' },
changeRecords: [{ propPath: 'fields.0.name' }],
},
],
} as unknown as DataSourceStepValue;
expect(describeDataSourceStep(step)).toBe('修改 用户列表 (id: ds_1) · fields.0.name');
});
test('修改无 title 时仅展示 id', () => {
const step = {
id: 'ds_1',
opType: 'update',
diff: [{ oldSchema: { id: 'ds_1' }, newSchema: { id: 'ds_1' } }],
} as unknown as DataSourceStepValue;
expect(describeDataSourceStep(step)).toBe('修改 ds_1');
});
});
describe('describeDataSourceGroup', () => {
test('多步组:聚合 propPath 与目标 id', () => {
const mkStep = (path: string) =>
({
id: 'ds_1',
opType: 'update',
diff: [
{
oldSchema: { id: 'ds_1', title: 'T' },
newSchema: { id: 'ds_1', title: 'T' },
changeRecords: [{ propPath: path }],
},
],
}) as unknown as DataSourceStepValue;
const group: DataSourceHistoryGroup = {
kind: 'data-source',
id: 'ds_1',
opType: 'update',
applied: true,
steps: [
{ step: mkStep('a'), index: 0, applied: true },
{ step: mkStep('b'), index: 1, applied: true },
],
};
expect(describeDataSourceGroup(group)).toBe('修改 T (id: ds_1) · a, b');
});
test('单步组:复用 describeDataSourceStep', () => {
const group: DataSourceHistoryGroup = {
kind: 'data-source',
id: 'ds_1',
opType: 'add',
applied: true,
steps: [
{
step: {
id: 'ds_1',
opType: 'add',
diff: [{ newSchema: { id: 'ds_1', title: 'T' } }],
} as unknown as DataSourceStepValue,
index: 0,
applied: true,
},
],
};
expect(describeDataSourceGroup(group)).toBe('创建 T (id: ds_1)');
});
test('historyDescription 优先', () => {
const group: DataSourceHistoryGroup = {
kind: 'data-source',
id: 'ds_1',
opType: 'update',
applied: true,
steps: [
{
step: {
id: 'ds_1',
opType: 'update',
diff: [{}],
historyDescription: '我的描述',
} as unknown as DataSourceStepValue,
index: 0,
applied: true,
},
],
};
expect(describeDataSourceGroup(group)).toBe('我的描述');
});
});
describe('describeCodeBlockStep', () => {
test('新增', () => {
const step = {
id: 'code_1',
opType: 'add',
diff: [{ newSchema: { id: 'code_1', name: 'onClick' } }],
} as unknown as CodeBlockStepValue;
expect(describeCodeBlockStep(step)).toBe('创建 onClick (id: code_1)');
});
test('删除', () => {
const step = {
id: 'code_1',
opType: 'remove',
diff: [{ oldSchema: { id: 'code_1', name: 'onClick' } }],
} as unknown as CodeBlockStepValue;
expect(describeCodeBlockStep(step)).toBe('删除 onClick (id: code_1)');
});
test('修改 + propPath', () => {
const step = {
id: 'code_1',
opType: 'update',
diff: [
{
oldSchema: { id: 'code_1', name: 'onClick' },
newSchema: { id: 'code_1', name: 'onClick' },
changeRecords: [{ propPath: 'content' }],
},
],
} as unknown as CodeBlockStepValue;
expect(describeCodeBlockStep(step)).toBe('修改 onClick (id: code_1) · content');
});
test('historyDescription 优先', () => {
const step = {
id: 'code_1',
opType: 'update',
diff: [{}],
historyDescription: '自定义说明',
} as unknown as CodeBlockStepValue;
expect(describeCodeBlockStep(step)).toBe('自定义说明');
});
});
describe('describeCodeBlockGroup', () => {
test('多步组:聚合 propPath', () => {
const mkStep = (path: string) =>
({
id: 'code_1',
opType: 'update',
diff: [
{
oldSchema: { id: 'code_1', name: 'fn' },
newSchema: { id: 'code_1', name: 'fn' },
changeRecords: [{ propPath: path }],
},
],
}) as unknown as CodeBlockStepValue;
const group: CodeBlockHistoryGroup = {
kind: 'code-block',
id: 'code_1',
opType: 'update',
applied: true,
steps: [
{ step: mkStep('content'), index: 0, applied: true },
{ step: mkStep('params'), index: 1, applied: true },
],
};
expect(describeCodeBlockGroup(group)).toBe('修改 fn (id: code_1) · content, params');
});
test('单步组:复用 step 描述', () => {
const group: CodeBlockHistoryGroup = {
kind: 'code-block',
id: 'code_1',
opType: 'remove',
applied: false,
steps: [
{
step: {
id: 'code_1',
opType: 'remove',
diff: [{ oldSchema: { id: 'code_1', name: 'fn' } }],
} as unknown as CodeBlockStepValue,
index: 0,
applied: false,
},
],
};
expect(describeCodeBlockGroup(group)).toBe('删除 fn (id: code_1)');
}); });
}); });
@ -684,38 +450,20 @@ describe('isPageStepRevertable', () => {
}); });
}); });
describe('isDataSourceStepRevertable', () => { describe('isSingleDiffStepRevertable', () => {
test('新增 / 删除 始终可回滚', () => { test('新增 / 删除 始终可回滚', () => {
expect(isDataSourceStepRevertable({ diff: [{ newSchema: { id: 'ds_1' } }] } as any)).toBe(true); expect(isSingleDiffStepRevertable({ diff: [{ newSchema: { id: 'ds_1' } }] } as any)).toBe(true);
expect(isDataSourceStepRevertable({ diff: [{ oldSchema: { id: 'ds_1' } }] } as any)).toBe(true); expect(isSingleDiffStepRevertable({ diff: [{ oldSchema: { id: 'ds_1' } }] } as any)).toBe(true);
}); });
test('更新有 changeRecords 才可回滚', () => { test('更新有 changeRecords 才可回滚', () => {
expect( expect(
isDataSourceStepRevertable({ isSingleDiffStepRevertable({
diff: [{ oldSchema: { id: 'ds_1' }, newSchema: { id: 'ds_1' }, changeRecords: [{ propPath: 'title' }] }], diff: [{ oldSchema: { id: 'ds_1' }, newSchema: { id: 'ds_1' }, changeRecords: [{ propPath: 'title' }] }],
} as any), } as any),
).toBe(true); ).toBe(true);
expect( expect(
isDataSourceStepRevertable({ diff: [{ oldSchema: { id: 'ds_1' }, newSchema: { id: 'ds_1' } }] } as any), isSingleDiffStepRevertable({ diff: [{ oldSchema: { id: 'ds_1' }, newSchema: { id: 'ds_1' } }] } as any),
).toBe(false);
});
});
describe('isCodeBlockStepRevertable', () => {
test('新增 / 删除 始终可回滚', () => {
expect(isCodeBlockStepRevertable({ diff: [{ newSchema: { id: 'code_1' } }] } as any)).toBe(true);
expect(isCodeBlockStepRevertable({ diff: [{ oldSchema: { id: 'code_1' } }] } as any)).toBe(true);
});
test('更新有 changeRecords 才可回滚', () => {
expect(
isCodeBlockStepRevertable({
diff: [{ oldSchema: { id: 'code_1' }, newSchema: { id: 'code_1' }, changeRecords: [{ propPath: 'content' }] }],
} as any),
).toBe(true);
expect(
isCodeBlockStepRevertable({ diff: [{ oldSchema: { id: 'code_1' }, newSchema: { id: 'code_1' } }] } as any),
).toBe(false); ).toBe(false);
}); });
}); });