diff --git a/src/Controller/ListUsageController.php b/src/Controller/ListUsageController.php index 023a3a38afa92d4b67ab9943277fa4c72136d8e9..324b67d2a5b46b409e7b4ddc1bf33b5ff6728416 100644 --- a/src/Controller/ListUsageController.php +++ b/src/Controller/ListUsageController.php @@ -26,6 +26,28 @@ class ListUsageController extends ControllerBase { */ const ITEMS_PER_PAGE_DEFAULT = 25; + /** + * The index for the default revision "group". + * + * @var int + */ + protected const REVISION_DEFAULT = 0; + + /** + * The index for the pending revision "group". + * + * @var int + */ + protected const REVISION_PENDING = 1; + + /** + * The index for the old revision "group". + * + * @var int + */ + protected const REVISION_OLD = -1; + + /** * The entity field manager. * @@ -142,7 +164,11 @@ class ListUsageController extends ControllerBase { // revision, we don't need the "Used in" column. $used_in_previous_revisions = FALSE; foreach ($page_rows as $row) { - if ($row[5] == $this->t('Translations or previous revisions')) { + $used_in = $row[5]['data']; + $only_default = fn(array $row) => count($row) === 1 && + !empty($row[0]['#plain_text']) && + $row[0]['#plain_text'] == $this->t('Default'); + if (!$only_default($used_in)) { $used_in_previous_revisions = TRUE; break; } @@ -193,6 +219,13 @@ class ListUsageController extends ControllerBase { $entity_types = $this->entityTypeManager->getDefinitions(); $languages = $this->languageManager()->getLanguages(LanguageInterface::STATE_ALL); $all_usages = $this->entityUsage->listSources($entity); + + $revision_groups = [ + static::REVISION_DEFAULT => $this->t("Default"), + static::REVISION_PENDING => $this->t("Pending revision(s) / Draft(s)"), + static::REVISION_OLD => $this->t("Old revision(s)"), + ]; + foreach ($all_usages as $source_type => $ids) { $type_storage = $this->entityTypeManager->getStorage($source_type); foreach ($ids as $source_id => $records) { @@ -208,16 +241,33 @@ class ListUsageController extends ControllerBase { if ($source_entity instanceof RevisionableInterface) { $default_revision_id = $source_entity->getRevisionId(); $default_langcode = $source_entity->language()->getId(); - $used_in_default = FALSE; - $default_key = 0; - foreach ($records as $key => $record) { - if (($default_revision_id === NULL || $record['source_vid'] == $default_revision_id) && $record['source_langcode'] == $default_langcode) { - $default_key = $key; - $used_in_default = TRUE; - break; + $revisions = []; + foreach (array_reverse($records) as $record) { + [ + 'source_vid' => $source_vid, + 'source_langcode' => $source_langcode, + 'field_name' => $field_name, + ] = $record; + // Track which languages are used in pending, default and old + // revisions. + $revision_group = (int) $source_vid <=> (int) $default_revision_id; + $revisions[$revision_group][$source_langcode] = $field_name; + } + + $used_in = []; + foreach ($revision_groups as $index => $label) { + if (!empty($revisions[$index])) { + $used_in[] = $this->summariseRevisionGroup($default_langcode, $label, $revisions[$index]); } } - $used_in_text = $used_in_default ? $this->t('Default') : $this->t('Translations or previous revisions'); + + if (count($used_in) > 1) { + $used_in = [ + '#theme' => 'item_list', + '#items' => $used_in, + '#list_type' => 'ul', + ]; + } } $link = $this->getSourceEntityLink($source_entity); // If the label is empty it means this usage shouldn't be shown @@ -226,14 +276,22 @@ class ListUsageController extends ControllerBase { continue; } $published = $this->getSourceEntityStatus($source_entity); - $field_label = isset($field_definitions[$records[$default_key]['field_name']]) ? $field_definitions[$records[$default_key]['field_name']]->getLabel() : $this->t('Unknown'); + $get_field_name = function (array $field_names) use ($default_langcode, $revision_groups) { + foreach (array_keys($revision_groups) as $group) { + if (isset($field_names[$group])) { + return $field_names[$group][$default_langcode] ?? reset($field_names[$group]); + } + } + }; + $field_name = $get_field_name($revisions); + $field_label = isset($field_definitions[$field_name]) ? $field_definitions[$field_name]->getLabel() : $this->t('Unknown'); $rows[] = [ $link, $entity_types[$source_type]->getLabel(), $languages[$default_langcode]->getName(), $field_label, $published, - $used_in_text, + ['data' => $used_in], ]; } } @@ -242,6 +300,53 @@ class ListUsageController extends ControllerBase { return $this->allRows; } + /** + * Returns a render array indicating a revision "type" and languages. + * + * For example it might return "Pending revision(s) / Draft(s): ES, NO.". + * + * @param string $default_langcode + * The default language code for the referencing entity. + * @param \Drupal\Core\StringTranslation\TranslatableMarkup $revision_label + * The translated revision type label eg 'Old revision(s)' or 'Default'. + * @param string[] $languages + * An indexed array of language codes that reference the entity in the given + * type. + * + * @return array + * A render array summarizing the information passed in. + */ + protected function summariseRevisionGroup($default_langcode, $revision_label, array $languages) { + $language_objects = $this->languageManager()->getLanguages(LanguageInterface::STATE_ALL); + if (count($languages) === 1 && !empty($languages[$default_langcode])) { + // If there's only one relevant revision and it's the entity's default + // language then just show the label. + return ['#plain_text' => $revision_label]; + } + else { + // Otherwise show the languages enumerated, ensuring the default language + // comes first if present. + if (!empty($languages[$default_langcode])) { + $languages = [$default_langcode => TRUE] + $languages; + } + return [ + '#type' => 'inline_template', + '#template' => '{{ label }}: {% for language in languages %}{{ language }}{{ loop.last ? "." : ", " }}{% endfor %}', + '#context' => [ + 'label' => $revision_label, + 'languages' => array_map(fn ($code) => [ + '#type' => 'inline_template', + '#template' => '<abbr title="{{ name|e("html_attr") }}">{{ code }}</abbr>', + '#context' => [ + 'code' => mb_strtoupper($code), + 'name' => $language_objects[$code]->getName(), + ], + ], array_keys($languages)), + ], + ]; + } + } + /** * Get rows for a given page. * diff --git a/tests/src/FunctionalJavascript/EntityUsageLayoutBuilderEntityBrowserBlockTest.php b/tests/src/FunctionalJavascript/EntityUsageLayoutBuilderEntityBrowserBlockTest.php index 2db6a215f5349ad8a51e1786dc06d7c6328350dd..0a056b3d63b05b7379f39f14a7c69492733502ce 100644 --- a/tests/src/FunctionalJavascript/EntityUsageLayoutBuilderEntityBrowserBlockTest.php +++ b/tests/src/FunctionalJavascript/EntityUsageLayoutBuilderEntityBrowserBlockTest.php @@ -204,7 +204,9 @@ class EntityUsageLayoutBuilderEntityBrowserBlockTest extends EntityUsageJavascri $this->assertStringContainsString($host_node->toUrl()->toString(), $first_row_title_link->getAttribute('href')); $first_row_field_label = $this->xpath('//table/tbody/tr[1]/td[4]')[0]; $this->assertEquals('Layout', $first_row_field_label->getText()); - $assert_session->pageTextNotContains('Translations or previous revisions'); + $assert_session->pageTextNotContains('Old revision(s)'); + $assert_session->pageTextNotContains('Pending revision(s) / Draft(s)'); + $assert_session->pageTextNotContains('Default:'); // Verify we can edit the layout and add another item to the same region. $page->clickLink($host_node->getTitle()); @@ -295,7 +297,7 @@ class EntityUsageLayoutBuilderEntityBrowserBlockTest extends EntityUsageJavascri $first_row_field_label = $this->xpath('//table/tbody/tr[1]/td[4]')[0]; $this->assertEquals('Layout', $first_row_field_label->getText()); $first_row_used_in = $this->xpath('//table/tbody/tr[1]/td[6]')[0]; - $this->assertEquals('Translations or previous revisions', $first_row_used_in->getText()); + $this->assertEquals('Old revision(s)', $first_row_used_in->getText()); } } diff --git a/tests/src/FunctionalJavascript/ListControllerTest.php b/tests/src/FunctionalJavascript/ListControllerTest.php index abdcf276fc2040a78f72cb677ef87ec6f08323fe..606e73533a1d3b5852898eef0eee3564e7ad1d23 100644 --- a/tests/src/FunctionalJavascript/ListControllerTest.php +++ b/tests/src/FunctionalJavascript/ListControllerTest.php @@ -153,7 +153,9 @@ class ListControllerTest extends EntityUsageJavascriptTestBase { // When all usages are shown on their default revisions, we don't see the // extra column. $assert_session->pageTextNotContains('Used in'); - $assert_session->pageTextNotContains('Translations or previous revisions'); + $assert_session->pageTextNotContains('Old revision(s)'); + $assert_session->pageTextNotContains('Pending revision(s) / Draft(s)'); + $assert_session->pageTextNotContains('Default:'); // If some sources reference our entity in a previous revision, an // additional column is shown. @@ -165,7 +167,7 @@ class ListControllerTest extends EntityUsageJavascriptTestBase { $second_row_used_in = $this->xpath('//table/tbody/tr[1]/td[6]')[0]; $this->assertEquals('Default', $second_row_used_in->getText()); $second_row_used_in = $this->xpath('//table/tbody/tr[2]/td[6]')[0]; - $this->assertEquals('Translations or previous revisions', $second_row_used_in->getText()); + $this->assertEquals('Old revision(s)', $second_row_used_in->getText()); // Make sure we only have 2 rows (so no previous revision shows up). $this->assertEquals(2, count($this->xpath('//table/tbody/tr'))); @@ -212,10 +214,10 @@ class ListControllerTest extends EntityUsageJavascriptTestBase { // Usage now should be the same as before. $this->drupalGet("/admin/content/entity-usage/node/{$node1->id()}"); $assert_session->pageTextContains('Used in'); - $second_row_used_in = $this->xpath('//table/tbody/tr[1]/td[6]')[0]; - $this->assertEquals('Default', $second_row_used_in->getText()); + $first_row_used_in = $this->xpath('//table/tbody/tr[1]/td[6]')[0]; + $this->assertEquals('Default', $first_row_used_in->getText()); $second_row_used_in = $this->xpath('//table/tbody/tr[2]/td[6]')[0]; - $this->assertEquals('Translations or previous revisions', $second_row_used_in->getText()); + $this->assertEquals('Default: ES. Old revision(s)', $second_row_used_in->getText()); $this->assertEquals(2, count($this->xpath('//table/tbody/tr'))); // Verify that it's possible to control the number of items per page. diff --git a/tests/src/FunctionalJavascript/RevisionsTranslationsTest.php b/tests/src/FunctionalJavascript/RevisionsTranslationsTest.php index d36f252ecbdc670637aebaf1dca88d78a37da1b1..4cebd65c322351d8669333224a06739fc1918873 100644 --- a/tests/src/FunctionalJavascript/RevisionsTranslationsTest.php +++ b/tests/src/FunctionalJavascript/RevisionsTranslationsTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\entity_usage\FunctionalJavascript; +use Drupal\Core\Entity\RevisionableInterface; +use Drupal\entity_test\Entity\EntityTest; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\node\Entity\Node; use Drupal\Tests\entity_usage\Traits\EntityUsageLastEntityQueryTrait; @@ -24,8 +26,32 @@ class RevisionsTranslationsTest extends EntityUsageJavascriptTestBase { protected static $modules = [ 'language', 'content_translation', + // To test entities which implement RevisionableInterface but do have + // revisions. + 'entity_test' ]; + /** + * {@inheritdoc} + */ + public function setUp(): void { + parent::setUp(); + // Grant the logged-in user permission to entity_test entities. + /** @var \Drupal\user\RoleInterface $role */ + $role = Role::load('authenticated'); + $this->grantPermissions($role, ['view test entity', 'access entity usage statistics']); + + // Allow absolute links to be picked up by entity usage and the node tab to + // be reached. + $current_request = \Drupal::request(); + $config = \Drupal::configFactory()->getEditable('entity_usage.settings'); + $config + ->set('site_domains', [$current_request->getHttpHost() . $current_request->getBasePath()]) + ->set('local_task_enabled_entity_types', ['node']) + ->save(); + \Drupal::service('router.builder')->rebuild(); + } + /** * Tests the tracking of nodes and revisions. */ @@ -257,6 +283,46 @@ class RevisionsTranslationsTest extends EntityUsageJavascriptTestBase { $usage = $usage_service->listSources($node3); $this->assertEquals([], $usage); + // Test a revisionable entity type without revisions. + $entity_test_1 = EntityTest::create([ + 'name' => 'Test entity', + // Use an absolute URL so that tests running Drupal in a subdirectory + // still work. + 'field_test_text' => '<a href="' . $node1->toUrl()->setAbsolute()->toString() . '">test</a>', + ]); + $entity_test_1->save(); + $this->assertInstanceOf(RevisionableInterface::class, $entity_test_1); + $this->assertNull($entity_test_1->getRevisionId()); + + $this->drupalGet("/node/{$node1->id()}/usage"); + $assert_session->pageTextContains('Entity usage information for Node 1'); + // Only two usages; the entity_test entity and node 2. + $assert_session->elementsCount('xpath', '//table/tbody/tr', 2); + $first_row_title = $this->xpath('//table/tbody/tr[1]/td[1]')[0]; + $this->assertEquals('Test entity', $first_row_title->getText()); + $first_row_used_in = $this->xpath('//table/tbody/tr[1]/td[6]')[0]; + $this->assertEquals('Default', $first_row_used_in->getText()); + $second_row_title = $this->xpath('//table/tbody/tr[2]/td[1]')[0]; + $this->assertEquals('Node 2', $second_row_title->getText()); + $second_row_used_in = $this->xpath('//table/tbody/tr[2]/td[6]')[0]; + $this->assertEquals('Old revision(s)', $second_row_used_in->getText()); + + // Create a pending revision of node 2 that links to node 1. + $node2 = \Drupal::entityTypeManager()->getStorage('node')->loadUnchanged($node2->id()); + $node2->setNewRevision(); + $node2->isDefaultRevision(FALSE); + $node2->field_eu_test_related_nodes->target_id = $node1->id(); + $node2->save(); + $this->drupalGet("/node/{$node1->id()}/usage"); + $assert_session->pageTextContains('Entity usage information for Node 1'); + $assert_session->elementsCount('xpath', '//table/tbody/tr', 2); + $second_row_title = $this->xpath('//table/tbody/tr[2]/td[1]')[0]; + $this->assertEquals('Node 2', $second_row_title->getText()); + $second_row_used_in = $this->xpath('//table/tbody/tr[2]/td[6]/ul/li[1]')[0]; + $this->assertEquals('Pending revision(s) / Draft(s)', $second_row_used_in->getText()); + $second_row_used_in = $this->xpath('//table/tbody/tr[2]/td[6]/ul/li[2]')[0]; + $this->assertEquals('Old revision(s)', $second_row_used_in->getText()); + // If we remove a node only being targeted in previous revisions (N1), all // usages tracked should also be deleted. $node1->delete(); @@ -289,7 +355,6 @@ class RevisionsTranslationsTest extends EntityUsageJavascriptTestBase { $assert_session->pageTextContains('has been deleted.'); $usage = $usage_service->listSources($node2); $this->assertEquals([], $usage); - } /** @@ -463,7 +528,7 @@ class RevisionsTranslationsTest extends EntityUsageJavascriptTestBase { $first_row_field_label = $this->xpath('//table/tbody/tr[1]/td[4]')[0]; $this->assertEquals('Related nodes', $first_row_field_label->getText()); $first_row_used_in = $this->xpath('//table/tbody/tr[1]/td[6]')[0]; - $this->assertEquals('Translations or previous revisions', $first_row_used_in->getText()); + $this->assertEquals('Default: ES.', $first_row_used_in->getText()); // There's no second row. $assert_session->elementNotExists('xpath', '//table/tbody/tr[2]');