@sequent-org/moodboard 1.4.11 → 1.4.12
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 +54 -71
- package/src/core/SaveManager.js +31 -0
- package/src/core/commands/EditFileNameCommand.js +0 -13
- package/src/core/index.js +16 -6
- package/src/moodboard/ActionHandler.js +2 -3
- package/src/moodboard/integration/MoodBoardLoadApi.js +11 -1
- package/src/services/FileUploadService.js +11 -340
- package/src/tools/manager/ToolEventRouter.js +4 -5
- package/src/tools/object-tools/placement/PlacementPayloadFactory.js +2 -3
- package/src/ui/FilePropertiesPanel.js +21 -15
package/package.json
CHANGED
package/src/core/ApiClient.js
CHANGED
|
@@ -70,14 +70,24 @@ export class ApiClient {
|
|
|
70
70
|
const imageObjectsWithoutSrc = imageObjects
|
|
71
71
|
.filter((obj) => !(typeof obj?.src === 'string' && obj.src.trim().length > 0))
|
|
72
72
|
.map((obj) => obj?.id || 'unknown');
|
|
73
|
+
const fileObjects = objects.filter((obj) => obj?.type === 'file');
|
|
74
|
+
const fileObjectsWithSrc = fileObjects.filter((obj) => typeof obj?.src === 'string' && obj.src.trim().length > 0);
|
|
75
|
+
const fileObjectsWithoutSrc = fileObjects
|
|
76
|
+
.filter((obj) => !(typeof obj?.src === 'string' && obj.src.trim().length > 0))
|
|
77
|
+
.map((obj) => obj?.id || 'unknown');
|
|
73
78
|
console.log('history/save payload stats:', {
|
|
74
79
|
totalObjects: objects.length,
|
|
75
80
|
imageObjects: imageObjects.length,
|
|
76
|
-
imageObjectsWithSrc: imageObjectsWithSrc.length
|
|
81
|
+
imageObjectsWithSrc: imageObjectsWithSrc.length,
|
|
82
|
+
fileObjects: fileObjects.length,
|
|
83
|
+
fileObjectsWithSrc: fileObjectsWithSrc.length
|
|
77
84
|
});
|
|
78
85
|
if (imageObjectsWithoutSrc.length > 0) {
|
|
79
86
|
console.warn('history/save warning: image objects without src (kept as broken placeholders):', imageObjectsWithoutSrc);
|
|
80
87
|
}
|
|
88
|
+
if (fileObjectsWithoutSrc.length > 0) {
|
|
89
|
+
console.warn('history/save warning: file objects without src:', fileObjectsWithoutSrc);
|
|
90
|
+
}
|
|
81
91
|
|
|
82
92
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
83
93
|
|
|
@@ -184,26 +194,35 @@ export class ApiClient {
|
|
|
184
194
|
}
|
|
185
195
|
|
|
186
196
|
if (obj.type === 'file') {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Убираем content из properties
|
|
197
|
-
if (cleanedObj.properties?.content) {
|
|
198
|
-
cleanedObj.properties = { ...cleanedObj.properties };
|
|
199
|
-
delete cleanedObj.properties.content;
|
|
200
|
-
}
|
|
197
|
+
const topSrcRaw = typeof obj.src === 'string' ? obj.src : '';
|
|
198
|
+
const propSrcRaw = typeof obj.properties?.src === 'string' ? obj.properties.src : '';
|
|
199
|
+
const topUrlRaw = typeof obj.url === 'string' ? obj.url : '';
|
|
200
|
+
const propUrlRaw = typeof obj.properties?.url === 'string' ? obj.properties.url : '';
|
|
201
|
+
const normalizedSrc = topSrcRaw.trim() || propSrcRaw.trim() || topUrlRaw.trim() || propUrlRaw.trim();
|
|
202
|
+
|
|
203
|
+
if (!normalizedSrc) {
|
|
204
|
+
throw new Error(`File object "${obj.id || 'unknown'}" has no src. Save is blocked.`);
|
|
201
205
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
if (/^data:/i.test(normalizedSrc) || /^blob:/i.test(normalizedSrc)) {
|
|
207
|
+
throw new Error(`File object "${obj.id || 'unknown'}" contains forbidden data/blob src. Save is blocked.`);
|
|
208
|
+
}
|
|
209
|
+
if (/^\/api\/v2\/files\//i.test(normalizedSrc)) {
|
|
210
|
+
throw new Error(`File object "${obj.id || 'unknown'}" has legacy id-based src URL. Save is blocked.`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const cleanedObj = {
|
|
214
|
+
...obj,
|
|
215
|
+
src: normalizedSrc
|
|
216
|
+
};
|
|
217
|
+
if (cleanedObj.url) delete cleanedObj.url;
|
|
218
|
+
if (cleanedObj.content) delete cleanedObj.content;
|
|
219
|
+
if (cleanedObj.fileId) delete cleanedObj.fileId;
|
|
220
|
+
if (cleanedObj.properties?.src || cleanedObj.properties?.url || cleanedObj.properties?.content) {
|
|
221
|
+
cleanedObj.properties = { ...cleanedObj.properties };
|
|
222
|
+
delete cleanedObj.properties.src;
|
|
223
|
+
delete cleanedObj.properties.url;
|
|
224
|
+
delete cleanedObj.properties.content;
|
|
205
225
|
}
|
|
206
|
-
|
|
207
226
|
return cleanedObj;
|
|
208
227
|
}
|
|
209
228
|
|
|
@@ -243,59 +262,23 @@ export class ApiClient {
|
|
|
243
262
|
}
|
|
244
263
|
|
|
245
264
|
if (obj.type === 'file') {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
url: fileUrl,
|
|
255
|
-
properties: {
|
|
256
|
-
...obj.properties,
|
|
257
|
-
url: fileUrl
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
// Пытаемся восстановить актуальные метаданные файла с сервера
|
|
262
|
-
// (Это будет выполнено асинхронно, чтобы не блокировать загрузку)
|
|
263
|
-
setTimeout(async () => {
|
|
264
|
-
try {
|
|
265
|
-
const response = await fetch(`/api/v2/files/${obj.fileId}`, {
|
|
266
|
-
headers: {
|
|
267
|
-
'Accept': 'application/json',
|
|
268
|
-
'X-Requested-With': 'XMLHttpRequest'
|
|
269
|
-
},
|
|
270
|
-
credentials: 'same-origin'
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
if (response.ok) {
|
|
274
|
-
const result = await response.json();
|
|
275
|
-
if (result.success && result.data) {
|
|
276
|
-
// Эмитим событие для обновления метаданных файла в состоянии
|
|
277
|
-
// (это будет обработано в core, если EventBus доступен)
|
|
278
|
-
if (typeof window !== 'undefined' && window.moodboardEventBus) {
|
|
279
|
-
window.moodboardEventBus.emit('file:metadata:updated', {
|
|
280
|
-
objectId: obj.id,
|
|
281
|
-
fileId: obj.fileId,
|
|
282
|
-
metadata: result.data
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
} catch (error) {
|
|
288
|
-
console.warn(`Не удалось обновить метаданные файла ${obj.fileId}:`, error);
|
|
289
|
-
}
|
|
290
|
-
}, 100);
|
|
291
|
-
|
|
292
|
-
return restoredObj;
|
|
293
|
-
} catch (error) {
|
|
294
|
-
console.warn(`Не удалось восстановить данные для файла ${obj.fileId}:`, error);
|
|
295
|
-
return obj;
|
|
296
|
-
}
|
|
265
|
+
const topSrc = typeof obj.src === 'string' ? obj.src.trim() : '';
|
|
266
|
+
const propSrc = typeof obj.properties?.src === 'string' ? obj.properties.src.trim() : '';
|
|
267
|
+
const topUrl = typeof obj.url === 'string' ? obj.url.trim() : '';
|
|
268
|
+
const propUrl = typeof obj.properties?.url === 'string' ? obj.properties.url.trim() : '';
|
|
269
|
+
const normalizedSrc = topSrc || propSrc || topUrl || propUrl;
|
|
270
|
+
const restoredObj = { ...obj };
|
|
271
|
+
if (normalizedSrc) {
|
|
272
|
+
restoredObj.src = normalizedSrc;
|
|
297
273
|
}
|
|
298
|
-
|
|
274
|
+
if (restoredObj.url) delete restoredObj.url;
|
|
275
|
+
if (restoredObj.fileId) delete restoredObj.fileId;
|
|
276
|
+
if (restoredObj.properties?.src || restoredObj.properties?.url) {
|
|
277
|
+
restoredObj.properties = { ...restoredObj.properties };
|
|
278
|
+
delete restoredObj.properties.src;
|
|
279
|
+
delete restoredObj.properties.url;
|
|
280
|
+
}
|
|
281
|
+
return restoredObj;
|
|
299
282
|
}
|
|
300
283
|
|
|
301
284
|
return obj;
|
package/src/core/SaveManager.js
CHANGED
|
@@ -108,6 +108,10 @@ export class SaveManager {
|
|
|
108
108
|
// - data:/blob: URL в image запрещены
|
|
109
109
|
// - image без src допускаются (legacy broken state), но логируются
|
|
110
110
|
this._assertImageSaveContract(saveData);
|
|
111
|
+
// Контракт сохранения файлов:
|
|
112
|
+
// - file обязан иметь src
|
|
113
|
+
// - data:/blob: URL и id-based /api/v2/files/* для file запрещены
|
|
114
|
+
this._assertFileSaveContract(saveData);
|
|
111
115
|
|
|
112
116
|
// Проверяем, изменились ли данные с последнего сохранения
|
|
113
117
|
if (this.lastSavedData && JSON.stringify(saveData) === JSON.stringify(this.lastSavedData)) {
|
|
@@ -252,6 +256,33 @@ export class SaveManager {
|
|
|
252
256
|
}
|
|
253
257
|
}
|
|
254
258
|
|
|
259
|
+
_assertFileSaveContract(saveData) {
|
|
260
|
+
const objects = Array.isArray(saveData?.boardData?.objects)
|
|
261
|
+
? saveData.boardData.objects
|
|
262
|
+
: Array.isArray(saveData?.objects)
|
|
263
|
+
? saveData.objects
|
|
264
|
+
: [];
|
|
265
|
+
|
|
266
|
+
for (const obj of objects) {
|
|
267
|
+
if (!obj || obj.type !== 'file') continue;
|
|
268
|
+
const topSrc = typeof obj.src === 'string' ? obj.src : '';
|
|
269
|
+
const propSrc = typeof obj.properties?.src === 'string' ? obj.properties.src : '';
|
|
270
|
+
const topUrl = typeof obj.url === 'string' ? obj.url : '';
|
|
271
|
+
const propUrl = typeof obj.properties?.url === 'string' ? obj.properties.url : '';
|
|
272
|
+
const effectiveSrc = topSrc.trim() || propSrc.trim() || topUrl.trim() || propUrl.trim();
|
|
273
|
+
|
|
274
|
+
if (!effectiveSrc) {
|
|
275
|
+
throw new Error(`File object "${obj.id || 'unknown'}" has no src. Save is blocked.`);
|
|
276
|
+
}
|
|
277
|
+
if (/^data:/i.test(effectiveSrc) || /^blob:/i.test(effectiveSrc)) {
|
|
278
|
+
throw new Error(`File object "${obj.id || 'unknown'}" contains forbidden data/blob src. Save is blocked.`);
|
|
279
|
+
}
|
|
280
|
+
if (/^\/api\/v2\/files\//i.test(effectiveSrc)) {
|
|
281
|
+
throw new Error(`File object "${obj.id || 'unknown'}" has legacy id-based src URL. Save is blocked.`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
255
286
|
_buildSavePayload(boardId, data, csrfToken = undefined) {
|
|
256
287
|
return {
|
|
257
288
|
moodboardId: boardId,
|
|
@@ -64,19 +64,6 @@ export class EditFileNameCommand extends BaseCommand {
|
|
|
64
64
|
}
|
|
65
65
|
objectData.properties.fileName = fileName;
|
|
66
66
|
|
|
67
|
-
// Синхронизируем с сервером, если есть fileId
|
|
68
|
-
if (objectData.fileId && this.coreMoodboard.fileUploadService) {
|
|
69
|
-
try {
|
|
70
|
-
await this.coreMoodboard.fileUploadService.updateFileMetadata(objectData.fileId, {
|
|
71
|
-
fileName: fileName
|
|
72
|
-
});
|
|
73
|
-
console.log('✅ Название файла успешно обновлено на сервере');
|
|
74
|
-
} catch (error) {
|
|
75
|
-
console.warn('⚠️ Ошибка синхронизации названия файла с сервером:', error);
|
|
76
|
-
// Не останавливаем выполнение, продолжаем с локальным обновлением
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
67
|
// Обновляем состояние
|
|
81
68
|
this.coreMoodboard.state.markDirty();
|
|
82
69
|
|
package/src/core/index.js
CHANGED
|
@@ -418,11 +418,17 @@ export class CoreMoodBoard {
|
|
|
418
418
|
},
|
|
419
419
|
...extraData
|
|
420
420
|
};
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
|
|
421
|
+
if (type === 'image' || type === 'revit-screenshot-img' || type === 'file') {
|
|
422
|
+
const propSrc = typeof properties?.src === 'string' ? properties.src.trim() : '';
|
|
423
|
+
const propUrl = typeof properties?.url === 'string' ? properties.url.trim() : '';
|
|
424
|
+
const normalizedSrc = propSrc || propUrl;
|
|
425
|
+
if (normalizedSrc) {
|
|
426
|
+
objectData.src = normalizedSrc;
|
|
427
|
+
}
|
|
428
|
+
if (objectData.properties && (objectData.properties.src || objectData.properties.url)) {
|
|
424
429
|
objectData.properties = { ...objectData.properties };
|
|
425
430
|
delete objectData.properties.src;
|
|
431
|
+
delete objectData.properties.url;
|
|
426
432
|
}
|
|
427
433
|
}
|
|
428
434
|
objectData.properties = normalizeMindmapPropertiesForCreate({
|
|
@@ -524,17 +530,21 @@ export class CoreMoodBoard {
|
|
|
524
530
|
properties: objectData.properties || {},
|
|
525
531
|
existingObjects: this.state?.state?.objects || [],
|
|
526
532
|
});
|
|
527
|
-
if (objectData.type === 'image' || objectData.type === 'revit-screenshot-img') {
|
|
533
|
+
if (objectData.type === 'image' || objectData.type === 'revit-screenshot-img' || objectData.type === 'file') {
|
|
528
534
|
const topSrc = typeof objectData.src === 'string' ? objectData.src.trim() : '';
|
|
529
535
|
const propSrc = typeof objectData.properties?.src === 'string' ? objectData.properties.src.trim() : '';
|
|
530
|
-
const
|
|
536
|
+
const topUrl = typeof objectData.url === 'string' ? objectData.url.trim() : '';
|
|
537
|
+
const propUrl = typeof objectData.properties?.url === 'string' ? objectData.properties.url.trim() : '';
|
|
538
|
+
const normalizedSrc = topSrc || propSrc || topUrl || propUrl;
|
|
531
539
|
if (normalizedSrc) {
|
|
532
540
|
objectData.src = normalizedSrc;
|
|
533
541
|
}
|
|
534
|
-
if (objectData.properties?.src) {
|
|
542
|
+
if (objectData.properties?.src || objectData.properties?.url) {
|
|
535
543
|
objectData.properties = { ...objectData.properties };
|
|
536
544
|
delete objectData.properties.src;
|
|
545
|
+
delete objectData.properties.url;
|
|
537
546
|
}
|
|
547
|
+
if (objectData.url) delete objectData.url;
|
|
538
548
|
}
|
|
539
549
|
if (objectData.type === 'mindmap') {
|
|
540
550
|
logMindmapCompoundDebug('core:load-object', {
|
|
@@ -26,9 +26,8 @@ export class ActionHandler {
|
|
|
26
26
|
case 'comment':
|
|
27
27
|
case 'file':
|
|
28
28
|
case 'mindmap':
|
|
29
|
-
// Для изображений используем только src
|
|
30
|
-
|
|
31
|
-
return this.handleCreateObject(action.type, action.position, action.properties || {}, extraData);
|
|
29
|
+
// Для изображений и файлов используем только src.
|
|
30
|
+
return this.handleCreateObject(action.type, action.position, action.properties || {}, {});
|
|
32
31
|
|
|
33
32
|
case 'delete-object':
|
|
34
33
|
if (action.id) {
|
|
@@ -89,11 +89,21 @@ export async function loadExistingBoard(board, version = null, options = {}) {
|
|
|
89
89
|
const loadedObjects = Array.isArray(normalizedData.objects) ? normalizedData.objects : [];
|
|
90
90
|
const imageObjects = loadedObjects.filter((obj) => obj?.type === 'image');
|
|
91
91
|
const imageObjectsWithSrc = imageObjects.filter((obj) => typeof obj?.src === 'string' && obj.src.trim().length > 0);
|
|
92
|
+
const fileObjects = loadedObjects.filter((obj) => obj?.type === 'file');
|
|
93
|
+
const fileObjectsWithSrc = fileObjects.filter((obj) => typeof obj?.src === 'string' && obj.src.trim().length > 0);
|
|
94
|
+
const fileObjectsWithoutSrc = fileObjects
|
|
95
|
+
.filter((obj) => !(typeof obj?.src === 'string' && obj.src.trim().length > 0))
|
|
96
|
+
.map((obj) => obj?.id || 'unknown');
|
|
92
97
|
console.log('MoodBoard load image src diagnostics:', {
|
|
93
98
|
totalObjects: loadedObjects.length,
|
|
94
99
|
imageObjects: imageObjects.length,
|
|
95
|
-
imageObjectsWithSrc: imageObjectsWithSrc.length
|
|
100
|
+
imageObjectsWithSrc: imageObjectsWithSrc.length,
|
|
101
|
+
fileObjects: fileObjects.length,
|
|
102
|
+
fileObjectsWithSrc: fileObjectsWithSrc.length
|
|
96
103
|
});
|
|
104
|
+
if (fileObjectsWithoutSrc.length > 0) {
|
|
105
|
+
console.warn('MoodBoard load warning: file objects without src:', fileObjectsWithoutSrc);
|
|
106
|
+
}
|
|
97
107
|
const loadedVersion = Number(normalizedData.version) || null;
|
|
98
108
|
board.currentLoadedVersion = loadedVersion;
|
|
99
109
|
board.historyCursorVersion = loadedVersion;
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { isV2FileDownloadUrl } from './AssetUrlPolicy.js';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Сервис для загрузки и управления файлами на сервере
|
|
5
3
|
*/
|
|
@@ -7,7 +5,6 @@ export class FileUploadService {
|
|
|
7
5
|
constructor(apiClient, options = {}) {
|
|
8
6
|
this.apiClient = apiClient;
|
|
9
7
|
this.uploadEndpoint = '/api/v2/files/upload';
|
|
10
|
-
this.deleteEndpoint = '/api/v2/files';
|
|
11
8
|
this.options = {
|
|
12
9
|
csrfToken: null, // Можно передать токен напрямую
|
|
13
10
|
csrfTokenSelector: 'meta[name="csrf-token"]', // Селектор для поиска токена в DOM
|
|
@@ -51,7 +48,7 @@ export class FileUploadService {
|
|
|
51
48
|
* Загружает файл на сервер
|
|
52
49
|
* @param {File|Blob} file - файл для загрузки
|
|
53
50
|
* @param {string} name - имя файла
|
|
54
|
-
* @returns {Promise<{
|
|
51
|
+
* @returns {Promise<{src: string, size: number, name: string, mimeType: string}>}
|
|
55
52
|
*/
|
|
56
53
|
async uploadFile(file, name = null) {
|
|
57
54
|
try {
|
|
@@ -89,30 +86,27 @@ export class FileUploadService {
|
|
|
89
86
|
}
|
|
90
87
|
|
|
91
88
|
const result = await response.json();
|
|
89
|
+
const status = response.status;
|
|
92
90
|
|
|
93
91
|
if (!result.success) {
|
|
94
92
|
throw new Error(result.message || 'Ошибка загрузки файла');
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
const fileId = result.data.fileId || result.data.id;
|
|
98
95
|
const serverUrl = typeof result.data.url === 'string' ? result.data.url.trim() : '';
|
|
99
|
-
if (!
|
|
100
|
-
throw new Error('Сервер не вернул
|
|
101
|
-
}
|
|
102
|
-
if (!isV2FileDownloadUrl(serverUrl)) {
|
|
103
|
-
throw new Error('Некорректный URL файла от сервера. Ожидается /api/v2/files/{fileId}/download');
|
|
104
|
-
}
|
|
105
|
-
if (!this._matchesFileIdInUrl(serverUrl, fileId)) {
|
|
106
|
-
throw new Error('fileId не совпадает с URL файла от сервера.');
|
|
96
|
+
if (!serverUrl) {
|
|
97
|
+
throw new Error('Сервер не вернул data.url для файла.');
|
|
107
98
|
}
|
|
99
|
+
console.log('File upload diagnostics:', {
|
|
100
|
+
status,
|
|
101
|
+
success: !!result.success,
|
|
102
|
+
url: serverUrl
|
|
103
|
+
});
|
|
108
104
|
|
|
109
105
|
return {
|
|
110
|
-
|
|
111
|
-
fileId, // Добавляем fileId для явного доступа
|
|
112
|
-
url: serverUrl,
|
|
106
|
+
src: serverUrl,
|
|
113
107
|
size: result.data.size,
|
|
114
108
|
name: result.data.name,
|
|
115
|
-
|
|
109
|
+
mimeType: result.data.mime_type || result.data.type || file.type || 'application/octet-stream'
|
|
116
110
|
};
|
|
117
111
|
|
|
118
112
|
} catch (error) {
|
|
@@ -121,329 +115,6 @@ export class FileUploadService {
|
|
|
121
115
|
}
|
|
122
116
|
}
|
|
123
117
|
|
|
124
|
-
_matchesFileIdInUrl(url, fileId) {
|
|
125
|
-
const id = typeof fileId === 'string' ? fileId.trim() : '';
|
|
126
|
-
if (!id || typeof url !== 'string') return false;
|
|
127
|
-
const raw = url.trim();
|
|
128
|
-
const relativeMatch = raw.match(/^\/api\/v2\/files\/([^/]+)\/download$/i);
|
|
129
|
-
if (relativeMatch) {
|
|
130
|
-
return decodeURIComponent(relativeMatch[1]) === id;
|
|
131
|
-
}
|
|
132
|
-
try {
|
|
133
|
-
const parsed = new URL(raw);
|
|
134
|
-
const absoluteMatch = parsed.pathname.match(/^\/api\/v2\/files\/([^/]+)\/download$/i);
|
|
135
|
-
return !!absoluteMatch && decodeURIComponent(absoluteMatch[1]) === id;
|
|
136
|
-
} catch (_) {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Обновляет метаданные файла на сервере
|
|
143
|
-
* @param {string} fileId - ID файла
|
|
144
|
-
* @param {Object} metadata - метаданные для обновления
|
|
145
|
-
* @returns {Promise<Object>}
|
|
146
|
-
*/
|
|
147
|
-
async updateFileMetadata(fileId, metadata) {
|
|
148
|
-
try {
|
|
149
|
-
const csrfToken = this._getCsrfToken();
|
|
150
|
-
|
|
151
|
-
if (this.options.requireCsrf && !csrfToken) {
|
|
152
|
-
throw new Error('CSRF токен не найден. Добавьте <meta name="csrf-token" content="{{ csrf_token() }}"> в HTML или передайте токен в опциях.');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const headers = {
|
|
156
|
-
'Content-Type': 'application/json',
|
|
157
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
158
|
-
'Accept': 'application/json'
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// Добавляем CSRF токен только если он есть
|
|
162
|
-
if (csrfToken) {
|
|
163
|
-
headers['X-CSRF-TOKEN'] = csrfToken;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const response = await fetch(`${this.deleteEndpoint}/${fileId}`, {
|
|
167
|
-
method: 'PUT',
|
|
168
|
-
headers,
|
|
169
|
-
credentials: 'same-origin',
|
|
170
|
-
body: JSON.stringify(metadata)
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
if (!response.ok) {
|
|
174
|
-
const errorData = await response.json().catch(() => null);
|
|
175
|
-
throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const result = await response.json();
|
|
179
|
-
|
|
180
|
-
if (!result.success) {
|
|
181
|
-
throw new Error(result.message || 'Ошибка обновления метаданных файла');
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return result.data;
|
|
185
|
-
|
|
186
|
-
} catch (error) {
|
|
187
|
-
console.error('Ошибка обновления метаданных файла:', error);
|
|
188
|
-
throw error;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Удаляет файл с сервера
|
|
194
|
-
* @param {string} fileId - ID файла
|
|
195
|
-
*/
|
|
196
|
-
async deleteFile(fileId) {
|
|
197
|
-
try {
|
|
198
|
-
const csrfToken = this._getCsrfToken();
|
|
199
|
-
|
|
200
|
-
const headers = {
|
|
201
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
202
|
-
'Accept': 'application/json'
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
// Добавляем CSRF токен только если он есть
|
|
206
|
-
if (csrfToken) {
|
|
207
|
-
headers['X-CSRF-TOKEN'] = csrfToken;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const response = await fetch(`${this.deleteEndpoint}/${fileId}`, {
|
|
211
|
-
method: 'DELETE',
|
|
212
|
-
headers,
|
|
213
|
-
credentials: 'same-origin'
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
if (!response.ok) {
|
|
217
|
-
const errorData = await response.json().catch(() => null);
|
|
218
|
-
throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const result = await response.json();
|
|
222
|
-
return result.success;
|
|
223
|
-
|
|
224
|
-
} catch (error) {
|
|
225
|
-
console.error('Ошибка удаления файла:', error);
|
|
226
|
-
throw error;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Получает информацию о файле
|
|
232
|
-
* @param {string} fileId - ID файла
|
|
233
|
-
*/
|
|
234
|
-
async getFileInfo(fileId) {
|
|
235
|
-
try {
|
|
236
|
-
const response = await fetch(`${this.deleteEndpoint}/${fileId}`, {
|
|
237
|
-
method: 'GET',
|
|
238
|
-
headers: {
|
|
239
|
-
'Accept': 'application/json',
|
|
240
|
-
'X-Requested-With': 'XMLHttpRequest'
|
|
241
|
-
},
|
|
242
|
-
credentials: 'same-origin'
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
if (!response.ok) {
|
|
246
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const result = await response.json();
|
|
250
|
-
return result.data;
|
|
251
|
-
|
|
252
|
-
} catch (error) {
|
|
253
|
-
console.error('Ошибка получения информации о файле:', error);
|
|
254
|
-
throw error;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Получает URL для скачивания файла
|
|
260
|
-
* @param {string} fileId - ID файла
|
|
261
|
-
*/
|
|
262
|
-
getDownloadUrl(fileId) {
|
|
263
|
-
return `${this.deleteEndpoint}/${fileId}/download`;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Скачивает файл с сервера
|
|
268
|
-
* @param {string} fileId - ID файла
|
|
269
|
-
* @param {string} fileName - имя файла для скачивания
|
|
270
|
-
*/
|
|
271
|
-
async downloadFile(fileId, fileName = null) {
|
|
272
|
-
try {
|
|
273
|
-
const downloadUrl = this.getDownloadUrl(fileId);
|
|
274
|
-
|
|
275
|
-
console.log('📥 FileUploadService: Начинаем скачивание файла:', {
|
|
276
|
-
fileId,
|
|
277
|
-
fileName,
|
|
278
|
-
downloadUrl,
|
|
279
|
-
userAgent: navigator.userAgent,
|
|
280
|
-
isSecureContext: window.isSecureContext
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// Метод 1: Попробуем через fetch + blob (более надежно для современных браузеров)
|
|
284
|
-
try {
|
|
285
|
-
console.log('🔄 Метод 1: Пробуем скачивание через fetch...');
|
|
286
|
-
|
|
287
|
-
const response = await fetch(downloadUrl, {
|
|
288
|
-
method: 'GET',
|
|
289
|
-
headers: {
|
|
290
|
-
'X-Requested-With': 'XMLHttpRequest'
|
|
291
|
-
},
|
|
292
|
-
credentials: 'same-origin'
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
console.log('📥 Ответ сервера:', {
|
|
296
|
-
status: response.status,
|
|
297
|
-
statusText: response.statusText,
|
|
298
|
-
contentType: response.headers.get('content-type'),
|
|
299
|
-
contentLength: response.headers.get('content-length'),
|
|
300
|
-
contentDisposition: response.headers.get('content-disposition')
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
if (!response.ok) {
|
|
304
|
-
// Пытаемся получить JSON ошибку от Laravel
|
|
305
|
-
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
306
|
-
|
|
307
|
-
try {
|
|
308
|
-
const errorData = await response.json();
|
|
309
|
-
console.error('🚨 Ошибка от сервера:', errorData);
|
|
310
|
-
|
|
311
|
-
if (errorData.message) {
|
|
312
|
-
errorMessage = `${errorMessage} - ${errorData.message}`;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Показываем пользователю детальную ошибку
|
|
316
|
-
if (errorData.success === false) {
|
|
317
|
-
alert(`Ошибка сервера: ${errorData.message || 'Файл не найден'}`);
|
|
318
|
-
}
|
|
319
|
-
} catch (jsonError) {
|
|
320
|
-
console.warn('Не удалось прочитать JSON ошибку:', jsonError);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
throw new Error(errorMessage);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Получаем blob файла
|
|
327
|
-
const blob = await response.blob();
|
|
328
|
-
console.log('📦 Получен blob:', {
|
|
329
|
-
size: blob.size,
|
|
330
|
-
type: blob.type
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
// Создаем URL для blob
|
|
334
|
-
const blobUrl = window.URL.createObjectURL(blob);
|
|
335
|
-
|
|
336
|
-
// Создаем ссылку для скачивания
|
|
337
|
-
const link = document.createElement('a');
|
|
338
|
-
link.href = blobUrl;
|
|
339
|
-
link.download = fileName || `file_${fileId}`;
|
|
340
|
-
|
|
341
|
-
// Добавляем в DOM, кликаем и удаляем
|
|
342
|
-
document.body.appendChild(link);
|
|
343
|
-
link.click();
|
|
344
|
-
document.body.removeChild(link);
|
|
345
|
-
|
|
346
|
-
// Освобождаем память
|
|
347
|
-
window.URL.revokeObjectURL(blobUrl);
|
|
348
|
-
|
|
349
|
-
console.log('✅ Файл успешно скачан через fetch/blob:', fileName || fileId);
|
|
350
|
-
return true;
|
|
351
|
-
|
|
352
|
-
} catch (fetchError) {
|
|
353
|
-
console.warn('❌ Ошибка скачивания через fetch:', fetchError);
|
|
354
|
-
|
|
355
|
-
// Метод 2: Fallback - открываем в новом окне
|
|
356
|
-
console.log('🔄 Метод 2: Пробуем открытие в новом окне...');
|
|
357
|
-
|
|
358
|
-
try {
|
|
359
|
-
const newWindow = window.open(downloadUrl, '_blank');
|
|
360
|
-
if (newWindow) {
|
|
361
|
-
console.log('✅ Файл открыт в новом окне');
|
|
362
|
-
return true;
|
|
363
|
-
} else {
|
|
364
|
-
throw new Error('Popup заблокирован браузером');
|
|
365
|
-
}
|
|
366
|
-
} catch (windowError) {
|
|
367
|
-
console.warn('❌ Ошибка открытия в новом окне:', windowError);
|
|
368
|
-
|
|
369
|
-
// Метод 3: Последний fallback - прямая ссылка
|
|
370
|
-
console.log('🔄 Метод 3: Создаем прямую ссылку...');
|
|
371
|
-
|
|
372
|
-
const link = document.createElement('a');
|
|
373
|
-
link.href = downloadUrl;
|
|
374
|
-
if (fileName) {
|
|
375
|
-
link.download = fileName;
|
|
376
|
-
}
|
|
377
|
-
link.target = '_blank';
|
|
378
|
-
|
|
379
|
-
document.body.appendChild(link);
|
|
380
|
-
link.click();
|
|
381
|
-
document.body.removeChild(link);
|
|
382
|
-
|
|
383
|
-
console.log('✅ Создана прямая ссылка для скачивания');
|
|
384
|
-
return true;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
} catch (error) {
|
|
389
|
-
console.error('❌ FileUploadService: Критическая ошибка скачивания файла:', error);
|
|
390
|
-
|
|
391
|
-
// Показываем пользователю альтернативную ссылку
|
|
392
|
-
if (confirm(`Автоматическое скачивание не удалось: ${error.message}\n\nОткрыть файл в новой вкладке?`)) {
|
|
393
|
-
window.open(this.getDownloadUrl(fileId), '_blank');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
throw error;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Очищает неиспользуемые файлы с сервера
|
|
402
|
-
* @returns {Promise<{deletedCount: number, errors: Array}>}
|
|
403
|
-
*/
|
|
404
|
-
async cleanupUnusedFiles() {
|
|
405
|
-
try {
|
|
406
|
-
const csrfToken = this._getCsrfToken();
|
|
407
|
-
|
|
408
|
-
const headers = {
|
|
409
|
-
'X-Requested-With': 'XMLHttpRequest',
|
|
410
|
-
'Accept': 'application/json'
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
// Добавляем CSRF токен только если он есть
|
|
414
|
-
if (csrfToken) {
|
|
415
|
-
headers['X-CSRF-TOKEN'] = csrfToken;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const response = await fetch(`${this.deleteEndpoint}/cleanup`, {
|
|
419
|
-
method: 'POST',
|
|
420
|
-
headers,
|
|
421
|
-
credentials: 'same-origin'
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
if (!response.ok) {
|
|
425
|
-
const errorData = await response.json().catch(() => null);
|
|
426
|
-
throw new Error(errorData?.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const result = await response.json();
|
|
430
|
-
|
|
431
|
-
if (result.success) {
|
|
432
|
-
const data = result.data || {};
|
|
433
|
-
return {
|
|
434
|
-
deletedCount: data.deleted_count || 0,
|
|
435
|
-
errors: data.errors || []
|
|
436
|
-
};
|
|
437
|
-
} else {
|
|
438
|
-
throw new Error(result.message || 'Ошибка очистки файлов');
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
} catch (error) {
|
|
442
|
-
console.error('Ошибка очистки неиспользуемых файлов:', error);
|
|
443
|
-
throw error;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
118
|
/**
|
|
448
119
|
* Проверяет, поддерживается ли тип файла для предварительного просмотра
|
|
449
120
|
*/
|
|
@@ -381,7 +381,7 @@ export class ToolEventRouter {
|
|
|
381
381
|
fileSize: file.size || 0,
|
|
382
382
|
mimeType: file.type || 'application/octet-stream',
|
|
383
383
|
formattedSize: null,
|
|
384
|
-
|
|
384
|
+
src: null,
|
|
385
385
|
width: 120,
|
|
386
386
|
height: 140
|
|
387
387
|
};
|
|
@@ -413,7 +413,7 @@ export class ToolEventRouter {
|
|
|
413
413
|
};
|
|
414
414
|
logDropDebug(diagnostics, 'file_upload_success', {
|
|
415
415
|
fileName: uploadResult?.name || fallbackProps.fileName,
|
|
416
|
-
|
|
416
|
+
srcPresent: !!(typeof uploadResult?.src === 'string' && uploadResult.src.trim())
|
|
417
417
|
});
|
|
418
418
|
return {
|
|
419
419
|
type: 'file',
|
|
@@ -424,11 +424,10 @@ export class ToolEventRouter {
|
|
|
424
424
|
fileSize: uploadResult.size,
|
|
425
425
|
mimeType: uploadResult.mimeType,
|
|
426
426
|
formattedSize: uploadResult.formattedSize,
|
|
427
|
-
|
|
427
|
+
src: uploadResult.src,
|
|
428
428
|
width: 120,
|
|
429
429
|
height: 140
|
|
430
|
-
}
|
|
431
|
-
fileId: uploadResult.fileId || uploadResult.id || null
|
|
430
|
+
}
|
|
432
431
|
};
|
|
433
432
|
} else {
|
|
434
433
|
showDropWarning(
|
|
@@ -72,11 +72,10 @@ export class PlacementPayloadFactory {
|
|
|
72
72
|
fileSize: uploadResult.size,
|
|
73
73
|
mimeType: uploadResult.mimeType,
|
|
74
74
|
formattedSize: uploadResult.formattedSize,
|
|
75
|
-
|
|
75
|
+
src: uploadResult.src,
|
|
76
76
|
width,
|
|
77
77
|
height
|
|
78
|
-
}
|
|
79
|
-
fileId: uploadResult.fileId || uploadResult.id
|
|
78
|
+
}
|
|
80
79
|
});
|
|
81
80
|
}
|
|
82
81
|
|
|
@@ -181,8 +181,8 @@ export class FilePropertiesPanel {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
async _handleDownload() {
|
|
184
|
-
if (!this.currentId || !this.core?.
|
|
185
|
-
console.warn('FilePropertiesPanel: не могу скачать файл - нет currentId или
|
|
184
|
+
if (!this.currentId || !this.core?.state?.getObjects) {
|
|
185
|
+
console.warn('FilePropertiesPanel: не могу скачать файл - нет currentId или state');
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
188
|
|
|
@@ -202,18 +202,18 @@ export class FilePropertiesPanel {
|
|
|
202
202
|
return;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
const
|
|
205
|
+
const fileSrc = typeof fileObject.src === 'string' ? fileObject.src.trim() : '';
|
|
206
206
|
const fileName = fileObject.properties?.fileName || 'file';
|
|
207
207
|
|
|
208
208
|
console.log('📎 FilePropertiesPanel: Данные файла для скачивания:', {
|
|
209
|
-
|
|
209
|
+
fileSrc,
|
|
210
210
|
fileName,
|
|
211
|
-
|
|
211
|
+
hasSrc: !!fileSrc
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
-
if (!
|
|
215
|
-
console.warn('FilePropertiesPanel: у файла нет
|
|
216
|
-
alert('Ошибка:
|
|
214
|
+
if (!fileSrc) {
|
|
215
|
+
console.warn('FilePropertiesPanel: у файла нет src');
|
|
216
|
+
alert('Ошибка: у файла отсутствует src для скачивания');
|
|
217
217
|
return;
|
|
218
218
|
}
|
|
219
219
|
|
|
@@ -228,8 +228,14 @@ export class FilePropertiesPanel {
|
|
|
228
228
|
`;
|
|
229
229
|
this.downloadButton.disabled = true;
|
|
230
230
|
|
|
231
|
-
// Скачиваем файл
|
|
232
|
-
|
|
231
|
+
// Скачиваем файл напрямую по src (без id-based endpoint)
|
|
232
|
+
const link = document.createElement('a');
|
|
233
|
+
link.href = fileSrc;
|
|
234
|
+
link.download = fileName;
|
|
235
|
+
link.target = '_blank';
|
|
236
|
+
document.body.appendChild(link);
|
|
237
|
+
link.click();
|
|
238
|
+
document.body.removeChild(link);
|
|
233
239
|
|
|
234
240
|
// Восстанавливаем кнопку
|
|
235
241
|
setTimeout(() => {
|
|
@@ -262,14 +268,14 @@ export class FilePropertiesPanel {
|
|
|
262
268
|
const fileObject = objects.find(obj => obj.id === this.currentId);
|
|
263
269
|
|
|
264
270
|
if (fileObject && fileObject.type === 'file') {
|
|
265
|
-
const
|
|
271
|
+
const hasFileSrc = typeof fileObject.src === 'string' && fileObject.src.trim().length > 0;
|
|
266
272
|
|
|
267
|
-
// Показываем/скрываем кнопку скачивания в зависимости от наличия
|
|
273
|
+
// Показываем/скрываем кнопку скачивания в зависимости от наличия src
|
|
268
274
|
if (this.downloadButton) {
|
|
269
|
-
// Всегда показываем кнопку,
|
|
275
|
+
// Всегда показываем кнопку, но отключаем без src
|
|
270
276
|
this.downloadButton.style.display = 'flex';
|
|
271
|
-
this.downloadButton.disabled = !
|
|
272
|
-
this.downloadButton.title =
|
|
277
|
+
this.downloadButton.disabled = !hasFileSrc;
|
|
278
|
+
this.downloadButton.title = hasFileSrc ? 'Скачать файл' : 'Файл недоступен для скачивания';
|
|
273
279
|
}
|
|
274
280
|
}
|
|
275
281
|
}
|