From 18ccc255f2f664849768b365ddfdc7b7e4730a08 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 13 Aug 2026 15:52:54 -0600 Subject: [PATCH 1/3] perf: defer Gutenberg work outside editors --- .../class-block-editor-widget-manager.php | 17 +++++----- inc/compat/class-gutenberg-support.php | 6 ++++ inc/functions/pages.php | 28 +++++++++++++++ inc/ui/class-current-site-element.php | 16 +-------- inc/ui/class-my-sites-element.php | 16 +-------- inc/ui/class-site-actions-element.php | 16 +-------- .../Block_Editor_Widget_Manager_Test.php | 28 +++++++++++++++ .../Functions/Pages_Functions_Test.php | 21 ++++++++++++ tests/WP_Ultimo/General_Compat_Test.php | 34 +++++++++++++++++++ 9 files changed, 128 insertions(+), 54 deletions(-) diff --git a/inc/builders/block-editor/class-block-editor-widget-manager.php b/inc/builders/block-editor/class-block-editor-widget-manager.php index f18ec29ee..d3ffb82eb 100644 --- a/inc/builders/block-editor/class-block-editor-widget-manager.php +++ b/inc/builders/block-editor/class-block-editor-widget-manager.php @@ -198,35 +198,34 @@ public function load_block_settings($blocks, $element) { } /** - * Generates the list of attributes supported based on the fields. + * Generates the list of attributes supported based on element defaults. * * @since 2.0.0 + * This deliberately avoids fields(), whose option providers only belong to + * the block editor settings payload. + * * @param \WP_Ultimo\UI\Base_Element $element The element being registered. * @return array */ public function get_attributes_from_fields($element) { - $fields = $element->fields(); - $defaults = $element->defaults(); $_fields = []; - foreach ($fields as $field_id => $field) { + foreach ($defaults as $field_id => $default_value) { $type = 'string'; - if ('toggle' === $field['type']) { + if (is_bool($default_value)) { $type = 'boolean'; } - if ('number' === $field['type']) { + if (is_int($default_value)) { $type = 'integer'; } - $default_value = wu_get_isset($defaults, $field_id, ''); - $_fields[ $field_id ] = [ - 'default' => wu_get_isset($field, 'value', $default_value), + 'default' => $default_value, 'type' => $type, ]; } diff --git a/inc/compat/class-gutenberg-support.php b/inc/compat/class-gutenberg-support.php index f9a2e5c73..df8c4b6ff 100644 --- a/inc/compat/class-gutenberg-support.php +++ b/inc/compat/class-gutenberg-support.php @@ -62,6 +62,12 @@ public function init(): void { */ public function add_scripts(): void { + $screen = get_current_screen(); + + if ( ! $screen || ! $screen->is_block_editor()) { + return; + } + wp_register_script('wu-gutenberg-support', wu_get_asset('gutenberg-support.js', 'js'), ['jquery'], wu_get_version(), true); // translators: the placeholder is replaced with the network name. diff --git a/inc/functions/pages.php b/inc/functions/pages.php index 85478fa2b..15f7c8170 100644 --- a/inc/functions/pages.php +++ b/inc/functions/pages.php @@ -107,6 +107,34 @@ function wu_is_new_site_page() { return absint(wu_get_setting('default_new_site_page', 0)) === $post->ID; } +/** + * Returns a request-memoized list of pages for select field options. + * + * @since 2.0.0 + * @param string $default_label The label for the current page option. + * @return array + */ +function wu_get_pages_as_options($default_label) { + + static $pages = null; + + if (null === $pages) { + $pages = get_pages( + [ + 'exclude' => [get_the_ID()], // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude + ] + ) ?: []; + } + + $pages_list = [0 => $default_label]; + + foreach ($pages as $page) { + $pages_list[ $page->ID ] = $page->post_title; + } + + return $pages_list; +} + /** * Checks if the current page is a login page. * diff --git a/inc/ui/class-current-site-element.php b/inc/ui/class-current-site-element.php index 197ed7c77..73603b5a8 100644 --- a/inc/ui/class-current-site-element.php +++ b/inc/ui/class-current-site-element.php @@ -162,26 +162,12 @@ public function fields() { 'value' => 1, ]; - $pages = get_pages( - [ - 'exclude' => [get_the_ID()], // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude - ] - ); - - $pages = $pages ?: []; - - $pages_list = [0 => __('Current Page', 'ultimate-multisite')]; - - foreach ($pages as $page) { - $pages_list[ $page->ID ] = $page->post_title; - } - $fields['breadcrumbs_my_sites_page'] = [ 'type' => 'select', 'title' => __('My Sites Page', 'ultimate-multisite'), 'value' => 0, 'desc' => __('The page with the customer sites list.', 'ultimate-multisite'), - 'options' => $pages_list, + 'options' => fn() => wu_get_pages_as_options(__('Current Page', 'ultimate-multisite')), ]; $fields['display_description'] = [ diff --git a/inc/ui/class-my-sites-element.php b/inc/ui/class-my-sites-element.php index b42f125b4..22de4d43f 100644 --- a/inc/ui/class-my-sites-element.php +++ b/inc/ui/class-my-sites-element.php @@ -162,20 +162,6 @@ public function fields() { ], ]; - $pages = get_pages( - [ - 'exclude' => [get_the_ID()], // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude - ] - ); - - $pages = $pages ?: []; - - $pages_list = [0 => __('Current Page', 'ultimate-multisite')]; - - foreach ($pages as $page) { - $pages_list[ $page->ID ] = $page->post_title; - } - $fields['custom_manage_page'] = [ 'type' => 'select', 'title' => __('Manage Redirect Page', 'ultimate-multisite'), @@ -185,7 +171,7 @@ public function fields() { 'required' => [ 'site_manage_type' => 'custom_page', ], - 'options' => $pages_list, + 'options' => fn() => wu_get_pages_as_options(__('Current Page', 'ultimate-multisite')), ]; $fields['columns'] = [ diff --git a/inc/ui/class-site-actions-element.php b/inc/ui/class-site-actions-element.php index 62d361f44..9d0c9dcc4 100644 --- a/inc/ui/class-site-actions-element.php +++ b/inc/ui/class-site-actions-element.php @@ -182,27 +182,13 @@ public function fields() { 'value' => 1, ]; - $pages = get_pages( - [ - 'exclude' => [get_the_ID()], // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude - ] - ); - - $pages = $pages ?: []; - - $pages_list = [0 => __('Default', 'ultimate-multisite')]; - - foreach ($pages as $page) { - $pages_list[ $page->ID ] = $page->post_title; - } - $fields['redirect_after_delete'] = [ 'type' => 'select', 'title' => __('Redirect After Delete', 'ultimate-multisite'), 'value' => 0, 'desc' => __('The page to redirect user after delete current site.', 'ultimate-multisite'), 'tooltip' => '', - 'options' => $pages_list, + 'options' => fn() => wu_get_pages_as_options(__('Default', 'ultimate-multisite')), ]; return $fields; diff --git a/tests/WP_Ultimo/Builders/Block_Editor/Block_Editor_Widget_Manager_Test.php b/tests/WP_Ultimo/Builders/Block_Editor/Block_Editor_Widget_Manager_Test.php index c6ddaf3e2..6c01307a4 100644 --- a/tests/WP_Ultimo/Builders/Block_Editor/Block_Editor_Widget_Manager_Test.php +++ b/tests/WP_Ultimo/Builders/Block_Editor/Block_Editor_Widget_Manager_Test.php @@ -116,4 +116,32 @@ public function test_is_block_preview_passes_through_outside_rest(): void { $this->assertFalse($result, 'Should return false when not in REST edit context.'); } + + /** + * Test attributes use defaults without evaluating field option providers. + */ + public function test_get_attributes_from_fields_does_not_evaluate_field_options(): void { + + $element = $this->getMockBuilder('\WP_Ultimo\UI\Simple_Text_Element') + ->disableOriginalConstructor() + ->onlyMethods(['defaults', 'fields']) + ->getMock(); + + $element->expects($this->once()) + ->method('defaults') + ->willReturn(['enabled' => true]); + + $element->expects($this->never()) + ->method('fields'); + + $this->assertSame( + [ + 'enabled' => [ + 'default' => true, + 'type' => 'boolean', + ], + ], + $this->manager->get_attributes_from_fields($element) + ); + } } diff --git a/tests/WP_Ultimo/Functions/Pages_Functions_Test.php b/tests/WP_Ultimo/Functions/Pages_Functions_Test.php index 43b0d00cd..5a5b8d349 100644 --- a/tests/WP_Ultimo/Functions/Pages_Functions_Test.php +++ b/tests/WP_Ultimo/Functions/Pages_Functions_Test.php @@ -75,6 +75,27 @@ public function test_is_new_site_page_false_without_post(): void { $this->assertFalse($result); } + /** + * Test page options are memoized per request with context-specific defaults. + */ + public function test_get_pages_as_options_memoizes_pages(): void { + + $page_id = self::factory()->post->create( + [ + 'post_type' => 'page', + 'post_title' => 'Example page', + ] + ); + + $first = wu_get_pages_as_options('Current Page'); + $second = wu_get_pages_as_options('Default'); + + $this->assertSame('Current Page', $first[0]); + $this->assertSame('Default', $second[0]); + $this->assertSame('Example page', $first[ $page_id ]); + $this->assertSame($first[ $page_id ], $second[ $page_id ]); + } + /** * Test wu_is_login_page returns bool. */ diff --git a/tests/WP_Ultimo/General_Compat_Test.php b/tests/WP_Ultimo/General_Compat_Test.php index 0655e8b6d..9094a7f7b 100644 --- a/tests/WP_Ultimo/General_Compat_Test.php +++ b/tests/WP_Ultimo/General_Compat_Test.php @@ -47,6 +47,40 @@ public function test_init_registers_divi_cache_purge_hook(): void { $this->assertNotFalse(has_action('wu_duplicate_site', [$instance, 'clear_divi_static_css_cache'])); } + /** + * Test Gutenberg support skips ordinary admin screens. + */ + public function test_gutenberg_support_skips_ordinary_admin_screens(): void { + + set_current_screen('dashboard'); + wp_deregister_script('wu-gutenberg-support'); + + Gutenberg_Support::get_instance()->add_scripts(); + + $this->assertFalse(wp_script_is('wu-gutenberg-support', 'registered')); + $this->assertFalse(wp_script_is('wu-gutenberg-support', 'enqueued')); + } + + /** + * Test Gutenberg support enqueues and localizes assets in the block editor. + */ + public function test_gutenberg_support_loads_on_block_editor_screens(): void { + + set_current_screen('post-post'); + $screen = get_current_screen(); + + if ( ! $screen || ! method_exists($screen, 'is_block_editor') || ! $screen->is_block_editor()) { + $this->markTestSkipped('The installed WordPress version does not support block editor screens.'); + } + + wp_deregister_script('wu-gutenberg-support'); + + Gutenberg_Support::get_instance()->add_scripts(); + + $this->assertTrue(wp_script_is('wu-gutenberg-support', 'enqueued')); + $this->assertNotEmpty(wp_scripts()->get_data('wu-gutenberg-support', 'data')); + } + /** * Test Divi et-cache files are deleted only for the cloned site. */ From 7e9a4aead5a991854405bced1644848dfac15a28 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 13 Aug 2026 16:24:08 -0600 Subject: [PATCH 2/3] test: exercise Gutenberg block editor screen --- tests/WP_Ultimo/General_Compat_Test.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/WP_Ultimo/General_Compat_Test.php b/tests/WP_Ultimo/General_Compat_Test.php index 9094a7f7b..1e0127119 100644 --- a/tests/WP_Ultimo/General_Compat_Test.php +++ b/tests/WP_Ultimo/General_Compat_Test.php @@ -66,12 +66,10 @@ public function test_gutenberg_support_skips_ordinary_admin_screens(): void { */ public function test_gutenberg_support_loads_on_block_editor_screens(): void { - set_current_screen('post-post'); + set_current_screen('post'); $screen = get_current_screen(); - if ( ! $screen || ! method_exists($screen, 'is_block_editor') || ! $screen->is_block_editor()) { - $this->markTestSkipped('The installed WordPress version does not support block editor screens.'); - } + $screen->is_block_editor(true); wp_deregister_script('wu-gutenberg-support'); From 1c9e2f9e5adbda55f1537ae64688b06727cac5d8 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 13 Aug 2026 17:58:56 -0600 Subject: [PATCH 3/3] wip: preserve block attribute types --- .../class-shortcodes-admin-page.php | 8 ++++- .../class-block-editor-widget-manager.php | 12 +++---- .../Block_Editor_Widget_Manager_Test.php | 34 ++++++++++++++++--- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/inc/admin-pages/class-shortcodes-admin-page.php b/inc/admin-pages/class-shortcodes-admin-page.php index e29466751..b98dc6eaa 100644 --- a/inc/admin-pages/class-shortcodes-admin-page.php +++ b/inc/admin-pages/class-shortcodes-admin-page.php @@ -151,7 +151,13 @@ public function get_data() { $params[ $key ]['options'] = '0 | 1'; break; case 'select': - $params[ $key ]['options'] = implode(' | ', array_keys(wu_get_isset($value, 'options', []))); + $options = wu_get_isset($value, 'options', []); + + if (is_callable($options)) { + $options = call_user_func($options); + } + + $params[ $key ]['options'] = implode(' | ', array_keys($options)); break; case 'int': $params[ $key ]['options'] = __('integer', 'ultimate-multisite'); diff --git a/inc/builders/block-editor/class-block-editor-widget-manager.php b/inc/builders/block-editor/class-block-editor-widget-manager.php index d3ffb82eb..b640c108c 100644 --- a/inc/builders/block-editor/class-block-editor-widget-manager.php +++ b/inc/builders/block-editor/class-block-editor-widget-manager.php @@ -198,11 +198,10 @@ public function load_block_settings($blocks, $element) { } /** - * Generates the list of attributes supported based on element defaults. + * Generates the list of attributes supported based on element field types and defaults. * * @since 2.0.0 - * This deliberately avoids fields(), whose option providers only belong to - * the block editor settings payload. + * Field definitions are read as metadata only; option providers are not evaluated. * * @param \WP_Ultimo\UI\Base_Element $element The element being registered. * @return array @@ -210,17 +209,18 @@ public function load_block_settings($blocks, $element) { public function get_attributes_from_fields($element) { $defaults = $element->defaults(); + $fields = $element->fields(); $_fields = []; foreach ($defaults as $field_id => $default_value) { - $type = 'string'; + $type = $fields[ $field_id ]['type'] ?? 'string'; - if (is_bool($default_value)) { + if ('toggle' === $type) { $type = 'boolean'; } - if (is_int($default_value)) { + if ('number' === $type) { $type = 'integer'; } diff --git a/tests/WP_Ultimo/Builders/Block_Editor/Block_Editor_Widget_Manager_Test.php b/tests/WP_Ultimo/Builders/Block_Editor/Block_Editor_Widget_Manager_Test.php index 6c01307a4..7a465702e 100644 --- a/tests/WP_Ultimo/Builders/Block_Editor/Block_Editor_Widget_Manager_Test.php +++ b/tests/WP_Ultimo/Builders/Block_Editor/Block_Editor_Widget_Manager_Test.php @@ -129,17 +129,43 @@ public function test_get_attributes_from_fields_does_not_evaluate_field_options( $element->expects($this->once()) ->method('defaults') - ->willReturn(['enabled' => true]); + ->willReturn( + [ + 'enabled' => true, + 'columns' => 4, + 'site_manage_type' => 'default', + ] + ); - $element->expects($this->never()) - ->method('fields'); + $element->expects($this->once()) + ->method('fields') + ->willReturn( + [ + 'enabled' => [ + 'type' => 'toggle', + 'options' => static function () { + throw new \RuntimeException('Block attribute registration must not evaluate options.'); + }, + ], + 'columns' => ['type' => 'number'], + 'site_manage_type' => ['type' => 'select'], + ] + ); $this->assertSame( [ - 'enabled' => [ + 'enabled' => [ 'default' => true, 'type' => 'boolean', ], + 'columns' => [ + 'default' => 4, + 'type' => 'integer', + ], + 'site_manage_type' => [ + 'default' => 'default', + 'type' => 'select', + ], ], $this->manager->get_attributes_from_fields($element) );