From 58ab2669f63bd5f09bc4e42c1f081663c4523645 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:16:23 +0000 Subject: [PATCH 1/3] chore(deps): bump codeinwp/themeisle-sdk from 3.3.50 to 3.3.51 Bumps [codeinwp/themeisle-sdk](https://github.com/Codeinwp/themeisle-sdk) from 3.3.50 to 3.3.51. - [Release notes](https://github.com/Codeinwp/themeisle-sdk/releases) - [Changelog](https://github.com/Codeinwp/themeisle-sdk/blob/v3.3.51/CHANGELOG.md) - [Commits](https://github.com/Codeinwp/themeisle-sdk/compare/v3.3.50...v3.3.51) --- updated-dependencies: - dependency-name: codeinwp/themeisle-sdk dependency-version: 3.3.51 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- composer.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.lock b/composer.lock index 6e22f64c..a1da148d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,25 +4,25 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "da17d7fb7088e8c3367db8d75da905de", + "content-hash": "83e8f88535db501a6924104796531662", "packages": [ { "name": "codeinwp/themeisle-sdk", - "version": "3.3.50", + "version": "3.3.51", "source": { "type": "git", "url": "https://github.com/Codeinwp/themeisle-sdk.git", - "reference": "3c1f8dfc2390e667bbc086c5d660900a7985efa6" + "reference": "bb2a8414b0418b18c68c9ff1df3d7fb10467928d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/3c1f8dfc2390e667bbc086c5d660900a7985efa6", - "reference": "3c1f8dfc2390e667bbc086c5d660900a7985efa6", + "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/bb2a8414b0418b18c68c9ff1df3d7fb10467928d", + "reference": "bb2a8414b0418b18c68c9ff1df3d7fb10467928d", "shasum": "" }, "require-dev": { "codeinwp/phpcs-ruleset": "dev-main", - "yoast/phpunit-polyfills": "^2.0" + "yoast/phpunit-polyfills": "^4.0" }, "type": "library", "notification-url": "https://packagist.org/downloads/", @@ -43,9 +43,9 @@ ], "support": { "issues": "https://github.com/Codeinwp/themeisle-sdk/issues", - "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.50" + "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.51" }, - "time": "2025-11-25T19:36:35+00:00" + "time": "2026-03-30T07:58:49+00:00" } ], "packages-dev": [ @@ -913,5 +913,5 @@ "platform-overrides": { "php": "7.2" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } From 82d1298c59bc42106578b0c7eae8f45380a864d3 Mon Sep 17 00:00:00 2001 From: selul Date: Thu, 23 Apr 2026 13:20:37 +0300 Subject: [PATCH 2/3] fix(importer): resolve nav_menu widget term_id from slug hint Widget instances exported from another site reference menus by numeric term_id (e.g. `nav_menu` => 3), which never match on the target after a fresh import. `Slug_Mapping::rewrite_value()` only rewrites URL strings, so the integer passes through and `wp_nav_menu(['menu' => 3])` silently renders nothing. Add a pre-import resolver that consumes a `_ti_nav_menu_slug` hint (carried by companion exporters), looks the slug up on the target site via `wp_get_nav_menu_object()`, rewrites `nav_menu` to the fresh term_id, and strips the hint so it never persists into `widget_nav_menu`. Unresolved slugs are non-fatal. Expose the resolution step as a `ti_tpc_widget_pre_import` filter so future ID-by-reference widgets (taxonomies, products) can plug in without further core changes. Covered by `tests/widgets-import-test.php` with three cases: successful resolution, unresolved-slug fallback, and filter-hook invocation. Made-with: Cursor --- includes/Importers/Widgets_Importer.php | 43 ++++++ tests/widgets-import-test.php | 179 ++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 tests/widgets-import-test.php diff --git a/includes/Importers/Widgets_Importer.php b/includes/Importers/Widgets_Importer.php index 02132c86..5461d27e 100755 --- a/includes/Importers/Widgets_Importer.php +++ b/includes/Importers/Widgets_Importer.php @@ -136,6 +136,14 @@ public function actually_import( $data ) { $widget = json_decode( wp_json_encode( $widget ), true ); $widget = Slug_Mapping::rewrite_value( $widget ); + // [dde-patch v1] pre-import widget filter + nav_menu slug resolver. + // Resolve ID-by-reference fields (menus, etc.) using slug hints carried + // in the widget instance, then let third parties hook in before persist. + $widget = $this->resolve_known_references( $widget, $id_base ); + // Filters a widget instance right before it is persisted. Receives + // ($widget, $id_base, $widget_instance_id, $sidebar_id). + $widget = apply_filters( 'ti_tpc_widget_pre_import', $widget, $id_base, $widget_instance_id, $sidebar_id ); + // Does widget with identical settings already exist in same sidebar? if ( ! $fail && isset( $widget_instances[ $id_base ] ) ) { @@ -266,6 +274,41 @@ public function available_widgets() { } + /** + * Resolve known ID-by-reference fields on a widget instance using + * slug hints carried in the exported payload. + * + * Currently handles: + * - `nav_menu` widget: reads `_ti_nav_menu_slug`, looks up the menu on + * the target site, rewrites `nav_menu` to the fresh term_id, and + * strips the hint so it never persists into `widget_nav_menu`. + * + * Behaviour on failure is intentionally non-fatal - if the slug does + * not resolve (menu import failed) we leave the stale id in place so + * the frontend renders an empty menu widget just like before this + * patch, rather than throwing and aborting the whole import. + * + * @param array $widget Widget instance. + * @param string $id_base Widget id_base (e.g. `nav_menu`). + * + * @return array + */ + private function resolve_known_references( $widget, $id_base ) { + if ( ! is_array( $widget ) ) { + return $widget; + } + + if ( 'nav_menu' === $id_base && ! empty( $widget['_ti_nav_menu_slug'] ) ) { + $menu = wp_get_nav_menu_object( $widget['_ti_nav_menu_slug'] ); + if ( $menu && ! is_wp_error( $menu ) ) { + $widget['nav_menu'] = (int) $menu->term_id; + } + unset( $widget['_ti_nav_menu_slug'] ); + } + + return $widget; + } + /** * Moves widgets to inactive widgets. */ diff --git a/tests/widgets-import-test.php b/tests/widgets-import-test.php new file mode 100644 index 00000000..4758510b --- /dev/null +++ b/tests/widgets-import-test.php @@ -0,0 +1,179 @@ + 'Footer Two', + 'id' => 'footer-two-widgets', + ) + ); + + // Make sure the classic nav_menu widget is registered so the + // importer's available_widgets() check passes. + if ( ! isset( $GLOBALS['wp_registered_widget_controls']['nav_menu-1'] ) ) { + wp_widgets_init(); + } + + // Start from a known-clean widget state. + update_option( 'widget_nav_menu', array( '_multiwidget' => 1 ) ); + update_option( + 'sidebars_widgets', + array( + 'wp_inactive_widgets' => array(), + 'footer-two-widgets' => array(), + ) + ); + } + + public function tearDown(): void { + delete_option( 'widget_nav_menu' ); + delete_option( 'sidebars_widgets' ); + parent::tearDown(); + } + + /** + * When a nav_menu widget carries `_ti_nav_menu_slug`, the importer + * must rewrite `nav_menu` to the term_id of the menu that matches + * that slug on the current site and strip the hint before persist. + */ + public function test_nav_menu_widget_slug_is_resolved_to_target_term_id() { + $menu_id = wp_create_nav_menu( 'Services' ); + $this->assertNotInstanceOf( WP_Error::class, $menu_id ); + + $menu = wp_get_nav_menu_object( $menu_id ); + $target = (int) $menu->term_id; + $this->assertNotSame( 999, $target, 'Freshly created menu should not collide with the stale stub id.' ); + + $payload = array( + 'footer-two-widgets' => array( + 'nav_menu-2' => array( + 'title' => 'Services', + 'nav_menu' => 999, + '_ti_nav_menu_slug' => $menu->slug, + ), + ), + ); + + $importer = new Widgets_Importer(); + $result = $importer->actually_import( $payload ); + + $this->assertNotInstanceOf( WP_Error::class, $result ); + + $stored = get_option( 'widget_nav_menu' ); + $this->assertIsArray( $stored ); + + $instance = null; + foreach ( $stored as $key => $value ) { + if ( '_multiwidget' === $key ) { + continue; + } + if ( isset( $value['title'] ) && 'Services' === $value['title'] ) { + $instance = $value; + break; + } + } + + $this->assertNotNull( $instance, 'Imported nav_menu widget instance should be present.' ); + $this->assertSame( $target, (int) $instance['nav_menu'], 'Resolver should rewrite nav_menu to the target term_id.' ); + $this->assertArrayNotHasKey( '_ti_nav_menu_slug', $instance, 'Slug hint should be stripped before persist.' ); + } + + /** + * When the hinted slug does not match any existing menu on the + * target site, the importer must leave the original `nav_menu` + * value intact (non-fatal fallback) and still strip the hint. + */ + public function test_nav_menu_widget_unresolvable_slug_keeps_stale_id() { + $payload = array( + 'footer-two-widgets' => array( + 'nav_menu-2' => array( + 'title' => 'Services', + 'nav_menu' => 999, + '_ti_nav_menu_slug' => 'does-not-exist-on-target', + ), + ), + ); + + $importer = new Widgets_Importer(); + $result = $importer->actually_import( $payload ); + + $this->assertNotInstanceOf( WP_Error::class, $result ); + + $stored = get_option( 'widget_nav_menu' ); + $instance = null; + foreach ( $stored as $key => $value ) { + if ( '_multiwidget' === $key ) { + continue; + } + if ( isset( $value['title'] ) && 'Services' === $value['title'] ) { + $instance = $value; + break; + } + } + + $this->assertNotNull( $instance ); + $this->assertSame( 999, (int) $instance['nav_menu'], 'Unresolved slug should leave the stale id alone.' ); + $this->assertArrayNotHasKey( '_ti_nav_menu_slug', $instance, 'Hint is always stripped, even on failure.' ); + } + + /** + * The `ti_tpc_widget_pre_import` filter must be invoked after the + * built-in resolver and be able to mutate the widget instance. + */ + public function test_ti_tpc_widget_pre_import_filter_runs() { + $captured = array(); + $filter = function ( $widget, $id_base, $instance_id, $sidebar_id ) use ( &$captured ) { + $captured[] = compact( 'widget', 'id_base', 'instance_id', 'sidebar_id' ); + $widget['title'] = 'Filtered'; + return $widget; + }; + add_filter( 'ti_tpc_widget_pre_import', $filter, 10, 4 ); + + $payload = array( + 'footer-two-widgets' => array( + 'nav_menu-2' => array( + 'title' => 'Services', + 'nav_menu' => 0, + ), + ), + ); + + $importer = new Widgets_Importer(); + $importer->actually_import( $payload ); + + remove_filter( 'ti_tpc_widget_pre_import', $filter, 10 ); + + $this->assertCount( 1, $captured ); + $this->assertSame( 'nav_menu', $captured[0]['id_base'] ); + $this->assertSame( 'nav_menu-2', $captured[0]['instance_id'] ); + $this->assertSame( 'footer-two-widgets', $captured[0]['sidebar_id'] ); + + $stored = get_option( 'widget_nav_menu' ); + $titles = array_column( array_filter( $stored, 'is_array' ), 'title' ); + $this->assertContains( 'Filtered', $titles, 'Filter mutation should be persisted.' ); + } +} From 71ed2bd6774ed4cdca8ee4afaeee0fc2b02452c1 Mon Sep 17 00:00:00 2001 From: selul Date: Thu, 23 Apr 2026 13:46:12 +0300 Subject: [PATCH 3/3] feat(onboarding): support TI_ONBOARDING_DEFAULT_SITE redirect When the TI_ONBOARDING_DEFAULT_SITE constant is defined, the post-activation redirect now lands on the preview/import screen for that site (admin.php?page=neve-onboarding&site=) instead of the default search/welcome screen. The onboarding store resolves the ?site= query param against the bootstrapped sites list (across all builders) and starts at step 3 with currentSite preset; if the slug is unknown the app falls back to the existing search screen. This lets distributions and embedded environments (e.g. WordPress Playground) deep-link a user straight to the import flow for a chosen starter site. Made-with: Cursor --- includes/Admin.php | 19 ++++++++++--------- onboarding/src/store/reducer.js | 21 +++++++++++++++++++-- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/includes/Admin.php b/includes/Admin.php index ec4b873a..46e4aa21 100755 --- a/includes/Admin.php +++ b/includes/Admin.php @@ -398,15 +398,16 @@ public function activation_redirect() { } delete_option( 'tpc_maybe_run_onboarding' ); - wp_safe_redirect( - add_query_arg( - array( - 'page' => 'neve-onboarding', - 'show' => 'welcome', - ), - admin_url( 'admin.php' ) - ) - ); + + $query_args = array( 'page' => 'neve-onboarding' ); + + if ( defined( 'TI_ONBOARDING_DEFAULT_SITE' ) && TI_ONBOARDING_DEFAULT_SITE ) { + $query_args['site'] = sanitize_key( TI_ONBOARDING_DEFAULT_SITE ); + } else { + $query_args['show'] = 'welcome'; + } + + wp_safe_redirect( add_query_arg( $query_args, admin_url( 'admin.php' ) ) ); exit(); } diff --git a/onboarding/src/store/reducer.js b/onboarding/src/store/reducer.js index 051ac6cf..3c16f323 100644 --- a/onboarding/src/store/reducer.js +++ b/onboarding/src/store/reducer.js @@ -16,15 +16,32 @@ const initialLicense = licenseTIOB || { tier: 0, }; +const params = new URLSearchParams( window.location.search ); +const defaultSiteSlug = params.get( 'site' ); + +const findSiteBySlug = ( slug ) => { + const builders = onboarding?.sites?.sites || {}; + for ( const builder of Object.keys( builders ) ) { + if ( builders[ builder ]?.[ slug ] ) { + return builders[ builder ][ slug ]; + } + } + return null; +}; + +const defaultSite = defaultSiteSlug ? findSiteBySlug( defaultSiteSlug ) : null; + const initialState = { sites: onboarding.sites || {}, editor: selectedEditor, category: '', - currentSite: null, + currentSite: defaultSite, fetching: false, searchQuery: '', license: initialLicense, - onboardingStep: window.location.search.includes('show=welcome') ? 1 : 2, + onboardingStep: defaultSite + ? 3 + : ( window.location.search.includes('show=welcome') ? 1 : 2 ), userCustomSettings: { siteName: null, siteLogo: null,