diff --git a/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php index 21dd637..271728e 100644 --- a/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php @@ -5,6 +5,7 @@ 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; @@ -33,6 +34,13 @@ class ModerationStateFilter extends InOperator implements DependentWithRemovalPl 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 @@ -42,9 +50,10 @@ class ModerationStateFilter extends InOperator implements DependentWithRemovalPl /** * Creates an instance of ModerationStateFilter. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityStorageInterface $workflow_storage) { + 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; } @@ -57,6 +66,7 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_id, $plugin_definition, $container->get('entity_type.manager'), + $container->get('entity_type.bundle.info'), $container->get('entity_type.manager')->getStorage('workflow') ); } @@ -149,6 +159,56 @@ protected function opSimple() { if (empty($this->value)) { return; } + + $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; + } + } + $this->ensureMyTable(); if ($this->operator == 'in') { @@ -181,15 +241,9 @@ protected function opSimple() { public function calculateDependencies() { $dependencies = parent::calculateDependencies(); - $workflow_ids = []; - foreach ((array) $this->value as $value) { - list($workflow_id) = explode('-', $value, 2); - $workflow_ids[$workflow_id] = TRUE; - } - - if (!empty($workflow_ids)) { + if ($workflow_ids = $this->getWorkflowIds()) { /** @var \Drupal\workflows\WorkflowInterface $workflow */ - foreach ($this->workflowStorage->loadMultiple(array_keys($workflow_ids)) as $workflow) { + foreach ($this->workflowStorage->loadMultiple($workflow_ids) as $workflow) { $dependencies[$workflow->getConfigDependencyKey()][] = $workflow->getConfigDependencyName(); } } @@ -222,4 +276,20 @@ public function onDependencyRemoval(array $dependencies) { 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/tests/src/Functional/ViewsModerationStateFilterTest.php b/core/modules/content_moderation/tests/src/Functional/ViewsModerationStateFilterTest.php index 45d1e97..05d2612 100644 --- a/core/modules/content_moderation/tests/src/Functional/ViewsModerationStateFilterTest.php +++ b/core/modules/content_moderation/tests/src/Functional/ViewsModerationStateFilterTest.php @@ -14,6 +14,7 @@ * @coversDefaultClass \Drupal\content_moderation\Plugin\views\filter\ModerationStateFilter * * @group content_moderation + * @group failing */ class ViewsModerationStateFilterTest extends ViewTestBase { @@ -122,6 +123,76 @@ public function testModerationStateFilterDependencyHandling() { } /** + * Tests the moderation state filter when the configured workflow is changed. + */ + public function testWorkflowChanges() { + $view_id = 'test_content_moderation_state_filter'; + + // 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: Content: Moderation state.'); + } + + /** * Tests the content moderation state filter caching is correct. */ public function testFilterRenderCache() { diff --git a/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php index 6c40bc3..ee7c460 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php @@ -16,6 +16,7 @@ * @coversDefaultClass \Drupal\content_moderation\Plugin\views\filter\ModerationStateFilter * * @group content_moderation + * @group failing */ class ViewsModerationStateFilterTest extends ViewsKernelTestBase {