Problem/Motivation

Revisionable entities with custom layout override only track layout changes incidentally when an entity is saved via the entity's edit form. In order to provide full layout revisioning, we should check that the entity type's bundle is revisionable and whether or not a new revision should be created before calling entity save in the Section Storage object.

Proposed resolution

Section storage for the field should determine if the entity's bundle is RevisionableEntityBundleInterface and check shouldCreateNewRevision(). If this is "true" then a new revision should be set on the entity before saving.

Remaining tasks

Write a patch
Write tests
Convince people

User interface changes

No direct changes to the UI. Layout saves for entity override would prompt the creation of new revision which would display the entity's revision tab. That "ui change" is incidental and expected.

API changes

None

Data model changes

None

Comments

EclipseGc created an issue. See original summary.

eclipsegc’s picture

StatusFileSize
new828 bytes

Something like this, but better probably.

Eclipse

kristen pol’s picture

This wasn't marked for review but I tested it anyway in order to write out some instructions for testing. I did the following:

  • Enabled layout modules
  • Used "Basic page" content type since it's already configured for revisions
  • Created a node with title and body text
  • For the layout, only added a block with the body
  • Edited the node and update the node body text so a new revision was created
  • Updated the layout so the content type block was shown below body block
  • Reverted back to the original version of the node
  • Confirmed that the layout went back to just showing the body
  • Reverted back to the 2nd version of the node
  • Confirmed that the layout went back to showing the body and content type

eclipsegc’s picture

Sounds like a pretty good test to me. The patch needs to be "more correct" but how'd you feel about the functionality? Thoughts?

Eclipse

kristen pol’s picture

@EclipseGc It worked exactly like I thought it would so it makes sense to me from a UX perspective. The patch code is readable and I understand you want to make it "more correct" but I don't know what would make it "more correct" ;)

tim.plunkett’s picture

https://www.drupal.org/node/2936357 recently changed the code for revisions

tim.plunkett’s picture

Component: layout.module » layout_builder.module
johnwebdev’s picture

Assigned: Unassigned » johnwebdev
johnwebdev’s picture

StatusFileSize
new1.41 KB

Here's a rerolled patch from #2 taking the new API into consideration and changes done since. Still needs tests, and everything else required to complete this issue.

johnwebdev’s picture

Assigned: johnwebdev » Unassigned
johnwebdev’s picture

Looking at this again, especially when working with inline blocks (https://www.drupal.org/project/drupal/issues/2957425 or serialized one). This one makes much sense.
If enforcing a new revision is bad, can we make some UX changes to allow for saving with a new revision?

andypost’s picture

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -230,6 +232,14 @@ public function label() {
+    $bundle = $this->entityTypeManager->getStorage($bundle_entity_type)->load($this->getEntity()->bundle());

bundle could be not loadable entity
This code will fail for user entity

tedbow’s picture

Status: Active » Needs review
StatusFileSize
new5.61 KB
new3.58 KB
new5.06 KB
  1. Fixed #12 include test for user
  2. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -230,6 +232,14 @@ public function label() {
    +      /** @var ContentEntityStorageInterface $storage */
    

    @var should use full space

  3. Added tests
tim.plunkett’s picture

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -230,7 +231,18 @@ public function label() {
-    return $this->getEntity()->save();
+    $entity = $this->getEntity();
+    if ($entity->getEntityType()->isRevisionable() && $bundle_entity_type = $this->getEntity()->getEntityType()->getBundleEntityType()) {
+      $bundle = $this->entityTypeManager->getStorage($bundle_entity_type)->load($entity->bundle());
+      if ($bundle instanceof RevisionableEntityBundleInterface && $bundle->shouldCreateNewRevision()) {
+        /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+        $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
+        $revision = $storage->createRevision($entity);
+        return $revision->save();
+      }
+    }
+
+    return $entity->save();

There is no way this is reasonable for modules to need to do themselves. I think we need an issue like #2942907: Entity system does not provide an API for retrieving an entity variant that is safe for editing and an @todo here pointing to it.

Status: Needs review » Needs work

The last submitted patch, 13: 2937199-13.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new974 bytes
new5.18 KB

Status: Needs review » Needs work

The last submitted patch, 16: 2937199-16.patch, failed testing. View results

johnwebdev’s picture

Status: Needs work » Needs review

Setting this back to NR because the fails in patch in earlier comment was due to #2973992: Permission issue in Nightwatch step marks all full testruns as unstable

Also retest the patch.

The last submitted patch, 2: 2937199-2.patch, failed testing. View results

twfahey’s picture

Status: Needs review » Reviewed & tested by the community

Patch in #16 looks good to me. Test coverage makes sense and I think is comprehensive enough. All coding standards check out. Manual testing is working as expected - changes to the layout are triggering revisions, and reverting back to a previous revision is restoring the previous layout. Looks like everything is well commented and readable.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 16: 2937199-16.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review

I am guessing this was random unrelated test failure: "Drupal\Tests\quickedit\FunctionalJavascript\FieldTest"

retesting

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

Yep unrelated random test failure. Patch is passing again.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 16: 2937199-16.patch, failed testing. View results

johnwebdev’s picture

Status: Needs work » Reviewed & tested by the community

Not sure why this is Needs work because 2937199-16.patch looks like it did pass.

tim.plunkett’s picture

Status: Reviewed & tested by the community » Needs work

Just got a chance to review this, had a couple suggestions and questions.

  1. --- a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    
    @@ -230,7 +231,20 @@ public function label() {
       public function save() {
    
    @@ -0,0 +1,107 @@
    +class RevisionsTest extends BrowserTestBase {
    

    This code is in an API method, why is it being testing with a BrowserTestBase?

  2. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -230,7 +231,20 @@ public function label() {
    +    $entity = $this->getEntity();
    ...
    +    if ($entity->getEntityType()->isRevisionable() && $bundle_entity_type = $this->getEntity()->getEntityType()->getBundleEntityType()) {
    

    $this->getEntity() is in here twice. Not a big deal, but it keeps jumping out at me :)

  3. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -230,7 +231,20 @@ public function label() {
    +    // @todo Request editable revision when API provided by entity API.
    +    // @see https://www.drupal.org/node/2942907
    

    @see is invalid inline, and I think the comment could be rewritten:

        @todo Replace this specific implementation with calls to the generic API
          from https://www.drupal.org/node/2942907.
    
  4. +++ b/core/modules/layout_builder/tests/src/Functional/RevisionsTest.php
    @@ -0,0 +1,107 @@
    +    // Create two nodes.
    

    These are bundles (node types), not nodes

  5. +++ b/core/modules/layout_builder/tests/src/Functional/RevisionsTest.php
    @@ -0,0 +1,107 @@
    +  public function testRevisions() {
    +
    +    $this->drupalLogin($this->drupalCreateUser([
    

    Extra blank line

  6. +++ b/core/modules/layout_builder/tests/src/Functional/RevisionsTest.php
    @@ -0,0 +1,107 @@
    +    /** @var \Drupal\node\NodeStorageInterface $node_storage */
    +    $node_storage = $this->container->get('entity_type.manager')->getStorage('node');
    ...
    +    $revision_id = $node_storage->getLatestRevisionId($revision_node->id());
    

    Nitpick, move this variable declaration down to where it's used

  7. +++ b/core/modules/layout_builder/tests/src/Functional/RevisionsTest.php
    @@ -0,0 +1,107 @@
    +    $revision_id = $node_storage->getLatestRevisionId($revision_node->id());
    +    $this->saveLayoutOverride($revision_node);
    +    $this->assertGreaterThan($revision_id, $node_storage->getLatestRevisionId($revision_node->id()));
    +
    +    $revision_id = $node_storage->getLatestRevisionId($no_revisions_node->id());
    +    $this->saveLayoutOverride($revision_node);
    +    $this->assertEquals($revision_id, $node_storage->getLatestRevisionId($no_revisions_node->id()));
    

    The reuse of $revision_id for both $revision_node and $no_revisions_node is confusing. Also, this could use some comments to explain why these two are different

  8. +++ b/core/modules/layout_builder/tests/src/Functional/RevisionsTest.php
    @@ -0,0 +1,107 @@
    +    // Save an override on a non-revisionable entity.
    +    $this->saveLayoutOverride(User::load(1));
    

    What are we actually asserting here? The saveLayoutOverride helper doesn't do that much on it's own.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new9.97 KB
new9.54 KB

1. added \Drupal\Tests\layout_builder\Unit\OverridesSectionStorageTest::testSave
2. fixed
3. fixed
4. fixed
5. fixed
6. fixed.
7. used more descriptive variable names and added comments.
8. I wanted to make sure that the logic didn't assume the entity is revisionable.
i.e. call

$storage = $this->entityTypeManager->getStorage($entity_type->id());
$revision = $storage->createRevision($entity);

saveLayoutOverride() checks the confirmation message which would not be the case if there was error. But maybe we don't need this now \Drupal\Tests\layout_builder\Unit\OverridesSectionStorageTest::testSave is added.

Version: 8.6.x-dev » 8.7.x-dev

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.

tim.plunkett’s picture

Thanks for all the changes! One more thought

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -230,7 +231,21 @@ public function label() {
+    $entity_type = $entity->getEntityType();
+    // @todo Replace this specific implementation with calls to the generic API
+    // from https://www.drupal.org/node/2942907.
+    if ($entity_type->isRevisionable() && $bundle_entity_type = $entity_type->getBundleEntityType()) {
+      $bundle = $this->entityTypeManager->getStorage($bundle_entity_type)->load($entity->bundle());
+      if ($bundle instanceof RevisionableEntityBundleInterface && $bundle->shouldCreateNewRevision()) {
+        /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+        $storage = $this->entityTypeManager->getStorage($entity_type->id());
+        $revision = $storage->createRevision($entity);
+        return $revision->save();
+      }
+    }

This is really precise and complex bundle/revision logic that LB should have no knowledge of (hence the @todo).

Can this patch provide methods getNewRevisionDefault() and getBundleEntity(), and @see the implementations on \Drupal\Core\Entity\ContentEntityForm? I think it would be much clearer that way, and easier to clean up in the long run.

Also, nit: every line after the first in an @todo should be indented an additionally 2 spaces

tedbow’s picture

StatusFileSize
new5.49 KB
new11.16 KB

@tim.plunkett good suggestion. Done

johnwebdev’s picture

  1. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -228,7 +229,56 @@ public function label() {
    +    $entity = $this->getEntity();
    ...
    +    return $entity->save();
    

    This could be a one-liner now.

  2. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -228,7 +229,56 @@ public function label() {
    +   *   generic API is available. Currently this method copied from
    ...
    +   *   generic API is available. Currently this method copied from
    

    Do we really need to mention these are copied from there?

tedbow’s picture

  1. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -228,7 +229,56 @@ public function label() {
    +    $entity = $this->getEntity();
    +    if ($this->getNewRevisionDefault()) {
    +      /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
    +      $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
    +      $revision = $storage->createRevision($entity);
    

    We have to use the entity 2 other times in the method so creating a local variable makes sense I think

  2. I am not sure I was responding to @tim.plunkett's suggestion in #29. @tim.plunkett is this normally done?
johnwebdev’s picture

#32.1 Yeah you're absolutely right, I totally over-looked that. Sorry :P

Status: Needs review » Needs work

The last submitted patch, 30: 2937199-30.patch, failed testing. View results

tim.plunkett’s picture

I agree with @tedbow on both counts.

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -230,21 +230,57 @@ public function label() {
   public function save() {
     $entity = $this->getEntity();
...
-    // @todo Replace this specific implementation with calls to the generic API
-    // from https://www.drupal.org/node/2942907.
...
+    if ($this->getNewRevisionDefault()) {
+      /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */

Can we keep an @todo here too?


Test failure is in InlineBlockPrivateFilesTest::testPrivateFiles, uh oh.

tim.plunkett’s picture

xjm’s picture

tim.plunkett’s picture

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new845 bytes
new11.29 KB

re #35 added back the todo.

Test failure is in InlineBlockPrivateFilesTest::testPrivateFiles, uh oh.

uh oh indeed

for anyone interested see security problem documented here #2957425-239: Allow the inline creation of non-reusable Custom Blocks in the layout builder

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

Now that the above issue was recommitted with the fix, this looks to be ready!

The last submitted patch, 27: 2937199-27.patch, failed testing. View results

larowlan’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -228,7 +229,58 @@ public function label() {
    +   * Should new revisions created on default.
    

    nit: language is off here, think the word 'be' might be missing

  2. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -228,7 +229,58 @@ public function label() {
    +      $new_revision_default = $bundle_entity->shouldCreateNewRevision();
    

    we could return here and avoid the local $new_revision_default variable, with a return FALSE later

  3. +++ b/core/modules/layout_builder/tests/src/Functional/RevisionsTest.php
    @@ -0,0 +1,115 @@
    +    // Ensure that saving a layout for a node of the bundle that does save a new
    +    // revision by default does create a new revision.
    

    the two uses of `does` here make this confusing to read, can we improve it?

    Suggest:

    Ensure that saving a layout for a node where the bundle defaults to saving new revisions causes a new revision to be saved.

    or something.

  4. +++ b/core/modules/layout_builder/tests/src/Functional/RevisionsTest.php
    @@ -0,0 +1,115 @@
    +    // Ensure that saving an entity type that does not revisionable does not
    +    // have an error.
    

    this comment needs work

  5. +++ b/core/modules/layout_builder/tests/src/Functional/RevisionsTest.php
    @@ -0,0 +1,115 @@
    +   * @throws \Behat\Mink\Exception\ResponseTextException
    +   * @throws \Drupal\Core\Entity\EntityMalformedException
    

    do we need these?

  6. +++ b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php
    @@ -387,4 +391,88 @@ public function testBuildRoutes() {
    +    $entity_type = $this->prophesize(EntityTypeInterface::class);
    

    am I the only one that things this test is highly contrived because of the amount of mocking required?

I've also asked some folks with better knowledge of revision api than I have to give it a review

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new3.09 KB
new11.1 KB

@larowlan thanks for the review

1. fixed
2. Fixed
3. Suggestion looks good. Fixed
4. fixed
5. removed.
6. I don't it shows that ::save() respects the entity type for whether there is bundle available and the bundle for whether a new revision is the default.

Status: Needs review » Needs work

The last submitted patch, 43: 2937199-43.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new856 bytes
new11.16 KB
+++ b/core/modules/layout_builder/tests/src/Functional/RevisionsTest.php
@@ -0,0 +1,112 @@
+    $this->assertGreaterThan($revision_id_before_new_revision, $node_storage->getLatestRevisionId($revision_node->id()));

This is where #43 failed. The difference that makes it fail is not actually anything in this issue but rather #2983887: Add static cache to \Drupal\Core\Entity\ContentEntityStorageBase::getLatest*RevisionId()

Basically before we could call $node_storage->getLatestRevisionId($revision_node->id()) before and we would always get the latest revision.

But now have to call $node_storage->resetCache([$revision_node->id()]); to make sure we get that latest revision.

I am actually wondering if #2983887: Add static cache to \Drupal\Core\Entity\ContentEntityStorageBase::getLatest*RevisionId() is actually a BC break because if a class gets the storage handler for an entity type \Drupal\Core\Entity\EntityTypeManager::getHandler() is going to give me an existing instance if another class has asked for the storage handler.

UPDATE: Actually @berdir pointed out that \Drupal\Core\Entity\EntityStorageBase::doPostSave() resets the cache and this is only a problem in tests.

jibran’s picture

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -267,7 +268,57 @@ public function label() {
+  protected function getNewRevisionDefault() {

It is not a getter so the method should be named better.

tedbow’s picture

@jibran

This function was added because of @tim.plunkett's comment in #29 and are copies of \Drupal\Core\Entity\ContentEntityForm. They will be removed in #2942907: Entity system does not provide an API for retrieving an entity variant that is safe for editing

sam152’s picture

For what it's worth, the proposal in #3004536: Move the Layout Builder UI into an entity form for better integration with other content authoring modules and core features subverts the need for this and also provides the same options content admins already have elsewhere: the option to create a new revision and the ability to write a revision log message.

tim.plunkett’s picture

Status: Needs review » Postponed

Let's postpone for now on that entityform approach.

johnwebdev’s picture

Status: Postponed » Needs review
StatusFileSize
new42.43 KB

Now that #3004536: Move the Layout Builder UI into an entity form for better integration with other content authoring modules and core features has been committed. We should be able to mark this issue as Closed.

Manual test:

1. Installed Drupal 8.7.x.
2. Installed Layout Builder and Enabled it and overrides on Basic page.
3. Created a new node.
4. Updated the Layout override and Saved. A new revision was made.
5. It's possible to revert back.

However, unticking "Create a new revision" in the Layout Builder UI still creates a new revision, which perhaps deserves its own issue? The timestamp on the new revision is also incorrect.

I created: #3033516: Revision log message field missing from layout overrides entity form.

phenaproxima’s picture

I tried the steps in #50 with the very tip of 8.7.x HEAD. Unchecking the box and saving the layout did not create additional revisions. So I'm not sure this issue, or #3033516: Revision log message field missing from layout overrides entity form, is needed anymore...

phenaproxima’s picture

I checked out 8.7.x to #2926508: Add DateTimeNormalizer+TimestampNormalizer, deprecate TimestampItemNormalizer: @DataType-level normalizers are reusable by JSON:API and gave this another quick manual test. I still couldn't reproduce a problem; if unchecked the "Create new revision" checkbox, no new revision was created. If I left the checkbox checked, then a new revision was created. That seems like expected behavior...

johnwebdev’s picture

Status: Needs review » Closed (works as designed)

Me and phenapromixa manually tested the concerns in #50 which seems to work as expected, which means we can now close this issue.

johnwebdev’s picture

Status: Closed (works as designed) » Closed (outdated)
tim.plunkett’s picture

Version: 8.7.x-dev » 8.8.x-dev
Status: Closed (outdated) » Active
Issue tags: -Layout Builder stable blocker

This issue has an @todo in code pointing to it, with what looks like a temporary workaround that was added in #2957425: Allow the inline creation of non-reusable Custom Blocks in the layout builder.
Reopening for more discussion, or to remove the @todo pointing here.

johnwebdev’s picture

Status: Active » Needs review
StatusFileSize
new1.11 KB

Correct Tim!

We always make a new revision on the inline blocks, regardless if the parent entity is saved with a new revision or not. Let's see what happens now when we update the code to do what was proposed in the todo.

sam152’s picture

Status: Needs review » Needs work

Patch looks great to me. I assume this is NW for tests? I imagine we'll need to test both the scenarios where new inline block revisions are created as a result of the parent creating a new revision and the opposite where a new revision is not created.

sam152’s picture

Hm, I've figured out a bunch of reasons why #56 would lead to rewriting the revision history of an item of content in #3053881: Reverting entity revisions that contain custom blocks erroneously triggers EntityChangedConstraint.

So I think this actually needs to go the opposite way, new revisions need to be enforced on all custom blocks that are modified as part of an entity save, regardless of the parent entity and tests should be introduced to enforce that.

Anything short of that will compromise the integrity of content revisions and essentially cause data loss.

sam152’s picture

Since this issue has been repurposes a couple of times already, I've spun off #3053887: Add test coverage and document why inline blocks require a new revision to be created when modified, regardless of whether a new revision of the parent has been created to add appropriate test coverage which would have marked #56 as red.

sam152’s picture

tim.plunkett’s picture

There's a link to this issue in the codebase, we need to remove that

bkosborne’s picture

Status: Closed (won't fix) » Active

Re-opening per #61

bkosborne’s picture