feat(editor): 支持按历史记录 uuid 回滚

This commit is contained in:
roymondchen 2026-06-05 19:24:23 +08:00
parent be3a900e6a
commit bddc6f343c
14 changed files with 927 additions and 30 deletions

View File

@ -235,6 +235,86 @@
`newContent=null` 的删除记录;不存在的 id 不会入历史。传入 `doNotPushHistory: true` 也可显式跳过写入历史栈。 `newContent=null` 的删除记录;不存在的 id 不会入历史。传入 `doNotPushHistory: true` 也可显式跳过写入历史栈。
::: :::
## setCodeDslByIdAndGetHistoryId
- **参数:** 同 [setCodeDslById](#setcodedslbyid)
- **返回:**
- {`Promise<string | null>`} 本次写入历史记录的 uuid未写入历史`doNotPushHistory: true` 等)时返回 `null`
- **详情:**
与 [setCodeDslById](#setcodedslbyid) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`,可用于精确引用 / 定位该条历史记录。
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
- **示例:**
```js
import { codeBlockService } from "@tmagic/editor";
const historyId = await codeBlockService.setCodeDslByIdAndGetHistoryId("code_1234", {
name: "代码块1",
content: "() => {}",
});
console.log(historyId); // 本次变更对应的历史记录 uuid或 null
```
## setCodeDslByIdSyncAndGetHistoryId
- **参数:** 同 [setCodeDslByIdSync](#setcodedslbyidsync)
- **返回:**
- {`string | null`} 本次写入历史记录的 uuid未写入历史`doNotPushHistory: true`、或 `force=false` 跳过等)时返回 `null`
- **详情:**
与 [setCodeDslByIdSync](#setcodedslbyidsync) 行为完全一致(同步),仅把返回值换成本次写入历史记录的 `uuid`
## deleteCodeDslByIdsAndGetHistoryId
- **参数:** 同 [deleteCodeDslByIds](#deletecodedslbyids)
- **返回:**
- {`Promise<string[]>`} 本次写入的全部历史记录 uuid按删除顺序未写入任何历史时返回空数组 `[]`
- **详情:**
与 [deleteCodeDslByIds](#deletecodedslbyids) 行为完全一致。由于一次可删除多个代码块、会产生多条历史记录,因此返回的是 uuid 数组(每条删除记录一个 uuid不存在的 id 不会入历史,也不会出现在返回数组中。
- **示例:**
```js
import { codeBlockService } from "@tmagic/editor";
const historyIds = await codeBlockService.deleteCodeDslByIdsAndGetHistoryId(["code_1", "code_2"]);
console.log(historyIds); // ['xxxx', 'yyyy'],或 []
```
## revertById
- **参数:**
- `{string}` uuid 目标历史记录的 uuid通常由 [setCodeDslByIdAndGetHistoryId](#setcodedslbyidandgethistoryid) 等方法返回)
- **返回:**
- {`Promise<CodeBlockStepValue | null>`} 反向应用后产生的新 step找不到对应 uuid / 该步未应用时返回 `null`
- **详情:**
通过历史记录 uuid「回滚」某条代码块历史步骤类 git revert 语义),语义同按 `(id, index)` 回滚,
仅无需调用方再传 `codeBlockId``index`:内部会按 uuid 在全部代码块栈中定位对应步骤后再回滚。
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
- **示例:**
```js
import { codeBlockService } from "@tmagic/editor";
const historyId = await codeBlockService.setCodeDslByIdAndGetHistoryId("code_1234", { name: "代码块1" });
if (historyId) {
await codeBlockService.revertById(historyId);
}
```
## undo ## undo
- **参数:** - **参数:**

View File

@ -406,6 +406,78 @@ import { dataSourceService } from "@tmagic/editor";
dataSourceService.remove("ds_123"); dataSourceService.remove("ds_123");
``` ```
## addAndGetHistoryId
- **参数:** 同 [add](#add)
- **返回:**
- {`string` | null} 本次写入历史记录的 uuid未写入历史`doNotPushHistory: true` 等)时返回 `null`
- **详情:**
与 [add](#add) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`,可用于精确引用 / 定位该条历史记录。
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
- **示例:**
```js
import { dataSourceService } from "@tmagic/editor";
const historyId = dataSourceService.addAndGetHistoryId({
type: "http",
title: "用户信息",
url: "/api/user",
});
console.log(historyId); // 本次新增对应的历史记录 uuid或 null
```
## updateAndGetHistoryId
- **参数:** 同 [update](#update)
- **返回:**
- {`string` | null} 本次写入历史记录的 uuid未写入历史时返回 `null`
- **详情:**
与 [update](#update) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## removeAndGetHistoryId
- **参数:** 同 [remove](#remove)
- **返回:**
- {`string` | null} 本次写入历史记录的 uuid删除的 id 不存在或未写入历史时返回 `null`
- **详情:**
与 [remove](#remove) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## revertById
- **参数:**
- `{string}` uuid 目标历史记录的 uuid通常由 [addAndGetHistoryId](#addandgethistoryid) 等方法返回)
- **返回:**
- {`DataSourceStepValue` | null} 反向应用后产生的新 step找不到对应 uuid / 该步未应用时返回 `null`
- **详情:**
通过历史记录 uuid「回滚」某条数据源历史步骤类 git revert 语义),语义同按 `(id, index)` 回滚,
仅无需调用方再传 `dataSourceId``index`:内部会按 uuid 在全部数据源栈中定位对应步骤后再回滚。
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
- **示例:**
```js
import { dataSourceService } from "@tmagic/editor";
const historyId = dataSourceService.addAndGetHistoryId({ type: "http", title: "用户信息" });
if (historyId) {
dataSourceService.revertById(historyId);
}
```
## createId ## createId
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是 - **[扩展支持](../../guide/editor-expand#行为扩展)** 是

View File

@ -12,6 +12,29 @@
编辑器内置交互(画布、树面板、配置面板、右键菜单、快捷键等)会自动传入对应的 `historySource` 编辑器内置交互(画布、树面板、配置面板、右键菜单、快捷键等)会自动传入对应的 `historySource`
业务侧程序化调用时建议显式传入(如 `api`),便于历史面板区分来源。 业务侧程序化调用时建议显式传入(如 `api`),便于历史面板区分来源。
## 历史记录 uuid 与 \*AndGetHistoryId
每条历史记录入栈时都会自动生成一个唯一标识 `uuid`(见 [StepValue](#undo)),可用于精确引用 / 定位某一条历史记录(如埋点、回滚、跨端同步等)。
DSL 操作方法(`add` / `remove` / `update` 等)默认返回操作结果(节点 / 节点集合 / void不会返回 `uuid`。若需要拿到本次写入历史记录的 `uuid`,可改用对应的 `*AndGetHistoryId` 方法:它们与原方法行为完全一致,仅把返回值换成本次写入历史记录的 `uuid``string`)。当本次操作未写入历史(`doNotPushHistory: true`、无实际变更或提前返回)时返回 `null`
| 原方法 | 取 uuid 的方法 | 返回值 |
| --- | --- | --- |
| [add](#add) | [addAndGetHistoryId](#addandgethistoryid) | `Promise<string \| null>` |
| [remove](#remove) | [removeAndGetHistoryId](#removeandgethistoryid) | `Promise<string \| null>` |
| [update](#update) | [updateAndGetHistoryId](#updateandgethistoryid) | `Promise<string \| null>` |
| [moveLayer](#movelayer) | [moveLayerAndGetHistoryId](#movelayerandgethistoryid) | `Promise<string \| null>` |
| [moveToContainer](#movetocontainer) | [moveToContainerAndGetHistoryId](#movetocontainerandgethistoryid) | `Promise<string \| null>` |
| [dragTo](#dragto) | [dragToAndGetHistoryId](#dragtoandgethistoryid) | `Promise<string \| null>` |
[dataSourceService](./dataSourceServiceMethods.md) / [codeBlockService](./codeBlockServiceMethods.md) 也提供了同名约定的 `*AndGetHistoryId` 方法。
拿到 `uuid` 后,可在需要时按 uuid「回滚」对应的历史记录类 git revert 语义,详见[历史记录面板](../../guide/advanced/history-list.md))。相比按 index 回滚uuid 不会随栈内步骤增删而变化,更适合业务侧持有引用后再回滚:
- 页面:[editorService.revertPageStepById(uuid)](#revertpagestepbyid)
- 数据源:[dataSourceService.revertById(uuid)](./dataSourceServiceMethods.md#revertbyid)
- 代码块:[codeBlockService.revertById(uuid)](./codeBlockServiceMethods.md#revertbyid)
::: details 查看 HistoryOpOptions / DslOpOptions / HistoryOpSource 类型定义 ::: details 查看 HistoryOpOptions / DslOpOptions / HistoryOpSource 类型定义
<<< @/../packages/editor/src/type.ts#HistoryOpOptions{ts} <<< @/../packages/editor/src/type.ts#HistoryOpOptions{ts}
@ -710,6 +733,115 @@ alignCenter可以支持一次水平居中多个组件alignCenter是通过调
将节点(支持多选)拖拽到目标容器的指定位置,会自动处理跨容器布局切换并记录历史 将节点(支持多选)拖拽到目标容器的指定位置,会自动处理跨容器布局切换并记录历史
## addAndGetHistoryId
- **参数:** 同 [add](#add)
- **返回:**
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- **详情:**
与 [add](#add) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`,见[历史记录 uuid 与 \*AndGetHistoryId](#历史记录-uuid-与-andgethistoryid)
- **示例:**
```js
import { editorService } from "@tmagic/editor";
const historyId = await editorService.addAndGetHistoryId(
{ type: "text", text: "hello" },
parent,
{ historySource: "api" },
);
console.log(historyId); // 本次新增对应的历史记录 uuid或 null
```
## removeAndGetHistoryId
- **参数:** 同 [remove](#remove)
- **返回:**
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- **详情:**
与 [remove](#remove) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## updateAndGetHistoryId
- **参数:** 同 [update](#update)
- **返回:**
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- **详情:**
与 [update](#update) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## moveLayerAndGetHistoryId
- **参数:** 同 [moveLayer](#movelayer)
- **返回:**
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- **详情:**
与 [moveLayer](#movelayer) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## moveToContainerAndGetHistoryId
- **参数:** 同 [moveToContainer](#movetocontainer)
- **返回:**
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- **详情:**
与 [moveToContainer](#movetocontainer) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## dragToAndGetHistoryId
- **参数:** 同 [dragTo](#dragto)
- **返回:**
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
- **详情:**
与 [dragTo](#dragto) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
## revertPageStepById
- **参数:**
- `{string}` uuid 目标历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid)(通常由 `*AndGetHistoryId` 方法返回)
- **返回:**
- {Promise<`StepValue` | null>} 反向应用后产生的新 step找不到对应 uuid / 该步未应用 / 反向失败时返回 `null`
- **详情:**
通过历史记录 uuid「回滚」当前页面的某条历史步骤类 git revert 语义):不移动游标、不丢弃任何步骤,而是把目标 step 的修改**反向应用为一条全新的步骤**压入栈顶。语义与按 index 回滚一致,仅入参从 index 改为 uuid更适合业务侧持有引用后再回滚。
::: tip
`opType: 'update'` 的步骤必须携带 `changeRecords` 才支持回滚(否则只能整节点替换,会冲掉后续无关变更);未应用(已被撤销)的步骤无法回滚。
:::
- **示例:**
```js
import { editorService } from "@tmagic/editor";
// 执行操作时拿到本次历史记录 uuid
const historyId = await editorService.addAndGetHistoryId({ type: "text", text: "hello" });
// 之后任意时机按 uuid 回滚该步骤
if (historyId) {
await editorService.revertPageStepById(historyId);
}
```
## undo ## undo
- **[扩展支持](../../guide/editor-expand#行为扩展)** 是 - **[扩展支持](../../guide/editor-expand#行为扩展)** 是

View File

@ -65,6 +65,12 @@
`changeRecords` 时退化为整节点替换(如 `sort` / `moveLayer` / 拖动等纯快照场景)。 `changeRecords` 时退化为整节点替换(如 `sort` / `moveLayer` / 拖动等纯快照场景)。
`StepValue` 上的 `historyDescription` / `source` 仅用于历史面板展示与埋点,不影响 undo/redo 行为。 `StepValue` 上的 `historyDescription` / `source` 仅用于历史面板展示与埋点,不影响 undo/redo 行为。
入栈时会为每条记录自动生成唯一标识 `uuid`(调用方未指定时),可用于精确引用 / 定位某一条历史记录。
若需要在执行 DSL 操作后拿到本次写入记录的 `uuid`,可使用 editorService / dataSourceService /
codeBlockService 提供的 `*AndGetHistoryId` 方法,参见
[editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
`pushCodeBlock` / `pushDataSource` 同样会自动写入 `uuid`
::: :::
## undo ## undo

View File

@ -61,6 +61,12 @@ const menu = ref({
- 数据源:`dataSourceService.revert(id, index)` - 数据源:`dataSourceService.revert(id, index)`
- 代码块:`codeBlockService.revert(id, index)` - 代码块:`codeBlockService.revert(id, index)`
如果业务侧在执行操作时已通过 `*AndGetHistoryId` 拿到了该条记录的 [uuid](/api/editor/editorServiceMethods.md#历史记录-uuid-与-andgethistoryid),也可以直接按 uuid 回滚(无需再关心 index / id且 uuid 不会随栈内步骤增删而变化):
- 页面:`editorService.revertPageStepById(uuid)`
- 数据源:`dataSourceService.revertById(uuid)`
- 代码块:`codeBlockService.revertById(uuid)`
### 4. 差异对比 ### 4. 差异对比
在前后值都存在的 `update` 步骤上提供「查看差异」入口,点击后弹出差异对话框。对话框支持两个维度的切换: 在前后值都存在的 `update` 步骤上提供「查看差异」入口,点击后弹出差异对话框。对话框支持两个维度的切换:

View File

@ -69,6 +69,17 @@ class CodeBlock extends BaseService {
paramsColConfig: undefined, paramsColConfig: undefined,
}); });
/**
* uuid /
* setCodeDslById(Sync)AndGetHistoryId
*/
private lastPushedHistoryId: string | null = null;
/**
* deleteCodeDslByIds uuid
* deleteCodeDslByIds deleteCodeDslByIdsAndGetHistoryId
*/
private lastDeletedHistoryIds: string[] = [];
constructor() { constructor() {
super([ super([
...canUsePluginMethods.async.map((methodName) => ({ name: methodName, isAsync: true })), ...canUsePluginMethods.async.map((methodName) => ({ name: methodName, isAsync: true })),
@ -187,13 +198,14 @@ class CodeBlock extends BaseService {
const newContent = cloneDeep(codeDsl[id]); const newContent = cloneDeep(codeDsl[id]);
if (!doNotPushHistory) { if (!doNotPushHistory) {
this.lastPushedHistoryId =
historyService.pushCodeBlock(id, { historyService.pushCodeBlock(id, {
oldContent, oldContent,
newContent, newContent,
changeRecords, changeRecords,
historyDescription, historyDescription,
source: historySource, source: historySource,
}); })?.uuid ?? null;
} }
this.emit('addOrUpdate', id, codeDsl[id]); this.emit('addOrUpdate', id, codeDsl[id]);
@ -295,6 +307,8 @@ class CodeBlock extends BaseService {
if (!currentDsl) return; if (!currentDsl) return;
this.lastDeletedHistoryIds = [];
codeIds.forEach((id) => { codeIds.forEach((id) => {
// 历史记录:删除前快照内容;不存在的 id 直接跳过历史推入 // 历史记录:删除前快照内容;不存在的 id 直接跳过历史推入
const oldContent: CodeBlockContent | null = currentDsl[id] ? cloneDeep(currentDsl[id]) : null; const oldContent: CodeBlockContent | null = currentDsl[id] ? cloneDeep(currentDsl[id]) : null;
@ -302,13 +316,62 @@ class CodeBlock extends BaseService {
delete currentDsl[id]; delete currentDsl[id];
if (oldContent && !doNotPushHistory) { if (oldContent && !doNotPushHistory) {
historyService.pushCodeBlock(id, { oldContent, newContent: null, historyDescription, source: historySource }); const uuid = historyService.pushCodeBlock(id, {
oldContent,
newContent: null,
historyDescription,
source: historySource,
})?.uuid;
if (uuid) this.lastDeletedHistoryIds.push(uuid);
} }
this.emit('remove', id); this.emit('remove', id);
}); });
} }
// #region AndGetHistoryId
/**
* *AndGetHistoryId
* uuid{@link CodeBlockStepValue.uuid}
* / revert
*
* doNotPushHistory true null
*/
/** 等价于 {@link setCodeDslById},但返回本次写入历史记录的 uuid未入栈时返回 null。 */
public async setCodeDslByIdAndGetHistoryId(
id: Id,
codeConfig: Partial<CodeBlockContent>,
options: HistoryOpOptionsWithChangeRecords = {},
): Promise<string | null> {
this.lastPushedHistoryId = null;
await this.setCodeDslById(id, codeConfig, options);
return this.lastPushedHistoryId;
}
/** 等价于 {@link setCodeDslByIdSync},但返回本次写入历史记录的 uuid未入栈时返回 null。 */
public setCodeDslByIdSyncAndGetHistoryId(
id: Id,
codeConfig: Partial<CodeBlockContent>,
force = true,
options: HistoryOpOptionsWithChangeRecords = {},
): string | null {
this.lastPushedHistoryId = null;
this.setCodeDslByIdSync(id, codeConfig, force, options);
return this.lastPushedHistoryId;
}
/**
* {@link deleteCodeDslByIds} uuid
*
*/
public async deleteCodeDslByIdsAndGetHistoryId(codeIds: Id[], options: HistoryOpOptions = {}): Promise<string[]> {
this.lastDeletedHistoryIds = [];
await this.deleteCodeDslByIds(codeIds, options);
return [...this.lastDeletedHistoryIds];
}
// #endregion AndGetHistoryId
public setParamsColConfig(config: TableColumnConfig): void { public setParamsColConfig(config: TableColumnConfig): void {
this.state.paramsColConfig = config; this.state.paramsColConfig = config;
} }
@ -400,6 +463,20 @@ class CodeBlock extends BaseService {
return await this.applyRevertStep(entry.step, description); return await this.applyRevertStep(entry.step, description);
} }
/**
* uuid {@link revert}
* codeBlockId index uuid{@link CodeBlockStepValue.uuid}
*
*
* @param uuid uuid {@link setCodeDslByIdAndGetHistoryId}
* @returns step uuid / null
*/
public async revertById(uuid: string): Promise<CodeBlockStepValue | null> {
const location = historyService.findCodeBlockStepLocationByUuid(uuid);
if (!location) return null;
return await this.revert(location.id, location.index);
}
/** /**
* id * id
* @returns {Id} id * @returns {Id} id

View File

@ -78,6 +78,13 @@ class DataSource extends BaseService {
methods: {}, methods: {},
}); });
/**
* uuid
* *AndGetHistoryId add / update / remove id
* *AndGetHistoryId null
*/
private lastPushedHistoryId: string | null = null;
constructor() { constructor() {
super(canUsePluginMethods.sync.map((methodName) => ({ name: methodName, isAsync: false }))); super(canUsePluginMethods.sync.map((methodName) => ({ name: methodName, isAsync: false })));
} }
@ -141,12 +148,13 @@ class DataSource extends BaseService {
this.get('dataSources').push(newConfig); this.get('dataSources').push(newConfig);
if (!doNotPushHistory) { if (!doNotPushHistory) {
this.lastPushedHistoryId =
historyService.pushDataSource(newConfig.id, { historyService.pushDataSource(newConfig.id, {
oldSchema: null, oldSchema: null,
newSchema: newConfig, newSchema: newConfig,
historyDescription, historyDescription,
source: historySource, source: historySource,
}); })?.uuid ?? null;
} }
this.emit('add', newConfig); this.emit('add', newConfig);
@ -181,13 +189,14 @@ class DataSource extends BaseService {
dataSources[index] = newConfig; dataSources[index] = newConfig;
if (!doNotPushHistory) { if (!doNotPushHistory) {
this.lastPushedHistoryId =
historyService.pushDataSource(newConfig.id, { historyService.pushDataSource(newConfig.id, {
oldSchema: oldConfig ? cloneDeep(oldConfig) : null, oldSchema: oldConfig ? cloneDeep(oldConfig) : null,
newSchema: newConfig, newSchema: newConfig,
changeRecords, changeRecords,
historyDescription, historyDescription,
source: historySource, source: historySource,
}); })?.uuid ?? null;
} }
this.emit('update', newConfig, { this.emit('update', newConfig, {
@ -212,17 +221,52 @@ class DataSource extends BaseService {
dataSources.splice(index, 1); dataSources.splice(index, 1);
if (oldConfig && !doNotPushHistory) { if (oldConfig && !doNotPushHistory) {
this.lastPushedHistoryId =
historyService.pushDataSource(id, { historyService.pushDataSource(id, {
oldSchema: cloneDeep(oldConfig), oldSchema: cloneDeep(oldConfig),
newSchema: null, newSchema: null,
historyDescription, historyDescription,
source: historySource, source: historySource,
}); })?.uuid ?? null;
} }
this.emit('remove', id); this.emit('remove', id);
} }
// #region AndGetHistoryId
/**
* *AndGetHistoryId add / update / remove
* uuid{@link DataSourceStepValue.uuid}
* / revert
*
* doNotPushHistory true null
*/
/** 等价于 {@link add},但返回本次写入历史记录的 uuid未入栈时返回 null。 */
public addAndGetHistoryId(config: DataSourceSchema, options: HistoryOpOptions = {}): string | null {
this.lastPushedHistoryId = null;
this.add(config, options);
return this.lastPushedHistoryId;
}
/** 等价于 {@link update},但返回本次写入历史记录的 uuid未入栈时返回 null。 */
public updateAndGetHistoryId(
config: DataSourceSchema,
options: HistoryOpOptionsWithChangeRecords = {},
): string | null {
this.lastPushedHistoryId = null;
this.update(config, options);
return this.lastPushedHistoryId;
}
/** 等价于 {@link remove},但返回本次写入历史记录的 uuid未入栈时返回 null。 */
public removeAndGetHistoryId(id: string, options: HistoryOpOptions = {}): string | null {
this.lastPushedHistoryId = null;
this.remove(id, options);
return this.lastPushedHistoryId;
}
// #endregion AndGetHistoryId
/** /**
* *
* *
@ -303,6 +347,20 @@ class DataSource extends BaseService {
return this.applyRevertStep(entry.step, description); return this.applyRevertStep(entry.step, description);
} }
/**
* uuid {@link revert}
* dataSourceId index uuid{@link DataSourceStepValue.uuid}
*
*
* @param uuid uuid {@link addAndGetHistoryId}
* @returns step uuid / null
*/
public revertById(uuid: string): DataSourceStepValue | null {
const location = historyService.findDataSourceStepLocationByUuid(uuid);
if (!location) return null;
return this.revert(location.id, location.index);
}
public createId(): string { public createId(): string {
return `ds_${guid()}`; return `ds_${guid()}`;
} }

View File

@ -23,7 +23,15 @@ import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions }
import { NodeType } from '@tmagic/core'; import { NodeType } from '@tmagic/core';
import type { ChangeRecord } from '@tmagic/form'; import type { ChangeRecord } from '@tmagic/form';
import { isFixed } from '@tmagic/stage'; import { isFixed } from '@tmagic/stage';
import { getNodeInfo, getNodePath, getValueByKeyPath, isPage, isPageFragment, setValueByKeyPath } from '@tmagic/utils'; import {
getNodeInfo,
getNodePath,
getValueByKeyPath,
guid,
isPage,
isPageFragment,
setValueByKeyPath,
} from '@tmagic/utils';
import BaseService from '@editor/services//BaseService'; import BaseService from '@editor/services//BaseService';
import propsService from '@editor/services//props'; import propsService from '@editor/services//props';
@ -116,6 +124,12 @@ class Editor extends BaseService {
alwaysMultiSelect: false, alwaysMultiSelect: false,
}); });
private selectionBeforeOp: Id[] | null = null; private selectionBeforeOp: Id[] | null = null;
/**
* pushOpHistory uuid
* *AndGetHistoryId id
* *AndGetHistoryId null
*/
private lastPushedHistoryId: string | null = null;
constructor() { constructor() {
super( super(
@ -1190,6 +1204,86 @@ class Editor extends BaseService {
this.emit('drag-to', { targetIndex, configs, targetParent }); this.emit('drag-to', { targetIndex, configs, targetParent });
} }
// #region AndGetHistoryId
/**
* *AndGetHistoryId add / remove / update ...
* uuid{@link StepValue.uuid}
* / / revert
*
* doNotPushHistory true / null
*/
/** 等价于 {@link add},但返回本次写入历史记录的 uuid未入栈时返回 null。 */
public async addAndGetHistoryId(
addNode: AddMNode | MNode[],
parent?: MContainer | null,
options: DslOpOptions = {},
): Promise<string | null> {
this.lastPushedHistoryId = null;
await this.add(addNode, parent, options);
return this.lastPushedHistoryId;
}
/** 等价于 {@link remove},但返回本次写入历史记录的 uuid未入栈时返回 null。 */
public async removeAndGetHistoryId(
nodeOrNodeList: MNode | MNode[],
options: DslOpOptions = {},
): Promise<string | null> {
this.lastPushedHistoryId = null;
await this.remove(nodeOrNodeList, options);
return this.lastPushedHistoryId;
}
/** 等价于 {@link update},但返回本次写入历史记录的 uuid未入栈时返回 null。 */
public async updateAndGetHistoryId(
config: MNode | MNode[],
data: {
changeRecords?: ChangeRecord[];
changeRecordList?: ChangeRecord[][];
doNotPushHistory?: boolean;
historyDescription?: string;
historySource?: HistoryOpSource;
} = {},
): Promise<string | null> {
this.lastPushedHistoryId = null;
await this.update(config, data);
return this.lastPushedHistoryId;
}
/** 等价于 {@link moveLayer},但返回本次写入历史记录的 uuid未入栈时返回 null。 */
public async moveLayerAndGetHistoryId(
offset: number | LayerOffset,
options: DslOpOptions = {},
): Promise<string | null> {
this.lastPushedHistoryId = null;
await this.moveLayer(offset, options);
return this.lastPushedHistoryId;
}
/** 等价于 {@link moveToContainer},但返回本次写入历史记录的 uuid未入栈时返回 null。 */
public async moveToContainerAndGetHistoryId(
config: MNode | MNode[],
targetId: Id,
options: DslOpOptions = {},
): Promise<string | null> {
this.lastPushedHistoryId = null;
await this.moveToContainer(config, targetId, options);
return this.lastPushedHistoryId;
}
/** 等价于 {@link dragTo},但返回本次写入历史记录的 uuid未入栈时返回 null。 */
public async dragToAndGetHistoryId(
config: MNode | MNode[],
targetParent: MContainer,
targetIndex: number,
options: DslOpOptions = {},
): Promise<string | null> {
this.lastPushedHistoryId = null;
await this.dragTo(config, targetParent, targetIndex, options);
return this.lastPushedHistoryId;
}
// #endregion AndGetHistoryId
/** /**
* *
* @returns * @returns
@ -1320,6 +1414,20 @@ class Editor extends BaseService {
return revertedStep; return revertedStep;
} }
/**
* uuid {@link revertPageStep}
* index uuid{@link StepValue.uuid}uuid
*
*
* @param uuid uuid *AndGetHistoryId
* @returns step uuid / / null
*/
public async revertPageStepById(uuid: string): Promise<StepValue | null> {
const index = historyService.getPageStepIndexByUuid(uuid);
if (index < 0) return null;
return this.revertPageStep(index);
}
/** /**
* *
* *
@ -1429,8 +1537,9 @@ class Editor extends BaseService {
historyDescription?: string; historyDescription?: string;
source?: HistoryOpSource; source?: HistoryOpSource;
}, },
) { ): string | null {
const step: StepValue = { const step: StepValue = {
uuid: guid(),
data: pageData, data: pageData,
opType, opType,
selectedBefore: this.selectionBeforeOp ?? [], selectedBefore: this.selectionBeforeOp ?? [],
@ -1442,8 +1551,12 @@ class Editor extends BaseService {
if (source) step.source = source; if (source) step.source = source;
// 显式按 step.data.id 入栈:跨页操作(如 moveToContainer 从源页搬到目标页) // 显式按 step.data.id 入栈:跨页操作(如 moveToContainer 从源页搬到目标页)
// 必须落到正确的页面栈,否则会把记录错误地推到当前活动页 / 操作发起页。 // 必须落到正确的页面栈,否则会把记录错误地推到当前活动页 / 操作发起页。
historyService.push(step, pageData.id); const pushed = historyService.push(step, pageData.id);
// push 返回 null 表示当前没有可写入的页面栈(未真正入栈),此时不应返回 uuid。
const historyId = pushed ? step.uuid : null;
this.lastPushedHistoryId = historyId;
this.selectionBeforeOp = null; this.selectionBeforeOp = null;
return historyId;
} }
/** /**

View File

@ -21,6 +21,7 @@ import { cloneDeep } from 'lodash-es';
import type { CodeBlockContent, DataSourceSchema, Id, MPage, MPageFragment } from '@tmagic/core'; import type { CodeBlockContent, DataSourceSchema, Id, MPage, MPageFragment } from '@tmagic/core';
import type { ChangeRecord } from '@tmagic/form'; import type { ChangeRecord } from '@tmagic/form';
import { guid } from '@tmagic/utils';
import type { import type {
CodeBlockHistoryGroup, CodeBlockHistoryGroup,
@ -255,6 +256,7 @@ class History extends BaseService {
public push(state: StepValue, pageId?: Id): StepValue | null { public push(state: StepValue, pageId?: Id): StepValue | null {
const undoRedo = this.getUndoRedo(pageId); const undoRedo = this.getUndoRedo(pageId);
if (!undoRedo) return null; if (!undoRedo) return null;
if (state.uuid === undefined) state.uuid = guid();
if (state.timestamp === undefined) state.timestamp = Date.now(); if (state.timestamp === undefined) state.timestamp = Date.now();
undoRedo.pushElement(state); undoRedo.pushElement(state);
// 仅当推入的是当前活动页时才需要刷新 canUndo/canRedo —— 其它页栈对当前 UI 状态没影响。 // 仅当推入的是当前活动页时才需要刷新 canUndo/canRedo —— 其它页栈对当前 UI 状态没影响。
@ -288,6 +290,7 @@ class History extends BaseService {
if (!codeBlockId) return null; if (!codeBlockId) return null;
const step: CodeBlockStepValue = { const step: CodeBlockStepValue = {
uuid: guid(),
id: codeBlockId, id: codeBlockId,
oldContent: payload.oldContent ? cloneDeep(payload.oldContent) : null, oldContent: payload.oldContent ? cloneDeep(payload.oldContent) : null,
newContent: payload.newContent ? cloneDeep(payload.newContent) : null, newContent: payload.newContent ? cloneDeep(payload.newContent) : null,
@ -321,6 +324,7 @@ class History extends BaseService {
if (!dataSourceId) return null; if (!dataSourceId) return null;
const step: DataSourceStepValue = { const step: DataSourceStepValue = {
uuid: guid(),
id: dataSourceId, id: dataSourceId,
oldSchema: payload.oldSchema ? cloneDeep(payload.oldSchema) : null, oldSchema: payload.oldSchema ? cloneDeep(payload.oldSchema) : null,
newSchema: payload.newSchema ? cloneDeep(payload.newSchema) : null, newSchema: payload.newSchema ? cloneDeep(payload.newSchema) : null,
@ -510,6 +514,41 @@ class History extends BaseService {
return list.map((step, index) => ({ step, index, applied: index < cursor })); return list.map((step, index) => ({ step, index, applied: index < cursor }));
} }
/**
* uuid
* -1 uuid uuid index 使
*/
public getPageStepIndexByUuid(uuid: string, pageId?: Id): number {
if (!uuid) return -1;
return this.getPageStepList(pageId).findIndex((entry) => entry.step.uuid === uuid);
}
/**
* uuid codeBlockId
* null
*/
public findCodeBlockStepLocationByUuid(uuid: string): { id: Id; index: number } | null {
if (!uuid) return null;
for (const id of Object.keys(this.state.codeBlockState)) {
const index = this.getCodeBlockStepList(id).findIndex((entry) => entry.step.uuid === uuid);
if (index >= 0) return { id, index };
}
return null;
}
/**
* uuid dataSourceId
* null
*/
public findDataSourceStepLocationByUuid(uuid: string): { id: Id; index: number } | null {
if (!uuid) return null;
for (const id of Object.keys(this.state.dataSourceState)) {
const index = this.getDataSourceStepList(id).findIndex((entry) => entry.step.uuid === uuid);
if (index >= 0) return { id, index };
}
return null;
}
/** /**
* dataSourceId * dataSourceId
*/ */

View File

@ -723,6 +723,11 @@ export type HistoryOpSource =
// #region StepValue // #region StepValue
export interface StepValue { export interface StepValue {
/**
* uuid historyService.push
* / revert
*/
uuid: string;
/** 页面信息 */ /** 页面信息 */
data: { name: string; id: Id }; data: { name: string; id: Id };
opType: HistoryOpType; opType: HistoryOpType;
@ -772,6 +777,11 @@ export interface StepValue {
* - newContent = nulloldContent = * - newContent = nulloldContent =
*/ */
export interface CodeBlockStepValue { export interface CodeBlockStepValue {
/**
* uuid /
* `id` id
*/
uuid: string;
/** 关联的代码块 id */ /** 关联的代码块 id */
id: Id; id: Id;
/** 变更前的代码块内容,新增时为 null */ /** 变更前的代码块内容,新增时为 null */
@ -800,6 +810,11 @@ export interface CodeBlockStepValue {
* - newSchema = nulloldSchema = schema * - newSchema = nulloldSchema = schema
*/ */
export interface DataSourceStepValue { export interface DataSourceStepValue {
/**
* uuid /
* `id` id
*/
uuid: string;
/** 关联的数据源 id */ /** 关联的数据源 id */
id: Id; id: Id;
/** 变更前的数据源 schema新增时为 null */ /** 变更前的数据源 schema新增时为 null */

View File

@ -231,6 +231,98 @@ describe('CodeBlockService - 历史记录接入', () => {
}); });
}); });
describe('CodeBlockService - *AndGetHistoryId', () => {
const lastStepUuid = (id: string) => {
const list = historyService.getCodeBlockStepList(id);
return list[list.length - 1]?.step.uuid;
};
test('setCodeDslByIdSyncAndGetHistoryId 返回本次写入历史记录的 uuid', async () => {
await codeBlockService.setCodeDsl({} as any);
const historyId = codeBlockService.setCodeDslByIdSyncAndGetHistoryId('a', { name: 'A' } as any);
expect(typeof historyId).toBe('string');
expect(historyId).toBe(lastStepUuid('a'));
// 与默认行为一致:内容仍被写入
expect(codeBlockService.getCodeContentById('a')?.name).toBe('A');
});
test('setCodeDslByIdSyncAndGetHistoryId - force=false 已存在时返回 null', async () => {
await codeBlockService.setCodeDsl({ a: { name: 'A' } } as any);
const historyId = codeBlockService.setCodeDslByIdSyncAndGetHistoryId('a', { name: 'NEW' } as any, false);
expect(historyId).toBeNull();
});
test('setCodeDslByIdSyncAndGetHistoryId - doNotPushHistory 时返回 null', async () => {
await codeBlockService.setCodeDsl({} as any);
const historyId = codeBlockService.setCodeDslByIdSyncAndGetHistoryId('a', { name: 'A' } as any, true, {
doNotPushHistory: true,
});
expect(historyId).toBeNull();
});
test('setCodeDslByIdAndGetHistoryIdasync返回本次写入历史记录的 uuid', async () => {
await codeBlockService.setCodeDsl({} as any);
const historyId = await codeBlockService.setCodeDslByIdAndGetHistoryId('a', { name: 'A' } as any);
expect(typeof historyId).toBe('string');
expect(historyId).toBe(lastStepUuid('a'));
});
test('deleteCodeDslByIdsAndGetHistoryId 返回每条删除记录的 uuid 数组', async () => {
await codeBlockService.setCodeDsl({ a: { name: 'A' }, b: { name: 'B' } } as any);
const historyIds = await codeBlockService.deleteCodeDslByIdsAndGetHistoryId(['a', 'b']);
expect(Array.isArray(historyIds)).toBe(true);
expect(historyIds).toHaveLength(2);
expect(historyIds[0]).toBe(lastStepUuid('a'));
expect(historyIds[1]).toBe(lastStepUuid('b'));
});
test('deleteCodeDslByIdsAndGetHistoryId - 不存在的 id 不计入返回数组', async () => {
await codeBlockService.setCodeDsl({ a: { name: 'A' } } as any);
const historyIds = await codeBlockService.deleteCodeDslByIdsAndGetHistoryId(['a', 'ghost']);
expect(historyIds).toHaveLength(1);
expect(historyIds[0]).toBe(lastStepUuid('a'));
});
test('deleteCodeDslByIdsAndGetHistoryId - 全部不存在时返回空数组', async () => {
await codeBlockService.setCodeDsl({} as any);
const historyIds = await codeBlockService.deleteCodeDslByIdsAndGetHistoryId(['ghost']);
expect(historyIds).toEqual([]);
});
});
describe('CodeBlockService - revertById', () => {
test('通过 uuid 回滚新增(删除代码块内容)', async () => {
await codeBlockService.setCodeDsl({} as any);
const uuid = codeBlockService.setCodeDslByIdSyncAndGetHistoryId('a', { name: 'A' } as any);
expect(typeof uuid).toBe('string');
expect(codeBlockService.getCodeContentById('a')?.name).toBe('A');
const reverted = await codeBlockService.revertById(uuid!);
expect(reverted).not.toBeNull();
expect(codeBlockService.getCodeContentById('a')).toBeNull();
});
test('按 uuid 能定位到对应 (id, index)', async () => {
await codeBlockService.setCodeDsl({} as any);
const uuid = codeBlockService.setCodeDslByIdSyncAndGetHistoryId('a', { name: 'A' } as any);
const location = historyService.findCodeBlockStepLocationByUuid(uuid!);
expect(location).toEqual({ id: 'a', index: 0 });
});
test('找不到 uuid 时返回 null', async () => {
await codeBlockService.setCodeDsl({} as any);
codeBlockService.setCodeDslByIdSyncAndGetHistoryId('a', { name: 'A' } as any);
await expect(codeBlockService.revertById('not-exist')).resolves.toBeNull();
await expect(codeBlockService.revertById('')).resolves.toBeNull();
});
});
describe('CodeBlockService - undo / redo', () => { describe('CodeBlockService - undo / redo', () => {
test('undo / redo - 新增场景:撤销=删除,重做=再写回', async () => { test('undo / redo - 新增场景:撤销=删除,重做=再写回', async () => {
await codeBlockService.setCodeDsl({} as any); await codeBlockService.setCodeDsl({} as any);

View File

@ -187,6 +187,79 @@ describe('DataSource service - 历史记录接入', () => {
}); });
}); });
describe('DataSource service - *AndGetHistoryId', () => {
const lastStepUuid = (id: string) => {
const list = historyService.getDataSourceStepList(id);
return list[list.length - 1]?.step.uuid;
};
test('addAndGetHistoryId 返回本次写入历史记录的 uuid', () => {
const ds = dataSource.add({ id: 'temp', title: 'a', type: 'base' } as any);
historyService.reset();
const historyId = dataSource.addAndGetHistoryId({ id: 'ds_new', title: 'a', type: 'base' } as any);
expect(typeof historyId).toBe('string');
expect(historyId).toBe(lastStepUuid('ds_new'));
// 与默认 add 行为一致:仍会写入数据源
expect(dataSource.getDataSourceById('ds_new')).toBeDefined();
expect(ds).toBeDefined();
});
test('addAndGetHistoryId 传 doNotPushHistory 时返回 null', () => {
const historyId = dataSource.addAndGetHistoryId({ id: 'ds_x', title: 'a', type: 'base' } as any, {
doNotPushHistory: true,
});
expect(historyId).toBeNull();
});
test('updateAndGetHistoryId 返回本次写入历史记录的 uuid', () => {
const created = dataSource.add({ title: 'a', type: 'base' } as any);
historyService.reset();
const historyId = dataSource.updateAndGetHistoryId({ ...created, title: 'b' } as any);
expect(typeof historyId).toBe('string');
expect(historyId).toBe(lastStepUuid(created.id!));
});
test('removeAndGetHistoryId 返回本次写入历史记录的 uuid不存在的 id 返回 null', () => {
const created = dataSource.add({ title: 'a', type: 'base' } as any);
historyService.reset();
const historyId = dataSource.removeAndGetHistoryId(created.id!);
expect(typeof historyId).toBe('string');
expect(historyId).toBe(lastStepUuid(created.id!));
expect(dataSource.removeAndGetHistoryId('ghost')).toBeNull();
});
});
describe('DataSource service - revertById', () => {
test('通过 uuid 回滚 add移除数据源', () => {
const created = dataSource.add({ title: 'a', type: 'base' } as any);
const uuid = historyService.getDataSourceStepList(created.id!).slice(-1)[0]?.step.uuid;
expect(typeof uuid).toBe('string');
expect(dataSource.getDataSourceById(created.id!)).toBeDefined();
const reverted = dataSource.revertById(uuid!);
expect(reverted).not.toBeNull();
expect(dataSource.getDataSourceById(created.id!)).toBeUndefined();
});
test('通过 uuid 回滚等价于按 (id, index) 回滚', () => {
const created = dataSource.add({ title: 'a', type: 'base' } as any);
const uuid = historyService.getDataSourceStepList(created.id!).slice(-1)[0]?.step.uuid;
const location = historyService.findDataSourceStepLocationByUuid(uuid!);
expect(location).toEqual({ id: created.id, index: 0 });
});
test('找不到 uuid 时返回 null', () => {
dataSource.add({ title: 'a', type: 'base' } as any);
expect(dataSource.revertById('not-exist')).toBeNull();
expect(dataSource.revertById('')).toBeNull();
});
});
describe('DataSource service - undo / redo', () => { describe('DataSource service - undo / redo', () => {
test('undo / redo - 新增场景:撤销=移除,重做=再添加', () => { test('undo / redo - 新增场景:撤销=移除,重做=再添加', () => {
const created = dataSource.add({ title: 'a', type: 'base' } as any); const created = dataSource.add({ title: 'a', type: 'base' } as any);

View File

@ -711,3 +711,99 @@ describe('undo redo', () => {
expect(editorService.getNodeById(NodeId.NODE_ID)?.style?.width).toBe(270); expect(editorService.getNodeById(NodeId.NODE_ID)?.style?.width).toBe(270);
}); });
}); });
describe('*AndGetHistoryId', () => {
const lastStepUuid = () => {
const list = historyService.getPageStepList();
return list[list.length - 1]?.step.uuid;
};
test('addAndGetHistoryId 返回本次写入历史记录的 uuid且与栈顶 step 一致', async () => {
editorService.set('root', cloneDeep(root));
historyService.reset();
await editorService.select(NodeId.PAGE_ID);
const historyId = await editorService.addAndGetHistoryId({ type: 'text' });
expect(typeof historyId).toBe('string');
expect(historyId).toBeTruthy();
expect(historyId).toBe(lastStepUuid());
});
test('addAndGetHistoryId 传 doNotPushHistory 时返回 null', async () => {
editorService.set('root', cloneDeep(root));
historyService.reset();
await editorService.select(NodeId.PAGE_ID);
const historyId = await editorService.addAndGetHistoryId({ type: 'text' }, null, { doNotPushHistory: true });
expect(historyId).toBeNull();
});
test('updateAndGetHistoryId 返回本次写入历史记录的 uuid', async () => {
editorService.set('root', cloneDeep(root));
historyService.reset();
await editorService.select(NodeId.PAGE_ID);
const historyId = await editorService.updateAndGetHistoryId({ id: NodeId.NODE_ID, type: 'text', text: 'x' });
expect(typeof historyId).toBe('string');
expect(historyId).toBe(lastStepUuid());
});
test('removeAndGetHistoryId 返回本次写入历史记录的 uuid', async () => {
editorService.set('root', cloneDeep(root));
historyService.reset();
await editorService.select(NodeId.PAGE_ID);
const historyId = await editorService.removeAndGetHistoryId({ id: NodeId.NODE_ID, type: 'text' });
expect(typeof historyId).toBe('string');
expect(historyId).toBe(lastStepUuid());
});
test('moveLayerAndGetHistoryId 返回本次写入历史记录的 uuid', async () => {
editorService.set('root', cloneDeep(root));
historyService.reset();
await editorService.select(NodeId.NODE_ID);
const historyId = await editorService.moveLayerAndGetHistoryId(1);
expect(typeof historyId).toBe('string');
expect(historyId).toBe(lastStepUuid());
});
});
describe('revertPageStepById', () => {
test('通过 uuid 回滚 add 步骤(删除被新增节点)', async () => {
editorService.set('root', cloneDeep(root));
historyService.reset();
await editorService.select(NodeId.PAGE_ID);
const uuid = await editorService.addAndGetHistoryId({ type: 'text' });
expect(typeof uuid).toBe('string');
const addedStep = historyService.getPageStepList().find((e) => e.step.uuid === uuid)!.step;
const addedId = addedStep.nodes![0].id;
expect(editorService.getNodeById(addedId)).toBeTruthy();
const reverted = await editorService.revertPageStepById(uuid!);
expect(reverted).not.toBeNull();
// 回滚git revert 语义)会把被新增的节点删掉
expect(editorService.getNodeById(addedId)).toBeNull();
});
test('与按 index 回滚结果一致', async () => {
editorService.set('root', cloneDeep(root));
historyService.reset();
await editorService.select(NodeId.PAGE_ID);
const uuid = await editorService.addAndGetHistoryId({ type: 'text' });
const index = historyService.getPageStepIndexByUuid(uuid!);
expect(index).toBeGreaterThanOrEqual(0);
});
test('找不到 uuid 时返回 null', async () => {
editorService.set('root', cloneDeep(root));
historyService.reset();
await editorService.select(NodeId.PAGE_ID);
expect(await editorService.revertPageStepById('not-exist')).toBeNull();
expect(await editorService.revertPageStepById('')).toBeNull();
});
});

View File

@ -103,6 +103,32 @@ describe('history service', () => {
} as any); } as any);
expect(step?.timestamp).toBe(123456); expect(step?.timestamp).toBe(123456);
}); });
test('push 未带 uuid 时自动生成 uuid', () => {
history.changePage({ id: 'p1' } as any);
const step = history.push({ data: { id: 'p1', name: '' }, modifiedNodeIds: new Map() } as any);
expect(typeof step?.uuid).toBe('string');
expect(step?.uuid).toBeTruthy();
});
test('push 已带 uuid 时保留调用方指定的值', () => {
history.changePage({ id: 'p1' } as any);
const step = history.push({
uuid: 'my-uuid',
data: { id: 'p1', name: '' },
modifiedNodeIds: new Map(),
} as any);
expect(step?.uuid).toBe('my-uuid');
});
test('push 为每条记录生成不同的 uuid', () => {
history.changePage({ id: 'p1' } as any);
const s1 = history.push({ data: { id: 'p1', name: '' }, modifiedNodeIds: new Map() } as any);
const s2 = history.push({ data: { id: 'p1', name: '' }, modifiedNodeIds: new Map() } as any);
expect(s1?.uuid).toBeTruthy();
expect(s2?.uuid).toBeTruthy();
expect(s1?.uuid).not.toBe(s2?.uuid);
});
}); });
describe('history service - codeBlock', () => { describe('history service - codeBlock', () => {
@ -138,6 +164,12 @@ describe('history service - codeBlock', () => {
expect(step?.timestamp).toBeLessThanOrEqual(after); expect(step?.timestamp).toBeLessThanOrEqual(after);
}); });
test('pushCodeBlock 自动生成 uuid', () => {
const step = history.pushCodeBlock('code_1', { oldContent: null, newContent: { name: 'A' } as any });
expect(typeof step?.uuid).toBe('string');
expect(step?.uuid).toBeTruthy();
});
test('undoCodeBlock / redoCodeBlock 走对应 id 的 UndoRedo 栈', () => { test('undoCodeBlock / redoCodeBlock 走对应 id 的 UndoRedo 栈', () => {
history.pushCodeBlock('code_1', { oldContent: null, newContent: { name: 'A' } as any }); history.pushCodeBlock('code_1', { oldContent: null, newContent: { name: 'A' } as any });
history.pushCodeBlock('code_1', { history.pushCodeBlock('code_1', {
@ -218,6 +250,12 @@ describe('history service - dataSource', () => {
expect(step?.timestamp).toBeLessThanOrEqual(after); expect(step?.timestamp).toBeLessThanOrEqual(after);
}); });
test('pushDataSource 自动生成 uuid', () => {
const step = history.pushDataSource('ds_1', { oldSchema: null, newSchema: { id: 'ds_1' } as any });
expect(typeof step?.uuid).toBe('string');
expect(step?.uuid).toBeTruthy();
});
test('undoDataSource / redoDataSource 走对应 id 的 UndoRedo 栈', () => { test('undoDataSource / redoDataSource 走对应 id 的 UndoRedo 栈', () => {
history.pushDataSource('ds_1', { history.pushDataSource('ds_1', {
oldSchema: null, oldSchema: null,