Jake Vanderwerf
2025-11-25 2a2303d1dccc120dd7aa5f6b6ade0f89e0064850
assets/js/concise/UploadManager.js
@@ -12,40 +12,41 @@
      this.queue = window.jvbQueue;
      this.a11y = window.jvbA11y;
      this.error = window.jvbError;
      // RECOVERY STORES - Cleared after successful upload
      this.fieldStore = window.jvbStore.register(
         'upload_fields',
         {
            storeName: 'fieldStates',
            keyPath: 'id',
            version: 2,
            indexes: [
               { name: 'fieldId', keyPath: 'fieldId' },
               { name: 'timestamp', keyPath: 'timestamp' },
               { name: 'content', keyPath: 'content' },
               { name: 'itemId', keyPath: 'itemId' },
               { name: 'status', keyPath: 'status' }
            ],
            TTL: 86400000 * 7, // 1 week
            delayFetch: true
         });
      this.uploadStore = window.jvbStore.register(
      this.fieldStoreReady = false;
      this.uploadStoreReady = false;
      this.hasCheckedForUploads = false;
      const {fields, uploads} = window.jvbStore.register(
         'uploads',
         {
            name: 'uploads',
            storeName: 'uploads',
            keyPath: 'id',
            storeBlobs: true,
            indexes: [
               { name: 'fieldId', keyPath: 'fieldId' },
               { name: 'status', keyPath: 'status' },
               { name: 'groupId', keyPath: 'groupId' },
               { name: 'attachmentId', keyPath: 'attachmentId' }
            ],
            delayFetch: true
         });
         [
            {
               storeName: 'fields',
               keyPath: 'id',
               indexes: [
                  { name: 'fieldId', keyPath: 'fieldId' },
                  { name: 'timestamp', keyPath: 'timestamp' },
                  { name: 'content', keyPath: 'content' },
                  { name: 'itemId', keyPath: 'itemId' },
                  { name: 'status', keyPath: 'status' }
               ],
               TTL: 7 * 24 * 60 * 60 * 1000, // 1 week
               delayFetch: true
            },
            {
               storeName: 'uploads',
               keyPath: 'id',
               storeBlobs: true,
               indexes: [
                  { name: 'fieldId', keyPath: 'fieldId' },
                  { name: 'status', keyPath: 'status' },
                  { name: 'groupId', keyPath: 'groupId' },
                  { name: 'attachmentId', keyPath: 'attachmentId' }
               ],
               delayFetch: true
            }
         ]
      );
      this.fieldStore = fields;
      this.uploadStore = uploads;
      window.jvbUploadBlobs = this.uploadStore;
@@ -70,9 +71,6 @@
      // Notification subscribers
      this.subscribers = new Set();
      // Controllers
      this.dragController = null;
      // Selectors
      this.selectors = {
         field: {
@@ -110,17 +108,6 @@
         'failed_permanent': 'Upload failed permanently'
      };
      // Sortable configuration
      this.sortableConfig = {
         animation: 150,
         draggable: '.item',
         handle: '.select-item, img',
         ghostClass: 'sortable-ghost',
         chosenClass: 'sortable-chosen',
         dragClass: 'sortable-drag',
         onEnd: (evt) => this.handleReorder(evt)
      };
      this.init();
   }
@@ -178,7 +165,6 @@
   /*******************************************************************************
    * FIELD MANAGEMENT
    *******************************************************************************/
   initializeFields() {
      const fields = document.querySelectorAll(this.selectors.field.field);
      fields.forEach(uploader => this.registerUploader(uploader));
@@ -213,10 +199,6 @@
      uploader.dataset.uploader = fieldId;
      this.addFieldSelectionHandler(fieldId);
      if (config.destination === 'post_group' && !this.dragController) {
         this.initGroupFeatures();
      }
      if (config.type !== 'single') {
         this.initSortable(fieldId);
      }
@@ -269,47 +251,219 @@
   /*******************************************************************************
    * SORTABLE INITIALIZATION
    *******************************************************************************/
   initSortable(fieldId) {
      if (!window.Sortable) return;
      // Mount MultiDrag plugin once
      if (!Sortable._multiDragMounted && Sortable.MultiDrag) {
         Sortable.mount(new Sortable.MultiDrag());
         Sortable._multiDragMounted = true;
      }
      const fieldEl = this.fieldElements.get(fieldId);
      if (!fieldEl) return;
      // Find all sortable grids (preview + group grids, but not restore grids)
      // Initialize sortable on all existing grids
      const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
      grids.forEach(grid => {
         const groupId = grid.classList.contains('group')
            ? grid.closest('.upload-group')?.dataset.groupId
            : null;
         this.createSortableForGrid(grid, fieldId, groupId);
      });
      grids.forEach((grid) => {
         // Skip if already initialized
         if (grid.sortableInstance) return;
      // Special handler for empty-group
      const emptyGroup = fieldEl.element.querySelector('.empty-group');
      if (emptyGroup && !emptyGroup.sortableInstance) {
         emptyGroup.sortableInstance = new Sortable(emptyGroup, {
            animation: 150,
            draggable: '.item',
            multiDrag: true,
            selectedClass: 'selected-for-drag',
            avoidImplicitDeselect: true,
            group: { name: fieldId, pull: false, put: true },
            ghostClass: 'sortable-ghost',
            chosenClass: 'sortable-chosen',
            dragClass: 'sortable-drag',
            onEnd: (evt) => this.handleDrop(evt, fieldId)
         });
      }
   }
         const isGroupGrid = grid.classList.contains('group');
         const gridId = isGroupGrid
            ? `${fieldId}-group-${grid.closest('.upload-group')?.dataset.groupId}`
            : `${fieldId}-preview`;
   syncSortableSelection(fieldId, selectedItems) {
      // Update Sortable's selection state to match checkboxes
      this.sortableInstances.forEach((instance, key) => {
         if (key.startsWith(fieldId)) {
            const grid = instance.el;
            const items = grid.querySelectorAll('.item');
         const sortableInstance = new Sortable(grid, {
            ...this.sortableConfig,
            group: {
               name: fieldId,
               pull: true,
               put: true
            },
            items.forEach(item => {
               const uploadId = item.dataset.uploadId;
               const shouldBeSelected = selectedItems.has(uploadId);
            onAdd: (evt) => {
               // Re-enable when items added
               this.updateSortableState(evt.to);
            },
            onRemove: (evt) => {
               // Disable source if now empty
               this.updateSortableState(evt.from);
            }
               if (shouldBeSelected) {
                  Sortable.utils.select(item);
               } else {
                  Sortable.utils.deselect(item);
               }
            });
         }
      });
   }
   handleDrop(evt, fieldId) {
      const dropTarget = evt.to;
      const sourceTarget = evt.from;
      const items = evt.items?.length > 0 ? evt.items : [evt.item];
      const uploadIds = items.map(item => item.dataset.uploadId);
      // Determine drop target type
      const targetType = this.getDropTargetType(dropTarget);
      switch (targetType) {
         case 'empty-group':
            this.handleDropToEmptyGroup(items, uploadIds, fieldId);
            break;
         case 'preview':
            this.handleDropToPreview(items, uploadIds, fieldId);
            break;
         case 'group':
            this.handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId);
            break;
         default:
            // Fallback: return to preview
            this.handleDropToPreview(items, uploadIds, fieldId);
            break;
      }
      // Update UI
      this.updateSortableState(dropTarget);
      if (sourceTarget !== dropTarget) {
         this.updateSortableState(sourceTarget);
      }
   }
   /**
    * Determine what type of drop target this is
    */
   getDropTargetType(target) {
      if (target.classList.contains('empty-group')) {
         return 'empty-group';
      }
      if (target.classList.contains('preview')) {
         return 'preview';
      }
      if (target.classList.contains('group')) {
         return 'group';
      }
      return 'unknown';
   }
   /**
    * Handle drop to group: add to existing group
    */
   handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId) {
      try {
         // If same container, it's just a reorder
         if (dropTarget === sourceTarget) {
            this.handleReorder({ to: dropTarget, items: items });
            return;
         }
         // Moving to different group
         uploadIds.forEach(uploadId => {
            this.addToGroup(uploadId, dropTarget, false);
         });
         // Store reference on element for easy access
         grid.sortableInstance = sortableInstance;
         this.sortableInstances.set(gridId, sortableInstance);
      });
         this.schedulePersistance(fieldId);
         const message = items.length > 1
            ? `Moved ${items.length} items to group`
            : 'Moved item to group';
         this.a11y.announce(message);
         // Clear selection
         const handler = this.selectionHandlers.get(fieldId);
         handler?.clearSelection();
      }  catch (error) {
         this.handleDropError(items, fieldId, error);
      }
   }
   /**
    * Handle drop to preview: remove from groups
    */
   handleDropToPreview(items, uploadIds, fieldId) {
      try {
         uploadIds.forEach(uploadId => {
            this.removeFromGroup(uploadId);
         });
         this.schedulePersistance(fieldId);
         const message = items.length > 1
            ? `Moved ${items.length} items to preview`
            : 'Moved item to preview';
         this.a11y.announce(message);
         // Clear selection
         const handler = this.selectionHandlers.get(fieldId);
         handler?.clearSelection();
      } catch (error) {
         this.handleDropError(items, fieldId, error);
      }
   }
   /**
    * Handle drop errors consistently
    */
   handleDropError(items, fieldId, error, message = 'An error occurred') {
      console.error('Drop error:', error);
      // Return items to preview as fallback
      const fieldEl = this.fieldElements.get(fieldId);
      if (fieldEl?.ui?.preview) {
         items.forEach(item => fieldEl.ui.preview.appendChild(item));
      }
      this.a11y.announce(`${message}. Items returned to preview.`);
   }
   /**
    * Handle drop to group: add to existing group
    */
   handleDropToEmptyGroup(items, uploadIds, fieldId) {
      try {
         const group = this.createGroup(fieldId);
         if (!group) {
            this.handleDropError(items, fieldId, new Error('Group creation failed'), 'Failed to create group');
            return;
         }
         // Move items to new group
         items.forEach((item, index) => {
            group.grid.appendChild(item);
            this.addToGroup(uploadIds[index], group.grid, false);
         });
         this.schedulePersistance(fieldId);
         const message = items.length > 1
            ? `Created group with ${items.length} items`
            : 'Created group with item';
         this.a11y.announce(message);
         // Clear selection after move
         const handler = this.selectionHandlers.get(fieldId);
         handler?.clearSelection();
      } catch (error) {
         this.handleDropError(items, fieldId, error);
      }
   }
   /**
@@ -319,8 +473,8 @@
      const sortable = grid?.sortableInstance;
      if (!sortable) return;
      const hasItems = grid.querySelectorAll('.item').length > 0;
      sortable.option('disabled', !hasItems);
      // const hasItems = grid.querySelectorAll('.item').length > 0;
      sortable.option('disabled', false);
   }
   /**
@@ -339,65 +493,55 @@
      const fieldWrapper = grid.closest('.field, .upload');
      if (!fieldWrapper) return;
      let hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
      let items = Array.from(fieldWrapper.querySelectorAll('.item')).map(upload => upload.dataset.id);
      console.log(items);
      hiddenInput.value = items.join(',');
      const movedItems = evt.items && evt.items.length > 0 ? evt.items : [evt.item];
      if (window.jvbA11y) {
         window.jvbA11y.announce('Item reordered');
      // Get current order from DOM
      let items = Array.from(grid.querySelectorAll('.item:not(.sortable-ghost):not(.sortable-clone)'))
         .map(upload => upload.dataset.uploadId)
         .filter(id => id);
      console.log('Reordered items:', items);
      // Update hidden input (for form submission)
      let hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
      if (hiddenInput && items.length > 0) {
         hiddenInput.value = items.join(',');
      }
      // âœ… Update fieldState with new order
      const fieldId = this.getFieldIdFromElement(grid);
      if (fieldId) {
         const fieldData = this.getFieldData(fieldId);
         // If reordering within a group, update that group's uploads array
         if (grid.classList.contains('group')) {
            const groupId = grid.dataset.groupId;
            const group = fieldData?.groups?.find(g => g.id === groupId);
            if (group) {
               group.uploads = items; // Update order
            }
         }
         // If reordering in preview, the order is implicit by DOM position
         // (we don't store preview order separately)
         this.schedulePersistance(fieldId); // âœ… Persist changes
      }
      this.a11y.announce('Item reordered');
      fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
         detail: { from: evt.from, to: evt.to, oldIndex: evt.oldIndex, newIndex: evt.newIndex },
         detail: {
            from: evt.from,
            to: evt.to,
            oldIndex: evt.oldIndex,
            newIndex: evt.newIndex,
            items: items
         },
         bubbles: true
      }));
   }
   /*******************************************************************************
    * DRAG & DROP INITIALIZATION
    *******************************************************************************/
   initGroupFeatures() {
      this.dragController = new window.jvbDragHandler({
         draggableSelector: this.selectors.items.item,
         dropTargetSelector: `${this.selectors.field.preview}, ${this.selectors.groups.grid}, .empty-group`,
         ignoreSelector: 'input:not(.upload-select), button, select, textarea, details, summary, a',
         previewElement: 'img, video, .icon',
         getItemId: (element) => element.dataset.uploadId,
         getSelectedItems: (element) => {
            const fieldId = this.getFieldIdFromElement(element);
            const uploadId = element.dataset.uploadId;
            const selected = this.getCurrentSelection(fieldId);
            return (selected && selected.includes(uploadId)) ? selected : [uploadId];
         },
         validateDrop: (itemIds, targetElement) => {
            const targetFieldId = this.getFieldIdFromElement(targetElement);
            const itemElement = document.querySelector(`[data-upload-id="${itemIds[0]}"]`);
            const itemFieldId = this.getFieldIdFromElement(itemElement);
            return targetFieldId === itemFieldId;
         },
         onDrop: (itemIds, targetElement) => {
            this.handleItemDrop(itemIds, targetElement);
            targetElement.scrollIntoView({behavior:'smooth', block:'center'});
         },
         onDragEnd: (itemIds, success) => {
            if (success) {
               const itemElement = document.querySelector(`[data-upload-id="${itemIds[0]}"]`);
               const fieldId = this.getFieldIdFromElement(itemElement);
               const handler = this.selectionHandlers.get(fieldId);
               handler?.clearSelection();
            }
         },
         previewOptions: {
            multiOffset: { x: -60, y: -80 },
            singleOffset: { x: -50, y: -60 },
            showCount: true
         }
      });
   }
   /*******************************************************************************
    * FILE DROP HANDLERS
    *******************************************************************************/
@@ -462,38 +606,6 @@
   }
   /*******************************************************************************
    * ITEM DROP HANDLER (for rearranging)
    *******************************************************************************/
   handleItemDrop(itemIds, targetElement) {
      const isPreviewDrop = targetElement.classList.contains('preview');
      let actualTarget = targetElement;
      // Handle drop on empty group placeholder
      if (targetElement.classList.contains('empty-group')) {
         const fieldId = this.getFieldIdFromElement(targetElement);
         const group = this.createGroup(fieldId);
         if (!group) return;
         actualTarget = group.grid;
      }
      // Move each item
      itemIds.forEach(uploadId => {
         if (isPreviewDrop) {
            this.removeFromGroup(uploadId);
         } else {
            this.addToGroup(uploadId, actualTarget);
         }
      });
      const fieldId = this.getFieldIdFromElement(targetElement);
      this.schedulePersistance(fieldId);
      const message = itemIds.length > 1 ? `Moved ${itemIds.length} items` : 'Moved item';
      this.a11y.announce(message);
   }
   /*******************************************************************************
    * CLICK & CHANGE HANDLERS
    *******************************************************************************/
@@ -563,7 +675,7 @@
         try {
            const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
            // Create upload data (without blob initially)
            // Create initial upload data
            const uploadData = {
               id: uploadId,
               attachmentId: null,
@@ -577,13 +689,20 @@
               }
            };
            // Save initial data
            await this.uploadStore.save(uploadData);
            // Process file
            const preview = this.createPreviewUrl(file);
            const processedFile = file.type.startsWith('image/')
               ? await this.processImage(file, fieldData.config.subtype)
               : file;
            // Store blob data as ArrayBuffer
            // Show progress
            this.showUploadProgress(uploadId, true);
            this.updateUploadItemProgress(uploadId, 50, 'local_processing');
            // Store blob data (this updates the existing uploadData)
            await this.saveBlobData(uploadId, processedFile || file);
            // Create DOM element
@@ -595,10 +714,6 @@
               subtype: subtype
            }, fieldData.config.destination === 'post_group');
            // Show progress
            this.showUploadProgress(uploadId, true);
            this.updateUploadItemProgress(uploadId, 50, 'local_processing');
            // Add to preview grid
            if (fieldEl.ui.preview) {
               fieldEl.ui.preview.appendChild(element);
@@ -611,9 +726,12 @@
               });
            }
            // Store persistent data
            uploadData.status = 'processed';
            await this.uploadStore.save(uploadData);
            // Update status (gets existing data with blobData intact)
            const storedUpload = this.uploadStore.get(uploadId);
            if (storedUpload) {
               storedUpload.status = 'processed';
               await this.uploadStore.save(storedUpload);
            }
            // Add to field
            fieldData.uploads.add(uploadId);
@@ -650,7 +768,7 @@
   }
   /*******************************************************************************
    * IMAGE PROCESSING (simplified - keeping existing logic)
    * IMAGE PROCESSING
    *******************************************************************************/
   async processImage(file, uploadId) {
@@ -1297,7 +1415,7 @@
    * Handle operation cancellation
    */
   async handleOperationCancelled(fieldId) {
      const fieldData = this.getFieldData(fieldId);  // âœ…
      const fieldData = this.getFieldData(fieldId);
      if (!fieldData) return;
      const uploadsArray = fieldData.uploads instanceof Set
@@ -1540,28 +1658,25 @@
      }
      for (const groupData of groups) {
         // Create group using existing method which handles all initialization
         const group = this.createGroup(fieldId, groupData.id);
         if (!group) {
            console.warn('Failed to create group:', groupData.id);
            continue;
         }
         // Find the group in fieldData (createGroup already added it)
         const storedGroup = fieldData.groups?.find(g => g.id === groupData.id);
         if (storedGroup) {
            // Restore metadata
            if (groupData.changes) {
               storedGroup.changes = { ...groupData.changes };
            }
            // âœ… Preserve upload order
            if (groupData.uploads) {
               storedGroup.uploads = [...groupData.uploads];
            }
            // Restore form field values if they exist
            // Restore form field values
            if (groupData.changes) {
               const titleInput = group.element.querySelector('[name*="post_title"]');
               const excerptInput = group.element.querySelector('[name*="post_excerpt"]');
@@ -1569,7 +1684,6 @@
               if (titleInput && groupData.changes.post_title) {
                  titleInput.value = groupData.changes.post_title;
               }
               if (excerptInput && groupData.changes.post_excerpt) {
                  excerptInput.value = groupData.changes.post_excerpt;
               }
@@ -1577,7 +1691,6 @@
         }
      }
      // Save updated field data
      await this.saveFieldData(fieldData);
   }
@@ -1801,19 +1914,76 @@
      const fieldData = this.getFieldData(fieldId);
      if (!fieldEl?.ui?.dropZone || !fieldData) return;
      if (fieldData.config.destination === 'post_group') return;
      const uploadCount = fieldData.uploads?.size || 0;
      const maxFiles = fieldData.config?.maxFiles || 999;
      // For groupable uploads, set max to 20
      const maxFiles = fieldData.config.destination === 'post_group'
         ? 20
         : (fieldData.config?.maxFiles || 999);
      fieldEl.ui.dropZone.hidden = uploadCount >= maxFiles;
      fieldEl.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
      // Show helpful message for groupable uploads
      if (fieldData.config.destination === 'post_group' && uploadCount >= maxFiles) {
         this.a11y.announce('Maximum of 20 uploads reached. Please submit current uploads before adding more.');
      }
   }
   /*******************************************************************************
    * GROUP MANAGEMENT
    *******************************************************************************/
   /**
    * Create sortable instance for a grid
    */
   createSortableForGrid(grid, fieldId, groupId = null) {
      if (!grid || grid.sortableInstance) return;
      const sortableInstance = new Sortable(grid, {
         animation: 150,
         draggable: '.item',
         multiDrag: true,
         selectedClass: 'selected-for-drag',
         avoidImplicitDeselect: true,
         group: { name: fieldId, pull: true, put: true },
         ghostClass: 'sortable-ghost',
         chosenClass: 'sortable-chosen',
         dragClass: 'sortable-drag',
         // Centralized drop handler
         onEnd: (evt) => this.handleDrop(evt, fieldId),
         // Selection sync
         onSelect: (evt) => {
            const checkbox = evt.item.querySelector('[name*="select-item"]');
            if (checkbox && !checkbox.checked) {
               checkbox.checked = true;
               checkbox.dispatchEvent(new Event('change', { bubbles: true }));
            }
         },
         onDeselect: (evt) => {
            const checkbox = evt.item.querySelector('[name*="select-item"]');
            if (checkbox && checkbox.checked) {
               checkbox.checked = false;
               checkbox.dispatchEvent(new Event('change', { bubbles: true }));
            }
         },
         onAdd: (evt) => this.updateSortableState(evt.to),
         onRemove: (evt) => this.updateSortableState(evt.from)
      });
      grid.sortableInstance = sortableInstance;
      const gridId = groupId
         ? `${fieldId}-group-${groupId}`
         : `${fieldId}-preview`;
      this.sortableInstances.set(gridId, sortableInstance);
      return sortableInstance;
   }
   createGroup(fieldId, groupId = null) {
      const fieldData = this.getFieldData(fieldId);
      const fieldEl = this.fieldElements.get(fieldId);
@@ -1855,32 +2025,25 @@
      // Add to field groups
      if (!fieldData.groups) fieldData.groups = [];
      fieldData.groups.push({
         id: groupId,
         uploads: [],
         changes: {}
      });
      this.saveFieldData(fieldData);
      const existingGroup = fieldData.groups.find(g => g.id === groupId);
      if (!existingGroup) {
         fieldData.groups.push({
            id: groupId,
            uploads: [],
            changes: {}
         });
         this.saveFieldData(fieldData);
      }
      // Initialize selection handler and sortable
      // Initialize selection handler
      this.addGroupSelectionHandler(fieldId, groupId);
      // Initialize sortable for this new group grid
      if (grid) {
         const sortableInstance = new Sortable(grid, {
            ...this.sortableConfig,
            group: { name: fieldId, pull: true, put: true },
            disabled: true, // Empty initially
            onAdd: (evt) => this.updateSortableState(evt.to),
            onRemove: (evt) => this.updateSortableState(evt.from)
         });
         grid.sortableInstance = sortableInstance;
         this.sortableInstances.set(`${fieldId}-group-${groupId}`, sortableInstance);
         this.createSortableForGrid(grid, fieldId, groupId);
      }
      return { id: groupId, element: groupElement, grid: grid };
   }
   createGroupElement(groupId, fieldId) {
      let groupElement = window.getTemplate('imageGroup');
      if (!groupElement) return;
@@ -2163,6 +2326,9 @@
         case 'restore':
            this.handleRestoreUploads().then(() => {});
            break;
         case 'restore-all':
            this.handleRestoreAll().then(() => {});
            break;
         case 'clear-cache':
            if (!confirm('Save these uploads for later?')) {
               this.cleanupStoredUploads();
@@ -2260,8 +2426,16 @@
      handler.subscribe((event, data) => {
         switch(event) {
            case 'item-selected':
               // Sync with Sortable
               this.syncSortableSelection(fieldId, data.selectedItems);
               this.selected.set(fieldId, data.selectedItems);
               break;
            case 'item-deselected':
               this.syncSortableSelection(fieldId, data.selectedItems);
               this.selected.set(fieldId, data.selectedItems);
               break;
            case 'range-selected':
               this.syncSortableSelection(fieldId, data.selectedItems);
               this.selected.set(fieldId, data.selectedItems);
               break;
            case 'select-all':
@@ -2356,6 +2530,11 @@
    * Save field data to store, converting Sets to Arrays
    */
   async saveFieldData(fieldData) {
      console.log('💾 Saving:', fieldData.id, {
         uploads: fieldData.uploads?.size,
         groups: fieldData.groups?.length
      });
      await this.fieldStore.save({
         ...fieldData,
         timestamp: Date.now()
@@ -2588,7 +2767,7 @@
      window.debouncer.schedule(
         key,
         () => this.persistFieldState(fieldId),
         1000
         250
      );
   }
@@ -2637,8 +2816,8 @@
   handleFieldStoreEvent(event, data) {
      switch(event) {
         case 'data-loaded':
            // Check for pending uploads to restore
            this.checkForStoredUploads();
            this.fieldStoreReady = true;
            this.checkIfBothStoresReady();
            break;
      }
   }
@@ -2646,6 +2825,8 @@
   handleUploadStoreEvent(event, data) {
      switch(event) {
         case 'data-loaded':
            this.uploadStoreReady = true;
            this.checkIfBothStoresReady();
            break;
         case 'item-saved':
            this.showSaveIndicator(data.key);
@@ -2653,6 +2834,13 @@
      }
   }
   checkIfBothStoresReady() {
      if (this.fieldStoreReady && this.uploadStoreReady && !this.hasCheckedForUploads) {
         this.hasCheckedForUploads = true;
         this.checkForStoredUploads();
      }
   }
   async checkForStoredUploads() {
      const allFieldStates = this.fieldStore.getAll();
@@ -2660,6 +2848,8 @@
         fieldStates: allFieldStates.length,
         uploadStoreSize: this.uploadStore.data.size
      });
      console.log(this.uploadStore.getAll());
      console.log(this.fieldStore.getAll());
      const pendingFields = allFieldStates.filter(field => {
         if (!field.uploads) return false;
@@ -2722,8 +2912,8 @@
         const itemGrid = fieldTemplate.querySelector('.item-grid.restore');
         // Process each upload
         for (const upload of field.uploads) {
         for (let uploadId of field.uploads) {
            const upload = this.uploadStore.get(uploadId);
            let uploadItem = window.getTemplate('uploadItem');
            if (!uploadItem) continue;
            //
@@ -2875,6 +3065,23 @@
      this.cleanupRestore();
   }
   async handleRestoreAll() {
      let notification = document.querySelector('dialog.restore-uploads');
      if (!notification) {
         return;
      }
      // Gets ALL uploads from notification without checking selection
      const allUploads = [];
      notification.querySelectorAll('.item.upload').forEach(item => {
         let uploadId = item.dataset.uploadId;
         let fieldId = item.dataset.fieldId;
         allUploads.push({ uploadId, fieldId });
      });
      await this.restoreSelectedUploads(allUploads);
      this.cleanupRestore();
   }
   showSaveIndicator(key) {
      // Optional: show user that state is being saved
   }