- 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10
Yes, we have no revision ui for block content, even though it supports revisions
- 🇳🇱Netherlands Bojhan
Wait, but do we need to expose a UI for everything? Or is this just a 'in case you want it' kinda thing.
- 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10
I don't think block content revision ui belongs in core. But a reusable base class I could add on contrib sounds efficient.
- 🇬🇧United Kingdom Xano Southampton
I agree with what has been said before. Having a generic base class that will only be used for node revisions by default offers developers a good starting point for making their own revision UI when they want to.
- 🇳🇱Netherlands jbekker
It should be usefull for custom entities as well, not just block content. I dont think we would need to expose a UI by default but a base class like larowlan says would be very helpful.
I'm looking forward to see some more opinions. The last submitted patch, 6: generic-revision-trait-2350939.1.patch, failed testing.
- 🇬🇧United Kingdom Xano Southampton
Thanks for bringing this back up! I had already forgotten about it.
In general I'm not sure we should make this a route controller, and considering the complexity and the fact the trait calls
entityManager()
which is not defined anywhere in the trait (and I know why), making this a trait sounds messy as well. Can we make this an entity handler base class? The code in its current form isn't usable for non-entities anyway. Besides, using a class rather than a trait for something this complex will allow us to build slightly more solid code (no dark magicentityManager()
for instance).In any case, here is a short review of the patch:
-
+++ b/core/lib/Drupal/Core/Revision/RevisionControllerTrait.php @@ -0,0 +1,212 @@ + public function revisionShow($revision_id) { + $entity = $this->entityManager()->getStorage($this->getRevisionEntityTypeId())->loadRevision($revision_id);
We generally start methods with a verb.
-
+++ b/core/lib/Drupal/Core/Revision/RevisionControllerTrait.php @@ -0,0 +1,212 @@ + if (isset($page[$this->getRevisionEntityTypeId() . 's'][$entity->id()]['#cache'])) {
unset()
works fine if the value to unset does not exist yet, so we don't need a condition here. -
+++ b/core/lib/Drupal/Core/Revision/RevisionControllerTrait.php @@ -0,0 +1,212 @@ + * Page title callback for an entity revision.
One-line summaries should also start with a verb.
-
+++ b/core/lib/Drupal/Core/Revision/RevisionControllerTrait.php @@ -0,0 +1,212 @@ + /** + * Determines if the user has permission to revert revisions. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to check revert access for. + * + * @return bool + * TRUE if the user has revert access. + */ + abstract protected function hasRevertRevisionPermission(EntityInterface $entity); + + /** + * Determines if the user has permission to delete revisions. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to check delete revision access for. + * + * @return bool + * TRUE if the user has delete revision access. + */ + abstract protected function hasDeleteRevisionPermission(EntityInterface $entity);
Did you check if this can be done through access controllers? I understand it's probably difficult at the least; I just want to make sure that approach has been researched.
-
+++ b/core/lib/Drupal/Core/Revision/RevisionControllerTrait.php @@ -0,0 +1,212 @@ + abstract protected function buildRevertRevisionLink(EntityInterface $entity, $revision_id); + + /** + * Builds a link to delete an entity revision. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to build a delete revision link for. + * @param int $revision_id + * The revision ID of the delete link. + * + * @return array + * A link render array + */ + abstract protected function buildDeleteRevisionLink(EntityInterface $entity, $revision_id);
The same goes for these link builders and entities' list builder classes.
-
+++ b/core/lib/Drupal/Core/Revision/RevisionControllerTrait.php @@ -0,0 +1,212 @@ + abstract protected function getRevisionDetails(EntityInterface $revision, $is_current = FALSE);
Maybe rename this to
getRevisionDescription()
, or something similar that indicates the output is human-readable? -
+++ b/core/lib/Drupal/Core/Revision/TimestampedRevisionInterface.php @@ -0,0 +1,33 @@ + * Defines an interface for entities with timestamped revisions.
This interface is not in an entity namespace, so we should not mention entities anywhere in the file. There is no technical reason for this interface to be entity-specific in its current form, except for my next comment.
+++ b/core/lib/Drupal/Core/Revision/TimestampedRevisionInterface.php @@ -0,0 +1,33 @@ + * @return \Drupal\Core\Entity\EntityInterface + * The called entity.
Should be
@return $this
. -
- 🇨🇭Switzerland miro_dietiker Switzerland
We tried to start something similar in context of Diff contrib:
#2452523: [Meta] Offer a revisions tab for all entities →We would be very happy to see this happen in core.
Note our discussions that the revision summary is a problem and there should be a handler that allows alteration in contrib.
A problem is here that i see you referring to $revision->revision_log that is only present in node revision entities from Node.php baseFieldDefinitions().
All other entities will provide an empty description.
I'm confused the patch above only implements the whole functionality on Node only? I thought the issue is about displaying a revision tab for every entity type. - 🇨🇭Switzerland berdir Switzerland
The issue is essentially just about making the code generic.
But yes, without using it somewhere else, at least in a test module (we could try to integrate it automatically in entity_test for all revisionable entity types there?), we won't see stuff like the revision_log and uid that are node specific...
- 🇪🇸Spain edurenye
Rebased.
Did all changes exept the 3, that I didn't know what to put there, was like this before, and can't get a verb for that.
And 4, 5, that I don't know how to do that.
Also in RevisionControllerTrait, I'm not sure if everything it's fine there. The last submitted patch, 13: implement_a_generic-2350939-13.patch, failed testing.
The last submitted patch, 13: implement_a_generic-2350939-13.patch, failed testing.
- 🇪🇸Spain edurenye
Fixed the failing test, basically improved the reroll, added changes that were made in this time.
mkalkbrenner → queued 16: implement_a_generic-2350939-16.patch for re-testing.
The last submitted patch, 16: implement_a_generic-2350939-16.patch, failed testing.
- 🇩🇪Germany mkalkbrenner 🇩🇪
I like this approach. But it has to be adjusted to the latest changes introduced by #2465907: Node revision UI reverts multiple languages when only one language should be reverted → .
+++ b/core/lib/Drupal/Core/Revision/RevisionControllerTrait.php @@ -0,0 +1,218 @@ + $links = []; + if ($revert_permission) { + $links['revert'] = $this->buildRevertRevisionLink($entity, $vid); + } + if ($delete_permission) { + $links['delete'] = $this->buildDeleteRevisionLink($entity, $vid); + }
In order to increase the re-usage of revisionOverview() the list of links should be moved to a separate method with the trait.
- 🇪🇸Spain edurenye
I did the rebase and moved the links to another method.
- 🇩🇪Germany mkalkbrenner 🇩🇪
I think we should go one step further. The operation link targets NodeRevisionDeleteForm, NodeRevisionRevertForm,
NodeRevisionRevertTranslationForm seem to be easily adjustable for different entities as well. We should therefor provide base classes or traits, too.Just some nitpicks:
-
+++ b/core/lib/Drupal/Core/Revision/RevisionControllerTrait.php @@ -0,0 +1,249 @@ + '%date' => format_date($entity->getRevisionCreationTime())
new code should use \Drupal\Core\Datetime\DateFormatter::format().
-
+++ b/core/lib/Drupal/Core/Revision/RevisionControllerTrait.php @@ -0,0 +1,249 @@ + protected function getLinks(EntityInterface $entity, $revision_id) {
I suggest to name this method getOperations() or getOperationLinks().
-
+++ b/core/modules/node/src/Controller/NodeController.php @@ -283,4 +190,112 @@ public function addPageTitle(NodeTypeInterface $node_type) { + 'url' => $has_translations ? + Url::fromRoute('node.revision_revert_translation_confirm', ['node' => $entity->id(), 'node_revision' => $revision_id, 'langcode' => $langcode]) : + Url::fromRoute('node.revision_revert_confirm', ['node' => $entity->id(), 'node_revision' => $revision_id]),
leading whitespaces
-
- 🇪🇸Spain edurenye
What you mean exactly? Like having a TranslatableRevisionControllerTrait that extends from RevisionControllerTrait for those entities that are translatable and add the methods there or just extract buildRevertRevisionTranslationLink from buildRevertRevisionLink in RevisionControllerTrait?
Those small fixes are solved in this patch.
- 🇩🇪Germany mkalkbrenner 🇩🇪
@edurenye:
The goal of this issue is to isolate the re-usable parts of node revision handling and make them available for all entities. But the current patch only targets the NodeController that lists node revisions. But reverting a revision is actually done by NodeRevisionRevertForm and NodeRevisionRevertTranslationForm.
If you want to implement a UI for any entity revisions, you need to implement their logic again. Therefor I suggest to create corresponding traits or something like a EntityRevisionRevertBaseForm. - 🇪🇸Spain edurenye
We discussed about this topic in the #DrupalCampVienna2015, and we agreed to implement the generic revision UI in entity API and then move it to Core.
Here is the issue I created for that: #2625122: [Meta] Implement a generic revision UI →
Drupal 8.1.0-beta1 → was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule → and the Allowed changes during the Drupal 8 release cycle → .
- 🇺🇸United States jhedstrom Portland, OR
Where, if at all, does this fit into the Worfklow Initiative? Does the content_moderation approach over in #2725533: Add experimental content_moderation module → supersede this?
- 🇩🇪Germany dawehner
One part, for example the generic revert controller could be totally brought in already into core.
Drupal 8.2.0-beta1 → was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule → and the Allowed changes during the Drupal 8 release cycle → .
- 🇺🇸United States jhedstrom Portland, OR
Adding the related Diff module issue.
Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule → and the Allowed changes during the Drupal 8 release cycle → .
Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule → and the Allowed changes during the Drupal 8 release cycle → .
- 🇩🇪Germany chr.fritsch 🇩🇪🇪🇺🌍
This is now feature complete in contrib entity module. We can start moving it to core. Media would also really benefit from it.
- 🇩🇪Germany chr.fritsch 🇩🇪🇪🇺🌍
This is far away from done, but i think i collected all relevant implementation and test classes into this patch.
Test are still failing...
The last submitted patch, 35: 2350939-35.patch, failed testing. View results →
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.- 🇩🇪Germany chr.fritsch 🇩🇪🇪🇺🌍
Some small push. Hopefully more people will jump in to push this forward.
The last submitted patch, 37: 2350939-37.patch, failed testing. View results →
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.- 🇪🇸Spain manuel garcia
1) Drupal\FunctionalTests\Routing\RevisionRouteAccessTest::testRevisionRouteAccess Unable to install modules entity_test, user, entity, block due to missing modules entity.
This should get the tests running.
The last submitted patch, 39: 2350939-39.patch, failed testing. View results →
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule → and the Allowed changes during the Drupal 8 release cycle → .
The last submitted patch, 42: 2350939-42.patch, failed testing. View results →
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.- 🇦🇺Australia Sam152
Thanks everyone for working on this, it would be a great step towards cutting out the work required to get to a node-like custom entity type.
My review is as follows. I think the biggest outstanding thing to resolve was mentioned back in #10, that is we shouldn't lock in and assume a set of magically named permissions for each entity type.
-
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionControllerTrait.php @@ -0,0 +1,200 @@ + // @todo Expand the entity storage to load multiple revisions.
We should link to #1730874: Add support for loading multiple revisions at once → here.
-
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionControllerTrait.php @@ -0,0 +1,200 @@ + /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */ + if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode) + ->isRevisionTranslationAffected() + ) {
Not sure splitting this on to multiple lines makes it more readable.
-
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionControllerTrait.php @@ -0,0 +1,200 @@ + $build[$entity->getEntityTypeId() . '_revisions_table'] = [ + '#theme' => 'table', + '#rows' => $rows, + '#header' => $header, + ];
Is adding the entity type ID into render array keys just making this harder to override?
-
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionControllerTrait.php @@ -0,0 +1,200 @@ + // We have no clue about caching yet. + $build['#cache']['max-age'] = 0;
We should add a follow up here at least.
-
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionControllerTrait.php @@ -0,0 +1,200 @@ + if ($this->hasDeleteRevisionAccess($entity_revision)) { + $links['delete'] = $this->buildDeleteRevisionLink($entity_revision); + } +++ b/core/lib/Drupal/Core/Entity/Controller/RevisionOverviewController.php @@ -0,0 +1,160 @@ + /** + * {@inheritdoc} + */ + protected function hasDeleteRevisionAccess(EntityInterface $entity) { + return $this->currentUser()->hasPermission("delete all {$entity->id()} revisions"); + }
We should probably add access controls to the delete route and then simply base visibility/access of the links themselves on those same access controls. If I deny access to a route outside of just the permission, that should propagate to all links to that route.
-
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionOverviewController.php @@ -0,0 +1,160 @@ + /** + * Creates a new RevisionOverviewController instance. + * + * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter + * The date formatter. + */ + public function __construct(DateFormatterInterface $date_formatter, RendererInterface $renderer) {
I disagree we even need to document these params, but if we document one we should probably document the other.
-
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionOverviewController.php @@ -0,0 +1,160 @@ + public function revisionOverviewController(RouteMatchInterface $route_match) { + return $this->revisionOverview($route_match->getParameter($route_match->getRouteObject()->getOption('entity_type_id'))); + }
I wonder why we need this method at all. Can't "revisionOverview" be the entry point?
-
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionOverviewController.php @@ -0,0 +1,160 @@ + 'message' => ['#markup' => $markup, '#allowed_tags' => Xss::getHtmlTagList()],
getRevisionLogMessage
is the "value" column from a "string_long" field. I think that'd already be considered unsafe markup and would be escaped? Is "allowed_tags" making this more or less permissive? -
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionOverviewController.php @@ -0,0 +1,160 @@ + protected function hasRevertRevisionAccess(EntityInterface $entity) { + return AccessResult::allowedIfHasPermission($this->currentUser(), "revert all {$entity->getEntityTypeId()} revisions")->orIf( + AccessResult::allowedIfHasPermission($this->currentUser(), "revert {$entity->bundle()} {$entity->getEntityTypeId()} revisions") + ); + }
I do agree with the previous comments, why isn't this
$entity->access('revert')
, with access control handlers deciding if such controls are based on a permission or not. -
+++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -322,7 +322,7 @@ protected function urlRouteParameters($rel) { - if ($rel === 'revision' && $this instanceof RevisionableInterface) { + if (in_array($rel, ['revision', 'revision-revert-form'], TRUE) && $this instanceof RevisionableInterface) { $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId(); }
We could possibly do #2927077: $entity->toUrl('revision-*') should fill revision parameter on all applicable routes. → ahead of this issue if we wanted to reduce the scope here.
-
+++ b/core/lib/Drupal/Core/Entity/EntityRevisionAccessCheck.php @@ -0,0 +1,162 @@ + $_entity = $route_match->getParameter($entity_type_id); ... + $_entity_revision = $route_match->getParameter($entity_type_id . '_revision');
Not sure why these are prefixed with "_"?
-
+++ b/core/lib/Drupal/Core/Entity/EntityRevisionAccessCheck.php @@ -0,0 +1,162 @@ + protected function checkAccess(ContentEntityInterface $entity, AccountInterface $account, $operation = 'view') {
This method is a good example of why delegating to an access control handler might be a good idea. This hard codes bundle based granularity and also seperate permissions for update/delete/revert. What if my entity type is simple and I just want a single "administer revisions" permission to cover the whole lot?
-
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php @@ -24,6 +24,7 @@ + * "revision" = "Drupal\Core\Entity\Routing\RevisionRouteProvider",
Given these are also providing HTML routes, maybe this should be
RevisionHtmlRouteProvider
for consistency? -
+++ b/core/tests/Drupal/FunctionalTests/Routing/RevisionRouteAccessTest.php @@ -0,0 +1,84 @@ + * @runTestsInSeparateProcesses + * + * @preserveGlobalState disabled
What are these annotations for?
-
- 🇪🇸Spain manuel garcia
Thanks for the review @Sam152!
Had just a bit of time, so here's just a bit of progress on that:- #45.1 - Yay, done (I hope)
- #45.2 - Done
- #45.3 - Possibly, changed to just using
'entity_revisions_table'
.
Setting to needs review for the tests to run, but obviously this is still
Needs work
to address the rest of #45. - 🇪🇸Spain manuel garcia
OK some more progress, did all I could but most likely someone else will need to help out here :)
#45.5 Had a look, but unsure how to do this,
RevisionRouteProvider::getRevisionRevertRoute()
is already adding:$route->addRequirements([ '_entity_access_revision' => "$entity_type_id.update", ]);
Is this where we should do this?
#45.6 I agree, replaced with{@inheritdoc}
#45.7 I dont see why not, done (I hope)
#45.8 I don't know, left as is.
#45.9 - Same deal as for #45.5
#45.11 No reason that I can think of, fixed.
#45.12 I agree.
#45.13 Done.
#45.14 Dont know :) The last submitted patch, 49: 2350939-49.patch, failed testing. View results →
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.- 🇪🇸Spain manuel garcia
Reverting the changes related to #45.7, apparently we cant use a method from a trait?
- 🇪🇸Spain manuel garcia
- 🇳🇴Norway matsbla
There is also an issue about replacing the revision table with a view #1863906: [PP-1] Replace content revision table with a view →
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Just want to throw my support behind this and the idea of using a View for the revision list, it would definitely make life easier for custom content entities, as well a reduce duplication in core and automatically fix the referenced issues for example where custom blocks have no revision UI.
Bottom line is if we can do it for the Field UI, then why not the Revision UI.
- 🇨🇭Switzerland berdir Switzerland
It's actually not so easy. We can write generic controllers/forms/handlers that are entity type agnostic and provide defaults for them, but we can *not* automatically generate views for all entity types, we have no API for that. Every entity type would have to create and provide that view themself and there's nothing that ensures that they are consistent. Best-case scenario there is that drupal console or so could generate those views.
Views are also much harder to extend for modules like diff.
> Bottom line is if we can do it for the Field UI, then why not the Revision UI.
Field UI has nothing to do with views, that's exactly why it can be generic.
- 🇪🇸Spain manuel garcia
Yeah, I believe #1863906: [PP-1] Replace content revision table with a view → to be blocked, see my comment #56 → there.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Fair points, although I can still see this working.
One possible solution would be for isntead of the Views being pages with rotues, the Views are essentially Blocks, then we create some kind of generic Controller that creates the pages needed utilising the Blocks. That way either like with the Fied UI in the Cotnent Entity configuration you just specify which route to use to generate the Revision Routes and Pages from, or you implmenet the generic Controllers and pass in whatever arguments are needed.
Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule → and the Allowed changes during the Drupal 8 release cycle → .
- 🇨🇦Canada jibran Toronto, Canada
+++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -322,7 +322,7 @@ protected function urlRouteParameters($rel) { - if ($rel === 'revision' && $this instanceof RevisionableInterface) { + if (in_array($rel, ['revision', 'revision-revert-form'], TRUE) && $this instanceof RevisionableInterface) {
I think #2927077: $entity->toUrl('revision-*') should fill revision parameter on all applicable routes. → has a better fix
if ($this instanceof RevisionableInterface && strpos($rel, 'revision') === 0) {
. - 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
After thinking about this quite a bit and looking at what you get when you create a custom Content Entity using Drupal Console, I propose the following plan:
- We postpone this issue on #1863906: [PP-1] Replace content revision table with a view → and re-purpose it so that it's not Node specific but instead works for all Content Entities.
- To achieve the above it will invovle converting the View from a Page to an Embed and then in the Revision Controllers for Node and other core module use views_embed_view() to embed it, passing the correct parameters that the View can then use as relationships or contextual filters. This also means that the View doesn't need to care about permissions that the Content Entity has defined.
- Once the above is committed and fixed we continue with this issue, build on the patch here to include the new View and continue to abstract away any boilerplate code that Content Entities have to deal with.
- Once committed and done, figure out anywhere that we can replace per Entity code with these changes, as well as encourage Drupal Console (and others?) to adopt this.
Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule → and the Allowed changes during the Drupal 8 release cycle → .
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Changing my mind regarding #54, #57 and #60, let's not worry about Views, if a particular Entity Type wants to use Views then they can but it would be easier here to just go the current approach of using a ListBuilder/Controller.
Just doing a quick review based on full patch in #51:
-
+++ b/core/lib/Drupal/Core/Entity/Plugin/Derivative/RevisionsOverviewDeriver.php + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $exclude = ['node'];
I'm assuming this is done just to avoid having to deal with any custom logic in the Node module along with all of the Core and Contrib modules which modify the Node Revision UI? But maybe we can make it work?
-
+++ b/core/lib/Drupal/Core/Entity/Routing/RevisionHtmlRouteProvider.php
I feel like we'd be better off just modifying the
DefaultHtmlRouteProvider
andAdminHtmlRouteProvider
classes instead and relying on the value of the Entity Type Keyshow_revision_ui
to determine whether to inject the new Routes. This means the new Routes are already in place for core Entities that use revisions, such as Custom Blocks and Media, and makes it easier for Contrib and Custom modules to integrate. Alternatively we could add a new Entity Type Annotation Key in the event an Entity Type doesn't want these new Routes but still wants the existing UI, this approach might be better for backwords compatibility with Contrib as well.
- We should provide a
revision_delete
Route, andtranslation_revert
for Entity Types which support revisions with different translations.
- We should support allowing Entity Types to provide their own handlers for Controllers and Forms, so an Entity Type could optionally specify these handler keys in the Entity Type Annotation, otherwise if the Entity Type doesn't specify them we just use the Controllers/Forms we provide here. This might also mean we don't need the exclude for Node that I mentioned in point 1 above.
- I noticed there's quite a few references written in plaintext in the code to class paths e.g.
'_controller' => '\Drupal\Core\Entity\Controller\EntityViewController::viewRevision'
, might be better to use Class Constants instead, e.g.'_controller' => EntityViewController::class . '::viewRevision'
Thanks
-Aaron -
- 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10
Added an issue to address the revision access api, as this issue shouldn't be the place we do that, for the sake of scope
I think we should postpone this on that - I couldn't find an existing issue - thoughts?
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Agree with #67, it makes sense to wait until we have clarification around access controls before doing anything more here.
- 🇦🇺Australia kim.pepper 🏄♂️🇦🇺Sydney, Australia
Is there a link to the issue re: revision access api?
- 🇦🇺Australia kim.pepper 🏄♂️🇦🇺Sydney, Australia
Don't know how I missed it in #67 above. 🤦♂️
- 🇦🇺Australia dpi Perth, Australia
Actually 64 is messed up, the change introduced as seen in the interdiff makes all revisions shown in the UI refer to the default revision.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Tagging for Drupal Camp Scotland 2019, hoping I'll have time to progress this a bit more during the Contribution Day.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Here is what this patch does:
- Moved
version_history
to be aboverevision
inRevisionHtmlRouteProvider
as this seems to make more sense. RevisionHtmlRouteProvider::getRoutes
renamed route variables to be consistent withDefaultHtmlRouteProvider
.- In
RevisionHtmlRouteProvider
started using::class
instead of writing the full class paths as strings. (#62.5)
Partly changed my mind about #62.2 but we will need to communicate that the additional route provider needs to be added for Entities.
That's all I had time to do today at Drupal Camp Scotland Contribution Day but there are a bunch of additional things I want to do so leaving this assigned to me and I'll continue working on this, also due to an annoying technical bug and now time constraints only have an interdiff on this occasion. Future changes will include:
- Need to add a
AdminRevisionHtmlRouteProvider
. - Add title callbacks for use in
RevisionHtmlRouteProvider
. - Investigate #62.1
- Introduce some handler classes and forms which can be used in the Entity Annotation, along with an interface. (#62.4)
- Add routes suggested in #62.3
- Moved
- 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺
Thanks for pushing this forward, @AaronMcHale! 👏I like what I'm seeing so far, and am interested to see where you're taking this!
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Thanks @Wim Leers :)
I discovered that this is also blocked on #2927077: $entity->toUrl('revision-*') should fill revision parameter on all applicable routes. → as the
version_history
route will throw an exception which that issue will fix.Building on my previous interdiff:
- Introduces a new handler class key
version_history
which is required for theversion_history
route. - Renamed
RevisionOverviewController
toVersionHistoryController
. - This patch introduces a new interface
VersionHistoryControllerInterface
, the purpose of this interface is to ensure that anyversion_history
handler class provides the most basic of methods to allow theversion_history
route to be generated. Currently it has only two methodsrenderVersionHistory
andversionHistoryTitle
, these are used for the_controller
and_title_callback
of theversion_history
route. I introduced this interface because revision routes have existed in a lot of places for a long time so I didn't want us making any assumptions about what a module might want to pass as the handler class, whether that be a dedicated Controller or a Controller class with other purposes, this really needs to be a easily compatible drop-in replacement. - Introduced
RevisionAdminHtmlRouteProvider
which works exactly like the existingAdminHtmlRouteProvider
but forversion_history
andrevision_revert
routes. - Renamed
RevisionsOverviewDeriver
toVersionHistoryDeriver
. - Removed
$exclude
check fromVersionHistoryDeriver
and replaced with a check for a new option on theversion_history
route namedadd_core_version_history_local_task
, this seems like the most backwords compatible approach that's safe for both core and contrib. Essentially this prevents two "Revisions" tabs from appearing for modules which provide their ownversion_history
route and local task. - Removed calls to deprecated functions in
RevisionRevertForm
. - Added a new method
RevisionHtmlRouteProvider::getHandlerClassWithImplementationChecks
which is being used to validate the handler class and throw aRuntimeException
with a friendly message if the class doesn't implementVersionHistoryControllerInterface
, I also made it reusable in case it's needed for another route. Example message:Attempted to get the version_history Handler Class for Entity Type block_content however the class specified Drupal\Core\Entity\Controller\VersionHistoryController is not an instance of Drupal\Core\Entity\Controller\VersionHistoryControllerInterface
.
Some notes, thoughts and ideas:
- With the introduction of
VersionHistoryControllerInterface
I'm tempted to say we don't really needRevisionControllerTrait
and it could just be merged intoVersionHistoryController
as I don't see any real benefit to having both, in most cases a module which needs to introduce some customisations would likely just overrideVersionHistoryController
anyway. - I wonder if we can use
_entity_form
instead of_form
for therevision_revert_form
route? - Still need to add new routes suggested in #62.3.
- Might be better to introduce a new Twig Template instead of using inline templating in
VersionHistoryController
andRevisionControllerTrait
. - I wonder how close we can get
VersionHistoryController
andRevisionControllerTrait
to the structure ofEntityListBuilder
andEntityListBuilderInterface
, maybe even just extending those. - Might be able to improve
VersionHistoryController::versionHistoryTitle
to include the Entity Label, similar to what Node does.
Will continue working on the above, manual testing done using the
block_content
Entity Type. I included a patch of the Entity Annotation which can be applied to enable the features in the main patch. Automated tests will need to be updated although didn't have time today and it makes sense to wait until all blockers are resolved and this isn't postponed anymore, since test bot won't test it anyway while postponed. - Introduces a new handler class key
- 🇬🇧United Kingdom joachim
-
+++ b/core/lib/Drupal/Core/Entity/Controller/VersionHistoryControllerInterface.php @@ -0,0 +1,34 @@ \ No newline at end of file
Missing newline.
-
+++ b/core/lib/Drupal/Core/Entity/EntityBase.php @@ -196,7 +196,7 @@ public function toUrl($rel = 'canonical', array $options = []) { - if ($rel === 'revision' && $this instanceof RevisionableInterface && $this->isDefaultRevision()) { + if (in_array($rel, ['revision', 'revision-revert-form'], TRUE) && $this instanceof RevisionableInterface && $this->isDefaultRevision()) {
The patch over at #2927077: $entity->toUrl('revision-*') should fill revision parameter on all applicable routes. → has looser logic and checks any link that starts with 'revision':
> However, entity annotations can provide other revision-specific routes, such as "revision_revert" or "revision_delete"
Should that apply here too?
-
+++ b/core/lib/Drupal/Core/Entity/Routing/RevisionHtmlRouteProvider.php @@ -0,0 +1,168 @@ +class RevisionHtmlRouteProvider implements EntityRouteProviderInterface {
Ignore, I was reading the code incorrectly!
-
/** * Provides local tasks for the revision version_history route. */ class VersionHistoryDeriver extends DeriverBase implements ContainerDeriverInterface {
I don't want to derail this issue, but it would be nice if the changes here that add derived links for entities were in tandem with 📌 add an entity Links handler, to provide menu links/tasks/actions for entity types Needs work .
Though given my comment above on the hierarchy of route providers, I maybe need to rethink 📌 add an entity Links handler, to provide menu links/tasks/actions for entity types Needs work to allow for multiple links providers, rather than just one.
-
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Thanks for the input Joachim, regarding your points in #78:
1. Thanks for pointing that out, I've added the new line to the end of
VersionHistoryControllerInterface
on my local branh, so when I submit my next patch that'll be addressed. (Hopefully I'll have time this coming weekend to continue on this)2. Yeah I think it's best to just address this over on #2927077: $entity->toUrl('revision-*') should fill revision parameter on all applicable routes. → , I'm happy for this issue to also be postponed on that unless there's a strong feeling that we shouldn't take that approach, but let's continue any further discussion over on that issue and come to an agreement there.
4. Yeah I'm aware of 📌 add an entity Links handler, to provide menu links/tasks/actions for entity types Needs work and I fully support introducing these new Entity Link Handlers. I think it definitely makes sense to consider it here, but I don't know if it's totally necessary to block this issue on that one since
VersionHistoryDeriver
still functions properly. What I will do though is add a note and @todo toVersionHistoryDeriver
saying that it is a temporary workaround until #2976861 is in. That way if #2976861 does land before this then we can simply swap it out here, otherwise we can just address in a follow-up and update the @todo with a link to the follow-up issue. Again doing that locally for now but will include it with my next patch.Many thanks,
-Aaron - 🇬🇧United Kingdom joachim
> What I will do though is add a note and @todo to VersionHistoryDeriver saying that it is a temporary workaround until #2976861 is in.
That sounds good to me. Thanks!
A bit more feedback on this patch:
-
* Gets the name of the handler class specified and performs compliance checks to make sure * it is a valid instance.
Line too long.
-
return $handler_class; } else { throw new \RuntimeException("Attempted to get the $handler Handler Class for Entity Type {$entity_type->id()} however the class specified $handler_class is not an instance of $implementing_class");
coddled else
-
private function getHandlerClassWithImplementationChecks(EntityTypeInterface $entity_type, $handler, $implementing_class) {
I'm confused by why we need this helper method. There's perhaps a case to be made for all entity handlers to get checked against the interface they ought to be implementing. But that's a wider issue. (And also one that leads to other issues to with declaring handler types in a formal manner, so we can declare the interface they need to have.)
-
if ($entity_type->hasLinkTemplate('version-history') && $version_history_class = $this->getHandlerClassWithImplementationChecks($entity_type, 'version_history', VersionHistoryControllerInterface::class)) {
This pattern isn't used by the other entity route providers -- they hardcode the controller class they use for their routes. I think it's best not to invent too many new patterns at once.
There's a case to be made in a follow-up for introducing a "controllers" annotation that mirrors the "route_provider" annotation, as I for one have often subclasses route providers just to switch the controller class set in route callbacks.
-
$revert_form_class = $entity_type->getFormClass('revision_revert'); if ($entity_type->hasLinkTemplate('revision-revert-form') && $revert_form_class != NULL) {
I've found with my own work with controllers that it's good DX to throw exceptions for this sort of thing rather than fail silently. Tell the developer that they have their entity type annotation set up incorrectly when they do 'drush cr' rather than when they load pages.
I don't think there is a use case for there not being a revision revert route, since without the form class defined, the page at ENTITYTYPE/x/revisions crashes because it can't find the 'entity.ENTITYTYPE.revision_revert_form' route.
So, throw an exception if the form class is missing. The same sort of thing probably applies to the other routes, but this is the one that's bitten me first!
-
Also, I'm not seeing a test entity type with a 'revision_revert' form class defined, so it looks like the test coverage is incomplete.
-
public function access(Route $route, AccountInterface $account, RouteMatchInterface $route_match = NULL) {
Rather than inheritdoc, follow the pattern of EntityAccessCheck and document what this access checker expects in the route.
-
protected function checkAccess(ContentEntityInterface $entity, AccountInterface $account, $operation = 'view') {
Missing docs.
-
$operation = $route->getRequirement('_entity_access_revision');
AFAIK, an access checker needs to be declared as a service, with the requirements key, '_entity_access_revision' in our case, also declared in services.yml. E.g.:
access_check.entity_revision: class: Drupal\Core\Entity\EntityRevisionAccessCheck arguments: ['@entity_type.manager', '@current_route_match'] tags: - { name: access_check, applies_to: _entity_access_revision }
Without this, I get an access denied on the routes.
Though, why do we need @current_route_match as an injection?
-
if (empty($route_match)) { $route_match = $this->routeMatch; }
Why do we need this? The access() method gets it as a parameter.
-
* @return string * Returns a string to provide the details of the revision. */ abstract protected function getRevisionDescription(ContentEntityInterface $revision, $is_current = FALSE);
but VersionHistoryController::getRevisionDescription() doesn't return a string but an array.
-
- 🇬🇧United Kingdom joachim
A few more things:
1. I don't understand the purpose of RevisionControllerTrait.
ignore, my frankencoding was to blame.
ignore, this was because I had older revisions of the entity that predated the addition of the revision_created field, and so the value was NULL.
- 🇩🇪Germany joachim namyslo Kulmbach 🇩🇪 🇪🇺
Would love to see this thing fixed. Crossing my fingers for you and all the others to see this soon. Commenting just to be able to see when it's ready with ease.
Thanks for the afford.
Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule → and the Allowed changes during the Drupal 8 and 9 release cycles → .
- 🇺🇸United States michaellander
Thanks for all your work on this!
Inside of
Drupal\Core\Entity\Controller\RevisionControllerTrait::revisionOverview()
at this point:if ($revision->isDefaultRevision()) { $row[] = [ 'data' => [ '#prefix' => '<em>', '#markup' => $this->t('Current revision'), '#suffix' => '</em>', ], ]; foreach ($row as &$current) { $current['class'] = ['revision-current']; } } else { $links = $this->getOperationLinks($revision); $row[] = [ 'data' => [ '#type' => 'operations', '#links' => $links, ], ]; }
You are opting to print links if it's not the current revision, but I could see a case where you'd want to copy the current revision to be the latest if you have multiple drafts ahead of your current revision.
If for example we start with the following revisions:
- 4 (Draft)
- 3 (Draft)
- 2 (Current/Default/Published)
- 1
and I need to make an update to 2, I could copy it to be the next revision in the sequence:
- 5 (Copy of 2 - Current/Default/Published)
- 4 (Draft)
- 3 (Draft)
- 2
- 1
I could make some edits and publish:
- 6 (Current/Default/Published)
- 5 (Copy of 2)
- 4 (Draft)
- 3 (Draft)
- 2
- 1
I could then continue work where I left off:
- 7 (Copy of 4 - Draft)
- 6 (Current/Default/Published)
- 5 (Copy of 2)
- 4 (Draft)
- 3 (Draft)
- 2
- 1
This is actually currently limited in node revision UI and the update action is explicitly blocked in
_access_node_revision: 'update'
, which means if you create new drafts, and decide to create a draft based on what's currently published, you'd have to delete each draft in front of the currently published one. I believe you've already fixed this issue in_access_entity_revision
, but the overview form is still hiding the action.One other thought is we might consider changing the 'Revert' naming, something like 'Copy as Latest Version' would match both the 'Latest Version' tab and the 'Copy of the revision from ...' log message. There is some additional discussion about that here: #2899719: Revision/version language on revision listing page is misleading with content moderation enabled → .
Thanks again, this would be a significant improvement.
Drupal 8.9.0-beta1 → was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule → and the Allowed changes during the Drupal 8 and 9 release cycles → .
- 🇬🇧United Kingdom andrewmacpherson
This is particularly important for media entities. The media bundles configured in Standard profile have a minimal set of fields, but there are lots of use cases for additional fields. For example:
- License, copyright, or access rights, etc.
- Details of how/when a recording was made.
- Participants or subjects of a recording.
- Various fields relating to learning management systems.
- Transcripts of audio/video media. These are something we need to support in core to address WCAG and ATAG time-based media accessibility criteria. Being addressed in 🌱 Provide authors with tools to manage transcripts and captions/subtitles for local video and audio Active
- Long descriptions for complex images
- 🇺🇸United States thhafner
Pushing this issue along a bit. Made updates that do not require a great deal of additional discussion.
Remaining Tasks:
-
+++ b/core/lib/Drupal/Core/Entity/EntityBase.php @@ -196,7 +196,7 @@ public function toUrl($rel = 'canonical', array $options = []) { - if ($rel === 'revision' && $this instanceof RevisionableInterface && $this->isDefaultRevision()) { + if (in_array($rel, ['revision', 'revision-revert-form'], TRUE) && $this instanceof RevisionableInterface && $this->isDefaultRevision()) {
The patch over at #2927077: $entity->toUrl('revision-*') → should fill revision parameter on all applicable routes. has looser logic and checks any link that starts with 'revision':
> However, entity annotations can provide other revision-specific routes, such as "revision_revert" or "revision_delete"
Should that apply here too?
-
private function getHandlerClassWithImplementationChecks(EntityTypeInterface $entity_type, $handler, $implementing_class) {
I'm confused by why we need this helper method. There's perhaps a case to be made for all entity handlers to get checked against the interface they ought to be implementing. But that's a wider issue. (And also one that leads to other issues to with declaring handler types in a formal manner, so we can declare the interface they need to have.)
-
if ($entity_type->hasLinkTemplate('version-history') && $version_history_class = $this->getHandlerClassWithImplementationChecks($entity_type, 'version_history', VersionHistoryControllerInterface::class)) {
This pattern isn't used by the other entity route providers -- they hardcode the controller class they use for their routes. I think it's best not to invent too many new patterns at once.
There's a case to be made in a follow-up for introducing a "controllers" annotation that mirrors the "route_provider" annotation, as I for one have often subclasses route providers just to switch the controller class set in route callbacks.
- Also, I'm not seeing a test entity type with a 'revision_revert' form class defined, so it looks like the test coverage is incomplete.
-
public function access(Route $route, AccountInterface $account, RouteMatchInterface $route_match = NULL) {
Rather than inheritdoc, follow the pattern of EntityAccessCheck and document what this access checker expects in the route.
-
protected function checkAccess(ContentEntityInterface $entity, AccountInterface $account, $operation = 'view') {
Missing docs.
-
operation = $route->getRequirement('_entity_access_revision');
AFAIK, an access checker needs to be declared as a service, with the requirements key, '_entity_access_revision' in our case, also declared in services.yml. E.g.:
access_check.entity_revision: class: Drupal\Core\Entity\EntityRevisionAccessCheck arguments: ['@entity_type.manager', '@current_route_match'] tags: - { name: access_check, applies_to: _entity_access_revision }
Without this, I get an access denied on the routes.
Though, why do we need @current_route_match as an injection?
-
if (empty($route_match)) { $route_match = $this->routeMatch; }
Why do we need this? The access() method gets it as a parameter.
-
* @return string * Returns a string to provide the details of the revision. */ abstract protected function getRevisionDescription(ContentEntityInterface $revision, $is_current = FALSE);
but VersionHistoryController::getRevisionDescription() doesn't return a string but an array.
-
- 🇺🇸United States thhafner
Patch for #88 ✨ Implement a generic revision UI Fixed did not upload correctly. Reuploading.
The last submitted patch, 89: 2350939-89-cleanup-generic-revision-ui.patch, failed testing. View results →
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.- 🇦🇺Australia acbramley
-
+++ b/core/lib/Drupal/Core/Entity/Form/RevisionRevertForm.php @@ -0,0 +1,167 @@ + public function submitForm(array &$form, FormStateInterface $form_state) { ... + $this->revision->save();
I've found a bug with this when using https://www.drupal.org/project/block_content_revision_ui →
The bug is around the EntityChangedConstraint and the fact that we're not updating the revision's changed time before saving it.
If you look at NodeRevisionRevertForm it explicitly sets this when reverting. Without this, reverting to a Draft when an entity has a published revision causes the entity to be un-editable due to the EntityChangedConstraint triggering.
-
+++ b/core/lib/Drupal/Core/Entity/Routing/RevisionHtmlRouteProvider.php @@ -0,0 +1,179 @@ +use mysql_xdevapi\Exception;
This needs to be removed and fixed.
-
+++ b/core/lib/Drupal/Core/Entity/Routing/RevisionHtmlRouteProvider.php @@ -0,0 +1,179 @@ + * @param EntityTypeInterface $entity_type ... + * @return Route|null
Return and param object types should be fully namespaced, there are other places in the patch this needs fixing as well.
-
- 🇨🇦Canada jibran Toronto, Canada
Here are some more observations.
-
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionControllerTrait.php @@ -0,0 +1,196 @@ + if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)->isRevisionTranslationAffected()) {
What if the entity is not translatable? We need a fallback condition for this.
-
+++ b/core/lib/Drupal/Core/Entity/Controller/VersionHistoryController.php @@ -0,0 +1,163 @@ + public function renderVersionHistory(RouteMatchInterface $route_match) { + return $this->revisionOverview($route_match->getParameter($route_match->getRouteObject()->getOption('entity_type_id')));
Instead of route match why can't we just pass $entity here?
-
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
HI all, I've recently been looking at the contrib Entity API → module, at least more than I have done in the past, which has a revision UI solution implemented, and as such will likely be widely used on many Drupal sites. Looking at the solution implemented in Entity API it looks like the approach taken here is broadly the same, and I think if we can maintain that similarity here, with an approach which is already in use and tested in the wild, then I think everyone wins, because it means the upgrade path is easy for modules.
Just wanted to throw that out there.
Thanks,
-Aaron - 🇦🇺Australia dpi Perth, Australia
Properly reverted
EntityBase
changes in favor of #2927077: $entity->toUrl('revision-*') should fill revision parameter on all applicable routes. → The last submitted patch, 97: 2350939-97-generic-revision-ui.patch, failed testing. View results →
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.- 🇨🇦Canada jibran Toronto, Canada
Wooh! Really, nice work @dpi. The interdiff in #96 is bigger than actual patch.
+1 for removing
VersionHistoryLocalTasks
let's keep the patch as simple as possible.It looks like this is fine, since its exactly what Node does.
I was getting run time error for a non-translatable entity with no langcode key. We already have tests for
entity_test_rev
but it has langcode key so I'm happy with current approach and tests. - 🇦🇺Australia dpi Perth, Australia
Long review, actioned points, and progress:
General changes
- Some PHP syntax changes now that Drupal 9 is target.
- CS fixups: missing docblocks, inheritdoc where overriding method, removing errenous inheritdocs from constructors,
\t()
to$this->t()
where applicable, short array syntax, inject when otherwise a method would pull from the global container. - Style changes: use chaining where possible, early exit (instead of nesting).
- Changed usage of ContentEntityInterface to RevisionableInterface. Nothing here is strictly for content.
- Where a revision is expected, variable name changed to $revision (from $entity or $entity_revision).
- Where an revisionable entity is expected, but the exact revision is not meaningful, $entity is used.
- Using asserts as typehinting and sanity checking.
- Removed use of "earlier" and "older", since revisions are not necessarily chronological, and there may be forward revisions.
- Updated interface docs so they dont describe implementation.
RevisionControllerTrait
- Consolidated hasRevertRevisionAccess, buildRevertRevisionLink, hasDeleteRevisionAccess, buildDeleteRevisionLink since access and its cacheability are combined concerns. hasDeleteRevisionAccess and hasRevertRevisionAccess were making use of undefined permissions. This issue is already postponed on figuring out an access control solution in #3043321: Use generic access API for node and media revision UI → . Ive switched out these permissions for the operation based approach there.
- This should be pulled into Node, or removed (Node switches to VersionHistoryController).
-
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionControllerTrait.php @@ -0,0 +1,196 @@ abstract protected function getRevisionDescription(ContentEntityInterface $revision, $is_current = FALSE);
Removed $is_current, can already be pulled from
$revision->isDefaultRevision()
. The parameter itself is currently unused in the implementation.-
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionControllerTrait.php @@ -0,0 +1,196 @@ @todo Follow pattern in <a href="https://www.drupal.org/project/drupal/issues/2899719">https://www.drupal.org/project/drupal/issues/2899719</a>.
Removed todo since it doesnt add value. This patch doesnt depend on it.
-
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionControllerTrait.php @@ -0,0 +1,196 @@ $build['#cache']['max-age'] = 0;
Added dependency on current language and entity.
VersionHistoryController
- Reordered methods in order of public, then protected.
- Optimised getRevisionDescription to build
$context
optimising for readability. - Other changes are outlined in General above.
VersionHistoryControllerInterface
This pattern isn't used by the other entity route providers -- they hardcode the controller class they use for their routes. I think it's best not to invent too many new patterns at once.
Agreed. There is only a loose contract between route provider and the controller. No need for an interface. In the least we dont need to be inventing this here.
Removed handler logic and interface.
EntityBase
We could possibly do #2927077: $entity->toUrl('revision-*') should fill revision parameter on all applicable routes. ahead of this issue if we wanted to reduce the scope here.
-
+++ b/core/lib/Drupal/Core/Entity/EntityBase.php @@ -197,7 +197,7 @@ public function toUrl($rel = 'canonical', array $options = []) { if ($rel === 'revision' && $this instanceof RevisionableInterface && $this->isDefaultRevision()) { if (in_array($rel, ['revision', 'revision-revert-form'], TRUE) && $this instanceof RevisionableInterface && $this->isDefaultRevision()) {
Reverted this change. This patch is blocked by #2927077: $entity->toUrl('revision-*') should fill revision parameter on all applicable routes. → .
EntityRevisionAccessCheck
- Added @todo to remove this class since this issue is blocked on #3043321: Use generic access API for node and media revision UI → .
- Removed unused countDefaultLanguageRevisions().
- Removed unused resetAccessCache().
RevisionRevertForm
- Switched to implement EntityFormInterface so the class can be used with _entity_form.
I tried using EntityConfirmFormBase but that only works with config, we want to be compatible with content and config. - Changed logger channel to entity type the same way as ContentEntityDeleteForm and others do it, instead of using Node's content channel.
- Removed unused
Request
variable.
-
+++ b/core/lib/Drupal/Core/Entity/Form/RevisionRevertForm.php @@ -0,0 +1,167 @@ // The revision timestamp will be updated when the revision is saved. Keep // the original one for the confirmation message.
Fixed errant placement of comment (copied from node).
VersionHistoryLocalTasks
- Changed class name from VersionHistoryDeriver to VersionHistoryLocalTasks to be in line with others in core.
-
+++ b/core/lib/Drupal/Core/Entity/Plugin/Derivative/VersionHistoryDeriver.php @@ -0,0 +1,80 @@ $this->derivatives["entity.$entity_type_id.version_history"] = [
Changed ID, 'entity.' part is not necessary. Same as other core implementations.
-
+++ b/core/lib/Drupal/Core/Entity/Plugin/Derivative/VersionHistoryDeriver.php @@ -0,0 +1,80 @@ 'title' => t('Revisions'), 'base_route' => "entity.$entity_type_id.canonical", 'weight' => 20,
Moved static values to the base plugin definition in
system.links.yml
-
+++ b/core/lib/Drupal/Core/Entity/Plugin/Derivative/VersionHistoryDeriver.php @@ -0,0 +1,80 @@ // two "Revisions" tabs for Entity Types which already define their own revision routes and local task if ($version_history_route == NULL || $version_history_route->getOption('add_core_version_history_local_task') == NULL) {
A workaround was added to the local task deriver, passing around a
add_core_version_history_local_task
option in the route: primarily to workaround Node. We shouldnt have to support this kind of thing long term.Ive added back the workaround for Node. Integrating Node into this patch remains a todo for this issue.
Personally I'm not a fan of this deriver, some thoughts on direction:
- Remove Node local task definition, and ultimately provide it with this deriver.
- Dont automatically create local tasks with a deriver. Entities should provide their own tasks, just as they already have to manually opt into functionality here with revision templates and form classes. Thusly remove this local task deriver and each entity type adds their own in a each links.tasks.yml
- Further to 2, implement this later as a link handler in 📌 add an entity Links handler, to provide menu links/tasks/actions for entity types Needs work .
✨ Implement a generic revision UI Fixed point 4 also has opinions about the deriver.
RevisionAdminHtmlRouteProvider
This one is a little odd, Entities are supposed to have the option of using this one instead of RevisionHtmlRouteProvider? The only difference being admin theme?
I think if entities want to override this they can subclass RevisionHtmlRouteProvider themselves and add the
_admin_route
option.Removing this customisation and defaulting to
_admin_route=false
since thats how Node is doing. Though node has a site builder option to change this.RevisionHtmlRouteProvider
- Switched from
_form
to_entity_form
for revert form.
I've found with my own work with controllers that it's good DX to throw exceptions for this sort of thing rather than fail silently. Tell the developer that they have their entity type annotation set up incorrectly when they do 'drush cr' rather than when they load pages.
Another case of we dont need to invent that here: "if a form handler class doesnt exist then an exception should be thrown". This should be a new issue if a special exception should be thrown. Right now, a
InvalidPluginDefinitionException
is thrown if a user attempts to access the route when the form class does not exist.+++ b/core/lib/Drupal/Core/Entity/Routing/RevisionHtmlRouteProvider.php @@ -0,0 +1,179 @@ +use mysql_xdevapi\Exception;
Removed errant import.
Addressing other queries from above:
Made updates that do not require a great deal of additional discussion.
Many of these were changes to typehinting in docs from fully qualified to just the symbol name. This is not consistent with coding standards. Reverted.
@jibran What if the entity is not translatable? We need a fallback condition for this.
It looks like this is fine, since its exactly what Node does.
Copied docs over from Node.
Instead of route match why can't we just pass $entity here?
Parameter name is different per entity type so I dont think we cant resolve that here.
- I don't understand the purpose of RevisionControllerTrait.
Probably so
\Drupal\node\Controller\NodeController::revisionOverview
can pull from it without usingVersionHistoryController
. I think we can get remove the trait if Node switches to the newVersionHistoryController
The bug is around the EntityChangedConstraint and the fact that we're not updating the revision's changed time before saving it.
Added setting time on revert.
Todos
- buildDeleteRevisionLink is implemented, but
revision-delete-form
is not implemented in this patch yet. - This issue is PP2. Blocked on both: #2927077: $entity->toUrl('revision-*') should fill revision parameter on all applicable routes. → #3043321: Use generic access API for node and media revision UI → . The patch also relies on the patch in each of these.
- Swap Node over to use this generic implementation, or in the least the trait. (especially to fix local task workaround)
- Tests covering revert and delete link templates.
- Tests generally need to be cleaned up/modernised.
- Figure out what to do about local tasks: Generate them automatically or force each entity type to add their own. (see above)
- We should change the term "Revert" to something that doesnt imply changing revision to somethign in the past. E.g synonymous with Git's checkout. This term is also problematic because of #3023194: [PP-2] Add parallel revisioning support → , and when switching to other versions when Content Moderation is in use.
- 🇦🇺Australia dpi Perth, Australia
97 test fails are anticipated since it relies on patch in #2927077: $entity->toUrl('revision-*') should fill revision parameter on all applicable routes. →
For those keeping track, Block Content Revision UI → is updated to use the latest patch.
- 🇦🇺Australia acbramley
+++ b/core/lib/Drupal/Core/Entity/Form/RevisionRevertForm.php @@ -0,0 +1,300 @@ + if ($this->revision instanceof EntityChangedInterface) {
I think we should also check for
RevisionLogInterface
here and callsetRevisionUserId
andsetRevisionCreationTime
too? - 🇨🇦Canada jibran Toronto, Canada
Seems like we need a D8.9 patch as well.
Fatal error: Class Drupal\Core\Entity\Form\RevisionRevertForm contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Drupal\Core\Entity\EntityFormInterface::setEntityManager) in core/lib/Drupal/Core/Entity/Form/RevisionRevertForm.php on line 24
- 🇨🇦Canada jibran Toronto, Canada
buildDeleteRevisionLink is implemented, but revision-delete-form is not implemented in this patch yet.
It can be a follow up.
This issue is PP2. Blocked on both: #2927077: $entity->toUrl('revision-*') should fill revision parameter on all applicable routes. → #3043321: Use generic access API for node and media revision UI → . The patch also relies on the patch in each of these.
Is #3043321: Use generic access API for node and media revision UI → a hard blocker or can we provide a temp layer and remove it without a need for BC break?
Swap Node over to use this generic implementation, or in the least the trait. (especially to fix local task workaround)
Should be a follow up.
- 🇦🇺Australia dpi Perth, Australia
Updated issue summary of with items needing direction/decisions.
- 🇦🇺Australia dpi Perth, Australia
@ravi.shankar its not possible to fix the tests until the blockers are committed. Changes in your patch are not needed.
Fatal error: Class Drupal\Core\Entity\Form\RevisionRevertForm contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Drupal\Core\Entity\EntityFormInterface::setEntityManager) in core/lib/Drupal/Core/Entity/Form/RevisionRevertForm.php on line 24
Fixed Drupal 8 compat
-
Also fixed a minor CS issue.
The last submitted patch, 106: 2350939-106-generic-revision-ui.patch, failed testing. View results →
- 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10
Addressing
Items needing discussion
I've also asked some other framework managers to chime in.
Whether to continue blocking this issue on #3043321: Implement a generic revision access API, an access checker will be added and immediately deprecated.
I think we need to postpone on that issue, providing an access check here is a convenience to those who want a single patch, but that's not how we do core development - so make that a blocker
Whether Node should use this in this issue.
Typically we need something that is using it in core for the patch to go in, otherwise it ends up getting out of date. However using it for node will require a lot of deprecation. So if we're going to add it for at least one entity-type here, then adding it to node perhaps should be a follow-up because the deprecations will be complicated. I'll ask some other framework managers.
Whether we should generate local tasks, or if entities should add their own.
If we don't do this, then how do users find the pages? Field UI generates it for entity-types so it feels like this is in the same vein. Speaking of Field UI, which can be turned off - would making this an experimental module that you could turn on/off help with velocity here? Would that even be possible? I guess not otherwise it would exist in contrib.
Revert terminology updated to be agnostic of target revision' time.
Should we adopt Version terminology (per patch)?These are both questions for the UX team - can we get some screenshots here (tagging as such) and then when we have them, tag it for usability review and flag it with the UX team (#ux in slack).
- 🇬🇧United Kingdom catch
Whether Node should use this in this issue.
I would suggest a follow-up patch for Node, but it would be good to see a working patch there prior to this one getting committed. That way we validate the API here covers Node, without doubling or tripling the patch size.
Whether we should generate local tasks, or if entities should add their own.
I think we should generate local tasks, the less that modules have to do when defining a new entity type the better.
Revert terminology updated to be agnostic of target revision' time.
We currently have 'Set as current revision' for forward revisions, and revert for older revisions. In Drupal 7 and earlier 'revert' was also shows for forward revisions when using contrib modules that created forward revisions. I think this should be a separate UX issue if we want to change it again.
Should we adopt Version terminology (per patch)
Is this in the UI or code? Or both? If it's in the UI it should be a separate UX issue again. In code I guess if we want to do that, we need to decide whether we're adding new code with 'version' and gradually updating the old code behind it - this goes all the way down to entity table naming.
One thing I will say for revision is that especially with draft revisions, it accurately describes tracking changes to something (whether or not each individual change is published). Also it doesn't conflict with software version. So at least in code, I'm not sure there's a compelling reason to change it at all and it might make things less clear, but for the interface we should open an issue.
- 🇨🇦Canada jibran Toronto, Canada
Thank you for your replies.
Whether Node should use this in this issue.
Typically we need something that is using it in core for the patch to go in, otherwise it ends up getting out of date. However using it for node will require a lot of deprecation. So if we're going to add it for at least one entity-type here, then adding it to node perhaps should be a follow-up because the deprecations will be complicated. I'll ask some other framework managers.
Whether Node should use this in this issue.
I would suggest a follow-up patch for Node, but it would be good to see a working patch there prior to this one getting committed. That way we validate the API here covers Node, without doubling or tripling the patch size.
We can safely do media here and node in the follow-up but PP-3 node issue using this patch is a good idea.
- 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10
- 🇦🇺Australia dpi Perth, Australia
Thanks for moving this forward @larowlan @catch.
Whether we should generate local tasks, or if entities should add their own.
I think we should generate local tasks, the less that modules have to do when defining a new entity type the better.
If we don't do this, then how do users find the pages? Field UI generates it for entity-types so it feels like this is in the same vein.
Similarly to entity routing before route providers were a thing, modules would be required to provide their own .local_tasks.yml until 📌 add an entity Links handler, to provide menu links/tasks/actions for entity types Needs work . The patch as is tries adds a deriver as an intermediate between these and would ultimately be deprecated.
We currently have 'Set as current revision' for forward revisions, and revert for older revisions. In Drupal 7 and earlier 'revert' was also shows for forward revisions when using contrib modules that created forward revisions. I think this should be a separate UX issue if we want to change it again.
Should we adopt Version terminology (per patch)
Is this in the UI or code? Or both? If it's in the UI it should be a separate UX issue again. In code I guess if we want to do that, we need to decide whether we're adding new code
We're creating a bunch of classes with both Revert and Version, arriving at a decision earlier would be preferable to changing symbols in the future.
- 🇦🇺Australia dpi Perth, Australia
Working on reworking and improving test coverage, removal of access checker, adding delete revision form.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Regarding Local Tasks, I agree3 with @larowlan and @catch, even if it's a temporary solution I think it's better to provide it, because:
- As a module developer who uses revisionable content entities, anything which lowers the amount of work I have to do to implement a functioning revision UI is a win.
- As already pointed out, Field UI already does this so there's president for doing this. Layout Builder also does this but that's slightly different because unlike the Field UI.* I don't see a problem with having it then deprecating it for removal in a follow-up once 📌 add an entity Links handler, to provide menu links/tasks/actions for entity types Needs work lands, it's not like one extra deprecation is going to add any extra work for a D10 release, I'm sure there will be many more deprecations to come before D10.
* Layout builder doesn't have to be opted-in by each entity type, any content entity can use it without any additional effort, which is why I see that use case as a little bit different.
Thanks,
-Aaron - 🇦🇺Australia dpi Perth, Australia
Created 📌 [PP1] Switch Node revision UI to generic UI Needs work for switching Node over, immediately postponed.
- 🇦🇺Australia acbramley
Things like ✨ Provide users with the option to select an appropriate moderation state when reverting to a previous revision Needs work require the $form_state to be passed to the prepareRevision function. I've added that in this now for better compatibility later. NodeRevisionRevertForm also has this so would be required for 📌 [PP1] Switch Node revision UI to generic UI Needs work as well.
The last submitted patch, 117: 2350939-117-generic-revision-ui.patch, failed testing. View results →
- 🇦🇺Australia acbramley
Fixes the deprecation warnings. Not sure about the other failure, seems unrelated?
- 🇦🇺Australia darvanen Sydney, Australia
Trying to piece this together with a custom entity to try it out, and I can't extend the route provider.
As one of the ideas here is to reduce boilerplate, would it not make sense to have a RevisionHtmlRouteProviderTrait that is then used by the RevisionHtmlRouteProvider? Happy to have a go at adding one in.
- 🇦🇺Australia dpi Perth, Australia
@Darvanen take a look at Block Content Revision UI → for an up to date example.
This module uses alters, but entities themselves would aim to add these to their own annotation. Which of course is not trait-able.
- 🇦🇺Australia darvanen Sydney, Australia
Thanks @dpi :)
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionControllerTrait.php @@ -0,0 +1,172 @@ + if ($revision->hasTranslation($currentLangcode) && $revision->getTranslation($currentLangcode)->isRevisionTranslationAffected()) {
I see that's already been addressed.
- 🇦🇺Australia acbramley
We need to set the revision creation time after formatting the log and message. The original comment about the creation time being set when the revision is created is incorrect, in manual testing without calling setRevisionCreationTime the creation timestamp was still showing the old revision's time.
The last submitted patch, 124: 2350939-124.patch, failed testing. View results →
- 🇦🇺Australia dpi Perth, Australia
Progress, and another tome...
Created a change record → , with up to date info on what is required of entity types that wish to make use of this new functionality. Test entities in the patch are a fully working implementation.
Contents of operations column largely is now decided based on route access and context.
Revision delete form added, functionality ported from Node implementation (
NodeRevisionDeleteForm
).With this patch, we don't need to postpone on #3043321: Use generic access API for node and media revision UI → if we're happy with the operations approach. I’ve manage to put together the patch and tests without needing to add permissions. Test entities are making use of an established pattern of granting entity operation base on entity labels. For regular [non-test] entity types, all they need to do is implement an access controller responding to these standard revision operations, defer to permissions if they like. Or even override or replace the operations approach by extending the route provider.
On permissions
Entities are responsible for granting access based on operations. There is no automation for creating permissions, then deferring to the permissions based on operation. I don't think we should be forcing modules to implement permissions, whether automatically or manually. How access is determined for these new revision routes should be left up to the implementer. I've added simple examples to the change record → . Its a matter of adding a few lines responding to revision operations.
Technical changes:
- Brings across more forgotten functionality of Node revisions: revision page title for entities supporting logs.
- Added completely new tests. Covers most cases and scenarios. removed old [disabled] tests.
- Adds revision delete form.
- Since we're keeping local task generation, instead of adding a Node-opt out to core, inverted so Node opts itself out. (See
node_local_tasks_alter
. Tests added for this. - A backwards compatibility shim added to
EntityViewController::viewRevision
to allow the revision page title route callback to work. Related 🐛 Node revision view page title no longer displays custom title with date Needs work . - Revision access checker has been gutted, per above, now only restricts to revert/delete-revision routes to protect model against deleting/reverting default revision. Implementors may override or remove if they desire.
- Improved abstracting revision list so it can can be usefully extended, e.g buildRow, naming table rows, for revision table. This should hopefully avoid the need for projects like Diff → , and Node in the future, needing to fully replace instead of subclass/alter.
Further work
- I wonder if the
RevisionControllerTrait
is useful anymore: should its functionality be brought intoVersionHistoryController
. - Unpostpone!
- Feedback on how translations fit in.
Screenshots
- 🇦🇺Australia acbramley
A cursory look through the interdiff is looking really good, extremely thorough tests, commentary and change record.
Very nice work @dpi!!
- 🇨🇦Canada jibran Toronto, Canada
Really nice work with the patch. I have updated config_revision module to use the latest patch in 8751ff7. I followed the change notice and the changes were straight forward to implement. I ran into just one issue with the patch.
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionControllerTrait.php @@ -0,0 +1,183 @@ + if ($revision->hasTranslation($currentLangcode) && $revision->getTranslation($currentLangcode)->isRevisionTranslationAffected()) {
This is returning false for non-translatable entities.
- 🇩🇪Germany tstoeckler Essen, Germany
Re #129: looks like we need to bring #2951528: Empty results for revisions of entities without langcode → over into this patch.
- 🇨🇦Canada jibran Toronto, Canada
Here is the test only patch. Turns out we are already testing it.
Test only patch is #127+interdiff.
The patch is #13+interdiff.-
+++ b/core/modules/system/tests/modules/entity_test_revlog/src/Entity/EntityTestWithRevisionLog.php @@ -26,13 +26,13 @@ + * translatable = FALSE, @@ -63,7 +63,6 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { - ->setTranslatable(TRUE)
These changes are not needed but I just made them to make it clear that this is not a translatable entity along with its fields.
-
+++ b/core/modules/system/tests/modules/entity_test_revlog/src/Entity/EntityTestWithRevisionLog.php @@ -26,13 +26,13 @@ - * "langcode" = "langcode",
As per #2634230: Remove langcode entity key → , this was the only change needed to fail
\Drupal\FunctionalTests\Entity\RevisionVersionHistoryTest::testOrder()
.
-
- 🇨🇦Canada jibran Toronto, Canada
I don't think we provide an operation, form, or route for 'Revert to earlier revision of a translation' aka as
node.revision_revert_translation_confirm
in node. - 🇦🇺Australia acbramley
I've got a bit of a strange issue with this patch where a specific block isn't showing some revisions that have been reverted in the revision list.
Debugging into it, I found that
$revision->getTranslation($currentLangcode)->isRevisionTranslationAffected()
is returning NULL inRevisionControllerTrait::loadRevisions
. Not too sure how this is happening as other blocks of the same bundle are fine going through the same process. - 🇭🇺Hungary nevergone Nyíregyháza, Hungary, Europe
Please test and review, thanks!
- 🇫🇷France Nono95230
I add to this issue the same patch, compatible with version 8.9.7 of the Drupal core.
The last submitted patch, 140: drupal_core-8.9.7-2350939-140.patch, failed testing. View results →
- 🇫🇷France Nono95230
I add to this issue the same patch, compatible with version 8.9.7 of the Drupal core and compatible with version 7.0 of PHP.
- 🇨🇭Switzerland miro_dietiker Switzerland
@acbramley revisions that don't show under some conditions, especially in multilingual / translation contexts, is highly confusing. But beside the seemingly buggy behavior of isRevisionTranslationAffected() which is maybe a separate issue, we should maybe discuss improving the presentation and filtering of language specific revisions in a separate issue... i have the feeling that simply outputting all of them and applying a client side filter with the option "all" (in addition to specific languages) would sometimes be much more helpful...
- 🇦🇺Australia acbramley
@miro_dietiker yeah I ended up tracking it down to an edge case issue which was when reverting to a published revision that had the exact same field values as the currently published revision. I guess that particular flag is NULL when no field values have changed when saving a revision 🤷♂️
Drupal 9.1.0-alpha1 → will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule → and the Allowed changes during the Drupal 9 release cycle → .
- 🇮🇳India shubhangi1995 GURGAON
#143 misses setEntityManager() missing in RivisionRevertForm due to which there is a fatal error on reverting revisions.
just added the missing function in #143 - 🇦🇺Australia dpi Perth, Australia
Above patches 139/143/147 should be ignored as this issue is targeted for Drupal 9.
- 🇫🇷France Nono95230
Hello shubhangi1995,
Thanks for the patch you proposed in comment 147, but it doesn't work on a Php 7.0 project:
To reproduce the error, you would need to be in version 7.0 of php and version 8.9.7 of the Drupal core, and to go to a url of the type "/block/{block_content}/revision/{block_content_revision}/revert", then you'll get the following error that will appear "The website encountered an unexpected error. Please try again later".For this reason, I propose a new patch that I tested before on my local environment and including the same type of correction that shubhangi1995 made.
Translated with www.DeepL.com/Translator (free version)
- 🇬🇧United Kingdom mikestar5
To complement #96 ✨ Implement a generic revision UI Fixed (and, by the way, thank you so much for that 8.9.x patch → , it was very useful), I needed to add the following:
\core\lib\Drupal\Core\Entity\Form\RevisionRevertForm.php
Add at line 12:
use Drupal\Core\Entity\EntityManagerInterface;
Add at line 301:
public function setEntityManager(EntityManagerInterface $entity_manager) { $this->entityManager = $entity_manager; return $this; }
After these additions I was able to use the Block Content Revision UI →
Thank you again.
- 🇬🇧United Kingdom mikestar5
Just as an addition, the Revision History page and revision form page aren't being displayed as Admin paths
\core\lib\Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider.php
in the different functions like
- getVersionHistoryRoute
- getRevisionViewRoute
- getRevisionRevertRoute
you can add the following line:
->setOption('_admin_route', TRUE)
to get all those paths as Admin paths.
- 🇬🇧United Kingdom mikestar5
Hi,
I added a few modifications to patch revisioning-2350939-96-generic-revision-ui.patch which was the one that allowed me to have all this working.
With my corrections I got it working ok, it took a lot of work to find out what was happening, especially without knowing the core in depth.
Anyways, feel free to test. I applied it to Drupal 8.9.12
cd siteRoot git apply 0001-Patching-revisioning-for-all-entities-mixing-differe.patch
NOTE: this is to be used with Block Content Revision UI →
Good luck!
- 🇳🇱Netherlands spokje
Reroll of #134 ✨ Implement a generic revision UI Fixed against
9.2.x-dev
- 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10
Can we please have an issue summary update here, there's no record of what the missing item is
- 🇦🇺Australia darvanen Sydney, Australia
Here's the summary that got deleted around 15 October. I haven't edited it.
- 🇦🇺Australia darvanen Sydney, Australia
Fixed the formatting of issue links
- 🇺🇸United States smustgrave
Does the patch in #156 include blocks revisions UI? Are there additional steps needed for testing?
- 🇦🇺Australia acbramley
@smustgrave nope but you can use https://www.drupal.org/project/block_content_revision_ui → in combination with the patch :)
- 🇺🇸United States smustgrave
Thanks for pointing that out! Is there any plan for this ticket or another to implement a UI for blocks, taxonomy, and media revisions?
- 🇦🇺Australia acbramley
@smustgrave not in this issue, there will be follow-ups once this is merged.
- 🇨🇦Canada jibran Toronto, Canada
+++ b/core/lib/Drupal/Core/Entity/Controller/RevisionControllerTrait.php @@ -0,0 +1,184 @@ + abstract protected function buildRevertRevisionLink(EntityInterface $revision): ?array; +++ b/core/lib/Drupal/Core/Entity/Controller/VersionHistoryController.php @@ -0,0 +1,187 @@ + protected function buildRevertRevisionLink(RevisionableInterface $revision): ?array {
Can you spot the difference?
- 🇺🇸United States tenken
Shouldn't the Change Record note that in order to see the "create new revision" checkbox and log message similar to Block Content and Node entities, a custom entity annotation should include:
/** * show_revision_ui = TRUE, * route_provider = { * ... * "revision" = "Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider", * } */
Otherwise, yes the CR enables the Revision tabs and local actions ... but a developer cannot create a new revision via the entity add/edit forms without additionally adding this annotation entry.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Re #165: while that is absolutely relevant, that does already exists in Core and wasn't implemented in this issue, thereby not something that has changed between the relevant versions, so I suspect it's probably out of scope in the context of the change record. I would imagine most custom entities which use revisions probably already include that?
- 🇺🇸United States tenken
@aaronmchale I am in the process of slowly learning D8/B9 development best-practices. I am making my first custom revisionable entity.
The official documentation for Drupal 8 does not show `show_revision_ui` anywhere in the Docs:
* https://www.drupal.org/docs/8/api/entity-api/making-an-entity-revisionable →
* https://www.drupal.org/docs/drupal-apis/entity-api/converting-a-content-... →I was able to piece together information from this issue, and the Block Content Revision UI, and Linky Revision UI modules as examples of what to implement for this functionality along with the CR. But all these documentation references omit `show_revision_ui` usage for enabling this aspect of the revision ui for ones custom entity. I eventually looked at core Block Content and Node and saw the `show_revision_ui` annotation was included there.
The CR appears to be documentation showing how to enable a complete revision UI for a custom entity, and while this is a legacy annotation item it would seem like including this annotation within the CR would benefit other newbie D8/D9 developers since there is no prior complete documentation on how to achieve showing a "complete" revision UI for revisionable entities.
If nothing else hopefully my comments will help others who are also fumbling around in the dark. Thanks to all for your efforts.
Drupal 9.2.0-alpha1 → will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule → and the Allowed changes during the Drupal core release cycle → .
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
@tenken Yeah that's a great example of where the documentation hasn't evolved much since the early days of D8, despite the Entity API continuing to evolve, definitely something that needs addressing.
- 🇬🇧United Kingdom joachim
#2612222: Provide a better way to alter entity revision overview → will require changes here if it gets in first.
- 🇨🇦Canada jibran Toronto, Canada
Rerolled on top of #3043321-78: Use generic access API for node and media revision UI → .
- 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium
The code in checkAccess remains slightly different for node and media but both are largely similar. Isn't there a way we can put that in a method on the base class and call it from both node and media's access control handler's checkAccess(). I'm thinking that every contrib module wanting to support revisions will end up copy-pasting that large block of code and we're back at square one regarding code duplication.
- 🇦🇺Australia acbramley
Isn't there a way we can put that in a method on the base class and call it from both node and media's access control handler's checkAccess(). I'm thinking that every contrib module wanting to support revisions will end up copy-pasting that large block of code and we're back at square one regarding code duplication.
That's a great idea for a follow-up :)
- 🇺🇸United States smustgrave
We are still on 9.2 which just released today. Unfortunately the 9.2 patch no longer seems to apply.
- 🇺🇸United States alison
I tried to do a reroll, but I need clarification on the relationship between this issue and #3043321: Use generic access API for node and media revision UI → -- are patches on this issue meant to be applied "after" applying a patch from #3043321? -- that's how I interpreted the comment on #171, but I'm not sure if I understood correctly.
If "yes", we'll need to use a newer version of the patch on #3043321 -- but, they seem to be actively discussing how to proceed over there, so idk if that means we need to wait, or if we should just use the latest patch ( #98 → ), or...
(And regarding 9.2.x, I think it gets even muddier -- but I'm not going to ramble in that direction until I know more.)
ANYWAY I would be happy to help with a reroll, if someone doesn't beat me to it, once I know a little more.
- 🇺🇸United States smustgrave
This patch should work with 9.2.0 if anyone needs it.
- 🇦🇺Australia mstrelan
#177 seems to be a re-roll of #171 which also includes #3043321-78: Use generic access API for node and media revision UI → .
Here is a re-roll of #164 for 9.2.x and 9.3.x.
- 🇺🇸United States smustgrave
That was my mistake. #178 applies perfectly to 9.2
Thanks! The last submitted patch, 178: 2350939-178.patch, failed testing. View results →
- @dpi opened merge request.
Patch #181 is no longer working for Drupal 9.2.4. Can this be revised please?
- 🇦🇺Australia acbramley
@inteeka-help what do you mean it's not working? We were running the patch on 9.2.4 and now on 9.2.5 and it works fine.
Please provide some more detail about your issues :)
- 🇳🇿New Zealand john pitcairn
I added a 9.2.x test at #181, the patch fails to apply for that.
- 🇦🇺Australia acbramley
@John Pitcairn it applies with fuzz so should be fine for local sites. Feel free to re-roll it if needed :)
- 🇦🇺Australia darvanen Sydney, Australia
#3043321: Use generic access API for node and media revision UI → just got committed so this issue is now un-blocked!
My guess is it will need a re-roll, but I've queued #189 against 9.3.x just to see what happens.
- 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺
Exciting! 🥳
The last screenshots here are from July 2020, posted by @dpi in #127. I think we should update the issue summary (tag already present) to include screenshots of the current state, to make it easier to start reviewing and contributing to this issue.
So cool to see that for the first time ever this is now within reach!
- 🇨🇭Switzerland berdir Switzerland
Not sure if it will be easier to reroll the latest patch and untangle it from the revision access issue or go back to 2350939-171-do-not-test.patch. Reviewing that patch a bit.
-
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityController.php @@ -251,6 +270,29 @@ public function title(RouteMatchInterface $route_match, EntityInterface $_entity + * The title for the entity revision view page. + */ + public function revisionTitle(RouteMatchInterface $route_match, RevisionableInterface $_entity_revision): TranslatableMarkup { + $revision = $this->doGetEntity($route_match, $_entity_revision); + $titleArgs = ['%title' => $revision->label()];
I'm not sure if doGetEntity() is really the right thing to use here? it just uses the first entity route parameter that it finds, but don't revision routes have both the default entity and the revision?
With the explicit entity revision upcasting that we have now, wouldn't it be easier to move this to the new revision controller and directly use the upcasted object? we don't have to worry about BC then.
-
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php @@ -121,12 +122,26 @@ public static function trustedCallbacks() { * A render array. */ - public function viewRevision(EntityInterface $_entity_revision, $view_mode = 'full') { - return $this->view($_entity_revision, $view_mode); + public function viewRevision(EntityInterface $_entity_revision, $view_mode = 'full', RouteMatchInterface $routeMatch = NULL) { + $page = $this->view($_entity_revision, $view_mode); + + // Remove buildTitle pre-render added by view() if the route has a title + // callback. + if (isset($routeMatch) && ($route = $routeMatch->getRouteObject()) && $route->getDefault('_title_callback')) { + foreach ($page['#pre_render'] ?? [] as $key => $preRender) { + if (is_array($preRender) && (($preRender[0] ?? NULL) instanceof static) && (($preRender[1] ?? NULL) === 'buildTitle')) { + unset($page['#pre_render'][$key]); + } + } + } + + return $page; }
not really happy about this, but will need to think more about a better solution.
update: more thoughts after seeing more of this patch.
I'd consider adding a new method and deprecate this one entirely. Then we could just always do that buildTitle() thing. I'm also not sure if it really makes sense to reuse view(). is it really worth adding those html head links on revision pages? If not, then we'd add almost more code (and certainly more comlex code) to remove this property than copying the method without that section. And even if we want to keep it, we could have that part in a helper method.
-
+++ b/core/lib/Drupal/Core/Entity/Controller/VersionHistoryController.php @@ -0,0 +1,187 @@ + * + * This controller leverages the revision controller trait, which is agnostic to + * any entity type, by using \Drupal\Core\Entity\RevisionLogInterface. + */ +class VersionHistoryController extends ControllerBase {
I'm wondering if we should try and build this to be more flexible out of the box?
For example, there's the fairly common diff module that replaces this to make it a form that can be submitted. So right now, it basically has to duplicate all of it again, risking that thinks break if core changes.
that would mean defining this as a form without actually having any form elements, and maybe some hooks.
Not sure, just thinking out loud.
-
+++ b/core/lib/Drupal/Core/Entity/Routing/RevisionHtmlRouteProvider.php @@ -0,0 +1,172 @@ + + if ($version_history_route = $this->getVersionHistoryRoute($entity_type)) { + $collection->add("entity.$entityTypeId.version_history", $version_history_route); + } + + if ($revision_view_route = $this->getRevisionViewRoute($entity_type)) { + $collection->add("entity.$entityTypeId.revision", $revision_view_route); + }
why does one route use version history and 3 use revision something? shouldn't that be consistent?
-
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php @@ -73,7 +73,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setTranslatable(TRUE) - ->setSetting('max_length', 32) + ->setSetting('max_length', 64) ->setDisplayOptions('view', [ 'label' => 'hidden', +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php @@ -56,6 +62,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['name']->setRevisionable(TRUE); + $fields['name']->setSetting('max_length', 64); $fields['user_id']->setRevisionable(TRUE);
I'm not sure I understand why this is necessary, but it has probably been discussed above.
but certainly at least the second change is unnecessary if the parent is changed?
-
+++ b/core/modules/system/tests/modules/entity_test/src/EntityTestAccessControlHandler.php @@ -65,6 +65,26 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter + elseif ($operation === 'revert') { + // Disallow deleting latest and current revision. + return AccessResult::allowedIf(!$entity->isDefaultRevision() && !$entity->isLatestRevision() && in_array('revert', $labels, TRUE)); + } + elseif ($operation === 'delete revision') { + // Disallow reverting to latest (a pointless exercise). + return AccessResult::allowedIf(!$entity->isLatestRevision() && in_array('delete revision', $labels, TRUE)); + + }
shouldn't these things be covered by default somehow?
-
- 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10
-
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityController.php @@ -77,14 +88,21 @@ class EntityController implements ContainerInjectionInterface { + @trigger_error('Calling ' . __METHOD__ . ' without the $date_formatter argument is deprecated in drupal:9.1.0 and will be required in drupal:10.0.0. See https://www.drupal.org/node/3160537', E_USER_DEPRECATED);
this needs updating to 9.4.0 and 11.0.0 now
-
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityController.php @@ -251,6 +270,29 @@ public function title(RouteMatchInterface $route_match, EntityInterface $_entity + else {
this else isn't needed, we returned above
-
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityController.php @@ -345,4 +387,14 @@ protected function loadBundleDescriptions(array $bundles, EntityTypeInterface $b + final protected function dateFormatter(): DateFormatterInterface { + return $this->dateFormatter;
why is this method needed?
-
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php @@ -121,12 +122,26 @@ public static function trustedCallbacks() { + $page = $this->view($_entity_revision, $view_mode); + + // Remove buildTitle pre-render added by view() if the route has a title + // callback. + if (isset($routeMatch) && ($route = $routeMatch->getRouteObject()) && $route->getDefault('_title_callback')) { + foreach ($page['#pre_render'] ?? [] as $key => $preRender) { + if (is_array($preRender) && (($preRender[0] ?? NULL) instanceof static) && (($preRender[1] ?? NULL) === 'buildTitle')) { + unset($page['#pre_render'][$key]); + } + } + }
would it not be simpler to add a new optional argument to ::view $add_title or similar instead of this juggling dance?
-
+++ b/core/lib/Drupal/Core/Entity/Controller/VersionHistoryController.php @@ -0,0 +1,187 @@ + $username = [ + '#theme' => 'username', + '#account' => $revision->getRevisionUser(), + ]; + $context['username'] = $this->renderer->render($username);
are we missing collecting cacheability metadata from the user here
-
- 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10
Now the generic access issue is in, this needs a reroll
- 🇦🇺Australia darvanen Sydney, Australia
Here's a plain reroll of 171-do-not-test and an interdiff.
The parts shown in the interdiff are where the patch wouldn't apply and I manually edited in the rejected hunks.
Leaving on NR so we can see how the tests go but true status is NW based on #192 and #193.
- 🇦🇺Australia darvanen Sydney, Australia
Oh. The interdiff patch utility also rejected this hunk when making the interdiff, which was the only other place I had to make an edit to recreate the patch:
*************** *** 75,81 **** $entity_test_rev->setNewRevision(TRUE); $entity_test_rev->isDefaultRevision(TRUE); $entity_test_rev->save(); - $this->drupalGet('entity_test_rev/' . $entity_test_rev->id() . '/revision/' . $entity_test_rev->revision_id->value . '/view'); $this->assertRaw($entity_test_rev->label()); $this->assertRaw($get_label_markup($entity_test_rev->label())); --- 75,81 ---- $entity_test_rev->setNewRevision(TRUE); $entity_test_rev->isDefaultRevision(TRUE); $entity_test_rev->save(); + $this->drupalGet($entity_test_rev->toUrl('revision')); $this->assertRaw($entity_test_rev->label()); $this->assertRaw($get_label_markup($entity_test_rev->label()));
And it looks like I left some .orig files in there... working on removing those now.
- 🇦🇺Australia darvanen Sydney, Australia
I think interdiff is quitting when it throws an error, so take that file with a grain of salt. I'm not sure how else to approach it.
Also I think I can make this NW and queue the test manually, going to try that.
- 🇦🇺Australia mstrelan
@darvanen see https://www.drupal.org/docs/develop/git/using-git-to-contribute-to-drupa... → for why you can't create an interdiff from a reroll
- 🇦🇺Australia darvanen Sydney, Australia
@mstrelan thanks!
In that case, per the recommendation on that page, here's a plain diff of the two patches.
- 🇦🇺Australia darvanen Sydney, Australia
No, sorry, it was just a painful re-roll. I didn’t have time to address the feedback.
Drupal 9.3.0-rc1 → was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule → and the Allowed changes during the Drupal core release cycle → .
- 🇦🇺Australia acbramley
The rest of #192 seems kinda up in the air so not really sure what to action and what not to. #193.5 - I'm unsure about this since it's rendering a theme hook, shouldn't that theme hook add the cacheability? Also not sure if it's possible to bubble cacheability from a single cell in a table (probably does work).
- 🇺🇸United States smustgrave
This seems to work for me. Can this be marked reviewed?
- 🇺🇸United States smustgrave
Actually question when it comes to views. Nodes have "Get the actual content from a content revision" filter. Should this be made generic too or per entity?
- 🇦🇺Australia darvanen Sydney, Australia
#205 sounds like a good follow-up to me.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Agree with #205 and #206, sounds like it would be worth exploring, but definitely needs its own follow-up issue, as the scope of this issue is already quite wide.
- 🇺🇸United States smustgrave
Sounds good. Then the latest patch with https://www.drupal.org/project/media_revisions_ui → is working great for me. Would say this can be marked as reviewed
- 🇬🇧United Kingdom catch
Not a comprehensive review but found a few things. I'm not sure what to do about the 'revert' language stuff, on the one hand UI issues shouldn't block this, but on the other it's turning one-off UI terminology into an API with the same language, so making the problem worse.
-
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityController.php @@ -77,14 +88,21 @@ class EntityController implements ContainerInjectionInterface { $this->urlGenerator = $url_generator; + if (!$date_formatter) { + @trigger_error('Calling ' . __METHOD__ . ' without the $date_formatter argument is deprecated in drupal:9.4.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3160537', E_USER_DEPRECATED); + $date_formatter = \Drupal::service('date.formatter'); + }
Could probably just be for Drupal 10, especially since it's a constructor deprecation.
-
+++ b/core/lib/Drupal/Core/Entity/Controller/VersionHistoryController.php @@ -0,0 +1,187 @@ + + /** + * {@inheritdoc} + */ + protected function buildRevertRevisionLink(RevisionableInterface $revision): ?array { + if (!$revision->hasLinkTemplate('revision-revert-form')) { + return NULL; + } + + $url = $revision->toUrl('revision-revert-form'); + // Merge in cacheability after + // https://www.drupal.org/project/drupal/issues/2473873. + if (!$url->access()) { + return NULL; + } + + return [ + 'title' => $this->t('Revert'), + 'url' => $url, + ]; + } +
Don't want to derail this, however 'revert' has bothered me in this UI for years, since what this actually does is 'clone and publish' the revision it refers to. Reverting would be going back to before this change, which is not what happens at all.
-
+++ b/core/lib/Drupal/Core/Entity/Form/RevisionDeleteForm.php @@ -0,0 +1,315 @@ + + // When there is more than one remaining revision, redirect to the version + // history page. + if ($this->revision->hasLinkTemplate('version-history')) { + $query = $this->entityTypeManager->getStorage($entityTypeId)->getQuery(); + $remainingRevisions = $query + ->accessCheck(FALSE) + ->allRevisions() + ->condition($this->revision->getEntityType()->getKey('id'), $this->revision->id()) + ->count() + ->execute(); + $versionHistoryUrl = $this->revision->toUrl('version-history'); + if ($remainingRevisions > 1 && $versionHistoryUrl->access($this->currentUser())) { + $form_state->setRedirectUrl($versionHistoryUrl); + } + } + + if (!$form_state->getRedirect()) { + $canonicalUrl = $this->revision->toUrl(); + if ($canonicalUrl->access($this->currentUser())) { + $form_state->setRedirectUrl($canonicalUrl); + }
Why not redirect to the revision page even if there's only one revision left? We show it now, so the redirect will work, saves some logic, and consistent behaviour every time.
-
+++ b/core/lib/Drupal/Core/Entity/Form/RevisionRevertForm.php @@ -0,0 +1,339 @@ + /** + * {@inheritdoc} + */ + public function getQuestion() { + if ($this->getEntity() instanceof RevisionLogInterface) { + return $this->t('Are you sure you want to revert to the revision from %revision-date?', ['%revision-date' => $this->dateFormatter->format($this->getEntity()->getRevisionCreationTime())]); + } + return $this->t('Are you sure you want to revert the revision?'); + } + + /**
As above - we're not reverting the revision, we're reverting to the revision (via a new copy of it).
-
- 🇦🇺Australia darvanen Sydney, Australia
FWIW I've always read 'revert' in this context as reverting the *state of the entity* to that contained in the revision... I may be in the minority, but I sense a bikeshedding coming on - though if it gives this feature a little momentum I won't complain :)
I agree with point #210.3
- 🇦🇺Australia acbramley
@darvanen I agree, I hope we can divert that discussion to a separate issue and get this across the line without the bikeshedding! :)
- 🇬🇧United Kingdom catch
Yeah I think only #1 and #3 need to be dealt with here, will try to find an existing issue discussing 'revert'.
- 🇺🇸United States azinck
@catch there are quite a few issues that touch on various aspects of the terminology discussion, but the most relevant one I'm aware of is: #2899719: Revision/version language on revision listing page is misleading with content moderation enabled →
- 🇦🇺Australia darvanen Sydney, Australia
Also re #210.1, see #193.1 ✨ Implement a generic revision UI Fixed , might need to discuss with @larowlan.
- 🇦🇺Australia mstrelan
Scrap that. I misread the quoted code due to Gmail highlighting.
- 🇬🇧United Kingdom catch
@darvanen November was before we'd finalised exactly which deprecations we'd accept for the 9.4.x branch, but we decided to treat it as a normal minor release due to the sheer amount of blocking changes required for 10.0.x, so 10.0.x is the right branch to target for removal.
- 🇬🇧United Kingdom catch
Tagging novice for #210.1, the rest of #210 we can address in #2899719: Revision/version language on revision listing page is misleading with content moderation enabled → , and also for 9.4.0 release highlights.
Could still use an issue summary update and screenshots too.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Just a few things that come to mind while scanning the patch:
- I don't quite understand the reason for
RevisionControllerTrait
, practically it's only used once, inVersionHistoryController
. Why not just merge it intoVersionHistoryController
, I can't see one being used without the other and the choice of what goes where seems to be somewhat arbitrary. Even if a contrib module (let's say Diff for example) was to provide its own revision UI, I suspect the logical thing to do would be to extendVersionHistoryController
. So unless there's a specific reason for a separate trait, to me it would seem cleaner to just have everything in the one class. - I've added a comment at
#3153559-17: [PP-2] Switch Node revision UI to generic UI →
noting we will need to do something about the
node_local_tasks_alter()
hook implementation that this issue will add, just so we don't forget it's there. - We potentially still need a follow-up issue for @smustgrave suggestion in #205.
- I don't quite understand the reason for
Drupal 9.4.0-alpha1 → was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule → and the Allowed changes during the Drupal core release cycle → .
- 🇺🇸United States scotwith1t Birmingham, AL
I had to +1 the issue with the "revert" terminology. Why not "restore"? To me, that communicates what "revert" was trying to but doesn't imply rolling back in time but rather bringing it back to the state it was in at that point in time. My $.02 :)
- 🇭🇺Hungary Gábor Hojtsy Hungary
This is not actually in 9.4.0 so removing "9.4.0 release highlights".
- 🇧🇷Brazil andregp
I'll work on #219 and also #210.1 + #210.3 that were left.
- 🇧🇷Brazil andregp
I addressed the points from #219 (including creating a followup issue for #205 #3284730: Update node's view filter "Get the actual content from a content revision" → ) and the remaining points from #210.
- 🇺🇸United States capysara
Is there a good way to manually test this?
I've applied the patch and I'm testing out contrib modules that depend on it ( Media Revisions UI → and Block Content Revision UI → ). I could update the IS and add screenshots as noted in #218 ✨ Implement a generic revision UI Fixed once I have a clearer path forward.
- 🇺🇸United States capysara
While testing the most recent patch from #224 with existing contrib modules Media Revisions UI and Block Content Revision UI, I noticed that the Revert button was being added to the Current revision, so I added the check for default revision before the operations links are added to the table. I also updated the markup for Current revision; see Interdiff.
I originally noted this issue (with screenshots) in Block Content Revision UI 🐛 Should not be able to Revert the Current revision Fixed .
Here is a comparison screenshot using contrib Media Revisions UI dev-3.x:
- 🇦🇺Australia acbramley
+++ b/core/lib/Drupal/Core/Entity/Controller/VersionHistoryController.php @@ -291,16 +291,22 @@ protected function buildRow(RevisionableInterface $revision): array { - $row['operations']['data']['status']['#markup'] = $this->t('<em>Current revision</em>'); + $row['operations']['data']['status'] = [ + '#prefix' => '<em>', + '#markup' => $this->t('Current revision'), + '#suffix' => '</em>',
This change isn't necessary. It's fine to have simple HTML tags inside the t() call.
- 🇺🇸United States capysara
Thanks! I was trying to match it up with what's already in node, but it's tidier the way it was.
So here's the same patch with that one change removed with the interdiff against #224.
- 🇦🇺Australia darvanen Sydney, Australia
Is this patch supposed to supply actions on the revision UI? I'm not getting any with either #228 or #202.
This was taken while logged in as user 1:
- 🇺🇸United States capysara
@darvanen The action buttons should already be there out of the box; the patch doesn't add them. Can you provide steps to reproduce? For example, what entity are you looking at? What core version? Do you see the same behavior on a fresh site with core Node entity?
My steps:
I spun up a site using the Drupalpod chrome extension, with the patch applied. Core version 9.5.0-dev. I created a node of type Article, edited the node, go to the Revisions UI page, and I'm seeing the button displayed on Revisions UI. - 🇦🇺Australia darvanen Sydney, Australia
@capysara Node already has a revisions interface, the point of this feature is to make an interface available to any revisionable entity.
I am using custom entities that were generated some time ago. If anyone else lands here looking for the answer to my question: the link template names are "revision-revert-form" and "revision-delete-form", not "revision_revert" and "revision_delete".
- 🇺🇸United States capysara
Does that mean that your issue is fixed now with the template names update--you can see the actions in your UI?
If not, for a custom entity, you might take a look at the two contrib modules that are using this patch and see how they implement the UI.
- 🇦🇺Australia darvanen Sydney, Australia
Yes thank you, changing the link template names solved my issue
- 🇺🇸United States smustgrave
Rerolled from #228
Got when trying to apply
error: patch failed: core/modules/system/tests/src/Unit/Menu/SystemLocalTasksTest.php:44
error: core/modules/system/tests/src/Unit/Menu/SystemLocalTasksTest.php: patch does not applyAlso looking for a good way to test if anyone could provide steps.
- 🇺🇸United States smustgrave
Testing patch #228 with media_revision_ui 3.x branch
Installed media_revision_ui
Ensured image media bundle has revisions enabled
Created an image bundle and created several revisions
Verified the revisions tab and all revisions existed
Verified I was able to revertMarking as RTBC if anyone disagrees please move back.
- 🇳🇿New Zealand quietone
Thanks everyone for working on this long standing issue.
An issue summary was asked for in #218 and I see a minor change was made later. The are decision to be made that are not shown as resolved. For this issue it would help to have steps to reproduce in the Issue Summary. If that were present it could be tagged for manual testing as well and good some more feedback.
This is tagged as needing screenshots. While there are screenshots in the later comments it is easier for reviewers and committers to find the latest ones in the issue summary.
There is a deprecation message in the patch for 9.4 and removal in 10.0. That will probably change to deprecated in 10.1.x, but check with catch or xjm to be sure.
And, are the change records for this up to date?
I am removing the novice tag because I don't think updating the IS and testing to get screenshots is suitable.
- 🇳🇿New Zealand quietone
I've been thinking this also needs a usability review. larowlan asked for one in #108 with some specific question. Those question were answered in the following comments which is great but I am assuming that a usability review looks at other things as well. Therefor adding the needs usability review tag.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Could we get some screenshots in the issue summary off each screen? This would be beneficial before a review at the UX meeting.
Thanks.
- 🇺🇸United States smustgrave
Looks like screenshots are added
So just needs an update issue summary and someone to review the usability. Though I don’t believe this ticket creates any UI, there’s tickets on hold for that, so not sure what’s needed there
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Thanks. Queued for review at #3300912: Drupal Usability Meeting 2022-08-05 → or a future meeting. We have a backlog of issues currently to review so it could be a few weeks.
- 🇺🇸United States capysara
I updated the Issue Summary with an attempt to summarize some of the discussion that's already occurred regarding the remaining Decisions.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Thank you @capysara :)
Hopefully at the UX meeting we can reach a consensus for some wording recommendations. Given how long this issue has been open for and some of the complexity that's surfaced, I think the more that can reasonably be done in separate/follow-up issues the easier this will be to get done.
Adding anchor links to comments in the issue summary so it's easy to jump to the referenced comments.
- 🇺🇸United States smustgrave
Cleaning up tags.
Should this be moved back to NR?
- 🇦🇺Australia enyug
foreach ($entityStorage->loadMultipleRevisions(array_keys($result)) as $revision) { // Only show revisions that are affected by the language that is being // displayed. if (!$translatable || ($revision->hasTranslation($currentLangcode) && $revision->getTranslation($currentLangcode)->isRevisionTranslationAffected())) { yield $revision; } }
Hi: @dpi, This will prevent revision showing which is default revision but has NULL in revision_translation_affected, as mentioned in #138 #145 by @acbramley
- 🇦🇺Australia enyug
protected function prepareRevision(RevisionableInterface $revision, FormStateInterface $formState): void { $revision->setNewRevision(); $revision->isDefaultRevision(TRUE); + assert($revision instanceof TranslatableRevisionableInterface); + // Apply the same behaviour in the node revisions. + $revision->setRevisionTranslationAffected(TRUE); $time = $this->time->getRequestTime(); if ($revision instanceof EntityChangedInterface) { $revision->setChangedTime($time);
Add this to match the behaviours in node revision, fix issue in #138 #145
- 🇦🇺Australia enyug
Hide revert link for latest revision, hide delete link for current revision, also include fix in #249
- 🇺🇸United States SamLerner
I tested @enyug's #251 patch on a Drupal 9.4.3 site and everything works as expected in the instructions.
- 🇦🇺Australia darvanen Sydney, Australia
@enyug is there a particular reason you forked at #224 rather than continuing from #234 or was that a typo?
Here's a couple of things I noticed in #251:
-
+++ b/core/lib/Drupal/Core/Entity/Controller/VersionHistoryController.php @@ -107,6 +107,11 @@ protected function buildRevertRevisionLink(RevisionableInterface $revision): ?ar + // Disallow reverting to latest (a pointless exercise).
Nit: While correct, the value judgement isn't necessary here, suggest "Disallow reverting to the latest revision."
-
+++ b/core/lib/Drupal/Core/Entity/Controller/VersionHistoryController.php @@ -134,6 +139,11 @@ protected function buildDeleteRevisionLink(EntityInterface $revision): ?array { + // Disallow to delete default revision.
Grammar nit: "Disallow to delete".
Suggest "Disallow deleting the default revision" -
+++ b/core/lib/Drupal/Core/Entity/Form/RevisionRevertForm.php @@ -235,6 +236,9 @@ public function submitForm(array &$form, FormStateInterface $form_state) { + assert($revision instanceof TranslatableRevisionableInterface); + // Apply the same behavior in the node revisions. + $revision->setRevisionTranslationAffected(TRUE);
This appears to be unfinished test code.
Finally, "Hide revert link for latest revision, hide delete link for current revision" needs tests.
-
Added reroll of patch #250 on Drupal 9.5.x, also fixed Drupal CS issue of patch #250, and addressed points 1 and 2 of comment #253.
keeping the status to needs work for #253.3 for tests.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
We reviewed this issue at #3303134: Drupal Usability Meeting 2022-08-19 → , that issue has a link to the recording and a transcript.
In reviewing this issue, we recognised that we are essentially extending something which already exists for Node, and making it available to all other content entities. This is of course a huge improvement, and so we focused on the principle of consistency when reviewing this, that being that it should be consistent with what users are already used to in the Node Revision UI.
To that effect, we noticed that the revision UI in this issue is not entirely consistent with the UI provided by Node, overall it is very similar, but we noticed a few small things that were missing. For example, the Node Revision UI has a yellow background for the current revision, which is not present in this implementation, we also noticed that the Node Revision UI has help text on the revision list, which is also not present in this UI. It would be worthwhile to go through the Node Revision UI in detail and ensure this UI is as close to a 1-to-1 implementation as possible.
We also agreed that the Revisions tab itself should always use the admin theme, partly because the UI contains components and styles which were designed for the admin themes and we cannot guarantee that those will work properly in front-end themes. For example, we observed that the actions list drop-button on the revisions list breaks in Umani.
In a follow-up issue, we also recommended exploring providing an option on the appearance page to control this behaviour, in case the site really does want to use the front-end theme; Similar to the existing option which controls if the admin theme should be used when adding/editing content. A follow-up issue should be created to explore this further, adding the tag for that.
The meeting attendees were myself, benjifisher, rkoller, gquisini, dancbatista, lucasbaralm, diegors, DyanneNova, worldlinemine, shaal, lucassc.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
We also agreed that the Revisions tab itself should always use the admin theme
Posting this in a separate comment as it's an implementation detail and so am not speaking on behalf of the group in this comment: an easy to implement solution for this would be to provide an
Admin
version ofRevisionHtmlRouteProvider
which extends that class and adds the_admin_route: true
route option. Basically, doing exactly what the existing AdminHtmlRouteProvider does.All of the core content entities implementing this UI could use the Admin route provider, and that also gives contrib the option to use it.
- 🇦🇺Australia enyug
Regarding this:
@enyug is there a particular reason you forked at #224 rather than continuing from #234 or was that a typo?
I notice the changes after #224 prevent showing any other operations for default revision which could have other custom operations for it.
- 🇦🇺Australia dpi Perth, Australia
Taking a look at this in the short term.
- 🇦🇺Australia dpi Perth, Australia
Addressing usability review, and about Node.
Thanks to the participants of the usability review.
Upfront, as a reminder, we are not porting Node to use the work from this issue. That will be done in 📌 [PP1] Switch Node revision UI to generic UI Needs work
Since this issue doesn't add revision UI to any public facing entities, I think its fine to not have a one-for-one replication of node. We are primarily extracting the key parts used for all entities.
Yellow background
I'm questioning why we have this kind of colorisation in the first place. Shouldn't coloring solved in the realm of a theme? Claro, etc.
I tracked down the introduction of yellow back in #42072: Rework revisions overview screen → , so might be the time do review how useful it actually is. (UX review)
As for custom frontend like the yellow highlighting, I personally don't think it should be a part of this issue, as I think where the CSS (+JS?), should live, and how it is maintained and customised by individual entity types could become a mess. I don't think we have precedent in core for any of the routing/controller bits tied to entities having any CSS/JS/library component. Might be wrong, would really like to hear of examples.
Some suggestions,
- Genericise visual enhancements for something, when Node is ported to this generic UI in 📌 [PP1] Switch Node revision UI to generic UI Needs work
- Don't port the UI stuff from node at all, let it remain exclusively a Node feature.
- Remove
- Leave it up to individual entities to add and maintain libraries adding UI sparkle (CSS/JS interactivity) (other word for...).
Admin theme
No matter what the dropbutton should always render correctly as a list of buttons. Up to the theme to implement.
I think its fine to suggest we should be using the admin theme, as it is a very backendish thing to do. However even with Node, the admin theme may not be used even for the edit form, as a config toggle exists for this at
node.settings:use_admin_theme
. This is why I suggested in #96 we simply don't try to set admin theme at all (omit, not true or false), and leave it up to implementors. In code, it is a one-liner, after the boilerplate routeprovider and method override.In a follow-up issue, we also recommended exploring providing an option on the appearance page to control this behaviour, in case the site really does want to use the front-end theme;
Since Node currently provides a checkbox for this toggle via
node_form_system_themes_admin_form_alter
, I think it would make sense for Node to divest this responsibility as a part of 📌 [PP1] Switch Node revision UI to generic UI Needs work , we can make it entity-generic. Keep the checkbox location on/admin/appearance
, and migrate the configuration/existing node config/form alters into system.module or similar. This method also allows the routeprovider to reach out to system config, instead of requiring implementation specific subclasses or us providing anAdminHtmlRouteProvider
equivalent.In my opinion there is nothing actionable in this issue. @AaronMcHale I'm happy to update 📌 [PP1] Switch Node revision UI to generic UI Needs work if you agree:
- Yellow current revision row: decide one of:
- Keep as customisation, only for Node
- Port to generic UI
- Remove
- Migrate
use admin theme
out of Node, including config, hook, update RouteProvider introduced by this issue.
I dont think we need to block this issue on these points.
- 🇦🇺Australia dpi Perth, Australia
@260 Missed the boat on 9 and 10.0, new features in 10.1.
- 🇦🇺Australia dpi Perth, Australia
Moving this forward again...
I went through all comments, reviews, code updates, since my last review at #184.
Code Reviews (action points)
- #192 Berdir: .1, .2, .3, .4, .5 (🩹#202), .6
- #193 Larowlan: .1 (🩹#202), .2 (🩹#202), 3(🩹202), 4 (🩹202), 5
- #210 catch: .1, .2 (⏱#218), .3 (⏱#218), .4 (⏱#218)
- #219 aaronmchale: .1 (🩹#224)
- #227 acbramley: .1 (🩹#228)
- #230.5 singularo: .1
- #253 darvanen: .1 (🩹#254), .2 (🩹#254), .3
Addressing unaddressed feedback
(Items without 🩹 above)
#192.1
I'm not sure if doGetEntity() is really the right thing to use here? it just uses the first entity route parameter that it finds,Because
$_entity_revision
is passed to the second arg ofdoGetEntity
, it avoids the loop (first entity it finds). See the firstif
block.I found we have coverage in
\Drupal\FunctionalTests\Entity\RevisionRouteProviderTest::testRevisionTitle
.Work for 192.2 directly below negates the need for the way this method is put together with doGetEntity. See the new
EntityRevisionViewController::revisionTitle
#192.2
I'd consider adding a new method and deprecate this one entirely. Then we could just always do that buildTitle() thing. I'm also not sure if it really makes sense to reuse view(). If not, then we'd add almost more code (and certainly more comlex code) to remove this property than copying the method without that section.Agreed, deprecating and adding a new method in this controller is going to be a bit difficult (naming is hard). So I've created a new controller just for entity revisions. It specifically duplicates parts of EntityViewController::view(), without head links, etc.
I've reverted all changes to
\Drupal\Core\Entity\Controller\EntityViewController::viewRevision
and deprecated it. Created CR @ https://www.drupal.org/node/3314346 → . This also makes the $date_formatter arg CR obsolete: https://www.drupal.org/node/3160537/ →#192.3
RE VersionHistoryController
I'm wondering if we should try and build this to be more flexible out of the box?
For example, there's the fairly common diff module that replaces this to make it a form that can be submitted.Switching this from a controller to a form would be a major departure from how this page has behaved by default.
I have some thoughts about how to switch this page to a form, such as with a module which can intercept these pages, replace with a form, add extensibility. All while retaining the work we've done here by calling out to this controller.
Not impossible, might be good to let this evolve a few generations in contrib.
192.4
why does one route use version history and 3 use revision something? shouldn't that be consistent?
Pile this onto the revision vs. version pile.
Bikesheddy.
#210.1
if (!$date_formatter) {
Could probably just be for Drupal 10,Obsolete as of #192.2 above.
230.5
I noticed that the view_builder key is duplicated here while testing this patch.Though not from this issue. And shouldn't be a problem.
Created https://www.drupal.org/project/drupal/issues/3314353 →
#253.3
Removed in this MR.
- 🇦🇺Australia dpi Perth, Australia
Translation issues mentioned in #138 and #145 are apparently resolved by setting
setRevisionTranslationAffected
, as mentioned by #250. This method call happens to be added in\Drupal\Core\Entity\ContentEntityStorageBase::createRevision
after the initial patches in this issue. Rather than settingsetRevisionTranslationAffected
and other revision metadata ourself, it would make sense to call out tocreateRevision
on storage.We also avoid needing to set setNewRevision, isDefaultRevision ourselves.
Though to accommodate all this we need to change the method signature to pass down the new revision object.
- 🇦🇺Australia dpi Perth, Australia
#226
#228I'm okay with this being reverted in subsequent patches. The patch prevented any operations on the default revision / Controlling whether the revert button is present should be done in routing, alters, or elsewhere.
Fragments added to
buildRevertRevisionLink
andbuildDeleteRevisionLink
mention they are disallowing something:// Disallow reverting to the latest revision. // Disallow deleting the default revision. if ($revision->isLatestRevision()) { return NULL; }
However they are not disallowing anything, as the route is accessible. We already have
$url->access()
directly above, which determines if a revert is actually possible. Entities need to implement the access control the same as our test AccessControlHandler's:Implementing above code in access control handlers solve for #226 + #228
I've updated the patch removing thse soft disallows. We already had coverage to ensure latest revision doesnt get the Revert/Delete buttons in
RevisionVersionHistoryTest::testOperationRevertRevision/testOperationDeleteRevision
. I've added extra coverage to ensure these options can show even if they are latest, if desired. - @dpi opened merge request.
- 🇦🇺Australia dpi Perth, Australia
Created a new MR with above feedback.
I'd greatly prefer feedback on the MR. The patch/interdiff is attached are supplied as a convenience only. The first two commits in the MR can also be diffed to get a interdiff since 252.
Addresses PHPCS, new PHPStan issues from CI, updates test themes (classy -> stark or other), updates our deprecations.
Rebased for Drupal 10.1, as this wont be in D9.
Updated code style/syntax, etc for PHP 8.0 as it is minimum. Constructor property promotion added as it has general consensus in 📌 Use PHP 8 constructor property promotion for existing code Needs work .
- 🇮🇳India bhanu951
Would it be more efficient to merge both issues https://www.drupal.org/project/drupal/issues/2976861 📌 add an entity Links handler, to provide menu links/tasks/actions for entity types Needs work ?
It would be good if we can push changes in a single release.
If it is Ok I can work on merging both patches.
- 🇦🇺Australia darvanen Sydney, Australia
@Bhanu951 in short: no.
Both of these issues have HUGE patches/MRs which will take the maintainers a long time to validate, joining them would only make that situation worse. Small changes are always preferable even if they're related.
- 🇦🇺Australia dpi Perth, Australia
This issue is already long in the tooth. I dont think we need to merge this and 📌 add an entity Links handler, to provide menu links/tasks/actions for entity types Needs work together.
- 🇦🇺Australia dpi Perth, Australia
#3314353: EntityTestRev has duplicate view_builder annotation entries → merged today, updated MR resolving conflicts.
- 🇬🇧United Kingdom joachim
I don't think we should combine 📌 add an entity Links handler, to provide menu links/tasks/actions for entity types Needs work with this one.
They're two issues fixing different things. If they overlap in the areas of the code that they're touching, whichever one gets in first will mean that the other one will need updating. - 🇧🇪Belgium dieterholvoet Brussels
3. getRevisionDescription() is crashing with: 'The timestamp must be numeric' ignore, this was because I had older revisions of the entity that predated the addition of the revision_created field, and so the value was NULL.
I just encountered the same issue when working on 📌 Convert comments to be revisionable Needs work . The error causes a page crash on the revision overview of a comment. IMO this shouldn't be ignored: either we should set an initial value for
revision_created
during the upgrade (taken from thecreated
column), or we should handle the field being empty throughout the UI. I can't seem to find code that set an initial value for this field (and therevision_user
field for that matter) on any entity types that were already converted to be revisionable (eg. block content, terms, menu links), so I'm assuming there's existing content in the wild with NULL values for those columns. - 🇦🇺Australia dpi Perth, Australia
Its an unfortunate situation to be in, but its not really callers of
getRevisionCreationTime
responsibility to add extra vigilance. The interface for\Drupal\Core\Entity\RevisionLogInterface::getRevisionCreationTime
clearly has int as a return type.I'd suggest fixing the data, or working on an upgrade path if something was neglected, or update the interface to allow NULL.
In the case of converting a entity to revisionable, its up to that migration path to ensure new fields and such are populated correctly.
- 🇧🇪Belgium dieterholvoet Brussels
Okay, I'll work on that in 📌 Convert comments to be revisionable Needs work . FYI, I just created Comment Revision UI → , a module using the work from this issue to implement a revision UI for comments.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Replying to @dpi in comment #261 regarding usability review:
Upfront, as a reminder, we are not porting Node to use the work from this issue. That will be done in 📌 [PP1] Switch Node revision UI to generic UI Needs work
Since this issue doesn't add revision UI to any public facing entities, I think its fine to not have a one-for-one replication of node. We are primarily extracting the key parts used for all entities.
Yes, that's a fair point, we don't necessarily have to use this issue to make it like-for-like with Node, particularly given there's nothing that would break BC if we did it in a later issue.
Yellow background [...]
Wow, a 5-digit issue number, you don't see much of those these days!
Thank you for your efforts in tracking that down. During the review we placed focus on consistency, and didn't really discuss if the current approach is even a good idea. Ultimately what I think we want is a way for the user to know which is the "current revision" in a potentially long list of revisions, and yes that might end up being the responsibility of the theme, not the UI itself, maybe it does makes more sense for the theme to be responsible for that.
It does sound like there is more to discuss and figure out, so I'm happy if we push that to a follow-up issue, give it it's own space to be properly discussed.
Admin theme [...]
Okay so, quick testing confirmed that the "Use admin theme" checkbox does indeed control whether the Node version history list uses the admin theme. I like the idea of exploring making that checkbox entity-generic, that is something we could explore in 📌 [PP1] Switch Node revision UI to generic UI Needs work or a follow-up issue to this one.
In the meantime, the various implementation issues for core entity types should probably default to using the admin theme, which would mean they all have to sub-class the route provider class. Not really a problem, just need to make sure that's noted on each of the issues.
In my opinion there is nothing actionable in this issue. @AaronMcHale I'm happy to update 📌 [PP1] Switch Node revision UI to generic UI Needs work if you agree:
- Yellow current revision row: decide one of:
- Keep as customisation, only for Node
- Port to generic UI
- Remove
- Migrate
use admin theme
out of Node, including config, hook, update RouteProvider introduced by this issue.
I dont think we need to block this issue on these points.
Agreed.
- Yellow current revision row: decide one of:
- 🇨🇦Canada jamesyao
Thanks @acbramley for providing the patch #202.
The patch #202 works on Drupal Core 9.4.7 & PHP 8.0. It also works well with the config_revision module.
- 🇦🇺Australia dpi Perth, Australia
@AaronMcHale #277 / Usability review
Added info as discussed to #3153559-23: [PP-2] Switch Node revision UI to generic UI → .
Pushed update to version history route to use admin theme by default. We'll wire it up to the old node checkbox later in 📌 [PP1] Switch Node revision UI to generic UI Needs work
- 🇦🇺Australia dpi Perth, Australia
Updated MR with feedback from @jibran + @catch. Merged 10.1.x base.
Theres some things in there you may want to look at @catch.
Back to NR
- 🇺🇸United States smustgrave
Testing the MR https://git.drupalcode.org/project/drupal/-/merge_requests/2847 on a 10.1.x instance
Testing Blocks
- Went to /admin/structure/block/block-content/manage/basic
- Enabled Create new revisions
- Created a block named "Block Revision 1"
- Clicked Edit and changed name to "Block Revision 2"
- Verified I do not see a Revisions tab
- Installed https://www.drupal.org/project/block_content_revision_ui →
- Verified I do see a Revisions tab now.
- Click on the Revisions tab
- Revert to the first reivsion
- Go back to Block Library view
- Verify title is back to "Block Revision 1"
Testing Media
- Enable Media module
- Go to /admin/structure/media/manage/image
- Verified Create new revisions is checked
- Create an Image bundle with imageA
- Edit the bundle and upload imageB
- Verified I do not see a Revisions tab
- Installed https://www.drupal.org/project/media_revisions_ui →
- Verified I do see a Revisions tab now
- Clicked the tab
- Reverted back to my first revision
- Verified imageA appears
Fantastic work! I know a lot of projects will be excited for this to land.
+1 for RTBC but will defer to what @catch has to say about the changes. - 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
if (!$entityType->hasLinkTemplate('version-history')) { return NULL; }
This is a good idea, I'd be keen to see a related issue to implement the same thing for
DefaultHtmlRouteProvider
and others. - 🇦🇺Australia dpi Perth, Australia
Major update to issue summary and change record → .
I removed reference to capybara.zip in the summary, ideally it lives in its own repo. Otherwise I think the contrib revision UI's mentioned are acceptable living examples of integration with this work.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Added a couple of minor comments.
I did some manual testing by following the steps in the CR and modifying the Media Entity. For reference I have attached a diff to show what changes I made to the Media Entity.
I noticed a problem (this is the case with multiple revisions as well):
The operations column show the text "Current revision", but also the "Revert" and "Delete" actions.
We probably should provide some logic so that the Revert and Delete routes are not accessible for the "current revision". While revert technically works it doesn't make sense, and following the Delete action results in a WSOD.
- 🇦🇺Australia dpi Perth, Australia
Added a couple of minor comments.
Revision revert and delete forms as admin routes.
Yep, makes sense to me. Resolved.
The operations column show the text "Current revision", but also the "Revert" and "Delete" actions.
Media Revision UI contrib needs a bit of glue to fix this up: see also #3315383: Disallow revert/delete if latest → + 🐛 Should not be able to Revert the Current revision Fixed .
A few comments ago this was hidden visually only, but we needed to do this in access control (or possible routing via accesscheck). So contribs need to catch up a little bit.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Revision revert and delete forms as admin routes.
Yep, makes sense to me. Resolved.
Great!
A few comments ago this was hidden visually only, but we needed to do this in access control (or possible routing via accesscheck). So contribs need to catch up a little bit.
Ah yes, I see the most recent mentioned in your comment #264. I definitely think it makes sense to do something here if we can, especially since we're providing the routes. If we don't address this here it'll basically mean every implementation needing the same boilerplate code and some may miss it out completely. So if we can fix it, I think we definitely should!
- 🇦🇺Australia dpi Perth, Australia
We did have an access checker in here at one point but it was removed at some point. Probably around when #3043321: Use generic access API for node and media revision UI → when entity operations were utilised more, instead of the checker.
every implementation needing the same boilerplate code
Every implementation already must have a somewhat boilerplate set of access control added in their accesscontrolhandler anyway (See CR → ).
- 🇮🇳India bhanu951
I Tried to test the MR #2847 , I created a custom entity with revisions support using the below command.
drush gen content-entity Welcome to content-entity generator! –––––––––––––––––––––––––––––––––––––– Module machine name [web]: ➤ test_2350939 Entity type label [Test 2350939]: ➤ Entity type ID [test_2350939]: ➤ Entity base path [/test-2350939]: ➤ Make the entity type fieldable? [Yes]: ➤ Make the entity type revisionable? [No]: ➤ Yes Make the entity type translatable? [No]: ➤ Yes The entity type has bundle? [No]: ➤ Yes Create canonical page? [Yes]: ➤ Create entity template? [Yes]: ➤ Create CRUD permissions? [No]: ➤ Yes Add "label" base field? [Yes]: ➤ Add "status" base field? [Yes]: ➤ Add "created" base field? [Yes]: ➤ Add "changed" base field? [Yes]: ➤ Add "author" base field? [Yes]: ➤ Add "description" base field? [Yes]: ➤ Create REST configuration for the entity? [No]: ➤ The following directories and files have been created or updated: ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– • /app/web/modules/custom/test_2350939/test_2350939.links.action.yml • /app/web/modules/custom/test_2350939/test_2350939.links.contextual.yml • /app/web/modules/custom/test_2350939/test_2350939.links.menu.yml • /app/web/modules/custom/test_2350939/test_2350939.links.task.yml • /app/web/modules/custom/test_2350939/test_2350939.module • /app/web/modules/custom/test_2350939/test_2350939.permissions.yml • /app/web/modules/custom/test_2350939/config/schema/test_2350939.entity_type.schema.yml • /app/web/modules/custom/test_2350939/src/Test2350939AccessControlHandler.php • /app/web/modules/custom/test_2350939/src/Test2350939Interface.php • /app/web/modules/custom/test_2350939/src/Test2350939ListBuilder.php • /app/web/modules/custom/test_2350939/src/Test2350939TypeListBuilder.php • /app/web/modules/custom/test_2350939/src/Entity/Test2350939.php • /app/web/modules/custom/test_2350939/src/Entity/Test2350939Type.php • /app/web/modules/custom/test_2350939/src/Form/Test2350939Form.php • /app/web/modules/custom/test_2350939/src/Form/Test2350939TypeForm.php • /app/web/modules/custom/test_2350939/templates/test-2350939.html.twig
And modified the Annotations of the Entity Class as per latest Draft CR → dated 25 Oct 2022 at 14:07 IST by dpi .
* bundle_label = @Translation("Test 2350939 type"), * handlers = { * "list_builder" = "Drupal\test_2350939\Test2350939ListBuilder", * "views_data" = "Drupal\views\EntityViewsData", * "access" = "Drupal\test_2350939\Test2350939AccessControlHandler", * "form" = { * "add" = "Drupal\test_2350939\Form\Test2350939Form", * "edit" = "Drupal\test_2350939\Form\Test2350939Form", * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", * "revision-delete" = "Drupal\Core\Entity\Form\RevisionDeleteForm::class", * "revision-revert" = "Drupal\Core\Entity\Form\RevisionRevertForm::class", * }, * "route_provider" = { * "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider", * "revision" = "Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider::class", * } * },
I enabled module and cleared cache, I am getting the following error :
drush cr PHP Fatal error: Uncaught Error: Class "Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider::class" not found in /var/www/html/web/core/lib/Drupal/Core/Entity/EntityTypeManager.php:272 Stack trace: #0 /var/www/html/web/core/lib/Drupal/Core/Entity/EntityTypeManager.php(227): Drupal\Core\Entity\EntityTypeManager->createHandlerInstance('Drupal\\Core\\Ent...', Object(Drupal\Core\Entity\ContentEntityType)) #1 /var/www/html/web/core/lib/Drupal/Core/EventSubscriber/EntityRouteProviderSubscriber.php(43): Drupal\Core\Entity\EntityTypeManager->getRouteProviders('test_2350939') #2 [internal function]: Drupal\Core\EventSubscriber\EntityRouteProviderSubscriber->onDynamicRouteEvent(Object(Drupal\Core\Routing\RouteBuildEvent), 'routing.route_d...', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher)) #3 /var/www/html/web/core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php(108): call_user_func(Array, Object(Drupal\Core\Routing\RouteBuildEvent), 'routing.route_d...', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher)) #4 /var/www/html/web/core/lib/Drupal/Core/Routing/RouteBuilder.php(184): Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object(Drupal\Core\Routing\RouteBuildEvent), 'routing.route_d...') #5 /var/www/html/web/core/lib/Drupal/Core/ProxyClass/Routing/RouteBuilder.php(83): Drupal\Core\Routing\RouteBuilder->rebuild() #6 /var/www/html/web/core/includes/common.inc(518): Drupal\Core\ProxyClass\Routing\RouteBuilder->rebuild() #7 /var/www/html/drush/Commands/core_development/DevelopmentProjectCommands.php(84): drupal_flush_all_caches(Object(Drupal\Core\DrupalKernel)) #8 /var/www/html/drush/Commands/core_development/DevelopmentProjectCommands.php(55): Drush\Commands\core_development\DevelopmentProjectCommands->drupal_rebuild(Object(Composer\Autoload\ClassLoader), Object(Symfony\Component\HttpFoundation\Request)) #9 [internal function]: Drush\Commands\core_development\DevelopmentProjectCommands->rebuild(Array) #10 /var/www/html/vendor/consolidation/annotated-command/src/CommandProcessor.php(257): call_user_func_array(Array, Array) #11 /var/www/html/vendor/consolidation/annotated-command/src/CommandProcessor.php(212): Consolidation\AnnotatedCommand\CommandProcessor->runCommandCallback(Array, Object(Consolidation\AnnotatedCommand\CommandData)) #12 /var/www/html/vendor/consolidation/annotated-command/src/CommandProcessor.php(176): Consolidation\AnnotatedCommand\CommandProcessor->validateRunAndAlter(Array, Array, Object(Consolidation\AnnotatedCommand\CommandData)) #13 /var/www/html/vendor/consolidation/annotated-command/src/AnnotatedCommand.php(350): Consolidation\AnnotatedCommand\CommandProcessor->process(Object(Symfony\Component\Console\Output\ConsoleOutput), Array, Array, Object(Consolidation\AnnotatedCommand\CommandData)) #14 /var/www/html/vendor/symfony/console/Command/Command.php(308): Consolidation\AnnotatedCommand\AnnotatedCommand->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) #15 /var/www/html/vendor/symfony/console/Application.php(1020): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) #16 /var/www/html/vendor/symfony/console/Application.php(299): Symfony\Component\Console\Application->doRunCommand(Object(Consolidation\AnnotatedCommand\AnnotatedCommand), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) #17 /var/www/html/vendor/symfony/console/Application.php(171): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) #18 /var/www/html/vendor/drush/drush/src/Runtime/Runtime.php(124): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) #19 /var/www/html/vendor/drush/drush/src/Runtime/Runtime.php(51): Drush\Runtime\Runtime->doRun(Array, Object(Symfony\Component\Console\Output\ConsoleOutput)) #20 /var/www/html/vendor/drush/drush/drush.php(77): Drush\Runtime\Runtime->run(Array) #21 /var/www/html/vendor/drush/drush/drush(4): require('/var/www/html/v...') #22 /var/www/html/vendor/bin/drush(120): include('/var/www/html/v...') #23 {main} thrown in /var/www/html/web/core/lib/Drupal/Core/Entity/EntityTypeManager.php on line 272 Fatal error: Uncaught Error: Class "Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider::class" not found in /var/www/html/web/core/lib/Drupal/Core/Entity/EntityTypeManager.php:272 Stack trace: #0 /var/www/html/web/core/lib/Drupal/Core/Entity/EntityTypeManager.php(227): Drupal\Core\Entity\EntityTypeManager->createHandlerInstance('Drupal\\Core\\Ent...', Object(Drupal\Core\Entity\ContentEntityType)) #1 /var/www/html/web/core/lib/Drupal/Core/EventSubscriber/EntityRouteProviderSubscriber.php(43): Drupal\Core\Entity\EntityTypeManager->getRouteProviders('test_2350939') #2 [internal function]: Drupal\Core\EventSubscriber\EntityRouteProviderSubscriber->onDynamicRouteEvent(Object(Drupal\Core\Routing\RouteBuildEvent), 'routing.route_d...', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher)) #3 /var/www/html/web/core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php(108): call_user_func(Array, Object(Drupal\Core\Routing\RouteBuildEvent), 'routing.route_d...', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher)) #4 /var/www/html/web/core/lib/Drupal/Core/Routing/RouteBuilder.php(184): Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object(Drupal\Core\Routing\RouteBuildEvent), 'routing.route_d...') #5 /var/www/html/web/core/lib/Drupal/Core/ProxyClass/Routing/RouteBuilder.php(83): Drupal\Core\Routing\RouteBuilder->rebuild() #6 /var/www/html/web/core/includes/common.inc(518): Drupal\Core\ProxyClass\Routing\RouteBuilder->rebuild() #7 /var/www/html/drush/Commands/core_development/DevelopmentProjectCommands.php(84): drupal_flush_all_caches(Object(Drupal\Core\DrupalKernel)) #8 /var/www/html/drush/Commands/core_development/DevelopmentProjectCommands.php(55): Drush\Commands\core_development\DevelopmentProjectCommands->drupal_rebuild(Object(Composer\Autoload\ClassLoader), Object(Symfony\Component\HttpFoundation\Request)) #9 [internal function]: Drush\Commands\core_development\DevelopmentProjectCommands->rebuild(Array) #10 /var/www/html/vendor/consolidation/annotated-command/src/CommandProcessor.php(257): call_user_func_array(Array, Array) #11 /var/www/html/vendor/consolidation/annotated-command/src/CommandProcessor.php(212): Consolidation\AnnotatedCommand\CommandProcessor->runCommandCallback(Array, Object(Consolidation\AnnotatedCommand\CommandData)) #12 /var/www/html/vendor/consolidation/annotated-command/src/CommandProcessor.php(176): Consolidation\AnnotatedCommand\CommandProcessor->validateRunAndAlter(Array, Array, Object(Consolidation\AnnotatedCommand\CommandData)) #13 /var/www/html/vendor/consolidation/annotated-command/src/AnnotatedCommand.php(350): Consolidation\AnnotatedCommand\CommandProcessor->process(Object(Symfony\Component\Console\Output\ConsoleOutput), Array, Array, Object(Consolidation\AnnotatedCommand\CommandData)) #14 /var/www/html/vendor/symfony/console/Command/Command.php(308): Consolidation\AnnotatedCommand\AnnotatedCommand->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) #15 /var/www/html/vendor/symfony/console/Application.php(1020): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) #16 /var/www/html/vendor/symfony/console/Application.php(299): Symfony\Component\Console\Application->doRunCommand(Object(Consolidation\AnnotatedCommand\AnnotatedCommand), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) #17 /var/www/html/vendor/symfony/console/Application.php(171): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) #18 /var/www/html/vendor/drush/drush/src/Runtime/Runtime.php(124): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput)) #19 /var/www/html/vendor/drush/drush/src/Runtime/Runtime.php(51): Drush\Runtime\Runtime->doRun(Array, Object(Symfony\Component\Console\Output\ConsoleOutput)) #20 /var/www/html/vendor/drush/drush/drush.php(77): Drush\Runtime\Runtime->run(Array) #21 /var/www/html/vendor/drush/drush/drush(4): require('/var/www/html/v...') #22 /var/www/html/vendor/bin/drush(120): include('/var/www/html/v...') #23 {main} thrown in /var/www/html/web/core/lib/Drupal/Core/Entity/EntityTypeManager.php on line 272 [warning] Drush command terminated abnormally.
I am able to cross verify that the class RevisionHtmlRouteProvider do exist in codebase.
- 🇦🇺Australia dpi Perth, Australia
@290. remove the double-quotes around the class. The CR is correct.
-
Please continue to review the MR, not intermediate posted patchfiles.
- 🇮🇳India bhanu951
@dpi Thanks for the suggestion.
I have modified annotations as per your suggestion. Now the Custom Entity Test module is enabled.
But I am not seeing revision local task on the entity view page. When I try to access revisions url directly ( "/test-2350939/1/revisions") I am getting access denied page. Same is the case for revert and delete page as well.
I am not sure If I have made any mistake during testing but I can reproduce this issue on multiple instances.
* bundle_label = @Translation("Test 2350939 type"), * handlers = { * "list_builder" = "Drupal\test_2350939\Test2350939ListBuilder", * "views_data" = "Drupal\views\EntityViewsData", * "access" = "Drupal\test_2350939\Test2350939AccessControlHandler", * "form" = { * "add" = "Drupal\test_2350939\Form\Test2350939Form", * "edit" = "Drupal\test_2350939\Form\Test2350939Form", * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", * "revision-delete" = \Drupal\Core\Entity\Form\RevisionDeleteForm::class, * "revision-revert" = \Drupal\Core\Entity\Form\RevisionRevertForm::class, * }, * "route_provider" = { * "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider", * "revision" = \Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider::class, * } * }, * base_table = "test_2350939",
And link handlers as below
* links = { * "collection" = "/admin/content/test-2350939", * "add-form" = "/test-2350939/add/{test_2350939_type}", * "add-page" = "/test-2350939/add", * "canonical" = "/test-2350939/{test_2350939}", * "edit-form" = "/test-2350939/{test_2350939}/edit", * "delete-form" = "/test-2350939/{test_2350939}/delete", * "revision" = "/test-2350939/{test_2350939}/revision/{test_2350939_revision}/view", * "revision-delete-form" = "/test-2350939/{test_2350939}/revision/{test_2350939_revision}/delete", * "revision-revert-form" = "/test-2350939/{test_2350939}/revision/{test_2350939_revision}/revert", * "version-history" = "/test-2350939/{test_2350939}/revisions", * },
- 🇦🇺Australia dpi Perth, Australia
@Bhanu951 please check the contribs listed in the issue summary, as they are living examples. Particularly Block Content and Comment Revision UI have been recently updated. The CR should contain references to all the glue you need, which has been utilised in these examples.
If you have further questions feel free to ping in Slack#contribute.
Trying to avoid pinging ~175 followers with support queries ;)
- 🇺🇸United States smustgrave
From testing the MR (steps taken in my previous comment) think we are good to mark RTBC
If anyone disagrees please feel free to move back.
- 🇬🇧United Kingdom catch
Reviewed this again and I think we're in a good state, so committed/pushed to 10.1.x, thanks!
Really nice to see this in, we must be around the tenth anniversary of the Drupal 8 entity API being introduced and this issue is 8 years old too.
Did my best with issue credits, apologies if any contributions got overlooked.
Would be great to continue the momentum here and get 📌 [PP1] Switch Node revision UI to generic UI Needs work done next.
- 🇨🇦Canada jibran Toronto, Canada
Thank you very much for the commit and for everyone for reviewing, testing, patches and making the push to get it in.
- 🇬🇧United Kingdom catch
@nevergone there's a deprecation in here so not a straight backport. It might be possible to backport it with just the deprecation removed, but I haven't spent time looking at whether any other changes have implications for this.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Fantastic to see this getting committed.
But... I still think the "current revision" showing Revert and Delete is a problem:
Replying to @dpi in #289
Every implementation already must have a somewhat boilerplate set of access control added in their accesscontrolhandler anyway (See CR).
While I think most implementations will provide an access checker, technically speaking that is optional (I didn't provide one in comment #286). Whereas because of this, which I would say is a bug, it would now be required to provide one and add boilerplate code. It's also worth being mindful of that each implantation could decide to handle access in different ways and use different permissions, which means it's not really boilerplate in the same way as this would be.
I do feel strongly that we should do something, of course now in a follow-up. I don't think it's great from a DX perspective that each implantation needs to provide the same code, and if they don't (because some will forget or not bother), the result is a poor UX and WSOD. I'm a fan of making the DX easier, and so I believe we should be making it as simple as is practical to implement.
- 🇬🇧United Kingdom catch
@AaronMcHale a follow-up for that sounds good, I think we can change the behaviour there if we find something that saves the boilerplate.
Would also be good to revive #2899719: Revision/version language on revision listing page is misleading with content moderation enabled → if we can.
- 🇦🇺Australia dpi Perth, Australia
While I think most implementations will provide an access checker, technically speaking that is optional (I didn't provide one in comment #286). Whereas because of this, which I would say is a bug, it would now be required to provide one and add boilerplate code
The accesschecker is not required if it is done with with a access control handler Only an extra one line is required as implementors already need to have one granting revision operations in the first place.
An access checker is desirable if we wanted to provide defaults for all entity types.
Pasting my comment from Slack:
I remain on the fence here. I do like being able to ask $entity->access('revert') and getting a definitive answer about whether im able to do that operation. moving it out of access control into routing makes the answer harder to determine. The whole thing is borderline bug/borderline preference
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
I do like being able to ask $entity->access('revert') and getting a definitive answer about whether im able to do that operation. moving it out of access control into routing makes the answer harder to determine.
Oh yeah totally agree with you there, I always prefer to do things at the "source of truth".
Here's what I'm thinking, and I'm going to use Media as an example.
The
media
entity usesMediaAccessControlHandler
, which extendsEntityAccessControlHandler
.What if we introduce a new helper class which extends
EntityAccessControlHandler
, something likeContentEntityAccessControlHandler
or if we want to be more specificEntityAccessControlHandlerWithRevision
. In that class we overrideaccess
orcheckAccess
(whichever is most appropriate) to implement the logic forrevert
anddelete
. That way, we don't break BC for any existing implementations while also providing the boilerplate code.Having said that,
EntityAccessControlHandler::access
already has some logic for if the Entity is an instance ofRevisionableInterface
, so maybe we could get away with just adding this to the existingaccess
method, or maybe we move that existing logic into the new class that I described.As a further thought, Route Providers have established the convention of having a method for every route, the interface doesn't prescribe such a structure but it's now an established pattern. Maybe we do something similar with Access Control Handlers? It might make things a little cleaner if we had a method for each operation. Maybe we even match the method names to the route provider class method names, just an idea, might not make sense in practice. If we did that though, it might work in our favour because it would mean restructuring, introducing new classes and likely deprecating the existing ones.
In any case, this is all things that we can further discuss in follow-up issues.
Automatically closed - issue fixed for 2 weeks with no activity.
- 🇳🇿New Zealand quietone
Changing tag to the more commonly used format to easy searching.
- 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland
Finally got around to creating 🐛 Revert and Delete actions should not be available for the current revision Fixed as a follow-up to the discussion in #286 to #289 and #304 to #307.
- 🇳🇿New Zealand quietone
@AaronMcHale, thanks for making the follow ups.
I am removing the tag.
- 🇦🇺Australia dpi Perth, Australia
Thanks! Confirming patch was required for 9.4.8 -> 9.4.9
- 🇺🇸United States bdanin
The patch in comment #314 applied cleanly for me in the latest Drupal 9.5.x
- 🇮🇳India sumit_saini
Patch in #314 did not apply to D9.5.2 for the same reason. So, here is a reroll for someone using D9.5.2.
- 🇺🇸United States DamienMcKenna NH, USA
@enyug: Please open a new issue for that addition.
- 🇺🇸United States AaronBauman Philadelphia
There have been a couple changes upstream since these patches were posted, namely this change which allows the patch to work even if an entity doesn't have a 'revision' link template.
Updated patches for 9.5.10 and 10.0.10 (10.0.x maybe?) attached for anyone who needs.
- heddn Nicaragua
I have a bunch of contrib and even custom entities that as of 10.1 now all have duplicate 'Revision' tabs. Is there any interest in opening a BC follow-up ticket to make it more obvious where these things need to be fixed (trigger_error) and meanwhile, not add 2 Revision tabs if there is already a revision tab created?
Or something, anything. Because otherwise the discussions happening in 🐛 Drupal 10.1: Revisions tab appears twice on Groups Needs review will happen across contrib for the next few years as we try to support 10.0, 10.1, etc.
- heddn Nicaragua
I've added 🐛 Duplicate "revision_history" local tasks created Needs review . Partially I added it to find all the occurrences of duplicate revisions. But hopefully we can land it and help others too.