diff --git a/admin/admin-menu-and-tabs.php b/admin/admin-menu-and-tabs.php index a242216..da01c05 100644 --- a/admin/admin-menu-and-tabs.php +++ b/admin/admin-menu-and-tabs.php @@ -61,6 +61,74 @@ public function __construct() { $this->page_title = __( 'Migration', 'disciple-tools-migration' ); } // End __construct() + /** + * Post type slugs returned by Disciple.Tools for migratable records, or a minimal fallback. + * + * @return string[] + */ + public static function get_migratable_post_types(): array { + if ( class_exists( 'DT_Posts' ) ) { + $types = DT_Posts::get_post_types(); + return is_array( $types ) ? array_values( array_unique( $types ) ) : []; + } + return [ 'contacts', 'groups' ]; + } + + /** + * Default "allowed" flag for record migration for a post type (new installs / newly registered types). + * + * @param string $post_type Sanitized post type slug. + */ + public static function get_default_record_allowed_for_type( string $post_type ): bool { + return in_array( $post_type, [ 'contacts', 'groups' ], true ); + } + + /** + * Merges stored record toggles with all current DT post types. + * + * @param array $stored Values from options (may omit new types or contain stale keys). + * + * @return array + */ + public static function normalize_records_allowed( array $stored ): array { + $types = self::get_migratable_post_types(); + $out = []; + + foreach ( $types as $post_type ) { + if ( array_key_exists( $post_type, $stored ) ) { + $out[ $post_type ] = ! empty( $stored[ $post_type ] ); + } else { + $out[ $post_type ] = self::get_default_record_allowed_for_type( $post_type ); + } + } + + return $out; + } + + /** + * Human-readable label for a DT post type (plural preferred). + * + * @param string $post_type Post type slug. + */ + public static function get_post_type_label( string $post_type ): string { + if ( ! class_exists( 'DT_Posts' ) ) { + return $post_type; + } + try { + $settings = DT_Posts::get_post_settings( $post_type, false ); + if ( is_array( $settings ) ) { + if ( ! empty( $settings['label_plural'] ) ) { + return (string) $settings['label_plural']; + } + if ( ! empty( $settings['label'] ) ) { + return (string) $settings['label']; + } + } + } catch ( Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + } + return $post_type; + } + /** * Returns the current migration settings from the options table. * @@ -102,7 +170,14 @@ public static function get_settings(): array { $current = []; } - return wp_parse_args( $current, $defaults ); + $parsed = wp_parse_args( $current, $defaults ); + if ( ! isset( $parsed['allowed_items'] ) || ! is_array( $parsed['allowed_items'] ) ) { + $parsed['allowed_items'] = $defaults['allowed_items']; + } + $rec = $parsed['allowed_items']['records'] ?? []; + $parsed['allowed_items']['records'] = self::normalize_records_allowed( is_array( $rec ) ? $rec : [] ); + + return $parsed; } /** diff --git a/admin/class-dt-migration-import-ajax.php b/admin/class-dt-migration-import-ajax.php index 302d3f4..928bb97 100644 --- a/admin/class-dt-migration-import-ajax.php +++ b/admin/class-dt-migration-import-ajax.php @@ -37,15 +37,19 @@ public function enqueue_scripts( string $hook ) : void { 'dt-migration-import', $plugin_url . 'admin/js/import.js', [ 'jquery' ], - '0.3.5', + '0.3.6', true ); + $record_order = class_exists( 'Disciple_Tools_Migration_Import_Engine' ) + ? Disciple_Tools_Migration_Import_Engine::get_record_import_order() + : [ 'peoplegroups', 'groups', 'contacts', 'trainings' ]; wp_localize_script( 'dt-migration-import', 'dtMigrationImport', [ - 'ajaxUrl' => admin_url( 'admin-ajax.php' ), - 'nonce' => wp_create_nonce( 'dt_migration_import' ), + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'dt_migration_import' ), + 'recordImportOrder' => array_values( $record_order ), 'strings' => [ 'continue' => __( 'Continue', 'disciple-tools-migration' ), 'confirm' => __( 'Confirm', 'disciple-tools-migration' ), @@ -218,6 +222,27 @@ public function handle_import_batch() : void { wp_send_json_error( [ 'message' => __( 'Post type required.', 'disciple-tools-migration' ) ] ); } + if ( $init_q ) { + $export_res = wp_remote_post( + $base . '/wp-json/dt-migration/v1/export', + [ + 'timeout' => 60, + 'headers' => [ + 'Authorization' => 'Bearer ' . $jwt, + 'Content-Type' => 'application/json', + ], + 'body' => wp_json_encode( [ 'settings_only' => true ] ), + ] + ); + if ( ! is_wp_error( $export_res ) ) { + $ex_code = wp_remote_retrieve_response_code( $export_res ); + $ex_body = json_decode( (string) wp_remote_retrieve_body( $export_res ), true ); + if ( $ex_code >= 200 && $ex_code < 300 && is_array( $ex_body ) ) { + Disciple_Tools_Migration_Import_Engine::bootstrap_post_types_from_export( $ex_body ); + } + } + } + $records_url = add_query_arg( [ 'offset' => $offset, 'limit' => $limit ], $base . '/wp-json/dt-migration/v1/records/' . $post_type @@ -331,6 +356,10 @@ private function handle_file_mode_batch( string $step, array $settings ) : void $slice = array_slice( $records_all, $offset, $limit ); $has_more = ( $offset + count( $slice ) ) < $total; + if ( $init_q ) { + Disciple_Tools_Migration_Import_Engine::bootstrap_post_types_from_export( $payload ); + } + try { $batch_result = Disciple_Tools_Migration_Import_Engine::import_records_batch( $post_type, $slice, $offset, $init_q ); } catch ( Throwable $e ) { diff --git a/admin/class-dt-migration-tab-import.php b/admin/class-dt-migration-tab-import.php index 392cb52..3e23f47 100644 --- a/admin/class-dt-migration-tab-import.php +++ b/admin/class-dt-migration-tab-import.php @@ -234,11 +234,12 @@ public function main_column( array $settings ) { connection_result['allowed_items']['records'] ?? []; $record_labels = []; - if ( ! empty( $records['contacts'] ) ) { - $record_labels[] = esc_html__( 'Contacts', 'disciple-tools-migration' ); - } - if ( ! empty( $records['groups'] ) ) { - $record_labels[] = esc_html__( 'Groups', 'disciple-tools-migration' ); + if ( is_array( $records ) ) { + foreach ( $records as $post_type => $enabled ) { + if ( ! empty( $enabled ) ) { + $record_labels[] = Disciple_Tools_Migration_Menu::get_post_type_label( (string) $post_type ); + } + } } echo esc_html( implode( ', ', $record_labels ) ); ?> diff --git a/admin/class-dt-migration-tab-settings.php b/admin/class-dt-migration-tab-settings.php index 0e40fde..8ce9d63 100644 --- a/admin/class-dt-migration-tab-settings.php +++ b/admin/class-dt-migration-tab-settings.php @@ -112,17 +112,28 @@ public function main_column( array $settings ) {
+
- + +

+

- +

@@ -170,16 +181,12 @@ public function process_form_fields(): void { $settings['allowed_items']['workflows'] = ! empty( $allowed['workflows'] ); $settings['allowed_items']['system_users'] = ! empty( $allowed['system_users'] ); - if ( ! isset( $settings['allowed_items']['records'] ) || ! is_array( $settings['allowed_items']['records'] ) ) { - $settings['allowed_items']['records'] = [ - 'contacts' => true, - 'groups' => true, - ]; + $incoming_records = isset( $allowed['records'] ) && is_array( $allowed['records'] ) ? $allowed['records'] : []; + $settings['allowed_items']['records'] = []; + foreach ( Disciple_Tools_Migration_Menu::get_migratable_post_types() as $post_type ) { + $settings['allowed_items']['records'][ $post_type ] = ! empty( $incoming_records[ $post_type ] ); } - $settings['allowed_items']['records']['contacts'] = ! empty( $allowed['records']['contacts'] ); - $settings['allowed_items']['records']['groups'] = ! empty( $allowed['records']['groups'] ); - Disciple_Tools_Migration_Menu::update_settings( $settings ); } diff --git a/admin/js/import.js b/admin/js/import.js index 9a9994a..f537a22 100644 --- a/admin/js/import.js +++ b/admin/js/import.js @@ -171,7 +171,9 @@ records: null } ); } - const order = [ 'peoplegroups', 'groups', 'contacts', 'trainings' ]; + const order = ( window.dtMigrationImport && Array.isArray( window.dtMigrationImport.recordImportOrder ) && window.dtMigrationImport.recordImportOrder.length ) + ? window.dtMigrationImport.recordImportOrder + : [ 'peoplegroups', 'groups', 'contacts', 'trainings' ]; const rest = Object.keys( records ).filter( pt => ! order.includes( pt ) ); const ordered = order.filter( pt => records[ pt ] ).concat( rest ); const recordPts = ordered.filter( pt => records[ pt ] ); diff --git a/includes/class-dt-migration-import-engine.php b/includes/class-dt-migration-import-engine.php index e9045eb..c64e1e8 100644 --- a/includes/class-dt-migration-import-engine.php +++ b/includes/class-dt-migration-import-engine.php @@ -18,6 +18,15 @@ class Disciple_Tools_Migration_Import_Engine { */ const POST_TYPE_ORDER = [ 'peoplegroups', 'groups', 'contacts', 'trainings' ]; + /** + * Preferred order for importing record types (dependencies and deferred connections). + * + * @return string[] + */ + public static function get_record_import_order(): array { + return self::POST_TYPE_ORDER; + } + /** * Whether we are currently in a record import (insert_or_update_post) call. * Used by get_post_metadata filter to fix theme bug when *_details meta returns ''. @@ -100,6 +109,14 @@ public static function import_settings( array $export_payload, array $selected ) return $result; } + // Register custom post types (dt_custom_post_types) before tiles/fields so the target site + // knows about CPTs that exist on the source (required for record import and Customizations UI). + if ( ! empty( $selected['general_settings'] ) || ! empty( $selected['tiles'] ) || ! empty( $selected['fields'] ) ) { + if ( self::apply_post_types( $dt_settings ) ) { + $result['applied']['post_types'] = true; + } + } + if ( ! empty( $selected['general_settings'] ) ) { $general = self::apply_general_settings( $export_payload ); if ( ! empty( $general['error'] ) ) { @@ -163,6 +180,151 @@ public static function import_settings( array $export_payload, array $selected ) return $result; } + /** + * Merges exported post type definitions into dt_custom_post_types and registers CPTs for the current request. + * + * Without this, DT_Posts::update_post fails with "Post type does not exist" for types that only exist on the + * source (e.g. trainings) and customizations UI will not list them. + * + * @param array $dt_settings Export block dt_settings (dt_post_types_settings, dt_post_types_custom_settings). + * + * @return bool True if post type data was present and processing ran. + */ + private static function apply_post_types( array $dt_settings ) : bool { + $base = $dt_settings['dt_post_types_settings']['values'] ?? []; + $custom = $dt_settings['dt_post_types_custom_settings']['values'] ?? []; + if ( ( empty( $base ) || ! is_array( $base ) ) && ( empty( $custom ) || ! is_array( $custom ) ) ) { + return false; + } + if ( ! class_exists( 'DT_Posts' ) ) { + return false; + } + + $registered = DT_Posts::get_post_types(); + $existing = get_option( 'dt_custom_post_types', [] ); + if ( ! is_array( $existing ) ) { + $existing = []; + } + + if ( ! empty( $custom ) && is_array( $custom ) ) { + foreach ( $custom as $slug => $meta ) { + if ( ! is_string( $slug ) || $slug === '' || ! is_array( $meta ) ) { + continue; + } + if ( in_array( $slug, $registered, true ) ) { + continue; + } + if ( ! isset( $existing[ $slug ] ) ) { + $existing[ $slug ] = []; + } + $existing[ $slug ] = array_merge( $existing[ $slug ], $meta ); + $existing[ $slug ]['is_custom'] = true; + } + } + + foreach ( $base as $slug => $settings ) { + if ( ! is_string( $slug ) || $slug === '' || ! is_array( $settings ) ) { + continue; + } + if ( in_array( $slug, $registered, true ) ) { + continue; + } + if ( ! isset( $existing[ $slug ] ) ) { + $existing[ $slug ] = []; + } + $existing[ $slug ]['label_singular'] = $settings['label_singular'] ?? $existing[ $slug ]['label_singular'] ?? $slug; + $existing[ $slug ]['label_plural'] = $settings['label_plural'] ?? $existing[ $slug ]['label_plural'] ?? $slug; + if ( isset( $settings['hidden'] ) ) { + $existing[ $slug ]['hidden'] = (bool) $settings['hidden']; + } + $existing[ $slug ]['is_custom'] = true; + } + + update_option( 'dt_custom_post_types', $existing, true ); + + $original_registered = $registered; + $to_register = []; + foreach ( array_keys( $custom ) as $slug ) { + if ( is_string( $slug ) && $slug !== '' && ! in_array( $slug, $original_registered, true ) ) { + $to_register[] = $slug; + } + } + foreach ( array_keys( $base ) as $slug ) { + if ( is_string( $slug ) && $slug !== '' && ! in_array( $slug, $original_registered, true ) ) { + $to_register[] = $slug; + } + } + foreach ( array_unique( $to_register ) as $slug ) { + self::register_post_type_for_current_request( $slug ); + } + + foreach ( array_keys( $existing ) as $slug ) { + if ( is_string( $slug ) ) { + wp_cache_delete( $slug . '_post_type_settings' ); + wp_cache_delete( $slug . '_type_settings' ); + } + } + + flush_rewrite_rules( false ); + + return true; + } + + /** + * Registers CPTs from an export payload (e.g. before record import when settings step was skipped). + * + * @param array $export_payload Same shape as API/file import (export.dt_settings). + */ + public static function bootstrap_post_types_from_export( array $export_payload ) : void { + $export = $export_payload['export'] ?? []; + if ( ! is_array( $export ) ) { + return; + } + $dt_settings = $export['dt_settings'] ?? []; + if ( ! is_array( $dt_settings ) ) { + return; + } + self::apply_post_types( $dt_settings ); + } + + /** + * Instantiates Disciple_Tools_Post_Type_Template and registers the post type for this request (after init). + * + * @param string $post_type Post type slug. + */ + private static function register_post_type_for_current_request( string $post_type ) : void { + if ( ! class_exists( 'DT_Posts' ) || ! class_exists( 'Disciple_Tools_Post_Type_Template' ) ) { + return; + } + if ( post_type_exists( $post_type ) ) { + return; + } + if ( in_array( $post_type, DT_Posts::get_post_types(), true ) ) { + return; + } + $custom = get_option( 'dt_custom_post_types', [] ); + if ( ! is_array( $custom ) || empty( $custom[ $post_type ] ) || empty( $custom[ $post_type ]['is_custom'] ) ) { + return; + } + $c = $custom[ $post_type ]; + $tpl = new Disciple_Tools_Post_Type_Template( + $post_type, + $c['label_singular'] ?? $post_type, + $c['label_plural'] ?? $post_type + ); + $tpl->register_post_type(); + $tpl->rewrite_init(); + } + + /** + * Ensures a post type from dt_custom_post_types is registered (e.g. new HTTP request after settings import). + * + * @param string $post_type Post type slug. + */ + private static function ensure_post_type_registered_from_options( string $post_type ) : void { + self::register_post_type_for_current_request( $post_type ); + } + /** * Applies general site settings. * @@ -346,6 +508,8 @@ public static function import_records_batch( string $post_type, array $records, return $result; } + self::ensure_post_type_registered_from_options( $post_type ); + // On first batch, delete existing posts of this type (destructive). if ( $offset === 0 ) { $deleted = self::delete_posts_by_type( $post_type ); diff --git a/rest-api/rest-api.php b/rest-api/rest-api.php index 00e6d2f..563062d 100644 --- a/rest-api/rest-api.php +++ b/rest-api/rest-api.php @@ -126,10 +126,7 @@ public function capabilities( WP_REST_Request $request ) : WP_REST_Response { 'supports_file_download' => true, 'supports_api_mode' => true, 'supports_file_mode' => true, - 'supports_records' => [ - 'contacts' => ! empty( $settings['allowed_items']['records']['contacts'] ), - 'groups' => ! empty( $settings['allowed_items']['records']['groups'] ), - ], + 'supports_records' => $settings['allowed_items']['records'] ?? [], ], ];