@sequent-org/moodboard 1.4.11 → 1.4.13

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequent-org/moodboard",
3
- "version": "1.4.11",
3
+ "version": "1.4.13",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
@@ -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 cleanedObj = { ...obj };
188
-
189
- // Если есть fileId, убираем content для экономии места
190
- if (obj.fileId && typeof obj.fileId === 'string' && obj.fileId.trim().length > 0) {
191
- // Убираем content с верхнего уровня
192
- if (cleanedObj.content) {
193
- delete cleanedObj.content;
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
- // Если нет fileId, предупреждаем о наличии content
203
- else {
204
- // Для файлов сейчас сохраняем поведение: без fileId не модифицируем объект.
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
- if (obj.fileId) {
247
- try {
248
- // Формируем URL файла для скачивания
249
- const fileUrl = `/api/v2/files/${obj.fileId}/download`;
250
-
251
- // Создаем обновленный объект с восстановленными данными
252
- const restoredObj = {
253
- ...obj,
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
- return obj;
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;
@@ -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 ((type === 'image' || type === 'revit-screenshot-img') && typeof properties?.src === 'string') {
422
- objectData.src = properties.src;
423
- if (objectData.properties && objectData.properties.src) {
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 normalizedSrc = topSrc || propSrc;
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', {
@@ -40,6 +40,49 @@ export class KeyboardClipboardImagePaste {
40
40
  }
41
41
  }
42
42
 
43
+ async handleFileUpload(file, fileName) {
44
+ try {
45
+ if (!(this.core && this.core.fileUploadService)) {
46
+ alert('Сервис загрузки файлов недоступен. Файл не добавлен.');
47
+ return;
48
+ }
49
+
50
+ const uploadResult = await this.core.fileUploadService.uploadFile(file, fileName);
51
+ const world = this.core?.pixi?.worldLayer || this.core?.pixi?.app?.stage;
52
+ const view = this.core?.pixi?.app?.view;
53
+ const s = world?.scale?.x || 1;
54
+ const hasCursor = Number.isFinite(this.core?._cursor?.x) && Number.isFinite(this.core?._cursor?.y);
55
+
56
+ const screenX = hasCursor ? this.core._cursor.x : (view?.clientWidth || 0) / 2;
57
+ const screenY = hasCursor ? this.core._cursor.y : (view?.clientHeight || 0) / 2;
58
+ const worldX = (screenX - (world?.x || 0)) / s;
59
+ const worldY = (screenY - (world?.y || 0)) / s;
60
+
61
+ const width = 120;
62
+ const height = 140;
63
+ this.eventBus.emit(Events.UI.ToolbarAction, {
64
+ type: 'file',
65
+ id: 'file',
66
+ position: {
67
+ x: Math.round(worldX - width / 2),
68
+ y: Math.round(worldY - height / 2)
69
+ },
70
+ properties: {
71
+ fileName: uploadResult.name,
72
+ fileSize: uploadResult.size,
73
+ mimeType: uploadResult.mimeType,
74
+ formattedSize: uploadResult.formattedSize,
75
+ src: uploadResult.src,
76
+ width,
77
+ height
78
+ }
79
+ });
80
+ } catch (error) {
81
+ console.error('Ошибка загрузки файла:', error);
82
+ alert('Ошибка загрузки файла на сервер. Файл не добавлен.');
83
+ }
84
+ }
85
+
43
86
  createPasteHandler() {
44
87
  return async (e) => {
45
88
  try {
@@ -66,6 +109,12 @@ export class KeyboardClipboardImagePaste {
66
109
  await this.handleImageFileUpload(imgFile, imgFile.name || 'clipboard-image.png');
67
110
  return;
68
111
  }
112
+ const plainFile = files.find(f => !(f.type && f.type.startsWith('image/')));
113
+ if (plainFile) {
114
+ e.preventDefault();
115
+ await this.handleFileUpload(plainFile, plainFile.name || 'clipboard-file');
116
+ return;
117
+ }
69
118
 
70
119
  const html = cd.getData && cd.getData('text/html');
71
120
  if (html && html.includes('<img')) {
@@ -26,9 +26,8 @@ export class ActionHandler {
26
26
  case 'comment':
27
27
  case 'file':
28
28
  case 'mindmap':
29
- // Для изображений используем только src; для файлов сохраняем fileId.
30
- const extraData = action.fileId ? { fileId: action.fileId } : {};
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<{id: string, url: string, size: number, name: string}>}
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 (!fileId) {
100
- throw new Error('Сервер не вернул fileId.');
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
- id: fileId, // Используем fileId как основное поле, id для обратной совместимости
111
- fileId, // Добавляем fileId для явного доступа
112
- url: serverUrl,
106
+ src: serverUrl,
113
107
  size: result.data.size,
114
108
  name: result.data.name,
115
- type: result.data.type
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
- url: null,
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
- fileId: uploadResult?.fileId || uploadResult?.id || null
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
- url: uploadResult.url,
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
- url: uploadResult.url,
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?.fileUploadService) {
185
- console.warn('FilePropertiesPanel: не могу скачать файл - нет currentId или fileUploadService');
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 fileId = fileObject.fileId;
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
- fileId,
209
+ fileSrc,
210
210
  fileName,
211
- downloadUrl: this.core.fileUploadService.getDownloadUrl(fileId)
211
+ hasSrc: !!fileSrc
212
212
  });
213
213
 
214
- if (!fileId) {
215
- console.warn('FilePropertiesPanel: у файла нет fileId');
216
- alert('Ошибка: файл не имеет ID для скачивания');
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
- await this.core.fileUploadService.downloadFile(fileId, fileName);
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 hasFileId = !!(fileObject.fileId);
271
+ const hasFileSrc = typeof fileObject.src === 'string' && fileObject.src.trim().length > 0;
266
272
 
267
- // Показываем/скрываем кнопку скачивания в зависимости от наличия fileId
273
+ // Показываем/скрываем кнопку скачивания в зависимости от наличия src
268
274
  if (this.downloadButton) {
269
- // Всегда показываем кнопку, даже без fileId
275
+ // Всегда показываем кнопку, но отключаем без src
270
276
  this.downloadButton.style.display = 'flex';
271
- this.downloadButton.disabled = !hasFileId;
272
- this.downloadButton.title = hasFileId ? 'Скачать файл' : 'Файл недоступен для скачивания';
277
+ this.downloadButton.disabled = !hasFileSrc;
278
+ this.downloadButton.title = hasFileSrc ? 'Скачать файл' : 'Файл недоступен для скачивания';
273
279
  }
274
280
  }
275
281
  }