diff --git a/core/lib/Drupal/Core/Config/Config.php b/core/lib/Drupal/Core/Config/Config.php index 584feb7c53..91d7695006 100644 --- a/core/lib/Drupal/Core/Config/Config.php +++ b/core/lib/Drupal/Core/Config/Config.php @@ -303,4 +303,28 @@ public function getOriginal($key = '', $apply_overrides = TRUE) { } } + /** + * Determines if overrides are applied to a key for this configuration object. + * + * @param string $key + * A string that maps to a key within the configuration data. + * + * @return bool + * TRUE if there are any overrides for the key, otherwise FALSE. + */ + public function hasOverrides($key = '') { + if ($key) { + $parts = explode('.', $key); + $override_exists = FALSE; + if (isset($this->moduleOverrides) && is_array($this->moduleOverrides)) { + $override_exists = NestedArray::keyExists($this->moduleOverrides, $parts); + } + if (!$override_exists && isset($this->settingsOverrides) && is_array($this->settingsOverrides)) { + $override_exists = NestedArray::keyExists($this->settingsOverrides, $parts); + } + return $override_exists; + } + return !empty($this->moduleOverrides) || !empty($this->settingsOverrides); + } + } diff --git a/core/modules/settings_tray/js/settings_tray.es6.js b/core/modules/settings_tray/js/settings_tray.es6.js index 6487690c7b..ea8f570464 100644 --- a/core/modules/settings_tray/js/settings_tray.es6.js +++ b/core/modules/settings_tray/js/settings_tray.es6.js @@ -5,7 +5,7 @@ * @private */ -(($, Drupal) => { +(($, Drupal, { settings_tray: settings }) => { const blockConfigureSelector = '[data-settings-tray-edit]'; const toggleEditSelector = '[data-drupal-settingstray="toggle"]'; const itemsToToggleSelector = '[data-off-canvas-main-canvas], #toolbar-bar, [data-drupal-settingstray="editable"] a, [data-drupal-settingstray="editable"] button'; @@ -169,6 +169,16 @@ if (!instance.options.data.hasOwnProperty('dialogOptions')) { instance.options.data.dialogOptions = {}; } + + if (settings && settings.hasOwnProperty('overridden_blocks')) { + Object.keys(settings.overridden_blocks).forEach((blockId) => { + // @see Route entity.block.off_canvas_form + if (instance.options.url.indexOf(`/admin/structure/block/manage/${blockId}/off-canvas`) !== -1) { + instance.options.url = settings.overridden_blocks[blockId]; + } + }); + } + instance.options.data.dialogOptions.settingsTrayActiveEditableId = $(instance.element).parents('.settings-tray-editable').attr('id'); instance.progress = { type: 'fullscreen' }; }); @@ -258,4 +268,4 @@ } }, }); -})(jQuery, Drupal); +})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/settings_tray/js/settings_tray.js b/core/modules/settings_tray/js/settings_tray.js index 7a83e156ca..9d3da50afe 100644 --- a/core/modules/settings_tray/js/settings_tray.js +++ b/core/modules/settings_tray/js/settings_tray.js @@ -5,7 +5,9 @@ * @preserve **/ -(function ($, Drupal) { +(function ($, Drupal, _ref) { + var settings = _ref.settings_tray; + var blockConfigureSelector = '[data-settings-tray-edit]'; var toggleEditSelector = '[data-drupal-settingstray="toggle"]'; var itemsToToggleSelector = '[data-off-canvas-main-canvas], #toolbar-bar, [data-drupal-settingstray="editable"] a, [data-drupal-settingstray="editable"] button'; @@ -100,6 +102,15 @@ if (!instance.options.data.hasOwnProperty('dialogOptions')) { instance.options.data.dialogOptions = {}; } + + if (settings && settings.hasOwnProperty('overridden_blocks')) { + Object.keys(settings.overridden_blocks).forEach(function (blockId) { + if (instance.options.url.indexOf('/admin/structure/block/manage/' + blockId + '/off-canvas') !== -1) { + instance.options.url = settings.overridden_blocks[blockId]; + } + }); + } + instance.options.data.dialogOptions.settingsTrayActiveEditableId = $(instance.element).parents('.settings-tray-editable').attr('id'); instance.progress = { type: 'fullscreen' }; }); @@ -153,4 +164,4 @@ } } }); -})(jQuery, Drupal); \ No newline at end of file +})(jQuery, Drupal, drupalSettings); \ No newline at end of file diff --git a/core/modules/settings_tray/settings_tray.libraries.yml b/core/modules/settings_tray/settings_tray.libraries.yml index de5c82c78d..11b55a478f 100644 --- a/core/modules/settings_tray/settings_tray.libraries.yml +++ b/core/modules/settings_tray/settings_tray.libraries.yml @@ -17,3 +17,4 @@ drupal.settings_tray: - core/drupal - core/jquery.once - core/drupal.ajax + - core/drupalSettings diff --git a/core/modules/settings_tray/settings_tray.module b/core/modules/settings_tray/settings_tray.module index af04450459..2322e24caa 100644 --- a/core/modules/settings_tray/settings_tray.module +++ b/core/modules/settings_tray/settings_tray.module @@ -10,6 +10,9 @@ use Drupal\settings_tray\Block\BlockEntityOffCanvasForm; use Drupal\settings_tray\Form\SystemBrandingOffCanvasForm; use Drupal\settings_tray\Form\SystemMenuOffCanvasForm; +use Drupal\block\BlockInterface; +use Drupal\Core\Url; +use Drupal\Core\EventSubscriber\MainContentViewSubscriber; /** * Implements hook_help(). @@ -48,6 +51,21 @@ function settings_tray_contextual_links_view_alter(&$element, $items) { } } +/** + * Checks if a block has overrides. + * + * @param \Drupal\block\BlockInterface $block + * The block to check for overrides. + * + * @return bool + * TRUE if the block has overrides otherwise FALSE. + * + * @internal + */ +function _settings_tray_has_block_overrides(BlockInterface $block) { + return \Drupal::config($block->getEntityType()->getConfigPrefix() . '.' . $block->id())->hasOverrides(); +} + /** * Implements hook_block_view_alter(). */ @@ -58,6 +76,29 @@ function settings_tray_block_view_alter(array &$build) { $build['#contextual_links']['settings_tray'] = [ 'route_parameters' => [], ]; + // If a block currently has configuration overrides it cannot be edited in the + // Settings Tray form. + if (_settings_tray_has_block_overrides($build['#block'])) { + // The url query options should use the off-canvas dialog and add the + // current page destination. + $query = [ + MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_dialog.off_canvas', + ] + \Drupal::destination()->getAsArray(); + + $url = Url::fromRoute('settings_tray.overridden_block_warning') + ->setRouteParameter('block', $build['#block']->id()) + ->setOptions(['query' => $query]); + + // Store the URL to the overridden notice for this block in drupalSettings. + // This will be used in the Javascript function prepareAjaxLinks() to + // replace the URL to the Settings Tray edit form. We cannot use + // settings_tray_contextual_links_view_alter() to alter the URL because the + // contextual links will not be rebuilt for every context that could have + // configuration overrides. + $build['#attached']['drupalSettings']['settings_tray']['overridden_blocks'][$build['#block']->id()] = $url->toString(); + $generated_url = $url->toString(TRUE); + \Drupal::service('renderer')->addCacheableDependency($build, $generated_url); + } } /** diff --git a/core/modules/settings_tray/settings_tray.routing.yml b/core/modules/settings_tray/settings_tray.routing.yml index 01109e4c79..c2f4f7c140 100644 --- a/core/modules/settings_tray/settings_tray.routing.yml +++ b/core/modules/settings_tray/settings_tray.routing.yml @@ -6,3 +6,10 @@ entity.block.off_canvas_form: requirements: _permission: 'administer blocks' _access_block_plugin_has_settings_tray_form: 'TRUE' +settings_tray.overridden_block_warning: + path: '/admin/settings-tray-overridden/{block}' + defaults: + _controller: '\Drupal\settings_tray\Controller\OverriddenBlockConfig::overrideNotice' + _title_callback: '\Drupal\settings_tray\Block\BlockEntityOffCanvasForm::title' + requirements: + _permission: 'administer blocks' diff --git a/core/modules/settings_tray/src/Controller/OverriddenBlockConfig.php b/core/modules/settings_tray/src/Controller/OverriddenBlockConfig.php new file mode 100644 index 0000000000..fa47aa986c --- /dev/null +++ b/core/modules/settings_tray/src/Controller/OverriddenBlockConfig.php @@ -0,0 +1,48 @@ + [ + '#type' => 'markup', + '#markup' => '

' . $this->t('This block cannot be edited in the Settings Tray form because it has configuration overrides in effect.') . '

', + ], + 'link' => [ + '#type' => 'link', + '#title' => $this->t('Edit Block'), + '#url' => Url::fromRoute('entity.block.edit_form') + ->setRouteParameter('block', $block->id()) + ->setOption('query', ['destination' => \Drupal::request()->get('destination')]), + ], + ]; + + } + +} diff --git a/core/modules/settings_tray/tests/modules/settings_tray_override_test/settings_tray_override_test.info.yml b/core/modules/settings_tray/tests/modules/settings_tray_override_test/settings_tray_override_test.info.yml new file mode 100644 index 0000000000..89f9732feb --- /dev/null +++ b/core/modules/settings_tray/tests/modules/settings_tray_override_test/settings_tray_override_test.info.yml @@ -0,0 +1,7 @@ +name: 'Configuration override test for Settings Tray' +type: module +package: Testing +version: VERSION +core: 8.x +dependencies: + - settings_tray diff --git a/core/modules/settings_tray/tests/modules/settings_tray_override_test/settings_tray_override_test.services.yml b/core/modules/settings_tray/tests/modules/settings_tray_override_test/settings_tray_override_test.services.yml new file mode 100644 index 0000000000..b23035bde2 --- /dev/null +++ b/core/modules/settings_tray/tests/modules/settings_tray_override_test/settings_tray_override_test.services.yml @@ -0,0 +1,5 @@ +services: + settings_tray_override_test.overrider: + class: Drupal\settings_tray_override_test\ConfigOverrider + tags: + - { name: config.factory.override} diff --git a/core/modules/settings_tray/tests/modules/settings_tray_override_test/src/ConfigOverrider.php b/core/modules/settings_tray/tests/modules/settings_tray_override_test/src/ConfigOverrider.php new file mode 100644 index 0000000000..0184a9e533 --- /dev/null +++ b/core/modules/settings_tray/tests/modules/settings_tray_override_test/src/ConfigOverrider.php @@ -0,0 +1,48 @@ + ['settings' => ['label' => 'Now this will be the label.']]]; + } + return $overrides; + } + + /** + * {@inheritdoc} + */ + public function getCacheSuffix() { + return 'ConfigOverrider'; + } + + /** + * {@inheritdoc} + */ + public function createConfigObject($name, $collection = StorageInterface::DEFAULT_COLLECTION) { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getCacheableMetadata($name) { + return new CacheableMetadata(); + } + +} diff --git a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php index fcfecde400..014cea2dbb 100644 --- a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php +++ b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\settings_tray\FunctionalJavascript; +use Drupal\block\BlockInterface; use Drupal\block\Entity\Block; use Drupal\block_content\Entity\BlockContent; use Drupal\block_content\Entity\BlockContentType; @@ -75,7 +76,7 @@ public function testBlocks($theme, $block_plugin, $new_page_text, $element_selec $page = $this->getSession()->getPage(); $this->enableTheme($theme); $block = $this->placeBlock($block_plugin); - $block_selector = str_replace('_', '-', $this->getBlockSelector($block)); + $block_selector = $this->getBlockCssSelector($block); $block_id = $block->id(); $this->drupalGet('user'); @@ -267,8 +268,10 @@ protected function assertOffCanvasBlockFormIsValid() { * @param string $contextual_link_container * The element that contains the contextual links. If none provide the * $block_selector will be used. + * @param bool $confirm_form + * Determines if the block form should be confirmed. */ - protected function openBlockForm($block_selector, $contextual_link_container = '') { + protected function openBlockForm($block_selector, $contextual_link_container = '', $confirm_form = TRUE) { if (!$contextual_link_container) { $contextual_link_container = $block_selector; } @@ -283,7 +286,10 @@ protected function openBlockForm($block_selector, $contextual_link_container = ' $this->assertSession()->assertWaitOnAjaxRequest(); $this->click($block_selector); $this->waitForOffCanvasToOpen(); - $this->assertOffCanvasBlockFormIsValid(); + if ($confirm_form) { + $this->assertOffCanvasBlockFormIsValid(); + } + } /** @@ -321,7 +327,7 @@ public function testQuickEditLinks() { $this->enableTheme($theme); $block = $this->placeBlock($block_plugin); - $block_selector = str_replace('_', '-', $this->getBlockSelector($block)); + $block_selector = $this->getBlockCssSelector($block); // Load the same page twice. foreach ([1, 2] as $page_load_times) { $this->drupalGet('node/' . $node->id()); @@ -577,4 +583,52 @@ protected function getTestThemes() { }); } + /** + * Tests that the blocks with configuration overrides are disabled. + */ + public function testOverriddenDisabled() { + $web_assert = $this->assertSession(); + $overridden_text = 'This block cannot be edited in the Settings Tray form because it has configuration overrides in effect.'; + $this->container->get('module_installer')->install(['settings_tray_override_test']); + // Test a overridden block does not show the form in the off-canvas dialog. + // @see \Drupal\settings_tray_override_test\ConfigOverrider + $overridden_block = $this->placeBlock('system_powered_by_block', + [ + 'id' => 'overridden_block', + 'label_display' => 1, + 'label' => 'This will be overridden.', + ]); + $this->drupalGet('user'); + // Confirm the label is actually overridden. + $web_assert->elementContains('css', $this->getBlockCssSelector($overridden_block), 'Now this will be the label.'); + $this->enableEditMode(); + $this->openBlockForm($this->getBlockCssSelector($overridden_block), '', FALSE); + $web_assert->elementContains('css', self::OFF_CANVAS_CSS_SELECTOR, $overridden_text); + + // Test a non-overridden block does show the form in the off-canvas dialog. + $block = $this->placeBlock('system_powered_by_block', + [ + 'label_display' => 1, + 'label' => 'Labely label', + ]); + $this->drupalGet('user'); + // Confirm the label is not overridden. + $web_assert->elementContains('css', $this->getBlockCssSelector($block), 'Labely label'); + $this->openBlockForm($this->getBlockCssSelector($block)); + $web_assert->elementNotContains('css', self::OFF_CANVAS_CSS_SELECTOR, $overridden_text); + } + + /** + * Gets the CSS selector for a block. + * + * @param \Drupal\block\BlockInterface $block + * The block. + * + * @return string + * The CSS selector for the block. + */ + protected function getBlockCssSelector(BlockInterface $block) { + return str_replace('_', '-', $this->getBlockSelector($block)); + } + } diff --git a/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTestBase.php b/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTestBase.php index d3f446cf6a..63d97badb9 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTestBase.php +++ b/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTestBase.php @@ -9,6 +9,11 @@ */ abstract class OffCanvasTestBase extends JavascriptTestBase { + /** + * CSS select for the Off-canvas dialog. + */ + const OFF_CANVAS_CSS_SELECTOR = '.ui-dialog[aria-describedby="drupal-off-canvas"]'; + /** * {@inheritdoc} */ @@ -83,7 +88,7 @@ protected function waitForOffCanvasToClose() { * @return \Behat\Mink\Element\NodeElement|null */ protected function getOffCanvasDialog() { - $off_canvas_dialog = $this->getSession()->getPage()->find('css', '.ui-dialog[aria-describedby="drupal-off-canvas"]'); + $off_canvas_dialog = $this->getSession()->getPage()->find('css', self::OFF_CANVAS_CSS_SELECTOR); $this->assertEquals(FALSE, empty($off_canvas_dialog), 'The off-canvas dialog was found.'); return $off_canvas_dialog; } diff --git a/core/tests/Drupal/Tests/Core/Config/ConfigTest.php b/core/tests/Drupal/Tests/Core/Config/ConfigTest.php index 41c3e93c99..d5b71edc07 100644 --- a/core/tests/Drupal/Tests/Core/Config/ConfigTest.php +++ b/core/tests/Drupal/Tests/Core/Config/ConfigTest.php @@ -173,6 +173,7 @@ public function testSaveExisting($data) { * @covers ::setModuleOverride * @covers ::setSettingsOverride * @covers ::getOriginal + * @covers ::hasOverrides * @dataProvider overrideDataProvider */ public function testOverrideData($data, $module_data, $setting_data) { @@ -184,26 +185,40 @@ public function testOverrideData($data, $module_data, $setting_data) { // Save so that the original data is stored. $this->config->save(); + $this->assertFalse($this->config->hasOverrides()); + $this->assertOverriddenKeys($data); // Set module override data and check value before and after save. $this->config->setModuleOverride($module_data); $this->assertConfigDataEquals($module_data); + $this->assertOverriddenKeys($data, $module_data); + $this->config->save(); $this->assertConfigDataEquals($module_data); + $this->assertOverriddenKeys($data, $module_data); + + // Reset the module overrides. + $this->config->setModuleOverride([]); + $this->assertOverriddenKeys($data); // Set setting override data and check value before and after save. $this->config->setSettingsOverride($setting_data); $this->assertConfigDataEquals($setting_data); + $this->assertOverriddenKeys($data, $setting_data); $this->config->save(); $this->assertConfigDataEquals($setting_data); + $this->assertOverriddenKeys($data, $setting_data); // Set module overrides again to ensure override order is correct. $this->config->setModuleOverride($module_data); + $merged_overrides = array_merge($module_data, $setting_data); // Setting data should be overriding module data. $this->assertConfigDataEquals($setting_data); + $this->assertOverriddenKeys($data, $merged_overrides); $this->config->save(); $this->assertConfigDataEquals($setting_data); + $this->assertOverriddenKeys($data, $merged_overrides); // Check original data has not changed. $this->assertOriginalConfigDataEquals($data, FALSE); @@ -216,6 +231,15 @@ public function testOverrideData($data, $module_data, $setting_data) { $config_value = $this->config->getOriginal($key); $this->assertEquals($value, $config_value); } + + // Check that the overrides can be completely reset. + $this->config->setModuleOverride([]); + $this->config->setSettingsOverride([]); + $this->assertConfigDataEquals($data); + $this->assertOverriddenKeys($data); + $this->config->save(); + $this->assertConfigDataEquals($data); + $this->assertOverriddenKeys($data); } /** @@ -449,6 +473,85 @@ public function overrideDataProvider() { 'a' => 'settingValue', ], ], + [ + // Original data. + [ + 'a' => 'originalValue', + 'b' => 'originalValue', + 'c' => 'originalValue', + ], + // Module overrides. + [ + 'a' => 'moduleValue', + 'b' => 'moduleValue', + ], + // Setting overrides. + [ + 'a' => 'settingValue', + ], + ], + [ + // Original data. + [ + 'a' => 'allTheSameValue', + ], + // Module overrides. + [ + 'a' => 'allTheSameValue', + ], + // Setting overrides. + [ + 'a' => 'allTheSameValue', + ], + ], + [ + // Original data. + [ + 'a' => [ + 'b' => 'originalValue' + ], + ], + // Module overrides. + [ + 'a' => [ + 'b' => 'moduleValue' + ], + ], + // Setting overrides. + [ + 'a' => [ + 'b' => 'settingValue' + ], + ], + ], + [ + // Original data. + [ + 'a' => [ + 'b' => 'originalValue' + ], + ], + // Module overrides. + [ + 'a' => [ + 'b' => 'moduleValue' + ], + ], + // Setting overrides. + [], + ], + [ + // Original data. + [ + 'a' => [ + 'b' => 'originalValue' + ], + ], + // Module overrides. + [], + // Setting overrides. + [], + ], ]; } @@ -545,4 +648,38 @@ public function testSafeStringHandling() { $this->assertSame($safe_string, $this->config->get('bar')); } + /** + * Assert that the correct keys are overridden. + * + * @param array $data + * The original data. + * @param array $overridden_data + * The overridden data. + */ + protected function assertOverriddenKeys(array $data, array $overridden_data = []) { + if (empty($overridden_data)) { + $this->assertFalse($this->config->hasOverrides()); + } + else { + $this->assertTrue($this->config->hasOverrides()); + foreach ($overridden_data as $key => $value) { + // If there are nested overrides test a combined key. + if (is_array($value)) { + $key = $key . '.' . key($value); + } + $this->assertTrue($this->config->hasOverrides($key)); + } + } + + $non_overridden_keys = array_diff(array_keys($data), array_keys($overridden_data)); + foreach ($non_overridden_keys as $non_overridden_key) { + $this->assertFalse($this->config->hasOverrides($non_overridden_key)); + // If there are nested overrides test a combined key also. + if (is_array($overridden_data)) { + $nested_non_overridden_key = $non_overridden_key . '.' . key($overridden_data); + $this->assertFalse($this->config->hasOverrides($nested_non_overridden_key)); + } + } + } + }