Change record status: 
Project: 
Introduced in branch: 
10.1.x
Introduced in version: 
10.1.0
Description: 

A new generic revision UI system is now available for entity types to make use of. The system allows developers to make use of all, or a combination of:

  • Revision history page
  • Revision view page
  • Revision revert form
  • Revision delete form


Example of revision history page.
Example of revision history page.

Add Revision UI to an entity type

Add the following to support revision UI:

  • Route provider
  • Link templates
  • Form handlers
  • Access control handler, and optionally, permissions

Route provider

Add the revision route provider to the entity annotation:

/**
 *   handlers = {
 *   ...
 *     route_provider = {
 *       ...
 *       "revision" = \Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider::class,
 *     }
 *   }
 */

Add a combination of any of the following link templates to the entity annotation:

The paths can be changed to any pattern, however the entity and entity revision parameters (in {curly braces}) are specially named. Replace MYENTITYTYPE with entity type ID.

/**
 *   links = {
 *     ...
 *     "revision" = "/myentitytype/{MYENTITYTYPE}/revision/{MYENTITYTYPE_revision}/view",
 *     "revision-delete-form" = "/myentitytype/{MYENTITYTYPE}/revision/{MYENTITYTYPE_revision}/delete",
 *     "revision-revert-form" = "/myentitytype/{MYENTITYTYPE}/revision/{MYENTITYTYPE_revision}/revert",
 *     "version-history" = "/myentitytype/{MYENTITYTYPE}/revisions",
 *   }
 */

Form handlers

Add a combination of the following form handlers to the entity annotation:

/**
 *   handlers = {
 *     ...
 *     "form" = {
 *       ...
 *       "revision-delete" = \Drupal\Core\Entity\Form\RevisionDeleteForm::class,
 *       "revision-revert" = \Drupal\Core\Entity\Form\RevisionRevertForm::class,
 *     }
 *   }
 */

Access control

By default access can be granted by responding to revision operations. However access may be overridden by altering the routes supplied by RevisionHtmlRouteProvider.

Example of responding to revision operations:

Modify or create an entity access control handler, and the following to checkAccess() method:

return match ($operation) {
  'view all revisions' => AccessResult::allowedIf(CONDITION),
  'view revision' => AccessResult::allowedIf(CONDITION),
  'revert' => AccessResult::allowedIf(CONDITION)->andIf(AccessResult::forbiddenIf($entity->isDefaultRevision() && $entity->isLatestRevision())),
  'delete revision' => AccessResult::allowedIf(CONDITION),
  default => throw new \LogicException('Unknown operation')
};

Access to revision routes could be based on permissions supplied by a module. Implement permissions, and change above to:

return match ($operation) {
  'view all revisions' => AccessResult::allowedIfHasPermission($account, 'my permission'),
  'view revision' => AccessResult::allowedIfHasPermission($account, 'my permission'),
  'revert' => AccessResult::allowedIfHasPermission($account, 'my permission')->andIf(AccessResult::forbiddenIf($entity->isDefaultRevision() && $entity->isLatestRevision())),
  'delete revision' => AccessResult::allowedIfHasPermission($account, 'my permission'),
  default => throw new \LogicException('Unknown operation')
};

Local tasks

Local tasks (tabs) are automatically generated. Existing entity types which implemented their own revision UI, or for example via Entity project, will need to remove their local task implementation to avoid duplicate/redundant local tasks.

Impacts: 
Module developers

Comments

askibinski’s picture

See the 3.x branch of media_revisions_ui for an example of this integration.

Albert Skibinski - Homepage

bhanu951’s picture

You might observe that EntityAccessControlHandler doesn't respect the EntityInterface of the $entity parameter as below.

------ --------------------------------------------------------------------------------------
Line Access/MyCustomEntityAccessControlHandler.php
------ --------------------------------------------------------------------------------------
33 Call to an undefined method Drupal\Core\Entity\EntityInterface::isDefaultRevision().
33 Call to an undefined method Drupal\Core\Entity\EntityInterface::isLatestRevision().
------ --------------------------------------------------------------------------------------

To fix this just assert our custom entity interface by adding below code in our checkAccess method.

assert($entity instanceof MyCustomEntityInterface);

Related #2951487: EntityViewBuilder::getBuildDefaults doesn't respect the EntityInterface of the $entity parameter