diff --git a/ctools.services.yml b/ctools.services.yml index eaa38b8..c98296a 100644 --- a/ctools.services.yml +++ b/ctools.services.yml @@ -31,6 +31,8 @@ services: ctools.context_mapper: class: Drupal\ctools\ContextMapper arguments: ['@entity.repository'] + ctools.views.handlers.helper: + class: Drupal\ctools\ViewsHandlersHelper ctools.serializable.tempstore.factory: class: Drupal\ctools\SerializableTempstoreFactory arguments: ['@keyvalue.expirable', '@lock', '@request_stack', '%user.tempstore.expire%'] diff --git a/modules/ctools_views/ctools_views.module b/modules/ctools_views/ctools_views.module index 845feb6..9876c64 100644 --- a/modules/ctools_views/ctools_views.module +++ b/modules/ctools_views/ctools_views.module @@ -61,6 +61,10 @@ function ctools_views_config_schema_info_alter(&$definitions) { ], ], ]; + $definitions['views_block']['mapping']['exposed'] = [ + 'type' => 'sequence', + 'label' => 'Exposed filter values', + ]; $definitions['views_block']['mapping']['sort'] = [ 'type' => 'sequence', 'label' => 'Sort settings', @@ -93,6 +97,10 @@ function ctools_views_config_schema_info_alter(&$definitions) { 'type' => 'string', 'label' => 'Sort fields', ]; + $definitions['views.display.block']['mapping']['allow']['mapping']['configure_filters'] = [ + 'type' => 'string', + 'label' => 'Configure filters', + ]; $definitions['views.display.block']['mapping']['allow']['mapping']['disable_filters'] = [ 'type' => 'string', 'label' => 'Disable filters', diff --git a/modules/ctools_views/src/Plugin/Display/Block.php b/modules/ctools_views/src/Plugin/Display/Block.php index e2d2a7c..1c4e326 100644 --- a/modules/ctools_views/src/Plugin/Display/Block.php +++ b/modules/ctools_views/src/Plugin/Display/Block.php @@ -2,6 +2,7 @@ namespace Drupal\ctools_views\Plugin\Display; +use Drupal\Core\Render\Element; use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormStateInterface; use Drupal\views\Plugin\Block\ViewsBlock; @@ -14,6 +15,12 @@ use Drupal\views\Plugin\views\filter\InOperator; */ class Block extends CoreBlock { + public function blockSettings(array $settings) { + $settings = parent::blockSettings($settings); + $settings['exposed'] = []; + return $settings; + } + /** * {@inheritdoc} */ @@ -26,6 +33,7 @@ class Block extends CoreBlock { 'pager' => $this->t('Pager type'), 'hide_fields' => $this->t('Hide fields'), 'sort_fields' => $this->t('Reorder fields'), + 'configure_filters' => $this->t('Configure filters'), 'disable_filters' => $this->t('Disable filters'), 'configure_sorts' => $this->t('Configure sorts') ]; @@ -48,6 +56,7 @@ class Block extends CoreBlock { $options['pager'] = $this->t('Pager type'); $options['hide_fields'] = $this->t('Hide fields'); $options['sort_fields'] = $this->t('Reorder fields'); + $options['configure_filters'] = $this->t('Configure filters'); $options['disable_filters'] = $this->t('Disable filters'); $options['configure_sorts'] = $this->t('Configure sorts'); $form['allow']['#options'] = $options; @@ -180,32 +189,103 @@ class Block extends CoreBlock { } } - // Provide "Configure filters" / "Disable filters" block settings form. - if (!empty($allow_settings['disable_filters'])) { - $items = []; - foreach ((array) $this->getOption('filters') as $filter_name => $item) { - $item['value'] = isset($block_configuration["filter"][$filter_name]['value']) ? $block_configuration["filter"][$filter_name]['value'] : ''; - $items[$filter_name] = $item; + // Provide "Configure filters" form elements. + if (!empty($allow_settings['configure_filters'])) { + $this->view->setExposedInput($block_configuration["exposed"]); + $exposed_form_state = new FormState(); + $exposed_form_state->setValidationEnforced(); + $exposed_form_state->set('view', $this->view); + $exposed_form_state->set('display', $this->view->current_display); + + $exposed_form_state->setUserInput($this->view->getExposedInput()); + + // Let form plugins know this is for exposed widgets. + $exposed_form_state->set('exposed', TRUE); + $exposed_form = []; + $exposed_form['#info'] = array(); + + // Initialize filter and sort handlers so that the exposed form alter + // method works as expected. + $this->view->filter = $this->getHandlers('filter'); + $this->view->sort = $this->getHandlers('sort'); + + // Go through each handler and let it generate its exposed widget. + /** @var \Drupal\views\Plugin\views\ViewsHandlerInterface $handler */ + foreach ($this->view->getDisplay()->getHandlers('filter') as $id => $handler) { + // If the current handler is exposed... + if ($handler->canExpose() && $handler->isExposed()) { + // Grouped exposed filters have their own forms. Instead of rendering + // the standard exposed form, a new Select or Radio form field is + // rendered with the available groups. When a user chooses an option + // the selected value is split into the operator and value that the + // item represents. + if ($handler->isAGroup()) { + if (isset($block_configuration['exposed']['filter-' . $id])) { + \Drupal::service('ctools.views.handlers.helper') + ->convertExposedValue($handler, $block_configuration['exposed']['filter-' . $id]); + } + + $handler->groupForm($exposed_form, $exposed_form_state); + $id = $handler->options['group_info']['identifier']; + } + else { + // If the current filter is not a group and has an exposed value in + // the block configuration... + if (isset($block_configuration['exposed']['filter-' . $id])) { + \Drupal::service('ctools.views.handlers.helper') + ->convertExposedValue($handler, $block_configuration['exposed']['filter-' . $id]); + } + + $handler->buildExposedForm($exposed_form, $exposed_form_state); + } + + if ($info = $handler->exposedInfo()) { + $exposed_form['#info']['filter-' . $id] = $info; + } + } } - $this->setOption('filters', $items); - $filters = $this->getHandlers('filter'); + /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase $exposed_form_plugin */ + $exposed_form_plugin = $this->view->display_handler->getPlugin('exposed_form'); + $exposed_form_plugin->exposedFormAlter($exposed_form, $exposed_form_state); + + $form['exposed'] = array( + '#tree' => TRUE, + '#title' => $this->t('Exposed filter values'), + '#description' => $this->t('If a value is set for an exposed filter, it will be removed from the block display.'), + '#type' => 'details', + '#open' => FALSE, + ); + + foreach ($exposed_form['#info'] as $id => $info) { + $form['exposed'][$id] = array( + '#type' => 'item', + '#id' => 'views-exposed-pane', + ); + + // @todo This can result in double titles for group filters. + if (!empty($info['label'])) { + $form['exposed'][$id]['#title'] = $info['label']; + } + + if (!empty($info['operator']) && !empty($exposed_form[$info['operator']])) { + $form['exposed'][$id][$info['operator']] = $exposed_form[$info['operator']]; + } + + $form['exposed'][$id][$info['value']] = $exposed_form[$info['value']]; + } + } + + if (!empty($allow_settings['disable_filters'])) { + $filters = $this->getHandlers('filter'); // Add a settings form for each exposed filter to configure or hide it. foreach ($filters as $filter_name => $plugin) { if ($plugin->isExposed() && $exposed_info = $plugin->exposedInfo()) { - $form['override']['filters'][$filter_name] = [ - '#type' => 'details', - '#title' => $exposed_info['label'], - ]; - $form['override']['filters'][$filter_name]['plugin'] = [ - '#type' => 'value', - '#value' => $plugin, - ]; // Render "Disable filters" settings form. if (!empty($allow_settings['disable_filters'])) { $form['override']['filters'][$filter_name]['disable'] = [ '#type' => 'checkbox', - '#title' => $this->t('Disable'), + '#title' => $this->t('Disable filter: @handler', ['@handler' => $plugin->options['expose']['label']]), '#default_value' => !empty($block_configuration['filter'][$filter_name]['disable']) ? $block_configuration['filter'][$filter_name]['disable'] : 0, ]; } @@ -249,6 +329,26 @@ class Block extends CoreBlock { /** * {@inheritdoc} */ + public function blockValidate(ViewsBlock $block, array $form, FormStateInterface $form_state) { + // checkout validateOptionsForm on filters before saving this. + foreach ($form_state->getValue('exposed') as $key => $values) { + list($type, $handler_name) = explode('-', $key, 2); + $handler = $this->view->getDisplay()->getHandler($type, $handler_name); + $handler_form_state = new FormState(); + $handler_form_state->setValues($values); + $handler->validateExposed($form, $handler_form_state); + foreach ($handler_form_state->getErrors() as $name => $message) { + $form_state->setErrorByName($name, $message); + } + if (property_exists($handler, 'validated_exposed_input')) { + $form_state->setValue(['exposed', $key], [$handler_name => $handler->validated_exposed_input]); + } + } + } + + /** + * {@inheritdoc} + */ public function blockSubmit(ViewsBlock $block, $form, FormStateInterface $form_state) { // Set default value for items_per_page if left blank. if (empty($form_state->getValue(array('override', 'items_per_page')))) { @@ -281,22 +381,18 @@ class Block extends CoreBlock { // Save "Configure filters" / "Disable filters" settings to block // configuration. + if (!empty($allow_settings['configure_filters'])) { + unset($configuration['exposed']); + $configuration['exposed'] = $form_state->getValue('exposed'); + } unset($configuration['filter']); + unset($configuration['filters']); if (!empty($allow_settings['disable_filters'])) { if ($filters = $form_state->getValue(['override', 'filters'])) { foreach ($filters as $filter_name => $filter) { - /** @var \Drupal\views\Plugin\views\filter\FilterPluginBase $plugin */ - $plugin = $form_state->getValue(['override', 'filters', $filter_name, 'plugin']); - $configuration["filter"][$filter_name]['type'] = $plugin->getPluginId(); - - // Check if we want to disable this filter. - if (!empty($allow_settings['disable_filters'])) { - $disable = $form_state->getValue(['override', 'filters', $filter_name, 'disable']); - // If marked disabled, we don't really care about other stuff. - if ($disable) { - $configuration["filter"][$filter_name]['disable'] = $disable; - continue; - } + $disable = $filter['disable']; + if ($disable) { + $configuration['filter'][$filter_name]['disable'] = $disable; } } } @@ -366,7 +462,50 @@ class Block extends CoreBlock { // and continue. if (!empty($allow_settings['disable_filters']) && !empty($config["filter"][$filter_name]['disable'])) { $this->view->removeHandler($display_id, 'filter', $filter_name); - continue; + // We don't want to needlessly set filter options later. + unset($config['exposed']['filter-' . $filter_name]); + } + } + } + + // Set an exposed filter value and remove it from the display if set in the + // block configuration. + if (!empty($allow_settings['configure_filters'])) { + $exposed = $this->view->getExposedInput(); + + // Loop over the exposed filter settings in the block configuration. + foreach ($config['exposed'] as $key => $value) { + // Load the handler related to the exposed filter. + list($handler_type, $handler_name) = explode('-', $key, 2); + $handler = $this->view->getDisplay()->getHandler($handler_type, $handler_name); + + // Set exposed filter input directly where they were entered in the + // block configuration. Otherwise only set them if they haven't been set + // already. + if (\Drupal::service('ctools.views.handlers.helper')->validValue($config['exposed'][$key], $handler)) { + $exposed[$handler_name] = $value[$handler_name]; + } + elseif (!isset($exposed[$handler_name])) { + $exposed[$handler_name] = $value[$handler_name]; + } + } + + // Set the updated exposed filter input array on the View. + $this->view->setExposedInput($exposed); + + // Loop over the exposed filter settings in the block configuration again. + foreach (array_keys($config['exposed']) as $key) { + // Load the handler related to this exposed filter. + list($handler_type, $handler_name) = explode('-', $key, 2); + + if ($handler_type == 'filter') { + $handler = $this->view->getDisplay()->getHandler($handler_type, $handler_name); + + // If the exposed filter input value for this filter came from the + // block configuration, do not expose it on the View. + if (\Drupal::service('ctools.views.handlers.helper')->validValue($config['exposed'][$key], $handler)) { + $handler->options['value_from_block_configuration'] = TRUE; + } } } } @@ -405,18 +544,43 @@ class Block extends CoreBlock { } /** - * Exposed widgets typically only work with ajax in Drupal core, however - * #2605218 totally breaks the rest of the functionality in this display and - * in Core's Block display as well, so we allow non-ajax block views to use - * exposed filters and manually set the #action to the current request uri. + * {@inheritdoc} */ public function elementPreRender(array $element) { /** @var \Drupal\views\ViewExecutable $view */ $view = $element['#view']; + + // Exposed widgets typically only work with Ajax in core, but #2605218 + // breaks the rest of the functionality in this display and in the core + // Block display as well. We allow non-Ajax block views to use exposed + // filters by manually setting the #action to the current request URI. if (!empty($view->exposed_widgets['#action']) && !$view->ajaxEnabled()) { $view->exposed_widgets['#action'] = \Drupal::request()->getRequestUri(); } - return parent::elementPreRender($element); + + // Allow the parent pre-render function to set the #exposed array on the + // element. This allows us to bypass hiding widgets if the array is emptied. + $element = parent::elementPreRender($element); + + // Loop over the filters on the current View looking for exposed filters + // whose values have been derived from block configuration. + if (!empty($element['#exposed'])) { + foreach ($view->getDisplay()->getHandlers('filter') as $id => $handler) { + /* @var \Drupal\views\Plugin\views\Filter\FilterPluginBase $handler */ + // If the current handler meets the conditions, hide its exposed widget. + if ($handler->canExpose() && $handler->isExposed() && !empty($handler->options['value_from_block_configuration'])) { + $element['#exposed'][$id]['#access'] = FALSE; + } + } + + // If there are no accessible child elements in the #exposed array other + // than the actions, reset it to an empty array. + if (Element::getVisibleChildren($element['#exposed']) == array('actions')) { + $element['#exposed'] = array(); + } + } + + return $element; } /** diff --git a/modules/ctools_views/src/Tests/CToolsViewsBasicViewBlockTest.php b/modules/ctools_views/src/Tests/CToolsViewsBasicViewBlockTest.php index 8520104..1b44c55 100644 --- a/modules/ctools_views/src/Tests/CToolsViewsBasicViewBlockTest.php +++ b/modules/ctools_views/src/Tests/CToolsViewsBasicViewBlockTest.php @@ -269,6 +269,83 @@ class CToolsViewsBasicViewBlockTest extends UITestBase { } /** + * Test ctools_views 'configure_filters' configuration. + */ + public function testConfigureFilters() { + $default_theme = $this->config('system.theme')->get('default'); + + // Get the "Configure block" form for our Views block. + $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_filter/' . $default_theme); + $this->assertFieldById('edit-settings-override-filters-job-form-job'); + + // Add block to sidebar_first region with default settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][filters][status][form][status]'] = "All"; + $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_filter/' . $default_theme, $edit, t('Save block')); + + // Assert configure_filters default settings. + $this->drupalGet(''); + // Check that the default settings return all results + $this->assertEqual(5, count($this->xpath('//div[contains(@class, "view-display-id-block_filter")]//table/tbody/tr'))); + $this->assertFieldById('edit-job'); + + // Override configure_filters settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][filters][status][form][status]'] = "All"; + $edit['settings[override][filters][job][form][job]'] = "Singer"; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_filter', $edit, t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_test_view_block_filter'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual("Singer", $config['filter']['job']['value']['job'], "'configure_filters' setting is properly saved."); + + // Assert configure_filters overridden settings. + $this->drupalGet(''); + // Check that the overridden settings return proper results + $this->assertEqual(2, count($this->xpath('//div[contains(@class, "view-display-id-block_filter")]//table/tbody/tr'))); + } + + /** + * Test ctools_views 'configure_filters' configuration with boolean values. + */ + public function testConfigureFiltersBoolean() { + $default_theme = $this->config('system.theme')->get('default'); + + // Get the "Configure block" form for our Views block. + $this->drupalGet('admin/structure/block/add/views_block:ctools_views_test_view-block_filter/' . $default_theme); + $this->assertFieldByXPath('//select[@name="settings[override][filters][status][form][status]"]'); + + // Add block to sidebar_first region with default settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][filters][status][form][status]'] = "All"; + $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_filter/' . $default_theme, $edit, t('Save block')); + + // Assert configure_filters default settings. + $this->drupalGet(''); + // Check that the default settings return all results + $this->assertEqual(5, count($this->xpath('//div[contains(@class, "view-display-id-block_filter")]//table/tbody/tr'))); + $this->assertFieldByXPath('//select[@name="status"]'); + + // Override configure_filters settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][filters][status][form][status]'] = 1; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_filter', $edit, t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_test_view_block_filter'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual(1, $config['filter']['status']['value']['status'], "'configure_filters' setting is properly saved."); + + // Assert configure_filters overridden settings. + $this->drupalGet(''); + // Check that the overridden settings return proper results + $this->assertEqual(3, count($this->xpath('//div[contains(@class, "view-display-id-block_filter")]//table/tbody/tr'))); + } + + /** * Test ctools_views 'disable_filters' configuration. */ public function testDisableFilters() { @@ -282,6 +359,7 @@ class CToolsViewsBasicViewBlockTest extends UITestBase { // Add block to sidebar_first region with default settings. $edit = array(); $edit['region'] = 'sidebar_first'; + $edit['settings[override][filters][status][form][status]'] = "All"; $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_test_view-block_filter/' . $default_theme, $edit, $this->t('Save block')); // Assert disable_filters default settings. @@ -295,6 +373,7 @@ class CToolsViewsBasicViewBlockTest extends UITestBase { $edit['region'] = 'sidebar_first'; $edit['settings[override][filters][status][disable]'] = 1; $edit['settings[override][filters][job][disable]'] = 1; + $edit['settings[override][filters][status][form][status]'] = "All"; $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_test_view_block_filter', $edit, $this->t('Save block')); $block = $this->storage->load('views_block__ctools_views_test_view_block_filter'); diff --git a/modules/ctools_views/src/Tests/CToolsViewsEntityViewBlockTest.php b/modules/ctools_views/src/Tests/CToolsViewsEntityViewBlockTest.php new file mode 100644 index 0000000..6a3f5a8 --- /dev/null +++ b/modules/ctools_views/src/Tests/CToolsViewsEntityViewBlockTest.php @@ -0,0 +1,416 @@ +drupalCreateContentType(array('type' => 'ctools_views', 'name' => 'Ctools views')); + + // Create test textfield + entity_create('field_storage_config', array( + 'entity_type' => 'node', + 'field_name' => 'field_ctools_views_text', + 'type' => 'text', + 'cardinality' => 1, + ))->save(); + entity_create('field_config', array( + 'entity_type' => 'node', + 'field_name' => 'field_ctools_views_text', + 'bundle' => 'ctools_views', + 'label' => 'Ctools Views test textfield', + 'translatable' => FALSE, + ))->save(); + + // Create a vocabulary named "Tags". + $vocabulary = Vocabulary::create(array( + 'name' => 'Tags', + 'vid' => 'tags', + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + )); + $vocabulary->save(); + $this->terms[] = $this->createTerm($vocabulary); + $this->terms[] = $this->createTerm($vocabulary); + $this->terms[] = $this->createTerm($vocabulary); + + $handler_settings = array( + 'target_bundles' => array( + $vocabulary->id() => $vocabulary->id(), + ), + ); + $this->createEntityReferenceField('node', 'ctools_views', 'field_ctools_views_tags', 'Tags', 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + + // Create list field + entity_create('field_storage_config', array( + 'entity_type' => 'node', + 'field_name' => 'field_ctools_views_list', + 'type' => 'list_string', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + 'settings' => [ + 'allowed_values' => [ + 'item1' => "Item 1", + 'item2' => "Item 2", + 'item3' => "Item 3", + ], + ], + ))->save(); + entity_create('field_config', array( + 'entity_type' => 'node', + 'field_name' => 'field_ctools_views_list', + 'bundle' => 'ctools_views', + 'label' => 'Ctools Views List', + 'translatable' => FALSE, + ))->save(); + + // Create date field + entity_create('field_storage_config', array( + 'entity_type' => 'node', + 'field_name' => 'field_ctools_views_date', + 'type' => 'datetime', + 'cardinality' => 1, + 'settings' => [ + 'datetime_type' => 'date', + ], + ))->save(); + entity_create('field_config', array( + 'entity_type' => 'node', + 'field_name' => 'field_ctools_views_date', + 'bundle' => 'ctools_views', + 'label' => 'Ctools Views Date', + 'translatable' => FALSE, + ))->save(); + + ViewTestData::createTestViews(get_class($this), array('ctools_views_test_views')); + $this->storage = $this->container->get('entity.manager')->getStorage('block'); + + // Create test entities + $values = array( + 'type' => 'ctools_views', + 'title' => 'Test entity 1', + 'uid' => 1, + 'field_ctools_views_text' => array( + 'value' => 'text_1', + 'format' => 'plain_text', + ), + 'field_ctools_views_tags' => array( + 'target_id' => $this->terms[0]->id(), + ), + 'field_ctools_views_list' => array( + 'value' => 'item1', + ), + 'field_ctools_views_date' => array( + 'value' => '1990-01-01', + ), + ); + $entity = entity_create('node', $values); + $entity->save(); + $this->entities[] = $entity; + + $values = array( + 'type' => 'ctools_views', + 'title' => 'Test entity 2', + 'uid' => 1, + 'field_ctools_views_text' => array( + 'value' => 'text_2', + 'format' => 'plain_text', + ), + 'field_ctools_views_tags' => array( + 'target_id' => $this->terms[1]->id(), + ), + 'field_ctools_views_list' => array( + 'value' => 'item2', + ), + 'field_ctools_views_date' => array( + 'value' => '2016-10-04', + ), + ); + $entity = entity_create('node', $values); + $entity->save(); + $this->entities[] = $entity; + + $values = array( + 'type' => 'ctools_views', + 'title' => 'Test entity 3', + 'uid' => 0, + 'field_ctools_views_text' => array( + 'value' => 'text_1', + 'format' => 'plain_text', + ), + 'field_ctools_views_tags' => array( + 'target_id' => $this->terms[2]->id(), + ), + 'field_ctools_views_list' => array( + 'value' => 'item3', + ), + 'field_ctools_views_date' => array( + 'value' => '2018-12-31', + ), + ); + $entity = entity_create('node', $values); + $entity->save(); + $this->entities[] = $entity; + } + + /** + * Test ctools_views 'configure_filters' configuration with text field values. + */ + public function testConfigureFiltersTextfield() { + $default_theme = $this->config('system.theme')->get('default'); + + // Get the "Configure block" form for our Views block. + $this->drupalGet('admin/structure/block/add/views_block:ctools_views_entity_test-block_filter_text/' . $default_theme); + $this->assertFieldByXPath('//input[@name="settings[override][filters][field_ctools_views_text_value][form][field_ctools_views_text_value]"]'); + + // Add block to sidebar_first region with default settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_entity_test-block_filter_text/' . $default_theme, $edit, t('Save block')); + + // Assert configure_filters default settings. + $this->drupalGet(''); + // Check that the default settings return all results + $this->assertEqual(3, count($this->xpath('//div[contains(@class, "view-display-id-block_filter_text")]//table//tbody//tr'))); + $this->assertFieldByXPath('//input[@name="field_ctools_views_text_value"]'); + + // Override configure_filters settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][filters][field_ctools_views_text_value][form][field_ctools_views_text_value]'] = 'text_1'; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_entity_test_block_filter_text', $edit, t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_entity_test_block_filter_text'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual('text_1', $config['filter']['field_ctools_views_text_value']['value']['field_ctools_views_text_value'], "'configure_filters' setting is properly saved."); + + // Assert configure_filters overridden settings. + $this->drupalGet(''); + // Check that the overridden settings return proper results + $this->assertEqual(2, count($this->xpath('//div[contains(@class, "view-display-id-block_filter_text")]//table//tbody//tr'))); + $this->assertNoFieldByXPath('//input[@name="field_ctools_views_text_value"]'); + } + + /** + * Test ctools_views 'configure_filters' configuration with taxonomy term field values. + */ + public function testConfigureFiltersTaxonomy() { + $default_theme = $this->config('system.theme')->get('default'); + $tid = $this->terms[0]->id(); + + // Get the "Configure block" form for our Views block. + $this->drupalGet('admin/structure/block/add/views_block:ctools_views_entity_test-block_filter_tax/' . $default_theme); + $this->assertFieldByXPath('//select[@name="settings[override][filters][field_ctools_views_tags_target_id][form][field_ctools_views_tags_target_id]"]'); + + // Add block to sidebar_first region with default settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_entity_test-block_filter_tax/' . $default_theme, $edit, t('Save block')); + + // Assert configure_filters default settings. + $this->drupalGet(''); + // Check that the default settings return all results + $this->assertEqual(3, count($this->xpath('//div[contains(@class, "view-display-id-block_filter_tax")]//table//tbody//tr'))); + $this->assertFieldByXPath('//select[@name="field_ctools_views_tags_target_id"]'); + + // Override configure_filters settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][filters][field_ctools_views_tags_target_id][form][field_ctools_views_tags_target_id]'] = $tid; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_entity_test_block_filter_tax', $edit, t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_entity_test_block_filter_tax'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual([$tid], $config['filter']['field_ctools_views_tags_target_id']['value'], "'configure_filters' setting is properly saved."); + + // Assert configure_filters overridden settings. + $this->drupalGet(''); + // Check that the overridden settings return proper results + $this->assertEqual(1, count($this->xpath('//div[contains(@class, "view-display-id-block_filter_tax")]//table//tbody//tr'))); + $this->assertNoFieldByXPath('//select[@name="field_ctools_views_tags_target_id"]'); + } + + /** + * Test ctools_views 'configure_filters' configuration with taxonomy term autocomplete. + */ + public function testConfigureFiltersTaxonomyAutocomplete() { + $default_theme = $this->config('system.theme')->get('default'); + $tid = $this->terms[0]->id(); + + // Get the "Configure block" form for our Views block. + $this->drupalGet('admin/structure/block/add/views_block:ctools_views_entity_test-block_filter_auto/' . $default_theme); + $this->assertFieldByXPath('//input[@name="settings[override][filters][field_ctools_views_tags_target_id][form][field_ctools_views_tags_target_id]"]'); + + // Add block to sidebar_first region with default settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_entity_test-block_filter_auto/' . $default_theme, $edit, t('Save block')); + + // Assert configure_filters default settings. + $this->drupalGet(''); + // Check that the default settings return all results + $this->assertEqual(3, count($this->xpath('//div[contains(@class, "view-display-id-block_filter_auto")]//table//tbody//tr'))); + $this->assertFieldByXPath('//input[@name="field_ctools_views_tags_target_id"]'); + + // Override configure_filters settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $filter_term = $this->terms[0]; + $filter_value = EntityAutocomplete::getEntityLabels([$filter_term]); + $edit['settings[override][filters][field_ctools_views_tags_target_id][form][field_ctools_views_tags_target_id]'] = $filter_value; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_entity_test_block_filter_auto', $edit, t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_entity_test_block_filter_auto'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual([$tid], $config['filter']['field_ctools_views_tags_target_id']['value'], "'configure_filters' setting is properly saved."); + + // Assert configure_filters overridden settings. + $this->drupalGet(''); + // Check that the overridden settings return proper results + $this->assertEqual(1, count($this->xpath('//div[contains(@class, "view-display-id-block_filter_auto")]//table//tbody//tr'))); + $this->assertNoFieldByXPath('//input[@name="field_ctools_views_tags_target_id"]'); + } + + /** + * Test ctools_views 'configure_filters' configuration with list field values. + */ + public function testConfigureFiltersList() { + $default_theme = $this->config('system.theme')->get('default'); + + // Get the "Configure block" form for our Views block. + $this->drupalGet('admin/structure/block/add/views_block:ctools_views_entity_test-block_filter_list/' . $default_theme); + $this->assertFieldByXPath('//select[@name="settings[override][filters][field_ctools_views_list_value][form][field_ctools_views_list_value]"]'); + + // Add block to sidebar_first region with default settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_entity_test-block_filter_list/' . $default_theme, $edit, t('Save block')); + + // Assert configure_filters default settings. + $this->drupalGet(''); + // Check that the default settings return all results + $this->assertEqual(3, count($this->xpath('//div[contains(@class, "view-display-id-block_filter_list")]//table//tbody//tr'))); + $this->assertFieldByXPath('//select[@name="field_ctools_views_list_value"]'); + + // Override configure_filters settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][filters][field_ctools_views_list_value][form][field_ctools_views_list_value]'] = 'item2'; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_entity_test_block_filter_list', $edit, t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_entity_test_block_filter_list'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual('item2', $config['filter']['field_ctools_views_list_value']['value']['field_ctools_views_list_value'], "'configure_filters' setting is properly saved."); + + // Assert configure_filters overridden settings. + $this->drupalGet(''); + // Check that the overridden settings return proper results + $this->assertEqual(1, count($this->xpath('//div[contains(@class, "view-display-id-block_filter_list")]//table//tbody//tr'))); + $this->assertNoFieldByXPath('//select[@name="field_ctools_views_list_value"]'); + } + + /** + * Test ctools_views 'configure_filters' configuration with date field values. + */ + public function testConfigureFiltersDate() { + $default_theme = $this->config('system.theme')->get('default'); + + // Get the "Configure block" form for our Views block. + $this->drupalGet('admin/structure/block/add/views_block:ctools_views_entity_test-block_filter_date/' . $default_theme); + $this->assertFieldByXPath('//input[@name="settings[override][filters][field_ctools_views_date_value][form][field_ctools_views_date_value][min]"]'); + $this->assertFieldByXPath('//input[@name="settings[override][filters][field_ctools_views_date_value][form][field_ctools_views_date_value][max]"]'); + + // Add block to sidebar_first region with default settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $this->drupalPostForm('admin/structure/block/add/views_block:ctools_views_entity_test-block_filter_date/' . $default_theme, $edit, t('Save block')); + + // Assert configure_filters default settings. + $this->drupalGet(''); + // Check that the default settings return all results + $this->assertEqual(3, count($this->xpath('//div[contains(@class, "view-display-id-block_filter_date")]//table//tbody//tr'))); + $this->assertFieldByXPath('//input[@name="field_ctools_views_date_value[min]"]'); + $this->assertFieldByXPath('//input[@name="field_ctools_views_date_value[max]"]'); + + // Override configure_filters settings. + $edit = array(); + $edit['region'] = 'sidebar_first'; + $edit['settings[override][filters][field_ctools_views_date_value][form][field_ctools_views_date_value][min]'] = '2016-01-01'; + $edit['settings[override][filters][field_ctools_views_date_value][form][field_ctools_views_date_value][max]'] = '2016-12-31'; + $this->drupalPostForm('admin/structure/block/manage/views_block__ctools_views_entity_test_block_filter_date', $edit, t('Save block')); + + $block = $this->storage->load('views_block__ctools_views_entity_test_block_filter_date'); + $config = $block->getPlugin()->getConfiguration(); + $this->assertEqual(['min' => '2016-01-01', 'max' => '2016-12-31'], $config['filter']['field_ctools_views_date_value']['value']['field_ctools_views_date_value'], "'configure_filters' setting is properly saved."); + + // Assert configure_filters overridden settings. + $this->drupalGet(''); + // Check that the overridden settings return proper results + $this->assertEqual(1, count($this->xpath('//div[contains(@class, "view-display-id-block_filter_date")]//table//tbody//tr'))); + $this->assertNoFieldByXPath('//input[@name="field_ctools_views_date_value[min]"]'); + $this->assertNoFieldByXPath('//input[@name="field_ctools_views_date_value[max]"]'); + } + +} diff --git a/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_entity_test.yml b/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_entity_test.yml index c2ece3c..eebf752 100644 --- a/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_entity_test.yml +++ b/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_entity_test.yml @@ -158,6 +158,109 @@ display: - 'user.node_grants:view' - user.permissions tags: { } + block_filter_auto: + display_plugin: block + id: block_filter_auto + display_title: 'Taxonomy autocomplete filter' + position: 2 + display_options: + display_extenders: { } + display_description: '' + title: 'Taxonomy filter' + defaults: + title: false + filters: false + filter_groups: false + filters: + status: + value: true + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + type: + id: type + table: node_field_data + field: type + value: + ctools_views: ctools_views + entity_type: node + entity_field: type + plugin_id: bundle + group: 1 + field_ctools_views_tags_target_id: + id: field_ctools_views_tags_target_id + table: node__field_ctools_views_tags + field: field_ctools_views_tags_target_id + relationship: none + group_type: group + admin_label: '' + operator: or + value: { } + group: 1 + exposed: true + expose: + operator_id: field_ctools_views_tags_target_id_op + label: 'Tags (field_ctools_views_tags)' + description: '' + use_operator: false + operator: field_ctools_views_tags_target_id_op + identifier: field_ctools_views_tags_target_id + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator1: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + reduce_duplicates: false + type: textfield + limit: true + vid: tags + hierarchy: false + error_message: true + plugin_id: taxonomy_index_tid + filter_groups: + operator: AND + groups: + 1: AND + allow: + configure_filters: configure_filters + items_per_page: false + offset: '0' + pager: '0' + hide_fields: '0' + sort_fields: '0' + disable_filters: '0' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - user + - 'user.node_grants:view' + - user.permissions + tags: { } block_filter_date: display_plugin: block id: block_filter_date @@ -236,6 +339,7 @@ display: groups: 1: AND allow: + configure_filters: configure_filters items_per_page: false offset: '0' pager: '0' @@ -331,6 +435,7 @@ display: groups: 1: AND allow: + configure_filters: configure_filters items_per_page: false offset: '0' pager: '0' @@ -432,6 +537,7 @@ display: groups: 1: AND allow: + configure_filters: configure_filters items_per_page: false offset: '0' pager: '0' @@ -526,6 +632,7 @@ display: groups: 1: AND allow: + configure_filters: configure_filters items_per_page: false offset: '0' pager: '0' diff --git a/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_test_view.yml b/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_test_view.yml index 7187b56..0905bfe 100644 --- a/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_test_view.yml +++ b/modules/ctools_views/tests/modules/ctools_views_test_views/test_views/views.view.ctools_views_test_view.yml @@ -420,6 +420,7 @@ display: items_per_page: false offset: '0' pager: '0' + configure_filters: '0' disable_filters: '0' block_description: 'CTools Views Fields Block' display_description: '' @@ -764,6 +765,7 @@ display: block_category: 'CTools Views' block_description: 'CTools Views Filter Block' allow: + configure_filters: configure_filters disable_filters: disable_filters items_per_page: false offset: '0' @@ -879,6 +881,7 @@ display: pager: pager hide_fields: '0' sort_fields: '0' + configure_filters: '0' disable_filters: '0' display_description: '' header: @@ -941,6 +944,7 @@ display: pager: '0' hide_fields: '0' sort_fields: '0' + configure_filters: '0' disable_filters: '0' cache_metadata: max-age: 0 diff --git a/src/ViewsHandlersHelper.php b/src/ViewsHandlersHelper.php new file mode 100644 index 0000000..1c91ab5 --- /dev/null +++ b/src/ViewsHandlersHelper.php @@ -0,0 +1,95 @@ +isAGroup()) { + $converted = $handler->convertExposedInput($value); + $handler->storeGroupInput($value, $converted); + } + else { + $converted = TRUE; + } + + if ($converted) { + // We manually validated all values on submit, so we should tell the + // handler so. Likewise, we manipulated the value we saved and must + // update that as well. + if (property_exists($handler, 'validated_exposed_input')) { + //$value = $value[$handler->options['expose']['identifier']]; + $handler->validated_exposed_input = $value; + } + + // The value passed to acceptExposedInput() can be expecting defaults + // that are not passed with the input values, so we have to attempt to + // merge the expected values on the plugin before overwriting them. + if (is_array($value[$handler->options['expose']['identifier']]) && is_array($handler->options['value'])) { + $value[$handler->options['expose']['identifier']] = $value[$handler->options['expose']['identifier']] + $handler->options['value']; + } + elseif (is_string($value[$handler->options['expose']['identifier']]) && is_array($handler->options['value'])) { + $value[$handler->options['expose']['identifier']] = ['value' => $value[$handler->options['expose']['identifier']]] + $handler->options['value']; + } + + $rc = $handler->acceptExposedInput($value); + $handler->storeExposedInput($value, $rc); + + if (is_array($handler->value) && is_array($handler->options['value'])) { + $handler->options['value'] = $handler->value + $handler->options['value']; + } + else { + $handler->options['value'] = $handler->value; + } + } + } + + /** + * Checks an exposed filter value array to see if it is non-empty and not All. + * + * @todo rename this function and document it more; it doesn't test validity. + * + * @param $value + * @param $handler + * + * @return bool + */ + public function validValue($value, ViewsHandlerInterface $handler) { + $handler_name = $handler->options['id']; + unset($value[$handler->options['expose']['operator']]); + $filter = (bool) array_filter($value, [$this, 'valueFilter']); + $not_all = $value[$handler_name] != 'All'; + $not_empty_or_zero = (!empty($value[$handler_name]) || (is_numeric($value[$handler_name]) && (int) $value[$handler_name] === 0)); + return ($filter && $not_all && $not_empty_or_zero); + } + + /** + * Filter a potential array of values to see if any are non-0 string lengths. + * + * @param mixed $value + * + * @return int + */ + protected function valueFilter($value) { + if (is_array($value)) { + foreach ($value as $key => $element) { + // If any element returns non-0, we know all we need to. + if ($test = $this->valueFilter($element)) { + return $test; + } + } + } + else { + return strlen($value); + } + } +}