mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-06-25 17:49:28 +08:00
fix(editor): 修复历史记录面板分组与展示逻辑
统一历史记录列表在不同维度下的分组与展示行为,避免对比信息与交互状态不一致,并补齐对应单测覆盖。
This commit is contained in:
parent
24b9b34f65
commit
bbf79fd6df
@ -135,6 +135,7 @@ import type { FormState } from '@tmagic/form';
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import type {
|
||||
BaseStepValue,
|
||||
CodeBlockStepValue,
|
||||
DataSourceStepValue,
|
||||
DiffDialogPayload,
|
||||
@ -143,15 +144,7 @@ import type {
|
||||
} from '@editor/type';
|
||||
|
||||
import BucketTab from './BucketTab.vue';
|
||||
import {
|
||||
describeCodeBlockGroup,
|
||||
describeCodeBlockStep,
|
||||
describeDataSourceGroup,
|
||||
describeDataSourceStep,
|
||||
isCodeBlockStepRevertable,
|
||||
isDataSourceStepRevertable,
|
||||
useHistoryList,
|
||||
} from './composables';
|
||||
import { describeStep, isSingleDiffStepRevertable, useHistoryList } from './composables';
|
||||
import HistoryDiffDialog from './HistoryDiffDialog.vue';
|
||||
import PageTab from './PageTab.vue';
|
||||
|
||||
@ -223,34 +216,28 @@ const {
|
||||
*/
|
||||
const pageMarker = computed(() => historyService.getPageMarker());
|
||||
|
||||
/** 数据源 step 仅 update(前后 schema 都存在)时可查看差异。 */
|
||||
const isDataSourceStepDiffable = (step: DataSourceStepValue) =>
|
||||
Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
|
||||
|
||||
/** 代码块 step 仅 update(前后 content 都存在)时可查看差异。 */
|
||||
const isCodeBlockStepDiffable = (step: CodeBlockStepValue) =>
|
||||
Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
|
||||
const isStepDiffable = (step: BaseStepValue) => Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
|
||||
|
||||
/**
|
||||
* 数据源 / 代码块两类 bucket 历史的整体渲染配置:把 title / prefix 与各自的描述、
|
||||
* 可差异、可回滚判定收敛为单一对象整体注入 BucketTab,组件内部按需读取。
|
||||
*/
|
||||
// 数据源/代码块不做相邻合并,每组恒为单步,省略 describeGroup,由 toRowGroup 回退到 describeStep。
|
||||
const dataSourceConfig: HistoryBucketConfig<DataSourceStepValue> = {
|
||||
title: '数据源',
|
||||
prefix: 'ds',
|
||||
describeGroup: describeDataSourceGroup,
|
||||
describeStep: describeDataSourceStep,
|
||||
isStepDiffable: isDataSourceStepDiffable,
|
||||
isStepRevertable: isDataSourceStepRevertable,
|
||||
describeStep: (step: DataSourceStepValue): string => describeStep(step, (schema) => schema?.title, '数据源'),
|
||||
isStepDiffable,
|
||||
isStepRevertable: isSingleDiffStepRevertable,
|
||||
};
|
||||
|
||||
const codeBlockConfig: HistoryBucketConfig<CodeBlockStepValue> = {
|
||||
title: '代码块',
|
||||
prefix: 'cb',
|
||||
describeGroup: describeCodeBlockGroup,
|
||||
describeStep: describeCodeBlockStep,
|
||||
isStepDiffable: isCodeBlockStepDiffable,
|
||||
isStepRevertable: isCodeBlockStepRevertable,
|
||||
describeStep: (step: CodeBlockStepValue): string => describeStep(step, (content) => content?.name, '代码块'),
|
||||
isStepDiffable,
|
||||
isStepRevertable: isSingleDiffStepRevertable,
|
||||
};
|
||||
|
||||
/** 把"目标 step 索引"翻译成"目标 cursor"(已应用步骤数量)。 */
|
||||
|
||||
@ -5,10 +5,6 @@ import { datetimeFormatter } from '@tmagic/form';
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import type {
|
||||
BaseStepValue,
|
||||
CodeBlockHistoryGroup,
|
||||
CodeBlockStepValue,
|
||||
DataSourceHistoryGroup,
|
||||
DataSourceStepValue,
|
||||
HistoryOpSource,
|
||||
HistoryOpType,
|
||||
HistoryRowDescriptor,
|
||||
@ -227,12 +223,14 @@ export const toRowGroup = <T extends BaseStepValue = BaseStepValue>(
|
||||
): HistoryRowGroup => {
|
||||
const { describeGroup, describeStep, isStepDiffable, isStepRevertable } = descriptor;
|
||||
const timestamp = groupTimestamp(group);
|
||||
// 无 describeGroup 时回退到组内最后一步的 describeStep:数据源/代码块不做相邻合并,每组恒为单步,二者等价。
|
||||
const lastStep = group.steps[group.steps.length - 1]?.step;
|
||||
return {
|
||||
key,
|
||||
applied: group.applied,
|
||||
isCurrent: Boolean(group.isCurrent),
|
||||
opType: group.opType,
|
||||
desc: describeGroup(group),
|
||||
desc: describeGroup ? describeGroup(group) : describeStep(lastStep),
|
||||
source: groupSource(group),
|
||||
time: formatHistoryTime(timestamp),
|
||||
timeTitle: formatHistoryFullTime(timestamp),
|
||||
@ -273,30 +271,43 @@ const pickLastDescription = (descs: (string | undefined)[]): string | 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;
|
||||
const { opType } = step;
|
||||
const items = step.diff ?? [];
|
||||
if (opType === 'add') {
|
||||
const count = items.length;
|
||||
const label = (schema?: T) => labelWithId(getLabel(schema), (schema as { id?: string | number } | undefined)?.id);
|
||||
|
||||
if (step.opType === 'add') {
|
||||
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') {
|
||||
const count = items.length;
|
||||
if (step.opType === 'remove') {
|
||||
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) {
|
||||
const { newSchema, changeRecords } = items[0];
|
||||
const propPath = changeRecords?.[0]?.propPath;
|
||||
const target = labelWithId(nameOf(newSchema), newSchema?.id);
|
||||
return `修改 ${target}${propPath ? ` · ${propPath}` : ''}`;
|
||||
const { newSchema, oldSchema, changeRecords } = items[0];
|
||||
const propPath = changeRecords?.map((changeRecord) => changeRecord.propPath).join(',');
|
||||
const target = label(newSchema ?? oldSchema);
|
||||
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(最近一次的描述更准确);
|
||||
@ -307,68 +318,8 @@ export const describePageGroup = (group: PageHistoryGroup) => {
|
||||
const lastDesc = pickLastDescription(group.steps.map((s) => s.step.historyDescription));
|
||||
if (lastDesc) return lastDesc;
|
||||
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) => {
|
||||
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}`;
|
||||
return labelWithId(group.targetName ?? (group.targetId !== undefined ? `${group.targetId}` : '节点'), group.targetId);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -385,22 +336,11 @@ export const isPageStepRevertable = (step: StepValue): boolean => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 数据源 step 是否支持「回滚」:
|
||||
* 单 diff 项历史(数据源 / 代码块)是否支持「回滚」:
|
||||
* - 新增(无 oldSchema)/ 删除(无 newSchema):不依赖 changeRecords,始终可回滚;
|
||||
* - 更新(前后 schema 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
|
||||
* - 更新(前后内容都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
|
||||
*/
|
||||
export const isDataSourceStepRevertable = (step: DataSourceStepValue): 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 => {
|
||||
export const isSingleDiffStepRevertable = (step: BaseStepValue): boolean => {
|
||||
const item = step.diff?.[0];
|
||||
if (!item?.oldSchema || !item?.newSchema) return true;
|
||||
return Boolean(item.changeRecords?.length);
|
||||
|
||||
@ -23,9 +23,7 @@ import type { ChangeRecord } from '@tmagic/form';
|
||||
import { guid } from '@tmagic/utils';
|
||||
|
||||
import type {
|
||||
CodeBlockHistoryGroup,
|
||||
CodeBlockStepValue,
|
||||
DataSourceHistoryGroup,
|
||||
DataSourceStepValue,
|
||||
HistoryOpSource,
|
||||
HistoryPersistOptions,
|
||||
@ -33,6 +31,7 @@ import type {
|
||||
PageHistoryGroup,
|
||||
PageHistoryStepEntry,
|
||||
PersistedHistoryState,
|
||||
StackHistoryGroup,
|
||||
StepValue,
|
||||
} from '@editor/type';
|
||||
import { getEditorConfig } from '@editor/utils/config';
|
||||
@ -527,8 +526,8 @@ class History extends BaseService {
|
||||
* 取出全部代码块的历史栈,按 codeBlockId 分桶展示。
|
||||
* 同一栈内每条操作记录独立成组,不做相邻 update 合并。
|
||||
*/
|
||||
public getCodeBlockHistoryGroups(): CodeBlockHistoryGroup[] {
|
||||
const groups: CodeBlockHistoryGroup[] = [];
|
||||
public getCodeBlockHistoryGroups(): StackHistoryGroup<CodeBlockStepValue, 'code-block'>[] {
|
||||
const groups: StackHistoryGroup<CodeBlockStepValue, 'code-block'>[] = [];
|
||||
Object.entries(this.state.codeBlockState).forEach(([id, undoRedo]) => {
|
||||
if (!undoRedo) return;
|
||||
const list = undoRedo.getElementList();
|
||||
@ -619,8 +618,8 @@ class History extends BaseService {
|
||||
/**
|
||||
* 取出全部数据源的历史栈,按 dataSourceId 分桶展示。同上,每条操作独立成组。
|
||||
*/
|
||||
public getDataSourceHistoryGroups(): DataSourceHistoryGroup[] {
|
||||
const groups: DataSourceHistoryGroup[] = [];
|
||||
public getDataSourceHistoryGroups(): StackHistoryGroup<DataSourceStepValue, 'data-source'>[] {
|
||||
const groups: StackHistoryGroup<DataSourceStepValue, 'data-source'>[] = [];
|
||||
Object.entries(this.state.dataSourceState).forEach(([id, undoRedo]) => {
|
||||
if (!undoRedo) return;
|
||||
const list = undoRedo.getElementList();
|
||||
|
||||
@ -962,32 +962,26 @@ export interface PageHistoryGroup {
|
||||
}
|
||||
|
||||
/**
|
||||
* 代码块历史面板分组。
|
||||
* - 同一 codeBlockId 的栈内,相邻的 'update' 操作会合并成一个 group;
|
||||
* - 'add' / 'remove' 始终独立成组(语义上是一次性事件)。
|
||||
* 数据源 / 代码块历史面板分组(按 id 分栈展示)。
|
||||
* 二者结构完全一致,仅 `kind` 与 step 类型不同,统一由该泛型描述:
|
||||
* - 数据源:`StackHistoryGroup<DataSourceStepValue, 'data-source'>`;
|
||||
* - 代码块:`StackHistoryGroup<CodeBlockStepValue, 'code-block'>`。
|
||||
*
|
||||
* 每条操作记录独立成组,不做相邻合并(与页面历史 {@link PageHistoryGroup} 不同),故 `steps` 恒为单元素。
|
||||
*/
|
||||
export interface CodeBlockHistoryGroup {
|
||||
kind: 'code-block';
|
||||
/** 关联的 codeBlock id */
|
||||
export interface StackHistoryGroup<
|
||||
T extends BaseStepValue = BaseStepValue,
|
||||
K extends 'code-block' | 'data-source' = 'code-block' | 'data-source',
|
||||
> {
|
||||
/** 区分代码块 / 数据源。 */
|
||||
kind: K;
|
||||
/** 关联的代码块 / 数据源 id。 */
|
||||
id: Id;
|
||||
/** 该分组的操作类型 */
|
||||
/** 该分组的操作类型。 */
|
||||
opType: HistoryOpType;
|
||||
/** 组内所有步骤,按时间正序 */
|
||||
steps: { step: CodeBlockStepValue; 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 }[];
|
||||
/** 组内所有步骤,按时间正序(不做相邻合并,恒为单元素)。 */
|
||||
steps: { step: T; index: number; applied: boolean; isCurrent?: boolean }[];
|
||||
/** 组内最后一步是否已应用,用于整组的状态展示。 */
|
||||
applied: boolean;
|
||||
/** 是否为当前所在的分组(包含该栈最近一次已应用步骤的那一组)。 */
|
||||
isCurrent?: boolean;
|
||||
@ -1296,8 +1290,11 @@ export interface DiffDialogPayload {
|
||||
* 各自实现一份,作为整体注入,避免把 describe* / isStep* 拆成多个独立 props 反复透传。
|
||||
*/
|
||||
export interface HistoryRowDescriptor<T extends BaseStepValue = BaseStepValue> {
|
||||
/** 组级描述文案生成器,接收一个 group,返回展示文本。 */
|
||||
describeGroup: (_group: any) => string;
|
||||
/**
|
||||
* 组级描述文案生成器,接收一个 group,返回展示文本。
|
||||
* 不传时回退到对组内最后一步调用 {@link describeStep}(适用于不做相邻合并、每组恒为单步的历史,如数据源/代码块)。
|
||||
*/
|
||||
describeGroup?: (_group: any) => string;
|
||||
/** 单步描述文案生成器,接收一个 step,返回展示文本(合并组展开后的子步列表用)。 */
|
||||
describeStep: (_step: T) => string;
|
||||
/** 判断某个 step 是否可查看差异(前后值都存在)。不传则一律不展示差异入口。 */
|
||||
|
||||
@ -26,9 +26,9 @@ import { guid } from '@tmagic/utils';
|
||||
import type {
|
||||
BaseStepValue,
|
||||
HistoryOpSource,
|
||||
HistoryOpType,
|
||||
PageHistoryGroup,
|
||||
PageHistoryStepEntry,
|
||||
StackHistoryGroup,
|
||||
StepDiffItem,
|
||||
StepValue,
|
||||
} from '@editor/type';
|
||||
@ -131,14 +131,7 @@ export const mergeStackSteps = <S extends BaseStepValue, K extends 'code-block'
|
||||
id: Id,
|
||||
list: S[],
|
||||
cursor: number,
|
||||
): {
|
||||
kind: K;
|
||||
id: Id;
|
||||
opType: HistoryOpType;
|
||||
steps: { step: S; index: number; applied: boolean; isCurrent?: boolean }[];
|
||||
applied: boolean;
|
||||
isCurrent?: boolean;
|
||||
}[] => {
|
||||
): StackHistoryGroup<S, K>[] => {
|
||||
const currentIndex = cursor - 1;
|
||||
return list.map((step, index) => {
|
||||
const applied = index < cursor;
|
||||
|
||||
@ -8,8 +8,8 @@ import { defineComponent, h } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import BucketTab from '@editor/layouts/history-list/BucketTab.vue';
|
||||
import { describeCodeBlockGroup, describeCodeBlockStep } from '@editor/layouts/history-list/composables';
|
||||
import type { CodeBlockHistoryGroup, CodeBlockStepValue } from '@editor/type';
|
||||
import { describeStep } from '@editor/layouts/history-list/composables';
|
||||
import type { CodeBlockStepValue, HistoryBucketConfig, StackHistoryGroup } from '@editor/type';
|
||||
|
||||
vi.mock('@tmagic/design', () => ({
|
||||
TMagicScrollbar: defineComponent({
|
||||
@ -40,7 +40,7 @@ const buildGroup = (
|
||||
steps: any[],
|
||||
applied = true,
|
||||
startIndex = 0,
|
||||
): CodeBlockHistoryGroup => ({
|
||||
): StackHistoryGroup<CodeBlockStepValue, 'code-block'> => ({
|
||||
kind: 'code-block',
|
||||
id,
|
||||
opType,
|
||||
@ -49,16 +49,17 @@ const buildGroup = (
|
||||
});
|
||||
|
||||
/** 代码块 tab 复用通用 BucketTab,固定注入代码块的 config(title/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> }) =>
|
||||
mount(BucketTab, {
|
||||
props: {
|
||||
config: {
|
||||
title: '代码块',
|
||||
prefix: 'cb',
|
||||
describeGroup: describeCodeBlockGroup,
|
||||
describeStep: describeCodeBlockStep,
|
||||
isStepDiffable: (step: CodeBlockStepValue) => Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema),
|
||||
},
|
||||
config: codeBlockConfig,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
@ -83,7 +84,7 @@ describe('CodeBlockTab.vue', () => {
|
||||
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();
|
||||
expect(desc).toBe('创建 fn (id: code_1)');
|
||||
expect(desc).toBe('fn (id: code_1)');
|
||||
});
|
||||
|
||||
test('toggle 透传:key 形如 cb-${id}-${idx}', async () => {
|
||||
@ -178,7 +179,7 @@ describe('CodeBlockTab.vue', () => {
|
||||
const items = wrapper.findAll('.m-editor-history-list-substeps li');
|
||||
expect(items).toHaveLength(2);
|
||||
// 子步倒序渲染(最新在上):params 在前,content 在后
|
||||
expect(items[0].text()).toContain('修改 fn (id: code_1) · params');
|
||||
expect(items[1].text()).toContain('修改 fn (id: code_1) · content');
|
||||
expect(items[0].text()).toContain('fn (id: code_1) · params');
|
||||
expect(items[1].text()).toContain('fn (id: code_1) · content');
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,8 +8,8 @@ import { defineComponent, h } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import BucketTab from '@editor/layouts/history-list/BucketTab.vue';
|
||||
import { describeDataSourceGroup, describeDataSourceStep } from '@editor/layouts/history-list/composables';
|
||||
import type { DataSourceHistoryGroup, DataSourceStepValue } from '@editor/type';
|
||||
import { describeStep } from '@editor/layouts/history-list/composables';
|
||||
import type { DataSourceStepValue, HistoryBucketConfig, StackHistoryGroup } from '@editor/type';
|
||||
|
||||
vi.mock('@tmagic/design', () => ({
|
||||
TMagicScrollbar: defineComponent({
|
||||
@ -40,7 +40,7 @@ const buildGroup = (
|
||||
steps: any[],
|
||||
applied = true,
|
||||
startIndex = 0,
|
||||
): DataSourceHistoryGroup => ({
|
||||
): StackHistoryGroup<DataSourceStepValue, 'data-source'> => ({
|
||||
kind: 'data-source',
|
||||
id,
|
||||
opType,
|
||||
@ -49,16 +49,17 @@ const buildGroup = (
|
||||
});
|
||||
|
||||
/** 数据源 tab 复用通用 BucketTab,固定注入数据源的 config(title/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> }) =>
|
||||
mount(BucketTab, {
|
||||
props: {
|
||||
config: {
|
||||
title: '数据源',
|
||||
prefix: 'ds',
|
||||
describeGroup: describeDataSourceGroup,
|
||||
describeStep: describeDataSourceStep,
|
||||
isStepDiffable: (step: DataSourceStepValue) => Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema),
|
||||
},
|
||||
config: dataSourceConfig,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
@ -92,9 +93,9 @@ describe('DataSourceTab.vue', () => {
|
||||
const rows = wrapper.findAll('.m-editor-history-list-group');
|
||||
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-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-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 () => {
|
||||
|
||||
@ -158,9 +158,9 @@ describe('HistoryListPanel.vue', () => {
|
||||
expect(rows.length).toBe(3);
|
||||
|
||||
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 === '创建 DS (id: ds_1)')).toBe(true);
|
||||
expect(descs.some((t) => t === '创建 CB (id: code_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 === 'CB (id: code_1)')).toBe(true);
|
||||
});
|
||||
|
||||
test('点击合并组头部能切换 expanded 状态(不触发 goto)', async () => {
|
||||
@ -288,7 +288,7 @@ describe('HistoryListPanel.vue', () => {
|
||||
|
||||
const heads = wrapper.findAll('.m-editor-history-list-group-head');
|
||||
// 找到数据源 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();
|
||||
await dsHead!.find('.m-editor-history-list-item-goto').trigger('click');
|
||||
expect(dataSourceService.goto).toHaveBeenCalledWith('ds_1', 1);
|
||||
@ -307,7 +307,7 @@ describe('HistoryListPanel.vue', () => {
|
||||
await nextTick();
|
||||
|
||||
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();
|
||||
await cbHead!.find('.m-editor-history-list-item-goto').trigger('click');
|
||||
expect(codeBlockService.goto).toHaveBeenCalledWith('code_1', 1);
|
||||
|
||||
@ -71,10 +71,10 @@ describe('PageTab.vue', () => {
|
||||
expect(rows).toHaveLength(2);
|
||||
// 第一组 add
|
||||
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
|
||||
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 时渲染时间元素', () => {
|
||||
|
||||
@ -8,31 +8,18 @@ import { defineComponent, h } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import {
|
||||
describeCodeBlockGroup,
|
||||
describeCodeBlockStep,
|
||||
describeDataSourceGroup,
|
||||
describeDataSourceStep,
|
||||
describePageGroup,
|
||||
describePageStep,
|
||||
formatHistoryFullTime,
|
||||
formatHistoryTime,
|
||||
groupTimestamp,
|
||||
isCodeBlockStepRevertable,
|
||||
isDataSourceStepRevertable,
|
||||
isPageStepRevertable,
|
||||
isSingleDiffStepRevertable,
|
||||
opLabel,
|
||||
useHistoryList,
|
||||
} from '@editor/layouts/history-list/composables';
|
||||
import historyService from '@editor/services/history';
|
||||
import type {
|
||||
CodeBlockHistoryGroup,
|
||||
CodeBlockStepValue,
|
||||
DataSourceHistoryGroup,
|
||||
DataSourceStepValue,
|
||||
PageHistoryGroup,
|
||||
PageHistoryStepEntry,
|
||||
StepValue,
|
||||
} from '@editor/type';
|
||||
import type { PageHistoryGroup, PageHistoryStepEntry, StepValue } from '@editor/type';
|
||||
|
||||
afterEach(() => {
|
||||
historyService.reset();
|
||||
@ -111,7 +98,7 @@ describe('describePageStep', () => {
|
||||
opType: 'add',
|
||||
diff: [{ newSchema: { id: 'btn_1', type: 'button', name: '主按钮' } }],
|
||||
} as unknown as StepValue;
|
||||
expect(describePageStep(step)).toBe('新增 1 个节点(主按钮 (id: btn_1))');
|
||||
expect(describePageStep(step)).toBe('主按钮 (id: btn_1)');
|
||||
});
|
||||
|
||||
test('add 节点无 name 但有 type:使用 type 作为名称', () => {
|
||||
@ -119,7 +106,7 @@ describe('describePageStep', () => {
|
||||
opType: 'add',
|
||||
diff: [{ newSchema: { id: 'n1', type: 'text' } }],
|
||||
} as unknown as StepValue;
|
||||
expect(describePageStep(step)).toBe('新增 1 个节点(text (id: n1))');
|
||||
expect(describePageStep(step)).toBe('text (id: n1)');
|
||||
});
|
||||
|
||||
test('add 节点 name 与 id 相同:仅显示 id', () => {
|
||||
@ -127,7 +114,7 @@ describe('describePageStep', () => {
|
||||
opType: 'add',
|
||||
diff: [{ newSchema: { id: 'n1', name: 'n1' } }],
|
||||
} as unknown as StepValue;
|
||||
expect(describePageStep(step)).toBe('新增 1 个节点(n1)');
|
||||
expect(describePageStep(step)).toBe('n1');
|
||||
});
|
||||
|
||||
test('add 多个节点:仅给出数量', () => {
|
||||
@ -135,12 +122,12 @@ describe('describePageStep', () => {
|
||||
opType: 'add',
|
||||
diff: [{ newSchema: { id: 'a' } }, { newSchema: { id: 'b' } }],
|
||||
} as unknown as StepValue;
|
||||
expect(describePageStep(step)).toBe('新增 2 个节点');
|
||||
expect(describePageStep(step)).toBe('2 个节点');
|
||||
});
|
||||
|
||||
test('add 无 nodes:count 为 0 且不附名称', () => {
|
||||
const step = { opType: 'add' } as unknown as StepValue;
|
||||
expect(describePageStep(step)).toBe('新增 0 个节点');
|
||||
expect(describePageStep(step)).toBe('0 个节点');
|
||||
});
|
||||
|
||||
test('remove 单个节点:含名称与 id', () => {
|
||||
@ -148,7 +135,7 @@ describe('describePageStep', () => {
|
||||
opType: 'remove',
|
||||
diff: [{ oldSchema: { id: 'btn_1', name: '主按钮' } }],
|
||||
} as unknown as StepValue;
|
||||
expect(describePageStep(step)).toBe('删除 1 个节点(主按钮 (id: btn_1))');
|
||||
expect(describePageStep(step)).toBe('主按钮 (id: btn_1)');
|
||||
});
|
||||
|
||||
test('remove 多个节点', () => {
|
||||
@ -156,7 +143,7 @@ describe('describePageStep', () => {
|
||||
opType: 'remove',
|
||||
diff: [{ oldSchema: { id: 'a' } }, { oldSchema: { id: 'b' } }],
|
||||
} as unknown as StepValue;
|
||||
expect(describePageStep(step)).toBe('删除 2 个节点');
|
||||
expect(describePageStep(step)).toBe('2 个节点');
|
||||
});
|
||||
|
||||
test('update 单节点:附 propPath 与 id', () => {
|
||||
@ -170,7 +157,7 @@ describe('describePageStep', () => {
|
||||
},
|
||||
],
|
||||
} 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:仅展示节点', () => {
|
||||
@ -178,7 +165,7 @@ describe('describePageStep', () => {
|
||||
opType: 'update',
|
||||
diff: [{ newSchema: { id: 'btn_1', name: '按钮' }, oldSchema: { id: 'btn_1' } }],
|
||||
} as unknown as StepValue;
|
||||
expect(describePageStep(step)).toBe('修改 按钮 (id: btn_1)');
|
||||
expect(describePageStep(step)).toBe('按钮 (id: btn_1)');
|
||||
});
|
||||
|
||||
test('update 多节点:返回数量', () => {
|
||||
@ -189,12 +176,12 @@ describe('describePageStep', () => {
|
||||
{ newSchema: { id: 'b' }, oldSchema: { id: 'b' } },
|
||||
],
|
||||
} 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;
|
||||
expect(describePageStep(step)).toBe('修改节点');
|
||||
expect(describePageStep(step)).toBe('节点');
|
||||
});
|
||||
});
|
||||
|
||||
@ -230,10 +217,10 @@ describe('describePageGroup', () => {
|
||||
applied: true,
|
||||
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) =>
|
||||
({
|
||||
opType: 'update',
|
||||
@ -255,10 +242,10 @@ describe('describePageGroup', () => {
|
||||
applied: true,
|
||||
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) =>
|
||||
({
|
||||
opType: 'update',
|
||||
@ -285,9 +272,7 @@ describe('describePageGroup', () => {
|
||||
buildPageEntry(mkStep('d'), 3),
|
||||
],
|
||||
};
|
||||
const desc = describePageGroup(group);
|
||||
expect(desc).toContain('修改 按钮 (id: btn_1) · a, b, c');
|
||||
expect(desc.endsWith('…')).toBe(true);
|
||||
expect(describePageGroup(group)).toBe('按钮 (id: btn_1)');
|
||||
});
|
||||
|
||||
test('多步合并组无 propPath 时仅展示目标', () => {
|
||||
@ -306,7 +291,7 @@ describe('describePageGroup', () => {
|
||||
applied: true,
|
||||
steps: [buildPageEntry(mkStep(), 0), buildPageEntry(mkStep(), 1)],
|
||||
};
|
||||
expect(describePageGroup(group)).toBe('修改 按钮 (id: btn_1)');
|
||||
expect(describePageGroup(group)).toBe('按钮 (id: btn_1)');
|
||||
});
|
||||
|
||||
test('多步组 targetName 缺省时使用 targetId 兜底', () => {
|
||||
@ -322,226 +307,7 @@ describe('describePageGroup', () => {
|
||||
],
|
||||
};
|
||||
// targetName 为 undefined,labelWithId 看 label === id 时只展示 id
|
||||
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)');
|
||||
expect(describePageGroup(group)).toBe('btn_1');
|
||||
});
|
||||
});
|
||||
|
||||
@ -684,38 +450,20 @@ describe('isPageStepRevertable', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDataSourceStepRevertable', () => {
|
||||
describe('isSingleDiffStepRevertable', () => {
|
||||
test('新增 / 删除 始终可回滚', () => {
|
||||
expect(isDataSourceStepRevertable({ diff: [{ newSchema: { id: 'ds_1' } }] } as any)).toBe(true);
|
||||
expect(isDataSourceStepRevertable({ diff: [{ oldSchema: { id: 'ds_1' } }] } as any)).toBe(true);
|
||||
expect(isSingleDiffStepRevertable({ diff: [{ newSchema: { id: 'ds_1' } }] } as any)).toBe(true);
|
||||
expect(isSingleDiffStepRevertable({ diff: [{ oldSchema: { id: 'ds_1' } }] } as any)).toBe(true);
|
||||
});
|
||||
|
||||
test('更新有 changeRecords 才可回滚', () => {
|
||||
expect(
|
||||
isDataSourceStepRevertable({
|
||||
isSingleDiffStepRevertable({
|
||||
diff: [{ oldSchema: { id: 'ds_1' }, newSchema: { id: 'ds_1' }, changeRecords: [{ propPath: 'title' }] }],
|
||||
} as any),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isDataSourceStepRevertable({ 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),
|
||||
isSingleDiffStepRevertable({ diff: [{ oldSchema: { id: 'ds_1' }, newSchema: { id: 'ds_1' } }] } as any),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user