mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-05-30 04:08:04 +00:00
feat(editor): 历史记录接入 changeRecords,undo/redo 按 propPath 局部更新
- 节点 / 数据源 / 代码块的 history step 增加 changeRecords 字段 - editor.update / dataSource.update / codeBlock.setCodeDslById(Sync) 透传 changeRecords 入历史 - applyHistoryOp 的 update 分支:携带 changeRecords 时,按 propPath 从 oldNode/newNode 取值 构造最小 patch 走 update,不冲掉同节点上其它无关变更;缺省退化为整节点替换 (覆盖 sort/moveLayer/拖动等纯快照场景) - editor.update 增加 changeRecordList 形参,多节点场景每个节点单独保留 records; use-stage 多选拖动 / 缩放改用 changeRecordList,避免 records 在多节点间共享 - use-code-block-edit.submitCodeBlockHandler 透传 form changeRecords - 同步更新 editor / dataSource / codeBlock / history service 文档
This commit is contained in:
parent
4c855ba50b
commit
09558fa027
@ -49,8 +49,13 @@
|
||||
- `{string | number}` id 代码块id
|
||||
- {Partial<`CodeBlockContent`>} codeConfig 代码块内容配置信息
|
||||
- `{Object}` options 可选配置
|
||||
- {`ChangeRecord`[]} changeRecords form 端变更记录,用于历史记录的精细化撤销/重做
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
|
||||
::: details 查看 ChangeRecord 类型定义
|
||||
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
|
||||
:::
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<void>}`
|
||||
|
||||
@ -65,6 +70,7 @@
|
||||
- {Partial<`CodeBlockContent`>} codeConfig 代码块内容配置信息
|
||||
- `{boolean}` force 是否强制写入,默认 `true`;为 `false` 时若同 id 已存在则跳过
|
||||
- `{Object}` options 可选配置
|
||||
- {`ChangeRecord`[]} changeRecords form 端变更记录,用于历史记录的精细化撤销/重做
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
|
||||
- **返回:**
|
||||
@ -77,6 +83,7 @@
|
||||
::: tip
|
||||
写入成功时(`force=false` 且同 id 已存在的跳过场景除外)会自动调用 `historyService.pushCodeBlock`
|
||||
把本次变更入历史栈,参见 [historyService.pushCodeBlock](./historyServiceMethods.md#pushcodeblock)。
|
||||
传入的 `changeRecords` 会一同写进 step,撤销/重做时调用方可据此按 `propPath` 局部回放。
|
||||
传入 `doNotPushHistory: true` 可跳过写入历史栈,常用于批量导入、外部同步等非用户操作场景。
|
||||
:::
|
||||
|
||||
|
||||
@ -352,7 +352,8 @@ console.log(newDs.id); // 自动生成的id
|
||||
|
||||
::: tip
|
||||
更新成功会自动调用 `historyService.pushDataSource` 入栈一条 `oldSchema` / `newSchema`
|
||||
均为对应 schema 的更新记录。传入 `doNotPushHistory: true` 可跳过写入历史栈。
|
||||
均为对应 schema 的更新记录,传入的 `changeRecords` 也会一并写进 step;撤销/重做时调用方可据此按
|
||||
`propPath` 局部回放,缺省才退化为整 schema 替换。传入 `doNotPushHistory: true` 可跳过写入历史栈。
|
||||
:::
|
||||
|
||||
- **示例:**
|
||||
|
||||
@ -456,9 +456,14 @@ editorService.highlight("text_123");
|
||||
- **参数:**
|
||||
- {`MNode` | `MNode`[]} config 新的节点或节点集合
|
||||
- `{Object}` data 可选配置
|
||||
- {`ChangeRecord`[]} changeRecords 变更记录
|
||||
- {`ChangeRecord`[]} changeRecords 单节点 form 端变更记录(多节点场景下被忽略,使用 `changeRecordList`)
|
||||
- {`ChangeRecord`[][]} changeRecordList 多节点 form 端变更记录列表,按 `config` 数组同序对应每个节点;优先级高于 `changeRecords`
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
|
||||
::: details 查看 ChangeRecord 类型定义
|
||||
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
|
||||
:::
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`MNode` | `MNode`[]>} 新的节点或节点集合
|
||||
|
||||
@ -474,6 +479,16 @@ editorService.highlight("text_123");
|
||||
编辑器内部更新组件都是调用update来实现的,update除了更新操作外,还会记录历史堆,还会更新[代码块](../../guide/advanced/code-block.md)关系链。
|
||||
:::
|
||||
|
||||
:::tip
|
||||
**多节点场景必须使用 `changeRecordList`**:每个节点应保留自己独立的 records,不能把多个节点的
|
||||
records 合并到同一个 `changeRecords` 数组里,否则 `doUpdate` / 依赖收集 / 历史回放都会按错误的
|
||||
`propPath` 处理。
|
||||
|
||||
写入历史时,每个节点的 records 会单独保存到 `updatedItems[i].changeRecords`;撤销/重做时若有
|
||||
records,则仅按 `propPath` 局部更新对应字段,避免整节点替换冲掉同节点上的其它无关变更;缺省
|
||||
才退化为整节点替换(如内部 `sort` / `moveLayer` / 拖动等纯快照场景)。
|
||||
:::
|
||||
|
||||
## sort
|
||||
|
||||
- **[扩展支持](../../guide/editor-expand#行为扩展):** 是
|
||||
|
||||
@ -46,6 +46,8 @@
|
||||
<<< @/../packages/schema/src/index.ts#Id{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#MNode{ts}
|
||||
|
||||
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
|
||||
:::
|
||||
|
||||
- **返回:**
|
||||
@ -55,6 +57,12 @@
|
||||
|
||||
添加一条历史记录
|
||||
|
||||
::: tip
|
||||
`opType: 'update'` 的每个 `updatedItems[i]` 上可携带 `changeRecords`,用于撤销 / 重做时仅按
|
||||
`propPath` 局部更新对应字段,避免整节点替换冲掉同节点上的其它无关变更;不带
|
||||
`changeRecords` 时退化为整节点替换(如 `sort` / `moveLayer` / 拖动等纯快照场景)。
|
||||
:::
|
||||
|
||||
## undo
|
||||
|
||||
- **返回:**
|
||||
@ -62,7 +70,8 @@
|
||||
|
||||
- **详情:**
|
||||
|
||||
撤销当前操作
|
||||
撤销当前操作。`opType: 'update'` 时,若 `updatedItems[i].changeRecords` 存在,会按
|
||||
`propPath` 从 `oldNode` 取值做局部回滚;否则用 `oldNode` 整节点替换。
|
||||
|
||||
## redo
|
||||
|
||||
@ -71,7 +80,8 @@
|
||||
|
||||
- **详情:**
|
||||
|
||||
恢复到下一步
|
||||
恢复到下一步。`opType: 'update'` 时,若 `updatedItems[i].changeRecords` 存在,会按
|
||||
`propPath` 从 `newNode` 取值做局部重做;否则用 `newNode` 整节点替换。
|
||||
|
||||
## pushCodeBlock
|
||||
|
||||
@ -80,11 +90,14 @@
|
||||
- `{Object} payload`
|
||||
- `{CodeBlockContent | null} oldContent` 变更前的代码块内容;新增时为 `null`
|
||||
- `{CodeBlockContent | null} newContent` 变更后的代码块内容;删除时为 `null`
|
||||
- `{ChangeRecord[]} changeRecords` 可选;form 端 propPath/value 变更列表,撤销/重做时若有则按 propPath 局部更新;缺省(或空数组)才退化为整内容替换
|
||||
|
||||
::: details 查看 CodeBlockStepValue 及关联类型定义
|
||||
<<< @/../packages/editor/src/type.ts#CodeBlockStepValue{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#CodeBlockContent{ts}
|
||||
|
||||
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
|
||||
:::
|
||||
|
||||
- **返回:**
|
||||
@ -158,9 +171,12 @@
|
||||
- `{Object} payload`
|
||||
- `{DataSourceSchema | null} oldSchema` 变更前的数据源 schema;新增时为 `null`
|
||||
- `{DataSourceSchema | null} newSchema` 变更后的数据源 schema;删除时为 `null`
|
||||
- `{ChangeRecord[]} changeRecords` 可选;form 端 propPath/value 变更列表,撤销/重做时若有则按 propPath 局部更新;缺省(或空数组)才退化为整 schema 替换
|
||||
|
||||
::: details 查看 DataSourceStepValue 及关联类型定义
|
||||
<<< @/../packages/editor/src/type.ts#DataSourceStepValue{ts}
|
||||
|
||||
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
|
||||
:::
|
||||
|
||||
- **返回:**
|
||||
|
||||
@ -3,6 +3,7 @@ import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { CodeBlockContent } from '@tmagic/core';
|
||||
import { tMagicMessage } from '@tmagic/design';
|
||||
import type { ContainerChangeEventData } from '@tmagic/form';
|
||||
|
||||
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
|
||||
import type { Services } from '@editor/type';
|
||||
@ -61,10 +62,12 @@ export const useCodeBlockEdit = (codeBlockService: Services['codeBlockService'])
|
||||
codeBlockService.deleteCodeDslByIds([key]);
|
||||
};
|
||||
|
||||
const submitCodeBlockHandler = async (values: CodeBlockContent) => {
|
||||
const submitCodeBlockHandler = async (values: CodeBlockContent, eventData?: ContainerChangeEventData) => {
|
||||
if (!codeId.value) return;
|
||||
|
||||
await codeBlockService.setCodeDslById(codeId.value, values);
|
||||
await codeBlockService.setCodeDslById(codeId.value, values, {
|
||||
changeRecords: eventData?.changeRecords,
|
||||
});
|
||||
|
||||
codeBlockEditorRef.value?.hide();
|
||||
};
|
||||
|
||||
@ -115,19 +115,21 @@ export const useStage = (stageOptions: StageOptions) => {
|
||||
}
|
||||
|
||||
// 多选拖动 / 多选缩放:所有元素整批走一次 update,避免历史栈被切成 N 条
|
||||
// changeRecordList 与 configs 同序,每个节点保留自己的 records;
|
||||
// 不能把多个节点的 records 合并到同一个数组里,否则 doUpdate / nodeUpdateHandler 会把别的节点的 propPath 当成自己的。
|
||||
const configs: MNode[] = [];
|
||||
const changeRecordsAll: ReturnType<typeof buildChangeRecords> = [];
|
||||
const changeRecordList: ReturnType<typeof buildChangeRecords>[] = [];
|
||||
ev.data.forEach((data) => {
|
||||
const id = getIdFromEl()(data.el);
|
||||
if (!id) return;
|
||||
|
||||
const { style = {} } = data;
|
||||
configs.push({ id, style });
|
||||
changeRecordsAll.push(...buildChangeRecords(style, 'style'));
|
||||
changeRecordList.push(buildChangeRecords(style, 'style'));
|
||||
});
|
||||
if (configs.length === 0) return;
|
||||
|
||||
editorService.update(configs, { changeRecords: changeRecordsAll });
|
||||
editorService.update(configs, { changeRecordList });
|
||||
});
|
||||
|
||||
stage.on('sort', (ev: SortEventData) => {
|
||||
|
||||
@ -22,7 +22,7 @@ import type { Writable } from 'type-fest';
|
||||
|
||||
import type { CodeBlockContent, CodeBlockDSL, Id, MNode, TargetOptions } from '@tmagic/core';
|
||||
import { Target, Watcher } from '@tmagic/core';
|
||||
import type { TableColumnConfig } from '@tmagic/form';
|
||||
import type { ChangeRecord, TableColumnConfig } from '@tmagic/form';
|
||||
|
||||
import editorService from '@editor/services/editor';
|
||||
import historyService from '@editor/services/history';
|
||||
@ -94,15 +94,16 @@ class CodeBlock extends BaseService {
|
||||
* @param {Id} id 代码块id
|
||||
* @param {CodeBlockContent} codeConfig 代码块内容配置信息
|
||||
* @param options 可选配置
|
||||
* @param options.changeRecords form 端 propPath/value 列表,用于历史记录的精细化撤销/重做
|
||||
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
* @returns {void}
|
||||
*/
|
||||
public async setCodeDslById(
|
||||
id: Id,
|
||||
codeConfig: Partial<CodeBlockContent>,
|
||||
{ doNotPushHistory = false }: { doNotPushHistory?: boolean } = {},
|
||||
{ changeRecords, doNotPushHistory = false }: { changeRecords?: ChangeRecord[]; doNotPushHistory?: boolean } = {},
|
||||
): Promise<void> {
|
||||
this.setCodeDslByIdSync(id, codeConfig, true, { doNotPushHistory });
|
||||
this.setCodeDslByIdSync(id, codeConfig, true, { changeRecords, doNotPushHistory });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -112,6 +113,7 @@ class CodeBlock extends BaseService {
|
||||
* @param {CodeBlockContent} codeConfig 代码块内容配置信息
|
||||
* @param {boolean} force 是否强制写入,默认true
|
||||
* @param options 可选配置
|
||||
* @param options.changeRecords form 端 propPath/value 列表,用于历史记录的精细化撤销/重做
|
||||
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
* @returns {void}
|
||||
*/
|
||||
@ -119,7 +121,7 @@ class CodeBlock extends BaseService {
|
||||
id: Id,
|
||||
codeConfig: Partial<CodeBlockContent>,
|
||||
force = true,
|
||||
{ doNotPushHistory = false }: { doNotPushHistory?: boolean } = {},
|
||||
{ changeRecords, doNotPushHistory = false }: { changeRecords?: ChangeRecord[]; doNotPushHistory?: boolean } = {},
|
||||
): void {
|
||||
const codeDsl = this.getCodeDsl();
|
||||
|
||||
@ -150,7 +152,7 @@ class CodeBlock extends BaseService {
|
||||
const newContent = cloneDeep(codeDsl[id]);
|
||||
|
||||
if (!doNotPushHistory) {
|
||||
historyService.pushCodeBlock(id, { oldContent, newContent });
|
||||
historyService.pushCodeBlock(id, { oldContent, newContent, changeRecords });
|
||||
}
|
||||
|
||||
this.emit('addOrUpdate', id, codeDsl[id]);
|
||||
|
||||
@ -153,6 +153,7 @@ class DataSource extends BaseService {
|
||||
historyService.pushDataSource(newConfig.id, {
|
||||
oldSchema: oldConfig ? cloneDeep(oldConfig) : null,
|
||||
newSchema: newConfig,
|
||||
changeRecords,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions }
|
||||
import { NodeType } from '@tmagic/core';
|
||||
import type { ChangeRecord } from '@tmagic/form';
|
||||
import { isFixed } from '@tmagic/stage';
|
||||
import { getNodeInfo, getNodePath, isPage, isPageFragment } from '@tmagic/utils';
|
||||
import { getNodeInfo, getNodePath, getValueByKeyPath, isPage, isPageFragment, setValueByKeyPath } from '@tmagic/utils';
|
||||
|
||||
import BaseService from '@editor/services//BaseService';
|
||||
import propsService from '@editor/services//props';
|
||||
@ -658,21 +658,33 @@ class Editor extends BaseService {
|
||||
* update后会触发依赖收集,收集完后会掉stage.update方法
|
||||
* @param config 新的节点配置,配置中需要有id信息
|
||||
* @param data 额外数据
|
||||
* @param data.changeRecords form 端变更记录
|
||||
* @param data.changeRecords 单节点 form 端变更记录(多节点场景下被忽略,使用 changeRecordList)
|
||||
* @param data.changeRecordList 多节点 form 端变更记录列表,按 config 数组同序对应每个节点;优先级高于 changeRecords
|
||||
* @param data.doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
* @returns 更新后的节点配置
|
||||
*/
|
||||
public async update(
|
||||
config: MNode | MNode[],
|
||||
data: { changeRecords?: ChangeRecord[]; doNotPushHistory?: boolean } = {},
|
||||
data: {
|
||||
changeRecords?: ChangeRecord[];
|
||||
changeRecordList?: ChangeRecord[][];
|
||||
doNotPushHistory?: boolean;
|
||||
} = {},
|
||||
): Promise<MNode | MNode[]> {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
const { doNotPushHistory = false } = data;
|
||||
const { doNotPushHistory = false, changeRecordList, changeRecords } = data;
|
||||
|
||||
const nodes = Array.isArray(config) ? config : [config];
|
||||
|
||||
const updateData = await Promise.all(nodes.map((node) => this.doUpdate(node, data)));
|
||||
// 多节点必须使用 changeRecordList 为每个节点提供独立的记录;
|
||||
// 否则同一份 changeRecords 会被复用到每个节点上,nodeUpdateHandler / 历史回放都会按错误的 propPath 处理。
|
||||
const updateData = await Promise.all(
|
||||
nodes.map((node, index) => {
|
||||
const recordsForNode = changeRecordList ? (changeRecordList[index] ?? []) : (changeRecords ?? []);
|
||||
return this.doUpdate(node, { changeRecords: recordsForNode });
|
||||
}),
|
||||
);
|
||||
|
||||
if (updateData[0].oldNode?.type !== NodeType.ROOT) {
|
||||
const curNodes = this.get('nodes');
|
||||
@ -685,6 +697,9 @@ class Editor extends BaseService {
|
||||
updatedItems: updateData.map((d) => ({
|
||||
oldNode: cloneDeep(d.oldNode),
|
||||
newNode: cloneDeep(toRaw(d.newNode)),
|
||||
// 每个节点单独保留自己的 changeRecords,便于撤销/重做时按 propPath 精细化更新;
|
||||
// 没有 changeRecords 的(如内部 sort/moveLayer 等整节点替换操作)会退化为全节点替换。
|
||||
changeRecords: d.changeRecords?.length ? cloneDeep(d.changeRecords) : undefined,
|
||||
})),
|
||||
},
|
||||
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
||||
@ -1272,7 +1287,26 @@ class Editor extends BaseService {
|
||||
}
|
||||
case 'update': {
|
||||
const items = step.updatedItems ?? [];
|
||||
const configs = items.map(({ oldNode, newNode }) => cloneDeep(reverse ? oldNode : newNode));
|
||||
// 优先按 changeRecords 局部 patch:仅触达 propPath 下的字段,避免整节点替换冲掉同节点上其它无关变更。
|
||||
// 没有 changeRecords 的(如内部 sort/moveLayer/拖动等整节点快照场景)才退化为整节点替换。
|
||||
const configs = items.map(({ oldNode, newNode, changeRecords }) => {
|
||||
if (changeRecords?.length) {
|
||||
const sourceForValues = reverse ? oldNode : newNode;
|
||||
// 仅保留 id / type 作为最小骨架,再按 propPath 写入需要回滚/重做的字段;
|
||||
// 后续 update -> mergeWith 会与现有节点深合并,patch 中未涉及的字段不会被改动。
|
||||
const patch: MNode = { id: newNode.id, type: newNode.type };
|
||||
for (const record of changeRecords) {
|
||||
if (!record.propPath) {
|
||||
// 没有 propPath 视为整节点替换
|
||||
return cloneDeep(sourceForValues);
|
||||
}
|
||||
const value = cloneDeep(getValueByKeyPath(record.propPath, sourceForValues));
|
||||
setValueByKeyPath(record.propPath, value, patch);
|
||||
}
|
||||
return patch;
|
||||
}
|
||||
return cloneDeep(reverse ? oldNode : newNode);
|
||||
});
|
||||
if (configs.length) {
|
||||
await this.update(configs, { doNotPushHistory: true });
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ import { reactive } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { CodeBlockContent, DataSourceSchema, Id, MPage, MPageFragment } from '@tmagic/core';
|
||||
import type { ChangeRecord } from '@tmagic/form';
|
||||
|
||||
import type { CodeBlockStepValue, DataSourceStepValue, HistoryState, StepValue } from '@editor/type';
|
||||
import { UndoRedo } from '@editor/utils/undo-redo';
|
||||
@ -101,6 +102,7 @@ class History extends BaseService {
|
||||
* - 新增:oldContent = null,newContent = 新内容
|
||||
* - 更新:oldContent / newContent 都为对应内容
|
||||
* - 删除:newContent = null,oldContent = 删除前内容
|
||||
* - `changeRecords` 来自 form 端,撤销/重做时若有则按 propPath 局部覆盖;缺省才退化为整内容替换。
|
||||
* - 不直接驱动 codeBlockService,调用方负责实际写回。
|
||||
*/
|
||||
public pushCodeBlock(
|
||||
@ -108,6 +110,7 @@ class History extends BaseService {
|
||||
payload: {
|
||||
oldContent: CodeBlockContent | null;
|
||||
newContent: CodeBlockContent | null;
|
||||
changeRecords?: ChangeRecord[];
|
||||
},
|
||||
): CodeBlockStepValue | null {
|
||||
if (!codeBlockId) return null;
|
||||
@ -116,6 +119,7 @@ class History extends BaseService {
|
||||
id: codeBlockId,
|
||||
oldContent: payload.oldContent ? cloneDeep(payload.oldContent) : null,
|
||||
newContent: payload.newContent ? cloneDeep(payload.newContent) : null,
|
||||
changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : undefined,
|
||||
};
|
||||
|
||||
this.getCodeBlockUndoRedo(codeBlockId).pushElement(step);
|
||||
@ -132,6 +136,7 @@ class History extends BaseService {
|
||||
payload: {
|
||||
oldSchema: DataSourceSchema | null;
|
||||
newSchema: DataSourceSchema | null;
|
||||
changeRecords?: ChangeRecord[];
|
||||
},
|
||||
): DataSourceStepValue | null {
|
||||
if (!dataSourceId) return null;
|
||||
@ -140,6 +145,7 @@ class History extends BaseService {
|
||||
id: dataSourceId,
|
||||
oldSchema: payload.oldSchema ? cloneDeep(payload.oldSchema) : null,
|
||||
newSchema: payload.newSchema ? cloneDeep(payload.newSchema) : null,
|
||||
changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : undefined,
|
||||
};
|
||||
|
||||
this.getDataSourceUndoRedo(dataSourceId).pushElement(step);
|
||||
|
||||
@ -637,8 +637,13 @@ export interface StepValue {
|
||||
indexMap?: Record<string, number>;
|
||||
/** opType 'remove': 被删除的节点及其位置信息 */
|
||||
removedItems?: { node: MNode; parentId: Id; index: number }[];
|
||||
/** opType 'update': 变更前后的节点快照 */
|
||||
updatedItems?: { oldNode: MNode; newNode: MNode }[];
|
||||
/**
|
||||
* opType 'update': 变更前后的节点快照
|
||||
*
|
||||
* `changeRecords` 来自 form 端的 propPath/value 列表,撤销/重做时只对这些 propPath 做局部更新;
|
||||
* 缺省(未传 / 空数组)才退化为整节点替换。
|
||||
*/
|
||||
updatedItems?: { oldNode: MNode; newNode: MNode; changeRecords?: ChangeRecord[] }[];
|
||||
}
|
||||
// #endregion StepValue
|
||||
|
||||
@ -656,6 +661,11 @@ export interface CodeBlockStepValue {
|
||||
oldContent: CodeBlockContent | null;
|
||||
/** 变更后的代码块内容,删除时为 null */
|
||||
newContent: CodeBlockContent | null;
|
||||
/**
|
||||
* form 端 propPath/value 列表。撤销/重做时若有则按 propPath 局部更新;
|
||||
* 缺省才退化为整内容替换。新增/删除场景通常无 changeRecords。
|
||||
*/
|
||||
changeRecords?: ChangeRecord[];
|
||||
}
|
||||
// #endregion CodeBlockStepValue
|
||||
|
||||
@ -673,6 +683,11 @@ export interface DataSourceStepValue {
|
||||
oldSchema: DataSourceSchema | null;
|
||||
/** 变更后的数据源 schema,删除时为 null */
|
||||
newSchema: DataSourceSchema | null;
|
||||
/**
|
||||
* form 端 propPath/value 列表。撤销/重做时若有则按 propPath 局部更新;
|
||||
* 缺省才退化为整 schema 替换。新增/删除场景通常无 changeRecords。
|
||||
*/
|
||||
changeRecords?: ChangeRecord[];
|
||||
}
|
||||
// #endregion DataSourceStepValue
|
||||
|
||||
|
||||
@ -119,7 +119,16 @@ describe('useCodeBlockEdit', () => {
|
||||
const hook = mountHook({ setCodeDslById });
|
||||
hook.codeId.value = 'id1';
|
||||
await hook.submitCodeBlockHandler({ name: 'b' } as any);
|
||||
expect(setCodeDslById).toHaveBeenCalledWith('id1', { name: 'b' });
|
||||
expect(setCodeDslById).toHaveBeenCalledWith('id1', { name: 'b' }, { changeRecords: undefined });
|
||||
expect(hideMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('submitCodeBlockHandler - 透传 eventData.changeRecords 给 setCodeDslById', async () => {
|
||||
const setCodeDslById = vi.fn();
|
||||
const hook = mountHook({ setCodeDslById });
|
||||
hook.codeId.value = 'id1';
|
||||
const records = [{ propPath: 'name', value: 'b' }];
|
||||
await hook.submitCodeBlockHandler({ name: 'b' } as any, { changeRecords: records } as any);
|
||||
expect(setCodeDslById).toHaveBeenCalledWith('id1', { name: 'b' }, { changeRecords: records });
|
||||
});
|
||||
});
|
||||
|
||||
@ -84,7 +84,9 @@ vi.mock('@editor/services/ui', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('@editor/utils/editor', () => ({
|
||||
buildChangeRecords: vi.fn(() => []),
|
||||
buildChangeRecords: vi.fn((style: Record<string, any>, basePath: string) =>
|
||||
Object.entries(style ?? {}).map(([k, v]) => ({ propPath: `${basePath}.${k}`, value: v })),
|
||||
),
|
||||
getGuideLineFromCache: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
@ -211,6 +213,11 @@ describe('useStage', () => {
|
||||
{ id: 'c1', style: { width: 10 } },
|
||||
{ id: 'c2', style: { width: 20 } },
|
||||
]);
|
||||
// changeRecordList 与 configs 同序,每个节点独立保有自己的 records,不能合并为一个数组
|
||||
expect(callArgs[1].changeRecordList).toHaveLength(2);
|
||||
expect(callArgs[1].changeRecordList[0]).toEqual([{ propPath: 'style.width', value: 10 }]);
|
||||
expect(callArgs[1].changeRecordList[1]).toEqual([{ propPath: 'style.width', value: 20 }]);
|
||||
expect(callArgs[1].changeRecords).toBeUndefined();
|
||||
});
|
||||
|
||||
test('sort 事件', () => {
|
||||
|
||||
@ -209,4 +209,24 @@ describe('CodeBlockService - 历史记录接入', () => {
|
||||
await codeBlockService.deleteCodeDslByIds(['ghost']);
|
||||
expect(historyService.canUndoCodeBlock('ghost')).toBe(false);
|
||||
});
|
||||
|
||||
test('setCodeDslByIdSync - 携带 changeRecords 时写入历史 step', async () => {
|
||||
historyService.reset();
|
||||
await codeBlockService.setCodeDsl({ a: { name: 'A' } } as any);
|
||||
codeBlockService.setCodeDslByIdSync('a', { name: 'A2' } as any, true, {
|
||||
changeRecords: [{ propPath: 'name', value: 'A2' }],
|
||||
});
|
||||
|
||||
const step = historyService.undoCodeBlock('a');
|
||||
expect(step?.changeRecords).toEqual([{ propPath: 'name', value: 'A2' }]);
|
||||
});
|
||||
|
||||
test('setCodeDslByIdSync - 不传 changeRecords 时 step.changeRecords 为 undefined', async () => {
|
||||
historyService.reset();
|
||||
await codeBlockService.setCodeDsl({ a: { name: 'A' } } as any);
|
||||
codeBlockService.setCodeDslByIdSync('a', { name: 'A2' } as any);
|
||||
|
||||
const step = historyService.undoCodeBlock('a');
|
||||
expect(step?.changeRecords).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -164,4 +164,25 @@ describe('DataSource service - 历史记录接入', () => {
|
||||
dataSource.remove('ghost');
|
||||
expect(historyService.canUndoDataSource('ghost')).toBe(false);
|
||||
});
|
||||
|
||||
test('update - 携带 changeRecords 时写入历史 step', () => {
|
||||
const created = dataSource.add({ title: 'a', type: 'base' } as any);
|
||||
historyService.reset();
|
||||
|
||||
dataSource.update({ ...created, title: 'b' } as any, {
|
||||
changeRecords: [{ propPath: 'title', value: 'b' }],
|
||||
});
|
||||
|
||||
const step = historyService.undoDataSource(created.id!);
|
||||
expect(step?.changeRecords).toEqual([{ propPath: 'title', value: 'b' }]);
|
||||
});
|
||||
|
||||
test('update - 不传 changeRecords 时 step.changeRecords 为 undefined', () => {
|
||||
const created = dataSource.add({ title: 'a', type: 'base' } as any);
|
||||
historyService.reset();
|
||||
|
||||
dataSource.update({ ...created, title: 'b' } as any);
|
||||
const step = historyService.undoDataSource(created.id!);
|
||||
expect(step?.changeRecords).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -667,4 +667,47 @@ describe('undo redo', () => {
|
||||
const redoNode = editorService.getNodeById(NodeId.NODE_ID);
|
||||
expect(redoNode?.id).toBeUndefined();
|
||||
});
|
||||
|
||||
test('update 携带 changeRecords 时,undo/redo 仅回滚/重做对应 propPath,不冲掉同节点其它字段', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
historyService.reset();
|
||||
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
// 先携带 changeRecords 改 width
|
||||
await editorService.update(
|
||||
{ id: NodeId.NODE_ID, type: 'text', style: { width: 500 } },
|
||||
{ changeRecords: [{ propPath: 'style.width', value: 500 }] },
|
||||
);
|
||||
expect(editorService.getNodeById(NodeId.NODE_ID)?.style?.width).toBe(500);
|
||||
|
||||
// 在 undo 之前再追加一个不入历史的字段(模拟"同节点上其它无关变更"),undo 不应把它冲掉
|
||||
await editorService.update(
|
||||
{ id: NodeId.NODE_ID, type: 'text', style: { width: 500, height: 80 } },
|
||||
{ doNotPushHistory: true },
|
||||
);
|
||||
expect(editorService.getNodeById(NodeId.NODE_ID)?.style?.height).toBe(80);
|
||||
|
||||
await editorService.undo();
|
||||
const afterUndo = editorService.getNodeById(NodeId.NODE_ID);
|
||||
// width 回退;height 因为不在 changeRecords 内不会被局部 patch 覆盖
|
||||
expect(afterUndo?.style?.width).toBe(270);
|
||||
expect(afterUndo?.style?.height).toBe(80);
|
||||
|
||||
await editorService.redo();
|
||||
const afterRedo = editorService.getNodeById(NodeId.NODE_ID);
|
||||
expect(afterRedo?.style?.width).toBe(500);
|
||||
expect(afterRedo?.style?.height).toBe(80);
|
||||
});
|
||||
|
||||
test('update 不带 changeRecords 时退化为整节点替换', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
historyService.reset();
|
||||
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
await editorService.update({ id: NodeId.NODE_ID, type: 'text', style: { width: 600 } });
|
||||
expect(editorService.getNodeById(NodeId.NODE_ID)?.style?.width).toBe(600);
|
||||
|
||||
await editorService.undo();
|
||||
expect(editorService.getNodeById(NodeId.NODE_ID)?.style?.width).toBe(270);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user