diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php index 167f169..f0d10ea 100644 --- a/core/modules/views/src/Entity/View.php +++ b/core/modules/views/src/Entity/View.php @@ -7,6 +7,7 @@ namespace Drupal\views\Entity; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\views\Views; @@ -242,6 +243,38 @@ public function &getDisplay($display_id) { /** * {@inheritdoc} */ + public function duplicateDisplayAsType($old_display_id, $new_display_type) { + $executable = $this->getExecutable(); + $display = $executable->newDisplay($new_display_type); + $new_display_id = $display->display['id']; + $displays = $this->get('display'); + + // Let the display title be generated by the addDisplay method and set the + // right display plugin, but keep the rest from the original display. + $display_duplicate = $displays[$old_display_id]; + unset($display_duplicate['display_title']); + unset($display_duplicate['display_plugin']); + + $displays[$new_display_id] = NestedArray::mergeDeep($displays[$new_display_id], $display_duplicate); + $displays[$new_display_id]['id'] = $new_display_id; + + // First set the displays. + $this->set('display', $displays); + + // Ensure that we just copy display options, which are provided by the new + // display plugin. + $executable->setDisplay($new_display_id); + + $executable->display_handler->filterByDefinedOptions($displays[$new_display_id]['display_options']); + // Update the display settings. + $this->set('display', $displays); + + return $new_display_id; + } + + /** + * {@inheritdoc} + */ public function calculateDependencies() { parent::calculateDependencies(); diff --git a/core/modules/views/src/Plugin/views/PluginBase.php b/core/modules/views/src/Plugin/views/PluginBase.php index 7992378..eba3135 100644 --- a/core/modules/views/src/Plugin/views/PluginBase.php +++ b/core/modules/views/src/Plugin/views/PluginBase.php @@ -179,6 +179,34 @@ protected function setOptionDefaults(array &$storage, array $options) { /** * {@inheritdoc} */ + public function filterByDefinedOptions(array &$storage) { + $this->doFilterByDefinedOptions($storage, $this->defineOptions()); + } + + /** + * Do the work to filter out stored options depending on the defined options. + * + * @param array $storage + * The stored options. + * + * @param array $options + * The defined options. + */ + protected function doFilterByDefinedOptions(array &$storage, array $options) { + foreach ($storage as $key => $sub_storage) { + if (!isset($options[$key])) { + unset($storage[$key]); + } + + if (isset($options[$key]['contains'])) { + $this->doFilterByDefinedOptions($storage[$key], $options[$key]['contains']); + } + } + } + + /** + * {@inheritdoc} + */ public function unpackOptions(&$storage, $options, $definition = NULL, $all = TRUE, $check = TRUE) { if ($check && !is_array($options)) { return; diff --git a/core/modules/views/src/Plugin/views/ViewsPluginInterface.php b/core/modules/views/src/Plugin/views/ViewsPluginInterface.php index 352aaf0..d36b4b2 100644 --- a/core/modules/views/src/Plugin/views/ViewsPluginInterface.php +++ b/core/modules/views/src/Plugin/views/ViewsPluginInterface.php @@ -39,6 +39,14 @@ public function pluginTitle(); public function usesOptions(); /** + * Filter out stored options depending on the defined options. + * + * @param array $storage + * The stored options. + */ + public function filterByDefinedOptions(array &$storage); + + /** * Validate the options form. */ public function validateOptionsForm(&$form, FormStateInterface $form_state); diff --git a/core/modules/views/src/Tests/TestHelperPlugin.php b/core/modules/views/src/Tests/TestHelperPlugin.php index 5ace3fa..6bd7a1b 100644 --- a/core/modules/views/src/Tests/TestHelperPlugin.php +++ b/core/modules/views/src/Tests/TestHelperPlugin.php @@ -17,6 +17,13 @@ class TestHelperPlugin extends PluginBase { /** + * Stores the defined options. + * + * @var array + */ + protected $definedOptions = []; + + /** * Calls the protected method setOptionDefaults(). * * @see \Drupal\views\Plugin\views\PluginBase::setOptionDefaults(). @@ -25,4 +32,24 @@ public function testSetOptionDefaults(&$storage, $options, $level = 0) { $this->setOptionDefaults($storage, $options, $level); } + /** + * Allows to set the defined options. + * + * @param array $options + * + * @return $this + */ + public function setDefinedOptions($options) { + $this->definedOptions = $options; + + return $this; + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + return $this->definedOptions; + } + } diff --git a/core/modules/views/src/ViewStorageInterface.php b/core/modules/views/src/ViewStorageInterface.php index 31b9f7b..d894452 100644 --- a/core/modules/views/src/ViewStorageInterface.php +++ b/core/modules/views/src/ViewStorageInterface.php @@ -15,6 +15,14 @@ interface ViewStorageInterface extends ConfigEntityInterface { /** + * Gets an executable instance for this view. + * + * @return \Drupal\views\ViewExecutable + * A view executable instance. + */ + public function getExecutable(); + + /** * Retrieves a specific display's configuration by reference. * * @param string $display_id @@ -30,4 +38,19 @@ public function &getDisplay($display_id); */ public function mergeDefaultDisplaysOptions(); + /** + * Duplicates an existing display into a new display type. + * + * For example clone to display a page display as a block display. + * + * @param string $old_display_id + * The origin of the duplicated display. + * @param string $new_display_type + * The display type of the new display. + * + * @return string + * The display ID of the new display. + */ + public function duplicateDisplayAsType($old_display_id, $new_display_type); + } diff --git a/core/modules/views/tests/src/Unit/PluginBaseTest.php b/core/modules/views/tests/src/Unit/PluginBaseTest.php index 4b57bd5..9c180a6 100644 --- a/core/modules/views/tests/src/Unit/PluginBaseTest.php +++ b/core/modules/views/tests/src/Unit/PluginBaseTest.php @@ -285,4 +285,39 @@ public function providerTestSetOptionDefault() { return $test_parameters; } + /** + * Tests filterByDefinedOptions(). + * + * @dataProvider providerTestFilterByDefinedOptions + */ + public function testFilterByDefinedOptions($storage, $options, $expected_storage) { + $this->testHelperPlugin->setDefinedOptions($options); + $this->testHelperPlugin->filterByDefinedOptions($storage); + $this->assertEquals($expected_storage, $storage); + } + + public function providerTestFilterByDefinedOptions() { + $data = []; + + // A simple defined option. + $values_1 = ['key1' => 'value1']; + $options_1 = ['key1' => ['default' => '']]; + $data[] = [$values_1, $options_1, $values_1]; + // Multiple defined options . + $values_2 = ['key1' => 'value1', 'key2' => 'value2']; + $options_2 = ['key1' => ['default' => ''], 'key2' => ['default' => '']]; + $data[] = [$values_2, $options_2, $values_2]; + + // Multiple options, just one defined. + $data[] = [$values_2, $options_1, $values_1]; + + // Nested options, all properly defined. + $data[] = [['sub1' => $values_2, 'sub2' => $values_2], ['sub1' => ['contains' => $options_2], 'sub2' => ['contains' => $options_2]], ['sub1' => $values_2, 'sub2' => $values_2]]; + + // Nested options, not all properly defined. + $data[] = [['sub1' => $values_2, 'sub2' => $values_2], ['sub1' => ['contains' => $options_2], 'sub2' => ['contains' => $options_1]], ['sub1' => $values_2, 'sub2' => $values_1]]; + + return $data; + } + } diff --git a/core/modules/views_ui/src/Tests/DisplayCRUDTest.php b/core/modules/views_ui/src/Tests/DisplayCRUDTest.php index c2dfd84..4f1d2e2 100644 --- a/core/modules/views_ui/src/Tests/DisplayCRUDTest.php +++ b/core/modules/views_ui/src/Tests/DisplayCRUDTest.php @@ -17,17 +17,6 @@ class DisplayCRUDTest extends UITestBase { /** - * Set to TRUE to strict check all configuration saved. - * - * @see \Drupal\Core\Config\Testing\ConfigSchemaChecker - * - * @todo https://www.drupal.org/node/2387157 - * - * @var bool - */ - protected $strictConfigSchema = FALSE; - - /** * Views used by this test. * * @var array @@ -114,6 +103,7 @@ public function testDefaultDisplay() { public function testDuplicateDisplay() { $view = $this->randomView(); $path_prefix = 'admin/structure/views/view/' . $view['id'] .'/edit'; + $path = $view['page[path]']; $this->drupalGet($path_prefix); $this->drupalPostForm(NULL, array(), 'Duplicate Page'); @@ -140,10 +130,12 @@ public function testDuplicateDisplay() { $page_2 = $view->displayHandlers->get('page_2'); $this->assertTrue($page_2, 'The new page display got saved.'); $this->assertEqual($page_2->display['display_title'], 'Page'); + $this->assertEqual($page_2->display['display_options']['path'], $path); $block_1 = $view->displayHandlers->get('block_1'); $this->assertTrue($block_1, 'The new block display got saved.'); $this->assertEqual($block_1->display['display_plugin'], 'block'); $this->assertEqual($block_1->display['display_title'], 'Block', 'The new display title got generated as expected.'); + $this->assertFalse(isset($block_1->display['display_options']['path'])); $this->assertEqual($block_1->getOption('title'), $random_title, 'The overridden title option from the display got copied into the duplicate'); $this->assertEqual($block_1->getOption('css_class'), $random_css, 'The overridden css_class option from the display got copied into the duplicate'); } diff --git a/core/modules/views_ui/src/ViewEditForm.php b/core/modules/views_ui/src/ViewEditForm.php index 7800580..c9d8955 100644 --- a/core/modules/views_ui/src/ViewEditForm.php +++ b/core/modules/views_ui/src/ViewEditForm.php @@ -855,25 +855,15 @@ public function submitDisplayAdd($form, FormStateInterface $form_state) { * Submit handler to Duplicate a display as another display type. */ public function submitDuplicateDisplayAsType($form, FormStateInterface $form_state) { + /** @var \Drupal\views\ViewStorageInterface $view */ $view = $this->entity; $display_id = $this->displayID; // Create the new display. $parents = $form_state->getTriggeringElement()['#parents']; $display_type = array_pop($parents); - $display = $view->getExecutable()->newDisplay($display_type); - $new_display_id = $display->display['id']; - $displays = $view->get('display'); - // Let the display title be generated by the addDisplay method and set the - // right display plugin, but keep the rest from the original display. - $display_duplicate = $displays[$display_id]; - unset($display_duplicate['display_title']); - unset($display_duplicate['display_plugin']); - - $displays[$new_display_id] = NestedArray::mergeDeep($displays[$new_display_id], $display_duplicate); - $displays[$new_display_id]['id'] = $new_display_id; - $view->set('display', $displays); + $new_display_id = $view->duplicateDisplayAsType($display_id, $display_type); // By setting the current display the changed marker will appear on the new // display. diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php index 9716499..4a08788 100644 --- a/core/modules/views_ui/src/ViewUI.php +++ b/core/modules/views_ui/src/ViewUI.php @@ -1074,6 +1074,20 @@ public static function postLoad(EntityStorageInterface $storage, array &$entitie /** * {@inheritdoc} */ + public function getExecutable() { + return $this->storage->getExecutable(); + } + + /** + * {@inheritdoc} + */ + public function duplicateDisplayAsType($old_display_id, $new_display_type) { + return $this->storage->duplicateDisplayAsType($old_display_id, $new_display_type); + } + + /** + * {@inheritdoc} + */ public function mergeDefaultDisplaysOptions() { $this->storage->mergeDefaultDisplaysOptions(); }