| @@ -1,676 +1,321 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | - * MLS Import Progressive Save Handlers | |
| 3 | + * WordPress persistence adapter for the Field Configuration module. | |
| 4 | 4 | * |
| 5 | - * Server-side handlers for the progressive save system: | |
| 6 | - * - Initial save detection and chunked saving | |
| 7 | - * - Individual field option saving | |
| 8 | - * - Optimized field order saving | |
| 5 | + * The browser exposes one mutation endpoint with four fixed POST variables: | |
| 6 | + * action, nonce, revision, and one JSON command. This file translates that | |
| 7 | + * request into the domain module, performs an exact database compare-and-swap, | |
| 8 | + * refreshes WordPress's option cache, and emits the authoritative result. The | |
| 9 | + * former chunk, individual, bulk, and position handlers intentionally do not | |
| 10 | + * survive as alternate mutation paths. | |
| 9 | 11 | * |
| 10 | - * @package MLSImport | |
| 11 | - * @subpackage MLSImport/includes | |
| 12 | + * @package MLSImport | |
| 12 | 13 | */ |
| 13 | 14 | |
| 14 | -// Exit if accessed directly | |
| 15 | 15 | if ( ! defined( 'ABSPATH' ) ) { |
| 16 | - exit; | |
| 16 | + exit; | |
| 17 | 17 | } |
| 18 | 18 | |
| 19 | - | |
| 20 | 19 | /** |
| 21 | - * AJAX handler for saving field order | |
| 22 | - * This stores the order directly in the mlsimport_admin_fields_select option | |
| 20 | + * Decode one connection's MLS metadata into the domain module's field map. | |
| 21 | + * | |
| 22 | + * Metadata can be stored as the original JSON string or as an already decoded | |
| 23 | + * array. Both shapes are accepted here; malformed or absent metadata becomes | |
| 24 | + * an empty map so every caller reaches the same normalization path. Since | |
| 25 | + * multi-MLS (#275) the blob is per-connection: the resolver returns the | |
| 26 | + * "_{mls_id}"-suffixed option for the given (or current) connection. | |
| 27 | + * | |
| 28 | + * @param int $mls_id Connection whose metadata to read; 0 = current connection. | |
| 29 | + * @return array That connection's MLS metadata keyed by RESO field name. | |
| 23 | 30 | */ |
| 24 | -function mlsimport_ajax_save_field_order() { | |
| 25 | - // Add detailed logging for debugging | |
| 31 | +function mlsimport_field_configuration_metadata( int $mls_id = 0 ): array { | |
| 32 | + $metadata = mlsimport_get_connection_option( 'mlsimport_mls_metadata_mls_data', '', $mls_id ); | |
| 33 | + $metadata = is_string( $metadata ) ? json_decode( $metadata, true ) : $metadata; | |
| 26 | 34 | |
| 27 | - // Check nonce | |
| 28 | - if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'mlsimport_field_selector_nonce')) { | |
| 29 | - //error_log('Invalid security token in field order save'); | |
| 30 | - wp_send_json_error('Invalid security token'); | |
| 31 | - return; | |
| 32 | - } | |
| 33 | - | |
| 34 | - // Check for both parameter formats | |
| 35 | - $field_order = array(); | |
| 36 | - | |
| 37 | - // Check for standard array format (field_order[]) | |
| 38 | - if (isset($_POST['field_order']) && is_array($_POST['field_order'])) { | |
| 39 | - $field_order = $_POST['field_order']; | |
| 40 | - //error_log('Using field_order parameter: ' . count($field_order) . ' items'); | |
| 41 | - } | |
| 42 | - // Check for older format (fields[]) | |
| 43 | - else if (isset($_POST['fields']) && is_array($_POST['fields'])) { | |
| 44 | - $field_order = $_POST['fields']; | |
| 45 | - //error_log('Using fields parameter: ' . count($field_order) . ' items'); | |
| 46 | - } | |
| 47 | - // Otherwise, try to get values directly by key pattern | |
| 48 | - else { | |
| 49 | - //error_log('No direct array found, checking for field_order[] pattern'); | |
| 50 | - // Check if we need to manually extract from the POST data (for field_order[]) | |
| 51 | - foreach ($_POST as $key => $value) { | |
| 52 | - if (preg_match('/^field_order(\[.*\])?$/', $key)) { | |
| 53 | - if (is_array($value)) { | |
| 54 | - $field_order = $value; | |
| 55 | - //error_log('Found field_order as array with ' . count($field_order) . ' items'); | |
| 56 | - break; | |
| 57 | - } | |
| 58 | - } | |
| 59 | - } | |
| 60 | - | |
| 61 | - // If we still don't have field order data, try to extract it directly from POST | |
| 62 | - if (empty($field_order)) { | |
| 63 | - //error_log('Trying to manually build array from field_order[]'); | |
| 64 | - foreach ($_POST as $key => $value) { | |
| 65 | - if (strpos($key, 'field_order[') === 0) { | |
| 66 | - $field_order[] = $value; | |
| 67 | - } | |
| 68 | - } | |
| 69 | - //error_log('Manually built array with ' . count($field_order) . ' items'); | |
| 70 | - } | |
| 71 | - } | |
| 72 | - | |
| 73 | - // Check if we have any field order data | |
| 74 | - if (empty($field_order)) { | |
| 75 | - //error_log('No field order data found in request'); | |
| 76 | - wp_send_json_error('No field order provided'); | |
| 77 | - return; | |
| 78 | - } | |
| 79 | - | |
| 80 | - // Log field count for debugging | |
| 81 | - //error_log('Saving ' . count($field_order) . ' fields in order'); | |
| 82 | - | |
| 83 | - // Get current options | |
| 84 | - $options = get_option('mlsimport_admin_fields_select', array()); | |
| 85 | - | |
| 86 | - // Make sure $options is an array | |
| 87 | - if (!is_array($options)) { | |
| 88 | - $options = array(); | |
| 89 | - } | |
| 90 | - | |
| 91 | - // Store the field order directly in the options | |
| 92 | - $options['field_order'] = array_map('sanitize_text_field', $field_order); | |
| 93 | - | |
| 94 | - // Save the updated options | |
| 95 | - $result = update_option('mlsimport_admin_fields_select', $options); | |
| 96 | - | |
| 97 | - if ($result) { | |
| 98 | - global $mlsimport; | |
| 99 | - if ( isset( $mlsimport->admin->env_data ) && method_exists( $mlsimport->admin->env_data, 'enviroment_custom_fields' ) ) { | |
| 100 | - $mlsimport->admin->env_data->enviroment_custom_fields( $mlsimport->get_plugin_name() ); | |
| 101 | - } | |
| 102 | - //error_log('Field order saved successfully'); | |
| 103 | - wp_send_json_success('Field order saved successfully'); | |
| 104 | - } else { | |
| 105 | - //error_log('Failed to save field order - update_option returned false'); | |
| 106 | - wp_send_json_error('Failed to save field order - update_option returned false'); | |
| 107 | - } | |
| 35 | + return is_array( $metadata ) ? $metadata : array(); | |
| 108 | 36 | } |
| 109 | 37 | |
| 110 | - | |
| 111 | - | |
| 112 | 38 | /** |
| 113 | - * Add this function to your mlsimport-admin-fields-select.php file | |
| 114 | - * Outputs the nonce field needed for field ordering | |
| 39 | + * Return taxonomy destinations registered for the active property post type. | |
| 40 | + * | |
| 41 | + * The adapter is resolved on demand because metadata gathering and AJAX calls | |
| 42 | + * run after the plugin has created its active theme environment. | |
| 43 | + * | |
| 44 | + * @return array Taxonomy slug-to-label map accepted by mutation validation. | |
| 115 | 45 | */ |
| 116 | -function mlsimport_add_field_selector_nonce() { | |
| 117 | - wp_nonce_field('mlsimport_field_selector_nonce', 'mlsimport_field_selector_nonce'); | |
| 118 | -} | |
| 46 | +function mlsimport_field_configuration_taxonomies(): array { | |
| 47 | + global $mlsimport; | |
| 119 | 48 | |
| 49 | + $post_type = ''; | |
| 50 | + if ( isset( $mlsimport->admin->env_data ) && method_exists( $mlsimport->admin->env_data, 'get_property_post_type' ) ) { | |
| 51 | + $post_type = $mlsimport->admin->env_data->get_property_post_type(); | |
| 52 | + } | |
| 120 | 53 | |
| 54 | + $taxonomies = mlsimport_get_custom_post_type_taxonomies( $post_type ); | |
| 121 | 55 | |
| 56 | + return is_array( $taxonomies ) ? $taxonomies : array(); | |
| 57 | +} | |
| 122 | 58 | |
| 123 | - | |
| 124 | - | |
| 125 | 59 | /** |
| 126 | - * Register AJAX handlers for progressive saving | |
| 60 | + * Atomically replace the one Field Configuration option. | |
| 61 | + * | |
| 62 | + * WordPress's update_option() has no expected-value condition, so two tabs can | |
| 63 | + * both pass an application-level revision check and the slower request can | |
| 64 | + * overwrite the newer one. The UPDATE below includes the exact serialized old | |
| 65 | + * value in its WHERE clause. One request wins; the other updates zero rows and | |
| 66 | + * is reported by the module as stale. Cache and public option hooks are updated | |
| 67 | + * once only after the database confirms the replacement. | |
| 68 | + * | |
| 69 | + * @param array $expected Exact option value previously loaded. | |
| 70 | + * @param array $replacement Complete normalized replacement. | |
| 71 | + * @param string $option_name Concrete (per-connection) option to swap; '' | |
| 72 | + * resolves the current connection's option (#275). | |
| 73 | + * @return bool True only when this caller won the compare-and-swap. | |
| 127 | 74 | */ |
| 128 | -function mlsimport_register_progressive_save_handlers() { | |
| 129 | - // Check if initial save is needed | |
| 130 | - add_action('wp_ajax_mlsimport_check_initial_save_needed', 'mlsimport_ajax_check_initial_save_needed'); | |
| 131 | - | |
| 132 | - // Save a chunk of fields | |
| 133 | - add_action('wp_ajax_mlsimport_save_field_chunk', 'mlsimport_ajax_save_field_chunk'); | |
| 75 | +function mlsimport_field_configuration_compare_and_swap( array $expected, array $replacement, string $option_name = '' ): bool { | |
| 76 | + global $wpdb; | |
| 134 | 77 | |
| 135 | - // Save an individual field option | |
| 136 | - add_action('wp_ajax_mlsimport_save_field_option', 'mlsimport_ajax_save_field_option'); | |
| 78 | + if ( '' === $option_name ) { | |
| 79 | + $option_name = mlsimport_connection_option_name( 'mlsimport_admin_fields_select' ); | |
| 80 | + } | |
| 81 | + $row = $wpdb->get_row( | |
| 82 | + $wpdb->prepare( | |
| 83 | + "SELECT option_value, autoload FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", | |
| 84 | + $option_name | |
| 85 | + ), | |
| 86 | + ARRAY_A | |
| 87 | + ); | |
| 137 | 88 | |
| 138 | - // Bulk save import selections | |
| 139 | - add_action('wp_ajax_mlsimport_save_bulk_import', 'mlsimport_ajax_save_bulk_import'); | |
| 89 | + // add_option() uses the option_name unique key as the atomic first-write gate. | |
| 90 | + if ( null === $row ) { | |
| 91 | + if ( array() !== $expected ) { | |
| 92 | + return false; | |
| 93 | + } | |
| 140 | 94 | |
| 141 | - // Bulk save admin visibility selections | |
| 142 | - add_action('wp_ajax_mlsimport_save_bulk_admin', 'mlsimport_ajax_save_bulk_admin'); | |
| 143 | -} | |
| 144 | -add_action('init', 'mlsimport_register_progressive_save_handlers'); | |
| 95 | + $added = add_option( $option_name, $replacement, '', false ); | |
| 96 | + if ( ! $added ) { | |
| 97 | + wp_cache_delete( $option_name, 'options' ); | |
| 98 | + wp_cache_delete( 'alloptions', 'options' ); | |
| 99 | + wp_cache_delete( 'notoptions', 'options' ); | |
| 100 | + } else { | |
| 101 | + if ( function_exists( 'mlsimport_active_field_configuration' ) ) { | |
| 102 | + mlsimport_active_field_configuration( true ); | |
| 103 | + } | |
| 104 | + // The theme-adapter resync listens on the BASE option's hook name | |
| 105 | + // (loader binds add/update_option_mlsimport_admin_fields_select). | |
| 106 | + // Per-connection storage (#275) uses a suffixed option name, so | |
| 107 | + // WordPress fires "add_option_{$option_name}" instead — announce the | |
| 108 | + // field-selection change on the base name explicitly. | |
| 109 | + if ( 'mlsimport_admin_fields_select' !== $option_name ) { | |
| 110 | + do_action( 'update_option_mlsimport_admin_fields_select', $expected, $replacement, $option_name ); | |
| 111 | + } | |
| 112 | + } | |
| 145 | 113 | |
| 114 | + return $added; | |
| 115 | + } | |
| 146 | 116 | |
| 117 | + $old_serialized = maybe_serialize( $expected ); | |
| 118 | + $new_serialized = maybe_serialize( $replacement ); | |
| 119 | + $updated = $wpdb->query( | |
| 120 | + $wpdb->prepare( | |
| 121 | + "UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND BINARY option_value = BINARY %s", | |
| 122 | + $new_serialized, | |
| 123 | + $option_name, | |
| 124 | + $old_serialized | |
| 125 | + ) | |
| 126 | + ); | |
| 127 | + if ( 1 !== $updated ) { | |
| 128 | + // Another request won after this process loaded its option. Discard every | |
| 129 | + // local option-cache route so the module can report the winner's revision. | |
| 130 | + wp_cache_delete( $option_name, 'options' ); | |
| 131 | + wp_cache_delete( 'alloptions', 'options' ); | |
| 132 | + wp_cache_delete( 'notoptions', 'options' ); | |
| 133 | + return false; | |
| 134 | + } | |
| 147 | 135 | |
| 136 | + // Mirror update_option() cache behavior while preserving the existing | |
| 137 | + // autoload choice. Large 1,000-field configurations are not newly autoloaded. | |
| 138 | + $alloptions = wp_load_alloptions( true ); | |
| 139 | + if ( isset( $alloptions[ $option_name ] ) ) { | |
| 140 | + $alloptions[ $option_name ] = $new_serialized; | |
| 141 | + wp_cache_set( 'alloptions', $alloptions, 'options' ); | |
| 142 | + } else { | |
| 143 | + wp_cache_set( $option_name, $new_serialized, 'options' ); | |
| 144 | + } | |
| 148 | 145 | |
| 146 | + mlsimport_active_field_configuration( true ); | |
| 147 | + // Announce on the BASE hook name: the theme-adapter resync listener is | |
| 148 | + // bound to update_option_mlsimport_admin_fields_select regardless of which | |
| 149 | + // connection's suffixed option (#275) actually stored the change. | |
| 150 | + do_action( 'update_option_mlsimport_admin_fields_select', $expected, $replacement, 'mlsimport_admin_fields_select' ); | |
| 151 | + do_action( 'updated_option', $option_name, $expected, $replacement ); | |
| 149 | 152 | |
| 153 | + return true; | |
| 154 | +} | |
| 150 | 155 | |
| 151 | -function mlsimport_ajax_save_field_chunk() { | |
| 152 | - // Check nonce | |
| 153 | - if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'mlsimport_field_selector_nonce')) { | |
| 154 | - wp_send_json_error('Invalid security token'); | |
| 155 | - return; | |
| 156 | - } | |
| 157 | - | |
| 158 | - // Check if fields were provided | |
| 159 | - if (!isset($_POST['fields']) || !is_array($_POST['fields'])) { | |
| 160 | - wp_send_json_error('No field data provided'); | |
| 161 | - return; | |
| 162 | - } | |
| 163 | - | |
| 164 | - // Get chunk info | |
| 165 | - $chunk_index = isset($_POST['chunk_index']) ? intval($_POST['chunk_index']) : 0; | |
| 166 | - $total_chunks = isset($_POST['total_chunks']) ? intval($_POST['total_chunks']) : 1; | |
| 167 | - $is_last_chunk = ($chunk_index + 1) == $total_chunks; | |
| 168 | - | |
| 169 | - // Get current options | |
| 170 | - $options = get_option('mlsimport_admin_fields_select', array()); | |
| 171 | - | |
| 172 | - // Initialize arrays if they don't exist | |
| 173 | - if (!is_array($options)) { | |
| 174 | - $options = array(); | |
| 175 | - } | |
| 176 | - | |
| 177 | - if (!isset($options['mls-fields'])) { | |
| 178 | - $options['mls-fields'] = array(); | |
| 179 | - } | |
| 180 | - | |
| 181 | - if (!isset($options['mls-fields-admin'])) { | |
| 182 | - $options['mls-fields-admin'] = array(); | |
| 183 | - } | |
| 184 | - | |
| 185 | - if (!isset($options['mls-fields-label'])) { | |
| 186 | - $options['mls-fields-label'] = array(); | |
| 187 | - } | |
| 188 | - | |
| 189 | - if (!isset($options['mls-fields-map-postmeta'])) { | |
| 190 | - $options['mls-fields-map-postmeta'] = array(); | |
| 191 | - } | |
| 192 | - | |
| 193 | - if (!isset($options['mls-fields-map-taxonomy'])) { | |
| 194 | - $options['mls-fields-map-taxonomy'] = array(); | |
| 195 | - } | |
| 156 | +/** | |
| 157 | + * Construct the authoritative module around one connection's option store. | |
| 158 | + * | |
| 159 | + * Step by step (#275 — per-connection field mapping): | |
| 160 | + * 1. Resolve the concrete option name once: the given connection's suffixed | |
| 161 | + * 'mlsimport_admin_fields_select_{mls_id}', or the current connection's | |
| 162 | + * when no mls_id is passed (every legacy call site). | |
| 163 | + * 2. Bind BOTH the loader and the compare-and-swap writer to that one name, | |
| 164 | + * so load and persistence can never address different connections. The | |
| 165 | + * revision key and CAS semantics are unchanged — just scoped per option. | |
| 166 | + * | |
| 167 | + * @param int $mls_id Connection to bind to; 0 = current connection. | |
| 168 | + * @return Mlsimport_Field_Configuration Configured domain service. | |
| 169 | + */ | |
| 170 | +function mlsimport_field_configuration( int $mls_id = 0 ): Mlsimport_Field_Configuration { | |
| 171 | + // Step 1: one resolution, shared by both storage callbacks. | |
| 172 | + $option_name = mlsimport_connection_option_name( 'mlsimport_admin_fields_select', $mls_id ); | |
| 196 | 173 | |
| 197 | - if (!isset($options['field_order'])) { | |
| 198 | - $options['field_order'] = array(); | |
| 199 | - $i = 0; | |
| 200 | - }else{ | |
| 201 | - $i = count($options['field_order'] ); | |
| 202 | - } | |
| 203 | - | |
| 204 | - | |
| 205 | - | |
| 206 | - // Process fields in the chunk | |
| 207 | - foreach ($_POST['fields'] as $field_key => $field_data) { | |
| 208 | - | |
| 209 | - | |
| 210 | - // Process field data as before | |
| 211 | - if (isset($field_data['import'])) { | |
| 212 | - $options['mls-fields'][$field_key] = intval($field_data['import']); | |
| 213 | - } | |
| 214 | - | |
| 215 | - if (isset($field_data['admin'])) { | |
| 216 | - $options['mls-fields-admin'][$field_key] = intval($field_data['admin']); | |
| 217 | - } | |
| 218 | - | |
| 219 | - if (isset($field_data['label'])) { | |
| 220 | - $options['mls-fields-label'][$field_key] = $field_data['label']; | |
| 221 | - } | |
| 222 | - | |
| 223 | - if (isset($field_data['postmeta'])) { | |
| 224 | - $options['mls-fields-map-postmeta'][$field_key] = $field_data['postmeta']; | |
| 225 | - } | |
| 226 | - | |
| 227 | - if (isset($field_data['taxonomy'])) { | |
| 228 | - $options['mls-fields-map-taxonomy'][$field_key] = $field_data['taxonomy']; | |
| 229 | - } | |
| 230 | - | |
| 231 | - | |
| 232 | - $options['field_order'][$field_key] = $i++; | |
| 233 | - | |
| 234 | - } | |
| 235 | - | |
| 236 | - | |
| 237 | - // print_r($options); | |
| 238 | - | |
| 239 | - // Save the options | |
| 240 | - $saved = update_option('mlsimport_admin_fields_select', $options); | |
| 241 | - if ( $saved ) { | |
| 242 | - global $mlsimport; | |
| 243 | - if ( isset( $mlsimport->admin->env_data ) && method_exists( $mlsimport->admin->env_data, 'enviroment_custom_fields' ) ) { | |
| 244 | - $mlsimport->admin->env_data->enviroment_custom_fields( $mlsimport->get_plugin_name() ); | |
| 245 | - } | |
| 246 | - } | |
| 247 | - | |
| 248 | - wp_send_json_success(array( | |
| 249 | - | |
| 250 | - 'field_order'=> $options['field_order'], | |
| 251 | - 'message' => 'Field chunk ' . ($chunk_index + 1) . ' of ' . $total_chunks . ' saved successfully', | |
| 252 | - 'chunkIndex' => $chunk_index, | |
| 253 | - 'totalChunks' => $total_chunks, | |
| 254 | - 'isLastChunk' => $is_last_chunk, | |
| 255 | - | |
| 256 | - )); | |
| 174 | + // Step 2: loader and CAS are closures over the same resolved name. | |
| 175 | + return new Mlsimport_Field_Configuration( | |
| 176 | + static function () use ( $option_name ) { | |
| 177 | + $value = get_option( $option_name, array() ); | |
| 178 | + return is_array( $value ) ? $value : array(); | |
| 179 | + }, | |
| 180 | + static function ( array $expected, array $replacement ) use ( $option_name ) { | |
| 181 | + return mlsimport_field_configuration_compare_and_swap( $expected, $replacement, $option_name ); | |
| 182 | + } | |
| 183 | + ); | |
| 257 | 184 | } |
| 258 | 185 | |
| 259 | 186 | /** |
| 260 | - * AJAX handler to save import selections in bulk | |
| 187 | + * Return normalized durable state for consumers that must remove dormant data. | |
| 188 | + * | |
| 189 | + * Theme custom-field registries need both active fields to add and dormant | |
| 190 | + * fields to remove from theme-owned display definitions. This read still enters | |
| 191 | + * through the module, preserving normalization and taxonomy rules without | |
| 192 | + * exposing a direct option read as an alternate persistence boundary. | |
| 193 | + * | |
| 194 | + * @return array Normalized active-and-dormant Field Configuration. | |
| 261 | 195 | */ |
| 262 | -function mlsimport_ajax_save_bulk_import() { | |
| 263 | - if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'mlsimport_field_selector_nonce')) { | |
| 264 | - wp_send_json_error('Invalid security token'); | |
| 265 | - } | |
| 266 | - | |
| 267 | - if (!isset($_POST['fields']) || !is_array($_POST['fields'])) { | |
| 268 | - wp_send_json_error('No field data provided'); | |
| 269 | - } | |
| 270 | - | |
| 271 | - $options = get_option('mlsimport_admin_fields_select', array()); | |
| 272 | - if (!isset($options['mls-fields']) || !is_array($options['mls-fields'])) { | |
| 273 | - $options['mls-fields'] = array(); | |
| 274 | - } | |
| 275 | - | |
| 276 | - foreach ($_POST['fields'] as $key => $value) { | |
| 277 | - $options['mls-fields'][sanitize_text_field($key)] = intval($value); | |
| 278 | - } | |
| 279 | - | |
| 280 | - $result = update_option('mlsimport_admin_fields_select', $options); | |
| 281 | - if ($result) { | |
| 282 | - global $mlsimport; | |
| 283 | - if ( isset( $mlsimport->admin->env_data ) && method_exists( $mlsimport->admin->env_data, 'enviroment_custom_fields' ) ) { | |
| 284 | - $mlsimport->admin->env_data->enviroment_custom_fields( $mlsimport->get_plugin_name() ); | |
| 285 | - } | |
| 286 | - wp_send_json_success('Bulk import selections saved'); | |
| 287 | - } else { | |
| 288 | - wp_send_json_error('Failed to save selections'); | |
| 289 | - } | |
| 196 | +function mlsimport_normalized_field_configuration(): array { | |
| 197 | + return mlsimport_field_configuration()->read( | |
| 198 | + mlsimport_field_configuration_metadata(), | |
| 199 | + mlsimport_hardocde_theme_schema(), | |
| 200 | + mlsimport_field_configuration_taxonomies() | |
| 201 | + ); | |
| 290 | 202 | } |
| 291 | 203 | |
| 292 | 204 | /** |
| 293 | - * AJAX handler to save admin visibility selections in bulk | |
| 205 | + * Return the active-only configuration for import and display consumers. | |
| 206 | + * | |
| 207 | + * The durable option retains Dormant MLS Fields so their choices can return. | |
| 208 | + * Runtime consumers use this projection to exclude those fields consistently | |
| 209 | + * without each theme reimplementing metadata intersection and array ordering. | |
| 210 | + * | |
| 211 | + * The projection is cached for the request because import adapters consult it | |
| 212 | + * once per listing. Rebuilding and sorting 1,000 parallel fields for every | |
| 213 | + * listing would turn schema safety into an avoidable import bottleneck. The | |
| 214 | + * cache is keyed by connection (#277 — a task-bound import may read another | |
| 215 | + * connection's projection); a refresh drops EVERY cached projection so the | |
| 216 | + * existing compare-and-swap invalidation stays a single call. | |
| 217 | + * | |
| 218 | + * @param bool $refresh Rebuild after this request has changed the option. | |
| 219 | + * @param int $mls_id Connection to read; 0 = the current connection. | |
| 220 | + * @return array Normalized configuration containing current metadata fields only. | |
| 294 | 221 | */ |
| 295 | -function mlsimport_ajax_save_bulk_admin() { | |
| 296 | - if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'mlsimport_field_selector_nonce')) { | |
| 297 | - wp_send_json_error('Invalid security token'); | |
| 298 | - } | |
| 222 | +function mlsimport_active_field_configuration( bool $refresh = false, int $mls_id = 0 ): array { | |
| 223 | + static $configurations = array(); | |
| 299 | 224 | |
| 300 | - if (!isset($_POST['fields']) || !is_array($_POST['fields'])) { | |
| 301 | - wp_send_json_error('No field data provided'); | |
| 302 | - } | |
| 225 | + // One cache slot per resolved connection; 0 resolves to the current one so | |
| 226 | + // legacy callers and task-scoped callers share a slot when they coincide. | |
| 227 | + $slot = $mls_id > 0 ? $mls_id : mlsimport_current_mls_id(); | |
| 303 | 228 | |
| 304 | - $options = get_option('mlsimport_admin_fields_select', array()); | |
| 305 | - if (!isset($options['mls-fields-admin']) || !is_array($options['mls-fields-admin'])) { | |
| 306 | - $options['mls-fields-admin'] = array(); | |
| 307 | - } | |
| 229 | + if ( $refresh ) { | |
| 230 | + $configurations = array(); | |
| 231 | + } | |
| 232 | + if ( isset( $configurations[ $slot ] ) ) { | |
| 233 | + return $configurations[ $slot ]; | |
| 234 | + } | |
| 308 | 235 | |
| 309 | - foreach ($_POST['fields'] as $key => $value) { | |
| 310 | - $options['mls-fields-admin'][sanitize_text_field($key)] = intval($value); | |
| 311 | - } | |
| 236 | + $configurations[ $slot ] = mlsimport_field_configuration( $mls_id )->read_active( | |
| 237 | + mlsimport_field_configuration_metadata( $mls_id ), | |
| 238 | + mlsimport_hardocde_theme_schema(), | |
| 239 | + mlsimport_field_configuration_taxonomies() | |
| 240 | + ); | |
| 312 | 241 | |
| 313 | - $result = update_option('mlsimport_admin_fields_select', $options); | |
| 314 | - if ($result) { | |
| 315 | - global $mlsimport; | |
| 316 | - if ( isset( $mlsimport->admin->env_data ) && method_exists( $mlsimport->admin->env_data, 'enviroment_custom_fields' ) ) { | |
| 317 | - $mlsimport->admin->env_data->enviroment_custom_fields( $mlsimport->get_plugin_name() ); | |
| 318 | - } | |
| 319 | - wp_send_json_success('Bulk admin selections saved'); | |
| 320 | - } else { | |
| 321 | - wp_send_json_error('Failed to save selections'); | |
| 322 | - } | |
| 242 | + return $configurations[ $slot ]; | |
| 323 | 243 | } |
| 324 | 244 | |
| 325 | - | |
| 326 | - | |
| 327 | - | |
| 328 | - | |
| 329 | - | |
| 330 | - | |
| 331 | - | |
| 332 | - | |
| 333 | - | |
| 334 | - | |
| 335 | - | |
| 336 | - | |
| 337 | - | |
| 338 | - | |
| 339 | - | |
| 340 | - | |
| 341 | - | |
| 342 | - | |
| 343 | - | |
| 344 | - | |
| 345 | - | |
| 346 | - | |
| 347 | 245 | /** |
| 348 | - * AJAX handler to check if initial save is needed | |
| 246 | + * Persist metadata initialization/reconciliation once on the server. | |
| 247 | + * | |
| 248 | + * @param array $metadata Newly gathered MLS metadata. | |
| 249 | + * @param array $theme_schema Active theme defaults. | |
| 250 | + * @param int $mls_id Connection to reconcile; 0 = current connection. | |
| 251 | + * @return array Field Configuration Result. | |
| 349 | 252 | */ |
| 350 | -function mlsimport_ajax_check_initial_save_needed() { | |
| 351 | - // Check nonce | |
| 352 | - if ( ! isset( $_POST['security'] ) || ! wp_verify_nonce( $_POST['security'], 'mlsimport_field_selector_nonce' ) ) { | |
| 353 | - wp_send_json_error( 'Invalid security token' ); | |
| 354 | - } | |
| 355 | - | |
| 356 | - // Check if option exists and has content | |
| 357 | - $options = get_option( 'mlsimport_admin_fields_select' ); | |
| 358 | - $mlsimport_mls_metadata_mls_data = get_option( 'mlsimport_mls_metadata_mls_data', '' ); | |
| 359 | - | |
| 360 | - // Decode MLS data | |
| 361 | - $mls_data = json_decode( $mlsimport_mls_metadata_mls_data, true ); | |
| 362 | - | |
| 363 | - // Check if we need an initial save | |
| 364 | - $initial_save_needed = false; | |
| 365 | - | |
| 366 | - // If MLS data exists but option is empty/incomplete | |
| 367 | - // had !empty( $mls_data ) && | |
| 368 | - if ( ( | |
| 369 | - empty( $options ) || | |
| 370 | - empty( $options['mls-fields'] ) | |
| 371 | - ) ) { | |
| 372 | - $initial_save_needed = true; | |
| 373 | - } | |
| 374 | - | |
| 375 | - wp_send_json_success( array( | |
| 376 | - 'initialSaveNeeded' => $initial_save_needed, | |
| 377 | - | |
| 378 | - 'options'=>$options, | |
| 379 | - 'fieldsInOption' => is_array( $options ) && isset( $options['mls-fields'] ) ? count( $options['mls-fields'] ) : 0, | |
| 380 | - 'fieldsInMlsData' => is_array( $mls_data ) ? count( $mls_data ) : 0 | |
| 381 | - ) ); | |
| 253 | +function mlsimport_reconcile_field_configuration( array $metadata, array $theme_schema, int $mls_id = 0 ): array { | |
| 254 | + return mlsimport_field_configuration( $mls_id )->reconcile( $metadata, $theme_schema, mlsimport_field_configuration_taxonomies() ); | |
| 382 | 255 | } |
| 383 | 256 | |
| 384 | 257 | /** |
| 385 | - * AJAX handler to save an individual field option | |
| 258 | + * Import an exported configuration through the same schema and storage owner. | |
| 259 | + * | |
| 260 | + * The target connection's own metadata blob drives normalization; fields the | |
| 261 | + * blob does not know become dormant until that MLS's metadata is gathered. | |
| 262 | + * | |
| 263 | + * @param array $incoming Exported legacy-compatible option array. | |
| 264 | + * @param int $mls_id Connection to import into; 0 = current connection. | |
| 265 | + * @return array Field Configuration Result. | |
| 386 | 266 | */ |
| 387 | -function mlsimport_ajax_save_field_ssswsd2option() { | |
| 388 | - // Check nonce | |
| 389 | - if ( ! isset( $_POST['security'] ) || ! wp_verify_nonce( $_POST['security'], 'mlsimport_field_selector_nonce' ) ) { | |
| 390 | - wp_send_json_error( 'Invalid security token' ); | |
| 391 | - } | |
| 392 | - | |
| 393 | - // Check if field key and option type were provided | |
| 394 | - if ( ! isset( $_POST['field_key'] ) || ! isset( $_POST['option_type'] ) ) { | |
| 395 | - wp_send_json_error( 'Missing field key or option type' ); | |
| 396 | - } | |
| 397 | - | |
| 398 | - // Sanitize inputs | |
| 399 | - $field_key = sanitize_text_field( $_POST['field_key'] ); | |
| 400 | - $option_type = sanitize_text_field( $_POST['option_type'] ); | |
| 401 | - $value = isset( $_POST['value'] ) ? $_POST['value'] : ''; | |
| 402 | - | |
| 403 | - // Validate option type | |
| 404 | - $valid_option_types = array( 'import', 'admin', 'label', 'postmeta', 'taxonomy' ); | |
| 405 | - | |
| 406 | - if ( ! in_array( $option_type, $valid_option_types ) ) { | |
| 407 | - wp_send_json_error( 'Invalid option type' ); | |
| 408 | - } | |
| 409 | - | |
| 410 | - // Get current options | |
| 411 | - $options = get_option( 'mlsimport_admin_fields_select', array() ); | |
| 412 | - // Keep a copy of the original options to detect changes | |
| 413 | - $original_options = $options; | |
| 414 | - | |
| 415 | - // Map option type to options array key | |
| 416 | - $option_map = array( | |
| 417 | - 'import' => 'mls-fields', | |
| 418 | - 'admin' => 'mls-fields-admin', | |
| 419 | - 'label' => 'mls-fields-label', | |
| 420 | - 'postmeta' => 'mls-fields-map-postmeta', | |
| 421 | - 'taxonomy' => 'mls-fields-map-taxonomy' | |
| 422 | - ); | |
| 423 | - | |
| 424 | - $option_key = $option_map[ $option_type ]; | |
| 425 | - | |
| 426 | - // Initialize array if it doesn't exist | |
| 427 | - if ( ! isset( $options[ $option_key ] ) ) { | |
| 428 | - $options[ $option_key ] = array(); | |
| 429 | - } | |
| 430 | - | |
| 431 | - // Sanitize and update value based on option type | |
| 432 | - switch ( $option_type ) { | |
| 433 | - case 'import': | |
| 434 | - case 'admin': | |
| 435 | - $options[ $option_key ][ $field_key ] = intval( $value ); | |
| 436 | - break; | |
| 437 | - | |
| 438 | - case 'label': | |
| 439 | - case 'postmeta': | |
| 440 | - case 'taxonomy': | |
| 441 | - $options[ $option_key ][ $field_key ] = sanitize_text_field( $value ); | |
| 442 | - break; | |
| 443 | - } | |
| 444 | - | |
| 445 | - // If nothing changed, treat as success without calling update_option | |
| 446 | - if ( $options === $original_options ) { | |
| 447 | - wp_send_json_success( array( | |
| 448 | - 'message' => 'Field option saved successfully', | |
| 449 | - 'fieldKey' => $field_key, | |
| 450 | - 'optionType' => $option_type, | |
| 451 | - 'value' => $value, | |
| 452 | - ) ); | |
| 453 | - } | |
| 454 | - | |
| 455 | - // Save updated options | |
| 456 | - $result = update_option( 'mlsimport_admin_fields_select', $options ); | |
| 457 | - | |
| 458 | - if ( false !== $result ) { | |
| 459 | - wp_send_json_success( array( | |
| 460 | - 'message' => 'Field option saved successfully', | |
| 461 | - 'fieldKey' => $field_key, | |
| 462 | - 'optionType' => $option_type, | |
| 463 | - 'value' => $value, | |
| 464 | - ) ); | |
| 465 | - } else { | |
| 466 | - wp_send_json_error( 'Failed to save field option' ); | |
| 467 | - } | |
| 267 | +function mlsimport_import_field_configuration( array $incoming, int $mls_id = 0 ): array { | |
| 268 | + return mlsimport_field_configuration( $mls_id )->import_configuration( | |
| 269 | + $incoming, | |
| 270 | + mlsimport_field_configuration_metadata( $mls_id ), | |
| 271 | + mlsimport_hardocde_theme_schema(), | |
| 272 | + mlsimport_field_configuration_taxonomies() | |
| 273 | + ); | |
| 468 | 274 | } |
| 469 | -function mlsimport_ajax_save_field_option() { | |
| 470 | - // Check nonce | |
| 471 | - if ( ! isset( $_POST['security'] ) || ! wp_verify_nonce( $_POST['security'], 'mlsimport_field_selector_nonce' ) ) { | |
| 472 | - //error_log( '[mlsimport] Invalid security token' ); | |
| 473 | - wp_send_json_error( 'Invalid security token' ); | |
| 474 | - } | |
| 475 | 275 | |
| 476 | - // Check if field key and option type were provided | |
| 477 | - if ( ! isset( $_POST['field_key'] ) || ! isset( $_POST['option_type'] ) ) { | |
| 478 | - //error_log( '[mlsimport] Missing field key or option type' ); | |
| 479 | - wp_send_json_error( 'Missing field key or option type' ); | |
| 480 | - } | |
| 276 | +/** | |
| 277 | + * Handle the sole browser Field Configuration mutation endpoint. | |
| 278 | + * | |
| 279 | + * Security and request-shape validation happen before decoding the compact | |
| 280 | + * command. The module then validates domain rules and either returns an | |
| 281 | + * authoritative saved result or a stable error code used by the queue UI. | |
| 282 | + * | |
| 283 | + * The handler terminates through WordPress JSON helpers. It intentionally has | |
| 284 | + * no return type because those helpers stop execution after sending a response. | |
| 285 | + */ | |
| 286 | +function mlsimport_ajax_change_field_configuration() { | |
| 287 | + check_ajax_referer( 'mlsimport_field_selector_nonce', 'security' ); | |
| 481 | 288 | |
| 482 | - // Sanitize inputs | |
| 483 | - $field_key = sanitize_text_field( $_POST['field_key'] ); | |
| 484 | - $option_type = sanitize_text_field( $_POST['option_type'] ); | |
| 485 | - $value = isset( $_POST['value'] ) ? $_POST['value'] : ''; | |
| 289 | + if ( ! current_user_can( 'manage_options' ) ) { | |
| 290 | + wp_send_json_error( array( 'error' => array( 'code' => 'forbidden', 'message' => 'You are not allowed to change Field Configuration.' ) ), 403 ); | |
| 291 | + } | |
| 486 | 292 | |
| 487 | - // Error log for debugging | |
| 488 | - //error_log( '[mlsimport] Field Key: ' . $field_key ); | |
| 489 | - //error_log( '[mlsimport] Option Type: ' . $option_type ); | |
| 490 | - //error_log( '[mlsimport] Raw Value: ' . print_r( $value, true ) ); | |
| 293 | + if ( ! isset( $_POST['revision'], $_POST['command'] ) || ! is_scalar( $_POST['revision'] ) || ! is_scalar( $_POST['command'] ) ) { | |
| 294 | + wp_send_json_error( array( 'error' => array( 'code' => 'invalid_request', 'message' => 'Revision and command are required.' ) ), 400 ); | |
| 295 | + } | |
| 491 | 296 | |
| 492 | - // Validate option type | |
| 493 | - $valid_option_types = array( 'import', 'admin', 'label', 'postmeta', 'taxonomy' ); | |
| 494 | - if ( ! in_array( $option_type, $valid_option_types ) ) { | |
| 495 | - //error_log( '[mlsimport] Invalid option type: ' . $option_type ); | |
| 496 | - wp_send_json_error( 'Invalid option type' ); | |
| 497 | - } | |
| 297 | + $revision = max( 0, (int) wp_unslash( $_POST['revision'] ) ); | |
| 298 | + $command = json_decode( wp_unslash( (string) $_POST['command'] ), true ); | |
| 299 | + if ( ! is_array( $command ) ) { | |
| 300 | + wp_send_json_error( array( 'error' => array( 'code' => 'invalid_json', 'message' => 'The Field Configuration command is not valid JSON.' ) ), 400 ); | |
| 301 | + } | |
| 498 | 302 | |
| 499 | - // Get current options | |
| 500 | - $options = get_option( 'mlsimport_admin_fields_select', array() ); | |
| 501 | - $original_options = $options; | |
| 303 | + // Per-connection scope: the tab posts the mls_id it was rendered for; a | |
| 304 | + // request without one (legacy) resolves to the current connection. | |
| 305 | + $mls_id = mlsimport_field_mapping_request_scope( isset( $_POST['mls_id'] ) && is_scalar( $_POST['mls_id'] ) ? wp_unslash( $_POST['mls_id'] ) : null ); | |
| 502 | 306 | |
| 503 | - // Map option type to options array key | |
| 504 | - $option_map = array( | |
| 505 | - 'import' => 'mls-fields', | |
| 506 | - 'admin' => 'mls-fields-admin', | |
| 507 | - 'label' => 'mls-fields-label', | |
| 508 | - 'postmeta' => 'mls-fields-map-postmeta', | |
| 509 | - 'taxonomy' => 'mls-fields-map-taxonomy' | |
| 510 | - ); | |
| 307 | + $result = mlsimport_field_configuration( $mls_id )->change( | |
| 308 | + $revision, | |
| 309 | + $command, | |
| 310 | + mlsimport_field_configuration_metadata( $mls_id ), | |
| 311 | + mlsimport_field_configuration_taxonomies(), | |
| 312 | + mlsimport_hardocde_theme_schema() | |
| 313 | + ); | |
| 314 | + if ( ! $result['success'] ) { | |
| 315 | + $status = 'stale_revision' === $result['error']['code'] ? 409 : ( 'persistence_failed' === $result['error']['code'] ? 500 : 422 ); | |
| 316 | + wp_send_json_error( $result, $status ); | |
| 317 | + } | |
| 511 | 318 | |
| 512 | - $option_key = $option_map[ $option_type ]; | |
| 513 | - | |
| 514 | - if ( ! isset( $options[ $option_key ] ) ) { | |
| 515 | - $options[ $option_key ] = array(); | |
| 516 | - //error_log( '[mlsimport] Initialized option key: ' . $option_key ); | |
| 517 | - } | |
| 518 | - | |
| 519 | - // Sanitize and update value | |
| 520 | - switch ( $option_type ) { | |
| 521 | - case 'import': | |
| 522 | - case 'admin': | |
| 523 | - $options[ $option_key ][ $field_key ] = intval( $value ); | |
| 524 | - break; | |
| 525 | - | |
| 526 | - case 'label': | |
| 527 | - case 'postmeta': | |
| 528 | - case 'taxonomy': | |
| 529 | - $options[ $option_key ][ $field_key ] = sanitize_text_field( $value ); | |
| 530 | - break; | |
| 531 | - } | |
| 532 | - | |
| 533 | - // //error_log( '[mlsimport] Updated Options: ' . print_r( $options, true ) ); | |
| 534 | - | |
| 535 | - // No changes | |
| 536 | - if ( $options === $original_options ) { | |
| 537 | - //error_log( '[mlsimport] No changes detected for option update.' ); | |
| 538 | - wp_send_json_success( array( | |
| 539 | - 'message' => 'Field option saved successfully', | |
| 540 | - 'fieldKey' => $field_key, | |
| 541 | - 'optionType' => $option_type, | |
| 542 | - 'value' => $value, | |
| 543 | - ) ); | |
| 544 | - } | |
| 545 | - //error_log('saving mls-fields '.count($options['mls-fields'])); | |
| 546 | - //error_log('saving mls-fields-admin '.count($options['mls-fields-admin'])); | |
| 547 | - //error_log('saving mls-fields-label '.count($options['mls-fields-label'])); | |
| 548 | - //error_log('saving mls-fields-map-postmeta '.count($options['mls-fields-map-postmeta'])); | |
| 549 | - //error_log('saving mls-fields-map-taxonomy '.count($options['mls-fields-map-taxonomy'])); | |
| 550 | - //error_log('saving field_order '.count($options['field_order'])); | |
| 551 | - | |
| 552 | - // Save updated options | |
| 553 | - $result = update_option( 'mlsimport_admin_fields_select', $options ); | |
| 554 | - | |
| 555 | - if ( false !== $result ) { | |
| 556 | - //error_log( '[mlsimport] Options updated successfully.' ); | |
| 557 | - wp_send_json_success( array( | |
| 558 | - 'message' => 'Field option saved successfully', | |
| 559 | - 'fieldKey' => $field_key, | |
| 560 | - 'optionType' => $option_type, | |
| 561 | - 'value' => $value, | |
| 562 | - ) ); | |
| 563 | - } else { | |
| 564 | - //error_log( '[mlsimport] Failed to update options.' ); | |
| 565 | - wp_send_json_error( 'Failed to save field option' ); | |
| 566 | - } | |
| 319 | + wp_send_json_success( $result ); | |
| 567 | 320 | } |
| 568 | - | |
| 569 | - | |
| 570 | - | |
| 571 | -function mlsimport_ajax_save_field_position() { | |
| 572 | - // Verify nonce | |
| 573 | - if (!isset($_POST['security']) || !wp_verify_nonce($_POST['security'], 'mlsimport_field_selector_nonce')) { | |
| 574 | - wp_send_json_error('Invalid security token'); | |
| 575 | - } | |
| 576 | - | |
| 577 | - // Validate required fields | |
| 578 | - if (!isset($_POST['moving_index'], $_POST['target_index'], $_POST['position'])) { | |
| 579 | - wp_send_json_error('Missing parameters'); | |
| 580 | - } | |
| 581 | - | |
| 582 | - $moving_index = intval($_POST['moving_index']); | |
| 583 | - $target_index = intval($_POST['target_index']); | |
| 584 | - $position = sanitize_text_field($_POST['position']); // 'before' or 'after' | |
| 585 | - | |
| 586 | - // Load current options | |
| 587 | - $options = get_option('mlsimport_admin_fields_select', []); | |
| 588 | - | |
| 589 | - if (empty($options['field_order']) || !is_array($options['field_order'])) { | |
| 590 | - wp_send_json_error('Field order not found'); | |
| 591 | - } | |
| 592 | - | |
| 593 | - | |
| 594 | - | |
| 595 | - | |
| 596 | - // Normalize field_order to ensure correct index order | |
| 597 | - $order_map = $options['field_order']; | |
| 598 | - | |
| 599 | - asort($order_map); // Sort by index | |
| 600 | - $ordered_keys = array_keys($order_map); | |
| 601 | - | |
| 602 | - // Safety checks | |
| 603 | - if (!isset($ordered_keys[$moving_index]) || !isset($ordered_keys[$target_index])) { | |
| 604 | - wp_send_json_error('Invalid indexes'); | |
| 605 | - } | |
| 606 | - | |
| 607 | - $moving_key = $ordered_keys[$moving_index]; | |
| 608 | - $target_key = $ordered_keys[$target_index]; | |
| 609 | - | |
| 610 | - // Remove moving key | |
| 611 | - array_splice($ordered_keys, $moving_index, 1); | |
| 612 | - | |
| 613 | - // Adjust target index if needed | |
| 614 | - if ($position === 'after' && $moving_index < $target_index) { | |
| 615 | - $target_index--; | |
| 616 | - } | |
| 617 | - | |
| 618 | - // Calculate new position | |
| 619 | - $insert_index = ($position === 'before') ? $target_index : $target_index + 1; | |
| 620 | - | |
| 621 | - // Insert the field key at the new position | |
| 622 | - array_splice($ordered_keys, $insert_index, 0, $moving_key); | |
| 623 | - | |
| 624 | - // Rebuild the new order map | |
| 625 | - $new_order = []; | |
| 626 | - foreach ($ordered_keys as $i => $key) { | |
| 627 | - $new_order[$key] = $i; | |
| 628 | - } | |
| 629 | - | |
| 630 | - // Save new field order | |
| 631 | - $options['field_order'] = $new_order; | |
| 632 | - | |
| 633 | - // Apply order to all related field maps | |
| 634 | - $targets = [ | |
| 635 | - 'mls-fields', | |
| 636 | - 'mls-fields-map-taxonomy', | |
| 637 | - 'mls-fields-map-postmeta', | |
| 638 | - 'mls-fields-map-admin', | |
| 639 | - 'mls-fields-map-label', | |
| 640 | - ]; | |
| 641 | - | |
| 642 | - foreach ($targets as $target) { | |
| 643 | - if (!empty($options[$target]) && is_array($options[$target])) { | |
| 644 | - $reordered = []; | |
| 645 | - | |
| 646 | - foreach ($new_order as $field_key => $index) { | |
| 647 | - if (isset($options[$target][$field_key])) { | |
| 648 | - $reordered[$field_key] = $options[$target][$field_key]; | |
| 649 | - } | |
| 650 | - } | |
| 651 | - | |
| 652 | - // Preserve any extra keys not in field_order | |
| 653 | - foreach ($options[$target] as $key => $value) { | |
| 654 | - if (!isset($reordered[$key])) { | |
| 655 | - $reordered[$key] = $value; | |
| 656 | - } | |
| 657 | - } | |
| 658 | - | |
| 659 | - $options[$target] = $reordered; | |
| 660 | - } | |
| 661 | - } | |
| 662 | - | |
| 663 | - // Save the updated options | |
| 664 | - $saved = update_option('mlsimport_admin_fields_select', $options); | |
| 665 | - if ( $saved ) { | |
| 666 | - global $mlsimport; | |
| 667 | - if ( isset( $mlsimport->admin->env_data ) && method_exists( $mlsimport->admin->env_data, 'enviroment_custom_fields' ) ) { | |
| 668 | - $mlsimport->admin->env_data->enviroment_custom_fields( $mlsimport->get_plugin_name() ); | |
| 669 | - } | |
| 670 | - } | |
| 671 | - | |
| 672 | - wp_send_json_success('Field order updated'); | |
| 673 | -} | |
| 674 | - | |
| 675 | -// Register the AJAX handler | |
| 676 | -add_action('wp_ajax_mlsimport_save_field_position', 'mlsimport_ajax_save_field_position'); | |
| 321 | +add_action( 'wp_ajax_mlsimport_change_field_configuration', 'mlsimport_ajax_change_field_configuration' ); | |