PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 6.3.5
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v6.3.5
7.2.1 7.2 7.1.2 7.1.1 7.1 7.0.4 7.0.6 7.0.7 6.3.8 6.3.7 6.3.6 6.3.5 6.3.4 6.3.3 6.3.1 trunk 5.7.3 5.7.5 5.8.1 5.8.2 5.8.3 5.8.4 5.8.6 6.0.4 6.0.5 All 36 releases
mlsimport / admin / js / progressive-save.js

progressive-save.js in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 6.3.5, at admin/js/progressive-save.js

570 lines 20.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * MLS Import Progressive Save System
3 *
4 * Handles saving field data progressively:
5 * - Initial chunked save if no data exists
6 * - Field-by-field saving on change
7 * - Optimized ordering save
8 */
9
10 (function($) {
11 'use strict';
12
13 // Configuration
14 const CONFIG = {
15 chunkSize: 50, // Fields per chunk for bulk operations
16 saveDelay: 500, // Milliseconds to wait before saving after change (debounce)
17 retryDelay: 1000, // Milliseconds to wait before retrying failed save
18 maxRetries: 3 // Maximum number of retry attempts
19 };
20
21 // Track saving state
22 const STATE = {
23 saving: false,
24 pendingSaves: {},
25 saveTimers: {},
26 retryCount: {},
27 saveQueue: [], // Queue of save keys waiting to be processed
28 initialSaveComplete: false
29 };
30
31 // Expose saving state globally so other scripts can wait
32 window.mlsimportSaving = false;
33
34 /**
35 * Initialize the progressive save system
36 */
37 function initProgressiveSave() {
38 console.log(' Initializing progressive save system');
39
40 // Check immediately if an initial save is needed
41 checkInitialSaveNeeded();
42
43 // Set up field change listeners
44 initFieldChangeListeners();
45
46 }
47
48 /**
49 * Handle manual save button click
50 */
51 function performManualSave() {
52 // Show saving message
53 $('.save-status-text').text('Saving all changes...');
54
55 // Set up array of save operations
56 const saveOperations = [];
57
58 // Add all pending saves
59 for (const key in STATE.pendingSaves) {
60 if (STATE.pendingSaves.hasOwnProperty(key)) {
61 saveOperations.push(saveField(key, true));
62 }
63 }
64
65 // Use Promise.all to wait for all saves to complete
66 Promise.all(saveOperations)
67 .then(function() {
68 $('.save-status-text').text('All changes saved successfully!');
69 setTimeout(function() {
70 $('.save-status-text').fadeOut();
71 }, 3000);
72 })
73 .catch(function() {
74 $('.save-status-text').text('Some changes could not be saved. Please check for errors.');
75 });
76 }
77
78 /**
79 * Check if an initial save is needed (option is empty)
80 */
81 function checkInitialSaveNeeded() {
82
83
84 const allFields = $('.mlsimport-field-row').toArray();
85 const totalChunks = Math.ceil(allFields.length / CONFIG.chunkSize);
86 let progressContainer;
87
88 $.ajax({
89 url: mlsimport_params.ajax_url,
90 type: 'POST',
91 data: {
92 action: 'mlsimport_check_initial_save_needed',
93 security: mlsimport_params.nonce
94 },
95 success: function(response) {
96 if (response.success && response.data.initialSaveNeeded) {
97 console.log('Initial save needed, starting chunked save');
98
99 progressContainer = $('<div class="mlsimport-save-progress"></div>');
100 const progressText = $('<div class="mlsimport-save-progress-text">Please wait while the initial field data is saved.<br>Initializing field data: <span class="current-chunk">0</span>/' + totalChunks + ' chunks</div>');
101 const progressBar = $('<div class="mlsimport-save-progress-bar"><div class="progress-fill"></div></div>');
102
103 progressContainer.append(progressText).append(progressBar);
104 $('body').append(progressContainer);
105
106 processChunk(allFields, 0, totalChunks, progressContainer);
107 } else {
108 console.log('No initial save needed');
109 STATE.initialSaveComplete = true;
110 }
111 },
112 error: function() {
113 console.error('Error checking if initial save needed');
114 if (progressContainer) {
115 progressContainer.remove();
116 }
117 // Retry in 5 seconds
118 setTimeout(checkInitialSaveNeeded, 5000);
119 }
120 });
121 }
122
123
124 /**
125 * Process a chunk of fields for initial save
126 */
127 function processChunk(allFields, chunkIndex, totalChunks, progressContainer) {
128 const startIndex = chunkIndex * CONFIG.chunkSize;
129 const endIndex = Math.min(startIndex + CONFIG.chunkSize, allFields.length);
130 const currentChunk = chunkIndex + 1;
131
132 // Update progress display
133 progressContainer.find('.current-chunk').text(currentChunk);
134 const percentage = (currentChunk / totalChunks) * 100;
135 progressContainer.find('.progress-fill').css('width', percentage + '%');
136
137 // Prepare chunk data
138 const chunkData = {};
139
140 for (let i = startIndex; i < endIndex; i++) {
141 const $field = $(allFields[i]);
142 const fieldKey = $field.data('field-key');
143 const isMandatory = $field.data('is-mandatory') === 'true';
144
145 // Get field values - CORRECTED VERSION
146 const isImportChecked = $field.find('.mlsimport-import-checkbox').is(':checked') || isMandatory;
147
148 chunkData[fieldKey] = {
149 import: isImportChecked ? 1 : 0, // Set to 1 if checked or mandatory
150 admin: $field.find('.mlsimport-admin-checkbox').is(':checked') ? 1 : 0,
151 label: $field.find('.mlsimport-label-input').val(),
152 postmeta: $field.find('.mlsimport-postmeta-input').val(),
153 taxonomy: $field.find('.mlsimport-taxonomy-select').val()
154 };
155 }
156 console.log('pricess mlsimport_save_field_chunk');
157 // Save chunk
158 $.ajax({
159 url: mlsimport_params.ajax_url,
160 type: 'POST',
161 data: {
162 action: 'mlsimport_save_field_chunk',
163 security: mlsimport_params.nonce,
164 chunk_index: chunkIndex,
165 total_chunks: totalChunks,
166 fields: chunkData
167 },
168 success: function(response) {
169 if (response.success) {
170 // Process next chunk or finish
171 if (currentChunk < totalChunks) {
172 setTimeout(function() {
173 processChunk(allFields, chunkIndex + 1, totalChunks, progressContainer);
174 }, 200); // Small delay between chunks
175 } else {
176 // All chunks processed
177 progressContainer.html('<div style="color: #46b450;">Initial field data saved successfully!</div>');
178 setTimeout(function() {
179 progressContainer.fadeOut(500, function() {
180 progressContainer.remove();
181 });
182 }, 2000);
183 STATE.initialSaveComplete = true;
184 }
185 } else {
186 // Error saving chunk
187 progressContainer.html('<div style="color: #dc3232;">Error saving field data. <button id="retry-chunk" class="button">Retry</button></div>');
188 $('#retry-chunk').on('click', function() {
189 processChunk(allFields, chunkIndex, totalChunks, progressContainer);
190 });
191 }
192 },
193 error: function() {
194 // Error saving chunk
195 progressContainer.html('<div style="color: #dc3232;">Network error saving field data. <button id="retry-chunk" class="button">Retry</button></div>');
196 $('#retry-chunk').on('click', function() {
197 processChunk(allFields, chunkIndex, totalChunks, progressContainer);
198 });
199 }
200 });
201 }
202
203 /**
204 * Set up listeners for field changes
205 */
206 function initFieldChangeListeners() {
207 // Import checkbox changes
208 $(document).on('change', '.mlsimport-import-checkbox:not([disabled])', function() {
209 const $field = $(this).closest('.mlsimport-field-row');
210 const fieldKey = $field.data('field-key');
211
212 // Queue save for this field
213 queueFieldSave(fieldKey, 'import', $(this).is(':checked') ? 1 : 0);
214 });
215
216 // Admin-only checkbox changes
217 $(document).on('change', '.mlsimport-admin-checkbox', function() {
218 const $field = $(this).closest('.mlsimport-field-row');
219 const fieldKey = $field.data('field-key');
220
221 // Queue save for this field
222 queueFieldSave(fieldKey, 'admin', $(this).is(':checked') ? 1 : 0);
223 });
224
225 // Label input changes
226 $(document).on('input', '.mlsimport-label-input', function() {
227 const $field = $(this).closest('.mlsimport-field-row');
228 const fieldKey = $field.data('field-key');
229
230 // Queue save for this field
231 queueFieldSave(fieldKey, 'label', $(this).val());
232 });
233
234 // Post meta input changes
235 $(document).on('input', '.mlsimport-postmeta-input', function() {
236 const $field = $(this).closest('.mlsimport-field-row');
237 const fieldKey = $field.data('field-key');
238
239 // Queue save for this field
240 queueFieldSave(fieldKey, 'postmeta', $(this).val());
241 });
242
243 // Taxonomy select changes
244 $(document).on('change', '.mlsimport-taxonomy-select', function() {
245 const $field = $(this).closest('.mlsimport-field-row');
246 const fieldKey = $field.data('field-key');
247
248 // Queue save for this field
249 queueFieldSave(fieldKey, 'taxonomy', $(this).val());
250 });
251 }
252
253 /**
254 * Queue a field save operation with debounce
255 */
256 function queueFieldSave(fieldKey, optionType, value) {
257 // Don't queue saves until initial save is complete
258 if (!STATE.initialSaveComplete) {
259 console.log(' Canceled - Initial save not complete, skipping individual field save');
260 // return;
261 }
262
263 // Create key for this field+option
264 const saveKey = fieldKey + '_' + optionType;
265
266 // Store the value to save
267 STATE.pendingSaves[saveKey] = {
268 fieldKey: fieldKey,
269 optionType: optionType,
270 value: value
271 };
272
273 // Clear existing timer for this field if any
274 if (STATE.saveTimers[saveKey]) {
275 clearTimeout(STATE.saveTimers[saveKey]);
276 }
277
278 // Set new timer that will enqueue the save
279 STATE.saveTimers[saveKey] = setTimeout(function() {
280 enqueueSave(saveKey);
281 }, CONFIG.saveDelay);
282 }
283
284 /**
285 * Add a save operation to the queue and start processing if idle
286 */
287 function enqueueSave(saveKey) {
288 if (!STATE.saveQueue.includes(saveKey)) {
289 STATE.saveQueue.push(saveKey);
290 }
291 processSaveQueue();
292 }
293
294 /**
295 * Process the next save in the queue if not already saving
296 */
297 function processSaveQueue() {
298 if (STATE.saving || window.mlsimportSaving) {
299 return;
300 }
301
302 const nextKey = STATE.saveQueue.shift();
303 if (nextKey) {
304 STATE.saving = true;
305 window.mlsimportSaving = true;
306 saveField(nextKey);
307 }
308 }
309
310 // Expose so other scripts (e.g., field selector) can resume the queue
311 window.processSaveQueue = processSaveQueue;
312
313 /**
314 * Save a field that was queued for saving
315 */
316 function saveField(saveKey) {
317 // Get the save data
318 const saveData = STATE.pendingSaves[saveKey];
319 if (!saveData) {
320
321 console.log('No pending save data for key', saveKey);
322 STATE.saving = false;
323 window.mlsimportSaving = false;
324 processSaveQueue();
325 return;
326 }
327 // Mark as saving
328 const $field = $('.mlsimport-field-row[data-field-key="' + saveData.fieldKey + '"]');
329
330 // Add visual indicator
331 addSavingIndicator($field, saveData.optionType);
332 console.log ('saving '+saveData.fieldKey+' / '+saveData.optionType+' / '+saveData.value);
333 // Send save request
334 $.ajax({
335 url: mlsimport_params.ajax_url,
336 type: 'POST',
337 data: {
338 action: 'mlsimport_save_field_option',
339 security: mlsimport_params.nonce,
340 field_key: saveData.fieldKey,
341 option_type: saveData.optionType,
342 value: saveData.value
343 },
344 success: function(response) {
345 console.log(response);
346 if (response.success) {
347 // Remove from pending saves
348 // Only remove from pending saves if this is the
349 // latest queued save for this key
350 if (STATE.pendingSaves[saveKey] === saveData) {
351 delete STATE.pendingSaves[saveKey];
352 }
353
354 // Show success indicator
355 updateSavingIndicator($field, saveData.optionType, 'success');
356
357 // Reset retry count
358 STATE.retryCount[saveKey] = 0;
359 } else {
360 // Show error indicator
361 updateSavingIndicator($field, saveData.optionType, 'error');
362
363 // Retry if under max retries
364 handleSaveRetry(saveKey);
365 }
366 },
367 error: function(e) {
368 console.log(e);
369 // Show error indicator
370 updateSavingIndicator($field, saveData.optionType, 'error');
371
372 // Retry if under max retries
373 handleSaveRetry(saveKey);
374 },
375 complete: function() {
376 console.log('cmpletee ');
377 STATE.saving = false;
378 window.mlsimportSaving = false;
379 processSaveQueue();
380 }
381 });
382 }
383
384 /**
385 * Handle retry logic for failed saves
386 */
387 function handleSaveRetry(saveKey) {
388 // Initialize retry count if needed
389 if (STATE.retryCount[saveKey] === undefined) {
390 STATE.retryCount[saveKey] = 0;
391 }
392
393 // Increment retry count
394 STATE.retryCount[saveKey]++;
395
396 // Check if we should retry
397 if (STATE.retryCount[saveKey] <= CONFIG.maxRetries) {
398 console.log('Retrying save for ' + saveKey + ' (attempt ' + STATE.retryCount[saveKey] + ' of ' + CONFIG.maxRetries + ')');
399
400 // Use exponential backoff
401 const delay = CONFIG.retryDelay * Math.pow(2, STATE.retryCount[saveKey] - 1);
402
403 // Schedule retry
404 setTimeout(function() {
405 enqueueSave(saveKey);
406 }, delay);
407 } else {
408 console.error('Max retries exceeded for ' + saveKey);
409
410 // Show more persistent error
411 const $field = $('.mlsimport-field-row[data-field-key="' + STATE.pendingSaves[saveKey].fieldKey + '"]');
412 showSaveError($field, 'Could not save this field. Please try again or refresh the page.');
413 }
414 }
415
416 /**
417 * Add saving indicator to a field
418 */
419 function addSavingIndicator($field, optionType) {
420 // Determine which element to add indicator to
421 let $element;
422
423 switch (optionType) {
424 case 'import':
425 $element = $field.find('.mlsimport-field-import');
426 break;
427 case 'admin':
428 $element = $field.find('.mlsimport-field-admin');
429 break;
430 case 'label':
431 $element = $field.find('.mlsimport-field-label');
432 break;
433 case 'postmeta':
434 $element = $field.find('.mlsimport-field-postmeta');
435 break;
436 case 'taxonomy':
437 $element = $field.find('.mlsimport-field-taxonomy');
438 break;
439 default:
440 $element = $field;
441 }
442
443 // Remove any existing indicators
444 $element.find('.save-indicator').remove();
445
446 // Add saving indicator
447 $element.append('<span class="save-indicator" style="margin-left: 5px; box-sizing: border-box;display: inline-block; width: 16px; height: 16px; border: 2px solid #635BFF; border-radius: 50%; border-top-color: transparent; animation: mlsimport-spin 1s linear infinite;"></span>');
448
449 // Add spin animation if it doesn't exist
450 if (!$('#mlsimport-spin-animation').length) {
451 $('head').append('<style id="mlsimport-spin-animation">@keyframes mlsimport-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }</style>');
452 }
453 }
454
455 /**
456 * Update saving indicator to show success or error
457 */
458 function updateSavingIndicator($field, optionType, status) {
459 // Determine which element has the indicator
460 let $element;
461
462 switch (optionType) {
463 case 'import':
464 $element = $field.find('.mlsimport-field-import');
465 break;
466 case 'admin':
467 $element = $field.find('.mlsimport-field-admin');
468 break;
469 case 'label':
470 $element = $field.find('.mlsimport-field-label');
471 break;
472 case 'postmeta':
473 $element = $field.find('.mlsimport-field-postmeta');
474 break;
475 case 'taxonomy':
476 $element = $field.find('.mlsimport-field-taxonomy');
477 break;
478 default:
479 $element = $field;
480 }
481
482 // Get indicator
483 const $indicator = $element.find('.save-indicator');
484
485 if (status === 'success') {
486 // Change to checkmark
487 $indicator.css({
488 'border': 'none',
489 'animation': 'none',
490 'color': '#46b450',
491 'font-size': '16px'
492 }).html('');
493
494 // Remove after delay
495 setTimeout(function() {
496 $indicator.fadeOut(500, function() {
497 $indicator.remove();
498 });
499 }, 1000);
500
501 } else if (status === 'error') {
502 // Change to X
503 $indicator.css({
504 'border': 'none',
505 'animation': 'none',
506 'color': '#dc3232',
507 'font-size': '16px'
508 }).html('');
509
510 // Make clickable to retry
511 $indicator.css('cursor', 'pointer').attr('title', 'Click to retry');
512
513 // Add click handler to retry
514 $indicator.on('click', function() {
515 const fieldKey = $field.data('field-key');
516 const saveKey = fieldKey + '_' + optionType;
517
518 // If save data still exists
519 if (STATE.pendingSaves[saveKey]) {
520 // Remove indicator
521 $indicator.remove();
522
523 // Retry save
524 saveField(saveKey);
525 }
526 });
527 }
528 }
529
530 /**
531 * Show a save error message for a field
532 */
533 function showSaveError($field, message) {
534 // Check if error message already exists
535 if ($field.find('.mlsimport-field-error').length === 0) {
536 // Create error message
537 const $error = $('<div class="mlsimport-field-error" style="color: #dc3232; margin-top: 5px; padding: 5px; background: #fbeaea; border-left: 3px solid #dc3232;">' + message + '</div>');
538
539 // Add close button
540 $error.append('<span class="close-error" style="float: right; cursor: pointer; font-weight: bold;">&times;</span>');
541
542 // Add to field
543 $field.append($error);
544
545 // Add close handler
546 $field.find('.close-error').on('click', function() {
547 $error.remove();
548 });
549 }
550 }
551
552
553 // Initialize when document is ready
554 $(document).ready(function() {
555
556
557 // Get the current URL
558 const currentUrl = window.location.href;
559 console.log (currentUrl);
560 // Check if the URL matches either of the target pages
561 if (
562 currentUrl.includes('admin.php?page=mlsimport_plugin_options&tab=field_options') ||
563 currentUrl.includes('admin.php?page=mlsimport-onboarding&step=field-mapping')
564 ) {
565 // Only initialize on the specified pages
566 console.log('start initProgressiveSave');
567 initProgressiveSave();
568 }
569 });
570 })(jQuery);