mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-05-30 04:08:04 +00:00
feat(editor): 代码块与数据源支持按 id 独立的历史记录
- history service 新增 pushCodeBlock/undoCodeBlock/redoCodeBlock /canUndoCodeBlock/canRedoCodeBlock 及数据源对称 API - 按 id 维度各自维护独立 UndoRedo 栈,与页面/节点历史完全解耦 - type 新增 CodeBlockStepValue / DataSourceStepValue 独立类型 - HistoryState 扩展 codeBlockState / dataSourceState 字段 - codeBlockService.setCodeDslByIdSync / deleteCodeDslByIds 自动入历史 - dataSourceService.add / update / remove 自动入历史 - 入栈成功时 emit code-block-history-change / data-source-history-change - 补充单测共 21 例,更新 history/codeBlock/dataSource 相关文档
This commit is contained in:
parent
a341c7d73e
commit
e2c065f90d
@ -70,6 +70,11 @@
|
||||
|
||||
同步版本的 [setCodeDslById](#setcodedslbyid),并会触发 `addOrUpdate` 事件
|
||||
|
||||
::: tip
|
||||
写入成功时(`force=false` 且同 id 已存在的跳过场景除外)会自动调用 `historyService.pushCodeBlock`
|
||||
把本次变更入历史栈,参见 [historyService.pushCodeBlock](./historyServiceMethods.md#pushcodeblock)。
|
||||
:::
|
||||
|
||||
## getCodeDslByIds
|
||||
|
||||
- **参数:**
|
||||
@ -202,6 +207,11 @@
|
||||
|
||||
在dsl数据源中删除指定id的代码块,每删除一个会触发一次 `remove` 事件
|
||||
|
||||
::: tip
|
||||
对每个实际存在并被删除的代码块,会自动调用 `historyService.pushCodeBlock` 入栈一条
|
||||
`newContent=null` 的删除记录;不存在的 id 不会入历史。
|
||||
:::
|
||||
|
||||
## setParamsColConfig
|
||||
|
||||
- **参数:**
|
||||
|
||||
@ -306,6 +306,11 @@ dataSourceService.setFormMethod("http", [
|
||||
|
||||
添加一个数据源,如果配置中没有id或id已存在,会自动生成新的id
|
||||
|
||||
::: tip
|
||||
添加成功会自动调用 `historyService.pushDataSource` 入栈一条 `oldSchema=null` 的新增记录,
|
||||
参见 [historyService.pushDataSource](./historyServiceMethods.md#pushdatasource)。
|
||||
:::
|
||||
|
||||
- **示例:**
|
||||
|
||||
```js
|
||||
@ -341,6 +346,11 @@ console.log(newDs.id); // 自动生成的id
|
||||
|
||||
更新数据源
|
||||
|
||||
::: tip
|
||||
更新成功会自动调用 `historyService.pushDataSource` 入栈一条 `oldSchema` / `newSchema`
|
||||
均为对应 schema 的更新记录。
|
||||
:::
|
||||
|
||||
- **示例:**
|
||||
|
||||
```js
|
||||
@ -370,6 +380,11 @@ console.log(updatedDs);
|
||||
|
||||
删除指定id的数据源
|
||||
|
||||
::: tip
|
||||
对实际存在的数据源会自动调用 `historyService.pushDataSource` 入栈一条 `newSchema=null`
|
||||
的删除记录;不存在的 id 不会入历史。
|
||||
:::
|
||||
|
||||
- **示例:**
|
||||
|
||||
```js
|
||||
|
||||
@ -29,3 +29,41 @@
|
||||
:::tip
|
||||
当游标处于历史栈边界(已经无法继续撤销或重做)时,`UndoRedo.undo()` / `redo()` 返回 `null`,对应 `change` 回调收到的 `state` 为 `null`
|
||||
:::
|
||||
|
||||
## code-block-history-change
|
||||
|
||||
- **详情:** 代码块历史记录发生变化(`pushCodeBlock` / `undoCodeBlock` / `redoCodeBlock` 成功时触发)
|
||||
|
||||
- **事件回调函数:** `(codeBlockId: Id, step: CodeBlockStepValue) => void`
|
||||
|
||||
::: details 查看 CodeBlockStepValue 及关联类型定义
|
||||
<<< @/../packages/editor/src/type.ts#CodeBlockStepValue{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#CodeBlockContent{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#Id{ts}
|
||||
:::
|
||||
|
||||
:::tip
|
||||
- 新增触发的 step 中 `oldContent` 为 `null`
|
||||
- 删除触发的 step 中 `newContent` 为 `null`
|
||||
- `undo` / `redo` 返回 `null`(边界状态)时不会触发该事件
|
||||
:::
|
||||
|
||||
## data-source-history-change
|
||||
|
||||
- **详情:** 数据源历史记录发生变化(`pushDataSource` / `undoDataSource` / `redoDataSource` 成功时触发)
|
||||
|
||||
- **事件回调函数:** `(dataSourceId: Id, step: DataSourceStepValue) => void`
|
||||
|
||||
::: details 查看 DataSourceStepValue 及关联类型定义
|
||||
<<< @/../packages/editor/src/type.ts#DataSourceStepValue{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#Id{ts}
|
||||
:::
|
||||
|
||||
:::tip
|
||||
- 新增触发的 step 中 `oldSchema` 为 `null`
|
||||
- 删除触发的 step 中 `newSchema` 为 `null`
|
||||
- `undo` / `redo` 返回 `null`(边界状态)时不会触发该事件
|
||||
:::
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
- **详情:**
|
||||
|
||||
重置记录
|
||||
重置全部历史记录(包括页面节点栈、代码块栈、数据源栈),并重置当前页面 id / canRedo / canUndo
|
||||
|
||||
## resetPage
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
- **详情:**
|
||||
|
||||
重置历史记录全部内部状态(清空 pageId、pageSteps、canRedo、canUndo)
|
||||
重置历史记录全部内部状态(清空 pageId、pageSteps、canRedo、canUndo、codeBlockState、dataSourceState)
|
||||
|
||||
## changePage
|
||||
|
||||
@ -73,6 +73,159 @@
|
||||
|
||||
恢复到下一步
|
||||
|
||||
## pushCodeBlock
|
||||
|
||||
- **参数:**
|
||||
- `{Id} codeBlockId` 代码块 id
|
||||
- `{Object} payload`
|
||||
- `{CodeBlockContent | null} oldContent` 变更前的代码块内容;新增时为 `null`
|
||||
- `{CodeBlockContent | null} newContent` 变更后的代码块内容;删除时为 `null`
|
||||
|
||||
::: details 查看 CodeBlockStepValue 及关联类型定义
|
||||
<<< @/../packages/editor/src/type.ts#CodeBlockStepValue{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#CodeBlockContent{ts}
|
||||
:::
|
||||
|
||||
- **返回:**
|
||||
- `{CodeBlockStepValue | null}` 入栈失败(未传 id)时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
推入一条代码块变更记录。与页面 / 节点完全无关,按 `codeBlockId` 维度独立一份 `UndoRedo` 栈,
|
||||
栈实例存放在 `historyService.state.codeBlockState[codeBlockId]`。
|
||||
|
||||
入栈成功后会触发 `code-block-history-change` 事件。
|
||||
|
||||
::: tip
|
||||
`codeBlockService.setCodeDslByIdSync` 与 `codeBlockService.deleteCodeDslByIds` 内部已经
|
||||
自动调用本方法,业务代码通常无需手动调用。
|
||||
:::
|
||||
|
||||
## undoCodeBlock
|
||||
|
||||
- **参数:**
|
||||
- `{Id} codeBlockId`
|
||||
|
||||
- **返回:**
|
||||
- `{CodeBlockStepValue | null}` 栈不存在或已无可撤销记录时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
撤销指定代码块的最近一次变更。成功时会触发 `code-block-history-change` 事件。
|
||||
拿到 step 后由调用方根据 `step.oldContent` 写回 `codeBlockService`(本方法不会自动回放)。
|
||||
|
||||
## redoCodeBlock
|
||||
|
||||
- **参数:**
|
||||
- `{Id} codeBlockId`
|
||||
|
||||
- **返回:**
|
||||
- `{CodeBlockStepValue | null}` 栈不存在或已无可重做记录时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
重做指定代码块的下一次变更。成功时会触发 `code-block-history-change` 事件。
|
||||
|
||||
## canUndoCodeBlock
|
||||
|
||||
- **参数:**
|
||||
- `{Id} codeBlockId`
|
||||
|
||||
- **返回:**
|
||||
- `{boolean}`
|
||||
|
||||
- **详情:**
|
||||
|
||||
指定代码块当前是否可撤销。栈不存在时返回 `false`。
|
||||
|
||||
## canRedoCodeBlock
|
||||
|
||||
- **参数:**
|
||||
- `{Id} codeBlockId`
|
||||
|
||||
- **返回:**
|
||||
- `{boolean}`
|
||||
|
||||
- **详情:**
|
||||
|
||||
指定代码块当前是否可重做。栈不存在时返回 `false`。
|
||||
|
||||
## pushDataSource
|
||||
|
||||
- **参数:**
|
||||
- `{Id} dataSourceId` 数据源 id
|
||||
- `{Object} payload`
|
||||
- `{DataSourceSchema | null} oldSchema` 变更前的数据源 schema;新增时为 `null`
|
||||
- `{DataSourceSchema | null} newSchema` 变更后的数据源 schema;删除时为 `null`
|
||||
|
||||
::: details 查看 DataSourceStepValue 及关联类型定义
|
||||
<<< @/../packages/editor/src/type.ts#DataSourceStepValue{ts}
|
||||
:::
|
||||
|
||||
- **返回:**
|
||||
- `{DataSourceStepValue | null}` 入栈失败(未传 id)时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
推入一条数据源变更记录。与页面 / 节点完全无关,按 `dataSourceId` 维度独立一份 `UndoRedo` 栈,
|
||||
栈实例存放在 `historyService.state.dataSourceState[dataSourceId]`。
|
||||
|
||||
入栈成功后会触发 `data-source-history-change` 事件。
|
||||
|
||||
::: tip
|
||||
`dataSourceService.add` / `update` / `remove` 内部已经自动调用本方法,业务代码通常无需手动调用。
|
||||
:::
|
||||
|
||||
## undoDataSource
|
||||
|
||||
- **参数:**
|
||||
- `{Id} dataSourceId`
|
||||
|
||||
- **返回:**
|
||||
- `{DataSourceStepValue | null}`
|
||||
|
||||
- **详情:**
|
||||
|
||||
撤销指定数据源的最近一次变更。成功时会触发 `data-source-history-change` 事件。
|
||||
拿到 step 后由调用方根据 `step.oldSchema` 写回 `dataSourceService`(本方法不会自动回放)。
|
||||
|
||||
## redoDataSource
|
||||
|
||||
- **参数:**
|
||||
- `{Id} dataSourceId`
|
||||
|
||||
- **返回:**
|
||||
- `{DataSourceStepValue | null}`
|
||||
|
||||
- **详情:**
|
||||
|
||||
重做指定数据源的下一次变更。成功时会触发 `data-source-history-change` 事件。
|
||||
|
||||
## canUndoDataSource
|
||||
|
||||
- **参数:**
|
||||
- `{Id} dataSourceId`
|
||||
|
||||
- **返回:**
|
||||
- `{boolean}`
|
||||
|
||||
- **详情:**
|
||||
|
||||
指定数据源当前是否可撤销。栈不存在时返回 `false`。
|
||||
|
||||
## canRedoDataSource
|
||||
|
||||
- **参数:**
|
||||
- `{Id} dataSourceId`
|
||||
|
||||
- **返回:**
|
||||
- `{boolean}`
|
||||
|
||||
- **详情:**
|
||||
|
||||
指定数据源当前是否可重做。栈不存在时返回 `false`。
|
||||
|
||||
## destroy
|
||||
|
||||
- **详情:**
|
||||
|
||||
@ -25,6 +25,7 @@ import { Target, Watcher } from '@tmagic/core';
|
||||
import type { TableColumnConfig } from '@tmagic/form';
|
||||
|
||||
import editorService from '@editor/services/editor';
|
||||
import historyService from '@editor/services/history';
|
||||
import storageService, { Protocol } from '@editor/services/storage';
|
||||
import type { AsyncHookPlugin, CodeState } from '@editor/type';
|
||||
import { CODE_DRAFT_STORAGE_KEY } from '@editor/type';
|
||||
@ -123,6 +124,9 @@ class CodeBlock extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
// 历史记录:在写入前快照旧内容,区分新增/更新
|
||||
const oldContent: CodeBlockContent | null = codeDsl[id] ? cloneDeep(codeDsl[id]) : null;
|
||||
|
||||
const existContent = codeDsl[id] || {};
|
||||
|
||||
codeDsl[id] = {
|
||||
@ -130,6 +134,10 @@ class CodeBlock extends BaseService {
|
||||
...codeConfigProcessed,
|
||||
};
|
||||
|
||||
const newContent = cloneDeep(codeDsl[id]);
|
||||
|
||||
historyService.pushCodeBlock(id, { oldContent, newContent });
|
||||
|
||||
this.emit('addOrUpdate', id, codeDsl[id]);
|
||||
}
|
||||
|
||||
@ -225,8 +233,15 @@ class CodeBlock extends BaseService {
|
||||
if (!currentDsl) return;
|
||||
|
||||
codeIds.forEach((id) => {
|
||||
// 历史记录:删除前快照内容;不存在的 id 直接跳过历史推入
|
||||
const oldContent: CodeBlockContent | null = currentDsl[id] ? cloneDeep(currentDsl[id]) : null;
|
||||
|
||||
delete currentDsl[id];
|
||||
|
||||
if (oldContent) {
|
||||
historyService.pushCodeBlock(id, { oldContent, newContent: null });
|
||||
}
|
||||
|
||||
this.emit('remove', id);
|
||||
});
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import type { ChangeRecord, FormConfig } from '@tmagic/form';
|
||||
import { guid, toLine } from '@tmagic/utils';
|
||||
|
||||
import editorService from '@editor/services/editor';
|
||||
import historyService from '@editor/services/history';
|
||||
import storageService, { Protocol } from '@editor/services/storage';
|
||||
import type { DatasourceTypeOption, SyncHookPlugin } from '@editor/type';
|
||||
import { getFormConfig, getFormValue } from '@editor/utils/data-source';
|
||||
@ -110,6 +111,8 @@ class DataSource extends BaseService {
|
||||
|
||||
this.get('dataSources').push(newConfig);
|
||||
|
||||
historyService.pushDataSource(newConfig.id, { oldSchema: null, newSchema: newConfig });
|
||||
|
||||
this.emit('add', newConfig);
|
||||
|
||||
return newConfig;
|
||||
@ -125,6 +128,11 @@ class DataSource extends BaseService {
|
||||
|
||||
dataSources[index] = newConfig;
|
||||
|
||||
historyService.pushDataSource(newConfig.id, {
|
||||
oldSchema: oldConfig ? cloneDeep(oldConfig) : null,
|
||||
newSchema: newConfig,
|
||||
});
|
||||
|
||||
this.emit('update', newConfig, {
|
||||
oldConfig,
|
||||
changeRecords,
|
||||
@ -136,8 +144,13 @@ class DataSource extends BaseService {
|
||||
public remove(id: string) {
|
||||
const dataSources = this.get('dataSources');
|
||||
const index = dataSources.findIndex((ds) => ds.id === id);
|
||||
const oldConfig = index !== -1 ? dataSources[index] : null;
|
||||
dataSources.splice(index, 1);
|
||||
|
||||
if (oldConfig) {
|
||||
historyService.pushDataSource(id, { oldSchema: cloneDeep(oldConfig), newSchema: null });
|
||||
}
|
||||
|
||||
this.emit('remove', id);
|
||||
}
|
||||
|
||||
|
||||
@ -17,10 +17,11 @@
|
||||
*/
|
||||
|
||||
import { reactive } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { Id, MPage, MPageFragment } from '@tmagic/core';
|
||||
import type { CodeBlockContent, DataSourceSchema, Id, MPage, MPageFragment } from '@tmagic/core';
|
||||
|
||||
import type { HistoryState, StepValue } from '@editor/type';
|
||||
import type { CodeBlockStepValue, DataSourceStepValue, HistoryState, StepValue } from '@editor/type';
|
||||
import { UndoRedo } from '@editor/utils/undo-redo';
|
||||
|
||||
import BaseService from './BaseService';
|
||||
@ -31,6 +32,8 @@ class History extends BaseService {
|
||||
pageId: undefined,
|
||||
canRedo: false,
|
||||
canUndo: false,
|
||||
codeBlockState: {},
|
||||
dataSourceState: {},
|
||||
});
|
||||
|
||||
constructor() {
|
||||
@ -41,6 +44,8 @@ class History extends BaseService {
|
||||
|
||||
public reset() {
|
||||
this.state.pageSteps = {};
|
||||
this.state.codeBlockState = {};
|
||||
this.state.dataSourceState = {};
|
||||
this.resetPage();
|
||||
}
|
||||
|
||||
@ -69,6 +74,8 @@ class History extends BaseService {
|
||||
this.state.pageSteps = {};
|
||||
this.state.canRedo = false;
|
||||
this.state.canUndo = false;
|
||||
this.state.codeBlockState = {};
|
||||
this.state.dataSourceState = {};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -88,6 +95,114 @@ class History extends BaseService {
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推入一条代码块变更记录(与页面/节点完全无关),按 `codeBlockId` 维度独立一份 UndoRedo 栈。
|
||||
*
|
||||
* - 新增:oldContent = null,newContent = 新内容
|
||||
* - 更新:oldContent / newContent 都为对应内容
|
||||
* - 删除:newContent = null,oldContent = 删除前内容
|
||||
* - 不直接驱动 codeBlockService,调用方负责实际写回。
|
||||
*/
|
||||
public pushCodeBlock(
|
||||
codeBlockId: Id,
|
||||
payload: {
|
||||
oldContent: CodeBlockContent | null;
|
||||
newContent: CodeBlockContent | null;
|
||||
},
|
||||
): CodeBlockStepValue | null {
|
||||
if (!codeBlockId) return null;
|
||||
|
||||
const step: CodeBlockStepValue = {
|
||||
id: codeBlockId,
|
||||
oldContent: payload.oldContent ? cloneDeep(payload.oldContent) : null,
|
||||
newContent: payload.newContent ? cloneDeep(payload.newContent) : null,
|
||||
};
|
||||
|
||||
this.getCodeBlockUndoRedo(codeBlockId).pushElement(step);
|
||||
this.emit('code-block-history-change', codeBlockId, step);
|
||||
return step;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推入一条数据源变更记录(与页面/节点完全无关),按 `dataSourceId` 维度独立一份 UndoRedo 栈。
|
||||
* 行为同 pushCodeBlock(新增 oldSchema=null;删除 newSchema=null)。
|
||||
*/
|
||||
public pushDataSource(
|
||||
dataSourceId: Id,
|
||||
payload: {
|
||||
oldSchema: DataSourceSchema | null;
|
||||
newSchema: DataSourceSchema | null;
|
||||
},
|
||||
): DataSourceStepValue | null {
|
||||
if (!dataSourceId) return null;
|
||||
|
||||
const step: DataSourceStepValue = {
|
||||
id: dataSourceId,
|
||||
oldSchema: payload.oldSchema ? cloneDeep(payload.oldSchema) : null,
|
||||
newSchema: payload.newSchema ? cloneDeep(payload.newSchema) : null,
|
||||
};
|
||||
|
||||
this.getDataSourceUndoRedo(dataSourceId).pushElement(step);
|
||||
this.emit('data-source-history-change', dataSourceId, step);
|
||||
return step;
|
||||
}
|
||||
|
||||
/** 撤销指定代码块的最近一次变更。 */
|
||||
public undoCodeBlock(codeBlockId: Id): CodeBlockStepValue | null {
|
||||
const undoRedo = this.state.codeBlockState[codeBlockId];
|
||||
if (!undoRedo) return null;
|
||||
const step = undoRedo.undo();
|
||||
if (step) this.emit('code-block-history-change', codeBlockId, step);
|
||||
return step;
|
||||
}
|
||||
|
||||
/** 重做指定代码块的下一次变更。 */
|
||||
public redoCodeBlock(codeBlockId: Id): CodeBlockStepValue | null {
|
||||
const undoRedo = this.state.codeBlockState[codeBlockId];
|
||||
if (!undoRedo) return null;
|
||||
const step = undoRedo.redo();
|
||||
if (step) this.emit('code-block-history-change', codeBlockId, step);
|
||||
return step;
|
||||
}
|
||||
|
||||
/** 是否可对指定代码块撤销。 */
|
||||
public canUndoCodeBlock(codeBlockId: Id): boolean {
|
||||
return this.state.codeBlockState[codeBlockId]?.canUndo() ?? false;
|
||||
}
|
||||
|
||||
/** 是否可对指定代码块重做。 */
|
||||
public canRedoCodeBlock(codeBlockId: Id): boolean {
|
||||
return this.state.codeBlockState[codeBlockId]?.canRedo() ?? false;
|
||||
}
|
||||
|
||||
/** 撤销指定数据源的最近一次变更。 */
|
||||
public undoDataSource(dataSourceId: Id): DataSourceStepValue | null {
|
||||
const undoRedo = this.state.dataSourceState[dataSourceId];
|
||||
if (!undoRedo) return null;
|
||||
const step = undoRedo.undo();
|
||||
if (step) this.emit('data-source-history-change', dataSourceId, step);
|
||||
return step;
|
||||
}
|
||||
|
||||
/** 重做指定数据源的下一次变更。 */
|
||||
public redoDataSource(dataSourceId: Id): DataSourceStepValue | null {
|
||||
const undoRedo = this.state.dataSourceState[dataSourceId];
|
||||
if (!undoRedo) return null;
|
||||
const step = undoRedo.redo();
|
||||
if (step) this.emit('data-source-history-change', dataSourceId, step);
|
||||
return step;
|
||||
}
|
||||
|
||||
/** 是否可对指定数据源撤销。 */
|
||||
public canUndoDataSource(dataSourceId: Id): boolean {
|
||||
return this.state.dataSourceState[dataSourceId]?.canUndo() ?? false;
|
||||
}
|
||||
|
||||
/** 是否可对指定数据源重做。 */
|
||||
public canRedoDataSource(dataSourceId: Id): boolean {
|
||||
return this.state.dataSourceState[dataSourceId]?.canRedo() ?? false;
|
||||
}
|
||||
|
||||
public undo(): StepValue | null {
|
||||
const undoRedo = this.getUndoRedo();
|
||||
if (!undoRedo) return null;
|
||||
@ -130,6 +245,26 @@ class History extends BaseService {
|
||||
this.state.canRedo = undoRedo?.canRedo() || false;
|
||||
this.state.canUndo = undoRedo?.canUndo() || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 id 获取(或创建)指定代码块的 UndoRedo 栈。
|
||||
*/
|
||||
private getCodeBlockUndoRedo(codeBlockId: Id): UndoRedo<CodeBlockStepValue> {
|
||||
if (!this.state.codeBlockState[codeBlockId]) {
|
||||
this.state.codeBlockState[codeBlockId] = new UndoRedo<CodeBlockStepValue>();
|
||||
}
|
||||
return this.state.codeBlockState[codeBlockId];
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 id 获取(或创建)指定数据源的 UndoRedo 栈。
|
||||
*/
|
||||
private getDataSourceUndoRedo(dataSourceId: Id): UndoRedo<DataSourceStepValue> {
|
||||
if (!this.state.dataSourceState[dataSourceId]) {
|
||||
this.state.dataSourceState[dataSourceId] = new UndoRedo<DataSourceStepValue>();
|
||||
}
|
||||
return this.state.dataSourceState[dataSourceId];
|
||||
}
|
||||
}
|
||||
|
||||
export type HistoryService = History;
|
||||
|
||||
@ -22,7 +22,17 @@ import type * as Monaco from 'monaco-editor';
|
||||
import type { default as Sortable, Options, SortableEvent } from 'sortablejs';
|
||||
import type { PascalCasedProperties, Writable } from 'type-fest';
|
||||
|
||||
import type { CodeBlockContent, CodeBlockDSL, Id, MApp, MContainer, MNode, MPage, MPageFragment } from '@tmagic/core';
|
||||
import type {
|
||||
CodeBlockContent,
|
||||
CodeBlockDSL,
|
||||
DataSourceSchema,
|
||||
Id,
|
||||
MApp,
|
||||
MContainer,
|
||||
MNode,
|
||||
MPage,
|
||||
MPageFragment,
|
||||
} from '@tmagic/core';
|
||||
import type { ChangeRecord, FormConfig, TableColumnConfig, TypeFunction } from '@tmagic/form';
|
||||
import type StageCore from '@tmagic/stage';
|
||||
import type {
|
||||
@ -632,11 +642,55 @@ export interface StepValue {
|
||||
}
|
||||
// #endregion StepValue
|
||||
|
||||
// #region CodeBlockStepValue
|
||||
/**
|
||||
* 代码块历史记录条目。按 codeBlock.id 分组保存到 historyState.codeBlockState。
|
||||
* - 新增:oldContent = null,newContent = 新内容
|
||||
* - 更新:oldContent / newContent 都为对应内容
|
||||
* - 删除:newContent = null,oldContent = 删除前内容
|
||||
*/
|
||||
export interface CodeBlockStepValue {
|
||||
/** 关联的代码块 id */
|
||||
id: Id;
|
||||
/** 变更前的代码块内容,新增时为 null */
|
||||
oldContent: CodeBlockContent | null;
|
||||
/** 变更后的代码块内容,删除时为 null */
|
||||
newContent: CodeBlockContent | null;
|
||||
}
|
||||
// #endregion CodeBlockStepValue
|
||||
|
||||
// #region DataSourceStepValue
|
||||
/**
|
||||
* 数据源历史记录条目。按 dataSource.id 分组保存到 historyState.dataSourceState。
|
||||
* - 新增:oldSchema = null,newSchema = 新 schema
|
||||
* - 更新:oldSchema / newSchema 都为对应 schema
|
||||
* - 删除:newSchema = null,oldSchema = 删除前 schema
|
||||
*/
|
||||
export interface DataSourceStepValue {
|
||||
/** 关联的数据源 id */
|
||||
id: Id;
|
||||
/** 变更前的数据源 schema,新增时为 null */
|
||||
oldSchema: DataSourceSchema | null;
|
||||
/** 变更后的数据源 schema,删除时为 null */
|
||||
newSchema: DataSourceSchema | null;
|
||||
}
|
||||
// #endregion DataSourceStepValue
|
||||
|
||||
export interface HistoryState {
|
||||
pageId?: Id;
|
||||
pageSteps: Record<Id, UndoRedo<StepValue>>;
|
||||
canRedo: boolean;
|
||||
canUndo: boolean;
|
||||
/**
|
||||
* 代码块历史栈,按 codeBlock.id 分组(每个代码块独立一份 UndoRedo)。
|
||||
* 与页面/节点无关,支持独立 undo/redo。
|
||||
*/
|
||||
codeBlockState: Record<Id, UndoRedo<CodeBlockStepValue>>;
|
||||
/**
|
||||
* 数据源历史栈,按 dataSource.id 分组(每个数据源独立一份 UndoRedo)。
|
||||
* 与页面/节点无关,支持独立 undo/redo。
|
||||
*/
|
||||
dataSourceState: Record<Id, UndoRedo<DataSourceStepValue>>;
|
||||
}
|
||||
|
||||
export enum KeyBindingCommand {
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import codeBlockService from '@editor/services/codeBlock';
|
||||
import historyService from '@editor/services/history';
|
||||
import storageService, { Protocol } from '@editor/services/storage';
|
||||
import { CODE_DRAFT_STORAGE_KEY } from '@editor/type';
|
||||
import { setEditorConfig } from '@editor/utils/config';
|
||||
@ -26,6 +27,7 @@ beforeAll(() => {
|
||||
|
||||
beforeEach(() => {
|
||||
codeBlockService.resetState();
|
||||
historyService.reset();
|
||||
globalThis.localStorage.clear();
|
||||
});
|
||||
|
||||
@ -166,3 +168,45 @@ describe('CodeBlockService - 基础', () => {
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodeBlockService - 历史记录接入', () => {
|
||||
test('setCodeDslByIdSync - 新增时入历史(oldContent=null)', async () => {
|
||||
await codeBlockService.setCodeDsl({} as any);
|
||||
codeBlockService.setCodeDslByIdSync('new_code', { name: 'A' } as any);
|
||||
|
||||
expect(historyService.canUndoCodeBlock('new_code')).toBe(true);
|
||||
const step = historyService.undoCodeBlock('new_code');
|
||||
expect(step?.oldContent).toBeNull();
|
||||
expect(step?.newContent).toEqual(expect.objectContaining({ name: 'A' }));
|
||||
});
|
||||
|
||||
test('setCodeDslByIdSync - 更新时入历史(oldContent / newContent 都非空)', async () => {
|
||||
await codeBlockService.setCodeDsl({ a: { name: 'A' } } as any);
|
||||
codeBlockService.setCodeDslByIdSync('a', { name: 'A2' } as any);
|
||||
|
||||
const step = historyService.undoCodeBlock('a');
|
||||
expect(step?.oldContent).toEqual({ name: 'A' });
|
||||
expect(step?.newContent).toEqual(expect.objectContaining({ name: 'A2' }));
|
||||
});
|
||||
|
||||
test('setCodeDslByIdSync - force=false 已存在时不入历史', async () => {
|
||||
await codeBlockService.setCodeDsl({ a: { name: 'A' } } as any);
|
||||
codeBlockService.setCodeDslByIdSync('a', { name: 'A2' } as any, false);
|
||||
expect(historyService.canUndoCodeBlock('a')).toBe(false);
|
||||
});
|
||||
|
||||
test('deleteCodeDslByIds - 删除已存在的代码块入历史(newContent=null)', async () => {
|
||||
await codeBlockService.setCodeDsl({ a: { name: 'A' }, b: { name: 'B' } } as any);
|
||||
await codeBlockService.deleteCodeDslByIds(['a']);
|
||||
|
||||
const step = historyService.undoCodeBlock('a');
|
||||
expect(step?.oldContent).toEqual({ name: 'A' });
|
||||
expect(step?.newContent).toBeNull();
|
||||
});
|
||||
|
||||
test('deleteCodeDslByIds - 删除不存在的 id 不入历史', async () => {
|
||||
await codeBlockService.setCodeDsl({} as any);
|
||||
await codeBlockService.deleteCodeDslByIds(['ghost']);
|
||||
expect(historyService.canUndoCodeBlock('ghost')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import dataSource from '@editor/services/dataSource';
|
||||
import historyService from '@editor/services/history';
|
||||
import storageService, { Protocol } from '@editor/services/storage';
|
||||
import { setEditorConfig } from '@editor/utils/config';
|
||||
import { COPY_DS_STORAGE_KEY } from '@editor/utils/editor';
|
||||
@ -27,6 +28,7 @@ beforeEach(() => {
|
||||
dataSource.set('values', {});
|
||||
dataSource.set('events', {});
|
||||
dataSource.set('methods', {});
|
||||
historyService.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -127,3 +129,39 @@ describe('DataSource service', () => {
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DataSource service - 历史记录接入', () => {
|
||||
test('add - 入历史(oldSchema=null)', () => {
|
||||
const ds = dataSource.add({ title: 'a', type: 'base' } as any);
|
||||
expect(historyService.canUndoDataSource(ds.id!)).toBe(true);
|
||||
const step = historyService.undoDataSource(ds.id!);
|
||||
expect(step?.oldSchema).toBeNull();
|
||||
expect(step?.newSchema?.title).toBe('a');
|
||||
});
|
||||
|
||||
test('update - 入历史,oldSchema 是旧值,newSchema 是新值', () => {
|
||||
const created = dataSource.add({ title: 'a', type: 'base' } as any);
|
||||
// 清掉 add 推入的那条
|
||||
historyService.reset();
|
||||
|
||||
dataSource.update({ ...created, title: 'b' } as any);
|
||||
const step = historyService.undoDataSource(created.id!);
|
||||
expect(step?.oldSchema?.title).toBe('a');
|
||||
expect(step?.newSchema?.title).toBe('b');
|
||||
});
|
||||
|
||||
test('remove - 入历史(newSchema=null)', () => {
|
||||
const created = dataSource.add({ title: 'a', type: 'base' } as any);
|
||||
historyService.reset();
|
||||
|
||||
dataSource.remove(created.id!);
|
||||
const step = historyService.undoDataSource(created.id!);
|
||||
expect(step?.oldSchema?.title).toBe('a');
|
||||
expect(step?.newSchema).toBeNull();
|
||||
});
|
||||
|
||||
test('remove - 不存在的 id 不入历史', () => {
|
||||
dataSource.remove('ghost');
|
||||
expect(historyService.canUndoDataSource('ghost')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
*
|
||||
* Copyright (C) 2025 Tencent.
|
||||
*/
|
||||
import { afterEach, describe, expect, test } from 'vitest';
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import history from '@editor/services/history';
|
||||
|
||||
@ -85,3 +85,144 @@ describe('history service', () => {
|
||||
expect(history.state.canUndo).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('history service - codeBlock', () => {
|
||||
test('pushCodeBlock 入栈并触发 code-block-history-change 事件', () => {
|
||||
const fn = vi.fn();
|
||||
history.on('code-block-history-change', fn);
|
||||
|
||||
const step = history.pushCodeBlock('code_1', {
|
||||
oldContent: null,
|
||||
newContent: { name: 'A', content: 'x' } as any,
|
||||
});
|
||||
|
||||
expect(step).not.toBeNull();
|
||||
expect(step?.id).toBe('code_1');
|
||||
expect(step?.oldContent).toBeNull();
|
||||
expect(step?.newContent).toEqual({ name: 'A', content: 'x' });
|
||||
expect((history.state.codeBlockState as any).code_1).toBeDefined();
|
||||
expect(history.canUndoCodeBlock('code_1')).toBe(true);
|
||||
expect(fn).toHaveBeenCalledWith('code_1', expect.objectContaining({ id: 'code_1' }));
|
||||
|
||||
history.off('code-block-history-change', fn);
|
||||
});
|
||||
|
||||
test('pushCodeBlock 不传 id 返回 null', () => {
|
||||
expect(history.pushCodeBlock('', { oldContent: null, newContent: null })).toBeNull();
|
||||
});
|
||||
|
||||
test('undoCodeBlock / redoCodeBlock 走对应 id 的 UndoRedo 栈', () => {
|
||||
history.pushCodeBlock('code_1', { oldContent: null, newContent: { name: 'A' } as any });
|
||||
history.pushCodeBlock('code_1', {
|
||||
oldContent: { name: 'A' } as any,
|
||||
newContent: { name: 'B' } as any,
|
||||
});
|
||||
|
||||
expect(history.canUndoCodeBlock('code_1')).toBe(true);
|
||||
const undone = history.undoCodeBlock('code_1');
|
||||
expect(undone?.newContent).toEqual({ name: 'B' });
|
||||
expect(history.canRedoCodeBlock('code_1')).toBe(true);
|
||||
|
||||
const redone = history.redoCodeBlock('code_1');
|
||||
expect(redone?.newContent).toEqual({ name: 'B' });
|
||||
});
|
||||
|
||||
test('undoCodeBlock 对不存在 id 返回 null', () => {
|
||||
expect(history.undoCodeBlock('not-exist')).toBeNull();
|
||||
expect(history.redoCodeBlock('not-exist')).toBeNull();
|
||||
expect(history.canUndoCodeBlock('not-exist')).toBe(false);
|
||||
expect(history.canRedoCodeBlock('not-exist')).toBe(false);
|
||||
});
|
||||
|
||||
test('不同代码块 id 的栈相互隔离', () => {
|
||||
history.pushCodeBlock('code_1', { oldContent: null, newContent: { name: 'A' } as any });
|
||||
history.pushCodeBlock('code_2', { oldContent: null, newContent: { name: 'B' } as any });
|
||||
|
||||
expect(history.canUndoCodeBlock('code_1')).toBe(true);
|
||||
expect(history.canUndoCodeBlock('code_2')).toBe(true);
|
||||
|
||||
history.undoCodeBlock('code_1');
|
||||
expect(history.canUndoCodeBlock('code_1')).toBe(false);
|
||||
// code_2 的栈不受影响
|
||||
expect(history.canUndoCodeBlock('code_2')).toBe(true);
|
||||
});
|
||||
|
||||
test('reset / resetState 清空 codeBlockState', () => {
|
||||
history.pushCodeBlock('code_1', { oldContent: null, newContent: { name: 'A' } as any });
|
||||
history.reset();
|
||||
expect(Object.keys(history.state.codeBlockState)).toHaveLength(0);
|
||||
|
||||
history.pushCodeBlock('code_1', { oldContent: null, newContent: { name: 'A' } as any });
|
||||
history.resetState();
|
||||
expect(Object.keys(history.state.codeBlockState)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('history service - dataSource', () => {
|
||||
test('pushDataSource 入栈并触发 data-source-history-change 事件', () => {
|
||||
const fn = vi.fn();
|
||||
history.on('data-source-history-change', fn);
|
||||
|
||||
const step = history.pushDataSource('ds_1', {
|
||||
oldSchema: null,
|
||||
newSchema: { id: 'ds_1', type: 'base', title: 'A' } as any,
|
||||
});
|
||||
|
||||
expect(step).not.toBeNull();
|
||||
expect(step?.id).toBe('ds_1');
|
||||
expect(step?.oldSchema).toBeNull();
|
||||
expect(step?.newSchema?.title).toBe('A');
|
||||
expect((history.state.dataSourceState as any).ds_1).toBeDefined();
|
||||
expect(history.canUndoDataSource('ds_1')).toBe(true);
|
||||
expect(fn).toHaveBeenCalledWith('ds_1', expect.objectContaining({ id: 'ds_1' }));
|
||||
|
||||
history.off('data-source-history-change', fn);
|
||||
});
|
||||
|
||||
test('pushDataSource 不传 id 返回 null', () => {
|
||||
expect(history.pushDataSource('', { oldSchema: null, newSchema: null })).toBeNull();
|
||||
});
|
||||
|
||||
test('undoDataSource / redoDataSource 走对应 id 的 UndoRedo 栈', () => {
|
||||
history.pushDataSource('ds_1', {
|
||||
oldSchema: null,
|
||||
newSchema: { id: 'ds_1', type: 'base', title: 'A' } as any,
|
||||
});
|
||||
history.pushDataSource('ds_1', {
|
||||
oldSchema: { id: 'ds_1', type: 'base', title: 'A' } as any,
|
||||
newSchema: { id: 'ds_1', type: 'base', title: 'B' } as any,
|
||||
});
|
||||
|
||||
const undone = history.undoDataSource('ds_1');
|
||||
expect(undone?.newSchema?.title).toBe('B');
|
||||
|
||||
const redone = history.redoDataSource('ds_1');
|
||||
expect(redone?.newSchema?.title).toBe('B');
|
||||
});
|
||||
|
||||
test('undoDataSource 对不存在 id 返回 null', () => {
|
||||
expect(history.undoDataSource('not-exist')).toBeNull();
|
||||
expect(history.redoDataSource('not-exist')).toBeNull();
|
||||
expect(history.canUndoDataSource('not-exist')).toBe(false);
|
||||
expect(history.canRedoDataSource('not-exist')).toBe(false);
|
||||
});
|
||||
|
||||
test('不同数据源 id 的栈相互隔离', () => {
|
||||
history.pushDataSource('ds_1', { oldSchema: null, newSchema: { id: 'ds_1' } as any });
|
||||
history.pushDataSource('ds_2', { oldSchema: null, newSchema: { id: 'ds_2' } as any });
|
||||
|
||||
history.undoDataSource('ds_1');
|
||||
expect(history.canUndoDataSource('ds_1')).toBe(false);
|
||||
expect(history.canUndoDataSource('ds_2')).toBe(true);
|
||||
});
|
||||
|
||||
test('reset / resetState 清空 dataSourceState', () => {
|
||||
history.pushDataSource('ds_1', { oldSchema: null, newSchema: { id: 'ds_1' } as any });
|
||||
history.reset();
|
||||
expect(Object.keys(history.state.dataSourceState)).toHaveLength(0);
|
||||
|
||||
history.pushDataSource('ds_1', { oldSchema: null, newSchema: { id: 'ds_1' } as any });
|
||||
history.resetState();
|
||||
expect(Object.keys(history.state.dataSourceState)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user