From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh
---
assets/js/concise/CRUD.js | 324 ++++++++++++++++++++++++++++++++++++++++-------------
1 files changed, 243 insertions(+), 81 deletions(-)
diff --git a/assets/js/concise/CRUD.js b/assets/js/concise/CRUD.js
index 2bbe11a..03ebd98 100644
--- a/assets/js/concise/CRUD.js
+++ b/assets/js/concise/CRUD.js
@@ -52,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??'';
}
@@ -130,17 +131,17 @@
setup({ el, refs, manyRefs, data }) {
baseSetup(el, refs, data);
- manyRefs?.inputs?.forEach(el => {
- let wrapper = el.closest('[data-field]');
- window.prefixInput(el, `${data.id}-`, wrapper);
- });
+ data.prefix = `${data.id}-`;
- manyRefs?.status?.forEach(el => {
- if (el.value === data.status) {
- el.checked = true;
- }
- });
+ window.jvbPopulate.populate(el, data);
+ //
+ // manyRefs?.status?.forEach(el => {
+ // if (el.value === data.status) {
+ // el.checked = true;
+ // }
+ // });
+ //
if (crud.isTimeline) {
if (refs.sharedRow) {
refs.sharedRow.querySelectorAll('input,select,textarea').forEach(input => {
@@ -286,18 +287,20 @@
month: 'dialog.date-range .month-select',
}
},
+ wrap: '#item-wrap',
grid: `.${this.content}.item-grid`,
table: {
nav: '#vertical',
form: 'form.table',
table: 'form.table table',
- body: 'form.table body',
+ body: 'form.table tbody',
head: 'form.table thead',
foot: 'form.table tfoot',
selectedColumns: '.all-filters .multi-select',
columns: 'thead th',
},
bulk: {
+ hasEdit: '.bulk-action-select [value="edit"]',
action: '.bulk-action-select',
count: '.bulk-controls .selected-count',
control: '.bulk-controls .bulk-actions',
@@ -323,7 +326,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);
@@ -339,17 +346,32 @@
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();
});
}
}
+
+ if (event === 'sent-to-queue' && data.field) {
+ const fieldName = data.field.config.name;
+ const itemId = data.field.config.itemID;
+ if (itemId && fieldName) {
+ if (this.changes.has(itemId)) {
+ delete this.changes.get(itemId)[fieldName];
+ }
+ }
+ }
});
}
initModals() {
@@ -411,6 +433,7 @@
{ name: 'modified', keyPath: 'modified'},
{ name: 'title', keyPath: 'title'},
],
+ isAuth: true,
filters: filters,
ignore: ['content', 'user'],
TTL: 60 * 60 * 1000, //1 hour cache
@@ -458,12 +481,12 @@
span: '.bulk-select label span'
},
wrapper: {
- wrapper: '.wrap'
+ wrapper: this.selectors.wrap,
},
item: {
idAttribute: 'itemId'
}
- });
+ }, this.ui.views.table !== null);
this.selectionHandler.subscribe((event, data) => {
this.selected = new Set([...data.selectedItems].map(id => parseInt(id)));
this.ui.bulk.control.hidden = this.selected.size === 0;
@@ -502,35 +525,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' && data.type === 'content_update') {
+ 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 = {
@@ -547,6 +621,10 @@
//current view (defaults to grid)
let defaultView = this.container.dataset.view??'grid'
this.view = this.cache.get('view')??defaultView;
+
+ if (!this.view) {
+ this.view = defaultView;
+ }
if (this.view !== defaultView) {
this.ui.views[this.view].checked = true;
}
@@ -583,6 +661,7 @@
if (this.ui.table.nav && tabDirection === 'vertical') {
this.ui.table.nav.checked = true;
}
+ this.tabNav = tabDirection !== 'horizontal';
@@ -593,7 +672,7 @@
default: 'closed',
},
showUploader: {
- element: this.ui.uploader,
+ element: this.ui.uploader.details,
default: 'open'
}
};
@@ -645,6 +724,7 @@
let title = `Saving changes for multiple ${this.plural}`;
this.scheduleSave(0);
+ this.modals.edit.handleClose();
}
async handleCreateSubmit(modal) {
@@ -699,6 +779,9 @@
const isBulkAction = e.target.matches('.bulk-action-select');
const isView = e.target.matches('[data-view]');
+ if (this.view === 'table') {
+ this.handleTableChange(e);
+ }
if (!inItem && !isFilter && !isBulkAction && !isView) return;
if (!this.isPopulating && inItem && !e.target.closest('[data-ignore], .select-item')) {
@@ -722,19 +805,21 @@
return;
}
- // Table-specific handlers
- if (this.view === 'table') {
- if (e.target.matches('details.multi-select')) {
- this.toggleColumn(e.target.id, e.target.checked);
- return;
- }
+ }
+ handleTableChange(e) {
+ // Table-specific handlers
+ if (this.view === 'table') {
+ if (e.target.matches('details.multi-select')) {
+ this.toggleColumn(e.target.id, e.target.checked);
+ return;
+ }
- if (e.target.matches(this.selectors.table.nav)) {
- this.tabNav = e.target.checked;
- this.cache.set('tabNav', e.target.checked ? 'vertical' : 'horizontal');
+ if (e.target.matches(this.selectors.table.nav)) {
+ this.tabNav = e.target.checked;
+ this.cache.set('tabNav', e.target.checked ? 'vertical' : 'horizontal');
+ }
}
}
- }
handleBulkAction(bulkAction) {
if (bulkAction.value.startsWith('tax-')) {
const selectedOption = bulkAction.options[bulkAction.selectedIndex];
@@ -789,27 +874,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;
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();
}
}
@@ -962,7 +1079,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();
}
}
@@ -970,8 +1087,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) {
@@ -1113,7 +1230,7 @@
// For regular tables, use all rows in tbody
if (!this.ui.table.body) return null;
- const rows = Array.from(this.ui.table.body.querySelectorAll('tr'))
+ const rows = Array.from(this.ui.table.body.children)
.filter(row => this.isEditableRow(row));
return goBackward ? rows[rows.length - 1] : rows[0];
@@ -1179,17 +1296,33 @@
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.isPopulating = true;
+
+ 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;
- this.modals.edit.handleOpen();
+ this.populate.populate(this.ui.modals.edit.form, item);
+ //For quill/taxonomy selector's async setups
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ setTimeout(() => {
+ this.isPopulating = false;
+ }, 50);
+ });
+ });
+
}
openBulkEditModal() {
window.removeChildren(this.ui.modals.bulkEdit.selected);
@@ -1215,11 +1348,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;
+ });
+ });
}
/*****************************************************************
@@ -1231,7 +1368,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 === '') {
@@ -1243,8 +1384,6 @@
changes.forEach(change => {
let itemId = change.id;
-
- // Create a new object without the id field (don't mutate original!)
const { id, ...changeWithoutId } = change;
allChanges[itemId] = changeWithoutId;
@@ -1260,6 +1399,7 @@
let operation = {
endpoint: this.endpoint,
headers: {
+ 'X-WP-Nonce': window.auth.getNonce(),
'X-Action-Nonce': window.auth.getNonce('dash'),
},
data: {
@@ -1272,6 +1412,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;
@@ -1360,7 +1538,7 @@
if (on && !this.ui.table.form) {
let table = window.jvbTemplates.create('contentTable');
- this.container.append(table);
+ this.ui.wrap.append(table);
this.ui.table = window.uiFromSelectors(this.selectors.table);
this.ui.table.columns = this.container.querySelectorAll(this.selectors.table.columns);
}
@@ -1414,15 +1592,12 @@
this.toggleTable();
window.removeChildren(this.ui.grid);
+
await window.chunkIt(
items,
(item) => this.renderTableItem(item),
(fragment) => {
- if (this.ui.table.body) {
- this.ui.table.body.append(fragment);
- } else {
- this.ui.table.table.insertBefore(fragment, this.ui.table.foot);
- }
+ this.ui.table.body.append(fragment);
},
5
);
@@ -1660,20 +1835,7 @@
CLEANUP
***************************************************************/
resetForm(form) {
- // Clear text inputs, textareas
- form.querySelectorAll('input[type="hidden"], input[type="text"], input[type="number"], input[type="email"], input[type="url"], textarea').forEach(input => {
- input.value = '';
- });
-
- // Uncheck checkboxes and radios
- form.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(input => {
- input.checked = false;
- });
-
- // Reset selects to first option
- form.querySelectorAll('select').forEach(select => {
- select.selectedIndex = 0;
- });
+ form.reset();
// Clear any selected items displays
form.querySelectorAll('.selected-items').forEach(container => {
--
Gitblit v1.10.0