@tmagic/editor 1.8.0-beta.1 → 1.8.0-beta.2
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/Editor.vue_vue_type_script_setup_true_lang.js +11 -2
- package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +18 -110
- package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +6 -0
- package/dist/es/components/CompareForm.js +5 -0
- package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +167 -0
- package/dist/es/fields/Code.vue_vue_type_script_setup_true_lang.js +19 -3
- package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +18 -2
- package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +37 -6
- package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +14 -6
- package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +3 -1
- package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +15 -7
- package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +14 -6
- package/dist/es/fields/DataSourceSelect.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +58 -11
- package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +11 -6
- package/dist/es/fields/UISelect.vue_vue_type_script_setup_true_lang.js +8 -5
- package/dist/es/hooks/use-code-block-edit.js +2 -2
- package/dist/es/hooks/use-stage.js +15 -7
- package/dist/es/index.js +3 -1
- package/dist/es/initService.js +6 -6
- package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +4 -0
- package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +12 -7
- package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +8 -0
- package/dist/es/layouts/history-list/Bucket.js +5 -0
- package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +84 -0
- package/dist/es/layouts/history-list/CodeBlockTab.js +5 -0
- package/dist/es/layouts/history-list/CodeBlockTab.vue_vue_type_script_setup_true_lang.js +62 -0
- package/dist/es/layouts/history-list/DataSourceTab.js +5 -0
- package/dist/es/layouts/history-list/DataSourceTab.vue_vue_type_script_setup_true_lang.js +62 -0
- package/dist/es/layouts/history-list/GroupRow.js +5 -0
- package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +180 -0
- package/dist/es/layouts/history-list/HistoryDiffDialog.js +5 -0
- package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +195 -0
- package/dist/es/layouts/history-list/HistoryListPanel.js +5 -0
- package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +270 -0
- package/dist/es/layouts/history-list/InitialRow.js +5 -0
- package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +48 -0
- package/dist/es/layouts/history-list/PageTab.js +5 -0
- package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +99 -0
- package/dist/es/layouts/history-list/composables.js +154 -0
- package/dist/es/layouts/sidebar/layer/use-node-status.js +1 -1
- package/dist/es/services/BaseService.js +31 -68
- package/dist/es/services/codeBlock.js +205 -8
- package/dist/es/services/componentList.js +2 -2
- package/dist/es/services/dataSource.js +217 -8
- package/dist/es/services/dep.js +21 -9
- package/dist/es/services/editor.js +350 -160
- package/dist/es/services/events.js +2 -2
- package/dist/es/services/history.js +407 -12
- package/dist/es/services/keybinding.js +2 -2
- package/dist/es/services/props.js +8 -8
- package/dist/es/services/stageOverlay.js +2 -2
- package/dist/es/services/storage.js +2 -2
- package/dist/es/services/ui.js +2 -2
- package/dist/es/style.css +347 -1
- package/dist/es/utils/code-block.js +122 -0
- package/dist/es/utils/content-menu.js +4 -5
- package/dist/es/utils/dep/worker.js +1 -1
- package/dist/es/utils/editor.js +2 -2
- package/dist/es/utils/undo-redo.js +20 -0
- package/dist/style.css +347 -1
- package/dist/tmagic-editor.umd.cjs +3165 -774
- package/package.json +7 -7
- package/src/Editor.vue +7 -0
- package/src/components/CodeBlockEditor.vue +20 -109
- package/src/components/CodeParams.vue +6 -0
- package/src/components/CompareForm.vue +225 -0
- package/src/editorProps.ts +2 -0
- package/src/fields/Code.vue +19 -3
- package/src/fields/CodeSelect.vue +17 -1
- package/src/fields/CodeSelectCol.vue +30 -2
- package/src/fields/DataSourceFieldSelect/FieldSelect.vue +7 -1
- package/src/fields/DataSourceFieldSelect/Index.vue +2 -2
- package/src/fields/DataSourceFields.vue +12 -3
- package/src/fields/DataSourceMethodSelect.vue +4 -1
- package/src/fields/DataSourceMethods.vue +14 -4
- package/src/fields/DataSourceMocks.vue +13 -3
- package/src/fields/DataSourceSelect.vue +4 -1
- package/src/fields/EventSelect.vue +57 -8
- package/src/fields/KeyValue.vue +17 -4
- package/src/fields/UISelect.vue +5 -2
- package/src/hooks/use-code-block-edit.ts +5 -2
- package/src/hooks/use-stage.ts +23 -7
- package/src/index.ts +1 -0
- package/src/layouts/CodeEditor.vue +15 -0
- package/src/layouts/Framework.vue +10 -5
- package/src/layouts/NavMenu.vue +9 -0
- package/src/layouts/history-list/Bucket.vue +99 -0
- package/src/layouts/history-list/CodeBlockTab.vue +61 -0
- package/src/layouts/history-list/DataSourceTab.vue +61 -0
- package/src/layouts/history-list/GroupRow.vue +207 -0
- package/src/layouts/history-list/HistoryDiffDialog.vue +212 -0
- package/src/layouts/history-list/HistoryListPanel.vue +271 -0
- package/src/layouts/history-list/InitialRow.vue +41 -0
- package/src/layouts/history-list/PageTab.vue +94 -0
- package/src/layouts/history-list/composables.ts +195 -0
- package/src/layouts/sidebar/layer/use-node-status.ts +9 -6
- package/src/services/BaseService.ts +35 -86
- package/src/services/codeBlock.ts +256 -5
- package/src/services/dataSource.ts +257 -6
- package/src/services/dep.ts +32 -9
- package/src/services/editor.ts +454 -171
- package/src/services/history.ts +452 -8
- package/src/services/props.ts +2 -2
- package/src/theme/history-list-panel.scss +423 -0
- package/src/theme/theme.scss +1 -0
- package/src/type.ts +179 -6
- package/src/utils/code-block.ts +150 -0
- package/src/utils/content-menu.ts +8 -5
- package/src/utils/dep/worker.ts +5 -3
- package/src/utils/index.ts +1 -0
- package/src/utils/undo-redo.ts +23 -0
- package/types/index.d.ts +2105 -810
- package/dist/es/utils/compose.js +0 -44
- package/dist/es/utils/editor-history.js +0 -100
- package/src/utils/compose.ts +0 -52
- package/src/utils/editor-history.ts +0 -138
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-misused-promises */
|
|
2
1
|
/*
|
|
3
2
|
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
|
4
3
|
*
|
|
@@ -19,45 +18,32 @@
|
|
|
19
18
|
|
|
20
19
|
import { EventEmitter } from 'events';
|
|
21
20
|
|
|
22
|
-
import { compose } from '@editor/utils/compose';
|
|
23
|
-
|
|
24
21
|
const methodName = (prefix: string, name: string) => `${prefix}${name[0].toUpperCase()}${name.substring(1)}`;
|
|
25
22
|
|
|
26
23
|
const isError = (error: any): boolean => Object.prototype.toString.call(error) === '[object Error]';
|
|
27
24
|
|
|
28
|
-
const doAction = (
|
|
29
|
-
args
|
|
30
|
-
scope: any,
|
|
31
|
-
sourceMethod: any,
|
|
32
|
-
beforeMethodName: string,
|
|
33
|
-
afterMethodName: string,
|
|
34
|
-
fn: (args: any[], next?: Function | undefined) => void,
|
|
35
|
-
) => {
|
|
36
|
-
try {
|
|
37
|
-
let beforeArgs = args;
|
|
25
|
+
const doAction = (args: any[], scope: any, sourceMethod: any, beforeMethodName: string, afterMethodName: string) => {
|
|
26
|
+
let beforeArgs = args;
|
|
38
27
|
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
for (const beforeMethod of scope.pluginOptionsList[beforeMethodName]) {
|
|
29
|
+
beforeArgs = beforeMethod(...beforeArgs) || [];
|
|
41
30
|
|
|
42
|
-
|
|
31
|
+
if (isError(beforeArgs)) throw beforeArgs;
|
|
43
32
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
33
|
+
if (!Array.isArray(beforeArgs)) {
|
|
34
|
+
beforeArgs = [beforeArgs];
|
|
47
35
|
}
|
|
36
|
+
}
|
|
48
37
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
for (const afterMethod of scope.pluginOptionsList[afterMethodName]) {
|
|
52
|
-
returnValue = afterMethod(returnValue, ...beforeArgs);
|
|
38
|
+
let returnValue: any = sourceMethod.apply(scope, beforeArgs);
|
|
53
39
|
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
for (const afterMethod of scope.pluginOptionsList[afterMethodName]) {
|
|
41
|
+
returnValue = afterMethod(returnValue, ...beforeArgs);
|
|
56
42
|
|
|
57
|
-
|
|
58
|
-
} catch (error) {
|
|
59
|
-
throw error;
|
|
43
|
+
if (isError(returnValue)) throw returnValue;
|
|
60
44
|
}
|
|
45
|
+
|
|
46
|
+
return returnValue;
|
|
61
47
|
};
|
|
62
48
|
|
|
63
49
|
const doAsyncAction = async (
|
|
@@ -66,40 +52,34 @@ const doAsyncAction = async (
|
|
|
66
52
|
sourceMethod: any,
|
|
67
53
|
beforeMethodName: string,
|
|
68
54
|
afterMethodName: string,
|
|
69
|
-
fn: (args: any[], next?: Function | undefined) => Promise<void> | void,
|
|
70
55
|
) => {
|
|
71
|
-
|
|
72
|
-
let beforeArgs = args;
|
|
56
|
+
let beforeArgs = args;
|
|
73
57
|
|
|
74
|
-
|
|
75
|
-
|
|
58
|
+
for (const beforeMethod of scope.pluginOptionsList[beforeMethodName]) {
|
|
59
|
+
beforeArgs = (await beforeMethod(...beforeArgs)) || [];
|
|
76
60
|
|
|
77
|
-
|
|
61
|
+
if (isError(beforeArgs)) throw beforeArgs;
|
|
78
62
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
63
|
+
if (!Array.isArray(beforeArgs)) {
|
|
64
|
+
beforeArgs = [beforeArgs];
|
|
82
65
|
}
|
|
66
|
+
}
|
|
83
67
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
for (const afterMethod of scope.pluginOptionsList[afterMethodName]) {
|
|
87
|
-
returnValue = await afterMethod(returnValue, ...beforeArgs);
|
|
68
|
+
let returnValue: any = await sourceMethod.apply(scope, beforeArgs);
|
|
88
69
|
|
|
89
|
-
|
|
90
|
-
|
|
70
|
+
for (const afterMethod of scope.pluginOptionsList[afterMethodName]) {
|
|
71
|
+
returnValue = await afterMethod(returnValue, ...beforeArgs);
|
|
91
72
|
|
|
92
|
-
|
|
93
|
-
} catch (error) {
|
|
94
|
-
throw error;
|
|
73
|
+
if (isError(returnValue)) throw returnValue;
|
|
95
74
|
}
|
|
75
|
+
|
|
76
|
+
return returnValue;
|
|
96
77
|
};
|
|
97
78
|
|
|
98
79
|
/**
|
|
99
|
-
*
|
|
100
|
-
* 方法1:
|
|
80
|
+
* 对Class进行扩展
|
|
101
81
|
* 给Class中的每个方法都添加before after两个钩子
|
|
102
|
-
* 给Class添加一个usePlugin方法,
|
|
82
|
+
* 给Class添加一个usePlugin方法,usePlugin方法可以传入一个包含before或者after方法的对象
|
|
103
83
|
*
|
|
104
84
|
* 例如:
|
|
105
85
|
* Class EditorService extends BaseService {
|
|
@@ -124,27 +104,9 @@ const doAsyncAction = async (
|
|
|
124
104
|
*
|
|
125
105
|
* 调用时的参数会透传到before方法的参数中, 然后before的return 会作为原方法的参数和after的参数,after第一个参数则是原方法的return值;
|
|
126
106
|
* 如需终止后续方法调用可以return new Error();
|
|
127
|
-
*
|
|
128
|
-
* 方法2:
|
|
129
|
-
* 给Class中的每个方法都添加中间件
|
|
130
|
-
* 给Class添加一个use方法,use方法可以传入一个包含源对象方法名作为key值的对象
|
|
131
|
-
*
|
|
132
|
-
* 例如:
|
|
133
|
-
* Class EditorService extends BaseService {
|
|
134
|
-
* constructor() {
|
|
135
|
-
* super([ { name: 'add', isAsync: true },]);
|
|
136
|
-
* }
|
|
137
|
-
* add(value) { return result; }
|
|
138
|
-
* };
|
|
139
|
-
*
|
|
140
|
-
* const editorService = new EditorService();
|
|
141
|
-
* editorService.use({
|
|
142
|
-
* add(value, next) { console.log(value); next() },
|
|
143
|
-
* });
|
|
144
107
|
*/
|
|
145
|
-
|
|
108
|
+
class BaseService extends EventEmitter {
|
|
146
109
|
private pluginOptionsList: Record<string, Function[]> = {};
|
|
147
|
-
private middleware: Record<string, Function[]> = {};
|
|
148
110
|
private taskList: (() => Promise<void>)[] = [];
|
|
149
111
|
private doingTask = false;
|
|
150
112
|
|
|
@@ -161,14 +123,12 @@ export default class extends EventEmitter {
|
|
|
161
123
|
|
|
162
124
|
this.pluginOptionsList[beforeMethodName] = [];
|
|
163
125
|
this.pluginOptionsList[afterMethodName] = [];
|
|
164
|
-
this.middleware[propertyName] = [];
|
|
165
126
|
|
|
166
|
-
const fn = compose(this.middleware[propertyName], isAsync);
|
|
167
127
|
Object.defineProperty(scope, propertyName, {
|
|
168
128
|
value: isAsync
|
|
169
129
|
? async (...args: any[]) => {
|
|
170
130
|
if (!serialMethods.includes(propertyName)) {
|
|
171
|
-
return doAsyncAction(args, scope, sourceMethod, beforeMethodName, afterMethodName
|
|
131
|
+
return doAsyncAction(args, scope, sourceMethod, beforeMethodName, afterMethodName);
|
|
172
132
|
}
|
|
173
133
|
|
|
174
134
|
// 由于async await,所以会出现函数执行到await时让出线程,导致执行顺序出错,例如调用了select(1) -> update -> select(2),这个时候就有可能出现update了2;
|
|
@@ -176,7 +136,7 @@ export default class extends EventEmitter {
|
|
|
176
136
|
const promise = new Promise<any>((resolve, reject) => {
|
|
177
137
|
this.taskList.push(async () => {
|
|
178
138
|
try {
|
|
179
|
-
const value = await doAsyncAction(args, scope, sourceMethod, beforeMethodName, afterMethodName
|
|
139
|
+
const value = await doAsyncAction(args, scope, sourceMethod, beforeMethodName, afterMethodName);
|
|
180
140
|
resolve(value);
|
|
181
141
|
} catch (e) {
|
|
182
142
|
reject(e);
|
|
@@ -190,20 +150,11 @@ export default class extends EventEmitter {
|
|
|
190
150
|
|
|
191
151
|
return promise;
|
|
192
152
|
}
|
|
193
|
-
: (...args: any[]) => doAction(args, scope, sourceMethod, beforeMethodName, afterMethodName
|
|
153
|
+
: (...args: any[]) => doAction(args, scope, sourceMethod, beforeMethodName, afterMethodName),
|
|
194
154
|
});
|
|
195
155
|
});
|
|
196
156
|
}
|
|
197
157
|
|
|
198
|
-
/**
|
|
199
|
-
* @deprecated 请使用usePlugin代替
|
|
200
|
-
*/
|
|
201
|
-
public use(options: Record<string, Function>) {
|
|
202
|
-
for (const [methodName, method] of Object.entries(options)) {
|
|
203
|
-
if (typeof method === 'function') this.middleware[methodName].push(method);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
158
|
public usePlugin(options: Record<string, Function>) {
|
|
208
159
|
for (const [methodName, method] of Object.entries(options)) {
|
|
209
160
|
if (typeof method === 'function' && !this.pluginOptionsList[methodName].includes(method)) {
|
|
@@ -224,10 +175,6 @@ export default class extends EventEmitter {
|
|
|
224
175
|
for (const key of Object.keys(this.pluginOptionsList)) {
|
|
225
176
|
this.pluginOptionsList[key] = [];
|
|
226
177
|
}
|
|
227
|
-
|
|
228
|
-
for (const key of Object.keys(this.middleware)) {
|
|
229
|
-
this.middleware[key] = [];
|
|
230
|
-
}
|
|
231
178
|
}
|
|
232
179
|
|
|
233
180
|
private async doTask() {
|
|
@@ -240,3 +187,5 @@ export default class extends EventEmitter {
|
|
|
240
187
|
this.doingTask = false;
|
|
241
188
|
}
|
|
242
189
|
}
|
|
190
|
+
|
|
191
|
+
export default BaseService;
|
|
@@ -23,10 +23,18 @@ import type { Writable } from 'type-fest';
|
|
|
23
23
|
import type { CodeBlockContent, CodeBlockDSL, Id, MNode, TargetOptions } from '@tmagic/core';
|
|
24
24
|
import { Target, Watcher } from '@tmagic/core';
|
|
25
25
|
import type { TableColumnConfig } from '@tmagic/form';
|
|
26
|
+
import { getValueByKeyPath, setValueByKeyPath } from '@tmagic/utils';
|
|
26
27
|
|
|
27
28
|
import editorService from '@editor/services/editor';
|
|
29
|
+
import historyService from '@editor/services/history';
|
|
28
30
|
import storageService, { Protocol } from '@editor/services/storage';
|
|
29
|
-
import type {
|
|
31
|
+
import type {
|
|
32
|
+
AsyncHookPlugin,
|
|
33
|
+
CodeBlockStepValue,
|
|
34
|
+
CodeState,
|
|
35
|
+
HistoryOpOptions,
|
|
36
|
+
HistoryOpOptionsWithChangeRecords,
|
|
37
|
+
} from '@editor/type';
|
|
30
38
|
import { CODE_DRAFT_STORAGE_KEY } from '@editor/type';
|
|
31
39
|
import { getEditorConfig } from '@editor/utils/config';
|
|
32
40
|
import { COPY_CODE_STORAGE_KEY } from '@editor/utils/editor';
|
|
@@ -40,6 +48,18 @@ const canUsePluginMethods = {
|
|
|
40
48
|
|
|
41
49
|
type AsyncMethodName = Writable<(typeof canUsePluginMethods)['async']>;
|
|
42
50
|
|
|
51
|
+
/**
|
|
52
|
+
* 「回滚」生成的新 step 简短描述。仅 service 层使用。
|
|
53
|
+
*/
|
|
54
|
+
const describeRevertCodeBlockStep = (step: CodeBlockStepValue): string => {
|
|
55
|
+
const { oldContent, newContent, changeRecords, id } = step;
|
|
56
|
+
if (oldContent === null && newContent) return `撤回新增 ${newContent.name || newContent.id || id}`;
|
|
57
|
+
if (oldContent && newContent === null) return `还原已删除的 ${oldContent.name || oldContent.id || id}`;
|
|
58
|
+
const name = newContent?.name || oldContent?.name || `${id}`;
|
|
59
|
+
const propPath = changeRecords?.[0]?.propPath;
|
|
60
|
+
return propPath ? `还原 ${name} · ${propPath}` : `还原 ${name}`;
|
|
61
|
+
};
|
|
62
|
+
|
|
43
63
|
class CodeBlock extends BaseService {
|
|
44
64
|
private state = reactive<CodeState>({
|
|
45
65
|
codeDsl: null,
|
|
@@ -92,10 +112,17 @@ class CodeBlock extends BaseService {
|
|
|
92
112
|
* 设置代码块ID和代码内容到源dsl
|
|
93
113
|
* @param {Id} id 代码块id
|
|
94
114
|
* @param {CodeBlockContent} codeConfig 代码块内容配置信息
|
|
115
|
+
* @param options 可选配置
|
|
116
|
+
* @param options.changeRecords form 端 propPath/value 列表,用于历史记录的精细化撤销/重做
|
|
117
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
95
118
|
* @returns {void}
|
|
96
119
|
*/
|
|
97
|
-
public async setCodeDslById(
|
|
98
|
-
|
|
120
|
+
public async setCodeDslById(
|
|
121
|
+
id: Id,
|
|
122
|
+
codeConfig: Partial<CodeBlockContent>,
|
|
123
|
+
{ changeRecords, doNotPushHistory = false }: HistoryOpOptionsWithChangeRecords = {},
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
this.setCodeDslByIdSync(id, codeConfig, true, { changeRecords, doNotPushHistory });
|
|
99
126
|
}
|
|
100
127
|
|
|
101
128
|
/**
|
|
@@ -104,9 +131,18 @@ class CodeBlock extends BaseService {
|
|
|
104
131
|
* @param {Id} id 代码块id
|
|
105
132
|
* @param {CodeBlockContent} codeConfig 代码块内容配置信息
|
|
106
133
|
* @param {boolean} force 是否强制写入,默认true
|
|
134
|
+
* @param options 可选配置
|
|
135
|
+
* @param options.changeRecords form 端 propPath/value 列表,用于历史记录的精细化撤销/重做
|
|
136
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
137
|
+
* @param options.historyDescription 入栈时附带的人类可读描述,用于历史面板展示
|
|
107
138
|
* @returns {void}
|
|
108
139
|
*/
|
|
109
|
-
public setCodeDslByIdSync(
|
|
140
|
+
public setCodeDslByIdSync(
|
|
141
|
+
id: Id,
|
|
142
|
+
codeConfig: Partial<CodeBlockContent>,
|
|
143
|
+
force = true,
|
|
144
|
+
{ changeRecords, doNotPushHistory = false, historyDescription }: HistoryOpOptionsWithChangeRecords = {},
|
|
145
|
+
): void {
|
|
110
146
|
const codeDsl = this.getCodeDsl();
|
|
111
147
|
|
|
112
148
|
if (!codeDsl) {
|
|
@@ -123,6 +159,9 @@ class CodeBlock extends BaseService {
|
|
|
123
159
|
}
|
|
124
160
|
}
|
|
125
161
|
|
|
162
|
+
// 历史记录:在写入前快照旧内容,区分新增/更新
|
|
163
|
+
const oldContent: CodeBlockContent | null = codeDsl[id] ? cloneDeep(codeDsl[id]) : null;
|
|
164
|
+
|
|
126
165
|
const existContent = codeDsl[id] || {};
|
|
127
166
|
|
|
128
167
|
codeDsl[id] = {
|
|
@@ -130,6 +169,12 @@ class CodeBlock extends BaseService {
|
|
|
130
169
|
...codeConfigProcessed,
|
|
131
170
|
};
|
|
132
171
|
|
|
172
|
+
const newContent = cloneDeep(codeDsl[id]);
|
|
173
|
+
|
|
174
|
+
if (!doNotPushHistory) {
|
|
175
|
+
historyService.pushCodeBlock(id, { oldContent, newContent, changeRecords, historyDescription });
|
|
176
|
+
}
|
|
177
|
+
|
|
133
178
|
this.emit('addOrUpdate', id, codeDsl[id]);
|
|
134
179
|
}
|
|
135
180
|
|
|
@@ -218,15 +263,27 @@ class CodeBlock extends BaseService {
|
|
|
218
263
|
/**
|
|
219
264
|
* 在dsl数据源中删除指定id的代码块
|
|
220
265
|
* @param {Id[]} codeIds 需要删除的代码块id数组
|
|
266
|
+
* @param options 可选配置
|
|
267
|
+
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
221
268
|
*/
|
|
222
|
-
public async deleteCodeDslByIds(
|
|
269
|
+
public async deleteCodeDslByIds(
|
|
270
|
+
codeIds: Id[],
|
|
271
|
+
{ doNotPushHistory = false, historyDescription }: HistoryOpOptions = {},
|
|
272
|
+
): Promise<void> {
|
|
223
273
|
const currentDsl = await this.getCodeDsl();
|
|
224
274
|
|
|
225
275
|
if (!currentDsl) return;
|
|
226
276
|
|
|
227
277
|
codeIds.forEach((id) => {
|
|
278
|
+
// 历史记录:删除前快照内容;不存在的 id 直接跳过历史推入
|
|
279
|
+
const oldContent: CodeBlockContent | null = currentDsl[id] ? cloneDeep(currentDsl[id]) : null;
|
|
280
|
+
|
|
228
281
|
delete currentDsl[id];
|
|
229
282
|
|
|
283
|
+
if (oldContent && !doNotPushHistory) {
|
|
284
|
+
historyService.pushCodeBlock(id, { oldContent, newContent: null, historyDescription });
|
|
285
|
+
}
|
|
286
|
+
|
|
230
287
|
this.emit('remove', id);
|
|
231
288
|
});
|
|
232
289
|
}
|
|
@@ -239,6 +296,87 @@ class CodeBlock extends BaseService {
|
|
|
239
296
|
return this.state.paramsColConfig;
|
|
240
297
|
}
|
|
241
298
|
|
|
299
|
+
/**
|
|
300
|
+
* 撤销指定代码块的最近一次变更。
|
|
301
|
+
*
|
|
302
|
+
* 内部走 setCodeDslByIdSync / deleteCodeDslByIds,因此会自动触发 codeBlockService 的
|
|
303
|
+
* `addOrUpdate` / `remove` 事件,由 initService 中的 handler 重新维护 dep target
|
|
304
|
+
* (DepTargetType.CODE_BLOCK 的 add / remove)。所有写回都带 `doNotPushHistory: true`,
|
|
305
|
+
* 确保不会在历史栈里产生新的记录。
|
|
306
|
+
*
|
|
307
|
+
* @param id 代码块 id
|
|
308
|
+
* @returns 撤销的 step;栈不存在或已无可撤销时返回 null
|
|
309
|
+
*/
|
|
310
|
+
public async undo(id: Id): Promise<CodeBlockStepValue | null> {
|
|
311
|
+
const step = historyService.undoCodeBlock(id);
|
|
312
|
+
if (!step) return null;
|
|
313
|
+
await this.applyHistoryStep(step, true);
|
|
314
|
+
return step;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 重做指定代码块的下一次变更。
|
|
319
|
+
* @param id 代码块 id
|
|
320
|
+
* @returns 重做的 step;栈不存在或已无可重做时返回 null
|
|
321
|
+
*/
|
|
322
|
+
public async redo(id: Id): Promise<CodeBlockStepValue | null> {
|
|
323
|
+
const step = historyService.redoCodeBlock(id);
|
|
324
|
+
if (!step) return null;
|
|
325
|
+
await this.applyHistoryStep(step, false);
|
|
326
|
+
return step;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** 是否可对指定代码块撤销。 */
|
|
330
|
+
public canUndo(id: Id): boolean {
|
|
331
|
+
return historyService.canUndoCodeBlock(id);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** 是否可对指定代码块重做。 */
|
|
335
|
+
public canRedo(id: Id): boolean {
|
|
336
|
+
return historyService.canRedoCodeBlock(id);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 跳转指定代码块的历史栈到目标游标。语义同 editor.gotoPageStep。
|
|
341
|
+
*
|
|
342
|
+
* @param id 代码块 id
|
|
343
|
+
* @param targetCursor 目标游标位置(已应用步骤数量)
|
|
344
|
+
* @returns 实际移动到的最终游标位置
|
|
345
|
+
*/
|
|
346
|
+
public async goto(id: Id, targetCursor: number): Promise<number> {
|
|
347
|
+
let cursor = historyService.getCodeBlockCursor(id);
|
|
348
|
+
const target = Math.max(0, targetCursor);
|
|
349
|
+
while (cursor > target) {
|
|
350
|
+
const step = await this.undo(id);
|
|
351
|
+
if (!step) break;
|
|
352
|
+
cursor -= 1;
|
|
353
|
+
}
|
|
354
|
+
while (cursor < target) {
|
|
355
|
+
const step = await this.redo(id);
|
|
356
|
+
if (!step) break;
|
|
357
|
+
cursor += 1;
|
|
358
|
+
}
|
|
359
|
+
return cursor;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* 「回滚」指定代码块历史步骤(类 git revert 语义):
|
|
364
|
+
* - 不动原始栈结构(不移动 cursor、不丢弃任何步骤);
|
|
365
|
+
* - 把目标 step 的修改**反向应用**一次,并作为**新步骤**追加到栈顶;
|
|
366
|
+
* - 仅对已应用的步骤生效。
|
|
367
|
+
*
|
|
368
|
+
* @param id 代码块 id
|
|
369
|
+
* @param index 目标 step 在该栈中的索引(0 为最早),通常由历史面板传入
|
|
370
|
+
* @returns 反向后产生的新 step;目标不存在 / 未应用时返回 null
|
|
371
|
+
*/
|
|
372
|
+
public async revert(id: Id, index: number): Promise<CodeBlockStepValue | null> {
|
|
373
|
+
const list = historyService.getCodeBlockStepList(id);
|
|
374
|
+
const entry = list[index];
|
|
375
|
+
if (!entry?.applied) return null;
|
|
376
|
+
const description = `回滚 #${index + 1}: ${describeRevertCodeBlockStep(entry.step)}`;
|
|
377
|
+
return await this.applyRevertStep(entry.step, description);
|
|
378
|
+
}
|
|
379
|
+
|
|
242
380
|
/**
|
|
243
381
|
* 生成代码块唯一id
|
|
244
382
|
* @returns {Id} 代码块唯一id
|
|
@@ -320,6 +458,119 @@ class CodeBlock extends BaseService {
|
|
|
320
458
|
public usePlugin(options: AsyncHookPlugin<AsyncMethodName, CodeBlock>): void {
|
|
321
459
|
super.usePlugin(options);
|
|
322
460
|
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* 反向应用一个 step 并以新 step 入栈。逻辑与 applyHistoryStep(reverse=true) 同构,
|
|
464
|
+
* 差异仅在于通过公开的 setCodeDslByIdSync / deleteCodeDslByIds 触发 push。
|
|
465
|
+
*/
|
|
466
|
+
private async applyRevertStep(
|
|
467
|
+
step: CodeBlockStepValue,
|
|
468
|
+
historyDescription: string,
|
|
469
|
+
): Promise<CodeBlockStepValue | null> {
|
|
470
|
+
const { id, oldContent, newContent, changeRecords } = step;
|
|
471
|
+
|
|
472
|
+
// 原本是新增 → revert 即删除
|
|
473
|
+
if (oldContent === null && newContent) {
|
|
474
|
+
await this.deleteCodeDslByIds([id], { historyDescription });
|
|
475
|
+
return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 原本是删除 → revert 即写回
|
|
479
|
+
if (oldContent && newContent === null) {
|
|
480
|
+
this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { historyDescription });
|
|
481
|
+
return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!oldContent || !newContent) return null;
|
|
485
|
+
|
|
486
|
+
// 原本是更新 → 把 oldContent 写回;优先按 changeRecords 局部 patch
|
|
487
|
+
if (changeRecords?.length) {
|
|
488
|
+
const current = this.getCodeContentById(id);
|
|
489
|
+
if (!current) return null;
|
|
490
|
+
const patched = cloneDeep(current) as CodeBlockContent;
|
|
491
|
+
let fallbackToFullReplace = false;
|
|
492
|
+
for (const record of changeRecords) {
|
|
493
|
+
if (!record.propPath) {
|
|
494
|
+
fallbackToFullReplace = true;
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
const value = cloneDeep(getValueByKeyPath(record.propPath, oldContent));
|
|
498
|
+
setValueByKeyPath(record.propPath, value, patched);
|
|
499
|
+
}
|
|
500
|
+
this.setCodeDslByIdSync(id, fallbackToFullReplace ? cloneDeep(oldContent) : patched, true, {
|
|
501
|
+
changeRecords,
|
|
502
|
+
historyDescription,
|
|
503
|
+
});
|
|
504
|
+
return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { historyDescription });
|
|
508
|
+
return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* 把一条历史 step 应用到当前代码块服务上。
|
|
513
|
+
*
|
|
514
|
+
* 复用现有的 setCodeDslByIdSync / deleteCodeDslByIds,目的是借助它们发出的事件
|
|
515
|
+
* 触发 initService 中的 dep target 维护(CODE_BLOCK 的 add / remove)。
|
|
516
|
+
* 所有写回都带 `doNotPushHistory: true`,确保不会在历史栈里产生新的记录。
|
|
517
|
+
*
|
|
518
|
+
* - oldContent=null, newContent≠null:原始为新增 → undo 删除;redo 再次 setCodeDslByIdSync
|
|
519
|
+
* - oldContent≠null, newContent=null:原始为删除 → undo 还原写入;redo 再次删除
|
|
520
|
+
* - 两侧都有:原始为更新 → 按 changeRecords 局部 patch;缺省退化为整内容替换
|
|
521
|
+
*
|
|
522
|
+
* @param step 历史 step
|
|
523
|
+
* @param reverse true=撤销,false=重做
|
|
524
|
+
*/
|
|
525
|
+
private async applyHistoryStep(step: CodeBlockStepValue, reverse: boolean): Promise<void> {
|
|
526
|
+
const { id, oldContent, newContent, changeRecords } = step;
|
|
527
|
+
|
|
528
|
+
// 新增 / 删除:直接 set 或 delete,不走 patch 逻辑
|
|
529
|
+
if (oldContent === null && newContent) {
|
|
530
|
+
if (reverse) {
|
|
531
|
+
await this.deleteCodeDslByIds([id], { doNotPushHistory: true });
|
|
532
|
+
} else {
|
|
533
|
+
this.setCodeDslByIdSync(id, cloneDeep(newContent), true, { doNotPushHistory: true });
|
|
534
|
+
}
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (oldContent && newContent === null) {
|
|
539
|
+
if (reverse) {
|
|
540
|
+
this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { doNotPushHistory: true });
|
|
541
|
+
} else {
|
|
542
|
+
await this.deleteCodeDslByIds([id], { doNotPushHistory: true });
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (!oldContent || !newContent) return;
|
|
548
|
+
|
|
549
|
+
// 更新场景:优先按 changeRecords 局部 patch;缺省退化为整内容替换
|
|
550
|
+
const sourceForValues = reverse ? oldContent : newContent;
|
|
551
|
+
|
|
552
|
+
if (changeRecords?.length) {
|
|
553
|
+
const current = this.getCodeContentById(id);
|
|
554
|
+
if (!current) return;
|
|
555
|
+
const patched = cloneDeep(current) as CodeBlockContent;
|
|
556
|
+
let fallbackToFullReplace = false;
|
|
557
|
+
for (const record of changeRecords) {
|
|
558
|
+
if (!record.propPath) {
|
|
559
|
+
fallbackToFullReplace = true;
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
const value = cloneDeep(getValueByKeyPath(record.propPath, sourceForValues));
|
|
563
|
+
setValueByKeyPath(record.propPath, value, patched);
|
|
564
|
+
}
|
|
565
|
+
this.setCodeDslByIdSync(id, fallbackToFullReplace ? cloneDeep(sourceForValues) : patched, true, {
|
|
566
|
+
changeRecords,
|
|
567
|
+
doNotPushHistory: true,
|
|
568
|
+
});
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
this.setCodeDslByIdSync(id, cloneDeep(sourceForValues), true, { doNotPushHistory: true });
|
|
573
|
+
}
|
|
323
574
|
}
|
|
324
575
|
|
|
325
576
|
export type CodeBlockService = CodeBlock;
|