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