/** * Simplified autosave approach for FormController * Leverages QueueManager for all server operations */ // In FormController class - replace the autosave-related methods with: class FormController { // ... existing constructor and init code ... /** * Initialize form tracking for autosave */ registerForm(formElement, options = {}) { const formId = formElement.dataset.formId || `form_${Date.now()}`; formElement.dataset.formId = formId; const formConfig = { element: formElement, id: formId, contentId: formElement.dataset.contentId || null, // For existing content contentType: formElement.dataset.contentType || null, options: { autoSave: true, saveDelay: this.autoSaveDefaults.delay, endpoint: formElement.dataset.save || formElement.action, ...options }, lastSnapshot: {}, // Last saved state isDirty: false }; // Take initial snapshot formConfig.lastSnapshot = this.collectFormData(formElement); // Initialize special fields this.initializeFormFields(formElement, formConfig); // Store form config this.forms.set(formId, formConfig); return formConfig; } /** * Simplified change handler */ handleChange(event) { const target = event.target; const form = target.form || target.closest('form'); if (!form) return; const formConfig = this.forms?.get(form.dataset.formId); if (!formConfig) return; // Check conditional fields (existing functionality) const dependencies = formConfig.dependencies?.get(target.name); if (dependencies) { dependencies.forEach(dep => { this.checkFieldDependency(form, dep.field, target.name, dep.requiredValue, dep.operator); }); } // Schedule autosave if enabled if (formConfig.options.autoSave && !form.dataset.noautosave) { const delay = this.getDelayForField(target); this.scheduleSave(formConfig, delay); } } /** * Get appropriate delay based on field type and context */ getDelayForField(field) { // Text fields get longer delay for typing if (field.type === 'text' || field.type === 'textarea') { return this.autoSaveDefaults.typingDelay; } // Checkboxes, radios, selects get shorter delay if (['checkbox', 'radio', 'select-one', 'select-multiple'].includes(field.type)) { return 1000; } // Default delay return this.autoSaveDefaults.delay; } /** * Simplified scheduleSave - just debounces the queue addition */ scheduleSave(formConfig, delay = this.autoSaveDefaults.delay) { const saveKey = `autosave_${formConfig.id}`; this.debouncer.schedule( saveKey, () => this.queueAutosave(formConfig), delay ); } /** * Queue the autosave operation */ async queueAutosave(formConfig) { const currentData = this.collectFormData(formConfig.element); const changes = this.getChangedFields(formConfig.lastSnapshot, currentData); // No changes? Don't save if (Object.keys(changes).length === 0) return; // For bulk edit forms, handle specially if (formConfig.element.classList.contains('bulk-edit')) { this.queueBulkSave(formConfig, changes); return; } // Build the queue operation const operation = { endpoint: formConfig.options.endpoint || '/wp-json/jvb/v1/autosave', method: formConfig.contentId ? 'PUT' : 'POST', data: { form_id: formConfig.id, content_id: formConfig.contentId, content_type: formConfig.contentType, changes: changes, full_data: currentData }, title: `Autosaving ${formConfig.contentType || 'form'}`, popup: null, // No popup for autosave headers: { 'X-Autosave': 'true' }, source: 'form', formId: formConfig.id, // For optimistic updates localUpdate: formConfig.contentId ? { action: 'update', id: formConfig.contentId, changes: changes } : null, dataStore: this.store }; // Add to queue const queueItem = await this.queue.addToQueue(operation); if (queueItem) { // Update snapshot to prevent re-saving same changes formConfig.lastSnapshot = currentData; formConfig.isDirty = false; // Visual feedback this.showFormStatus(formConfig.element, 'queued'); // Track the operation this.trackOperation(formConfig, queueItem.id); } } /** * Handle bulk edit forms specially */ queueBulkSave(formConfig, changes) { const selectedItems = formConfig.element.querySelectorAll('.bulk-item input:checked'); const itemIds = Array.from(selectedItems).map(cb => cb.value); if (itemIds.length === 0) return; const operation = { endpoint: formConfig.options.endpoint || '/wp-json/jvb/v1/bulk-update', method: 'PUT', data: { content_type: formConfig.contentType, item_ids: itemIds, changes: changes }, title: `Updating ${itemIds.length} items`, popup: `${itemIds.length} items queued for update`, headers: { 'X-Bulk-Operation': 'true' }, source: 'form', formId: formConfig.id, // Optimistic update for each item localUpdate: { action: 'bulk-update', ids: itemIds, changes: changes }, dataStore: this.store }; // Add to queue const queueItem = this.queue.addToQueue(operation); if (queueItem) { formConfig.lastSnapshot = this.collectFormData(formConfig.element); formConfig.isDirty = false; this.showFormStatus(formConfig.element, 'queued'); } } /** * Track operations for forms */ trackOperation(formConfig, operationId) { if (!formConfig.operations) { formConfig.operations = new Set(); } formConfig.operations.add(operationId); // Subscribe to queue updates for this operation const unsubscribe = this.queue.subscribe((event, data) => { if (data?.id !== operationId) return; switch(event) { case 'operation-status': this.updateFormStatus(formConfig, data.status); break; case 'operation-completed': this.handleSaveSuccess(formConfig, data); formConfig.operations.delete(operationId); unsubscribe(); break; case 'operation-failed': this.handleSaveFailure(formConfig, data); formConfig.operations.delete(operationId); unsubscribe(); break; } }); } /** * Update form status based on queue status */ updateFormStatus(formConfig, status) { const statusMap = { 'queued': 'queued', 'uploading': 'saving', 'processing': 'saving', 'pending': 'saving', 'completed': 'saved', 'failed': 'error', 'failed_permanent': 'error' }; this.showFormStatus(formConfig.element, statusMap[status] || status); } /** * Handle successful save from queue */ handleSaveSuccess(formConfig, data) { // Update content ID if this was a create operation if (!formConfig.contentId && data.result?.id) { formConfig.contentId = data.result.id; formConfig.element.dataset.contentId = data.result.id; } // Reset dirty flag formConfig.isDirty = false; // Show success this.showFormStatus(formConfig.element, 'saved'); // Notify subscribers this.notify('form-saved', { formId: formConfig.id, contentId: formConfig.contentId, data: data }); } /** * Handle save failure from queue */ handleSaveFailure(formConfig, data) { // Mark as dirty so it will retry formConfig.isDirty = true; // Show error this.showFormStatus(formConfig.element, 'error'); // Notify subscribers this.notify('form-save-failed', { formId: formConfig.id, error: data.lastError }); } /** * Manual save (for submit button) */ async handleSubmit(event) { const form = event.target; if (!form.dataset.formId) return; event.preventDefault(); const formConfig = this.forms.get(form.dataset.formId); if (!formConfig) return; // Force immediate save this.debouncer.cancel(`autosave_${formConfig.id}`); // Add to queue with higher priority const currentData = this.collectFormData(form); const operation = { endpoint: formConfig.options.endpoint, method: formConfig.contentId ? 'PUT' : 'POST', data: { content_id: formConfig.contentId, content_type: formConfig.contentType, ...currentData }, title: `Saving ${formConfig.contentType || 'form'}`, popup: 'Saved successfully', priority: 'high', // Process before autosaves source: 'form', formId: formConfig.id, localUpdate: formConfig.contentId ? { action: 'update', id: formConfig.contentId, changes: currentData } : { action: 'create', data: currentData }, dataStore: this.store }; const queueItem = await this.queue.addToQueue(operation); if (queueItem) { formConfig.lastSnapshot = currentData; this.trackOperation(formConfig, queueItem.id); this.showFormStatus(form, 'saving'); } } /** * Check if form has unsaved changes */ hasUnsavedChanges(formId) { const formConfig = this.forms.get(formId); if (!formConfig) return false; // Check if there are pending operations if (formConfig.operations?.size > 0) return true; // Check if current data differs from snapshot const currentData = this.collectFormData(formConfig.element); const changes = this.getChangedFields(formConfig.lastSnapshot, currentData); return Object.keys(changes).length > 0; } /** * Cleanup when form is closed/destroyed */ cleanupForm(formId) { const formConfig = this.forms.get(formId); if (!formConfig) return; // Cancel any pending debounced saves this.debouncer.cancel(`autosave_${formId}`); // Check for unsaved changes if (this.hasUnsavedChanges(formId)) { // Could show a warning or auto-save this.queueAutosave(formConfig); } // Clean up special fields this.cleanupSpecialFields(); // Remove form config this.forms.delete(formId); } }