Jake Vanderwerf
2026-06-29 4089ba01e0881c89a72332e13bc3a80b6bddec2a
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,12 +346,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();
                  });
@@ -421,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
@@ -468,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;
@@ -512,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 = {
@@ -557,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;
      }
@@ -593,6 +661,7 @@
      if (this.ui.table.nav && tabDirection === 'vertical') {
         this.ui.table.nav.checked = true;
      }
      this.tabNav = tabDirection !== 'horizontal';
@@ -603,7 +672,7 @@
            default: 'closed',
         },
         showUploader: {
            element: this.ui.uploader,
            element: this.ui.uploader.details,
            default: 'open'
         }
      };
@@ -655,6 +724,7 @@
      let title = `Saving changes for multiple ${this.plural}`;
      this.scheduleSave(0);
      this.modals.edit.handleClose();
   }
   async handleCreateSubmit(modal) {
@@ -709,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')) {
@@ -732,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];
@@ -798,13 +873,22 @@
   }
   handleItemUpdate(e) {
      let item = window.targetCheck(e, '[data-item-id]');
      if (!item) return;
      let field = e.target.closest('[data-field-type="repeater"],[data-field-type="tag-list"],[data-field]');
      let name = field.dataset.field;
      let value = this.forms.getFieldValue(e.target);
      // 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 => {
         this.updateItem(itemId, name, value);
      });
@@ -813,6 +897,7 @@
      if (this.isPopulating) {
         return;
      }
      name.replace(`[${itemId}]`, '');
      const stored = this.store.get(itemId);
      if (stored) {
@@ -994,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();
      }
   }
@@ -1145,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];
@@ -1211,22 +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);
      //For quill/taxonomy selector's async setups
      requestAnimationFrame(() => {
         requestAnimationFrame(() => {
            this.isPopulating = false;
            setTimeout(() => {
               this.isPopulating = false;
            }, 50);
         });
      });
      this.modals.edit.handleOpen();
   }
   openBulkEditModal() {
      window.removeChildren(this.ui.modals.bulkEdit.selected);
@@ -1303,6 +1399,7 @@
      let operation = {
         endpoint: this.endpoint,
         headers: {
            'X-WP-Nonce': window.auth.getNonce(),
            'X-Action-Nonce': window.auth.getNonce('dash'),
         },
         data: {
@@ -1441,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);
      }
@@ -1495,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
      );
@@ -1741,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 => {