| @@ -1,238 +1,121 @@ | ||
| 1 | -<?php | |
| 1 | +<?php | |
| 2 | 2 | /** |
| 3 | - * Admin partial: "Select Import fields" tab (Field Options). | |
| 3 | + * Read-only renderer for the Field Configuration settings tab. | |
| 4 | 4 | * |
| 5 | - * Renders the RESO field-selection interface for the active MLS. It first gates | |
| 6 | - * on connectivity (a valid SaaS token AND a successful MLS connection), then — if | |
| 7 | - * the MLS metadata has been populated — builds the per-field options structure | |
| 8 | - * (defaults derived from the theme schema) and hands it to | |
| 9 | - * render_mls_field_selection_interface(). Until metadata arrives it shows a | |
| 10 | - * "please stand by" notice. The progressive-save JS persists edits field by field. | |
| 5 | + * Metadata gathering owns initialization and persistence. This partial only | |
| 6 | + * checks connection state, asks the deep module for an in-memory normalized | |
| 7 | + * view, and renders the active fields. There is deliberately no settings form, | |
| 8 | + * options.php submission, pagination state, or browser-triggered initial save; | |
| 9 | + * the JavaScript controller sends compact commands to the single AJAX seam. | |
| 11 | 10 | * |
| 12 | - * @package mlsimport | |
| 13 | - * @subpackage mlsimport/admin/partials | |
| 11 | + * PER-CONNECTION SCOPE (multi-MLS): the Field Configuration is stored per | |
| 12 | + * connection (#275). This tab addresses ONE connection, resolved by the shared | |
| 13 | + * scope rule (includes/mlsimport-field-mapping-scope.php): ?mls=<id> when it | |
| 14 | + * names a registered connection, the current connection otherwise. With 2+ | |
| 15 | + * connections a selector at the top switches the scope by reloading the page; | |
| 16 | + * the resolved scope is exposed to the JS in the hidden #mlsimport_field_scope | |
| 17 | + * input so every mutation and gather request stays on the same connection. | |
| 18 | + * | |
| 19 | + * Step by step: | |
| 20 | + * 1. Resolve the scope and read that connection's populated flag. | |
| 21 | + * 2. Gate on the global SaaS account token (the account is install-wide). | |
| 22 | + * 3. Gate on the MLS connection: the CURRENT connection keeps the historic | |
| 23 | + * global flag + refresh flow; a non-current scope uses its own record's | |
| 24 | + * tested status (the global flag belongs to the current connection only). | |
| 25 | + * 4. With 2+ connections, render the scope selector. | |
| 26 | + * 5. Render the scoped configuration, or the "gathering" notice when the | |
| 27 | + * scope's metadata is not populated yet. | |
| 28 | + * | |
| 29 | + * @package MLSImport | |
| 30 | + * @subpackage MLSImport/admin/partials | |
| 14 | 31 | */ |
| 15 | 32 | |
| 16 | -// Block direct access outside of WordPress. | |
| 17 | 33 | if ( ! defined( 'ABSPATH' ) ) { |
| 18 | - exit; // Exit if accessed directly | |
| 34 | + exit; | |
| 19 | 35 | } |
| 20 | -// Theme-specific RESO->post-meta schema (defines which fields map where by default). | |
| 21 | -$theme_schema= mlsimport_hardocde_theme_schema(); | |
| 22 | 36 | |
| 23 | -// Primary plugin options and the metadata-populated flag for this MLS. | |
| 24 | -$options = get_option( 'mlsimport_admin_options' ); | |
| 25 | -$mlsimport_mls_metadata_populated = get_option( 'mlsimport_mls_metadata_populated', '' ); | |
| 26 | -// Allowed HTML tag set for any rendered content. | |
| 27 | -$permited_tags = mlsimport_allowed_html_tags_content(); | |
| 28 | -// Resolve the active theme's property post type (if the adapter exposes it). | |
| 29 | -$post_type = ''; | |
| 30 | -if (method_exists($this->env_data, 'get_property_post_type')) { | |
| 31 | - $post_type = $this->env_data->get_property_post_type(); | |
| 32 | -} | |
| 37 | +global $mlsimport; | |
| 33 | 38 | |
| 34 | -// Taxonomies registered against that post type — the taxonomy-mapping dropdowns use these. | |
| 35 | -$available_taxonomies = mlsimport_get_custom_post_type_taxonomies($post_type); | |
| 39 | +// Step 1: which connection is this tab editing? (?mls=<registered id> wins, | |
| 40 | +// anything else resolves to the current connection.) | |
| 41 | +$mlsimport_field_scope = mlsimport_field_mapping_request_scope( isset( $_GET['mls'] ) ? wp_unslash( $_GET['mls'] ) : null ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only view selector. | |
| 42 | +$mlsimport_connections = Mlsimport_Connections::all(); | |
| 43 | +$mlsimport_scope_is_current = $mlsimport_field_scope === mlsimport_current_mls_id(); | |
| 36 | 44 | |
| 45 | +$metadata_populated = mlsimport_get_connection_option( 'mlsimport_mls_metadata_populated', '', $mlsimport_field_scope ); | |
| 46 | +$token = $mlsimport->admin->mlsimport_saas_get_mls_api_token_from_transient(); | |
| 37 | 47 | |
| 38 | -// SaaS/MLS connection gate: fetch the cached API token and prior connection result. | |
| 39 | -global $mlsimport; | |
| 40 | -$token = $mlsimport->admin->mlsimport_saas_get_mls_api_token_from_transient(); | |
| 41 | -$is_mls_connected = get_option('mlsimport_connection_test', ''); | |
| 48 | +// Step 2: no SaaS account token — nothing on this tab can work. | |
| 42 | 49 | $mlsimport->admin->mlsimport_saas_setting_up(); |
| 50 | +if ( '' === trim( $token ) ) { | |
| 51 | + // Names the reason (no subscription vs wrong password). | |
| 52 | + echo mlsimport_account_not_connected_html(); // phpcs:ignore WordPress.Security.EscapeOutput -- escaped by the builder. | |
| 53 | + return; | |
| 54 | +} | |
| 43 | 55 | |
| 44 | -// Not yet confirmed connected — re-test the MLS connection now and re-read the flag. | |
| 45 | -if ('yes' !== $is_mls_connected) { | |
| 46 | - $mlsimport->admin->mlsimport_saas_check_mls_connection(); | |
| 47 | - $is_mls_connected = get_option('mlsimport_connection_test', ''); | |
| 56 | +// Step 3: the MLS-connected gate, per scope. | |
| 57 | +if ( $mlsimport_scope_is_current ) { | |
| 58 | + // Historic flow: refresh the global connection state before deciding. | |
| 59 | + $is_mls_connected = get_option( 'mlsimport_connection_test', '' ); | |
| 60 | + if ( 'yes' !== $is_mls_connected ) { | |
| 61 | + $mlsimport->admin->mlsimport_saas_check_mls_connection(); | |
| 62 | + $is_mls_connected = get_option( 'mlsimport_connection_test', '' ); | |
| 63 | + } | |
| 64 | +} else { | |
| 65 | + // A non-current connection answers from its own record (#277 status). | |
| 66 | + $mlsimport_scope_record = Mlsimport_Connections::get( $mlsimport_field_scope ); | |
| 67 | + $is_mls_connected = is_array( $mlsimport_scope_record ) ? (string) ( $mlsimport_scope_record['status'] ?? '' ) : ''; | |
| 48 | 68 | } |
| 49 | 69 | |
| 50 | -// No SaaS token — the account credentials are missing/invalid; stop here. | |
| 51 | -if (trim($token) === '') { | |
| 52 | - echo '<div class="mlsimport_warning">' . esc_html__('You are not connected to MlsImport - Please check your Username and Password.', 'mlsimport') . '</div>'; | |
| 53 | - return; | |
| 70 | +if ( 'yes' !== $is_mls_connected ) { | |
| 71 | + echo '<div class="mlsimport_warning">' . esc_html__( 'The connection to your MLS was NOT succesful. Please check the authentication token is correct and check your MLS Data Access Application is approved.', 'mlsimport' ) . '</div>'; | |
| 72 | + return; | |
| 54 | 73 | } |
| 55 | 74 | |
| 56 | -// Have a token but the MLS itself did not connect — stop here. | |
| 57 | -if ('yes' !== $is_mls_connected) { | |
| 58 | - echo '<div class="mlsimport_warning">' . esc_html__('The connection to your MLS was NOT succesful. Please check the authentication token is correct and check your MLS Data Access Application is approved.', 'mlsimport') . '</div>'; | |
| 59 | - return; | |
| 75 | +// Step 4: with 2+ connections the user picks which MLS's fields to edit. | |
| 76 | +if ( count( $mlsimport_connections ) > 1 ) { | |
| 77 | + echo '<div class="mlsimport-field-scope">'; | |
| 78 | + echo '<label for="mlsimport-field-mls-scope">' . esc_html__( 'Editing fields for', 'mlsimport' ) . '</label> '; | |
| 79 | + echo '<select id="mlsimport-field-mls-scope">'; | |
| 80 | + foreach ( $mlsimport_connections as $mlsimport_scope_option ) { | |
| 81 | + $mlsimport_scope_id = (int) $mlsimport_scope_option['mls_id']; | |
| 82 | + $mlsimport_scope_label = '' !== $mlsimport_scope_option['mls_name'] | |
| 83 | + ? $mlsimport_scope_option['mls_name'] | |
| 84 | + : __( 'MLS', 'mlsimport' ) . ' ' . $mlsimport_scope_id; | |
| 85 | + printf( | |
| 86 | + '<option value="%d"%s>%s</option>', | |
| 87 | + esc_attr( $mlsimport_scope_id ), | |
| 88 | + selected( $mlsimport_field_scope, $mlsimport_scope_id, false ), | |
| 89 | + esc_html( $mlsimport_scope_label ) | |
| 90 | + ); | |
| 91 | + } | |
| 92 | + echo '</select>'; | |
| 93 | + echo '</div>'; | |
| 60 | 94 | } |
| 61 | 95 | |
| 62 | -// Metadata is present — render the full field-selection form. | |
| 63 | -if ( 'yes' === $mlsimport_mls_metadata_populated ) { | |
| 64 | - // We have MLS metadata, so we can show the field selection interface | |
| 65 | - ?> | |
| 66 | - <form method="post" class= "mlsimport-import-fields-form" name="cleanup_options" action="options.php"> | |
| 67 | - <?php | |
| 96 | +// Step 5: render the scoped configuration (or the gathering notice). | |
| 97 | +if ( 'yes' === $metadata_populated ) { | |
| 98 | + $metadata = mlsimport_field_configuration_metadata( $mlsimport_field_scope ); | |
| 99 | + $theme_schema = mlsimport_hardocde_theme_schema(); | |
| 100 | + $configuration = mlsimport_field_configuration( $mlsimport_field_scope )->read( $metadata, $theme_schema, mlsimport_field_configuration_taxonomies() ); | |
| 68 | 101 | |
| 69 | - | |
| 70 | - // Add this to the beginning of the form processing | |
| 71 | - // Current saved field-selection options (empty on first visit). | |
| 72 | - $options = get_option('mlsimport_admin_fields_select', array()); | |
| 73 | - | |
| 74 | - | |
| 75 | - // Ensure all arrays are initialized | |
| 76 | - // First run (no saved options) — seed the structure from the MLS metadata. | |
| 77 | - if (!is_array($options) || | |
| 78 | - (is_array($options) && empty($options)) ) { | |
| 79 | - | |
| 80 | - // Raw MLS field metadata (JSON) captured during connection. | |
| 81 | - $mlsimport_mls_metadata_mls_data = get_option( 'mlsimport_mls_metadata_mls_data', '' ); | |
| 82 | - $metadata_api_call_data_service_property = json_decode( $mlsimport_mls_metadata_mls_data, true ); | |
| 83 | - $options = array(); | |
| 84 | - | |
| 85 | - | |
| 86 | - | |
| 87 | - // Whether each field is imported (0/1). | |
| 88 | - if (!isset($options['mls-fields'])) { | |
| 89 | - $options['mls-fields'] = array(); | |
| 90 | - } | |
| 91 | - | |
| 92 | - // Whether each field is admin-only (0/1). | |
| 93 | - if (!isset($options['mls-fields-admin'])) { | |
| 94 | - $options['mls-fields-admin'] = array(); | |
| 95 | - } | |
| 96 | - | |
| 97 | - // Per-field display label override. | |
| 98 | - if (!isset($options['mls-fields-label'])) { | |
| 99 | - $options['mls-fields-label'] = array(); | |
| 100 | - } | |
| 101 | - | |
| 102 | - // Per-field target post-meta key. | |
| 103 | - if (!isset($options['mls-fields-map-postmeta'])) { | |
| 104 | - $options['mls-fields-map-postmeta'] = array(); | |
| 105 | - } | |
| 106 | - | |
| 107 | - // Per-field target taxonomy. | |
| 108 | - if (!isset($options['mls-fields-map-taxonomy'])) { | |
| 109 | - $options['mls-fields-map-taxonomy'] = array(); | |
| 110 | - } | |
| 111 | - | |
| 112 | - | |
| 113 | - // Running index used to record each field's display order. | |
| 114 | - $order_item=0; | |
| 115 | - // Guard against non-array metadata (bad/empty JSON). | |
| 116 | - if (!is_array($metadata_api_call_data_service_property)) { | |
| 117 | - $metadata_api_call_data_service_property = []; | |
| 118 | - } | |
| 119 | - | |
| 120 | - // Alphabetise fields by key for a stable initial order. | |
| 121 | - ksort($metadata_api_call_data_service_property); | |
| 122 | - | |
| 123 | - // Seed defaults for every MLS field. | |
| 124 | - foreach ( $metadata_api_call_data_service_property as $key => $value ) { | |
| 125 | - $description = 'no description '; | |
| 126 | - | |
| 127 | - // Default: not imported, next in order, not admin-only, no meta/label mapping. | |
| 128 | - $options['mls-fields'][ $key ]=0; | |
| 129 | - $options['field_order'][ $key ]=$order_item++; | |
| 130 | - $options['mls-fields-admin'][ $key ]=0 ; | |
| 131 | - $options['mls-fields-map-postmeta'][ $key ]=''; | |
| 132 | - | |
| 133 | - $options['mls-fields-label'][ $key ]=''; | |
| 134 | - | |
| 135 | - // Field is part of the theme schema — turn it on by default. | |
| 136 | - if ( array_key_exists( $key, $theme_schema ) ) { | |
| 137 | - $options['mls-fields'][ $key ]=1; | |
| 138 | - | |
| 139 | - // Schema marks it as a taxonomy — pre-map it to that taxonomy name. | |
| 140 | - if( isset( $theme_schema[$key]['type']) && $theme_schema[$key]['type']=='taxonomy' ){ | |
| 141 | - $options['mls-fields-map-taxonomy'][ $key ]=$theme_schema[$key]['name']; | |
| 142 | - }else{ | |
| 143 | - // Otherwise leave the taxonomy mapping empty (meta field). | |
| 144 | - $options['mls-fields-map-taxonomy'][ $key ]=''; | |
| 145 | - } | |
| 146 | - | |
| 147 | - } | |
| 148 | - } | |
| 149 | - | |
| 150 | - | |
| 151 | - } | |
| 152 | - | |
| 153 | - ?> | |
| 154 | - | |
| 155 | - | |
| 156 | - | |
| 157 | - <h3><?php esc_html_e( 'Select the extra fields you want to import', 'mlsimport' ); ?>:</h3> | |
| 158 | - | |
| 159 | - <?php | |
| 160 | - | |
| 161 | - | |
| 162 | - // $options = get_option( 'mlsimport_admin_fields_select', array() ); | |
| 163 | - // print_r($options); | |
| 164 | - | |
| 165 | - | |
| 166 | - | |
| 167 | - // Get the current page and search parameters | |
| 168 | - // Pagination + filter/search state, all read from the query string. | |
| 169 | - $current_page = isset( $_GET['mlsimport_page'] ) ? intval( $_GET['mlsimport_page'] ) : 1; | |
| 170 | - $search_term = isset( $_GET['mlsimport_search'] ) ? sanitize_text_field( $_GET['mlsimport_search'] ) : ''; | |
| 171 | - $import_filter = isset( $_GET['mlsimport_filter'] ) ? sanitize_text_field( $_GET['mlsimport_filter'] ) : 'all'; | |
| 172 | - $alpha_filter = isset( $_GET['mlsimport_alpha'] ) ? sanitize_text_field( $_GET['mlsimport_alpha'] ) : ''; | |
| 173 | - | |
| 174 | - // Set up render parameters | |
| 175 | - // Config passed to the field-selection renderer (paging, filters, form, UI toggles). | |
| 176 | - $render_params = array( | |
| 177 | - 'page' => $current_page, | |
| 178 | - 'fields_per_page' => 99999, // Adjust as needed | |
| 179 | - 'search_term' => $search_term, | |
| 180 | - 'import_filter' => $import_filter, | |
| 181 | - 'alpha_filter' => $alpha_filter, | |
| 182 | - 'show_pagination' => true, | |
| 183 | - 'show_filters' => true, | |
| 184 | - 'show_stats' => true, | |
| 185 | - 'enable_drag_drop' => true, | |
| 186 | - 'form_action' => 'options.php', | |
| 187 | - 'form_method' => 'post', | |
| 188 | - 'form_name' => 'cleanup_options', | |
| 189 | - 'nonce_field' => 'mlsimport_admin_fields_select', | |
| 190 | - 'plugin_name' => 'mlsimport', | |
| 191 | - ); | |
| 192 | - | |
| 193 | - | |
| 194 | - | |
| 195 | - | |
| 196 | - | |
| 197 | - | |
| 198 | - | |
| 199 | - | |
| 200 | - // Then in the admin page, update the function call: | |
| 201 | - // Render the full drag-and-drop field-selection table. | |
| 202 | - echo render_mls_field_selection_interface( | |
| 203 | - $options, | |
| 204 | - $options, | |
| 205 | - $render_params, | |
| 206 | - $theme_schema | |
| 207 | - ); | |
| 208 | - ?> | |
| 209 | - | |
| 210 | - <input type="hidden" name="mlsimport_admin_fields_select[mls-fields-admin][force_rand]" value="<?php echo esc_attr( wp_rand() ); ?>"> | |
| 211 | - | |
| 212 | - | |
| 213 | - <?php | |
| 214 | - // Output the security nonce that the field-selector save handler checks. | |
| 215 | - mlsimport_add_field_selector_nonce(); ?> | |
| 216 | - </form> | |
| 217 | - <?php | |
| 102 | + echo '<h3>' . esc_html__( 'Select the extra fields you want to import', 'mlsimport' ) . ':</h3>'; | |
| 103 | + echo render_mls_field_selection_interface( | |
| 104 | + $metadata, | |
| 105 | + $configuration, | |
| 106 | + array( | |
| 107 | + 'search_term' => '', | |
| 108 | + 'import_filter' => 'all', | |
| 109 | + 'show_filters' => true, | |
| 110 | + 'show_stats' => true, | |
| 111 | + 'enable_drag_drop' => true, | |
| 112 | + 'plugin_name' => 'mlsimport', | |
| 113 | + ), | |
| 114 | + $theme_schema | |
| 115 | + ); | |
| 218 | 116 | } else { |
| 219 | - // We don't have MLS metadata yet, show waiting message | |
| 220 | - // Connected but metadata still being gathered — show a stand-by notice. | |
| 221 | - global $mlsimport; | |
| 222 | - $token = $mlsimport->admin->mlsimport_saas_get_mls_api_token_from_transient(); | |
| 223 | - // Token present — connected, just waiting on metadata. | |
| 224 | - if ( trim( $token ) !== '' ) { | |
| 225 | - ?> | |
| 226 | - <div class="mlsimport_warning mlsimport_validated"> | |
| 227 | - <?php | |
| 228 | - esc_html_e( 'We need to gather some information about your MLS. Please Stand By! ', 'mlsimport' ); | |
| 229 | - ?> | |
| 230 | - </div> | |
| 231 | - <?php | |
| 232 | - } else { | |
| 233 | - // No token — not connected to the SaaS at all. | |
| 234 | - esc_html_e( 'You are not connected to MLS Import', 'mlsimport' ); | |
| 235 | - } | |
| 117 | + echo '<div class="mlsimport_warning mlsimport_validated">' . esc_html__( 'We need to gather some information about your MLS. Please Stand By! ', 'mlsimport' ) . '</div>'; | |
| 236 | 118 | } |
| 237 | 119 | ?> |
| 238 | -<input type="hidden" id="mlsimport_saas_get_metadata" value="<?php echo esc_attr( wp_create_nonce( "mlsimport_saas_get_metadata" ) ); ?>" /> | |
| 120 | +<input type="hidden" id="mlsimport_field_scope" value="<?php echo esc_attr( (string) $mlsimport_field_scope ); ?>"> | |
| 121 | +<input type="hidden" id="mlsimport_saas_get_metadata" value="<?php echo esc_attr( wp_create_nonce( 'mlsimport_saas_get_metadata' ) ); ?>"> | |