Problem/Motivation
If two users attempt to edit the same layout, they will both see the changes the other is making.
Proposed resolution
Mimic the Views UI (which also uses tempstore.shared
) and lock the Layout Builder UI when another user is editing the layout.
Remaining tasks
N/A
User interface changes
The LB UI will now show a message when there is a lock of another user. Breaking the lock will bring you back to the LB UI to make your own changes.
API changes
N/A
Data model changes
N/A
Release notes snippet
N/A
Comment | File | Size | Author |
---|
Issue fork drupal-3025231
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
tim.plunkettComment #4
tim.plunkettFail patch was good. Here are some fixes for other tests.
Comment #6
tim.plunkettD.o git infra snafu
Comment #8
tim.plunkettComment #9
Kristen PolThanks for the patch. I reviewed the code for clarity and code formatting and didn't see anything obviously wrong. I compared the code against some of the Views lock code and when looking at this:
I'm wondering why the Lock class was created rather than using the same approach as Views. And, if the Lock class is better, then should Views be refactored to use that?
I'll do some testing.
Comment #10
Kristen PolI tested this by:
One oddity I found was after I broke the lock with the form and got back to the layout page, if I reloaded the page, it would show a message that there were unsaved changes even when there weren't any changes. This might not be due to this patch but it seems like it might be. Given that and my previous comment in #9 I'll mark this back to needs work for feedback.
Comment #11
tim.plunkett#9
I wrote up a small Lock class because using stdClass is kinda gross.
Ideally it would be made a part of TempStore more directly, but the TempStore subsystem needs some love anyway. In the meantime, putting a
final
and@internal
implementation in this experimental module should be fine.#10
The unsaved changes bit is the current expected behavior, even if that makes no sense.
It's tied to #2936501: Reverting an override redirects the user to an edit form for a new override and therefore still sets the new override as "unsaved changes", which is confusing and #2917777: Improvements to the styling, grouping, etc. of the Layout Builder UI actions form and even #2988970: Layout Builder should make it easier to modify the default layout for an entity type when viewing an entity
Comment #12
Kristen PolGiven comments from #11, marking RTBC.
Comment #13
xjmAt a first read, this seems out of scope?
Comment #14
xjmSo given the impact on user input (where the user gets seemingly broken and irrelevant data on editing a layout the same time as someone else), this is a major bug.
I discussed this issue with @tim.plunkett a bit. There are two parts to the fix:
I suggest that we split the first off into its own blocking issue, since it's a good change on its own. For the second, reviewing it here is a little difficult because we have a slightly forked, slightly improved version of the API that the Views tempstore uses, which I don't think we should do. @tim.plunkett indicated that his main concern was using a
stdClass
object to pass around the data the way that Views currently does. We agreed to work on an upstream patch to fix that. At a minimum, knowing what the upstream patch will look like will make this issue easier to review, and ideally, we can just make that upstream fix without adding technical debt for this issue on top of what already exists in views.Thanks!
Comment #15
tim.plunkettPostponed on #3025867: Provide a classed object for TempStore metadata and #3025870: Cancelling changes in the Layout Builder UI should use a confirmation form
Comment #16
tim.plunkettHere's a patch for this issue that is built on those other two.
Comment #17
tim.plunkettStill postponed, but rerolled.
Comment #18
tim.plunkettBlocker is in!
This is the same as the 17-do-not-test
Comment #19
phenaproximaIt's not a big deal, but I'm not clear why we need to store this in a boolean property just for getQuestion(). Perhaps a new protected method would be better?
Other than that, I think I have no complaints. Patch seems very straightforward and consistent with the Views UI locking mechanism.
Comment #20
tim.plunkettI don't see any reason to either store the lock statefully or cause multiple tempstore retrievals. Putting this in a property allows the other method to utilize it, treats buildForm like the form equivalent of __construct that it is, and allows future customization of the form based on this Boolean
Comment #21
phenaproximaThought about it a bit, and since the class is relatively sparse, I think this is probably fine. I'd be less comfortable with it if this were a more complex form, but for what it is, I don't think there are too many risks here. RTBC.
Comment #22
Kristen PolDoes this need to be manually tested again?
Comment #23
tim.plunkettHere's another reroll. Idk if it needs another manual test, but it shouldn't because we have automated tests. Can't hurt though.
Comment #24
xjmFWIW I would end up manually testing this myself before commit if the testing were not repeated, so if #10 were repeated with the final patch that would save committer time. :)
Comment #25
Kristen PolI tried to test the patch on simplytest.me a few times but it kept bombing out. I'll try to remember to try again tomorrow.
Comment #26
xjmI tested with defaults (instead of overrides) using #10's steps and did not get this message with the patch applied. However, I did get redirected back to the "Manage display" page when I broke the lock, instead of the form I was on, which is confusing. Marking NW for that.
Comment #27
tim.plunkettAha, yes that is odd. And turns out, the test coverage I wrote was odd because of it!
After breaking the lock the test was drupalGet()ing back to where it should have just redirected in the first place.
Comment #29
tim.plunkettUgh, extra newline.
Comment #30
phenaproximaI briefly tested this, using defaults (not overrides), and was redirected back to the layout editing page when I broke the lock. Based on the last few comments, that seems to be the desired behavior. It worked for me, then, so I guess this is good to go.
Comment #31
xjmI think the message or description here should indicate that "breaking the lock" means throwing away the other user's changes. (Edit: The "break this lock" part of the message on the UI itself should probably indicate that as well.)
Comment #32
tim.plunkettBoth this string and the string in the message are mimicking Views UI. And the message string is literally in Drupal\Core code now.
For the reference, other string is
'This @label is being edited by user @user, and is therefore locked from editing by others. This lock is @age old. Click here to <a href=":url">break this lock</a>.'
A follow-up to consider improving this in both Layout Builder and Views UI (and the generic shared code) would be fine, but I think the message is sufficient.
Comment #33
xjmI know the original string message about the existence of the lock is from the Views UI, but the confirm form saying "Are you sure you want to break this lock?" does not give the user enough information. It just says "Are you sure you want to break this lock? This action cannot be undone." Which sounds fairly harmless. But it's deleting another person's work and data from the database.
Tagging for UX review.
Comment #34
xjmI am talking about
core/modules/layout_builder/src/Form/DiscardLayoutChangesForm.php
, a change made to it in this patch. See #31. It's not inDrupal\Core
.Comment #35
xjmAlso, let's please not use Views UI as our standard of acceptable UX for LB (or anything). :P Views UI is very very admin-facing; LB is OTOH available to content authors.
Screenshots for the UX review. The user clicks the "break this lock" link in this message:
That link takes them to this form:
The result of the action is that another user's unsaved changes to the layout are lost forever, including their unsaved changes if they are on the form making edits right now. I don't think "break the lock on layout changes" sufficiently conveys to the user that the in-progress changes from @otheruser are going to be immediately and permanently lost.
Comment #36
tedbowJust testing with defaults but presumably this would happen with overrides.
These links also work so the other locked out user can actually discard or save the active users work with these links.
We don't want to add an access check for locks in
\Drupal\layout_builder\Access\LayoutBuilderAccessCheck::access()
because this would stop the user from even seeing the Manage layout page with the lock message.But we could make another another access checker service and tag routes like we are tagging with '_layout_builder_access'. Maybe '_layout_builder_locked_access'. Then check if the section storage is locked by another user so we don't allow access Save and Discard links.
I think if we add the new access tag '_layout_builder_locked_access' to all the "add section" "add block", etc routes then this would stop the both users from making changes at the same time. It would be good if we could detect the 403 on the dilag and reroute the page back to manage layout where they would then see the lock message.
Comment #37
tim.plunkettOkay I'm postponing this on #3004536: Move the Layout Builder UI into an entity form for better integration with other content authoring modules and core features when we can actually manipulate those actions (save, discard, etc) more dynamically.
Comment #38
xjmComment #39
tim.plunkettComment #40
xjmThis will need to be updated with:
following #3026698: Allow section storage to provide a more granular ID for tempstore.
Edit: Forgot to mention the above is from @tim.plunkett telling me what to type. :D
Comment #41
tim.plunkettRerolled! Also addressed #36 by hiding the actions if there is a lock.
Before anyone points it out: Yes, there is some duplicated code between OverridesEntityForm and DefaultsEntityForm.
There will likely be more added in future issues.
But refactoring now could jeopardize the ability to refactor it correctly later. I'd rather duplicate it for now than get stuck with something worse.
Comment #43
tim.plunkettWe can't 100% know in advance which UID we'll get in the test, so adding an assertion for both. Using
==
because even though it is documented that the ID will always be an int, sometimes it's a string :(Comment #45
tim.plunkettComment #46
tim.plunkettReroll
Comment #48
tim.plunkettFixing selectors for the BEM changes
Comment #50
tim.plunkettReroll
Comment #52
tim.plunkettFixing the tests
Comment #53
tim.plunkett#36.1 is obsolete since those are form buttons now, not routes
This patch addresses #36.2
Comment #54
johnwebdev CreditAttribution: johnwebdev commentedThe properties should probably be in top of the class.
Maybe change to: Checks for a lock on the layout
Should be injected as well(?)
Comment #56
tim.plunkett#54
1) Not sure how it ended up in the wrong place! Thanks
2) Sure
3) I really didn't want to inject this because it's stupid that the messenger service doesn't accept render arrays, but I don't think that's going to change any time soon.
Comment #57
johnwebdev CreditAttribution: johnwebdev commented#56 Those changes looks great!
Did some manual testing. The base functionality works as expected. Awesome!
I think it would be more nice if the message was in the content area, and the other UI elements (i.e. revisions) was hidden and maybe also the "You are editing message", because you aren't really editing it, right? IMO, this can be in a follow-up.
Did some edge case testing as well:
Assume two user's has the same layout opened, but they have not yet done any changes. If one of the user adds the block, they are now the owner. If the second user now tries to add a block they will get a 403 as expected, yay! Though there are no message explaining why, so it just looks like the UI is broken. Should we adress that? Maybe in follow-up?
Why do we have a if-else here?
Not showing in patch, but the comment for the test is wrong.
Comment #58
phenaproximaThe term "locked" could use a bit of explanation here. How about "...is locked (i.e., changed by another user)"?
Even though these are technically optional, not passing them triggers deprecation errors, so they're not truly optional. Therefore, +1 on not starting the descriptions with "(optional)".
Why doesn't this parameter also trigger a deprecation notice?
I find this logic jarring; there's a weird double-negative introduced by switching on
!$lock
. I guess this is a nitpick, but ternary expressions really read better (to me, anyway; I'm not sure if I'm in the majority here) if the thing being tested isn't inverted. If it's not too much of an eye-roller, can this be changed? :)Nice, clear comment. Thanks!
This is a good API addition, but I think it also condemns this patch to 8.8.x and beyond only. So 8.7.x users will need to deal with confusing concurrency when editing layouts. :(
@tim.plunkett also asked me for my opinion on the discussion that took place between #31 and #35. I completely agree with @xjm -- we should not use the terminology of Views UI. In my ideal world, we wouldn't even use the word "lock" in this warning; that's a technical term we'd be exposing to content editors. My preferred wording would be something like this: "This layout is being edited by $other_user. If you edit this layout now, their changes will be lost. [link]Continue editing the layout anyway[/link]."
I like that the break_lock_link stuff is abstracted into a render element, but I don't really like the rigidity of its phrasing. So what if we added a new property to it with a default value that keeps the current wording? Something like:
Using predefined placeholders (like formatPlural() does), calling code could easily customize the phrasing of the link:
Comment #59
tim.plunkett#57
1) Fixed that here, it drastically changes where we do this.
2) Hmm I think we should decide on that now. It could be really messy in a follow-up.
@TODO
3) That was a debugging thing I think, it can be simplified now.
#58
1) Punting on this for now until we settle on the phrase we're going to use throughout.
@TODO
2/3) This is all changed in response to #57
4) I rewrote it. I think it looks better as you suggest, but more closely matches to the intuitive logic the way I had it. /shrug
6) Nothing we can do about that :(
"7") Okay, I will look into this proposed approach
@TODO
Posting a patch for some intermediate changes, then going back for
#57.3
#58.1
#58.7
Comment #61
bnjmnmIn #59 there were test failures in LayoutBuilderUiTest and LayoutBuilderTest... fixed half of them
Fixed LayoutBuilderUiTest by changing LayoutRebuildTrait to refresh the entire entity form, not just the layout builder render element. This required a decent amount of refactoring, which is what the majority of changes in this patch came from. It also surfaced a problem reported in #3045171: Form blocks rendered inside layout builder break save, where adding the search block to a layout would prevent the layout from being saveable. The solution in this patch fixes the issue without removing the form from the Layout Builder UI.
I was not able to fix LayoutBuilderTest::testConcurrentEditing(), but I was able to narrow down it down enough that someone may be able to identify the problem. The test fails when a user unlocks the layout to make it editable, but when they return to the layout it is still locked by the other user. This does not happen with manual testing, and I added assertions to the test that confirm the layout's temp storage has been cleared. To rule out a timing issue, I tried refreshing the page and adding a pause before going to the Layout Builder UI, but the test still thinks the layout is locked.
There are also some changes where I moved code shared by DefaultSectionStorage and OverridesSectionStorage into a shared trait.
Comment #62
tim.plunkettSoooo we definitely can't do that. But I'm not sure what to do instead....
Nice!
Weird double quotes. Could use a comment
Can't this use getLayoutBuilderUrl in both?
Comment #63
bnjmnmThis addresses all the items in #62
#62.1 created a FormEditableSectionStorageInterface interface with a method for getting the entity directly from section storage. For section storage instances that don't implement the interface, the rebuild process after an ajax update will rebuild layout builder UI as opposed to the entire entity form
#62.3 the weird double quotes were just the byproduct of fatigue -- changed to single.
#62
yeppp... (see above)
Still not sure how to address the failure in
\Drupal\Tests\layout_builder\Functional\LayoutBuilderTest::testConcurrentEditing
, another set of eyes on that would be appreciated. Manual tests work, and the test confirms the tempstore is emptied, but the UI within the test does not reflect that.Comment #65
bnjmnmTests were failing because Layout Builder element was set to be uncacheable, but not the entity forms. Dynamic page cache was retaining the markup generated while locked.
I also expanded the concurrent editing test to cover overrides in addition to defaults and removed a method I added in a previous patch that wasn't supposed to be there to begin with (artifact of some earlier attempts at changing the AJAX refresh)
Comment #67
bnjmnmCorrected unit tests to reflect the changed cache config for layout entity form routes.
Comment #68
bnjmnm#67 is a mistake as the branch was not rebased. Here is the corrected patch, interdiffed from #65
Comment #69
effulgentsia CreditAttribution: effulgentsia at Acquia commentedI haven't read or tested this patch yet to know if it applies to the Layout tab of an entity that's using overrides. Under the assumption that it does, I added #3063373: Add content locking (anti-concurrent editing) to core? to the Ideas queue.
Comment #71
tim.plunkettNeeds a reroll
Comment #72
rpayanmComment #73
rpayanmComment #75
tim.plunkettAnd another one needed. I will try to review more promptly this time.
Comment #76
mrinalini9 CreditAttribution: mrinalini9 at Srijan | A Material+ Company for Drupal India Association commentedComment #77
mrinalini9 CreditAttribution: mrinalini9 at Srijan | A Material+ Company for Drupal India Association commentedRerolled patch for 9.1.x, please review.
Comment #79
sanjayk CreditAttribution: sanjayk as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedComment #80
sanjayk CreditAttribution: sanjayk as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedLittle bit busy in next couple of days. Later will plan to work on it.
Comment #81
deepakaryan1988Comment #82
deepakaryan1988Comment #83
tim.plunkettActive means there is no patch yet.
Comment #84
rpayanmComment #85
tim.plunkett@rpayanm Please include an interdiff when changing a patch and include a comment of some sort explaining your changes.
If it's a reroll and you didn't make other changes, please say that, and remove the `needs reroll` tag
Comment #86
rpayanm@tim.plunkett It was a reroll.
Comment #88
arshadkhan35 CreditAttribution: arshadkhan35 as a volunteer commentedRe-rolling #72, as patch was failing against 8.9.x.
Comment #89
tim.plunkettThe patch needs to be rolled against 9.2.x (the current dev branch) for this issue to make any progress.
Comment #90
arshadkhan35 CreditAttribution: arshadkhan35 as a volunteer commentedPatch #84 works with 8.9.x as well, please ignore #88 re-roll.
I have applied #84 and it seems to be working, however if we use this patch along with layout_builder_modal module, it introduces an issue where modal popup on layout while adding or updating block does not close.
Steps to reproduce:
Comment #91
sanjayk CreditAttribution: sanjayk as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedRe-roll patch for 9.2
Comment #92
tim.plunkettA bit of code was lost in the last reroll. I'm going to tackle this myself, as it's much more confusing now after #3143635: Change Layout Preparation into an Event to allow proper alterations
Comment #93
tim.plunkett#3045171: Form blocks rendered inside layout builder break save made this MUCH simpler by moving the Layout UI out of the form itself. I was able to remove all of the entity form parts of this (I hope that was correct, but it's in the git history now so not impossible to revert)
This could do with a FunctionalJavascript test, so restoring the tag for that.
Comment #95
tim.plunkettHiding old patches now that this is using a Merge Request
Comment #96
tim.plunkettHmm, I was wrong. One of my changes broke this, so I will work to fix it.
Comment #99
Amber Himes MatzLooks like the MR needs a reroll (rebase).
Comment #100
larowlanDoes this still need tests? Can that tag be removed.
For the re-roller, please try to debug the test fails too rather than a straight re-roll.
Comment #101
larowlanWe also need type-hints and return type-hints here on the new code.
Didn't comment on each line that needed it in the MR, because it applies to all new methods.
Comment #105
NivethaSubramaniyan CreditAttribution: NivethaSubramaniyan as a volunteer and at Srijan | A Material+ Company for Drupal India Association commentedCreating patch for 10.1
Comment #106
Nitin shrivastava CreditAttribution: Nitin shrivastava at OpenSense Labs commentedtry to fix #105
Comment #108
Bhanu951 CreditAttribution: Bhanu951 as a volunteer commented@timplunkett
Can you pls change the target branch to 10.1.x for this MR #322 rerolled it to 10.1.x branch.
As it has review comments.
Comment #112
andypostComment #114
tim.plunkett