Jake Vanderwerf
2025-11-23 d7dbe7fee362d587dfc334135d9581b6216a4295
assets/js/dash/CRUD.js
@@ -4,7 +4,6 @@
class CRUDManager {
   constructor(config) {
      this.queue = window.jvbQueue;
      console.log(this.queue);
      this.config = config;
      this.content = config.content || false;
      this.settings = window.jvbUserSettings;
@@ -13,37 +12,71 @@
         return;
      }
      this.isTimeline = false;
      this.currentItemID = null;
      this.initElements();
      this.updateBulkOptions();
      // Initialize components
      this.store = new window.jvbStore({
         name: this.content,
         storeName: this.content,
         endpoint: 'content',
         headers: {
            'action_nonce': jvbSettings.dash,
         },
         indexes: [
            { name: 'status', keyPath: 'post_status'},
            { name: 'modified', keyPath: 'modified'},
         ],
         filters: {
            content: this.content,
            user: jvbSettings.currentUser,
            page: 1,
            status: 'all'
         },
         TTL: 3600000,
         cacheDOM: true
      });
      this.store = window.jvbStore.register(
         this.content,
         {
            keyPath: 'id',
            endpoint: 'content',
            headers: {
               'action_nonce': jvbSettings.dash,
            },
            indexes: [
               {name: 'id', keyPath: 'id'},
               { name: 'status', keyPath: 'status'},
               { name: 'date', keyPath: 'date'},
               { name: 'modified', keyPath: 'modified'},
               { name: 'title', keyPath: 'title'}
            ],
            filters: {
               content: this.content,
               user: jvbSettings.currentUser,
               page: 1,
               status: 'all',
               orderby: 'modified', //or title
               order: 'desc'
            },
            TTL: 30 * 60 * 1000, //30 minutes cache
         });
      this.status = 'all';
      this.filterTimeout = null;
      this.viewController = new window.jvbViews(this.ui.container, this.store);
      this.formController = new window.jvbForm();
      this.tableForm = null;
      this.tableChanges = new Map();
      this.formController = (this.isTimeline) ? new window.jvbForm({collectFormData: () => this.collectTimelineData.bind(this)}) : new window.jvbForm();
      this.viewController.subscribe((event, form) => {
         if (event === 'table-view' && !this.tableForm) {
            if (!this.tableForm) {
               this.tableForm = this.formController.registerForm(form, {
                  autosave: false,
                  formStatus: false,
                  isTable: true,
               });
            }
         } else if (event === 'not-table-view') {
            if (this.tableForm) {
            }
         } else if (event === 'order-changed') {
            let data = this.store.get(form);
            if (!data) {
               return;
            }
            let changes = {};
            changes[form] = data;
            this.savePosts(changes, `Updating progression order`);
         }
      });
      this.formController.subscribe((event, data) => {
         switch(event) {
@@ -54,15 +87,16 @@
         }
      });
      if (window.jvbQueue) {
         window.jvbQueue.subscribe((event, data) => {
            if (event === 'operation-completed' && data.source === 'form') {
               this.handleQueueSuccess(event, data);
            } else if (event === 'operation-failed-permanent' && data.source === 'form') {
               this.handleQueueFailure(event, data);
            }
         });
      }
      this.queue.subscribe((event, data) => {
         if (!Object.hasOwn(data, 'endpoint') || data.endpoint !== 'content') return;
         console.log('Queue Subscription in CRUD.js: ', data);
         if (event === 'operation-completed') {
            this.handleQueueSuccess(event, data);
         } else if (event === 'operation-failed-permanent') {
            this.handleQueueFailure(event, data);
         }
      });
      // Track initialization
      this.initialized = false;
@@ -70,35 +104,32 @@
      this.init();
   }
   handleFormChange(event, data) {
      let title = data.fullData.post_title;
      let changes = (Object.hasOwn(data, 'changes')) ? data.changes : data.fullData;
      let theChanges = {};
      if (this.isTimeline) {
         data.changes.content = this.content;
         let changes = {};
         let postID = data.config.id.replace('edit-', '');
         changes[postID] = data.changes;
         this.savePosts(changes, `Saving ${data.fullData.post_title}'s Changes`);
         theChanges[this.currentItemID] = changes;
         this.savePosts(theChanges, title);
         return;
      }
      data.changes.content = this.content;
      let changes = {};
      let title = '';
      let itemsToRemove = [];
      switch (true) {
         case data.config.element === this.ui.forms.edit:
            let postID = data.config.id.replace('edit-', '');
            console.log(postID);
            changes[postID] = data.changes;
            title = `Saving ${data.fullData['post_title']} Changes`;
            theChanges[this.currentItemID] = changes;
            title = `Saving ${title} Changes`;
            // Check if status change requires removal
            if (data.changes.post_status && this.shouldRemoveItem(data.changes.post_status)) {
               itemsToRemove.push(postID);
            if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
               itemsToRemove.push(this.currentItemID);
            }
            break;
         case data.config.element === this.ui.forms.bulkEdit:
            let selected = data.config.element.querySelectorAll('.selected input:checked');
            selected.forEach(sel => {
               changes[sel.value] = data.changes;
               theChanges[sel.value] = changes;
               // Check if status change requires removal
               if (data.changes.post_status && this.shouldRemoveItem(data.changes.post_status)) {
               if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
                  itemsToRemove.push(sel.value);
               }
            });
@@ -107,8 +138,8 @@
            break;
         case data.config.element === this.ui.forms.create:
            if (event === 'form-submit') {
               changes[data.config.data['form-id']] = data.fullData;
               title = `Saving ${data.fullData['post_title']} Changes`;
               theChanges[data.config.data['form-id']] = changes;
               title = `Saving ${title} Changes`;
            }
            break;
      }
@@ -134,11 +165,11 @@
         }
      }
      if (window.isEmptyObject(changes)) {
      if (window.isEmptyObject(theChanges)) {
         return;
      }
      this.savePosts(changes, title);
      this.savePosts(theChanges, title);
   }
   shouldRemoveItem(newStatus) {
@@ -150,6 +181,13 @@
      if (window.isEmptyObject(changes)) {
         return;
      }
      //ensure content is in each post
      for (let postId in changes) {
         if (!changes[postId]['content']) {
            changes[postId]['content'] = this.content;
         }
      }
      let operation = {
         endpoint: 'content',
         headers: {
@@ -165,15 +203,15 @@
      this.queue.addToQueue(operation);
   }
   handleQueueSuccess(event, data) {
      console.log('Handling queue success...');
      console.log('Event', event);
      console.log('Data', data);
   async handleQueueSuccess(event, data) {
      this.store.clearCache();
      this.store.clearHttpHeaders();
      this.store.fetch();
   }
   handleQueueFailure(event, data) {
      console.log('Handling queue failure...');
      console.log('Event', event);
      console.log('Data', data);
      console.error('Operation failed permanently:', data);
      // Optionally show error notification to user
      this.a11y?.announce(`Operation failed: ${data.error_message || 'Unknown error'}`);
   }
   initElements() {
@@ -194,14 +232,11 @@
         uploader: 'details.uploader'
      };
      this.ui = window.uiFromSelectors(this.elements);
      this.isTimeline = Object.hasOwn(this.ui.forms.edit.dataset, 'timeline');
      this.isTimeline = !!document.querySelector('[data-timeline]');
   }
   init() {
      this.settings.addSetting(this.ui.uploader, 'open');
      this.ui.uploader.addEventListener('toggle', (e) =>{
         console.log(e);
         console.log('Is Open: ', this.ui.uploader.open);
         console.log(this.ui.uploader.open ? 'on' : 'off');
         this.settings.saveSetting('open', this.ui.uploader.open ? 'on' : 'off');
      });
@@ -219,9 +254,9 @@
         this.modals[name].subscribe((event, data) => {
            switch (event) {
               case 'modal-close':
                  this.currentItemID = null;
                  this.formController.cleanupForm(this.modals[name].modal.querySelector('form').dataset.formId);
                  //double check we have finished saving
                  console.log('Data on modal close: ', data);
                  break;
               case 'modal-open':
                  //probably not needed in this class
@@ -235,15 +270,6 @@
      this.setupFilters();
      this.queue.subscribe((event, data) => {
         switch (event) {
            case 'operation-status':
               //update items?
               break;
         }
      });
      this.initialized = true;
   }
@@ -347,6 +373,14 @@
      });
   }
   handleChange(e) {
      if (e.target.closest('[data-id]')) {
         if (this.isTimeline) {
            this.handleTimelineTableChange(e);
         } else {
            this.handleTableChange(e);
         }
         return;
      }
      if (e.target.classList.contains('bulk-action-select')) {
         if (e.target.value.startsWith('tax-')) {
            const taxonomy = e.target.value.replace('tax-', '');
@@ -378,6 +412,73 @@
         }
      }
   }
   handleTableChange(e) {
      const row = e.target.closest('tr[data-id]');
      if (!row) return;
      const input = e.target;
      const postID = parseInt(row.dataset.id);
      const fieldName = input.closest(['data-field'])?.dataset.field;
      if (!fieldName) return;
      const item = this.store.get(postID);
      if (!item) return;
      item.fields[fieldName] = this.getInputValue(input);
      this.store.save(item);
      let post = {};
      post[postID] = item.fields;
      this.savePosts(post, `Saving changes to ${this.content}`);
   }
   handleTimelineTableChange(e) {
      const tbody = e.target.closest('tbody[data-id]');
      if (!tbody) return;
      const input = e.target;
      const fieldName = input.closest('[data-field]')?.dataset.field;
      if (!fieldName) return;
      const parentID = parseInt(tbody.dataset.id);
      const timelinePoint = input.closest('tr.timeline-point');
      const item = this.store.get(parentID);
      if (!item) return;
      const value = this.getInputValue(input);
      // Check if this is a specific point, or a shared value
      if (timelinePoint) {
         const imgID = timelinePoint.dataset.imageId;
         if (!item.fields.timeline) {
            item.fields.timeline = {};
         }
         if (!item.fields.timeline[imgID]) {
            item.fields.timeline[imgID] = {};
         }
         item.fields.timeline[imgID][fieldName] = value;
      } else {
         item.fields[fieldName] = value;
      }
      //Update store directly
      this.store.save(item);
      let changes = {};
      changes[parentID] = item.fields;
      this.savePosts(changes, 'Updating progress post');
   }
   getInputValue(input) {
      if (input.type === 'checkbox') {
         return input.checked ? (input.value || '1') : '';
      }
      if (input.type === 'radio') {
         return input.checked ? input.value : null;
      }
      return input.value;
   }
   openTaxonomyModal(taxonomy) {
      // Check if jvbSelector exists
@@ -393,14 +494,13 @@
      );
   }
   handleBulkTaxonomy(selectedIds, taxonomy) {
      console.log(taxonomy, selectedIds);
      // Callback when terms are selected
      if (selectedIds.length > 0) {
         selectedIds = selectedIds.join(',');
         let changes = {};
         let selected = Array.from(this.viewController.selectedItems);
         console.log('selected',selected);
         selected.forEach(sel => {
            changes[sel] = {
               content: this.content
@@ -408,7 +508,6 @@
            changes[sel][taxonomy] = selectedIds;
         });
         console.log('Taxonomy changes: ', changes);
         let title = `Adding ${selected.length} ${this.config.plural??'posts'} to ${selectedIds.length} ${jvbSettings.labels[taxonomy].plural}`;
         this.viewController.clearSelection();
@@ -420,7 +519,7 @@
      if (!['publish', 'draft', 'trash', 'delete'].includes(status)){
         return;
      }
      console.log(`Setting status: ${status}`);
      let changes = {};
      for (let selected of this.viewController.selectedItems) {
         changes[selected] = {
@@ -436,7 +535,7 @@
         default:
            title = window.uppercaseFirst(status)+'ing';
      }
      console.log(this.status);
      if ((this.status === 'all' && !['publish', 'draft'].includes(status)) || status !== this.status) {
         let delay = 0;
         for (let selected of this.viewController.selectedItems) {
@@ -503,9 +602,8 @@
      window.removeChildren(container);
      for (let selected of this.viewController.selectedItems) {
         console.log(selected);
         let item = this.store.get(selected);
         console.log(item);
         const img = window.getTemplate('bulkItem');
         if (!img) return;
@@ -533,14 +631,12 @@
      ];
      this.formController.registerForm(this.ui.forms.bulkEdit);
      console.log('Bulk Edit form registered');
   }
   populateEditForm(itemID) {
      console.log(itemID);
      this.currentItemID = itemID;
      let item = this.store.get(parseInt(itemID));
      console.log(item);
      if (item) {
         this.ui.modals.edit.dataset.itemID = itemID;
         this.ui.modals.edit.dataset.content = this.content;
@@ -552,10 +648,8 @@
            `Editing ${item.fields.post_title}`
         ];
         form.dataset.formId = `edit-${itemID}`;
         console.log(form.dataset.formId);
         new window.jvbPopulate(form, item.fields, item.images);
         this.formController.registerForm(this.ui.forms.edit);
         console.log('Edit form registered');
      }
   }
@@ -593,7 +687,6 @@
      document.querySelectorAll('[data-filter]').forEach(filter => {
         filter.removeEventListener('change', this.filterHandler);
      });
      this.store.subscribers.clear();
   }
}