@sequent-org/moodboard 1.4.5 → 1.4.7

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 (29) hide show
  1. package/package.json +1 -1
  2. package/src/core/ApiClient.js +11 -3
  3. package/src/core/SaveManager.js +7 -0
  4. package/src/core/commands/CreateObjectCommand.js +1 -9
  5. package/src/core/commands/DeleteObjectCommand.js +19 -88
  6. package/src/core/commands/EditFileNameCommand.js +1 -2
  7. package/src/core/commands/GroupDeleteCommand.js +11 -44
  8. package/src/core/commands/GroupMoveCommand.js +1 -26
  9. package/src/core/commands/GroupReorderZCommand.js +1 -2
  10. package/src/core/commands/GroupResizeCommand.js +1 -4
  11. package/src/core/commands/GroupRotateCommand.js +1 -9
  12. package/src/core/commands/MindmapStatePatchCommand.js +1 -1
  13. package/src/core/commands/MoveObjectCommand.js +1 -3
  14. package/src/core/commands/PasteObjectCommand.js +1 -10
  15. package/src/core/commands/ReorderZCommand.js +1 -1
  16. package/src/core/commands/ResizeObjectCommand.js +1 -3
  17. package/src/core/commands/RotateObjectCommand.js +1 -9
  18. package/src/core/commands/UpdateContentCommand.js +1 -1
  19. package/src/core/commands/UpdateFramePropertiesCommand.js +1 -1
  20. package/src/core/commands/UpdateFrameTypeCommand.js +1 -1
  21. package/src/core/commands/UpdateNoteStyleCommand.js +1 -1
  22. package/src/core/commands/UpdateTextStyleCommand.js +1 -1
  23. package/src/core/flows/ClipboardFlow.js +20 -6
  24. package/src/core/flows/SaveFlow.js +0 -2
  25. package/src/core/index.js +0 -5
  26. package/src/moodboard/integration/MoodBoardLoadApi.js +4 -6
  27. package/src/services/AssetUrlPolicy.js +8 -0
  28. package/src/services/FileUploadService.js +16 -5
  29. package/src/services/ImageUploadService.js +16 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequent-org/moodboard",
3
- "version": "1.4.5",
3
+ "version": "1.4.7",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
@@ -1,3 +1,5 @@
1
+ import { isV2ImageDownloadUrl } from '../services/AssetUrlPolicy.js';
2
+
1
3
  // src/core/ApiClient.js
2
4
  export class ApiClient {
3
5
  constructor(baseUrl, authToken = null) {
@@ -159,6 +161,12 @@ export class ApiClient {
159
161
  if (hasForbiddenInlineSrc) {
160
162
  throw new Error(`Image object "${obj.id || 'unknown'}" contains forbidden data/blob src. Save is blocked.`);
161
163
  }
164
+ if (topSrc && !isV2ImageDownloadUrl(topSrc)) {
165
+ throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 src URL. Save is blocked.`);
166
+ }
167
+ if (propSrc && !isV2ImageDownloadUrl(propSrc)) {
168
+ throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 properties.src URL. Save is blocked.`);
169
+ }
162
170
 
163
171
  const cleanedObj = { ...obj };
164
172
 
@@ -221,7 +229,7 @@ export class ApiClient {
221
229
  if (obj.imageId && (!obj.src && !obj.properties?.src)) {
222
230
  try {
223
231
  // Формируем URL изображения
224
- const imageUrl = `/api/images/${obj.imageId}/file`;
232
+ const imageUrl = `/api/v2/images/${obj.imageId}/download`;
225
233
 
226
234
  return {
227
235
  ...obj,
@@ -243,7 +251,7 @@ export class ApiClient {
243
251
  if (obj.fileId) {
244
252
  try {
245
253
  // Формируем URL файла для скачивания
246
- const fileUrl = `/api/files/${obj.fileId}/download`;
254
+ const fileUrl = `/api/v2/files/${obj.fileId}/download`;
247
255
 
248
256
  // Создаем обновленный объект с восстановленными данными
249
257
  const restoredObj = {
@@ -259,7 +267,7 @@ export class ApiClient {
259
267
  // (Это будет выполнено асинхронно, чтобы не блокировать загрузку)
260
268
  setTimeout(async () => {
261
269
  try {
262
- const response = await fetch(`/api/files/${obj.fileId}`, {
270
+ const response = await fetch(`/api/v2/files/${obj.fileId}`, {
263
271
  headers: {
264
272
  'Accept': 'application/json',
265
273
  'X-Requested-With': 'XMLHttpRequest'
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { Events } from './events/Events.js';
5
5
  import { logMindmapCompoundDebug } from '../mindmap/MindmapCompoundContract.js';
6
+ import { isV2ImageDownloadUrl } from '../services/AssetUrlPolicy.js';
6
7
  export class SaveManager {
7
8
  constructor(eventBus, options = {}) {
8
9
  this.eventBus = eventBus;
@@ -245,6 +246,12 @@ export class SaveManager {
245
246
  if (/^data:/i.test(topSrc) || /^blob:/i.test(topSrc) || /^data:/i.test(propSrc) || /^blob:/i.test(propSrc)) {
246
247
  throw new Error(`Image object "${obj.id || 'unknown'}" contains forbidden data/blob src. Save is blocked.`);
247
248
  }
249
+ if (topSrc && !isV2ImageDownloadUrl(topSrc)) {
250
+ throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 src URL. Save is blocked.`);
251
+ }
252
+ if (propSrc && !isV2ImageDownloadUrl(propSrc)) {
253
+ throw new Error(`Image object "${obj.id || 'unknown'}" has non-v2 properties.src URL. Save is blocked.`);
254
+ }
248
255
  }
249
256
  }
250
257
 
@@ -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
  }
@@ -47,32 +47,7 @@ export class GroupMoveCommand extends BaseCommand {
47
47
  }
48
48
 
49
49
  undo() {
50
- // Возвращаем исходные позиции
51
- for (const item of this.moves) {
52
- if (this.coordinatesAreTopLeft) {
53
- // Координаты уже левый-верх (Frame перемещение)
54
- this.core.updateObjectPositionDirect(item.id, item.from);
55
- this.emit(Events.Object.TransformUpdated, {
56
- objectId: item.id,
57
- type: 'position',
58
- position: item.from
59
- });
60
- } else {
61
- // Координаты - центры PIXI (обычное групповое перемещение)
62
- const pixiObject = this.core?.pixi?.objects?.get(item.id);
63
- if (pixiObject) {
64
- const halfW = (pixiObject.width || 0) / 2;
65
- const halfH = (pixiObject.height || 0) / 2;
66
- const topLeft = { x: item.from.x - halfW, y: item.from.y - halfH };
67
- this.core.updateObjectPositionDirect(item.id, topLeft);
68
- this.emit(Events.Object.TransformUpdated, {
69
- objectId: item.id,
70
- type: 'position',
71
- position: topLeft
72
- });
73
- }
74
- }
75
- }
50
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
76
51
  }
77
52
 
78
53
  getDescription() {
@@ -22,8 +22,7 @@ export class GroupReorderZCommand extends BaseCommand {
22
22
  }
23
23
 
24
24
  undo() {
25
- if (!this.beforeOrder) return;
26
- this.applyOrder(this.beforeOrder);
25
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
27
26
  }
28
27
 
29
28
  applyOrder(idOrder) {
@@ -23,10 +23,7 @@ export class GroupResizeCommand extends BaseCommand {
23
23
  }
24
24
 
25
25
  undo() {
26
- for (const c of this.changes) {
27
- this.core.updateObjectSizeAndPositionDirect(c.id, c.fromSize, c.fromPos, c.type || null);
28
- this.emit(Events.Object.TransformUpdated, { objectId: c.id, type: 'resize', size: c.fromSize, position: c.fromPos });
29
- }
26
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
30
27
  }
31
28
 
32
29
  getDescription() {
@@ -28,15 +28,7 @@ export class GroupRotateCommand extends BaseCommand {
28
28
  }
29
29
 
30
30
  undo() {
31
- for (const c of this.changes) {
32
- if (this.core.pixi?.updateObjectRotation) {
33
- this.core.pixi.updateObjectRotation(c.id, c.fromAngle);
34
- }
35
- this.core.updateObjectRotationDirect(c.id, c.fromAngle);
36
- this.core.updateObjectPositionDirect(c.id, c.fromPos);
37
- this.emit(Events.Object.TransformUpdated, { objectId: c.id, type: 'rotation', angle: c.fromAngle });
38
- this.emit(Events.Object.TransformUpdated, { objectId: c.id, type: 'position', position: c.fromPos });
39
- }
31
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
40
32
  }
41
33
 
42
34
  getDescription() {
@@ -29,7 +29,7 @@ export class MindmapStatePatchCommand extends BaseCommand {
29
29
  }
30
30
 
31
31
  undo() {
32
- this._applyEntries(this.beforeEntries);
32
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
33
33
  }
34
34
 
35
35
  _applyEntries(entries) {
@@ -23,9 +23,7 @@ export class MoveObjectCommand extends BaseCommand {
23
23
  }
24
24
 
25
25
  undo() {
26
- // Возвращаем старую позицию
27
- this._setPosition(this.oldPosition);
28
-
26
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
29
27
  }
30
28
 
31
29
  _setPosition(position) {
@@ -96,16 +96,7 @@ export class PasteObjectCommand extends BaseCommand {
96
96
  }
97
97
 
98
98
  undo() {
99
- if (this.newObjectId) {
100
- // Удаляем созданный объект
101
- this.coreMoodboard.state.removeObject(this.newObjectId);
102
- this.coreMoodboard.pixi.removeObject(this.newObjectId);
103
-
104
- // Соответствующего константного события нет — остаёмся без эмита или используем Object.Deleted, если надо глобально
105
- this.emit(Events.Object.Deleted, {
106
- objectId: this.newObjectId
107
- });
108
- }
99
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
109
100
  }
110
101
 
111
102
  getDescription() {
@@ -18,7 +18,7 @@ export class ReorderZCommand extends BaseCommand {
18
18
  }
19
19
 
20
20
  undo() {
21
- this.apply(this.toIndex, this.fromIndex);
21
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
22
22
  }
23
23
 
24
24
  apply(from, to) {
@@ -25,9 +25,7 @@ export class ResizeObjectCommand extends BaseCommand {
25
25
  }
26
26
 
27
27
  undo() {
28
- // Возвращаем старый размер и позицию
29
- this._setSizeAndPosition(this.oldSize, this.oldPosition);
30
- this._updateResizeHandles();
28
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
31
29
  }
32
30
 
33
31
  _setSizeAndPosition(size, position = null) {
@@ -25,15 +25,7 @@ export class RotateObjectCommand extends BaseCommand {
25
25
  }
26
26
 
27
27
  undo() {
28
- // Обновляем угол поворота в состоянии
29
- this._setRotation(this.oldAngle);
30
-
31
- // Возвращаем старый угол поворота
32
- this.emit(Events.Object.Rotate, {
33
- objectId: this.objectId,
34
- angle: this.oldAngle
35
- });
36
- console.log(`↩️ Отменяем поворот объекта ${this.objectId}, возвращаем ${this.oldAngle}°`);
28
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
37
29
  }
38
30
 
39
31
  /**
@@ -22,7 +22,7 @@ export class UpdateContentCommand extends BaseCommand {
22
22
  }
23
23
 
24
24
  undo() {
25
- this._applyContent(this.oldContent, this.oldSize, this.oldPosition);
25
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
26
26
  }
27
27
 
28
28
  canMergeWith(otherCommand) {
@@ -34,7 +34,7 @@ export class UpdateFramePropertiesCommand extends BaseCommand {
34
34
  }
35
35
 
36
36
  undo() {
37
- this._apply(this.oldValue);
37
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
38
38
  }
39
39
 
40
40
  canMergeWith(otherCommand) {
@@ -32,7 +32,7 @@ export class UpdateFrameTypeCommand extends BaseCommand {
32
32
  }
33
33
 
34
34
  undo() {
35
- this._apply(this.oldType, this.oldSize, this.oldPosition);
35
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
36
36
  }
37
37
 
38
38
  _apply(typeValue, size, position) {
@@ -29,7 +29,7 @@ export class UpdateNoteStyleCommand extends BaseCommand {
29
29
  }
30
30
 
31
31
  undo() {
32
- this._apply(this.oldValue);
32
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
33
33
  }
34
34
 
35
35
  canMergeWith(otherCommand) {
@@ -28,7 +28,7 @@ export class UpdateTextStyleCommand extends BaseCommand {
28
28
  }
29
29
 
30
30
  undo() {
31
- this._apply(this.oldValue);
31
+ // Локальный undo отключен: история состояния загружается с сервера по версиям.
32
32
  }
33
33
 
34
34
  canMergeWith(otherCommand) {
@@ -1,6 +1,7 @@
1
1
  import { Events } from '../events/Events.js';
2
2
  import { PasteObjectCommand } from '../commands/index.js';
3
3
  import { RevitScreenshotMetadataService } from '../../services/RevitScreenshotMetadataService.js';
4
+ import { isV2ImageDownloadUrl } from '../../services/AssetUrlPolicy.js';
4
5
 
5
6
  export function setupClipboardFlow(core) {
6
7
  const revitMetadataService = new RevitScreenshotMetadataService(console);
@@ -21,7 +22,12 @@ export function setupClipboardFlow(core) {
21
22
 
22
23
  const ensureServerImage = async ({ src, name, imageId }) => {
23
24
  if (imageId) {
24
- return { src, name, imageId };
25
+ const serverUrl = typeof src === 'string' ? src.trim() : '';
26
+ if (!isV2ImageDownloadUrl(serverUrl)) {
27
+ alert('Некорректный адрес изображения. Изображение не добавлено.');
28
+ return null;
29
+ }
30
+ return { src: serverUrl, name, imageId };
25
31
  }
26
32
  if (!core.imageUploadService) {
27
33
  alert('Сервис загрузки изображений недоступен. Изображение не добавлено.');
@@ -39,8 +45,12 @@ export function setupClipboardFlow(core) {
39
45
  const blob = await response.blob();
40
46
  uploadResult = await core.imageUploadService.uploadImage(blob, name || 'clipboard-image');
41
47
  }
48
+ const serverUrl = typeof uploadResult.url === 'string' ? uploadResult.url.trim() : '';
49
+ if (!isV2ImageDownloadUrl(serverUrl)) {
50
+ throw new Error('Сервер вернул некорректный URL изображения');
51
+ }
42
52
  return {
43
- src: uploadResult.url,
53
+ src: serverUrl,
44
54
  name: uploadResult.name || name,
45
55
  imageId: uploadResult.imageId || uploadResult.id
46
56
  };
@@ -272,10 +282,12 @@ export function setupClipboardFlow(core) {
272
282
  const img = new Image();
273
283
  img.decoding = 'async';
274
284
  img.onload = () => { void placeWithAspect(img.naturalWidth || 0, img.naturalHeight || 0); };
275
- img.onerror = () => { void placeWithAspect(0, 0); };
285
+ img.onerror = () => {
286
+ alert('Не удалось загрузить изображение с сервера. Изображение не добавлено.');
287
+ };
276
288
  img.src = uploaded.src;
277
289
  } catch (_) {
278
- void placeWithAspect(0, 0);
290
+ alert('Не удалось загрузить изображение с сервера. Изображение не добавлено.');
279
291
  }
280
292
  });
281
293
 
@@ -320,10 +332,12 @@ export function setupClipboardFlow(core) {
320
332
  const img = new Image();
321
333
  img.decoding = 'async';
322
334
  img.onload = () => { void placeWithAspect(img.naturalWidth || 0, img.naturalHeight || 0); };
323
- img.onerror = () => { void placeWithAspect(0, 0); };
335
+ img.onerror = () => {
336
+ alert('Не удалось загрузить изображение с сервера. Изображение не добавлено.');
337
+ };
324
338
  img.src = uploaded.src;
325
339
  } catch (_) {
326
- void placeWithAspect(0, 0);
340
+ alert('Не удалось загрузить изображение с сервера. Изображение не добавлено.');
327
341
  }
328
342
  });
329
343
 
@@ -25,8 +25,6 @@ export function setupSaveFlow(core) {
25
25
  core.eventBus.on(Events.Save.Success, async () => {
26
26
  if (typeof core.revealPendingObjectsAfterSave === 'function') {
27
27
  core.revealPendingObjectsAfterSave();
28
- } else if (typeof core.revealPendingImageObjectsAfterSave === 'function') {
29
- core.revealPendingImageObjectsAfterSave();
30
28
  }
31
29
  // ВРЕМЕННО ОТКЛЮЧЕНО:
32
30
  // cleanup-фича требует доработки контракта и серверной поддержки.
package/src/core/index.js CHANGED
@@ -465,11 +465,6 @@ export class CoreMoodBoard {
465
465
  this._pendingPersistAckVisibilityIds.clear();
466
466
  }
467
467
 
468
- // Backward-compat alias for tests/integrations created in previous step.
469
- revealPendingImageObjectsAfterSave() {
470
- this.revealPendingObjectsAfterSave();
471
- }
472
-
473
468
  // === Прикрепления к фреймам ===
474
469
  // Логика фреймов перенесена в FrameService
475
470
 
@@ -21,12 +21,10 @@ function resolveMoodboardApiBase(board) {
21
21
  const raw = String(board?.options?.apiUrl || '').trim();
22
22
  if (!raw) return '/api/v2/moodboard';
23
23
 
24
- // Совместимость с legacy конфигом: /api/moodboard -> /api/v2/moodboard
25
- if (raw.endsWith('/api/moodboard')) {
26
- return raw.replace(/\/api\/moodboard$/, '/api/v2/moodboard');
27
- }
28
- if (raw.endsWith('/api/moodboard/')) {
29
- return raw.replace(/\/api\/moodboard\/$/, '/api/v2/moodboard/');
24
+ const hasLegacyPath = /\/api\/moodboard(?:\/|$)/i.test(raw);
25
+ const hasV2Path = /\/api\/v2\/moodboard(?:\/|$)/i.test(raw);
26
+ if (hasLegacyPath && !hasV2Path) {
27
+ throw new Error('Legacy apiUrl "/api/moodboard" is not supported. Use "/api/v2/moodboard".');
30
28
  }
31
29
 
32
30
  return raw;
@@ -0,0 +1,8 @@
1
+ export function isV2ImageDownloadUrl(url) {
2
+ return typeof url === 'string' && /^\/api\/v2\/images\/[^/]+\/download$/i.test(url.trim());
3
+ }
4
+
5
+ export function isV2FileDownloadUrl(url) {
6
+ return typeof url === 'string' && /^\/api\/v2\/files\/[^/]+\/download$/i.test(url.trim());
7
+ }
8
+
@@ -1,11 +1,13 @@
1
+ import { isV2FileDownloadUrl } from './AssetUrlPolicy.js';
2
+
1
3
  /**
2
4
  * Сервис для загрузки и управления файлами на сервере
3
5
  */
4
6
  export class FileUploadService {
5
7
  constructor(apiClient, options = {}) {
6
8
  this.apiClient = apiClient;
7
- this.uploadEndpoint = '/api/files/upload';
8
- this.deleteEndpoint = '/api/files';
9
+ this.uploadEndpoint = '/api/v2/files/upload';
10
+ this.deleteEndpoint = '/api/v2/files';
9
11
  this.options = {
10
12
  csrfToken: null, // Можно передать токен напрямую
11
13
  csrfTokenSelector: 'meta[name="csrf-token"]', // Селектор для поиска токена в DOM
@@ -92,10 +94,19 @@ export class FileUploadService {
92
94
  throw new Error(result.message || 'Ошибка загрузки файла');
93
95
  }
94
96
 
97
+ const fileId = result.data.fileId || result.data.id;
98
+ const serverUrl = typeof result.data.url === 'string' ? result.data.url.trim() : '';
99
+ if (!fileId) {
100
+ throw new Error('Сервер не вернул fileId.');
101
+ }
102
+ if (!isV2FileDownloadUrl(serverUrl)) {
103
+ throw new Error('Некорректный URL файла от сервера. Ожидается /api/v2/files/{fileId}/download');
104
+ }
105
+
95
106
  return {
96
- id: result.data.fileId || result.data.id, // Используем fileId как основное поле, id для обратной совместимости
97
- fileId: result.data.fileId || result.data.id, // Добавляем fileId для явного доступа
98
- url: result.data.url,
107
+ id: fileId, // Используем fileId как основное поле, id для обратной совместимости
108
+ fileId, // Добавляем fileId для явного доступа
109
+ url: serverUrl,
99
110
  size: result.data.size,
100
111
  name: result.data.name,
101
112
  type: result.data.type
@@ -1,11 +1,13 @@
1
+ import { isV2ImageDownloadUrl } from './AssetUrlPolicy.js';
2
+
1
3
  /**
2
4
  * Сервис для загрузки и управления изображениями на сервере
3
5
  */
4
6
  export class ImageUploadService {
5
7
  constructor(apiClient, options = {}) {
6
8
  this.apiClient = apiClient;
7
- this.uploadEndpoint = '/api/images/upload';
8
- this.deleteEndpoint = '/api/images';
9
+ this.uploadEndpoint = '/api/v2/images/upload';
10
+ this.deleteEndpoint = '/api/v2/images';
9
11
  this.options = {
10
12
  csrfToken: null, // Можно передать токен напрямую
11
13
  csrfTokenSelector: 'meta[name="csrf-token"]', // Селектор для поиска токена в DOM
@@ -97,10 +99,19 @@ export class ImageUploadService {
97
99
  throw new Error(result.message || 'Ошибка загрузки изображения');
98
100
  }
99
101
 
102
+ const imageId = result.data.imageId || result.data.id;
103
+ const serverUrl = typeof result.data.url === 'string' ? result.data.url.trim() : '';
104
+ if (!imageId) {
105
+ throw new Error('Сервер не вернул imageId.');
106
+ }
107
+ if (!isV2ImageDownloadUrl(serverUrl)) {
108
+ throw new Error('Некорректный URL изображения от сервера. Ожидается /api/v2/images/{imageId}/download');
109
+ }
110
+
100
111
  return {
101
- id: result.data.imageId || result.data.id, // Используем imageId как основное поле, id для обратной совместимости
102
- imageId: result.data.imageId || result.data.id, // Добавляем imageId для явного доступа
103
- url: result.data.url,
112
+ id: imageId, // Используем imageId как основное поле, id для обратной совместимости
113
+ imageId, // Добавляем imageId для явного доступа
114
+ url: serverUrl,
104
115
  width: result.data.width,
105
116
  height: result.data.height,
106
117
  name: result.data.name,