@tmagic/editor 1.8.0-beta.4 → 1.8.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/ToolButton.vue_vue_type_script_setup_true_lang.js +6 -6
- package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +8 -2
- package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +25 -8
- package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +6 -4
- package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +25 -12
- package/dist/es/fields/StyleSetter/components/Border.vue_vue_type_script_setup_true_lang.js +27 -5
- package/dist/es/index.js +6 -3
- package/dist/es/initService.js +1 -1
- package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +19 -55
- package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +22 -39
- package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +149 -103
- package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +32 -8
- package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +335 -213
- package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +36 -7
- package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +47 -60
- package/dist/es/layouts/history-list/composables.js +73 -32
- package/dist/es/layouts/page-bar/PageBar.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +16 -3
- package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +23 -1
- package/dist/es/services/codeBlock.js +96 -37
- package/dist/es/services/dataSource.js +70 -26
- package/dist/es/services/editor.js +266 -104
- package/dist/es/services/history.js +271 -212
- package/dist/es/services/ui.js +3 -0
- package/dist/es/style.css +53 -7
- package/dist/es/utils/data-source/index.js +2 -0
- package/dist/es/utils/editor.js +103 -49
- package/dist/es/utils/history.js +232 -0
- package/dist/es/utils/indexed-db.js +86 -0
- package/dist/es/utils/undo-redo.js +60 -1
- package/dist/style.css +53 -7
- package/dist/tmagic-editor.umd.cjs +2064 -1000
- package/package.json +7 -7
- package/src/components/CompareForm.vue +3 -1
- package/src/components/ToolButton.vue +2 -2
- package/src/fields/CodeSelectCol.vue +7 -1
- package/src/fields/DataSourceFieldSelect/FieldSelect.vue +16 -2
- package/src/fields/DataSourceFields.vue +37 -8
- package/src/fields/DataSourceMethodSelect.vue +9 -4
- package/src/fields/DataSourceMethods.vue +42 -21
- package/src/fields/StyleSetter/components/Border.vue +15 -6
- package/src/index.ts +1 -0
- package/src/initService.ts +1 -1
- package/src/layouts/Framework.vue +1 -1
- package/src/layouts/history-list/Bucket.vue +34 -71
- package/src/layouts/history-list/BucketTab.vue +46 -54
- package/src/layouts/history-list/GroupRow.vue +146 -111
- package/src/layouts/history-list/HistoryDiffDialog.vue +94 -68
- package/src/layouts/history-list/HistoryListPanel.vue +298 -115
- package/src/layouts/history-list/InitialRow.vue +28 -9
- package/src/layouts/history-list/PageTab.vue +57 -51
- package/src/layouts/history-list/composables.ts +157 -36
- package/src/layouts/page-bar/PageBar.vue +4 -2
- package/src/layouts/sidebar/Sidebar.vue +6 -1
- package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +30 -2
- package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +26 -1
- package/src/services/codeBlock.ts +113 -37
- package/src/services/dataSource.ts +94 -40
- package/src/services/editor.ts +376 -136
- package/src/services/history.ts +306 -209
- package/src/services/ui.ts +7 -0
- package/src/theme/history-list-panel.scss +72 -5
- package/src/theme/page-bar.scss +0 -4
- package/src/theme/style-setter/border.scss +4 -1
- package/src/type.ts +183 -64
- package/src/utils/data-source/index.ts +2 -0
- package/src/utils/editor.ts +166 -60
- package/src/utils/history.ts +308 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/indexed-db.ts +122 -0
- package/src/utils/undo-redo.ts +88 -0
- package/types/index.d.ts +813 -302
package/src/services/editor.ts
CHANGED
|
@@ -17,13 +17,21 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { reactive, toRaw } from 'vue';
|
|
20
|
-
import { cloneDeep, isObject, mergeWith, uniq } from 'lodash-es';
|
|
20
|
+
import { cloneDeep, isEmpty, isEqual, isObject, mergeWith, uniq } from 'lodash-es';
|
|
21
21
|
|
|
22
22
|
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
|
|
23
23
|
import { NodeType } from '@tmagic/core';
|
|
24
24
|
import type { ChangeRecord } from '@tmagic/form';
|
|
25
25
|
import { isFixed } from '@tmagic/stage';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
getNodeInfo,
|
|
28
|
+
getNodePath,
|
|
29
|
+
getValueByKeyPath,
|
|
30
|
+
guid,
|
|
31
|
+
isPage,
|
|
32
|
+
isPageFragment,
|
|
33
|
+
setValueByKeyPath,
|
|
34
|
+
} from '@tmagic/utils';
|
|
27
35
|
|
|
28
36
|
import BaseService from '@editor/services//BaseService';
|
|
29
37
|
import propsService from '@editor/services//props';
|
|
@@ -34,11 +42,13 @@ import type {
|
|
|
34
42
|
AsyncHookPlugin,
|
|
35
43
|
AsyncMethodName,
|
|
36
44
|
DslOpOptions,
|
|
45
|
+
DslOpWithHistoryIdsResult,
|
|
37
46
|
EditorEvents,
|
|
38
47
|
EditorNodeInfo,
|
|
39
48
|
HistoryOpSource,
|
|
40
49
|
HistoryOpType,
|
|
41
50
|
PastePosition,
|
|
51
|
+
StepDiffItem,
|
|
42
52
|
StepValue,
|
|
43
53
|
StoreState,
|
|
44
54
|
StoreStateKey,
|
|
@@ -51,6 +61,7 @@ import {
|
|
|
51
61
|
classifyDragSources,
|
|
52
62
|
collectRelatedNodes,
|
|
53
63
|
COPY_STORAGE_KEY,
|
|
64
|
+
describeStepForRevert,
|
|
54
65
|
editorNodeMergeCustomizer,
|
|
55
66
|
fixNodePosition,
|
|
56
67
|
getInitPositionStyle,
|
|
@@ -63,41 +74,24 @@ import {
|
|
|
63
74
|
setLayout,
|
|
64
75
|
toggleFixedPosition,
|
|
65
76
|
} from '@editor/utils/editor';
|
|
77
|
+
import { getLastPushedHistoryIds } from '@editor/utils/history';
|
|
66
78
|
import { beforePaste, getAddParent } from '@editor/utils/operator';
|
|
67
79
|
|
|
68
80
|
type MoveItem = { node: MNode; parent: MContainer; pageForOp: { name: string; id: Id } | null };
|
|
69
81
|
|
|
70
82
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
83
|
+
* 把「变更前后节点快照」列表归一成 update 类型的 {@link StepDiffItem} 列表,供 {@link StepValue.diff} 使用。
|
|
84
|
+
* `changeRecords` 来自 form 端的 propPath/value 列表,撤销/重做时只对这些 propPath 做局部更新;
|
|
85
|
+
* 缺省(未传 / 空数组)才退化为整节点替换。
|
|
73
86
|
*/
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
case 'remove': {
|
|
83
|
-
const count = step.removedItems?.length ?? 0;
|
|
84
|
-
const node = step.removedItems?.[0]?.node;
|
|
85
|
-
const label = node?.name || node?.type || (node?.id !== undefined ? `${node.id}` : '');
|
|
86
|
-
return `还原已删除的 ${count} 个节点${count === 1 && label ? `(${label})` : ''}`;
|
|
87
|
-
}
|
|
88
|
-
case 'update':
|
|
89
|
-
default: {
|
|
90
|
-
const items = step.updatedItems ?? [];
|
|
91
|
-
if (items.length === 1) {
|
|
92
|
-
const { newNode, oldNode, changeRecords } = items[0];
|
|
93
|
-
const target = newNode?.name || newNode?.type || oldNode?.name || oldNode?.type || `${newNode?.id ?? ''}`;
|
|
94
|
-
const propPath = changeRecords?.[0]?.propPath;
|
|
95
|
-
return propPath ? `还原 ${target} · ${propPath}` : `还原 ${target}`;
|
|
96
|
-
}
|
|
97
|
-
return `还原 ${items.length} 个节点的修改`;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
};
|
|
87
|
+
const buildUpdateDiff = (
|
|
88
|
+
items: { oldNode: MNode; newNode: MNode; changeRecords?: ChangeRecord[] }[],
|
|
89
|
+
): StepDiffItem<MNode>[] =>
|
|
90
|
+
items.map(({ oldNode, newNode, changeRecords }) => ({
|
|
91
|
+
oldSchema: oldNode,
|
|
92
|
+
newSchema: newNode,
|
|
93
|
+
...(changeRecords?.length ? { changeRecords } : {}),
|
|
94
|
+
}));
|
|
101
95
|
|
|
102
96
|
class Editor extends BaseService {
|
|
103
97
|
public state: StoreState = reactive({
|
|
@@ -116,6 +110,12 @@ class Editor extends BaseService {
|
|
|
116
110
|
alwaysMultiSelect: false,
|
|
117
111
|
});
|
|
118
112
|
private selectionBeforeOp: Id[] | null = null;
|
|
113
|
+
/**
|
|
114
|
+
* 最近一次 pushOpHistory 写入的历史记录 uuid。
|
|
115
|
+
* 供 *AndGetHistoryId 系列方法在调用普通操作后取回本次产生的历史记录 id;
|
|
116
|
+
* 普通操作不会读取它,调用前由 *AndGetHistoryId 重置为 null。
|
|
117
|
+
*/
|
|
118
|
+
private lastPushedHistoryId: string | null = null;
|
|
119
119
|
|
|
120
120
|
constructor() {
|
|
121
121
|
super(
|
|
@@ -129,8 +129,13 @@ class Editor extends BaseService {
|
|
|
129
129
|
* 设置当前指点节点配置
|
|
130
130
|
* @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength
|
|
131
131
|
* @param value MNode
|
|
132
|
+
* @param options.historySource 设置 root 时,本次变更写入历史记录的「操作来源」(仅 name === 'root' 时生效)
|
|
132
133
|
*/
|
|
133
|
-
public set<K extends StoreStateKey, T extends StoreState[K]>(
|
|
134
|
+
public set<K extends StoreStateKey, T extends StoreState[K]>(
|
|
135
|
+
name: K,
|
|
136
|
+
value: T,
|
|
137
|
+
options: { historySource?: HistoryOpSource } = {},
|
|
138
|
+
) {
|
|
134
139
|
const preValue = this.state[name];
|
|
135
140
|
this.state[name] = value;
|
|
136
141
|
|
|
@@ -149,13 +154,32 @@ class Editor extends BaseService {
|
|
|
149
154
|
this.state.pageLength = getPageList(app).length || 0;
|
|
150
155
|
this.state.pageFragmentLength = getPageFragmentList(app).length || 0;
|
|
151
156
|
this.state.stageLoading = this.state.pageLength !== 0;
|
|
157
|
+
|
|
158
|
+
if (preValue && !isEmpty(preValue)) {
|
|
159
|
+
// 编辑期间再次整体设置 root(源码保存 / 外部重设 DSL / root 节点更新):与上一次 root
|
|
160
|
+
// 做页面级 diff,按 update / add / remove 入栈,作为正常历史记录体现整体替换。
|
|
161
|
+
this.pushRootDiffHistory(preValue as MApp, app, options.historySource);
|
|
162
|
+
} else {
|
|
163
|
+
// 首次设置 root:仅当该页面 / 页面片尚无基线标记时,才写入「未修改的初始状态」基线。
|
|
164
|
+
// 配合「先恢复历史再 set root」:若基线已随历史恢复建立(恢复后已有基线),则此处不再
|
|
165
|
+
// 重复创建,set root 不额外产生记录,由恢复出的历史栈作为当前状态来源。
|
|
166
|
+
// 标记不进入撤销/重做栈,仅作为该页历史列表底部的初始基线展示。
|
|
167
|
+
app.items?.forEach((pageNode) => {
|
|
168
|
+
if (pageNode?.id !== undefined && !historyService.getPageMarker(pageNode.id)) {
|
|
169
|
+
historyService.setPageMarker(pageNode.id, {
|
|
170
|
+
name: pageNode.name,
|
|
171
|
+
source: options.historySource,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
152
176
|
} else {
|
|
153
177
|
this.state.pageLength = 0;
|
|
154
178
|
this.state.pageFragmentLength = 0;
|
|
155
179
|
this.state.stageLoading = false;
|
|
156
180
|
}
|
|
157
181
|
|
|
158
|
-
this.emit('root-change', value as StoreState['root'], preValue as StoreState['root']);
|
|
182
|
+
this.emit('root-change', value as StoreState['root'], preValue as StoreState['root'], options);
|
|
159
183
|
}
|
|
160
184
|
}
|
|
161
185
|
|
|
@@ -180,7 +204,30 @@ class Editor extends BaseService {
|
|
|
180
204
|
root = toRaw(root);
|
|
181
205
|
}
|
|
182
206
|
|
|
183
|
-
|
|
207
|
+
if (!root) {
|
|
208
|
+
return { node: null, parent: null, page: null, path: [] };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (id === root.id) {
|
|
212
|
+
return { node: root, parent: null, page: null, path: [] };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 大多数查找的目标都在当前页面内,优先在当前页面子树中查找以避免对整棵树做全量遍历。
|
|
216
|
+
// 注意:不能直接使用 state.page,它可能与当前 root 不同步(指向已脱离的旧页面对象),
|
|
217
|
+
// 因此仅借用其 id,再从当前 root 中取回真正的页面对象(页面均为 root 的直接子节点,数量很少)。
|
|
218
|
+
const pageIdStr = `${this.get('page')?.id || ''}`;
|
|
219
|
+
const currentPageNode = root.items?.find((item) => `${item.id}` === pageIdStr);
|
|
220
|
+
if (currentPageNode && `${id}` !== pageIdStr) {
|
|
221
|
+
// util 仅读取 root.id 与 root.items,按容器结构传入当前页面是安全的
|
|
222
|
+
const info = getNodeInfo(id, currentPageNode);
|
|
223
|
+
if (info.node) {
|
|
224
|
+
return info;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 回退:在完整 root 上查找;当前页面已搜索过,用 skip 跳过其子树避免重复遍历,
|
|
229
|
+
// 同时保留真实的 parent / page 引用(id 命中当前页面节点本身时会在跳过子树前先匹配到)
|
|
230
|
+
return getNodeInfo(id, root, currentPageNode);
|
|
184
231
|
}
|
|
185
232
|
|
|
186
233
|
/**
|
|
@@ -473,17 +520,17 @@ class Editor extends BaseService {
|
|
|
473
520
|
if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
|
|
474
521
|
const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
|
|
475
522
|
if (!doNotPushHistory) {
|
|
523
|
+
const parentId = (this.getParentById(newNodes[0].id, false) ?? this.get('root'))!.id;
|
|
476
524
|
this.pushOpHistory('add', {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
},
|
|
525
|
+
diff: newNodes.map((n) => {
|
|
526
|
+
const p = this.getParentById(n.id, false) as MContainer;
|
|
527
|
+
const idx = p ? getNodeIndex(n.id, p) : -1;
|
|
528
|
+
return {
|
|
529
|
+
newSchema: cloneDeep(toRaw(n)),
|
|
530
|
+
parentId,
|
|
531
|
+
index: typeof idx === 'number' ? idx : -1,
|
|
532
|
+
};
|
|
533
|
+
}),
|
|
487
534
|
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
488
535
|
historyDescription,
|
|
489
536
|
source: historySource,
|
|
@@ -596,7 +643,7 @@ class Editor extends BaseService {
|
|
|
596
643
|
|
|
597
644
|
const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
|
|
598
645
|
|
|
599
|
-
const removedItems:
|
|
646
|
+
const removedItems: StepDiffItem<MNode>[] = [];
|
|
600
647
|
let pageForOp: { name: string; id: Id } | null = null;
|
|
601
648
|
if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) {
|
|
602
649
|
for (const n of nodes) {
|
|
@@ -607,7 +654,7 @@ class Editor extends BaseService {
|
|
|
607
654
|
}
|
|
608
655
|
const idx = getNodeIndex(curNode.id, parent);
|
|
609
656
|
removedItems.push({
|
|
610
|
-
|
|
657
|
+
oldSchema: cloneDeep(toRaw(curNode)),
|
|
611
658
|
parentId: parent.id,
|
|
612
659
|
index: typeof idx === 'number' ? idx : -1,
|
|
613
660
|
});
|
|
@@ -620,7 +667,7 @@ class Editor extends BaseService {
|
|
|
620
667
|
if (removedItems.length > 0 && pageForOp) {
|
|
621
668
|
if (!doNotPushHistory) {
|
|
622
669
|
this.pushOpHistory('remove', {
|
|
623
|
-
|
|
670
|
+
diff: removedItems,
|
|
624
671
|
pageData: pageForOp,
|
|
625
672
|
historyDescription,
|
|
626
673
|
source: historySource,
|
|
@@ -635,7 +682,7 @@ class Editor extends BaseService {
|
|
|
635
682
|
|
|
636
683
|
public async doUpdate(
|
|
637
684
|
config: MNode,
|
|
638
|
-
{ changeRecords = [] }: { changeRecords?: ChangeRecord[] } = {},
|
|
685
|
+
{ changeRecords = [], historySource }: { changeRecords?: ChangeRecord[]; historySource?: HistoryOpSource } = {},
|
|
639
686
|
): Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }> {
|
|
640
687
|
const root = this.get('root');
|
|
641
688
|
if (!root) throw new Error('root为空');
|
|
@@ -648,14 +695,14 @@ class Editor extends BaseService {
|
|
|
648
695
|
|
|
649
696
|
const node = toRaw(info.node);
|
|
650
697
|
|
|
651
|
-
let newConfig = await toggleFixedPosition(toRaw(config), node,
|
|
698
|
+
let newConfig = await toggleFixedPosition(toRaw(config), node, info.path, this.getLayout);
|
|
652
699
|
|
|
653
700
|
newConfig = mergeWith(cloneDeep(node), newConfig, editorNodeMergeCustomizer);
|
|
654
701
|
|
|
655
702
|
if (!newConfig.type) throw new Error('配置缺少type值');
|
|
656
703
|
|
|
657
704
|
if (newConfig.type === NodeType.ROOT) {
|
|
658
|
-
this.set('root', newConfig as MApp);
|
|
705
|
+
this.set('root', newConfig as MApp, { historySource });
|
|
659
706
|
return {
|
|
660
707
|
oldNode: node,
|
|
661
708
|
newNode: newConfig,
|
|
@@ -736,7 +783,7 @@ class Editor extends BaseService {
|
|
|
736
783
|
const updateData = await Promise.all(
|
|
737
784
|
nodes.map((node, index) => {
|
|
738
785
|
const recordsForNode = changeRecordList ? (changeRecordList[index] ?? []) : (changeRecords ?? []);
|
|
739
|
-
return this.doUpdate(node, { changeRecords: recordsForNode });
|
|
786
|
+
return this.doUpdate(node, { changeRecords: recordsForNode, historySource });
|
|
740
787
|
}),
|
|
741
788
|
);
|
|
742
789
|
|
|
@@ -746,15 +793,15 @@ class Editor extends BaseService {
|
|
|
746
793
|
if (!doNotPushHistory) {
|
|
747
794
|
const pageForOp = this.getNodeInfo(nodes[0].id, false).page;
|
|
748
795
|
this.pushOpHistory('update', {
|
|
749
|
-
|
|
750
|
-
|
|
796
|
+
// 每个节点单独保留自己的 changeRecords,便于撤销/重做时按 propPath 精细化更新;
|
|
797
|
+
// 没有 changeRecords 的(如内部 sort/moveLayer 等整节点替换操作)会退化为全节点替换。
|
|
798
|
+
diff: buildUpdateDiff(
|
|
799
|
+
updateData.map((d) => ({
|
|
751
800
|
oldNode: cloneDeep(d.oldNode),
|
|
752
|
-
newNode: cloneDeep(
|
|
753
|
-
// 每个节点单独保留自己的 changeRecords,便于撤销/重做时按 propPath 精细化更新;
|
|
754
|
-
// 没有 changeRecords 的(如内部 sort/moveLayer 等整节点替换操作)会退化为全节点替换。
|
|
801
|
+
newNode: cloneDeep(d.newNode),
|
|
755
802
|
changeRecords: d.changeRecords?.length ? cloneDeep(d.changeRecords) : undefined,
|
|
756
803
|
})),
|
|
757
|
-
|
|
804
|
+
),
|
|
758
805
|
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
759
806
|
historyDescription,
|
|
760
807
|
source: historySource,
|
|
@@ -996,9 +1043,7 @@ class Editor extends BaseService {
|
|
|
996
1043
|
'update',
|
|
997
1044
|
|
|
998
1045
|
{
|
|
999
|
-
|
|
1000
|
-
updatedItems: [{ oldNode: oldParent, newNode: cloneDeep(toRaw(parent)) }],
|
|
1001
|
-
},
|
|
1046
|
+
diff: buildUpdateDiff([{ oldNode: oldParent, newNode: cloneDeep(toRaw(parent)) }]),
|
|
1002
1047
|
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
1003
1048
|
historyDescription,
|
|
1004
1049
|
source: historySource,
|
|
@@ -1098,7 +1143,7 @@ class Editor extends BaseService {
|
|
|
1098
1143
|
}));
|
|
1099
1144
|
const historyPage = moves[0].pageForOp ?? { name: '', id: target.id };
|
|
1100
1145
|
this.pushOpHistory('update', {
|
|
1101
|
-
|
|
1146
|
+
diff: buildUpdateDiff(updatedItems),
|
|
1102
1147
|
pageData: historyPage,
|
|
1103
1148
|
historyDescription,
|
|
1104
1149
|
source: historySource,
|
|
@@ -1178,7 +1223,7 @@ class Editor extends BaseService {
|
|
|
1178
1223
|
if (!doNotPushHistory) {
|
|
1179
1224
|
const pageForOp = this.getNodeInfo(configs[0].id, false).page;
|
|
1180
1225
|
this.pushOpHistory('update', {
|
|
1181
|
-
|
|
1226
|
+
diff: buildUpdateDiff(updatedItems),
|
|
1182
1227
|
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
1183
1228
|
historyDescription,
|
|
1184
1229
|
source: historySource,
|
|
@@ -1190,6 +1235,86 @@ class Editor extends BaseService {
|
|
|
1190
1235
|
this.emit('drag-to', { targetIndex, configs, targetParent });
|
|
1191
1236
|
}
|
|
1192
1237
|
|
|
1238
|
+
// #region AndGetHistoryId
|
|
1239
|
+
/**
|
|
1240
|
+
* 下列 *AndGetHistoryId 方法与对应的普通操作(add / remove / update ...)行为完全一致,
|
|
1241
|
+
* 返回值在 {@link DslOpWithHistoryIdsResult} 中同时包含原操作结果与本次写入历史栈的 uuid 列表({@link StepValue.uuid}),
|
|
1242
|
+
* 可用于精确引用 / 定位该条历史记录(埋点、revert、跨端同步等)。
|
|
1243
|
+
*
|
|
1244
|
+
* 当本次操作未写入历史(doNotPushHistory 为 true、或操作无实际变更 / 提前返回)时 historyIds 为 `[]`。
|
|
1245
|
+
*/
|
|
1246
|
+
|
|
1247
|
+
/** 等价于 {@link add},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1248
|
+
public async addAndGetHistoryId(
|
|
1249
|
+
addNode: AddMNode | MNode[],
|
|
1250
|
+
parent?: MContainer | null,
|
|
1251
|
+
options: DslOpOptions = {},
|
|
1252
|
+
): Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> {
|
|
1253
|
+
this.lastPushedHistoryId = null;
|
|
1254
|
+
const result = await this.add(addNode, parent, options);
|
|
1255
|
+
return { result, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/** 等价于 {@link remove},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1259
|
+
public async removeAndGetHistoryId(
|
|
1260
|
+
nodeOrNodeList: MNode | MNode[],
|
|
1261
|
+
options: DslOpOptions = {},
|
|
1262
|
+
): Promise<DslOpWithHistoryIdsResult<void>> {
|
|
1263
|
+
this.lastPushedHistoryId = null;
|
|
1264
|
+
await this.remove(nodeOrNodeList, options);
|
|
1265
|
+
return { result: undefined, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/** 等价于 {@link update},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1269
|
+
public async updateAndGetHistoryId(
|
|
1270
|
+
config: MNode | MNode[],
|
|
1271
|
+
data: {
|
|
1272
|
+
changeRecords?: ChangeRecord[];
|
|
1273
|
+
changeRecordList?: ChangeRecord[][];
|
|
1274
|
+
doNotPushHistory?: boolean;
|
|
1275
|
+
historyDescription?: string;
|
|
1276
|
+
historySource?: HistoryOpSource;
|
|
1277
|
+
} = {},
|
|
1278
|
+
): Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> {
|
|
1279
|
+
this.lastPushedHistoryId = null;
|
|
1280
|
+
const result = await this.update(config, data);
|
|
1281
|
+
return { result, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/** 等价于 {@link moveLayer},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1285
|
+
public async moveLayerAndGetHistoryId(
|
|
1286
|
+
offset: number | LayerOffset,
|
|
1287
|
+
options: DslOpOptions = {},
|
|
1288
|
+
): Promise<DslOpWithHistoryIdsResult<void>> {
|
|
1289
|
+
this.lastPushedHistoryId = null;
|
|
1290
|
+
await this.moveLayer(offset, options);
|
|
1291
|
+
return { result: undefined, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/** 等价于 {@link moveToContainer},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1295
|
+
public async moveToContainerAndGetHistoryId(
|
|
1296
|
+
config: MNode | MNode[],
|
|
1297
|
+
targetId: Id,
|
|
1298
|
+
options: DslOpOptions = {},
|
|
1299
|
+
): Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> {
|
|
1300
|
+
this.lastPushedHistoryId = null;
|
|
1301
|
+
const result = await this.moveToContainer(config, targetId, options);
|
|
1302
|
+
return { result, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/** 等价于 {@link dragTo},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1306
|
+
public async dragToAndGetHistoryId(
|
|
1307
|
+
config: MNode | MNode[],
|
|
1308
|
+
targetParent: MContainer,
|
|
1309
|
+
targetIndex: number,
|
|
1310
|
+
options: DslOpOptions = {},
|
|
1311
|
+
): Promise<DslOpWithHistoryIdsResult<void>> {
|
|
1312
|
+
this.lastPushedHistoryId = null;
|
|
1313
|
+
await this.dragTo(config, targetParent, targetIndex, options);
|
|
1314
|
+
return { result: undefined, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1315
|
+
}
|
|
1316
|
+
// #endregion AndGetHistoryId
|
|
1317
|
+
|
|
1193
1318
|
/**
|
|
1194
1319
|
* 撤销当前操作
|
|
1195
1320
|
* @returns 被撤销的操作
|
|
@@ -1236,12 +1361,14 @@ class Editor extends BaseService {
|
|
|
1236
1361
|
if (!entry?.applied) return null;
|
|
1237
1362
|
|
|
1238
1363
|
const { step } = entry;
|
|
1364
|
+
// 初始基线(index 0 的 initial step)是栈底线,不可回滚。
|
|
1365
|
+
if (step.opType === 'initial') return null;
|
|
1239
1366
|
const root = this.get('root');
|
|
1240
1367
|
if (!root) return null;
|
|
1241
1368
|
|
|
1242
1369
|
// 更新类步骤必须带 changeRecords 才支持回滚:缺失时只能整节点替换,会冲掉后续无关变更,故不支持。
|
|
1243
1370
|
if (step.opType === 'update') {
|
|
1244
|
-
const items = step.
|
|
1371
|
+
const items = step.diff ?? [];
|
|
1245
1372
|
if (!items.length || !items.every((item) => item.changeRecords?.length)) return null;
|
|
1246
1373
|
}
|
|
1247
1374
|
|
|
@@ -1260,9 +1387,9 @@ class Editor extends BaseService {
|
|
|
1260
1387
|
switch (step.opType) {
|
|
1261
1388
|
case 'add': {
|
|
1262
1389
|
// 原本是新增 → revert 即删除当时被加入的节点
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
const existing = this.getNodeById(
|
|
1390
|
+
for (const { newSchema } of step.diff ?? []) {
|
|
1391
|
+
if (!newSchema) continue;
|
|
1392
|
+
const existing = this.getNodeById(newSchema.id, false);
|
|
1266
1393
|
if (existing) {
|
|
1267
1394
|
await this.remove(existing, opts);
|
|
1268
1395
|
}
|
|
@@ -1272,35 +1399,40 @@ class Editor extends BaseService {
|
|
|
1272
1399
|
case 'remove': {
|
|
1273
1400
|
// 原本是删除 → revert 即把节点按原父容器加回来。
|
|
1274
1401
|
// 按原 index 升序逐个插回,先小后大避免索引漂移。
|
|
1275
|
-
const items = step.
|
|
1276
|
-
const sorted = [...items].sort((a, b) => a.index - b.index);
|
|
1277
|
-
for (const {
|
|
1402
|
+
const items = step.diff ?? [];
|
|
1403
|
+
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1404
|
+
for (const { oldSchema, parentId } of sorted) {
|
|
1405
|
+
if (!oldSchema || parentId === undefined) continue;
|
|
1278
1406
|
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1279
1407
|
if (parent) {
|
|
1280
|
-
await this.add([cloneDeep(
|
|
1408
|
+
await this.add([cloneDeep(oldSchema)] as MNode[], parent, opts);
|
|
1281
1409
|
}
|
|
1282
1410
|
}
|
|
1283
1411
|
break;
|
|
1284
1412
|
}
|
|
1285
1413
|
case 'update': {
|
|
1286
|
-
// 原本是更新 → revert 即把
|
|
1414
|
+
// 原本是更新 → revert 即把 oldSchema 的值写回;
|
|
1287
1415
|
// 优先按 changeRecords 局部 patch(仅触达 propPath 下的字段,避免冲掉同节点上其它无关变更)。
|
|
1288
|
-
const items = step.
|
|
1289
|
-
const configs = items
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1416
|
+
const items = step.diff ?? [];
|
|
1417
|
+
const configs = items
|
|
1418
|
+
.filter((item) => item.oldSchema && item.newSchema)
|
|
1419
|
+
.map(({ oldSchema, newSchema, changeRecords }) => {
|
|
1420
|
+
const oldNode = oldSchema!;
|
|
1421
|
+
const newNode = newSchema!;
|
|
1422
|
+
if (changeRecords?.length) {
|
|
1423
|
+
const patch: MNode = { id: newNode.id, type: newNode.type };
|
|
1424
|
+
for (const record of changeRecords) {
|
|
1425
|
+
if (!record.propPath) {
|
|
1426
|
+
// 没有 propPath 视为整节点替换
|
|
1427
|
+
return cloneDeep(oldNode);
|
|
1428
|
+
}
|
|
1429
|
+
const value = cloneDeep(getValueByKeyPath(record.propPath, oldNode));
|
|
1430
|
+
setValueByKeyPath(record.propPath, value, patch);
|
|
1296
1431
|
}
|
|
1297
|
-
|
|
1298
|
-
setValueByKeyPath(record.propPath, value, patch);
|
|
1432
|
+
return patch;
|
|
1299
1433
|
}
|
|
1300
|
-
return
|
|
1301
|
-
}
|
|
1302
|
-
return cloneDeep(oldNode);
|
|
1303
|
-
});
|
|
1434
|
+
return cloneDeep(oldNode);
|
|
1435
|
+
});
|
|
1304
1436
|
if (configs.length) {
|
|
1305
1437
|
await this.update(configs, { historyDescription, historySource: 'rollback' });
|
|
1306
1438
|
}
|
|
@@ -1320,6 +1452,22 @@ class Editor extends BaseService {
|
|
|
1320
1452
|
return revertedStep;
|
|
1321
1453
|
}
|
|
1322
1454
|
|
|
1455
|
+
/**
|
|
1456
|
+
* 通过历史记录 uuid 回滚当前页面的历史步骤,语义与 {@link revertPageStep} 完全一致,
|
|
1457
|
+
* 仅入参从 index 改为 uuid 列表({@link StepValue.uuid})。按数组顺序依次回滚,
|
|
1458
|
+
* 返回与入参同序的结果列表(某项失败时为 `null`)。
|
|
1459
|
+
*
|
|
1460
|
+
* @param uuids 目标历史记录的 uuid 列表,通常由 *AndGetHistoryId 方法返回的 `historyIds`
|
|
1461
|
+
*/
|
|
1462
|
+
public async revertPageStepById(uuids: string[]): Promise<(StepValue | null)[]> {
|
|
1463
|
+
const results: (StepValue | null)[] = [];
|
|
1464
|
+
for (const uuid of uuids) {
|
|
1465
|
+
const index = historyService.getPageStepIndexByUuid(uuid);
|
|
1466
|
+
results.push(index < 0 ? null : await this.revertPageStep(index));
|
|
1467
|
+
}
|
|
1468
|
+
return results;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1323
1471
|
/**
|
|
1324
1472
|
* 跳转当前页面历史栈到指定游标位置。
|
|
1325
1473
|
*
|
|
@@ -1416,34 +1564,119 @@ class Editor extends BaseService {
|
|
|
1416
1564
|
this.selectionBeforeOp = this.get('nodes').map((n) => n.id);
|
|
1417
1565
|
}
|
|
1418
1566
|
|
|
1567
|
+
/**
|
|
1568
|
+
* 比较「上一次 root」与「新 root」的页面 / 页面片,按页面粒度把整体替换拆成历史记录:
|
|
1569
|
+
* - 新旧都存在且内容变化的页面 → 一条 `update`(整页快照替换,无 changeRecords);
|
|
1570
|
+
* - 仅新 root 存在的页面 → 一条 `add`;
|
|
1571
|
+
* - 仅旧 root 存在的页面 → 一条 `remove`。
|
|
1572
|
+
*
|
|
1573
|
+
* 每条记录落到对应页面自己的历史栈(与普通节点操作一致),并标记来源 `source`。
|
|
1574
|
+
* 内容未变化的页面不产生记录,避免重复设置相同 DSL 时产生噪声。
|
|
1575
|
+
*/
|
|
1576
|
+
private pushRootDiffHistory(preRoot: MApp, nextRoot: MApp, source?: HistoryOpSource): void {
|
|
1577
|
+
const prevPages = preRoot?.items || [];
|
|
1578
|
+
const nextPages = nextRoot?.items || [];
|
|
1579
|
+
const prevMap = new Map(prevPages.map((p) => [`${p.id}`, p]));
|
|
1580
|
+
const nextMap = new Map(nextPages.map((p) => [`${p.id}`, p]));
|
|
1581
|
+
const indexInItems = (root: MApp, id: Id) => (root.items ?? []).findIndex((item) => `${item.id}` === `${id}`);
|
|
1582
|
+
|
|
1583
|
+
nextPages.forEach((nextPage) => {
|
|
1584
|
+
const prevPage = prevMap.get(`${nextPage.id}`);
|
|
1585
|
+
if (!prevPage) {
|
|
1586
|
+
this.pushPageDiffStep(
|
|
1587
|
+
'add',
|
|
1588
|
+
nextPage,
|
|
1589
|
+
{ newSchema: cloneDeep(toRaw(nextPage)), parentId: nextRoot.id, index: indexInItems(nextRoot, nextPage.id) },
|
|
1590
|
+
source,
|
|
1591
|
+
);
|
|
1592
|
+
} else if (!isEqual(toRaw(prevPage), toRaw(nextPage))) {
|
|
1593
|
+
this.pushPageDiffStep(
|
|
1594
|
+
'update',
|
|
1595
|
+
nextPage,
|
|
1596
|
+
{ oldSchema: cloneDeep(toRaw(prevPage)), newSchema: cloneDeep(toRaw(nextPage)) },
|
|
1597
|
+
source,
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
prevPages.forEach((prevPage) => {
|
|
1603
|
+
if (!nextMap.has(`${prevPage.id}`)) {
|
|
1604
|
+
this.pushPageDiffStep(
|
|
1605
|
+
'remove',
|
|
1606
|
+
prevPage,
|
|
1607
|
+
{ oldSchema: cloneDeep(toRaw(prevPage)), parentId: preRoot.id, index: indexInItems(preRoot, prevPage.id) },
|
|
1608
|
+
source,
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
/**
|
|
1615
|
+
* 构造一条页面级「set root」历史记录(不携带选区 / modifiedNodeIds 上下文)并落到该页面自己的栈。
|
|
1616
|
+
*
|
|
1617
|
+
* 连续 set root 替换:若该页栈最新一条已是**同来源**的 set root 记录({@link StepValue.rootStep} 且 `source` 相同),
|
|
1618
|
+
* 则用本次记录**替换**它而非新增,避免源码反复保存 / 外部重设 DSL 时堆积多条 root 记录;
|
|
1619
|
+
* 来源不同则照常新增(initial 基线不是 rootStep,不在此列)。
|
|
1620
|
+
*/
|
|
1621
|
+
private pushPageDiffStep(
|
|
1622
|
+
opType: HistoryOpType,
|
|
1623
|
+
page: MPage | MPageFragment,
|
|
1624
|
+
diffItem: StepDiffItem<MNode>,
|
|
1625
|
+
source?: HistoryOpSource,
|
|
1626
|
+
): void {
|
|
1627
|
+
const step: StepValue = {
|
|
1628
|
+
uuid: guid(),
|
|
1629
|
+
data: { name: page.name || '', id: page.id },
|
|
1630
|
+
opType,
|
|
1631
|
+
selectedBefore: [],
|
|
1632
|
+
selectedAfter: [],
|
|
1633
|
+
modifiedNodeIds: new Map(),
|
|
1634
|
+
diff: [diffItem],
|
|
1635
|
+
rootStep: true,
|
|
1636
|
+
};
|
|
1637
|
+
if (source) step.source = source;
|
|
1638
|
+
|
|
1639
|
+
const top = historyService.getCurrentPageStep(page.id);
|
|
1640
|
+
if (top?.rootStep && top.source === source) {
|
|
1641
|
+
historyService.replaceCurrentPageStep(step, page.id);
|
|
1642
|
+
} else {
|
|
1643
|
+
historyService.push(step, page.id);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1419
1647
|
private pushOpHistory(
|
|
1420
1648
|
opType: HistoryOpType,
|
|
1421
1649
|
{
|
|
1422
|
-
|
|
1650
|
+
diff,
|
|
1423
1651
|
pageData,
|
|
1424
1652
|
historyDescription,
|
|
1425
1653
|
source,
|
|
1426
1654
|
}: {
|
|
1427
|
-
|
|
1655
|
+
diff: StepDiffItem<MNode>[];
|
|
1428
1656
|
pageData: { name: string; id: Id };
|
|
1429
1657
|
historyDescription?: string;
|
|
1430
1658
|
source?: HistoryOpSource;
|
|
1431
1659
|
},
|
|
1432
|
-
) {
|
|
1660
|
+
): string | null {
|
|
1433
1661
|
const step: StepValue = {
|
|
1662
|
+
uuid: guid(),
|
|
1434
1663
|
data: pageData,
|
|
1435
1664
|
opType,
|
|
1436
1665
|
selectedBefore: this.selectionBeforeOp ?? [],
|
|
1437
1666
|
selectedAfter: this.get('nodes').map((n) => n.id),
|
|
1438
1667
|
modifiedNodeIds: new Map(this.get('modifiedNodeIds')),
|
|
1439
|
-
|
|
1668
|
+
diff,
|
|
1440
1669
|
};
|
|
1441
1670
|
if (historyDescription) step.historyDescription = historyDescription;
|
|
1442
1671
|
if (source) step.source = source;
|
|
1443
1672
|
// 显式按 step.data.id 入栈:跨页操作(如 moveToContainer 从源页搬到目标页)
|
|
1444
1673
|
// 必须落到正确的页面栈,否则会把记录错误地推到当前活动页 / 操作发起页。
|
|
1445
|
-
historyService.push(step, pageData.id);
|
|
1674
|
+
const pushed = historyService.push(step, pageData.id);
|
|
1675
|
+
// push 返回 null 表示当前没有可写入的页面栈(未真正入栈),此时不应返回 uuid。
|
|
1676
|
+
const historyId = pushed ? step.uuid : null;
|
|
1677
|
+
this.lastPushedHistoryId = historyId;
|
|
1446
1678
|
this.selectionBeforeOp = null;
|
|
1679
|
+
return historyId;
|
|
1447
1680
|
}
|
|
1448
1681
|
|
|
1449
1682
|
/**
|
|
@@ -1459,6 +1692,8 @@ class Editor extends BaseService {
|
|
|
1459
1692
|
* @param reverse true = 撤销,false = 重做
|
|
1460
1693
|
*/
|
|
1461
1694
|
private async applyHistoryOp(step: StepValue, reverse: boolean) {
|
|
1695
|
+
// 初始基线 step 仅作展示,不承载任何变更,撤销/重做时无需应用(正常流程下也不会被触达)。
|
|
1696
|
+
if (step.opType === 'initial') return;
|
|
1462
1697
|
const root = this.get('root');
|
|
1463
1698
|
const stage = this.get('stage');
|
|
1464
1699
|
if (!root) return;
|
|
@@ -1467,52 +1702,52 @@ class Editor extends BaseService {
|
|
|
1467
1702
|
|
|
1468
1703
|
switch (step.opType) {
|
|
1469
1704
|
case 'add': {
|
|
1470
|
-
const
|
|
1705
|
+
const items = step.diff ?? [];
|
|
1471
1706
|
if (reverse) {
|
|
1472
1707
|
// 撤销 add:把当时加入的节点删除
|
|
1473
|
-
for (const
|
|
1474
|
-
|
|
1708
|
+
for (const { newSchema } of items) {
|
|
1709
|
+
if (!newSchema) continue;
|
|
1710
|
+
const existing = this.getNodeById(newSchema.id, false);
|
|
1475
1711
|
if (existing) {
|
|
1476
1712
|
await this.remove(existing, commonOpts);
|
|
1477
1713
|
}
|
|
1478
1714
|
}
|
|
1479
1715
|
} else {
|
|
1480
|
-
// 重做 add:按记录的
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
if (parent.items) {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
parent.items.push(cloneDeep(n));
|
|
1492
|
-
}
|
|
1493
|
-
await stage?.add({
|
|
1494
|
-
config: cloneDeep(n),
|
|
1495
|
-
parent: cloneDeep(parent),
|
|
1496
|
-
parentId: parent.id,
|
|
1497
|
-
root: cloneDeep(root),
|
|
1498
|
-
});
|
|
1716
|
+
// 重做 add:按记录的 parentId / index 把节点重新插回父容器。
|
|
1717
|
+
// 按目标 index 升序逐个插入,先小后大避免索引漂移
|
|
1718
|
+
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1719
|
+
for (const { newSchema, parentId, index } of sorted) {
|
|
1720
|
+
if (!newSchema || parentId === undefined) continue;
|
|
1721
|
+
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1722
|
+
if (parent?.items) {
|
|
1723
|
+
if (typeof index === 'number' && index >= 0 && index < parent.items.length) {
|
|
1724
|
+
parent.items.splice(index, 0, cloneDeep(newSchema));
|
|
1725
|
+
} else {
|
|
1726
|
+
parent.items.push(cloneDeep(newSchema));
|
|
1499
1727
|
}
|
|
1728
|
+
await stage?.add({
|
|
1729
|
+
config: cloneDeep(newSchema),
|
|
1730
|
+
parent: cloneDeep(parent),
|
|
1731
|
+
parentId: parent.id,
|
|
1732
|
+
root: cloneDeep(root),
|
|
1733
|
+
});
|
|
1500
1734
|
}
|
|
1501
1735
|
}
|
|
1502
1736
|
}
|
|
1503
1737
|
break;
|
|
1504
1738
|
}
|
|
1505
1739
|
case 'remove': {
|
|
1506
|
-
const items = step.
|
|
1740
|
+
const items = step.diff ?? [];
|
|
1507
1741
|
if (reverse) {
|
|
1508
1742
|
// 撤销 remove:按原 index 升序逐个插回(先小后大避免索引漂移)
|
|
1509
|
-
const sorted = [...items].sort((a, b) => a.index - b.index);
|
|
1510
|
-
for (const {
|
|
1743
|
+
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1744
|
+
for (const { oldSchema, parentId, index } of sorted) {
|
|
1745
|
+
if (!oldSchema || parentId === undefined) continue;
|
|
1511
1746
|
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1512
1747
|
if (parent?.items) {
|
|
1513
|
-
parent.items.splice(index, 0, cloneDeep(
|
|
1748
|
+
parent.items.splice(index ?? parent.items.length, 0, cloneDeep(oldSchema));
|
|
1514
1749
|
await stage?.add({
|
|
1515
|
-
config: cloneDeep(
|
|
1750
|
+
config: cloneDeep(oldSchema),
|
|
1516
1751
|
parent: cloneDeep(parent),
|
|
1517
1752
|
parentId,
|
|
1518
1753
|
root: cloneDeep(root),
|
|
@@ -1521,8 +1756,9 @@ class Editor extends BaseService {
|
|
|
1521
1756
|
}
|
|
1522
1757
|
} else {
|
|
1523
1758
|
// 重做 remove:再删一次
|
|
1524
|
-
for (const {
|
|
1525
|
-
|
|
1759
|
+
for (const { oldSchema } of items) {
|
|
1760
|
+
if (!oldSchema) continue;
|
|
1761
|
+
const existing = this.getNodeById(oldSchema.id, false);
|
|
1526
1762
|
if (existing) {
|
|
1527
1763
|
await this.remove(existing, commonOpts);
|
|
1528
1764
|
}
|
|
@@ -1531,27 +1767,31 @@ class Editor extends BaseService {
|
|
|
1531
1767
|
break;
|
|
1532
1768
|
}
|
|
1533
1769
|
case 'update': {
|
|
1534
|
-
const items = step.
|
|
1770
|
+
const items = step.diff ?? [];
|
|
1535
1771
|
// 优先按 changeRecords 局部 patch:仅触达 propPath 下的字段,避免整节点替换冲掉同节点上其它无关变更。
|
|
1536
1772
|
// 没有 changeRecords 的(如内部 sort/moveLayer/拖动等整节点快照场景)才退化为整节点替换。
|
|
1537
|
-
const configs = items
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1773
|
+
const configs = items
|
|
1774
|
+
.filter((item) => item.oldSchema && item.newSchema)
|
|
1775
|
+
.map(({ oldSchema, newSchema, changeRecords }) => {
|
|
1776
|
+
const oldNode = oldSchema!;
|
|
1777
|
+
const newNode = newSchema!;
|
|
1778
|
+
if (changeRecords?.length) {
|
|
1779
|
+
const sourceForValues = reverse ? oldNode : newNode;
|
|
1780
|
+
// 仅保留 id / type 作为最小骨架,再按 propPath 写入需要回滚/重做的字段;
|
|
1781
|
+
// 后续 update -> mergeWith 会与现有节点深合并,patch 中未涉及的字段不会被改动。
|
|
1782
|
+
const patch: MNode = { id: newNode.id, type: newNode.type };
|
|
1783
|
+
for (const record of changeRecords) {
|
|
1784
|
+
if (!record.propPath) {
|
|
1785
|
+
// 没有 propPath 视为整节点替换
|
|
1786
|
+
return cloneDeep(sourceForValues);
|
|
1787
|
+
}
|
|
1788
|
+
const value = cloneDeep(getValueByKeyPath(record.propPath, sourceForValues));
|
|
1789
|
+
setValueByKeyPath(record.propPath, value, patch);
|
|
1547
1790
|
}
|
|
1548
|
-
|
|
1549
|
-
setValueByKeyPath(record.propPath, value, patch);
|
|
1791
|
+
return patch;
|
|
1550
1792
|
}
|
|
1551
|
-
return
|
|
1552
|
-
}
|
|
1553
|
-
return cloneDeep(reverse ? oldNode : newNode);
|
|
1554
|
-
});
|
|
1793
|
+
return cloneDeep(reverse ? oldNode : newNode);
|
|
1794
|
+
});
|
|
1555
1795
|
if (configs.length) {
|
|
1556
1796
|
await this.update(configs, { doNotPushHistory: true });
|
|
1557
1797
|
}
|