diff --git a/core/modules/block_content/config/optional/views.view.block_content.yml b/core/modules/block_content/config/optional/views.view.block_content.yml index 2c008864f3..cc4ec3a2bd 100644 --- a/core/modules/block_content/config/optional/views.view.block_content.yml +++ b/core/modules/block_content/config/optional/views.view.block_content.yml @@ -375,6 +375,8 @@ display: authenticated: authenticated anonymous: '0' administrator: '0' + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -416,6 +418,8 @@ display: anonymous: '0' administrator: '0' reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_exposed_filter_datetime.yml b/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_exposed_filter_datetime.yml new file mode 100644 index 0000000000..cf4af75e41 --- /dev/null +++ b/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_exposed_filter_datetime.yml @@ -0,0 +1,129 @@ +langcode: und +status: true +dependencies: + module: + - datetime + - node +id: test_exposed_filter_datetime +label: test_exposed_filter_datetime +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: '8' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + fields: + nid: + field: nid + id: nid + table: node_field_data + plugin_id: node + filters: + field_date_value: + id: field_date_value + table: node__field_date + field: field_date_value + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: + min: '' + max: '' + value: '' + type: date + group: 1 + exposed: true + expose: + operator_id: field_date_value_op + label: 'field_date (field_date)' + description: '' + use_operator: true + operator: field_date_value_op + operator_limit_selection: true + operator_list: + '=': '=' + '!=': '!=' + identifier: field_date_value + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + placeholder: '' + min_placeholder: '' + max_placeholder: '' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: datetime + sorts: + id: + field: nid + id: nid + order: ASC + relationship: none + table: node_field_data + plugin_id: numeric + pager: + type: full + query: + options: + query_comment: '' + type: views_query + style: + type: default + row: + type: fields + display_extenders: { } + display_plugin: default + display_title: Master + id: default + position: 0 + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: test_exposed_filter_datetime + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + tags: { } + diff --git a/core/modules/datetime/tests/src/Functional/DateFilterTest.php b/core/modules/datetime/tests/src/Functional/DateFilterTest.php new file mode 100644 index 0000000000..37018d8341 --- /dev/null +++ b/core/modules/datetime/tests/src/Functional/DateFilterTest.php @@ -0,0 +1,120 @@ + 'page', + 'name' => 'page', + ]); + $node_type->save(); + $fieldStorage = FieldStorageConfig::create([ + 'field_name' => 'field_date', + 'entity_type' => 'node', + 'type' => 'datetime', + 'settings' => ['datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME], + ]); + $fieldStorage->save(); + $field = FieldConfig::create([ + 'field_storage' => $fieldStorage, + 'bundle' => 'page', + 'required' => TRUE, + ]); + $field->save(); + + $this->adminUser = $this->drupalCreateUser(['administer views']); + $this->drupalLogin($this->adminUser); + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + } + + /** + * Tests the limit of the expose operator functionality. + */ + public function testLimitExposedOperators() { + + $this->drupalGet('test_exposed_filter_datetime'); + $this->assertResponse(200); + $this->assertOption('edit-field-date-value-op', '='); + $this->assertNoOption('edit-field-date-value-op', '>'); + $this->assertNoOption('edit-field-date-value-op', '>='); + + // Because there are not operators that use the min and max fields, those + // fields should not be in the exposed form. + $this->assertFieldById('edit-field-date-value-value'); + $this->assertNoFieldById('edit-field-date-value-min'); + $this->assertNoFieldById('edit-field-date-value-max'); + + $edit = []; + $edit['options[operator]'] = '>'; + $edit['options[expose][operator_list][]'] = ['>', '>=', 'between']; + $this->drupalPostForm('admin/structure/views/nojs/handler/test_exposed_filter_datetime/default/filter/field_date_value', $edit, t('Apply')); + $this->drupalPostForm('admin/structure/views/view/test_exposed_filter_datetime/edit/default', [], t('Save')); + + $this->drupalGet('test_exposed_filter_datetime'); + $this->assertResponse(200); + $this->assertNoOption('edit-field-date-value-op', '<'); + $this->assertNoOption('edit-field-date-value-op', '<='); + $this->assertNoOption('edit-field-date-value-op', '='); + $this->assertOption('edit-field-date-value-op', '>'); + $this->assertOption('edit-field-date-value-op', '>='); + + $this->assertFieldById('edit-field-date-value-value'); + $this->assertFieldById('edit-field-date-value-min'); + $this->assertFieldById('edit-field-date-value-max'); + + // Set the default to an excluded operator. + $edit = []; + $edit['options[operator]'] = '='; + $edit['options[expose][operator_list][]'] = ['<', '>']; + $this->drupalPostForm('admin/structure/views/nojs/handler/test_exposed_filter_datetime/default/filter/field_date_value', $edit, t('Apply')); + $this->assertText('You selected the "Is equal to" operator as the default value but is not included in the list of limited operators.'); + } + +} diff --git a/core/modules/dblog/config/optional/views.view.watchdog.yml b/core/modules/dblog/config/optional/views.view.watchdog.yml index f3751808ba..5781250e0d 100644 --- a/core/modules/dblog/config/optional/views.view.watchdog.yml +++ b/core/modules/dblog/config/optional/views.view.watchdog.yml @@ -582,6 +582,8 @@ display: anonymous: '0' administrator: '0' reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -621,6 +623,8 @@ display: anonymous: '0' administrator: '0' reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/modules/file/config/optional/views.view.files.yml b/core/modules/file/config/optional/views.view.files.yml index 053b72df45..74ad00c31a 100644 --- a/core/modules/file/config/optional/views.view.files.yml +++ b/core/modules/file/config/optional/views.view.files.yml @@ -595,6 +595,8 @@ display: authenticated: authenticated anonymous: '0' administrator: '0' + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -635,6 +637,8 @@ display: authenticated: authenticated anonymous: '0' administrator: '0' + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -676,6 +680,8 @@ display: anonymous: '0' administrator: '0' reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/modules/media/config/optional/views.view.media.yml b/core/modules/media/config/optional/views.view.media.yml index 7cac70aba5..d8ae386184 100644 --- a/core/modules/media/config/optional/views.view.media.yml +++ b/core/modules/media/config/optional/views.view.media.yml @@ -639,6 +639,8 @@ display: authenticated: authenticated anonymous: '0' administrator: '0' + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -680,6 +682,8 @@ display: anonymous: '0' administrator: '0' reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -718,6 +722,8 @@ display: multiple: false remember_roles: authenticated: authenticated + operator_limit_selection: false + operator_list: { } is_grouped: true group_info: label: 'Published status' @@ -767,6 +773,8 @@ display: anonymous: '0' administrator: '0' reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/modules/media_library/config/install/views.view.media_library.yml b/core/modules/media_library/config/install/views.view.media_library.yml index 5c14762aef..c7c0a3acd1 100644 --- a/core/modules/media_library/config/install/views.view.media_library.yml +++ b/core/modules/media_library/config/install/views.view.media_library.yml @@ -212,6 +212,8 @@ display: multiple: false remember_roles: authenticated: authenticated + operator_limit_selection: false + operator_list: { } is_grouped: true group_info: label: Published @@ -260,6 +262,8 @@ display: authenticated: authenticated anonymous: '0' administrator: '0' + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -301,6 +305,8 @@ display: anonymous: '0' administrator: '0' reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: 'Media type' @@ -565,6 +571,8 @@ display: multiple: false remember_roles: authenticated: authenticated + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -605,6 +613,8 @@ display: authenticated: authenticated anonymous: '0' administrator: '0' + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -808,6 +818,8 @@ display: multiple: false remember_roles: authenticated: authenticated + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -848,6 +860,8 @@ display: authenticated: authenticated anonymous: '0' administrator: '0' + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/modules/node/config/optional/views.view.archive.yml b/core/modules/node/config/optional/views.view.archive.yml index 73e0477532..1f65c4f7f5 100644 --- a/core/modules/node/config/optional/views.view.archive.yml +++ b/core/modules/node/config/optional/views.view.archive.yml @@ -110,6 +110,8 @@ display: group: 0 expose: operator: '0' + operator_limit_selection: false + operator_list: { } plugin_id: boolean entity_type: node entity_field: status @@ -138,6 +140,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/modules/node/config/optional/views.view.content.yml b/core/modules/node/config/optional/views.view.content.yml index cef43c7eaa..cadb12c077 100644 --- a/core/modules/node/config/optional/views.view.content.yml +++ b/core/modules/node/config/optional/views.view.content.yml @@ -378,6 +378,8 @@ display: authenticated: authenticated anonymous: '0' administrator: '0' + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -419,6 +421,8 @@ display: anonymous: '0' administrator: '0' reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -457,6 +461,8 @@ display: multiple: false remember_roles: authenticated: authenticated + operator_limit_selection: false + operator_list: { } is_grouped: true group_info: label: 'Published status' @@ -506,6 +512,8 @@ display: anonymous: '0' administrator: '0' reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -530,6 +538,9 @@ display: plugin_id: node_status group: 1 entity_type: node + expose: + operator_limit_selection: false + operator_list: { } sorts: { } title: Content empty: diff --git a/core/modules/node/config/optional/views.view.content_recent.yml b/core/modules/node/config/optional/views.view.content_recent.yml index 1db249ee56..9b28edf251 100644 --- a/core/modules/node/config/optional/views.view.content_recent.yml +++ b/core/modules/node/config/optional/views.view.content_recent.yml @@ -185,6 +185,8 @@ display: multiple: false remember_roles: authenticated: authenticated + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -224,6 +226,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/modules/node/config/optional/views.view.glossary.yml b/core/modules/node/config/optional/views.view.glossary.yml index 10f6d55a04..6ad92fc243 100644 --- a/core/modules/node/config/optional/views.view.glossary.yml +++ b/core/modules/node/config/optional/views.view.glossary.yml @@ -333,6 +333,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/modules/user/config/optional/views.view.user_admin_people.yml b/core/modules/user/config/optional/views.view.user_admin_people.yml index e829a2098f..b93268f47f 100644 --- a/core/modules/user/config/optional/views.view.user_admin_people.yml +++ b/core/modules/user/config/optional/views.view.user_admin_people.yml @@ -601,6 +601,8 @@ display: authenticated: authenticated anonymous: '0' administrator: '0' + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -642,6 +644,8 @@ display: authenticated: authenticated anonymous: '0' administrator: '0' + operator_limit_selection: false + operator_list: { } is_grouped: true group_info: label: Status @@ -691,6 +695,8 @@ display: anonymous: '0' administrator: '0' reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -731,6 +737,8 @@ display: anonymous: '0' administrator: '0' reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -768,6 +776,8 @@ display: multiple: false remember_roles: authenticated: authenticated + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -809,6 +819,8 @@ display: multiple: false remember_roles: authenticated: authenticated + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/modules/user/config/optional/views.view.who_s_new.yml b/core/modules/user/config/optional/views.view.who_s_new.yml index 7d4ed34a8e..eccb1aabad 100644 --- a/core/modules/user/config/optional/views.view.who_s_new.yml +++ b/core/modules/user/config/optional/views.view.who_s_new.yml @@ -95,6 +95,8 @@ display: id: status expose: operator: '0' + operator_limit_selection: false + operator_list: { } group: 1 plugin_id: boolean entity_type: user @@ -126,6 +128,8 @@ display: multiple: false remember_roles: authenticated: authenticated + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/modules/user/config/optional/views.view.who_s_online.yml b/core/modules/user/config/optional/views.view.who_s_online.yml index 31160361a6..66c94e8cfc 100644 --- a/core/modules/user/config/optional/views.view.who_s_online.yml +++ b/core/modules/user/config/optional/views.view.who_s_online.yml @@ -102,6 +102,8 @@ display: id: status expose: operator: '0' + operator_limit_selection: false + operator_list: { } group: 1 plugin_id: boolean entity_type: user @@ -135,6 +137,8 @@ display: authenticated: authenticated anonymous: '0' administrator: '0' + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/modules/views/config/schema/views.data_types.schema.yml b/core/modules/views/config/schema/views.data_types.schema.yml index 7736a747d8..708e12f6d8 100644 --- a/core/modules/views/config/schema/views.data_types.schema.yml +++ b/core/modules/views/config/schema/views.data_types.schema.yml @@ -701,6 +701,15 @@ views_filter: operator: type: string label: 'Operator' + operator_limit_selection: + type: boolean + label: 'Limit the available operators' + operator_list: + type: sequence + label: 'List of available operators' + sequence: + type: string + label: 'Operator' identifier: type: string label: 'Filter identifier' diff --git a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php index 8821ea85a4..3b6ce63a33 100644 --- a/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php +++ b/core/modules/views/src/Plugin/views/filter/FilterPluginBase.php @@ -128,6 +128,8 @@ protected function defineOptions() { 'description' => ['default' => ''], 'use_operator' => ['default' => FALSE], 'operator' => ['default' => ''], + 'operator_limit_selection' => ['default' => FALSE], + 'operator_list' => ['default' => []], 'identifier' => ['default' => ''], 'required' => ['default' => FALSE], 'remember' => ['default' => FALSE], @@ -559,6 +561,36 @@ public function buildExposeForm(&$form, FormStateInterface $form_state) { '#description' => $this->t('Allow the user to choose the operator.'), '#default_value' => !empty($this->options['expose']['use_operator']), ]; + + $operators = $this->operatorOptions(); + if (!empty($operators) && count($operators) > 1) { + $form['expose']['operator_limit_selection'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Limit the available operators'), + '#description' => $this->t('Limit the available operators to be shown on the exposed filter.'), + '#default_value' => !empty($this->options['expose']['operator_limit_selection']), + '#states' => [ + 'visible' => [ + ':input[name="options[expose][use_operator]"]' => ['checked' => TRUE], + ], + ], + ]; + $form['expose']['operator_list'] = [ + '#type' => 'select', + '#title' => $this->t('Restrict operators to'), + '#default_value' => $this->options['expose']['operator_list'], + '#options' => $operators, + '#multiple' => TRUE, + '#description' => $this->t('Only the selected operators will be shown on the exposed filter.'), + '#states' => [ + 'visible' => [ + ':input[name="options[expose][operator_limit_selection]"]' => ['checked' => TRUE], + ':input[name="options[expose][use_operator]"]' => ['checked' => TRUE], + ], + ], + ]; + } + $form['expose']['operator_id'] = [ '#type' => 'textfield', '#default_value' => $this->options['expose']['operator_id'], @@ -623,6 +655,15 @@ public function buildExposeForm(&$form, FormStateInterface $form_state) { public function validateExposeForm($form, FormStateInterface $form_state) { $identifier = $form_state->getValue(['options', 'expose', 'identifier']); $this->validateIdentifier($identifier, $form_state, $form['expose']['identifier']); + + $limit_operators = $form_state->getValue(['options', 'expose', 'operator_limit_selection']); + $operators_selected = $form_state->getValue(['options', 'expose', 'operator_list']); + $selected_operator = $form_state->getValue(['options', 'operator']); + if ($limit_operators && !in_array($selected_operator, $operators_selected)) { + $form_state->setError( + $form['expose']['operator_list'], + $this->t('You selected the "@operator" operator as the default value but is not included in the list of limited operators.', ['@operator' => $this->operatorOptions()[$selected_operator]])); + } } /** @@ -763,6 +804,8 @@ public function defaultExposeOptions() { $this->options['expose'] = [ 'use_operator' => FALSE, 'operator' => $this->options['id'] . '_op', + 'operator_limit_selection' => FALSE, + 'operator_list' => [], 'identifier' => $this->options['id'], 'label' => $this->definition['title'], 'description' => NULL, @@ -848,6 +891,15 @@ public function buildExposedForm(&$form, FormStateInterface $form_state) { if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id'])) { $operator = $this->options['expose']['operator_id']; $this->operatorForm($form, $form_state); + + // Limit the exposed operators if needed. + if (!empty($this->options['expose']['operator_limit_selection']) && + !empty($this->options['expose']['operator_list'])) { + + $options = $this->operatorOptions(); + $operator_list = $this->options['expose']['operator_list']; + $form['operator']['#options'] = array_intersect_key($options, $operator_list); + } $form[$operator] = $form['operator']; $this->exposedTranslate($form[$operator], 'operator'); diff --git a/core/modules/views/src/Plugin/views/filter/NumericFilter.php b/core/modules/views/src/Plugin/views/filter/NumericFilter.php index d2a6ab0997..3aba8c2f29 100644 --- a/core/modules/views/src/Plugin/views/filter/NumericFilter.php +++ b/core/modules/views/src/Plugin/views/filter/NumericFilter.php @@ -258,7 +258,23 @@ protected function valueForm(&$form, FormStateInterface $form_state) { } } - if ($which == 'all' || $which == 'minmax') { + // Minimum and maximum fields associated to some particular operators, + // like 'between'. Ensure that minmax files are only visible if the + // operator is not excluded from the operator list. + $two_value_operators_available = ($which == 'all' || $which == 'minmax'); + + if (!empty($this->options['expose']['operator_limit_selection']) && + !empty($this->options['expose']['operator_list'])) { + $two_value_operators_available = FALSE; + foreach ($this->options['expose']['operator_list'] as $operator) { + if (in_array($operator, $this->operatorValues(2), TRUE)) { + $two_value_operators_available = TRUE; + break; + } + } + } + + if ($two_value_operators_available) { $form['value']['min'] = [ '#type' => 'textfield', '#title' => !$exposed ? $this->t('Min') : $this->exposedInfo()['label'], diff --git a/core/modules/views/tests/fixtures/update/limit-exposed-operators.php b/core/modules/views/tests/fixtures/update/limit-exposed-operators.php new file mode 100644 index 0000000000..92c173aea1 --- /dev/null +++ b/core/modules/views/tests/fixtures/update/limit-exposed-operators.php @@ -0,0 +1,19 @@ +insert('config') + ->fields([ + 'collection' => '', + 'name' => 'views.view.test_exposed_filters', + 'data' => serialize(Yaml::decode(file_get_contents('core/modules/views/tests/fixtures/update/views.view.test_exposed_filters.yml'))), + ]) + ->execute(); diff --git a/core/modules/views/tests/fixtures/update/views.view.test_exposed_filters.yml b/core/modules/views/tests/fixtures/update/views.view.test_exposed_filters.yml new file mode 100644 index 0000000000..c851c39371 --- /dev/null +++ b/core/modules/views/tests/fixtures/update/views.view.test_exposed_filters.yml @@ -0,0 +1,272 @@ +langcode: en +status: true +dependencies: + module: + - node + - user +id: test_exposed_filters +label: 'Test Exposed filters' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + row: + type: fields + options: + default_field_elements: true + inline: { } + separator: '' + hide_empty: false + fields: + title: + id: title + table: node_field_data + field: title + entity_type: node + entity_field: title + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: true + plugin_id: field + relationship: none + group_type: group + admin_label: '' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + filters: + status: + value: '1' + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + title: + id: title + table: node_field_data + field: title + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '' + group: 1 + exposed: true + expose: + operator_id: title_op + label: Title + description: '' + use_operator: true + operator: title_op + identifier: title + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + placeholder: '' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + entity_field: title + plugin_id: string + created: + id: created + table: node_field_data + field: created + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: + min: '' + max: '' + value: '' + type: date + group: 1 + exposed: true + expose: + operator_id: created_op + label: 'Authored on' + description: '' + use_operator: true + operator: created_op + identifier: created + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + placeholder: '' + min_placeholder: '' + max_placeholder: '' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + entity_field: created + plugin_id: date + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + title: 'Test Exposed filters' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: test-exposed-filters + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_filter_in_operator_ui.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_filter_in_operator_ui.yml index ef0188af90..eaaa828359 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_filter_in_operator_ui.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_filter_in_operator_ui.yml @@ -1,12 +1,15 @@ langcode: en status: true -dependencies: { } +dependencies: + module: + - node id: test_filter_in_operator_ui label: '' module: views description: '' tag: '' base_table: node_field_data +base_field: nid core: '8' display: default: @@ -17,6 +20,38 @@ display: type: tag exposed_form: type: basic + fields: + nid: + id: nid + table: node_field_data + field: nid + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: nid + plugin_id: field filters: type: expose: @@ -32,13 +67,90 @@ display: plugin_id: in_operator entity_type: node entity_field: type + nid: + id: nid + table: node_field_data + field: nid + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: + min: '' + max: '' + value: '' + group: 1 + exposed: true + expose: + operator_id: nid_op + label: ID + description: '' + use_operator: true + operator: nid_op + operator_limit_selection: true + operator_list: + '<': '<' + '<=': '<=' + '=': '=' + identifier: nid + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + entity_field: nid + plugin_id: numeric pager: type: full style: type: default row: type: fields + display_extenders: { } + display_plugin: default display_title: Master id: default position: 0 + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + tags: { } + page_1: + display_options: + path: test_filter_in_operator_ui + display_extenders: { } + display_plugin: page + display_title: Page + id: page_1 + position: 0 + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + tags: { } + diff --git a/core/modules/views/tests/src/Functional/Plugin/FilterTest.php b/core/modules/views/tests/src/Functional/Plugin/FilterTest.php index 60424108f5..a08bad8438 100644 --- a/core/modules/views/tests/src/Functional/Plugin/FilterTest.php +++ b/core/modules/views/tests/src/Functional/Plugin/FilterTest.php @@ -164,4 +164,49 @@ public function testInOperatorSelectAllOptions() { $this->assertNoText('An illegal choice has been detected.'); } + /** + * Tests the limit of the expose operator functionality. + */ + public function testLimitExposedOperators() { + + $this->drupalGet('test_filter_in_operator_ui'); + $this->assertResponse(200); + $this->assertOption('edit-nid-op', '<'); + $this->assertOption('edit-nid-op', '<='); + $this->assertOption('edit-nid-op', '='); + $this->assertNoOption('edit-nid-op', '>'); + $this->assertNoOption('edit-nid-op', '>='); + + // Because there are not operators that use the min and max fields, those + // fields should not be in the exposed form. + $this->assertFieldById('edit-nid-value'); + $this->assertNoFieldById('edit-nid-min'); + $this->assertNoFieldById('edit-nid-max'); + + $edit = []; + $edit['options[operator]'] = '>'; + $edit['options[expose][operator_list][]'] = ['>', '>=', 'between']; + $this->drupalPostForm('admin/structure/views/nojs/handler/test_filter_in_operator_ui/default/filter/nid', $edit, t('Apply')); + $this->drupalPostForm('admin/structure/views/view/test_filter_in_operator_ui/edit/default', [], t('Save')); + + $this->drupalGet('test_filter_in_operator_ui'); + $this->assertResponse(200); + $this->assertNoOption('edit-nid-op', '<'); + $this->assertNoOption('edit-nid-op', '<='); + $this->assertNoOption('edit-nid-op', '='); + $this->assertOption('edit-nid-op', '>'); + $this->assertOption('edit-nid-op', '>='); + + $this->assertFieldById('edit-nid-value'); + $this->assertFieldById('edit-nid-min'); + $this->assertFieldById('edit-nid-max'); + + // Set the default to an excluded operator. + $edit = []; + $edit['options[operator]'] = '='; + $edit['options[expose][operator_list][]'] = ['<', '>']; + $this->drupalPostForm('admin/structure/views/nojs/handler/test_filter_in_operator_ui/default/filter/nid', $edit, t('Apply')); + $this->assertText('You selected the "Is equal to" operator as the default value but is not included in the list of limited operators.'); + } + } diff --git a/core/modules/views/tests/src/Functional/Update/LimitOperatorsDefaultsTest.php b/core/modules/views/tests/src/Functional/Update/LimitOperatorsDefaultsTest.php new file mode 100644 index 0000000000..64c74a4c3a --- /dev/null +++ b/core/modules/views/tests/src/Functional/Update/LimitOperatorsDefaultsTest.php @@ -0,0 +1,61 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz', + __DIR__ . '/../../../fixtures/update/limit-exposed-operators.php', + ]; + } + + /** + * Tests that default settings for limit operators are present. + */ + public function testViewsPostUpdateLimitOperatorsDefaultValues() { + // Load and initialize our test view. + $view = View::load('test_exposed_filters'); + $data = $view->toArray(); + + // Check that the filters have no defaults values to limit operators. + $title_filter = $data['display']['default']['display_options']['filters']['title']['expose']; + $this->assertArrayNotHasKey('operator_limit_selection', $title_filter); + $this->assertArrayNotHasKey('operator_list', $title_filter); + + $created_filter = $data['display']['default']['display_options']['filters']['created']['expose']; + $this->assertArrayNotHasKey('operator_limit_selection', $created_filter); + $this->assertArrayNotHasKey('operator_list', $created_filter); + + $this->runUpdates(); + + // Load and initialize our test view. + $view = View::load('test_exposed_filters'); + $data = $view->toArray(); + + // Check that the filters have defaults values to limit operators. + $title_filter = $data['display']['default']['display_options']['filters']['title']['expose']; + $this->assertIdentical(FALSE, $title_filter['operator_limit_selection']); + $this->assertIdentical([], $title_filter['operator_list']); + + $created_filter = $data['display']['default']['display_options']['filters']['created']['expose']; + $this->assertIdentical(FALSE, $created_filter['operator_limit_selection']); + $this->assertIdentical([], $created_filter['operator_list']); + } + +} diff --git a/core/modules/views/views.module b/core/modules/views/views.module index 9127c388f5..75393959c2 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -894,6 +894,18 @@ function views_view_presave(ViewEntityInterface $view) { } } } + if (isset($display['display_options']['filters'])) { + foreach ($display['display_options']['filters'] as $filter_name => &$filter) { + if (!isset($filter['expose']['operator_limit_selection'])) { + $filter['expose']['operator_limit_selection'] = FALSE; + $changed = TRUE; + } + if (!isset($filter['expose']['operator_list'])) { + $filter['expose']['operator_list'] = []; + $changed = TRUE; + } + } + } } if ($changed) { $view->set('display', $displays); diff --git a/core/modules/views/views.post_update.php b/core/modules/views/views.post_update.php index 30749fcba8..878eaa0305 100644 --- a/core/modules/views/views.post_update.php +++ b/core/modules/views/views.post_update.php @@ -389,3 +389,35 @@ function views_post_update_make_placeholders_translatable() { // Empty update to cause a cache rebuild to allow placeholder texts to be // translatable. } + +/** + * Define default values for limit operators settings in all filters. + */ +function views_post_update_limit_operator_defaults(&$sandbox = NULL) { + \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function ($view) { + /** @var \Drupal\views\ViewEntityInterface $view */ + $displays = $view->get('display'); + + $update = FALSE; + foreach ($displays as $display_name => &$display) { + if (!isset($display['display_options']['filters'])) { + continue; + } + + foreach ($display['display_options']['filters'] as $filter_name => $filter) { + if (!isset($filter['expose']['operator_limit_selection'])) { + $filter['expose']['operator_limit_selection'] = FALSE; + $update = TRUE; + } + if (!isset($filter['expose']['operator_list'])) { + $filter['expose']['operator_list'] = []; + $update = TRUE; + } + if ($update) { + $view->set("display.$display_name.display_options.filters.$filter_name", $filter); + } + } + } + return $update; + }); +} diff --git a/core/profiles/demo_umami/config/install/views.view.articles_aside.yml b/core/profiles/demo_umami/config/install/views.view.articles_aside.yml index c21651d1fb..d93e56ca4b 100644 --- a/core/profiles/demo_umami/config/install/views.view.articles_aside.yml +++ b/core/profiles/demo_umami/config/install/views.view.articles_aside.yml @@ -118,6 +118,8 @@ display: id: status expose: operator: '' + operator_limit_selection: false + operator_list: { } group: 1 type: id: type @@ -128,6 +130,9 @@ display: entity_type: node entity_field: type plugin_id: bundle + expose: + operator_limit_selection: false + operator_list: { } langcode: id: langcode table: node_field_data @@ -153,6 +158,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/profiles/demo_umami/config/install/views.view.featured_articles.yml b/core/profiles/demo_umami/config/install/views.view.featured_articles.yml index 9de30a3fe2..acb565ea72 100644 --- a/core/profiles/demo_umami/config/install/views.view.featured_articles.yml +++ b/core/profiles/demo_umami/config/install/views.view.featured_articles.yml @@ -132,6 +132,8 @@ display: id: status expose: operator: '' + operator_limit_selection: false + operator_list: { } group: 1 type: id: type @@ -142,6 +144,9 @@ display: entity_type: node entity_field: type plugin_id: bundle + expose: + operator_limit_selection: false + operator_list: { } langcode: id: langcode table: node_field_data @@ -167,6 +172,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/profiles/demo_umami/config/install/views.view.frontpage.yml b/core/profiles/demo_umami/config/install/views.view.frontpage.yml index a97299283b..f7a5e9efd9 100644 --- a/core/profiles/demo_umami/config/install/views.view.frontpage.yml +++ b/core/profiles/demo_umami/config/install/views.view.frontpage.yml @@ -86,6 +86,8 @@ display: authenticated: authenticated required: false use_operator: false + operator_limit_selection: false + operator_list: { } exposed: false field: promote group: 1 @@ -113,6 +115,8 @@ display: status: expose: operator: '' + operator_limit_selection: false + operator_list: { } field: status group: 1 id: status @@ -146,6 +150,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -186,6 +192,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/profiles/demo_umami/config/install/views.view.promoted_items.yml b/core/profiles/demo_umami/config/install/views.view.promoted_items.yml index 6feb93d70f..7dcc0cb20b 100644 --- a/core/profiles/demo_umami/config/install/views.view.promoted_items.yml +++ b/core/profiles/demo_umami/config/install/views.view.promoted_items.yml @@ -120,6 +120,8 @@ display: id: status expose: operator: '' + operator_limit_selection: false + operator_list: { } group: 1 promote: id: promote @@ -144,6 +146,8 @@ display: multiple: false remember_roles: authenticated: authenticated + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -185,6 +189,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -273,6 +279,8 @@ display: id: status expose: operator: '' + operator_limit_selection: false + operator_list: { } group: 1 promote: id: promote @@ -297,6 +305,8 @@ display: multiple: false remember_roles: authenticated: authenticated + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -337,6 +347,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -377,6 +389,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -428,6 +442,8 @@ display: id: status expose: operator: '' + operator_limit_selection: false + operator_list: { } group: 1 promote: id: promote @@ -452,6 +468,8 @@ display: multiple: false remember_roles: authenticated: authenticated + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -492,6 +510,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -532,6 +552,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/profiles/demo_umami/config/install/views.view.recipe_collections.yml b/core/profiles/demo_umami/config/install/views.view.recipe_collections.yml index c2515a085d..ca1bf787ff 100644 --- a/core/profiles/demo_umami/config/install/views.view.recipe_collections.yml +++ b/core/profiles/demo_umami/config/install/views.view.recipe_collections.yml @@ -123,6 +123,9 @@ display: entity_type: taxonomy_term entity_field: vid plugin_id: bundle + expose: + operator_limit_selection: false + operator_list: { } langcode: id: langcode table: taxonomy_term_field_data @@ -148,6 +151,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/profiles/demo_umami/config/install/views.view.recipes.yml b/core/profiles/demo_umami/config/install/views.view.recipes.yml index 353b3266a0..95b6b885e1 100644 --- a/core/profiles/demo_umami/config/install/views.view.recipes.yml +++ b/core/profiles/demo_umami/config/install/views.view.recipes.yml @@ -132,6 +132,8 @@ display: id: status expose: operator: '' + operator_limit_selection: false + operator_list: { } group: 1 type: id: type @@ -142,6 +144,9 @@ display: entity_type: node entity_field: type plugin_id: bundle + expose: + operator_limit_selection: false + operator_list: { } langcode: id: langcode table: node_field_data @@ -167,6 +172,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' diff --git a/core/profiles/demo_umami/config/install/views.view.taxonomy_term.yml b/core/profiles/demo_umami/config/install/views.view.taxonomy_term.yml index d3f2d8e8b9..9f3daa6aea 100644 --- a/core/profiles/demo_umami/config/install/views.view.taxonomy_term.yml +++ b/core/profiles/demo_umami/config/install/views.view.taxonomy_term.yml @@ -159,6 +159,8 @@ display: remember_roles: authenticated: authenticated reduce: false + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: '' @@ -197,6 +199,8 @@ display: multiple: false remember_roles: authenticated: authenticated + operator_limit_selection: false + operator_list: { } is_grouped: false group_info: label: ''