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 f18ec29ee..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,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 field types and defaults. * * @since 2.0.0 + * 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 */ public function get_attributes_from_fields($element) { - $fields = $element->fields(); - $defaults = $element->defaults(); + $fields = $element->fields(); $_fields = []; - foreach ($fields as $field_id => $field) { - $type = 'string'; + foreach ($defaults as $field_id => $default_value) { + $type = $fields[ $field_id ]['type'] ?? 'string'; - if ('toggle' === $field['type']) { + if ('toggle' === $type) { $type = 'boolean'; } - if ('number' === $field['type']) { + if ('number' === $type) { $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..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 @@ -116,4 +116,58 @@ 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, + 'columns' => 4, + 'site_manage_type' => 'default', + ] + ); + + $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' => [ + 'default' => true, + 'type' => 'boolean', + ], + 'columns' => [ + 'default' => 4, + 'type' => 'integer', + ], + 'site_manage_type' => [ + 'default' => 'default', + 'type' => 'select', + ], + ], + $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..1e0127119 100644 --- a/tests/WP_Ultimo/General_Compat_Test.php +++ b/tests/WP_Ultimo/General_Compat_Test.php @@ -47,6 +47,38 @@ 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'); + $screen = get_current_screen(); + + $screen->is_block_editor(true); + + 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. */