@tmagic/editor 1.8.0-beta.21 → 1.8.0-beta.23
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/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/es/components/Resizer.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/es/fields/CondOpSelect.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/es/fields/DataSourceInput.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/es/fields/StyleSetter/components/Box.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +6 -1
- package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +35 -3
- package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +26 -5
- package/dist/es/index.js +2 -2
- package/dist/es/initService.js +2 -1
- package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +23 -12
- package/dist/es/layouts/sidebar/ComponentListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +3 -2
- package/dist/es/services/codeBlock.js +4 -2
- package/dist/es/services/editor.js +12 -8
- package/dist/es/utils/dep/collect-worker-client.js +67 -27
- package/dist/es/utils/dep/worker.js +1 -1
- package/dist/es/utils/type-match-rules.js +24 -36
- package/dist/tmagic-editor.umd.cjs +413 -306
- package/package.json +12 -12
- package/src/fields/StyleSetter/components/Box.vue +1 -1
- package/src/fields/StyleSetter/components/Position.vue +1 -1
- package/src/fields/StyleSetter/pro/Background.vue +7 -0
- package/src/fields/StyleSetter/pro/Font.vue +57 -0
- package/src/fields/StyleSetter/pro/Position.vue +31 -0
- package/src/layouts/props-panel/PropsPanel.vue +30 -16
- package/src/layouts/sidebar/ComponentListPanel.vue +5 -1
- package/src/layouts/sidebar/layer/LayerPanel.vue +1 -1
- package/src/services/editor.ts +27 -8
- package/src/utils/dep/collect-worker-client.ts +97 -31
- package/src/utils/type-match-rules.ts +41 -34
- package/types/index.d.ts +106 -153
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.8.0-beta.
|
|
2
|
+
"version": "1.8.0-beta.23",
|
|
3
3
|
"name": "@tmagic/editor",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
@@ -54,29 +54,29 @@
|
|
|
54
54
|
"events": "^3.3.0",
|
|
55
55
|
"gesto": "^1.19.4",
|
|
56
56
|
"keycon": "^1.4.0",
|
|
57
|
-
"lodash-es": "^4.
|
|
57
|
+
"lodash-es": "^4.18.1",
|
|
58
58
|
"moveable": "^0.53.0",
|
|
59
|
-
"serialize-javascript": "^7.0.
|
|
60
|
-
"sortablejs": "^1.15.
|
|
61
|
-
"@tmagic/design": "1.8.0-beta.
|
|
62
|
-
"@tmagic/
|
|
63
|
-
"@tmagic/
|
|
64
|
-
"@tmagic/table": "1.8.0-beta.
|
|
65
|
-
"@tmagic/
|
|
59
|
+
"serialize-javascript": "^7.0.7",
|
|
60
|
+
"sortablejs": "^1.15.7",
|
|
61
|
+
"@tmagic/design": "1.8.0-beta.23",
|
|
62
|
+
"@tmagic/stage": "1.8.0-beta.23",
|
|
63
|
+
"@tmagic/utils": "1.8.0-beta.23",
|
|
64
|
+
"@tmagic/table": "1.8.0-beta.23",
|
|
65
|
+
"@tmagic/form": "1.8.0-beta.23"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/events": "^3.0.3",
|
|
69
|
-
"@types/lodash-es": "^4.17.
|
|
69
|
+
"@types/lodash-es": "^4.17.12",
|
|
70
70
|
"@types/serialize-javascript": "^5.0.4",
|
|
71
71
|
"@types/sortablejs": "^1.15.9",
|
|
72
|
-
"@vue/test-utils": "^2.4.
|
|
72
|
+
"@vue/test-utils": "^2.4.11"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"monaco-editor": "^0.55.1 ",
|
|
76
76
|
"type-fest": "^5.2.0",
|
|
77
77
|
"typescript": "^6.0.3",
|
|
78
78
|
"vue": "^3.5.40",
|
|
79
|
-
"@tmagic/core": "1.8.0-beta.
|
|
79
|
+
"@tmagic/core": "1.8.0-beta.23"
|
|
80
80
|
},
|
|
81
81
|
"peerDependenciesMeta": {
|
|
82
82
|
"typescript": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
<script lang="ts" setup>
|
|
18
18
|
import { markRaw } from 'vue';
|
|
19
19
|
|
|
20
|
+
import { appendValidateSuggestion } from '@tmagic/design';
|
|
20
21
|
import { type ContainerChangeEventData, defineFormConfig, MContainer } from '@tmagic/form';
|
|
21
22
|
import type { StyleSchema } from '@tmagic/schema';
|
|
22
23
|
|
|
@@ -47,6 +48,12 @@ const formConfig = defineFormConfig([
|
|
|
47
48
|
fieldConfig: {
|
|
48
49
|
type: 'colorPicker',
|
|
49
50
|
},
|
|
51
|
+
rules: [
|
|
52
|
+
{
|
|
53
|
+
typeMatch: true,
|
|
54
|
+
message: appendValidateSuggestion('背景色应为字符串', '请参考以下示例值:"#000000"'),
|
|
55
|
+
},
|
|
56
|
+
],
|
|
50
57
|
},
|
|
51
58
|
{
|
|
52
59
|
name: 'backgroundImage',
|
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
<script lang="ts" setup>
|
|
18
18
|
import { markRaw } from 'vue';
|
|
19
19
|
|
|
20
|
+
import { appendValidateSuggestion } from '@tmagic/design';
|
|
20
21
|
import { type ContainerChangeEventData, defineFormConfig, MContainer } from '@tmagic/form';
|
|
21
22
|
import type { StyleSchema } from '@tmagic/schema';
|
|
22
23
|
|
|
24
|
+
import { validateDataSourceFieldSelectValue } from '@editor/utils/type-match-rules';
|
|
25
|
+
|
|
23
26
|
import { AlignCenter, AlignLeft, AlignRight } from '../icons/text-align';
|
|
24
27
|
|
|
25
28
|
defineProps<{
|
|
@@ -48,6 +51,12 @@ const formConfig = defineFormConfig([
|
|
|
48
51
|
fieldConfig: {
|
|
49
52
|
type: 'text',
|
|
50
53
|
},
|
|
54
|
+
rules: [
|
|
55
|
+
{
|
|
56
|
+
typeMatch: true,
|
|
57
|
+
message: appendValidateSuggestion('字号应为字符串或数字', '请参考以下示例值:24 或 "24"'),
|
|
58
|
+
},
|
|
59
|
+
],
|
|
51
60
|
},
|
|
52
61
|
{
|
|
53
62
|
labelWidth: '68px',
|
|
@@ -65,8 +74,10 @@ const formConfig = defineFormConfig([
|
|
|
65
74
|
text: '字重',
|
|
66
75
|
labelWidth: '68px',
|
|
67
76
|
type: 'data-source-field-select',
|
|
77
|
+
dataSourceFieldType: ['string', 'number'],
|
|
68
78
|
fieldConfig: {
|
|
69
79
|
type: 'select',
|
|
80
|
+
allowCreate: true,
|
|
70
81
|
options: ['normal', 'bold']
|
|
71
82
|
.concat(
|
|
72
83
|
Array(7)
|
|
@@ -78,6 +89,46 @@ const formConfig = defineFormConfig([
|
|
|
78
89
|
text: item,
|
|
79
90
|
})),
|
|
80
91
|
},
|
|
92
|
+
rules: [
|
|
93
|
+
{
|
|
94
|
+
typeMatch: false,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
validator: ({ value, callback }, { config, model, prop }, mForm) => {
|
|
98
|
+
if (value === '' || value === null || value === undefined) {
|
|
99
|
+
return callback();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = validateDataSourceFieldSelectValue(
|
|
103
|
+
value,
|
|
104
|
+
{
|
|
105
|
+
fieldType: 'data-source-field-select',
|
|
106
|
+
mForm,
|
|
107
|
+
props: { config, model, prop },
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
// 字重允许 string(含可创建项)与 number(如 700)
|
|
111
|
+
validatePlainValue: (plainValue) => {
|
|
112
|
+
if (typeof plainValue === 'string' || (typeof plainValue === 'number' && !Number.isNaN(plainValue))) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return '字重应为字符串或数字';
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (result && typeof (result as Promise<string | undefined>).then === 'function') {
|
|
121
|
+
(result as Promise<string | undefined>).then(
|
|
122
|
+
(error) => callback(error),
|
|
123
|
+
(error) => callback(error),
|
|
124
|
+
);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return callback(result);
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
],
|
|
81
132
|
},
|
|
82
133
|
{
|
|
83
134
|
labelWidth: '68px',
|
|
@@ -87,6 +138,12 @@ const formConfig = defineFormConfig([
|
|
|
87
138
|
fieldConfig: {
|
|
88
139
|
type: 'colorPicker',
|
|
89
140
|
},
|
|
141
|
+
rules: [
|
|
142
|
+
{
|
|
143
|
+
typeMatch: true,
|
|
144
|
+
message: appendValidateSuggestion('颜色应为字符串', '请参考以下示例值:"#000000"'),
|
|
145
|
+
},
|
|
146
|
+
],
|
|
90
147
|
},
|
|
91
148
|
{
|
|
92
149
|
name: 'textAlign',
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
</template>
|
|
16
16
|
|
|
17
17
|
<script lang="ts" setup>
|
|
18
|
+
import { appendValidateSuggestion } from '@tmagic/design';
|
|
18
19
|
import { type ContainerChangeEventData, defineFormConfig, MContainer } from '@tmagic/form';
|
|
19
20
|
import type { StyleSchema } from '@tmagic/schema';
|
|
20
21
|
|
|
@@ -66,6 +67,12 @@ const formConfig = defineFormConfig([
|
|
|
66
67
|
fieldConfig: {
|
|
67
68
|
type: 'text',
|
|
68
69
|
},
|
|
70
|
+
rules: [
|
|
71
|
+
{
|
|
72
|
+
typeMatch: true,
|
|
73
|
+
message: appendValidateSuggestion('left 应为字符串', '请参考以下示例值:"10"'),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
69
76
|
},
|
|
70
77
|
{
|
|
71
78
|
name: 'top',
|
|
@@ -74,6 +81,12 @@ const formConfig = defineFormConfig([
|
|
|
74
81
|
fieldConfig: {
|
|
75
82
|
type: 'text',
|
|
76
83
|
},
|
|
84
|
+
rules: [
|
|
85
|
+
{
|
|
86
|
+
typeMatch: true,
|
|
87
|
+
message: appendValidateSuggestion('top 应为字符串', '请参考以下示例值:"10"'),
|
|
88
|
+
},
|
|
89
|
+
],
|
|
77
90
|
},
|
|
78
91
|
],
|
|
79
92
|
},
|
|
@@ -89,6 +102,12 @@ const formConfig = defineFormConfig([
|
|
|
89
102
|
fieldConfig: {
|
|
90
103
|
type: 'text',
|
|
91
104
|
},
|
|
105
|
+
rules: [
|
|
106
|
+
{
|
|
107
|
+
typeMatch: true,
|
|
108
|
+
message: appendValidateSuggestion('right 应为字符串', '请参考以下示例值:"10"'),
|
|
109
|
+
},
|
|
110
|
+
],
|
|
92
111
|
},
|
|
93
112
|
{
|
|
94
113
|
name: 'bottom',
|
|
@@ -97,6 +116,12 @@ const formConfig = defineFormConfig([
|
|
|
97
116
|
fieldConfig: {
|
|
98
117
|
type: 'text',
|
|
99
118
|
},
|
|
119
|
+
rules: [
|
|
120
|
+
{
|
|
121
|
+
typeMatch: true,
|
|
122
|
+
message: appendValidateSuggestion('bottom 应为字符串', '请参考以下示例值:"10"'),
|
|
123
|
+
},
|
|
124
|
+
],
|
|
100
125
|
},
|
|
101
126
|
],
|
|
102
127
|
},
|
|
@@ -108,6 +133,12 @@ const formConfig = defineFormConfig([
|
|
|
108
133
|
fieldConfig: {
|
|
109
134
|
type: 'text',
|
|
110
135
|
},
|
|
136
|
+
rules: [
|
|
137
|
+
{
|
|
138
|
+
typeMatch: true,
|
|
139
|
+
message: appendValidateSuggestion('zIndex 应为数字', '请参考以下示例值:10'),
|
|
140
|
+
},
|
|
141
|
+
],
|
|
111
142
|
},
|
|
112
143
|
]);
|
|
113
144
|
|
|
@@ -158,14 +158,34 @@ const submit = async (
|
|
|
158
158
|
v.id = values.value.id;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
161
|
+
// 区分操作途径:表单字段编辑(MForm @change)会带上 eventData(含 changeRecords);
|
|
162
|
+
// 源码编辑器(CodeEditor @save → saveCode)保存时不带 eventData,据此标记为「源码编辑器」。
|
|
163
|
+
const historySource = eventData ? 'props' : 'code';
|
|
164
|
+
// 源码编辑统一走整节点 replace,避免 merge 保留已删除字段。
|
|
165
|
+
const replace = historySource === 'code';
|
|
166
|
+
|
|
167
|
+
let newValue: MNode;
|
|
168
|
+
|
|
169
|
+
if (replace) {
|
|
170
|
+
if (source === 'style') {
|
|
171
|
+
// 样式面板源码仅提交 { style }:先把新 style 替换到原节点上,再整节点覆盖。
|
|
172
|
+
newValue = {
|
|
173
|
+
...values.value,
|
|
174
|
+
id: v.id || values.value.id,
|
|
175
|
+
style: { ...(v.style || {}) },
|
|
176
|
+
};
|
|
177
|
+
} else {
|
|
178
|
+
// 属性面板源码提交完整节点 DSL
|
|
179
|
+
newValue = { ...v };
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
newValue = {
|
|
183
|
+
...v,
|
|
184
|
+
style: {},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (v.style) {
|
|
188
|
+
// 空字符串样式值表示「清除该样式」,需保留才能在 doUpdate 的 mergeWith 中覆盖旧值。
|
|
169
189
|
// 表单编辑:先过滤掉空字符串(避免表单默认空值污染 DSL),
|
|
170
190
|
// 再按 changeRecords 恢复被主动清空的字段。
|
|
171
191
|
Object.entries(v.style).forEach(([key, value]) => {
|
|
@@ -174,24 +194,18 @@ const submit = async (
|
|
|
174
194
|
}
|
|
175
195
|
});
|
|
176
196
|
|
|
177
|
-
eventData
|
|
197
|
+
eventData?.changeRecords?.forEach((record) => {
|
|
178
198
|
if (record.propPath?.startsWith('style') && record.value === '') {
|
|
179
199
|
setValueByKeyPath(record.propPath, record.value, newValue);
|
|
180
200
|
}
|
|
181
201
|
});
|
|
182
|
-
} else {
|
|
183
|
-
// 源码编辑器保存(无 eventData):style 原样保留,其中的空字符串视为用户主动清除该样式。
|
|
184
|
-
newValue.style = { ...v.style };
|
|
185
202
|
}
|
|
186
203
|
}
|
|
187
204
|
|
|
188
|
-
// 区分操作途径:表单字段编辑(MForm @change)会带上 eventData(含 changeRecords);
|
|
189
|
-
// 源码编辑器(CodeEditor @save → saveCode)保存时不带 eventData,据此标记为「源码编辑器」。
|
|
190
|
-
const historySource = eventData ? 'props' : 'code';
|
|
191
|
-
|
|
192
205
|
editorService.update(newValue, {
|
|
193
206
|
changeRecords: eventData?.changeRecords,
|
|
194
207
|
historySource,
|
|
208
|
+
replace,
|
|
195
209
|
// 启用校验联动时,仅校验失败(error 存在)才把错误信息随更新传入 editorService 记录;
|
|
196
210
|
// 其余情况(含表单校验成功、CodeEditor 源码保存)不携带 invalidInfo,由 editorService 在执行 update 时统一清除该节点错误。
|
|
197
211
|
...(enablePropsFormValidate && error ? { invalidInfo: { id: newValue.id, source, error: error?.message } } : {}),
|
|
@@ -71,7 +71,11 @@ const stage = computed(() => editorService.get('stage'));
|
|
|
71
71
|
const list = computed<ComponentGroup[]>(() =>
|
|
72
72
|
componentListService.getList().map((group: ComponentGroup) => ({
|
|
73
73
|
...group,
|
|
74
|
-
items: group.items.filter((item: ComponentItem) =>
|
|
74
|
+
items: group.items.filter((item: ComponentItem) =>
|
|
75
|
+
`${item.text || ''}${item.desc || ''}${item.type}`
|
|
76
|
+
.toLocaleLowerCase()
|
|
77
|
+
.includes(searchText.value.toLocaleLowerCase()),
|
|
78
|
+
),
|
|
75
79
|
})),
|
|
76
80
|
);
|
|
77
81
|
|
|
@@ -120,7 +120,7 @@ const filterNodeMethod = (v: string, data: MNode): boolean => {
|
|
|
120
120
|
name = 'container';
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
return `${data.id}${name}${data.type}`.includes(v);
|
|
123
|
+
return `${data.id}${name}${data.type || ''}`.toLocaleLowerCase().includes(v.toLocaleLowerCase());
|
|
124
124
|
};
|
|
125
125
|
|
|
126
126
|
const { filterTextChangeHandler } = useFilter(nodeData, nodeStatusMap, filterNodeMethod);
|
package/src/services/editor.ts
CHANGED
|
@@ -720,7 +720,11 @@ class Editor extends BaseService {
|
|
|
720
720
|
|
|
721
721
|
public async doUpdate(
|
|
722
722
|
config: MNode,
|
|
723
|
-
{
|
|
723
|
+
{
|
|
724
|
+
changeRecords = [],
|
|
725
|
+
historySource,
|
|
726
|
+
replace = false,
|
|
727
|
+
}: { changeRecords?: ChangeRecord[]; historySource?: HistoryOpSource; replace?: boolean } = {},
|
|
724
728
|
): Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }> {
|
|
725
729
|
const root = this.get('root');
|
|
726
730
|
if (!root) throw new Error('root为空');
|
|
@@ -733,9 +737,14 @@ class Editor extends BaseService {
|
|
|
733
737
|
|
|
734
738
|
const node = toRaw(info.node);
|
|
735
739
|
|
|
736
|
-
|
|
740
|
+
// replace=true 时跳过 toggleFixedPosition / mergeWith / setChildrenLayout,直接用传入配置整节点替换
|
|
741
|
+
let newConfig = replace
|
|
742
|
+
? cloneDeep(toRaw(config))
|
|
743
|
+
: await toggleFixedPosition(toRaw(config), node, info.path, this.getLayout);
|
|
737
744
|
|
|
738
|
-
|
|
745
|
+
if (!replace) {
|
|
746
|
+
newConfig = mergeWith(cloneDeep(node), newConfig, editorNodeMergeCustomizer);
|
|
747
|
+
}
|
|
739
748
|
|
|
740
749
|
if (!newConfig.type) throw new Error('配置缺少type值');
|
|
741
750
|
|
|
@@ -756,10 +765,12 @@ class Editor extends BaseService {
|
|
|
756
765
|
|
|
757
766
|
if (!parentNodeItems || typeof index === 'undefined' || index === -1) throw new Error('更新的节点未找到');
|
|
758
767
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
768
|
+
if (!replace) {
|
|
769
|
+
const newLayout = await this.getLayout(newConfig);
|
|
770
|
+
const layout = await this.getLayout(node);
|
|
771
|
+
if (Array.isArray(newConfig.items) && newLayout !== layout) {
|
|
772
|
+
newConfig = setChildrenLayout(newConfig as MContainer, newLayout);
|
|
773
|
+
}
|
|
763
774
|
}
|
|
764
775
|
|
|
765
776
|
parentNodeItems[index] = newConfig;
|
|
@@ -798,6 +809,7 @@ class Editor extends BaseService {
|
|
|
798
809
|
* @param data.changeRecordList 多节点 form 端变更记录列表,按 config 数组同序对应每个节点;优先级高于 changeRecords
|
|
799
810
|
* @param data.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
800
811
|
* @param data.historyDescription 入栈时附带的人类可读描述,用于历史面板展示(不影响 undo/redo 行为)
|
|
812
|
+
* @param data.replace 是否整节点替换:为 true 时跳过 mergeWith / toggleFixedPosition / setChildrenLayout,直接用传入配置覆盖(默认 false)
|
|
801
813
|
* @returns 更新后的节点配置
|
|
802
814
|
*/
|
|
803
815
|
public async update(
|
|
@@ -808,6 +820,11 @@ class Editor extends BaseService {
|
|
|
808
820
|
doNotPushHistory?: boolean;
|
|
809
821
|
historyDescription?: string;
|
|
810
822
|
historySource?: HistoryOpSource;
|
|
823
|
+
/**
|
|
824
|
+
* 为 true 时不做深合并等变换,直接用传入配置整节点替换现有节点。
|
|
825
|
+
* 适用于源码编辑、历史整节点快照回放等「完整 DSL」场景;默认 false(局部属性更新走 merge)。
|
|
826
|
+
*/
|
|
827
|
+
replace?: boolean;
|
|
811
828
|
/**
|
|
812
829
|
* 属性面板提交时携带的校验错误信息,在写入历史记录之前落库,
|
|
813
830
|
* 使历史快照与本次变更对齐,从而 undo/redo 能正确还原错误标记。
|
|
@@ -823,6 +840,7 @@ class Editor extends BaseService {
|
|
|
823
840
|
changeRecords,
|
|
824
841
|
historyDescription,
|
|
825
842
|
historySource,
|
|
843
|
+
replace = false,
|
|
826
844
|
invalidInfo,
|
|
827
845
|
} = data;
|
|
828
846
|
|
|
@@ -833,7 +851,7 @@ class Editor extends BaseService {
|
|
|
833
851
|
const updateData = await Promise.all(
|
|
834
852
|
nodes.map((node, index) => {
|
|
835
853
|
const recordsForNode = changeRecordList ? (changeRecordList[index] ?? []) : (changeRecords ?? []);
|
|
836
|
-
return this.doUpdate(node, { changeRecords: recordsForNode, historySource });
|
|
854
|
+
return this.doUpdate(node, { changeRecords: recordsForNode, historySource, replace });
|
|
837
855
|
}),
|
|
838
856
|
);
|
|
839
857
|
|
|
@@ -1348,6 +1366,7 @@ class Editor extends BaseService {
|
|
|
1348
1366
|
doNotPushHistory?: boolean;
|
|
1349
1367
|
historyDescription?: string;
|
|
1350
1368
|
historySource?: HistoryOpSource;
|
|
1369
|
+
replace?: boolean;
|
|
1351
1370
|
} = {},
|
|
1352
1371
|
): Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> {
|
|
1353
1372
|
this.lastPushedHistoryId = null;
|
|
@@ -12,6 +12,14 @@ export interface CollectWorkerResult {
|
|
|
12
12
|
nodeIds: Id[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
interface CollectTask {
|
|
16
|
+
id: number;
|
|
17
|
+
createRequest: (id: number) => CollectWorkerRequest;
|
|
18
|
+
resolve: (response: CollectWorkerResponse | null) => void;
|
|
19
|
+
/** 已被 abort 丢弃:调用方已按 null 结算,结果回来后不再回传 */
|
|
20
|
+
discarded: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
/**
|
|
16
24
|
* 依赖收集 worker 的主线程客户端
|
|
17
25
|
*
|
|
@@ -20,13 +28,20 @@ export interface CollectWorkerResult {
|
|
|
20
28
|
*
|
|
21
29
|
* 全量收集与增量收集共用一个常驻 worker:增量收集调用频繁(每次节点更新都会触发),
|
|
22
30
|
* 按次创建 worker 的启动开销无法接受,也容易漏掉销毁导致线程泄漏。
|
|
31
|
+
*
|
|
32
|
+
* 请求在主线程侧排队后逐个投递(worker 本身也是串行处理),这样 abort 时能直接丢掉尚未投递的请求,
|
|
33
|
+
* 无需销毁线程:worker 只在真正异常或销毁时才重建。
|
|
23
34
|
*/
|
|
24
35
|
export class CollectWorkerClient {
|
|
25
36
|
private worker: Worker | null = null;
|
|
26
37
|
|
|
27
38
|
private seed = 0;
|
|
28
39
|
|
|
29
|
-
|
|
40
|
+
/** 等待投递给 worker 的请求 */
|
|
41
|
+
private queue: CollectTask[] = [];
|
|
42
|
+
|
|
43
|
+
/** 已投递、等待 worker 响应的请求,worker 串行处理,同一时刻最多一个 */
|
|
44
|
+
private inflight: CollectTask | null = null;
|
|
30
45
|
|
|
31
46
|
public get isSupported() {
|
|
32
47
|
return typeof Worker !== 'undefined';
|
|
@@ -53,24 +68,37 @@ export class CollectWorkerClient {
|
|
|
53
68
|
public terminate() {
|
|
54
69
|
this.worker?.terminate();
|
|
55
70
|
this.worker = null;
|
|
56
|
-
this.
|
|
71
|
+
this.settleAll();
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
75
|
+
* 丢弃在途请求,但保留常驻 worker
|
|
76
|
+
*
|
|
77
|
+
* clearIdleTasks / reset 中断收集时调用。这里不能销毁 worker:worker 重建要重新加载并启动整份收集逻辑,
|
|
78
|
+
* 而 clearIdleTasks / reset 触发频繁(root 更新、数据源变更、历史回滚等),一旦下一次 abort 落在
|
|
79
|
+
* 上一个 worker 还没启动完的窗口内,就会陷入「加载中被销毁 → 重建 → 又被销毁」的循环,
|
|
80
|
+
* 表现为 worker 反复加载且依赖始终收集不完。
|
|
81
|
+
*
|
|
82
|
+
* 因此 abort 只做「不要这些结果」:尚未投递的请求直接丢掉,已投递的那一个标记为丢弃,
|
|
83
|
+
* 结果回来后忽略(不写回主线程),worker 继续复用。
|
|
62
84
|
*/
|
|
63
85
|
public abort() {
|
|
64
|
-
this
|
|
86
|
+
const { queue } = this;
|
|
87
|
+
this.queue = [];
|
|
88
|
+
|
|
89
|
+
for (const task of queue) {
|
|
90
|
+
task.resolve(null);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 已投递的请求无法从外部打断(worker 忙时收不到取消消息),只能丢弃其结果
|
|
94
|
+
if (this.inflight && !this.inflight.discarded) {
|
|
95
|
+
this.inflight.discarded = true;
|
|
96
|
+
this.inflight.resolve(null);
|
|
97
|
+
}
|
|
65
98
|
}
|
|
66
99
|
|
|
67
|
-
/**
|
|
68
|
-
* worker 按收到的顺序逐个处理请求,因此请求间不会互相打断,用 id 把结果分发回各自的调用方
|
|
69
|
-
*/
|
|
70
100
|
private request(createRequest: (id: number) => CollectWorkerRequest): Promise<CollectWorkerResponse | null> {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!worker) {
|
|
101
|
+
if (!this.isSupported) {
|
|
74
102
|
return Promise.resolve(null);
|
|
75
103
|
}
|
|
76
104
|
|
|
@@ -78,19 +106,44 @@ export class CollectWorkerClient {
|
|
|
78
106
|
const id = this.seed;
|
|
79
107
|
|
|
80
108
|
return new Promise<CollectWorkerResponse | null>((resolve) => {
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
// 节点配置中可能存在函数等无法结构化克隆的值,需要先序列化再传递
|
|
85
|
-
worker.postMessage(createRequest(id));
|
|
86
|
-
} catch (e) {
|
|
87
|
-
error('magic editor: 依赖收集 worker 通信失败', e);
|
|
88
|
-
this.pending.delete(id);
|
|
89
|
-
resolve(null);
|
|
90
|
-
}
|
|
109
|
+
this.queue.push({ id, createRequest, resolve, discarded: false });
|
|
110
|
+
this.flush();
|
|
91
111
|
});
|
|
92
112
|
}
|
|
93
113
|
|
|
114
|
+
/**
|
|
115
|
+
* 投递队首请求,worker 串行处理,因此同一时刻只投递一个,用 id 把结果分发回各自的调用方
|
|
116
|
+
*/
|
|
117
|
+
private flush() {
|
|
118
|
+
if (this.inflight || !this.queue.length) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const worker = this.getWorker();
|
|
123
|
+
|
|
124
|
+
if (!worker) {
|
|
125
|
+
this.settleAll();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const task = this.queue.shift()!;
|
|
130
|
+
this.inflight = task;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
// 节点配置中可能存在函数等无法结构化克隆的值,需要先序列化再传递
|
|
134
|
+
worker.postMessage(task.createRequest(task.id));
|
|
135
|
+
} catch (e) {
|
|
136
|
+
error('magic editor: 依赖收集 worker 通信失败', e);
|
|
137
|
+
this.inflight = null;
|
|
138
|
+
|
|
139
|
+
if (!task.discarded) {
|
|
140
|
+
task.resolve(null);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.flush();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
94
147
|
private getWorker() {
|
|
95
148
|
if (!this.isSupported) {
|
|
96
149
|
return null;
|
|
@@ -122,28 +175,41 @@ export class CollectWorkerClient {
|
|
|
122
175
|
}
|
|
123
176
|
|
|
124
177
|
private handleResponse(response: CollectWorkerResponse) {
|
|
125
|
-
const
|
|
178
|
+
const task = this.inflight;
|
|
126
179
|
|
|
127
|
-
if (
|
|
180
|
+
if (task?.id !== response?.id) {
|
|
128
181
|
return;
|
|
129
182
|
}
|
|
130
183
|
|
|
131
|
-
this.
|
|
132
|
-
|
|
184
|
+
this.inflight = null;
|
|
185
|
+
|
|
186
|
+
// 被 abort 丢弃的请求已按 null 结算,结果不能再写回主线程
|
|
187
|
+
if (!task.discarded) {
|
|
188
|
+
task.resolve(response.failed ? null : response);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.flush();
|
|
133
192
|
}
|
|
134
193
|
|
|
135
194
|
private handleFatalError() {
|
|
136
195
|
this.worker?.terminate();
|
|
137
196
|
this.worker = null;
|
|
138
|
-
this.
|
|
197
|
+
this.settleAll();
|
|
139
198
|
}
|
|
140
199
|
|
|
141
|
-
private
|
|
142
|
-
const
|
|
143
|
-
this.
|
|
200
|
+
private settleAll() {
|
|
201
|
+
const tasks = this.queue;
|
|
202
|
+
this.queue = [];
|
|
144
203
|
|
|
145
|
-
|
|
146
|
-
|
|
204
|
+
if (this.inflight) {
|
|
205
|
+
tasks.push(this.inflight);
|
|
206
|
+
this.inflight = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const task of tasks) {
|
|
210
|
+
if (!task.discarded) {
|
|
211
|
+
task.resolve(null);
|
|
212
|
+
}
|
|
147
213
|
}
|
|
148
214
|
}
|
|
149
215
|
}
|