Skip to content

Commit 4f5476c

Browse files
authored
release: fixes
- Menus and widgets are now matched more reliably, reducing the chance of things breaking or linking incorrectly. - Updated dependencies
2 parents 0b63023 + 42ca3d8 commit 4f5476c

5 files changed

Lines changed: 260 additions & 20 deletions

File tree

composer.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

includes/Admin.php

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -398,15 +398,16 @@ public function activation_redirect() {
398398
}
399399

400400
delete_option( 'tpc_maybe_run_onboarding' );
401-
wp_safe_redirect(
402-
add_query_arg(
403-
array(
404-
'page' => 'neve-onboarding',
405-
'show' => 'welcome',
406-
),
407-
admin_url( 'admin.php' )
408-
)
409-
);
401+
402+
$query_args = array( 'page' => 'neve-onboarding' );
403+
404+
if ( defined( 'TI_ONBOARDING_DEFAULT_SITE' ) && TI_ONBOARDING_DEFAULT_SITE ) {
405+
$query_args['site'] = sanitize_key( TI_ONBOARDING_DEFAULT_SITE );
406+
} else {
407+
$query_args['show'] = 'welcome';
408+
}
409+
410+
wp_safe_redirect( add_query_arg( $query_args, admin_url( 'admin.php' ) ) );
410411
exit();
411412
}
412413

includes/Importers/Widgets_Importer.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,14 @@ public function actually_import( $data ) {
136136
$widget = json_decode( wp_json_encode( $widget ), true );
137137
$widget = Slug_Mapping::rewrite_value( $widget );
138138

139+
// [dde-patch v1] pre-import widget filter + nav_menu slug resolver.
140+
// Resolve ID-by-reference fields (menus, etc.) using slug hints carried
141+
// in the widget instance, then let third parties hook in before persist.
142+
$widget = $this->resolve_known_references( $widget, $id_base );
143+
// Filters a widget instance right before it is persisted. Receives
144+
// ($widget, $id_base, $widget_instance_id, $sidebar_id).
145+
$widget = apply_filters( 'ti_tpc_widget_pre_import', $widget, $id_base, $widget_instance_id, $sidebar_id );
146+
139147
// Does widget with identical settings already exist in same sidebar?
140148
if ( ! $fail && isset( $widget_instances[ $id_base ] ) ) {
141149

@@ -266,6 +274,41 @@ public function available_widgets() {
266274

267275
}
268276

277+
/**
278+
* Resolve known ID-by-reference fields on a widget instance using
279+
* slug hints carried in the exported payload.
280+
*
281+
* Currently handles:
282+
* - `nav_menu` widget: reads `_ti_nav_menu_slug`, looks up the menu on
283+
* the target site, rewrites `nav_menu` to the fresh term_id, and
284+
* strips the hint so it never persists into `widget_nav_menu`.
285+
*
286+
* Behaviour on failure is intentionally non-fatal - if the slug does
287+
* not resolve (menu import failed) we leave the stale id in place so
288+
* the frontend renders an empty menu widget just like before this
289+
* patch, rather than throwing and aborting the whole import.
290+
*
291+
* @param array $widget Widget instance.
292+
* @param string $id_base Widget id_base (e.g. `nav_menu`).
293+
*
294+
* @return array
295+
*/
296+
private function resolve_known_references( $widget, $id_base ) {
297+
if ( ! is_array( $widget ) ) {
298+
return $widget;
299+
}
300+
301+
if ( 'nav_menu' === $id_base && ! empty( $widget['_ti_nav_menu_slug'] ) ) {
302+
$menu = wp_get_nav_menu_object( $widget['_ti_nav_menu_slug'] );
303+
if ( $menu && ! is_wp_error( $menu ) ) {
304+
$widget['nav_menu'] = (int) $menu->term_id;
305+
}
306+
unset( $widget['_ti_nav_menu_slug'] );
307+
}
308+
309+
return $widget;
310+
}
311+
269312
/**
270313
* Moves widgets to inactive widgets.
271314
*/

onboarding/src/store/reducer.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,32 @@ const initialLicense = licenseTIOB || {
1616
tier: 0,
1717
};
1818

19+
const params = new URLSearchParams( window.location.search );
20+
const defaultSiteSlug = params.get( 'site' );
21+
22+
const findSiteBySlug = ( slug ) => {
23+
const builders = onboarding?.sites?.sites || {};
24+
for ( const builder of Object.keys( builders ) ) {
25+
if ( builders[ builder ]?.[ slug ] ) {
26+
return builders[ builder ][ slug ];
27+
}
28+
}
29+
return null;
30+
};
31+
32+
const defaultSite = defaultSiteSlug ? findSiteBySlug( defaultSiteSlug ) : null;
33+
1934
const initialState = {
2035
sites: onboarding.sites || {},
2136
editor: selectedEditor,
2237
category: '',
23-
currentSite: null,
38+
currentSite: defaultSite,
2439
fetching: false,
2540
searchQuery: '',
2641
license: initialLicense,
27-
onboardingStep: window.location.search.includes('show=welcome') ? 1 : 2,
42+
onboardingStep: defaultSite
43+
? 3
44+
: ( window.location.search.includes('show=welcome') ? 1 : 2 ),
2845
userCustomSettings: {
2946
siteName: null,
3047
siteLogo: null,

tests/widgets-import-test.php

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
/**
3+
* Widgets importer test cases.
4+
*
5+
* Covers the dde-patch v1 slug resolver that rewrites the
6+
* `nav_menu` widget setting from a source-site term_id to the
7+
* fresh term_id on the target site based on a `_ti_nav_menu_slug`
8+
* hint carried in the exported payload.
9+
*
10+
* @package templates-patterns-collection
11+
*/
12+
13+
use TIOB\Importers\Widgets_Importer;
14+
15+
/**
16+
* Class Widgets_Import_Test
17+
*/
18+
class Widgets_Import_Test extends WP_UnitTestCase {
19+
20+
/**
21+
* Register the Neve-style footer sidebar used by the Beauty demo so
22+
* the importer treats it as a known sidebar and does not park
23+
* widgets in `wp_inactive_widgets`.
24+
*/
25+
public function setUp(): void {
26+
parent::setUp();
27+
28+
register_sidebar(
29+
array(
30+
'name' => 'Footer Two',
31+
'id' => 'footer-two-widgets',
32+
)
33+
);
34+
35+
// Make sure the classic nav_menu widget is registered so the
36+
// importer's available_widgets() check passes.
37+
if ( ! isset( $GLOBALS['wp_registered_widget_controls']['nav_menu-1'] ) ) {
38+
wp_widgets_init();
39+
}
40+
41+
// Start from a known-clean widget state.
42+
update_option( 'widget_nav_menu', array( '_multiwidget' => 1 ) );
43+
update_option(
44+
'sidebars_widgets',
45+
array(
46+
'wp_inactive_widgets' => array(),
47+
'footer-two-widgets' => array(),
48+
)
49+
);
50+
}
51+
52+
public function tearDown(): void {
53+
delete_option( 'widget_nav_menu' );
54+
delete_option( 'sidebars_widgets' );
55+
parent::tearDown();
56+
}
57+
58+
/**
59+
* When a nav_menu widget carries `_ti_nav_menu_slug`, the importer
60+
* must rewrite `nav_menu` to the term_id of the menu that matches
61+
* that slug on the current site and strip the hint before persist.
62+
*/
63+
public function test_nav_menu_widget_slug_is_resolved_to_target_term_id() {
64+
$menu_id = wp_create_nav_menu( 'Services' );
65+
$this->assertNotInstanceOf( WP_Error::class, $menu_id );
66+
67+
$menu = wp_get_nav_menu_object( $menu_id );
68+
$target = (int) $menu->term_id;
69+
$this->assertNotSame( 999, $target, 'Freshly created menu should not collide with the stale stub id.' );
70+
71+
$payload = array(
72+
'footer-two-widgets' => array(
73+
'nav_menu-2' => array(
74+
'title' => 'Services',
75+
'nav_menu' => 999,
76+
'_ti_nav_menu_slug' => $menu->slug,
77+
),
78+
),
79+
);
80+
81+
$importer = new Widgets_Importer();
82+
$result = $importer->actually_import( $payload );
83+
84+
$this->assertNotInstanceOf( WP_Error::class, $result );
85+
86+
$stored = get_option( 'widget_nav_menu' );
87+
$this->assertIsArray( $stored );
88+
89+
$instance = null;
90+
foreach ( $stored as $key => $value ) {
91+
if ( '_multiwidget' === $key ) {
92+
continue;
93+
}
94+
if ( isset( $value['title'] ) && 'Services' === $value['title'] ) {
95+
$instance = $value;
96+
break;
97+
}
98+
}
99+
100+
$this->assertNotNull( $instance, 'Imported nav_menu widget instance should be present.' );
101+
$this->assertSame( $target, (int) $instance['nav_menu'], 'Resolver should rewrite nav_menu to the target term_id.' );
102+
$this->assertArrayNotHasKey( '_ti_nav_menu_slug', $instance, 'Slug hint should be stripped before persist.' );
103+
}
104+
105+
/**
106+
* When the hinted slug does not match any existing menu on the
107+
* target site, the importer must leave the original `nav_menu`
108+
* value intact (non-fatal fallback) and still strip the hint.
109+
*/
110+
public function test_nav_menu_widget_unresolvable_slug_keeps_stale_id() {
111+
$payload = array(
112+
'footer-two-widgets' => array(
113+
'nav_menu-2' => array(
114+
'title' => 'Services',
115+
'nav_menu' => 999,
116+
'_ti_nav_menu_slug' => 'does-not-exist-on-target',
117+
),
118+
),
119+
);
120+
121+
$importer = new Widgets_Importer();
122+
$result = $importer->actually_import( $payload );
123+
124+
$this->assertNotInstanceOf( WP_Error::class, $result );
125+
126+
$stored = get_option( 'widget_nav_menu' );
127+
$instance = null;
128+
foreach ( $stored as $key => $value ) {
129+
if ( '_multiwidget' === $key ) {
130+
continue;
131+
}
132+
if ( isset( $value['title'] ) && 'Services' === $value['title'] ) {
133+
$instance = $value;
134+
break;
135+
}
136+
}
137+
138+
$this->assertNotNull( $instance );
139+
$this->assertSame( 999, (int) $instance['nav_menu'], 'Unresolved slug should leave the stale id alone.' );
140+
$this->assertArrayNotHasKey( '_ti_nav_menu_slug', $instance, 'Hint is always stripped, even on failure.' );
141+
}
142+
143+
/**
144+
* The `ti_tpc_widget_pre_import` filter must be invoked after the
145+
* built-in resolver and be able to mutate the widget instance.
146+
*/
147+
public function test_ti_tpc_widget_pre_import_filter_runs() {
148+
$captured = array();
149+
$filter = function ( $widget, $id_base, $instance_id, $sidebar_id ) use ( &$captured ) {
150+
$captured[] = compact( 'widget', 'id_base', 'instance_id', 'sidebar_id' );
151+
$widget['title'] = 'Filtered';
152+
return $widget;
153+
};
154+
add_filter( 'ti_tpc_widget_pre_import', $filter, 10, 4 );
155+
156+
$payload = array(
157+
'footer-two-widgets' => array(
158+
'nav_menu-2' => array(
159+
'title' => 'Services',
160+
'nav_menu' => 0,
161+
),
162+
),
163+
);
164+
165+
$importer = new Widgets_Importer();
166+
$importer->actually_import( $payload );
167+
168+
remove_filter( 'ti_tpc_widget_pre_import', $filter, 10 );
169+
170+
$this->assertCount( 1, $captured );
171+
$this->assertSame( 'nav_menu', $captured[0]['id_base'] );
172+
$this->assertSame( 'nav_menu-2', $captured[0]['instance_id'] );
173+
$this->assertSame( 'footer-two-widgets', $captured[0]['sidebar_id'] );
174+
175+
$stored = get_option( 'widget_nav_menu' );
176+
$titles = array_column( array_filter( $stored, 'is_array' ), 'title' );
177+
$this->assertContains( 'Filtered', $titles, 'Filter mutation should be persisted.' );
178+
}
179+
}

0 commit comments

Comments
 (0)