diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php index bb052164da..4082118748 100644 --- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php @@ -367,20 +367,30 @@ protected function addContextualLinks(array &$build, EntityInterface $entity) { if ($entity->isNew()) { return; } - $key = $entity->getEntityTypeId(); - $rel = 'canonical'; + $links = []; + // Only attach one of either the revision or canonical contextual link + // group. if ($entity instanceof ContentEntityInterface && !$entity->isDefaultRevision()) { - $rel = 'revision'; - $key .= '_revision'; + $links[$entity->getEntityTypeId() . '_revision'] = 'revision'; } - if ($entity->hasLinkTemplate($rel)) { - $build['#contextual_links'][$key] = [ - 'route_parameters' => $entity->toUrl($rel)->getRouteParameters(), - ]; - if ($entity instanceof EntityChangedInterface) { - $build['#contextual_links'][$key]['metadata'] = [ - 'changed' => $entity->getChangedTime(), + else { + $links[$entity->getEntityTypeId()] = 'canonical'; + } + // For entities which are non-default and are the latest version, add an + // additional group of contextual links. + if ($entity instanceof ContentEntityInterface && !$entity->isDefaultRevision() && $entity->isLatestRevision()) { + $links[$entity->getEntityTypeId() . '_latest_version'] = 'revision'; + } + foreach ($links as $group => $link_template) { + if ($entity->hasLinkTemplate($link_template)) { + $build['#contextual_links'][$group] = [ + 'route_parameters' => $entity->toUrl($link_template)->getRouteParameters(), ]; + if ($entity instanceof EntityChangedInterface) { + $build['#contextual_links'][$group]['metadata'] = [ + 'changed' => $entity->getChangedTime(), + ]; + } } } } diff --git a/core/modules/block_content/block_content.links.contextual.yml b/core/modules/block_content/block_content.links.contextual.yml index 547f391ff6..da8f45c2f0 100644 --- a/core/modules/block_content/block_content.links.contextual.yml +++ b/core/modules/block_content/block_content.links.contextual.yml @@ -2,3 +2,8 @@ block_content.block_edit: title: 'Edit' group: block_content route_name: 'entity.block_content.canonical' + +block_content.block_edit_latest_version: + title: 'Edit' + group: block_content_latest_version + route_name: 'entity.block_content.canonical' diff --git a/core/modules/editor/editor.routing.yml b/core/modules/editor/editor.routing.yml index 0c5b2249bb..da1f31c6a7 100644 --- a/core/modules/editor/editor.routing.yml +++ b/core/modules/editor/editor.routing.yml @@ -13,6 +13,7 @@ editor.field_untransformed_text: parameters: entity: type: entity:{entity_type} + load_latest_revision: true requirements: _permission: 'access in-place editing' _access_quickedit_entity_field: 'TRUE' diff --git a/core/modules/editor/tests/src/Functional/QuickEditIntegrationLoadingTest.php b/core/modules/editor/tests/src/Functional/QuickEditIntegrationLoadingTest.php index 56916776f6..39952e7d13 100644 --- a/core/modules/editor/tests/src/Functional/QuickEditIntegrationLoadingTest.php +++ b/core/modules/editor/tests/src/Functional/QuickEditIntegrationLoadingTest.php @@ -4,6 +4,7 @@ use Drupal\Component\Serialization\Json; use Drupal\Core\EventSubscriber\MainContentViewSubscriber; +use Drupal\Core\Url; use Drupal\filter\Entity\FilterFormat; use Drupal\Tests\BrowserTestBase; @@ -26,6 +27,13 @@ class QuickEditIntegrationLoadingTest extends BrowserTestBase { */ protected $defaultTheme = 'classy'; + /** + * The test node. + * + * @var \Drupal\node\NodeInterface + */ + protected $testNode; + /** * The basic permissions necessary to view content and use in-place editing. * @@ -56,7 +64,7 @@ protected function setUp() { ]); // Create one node of the above node type using the above text format. - $this->drupalCreateNode([ + $this->testNode = $this->drupalCreateNode([ 'type' => 'article', 'body' => [ 0 => [ @@ -116,6 +124,33 @@ public function testUsersWithoutPermission() { } } + /** + * Test the latest revision of an entity is loaded for editing. + */ + public function testLatestRevisionLoaded() { + $user = $this->drupalCreateUser(array_merge(static::$basicPermissions, ['edit any article content', 'access in-place editing'])); + $this->drupalLogin($user); + + $this->testNode->setNewRevision(TRUE); + $this->testNode->isDefaultRevision(FALSE); + $this->testNode->body->value = '
Content in a pending revision.
'; + $this->testNode->save(); + + // Ensure the content from the latest revision is loaded from the quickedit + // editor route. + $url = Url::fromRoute('editor.field_untransformed_text') + ->setRouteParameter('entity_type', 'node') + ->setRouteParameter('entity', $this->testNode->id()) + ->setRouteParameter('field_name', 'body') + ->setRouteParameter('langcode', 'en') + ->setRouteParameter('view_mode_id', 'full') + ->setOption('query', [ + MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax', + ]); + $this->drupalGet($url); + $this->assertSession()->responseContains('Content in a pending revision.'); + } + /** * Test loading of untransformed text when a user does have access to it. */ diff --git a/core/modules/image/image.routing.yml b/core/modules/image/image.routing.yml index 4a0cb88a83..6149eea09a 100644 --- a/core/modules/image/image.routing.yml +++ b/core/modules/image/image.routing.yml @@ -80,6 +80,7 @@ image.upload: parameters: entity: type: entity:{entity_type} + load_latest_revision: true requirements: _permission: 'access in-place editing' _access_quickedit_entity_field: 'TRUE' @@ -93,6 +94,7 @@ image.info: parameters: entity: type: entity:{entity_type} + load_latest_revision: true requirements: _permission: 'access in-place editing' _access_quickedit_entity_field: 'TRUE' diff --git a/core/modules/image/tests/src/Functional/QuickEditImageControllerTest.php b/core/modules/image/tests/src/Functional/QuickEditImageControllerTest.php index 853a030b89..c6fc925243 100644 --- a/core/modules/image/tests/src/Functional/QuickEditImageControllerTest.php +++ b/core/modules/image/tests/src/Functional/QuickEditImageControllerTest.php @@ -3,6 +3,8 @@ namespace Drupal\Tests\image\Functional; use Drupal\Component\Serialization\Json; +use Drupal\Core\Url; +use Drupal\file\Entity\File; use Drupal\Tests\BrowserTestBase; use Drupal\Tests\image\Kernel\ImageFieldCreationTrait; use Drupal\Tests\TestFileCreationTrait; @@ -167,6 +169,79 @@ public function testInvalidUpload() { $this->assertContains('"main_error":"The image failed validation."', $this->getSession()->getPage()->getContent(), 'Invalid upload returned errors.'); } + /** + * Test the latest revision of an entity is loaded for editing. + */ + public function testLatestRevisionLoaded() { + $node = $this->drupalCreateNode([ + 'type' => 'article', + 'title' => t('Test Node'), + ]); + + // Create an image which only exists on a pending revision to make sure the + // latest revision is loaded for the quickedit info route. + $file = File::create([ + 'uri' => 'public://example.png', + 'filename' => 'example.png', + ]); + $file->save(); + $node->{$this->fieldName} = [ + 'target_id' => $file->id(), + 'alt' => 'test alt', + 'title' => 'test title', + 'width' => 10, + 'height' => 11, + ]; + $node->setNewRevision(TRUE); + $node->isDefaultRevision(FALSE); + $node->save(); + + $url = Url::fromRoute('image.info') + ->setRouteParameter('entity_type', 'node') + ->setRouteParameter('entity', $node->id()) + ->setRouteParameter('field_name', $this->fieldName) + ->setRouteParameter('langcode', $node->language()->getId()) + ->setRouteParameter('view_mode_id', 'default') + ->setOption('query', [ + '_format' => 'json', + ]); + + $info = Json::decode($this->drupalGet($url)); + $this->assertSame('test alt', $info['alt']); + $this->assertSame('test title', $info['title']); + + // Find an image that passes field validation and upload it. + $image_factory = $this->container->get('image.factory'); + foreach ($this->drupalGetTestFiles('image') as $image) { + $image_file = $image_factory->get($image->uri); + if ($image_file->getWidth() > 50 && $image_file->getWidth() < 100) { + $valid_image = $image; + break; + } + } + $this->assertNotNull($valid_image); + $this->uploadImage($valid_image, $node->id(), $this->fieldName, $node->language()->getId()); + + // Save the tempstore changes. + $this->container + ->get('tempstore.private') + ->get('quickedit') + ->get($node->uuid()) + ->save(); + + // Load the default and latest revision. + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = $this->container->get('entity_type.manager')->getStorage('node'); + $default = $storage->load($node->id()); + $latest_revision_id = $storage->getLatestRevisionId($node->id()); + $latest_revision = $storage->loadRevision($latest_revision_id); + + // Ensure that the file was uploaded to the latest revision. + $this->assertSame($latest_revision->{$this->fieldName}->entity->filename->value, $valid_image->filename); + // Ensure that the default revision was unchanged. + $this->assertTrue($default->{$this->fieldName}->isEmpty()); + } + /** * Uploads an image using the image module's Quick Edit route. * diff --git a/core/modules/node/node.links.contextual.yml b/core/modules/node/node.links.contextual.yml index f1d8a9eec8..c6cb0ceed5 100644 --- a/core/modules/node/node.links.contextual.yml +++ b/core/modules/node/node.links.contextual.yml @@ -3,6 +3,11 @@ entity.node.edit_form: group: node title: Edit +entity.node.latest_version_edit_form: + route_name: entity.node.edit_form + group: node_latest_version + title: Edit + entity.node.delete_form: route_name: entity.node.delete_form group: node diff --git a/core/modules/quickedit/quickedit.routing.yml b/core/modules/quickedit/quickedit.routing.yml index af22056cb7..633ef908aa 100644 --- a/core/modules/quickedit/quickedit.routing.yml +++ b/core/modules/quickedit/quickedit.routing.yml @@ -20,6 +20,7 @@ quickedit.field_form: parameters: entity: type: entity:{entity_type} + load_latest_revision: true requirements: _permission: 'access in-place editing' _access_quickedit_entity_field: 'TRUE' diff --git a/core/modules/taxonomy/taxonomy.links.contextual.yml b/core/modules/taxonomy/taxonomy.links.contextual.yml index 7f8ce510b5..606dd3f69d 100644 --- a/core/modules/taxonomy/taxonomy.links.contextual.yml +++ b/core/modules/taxonomy/taxonomy.links.contextual.yml @@ -4,6 +4,12 @@ entity.taxonomy_term.edit_form: route_name: entity.taxonomy_term.edit_form weight: 10 +entity.taxonomy_term.latest_version_edit_form: + title: Edit + group: taxonomy_term_latest_version + route_name: entity.taxonomy_term.edit_form + weight: 10 + entity.taxonomy_term.delete_form: title: Delete group: taxonomy_term diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewBuilderTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewBuilderTest.php index 5a82ee6612..1fb82442f3 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewBuilderTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewBuilderTest.php @@ -7,6 +7,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\Core\Entity\Entity\EntityViewMode; +use Drupal\entity_test\Entity\EntityTestMulRev; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\language\Entity\ConfigurableLanguage; @@ -29,6 +30,7 @@ class EntityViewBuilderTest extends EntityKernelTestBase { protected function setUp() { parent::setUp(); $this->installConfig(['user', 'entity_test']); + $this->installEntitySchema('entity_test_mulrev'); // Give anonymous users permission to view test entities. Role::load(RoleInterface::ANONYMOUS_ID) @@ -339,4 +341,66 @@ public function testNoTemplate() { $this->assertFalse(array_key_exists('#theme', $build)); } + /** + * Test correct contextual links are rendered. + */ + public function testContextualLinks() { + $view_builder = $this->container->get('entity.manager')->getViewBuilder('entity_test_mulrev'); + $storage = $this->container->get('entity.manager')->getStorage('entity_test_mulrev'); + + $entity_test_mulrev = EntityTestMulRev::create([]); + $entity_test_mulrev->save(); + $original_revision_id = $entity_test_mulrev->getRevisionId(); + // By default contextual links will be added for the group matching the + // entity type ID. + $built = $view_builder->build($view_builder->view($entity_test_mulrev, 'full')); + $this->assertEquals([ + 'entity_test_mulrev' => [ + 'route_parameters' => [ + 'entity_test_mulrev' => $entity_test_mulrev->id(), + ], + ], + ], $built['#contextual_links']); + + // Create a new pending revision. + $entity_test_mulrev->setNewRevision(TRUE); + $entity_test_mulrev->isDefaultRevision(FALSE); + $entity_test_mulrev->save(); + // The latest non-default revision will contain both the "revision" group + // and the "latest_version" group. + $built = $view_builder->build($view_builder->view($entity_test_mulrev, 'full')); + $this->assertEquals([ + 'entity_test_mulrev_revision' => [ + 'route_parameters' => [ + 'entity_test_mulrev' => $entity_test_mulrev->id(), + 'entity_test_mulrev_revision' => $entity_test_mulrev->getRevisionId(), + ], + ], + 'entity_test_mulrev_latest_version' => [ + 'route_parameters' => [ + 'entity_test_mulrev' => $entity_test_mulrev->id(), + 'entity_test_mulrev_revision' => $entity_test_mulrev->getRevisionId(), + ], + ], + ], $built['#contextual_links']); + + // Create a new default revision and load the original (now non-default) + // revision. + $entity_test_mulrev->isDefaultRevision(TRUE); + $entity_test_mulrev->setNewRevision(TRUE); + $entity_test_mulrev->save(); + // Non-default revisions which are not the latest revision will only contain + // the "revision" group. + $original_revision = $storage->loadRevision($original_revision_id); + $built = $view_builder->build($view_builder->view($original_revision, 'full')); + $this->assertEquals([ + 'entity_test_mulrev_revision' => [ + 'route_parameters' => [ + 'entity_test_mulrev' => $original_revision->id(), + 'entity_test_mulrev_revision' => $original_revision->getRevisionId(), + ], + ], + ], $built['#contextual_links']); + } + }