summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLee Rowlands2018-01-13 07:37:08 (GMT)
committerLee Rowlands2018-01-13 07:37:08 (GMT)
commit418e4991b59dbc7cf063ab30bb209b027a7eb288 (patch)
tree5c25b4fc7e9f05325a8a26da0982d6e9d50d4bd6
parent45662bb9e2583b5f744ff9b037a51c02b6a91c5b (diff)
Issue #2862041 by Sam152, amateescu, timmillwood, jibran, hamrant, dawehner, plach, Manuel Garcia, Jo Fitzgerald, mgalalm, xjm, larowlan, webchick, Wim Leers, gambry, kkus, tim.plunkett, tedbow, CatherineOmega: Provide useful Views filters for Content Moderation State fields
-rw-r--r--core/modules/content_moderation/config/schema/content_moderation.views.schema.yml18
-rw-r--r--core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php291
-rw-r--r--core/modules/content_moderation/src/ViewsData.php2
-rw-r--r--core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_base_table.yml261
-rw-r--r--core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_base_table_filter_on_revision.yml223
-rw-r--r--core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_entity_test.yml168
-rw-r--r--core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_revision_table.yml218
-rw-r--r--core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.info.yml1
-rw-r--r--core/modules/content_moderation/tests/src/Functional/ViewsModerationStateFilterTest.php310
-rw-r--r--core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php298
-rw-r--r--core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoBundle.php3
-rw-r--r--core/modules/views/src/Plugin/views/filter/InOperator.php4
-rw-r--r--core/modules/workflows/src/WorkflowTypeInterface.php2
13 files changed, 1797 insertions, 2 deletions
diff --git a/core/modules/content_moderation/config/schema/content_moderation.views.schema.yml b/core/modules/content_moderation/config/schema/content_moderation.views.schema.yml
new file mode 100644
index 0000000..c9ae1af
--- /dev/null
+++ b/core/modules/content_moderation/config/schema/content_moderation.views.schema.yml
@@ -0,0 +1,18 @@
+views.filter.latest_revision:
+ type: views_filter
+ label: 'Latest revision'
+ mapping:
+ value:
+ type: string
+ label: 'Value'
+
+views.filter.moderation_state_filter:
+ type: views.filter.in_operator
+ label: 'Moderation state filter'
+ mapping:
+ limit_workflows:
+ type: sequence
+ label: 'Workflow'
+ sequence:
+ type: string
+ label: 'Workflow'
diff --git a/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
new file mode 100644
index 0000000..035ad3e
--- /dev/null
+++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
@@ -0,0 +1,291 @@
+<?php
+
+namespace Drupal\content_moderation\Plugin\views\filter;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Database\Query\Condition;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\views\Plugin\DependentWithRemovalPluginInterface;
+use Drupal\views\Plugin\views\filter\InOperator;
+use Drupal\views\Views;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a filter for the moderation state of an entity.
+ *
+ * @ingroup views_filter_handlers
+ *
+ * @ViewsFilter("moderation_state_filter")
+ */
+class ModerationStateFilter extends InOperator implements DependentWithRemovalPluginInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $valueFormType = 'select';
+
+ /**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * The bundle information service.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+ */
+ protected $bundleInfo;
+
+ /**
+ * The storage handler of the workflow entity type.
+ *
+ * @var \Drupal\Core\Entity\EntityStorageInterface
+ */
+ protected $workflowStorage;
+
+ /**
+ * Creates an instance of ModerationStateFilter.
+ */
+ public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info, EntityStorageInterface $workflow_storage) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ $this->entityTypeManager = $entity_type_manager;
+ $this->bundleInfo = $bundle_info;
+ $this->workflowStorage = $workflow_storage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('entity_type.manager'),
+ $container->get('entity_type.bundle.info'),
+ $container->get('entity_type.manager')->getStorage('workflow')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheTags() {
+ return Cache::mergeTags(parent::getCacheTags(), $this->entityTypeManager->getDefinition('workflow')->getListCacheTags());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheContexts() {
+ return Cache::mergeContexts(parent::getCacheContexts(), $this->entityTypeManager->getDefinition('workflow')->getListCacheContexts());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValueOptions() {
+ if (isset($this->valueOptions)) {
+ return $this->valueOptions;
+ }
+ $this->valueOptions = [];
+
+ // Find all workflows which are moderating entity types of the same type the
+ // view is displaying.
+ foreach ($this->workflowStorage->loadByProperties(['type' => 'content_moderation']) as $workflow) {
+ /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModerationInterface $workflow_type */
+ $workflow_type = $workflow->getTypePlugin();
+ if (in_array($this->getEntityType(), $workflow_type->getEntityTypes(), TRUE)) {
+ foreach ($workflow_type->getStates() as $state_id => $state) {
+ $this->valueOptions[$workflow->label()][implode('-', [$workflow->id(), $state_id])] = $state->label();
+ }
+ }
+ }
+
+ return $this->valueOptions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function ensureMyTable() {
+ if (!isset($this->tableAlias)) {
+ $table_alias = $this->query->ensureTable($this->table, $this->relationship);
+
+ // Filter the moderation states of the content via the
+ // ContentModerationState field revision table, joining either the entity
+ // field data or revision table. This allows filtering states against
+ // either the default or latest revision, depending on the relationship of
+ // the filter.
+ $left_entity_type = $this->entityTypeManager->getDefinition($this->getEntityType());
+ $entity_type = $this->entityTypeManager->getDefinition('content_moderation_state');
+ $configuration = [
+ 'table' => $entity_type->getRevisionDataTable(),
+ 'field' => 'content_entity_revision_id',
+ 'left_table' => $table_alias,
+ 'left_field' => $left_entity_type->getKey('revision'),
+ 'extra' => [
+ [
+ 'field' => 'content_entity_type_id',
+ 'value' => $left_entity_type->id(),
+ ],
+ ],
+ ];
+ if ($left_entity_type->isTranslatable()) {
+ $configuration['extra'][] = [
+ 'field' => $entity_type->getKey('langcode'),
+ 'left_field' => $left_entity_type->getKey('langcode'),
+ ];
+ }
+ $join = Views::pluginManager('join')->createInstance('standard', $configuration);
+ $this->tableAlias = $this->query->addRelationship('content_moderation_state', $join, 'content_moderation_state_field_revision');
+ }
+
+ return $this->tableAlias;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function opSimple() {
+ if (empty($this->value)) {
+ return;
+ }
+
+ $this->ensureMyTable();
+
+ $entity_type = $this->entityTypeManager->getDefinition($this->getEntityType());
+ if ($entity_type->hasKey('bundle')) {
+ // Get a list of bundles that are being moderated by the workflows
+ // configured in this filter.
+ $workflow_ids = $this->getWorkflowIds();
+ $moderated_bundles = [];
+ foreach ($this->bundleInfo->getBundleInfo($this->getEntityType()) as $bundle_id => $bundle) {
+ if (isset($bundle['workflow']) && in_array($bundle['workflow'], $workflow_ids, TRUE)) {
+ $moderated_bundles[] = $bundle_id;
+ }
+ }
+
+ // If we have a list of moderated bundles, restrict the query to show only
+ // entities in those bundles.
+ if ($moderated_bundles) {
+ $entity_base_table_alias = $this->table;
+
+ // The bundle field of an entity type is not revisionable so we need to
+ // join the data table.
+ $entity_base_table = $entity_type->isTranslatable() ? $entity_type->getDataTable() : $entity_type->getBaseTable();
+ $entity_revision_base_table = $entity_type->isTranslatable() ? $entity_type->getRevisionDataTable() : $entity_type->getRevisionTable();
+ if ($this->table === $entity_revision_base_table) {
+ $configuration = [
+ 'table' => $entity_base_table,
+ 'field' => $entity_type->getKey('id'),
+ 'left_table' => $entity_revision_base_table,
+ 'left_field' => $entity_type->getKey('id'),
+ 'type' => 'INNER',
+ ];
+ if ($entity_type->isTranslatable()) {
+ $configuration['extra'][] = [
+ 'field' => $entity_type->getKey('langcode'),
+ 'left_field' => $entity_type->getKey('langcode'),
+ ];
+ }
+
+ $join = Views::pluginManager('join')->createInstance('standard', $configuration);
+ $entity_base_table_alias = $this->query->addRelationship($entity_base_table, $join, $entity_revision_base_table);
+ }
+
+ $this->query->addWhere($this->options['group'], "$entity_base_table_alias.{$entity_type->getKey('bundle')}", $moderated_bundles, 'IN');
+ }
+ // Otherwise, force the query to return an empty result.
+ else {
+ $this->query->addWhereExpression($this->options['group'], '1 = 0');
+ return;
+ }
+ }
+
+ if ($this->operator === 'in') {
+ $operator = "=";
+ }
+ else {
+ $operator = "<>";
+ }
+
+ // The values are strings composed from the workflow ID and the state ID, so
+ // we need to create a complex WHERE condition.
+ $field = new Condition('OR');
+ foreach ((array) $this->value as $value) {
+ list($workflow_id, $state_id) = explode('-', $value, 2);
+
+ $and = new Condition('AND');
+ $and
+ ->condition("$this->tableAlias.workflow", $workflow_id, '=')
+ ->condition("$this->tableAlias.$this->realField", $state_id, $operator);
+
+ $field->condition($and);
+ }
+
+ $this->query->addWhere($this->options['group'], $field);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $dependencies = parent::calculateDependencies();
+
+ if ($workflow_ids = $this->getWorkflowIds()) {
+ /** @var \Drupal\workflows\WorkflowInterface $workflow */
+ foreach ($this->workflowStorage->loadMultiple($workflow_ids) as $workflow) {
+ $dependencies[$workflow->getConfigDependencyKey()][] = $workflow->getConfigDependencyName();
+ }
+ }
+
+ return $dependencies;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onDependencyRemoval(array $dependencies) {
+ // See if this handler is responsible for any of the dependencies being
+ // removed. If this is the case, indicate that this handler needs to be
+ // removed from the View.
+ $remove = FALSE;
+ // Get all the current dependencies for this handler.
+ $current_dependencies = $this->calculateDependencies();
+ foreach ($current_dependencies as $group => $dependency_list) {
+ // Check if any of the handler dependencies match the dependencies being
+ // removed.
+ foreach ($dependency_list as $config_key) {
+ if (isset($dependencies[$group]) && array_key_exists($config_key, $dependencies[$group])) {
+ // This handlers dependency matches a dependency being removed,
+ // indicate that this handler needs to be removed.
+ $remove = TRUE;
+ break 2;
+ }
+ }
+ }
+ return $remove;
+ }
+
+ /**
+ * Gets the list of Workflow IDs configured for this filter.
+ *
+ * @return array
+ * And array of workflow IDs.
+ */
+ protected function getWorkflowIds() {
+ $workflow_ids = [];
+ foreach ((array) $this->value as $value) {
+ list($workflow_id) = explode('-', $value, 2);
+ $workflow_ids[] = $workflow_id;
+ }
+
+ return array_unique($workflow_ids);
+ }
+
+}
diff --git a/core/modules/content_moderation/src/ViewsData.php b/core/modules/content_moderation/src/ViewsData.php
index 9ade858..40be303 100644
--- a/core/modules/content_moderation/src/ViewsData.php
+++ b/core/modules/content_moderation/src/ViewsData.php
@@ -83,6 +83,7 @@ class ViewsData {
'default_formatter' => 'content_moderation_state',
'field_name' => 'moderation_state',
],
+ 'filter' => ['id' => 'moderation_state_filter', 'allow empty' => TRUE],
];
$revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
@@ -106,6 +107,7 @@ class ViewsData {
'default_formatter' => 'content_moderation_state',
'field_name' => 'moderation_state',
],
+ 'filter' => ['id' => 'moderation_state_filter', 'allow empty' => TRUE],
];
}
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_base_table.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_base_table.yml
new file mode 100644
index 0000000..e844f47
--- /dev/null
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_base_table.yml
@@ -0,0 +1,261 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - content_moderation
+ - node
+ - user
+id: test_content_moderation_state_filter_base_table
+label: test_content_moderation_state_filter_base_table
+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: none
+ options:
+ offset: 0
+ style:
+ type: default
+ options:
+ grouping: { }
+ row_class: ''
+ default_row_class: true
+ uses_fields: false
+ row:
+ type: fields
+ options:
+ inline: { }
+ separator: ''
+ hide_empty: false
+ default_field_elements: true
+ fields:
+ nid:
+ id: nid
+ table: node_field_data
+ field: nid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: ''
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: false
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: number_integer
+ settings:
+ thousand_separator: ''
+ prefix_suffix: false
+ 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:
+ moderation_state:
+ id: moderation_state
+ table: node_field_data
+ field: moderation_state
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: in
+ value: { }
+ group: 1
+ exposed: true
+ expose:
+ operator_id: moderation_state_op
+ label: 'Default Revision State'
+ description: ''
+ use_operator: false
+ operator: moderation_state_op
+ identifier: default_revision_state
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '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: { }
+ entity_type: node
+ plugin_id: moderation_state_filter
+ moderation_state_1:
+ id: moderation_state_1
+ table: node_field_data
+ field: moderation_state
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: 'not empty'
+ value: { }
+ group: 1
+ exposed: false
+ expose:
+ operator_id: ''
+ label: ''
+ description: ''
+ use_operator: false
+ operator: ''
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ 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: { }
+ entity_type: node
+ plugin_id: moderation_state_filter
+ sorts:
+ nid:
+ id: nid
+ table: node_field_data
+ field: nid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: ASC
+ exposed: false
+ expose:
+ label: ''
+ entity_type: node
+ entity_field: nid
+ plugin_id: standard
+ header: { }
+ footer: { }
+ empty: { }
+ relationships: { }
+ arguments: { }
+ display_extenders: { }
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - 'user.node_grants:view'
+ - user.permissions
+ tags:
+ - 'config:workflow_list'
+ page_1:
+ display_plugin: page
+ id: page_1
+ display_title: Page
+ position: 1
+ display_options:
+ display_extenders: { }
+ path: filter-test-path
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - 'user.node_grants:view'
+ - user.permissions
+ tags:
+ - 'config:workflow_list'
+
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_base_table_filter_on_revision.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_base_table_filter_on_revision.yml
new file mode 100644
index 0000000..548e6db
--- /dev/null
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_base_table_filter_on_revision.yml
@@ -0,0 +1,223 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - content_moderation
+ - node
+ - user
+id: test_content_moderation_state_filter_base_table_filter_on_revision
+label: test_content_moderation_state_filter_base_table_filter_on_revision
+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: none
+ options:
+ offset: 0
+ style:
+ type: default
+ options:
+ grouping: { }
+ row_class: ''
+ default_row_class: true
+ uses_fields: false
+ row:
+ type: fields
+ options:
+ inline: { }
+ separator: ''
+ hide_empty: false
+ default_field_elements: true
+ fields:
+ nid:
+ id: nid
+ table: node_field_data
+ field: nid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: ''
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: false
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: number_integer
+ settings:
+ thousand_separator: ''
+ prefix_suffix: false
+ 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:
+ moderation_state:
+ id: moderation_state
+ table: node_field_revision
+ field: moderation_state
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: in
+ value: { }
+ group: 1
+ exposed: true
+ expose:
+ operator_id: moderation_state_op
+ label: 'Moderation state'
+ description: ''
+ use_operator: false
+ operator: moderation_state_op
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '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: { }
+ entity_type: node
+ plugin_id: moderation_state_filter
+ sorts:
+ nid:
+ id: nid
+ table: node_field_data
+ field: nid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: ASC
+ exposed: false
+ expose:
+ label: ''
+ entity_type: node
+ entity_field: nid
+ plugin_id: standard
+ header: { }
+ footer: { }
+ empty: { }
+ relationships: { }
+ arguments: { }
+ display_extenders: { }
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - 'user.node_grants:view'
+ - user.permissions
+ tags:
+ - 'config:workflow_list'
+ page_1:
+ display_plugin: page
+ id: page_1
+ display_title: Page
+ position: 1
+ display_options:
+ display_extenders: { }
+ path: filter-on-revision-test-path
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - 'user.node_grants:view'
+ - user.permissions
+ tags:
+ - 'config:workflow_list'
+
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_entity_test.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_entity_test.yml
new file mode 100644
index 0000000..0683b98
--- /dev/null
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_entity_test.yml
@@ -0,0 +1,168 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - content_moderation
+ - entity_test
+id: test_content_moderation_state_filter_entity_test
+label: test_content_moderation_state_filter_entity_test
+module: views
+description: ''
+tag: ''
+base_table: entity_test_no_bundle
+base_field: id
+core: 8.x
+display:
+ default:
+ display_plugin: default
+ id: default
+ display_title: Master
+ position: 0
+ display_options:
+ access:
+ type: none
+ options: { }
+ 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: none
+ options:
+ offset: 0
+ style:
+ type: default
+ options:
+ grouping: { }
+ row_class: ''
+ default_row_class: true
+ uses_fields: false
+ row:
+ type: fields
+ options:
+ inline: { }
+ separator: ''
+ hide_empty: false
+ default_field_elements: true
+ fields:
+ entity_id:
+ id: entity_id
+ table: content_revision_tracker
+ field: entity_id
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: ''
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: false
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ plugin_id: standard
+ filters:
+ moderation_state:
+ id: moderation_state
+ table: entity_test_no_bundle
+ field: moderation_state
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: in
+ value: { }
+ group: 1
+ exposed: true
+ expose:
+ operator_id: moderation_state_op
+ label: 'Moderation state'
+ description: ''
+ use_operator: false
+ operator: moderation_state_op
+ identifier: moderation_state
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '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: { }
+ entity_type: entity_test_no_bundle
+ plugin_id: moderation_state_filter
+ sorts: { }
+ header: { }
+ footer: { }
+ empty: { }
+ relationships: { }
+ arguments: { }
+ display_extenders: { }
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_interface'
+ - url
+ tags: { }
+
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_revision_table.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_revision_table.yml
new file mode 100644
index 0000000..1cb6b6d
--- /dev/null
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_revision_table.yml
@@ -0,0 +1,218 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - content_moderation
+ - user
+id: test_content_moderation_state_filter_revision_table
+label: test_content_moderation_state_filter_revision_table
+module: views
+description: ''
+tag: ''
+base_table: node_field_revision
+base_field: vid
+core: 8.x
+display:
+ default:
+ display_plugin: default
+ id: default
+ display_title: Master
+ position: 0
+ display_options:
+ access:
+ type: perm
+ options:
+ perm: 'view all revisions'
+ 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
+ options:
+ grouping: { }
+ row_class: ''
+ default_row_class: true
+ uses_fields: false
+ row:
+ type: fields
+ options:
+ inline: { }
+ separator: ''
+ hide_empty: false
+ default_field_elements: true
+ fields:
+ nid:
+ id: nid
+ table: node_field_revision
+ field: nid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: ''
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: false
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ 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:
+ moderation_state:
+ id: moderation_state
+ table: node_field_revision
+ field: moderation_state
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: in
+ value: { }
+ group: 1
+ exposed: true
+ expose:
+ operator_id: moderation_state_op
+ label: 'Moderation state'
+ description: ''
+ use_operator: false
+ operator: moderation_state_op
+ identifier: moderation_state
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '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: { }
+ entity_type: node
+ plugin_id: moderation_state_filter
+ sorts:
+ vid:
+ id: vid
+ table: node_field_revision
+ field: vid
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: ASC
+ exposed: false
+ expose:
+ label: ''
+ entity_type: node
+ entity_field: vid
+ plugin_id: standard
+ 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: { }
+
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.info.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.info.yml
index b96ef84..e3d37c3 100644
--- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.info.yml
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.info.yml
@@ -8,3 +8,4 @@ dependencies:
- content_moderation
- node
- views
+ - entity_test
diff --git a/core/modules/content_moderation/tests/src/Functional/ViewsModerationStateFilterTest.php b/core/modules/content_moderation/tests/src/Functional/ViewsModerationStateFilterTest.php
new file mode 100644
index 0000000..e5b296d
--- /dev/null
+++ b/core/modules/content_moderation/tests/src/Functional/ViewsModerationStateFilterTest.php
@@ -0,0 +1,310 @@
+<?php
+
+namespace Drupal\Tests\content_moderation\Functional;
+
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\views\Functional\ViewTestBase;
+use Drupal\views\ViewExecutable;
+use Drupal\views\Views;
+use Drupal\workflows\Entity\Workflow;
+
+/**
+ * Tests the views 'moderation_state_filter' filter plugin.
+ *
+ * @coversDefaultClass \Drupal\content_moderation\Plugin\views\filter\ModerationStateFilter
+ *
+ * @group content_moderation
+ */
+class ViewsModerationStateFilterTest extends ViewTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = [
+ 'content_moderation_test_views',
+ 'node',
+ 'content_moderation',
+ 'workflows',
+ 'workflow_type_test',
+ 'entity_test',
+ 'language',
+ 'content_translation',
+ 'views_ui',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp($import_test_views = TRUE) {
+ parent::setUp(FALSE);
+
+ NodeType::create([
+ 'type' => 'example_a',
+ ])->save();
+ NodeType::create([
+ 'type' => 'example_b',
+ ])->save();
+
+ $new_workflow = Workflow::create([
+ 'type' => 'content_moderation',
+ 'id' => 'new_workflow',
+ 'label' => 'New workflow',
+ ]);
+ $new_workflow->getTypePlugin()->addState('bar', 'Bar');
+ $new_workflow->save();
+
+ $this->drupalLogin($this->drupalCreateUser(['administer workflows', 'administer views']));
+ }
+
+ /**
+ * Tests the dependency handling of the moderation state filter.
+ *
+ * @covers ::calculateDependencies
+ * @covers ::onDependencyRemoval
+ */
+ public function testModerationStateFilterDependencyHandling() {
+ // First, check that the view doesn't have any config dependency when there
+ // are no states configured in the filter.
+ $view_id = 'test_content_moderation_state_filter_base_table';
+ $view = Views::getView($view_id);
+
+ $this->assertWorkflowDependencies([], $view);
+ $this->assertTrue($view->storage->status());
+
+ // Configure the Editorial workflow for a node bundle, set the filter value
+ // to use one of its states and check that the workflow is now a dependency
+ // of the view.
+ $this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/type/node', [
+ 'bundles[example_a]' => TRUE,
+ ], 'Save');
+
+ $edit['options[value][]'] = ['editorial-published'];
+ $this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/filter/moderation_state", $edit, 'Apply');
+ $this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
+
+ $view = Views::getView($view_id);
+ $this->assertWorkflowDependencies(['editorial'], $view);
+ $this->assertTrue($view->storage->status());
+
+ // Create another workflow and repeat the checks above.
+ $this->drupalPostForm('admin/config/workflow/workflows/add', [
+ 'label' => 'Translation',
+ 'id' => 'translation',
+ 'workflow_type' => 'content_moderation',
+ ], 'Save');
+ $this->drupalPostForm('admin/config/workflow/workflows/manage/translation/add_state', [
+ 'label' => 'Needs Review',
+ 'id' => 'needs_review',
+ ], 'Save');
+ $this->drupalPostForm('admin/config/workflow/workflows/manage/translation/type/node', [
+ 'bundles[example_b]' => TRUE,
+ ], 'Save');
+
+ $edit['options[value][]'] = ['editorial-published', 'translation-needs_review'];
+ $this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/filter/moderation_state", $edit, 'Apply');
+ $this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
+
+ $view = Views::getView($view_id);
+ $this->assertWorkflowDependencies(['editorial', 'translation'], $view);
+ $this->assertTrue(isset($view->storage->getDisplay('default')['display_options']['filters']['moderation_state']));
+ $this->assertTrue($view->storage->status());
+
+ // Remove the 'Translation' workflow.
+ $this->drupalPostForm('admin/config/workflow/workflows/manage/translation/delete', [], 'Delete');
+
+ // Check that the view has been disabled, the filter has been deleted, the
+ // view can be saved and there are no more config dependencies.
+ $view = Views::getView($view_id);
+ $this->assertFalse($view->storage->status());
+ $this->assertFalse(isset($view->storage->getDisplay('default')['display_options']['filters']['moderation_state']));
+ $this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
+ $this->assertWorkflowDependencies([], $view);
+ }
+
+ /**
+ * Tests the moderation state filter when the configured workflow is changed.
+ *
+ * @dataProvider providerTestWorkflowChanges
+ */
+ public function testWorkflowChanges($view_id, $filter_name) {
+ // Update the view and make the default filter not exposed anymore,
+ // otherwise all results will be shown when there are no more moderated
+ // bundles left.
+ $this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/filter/moderation_state", [], 'Hide filter');
+ $this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
+
+ // First, apply the Editorial workflow to both of our content types.
+ $this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/type/node', [
+ 'bundles[example_a]' => TRUE,
+ 'bundles[example_b]' => TRUE,
+ ], 'Save');
+ \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+
+ // Add a few nodes in various moderation states.
+ $this->createNode(['type' => 'example_a', 'moderation_state' => 'published']);
+ $this->createNode(['type' => 'example_b', 'moderation_state' => 'published']);
+ $archived_node_a = $this->createNode(['type' => 'example_a', 'moderation_state' => 'archived']);
+ $archived_node_b = $this->createNode(['type' => 'example_b', 'moderation_state' => 'archived']);
+
+ // Configure the view to only show nodes in the 'archived' moderation state.
+ $edit['options[value][]'] = ['editorial-archived'];
+ $this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/filter/moderation_state", $edit, 'Apply');
+ $this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
+
+ // Check that only the archived nodes from both bundles are displayed by the
+ // view.
+ $view = Views::getView($view_id);
+ $this->executeView($view);
+ $this->assertIdenticalResultset($view, [['nid' => $archived_node_a->id()], ['nid' => $archived_node_b->id()]], ['nid' => 'nid']);
+
+ // Remove the Editorial workflow from one of the bundles.
+ $this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/type/node', [
+ 'bundles[example_a]' => TRUE,
+ 'bundles[example_b]' => FALSE,
+ ], 'Save');
+ \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+
+ $view = Views::getView($view_id);
+ $this->executeView($view);
+ $this->assertIdenticalResultset($view, [['nid' => $archived_node_a->id()]], ['nid' => 'nid']);
+
+ // Check that the view can still be edited and saved without any
+ // intervention.
+ $this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
+
+ // Remove the Editorial workflow from both bundles.
+ $this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/type/node', [
+ 'bundles[example_a]' => FALSE,
+ 'bundles[example_b]' => FALSE,
+ ], 'Save');
+ \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+
+ $view = Views::getView($view_id);
+ $this->executeView($view);
+
+ // Check that the view doesn't return any result.
+ $this->assertEmpty($view->result);
+
+ // Check that the view can not be edited without any intervention anymore
+ // because the user needs to fix the filter.
+ $this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
+ $this->assertSession()->pageTextContains("No valid values found on filter: $filter_name.");
+ }
+
+ /**
+ * Data provider for testWorkflowChanges.
+ *
+ * @return string[]
+ * An array of view IDs.
+ */
+ public function providerTestWorkflowChanges() {
+ return [
+ 'view on base table, filter on base table' => [
+ 'test_content_moderation_state_filter_base_table',
+ 'Content: Moderation state'
+ ],
+ 'view on base table, filter on revision table' => [
+ 'test_content_moderation_state_filter_base_table_filter_on_revision',
+ 'Content revision: Moderation state'
+ ],
+ ];
+ }
+
+ /**
+ * Tests the content moderation state filter caching is correct.
+ */
+ public function testFilterRenderCache() {
+ // Initially all states of the workflow are displayed.
+ $this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/type/node', [
+ 'bundles[example_a]' => TRUE,
+ ], 'Save');
+ $this->assertFilterStates(['All', 'editorial-draft', 'editorial-published', 'editorial-archived']);
+
+ // Adding a new state to the editorial workflow will display that state in
+ // the list of filters.
+ $this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/add_state', [
+ 'label' => 'Foo',
+ 'id' => 'foo',
+ ], 'Save');
+ $this->assertFilterStates(['All', 'editorial-draft', 'editorial-published', 'editorial-archived', 'editorial-foo']);
+
+ // Adding a second workflow to nodes will also show new states.
+ $this->drupalPostForm('admin/config/workflow/workflows/manage/new_workflow/type/node', [
+ 'bundles[example_b]' => TRUE,
+ ], 'Save');
+ $this->assertFilterStates(['All', 'editorial-draft', 'editorial-published', 'editorial-archived', 'editorial-foo', 'new_workflow-draft', 'new_workflow-published', 'new_workflow-bar']);
+
+ // Add a few more states and change the exposed filter to allow multiple
+ // selections so we can check that the size of the select element does not
+ // exceed 8 options.
+ $this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/add_state', [
+ 'label' => 'Foo 2',
+ 'id' => 'foo2',
+ ], 'Save');
+ $this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/add_state', [
+ 'label' => 'Foo 3',
+ 'id' => 'foo3',
+ ], 'Save');
+
+ $view_id = 'test_content_moderation_state_filter_base_table';
+ $edit['options[expose][multiple]'] = TRUE;
+ $this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/filter/moderation_state", $edit, 'Apply');
+ $this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
+
+ $this->assertFilterStates(['editorial-draft', 'editorial-published', 'editorial-archived', 'editorial-foo', 'editorial-foo2', 'editorial-foo3', 'new_workflow-draft', 'new_workflow-published', 'new_workflow-bar'], TRUE);
+ }
+
+ /**
+ * Assert the states which appear in the filter.
+ *
+ * @param array $states
+ * The states which should appear in the filter.
+ * @param bool $check_size
+ * (optional) Whether to check that size of the select element is not
+ * greater than 8. Defaults to FALSE.
+ */
+ protected function assertFilterStates($states, $check_size = FALSE) {
+ $this->drupalGet('/filter-test-path');
+
+ $assert_session = $this->assertSession();
+
+ // Check that the select contains the correct number of options.
+ $assert_session->elementsCount('css', '#edit-default-revision-state option', count($states));
+
+ // Check that the size of the select element does not exceed 8 options.
+ if ($check_size) {
+ $this->assertGreaterThan(8, count($states));
+ $assert_session->elementAttributeContains('css', '#edit-default-revision-state', 'size', 8);
+ }
+
+ // Check that an option exists for each of the expected states.
+ foreach ($states as $state) {
+ $assert_session->optionExists('Default Revision State', $state);
+ }
+ }
+
+ /**
+ * Asserts the views dependencies on workflow config entities.
+ *
+ * @param string[] $workflow_ids
+ * An array of workflow IDs to check.
+ * @param \Drupal\views\ViewExecutable $view
+ * An executable View object.
+ */
+ protected function assertWorkflowDependencies(array $workflow_ids, ViewExecutable $view) {
+ $dependencies = $view->getDependencies();
+
+ $expected = [];
+ foreach (Workflow::loadMultiple($workflow_ids) as $workflow) {
+ $expected[] = $workflow->getConfigDependencyName();
+ }
+
+ if ($expected) {
+ $this->assertSame($expected, $dependencies['config']);
+ }
+ else {
+ $this->assertTrue(!isset($dependencies['config']));
+ }
+ }
+
+}
diff --git a/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php
new file mode 100644
index 0000000..637e70a
--- /dev/null
+++ b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php
@@ -0,0 +1,298 @@
+<?php
+
+namespace Drupal\Tests\content_moderation\Kernel;
+
+use Drupal\entity_test\Entity\EntityTestNoBundle;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
+use Drupal\views\Views;
+use Drupal\workflows\Entity\Workflow;
+
+/**
+ * Tests the views 'moderation_state_filter' filter plugin.
+ *
+ * @coversDefaultClass \Drupal\content_moderation\Plugin\views\filter\ModerationStateFilter
+ *
+ * @group content_moderation
+ */
+class ViewsModerationStateFilterTest extends ViewsKernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = [
+ 'content_moderation_test_views',
+ 'node',
+ 'content_moderation',
+ 'workflows',
+ 'workflow_type_test',
+ 'entity_test',
+ 'language',
+ 'content_translation',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp($import_test_views = TRUE) {
+ parent::setUp(FALSE);
+
+ $this->installEntitySchema('user');
+ $this->installEntitySchema('node');
+ $this->installEntitySchema('content_moderation_state');
+ $this->installEntitySchema('entity_test_no_bundle');
+ $this->installSchema('node', 'node_access');
+ $this->installConfig('content_moderation_test_views');
+ $this->installConfig('content_moderation');
+
+ $node_type = NodeType::create([
+ 'type' => 'example',
+ ]);
+ $node_type->save();
+
+ $node_type = NodeType::create([
+ 'type' => 'another_example',
+ ]);
+ $node_type->save();
+
+ $node_type = NodeType::create([
+ 'type' => 'example_non_moderated',
+ ]);
+ $node_type->save();
+
+ ConfigurableLanguage::createFromLangcode('fr')->save();
+ }
+
+ /**
+ * Tests the content moderation state filter.
+ */
+ public function testStateFilterViewsRelationship() {
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
+ $workflow->getTypePlugin()->addState('translated_draft', 'Bar');
+ $configuration = $workflow->getTypePlugin()->getConfiguration();
+ $configuration['states']['translated_draft'] += [
+ 'published' => FALSE,
+ 'default_revision' => FALSE,
+ ];
+ $workflow->getTypePlugin()->setConfiguration($configuration);
+ $workflow->save();
+
+ // Create a published default revision and one forward draft revision.
+ $node = Node::create([
+ 'type' => 'example',
+ 'title' => 'Test Node',
+ 'moderation_state' => 'published',
+ ]);
+ $node->save();
+ $node->setNewRevision();
+ $node->moderation_state = 'draft';
+ $node->save();
+
+ // Create a draft default revision.
+ $second_node = Node::create([
+ 'type' => 'example',
+ 'title' => 'Second Node',
+ 'moderation_state' => 'draft',
+ ]);
+ $second_node->save();
+
+ // Create a published default revision.
+ $third_node = Node::create([
+ 'type' => 'example',
+ 'title' => 'Third node',
+ 'moderation_state' => 'published',
+ ]);
+ $third_node->save();
+
+ // Add a non-moderated node.
+ $fourth_node = Node::create([
+ 'type' => 'example_non_moderated',
+ 'title' => 'Fourth node',
+ ]);
+ $fourth_node->save();
+
+ // Create a translated published revision.
+ $translated_forward_revision = $third_node->addTranslation('fr');
+ $translated_forward_revision->title = 'Translated Node';
+ $translated_forward_revision->setNewRevision(TRUE);
+ $translated_forward_revision->moderation_state = 'translated_draft';
+ $translated_forward_revision->save();
+
+ // Four revisions for the nodes when no filter.
+ $this->assertNodesWithFilters([$node, $second_node, $third_node, $third_node], []);
+
+ // The default revision of node one and three is published.
+ $this->assertNodesWithFilters([$node, $third_node], [
+ 'default_revision_state' => 'editorial-published',
+ ]);
+
+ // The default revision of node two is draft.
+ $this->assertNodesWithFilters([$second_node], [
+ 'default_revision_state' => 'editorial-draft',
+ ]);
+
+ // Test the same three revisions on a view displaying content revisions.
+ // Both nodes have one draft revision.
+ $this->assertNodesWithFilters([$node, $second_node], [
+ 'moderation_state' => 'editorial-draft',
+ ], 'test_content_moderation_state_filter_revision_table');
+ // Creating a new forward revision of node three, creates a second published
+ // revision of of the original language, hence there are two published
+ // revisions of node three.
+ $this->assertNodesWithFilters([$node, $third_node, $third_node], [
+ 'moderation_state' => 'editorial-published',
+ ], 'test_content_moderation_state_filter_revision_table');
+ // There is a single forward translated revision with a new state, which is
+ // also filterable.
+ $this->assertNodesWithFilters([$translated_forward_revision], [
+ 'moderation_state' => 'editorial-translated_draft',
+ ], 'test_content_moderation_state_filter_revision_table');
+ }
+
+ /**
+ * Test the moderation filter with a non-translatable entity type.
+ */
+ public function testNonTranslatableEntityType() {
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_no_bundle', 'entity_test_no_bundle');
+ $workflow->save();
+
+ $test_entity = EntityTestNoBundle::create([
+ 'moderation_state' => 'draft',
+ ]);
+ $test_entity->save();
+
+ $view = Views::getView('test_content_moderation_state_filter_entity_test');
+ $view->setExposedInput([
+ 'moderation_state' => 'editorial-draft',
+ ]);
+ $view->execute();
+ $this->assertIdenticalResultset($view, [['id' => $test_entity->id()]], ['id' => 'id']);
+ }
+
+ /**
+ * Tests the list of states in the filter plugin.
+ */
+ public function testStateFilterStatesList() {
+ // By default a view of nodes will not have states to filter.
+ $this->assertPluginStates([]);
+
+ // Adding a content type to the editorial workflow will enable all of the
+ // editorial states.
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
+ $workflow->save();
+ $this->assertPluginStates([
+ 'Editorial' => [
+ 'editorial-draft' => 'Draft',
+ 'editorial-published' => 'Published',
+ 'editorial-archived' => 'Archived',
+ ],
+ ]);
+
+ // Adding a workflow which is not content moderation will not add any
+ // additional states to the views filter.
+ $workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_complex_test']);
+ $workflow->getTypePlugin()->addState('draft', 'Draft');
+ $workflow->save();
+ $this->assertPluginStates([
+ 'Editorial' => [
+ 'editorial-draft' => 'Draft',
+ 'editorial-published' => 'Published',
+ 'editorial-archived' => 'Archived',
+ ],
+ ]);
+
+ // Adding a new content moderation workflow will add additional states to
+ // filter.
+ $workflow = Workflow::create(['id' => 'moderation_test', 'type' => 'content_moderation', 'label' => 'Moderation test']);
+ $workflow->getTypePlugin()->addState('foo', 'Foo State');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
+ $workflow->save();
+ $this->assertPluginStates([
+ 'Editorial' => [
+ 'editorial-draft' => 'Draft',
+ 'editorial-published' => 'Published',
+ 'editorial-archived' => 'Archived',
+ ],
+ 'Moderation test' => [
+ 'moderation_test-foo' => 'Foo State',
+ 'moderation_test-draft' => 'Draft',
+ 'moderation_test-published' => 'Published',
+ ],
+ ]);
+
+ // Deleting a workflow will remove the states from the filter.
+ $workflow = Workflow::load('moderation_test');
+ $workflow->delete();
+ $this->assertPluginStates([
+ 'Editorial' => [
+ 'editorial-draft' => 'Draft',
+ 'editorial-published' => 'Published',
+ 'editorial-archived' => 'Archived',
+ ],
+ ]);
+
+ // Deleting a state from a workflow will remove the state from the filter.
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->deleteState('archived');
+ $workflow->save();
+ $this->assertPluginStates([
+ 'Editorial' => [
+ 'editorial-draft' => 'Draft',
+ 'editorial-published' => 'Published',
+ ],
+ ]);
+ }
+
+ /**
+ * Assert the plugin states.
+ *
+ * @param string[] $states
+ * The states which should appear in the filter.
+ */
+ protected function assertPluginStates($states) {
+ $plugin = Views::pluginManager('filter')->createInstance('moderation_state_filter', []);
+ $view = Views::getView('test_content_moderation_state_filter_base_table');
+ $plugin->init($view, $view->getDisplay());
+ $this->assertEquals($states, $plugin->getValueOptions());
+ }
+
+ /**
+ * Assert the nodes appear when the test view is executed.
+ *
+ * @param \Drupal\node\NodeInterface[] $nodes
+ * Nodes to assert are in the views result.
+ * @param array $filters
+ * An array of filters to apply to the view.
+ * @param string $view_id
+ * The view to execute for the results.
+ */
+ protected function assertNodesWithFilters(array $nodes, array $filters, $view_id = 'test_content_moderation_state_filter_base_table') {
+ $view = Views::getView($view_id);
+ $view->setExposedInput($filters);
+ $view->execute();
+
+ // Verify the join configuration.
+ $query = $view->getQuery();
+ $join = $query->getTableInfo('content_moderation_state')['join'];
+ $configuration = $join->configuration;
+ $this->assertEquals('content_moderation_state_field_revision', $configuration['table']);
+ $this->assertEquals('content_entity_revision_id', $configuration['field']);
+ $this->assertEquals('vid', $configuration['left_field']);
+ $this->assertEquals('content_entity_type_id', $configuration['extra'][0]['field']);
+ $this->assertEquals('node', $configuration['extra'][0]['value']);
+ $this->assertEquals('langcode', $configuration['extra'][1]['field']);
+ $this->assertEquals('langcode', $configuration['extra'][1]['left_field']);
+
+ $expected_result = [];
+ foreach ($nodes as $node) {
+ $expected_result[] = ['nid' => $node->id()];
+ }
+ $this->assertIdenticalResultset($view, $expected_result, ['nid' => 'nid']);
+ }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoBundle.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoBundle.php
index 5907021..0369a64 100644
--- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoBundle.php
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoBundle.php
@@ -9,6 +9,9 @@ namespace Drupal\entity_test\Entity;
* id = "entity_test_no_bundle",
* label = @Translation("Entity Test without bundle"),
* base_table = "entity_test_no_bundle",
+ * handlers = {
+ * "views_data" = "Drupal\views\EntityViewsData"
+ * },
* entity_keys = {
* "id" = "id",
* "revision" = "revision_id",
diff --git a/core/modules/views/src/Plugin/views/filter/InOperator.php b/core/modules/views/src/Plugin/views/filter/InOperator.php
index cff42b9..1e6682d 100644
--- a/core/modules/views/src/Plugin/views/filter/InOperator.php
+++ b/core/modules/views/src/Plugin/views/filter/InOperator.php
@@ -229,7 +229,9 @@ class InOperator extends FilterPluginBase {
'#default_value' => $default_value,
// These are only valid for 'select' type, but do no harm to checkboxes.
'#multiple' => TRUE,
- '#size' => count($options) > 8 ? 8 : count($options),
+ // The value options can be a multidimensional array if the value form
+ // type is a select list, so make sure that they are counted correctly.
+ '#size' => min(count($options, COUNT_RECURSIVE), 8),
];
$user_input = $form_state->getUserInput();
if ($exposed && !isset($user_input[$identifier])) {
diff --git a/core/modules/workflows/src/WorkflowTypeInterface.php b/core/modules/workflows/src/WorkflowTypeInterface.php
index 7509e7c..0cb47a4 100644
--- a/core/modules/workflows/src/WorkflowTypeInterface.php
+++ b/core/modules/workflows/src/WorkflowTypeInterface.php
@@ -124,7 +124,7 @@ interface WorkflowTypeInterface extends PluginWithFormsInterface, DerivativeInsp
* A list of state IDs to get. If NULL then all states will be returned.
*
* @return \Drupal\workflows\StateInterface[]
- * An array of workflow states.
+ * An array of workflow states, keyed by state IDs.
*
* @throws \InvalidArgumentException
* Thrown if $state_ids contains a state ID that does not exist.