@sequent-org/moodboard 1.4.4 → 1.4.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/package.json +1 -1
- package/src/core/ApiClient.js +30 -86
- package/src/core/HistoryManager.js +0 -102
- package/src/core/SaveManager.js +27 -0
- package/src/core/commands/CreateObjectCommand.js +1 -9
- package/src/core/commands/DeleteObjectCommand.js +19 -88
- package/src/core/commands/EditFileNameCommand.js +1 -2
- package/src/core/commands/GroupDeleteCommand.js +11 -44
- package/src/core/commands/GroupMoveCommand.js +1 -26
- package/src/core/commands/GroupReorderZCommand.js +1 -2
- package/src/core/commands/GroupResizeCommand.js +1 -4
- package/src/core/commands/GroupRotateCommand.js +1 -9
- package/src/core/commands/MindmapStatePatchCommand.js +1 -1
- package/src/core/commands/MoveObjectCommand.js +1 -3
- package/src/core/commands/PasteObjectCommand.js +1 -10
- package/src/core/commands/ReorderZCommand.js +1 -1
- package/src/core/commands/ResizeObjectCommand.js +1 -3
- package/src/core/commands/RotateObjectCommand.js +1 -9
- package/src/core/commands/UpdateContentCommand.js +1 -1
- package/src/core/commands/UpdateFramePropertiesCommand.js +1 -1
- package/src/core/commands/UpdateFrameTypeCommand.js +1 -1
- package/src/core/commands/UpdateNoteStyleCommand.js +1 -1
- package/src/core/commands/UpdateTextStyleCommand.js +1 -1
- package/src/core/events/Events.js +0 -2
- package/src/core/flows/ClipboardFlow.js +50 -14
- package/src/core/flows/SaveFlow.js +5 -0
- package/src/core/index.js +32 -0
- package/src/core/keyboard/KeyboardClipboardImagePaste.js +6 -30
- package/src/services/FileUploadService.js +2 -2
- package/src/services/ImageUploadService.js +2 -2
- package/src/tools/manager/ToolEventRouter.js +34 -74
- package/src/tools/object-tools/PlacementTool.js +5 -29
- package/src/tools/object-tools/placement/PlacementInputRouter.js +1 -6
- package/src/tools/object-tools/placement/PlacementPayloadFactory.js +0 -29
package/package.json
CHANGED
package/src/core/ApiClient.js
CHANGED
|
@@ -144,90 +144,55 @@ export class ApiClient {
|
|
|
144
144
|
|
|
145
145
|
const cleanedObjects = boardData.objects.map(obj => {
|
|
146
146
|
if (obj.type === 'image') {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
147
|
+
const imageId = typeof obj.imageId === 'string' ? obj.imageId.trim() : '';
|
|
148
|
+
const topSrc = typeof obj.src === 'string' ? obj.src : '';
|
|
149
|
+
const propSrc = typeof obj.properties?.src === 'string' ? obj.properties.src : '';
|
|
150
|
+
const hasForbiddenInlineSrc = /^data:/i.test(topSrc)
|
|
151
|
+
|| /^blob:/i.test(topSrc)
|
|
152
|
+
|| /^data:/i.test(propSrc)
|
|
153
|
+
|| /^blob:/i.test(propSrc);
|
|
154
|
+
|
|
155
|
+
// Жесткий контракт v2: сохраняем image только через server imageId.
|
|
156
|
+
if (!imageId) {
|
|
157
|
+
throw new Error(`Image object "${obj.id || 'unknown'}" has no imageId. Save is blocked.`);
|
|
158
|
+
}
|
|
159
|
+
if (hasForbiddenInlineSrc) {
|
|
160
|
+
throw new Error(`Image object "${obj.id || 'unknown'}" contains forbidden data/blob src. Save is blocked.`);
|
|
161
|
+
}
|
|
162
|
+
|
|
156
163
|
const cleanedObj = { ...obj };
|
|
157
|
-
|
|
158
|
-
//
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// Убираем src с верхнего уровня
|
|
163
|
-
if (cleanedObj.src) {
|
|
164
|
-
delete cleanedObj.src;
|
|
165
|
-
console.log('🧹 DEBUG: удален src с верхнего уровня');
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Убираем src из properties
|
|
169
|
-
if (cleanedObj.properties?.src) {
|
|
170
|
-
cleanedObj.properties = { ...cleanedObj.properties };
|
|
171
|
-
delete cleanedObj.properties.src;
|
|
172
|
-
console.log('🧹 DEBUG: удален src из properties');
|
|
173
|
-
}
|
|
164
|
+
|
|
165
|
+
// imageId валиден — src можно безопасно убрать из history payload.
|
|
166
|
+
if (cleanedObj.src) {
|
|
167
|
+
delete cleanedObj.src;
|
|
174
168
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (cleanedObj.properties?.src && cleanedObj.properties.src.startsWith('data:')) {
|
|
179
|
-
console.warn('❌ Изображение сохраняется с base64 в properties, так как нет imageId:', cleanedObj.id);
|
|
180
|
-
}
|
|
181
|
-
if (cleanedObj.src && cleanedObj.src.startsWith('data:')) {
|
|
182
|
-
console.warn('❌ Изображение сохраняется с base64 в src, так как нет imageId:', cleanedObj.id);
|
|
183
|
-
}
|
|
184
|
-
if (!obj.imageId) {
|
|
185
|
-
console.warn('❌ У изображения отсутствует imageId:', cleanedObj.id);
|
|
186
|
-
}
|
|
169
|
+
if (cleanedObj.properties?.src) {
|
|
170
|
+
cleanedObj.properties = { ...cleanedObj.properties };
|
|
171
|
+
delete cleanedObj.properties.src;
|
|
187
172
|
}
|
|
188
|
-
|
|
173
|
+
|
|
189
174
|
return cleanedObj;
|
|
190
175
|
}
|
|
191
176
|
|
|
192
177
|
if (obj.type === 'file') {
|
|
193
|
-
console.log('🧹 DEBUG _cleanObjectData: обрабатываем файл:', {
|
|
194
|
-
id: obj.id,
|
|
195
|
-
fileId: obj.fileId,
|
|
196
|
-
hasContent: !!obj.content,
|
|
197
|
-
hasPropertiesContent: !!obj.properties?.content
|
|
198
|
-
});
|
|
199
|
-
|
|
200
178
|
const cleanedObj = { ...obj };
|
|
201
179
|
|
|
202
180
|
// Если есть fileId, убираем content для экономии места
|
|
203
181
|
if (obj.fileId && typeof obj.fileId === 'string' && obj.fileId.trim().length > 0) {
|
|
204
|
-
console.log('🧹 DEBUG _cleanObjectData: у файла есть fileId, убираем content');
|
|
205
|
-
|
|
206
182
|
// Убираем content с верхнего уровня
|
|
207
183
|
if (cleanedObj.content) {
|
|
208
184
|
delete cleanedObj.content;
|
|
209
|
-
console.log('🧹 DEBUG: удален content с верхнего уровня');
|
|
210
185
|
}
|
|
211
186
|
|
|
212
187
|
// Убираем content из properties
|
|
213
188
|
if (cleanedObj.properties?.content) {
|
|
214
189
|
cleanedObj.properties = { ...cleanedObj.properties };
|
|
215
190
|
delete cleanedObj.properties.content;
|
|
216
|
-
console.log('🧹 DEBUG: удален content из properties');
|
|
217
191
|
}
|
|
218
192
|
}
|
|
219
193
|
// Если нет fileId, предупреждаем о наличии content
|
|
220
194
|
else {
|
|
221
|
-
|
|
222
|
-
if (cleanedObj.properties?.content) {
|
|
223
|
-
console.warn('❌ Файл сохраняется с content в properties, так как нет fileId:', cleanedObj.id);
|
|
224
|
-
}
|
|
225
|
-
if (cleanedObj.content) {
|
|
226
|
-
console.warn('❌ Файл сохраняется с content, так как нет fileId:', cleanedObj.id);
|
|
227
|
-
}
|
|
228
|
-
if (!obj.fileId) {
|
|
229
|
-
console.warn('❌ У файла отсутствует fileId:', cleanedObj.id);
|
|
230
|
-
}
|
|
195
|
+
// Для файлов сейчас сохраняем поведение: без fileId не модифицируем объект.
|
|
231
196
|
}
|
|
232
197
|
|
|
233
198
|
return cleanedObj;
|
|
@@ -253,18 +218,10 @@ export class ApiClient {
|
|
|
253
218
|
const restoredObjects = await Promise.all(
|
|
254
219
|
boardData.objects.map(async (obj) => {
|
|
255
220
|
if (obj.type === 'image') {
|
|
256
|
-
console.log('🔗 DEBUG restoreImageUrls: обрабатываем изображение:', {
|
|
257
|
-
id: obj.id,
|
|
258
|
-
imageId: obj.imageId,
|
|
259
|
-
hasSrc: !!obj.src,
|
|
260
|
-
hasPropertiesSrc: !!obj.properties?.src
|
|
261
|
-
});
|
|
262
|
-
|
|
263
221
|
if (obj.imageId && (!obj.src && !obj.properties?.src)) {
|
|
264
|
-
console.log('🔗 DEBUG: восстанавливаем URL для изображения');
|
|
265
222
|
try {
|
|
266
223
|
// Формируем URL изображения
|
|
267
|
-
const imageUrl = `/api/images/${obj.imageId}/
|
|
224
|
+
const imageUrl = `/api/v2/images/${obj.imageId}/download`;
|
|
268
225
|
|
|
269
226
|
return {
|
|
270
227
|
...obj,
|
|
@@ -278,25 +235,15 @@ export class ApiClient {
|
|
|
278
235
|
console.warn(`Не удалось восстановить URL для изображения ${obj.imageId}:`, error);
|
|
279
236
|
return obj;
|
|
280
237
|
}
|
|
281
|
-
} else {
|
|
282
|
-
console.log('🔗 DEBUG: изображение уже имеет URL или нет imageId, оставляем как есть');
|
|
283
|
-
return obj;
|
|
284
238
|
}
|
|
239
|
+
return obj;
|
|
285
240
|
}
|
|
286
241
|
|
|
287
242
|
if (obj.type === 'file') {
|
|
288
|
-
console.log('🔗 DEBUG restoreObjectUrls: обрабатываем файл:', {
|
|
289
|
-
id: obj.id,
|
|
290
|
-
fileId: obj.fileId,
|
|
291
|
-
hasUrl: !!obj.url,
|
|
292
|
-
hasPropertiesUrl: !!obj.properties?.url
|
|
293
|
-
});
|
|
294
|
-
|
|
295
243
|
if (obj.fileId) {
|
|
296
|
-
console.log('🔗 DEBUG: восстанавливаем данные для файла');
|
|
297
244
|
try {
|
|
298
245
|
// Формируем URL файла для скачивания
|
|
299
|
-
const fileUrl = `/api/files/${obj.fileId}/download`;
|
|
246
|
+
const fileUrl = `/api/v2/files/${obj.fileId}/download`;
|
|
300
247
|
|
|
301
248
|
// Создаем обновленный объект с восстановленными данными
|
|
302
249
|
const restoredObj = {
|
|
@@ -312,7 +259,7 @@ export class ApiClient {
|
|
|
312
259
|
// (Это будет выполнено асинхронно, чтобы не блокировать загрузку)
|
|
313
260
|
setTimeout(async () => {
|
|
314
261
|
try {
|
|
315
|
-
const response = await fetch(`/api/files/${obj.fileId}`, {
|
|
262
|
+
const response = await fetch(`/api/v2/files/${obj.fileId}`, {
|
|
316
263
|
headers: {
|
|
317
264
|
'Accept': 'application/json',
|
|
318
265
|
'X-Requested-With': 'XMLHttpRequest'
|
|
@@ -323,7 +270,6 @@ export class ApiClient {
|
|
|
323
270
|
if (response.ok) {
|
|
324
271
|
const result = await response.json();
|
|
325
272
|
if (result.success && result.data) {
|
|
326
|
-
console.log('🔄 Обновляем метаданные файла с сервера:', result.data);
|
|
327
273
|
// Эмитим событие для обновления метаданных файла в состоянии
|
|
328
274
|
// (это будет обработано в core, если EventBus доступен)
|
|
329
275
|
if (typeof window !== 'undefined' && window.moodboardEventBus) {
|
|
@@ -345,10 +291,8 @@ export class ApiClient {
|
|
|
345
291
|
console.warn(`Не удалось восстановить данные для файла ${obj.fileId}:`, error);
|
|
346
292
|
return obj;
|
|
347
293
|
}
|
|
348
|
-
} else {
|
|
349
|
-
console.log('🔗 DEBUG: файл не имеет fileId, оставляем как есть');
|
|
350
|
-
return obj;
|
|
351
294
|
}
|
|
295
|
+
return obj;
|
|
352
296
|
}
|
|
353
297
|
|
|
354
298
|
return obj;
|
|
@@ -15,8 +15,6 @@ export class HistoryManager {
|
|
|
15
15
|
this.history = [];
|
|
16
16
|
// Текущая позиция в истории
|
|
17
17
|
this.currentIndex = -1;
|
|
18
|
-
// Флаг для предотвращения зацикливания при undo/redo
|
|
19
|
-
this.isExecutingCommand = false;
|
|
20
18
|
this._listenersAttached = false;
|
|
21
19
|
this._onDebug = () => this.debugHistory();
|
|
22
20
|
|
|
@@ -34,14 +32,6 @@ export class HistoryManager {
|
|
|
34
32
|
* Выполнить команду и добавить в историю
|
|
35
33
|
*/
|
|
36
34
|
executeCommand(command) {
|
|
37
|
-
if (this.isExecutingCommand) {
|
|
38
|
-
// Если мы в процессе undo/redo, не добавляем в историю
|
|
39
|
-
this._executeCommandSafely(command);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
35
|
// Проверяем, можно ли объединить с последней командой
|
|
46
36
|
const lastCommand = this.getLastCommand();
|
|
47
37
|
if (lastCommand &&
|
|
@@ -51,8 +41,6 @@ export class HistoryManager {
|
|
|
51
41
|
lastCommand.mergeWith(command);
|
|
52
42
|
this._executeCommandSafely(lastCommand);
|
|
53
43
|
this.eventBus.emit('history:changed', {
|
|
54
|
-
canUndo: this.canUndo(),
|
|
55
|
-
canRedo: this.canRedo(),
|
|
56
44
|
historySize: this.history.length
|
|
57
45
|
});
|
|
58
46
|
return;
|
|
@@ -78,8 +66,6 @@ export class HistoryManager {
|
|
|
78
66
|
|
|
79
67
|
// Уведомляем об изменении истории
|
|
80
68
|
this.eventBus.emit(Events.History.Changed, {
|
|
81
|
-
canUndo: this.canUndo(),
|
|
82
|
-
canRedo: this.canRedo(),
|
|
83
69
|
historySize: this.history.length,
|
|
84
70
|
currentCommand: command.toString()
|
|
85
71
|
});
|
|
@@ -105,89 +91,6 @@ export class HistoryManager {
|
|
|
105
91
|
}
|
|
106
92
|
}
|
|
107
93
|
|
|
108
|
-
/**
|
|
109
|
-
* Отменить последнюю команду
|
|
110
|
-
*/
|
|
111
|
-
undo() {
|
|
112
|
-
if (!this.canUndo()) {
|
|
113
|
-
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const command = this.history[this.currentIndex];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
this.isExecutingCommand = true;
|
|
121
|
-
try {
|
|
122
|
-
command.undo();
|
|
123
|
-
this.currentIndex--;
|
|
124
|
-
|
|
125
|
-
this.eventBus.emit(Events.History.Changed, {
|
|
126
|
-
canUndo: this.canUndo(),
|
|
127
|
-
canRedo: this.canRedo(),
|
|
128
|
-
historySize: this.history.length,
|
|
129
|
-
lastUndone: command.toString()
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return true;
|
|
134
|
-
} catch (error) {
|
|
135
|
-
console.error('❌ Ошибка при отмене команды:', error);
|
|
136
|
-
return false;
|
|
137
|
-
} finally {
|
|
138
|
-
this.isExecutingCommand = false;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Повторить отмененную команду
|
|
144
|
-
*/
|
|
145
|
-
redo() {
|
|
146
|
-
if (!this.canRedo()) {
|
|
147
|
-
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
this.currentIndex++;
|
|
152
|
-
const command = this.history[this.currentIndex];
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
this.isExecutingCommand = true;
|
|
156
|
-
try {
|
|
157
|
-
this._executeCommandSafely(command);
|
|
158
|
-
|
|
159
|
-
this.eventBus.emit(Events.History.Changed, {
|
|
160
|
-
canUndo: this.canUndo(),
|
|
161
|
-
canRedo: this.canRedo(),
|
|
162
|
-
historySize: this.history.length,
|
|
163
|
-
lastRedone: command.toString()
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return true;
|
|
168
|
-
} catch (error) {
|
|
169
|
-
console.error('❌ Ошибка при повторе команды:', error);
|
|
170
|
-
this.currentIndex--; // Откатываем индекс при ошибке
|
|
171
|
-
return false;
|
|
172
|
-
} finally {
|
|
173
|
-
this.isExecutingCommand = false;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Можно ли отменить команду
|
|
179
|
-
*/
|
|
180
|
-
canUndo() {
|
|
181
|
-
return this.currentIndex >= 0;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Можно ли повторить команду
|
|
186
|
-
*/
|
|
187
|
-
canRedo() {
|
|
188
|
-
return this.currentIndex < this.history.length - 1;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
94
|
/**
|
|
192
95
|
* Получить последнюю команду
|
|
193
96
|
*/
|
|
@@ -204,8 +107,6 @@ export class HistoryManager {
|
|
|
204
107
|
this.currentIndex = -1;
|
|
205
108
|
|
|
206
109
|
this.eventBus.emit(Events.History.Changed, {
|
|
207
|
-
canUndo: false,
|
|
208
|
-
canRedo: false,
|
|
209
110
|
historySize: 0
|
|
210
111
|
});
|
|
211
112
|
|
|
@@ -219,8 +120,6 @@ export class HistoryManager {
|
|
|
219
120
|
return {
|
|
220
121
|
totalCommands: this.history.length,
|
|
221
122
|
currentIndex: this.currentIndex,
|
|
222
|
-
canUndo: this.canUndo(),
|
|
223
|
-
canRedo: this.canRedo(),
|
|
224
123
|
commands: this.history.map((cmd, index) => ({
|
|
225
124
|
index,
|
|
226
125
|
isCurrent: index === this.currentIndex,
|
|
@@ -238,7 +137,6 @@ export class HistoryManager {
|
|
|
238
137
|
console.group('📚 История команд');
|
|
239
138
|
console.table(info.commands);
|
|
240
139
|
console.log(`Позиция: ${this.currentIndex + 1}/${this.history.length}`);
|
|
241
|
-
console.log(`Undo: ${this.canUndo()}, Redo: ${this.canRedo()}`);
|
|
242
140
|
console.groupEnd();
|
|
243
141
|
}
|
|
244
142
|
|
package/src/core/SaveManager.js
CHANGED
|
@@ -103,6 +103,11 @@ export class SaveManager {
|
|
|
103
103
|
sample: mindmapNodes.slice(0, 5),
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
|
+
|
|
107
|
+
// Жесткий контракт для сохранения картинок:
|
|
108
|
+
// - каждый image обязан иметь imageId
|
|
109
|
+
// - data:/blob: URL в image запрещены
|
|
110
|
+
this._assertImageSaveContract(saveData);
|
|
106
111
|
|
|
107
112
|
// Проверяем, изменились ли данные с последнего сохранения
|
|
108
113
|
if (this.lastSavedData && JSON.stringify(saveData) === JSON.stringify(this.lastSavedData)) {
|
|
@@ -221,6 +226,28 @@ export class SaveManager {
|
|
|
221
226
|
return await response.json();
|
|
222
227
|
}
|
|
223
228
|
|
|
229
|
+
_assertImageSaveContract(saveData) {
|
|
230
|
+
const objects = Array.isArray(saveData?.boardData?.objects)
|
|
231
|
+
? saveData.boardData.objects
|
|
232
|
+
: Array.isArray(saveData?.objects)
|
|
233
|
+
? saveData.objects
|
|
234
|
+
: [];
|
|
235
|
+
|
|
236
|
+
for (const obj of objects) {
|
|
237
|
+
if (!obj || obj.type !== 'image') continue;
|
|
238
|
+
const imageId = typeof obj.imageId === 'string' ? obj.imageId.trim() : '';
|
|
239
|
+
const topSrc = typeof obj.src === 'string' ? obj.src : '';
|
|
240
|
+
const propSrc = typeof obj.properties?.src === 'string' ? obj.properties.src : '';
|
|
241
|
+
|
|
242
|
+
if (!imageId) {
|
|
243
|
+
throw new Error(`Image object "${obj.id || 'unknown'}" has no imageId. Save is blocked.`);
|
|
244
|
+
}
|
|
245
|
+
if (/^data:/i.test(topSrc) || /^blob:/i.test(topSrc) || /^data:/i.test(propSrc) || /^blob:/i.test(propSrc)) {
|
|
246
|
+
throw new Error(`Image object "${obj.id || 'unknown'}" contains forbidden data/blob src. Save is blocked.`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
224
251
|
_buildSavePayload(boardId, data, csrfToken = undefined) {
|
|
225
252
|
return {
|
|
226
253
|
moodboardId: boardId,
|
|
@@ -33,14 +33,6 @@ export class CreateObjectCommand extends BaseCommand {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
undo() {
|
|
36
|
-
//
|
|
37
|
-
this.coreMoodboard.state.removeObject(this.objectData.id);
|
|
38
|
-
this.coreMoodboard.pixi.removeObject(this.objectData.id);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.coreMoodboard.eventBus.emit(Events.Object.Deleted, {
|
|
43
|
-
objectId: this.objectData.id
|
|
44
|
-
});
|
|
36
|
+
// Локальный undo отключен: история состояния загружается с сервера по версиям.
|
|
45
37
|
}
|
|
46
38
|
}
|
|
@@ -9,116 +9,47 @@ export class DeleteObjectCommand extends BaseCommand {
|
|
|
9
9
|
super('delete_object', `Удалить объект`);
|
|
10
10
|
this.coreMoodboard = coreMoodboard;
|
|
11
11
|
this.objectId = objectId;
|
|
12
|
-
|
|
13
|
-
// Сохраняем данные объекта для возможности восстановления
|
|
14
|
-
const objects = this.coreMoodboard.state.getObjects();
|
|
15
|
-
const originalData = objects.find(obj => obj.id === objectId);
|
|
16
|
-
|
|
17
|
-
if (!originalData) {
|
|
18
|
-
throw new Error(`Object with id ${objectId} not found`);
|
|
19
|
-
}
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Для изображений убедимся, что есть src URL для восстановления
|
|
25
|
-
if (this.objectData.type === 'image') {
|
|
26
|
-
|
|
27
|
-
if (this.objectData.imageId) {
|
|
28
|
-
const imageUrl = `/api/images/${this.objectData.imageId}/file`;
|
|
29
|
-
|
|
30
|
-
// Всегда восстанавливаем URL из imageId для гарантии
|
|
31
|
-
// (может быть удален при предыдущих сохранениях)
|
|
32
|
-
this.objectData.src = imageUrl;
|
|
33
|
-
|
|
34
|
-
if (!this.objectData.properties) {
|
|
35
|
-
this.objectData.properties = {};
|
|
36
|
-
}
|
|
37
|
-
this.objectData.properties.src = imageUrl;
|
|
38
|
-
|
|
39
|
-
} else {
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Для файлов сохраняем информацию для возможной очистки с сервера
|
|
44
|
-
if (this.objectData.type === 'file') {
|
|
45
|
-
|
|
46
|
-
if (this.objectData.fileId) {
|
|
47
|
-
// Сохраняем fileId для удаления с сервера
|
|
48
|
-
this.fileIdToDelete = this.objectData.fileId;
|
|
49
|
-
}
|
|
13
|
+
const currentObject = this._getCurrentObject();
|
|
14
|
+
if (!currentObject) {
|
|
15
|
+
throw new Error(`Object with id ${objectId} not found`);
|
|
50
16
|
}
|
|
51
|
-
|
|
52
|
-
// Обновляем описание с типом объекта
|
|
53
|
-
this.description = `Удалить ${this.objectData.type}`;
|
|
17
|
+
this.description = `Удалить ${currentObject.type}`;
|
|
54
18
|
}
|
|
55
19
|
|
|
56
20
|
async execute() {
|
|
57
21
|
console.log('🗑️ DeleteObjectCommand: начинаем удаление объекта:', this.objectId);
|
|
58
|
-
|
|
22
|
+
|
|
23
|
+
const currentObject = this._getCurrentObject();
|
|
24
|
+
const blobSrc = currentObject?.properties?.src || currentObject?.src;
|
|
25
|
+
|
|
59
26
|
// Удаляем объект из состояния и PIXI
|
|
60
27
|
this.coreMoodboard.state.removeObject(this.objectId);
|
|
61
28
|
this.coreMoodboard.pixi.removeObject(this.objectId);
|
|
62
|
-
|
|
29
|
+
|
|
63
30
|
console.log('🗑️ DeleteObjectCommand: объект удален из state и PIXI');
|
|
64
31
|
|
|
65
32
|
// Освобождаем blob URL у изображений (утечка памяти при fallback без upload)
|
|
66
|
-
const blobSrc = this.objectData?.properties?.src || this.objectData?.src;
|
|
67
33
|
if (typeof blobSrc === 'string' && blobSrc.startsWith('blob:')) {
|
|
68
34
|
try {
|
|
69
35
|
URL.revokeObjectURL(blobSrc);
|
|
70
36
|
} catch (_) {}
|
|
71
37
|
}
|
|
72
|
-
|
|
73
|
-
// Если это файловый объект с fileId, удаляем файл с сервера
|
|
74
|
-
if (this.fileIdToDelete && this.coreMoodboard.fileUploadService) {
|
|
75
|
-
try {
|
|
76
|
-
console.log('🗑️ Удаляем файл с сервера:', this.fileIdToDelete);
|
|
77
|
-
await this.coreMoodboard.fileUploadService.deleteFile(this.fileIdToDelete);
|
|
78
|
-
console.log('✅ Файл успешно удален с сервера:', this.fileIdToDelete);
|
|
79
|
-
} catch (error) {
|
|
80
|
-
console.warn('⚠️ Ошибка удаления файла с сервера:', error);
|
|
81
|
-
// Не останавливаем выполнение команды, так как объект уже удален из UI
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
38
|
+
|
|
85
39
|
// Эмитим событие удаления для обновления всех UI компонентов
|
|
86
|
-
this.coreMoodboard.eventBus.emit(Events.Object.Deleted, {
|
|
87
|
-
objectId: this.objectId
|
|
40
|
+
this.coreMoodboard.eventBus.emit(Events.Object.Deleted, {
|
|
41
|
+
objectId: this.objectId
|
|
88
42
|
});
|
|
89
|
-
|
|
43
|
+
|
|
90
44
|
console.log('✅ DeleteObjectCommand: событие Events.Object.Deleted отправлено');
|
|
91
45
|
}
|
|
92
46
|
|
|
93
47
|
undo() {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (restoredObjectData.properties) {
|
|
101
|
-
restoredObjectData.properties = {
|
|
102
|
-
...restoredObjectData.properties,
|
|
103
|
-
fileName: `[УДАЛЕН] ${restoredObjectData.properties.fileName || 'файл'}`,
|
|
104
|
-
isDeleted: true // Флаг для FileObject чтобы показать другую иконку
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Восстанавливаем объект с измененными данными
|
|
109
|
-
this.coreMoodboard.state.addObject(restoredObjectData);
|
|
110
|
-
this.coreMoodboard.pixi.createObject(restoredObjectData);
|
|
111
|
-
|
|
112
|
-
console.warn('⚠️ Файл восстановлен на холсте, но был удален с сервера');
|
|
113
|
-
} else {
|
|
114
|
-
// Восстанавливаем объект с сохраненными данными (для всех остальных типов)
|
|
115
|
-
this.coreMoodboard.state.addObject(this.objectData);
|
|
116
|
-
this.coreMoodboard.pixi.createObject(this.objectData);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
this.coreMoodboard.eventBus.emit(Events.Object.Created, {
|
|
120
|
-
objectId: this.objectId,
|
|
121
|
-
objectData: this.objectData
|
|
122
|
-
});
|
|
48
|
+
// Локальный undo-restore отключен: история состояния загружается с сервера по версиям.
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_getCurrentObject() {
|
|
52
|
+
const objects = this.coreMoodboard.state.getObjects();
|
|
53
|
+
return objects.find(obj => obj.id === this.objectId);
|
|
123
54
|
}
|
|
124
55
|
}
|
|
@@ -10,66 +10,33 @@ export class GroupDeleteCommand extends BaseCommand {
|
|
|
10
10
|
super('group_delete', `Удалить группу (${objectIds.length} объектов)`);
|
|
11
11
|
this.coreMoodboard = coreMoodboard;
|
|
12
12
|
this.objectIds = Array.isArray(objectIds) ? [...objectIds] : [];
|
|
13
|
-
|
|
14
|
-
const objects = this.coreMoodboard.state.getObjects();
|
|
15
|
-
this.objectsData = [];
|
|
16
|
-
for (const id of this.objectIds) {
|
|
17
|
-
const obj = objects.find((o) => o.id === id);
|
|
18
|
-
if (obj) {
|
|
19
|
-
const data = JSON.parse(JSON.stringify(obj));
|
|
20
|
-
if (data.type === 'image') {
|
|
21
|
-
if (data.imageId) {
|
|
22
|
-
const imageUrl = `/api/images/${data.imageId}/file`;
|
|
23
|
-
data.src = imageUrl;
|
|
24
|
-
if (!data.properties) data.properties = {};
|
|
25
|
-
data.properties.src = imageUrl;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
this.objectsData.push({ id, data });
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
13
|
}
|
|
32
14
|
|
|
33
15
|
async execute() {
|
|
34
|
-
for (const
|
|
16
|
+
for (const id of this.objectIds) {
|
|
17
|
+
const obj = this._getObjectById(id);
|
|
18
|
+
if (!obj) continue;
|
|
19
|
+
|
|
20
|
+
const blobSrc = obj?.properties?.src || obj?.src;
|
|
35
21
|
this.coreMoodboard.state.removeObject(id);
|
|
36
22
|
this.coreMoodboard.pixi.removeObject(id);
|
|
37
23
|
|
|
38
|
-
const blobSrc = data?.properties?.src || data?.src;
|
|
39
24
|
if (typeof blobSrc === 'string' && blobSrc.startsWith('blob:')) {
|
|
40
25
|
try {
|
|
41
26
|
URL.revokeObjectURL(blobSrc);
|
|
42
27
|
} catch (_) {}
|
|
43
28
|
}
|
|
44
29
|
|
|
45
|
-
if (data.type === 'file' && data.fileId && this.coreMoodboard.fileUploadService) {
|
|
46
|
-
try {
|
|
47
|
-
await this.coreMoodboard.fileUploadService.deleteFile(data.fileId);
|
|
48
|
-
} catch (_) {}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
30
|
this.coreMoodboard.eventBus.emit(Events.Object.Deleted, { objectId: id });
|
|
52
31
|
}
|
|
53
32
|
}
|
|
54
33
|
|
|
55
34
|
undo() {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
fileName: `[УДАЛЕН] ${restored.properties.fileName || 'файл'}`,
|
|
63
|
-
isDeleted: true,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
this.coreMoodboard.state.addObject(restored);
|
|
67
|
-
this.coreMoodboard.pixi.createObject(restored);
|
|
68
|
-
} else {
|
|
69
|
-
this.coreMoodboard.state.addObject(data);
|
|
70
|
-
this.coreMoodboard.pixi.createObject(data);
|
|
71
|
-
}
|
|
72
|
-
this.coreMoodboard.eventBus.emit(Events.Object.Created, { objectId: id, objectData: data });
|
|
73
|
-
}
|
|
35
|
+
// Локальный undo-restore отключен: история состояния загружается с сервера по версиям.
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_getObjectById(id) {
|
|
39
|
+
const objects = this.coreMoodboard.state.getObjects();
|
|
40
|
+
return objects.find((obj) => obj.id === id);
|
|
74
41
|
}
|
|
75
42
|
}
|