@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.
- package/package.json +1 -1
- package/src/core/ApiClient.js +11 -3
- package/src/core/SaveManager.js +7 -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/flows/ClipboardFlow.js +20 -6
- package/src/core/flows/SaveFlow.js +0 -2
- package/src/core/index.js +0 -5
- package/src/moodboard/integration/MoodBoardLoadApi.js +4 -6
- package/src/services/AssetUrlPolicy.js +8 -0
- package/src/services/FileUploadService.js +16 -5
- package/src/services/ImageUploadService.js +16 -5
package/package.json
CHANGED
package/src/core/ApiClient.js
CHANGED
|
@@ -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}/
|
|
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'
|
package/src/core/SaveManager.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|
|
@@ -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
|
-
|
|
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() {
|
|
@@ -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
|
-
|
|
25
|
+
// Локальный undo отключен: история состояния загружается с сервера по версиям.
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
canMergeWith(otherCommand) {
|
|
@@ -32,7 +32,7 @@ export class UpdateFrameTypeCommand extends BaseCommand {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
undo() {
|
|
35
|
-
|
|
35
|
+
// Локальный undo отключен: история состояния загружается с сервера по версиям.
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
_apply(typeValue, size, position) {
|
|
@@ -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
|
-
|
|
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:
|
|
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 = () => {
|
|
285
|
+
img.onerror = () => {
|
|
286
|
+
alert('Не удалось загрузить изображение с сервера. Изображение не добавлено.');
|
|
287
|
+
};
|
|
276
288
|
img.src = uploaded.src;
|
|
277
289
|
} catch (_) {
|
|
278
|
-
|
|
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 = () => {
|
|
335
|
+
img.onerror = () => {
|
|
336
|
+
alert('Не удалось загрузить изображение с сервера. Изображение не добавлено.');
|
|
337
|
+
};
|
|
324
338
|
img.src = uploaded.src;
|
|
325
339
|
} catch (_) {
|
|
326
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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:
|
|
97
|
-
fileId
|
|
98
|
-
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:
|
|
102
|
-
imageId
|
|
103
|
-
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,
|