From 2a2303d1dccc120dd7aa5f6b6ade0f89e0064850 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Nov 2025 07:42:23 +0000
Subject: [PATCH] =Feed block mostly good! Referrals look good to go. Ready for Madi and Heidi to approve
---
assets/js/concise/UploadManager.js | 623 +++++++++++++++++++++++++++++++++++++------------------
1 files changed, 415 insertions(+), 208 deletions(-)
diff --git a/assets/js/concise/UploadManager.js b/assets/js/concise/UploadManager.js
index 59a0d5f..56fc593 100644
--- a/assets/js/concise/UploadManager.js
+++ b/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
}
--
Gitblit v1.10.0