| | |
| | | |
| | | const baseSetup = (el, refs, data) => { |
| | | el.dataset.itemId = data.id; |
| | | |
| | | window.prefixInput(refs.checkbox, `select-${data.id}`, true); |
| | | let wrapper = refs.checkbox.closest('.preview'); |
| | | window.prefixInput(refs.checkbox, `select-${data.id}`, wrapper, true); |
| | | refs.checkbox.value = data.id; |
| | | refs.checkbox.checked = crud.selected.has(parseInt(data.id)); |
| | | if (refs.selectLabel) refs.selectLabel.htmlFor = `select-${data.id}`; |
| | |
| | | baseSetup(el, refs, data); |
| | | |
| | | manyRefs?.inputs?.forEach(el => { |
| | | window.prefixInput(el, `${data.id}-`); |
| | | let wrapper = el.closest('[data-field]'); |
| | | window.prefixInput(el, `${data.id}-`, wrapper); |
| | | }); |
| | | |
| | | manyRefs?.status?.forEach(el => { |
| | |
| | | if (crud.isTimeline) { |
| | | if (refs.sharedRow) { |
| | | refs.sharedRow.querySelectorAll('input,select,textarea').forEach(input => { |
| | | window.prefixInput(input, `${data.id}-`); |
| | | let wrapper = input.closest('[data-field]'); |
| | | window.prefixInput(input, `${data.id}-`, wrapper); |
| | | }); |
| | | |
| | | crud.populate.populate(refs.sharedRow, data); |
| | |
| | | point.dataset.itemId = timeline.id; |
| | | |
| | | point.querySelectorAll('input,select,textarea').forEach(input => { |
| | | window.prefixInput(input, `${timeline.id}-`); |
| | | let wrapper = input.closest('[data-field]'); |
| | | window.prefixInput(input, `${timeline.id}-`, wrapper); |
| | | }); |
| | | |
| | | crud.populate.populate(point, { |
| | |
| | | if (crud.ui.table.form?.dataset.edit !== undefined) { |
| | | // Non-timeline: prefix all inputs normally |
| | | manyRefs?.inputs?.forEach(input => { |
| | | window.prefixInput(input, `${data.id}-`); |
| | | let wrapper = input.closest('[data-field]'); |
| | | window.prefixInput(input, `${data.id}-`, wrapper); |
| | | }); |
| | | |
| | | manyRefs?.status?.forEach(el => { |
| | |
| | | this.forms.clearForm(formId); |
| | | } |
| | | |
| | | this.ui.modals[name].form.reset(); |
| | | this.resetForm(this.ui.modals[name].form); |
| | | |
| | | if (name === 'date') { |
| | | this.handleCustomDateSelection() |
| | | } |
| | | if (['edit','bulkEdit','create'].includes(name)) { |
| | | //handle escapes (not form submits) |
| | | if (window.debouncer.timeouts.has(`save-${this.content}`)) { |
| | | this.scheduleSave(0); |
| | | } |
| | | } |
| | | break; |
| | | case 'modal-open': |
| | | |
| | |
| | | } |
| | | if (event === 'operation-status' |
| | | && data.status === 'completed' |
| | | && data.endpoint === 'content' |
| | | && Object.keys(data.data?.posts??{}).length > 0) { |
| | | && data.endpoint === 'uploads/groups') { |
| | | |
| | | console.log('Cleared local cache. Refresh to see changes'); |
| | | this.store.clearCache(); |
| | | let ids = Object.keys(data.data.posts); |
| | | let storedChanges = this.changesStore.getMany(ids); |
| | | } |
| | | if (event === 'operation-status' |
| | | && data.status === 'completed' |
| | | && data.type === 'content_update') { |
| | | console.log('Cleared local cache. Refresh to see changes'); |
| | | this.store.clearCache(); |
| | | |
| | | this.changesStore.deleteMany(ids); |
| | | |
| | | for (let id of ids) { |
| | | let stored = storedChanges.filter(change => change.id === id)[0]??false; |
| | | |
| | | let sentChanges = data.data.posts[id]; |
| | | let remainingChanges = {}; |
| | | |
| | | for (let [key, value] of Object.entries(sentChanges)) { |
| | | if (stored && !Object.hasOwn(stored, key)) continue; |
| | | if (stored[key] === value) { |
| | | delete stored[key]; |
| | | } |
| | | remainingChanges[key] = value; |
| | | } |
| | | if (Object.keys(remainingChanges).length > 0) { |
| | | remainingChanges['id'] = id; |
| | | remainingChanges['content'] = this.content; |
| | | this.changes.set(id, remainingChanges); |
| | | } |
| | | // Check for result data (from ContentExecutor) |
| | | if (!data.result || !data.result.posts) { |
| | | console.warn('Content update completed but no result.posts', data); |
| | | return; |
| | | } |
| | | if (Object.values(this.changes).length > 0) { |
| | | this.scheduleBackup(); |
| | | |
| | | // Get successfully processed post IDs |
| | | const successfulIds = Object.keys(data.result.posts).filter(id => { |
| | | return data.result.posts[id]?.success === true; |
| | | }); |
| | | |
| | | if (successfulIds.length === 0) { |
| | | return; |
| | | } |
| | | |
| | | // Clear from both persistent and in-memory storage |
| | | this.changesStore.deleteMany(successfulIds); |
| | | successfulIds.forEach(id => this.changes.delete(id)); |
| | | } |
| | | |
| | | }); |
| | |
| | | } else if (modal.classList.contains('create')) { |
| | | title = `Creating your new ${this.singular}`; |
| | | } |
| | | this.savePosts(title,false); |
| | | this.scheduleSave(0); |
| | | } |
| | | handleChange(e) { |
| | | // Early bailout - target must be in an item or be a filter |
| | |
| | | handleBulkTaxonomy(result) { |
| | | if (!result.termIds.length || !this.selected.size) return; |
| | | |
| | | const changes = {}; |
| | | const taxonomyField = `tax_${result.taxonomy}`; |
| | | |
| | | this.selected.forEach(itemID => { |
| | | const item = this.store.get(parseInt(itemID)); |
| | | const item = this.store.get(itemID); |
| | | if (!item) return; |
| | | |
| | | // Merge existing terms with new ones |
| | |
| | | const existingIds = existingTerms.map(t => t.id); |
| | | const newIds = [...new Set([...existingIds, ...result.termIds])]; |
| | | |
| | | changes[itemID] = { |
| | | [taxonomyField]: newIds.join(','), |
| | | content: this.content |
| | | }; |
| | | this.updateItem(itemID, result.taxonomy, newIds); |
| | | }); |
| | | |
| | | if (Object.keys(changes).length > 0) { |
| | | this.savePosts( |
| | | `Adding ${result.terms.length} ${result.taxonomy} to ${this.selected.size} ${this.plural}...`, |
| | | false |
| | | ).then(()=>{}); |
| | | } |
| | | this.savePosts(`Adding ${result.terms.length} ${result.taxonomy} to ${this.selected.size} ${this.plural}...`,).then(()=> {}); |
| | | |
| | | this.selectionHandler.clearSelection(); |
| | | } |
| | |
| | | if (!item) return; |
| | | 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); |
| | | }); |
| | | this.savePosts('', true).then(()=>{}); |
| | | } |
| | | updateItem(itemId, name, value) { |
| | | if (!this.changes.has(itemId)) { |
| | |
| | | this.changes.get(itemId)[name] = value; |
| | | |
| | | this.scheduleBackup(); |
| | | this.scheduleSave(); |
| | | } |
| | | scheduleBackup() { |
| | | window.debouncer.schedule( |
| | | `changes-${this.content}`, |
| | | async () => { |
| | | if (this.changes.size > 0) { |
| | | await this.changesStore.saveMany(this.changes); |
| | | this.changes.clear(); |
| | | await this.handleBackup(); |
| | | } |
| | | }, |
| | | 2000 |
| | | ); |
| | | } |
| | | cancelBackup() { |
| | | window.debouncer.cancel(`changes-${this.content}`); |
| | | } |
| | | async handleBackup() { |
| | | const changesArray = Array.from(this.changes.values()); |
| | | this.changes.clear(); |
| | | |
| | | const ids = changesArray.map(c => c.id); |
| | | const existing = await Promise.all( |
| | | ids.map(id => this.changesStore.get(id)) |
| | | ); |
| | | |
| | | const changes = changesArray.map((change, i) => |
| | | existing[i] ? window.deepMerge(existing[i], change) : change |
| | | ); |
| | | |
| | | await this.changesStore.saveMany(changes); |
| | | } |
| | | |
| | | scheduleSave(delay = 10000) { |
| | | window.debouncer.schedule( |
| | | `save-${this.content}`, |
| | | async () => { |
| | | // Ensure latest changes are in IndexedDB |
| | | if (this.changes.size > 0) { |
| | | this.cancelBackup(); |
| | | await this.handleBackup(); |
| | | } |
| | | |
| | | await this.savePosts('', false); |
| | | }, |
| | | delay |
| | | ); |
| | | } |
| | | handleFilterChange(target) { |
| | | let filter = target.dataset.filter; |
| | | |
| | |
| | | } |
| | | } |
| | | openCreateModal(){ |
| | | this.ui.modals.create.form.reset(); |
| | | this.forms.registerForm(this.ui.modals.create.form,{ |
| | | cache: false, |
| | | }); |
| | |
| | | } |
| | | break; |
| | | case 'trash': |
| | | this.updateItem(itemID, 'post_status', 'trash'); |
| | | window.fade(button.closest('.item'), false); |
| | | this.savePosts(`Sending ${this.singular} to trash...`).then(()=>{}); |
| | | if (this.status === 'trash') { |
| | | if (confirm('Delete this item? This cannot be undone')) { |
| | | this.updateItem(itemID, 'post_status', 'delete'); |
| | | window.fade(button.closest('.item'), false); |
| | | this.savePosts(`Permanently deleting ${this.singular}...`).then(()=>{}); |
| | | this.store.delete(itemID); |
| | | } |
| | | } else { |
| | | this.updateItem(itemID, 'post_status', 'trash'); |
| | | window.fade(button.closest('.item'), false); |
| | | this.savePosts(`Sending ${this.singular} to trash...`).then(()=>{}); |
| | | } |
| | | break; |
| | | case 'bulk-edit': |
| | | if (this.selected.size > 0) { |
| | |
| | | } |
| | | } |
| | | handleBulkDelete() { |
| | | let isTrash = this.store.filters.status === 'trash'; |
| | | let isTrash = this.status === 'trash'; |
| | | if (this.selected.size > 0 && confirm(`${isTrash ? 'Permanently delete' : 'Send'} ${this.selected.size} ${this.selected.size === 1 ? this.singular : this.plural}${isTrash ? '' : 'to trash'}?`)) { |
| | | this.selected.forEach(id => { |
| | | this.store.delete(id); |
| | |
| | | this.ui.modals.edit.h2.textContent = `Editing ${item.fields.post_title === '' ? this.singular : item.fields.post_title}`; |
| | | this.ui.modals.edit.form.dataset.formId = `edit-${itemID}`; |
| | | |
| | | this.ui.modals.edit.form.reset(); |
| | | |
| | | this.forms.registerForm(this.ui.modals.edit.form, {cache: false}); |
| | | |
| | | this.isPopulating = true; |
| | |
| | | |
| | | 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; |
| | | this.isPopulating = true; |
| | | this.populate.populate(this.ui.modals.edit.form, item); |
| | | this.isPopulating = false; |
| | | } |
| | | |
| | | /***************************************************************** |
| | |
| | | *****************************************************************/ |
| | | |
| | | async savePosts(title = '', delay = false) { |
| | | const memoryChanges = Array.from(this.changes.values()); |
| | | const storedChanges = await this.changesStore.getAll(); |
| | | const changes = window.deepMerge(storedChanges, memoryChanges); |
| | | if (this.changes.size > 0) { |
| | | this.cancelBackup(); |
| | | await this.handleBackup(); |
| | | } |
| | | const changes = await this.changesStore.getAll(); |
| | | console.log('Saving Changes: ', changes); |
| | | if (changes.length === 0) return; |
| | | |
| | | if (title === '') { |
| | |
| | | updateUI() { |
| | | if (this.ui.bulk.action) { |
| | | let options = false; |
| | | let hasEdit =this.ui.bulk.action.querySelector('[value="edit"]'); |
| | | if (this.status === 'trash' && hasEdit) { |
| | | let hasEdit = this.ui.bulk.action.querySelector('[value="edit"]'); |
| | | let currentStatus = this.status; |
| | | |
| | | if (currentStatus === 'trash' && hasEdit) { |
| | | window.removeChildren(this.ui.bulk.action); |
| | | options = window.jvbTemplates.create('trashOptions'); |
| | | } else if (this.status !== 'trash' && !hasEdit) { |
| | | } else if (currentStatus !== 'trash' && !hasEdit) { |
| | | window.removeChildren(this.ui.bulk.action); |
| | | options = window.jvbTemplates.create('notTrashOptions'); |
| | | } |
| | |
| | | setFilter(name, value) { |
| | | if (!this.allowedFilters.includes(name)) return; |
| | | this.cache.set(name, value); |
| | | |
| | | if (name === 'status') this.status = value; |
| | | if (name === 'orderby') this.orderby = value; |
| | | if (name === 'order') this.order = value; |
| | | |
| | | let el = this.findFilterEl(name, value); |
| | | this.setElValue(el, value); |
| | | //TODO: If we set the element to checked, does that automatically call the change listener, which then also sets the store filter and cache? |
| | | this.store.setFilter(name, value); |
| | | } |
| | | |
| | |
| | | /*************************************************************** |
| | | 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; |
| | | }); |
| | | |
| | | // Clear any selected items displays |
| | | form.querySelectorAll('.selected-items').forEach(container => { |
| | | window.removeChildren(container); |
| | | }); |
| | | |
| | | // Clear upload previews |
| | | form.querySelectorAll('.item-grid.preview').forEach(grid => { |
| | | window.removeChildren(grid); |
| | | }); |
| | | } |
| | | destroy() { |
| | | window.debouncer.cancel(`changes-${this.content}`); |
| | | if (this.changes.size > 0) { |