Problem/Motivation

In #2453153: Node revisions cannot be reverted per translation we address the problem of reverting node revisions without overwriting changes in translations that were not affected by the reverted revision.
Several UI improvements were suggested but we decided to implement a minimal fix there and discuss a better UI in a separate task.

Note: To appreciate the severity of this issue, it is important to understand that, without this patch, editors and site administrators may experience loss of content. (The revisions are not actually lost, but the UI indicates they are reverting one language, when in fact they are reverting the whole entity and may not even realize that translations of the entity are now gone.) In D7 this works correctly, as expected, so without this patch it amounts to a loss of functionality in D8. That makes this an issue severe enough to warrant the "UI changes" involved.

The following use-cases were identified that are poorly or not at all supported by the current UI:

  • List only revisions affecting the current translation.
  • Select translation languages to be reverted.

Proposed resolution

Revision listing could be improved by simply filtering out from the revisions list non-relevant items. Therefor the revision overview will be limited to those revisions that affected the current translation the editor is working with.
(In Barcelona we decided to just leverage the content language negotiation that's already in place.)

If a node has no translations, the complete entity will be reverted, in other words all fields of it regardless if these fields are translatable or not.

If a node has translations we will per default only revert translatable fields in the current content language.
In addition we add a checkbox to the confirmation page to optionally include the non-translatable fields in this revert.

If more sophisticated UIs should be implemented that has to happen in contrib. All the required data is available at the entity level. In core we have to focus on an easy UI that works like the on in Drupal 7 which the users are already familiar with.
Therefor the Revision UI project has been started in contrib.

Remaining tasks

None

User interface changes

  • The revision list is now filtered by current language
  • The checkbox below has been added to the revert confirmation form for translated entities

API changes

None.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug because in oposite to D7 an editor of one translation will accidentally revert or removedifferent translations.
Issue priority Major because we broke a well known D7 feature. It's the last patch in a series of issues regarding revisions and translations that were critical. This last one is not critical because there's no more data loss and all values required are in the database, but so far the user can't deal with it without this patch.
Prioritized changes The main goal of this issue is usability/accessibility. It doesn't break anything, it just adds the missing pieces to the UI. Like @webchick stated in https://www.drupal.org/node/2428795#comment-9842025 : "The issue ... is legitimate, and needs to be fixed."
CommentFileSizeAuthor
#82 Are_you_sure_you_want_to_revert_English_translation_to_the_revision_from_Fri__10_02_2015_-_01_51____Drupal_8_-_Dev.png118.83 KBplach
#79 2465907-79.patch23.15 KBmkalkbrenner
#79 76-79-interdiff.txt8.24 KBmkalkbrenner
#76 2465907-76.patch22.43 KBmkalkbrenner
#76 63-76-interdiff.txt14.52 KBmkalkbrenner
#63 2465907-63.patch24.98 KBmkalkbrenner
#63 60-63-interdiff.txt6.56 KBmkalkbrenner
#60 2465907-60.patch21.16 KBmkalkbrenner
#60 46-60-interdiff.txt8.12 KBmkalkbrenner
#52 Revisions_for_Test_1_3_EN___Drupal_8_-_Dev.png107.42 KBplach
#52 Revisions_for_Test_1_2_IT___Drupal_8_-_Dev.png91.21 KBplach
#52 Revisions_for_Test_1_1_IT___Drupal_8_-_Dev.png159.11 KBplach
#48 2465907PatchResult.png124.83 KBshwetaneelsharma
#46 2465907_46.patch16.01 KBmkalkbrenner
#46 40-46-interdiff.txt550 bytesmkalkbrenner
#43 revert.png662.95 KBmkalkbrenner
#40 2465907_40.patch16.01 KBmkalkbrenner
#33 Revert_German.png44.64 KBmkalkbrenner
#30 2465907_reroll_30.patch18.72 KBmkalkbrenner
#30 28-30-interdiff.txt688 bytesmkalkbrenner
#28 2465907_reroll_28.patch18.72 KBmkalkbrenner
#27 2465907_reroll_26.patch23.73 KBcedric_a
#20 2465907_20_including_2453153_105.patch39.51 KBmkalkbrenner
#20 2465907_20-do-not-test.patch19.26 KBmkalkbrenner
#20 16-20-interdiff.txt1.06 KBmkalkbrenner
#16 2465907_16_including_2453153_105.patch39.58 KBmkalkbrenner
#16 2465907_16-do-not-test.patch19.33 KBmkalkbrenner
#7 Bildschirmfoto 2015-04-08 um 20.58.08.png122.66 KBmkalkbrenner
#7 Bildschirmfoto 2015-04-08 um 20.57.34.png114.99 KBmkalkbrenner

Comments

plach’s picture

Issue summary: View changes
plach’s picture

Fabianx wrote in #2453153-38: Node revisions cannot be reverted per translation:

Another possibility to solve this is to make revert fill in an edit form with the old revision of the entity. We use this approach (indirectly) for the CPS module (by design).

That allows to keep UI changes small and just bases reverts off an older entity. Still need a little special code to only replace those fields with data from the revision that is different and only for the loaded language, but then the language is explicit.

gábor hojtsy’s picture

The proposed UI does not handle the case where a revision may change multiple languages at once. Eg. migrated entities. How would this UI display multilingual revision changes (where multiple language content changed)?

mkalkbrenner’s picture

@Gabor Hojtsy: My last patch for #2453153: Node revisions cannot be reverted per translation already listed every language that has been changed in a revision and offered a revert link per language. So it's an improved version of the screenshot posted above.

gábor hojtsy’s picture

Issue tags: +Needs screenshots

Would be great to get that screenshot here then so we have the up to date baseline for the discussion :) Can you help with that?

mkalkbrenner’s picture

gábor hojtsy’s picture

Issue summary: View changes
Issue tags: -Needs screenshots

Updated issue summary with the new image.

As for the suggestions in the issue summary, while the view and edit pages can clearly only allow for viewing or editing one language variant, the revisions page is a different animal IMHO. It may very well not be evident if we limit the list by language and require you to use the regular language selector. IMHO it would be better to expose a select list on the page with languages and filter using that. We may filter the revisions by the current content language by default and allow an option to filter by none. My 2 cents.

matsbla’s picture

Yes good idea to filter the whole revision page by language and set it to current language by default.

In addition to see current language I think it would make sense to see changes in global values (non-translatable values) by default as only seeing the translatable values can make you loose some context.

"Global values" could be an option on the select list, then you could even see revisions in non-translatable values only.

Maybe make it possible to choose several options on the select list, like an autocomplete widget, then you could choose if you only want to see revision for current language or also have global values. Or even to check revisions for two languages at the same time: It can make sense, let's say the source language have changed and you want to check revisions of the current language side-by-side with the source language to understand why another translator did what she did.

mkalkbrenner’s picture

For the people at the dev days who want to test the UI as shown in the screenshots at #7:

Apply these patches to the current core dev version:

mkalkbrenner’s picture

Issue tags: +drupaldevdays

The UI shown in the screenshots in #7 could be easily extended after the patches I mentioned in #10 are applied.
For example it's possible to show the last author and the last date for every translation edit listed in the "Language Edit" column. All the required information is already stored in the database after the patches are applied.
If we want to this could be offered as a novice task/issue for devdays.

plach’s picture

Title: [PP-2] Make node revision UI work with translations » [PP-1] Make node revision UI work with translations
matsbla’s picture

I tested patch in:
https://www.drupal.org/node/2453153#comment-9989239

It works great!

- I think it would be most intuitive to see revisions for UI language only, or make it possible to filter the revision log by language (to choose only one or several languages).

- would it be possible to get an option to revert "global values" (not translatable fields)? If a person edit global values while editing a translation this will now not be marked clearly in the revision log, so 1 it makes it more difficult to keep overview of changes in revision log and 2 it should also be possible to revert "global value" too.

- On node edit form: could it be possible to make an option "Create new revision (this translation only)" on the side of "Create new revision (all languages)"?

plach’s picture

matsbla’s picture

I have one more comment on:
https://www.drupal.org/node/2453153#comment-9989239

- When a translation is reverted, maybe it should be clearly marked in the log? Like "Revertion of English translation 06/13/2015"

- Would also be nice to be able to edit/add the "Revision log message" (now it just copy the old revision log, but in many cases it would make more sense that a person write a comment on why they revert the translation).

mkalkbrenner’s picture

Status: Postponed » Needs review
StatusFileSize
new19.33 KB
new39.58 KB

I removed all the UI related code from #2453153: Node revisions cannot be reverted per translation and created an initial patch here that only contains that UI code (2465907_16-do-not-test.patch).

The code has been adjusted to the current draft of #2453153: Node revisions cannot be reverted per translation and should be independent from the ongoing discussion about "filed API vs. storage", or in other words work with both approaches.

In order to have first test results here I also uploaded 2465907_16_including_2453153_105.patch which includes patch #105 of #2453153: Node revisions cannot be reverted per translation.

The useful suggestions by @matsbla are not taken into account yet.

Status: Needs review » Needs work

The last submitted patch, 16: 2465907_16_including_2453153_105.patch, failed testing.

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 16: 2465907_16_including_2453153_105.patch, failed testing.

mkalkbrenner’s picture

Status: Needs work » Needs review
StatusFileSize
new1.06 KB
new19.26 KB
new39.51 KB
mkalkbrenner’s picture

Title: [PP-1] Make node revision UI work with translations » Make node revision UI work with translations
Assigned: Unassigned » mkalkbrenner

I will adjust the existing patch to the latest changes in core introduced by #2453153: Node revisions cannot be reverted per translation to have a good starting point for further discussions.

cedric_a’s picture

Assigned: mkalkbrenner » cedric_a
mkalkbrenner’s picture

Issue tags: -drupaldevdays +sprint
hass’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/ChangedFlagItem.php
    @@ -0,0 +1,77 @@
    + *   label = @Translation("Changed Flag"),
    
    +++ b/core/modules/node/src/Controller/NodeController.php
    @@ -162,21 +174,37 @@ public function revisionOverview(NodeInterface $node) {
    +      $header[] = $this->t('Languages Edited');
    

    "Edited" need to be lowercase. "Flag" , too.

  2. +++ b/core/modules/node/src/Controller/NodeController.php
    @@ -196,8 +229,22 @@ public function revisionOverview(NodeInterface $node) {
    +                    'title' => $this->t('Revert !language', array('!language' => $language_name)),
    

    Language names can be user entered. This placeholder allows XSS I think.

mkalkbrenner’s picture

@hass: thanks for your review, but the patch is totally outdated ;-)
I'm working with @cederic_a on a new version since 5 hours at the drupal con sprint. We hope to upload a new patch soon. It would be really nice if you could review that one again.

cedric_a’s picture

Status: Needs work » Needs review

We removed the parts now implemented in the core and upload to see what the testbot says

cedric_a’s picture

StatusFileSize
new23.73 KB
mkalkbrenner’s picture

Title: Make node revision UI work with translations » Node revision UI doesn't respect CONTENT_LANGUAGE
Assigned: cedric_a » mkalkbrenner
Category: Task » Bug report
StatusFileSize
new18.72 KB

The last patch uploaded by @cedric_a accidentally included a different patch. Here is the right version.

Status: Needs review » Needs work

The last submitted patch, 28: 2465907_reroll_28.patch, failed testing.

mkalkbrenner’s picture

Title: Node revision UI doesn't respect CONTENT_LANGUAGE » Node revision UI doesn't respect LanguageInterface::TYPE_CONTENT
Status: Needs work » Needs review
StatusFileSize
new688 bytes
new18.72 KB

patch in #29 contains a copy&paste bug.

Status: Needs review » Needs work

The last submitted patch, 30: 2465907_reroll_30.patch, failed testing.

lomo’s picture

Title: Node revision UI doesn't respect LanguageInterface::TYPE_CONTENT » Node revision UI reverts all languages when only one language should be reverted
Issue summary: View changes
mkalkbrenner’s picture

StatusFileSize
new44.64 KB
mkalkbrenner’s picture

lomo’s picture

Added new screenshot from @mkalkbrenner to the "UI Changes" section of the issue description.

lomo’s picture

Issue summary: View changes

Edit collision? Fixed what should have been done by my last change to the issue description.

The last submitted patch, 27: 2465907_reroll_26.patch, failed testing.

The last submitted patch, 28: 2465907_reroll_28.patch, failed testing.

The last submitted patch, 30: 2465907_reroll_30.patch, failed testing.

mkalkbrenner’s picture

Title: Node revision UI reverts all languages when only one language should be reverted » Node revision UI reverts multiple languages when only one language should be reverted
Status: Needs work » Needs review
StatusFileSize
new16.01 KB
mkalkbrenner’s picture

hass’s picture

Status: Needs review » Needs work
+++ b/core/modules/node/src/Controller/NodeController.php
@@ -167,74 +171,85 @@ public function revisionOverview(NodeInterface $node) {
+              '#markup' => $this->t('current revision'),

This string need to be ucfirst! -> t('Current revision')

mkalkbrenner’s picture

Issue summary: View changes
StatusFileSize
new662.95 KB
mkalkbrenner’s picture

Status: Needs work » Needs review

@hass: thanks.

This string need to be ucfirst! -> t('Current revision')

Mayby you're right but that patch didn't touch that ;-)
We can touch this on commit, but it will break existing string translations.

hass’s picture

Status: Needs review » Needs work

It will not break anything and it is incorrect. Additional translations are not done. The latest placeholder change with :placeholder will break thousands of strings... much worse than this minor fix here.

Let's fix this single bug now or we may lose track of this again.

mkalkbrenner’s picture

Status: Needs work » Needs review
StatusFileSize
new550 bytes
new16.01 KB

just re-rolled against current HEAD and upper-cased the "C" in "Current revision".

mkalkbrenner’s picture

shwetaneelsharma’s picture

StatusFileSize
new124.83 KB

Patch "2465907_46.patch" has fixed the ucletter 'c' bug in current revisions. Attaching screenshot.

mkalkbrenner’s picture

Issue summary: View changes
mkalkbrenner’s picture

Issue summary: View changes
jhedstrom’s picture

This is looking good. @mkalkbrenner could you upload the test-only patch to illustrate the current behavior and the fix?

plach’s picture

I tested this and it seems to work nicely. I'm no UX expert but from a site builder perspective I think I could use an additional column listing which translations are affected by each revision.

The column could be optional and be displayed only whether at least one revision is affecting more than one translation, this way monolingual sites and multilingual sites always creating a new revision on each save would avoid this additional visual clutter.

Definitely we should list the affected translations, when we are reverting more than one, otherwise the user may be unaware of what she is actually reverting.

While testing this, I found an edge case that may require some more thoughts:

  1. Create an English node (rev 1)
  2. Translate it to another language
  3. Create a new English revision (rev 2)
  4. Translate the new revision
  5. Create a new English revision (rev 3)
  6. Switch to the translation and revert rev 1

Expected: result the revision log is "Copy of the revision from ..."
Actual: result the revision log is "rev 2"

And finally here's a code review, mostly nitpicks:

  1. +++ b/core/modules/node/node.routing.yml
    @@ -68,6 +68,16 @@ node.revision_revert_confirm:
    +    _title: 'Revert to earlier revision of a translation'
    

    Missing trailing dot.

  2. +++ b/core/modules/node/src/Controller/NodeController.php
    @@ -71,7 +72,7 @@ public static function create(ContainerInterface $container) {
    -    $content = array();
    +    $content = [];
    
    @@ -86,10 +87,10 @@ public function addPage() {
    -    return array(
    +    return [
    ...
    -    );
    +    ];
    

    Unrelated changes, alex doesn't like them :)

  3. +++ b/core/modules/node/src/Controller/NodeController.php
    @@ -153,6 +154,9 @@ public function revisionPageTitle($node_revision) {
    +    $language = $this->languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT);
    

    Do we need the $language variable?

  4. +++ b/core/modules/node/src/Controller/NodeController.php
    @@ -167,74 +171,85 @@ public function revisionOverview(NodeInterface $node) {
    +            if (count($languages) > 1) {
    

    Can we store count($languages) > 1 into a variable a reuse it below to make things more robust?

  5. +++ b/core/modules/node/src/Controller/NodeController.php
    @@ -167,74 +171,85 @@ public function revisionOverview(NodeInterface $node) {
    +              'title' => $this->t((count($languages) > 1) ? 'Revert all translations' : 'Revert'),
    

    This will break string parsing, it needs to be changed to:

    (count($languages) > 1) ? $this->t('Revert all translations') : $this->t('Revert'),
    

    Setting to "needs work" for this one.

  6. +++ b/core/modules/node/src/Form/NodeRevisionRevertTranslationForm.php
    @@ -0,0 +1,118 @@
    +   * @param \Drupal\Core\Language\LanguageManagerInterface
    

    Missing param name.

plach’s picture

Status: Needs review » Needs work
webchick’s picture

@plach and I talked about this at DrupalCon, and he walked me through the problem space.

From a "where we are in the release" standpoint, I think I'd like to see us simply return to the Drupal 7 behaviour. It seems like it simplifies things significantly, and "Revert all translations" could always be something we added in a later minor version if we decide that is a desired feature.

My concern is that if we keep it, in order to really support it well, and without causing a problem, we would need to greatly expand the overview table to include a bunch of information to prevent people from accidentally delete more than they intended. For example, they can only see the English revisions on the node revision page when their current language is English, but this option would also delete DE and IT translations, but there'd be nothing to alert them to this fact until they switched their interface language, which is an additional 3+ clicks.

I realize that's how HEAD works now and it's a bit weird to change that, but I think if we restore D7 behaviour, that will likely more closely match user expectations. And even if there is a use case for it, it seems like something more advanced that could live in contrib for now.

gábor hojtsy’s picture

So D7 behavior is: 1. filter list to specific language 2. when rolling back to a revision, roll back only the fields in this language, not any others, right?

plach’s picture

Exactly.

Which means keeping the behavior currently implemented for the "Revert" operation and removing the "Revert all translations" operation.

gábor hojtsy’s picture

Yeah I can see how thats the simplest solution for core for now and then we can expand on it later.

mkalkbrenner’s picture

My last patch brings the behavior of "Revert" for entity translations very close to the known D7 behavior:

  • The listed revisions are filtered by the current content language.
  • Only translatable fields are reverted.

But it's impossible to 100% recreate the original D7 behavior because entity translation is fundamentally different from node translation (and somehow incompatible, which does NOT mean that it is worse.)

To get back all features from D7 (and more new advantages) we need to create a new sophisticated UI. But that's out of scope for 8.0.0.

The reason why we can't rebuild the UI as it was in D7 are untranslated fields. If we revert an untranslated entity it is obvious to revert all fields, no matter if they are defined as translatable or not. That's 100% like D7. But as soon as the entity is translated, we have two possibilities:

  1. Revert only translated fields.
  2. Revert translated and untranslated fields.

I'm convinced that option #1 matches at least 80% of the use-cases and should therefor be what happens on "Revert".
Option #2 is a no-go because it basically reintroduces the issues we wanted to fix. This will cause unexpected overwrites of field values with probably very old values.
If I look at the last comments, I think we're all of the same opinion.

But if we go for that approach, there will be no possibility to revert a complete entity including all translations or to revert untranslated fields via the UI in core. That's why I introduced at least an additional "Revert all translations" action that includes the untranslated fields. From my point of view that is the minimal version of a new sophisticated UI in core. More than that should be build in contrib first. There you should be able to select the exact fields you want to revert and not rely on a guess by the core or its developers ;-)

So I suggest to keep the new action as well to at least have "something" in core for untranslated fields. But I would introduce a new dedicated permission for that action. This way it could be reserved for experienced users that know what they're doing.

mkalkbrenner’s picture

Status: Needs work » Needs review

If you agree with my explanations in #58 I will add the details @plach addressed in #52 and introduce a new permission for "Revert all translations".

mkalkbrenner’s picture

StatusFileSize
new8.12 KB
new21.16 KB

I discussed the issue again with @plach in IRC. He second me to go for the solution I suggested in #58.
I adjusted the patch accordingly and addressed all the small things mentioned in #52, except the trailing dots mentioned in number one. These dots are not present in other routing.yml files as well and the breadcrumb doesn't look consistent if we add them.

Regarding the revision log message I reproduced the issue as shown in the screenshots in #52. But that's a different issue and out of scope here. The code here is correct and the revision log message should be correct if #2480921: Make the node entity's revision_log untranslatable and #2506213: Update content entity changed timestamp on UI save are addressed.

mkalkbrenner’s picture

plach’s picture

I pinged @webchick about this: if she's ok with the current solution, I think we just need explicit test coverage for the new permission and then we should be good.

+++ b/core/modules/node/node.permissions.yml
@@ -22,6 +22,9 @@ view all revisions:
+  title: 'Revert all tranlations for all revisions'

typo

Edit: well, I still think at least we should list the affected translations on the confirmation form, if they are more then one.

mkalkbrenner’s picture

StatusFileSize
new6.56 KB
new24.98 KB

I extended the patch to list of affected translations as soon as a node has translations.
In addition I "fixed" NodeRevisionAccessCheck to deal with translations (the optional langcode paramter was already in place).
So far there's no explicit test coverage for the new permission. But I want to know if anything else breaks in the test bot run and provide a new version of the patch that could be used for manual tests.

plach’s picture

@mkalkbrenner:

I had a very long discussion about this with @webchick: I walked her through #58 and she agreed that we have a real issue to address. We discussed various UI solutions but we were not able to come up with a UX that seemed even remotely compelling.

The main problem is that, as you already pointed out, to make clear to the user what's going on with untranslatable fields, we need probably need a very advanced UI displaying diffs and other fancy visual clues, combined with a very rich permissions set.

Neither Angie nor I feel very confortable about designing and rushing-in such a UI one week before RC1, without intimately knowing the use cases it would need to support, and above all without the chance of having it properly user-tested. The risk of getting it wrong and having to support it until D8 ends its life is simply too high.

That said we were envisioning the following solution: in 8.0.0 we would have a revision UI offering only a "Revert" link targeting a single translation, i.e. the one matching the current content language, as already implemented by the patch. We discussed a lot how to deal with untranslatable fields, and we initially agreed that for now we should always revert also untranslatable fields. However after the end of the discussion, I realized you were outlining this can be problematic not only for revisions affecting multiple translations, but even when an untranslatable field has been changed while editing translations belonging to different revisions:

| REV | LANG | TITLE       | UNTRANSLATABLE_FOO |
| 1   | en   | Title EN 1  | Foo 1              |
| 2   | it   | Title IT 1  | Foo 2              |
| 3   | fr   | Title FR 1  | Foo 3              |
| 4   | en   | Title EN 2  | Foo 4              | <- Reverting "Title EN 2" 
| 5   | fr   | Title FR 2  | Foo 5              |    would mean reverting
| 6   | en   | Title EN 3  | Foo 6              |    also "Foo 4", but we
                                                     may want to keep "Foo 5"
                                                     instead.

Since there is no permission configuration allowing us to prevent the scenario above from happening, I'd suggest to perform just a tiny addition to the current UI: a new checkbox in the confirmation form saying "Revert also untranslatable content", appearing only on translated nodes and off by default.

I realize this is not ideal as one would still need to figure out what the untranslatable content is, OTOH I guess that since it would be off by default, explicitly checking it would imply having a clue of what one is doing. Moreover since revisions are not overwritten the user could always revert the changes and try again with the correct checkbox setting.

This UI could be easily enhanced/replaced in contrib and in 8.1.0 or 8.2.0 we would provide an advanced and battle-tested Revision UI addressing all the use cases properly.

What do you think?

plach’s picture

Issue tags: +rc target
matsbla’s picture

What if the revision contain changes *only* on untranslatable content? Do I then need to check the "Revert also untranslatable content" check-box that is turned off by default, even though the revision only contain changes on untranslatable content?

I think it would be better to remove "also" and write:

- "Revert untranslatable content"

and accompany it with another checkbox like:

- "Revert translatable content"

That "translatable" check-box should maybe contain the specific language name like "Revert English content", "Revert French content", to make it more clear.

Then only show the check-boxes that can be applied (and make them mandatory if only on of them can be applied).

In this way a user can also choose to keep the translations and only revert the untranslatable content (it will be difficult without the second check-box). The way it is suggested now it can only be done from another language version than the change was originally made on (and remember to check "Revert also untranslatable content" even though you in fact only revert untranslatable content).

And why not show both check-boxes also on source language? If not a user can believe they revert changes only on the English node, while they in fact make changes (that will create new revisions) on all translations of the node. They will also not be able to revert only translatable or untranslatable content if the revision contain both.

webchick’s picture

From where I sit, "revert" simply reverts all changes made in a given revision. That's how it behaved in D7, which didn't have this notion of translatable vs. untranslatable content. That's also how it behaves even in a non-translation context. For example, if you update the issue summary and the tags of an issue in the same revision, and someone likes your issue summary update but hates your tags, they can either revert your revision and re-input the issue summary updates you made by hand, or they can leave the revision in place and fix the tags, creating a new revision.

There is no notion of a "partial" revert anywhere else in the system, and it complicates things significantly. I definitely would love to see us provide a proper "untrusted translator" workflow, but that's a lot more work than what we can realistically do in 5 days. It's a lot easier to iterate on that in contrib, where we can change the UI cheaply as we come up with better ideas, and target for core in 8.1 or 8.2.

AKA, "what plach said." :)

mkalkbrenner’s picture

What if the revision contain changes *only* on untranslatable content?

@matsbla: Such a revision will not be listed at all if we filter be content language.

@webchick: I second you that we can't implement a good solution within 5 days. That's why I came up with my first UI proposal more than 5 months ago ;-)
But to be honest I don't understand your comment #67. What exactly are you proposing? Should we simply keep the current implementation in core? This one already implements a partial revert and is different from D7.
Or should we go back to a real "revert all" in core that reverts a complete entity as it was in core months ago but what is completely different from D7?
Or should we go for that checkbox thing, aka "what plach said."?

BTW I will start that contrib module now. Luckily all my patches that are essential to be able to that went into core over the last months :-)

matsbla’s picture

@mkalkbrenner: Okay, so in other words, if a translator make changes to untranslatable content only they will not be able to see these changes in their revision log after? Should their revision then only be visible in the revision log of the source language, or not visible at all ("filter by content language")? Would that not be a new change from D7?

I know big changes will not happen in 5 days.. anyway look forward to check your contrib module!

mkalkbrenner’s picture

@matsbla: I think our goal here is to have at least "something working" that is as close as possible to known behavior of D7. I know that this is impossible for multilingual content for 8.0.x in the remaining time frame :-(

I just started the Revision UI module to be able to still provide a usable solution for 8.0.x. It would be nice if you post your suggestions as feature requests there.

@webchick, @plach: Maybe we can support the 80%-use-case by going back to always list all revisions and revert the complete entity. In other words we don't support revisions of translations at all in the UI in core and point to the contrib module instead.

plach’s picture

@mkalkbrenner

What if the revision contain changes *only* on untranslatable content?

@matsbla: Such a revision will not be listed at all if we filter be content language.

With #2506213: Update content entity changed timestamp on UI save every change performed through the UI will always be tied to a translation, so every change will be listed at least in one revision translation list. I'm not concerned about API usages since we have no need to support cases that are not possible with the core UIs alone. It's perfectly fine to require a contrib module to address those. Again, I'm totally aware that the solution I proposed is not perfect, but the goal was addressing the widest range of use cases with the minimum effort.

I second you that we can't implement a good solution within 5 days. That's why I came up with my first UI proposal more than 5 months ago ;-)

Yes, Markus, I have no problem to admit you did everything you could to have this fixed properly in 8.0.0. If there's anyone to blame, that's me. I simply wasn't able, as the module maintainer, to allocate the required amount of time to make this happen. I deliberately decided to prioritize issues that cannot be fixed after 8.0.0 is released (and to help that actually happen :), even if that meant accepting suboptimal solutions. However you did a great job in taking us to a point where we can improve the situation in contrib instead of having a completely broken system :)

That said Angie, as the product manager, has to make decisions and I think being conservative in this particular issue is the right choice.

I just started the Revision UI module to be able to still provide a usable solution for 8.0.x. It would be nice if you post your suggestions as feature requests there.

This sounds great :)

Please be aware I agreed with @catch, @fago, @dixon_ and other people interested in the Entity API area to start working on an improved version of the Entity API (Entity module) to be included in core in 8.1 or 8.2, and one of the goals is providing a generic Entity revision UI, so we should definitely try to coordinate our efforts.

Maybe we can support the 80%-use-case by going back to always list all revisions and revert the complete entity. In other words we don't support revisions of translations at all in the UI in core and point to the contrib module instead.

I think what the patch is doing is already addressing 80% of the use cases, I'm just suggesting to trade the "revert all" functionality with the checkbox. No need to start everything from scratch IMO. Hopefully we can get a clarification ASAP about what @webchick meant we should be doing here :)

webchick’s picture

I am so sorry about my confusing comment in #67. I had to rush off right after plach and my discussion and didn't get back to my computer until 11pm, merely scanned plach's comment and said "what plach said" thinking that it was a summary of what we talked about. Oops. :\

I think the counter-suggestion that plach posted in #64, of a conditional checkbox on the confirm form that only appears on revisions that have the condition of affecting both translated and untranslated content, sounds great. That minimizes its effect on the overall revisions UI (which was my main concern, along with permissions bloat) for something that is hopefully a very small edge case, assuming you've set up your revisions correctly and are doing things in well-defined edits.

I get that it will still potentially allow a hypothetical translation intern to revert changes to a hero image when they go back to fix a spelling error in Russian or whatever, but IMO that's an acceptable risk. The stuff will still be around in past revisions if it needs to be restored.

The one nit-pick I'd have with the label is I don't think most people would know what "untranslatable content" means, since that's a site builder term, and this is a content editor UI. Would be ideal (only if it's easy) to label the checkbox like "Also revert changes to the Blarg, Blee fields" instead, since that will match labeling they saw in the node form. If that's not easy, no big deal. We can always adjust this label in 8.1.x or whenever.

matsbla’s picture

Maybe it could be a good idea to use same expression in the label as on the edit form:
#2290101: UI telling you a field is shared across languages is way too subtle
"All languages"

plach’s picture

I think that may be confusing: it wouldn't revert all affected translations, just untranslatable fields + translatable fields in the current language.

plach’s picture

mkalkbrenner’s picture

StatusFileSize
new14.52 KB
new22.43 KB

OK, I implemented the checkbox solution.
As long as a node isn't translated, the complete entity gets reverted => like D7.
If a node has translations,

  • the revisions on the Revisions tab are limited to the current content language => like D7
  • an additional checkbox is shown on the revert confirmation form to select if the non-translatable fields should be reverted as well

BTW the interdiff doesn't make that much sense ;-)

plach’s picture

Status: Needs review » Needs work

This is awesome, it's working exactly as I expected!

We are very close, the only remark I have is that we should probably make sure the two revert routes are accessible only in the correct case, that is the global revert route is accessible only for untranslated entities and the translation revert route is accessible only for translated entities. Otherwise a user might inadvertently revert the entity to a state where some translations were missing and completely hide them, in fact they would appear as lost (I tested this case).

And finally a code review:

  1. +++ b/core/modules/node/src/Access/NodeRevisionAccessCheck.php
    @@ -70,16 +70,20 @@ public function __construct(EntityManagerInterface $entity_manager) {
    +   * @param string|null $langcode
    +   *   (optional) Language code for the variant of the node. Different language
    +   *   variants might have different permissions associated. If NULL, the
    +   *   original langcode of the node is used. Defaults to NULL.
    ...
    +  public function access(Route $route, AccountInterface $account, $node_revision = NULL, NodeInterface $node = NULL, $langcode = NULL) {
    

    Why do we need a new parameter here? Can't we use the node language? See #2072945: Remove the $langcode parameter in EntityAccessControllerInterface::access() and friends.

  2. +++ b/core/modules/node/src/Form/NodeRevisionRevertForm.php
    @@ -101,17 +101,16 @@ public function buildForm(array $form, FormStateInterface $form_state, $node_rev
    +    $this->revision->revision_log = t('Copy of the revision from %date.', array('%date' => \Drupal::service('date.formatter')->format($original_revision_timestamp)));
    

    Can we inject the date formatter and use [] for the array?

  3. +++ b/core/modules/node/src/Form/NodeRevisionRevertForm.php
    @@ -101,17 +101,16 @@ public function buildForm(array $form, FormStateInterface $form_state, $node_rev
    +    drupal_set_message(t('@type %title has been reverted to the revision from %revision-date.', array('@type' => node_get_type_label($this->revision), '%title' => $this->revision->label(), '%revision-date' => \Drupal::service('date.formatter')->format($original_revision_timestamp))));
    

    Can we use [] for the array?

  4. +++ b/core/modules/node/src/Form/NodeRevisionRevertTranslationForm.php
    @@ -0,0 +1,119 @@
    +    return t('Are you sure you want to revert @language translation to the revision from %revision-date?', ['@language' => $this->languageManager->getLanguageName($this->langcode), '%revision-date' => format_date($this->revision->getRevisionCreationTime())]);
    

    Can we use an injected date formatter here too?

  5. +++ b/core/modules/node/src/Form/NodeRevisionRevertTranslationForm.php
    @@ -0,0 +1,119 @@
    +      '#title' => $this->t('Revert untranslated fields.'),
    

    What about "Revert content shared among translations"?
    (without the trailing dot :)

  6. +++ b/core/modules/node/src/Tests/NodeRevisionsTest.php
    @@ -34,9 +35,22 @@ protected function setUp() {
    +    $field_storage = entity_create('field_storage_config', $field_storage_definition);
    

    We can use FieldStorageConfig::create() here.

  7. +++ b/core/modules/node/src/Tests/NodeRevisionsTest.php
    @@ -34,9 +35,22 @@ protected function setUp() {
    +    $field = entity_create('field_config', $field_definition);
    

    We can use FieldConfig::create() here.

  8. +++ b/core/modules/node/src/Tests/NodeRevisionsTest.php
    @@ -256,6 +280,38 @@ public function testRevisionTranslationRevert() {
    +    // Now revert the a translation revision preceding the last default
    

    typo

plach’s picture

Also :)

mkalkbrenner’s picture

Status: Needs work » Needs review
StatusFileSize
new8.24 KB
new23.15 KB

I addressed the findings in #77.

... the only remark I have is that we should probably make sure the two revert routes are accessible only in the correct case, that is the global revert route is accessible only for untranslated entities and the translation revert route is accessible only for translated entities. Otherwise a user might inadvertently revert the entity to a state where some translations were missing and completely hide them, in fact they would appear as lost (I tested this case).

I don't agree to limit access this way.

I wonder how a user should accidentally call this route. He must explicitly type this URL including the node id. If he finds it in the browser history he already did it ;-)
And the data isn't really lost. It's still in the database as an older revision.

If you have a look at the test case implemented, I already included this use-case as a "hidden" feature. Using this route it's possible to revert a complete node. I assume that someone who manually enters this URL knows what he's doing. Personally I will support this use-case in Revision UI and don't want to be limited by a hard-coded rule for node-translations in the access check that will force my to swap out the access check. What do you think?

mkalkbrenner’s picture

Issue summary: View changes
mkalkbrenner’s picture

plach’s picture

Assigned: mkalkbrenner » webchick
Issue summary: View changes
Status: Needs review » Reviewed & tested by the community
StatusFileSize
new118.83 KB

What do you think?

I'm not entirely convinced, but I'll let @webchick have the final call about #79. If we end up agreeing on my suggestion we can certainly implement it in a non-RC-bound issue, since that would then be an access control bug.

Great work! :)

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 79: 2465907-79.patch, failed testing.

webchick’s picture

Checkbox label looks great.

On the access issue, I don't think that we should blow it off necessarily since it requires manually typing a URL; malicious users craft explicit URLs all the time for use in XSS attacks, etc.

However, looking at the existing permissions for node.revision_revert_confirm in HEAD, surprisingly the only thing it checks for is:

  requirements:
    _access_node_revision: 'update'

...not even the "revert Foo revisions" permission. (Though maybe that's the same thing, just wrapped in Entity API-ish language.)

So I guess if that's a problem, we should be fixing it in a separate issue for all "revisiony" routes; this patch merely makes the new route consistent with the existing behaviour.

mkalkbrenner queued 79: 2465907-79.patch for re-testing.

gábor hojtsy’s picture

Status: Needs work » Reviewed & tested by the community
plach’s picture

Status: Reviewed & tested by the community » Needs work

It seems we actually have some failures on the latest head, let's wait for the bot.

mkalkbrenner’s picture

@webchick:
The 'update' permission is mapped to 'revert Foo revisions' within \Drupal\node\Access\NodeRevisionAccessCheck::checkAccess():

  public function checkAccess(NodeInterface $node, AccountInterface $account, $op = 'view', $langcode = NULL) {
    $map = array(
      'view' => 'view all revisions',
      'update' => 'revert all revisions',
      'delete' => 'delete all revisions',
    );
    $bundle = $node->bundle();
    $type_map = array(
      'view' => "view $bundle revisions",
      'update' => "revert $bundle revisions",
      'delete' => "delete $bundle revisions",
    );
    ...
  }

Changing this out of scope for this issue here.

The last submitted patch, 79: 2465907-79.patch, failed testing.

The last submitted patch, 79: 2465907-79.patch, failed testing.

plach’s picture

Assigned: webchick » Unassigned
Status: Needs work » Postponed
mkalkbrenner’s picture

This patch here is RTBC again. But in combination with the commit of #2090983: ContentEntityInterface::getTranslation() should throw an exception when an invalid language is specified this afternoon my tests included here discovered another issue. I opened another issue. So we're currently blocked by #2579187: Revert to an older entity revision with less translations leads to fatal error caused by EntityStorageException :-(

plach’s picture

Status: Postponed » Reviewed & tested by the community

plach queued 79: 2465907-79.patch for re-testing.

plach’s picture

Assigned: Unassigned » webchick

And back to @webchick, since we discussed this with her extensively.

  • webchick committed 7940793 on 8.0.x
    Issue #2465907 by mkalkbrenner, cedric_a, plach, Gábor Hojtsy, matsbla:...
webchick’s picture

Status: Reviewed & tested by the community » Fixed

Awesome, thanks for all the hard work on this one. Glad we were able to come to a good solution here!

Committed and pushed to 8.0.x. Thanks!

gábor hojtsy’s picture

Issue tags: -sprint

Content editors rejoice! Thanks folks!

cedric_a’s picture

I just realized that you gave me credit on this commit, thank you so much.
I want to thank you mkalkbrenner once again for the time you spent to explain me the issue and to make me step in Drupal contribution and D8, it was a great experience!
Following your discussions in this issue was very interesting too, thank you all for what you give to Drupal.

plach’s picture

@cedric_a:

Welcome onboard and thanks for helping :)

gábor hojtsy’s picture

@cedric_a: thank you too!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.