@sequent-org/moodboard 1.4.4 → 1.4.5

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.4",
3
+ "version": "1.4.5",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
@@ -144,90 +144,55 @@ export class ApiClient {
144
144
 
145
145
  const cleanedObjects = boardData.objects.map(obj => {
146
146
  if (obj.type === 'image') {
147
- console.log('🧹 DEBUG _cleanImageData: обрабатываем изображение:', {
148
- id: obj.id,
149
- imageId: obj.imageId,
150
- hasSrc: !!obj.src,
151
- hasPropertiesSrc: !!obj.properties?.src,
152
- srcIsBase64: !!(obj.src && obj.src.startsWith('data:')),
153
- propertiesSrcIsBase64: !!(obj.properties?.src && obj.properties.src.startsWith('data:'))
154
- });
155
-
147
+ const imageId = typeof obj.imageId === 'string' ? obj.imageId.trim() : '';
148
+ const topSrc = typeof obj.src === 'string' ? obj.src : '';
149
+ const propSrc = typeof obj.properties?.src === 'string' ? obj.properties.src : '';
150
+ const hasForbiddenInlineSrc = /^data:/i.test(topSrc)
151
+ || /^blob:/i.test(topSrc)
152
+ || /^data:/i.test(propSrc)
153
+ || /^blob:/i.test(propSrc);
154
+
155
+ // Жесткий контракт v2: сохраняем image только через server imageId.
156
+ if (!imageId) {
157
+ throw new Error(`Image object "${obj.id || 'unknown'}" has no imageId. Save is blocked.`);
158
+ }
159
+ if (hasForbiddenInlineSrc) {
160
+ throw new Error(`Image object "${obj.id || 'unknown'}" contains forbidden data/blob src. Save is blocked.`);
161
+ }
162
+
156
163
  const cleanedObj = { ...obj };
157
-
158
- // Если есть imageId, убираем src для экономии места
159
- if (obj.imageId && typeof obj.imageId === 'string' && obj.imageId.trim().length > 0) {
160
- console.log('🧹 DEBUG _cleanImageData: у изображения есть imageId, убираем src');
161
-
162
- // Убираем src с верхнего уровня
163
- if (cleanedObj.src) {
164
- delete cleanedObj.src;
165
- console.log('🧹 DEBUG: удален src с верхнего уровня');
166
- }
167
-
168
- // Убираем src из properties
169
- if (cleanedObj.properties?.src) {
170
- cleanedObj.properties = { ...cleanedObj.properties };
171
- delete cleanedObj.properties.src;
172
- console.log('🧹 DEBUG: удален src из properties');
173
- }
164
+
165
+ // imageId валиден src можно безопасно убрать из history payload.
166
+ if (cleanedObj.src) {
167
+ delete cleanedObj.src;
174
168
  }
175
- // Если нет imageId, предупреждаем о base64
176
- else {
177
- console.log('🧹 DEBUG _cleanImageData: у изображения НЕТ imageId, оставляем src как есть');
178
- if (cleanedObj.properties?.src && cleanedObj.properties.src.startsWith('data:')) {
179
- console.warn('❌ Изображение сохраняется с base64 в properties, так как нет imageId:', cleanedObj.id);
180
- }
181
- if (cleanedObj.src && cleanedObj.src.startsWith('data:')) {
182
- console.warn('❌ Изображение сохраняется с base64 в src, так как нет imageId:', cleanedObj.id);
183
- }
184
- if (!obj.imageId) {
185
- console.warn('❌ У изображения отсутствует imageId:', cleanedObj.id);
186
- }
169
+ if (cleanedObj.properties?.src) {
170
+ cleanedObj.properties = { ...cleanedObj.properties };
171
+ delete cleanedObj.properties.src;
187
172
  }
188
-
173
+
189
174
  return cleanedObj;
190
175
  }
191
176
 
192
177
  if (obj.type === 'file') {
193
- console.log('🧹 DEBUG _cleanObjectData: обрабатываем файл:', {
194
- id: obj.id,
195
- fileId: obj.fileId,
196
- hasContent: !!obj.content,
197
- hasPropertiesContent: !!obj.properties?.content
198
- });
199
-
200
178
  const cleanedObj = { ...obj };
201
179
 
202
180
  // Если есть fileId, убираем content для экономии места
203
181
  if (obj.fileId && typeof obj.fileId === 'string' && obj.fileId.trim().length > 0) {
204
- console.log('🧹 DEBUG _cleanObjectData: у файла есть fileId, убираем content');
205
-
206
182
  // Убираем content с верхнего уровня
207
183
  if (cleanedObj.content) {
208
184
  delete cleanedObj.content;
209
- console.log('🧹 DEBUG: удален content с верхнего уровня');
210
185
  }
211
186
 
212
187
  // Убираем content из properties
213
188
  if (cleanedObj.properties?.content) {
214
189
  cleanedObj.properties = { ...cleanedObj.properties };
215
190
  delete cleanedObj.properties.content;
216
- console.log('🧹 DEBUG: удален content из properties');
217
191
  }
218
192
  }
219
193
  // Если нет fileId, предупреждаем о наличии content
220
194
  else {
221
- console.log('🧹 DEBUG _cleanObjectData: у файла НЕТ fileId, оставляем content как есть');
222
- if (cleanedObj.properties?.content) {
223
- console.warn('❌ Файл сохраняется с content в properties, так как нет fileId:', cleanedObj.id);
224
- }
225
- if (cleanedObj.content) {
226
- console.warn('❌ Файл сохраняется с content, так как нет fileId:', cleanedObj.id);
227
- }
228
- if (!obj.fileId) {
229
- console.warn('❌ У файла отсутствует fileId:', cleanedObj.id);
230
- }
195
+ // Для файлов сейчас сохраняем поведение: без fileId не модифицируем объект.
231
196
  }
232
197
 
233
198
  return cleanedObj;
@@ -253,15 +218,7 @@ export class ApiClient {
253
218
  const restoredObjects = await Promise.all(
254
219
  boardData.objects.map(async (obj) => {
255
220
  if (obj.type === 'image') {
256
- console.log('🔗 DEBUG restoreImageUrls: обрабатываем изображение:', {
257
- id: obj.id,
258
- imageId: obj.imageId,
259
- hasSrc: !!obj.src,
260
- hasPropertiesSrc: !!obj.properties?.src
261
- });
262
-
263
221
  if (obj.imageId && (!obj.src && !obj.properties?.src)) {
264
- console.log('🔗 DEBUG: восстанавливаем URL для изображения');
265
222
  try {
266
223
  // Формируем URL изображения
267
224
  const imageUrl = `/api/images/${obj.imageId}/file`;
@@ -278,22 +235,12 @@ export class ApiClient {
278
235
  console.warn(`Не удалось восстановить URL для изображения ${obj.imageId}:`, error);
279
236
  return obj;
280
237
  }
281
- } else {
282
- console.log('🔗 DEBUG: изображение уже имеет URL или нет imageId, оставляем как есть');
283
- return obj;
284
238
  }
239
+ return obj;
285
240
  }
286
241
 
287
242
  if (obj.type === 'file') {
288
- console.log('🔗 DEBUG restoreObjectUrls: обрабатываем файл:', {
289
- id: obj.id,
290
- fileId: obj.fileId,
291
- hasUrl: !!obj.url,
292
- hasPropertiesUrl: !!obj.properties?.url
293
- });
294
-
295
243
  if (obj.fileId) {
296
- console.log('🔗 DEBUG: восстанавливаем данные для файла');
297
244
  try {
298
245
  // Формируем URL файла для скачивания
299
246
  const fileUrl = `/api/files/${obj.fileId}/download`;
@@ -323,7 +270,6 @@ export class ApiClient {
323
270
  if (response.ok) {
324
271
  const result = await response.json();
325
272
  if (result.success && result.data) {
326
- console.log('🔄 Обновляем метаданные файла с сервера:', result.data);
327
273
  // Эмитим событие для обновления метаданных файла в состоянии
328
274
  // (это будет обработано в core, если EventBus доступен)
329
275
  if (typeof window !== 'undefined' && window.moodboardEventBus) {
@@ -345,10 +291,8 @@ export class ApiClient {
345
291
  console.warn(`Не удалось восстановить данные для файла ${obj.fileId}:`, error);
346
292
  return obj;
347
293
  }
348
- } else {
349
- console.log('🔗 DEBUG: файл не имеет fileId, оставляем как есть');
350
- return obj;
351
294
  }
295
+ return obj;
352
296
  }
353
297
 
354
298
  return obj;
@@ -15,8 +15,6 @@ export class HistoryManager {
15
15
  this.history = [];
16
16
  // Текущая позиция в истории
17
17
  this.currentIndex = -1;
18
- // Флаг для предотвращения зацикливания при undo/redo
19
- this.isExecutingCommand = false;
20
18
  this._listenersAttached = false;
21
19
  this._onDebug = () => this.debugHistory();
22
20
 
@@ -34,14 +32,6 @@ export class HistoryManager {
34
32
  * Выполнить команду и добавить в историю
35
33
  */
36
34
  executeCommand(command) {
37
- if (this.isExecutingCommand) {
38
- // Если мы в процессе undo/redo, не добавляем в историю
39
- this._executeCommandSafely(command);
40
- return;
41
- }
42
-
43
-
44
-
45
35
  // Проверяем, можно ли объединить с последней командой
46
36
  const lastCommand = this.getLastCommand();
47
37
  if (lastCommand &&
@@ -51,8 +41,6 @@ export class HistoryManager {
51
41
  lastCommand.mergeWith(command);
52
42
  this._executeCommandSafely(lastCommand);
53
43
  this.eventBus.emit('history:changed', {
54
- canUndo: this.canUndo(),
55
- canRedo: this.canRedo(),
56
44
  historySize: this.history.length
57
45
  });
58
46
  return;
@@ -78,8 +66,6 @@ export class HistoryManager {
78
66
 
79
67
  // Уведомляем об изменении истории
80
68
  this.eventBus.emit(Events.History.Changed, {
81
- canUndo: this.canUndo(),
82
- canRedo: this.canRedo(),
83
69
  historySize: this.history.length,
84
70
  currentCommand: command.toString()
85
71
  });
@@ -105,89 +91,6 @@ export class HistoryManager {
105
91
  }
106
92
  }
107
93
 
108
- /**
109
- * Отменить последнюю команду
110
- */
111
- undo() {
112
- if (!this.canUndo()) {
113
-
114
- return false;
115
- }
116
-
117
- const command = this.history[this.currentIndex];
118
-
119
-
120
- this.isExecutingCommand = true;
121
- try {
122
- command.undo();
123
- this.currentIndex--;
124
-
125
- this.eventBus.emit(Events.History.Changed, {
126
- canUndo: this.canUndo(),
127
- canRedo: this.canRedo(),
128
- historySize: this.history.length,
129
- lastUndone: command.toString()
130
- });
131
-
132
-
133
- return true;
134
- } catch (error) {
135
- console.error('❌ Ошибка при отмене команды:', error);
136
- return false;
137
- } finally {
138
- this.isExecutingCommand = false;
139
- }
140
- }
141
-
142
- /**
143
- * Повторить отмененную команду
144
- */
145
- redo() {
146
- if (!this.canRedo()) {
147
-
148
- return false;
149
- }
150
-
151
- this.currentIndex++;
152
- const command = this.history[this.currentIndex];
153
-
154
-
155
- this.isExecutingCommand = true;
156
- try {
157
- this._executeCommandSafely(command);
158
-
159
- this.eventBus.emit(Events.History.Changed, {
160
- canUndo: this.canUndo(),
161
- canRedo: this.canRedo(),
162
- historySize: this.history.length,
163
- lastRedone: command.toString()
164
- });
165
-
166
-
167
- return true;
168
- } catch (error) {
169
- console.error('❌ Ошибка при повторе команды:', error);
170
- this.currentIndex--; // Откатываем индекс при ошибке
171
- return false;
172
- } finally {
173
- this.isExecutingCommand = false;
174
- }
175
- }
176
-
177
- /**
178
- * Можно ли отменить команду
179
- */
180
- canUndo() {
181
- return this.currentIndex >= 0;
182
- }
183
-
184
- /**
185
- * Можно ли повторить команду
186
- */
187
- canRedo() {
188
- return this.currentIndex < this.history.length - 1;
189
- }
190
-
191
94
  /**
192
95
  * Получить последнюю команду
193
96
  */
@@ -204,8 +107,6 @@ export class HistoryManager {
204
107
  this.currentIndex = -1;
205
108
 
206
109
  this.eventBus.emit(Events.History.Changed, {
207
- canUndo: false,
208
- canRedo: false,
209
110
  historySize: 0
210
111
  });
211
112
 
@@ -219,8 +120,6 @@ export class HistoryManager {
219
120
  return {
220
121
  totalCommands: this.history.length,
221
122
  currentIndex: this.currentIndex,
222
- canUndo: this.canUndo(),
223
- canRedo: this.canRedo(),
224
123
  commands: this.history.map((cmd, index) => ({
225
124
  index,
226
125
  isCurrent: index === this.currentIndex,
@@ -238,7 +137,6 @@ export class HistoryManager {
238
137
  console.group('📚 История команд');
239
138
  console.table(info.commands);
240
139
  console.log(`Позиция: ${this.currentIndex + 1}/${this.history.length}`);
241
- console.log(`Undo: ${this.canUndo()}, Redo: ${this.canRedo()}`);
242
140
  console.groupEnd();
243
141
  }
244
142
 
@@ -103,6 +103,11 @@ export class SaveManager {
103
103
  sample: mindmapNodes.slice(0, 5),
104
104
  });
105
105
  }
106
+
107
+ // Жесткий контракт для сохранения картинок:
108
+ // - каждый image обязан иметь imageId
109
+ // - data:/blob: URL в image запрещены
110
+ this._assertImageSaveContract(saveData);
106
111
 
107
112
  // Проверяем, изменились ли данные с последнего сохранения
108
113
  if (this.lastSavedData && JSON.stringify(saveData) === JSON.stringify(this.lastSavedData)) {
@@ -221,6 +226,28 @@ export class SaveManager {
221
226
  return await response.json();
222
227
  }
223
228
 
229
+ _assertImageSaveContract(saveData) {
230
+ const objects = Array.isArray(saveData?.boardData?.objects)
231
+ ? saveData.boardData.objects
232
+ : Array.isArray(saveData?.objects)
233
+ ? saveData.objects
234
+ : [];
235
+
236
+ for (const obj of objects) {
237
+ if (!obj || obj.type !== 'image') continue;
238
+ const imageId = typeof obj.imageId === 'string' ? obj.imageId.trim() : '';
239
+ const topSrc = typeof obj.src === 'string' ? obj.src : '';
240
+ const propSrc = typeof obj.properties?.src === 'string' ? obj.properties.src : '';
241
+
242
+ if (!imageId) {
243
+ throw new Error(`Image object "${obj.id || 'unknown'}" has no imageId. Save is blocked.`);
244
+ }
245
+ if (/^data:/i.test(topSrc) || /^blob:/i.test(topSrc) || /^data:/i.test(propSrc) || /^blob:/i.test(propSrc)) {
246
+ throw new Error(`Image object "${obj.id || 'unknown'}" contains forbidden data/blob src. Save is blocked.`);
247
+ }
248
+ }
249
+ }
250
+
224
251
  _buildSavePayload(boardId, data, csrfToken = undefined) {
225
252
  return {
226
253
  moodboardId: boardId,
@@ -96,8 +96,6 @@ export const Events = {
96
96
  Move: 'keyboard:move',
97
97
  Copy: 'keyboard:copy',
98
98
  Paste: 'keyboard:paste',
99
- Undo: 'keyboard:undo',
100
- Redo: 'keyboard:redo',
101
99
  },
102
100
 
103
101
  Object: {
@@ -19,6 +19,38 @@ export function setupClipboardFlow(core) {
19
19
  };
20
20
  };
21
21
 
22
+ const ensureServerImage = async ({ src, name, imageId }) => {
23
+ if (imageId) {
24
+ return { src, name, imageId };
25
+ }
26
+ if (!core.imageUploadService) {
27
+ alert('Сервис загрузки изображений недоступен. Изображение не добавлено.');
28
+ return null;
29
+ }
30
+ try {
31
+ let uploadResult = null;
32
+ if (typeof src === 'string' && /^data:image\//i.test(src)) {
33
+ uploadResult = await core.imageUploadService.uploadFromDataUrl(src, name || 'clipboard-image.png');
34
+ } else {
35
+ const response = await fetch(src);
36
+ if (!response.ok) {
37
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
38
+ }
39
+ const blob = await response.blob();
40
+ uploadResult = await core.imageUploadService.uploadImage(blob, name || 'clipboard-image');
41
+ }
42
+ return {
43
+ src: uploadResult.url,
44
+ name: uploadResult.name || name,
45
+ imageId: uploadResult.imageId || uploadResult.id
46
+ };
47
+ } catch (error) {
48
+ console.error('Ошибка загрузки вставленного изображения на сервер:', error);
49
+ alert('Ошибка загрузки изображения на сервер. Изображение не добавлено.');
50
+ return null;
51
+ }
52
+ };
53
+
22
54
  core.eventBus.on(Events.UI.CopyObject, ({ objectId }) => {
23
55
  if (!objectId) return;
24
56
  core.copyObject(objectId);
@@ -186,8 +218,10 @@ export function setupClipboardFlow(core) {
186
218
  core._cursor.y = y;
187
219
  });
188
220
 
189
- core.eventBus.on(Events.UI.PasteImage, ({ src, name, imageId }) => {
221
+ core.eventBus.on(Events.UI.PasteImage, async ({ src, name, imageId }) => {
190
222
  if (!src) return;
223
+ const uploaded = await ensureServerImage({ src, name, imageId });
224
+ if (!uploaded?.imageId) return;
191
225
  const view = core.pixi.app.view;
192
226
  const world = core.pixi.worldLayer || core.pixi.app.stage;
193
227
  const s = world?.scale?.x || 1;
@@ -214,18 +248,18 @@ export function setupClipboardFlow(core) {
214
248
  w = 300;
215
249
  h = Math.max(1, Math.round(w / ar));
216
250
  }
217
- const revitPayload = await resolveRevitImagePayload(src, {
251
+ const revitPayload = await resolveRevitImagePayload(uploaded.src, {
218
252
  source: 'clipboard:paste-image',
219
- name
253
+ name: uploaded.name
220
254
  });
221
255
  const properties = {
222
- src,
223
- name,
256
+ src: uploaded.src,
257
+ name: uploaded.name,
224
258
  width: w,
225
259
  height: h,
226
260
  ...revitPayload.properties
227
261
  };
228
- const extraData = imageId ? { imageId } : {};
262
+ const extraData = uploaded.imageId ? { imageId: uploaded.imageId } : {};
229
263
  core.createObject(
230
264
  revitPayload.type,
231
265
  { x: Math.round(worldX - Math.round(w / 2)), y: Math.round(worldY - Math.round(h / 2)) },
@@ -239,14 +273,16 @@ export function setupClipboardFlow(core) {
239
273
  img.decoding = 'async';
240
274
  img.onload = () => { void placeWithAspect(img.naturalWidth || 0, img.naturalHeight || 0); };
241
275
  img.onerror = () => { void placeWithAspect(0, 0); };
242
- img.src = src;
276
+ img.src = uploaded.src;
243
277
  } catch (_) {
244
278
  void placeWithAspect(0, 0);
245
279
  }
246
280
  });
247
281
 
248
- core.eventBus.on(Events.UI.PasteImageAt, ({ x, y, src, name, imageId }) => {
282
+ core.eventBus.on(Events.UI.PasteImageAt, async ({ x, y, src, name, imageId }) => {
249
283
  if (!src) return;
284
+ const uploaded = await ensureServerImage({ src, name, imageId });
285
+ if (!uploaded?.imageId) return;
250
286
  const world = core.pixi.worldLayer || core.pixi.app.stage;
251
287
  const s = world?.scale?.x || 1;
252
288
  const worldX = (x - (world?.x || 0)) / s;
@@ -260,18 +296,18 @@ export function setupClipboardFlow(core) {
260
296
  w = 300;
261
297
  h = Math.max(1, Math.round(w / ar));
262
298
  }
263
- const revitPayload = await resolveRevitImagePayload(src, {
299
+ const revitPayload = await resolveRevitImagePayload(uploaded.src, {
264
300
  source: 'clipboard:paste-image-at',
265
- name
301
+ name: uploaded.name
266
302
  });
267
303
  const properties = {
268
- src,
269
- name,
304
+ src: uploaded.src,
305
+ name: uploaded.name,
270
306
  width: w,
271
307
  height: h,
272
308
  ...revitPayload.properties
273
309
  };
274
- const extraData = imageId ? { imageId } : {};
310
+ const extraData = uploaded.imageId ? { imageId: uploaded.imageId } : {};
275
311
  core.createObject(
276
312
  revitPayload.type,
277
313
  { x: Math.round(worldX - Math.round(w / 2)), y: Math.round(worldY - Math.round(h / 2)) },
@@ -285,7 +321,7 @@ export function setupClipboardFlow(core) {
285
321
  img.decoding = 'async';
286
322
  img.onload = () => { void placeWithAspect(img.naturalWidth || 0, img.naturalHeight || 0); };
287
323
  img.onerror = () => { void placeWithAspect(0, 0); };
288
- img.src = src;
324
+ img.src = uploaded.src;
289
325
  } catch (_) {
290
326
  void placeWithAspect(0, 0);
291
327
  }
@@ -23,6 +23,11 @@ export function setupSaveFlow(core) {
23
23
  });
24
24
 
25
25
  core.eventBus.on(Events.Save.Success, async () => {
26
+ if (typeof core.revealPendingObjectsAfterSave === 'function') {
27
+ core.revealPendingObjectsAfterSave();
28
+ } else if (typeof core.revealPendingImageObjectsAfterSave === 'function') {
29
+ core.revealPendingImageObjectsAfterSave();
30
+ }
26
31
  // ВРЕМЕННО ОТКЛЮЧЕНО:
27
32
  // cleanup-фича требует доработки контракта и серверной поддержки.
28
33
  // Автоматический вызов удален, чтобы не запускать cleanup после сохранения.
package/src/core/index.js CHANGED
@@ -66,6 +66,8 @@ export class CoreMoodBoard {
66
66
  csrfToken: this.options.csrfToken
67
67
  });
68
68
  this.gridSnapResolver = new GridSnapResolver(this);
69
+ // Объекты, требующие подтверждения сохранения (image/file), показываем только после save:success.
70
+ this._pendingPersistAckVisibilityIds = new Set();
69
71
 
70
72
  // Связываем SaveManager с ApiClient для правильной обработки изображений
71
73
  this.saveManager.setApiClient(this.apiClient);
@@ -435,9 +437,39 @@ export class CoreMoodBoard {
435
437
  const command = new CreateObjectCommand(this, objectData);
436
438
  this.history.executeCommand(command);
437
439
 
440
+ // Строгий UX-контракт: image/file появляются только после успешного сохранения.
441
+ if (this._isPersistAckRequiredType(type)) {
442
+ this._pendingPersistAckVisibilityIds.add(objectData.id);
443
+ this._setObjectVisibility(objectData.id, false);
444
+ }
445
+
438
446
  return objectData;
439
447
  }
440
448
 
449
+ _isPersistAckRequiredType(type) {
450
+ return type === 'image' || type === 'revit-screenshot-img' || type === 'file';
451
+ }
452
+
453
+ _setObjectVisibility(objectId, visible) {
454
+ const pixiObject = this.pixi?.objects?.get?.(objectId);
455
+ if (pixiObject) {
456
+ pixiObject.visible = !!visible;
457
+ }
458
+ }
459
+
460
+ revealPendingObjectsAfterSave() {
461
+ if (!this._pendingPersistAckVisibilityIds || this._pendingPersistAckVisibilityIds.size === 0) return;
462
+ for (const objectId of this._pendingPersistAckVisibilityIds) {
463
+ this._setObjectVisibility(objectId, true);
464
+ }
465
+ this._pendingPersistAckVisibilityIds.clear();
466
+ }
467
+
468
+ // Backward-compat alias for tests/integrations created in previous step.
469
+ revealPendingImageObjectsAfterSave() {
470
+ this.revealPendingObjectsAfterSave();
471
+ }
472
+
441
473
  // === Прикрепления к фреймам ===
442
474
  // Логика фреймов перенесена в FrameService
443
475
 
@@ -16,11 +16,11 @@ export class KeyboardClipboardImagePaste {
16
16
  imageId: uploadResult.imageId || uploadResult.id
17
17
  });
18
18
  } else {
19
- this.eventBus.emit(Events.UI.PasteImage, { src: dataUrl, name: fileName });
19
+ alert('Сервис загрузки изображений недоступен. Изображение не добавлено.');
20
20
  }
21
21
  } catch (error) {
22
22
  console.error('Ошибка загрузки изображения:', error);
23
- this.eventBus.emit(Events.UI.PasteImage, { src: dataUrl, name: fileName });
23
+ alert('Ошибка загрузки изображения на сервер. Изображение не добавлено.');
24
24
  }
25
25
  }
26
26
 
@@ -34,29 +34,11 @@ export class KeyboardClipboardImagePaste {
34
34
  imageId: uploadResult.imageId || uploadResult.id
35
35
  });
36
36
  } else {
37
- const reader = new FileReader();
38
- reader.onload = () => {
39
- this.eventBus.emit(Events.UI.PasteImage, {
40
- src: reader.result,
41
- name: fileName
42
- });
43
- };
44
- reader.readAsDataURL(file);
37
+ alert('Сервис загрузки изображений недоступен. Изображение не добавлено.');
45
38
  }
46
39
  } catch (error) {
47
40
  console.error('Ошибка загрузки файла изображения:', error);
48
- try {
49
- const reader = new FileReader();
50
- reader.onload = () => {
51
- this.eventBus.emit(Events.UI.PasteImage, {
52
- src: reader.result,
53
- name: fileName
54
- });
55
- };
56
- reader.readAsDataURL(file);
57
- } catch (fallbackError) {
58
- console.error('Критическая ошибка при чтении файла:', fallbackError);
59
- }
41
+ alert('Ошибка загрузки изображения на сервер. Изображение не добавлено.');
60
42
  }
61
43
  }
62
44
 
@@ -105,10 +87,7 @@ export class KeyboardClipboardImagePaste {
105
87
  const dataUrl = await this._blobToDataUrl(blob);
106
88
  this.handleImageUpload(dataUrl, srcInHtml.split('/').pop() || 'image');
107
89
  } catch (_) {
108
- this.eventBus.emit(Events.UI.PasteImage, {
109
- src: srcInHtml,
110
- name: srcInHtml.split('/').pop() || 'image'
111
- });
90
+ alert('Не удалось загрузить изображение из URL. Изображение не добавлено.');
112
91
  }
113
92
  return;
114
93
  }
@@ -151,10 +130,7 @@ export class KeyboardClipboardImagePaste {
151
130
  this.handleImageUpload(dataUrl, trimmed.split('/').pop() || 'image');
152
131
  return;
153
132
  } catch (_) {
154
- this.eventBus.emit(Events.UI.PasteImage, {
155
- src: trimmed,
156
- name: trimmed.split('/').pop() || 'image'
157
- });
133
+ alert('Не удалось загрузить изображение из URL. Изображение не добавлено.');
158
134
  return;
159
135
  }
160
136
  }
@@ -339,28 +339,13 @@ export class ToolEventRouter {
339
339
  index
340
340
  };
341
341
  } else {
342
- const localSrc = await new Promise((resolve) => {
343
- const reader = new FileReader();
344
- reader.onload = () => {
345
- resolve(reader.result);
346
- };
347
- reader.readAsDataURL(file);
348
- });
349
- if (!isCurrentDrop()) {
350
- logDropDebug(diagnostics, 'image_local_fallback_stale_drop_ignored', {
351
- fileName: file.name || 'image'
352
- });
353
- return null;
354
- }
355
- logDropDebug(diagnostics, 'image_local_fallback_success', {
356
- fileName: file.name || 'image'
357
- });
358
- return {
359
- src: localSrc,
360
- name: file.name || 'image',
361
- imageId: null,
362
- index
363
- };
342
+ showDropWarning(
343
+ manager,
344
+ `Не удалось добавить "${file.name || 'image'}": сервис загрузки изображений недоступен`,
345
+ diagnostics,
346
+ { fileName: file.name || 'image' }
347
+ );
348
+ return null;
364
349
  }
365
350
  } catch (error) {
366
351
  console.warn('Ошибка загрузки изображения через drag-and-drop:', error);
@@ -368,28 +353,16 @@ export class ToolEventRouter {
368
353
  fileName: file.name || 'image',
369
354
  message: error?.message || String(error)
370
355
  });
371
- const fallbackSrc = await new Promise((resolve) => {
372
- const reader = new FileReader();
373
- reader.onload = () => {
374
- resolve(reader.result);
375
- };
376
- reader.readAsDataURL(file);
377
- });
378
- if (!isCurrentDrop()) {
379
- logDropDebug(diagnostics, 'image_error_fallback_stale_drop_ignored', {
380
- fileName: file.name || 'image'
381
- });
382
- return null;
383
- }
384
- logDropDebug(diagnostics, 'image_error_fallback_success', {
385
- fileName: file.name || 'image'
386
- });
387
- return {
388
- src: fallbackSrc,
389
- name: file.name || 'image',
390
- imageId: null,
391
- index
392
- };
356
+ showDropWarning(
357
+ manager,
358
+ `Не удалось загрузить "${file.name || 'image'}" на сервер. Изображение не добавлено.`,
359
+ diagnostics,
360
+ {
361
+ fileName: file.name || 'image',
362
+ message: error?.message || String(error)
363
+ }
364
+ );
365
+ return null;
393
366
  }
394
367
  });
395
368
  for (const placement of imagePlacements) {
@@ -461,21 +434,13 @@ export class ToolEventRouter {
461
434
  fileId: uploadResult.fileId || uploadResult.id || null
462
435
  };
463
436
  } else {
464
- if (!isCurrentDrop()) {
465
- logDropDebug(diagnostics, 'file_local_fallback_stale_drop_ignored', {
466
- fileName: fallbackProps.fileName
467
- });
468
- return null;
469
- }
470
- logDropDebug(diagnostics, 'file_local_fallback_success', {
471
- fileName: fallbackProps.fileName
472
- });
473
- return {
474
- type: 'file',
475
- id: 'file',
476
- position,
477
- properties: fallbackProps
478
- };
437
+ showDropWarning(
438
+ manager,
439
+ `Не удалось добавить "${fallbackProps.fileName}": сервис загрузки файлов недоступен`,
440
+ diagnostics,
441
+ { fileName: fallbackProps.fileName }
442
+ );
443
+ return null;
479
444
  }
480
445
  } catch (error) {
481
446
  console.warn('Ошибка загрузки файла через drag-and-drop:', error);
@@ -483,21 +448,16 @@ export class ToolEventRouter {
483
448
  fileName: fallbackProps.fileName,
484
449
  message: error?.message || String(error)
485
450
  });
486
- if (!isCurrentDrop()) {
487
- logDropDebug(diagnostics, 'file_error_fallback_stale_drop_ignored', {
488
- fileName: fallbackProps.fileName
489
- });
490
- return null;
491
- }
492
- logDropDebug(diagnostics, 'file_error_fallback_success', {
493
- fileName: fallbackProps.fileName
494
- });
495
- return {
496
- type: 'file',
497
- id: 'file',
498
- position,
499
- properties: fallbackProps
500
- };
451
+ showDropWarning(
452
+ manager,
453
+ `Не удалось загрузить "${fallbackProps.fileName}" на сервер. Файл не добавлен.`,
454
+ diagnostics,
455
+ {
456
+ fileName: fallbackProps.fileName,
457
+ message: error?.message || String(error)
458
+ }
459
+ );
460
+ return null;
501
461
  }
502
462
  });
503
463
  for (const actionPayload of filePlacements) {
@@ -225,18 +225,8 @@ export class PlacementTool extends BaseTool {
225
225
 
226
226
  } catch (uploadError) {
227
227
  console.error('Ошибка загрузки файла на сервер:', uploadError);
228
- // Fallback: создаем объект файла с локальными данными
229
- this.payloadFactory.emitFileFallback(
230
- position,
231
- this.selectedFile.fileName,
232
- this.selectedFile.fileSize,
233
- this.selectedFile.mimeType,
234
- props.width || 120,
235
- props.height || 140
236
- );
237
-
238
- // Показываем предупреждение пользователю
239
- alert('Ошибка загрузки файла на сервер. Файл добавлен локально.');
228
+ // Строгий режим: локальное сохранение файлов запрещено.
229
+ alert('Ошибка загрузки файла на сервер. Файл не добавлен.');
240
230
  }
241
231
 
242
232
  // Убираем призрак и возвращаемся к инструменту выделения
@@ -341,23 +331,9 @@ export class PlacementTool extends BaseTool {
341
331
 
342
332
  } catch (uploadError) {
343
333
  console.error('Ошибка загрузки изображения на сервер:', uploadError);
344
-
345
- // Fallback: создаем объект изображения с локальными данными
346
- const imageUrl = URL.createObjectURL(this.selectedImage.file);
347
- const targetW = this.selectedImage.properties.width || 300;
348
- const targetH = this.selectedImage.properties.height || 200;
349
-
350
- const halfW = targetW / 2;
351
- const halfH = targetH / 2;
352
- const position = {
353
- x: Math.round(worldPoint.x - halfW),
354
- y: Math.round(worldPoint.y - halfH)
355
- };
356
-
357
- this.payloadFactory.emitImageFallback(position, imageUrl, this.selectedImage.fileName, targetW, targetH, objectType, extraProperties);
358
-
359
- // Показываем предупреждение пользователю
360
- alert('Ошибка загрузки изображения на сервер. Изображение добавлено локально.');
334
+
335
+ // Строгий режим: локальное сохранение изображений запрещено.
336
+ alert('Ошибка загрузки изображения на сервер. Изображение не добавлено.');
361
337
  }
362
338
 
363
339
  // Убираем призрак и возвращаемся к инструменту выделения
@@ -227,13 +227,8 @@ export class PlacementInputRouter {
227
227
  host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
228
228
  } catch (uploadError) {
229
229
  console.error('Ошибка загрузки файла на сервер:', uploadError);
230
- const fileName = file.name;
231
- const fileSize = file.size;
232
- const mimeType = file.type;
233
-
234
- host.payloadFactory.emitFileFallback(position, fileName, fileSize, mimeType, props.width || 120, props.height || 140);
235
230
  host.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
236
- alert('Ошибка загрузки файла на сервер. Файл добавлен локально.');
231
+ alert('Ошибка загрузки файла на сервер. Файл не добавлен.');
237
232
  }
238
233
  } catch (error) {
239
234
  console.error('Ошибка при выборе файла:', error);
@@ -63,21 +63,6 @@ export class PlacementPayloadFactory {
63
63
  });
64
64
  }
65
65
 
66
- emitImageFallback(position, imageUrl, fileName, width, height, objectType = 'image', extraProperties = {}) {
67
- this.host.eventBus.emit(Events.UI.ToolbarAction, {
68
- type: objectType,
69
- id: objectType,
70
- position,
71
- properties: {
72
- src: imageUrl,
73
- name: fileName,
74
- width,
75
- height,
76
- ...extraProperties
77
- }
78
- });
79
- }
80
-
81
66
  emitFileUploaded(position, uploadResult, width, height) {
82
67
  this.host.eventBus.emit(Events.UI.ToolbarAction, {
83
68
  type: 'file',
@@ -96,18 +81,4 @@ export class PlacementPayloadFactory {
96
81
  });
97
82
  }
98
83
 
99
- emitFileFallback(position, fileName, fileSize, mimeType, width, height) {
100
- this.host.eventBus.emit(Events.UI.ToolbarAction, {
101
- type: 'file',
102
- id: 'file',
103
- position,
104
- properties: {
105
- fileName,
106
- fileSize,
107
- mimeType,
108
- width,
109
- height
110
- }
111
- });
112
- }
113
84
  }