@yogiswara/honcho-editor-ui 2.6.6 → 2.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -120,22 +120,49 @@ export function useAdjustmentHistoryBatch(options = {}) {
120
120
  const [selectedIds, setSelectedIds] = useState([]);
121
121
  const [imageHistories, setImageHistories] = useState([]);
122
122
  const [currentBatch, setCurrentBatch] = useState(createEmptyBatchState());
123
+ const [lastUpdateTimestamp, setLastUpdateTimestamp] = useState(Date.now());
123
124
  // Configuration refs
124
125
  const maxSizeRef = useRef(internalOptions.maxSize);
125
126
  const devWarningsRef = useRef(internalOptions.devWarnings);
126
127
  // Helper function to rebuild currentBatch from imageHistories
127
128
  const rebuildCurrentBatch = useCallback(() => {
129
+ console.log('[useAdjustmentHistoryBatch] 🔧 rebuildCurrentBatch called with imageHistories:', imageHistories.map(h => ({
130
+ imageId: h.imageId,
131
+ currentHistoryEntryId: h.currentHistoryEntryId,
132
+ historyLength: h.history.length,
133
+ historyIds: h.history.map(entry => entry.id)
134
+ })));
128
135
  const newBatch = createEmptyBatchState();
129
136
  imageHistories.forEach(imageHistory => {
130
137
  // Find current adjustment using currentHistoryEntryId
131
138
  const currentEntry = imageHistory.history.find(entry => entry.id === imageHistory.currentHistoryEntryId);
139
+ console.log(`[useAdjustmentHistoryBatch] 🔍 rebuildCurrentBatch for image ${imageHistory.imageId}:`, {
140
+ currentHistoryEntryId: imageHistory.currentHistoryEntryId,
141
+ foundCurrentEntry: !!currentEntry,
142
+ historyLength: imageHistory.history.length,
143
+ historyIds: imageHistory.history.map(entry => entry.id)
144
+ });
132
145
  if (currentEntry) {
133
146
  newBatch.allImages[imageHistory.imageId] = currentEntry.adjustment;
134
147
  if (selectedIds.includes(imageHistory.imageId)) {
135
148
  newBatch.currentSelection[imageHistory.imageId] = currentEntry.adjustment;
136
149
  }
150
+ console.log(`[useAdjustmentHistoryBatch] ✅ Successfully rebuilt batch for image ${imageHistory.imageId}`);
151
+ }
152
+ else {
153
+ console.error(`[useAdjustmentHistoryBatch] ❌ CRITICAL: Current entry not found for image ${imageHistory.imageId}!`, {
154
+ searchingFor: imageHistory.currentHistoryEntryId,
155
+ availableIds: imageHistory.history.map(entry => entry.id),
156
+ historyLength: imageHistory.history.length
157
+ });
137
158
  }
138
159
  });
160
+ console.log('[useAdjustmentHistoryBatch] 🔧 rebuildCurrentBatch result:', {
161
+ allImagesCount: Object.keys(newBatch.allImages).length,
162
+ currentSelectionCount: Object.keys(newBatch.currentSelection).length,
163
+ allImagesIds: Object.keys(newBatch.allImages),
164
+ currentSelectionIds: Object.keys(newBatch.currentSelection)
165
+ });
139
166
  return newBatch;
140
167
  }, [imageHistories, selectedIds]);
141
168
  // Sync currentBatch with imageHistories
@@ -184,76 +211,105 @@ export function useAdjustmentHistoryBatch(options = {}) {
184
211
  }
185
212
  return;
186
213
  }
187
- // Store backend operations to perform after state update
214
+ // Prepare backend operations BEFORE state update (outside setImageHistories)
188
215
  const backendOperations = [];
189
- setImageHistories(prevHistories => {
190
- return prevHistories.map(imageHistory => {
191
- if (!selectedIds.includes(imageHistory.imageId)) {
192
- return imageHistory; // No change for unselected images
216
+ // Process each image to prepare operations
217
+ const operationsToApply = [];
218
+ imageHistories.forEach(imageHistory => {
219
+ if (!selectedIds.includes(imageHistory.imageId)) {
220
+ return; // Skip images not being adjusted
221
+ }
222
+ // Get current adjustment from current entry
223
+ const currentEntry = imageHistory.history.find(entry => entry.id === imageHistory.currentHistoryEntryId);
224
+ const currentAdjustment = currentEntry?.adjustment || createDefaultAdjustmentState(internalOptions.defaultAdjustmentState);
225
+ // Apply deltas with clamping
226
+ const newAdjustment = { ...currentAdjustment };
227
+ Object.keys(delta).forEach(key => {
228
+ const deltaValue = delta[key];
229
+ if (typeof deltaValue === 'number') {
230
+ const currentValue = newAdjustment[key];
231
+ newAdjustment[key] = Math.max(-100, Math.min(100, currentValue + deltaValue));
193
232
  }
194
- // Get current adjustment from current entry
195
- const currentEntry = imageHistory.history.find(entry => entry.id === imageHistory.currentHistoryEntryId);
196
- const currentAdjustment = currentEntry?.adjustment || createDefaultAdjustmentState(internalOptions.defaultAdjustmentState);
197
- // Apply deltas with clamping
198
- const newAdjustment = { ...currentAdjustment };
199
- Object.keys(delta).forEach(key => {
200
- const deltaValue = delta[key];
201
- if (typeof deltaValue === 'number') {
202
- const currentValue = newAdjustment[key];
203
- newAdjustment[key] = Math.max(-100, Math.min(100, currentValue + deltaValue));
204
- }
233
+ });
234
+ // Check if user is in the middle of history (not at latest state)
235
+ const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
236
+ const isInMiddleOfHistory = currentEntryIndex < imageHistory.history.length - 1;
237
+ let replaceFromTaskId;
238
+ console.log(`[useAdjustmentHistoryBatch] đŸŽ¯ Delta replace logic for image ${imageHistory.imageId}:`, {
239
+ currentEntryIndex,
240
+ historyLength: imageHistory.history.length,
241
+ isInMiddleOfHistory,
242
+ currentEntryId: currentEntry?.id
243
+ });
244
+ if (isInMiddleOfHistory) {
245
+ // If user is in middle of history, get the task ID of current position
246
+ replaceFromTaskId = currentEntry?.id;
247
+ console.log(`[useAdjustmentHistoryBatch] 🔄 DELTA will REPLACE current position. replaceFromTaskId=${replaceFromTaskId}`);
248
+ }
249
+ else {
250
+ console.log(`[useAdjustmentHistoryBatch] ➕ DELTA will ADD new entry (at end of history)`);
251
+ }
252
+ // Generate new task ID for backend (same as entry ID)
253
+ const taskId = generateTaskId();
254
+ // Create new entry with task ID
255
+ const newEntryId = taskId; // Use the same ID for both entry and task
256
+ const newEntry = {
257
+ id: newEntryId,
258
+ adjustment: newAdjustment
259
+ };
260
+ // Build new history
261
+ let newHistory;
262
+ if (isInMiddleOfHistory) {
263
+ // If in middle of history, truncate from current position and add new entry
264
+ newHistory = [...imageHistory.history.slice(0, currentEntryIndex + 1), newEntry];
265
+ }
266
+ else {
267
+ // If at end of history, just add new entry
268
+ newHistory = [...imageHistory.history, newEntry];
269
+ }
270
+ // Trim if needed
271
+ const maxSize = maxSizeRef.current;
272
+ const trimmedHistory = typeof maxSize === 'number' && newHistory.length > maxSize
273
+ ? newHistory.slice(-maxSize)
274
+ : newHistory;
275
+ // Store operation for state update
276
+ operationsToApply.push({
277
+ imageId: imageHistory.imageId,
278
+ newEntryId,
279
+ newHistory: trimmedHistory
280
+ });
281
+ // Prepare backend operation (OUTSIDE state setter)
282
+ if (internalOptions.controller && internalOptions.firebaseUid) {
283
+ backendOperations.push({
284
+ imageId: imageHistory.imageId,
285
+ taskId,
286
+ adjustment: newAdjustment,
287
+ replaceFromTaskId
205
288
  });
206
- // Check if user is in the middle of history (not at latest state)
207
- const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
208
- const isInMiddleOfHistory = currentEntryIndex < imageHistory.history.length - 1;
209
- let replaceFromTaskId;
210
- if (isInMiddleOfHistory) {
211
- // If user is in middle of history, get the task ID of current position
212
- replaceFromTaskId = currentEntry?.id;
213
- }
214
- // Generate new task ID for backend (same as entry ID)
215
- const taskId = generateTaskId();
216
- // Create new entry with task ID
217
- const newEntryId = taskId; // Use the same ID for both entry and task
218
- const newEntry = {
219
- id: newEntryId,
220
- adjustment: newAdjustment
221
- };
222
- // Prepare backend operation
223
- if (internalOptions.controller && internalOptions.firebaseUid) {
224
- backendOperations.push({
225
- imageId: imageHistory.imageId,
226
- taskId,
227
- adjustment: newAdjustment,
228
- replaceFromTaskId
229
- });
230
- }
231
- // Build new history
232
- let newHistory;
233
- if (isInMiddleOfHistory) {
234
- // If in middle of history, truncate from current position and add new entry
235
- newHistory = [...imageHistory.history.slice(0, currentEntryIndex + 1), newEntry];
236
- }
237
- else {
238
- // If at end of history, just add new entry
239
- newHistory = [...imageHistory.history, newEntry];
289
+ }
290
+ });
291
+ // Now update state with prepared operations (no backend preparation inside)
292
+ setImageHistories(prevHistories => {
293
+ return prevHistories.map(imageHistory => {
294
+ // Find operation for this image
295
+ const operation = operationsToApply.find(op => op.imageId === imageHistory.imageId);
296
+ if (!operation) {
297
+ return imageHistory; // No change for images not being adjusted
240
298
  }
241
- // Trim if needed
242
- const maxSize = maxSizeRef.current;
243
- const trimmedHistory = typeof maxSize === 'number' && newHistory.length > maxSize
244
- ? newHistory.slice(-maxSize)
245
- : newHistory;
299
+ console.log(`[useAdjustmentHistoryBatch] 📝 Applying state update for delta on image ${imageHistory.imageId}`);
246
300
  return {
247
301
  ...imageHistory,
248
- history: trimmedHistory,
249
- currentHistoryEntryId: newEntryId // Update current pointer
302
+ history: operation.newHistory,
303
+ currentHistoryEntryId: operation.newEntryId // Update current pointer
250
304
  };
251
305
  });
252
306
  });
253
- // Perform backend operations asynchronously
307
+ // Perform backend operations asynchronously (already prepared)
254
308
  if (backendOperations.length > 0 && internalOptions.controller && internalOptions.firebaseUid) {
255
309
  try {
310
+ console.log(`[useAdjustmentHistoryBatch] 📤 Syncing ${backendOperations.length} adjustments to backend (createEditorConfig for each image)`);
256
311
  const promises = backendOperations.map(async (operation) => {
312
+ console.log(`[useAdjustmentHistoryBatch] 🔄 Calling createEditorConfig for image ${operation.imageId} with taskId ${operation.taskId}`);
257
313
  await internalOptions.controller.createEditorConfig(internalOptions.firebaseUid, {
258
314
  gallery_id: operation.imageId,
259
315
  task_id: operation.taskId,
@@ -263,89 +319,118 @@ export function useAdjustmentHistoryBatch(options = {}) {
263
319
  });
264
320
  await Promise.all(promises);
265
321
  if (devWarningsRef.current) {
266
- console.log(`[useAdjustmentHistoryBatch] Synced ${backendOperations.length} adjustments to backend`);
322
+ console.log(`[useAdjustmentHistoryBatch] ✅ Successfully synced ${backendOperations.length} adjustments to backend`);
267
323
  }
268
324
  }
269
325
  catch (error) {
270
- console.error('[useAdjustmentHistoryBatch] Failed to sync adjustments to backend:', error);
326
+ console.error('[useAdjustmentHistoryBatch] ❌ Failed to sync adjustments to backend:', error);
271
327
  }
272
328
  }
273
- }, [selectedIds, internalOptions]);
274
- // Apply preset values directly to selected images - preserves history for each image individually
275
- const adjustSelectedWithPreset = useCallback(async (presetAdjustments) => {
276
- if (selectedIds.length === 0) {
329
+ }, [selectedIds, internalOptions, imageHistories]);
330
+ // Shared function for applying adjustments (used by both reset and preset)
331
+ const applyAdjustmentToSelected = useCallback(async (adjustment, operationType, targetImageIds) => {
332
+ const idsToProcess = targetImageIds || selectedIds;
333
+ if (idsToProcess.length === 0) {
277
334
  if (devWarningsRef.current) {
278
- console.warn('[useAdjustmentHistoryBatch] adjustSelectedWithPreset called with no selection');
335
+ console.warn(`[useAdjustmentHistoryBatch] ❌ ${operationType} called with no images to process`);
279
336
  }
280
337
  return;
281
338
  }
282
- // Store backend operations to perform after state update
339
+ console.log(`[useAdjustmentHistoryBatch] 🔄 ${operationType.toUpperCase()} called for images:`, idsToProcess);
340
+ // Prepare backend operations BEFORE state update (outside setImageHistories)
283
341
  const backendOperations = [];
342
+ // Process each image to prepare operations
343
+ const operationsToApply = [];
344
+ imageHistories.forEach(imageHistory => {
345
+ if (!idsToProcess.includes(imageHistory.imageId)) {
346
+ return; // Skip images not being processed
347
+ }
348
+ console.log(`[useAdjustmentHistoryBatch] 🔄 Processing ${operationType} for image ${imageHistory.imageId}`);
349
+ // Get current entry for replace_from logic
350
+ const currentEntry = imageHistory.history.find(entry => entry.id === imageHistory.currentHistoryEntryId);
351
+ const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
352
+ const isInMiddleOfHistory = currentEntryIndex < imageHistory.history.length - 1;
353
+ let replaceFromTaskId;
354
+ console.log(`[useAdjustmentHistoryBatch] đŸŽ¯ ${operationType.toUpperCase()} replace logic for image ${imageHistory.imageId}:`, {
355
+ currentEntryIndex,
356
+ historyLength: imageHistory.history.length,
357
+ isInMiddleOfHistory,
358
+ currentEntryId: currentEntry?.id
359
+ });
360
+ if (isInMiddleOfHistory && currentEntry?.id) {
361
+ // Only use replace_from when truly in middle of history (not at latest)
362
+ replaceFromTaskId = currentEntry.id;
363
+ console.log(`[useAdjustmentHistoryBatch] 🔄 ${operationType.toUpperCase()} will REPLACE current position. replaceFromTaskId=${replaceFromTaskId}`);
364
+ }
365
+ else {
366
+ console.log(`[useAdjustmentHistoryBatch] ➕ ${operationType.toUpperCase()} will ADD new entry (at end of history)`);
367
+ }
368
+ // Create new entry with adjustment
369
+ const newEntryId = generateEntryId();
370
+ const newEntry = {
371
+ id: newEntryId,
372
+ adjustment
373
+ };
374
+ // Build new history
375
+ let newHistory;
376
+ if (isInMiddleOfHistory) {
377
+ // If in middle of history, truncate from current position and add new entry
378
+ newHistory = [...imageHistory.history.slice(0, currentEntryIndex + 1), newEntry];
379
+ }
380
+ else {
381
+ // If at end of history, just add new entry
382
+ newHistory = [...imageHistory.history, newEntry];
383
+ }
384
+ // Trim if needed
385
+ const maxSize = maxSizeRef.current;
386
+ const trimmedHistory = typeof maxSize === 'number' && newHistory.length > maxSize
387
+ ? newHistory.slice(-maxSize)
388
+ : newHistory;
389
+ // Store operation for state update
390
+ operationsToApply.push({
391
+ imageId: imageHistory.imageId,
392
+ newEntryId,
393
+ newHistory: trimmedHistory
394
+ });
395
+ // Prepare backend operation (OUTSIDE state setter)
396
+ if (internalOptions.controller && internalOptions.firebaseUid) {
397
+ console.log(`[useAdjustmentHistoryBatch] ✅ Adding ${operationType.toUpperCase()} backend operation for image ${imageHistory.imageId} with taskId ${newEntryId}`, {
398
+ replaceFromTaskId,
399
+ willReplace: !!replaceFromTaskId
400
+ });
401
+ backendOperations.push({
402
+ imageId: imageHistory.imageId,
403
+ taskId: newEntryId,
404
+ adjustment,
405
+ replaceFromTaskId
406
+ });
407
+ }
408
+ });
409
+ // Now update state with prepared operations (no backend preparation inside)
284
410
  setImageHistories(prevHistories => {
285
411
  return prevHistories.map(imageHistory => {
286
- if (!selectedIds.includes(imageHistory.imageId)) {
287
- return imageHistory; // No change for unselected images
288
- }
289
- // Get current adjustment from current entry
290
- const currentEntry = imageHistory.history.find(entry => entry.id === imageHistory.currentHistoryEntryId);
291
- const currentAdjustment = currentEntry?.adjustment || createDefaultAdjustmentState(internalOptions.defaultAdjustmentState);
292
- // Apply preset values with clamping (same as adjustSelected logic)
293
- const newAdjustment = { ...presetAdjustments };
294
- Object.keys(newAdjustment).forEach(key => {
295
- const presetValue = newAdjustment[key];
296
- newAdjustment[key] = Math.max(-100, Math.min(100, presetValue));
297
- });
298
- // Check if user is in the middle of history (not at latest state)
299
- const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
300
- const isInMiddleOfHistory = currentEntryIndex < imageHistory.history.length - 1;
301
- let replaceFromTaskId;
302
- if (isInMiddleOfHistory) {
303
- // If user is in middle of history, get the task ID of current position
304
- replaceFromTaskId = currentEntry?.id;
305
- }
306
- // Generate new task ID for backend (same as entry ID)
307
- const taskId = generateTaskId();
308
- // Create new entry with task ID
309
- const newEntryId = taskId; // Use the same ID for both entry and task
310
- const newEntry = {
311
- id: newEntryId,
312
- adjustment: newAdjustment
313
- };
314
- // Prepare backend operation
315
- if (internalOptions.controller && internalOptions.firebaseUid) {
316
- backendOperations.push({
317
- imageId: imageHistory.imageId,
318
- taskId,
319
- adjustment: newAdjustment,
320
- replaceFromTaskId
321
- });
322
- }
323
- // Build new history
324
- let newHistory;
325
- if (isInMiddleOfHistory) {
326
- // If in middle of history, truncate from current position and add new entry
327
- newHistory = [...imageHistory.history.slice(0, currentEntryIndex + 1), newEntry];
412
+ // Find operation for this image
413
+ const operation = operationsToApply.find(op => op.imageId === imageHistory.imageId);
414
+ if (!operation) {
415
+ return imageHistory; // No change for images not being processed
328
416
  }
329
- else {
330
- // If at end of history, just add new entry
331
- newHistory = [...imageHistory.history, newEntry];
332
- }
333
- // Trim if needed
334
- const maxSize = maxSizeRef.current;
335
- const trimmedHistory = typeof maxSize === 'number' && newHistory.length > maxSize
336
- ? newHistory.slice(-maxSize)
337
- : newHistory;
417
+ console.log(`[useAdjustmentHistoryBatch] 📝 Applying state update for ${operationType} on image ${imageHistory.imageId}`);
338
418
  return {
339
419
  ...imageHistory,
340
- history: trimmedHistory,
341
- currentHistoryEntryId: newEntryId // Update current pointer
420
+ history: operation.newHistory,
421
+ currentHistoryEntryId: operation.newEntryId // Update current pointer
342
422
  };
343
423
  });
344
424
  });
345
- // Perform backend operations asynchronously
425
+ // Perform backend operations asynchronously (already prepared)
346
426
  if (backendOperations.length > 0 && internalOptions.controller && internalOptions.firebaseUid) {
347
427
  try {
428
+ console.log(`[useAdjustmentHistoryBatch] 📤 Syncing ${backendOperations.length} ${operationType} operations to backend (createEditorConfig for each image)`);
348
429
  const promises = backendOperations.map(async (operation) => {
430
+ console.log(`[useAdjustmentHistoryBatch] 🔄 Calling createEditorConfig for ${operationType} on image ${operation.imageId} with taskId ${operation.taskId}`, {
431
+ replaceFrom: operation.replaceFromTaskId,
432
+ hasReplaceFrom: !!operation.replaceFromTaskId
433
+ });
349
434
  await internalOptions.controller.createEditorConfig(internalOptions.firebaseUid, {
350
435
  gallery_id: operation.imageId,
351
436
  task_id: operation.taskId,
@@ -355,14 +440,48 @@ export function useAdjustmentHistoryBatch(options = {}) {
355
440
  });
356
441
  await Promise.all(promises);
357
442
  if (devWarningsRef.current) {
358
- console.log(`[useAdjustmentHistoryBatch] Synced ${backendOperations.length} preset adjustments to backend`);
443
+ console.log(`[useAdjustmentHistoryBatch] ✅ Successfully synced ${backendOperations.length} ${operationType} operations to backend`);
359
444
  }
360
445
  }
361
446
  catch (error) {
362
- console.error('[useAdjustmentHistoryBatch] Failed to sync preset adjustments to backend:', error);
447
+ console.error(`[useAdjustmentHistoryBatch] ❌ Failed to sync ${operationType} to backend:`, error);
363
448
  }
364
449
  }
365
- }, [selectedIds, internalOptions]);
450
+ console.log(`[useAdjustmentHistoryBatch] 📊 ${operationType.toUpperCase()} backend operations prepared: ${backendOperations.length}`);
451
+ }, [selectedIds, internalOptions, imageHistories]);
452
+ // Apply preset values directly to selected images - now uses shared logic
453
+ const adjustSelectedWithPreset = useCallback(async (presetAdjustments) => {
454
+ console.log('[useAdjustmentHistoryBatch] 🎨 adjustSelectedWithPreset called with:', {
455
+ selectedIds,
456
+ selectedCount: selectedIds.length,
457
+ presetAdjustments,
458
+ hasController: !!internalOptions.controller,
459
+ hasFirebaseUid: !!internalOptions.firebaseUid,
460
+ imageHistoriesCount: imageHistories.length,
461
+ imageHistoriesIds: imageHistories.map(h => h.imageId)
462
+ });
463
+ console.log('[useAdjustmentHistoryBatch] 🔍 PRESET: Current imageHistories state at start:', imageHistories.map(h => ({
464
+ imageId: h.imageId,
465
+ currentHistoryEntryId: h.currentHistoryEntryId,
466
+ historyLength: h.history.length,
467
+ historyIds: h.history.map(entry => entry.id)
468
+ })));
469
+ if (selectedIds.length === 0) {
470
+ if (devWarningsRef.current) {
471
+ console.warn('[useAdjustmentHistoryBatch] ❌ adjustSelectedWithPreset called with no selection');
472
+ }
473
+ return;
474
+ }
475
+ // Apply preset values with clamping
476
+ const clampedPreset = { ...presetAdjustments };
477
+ Object.keys(clampedPreset).forEach(key => {
478
+ const presetValue = clampedPreset[key];
479
+ clampedPreset[key] = Math.max(-100, Math.min(100, presetValue));
480
+ });
481
+ console.log('[useAdjustmentHistoryBatch] 🎨 Using SHARED logic for preset (same as reset)');
482
+ // Use the same logic as reset, just with preset values instead of default values
483
+ await applyAdjustmentToSelected(clampedPreset, 'preset');
484
+ }, [selectedIds, internalOptions, imageHistories, applyAdjustmentToSelected]);
366
485
  // Set specific adjustment states for specified images (removed since not needed)
367
486
  // Undo last changes to selected images - entry-based history version with backend sync
368
487
  const undo = useCallback(async () => {
@@ -372,53 +491,102 @@ export function useAdjustmentHistoryBatch(options = {}) {
372
491
  }
373
492
  return;
374
493
  }
494
+ console.log('[useAdjustmentHistoryBatch] 🔍 UNDO: Before undo, current imageHistories state:', imageHistories.map(h => ({
495
+ imageId: h.imageId,
496
+ currentHistoryEntryId: h.currentHistoryEntryId,
497
+ historyLength: h.history.length,
498
+ historyIds: h.history.map(entry => entry.id)
499
+ })));
500
+ // Prepare backend operations BEFORE state update (outside setImageHistories)
375
501
  let anyChanges = false;
376
502
  const backendOperations = [];
503
+ // Process each image to prepare operations
504
+ const operationsToApply = [];
505
+ imageHistories.forEach(imageHistory => {
506
+ if (!selectedIds.includes(imageHistory.imageId)) {
507
+ return; // Skip images not selected
508
+ }
509
+ console.log(`[useAdjustmentHistoryBatch] 🔍 UNDO: Processing image ${imageHistory.imageId} with history:`, {
510
+ currentHistoryEntryId: imageHistory.currentHistoryEntryId,
511
+ historyLength: imageHistory.history.length,
512
+ historyIds: imageHistory.history.map(entry => entry.id)
513
+ });
514
+ // Find current entry index
515
+ const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
516
+ if (currentEntryIndex <= 0) {
517
+ console.log(`[useAdjustmentHistoryBatch] 🔍 UNDO: Cannot undo image ${imageHistory.imageId} - at first entry or not found`);
518
+ return; // Cannot undo if at first entry or entry not found
519
+ }
520
+ // Move to previous entry
521
+ const previousEntry = imageHistory.history[currentEntryIndex - 1];
522
+ anyChanges = true;
523
+ console.log(`[useAdjustmentHistoryBatch] 🔍 UNDO: Moving image ${imageHistory.imageId} from index ${currentEntryIndex} to ${currentEntryIndex - 1}`, {
524
+ fromId: imageHistory.currentHistoryEntryId,
525
+ toId: previousEntry.id,
526
+ preservedHistoryLength: imageHistory.history.length
527
+ });
528
+ // Store operation for state update
529
+ operationsToApply.push({
530
+ imageId: imageHistory.imageId,
531
+ newCurrentHistoryEntryId: previousEntry.id
532
+ });
533
+ // Prepare backend sync operation (OUTSIDE state setter)
534
+ if (previousEntry.id && internalOptions.controller && internalOptions.firebaseUid) {
535
+ backendOperations.push({
536
+ imageId: imageHistory.imageId,
537
+ taskId: previousEntry.id
538
+ });
539
+ }
540
+ });
541
+ // Now update state with prepared operations (no backend preparation inside)
377
542
  setImageHistories(prevHistories => {
378
- return prevHistories.map(imageHistory => {
379
- if (!selectedIds.includes(imageHistory.imageId)) {
380
- return imageHistory; // No change for unselected images
381
- }
382
- // Find current entry index
383
- const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
384
- if (currentEntryIndex <= 0) {
385
- return imageHistory; // Cannot undo if at first entry or entry not found
386
- }
387
- // Move to previous entry
388
- const previousEntry = imageHistory.history[currentEntryIndex - 1];
389
- anyChanges = true;
390
- // Prepare backend sync operation
391
- if (previousEntry.id && internalOptions.controller && internalOptions.firebaseUid) {
392
- backendOperations.push({
393
- imageId: imageHistory.imageId,
394
- taskId: previousEntry.id
395
- });
543
+ const result = prevHistories.map(imageHistory => {
544
+ // Find operation for this image
545
+ const operation = operationsToApply.find(op => op.imageId === imageHistory.imageId);
546
+ if (!operation) {
547
+ return imageHistory; // No change for images not selected
396
548
  }
397
- return {
549
+ console.log(`[useAdjustmentHistoryBatch] 📝 Applying state update for undo on image ${imageHistory.imageId}`);
550
+ const updatedHistory = {
398
551
  ...imageHistory,
399
- currentHistoryEntryId: previousEntry.id
552
+ currentHistoryEntryId: operation.newCurrentHistoryEntryId
400
553
  };
554
+ console.log(`[useAdjustmentHistoryBatch] 🔍 UNDO: Updated image ${imageHistory.imageId} history:`, {
555
+ currentHistoryEntryId: updatedHistory.currentHistoryEntryId,
556
+ historyLength: updatedHistory.history.length,
557
+ historyIds: updatedHistory.history.map(entry => entry.id)
558
+ });
559
+ return updatedHistory;
401
560
  });
561
+ console.log('[useAdjustmentHistoryBatch] 🔍 UNDO: After undo, final imageHistories state:', result.map(h => ({
562
+ imageId: h.imageId,
563
+ currentHistoryEntryId: h.currentHistoryEntryId,
564
+ historyLength: h.history.length,
565
+ historyIds: h.history.map(entry => entry.id)
566
+ })));
567
+ return result;
402
568
  });
403
- // Sync with backend
569
+ // Sync with backend (already prepared)
404
570
  if (backendOperations.length > 0 && internalOptions.controller && internalOptions.firebaseUid) {
405
571
  try {
572
+ console.log(`[useAdjustmentHistoryBatch] âĒ Syncing ${backendOperations.length} undo operations to backend (setHistoryIndex for each image)`);
406
573
  const promises = backendOperations.map(async (operation) => {
574
+ console.log(`[useAdjustmentHistoryBatch] 🔄 Calling setHistoryIndex for undo on image ${operation.imageId} to taskId ${operation.taskId}`);
407
575
  await internalOptions.controller.setHistoryIndex(internalOptions.firebaseUid, operation.imageId, operation.taskId);
408
576
  });
409
577
  await Promise.all(promises);
410
578
  if (devWarningsRef.current) {
411
- console.log(`[useAdjustmentHistoryBatch] Synced ${backendOperations.length} undo operations to backend`);
579
+ console.log(`[useAdjustmentHistoryBatch] ✅ Successfully synced ${backendOperations.length} undo operations to backend`);
412
580
  }
413
581
  }
414
582
  catch (error) {
415
- console.error('[useAdjustmentHistoryBatch] Failed to sync undo to backend:', error);
583
+ console.error('[useAdjustmentHistoryBatch] ❌ Failed to sync undo to backend:', error);
416
584
  }
417
585
  }
418
586
  if (!anyChanges && devWarningsRef.current) {
419
587
  console.warn('[useAdjustmentHistoryBatch] Undo skipped - no changes to undo for selected images');
420
588
  }
421
- }, [selectedIds, internalOptions]);
589
+ }, [selectedIds, internalOptions, imageHistories]);
422
590
  // Redo next changes to selected images - entry-based history version with backend sync
423
591
  const redo = useCallback(async () => {
424
592
  if (selectedIds.length === 0) {
@@ -427,53 +595,70 @@ export function useAdjustmentHistoryBatch(options = {}) {
427
595
  }
428
596
  return;
429
597
  }
598
+ // Step 1: Prepare redo operations from current state
430
599
  let anyChanges = false;
600
+ const operationsToApply = [];
431
601
  const backendOperations = [];
432
- setImageHistories(prevHistories => {
433
- return prevHistories.map(imageHistory => {
434
- if (!selectedIds.includes(imageHistory.imageId)) {
435
- return imageHistory; // No change for unselected images
436
- }
437
- // Find current entry index
438
- const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
439
- if (currentEntryIndex >= imageHistory.history.length - 1 || currentEntryIndex === -1) {
440
- return imageHistory; // Cannot redo if at last entry or entry not found
441
- }
442
- // Move to next entry
443
- const nextEntry = imageHistory.history[currentEntryIndex + 1];
444
- anyChanges = true;
445
- // Prepare backend sync operation
446
- if (nextEntry.id && internalOptions.controller && internalOptions.firebaseUid) {
447
- backendOperations.push({
448
- imageId: imageHistory.imageId,
449
- taskId: nextEntry.id
450
- });
451
- }
452
- return {
453
- ...imageHistory,
454
- currentHistoryEntryId: nextEntry.id
455
- };
602
+ imageHistories.forEach(imageHistory => {
603
+ if (!selectedIds.includes(imageHistory.imageId)) {
604
+ return; // Skip unselected images
605
+ }
606
+ // Find current entry index
607
+ const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
608
+ if (currentEntryIndex >= imageHistory.history.length - 1 || currentEntryIndex === -1) {
609
+ return; // Cannot redo if at last entry or entry not found
610
+ }
611
+ // Move to next entry
612
+ const nextEntry = imageHistory.history[currentEntryIndex + 1];
613
+ anyChanges = true;
614
+ operationsToApply.push({
615
+ imageId: imageHistory.imageId,
616
+ nextEntryId: nextEntry.id
456
617
  });
618
+ // Prepare backend sync operation
619
+ if (nextEntry.id && internalOptions.controller && internalOptions.firebaseUid) {
620
+ backendOperations.push({
621
+ imageId: imageHistory.imageId,
622
+ taskId: nextEntry.id
623
+ });
624
+ }
457
625
  });
458
- // Sync with backend
626
+ // Step 2: Apply state updates cleanly
627
+ if (operationsToApply.length > 0) {
628
+ setImageHistories(prevHistories => {
629
+ return prevHistories.map(imageHistory => {
630
+ const operation = operationsToApply.find(op => op.imageId === imageHistory.imageId);
631
+ if (!operation) {
632
+ return imageHistory; // No change for this image
633
+ }
634
+ return {
635
+ ...imageHistory,
636
+ currentHistoryEntryId: operation.nextEntryId
637
+ };
638
+ });
639
+ });
640
+ }
641
+ // Step 3: Sync with backend
459
642
  if (backendOperations.length > 0 && internalOptions.controller && internalOptions.firebaseUid) {
460
643
  try {
644
+ console.log(`[useAdjustmentHistoryBatch] ⏊ Syncing ${backendOperations.length} redo operations to backend (setHistoryIndex for each image)`);
461
645
  const promises = backendOperations.map(async (operation) => {
646
+ console.log(`[useAdjustmentHistoryBatch] 🔄 Calling setHistoryIndex for redo on image ${operation.imageId} to taskId ${operation.taskId}`);
462
647
  await internalOptions.controller.setHistoryIndex(internalOptions.firebaseUid, operation.imageId, operation.taskId);
463
648
  });
464
649
  await Promise.all(promises);
465
650
  if (devWarningsRef.current) {
466
- console.log(`[useAdjustmentHistoryBatch] Synced ${backendOperations.length} redo operations to backend`);
651
+ console.log(`[useAdjustmentHistoryBatch] ✅ Successfully synced ${backendOperations.length} redo operations to backend`);
467
652
  }
468
653
  }
469
654
  catch (error) {
470
- console.error('[useAdjustmentHistoryBatch] Failed to sync redo to backend:', error);
655
+ console.error('[useAdjustmentHistoryBatch] ❌ Failed to sync redo to backend:', error);
471
656
  }
472
657
  }
473
658
  if (!anyChanges && devWarningsRef.current) {
474
659
  console.warn('[useAdjustmentHistoryBatch] Redo skipped - no changes to redo for selected images');
475
660
  }
476
- }, [selectedIds, internalOptions]);
661
+ }, [selectedIds, internalOptions, imageHistories]);
477
662
  // Check if any selected image can be undone
478
663
  const canUndoSelected = useCallback(() => {
479
664
  return selectedIds.some(imageId => {
@@ -494,38 +679,16 @@ export function useAdjustmentHistoryBatch(options = {}) {
494
679
  return currentEntryIndex >= 0 && currentEntryIndex < imageHistory.history.length - 1;
495
680
  });
496
681
  }, [selectedIds, imageHistories]);
497
- // Reset selected images to default state - entry-based history version
498
- const reset = useCallback((imageIds) => {
682
+ // Reset selected images to default state - now uses shared logic
683
+ const reset = useCallback(async (imageIds) => {
499
684
  const idsToReset = imageIds || selectedIds;
500
- if (idsToReset.length === 0)
501
- return;
685
+ console.log('[useAdjustmentHistoryBatch] 🔄 reset called for images:', idsToReset);
686
+ // Create default state
502
687
  const defaultState = createDefaultAdjustmentState(internalOptions.defaultAdjustmentState);
503
- setImageHistories(prevHistories => {
504
- return prevHistories.map(imageHistory => {
505
- if (!idsToReset.includes(imageHistory.imageId)) {
506
- return imageHistory; // No change for images not being reset
507
- }
508
- // Create new entry with default state
509
- const newEntryId = generateEntryId();
510
- const newEntry = {
511
- id: newEntryId,
512
- adjustment: defaultState
513
- };
514
- // Add to this image's history
515
- const newHistory = [...imageHistory.history, newEntry];
516
- // Trim if needed
517
- const maxSize = maxSizeRef.current;
518
- const trimmedHistory = typeof maxSize === 'number' && newHistory.length > maxSize
519
- ? newHistory.slice(-maxSize)
520
- : newHistory;
521
- return {
522
- ...imageHistory,
523
- history: trimmedHistory,
524
- currentHistoryEntryId: newEntryId
525
- };
526
- });
527
- });
528
- }, [selectedIds, internalOptions.defaultAdjustmentState]);
688
+ console.log('[useAdjustmentHistoryBatch] 🔄 Using SHARED logic for reset (same as preset)');
689
+ // Use the same logic as preset, just with default values instead of preset values
690
+ await applyAdjustmentToSelected(defaultState, 'reset', idsToReset);
691
+ }, [selectedIds, internalOptions.defaultAdjustmentState, applyAdjustmentToSelected]);
529
692
  // Selection management with initial adjustments - single state update
530
693
  const setSelection = useCallback((configs) => {
531
694
  const imageIds = configs.map(config => config.imageId);
@@ -859,6 +1022,114 @@ export function useAdjustmentHistoryBatch(options = {}) {
859
1022
  });
860
1023
  }
861
1024
  }, []);
1025
+ // Sync gallery updates - check for updated galleries and refresh their history
1026
+ const syncGalleryUpdates = useCallback(async () => {
1027
+ if (!internalOptions.controller || !internalOptions.firebaseUid || !internalOptions.eventId) {
1028
+ if (devWarningsRef.current) {
1029
+ console.warn('[useAdjustmentHistoryBatch] syncGalleryUpdates: Missing required options (controller, firebaseUid, or eventId)');
1030
+ }
1031
+ return;
1032
+ }
1033
+ try {
1034
+ // Step 1: Check for gallery updates since last timestamp
1035
+ console.log(`[useAdjustmentHistoryBatch] 🔄 Checking for gallery updates since timestamp: ${lastUpdateTimestamp}`);
1036
+ const updateResponse = await internalOptions.controller.getGalleryUpdateTimestamp(internalOptions.firebaseUid, internalOptions.eventId, lastUpdateTimestamp);
1037
+ if (!updateResponse.gallery || updateResponse.gallery.length === 0) {
1038
+ if (devWarningsRef.current) {
1039
+ console.log('[useAdjustmentHistoryBatch] ✅ No gallery updates found');
1040
+ }
1041
+ return;
1042
+ }
1043
+ console.log(`[useAdjustmentHistoryBatch] đŸ“Ĩ Found ${updateResponse.gallery.length} updated galleries:`, updateResponse.gallery);
1044
+ // Step 2: Fetch history for each updated gallery
1045
+ const historyPromises = updateResponse.gallery.map(async (galleryId) => {
1046
+ try {
1047
+ console.log(`[useAdjustmentHistoryBatch] 🔄 Fetching history for gallery: ${galleryId}`);
1048
+ const historyResponse = await internalOptions.controller.getEditorHistory(internalOptions.firebaseUid, galleryId);
1049
+ return {
1050
+ imageId: galleryId,
1051
+ currentTaskId: historyResponse.current_task_id,
1052
+ backendHistory: historyResponse.history || [],
1053
+ success: true
1054
+ };
1055
+ }
1056
+ catch (error) {
1057
+ console.error(`[useAdjustmentHistoryBatch] ❌ Failed to fetch history for gallery ${galleryId}:`, error);
1058
+ return {
1059
+ imageId: galleryId,
1060
+ currentTaskId: '',
1061
+ backendHistory: [],
1062
+ success: false
1063
+ };
1064
+ }
1065
+ });
1066
+ const historyResults = await Promise.all(historyPromises);
1067
+ const successfulUpdates = historyResults.filter(result => result.success && result.backendHistory.length > 0);
1068
+ if (successfulUpdates.length === 0) {
1069
+ if (devWarningsRef.current) {
1070
+ console.log('[useAdjustmentHistoryBatch] â„šī¸ No valid history updates to apply');
1071
+ }
1072
+ // Update timestamp even if no valid updates
1073
+ setLastUpdateTimestamp(Date.now());
1074
+ return;
1075
+ }
1076
+ // Step 3: Prepare history updates from backend data
1077
+ const historyUpdates = [];
1078
+ successfulUpdates.forEach(result => {
1079
+ const { imageId, currentTaskId, backendHistory } = result;
1080
+ if (backendHistory.length > 0) {
1081
+ // Convert backend history to local history entries
1082
+ const historyEntries = backendHistory.map((entry) => ({
1083
+ id: entry.task_id, // Use backend task_id as our entry id
1084
+ adjustment: mapColorAdjustmentToAdjustmentState ?
1085
+ mapColorAdjustmentToAdjustmentState(entry.editor_config.color_adjustment) :
1086
+ createDefaultAdjustmentState(internalOptions.defaultAdjustmentState)
1087
+ }));
1088
+ historyUpdates.push({
1089
+ imageId,
1090
+ newHistory: historyEntries,
1091
+ newCurrentEntryId: currentTaskId // Use current_task_id as current history entry
1092
+ });
1093
+ console.log(`[useAdjustmentHistoryBatch] 📝 Prepared history update for gallery ${imageId}:`, {
1094
+ historyEntries: historyEntries.length,
1095
+ currentEntryId: currentTaskId
1096
+ });
1097
+ }
1098
+ });
1099
+ // Step 4: Apply history updates cleanly to state
1100
+ if (historyUpdates.length > 0) {
1101
+ setImageHistories(prevHistories => {
1102
+ return prevHistories.map(imageHistory => {
1103
+ const update = historyUpdates.find(u => u.imageId === imageHistory.imageId);
1104
+ if (!update) {
1105
+ return imageHistory; // No update for this image
1106
+ }
1107
+ // Replace entire history with updated backend data
1108
+ return {
1109
+ ...imageHistory,
1110
+ history: update.newHistory,
1111
+ currentHistoryEntryId: update.newCurrentEntryId
1112
+ };
1113
+ });
1114
+ });
1115
+ console.log(`[useAdjustmentHistoryBatch] ✅ Successfully updated history for ${historyUpdates.length} galleries`);
1116
+ }
1117
+ // Step 5: Update timestamp to mark successful sync
1118
+ setLastUpdateTimestamp(Date.now());
1119
+ if (devWarningsRef.current) {
1120
+ console.log('[useAdjustmentHistoryBatch] 🔄 Gallery sync completed', {
1121
+ totalChecked: updateResponse.gallery.length,
1122
+ successfulUpdates: successfulUpdates.length,
1123
+ appliedUpdates: historyUpdates.length,
1124
+ newTimestamp: Date.now()
1125
+ });
1126
+ }
1127
+ }
1128
+ catch (error) {
1129
+ console.error('[useAdjustmentHistoryBatch] ❌ Failed to sync gallery updates:', error);
1130
+ // Don't update timestamp on error, so we can retry
1131
+ }
1132
+ }, [internalOptions, lastUpdateTimestamp, mapColorAdjustmentToAdjustmentState]);
862
1133
  // Configuration actions
863
1134
  const setMaxSize = useCallback((size) => {
864
1135
  maxSizeRef.current = size;
@@ -885,6 +1156,7 @@ export function useAdjustmentHistoryBatch(options = {}) {
885
1156
  reset,
886
1157
  setSelection,
887
1158
  syncAdjustment,
1159
+ syncGalleryUpdates,
888
1160
  toggleSelection,
889
1161
  selectAll,
890
1162
  clearSelection,
@@ -894,7 +1166,7 @@ export function useAdjustmentHistoryBatch(options = {}) {
894
1166
  syncBatch
895
1167
  }), [
896
1168
  adjustSelected, adjustSelectedWithPreset, undo, redo, reset,
897
- setSelection, syncAdjustment, toggleSelection, selectAll, clearSelection,
1169
+ setSelection, syncAdjustment, syncGalleryUpdates, toggleSelection, selectAll, clearSelection,
898
1170
  jumpToIndex, clearHistory, getCurrentBatch, syncBatch
899
1171
  ]);
900
1172
  // Config object - stabilized with useMemo