@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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/core/ApiClient.js +30 -86
  3. package/src/core/HistoryManager.js +0 -102
  4. package/src/core/SaveManager.js +27 -0
  5. package/src/core/commands/CreateObjectCommand.js +1 -9
  6. package/src/core/commands/DeleteObjectCommand.js +19 -88
  7. package/src/core/commands/EditFileNameCommand.js +1 -2
  8. package/src/core/commands/GroupDeleteCommand.js +11 -44
  9. package/src/core/commands/GroupMoveCommand.js +1 -26
  10. package/src/core/commands/GroupReorderZCommand.js +1 -2
  11. package/src/core/commands/GroupResizeCommand.js +1 -4
  12. package/src/core/commands/GroupRotateCommand.js +1 -9
  13. package/src/core/commands/MindmapStatePatchCommand.js +1 -1
  14. package/src/core/commands/MoveObjectCommand.js +1 -3
  15. package/src/core/commands/PasteObjectCommand.js +1 -10
  16. package/src/core/commands/ReorderZCommand.js +1 -1
  17. package/src/core/commands/ResizeObjectCommand.js +1 -3
  18. package/src/core/commands/RotateObjectCommand.js +1 -9
  19. package/src/core/commands/UpdateContentCommand.js +1 -1
  20. package/src/core/commands/UpdateFramePropertiesCommand.js +1 -1
  21. package/src/core/commands/UpdateFrameTypeCommand.js +1 -1
  22. package/src/core/commands/UpdateNoteStyleCommand.js +1 -1
  23. package/src/core/commands/UpdateTextStyleCommand.js +1 -1
  24. package/src/core/events/Events.js +0 -2
  25. package/src/core/flows/ClipboardFlow.js +50 -14
  26. package/src/core/flows/SaveFlow.js +5 -0
  27. package/src/core/index.js +32 -0
  28. package/src/core/keyboard/KeyboardClipboardImagePaste.js +6 -30
  29. package/src/services/FileUploadService.js +2 -2
  30. package/src/services/ImageUploadService.js +2 -2
  31. package/src/tools/manager/ToolEventRouter.js +34 -74
  32. package/src/tools/object-tools/PlacementTool.js +5 -29
  33. package/src/tools/object-tools/placement/PlacementInputRouter.js +1 -6
  34. package/src/tools/object-tools/placement/PlacementPayloadFactory.js +0 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequent-org/moodboard",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
@@ -144,90 +144,55 @@ export class ApiClient {
144
144
 
145
145
  const cleanedObjects = boardData.objects.map(obj => {
146
146
  if (obj.type === 'image') {
147
- console.log('🧹 DEBUG _cleanImageData: обрабатываем изображение:', {
148
- id: obj.id,
149
- imageId: obj.imageId,
150
- hasSrc: !!obj.src,
151
- hasPropertiesSrc: !!obj.properties?.src,
152
- srcIsBase64: !!(obj.src && obj.src.startsWith('data:')),
153
- propertiesSrcIsBase64: !!(obj.properties?.src && obj.properties.src.startsWith('data:'))
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
- // Если есть imageId, убираем src для экономии места
159
- if (obj.imageId && typeof obj.imageId === 'string' && obj.imageId.trim().length > 0) {
160
- console.log('🧹 DEBUG _cleanImageData: у изображения есть imageId, убираем src');
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
- // Если нет imageId, предупреждаем о base64
176
- else {
177
- console.log('🧹 DEBUG _cleanImageData: у изображения НЕТ imageId, оставляем src как есть');
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
- console.log('🧹 DEBUG _cleanObjectData: у файла НЕТ fileId, оставляем content как есть');
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}/file`;
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
 
@@ -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
- // Удаляем объект из состояния и PIXI
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
- this.objectData = JSON.parse(JSON.stringify(originalData));
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
- if (this.objectData.type === 'file' && this.fileIdToDelete) {
97
-
98
- // Файл был удален с сервера, создаем объект с предупреждением
99
- const restoredObjectData = { ...this.objectData };
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
  }
@@ -22,8 +22,7 @@ export class EditFileNameCommand extends BaseCommand {
22
22
  }
23
23
 
24
24
  async undo() {
25
- // Возвращаем старое название
26
- await this._setFileName(this.oldName);
25
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
27
26
  }
28
27
 
29
28
  /**
@@ -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 { id, data } of this.objectsData) {
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
- for (const { id, data } of this.objectsData) {
57
- if (data.type === 'file' && data.fileId) {
58
- const restored = { ...data };
59
- if (restored.properties) {
60
- restored.properties = {
61
- ...restored.properties,
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
  }