From 457c329237f97069063e641b10f384a52d584f21 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 12 May 2026 17:50:11 +0000
Subject: [PATCH] =minor tweaks

---
 assets/js/concise/CRUD.js |  256 ++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 190 insertions(+), 66 deletions(-)

diff --git a/assets/js/concise/CRUD.js b/assets/js/concise/CRUD.js
index 1dba72a..3255643 100644
--- a/assets/js/concise/CRUD.js
+++ b/assets/js/concise/CRUD.js
@@ -12,7 +12,6 @@
 		this.error = window.jvbError;
 		this.populate = window.jvbPopulate;
 		this.cache = new window.jvbCache(this.content);
-		this.uploadedFields = new Set(); //tracks which upload fields are currently uploading; so don't send any of these changes to server
 
 		this.activeItem = null;
 		this.isTimeline = false;
@@ -53,8 +52,9 @@
 			if (refs.trash) refs.trash.dataset.id = data.id;
 		};
 		const imageSetup = function(el, refs, data) {
-			if (data?.fields?.post_thumbnail) {
-				const thumbnail = data.images[data.fields.post_thumbnail] ?? {};
+			let hasThumbnail = data?.fields?.post_thumbnail || data?.fields?.thumbnail;
+			if (hasThumbnail) {
+				const thumbnail = data.images[hasThumbnail] ?? {};
 				refs.img.src = thumbnail.medium??'';
 				refs.img.alt = thumbnail.alt??data.fields.post_title??'';
 			}
@@ -324,7 +324,11 @@
 				},
 				date: '[data-filter="date"]'
 			},
-			uploader: 'details.uploader'
+			uploader: {
+				details: 'details.uploader',
+				form: 'details.uploader form',
+				uploader: 'details.uploader [data-field-type="upload"]'
+			}
 		}
 
 		this.ui = window.uiFromSelectors(this.selectors);
@@ -340,12 +344,17 @@
 		this.isTimeline = !!document.querySelector('[data-timeline]');
 	}
 		initUploader() {
-			if (!this.ui.uploader) return;
+			if (!this.ui.uploader.form) return;
+			this.uploadForm = this.forms.registerForm(this.ui.uploader.form).id??false;
 
-			window.jvbUploads.scanFields(this.ui.uploader);
+			// window.jvbUploads.scanFields(this.ui.uploader);
 			window.jvbUploads.subscribe((event, data) => {
 				if (event === 'sent-to-queue') {
-					if (data === this.ui.uploader.dataset.uploader) {
+					if (data.field.id === this.ui.uploader.uploader.dataset.uploader) {
+						if (this.uploadForm ) {
+							this.forms.store.delete(this.uploadForm);
+						}
+
 						window.debouncer.schedule('crud-complete', ()=> {
 							this.store.clearCache();
 						});
@@ -356,16 +365,11 @@
 					const fieldName = data.field.config.name;
 					const itemId = data.field.config.itemID;
 					if (itemId && fieldName) {
-						this.uploadedFields.add(`${itemId}_${fieldName}`);
 						if (this.changes.has(itemId)) {
 							delete this.changes.get(itemId)[fieldName];
 						}
 					}
 				}
-
-				if (event === 'upload_complete') {
-					this.uploadedFields.delete(`${data['item_id']}_${data['field']}`);
-				}
 			});
 		}
 		initModals() {
@@ -427,6 +431,7 @@
 						{ name: 'modified', keyPath: 'modified'},
 						{ name: 'title', keyPath: 'title'},
 					],
+					isAuth: true,
 					filters: filters,
 					ignore: ['content', 'user'],
 					TTL: 60 * 60 * 1000, 		//1 hour cache
@@ -518,35 +523,86 @@
 				&& data.status === 'completed'
 				&& data.endpoint === 'uploads/groups') {
 				if (data.result && data.result.group_mappings) {
+					console.log('Handling group mapping from queue response');
 					this.handleGroupMappings(data.result.group_mappings);
 				}
+
 				this.store.clearCache();
 			}
+
 			if (event === 'operation-status'
 				&& data.status === 'completed'
 				&& data.type === 'content_update') {
+
 				this.store.clearCache();
 
-				// Check for result data (from ContentExecutor)
-				if (!data.result || !data.result.posts) {
-					console.warn('Content update completed but no result.posts', data);
+				if (!data.result || !data.result.success || !data.result.errors)
+				{
+					console.warn('Content update completed but no results', data);
 					return;
 				}
 
-				// Get successfully processed post IDs
-				const successfulIds = Object.keys(data.result.posts);
-
-				if (successfulIds.length === 0) {
+				if (Object.keys(data.result.success).length > 0) {
+					this.checkCompletedChanges(Object.entries(data.result.success));
+				}
+				if (Object.keys(data.result.errors).length > 0) {
+					this.checkFailedChanges(Object.entries(data.result.errors));
 					return;
 				}
 
-				// Clear from both persistent and in-memory storage
-				this.changesStore.deleteMany(successfulIds);
-				successfulIds.forEach(id => this.changes.delete(id));
+				if (Object.keys(data.result.success).length === 0) {
+					console.log(data.result.success);
+					data.result.success.forEach(id => this.changesStore.delete(id));
+
+					this.store.clearCache();
+				}
+			}
+
+			if (event === 'sent-to-server') {
+				if (data instanceof FormData) return;
+
+				for ( let [id, changes] of Object.entries(data.posts)) {
+					this.compareStored(id, changes);
+				}
 			}
 
 		});
 	}
+	checkCompletedChanges(items) {
+		for (let [id, data] of items) {
+			this.compareStored(id, data);
+		}
+	}
+		compareStored(id, data) {
+			let stored = this.changesStore.get(id);
+			if (!stored) return;
+
+			for (let [field, value] of Object.entries(data)) {
+				if (Object.hasOwn(stored, field)) {
+					let changes = window.getDifferences.map(stored[field], value);
+					if (!changes) {
+						delete stored[field];
+					} else {
+						stored[field] = changes;
+					}
+				}
+			}
+
+			let hasID = Object.hasOwn(stored, 'id');
+			let hasContent = Object.hasOwn(stored, 'content');
+			if ((hasID && hasContent && Object.keys(stored).length === 2)
+				|| ((hasID || hasContent) && Object.keys(stored).length === 1)
+				|| Object.keys(stored).length === 0
+			) {
+				this.changesStore.delete(id);
+				this.store.clearCache();
+			} else {
+				this.changesStore.save(stored);
+			}
+		}
+	checkFailedChanges(items) {
+		//TODO do something.
+	}
 
 	initSettings() {
 		this.defaults = {
@@ -609,7 +665,7 @@
 				default: 'closed',
 			},
 			showUploader: {
-				element: this.ui.uploader,
+				element: this.ui.uploader.details,
 				default: 'open'
 			}
 		};
@@ -661,6 +717,7 @@
 		let title = `Saving changes for multiple ${this.plural}`;
 
 		this.scheduleSave(0);
+		this.modals.edit.handleClose();
 	}
 
 	async handleCreateSubmit(modal) {
@@ -805,42 +862,59 @@
 
 	handleItemUpdate(e) {
 		let item = window.targetCheck(e, '[data-item-id]');
-
 		if (!item) return;
+
+		// Check if inside a collection field first
+		const collection = e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');
+
+		let name, value;
+		if (collection) {
+			name = collection.dataset.field;
+			value = this.forms.getFieldValue(collection);
+		} else {
+			let field = e.target.closest('[data-field]');
+			name = field.dataset.field;
+			value = this.forms.getFieldValue(e.target);
+		}
+
 		item.dataset.itemId.split(',').forEach(itemId => {
-			let field = this.forms.getField(e.target);
-			if (['repeater', 'tag-list'].includes(field.dataset.fieldType)) {
-				return;
-			}
-			let name = field.dataset.field;
-			let value = this.forms.getFieldValue(e.target);
 			this.updateItem(itemId, name, value);
 		});
 	}
 	updateItem(itemId, name, value) {
+		if (this.isPopulating) {
+			return;
+		}
+		name.replace(`[${itemId}]`, '');
+
+		const stored = this.store.get(itemId);
+		if (stored) {
+			const storedValue = stored.fields?.[name] ?? stored[name];
+			const diff = window.getDifferences.map(storedValue, value);
+
+			if (diff === null) {
+				// Value matches stored — clean up any pending change for this field
+				if (this.changes.has(itemId)) {
+					delete this.changes.get(itemId)[name];
+					// If no real changes left, remove the item entirely
+					const remaining = Object.keys(this.changes.get(itemId))
+						.filter(k => k !== 'id' && k !== 'content');
+					if (remaining.length === 0) {
+						this.changes.delete(itemId);
+						this.changesStore.delete(itemId);
+					}
+				}
+				return;
+			}
+		}
+
 		if (!this.changes.has(itemId)) {
 			this.changes.set(itemId, { id: itemId, content: this.content });
 		}
 		this.changes.get(itemId)[name] = value;
 
-		for (const key of this.uploadedFields) {
-			const [itemId, fieldName] = key.split('_');
-			if (this.changes.has(itemId)) {
-				delete this.changes.get(itemId)[fieldName];
-			}
-		}
-
-		// Don't schedule if only base keys remain
-		const change = this.changes.get(itemId);
-		const realKeys = Object.keys(change).filter(k => k !== 'id' && k !== 'content');
-		if (realKeys.length === 0) {
-			this.changes.delete(itemId);
-			return;
-		}
-
 		this.scheduleBackup();
-		//Only send actual itemIds to server. If this is a recently uploaded item, just store changes for now
-		if (typeof itemId === 'number' || !itemId.includes('group')) {
+		if (typeof itemId === 'number' || !String(itemId).includes('group')) {
 			this.scheduleSave();
 		}
 	}
@@ -993,7 +1067,7 @@
 			return;
 		}
 
-		if (e.target.matches(this.selectors.buttons.create)) {
+		if (e.target.matches(this.selectors.buttons.create) || e.target.closest(this.selectors.buttons.create)) {
 			this.openCreateModal();
 		}
 	}
@@ -1001,8 +1075,8 @@
 			this.forms.registerForm(this.ui.modals.create.form,{
 				cache: false,
 			});
-
 			this.ui.modals.create.modal.dataset.itemId = window.generateID('new');
+
 			this.modals.create.handleOpen();
 		}
 		handleActionButton(button) {
@@ -1210,17 +1284,30 @@
 		this.activeItem = item.id;
 		this.ui.modals.edit.modal.dataset.itemId = itemID;
 		this.ui.modals.edit.modal.dataset.content = this.content;
-		this.ui.modals.edit.h2.textContent = `Editing ${item.fields.post_title === '' ? this.singular : item.fields.post_title}`;
+		let title;
+		if (Object.hasOwn(item.fields, 'post_title')) {
+			title = item.fields.post_title;
+		} else if (Object.hasOwn(item.fields, 'name')) {
+			title = item.fields.name;
+		}
+		this.ui.modals.edit.h2.textContent = `Editing ${title === '' ? this.singular : title}`;
 		this.ui.modals.edit.form.dataset.formId = `edit-${itemID}`;
 
+
+		this.modals.edit.handleOpen();
 		this.forms.registerForm(this.ui.modals.edit.form, {cache: false,
 			autoUpload: true,});
 
+
 		this.isPopulating = true;
 		this.populate.populate(this.ui.modals.edit.form, item);
-		this.isPopulating = false;
+		//For quill/taxonomy selector's async setups
+		requestAnimationFrame(() => {
+			requestAnimationFrame(() => {
+				this.isPopulating = false;
+			});
+		});
 
-		this.modals.edit.handleOpen();
 	}
 	openBulkEditModal() {
 		window.removeChildren(this.ui.modals.bulkEdit.selected);
@@ -1246,11 +1333,15 @@
 		}
 		this.modals.bulkEdit.handleOpen();
 
-		this.forms.registerForm(this.ui.modals.bulkEdit.form, {cache:false});
 
+		this.forms.registerForm(this.ui.modals.bulkEdit.form, {cache:false});
 		this.isPopulating = true;
 		this.populate.populate(this.ui.modals.edit.form, item);
-		this.isPopulating = false;
+		requestAnimationFrame(() => {
+			requestAnimationFrame(() => {
+				this.isPopulating = false;
+			});
+		});
 	}
 
 	/*****************************************************************
@@ -1262,7 +1353,11 @@
 			this.cancelBackup();
 			await this.handleBackup();
 		}
-		const changes = await this.changesStore.getAll();
+		let changes = await this.changesStore.getAll();
+		if (changes.length === 0) return;
+
+		// Filter out false positives
+		changes = this.validateChanges(changes);
 		if (changes.length === 0) return;
 
 		if (title === '') {
@@ -1274,17 +1369,8 @@
 
 		changes.forEach(change => {
 			let itemId = change.id;
-			const { id, content, ...fields } = change;
-
-			// Filter out uploaded fields
-			for (const key of this.uploadedFields) {
-				const [uid, fieldName] = key.split('_');
-				if (uid === itemId) delete fields[fieldName];
-			}
-
-			if (Object.keys(fields).length > 0) {
-				allChanges[itemId] = { content, ...fields };
-			}
+			const { id, ...changeWithoutId } = change;
+			allChanges[itemId] = changeWithoutId;
 
 			if (change.post_status && this.shouldRemoveItemUI(change.post_status)) {
 				remove.push(itemId);
@@ -1310,6 +1396,44 @@
 		this.queue.addToQueue(operation);
 	}
 
+	/**
+	 * Compare pending changes against the store, removing unchanged fields.
+	 * Returns cleaned array (may be empty if nothing actually changed).
+	 */
+	validateChanges(changes) {
+		return changes.reduce((valid, change) => {
+			const { id, content, ...fields } = change;
+			const stored = this.store.get(id);
+
+			if (!stored) {
+				valid.push(change);
+				return valid;
+			}
+
+			const realChanges = { id, content };
+			let hasRealChange = false;
+
+			for (const [name, value] of Object.entries(fields)) {
+				const storedValue = stored.fields?.[name] ?? stored[name];
+				const diff = window.getDifferences.map(storedValue, value);
+
+				if (diff !== null) {
+					realChanges[name] = value;
+					hasRealChange = true;
+				}
+			}
+
+			if (hasRealChange) {
+				valid.push(realChanges);
+			} else {
+				this.changes.delete(id);
+				this.changesStore.delete(id);
+			}
+
+			return valid;
+		}, []);
+	}
+
 
 	setBulkStatus(status) {
 		if (!['publish', 'draft', 'trash', 'delete'].includes(status)) return;

--
Gitblit v1.10.0