Problem/Motivation

Views entity bulk operations are not translation aware, in that they treat the original entity and its translations as a single entity. This leads to various bugs. For example, when selecting the delete action on a "translation row" will delete the whole entity with ALL translations (even those not selected). This leads to unintended data loss. When selecting the publish/unpublish action on a "translation row", instead of publishing/unpublishing the translation, the original language version will be published. This leads to unwanted information disclosure.

Proposed resolution

For deletion, a similar issue was fixed in #2486177: Deleting an entity translation from the UI deletes the whole entity by making the deletion form contextual. When accessing the deletion form in a language different from the default one, only the current translation is deleted. If trying to delete the default translation, the whole entity is deleted but a list of translations is presented to the user in the confirmation form.

A similar approach can be used to fix the operations:

  • Make the bulk operations entity translation aware, so in case a translation is chosen, just delete/publish/unpublish/etc. that translation
  • If the default translation is selected for deletion, list all the translations in the confirmation form, otherwise list only the selected translations.
  • If the default translation is selected for other operations but deletion, only operate on that translation.

Remaining tasks

  • Validate the proposed solution
  • Write a patch
  • UX reviews
  • Code reviews

User interface changes

API changes

None foreseen

CommentFileSizeAuthor
#86 views-multilingual_vbo-2484037-84.patch43.35 KBGábor Hojtsy
#86 views-multilingual_vbo-2484037-84.interdiff.txt766 bytesGábor Hojtsy
#85 Content | drupal 8.0.x 2015-06-10 12-27-31.png123.09 KBGábor Hojtsy
#85 Are you sure you want to delete these items? | drupal 8.0.x 2015-06-10 12-26-55.png86.99 KBGábor Hojtsy
#85 Content | drupal 8.0.x 2015-06-10 12-25-54.png157.21 KBGábor Hojtsy
#6 Screen Shot 2015-05-07 at 18.02.13.png39.06 KBdawehner
#6 2484037-6.patch4.4 KBdawehner
#22 Content | werwerwerewrwerewre 2015-06-01 13-23-34.png81.88 KBGábor Hojtsy
#22 Content | werwerwerewrwerewre 2015-06-01 13-24-16.png84.55 KBGábor Hojtsy
#23 2484037-23.patch2.38 KBGábor Hojtsy
#23 interdiff.txt1003 bytesGábor Hojtsy
#27 2484037-27.patch1.41 KBGábor Hojtsy
#27 interdiff.txt997 bytesGábor Hojtsy
#36 views-multilingual_vbo-2484037-36.interdiff.txt11.72 KBplach
#36 views-multilingual_vbo-2484037-36.patch13.12 KBplach
#39 views-multilingual_vbo-2484037-39.interdiff.txt7.04 KBplach
#39 views-multilingual_vbo-2484037-39.patch15.17 KBplach
#39 Content___Drupal_8_-_TMP.png260.14 KBplach
#39 Are_you_sure_you_want_to_delete_these_items____Drupal_8_-_TMP.png123.63 KBplach
#39 Content___Drupal_8_-_TMP 2.png161.53 KBplach
#41 views-multilingual_vbo-2484037-41.patch18.63 KBplach
#49 views-multilingual_vbo-2484037-48.interdiff.txt4.75 KBplach
#49 views-multilingual_vbo-2484037-48.patch21.9 KBplach
#57 original-language.png138.24 KBYesCT
#57 source-original.png166.13 KBYesCT
#58 more-words.png128.67 KBYesCT
#58 views-multilingual_vbo-2484037-58.patch21.99 KBYesCT
#58 interdiff.2484037.49.58.txt1.15 KBYesCT
#64 views-multilingual_vbo-2484037-64.patch24.06 KBplach
#64 views-multilingual_vbo-2484037-64.interdiff.txt9.09 KBplach
#72 views-multilingual_vbo-2484037-72.interdiff.txt4.8 KBplach
#72 views-multilingual_vbo-2484037-72.patch28.29 KBplach
#76 views-multilingual_vbo-2484037-76.patch28.25 KBplach
#76 views-multilingual_vbo-2484037-76.interdiff.txt2.55 KBplach
#77 views-multilingual_vbo-2484037-77.interdiff.txt12.2 KBplach
#77 views-multilingual_vbo-2484037-77-test.patch12.2 KBplach
#77 views-multilingual_vbo-2484037-77.patch40.45 KBplach
#80 views-multilingual_vbo-2484037-80.patch43.29 KBplach
#80 views-multilingual_vbo-2484037-80-test.patch15 KBplach
#80 views-multilingual_vbo-2484037-80.interdiff.txt11 KBplach
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

dawehner’s picture

dawehner’s picture

Gábor Hojtsy’s picture

Issue tags: +D8MI, +language-content, +sprint
plach’s picture

dawehner’s picture

Issue summary: View changes

Updating the issue summary for things we want to address as part of the issue.

dawehner’s picture

Step 1) Document people that we remove translations in case we remove the source language, see screenshot

Wim Leers’s picture

Issue summary: View changes
webchick’s picture

jibran’s picture

Status: Active » Needs review

Let's test it for a change.

plach’s picture

Title: Make bulk operations entity translation aware » Make Views bulk deletion entity translation aware
Issue summary: View changes

Update IS

Gábor Hojtsy’s picture

Why just delete? The rest is also problematic. Is there another issue?

Gábor Hojtsy’s picture

Priority: Major » Critical

Promoting to critical based on discussion on the D8 criticals call with @alexpott and @catch in agreement. This is even worse than the #2486177: Deleting an entity translation from the UI deletes the whole entity issue because you can delete even more things unintentionally.

dawehner’s picture

Why just delete? The rest is also problematic. Is there another issue?

So you think also about publish / unpublish etc. ? I totally agree.
In general we should maybe also make clear that just the current (selected potentially) revision is touched?

Gábor Hojtsy’s picture

@dawehner: I can see how the delete is critical. It is not *that* big of an issue if you publish all languages of something (that you did not intend) vs. to remove all languages of something that you did not intend. Either way all of the operations need to be resolved. If we don't consider operations other than delete critical, we need yet one more issue for those :)

dawehner’s picture

Well, if you publish more content than you intended to do I could easily see an argument for an unwanted information disclosure.

Gábor Hojtsy’s picture

Should we expand this again to all bulk operations then?

dawehner’s picture

Yeah it sounds for me like a more general problem. But I agree with you, not all of the operations might be critical.

plach’s picture

Title: Make Views bulk deletion entity translation aware » Make Views bulk operations entity translation aware

Sorry, I thought the other operations were already ok, but actually they are only if we act on individual translations, which will need to do anyway to fix deletion.

dawehner’s picture

Just had a quick look at this issue.

There are multiple problems: Many of the other operations don't have confirm forms, like for example the publish action we talked about earlier.
Given that there is no place to ask users, whether they really want to apply the action.

Maybe though it makes sense to always have a confirm form for VBO.

bojanz’s picture

VBO has always had an action-independent confirm step (and a per-action "skip confirmation" option).
Guessing that wasn't brought into core for simplicity, but to me it still makes perfect sense to have it.

Of course, that would mean figuring out the API for how an action can affect the confirm step, that's something that this issue needs, but VBO never had.

Gábor Hojtsy’s picture

To see how this works (or not) now, I created a site with 3 languages and an article translated to all 3 and a page in a single language. After selecting all in the table, I wanted to unpublish them:

The result is all originals are unpublished but translations are not. Then I selected both published and wanted to publish them "again". Then of course the original translation of the article was published. (The page that I did not select remained unpublished).

Clearly operations are only operating on the original language of each entity, even when I only pick a translation to act on.

Gábor Hojtsy’s picture

FileSize
2.38 KB
1003 bytes

I think the first step to resolve this would be to handle on the VBO level that each row has their own language. Something like this attached. I looked at how Node.php does this for linking with the result rows, and $this->aliases is undocumented on field plugins, so I am not sure what would that one do, so this may or may not work.

(I also removed the unrelated changes from @dawehner's patch which I am sure crept in unintentionally. Only included the new thing in the interdiff).

Gábor Hojtsy’s picture

#2486433: Make ViewsForm stop marking itself as needing to be cached makes the operations aware of language, so this one indeed only has the delete operations left then and/or adding confirm steps to all of them for extra safety. But at least following #2486433: Make ViewsForm stop marking itself as needing to be cached only the found translation would be affected by the operation. Technically we should postpone on that one I think but @dawehner said that @plach wanted to add test coverage for the language specific operations here. While that patch introduces loading the right entity variant, it does not test it specifically.

dawehner’s picture

Issue tags: +Needs tests

Yeah that sounds like the plan we (plach, dawehner) talked about earlier.

Gábor Hojtsy’s picture

#2486433: Make ViewsForm stop marking itself as needing to be cached landed, so the non-delete operations should be fixed now I believe. Will test manually now. Needs tests for those as well as the delete operation to have confirmation properly because in that case, deleting the original language does not only delete that (unlike all other operations). I don't think confirmation for the rest of the operations is a requirement here then, because following #2486433: Make ViewsForm stop marking itself as needing to be cached the operations are doing the thing the user selected except the delete.

Gábor Hojtsy’s picture

FileSize
1.41 KB
997 bytes

Then my change to load the entity in the language is duplicate, the bulk key already does that. So this is now only @dawehner's code rerolled.

Gábor Hojtsy’s picture

Status: Needs review » Needs work

Oh, manually testing #2486433: Make ViewsForm stop marking itself as needing to be cached did not fix the operations because the language is always the original one in the VBO result even for translation rows. So it works the same as before, even though now the id should be different per row. I set up the same scenario as above (3 article translations in the same article, 1 page). The article translations show up as "en-1-1" for all of them in the table instead of their own language :/ So needs fix for that too. And tests.

Gábor Hojtsy’s picture

That is because this hunk is not getting the right language:

@@ -175,6 +186,8 @@ public function viewsForm(&$form, FormStateInterface $form_state) {
       // Render checkboxes for all rows.
       $form[$this->options['id']]['#tree'] = TRUE;
       foreach ($this->view->result as $row_index => $row) {
+        $entity = $this->getEntity($row);
+

Let's ensure in this issue that it does.

The last submitted patch, 27: 2484037-27.patch, failed testing.

plach’s picture

Assigned: Unassigned » plach
Issue tags: +D8 Accelerate

Working a bit on this

plach’s picture

Assigned: plach » Unassigned

Not yet, sorry, I'll get back to this in a while, feel free to assign if you have tome to work on this.

Berdir queued 27: 2484037-27.patch for re-testing.

dawehner’s picture

Working on just the test coverage for now.

plach’s picture

Assigned: Unassigned » plach

For reals now

plach’s picture

This should fix translation selection.

Status: Needs review » Needs work

The last submitted patch, 36: views-multilingual_vbo-2484037-36.patch, failed testing.

dawehner’s picture

Looks pretty much how I expect it to look like!

plach’s picture

Assigned: plach » Unassigned
Issue summary: View changes
Status: Needs work » Needs review
FileSize
7.04 KB
15.17 KB
260.14 KB
123.63 KB
161.53 KB

This implements individual translation deletion. Next steps:

  • Generalize this code in a base class, so the user form does not need to repeat it.
  • Fix the FIXME: there's a bad issue with lost translation object internal references, due to the temp store, I think.
  • Verify this fixes also the other bulk operations.
  • Provide additional test coverage.

Here are a few screenshots, I tried to mimic the single deletion form:

I'm done for today :)

tim.plunkett’s picture

+++ b/core/modules/node/src/Form/DeleteMultiple.php
@@ -102,11 +101,22 @@ public function buildForm(array $form, FormStateInterface $form_state) {
+          $items[] = $node->label();
...
-        return SafeMarkup::checkPlain($node->label());

Losing a checkPlain

plach’s picture

Sorry, I uploaded the wrong patch, the interdiff was correct, though.

Status: Needs review » Needs work

The last submitted patch, 41: views-multilingual_vbo-2484037-41.patch, failed testing.

dawehner’s picture

We still need the test coverage indeed ... maybe I'm motivated to write still one later ...

  1. +++ b/core/lib/Drupal/Core/Entity/EntityManager.php
    @@ -972,20 +972,22 @@ public function getTranslationFromContext(EntityInterface $entity, $langcode = N
    +      // negotiation, unless the current translation is already the desired one.
    +      if ($entity->language()->getId() != $langcode) {
    

    Good catch!

  2. +++ b/core/modules/node/src/Form/DeleteMultiple.php
    @@ -102,11 +104,43 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     
    +    $items = [];
    +    foreach ($this->nodes as $node) {
    

    We should add a comment what kind of message we create here.

  3. +++ b/core/modules/node/src/Form/DeleteMultiple.php
    @@ -102,11 +104,43 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +          $items[$key] = SafeMarkup::checkPlain($node->label());
    ...
    +        $items[$key] = SafeMarkup::checkPlain($node->label());
    

    OH, do we really still need to check plain for our own or can we rely on twig autoescape?

  4. +++ b/core/modules/node/src/Form/DeleteMultiple.php
    @@ -118,12 +152,53 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +        // FIXME We shouldn't need to load fresh objects.
    +        $this->storage->loadMultiple(array_keys($delete_translations));
    ...
    +          /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    +          $entity = $this->storage->load($id);
    

    Indeed, why do we need it? Even multiple times?

  5. +++ b/core/modules/system/src/Plugin/views/field/BulkForm.php
    @@ -63,19 +74,71 @@ class BulkForm extends FieldPluginBase {
    +  /**
    +   * {@inheritdoc}
    +   */
    +
    +  protected function getLanguageManager() {
    +    return $this->languageManager;
    +  }
    

    this empty line is not needed

  6. +++ b/core/modules/system/src/Plugin/views/field/BulkForm.php
    @@ -186,7 +249,7 @@ public function viewsForm(&$form, FormStateInterface $form_state) {
           foreach ($this->view->result as $row_index => $row) {
    -        $entity = $this->getEntity($row);
    +        $entity = $this->getEntityTranslation($this->getEntity($row), $row);
     
    

    this is perfect

  7. +++ b/core/modules/views/src/Entity/Render/EntityTranslationRenderTrait.php
    index 25b6a94..11b7ced 100644
    --- a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
    
    --- a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
    +++ b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
    
    +++ b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
    +++ b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php
    @@ -1106,7 +1106,7 @@ public function testGetEntityTypeLabels() {
    
    @@ -1106,7 +1106,7 @@ public function testGetEntityTypeLabels() {
       public function testGetTranslationFromContext() {
         $this->setUpEntityManager();
     
    -    $this->languageManager->expects($this->exactly(2))
    +    $this->languageManager->expects($this->exactly(1))
           ->method('getFallbackCandidates')
           ->will($this->returnCallback(function (array $context = array()) {
             $candidates = array();
    @@ -1117,17 +1117,17 @@ public function testGetTranslationFromContext() {
    
    @@ -1117,17 +1117,17 @@ public function testGetTranslationFromContext() {
           }));
     
         $entity = $this->getMock('Drupal\Tests\Core\Entity\TestContentEntityInterface');
    -    $entity->expects($this->exactly(2))
    +    $entity->expects($this->exactly(1))
           ->method('getUntranslated')
           ->will($this->returnValue($entity));
         $language = $this->getMock('\Drupal\Core\Language\LanguageInterface');
         $language->expects($this->any())
           ->method('getId')
           ->will($this->returnValue('en'));
    -    $entity->expects($this->exactly(2))
    +    $entity->expects($this->exactly(3))
           ->method('language')
           ->will($this->returnValue($language));
    -    $entity->expects($this->exactly(2))
    +    $entity->expects($this->exactly(1))
    

    Did anyone opened up an issue already to get rid of all this super strictness in EntityManager?

YesCT’s picture

I got part way testing this, and looking into 3.
I think we can take out the checkplain. I tried to verify by taking out the checkplain and then making a node with the title <script>alert("xss!"); and looking at the message from deleting some of the nodes. and the title showed like that, and did not cause an alert. Is that enough proof it is ok to take out the checkplain?

[edit] I noticed something I will double check again later that is not a problem in head:
If make a node first, before enabling content translation, and then enable content translation, add a language and make another node.
The second node has translate as a contextual link in the all content listing, but the first node does not.

But clearing all the caches makes the translate contextual link show on the first node, and this is the same in head. Not introduced by this patch.

YesCT’s picture

+++ b/core/modules/node/src/Form/DeleteMultiple.php
@@ -102,11 +104,43 @@ public function buildForm(array $form, FormStateInterface $form_state) {
+      elseif (!isset($items[$default_key])) {

Is this working dependent on the order of the results of $this->nodes?

because it assumes that the source langcode-node is processed first, so that translations are not separately added if the original was already selected (and thus all of its translations)?

plach’s picture

Assigned: Unassigned » plach

On this again

plach’s picture

Issue summary: View changes
Issue tags: +Usability, +Needs usability review

AS pointed out by Gabor in today's critical issues call, we need an usability review here. Added the screenshots to the IS.

webchick’s picture

I'll try and review a bit later, but one thing right off the bat:

On the admin/content page, how would a user know that "test 2 EN" is a default translation versus "test 2 FR" is a regular translation? The rows look identical to me in the table, so the difference in behaviour between the two would be extremely surprising.

plach’s picture

This should fix the user cancellation form. Actually it does not make any sense to display translations on the People view, since it's meant to administrate user accounts and not the content associated to them. Additionally the user name is not translatable, hence we'd show just straight duplicates. A dedicated custom view would be way better suited to deal with profile translations.

For these reasons I just added a filter on the default language and did not change the logic of the bulk cancellation form, which is meant to cancel accounts, again nothing to do with the field content attached to the user entities.

If we are ok with this solutions, these are the updated next steps:

  • Generalize this code in a base class, so the user form does not need to repeat it. - still good to have, but not critical.
  • Fix the FIXME: there's a bad issue with lost translation object internal references, due to the temp store, I think.
  • Verify this fixes also the other bulk operations.
  • Provide additional test coverage.

And obviously address any feedback from the UX team.

plach’s picture

@webchick:

In #2473989: Lack of dynamic language field / filter makes shipping core views hard to be both compatible with mono and multilingual there's a patch to add the language column, we could add also the default translation column, if you think that would address your concerns.

plach’s picture

@YesCT:

Is this working dependent on the order of the results of $this->nodes?

Yep, the unset call in the loop does the trick. You can verify it via manual testing by updating a translation so it comes before the original in the selection.

damiankloip’s picture

Nice work.

  1. +++ b/core/modules/node/src/Form/DeleteMultiple.php
    @@ -118,12 +154,53 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +        $this->storage->loadMultiple(array_keys($delete_translations));
    

    Isn't that better than serializing entities in the form state? Getting away from that is a good thing IMO. Or you mean something else here?

  2. +++ b/core/modules/system/src/Plugin/views/field/BulkForm.php
    @@ -294,7 +356,7 @@ public function viewsFormSubmit(&$form, FormStateInterface $form_state) {
    +        $entities[$bulk_form_key] = $entity;
    

    It's fine to use the entity ID again here as it's from the entities we have already loaded based on the bulk form key. The bulk form key is just for this plugin really.

  3. +++ b/core/modules/system/src/Plugin/views/field/BulkForm.php
    @@ -63,19 +74,70 @@ class BulkForm extends FieldPluginBase {
    +  public function isCacheable() {
    ...
    +  public function getCacheContexts() {
    ...
    +  public function getEntityTypeId() {
    ...
    +  protected function getEntityManager() {
    ...
    +  protected function getLanguageManager() {
    ...
    +  protected function getView() {
    

    Can we keep the init() method above all of these please (Sorry :)).

I don't know all the implications of implementing CacheablePluginInterface here but it sounds dangerous for a form like this.

webchick’s picture

Per #48/#50, plach and I spoke on IRC and suggested adding a language column to the table (there was one on D7) with "default" denoted in that column for ones that are default translations. That'd look something like this:

+------------+-----+-------------------+-----+
| Title      | ... | Language          | ... |
+------------+-----+-------------------+-----+
| Hello      | ... | English (default) | ... |
| Bonjour    | ... | French            | ... |
+------------+-----+-------------------+-----+

Another option is to remove the "specialness" of default translations but this does not appear to be easy: #2485499: Allow source translations to be removed #2443991: Allow default_langcode field value to be changed Excerpt:

"Anyway, I'm pretty sure just changing the default_langcode field value is not enough, unless the entity is reloaded, because the internal data structures need to be adjusted to take the new default langcode into account."

webchick’s picture

Incidentally, I don't know that that needs to be done as part of this issue since it's a "pre-existing condition," but figured I'd mention it since it sounds like it might be easy to fix.

dawehner’s picture

Regarding #53, https://www.drupal.org/node/2473989#comment-9881101 had already a patch with tests and what not ...

Incidentally, I don't know that that needs to be done as part of this issue since it's a "pre-existing condition," but figured I'd mention it since it sounds like it might be easy to fix.

I kinda agree, its not part of the criticality of the patch. You know, we demoted those issues before, so let's stay with that decision.

YesCT’s picture

+++ b/core/modules/node/src/Form/DeleteMultiple.php
@@ -130,11 +132,11 @@ public function buildForm(array $form, FormStateInterface $form_state) {
-          $items[$key] = SafeMarkup::checkPlain($node->label());
+          $items[$key] = $node->label();
...
-        $items[$key] = SafeMarkup::checkPlain($node->label());
+        $items[$key] = $node->label();

I can confirm with more certainty that this is totally fine.

For the record:

I checked (with help in IRC from @dawehner) that
core/lib/Drupal/Core/Template/TwigEnvironment.php:59
set to FALSE
(and a drush cr)

And a title (label) of <script>alert("xss!");</script>

with the change to not have checkPlain()

makes the alert happen.

And, changing
core/lib/Drupal/Core/Template/TwigEnvironment.php:59
back to TRUE
(and then a drush cr)

makes the alert not happen.
So the autoescape is enough for us.

YesCT’s picture

I think we do not need to add language (or if something is the default/source translation) to the all content overview listing.
Note #1279624: Add translation filter to content listing admin page looked into that for a bit, and concluded that since it is a view, people can configure that if they want it. (at least that is what we concluded at the time)

also, for the translation tab for a piece of content, I remember us going around and around on what words to use for the source/default/original translation.

We ended up with: (Original language)

Note there is some complicated edge cases, where the language of the original translation can be changed, and then translation can have different sources...

I dont want this to get blocked on that discussion again as to if the "original" is really the original (it means the current *actual* language of the *the* node #1833180: decide on name of: Original content vs Original language vs Source language vs n/a in translations overview)

----

I think instead of changes to the content listing (which IMO should be a separate non-critical issue), we can help a bit by improving the wording on the delete confirmation... I'll try and get a patch right now.

YesCT’s picture

here are some more (hopefully also clarifying words) which add why the multiple translations are removed in some cases (when the original node is deleted) and also the titles of each of the translations.

dawehner’s picture

Good idea!

YesCT’s picture

Issue summary: View changes

updated remaining tasks.

plach’s picture

Not sure about #58, I tried to model the nested list on the single entity deletion form:

https://www.drupal.org/files/issues/Translation_Delete_Content.png

Now single and multiple deletion forms are inconsistent.

YesCT’s picture

Status: Needs review » Needs work
+++ b/core/modules/node/src/Form/DeleteMultiple.php
@@ -7,14 +7,16 @@
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\ContentEntityTypeInterface;

I'm pretty sure we do not need the SafeMarkup use anymore. Not sure about the other changes to this use hunk.

---------

Did some manual testing on operations other than delete.

When selecting a translation without also selecting the original source, and picking remove from the front page, or promote to the front page, it works.

When selecting the original source, and not its translations, and picking remove from the front page, or promote to the front page, it works, effecting only the source original translation and not the other ones.

When selecting a translation and its original source, the remove from front page, or promote to front page, only effects the original source (and not the translation), even though it was also checked. Note, the message says the correct number of things was altered, but really not all of them were.

Reading the issue summary:
"If the default translation is selected for other operations but deletion, only operate on that translation."

So I think we need to alter the implementation of this.

=========

I think setting this to needs work for that, tests, and the fix me probably makes sense.

(I wont have time to work on this (for example writing tests) for at least a few hours, so that is up for grabs for someone to do.)

YesCT’s picture

Issue summary: View changes

@plach I can see that point of view also. If we decide we want to not change it here, without also changing it on the single delete form, we can make a separate (normal) issue to change it on both after this critical goes in. It was an idea so that #53 did not have to be scoped into this issue. (which I still think is a separate issue either way.)

plach’s picture

@damiankloip:

Isn't that better than serializing entities in the form state? Getting away from that is a good thing IMO. Or you mean something else here?

Good point. Serializing entities breaks internal translation references, so it's a bad idea for at least two reasons :)
The attached patch implements this suggestion and thus should fix the FIXME.

It's fine to use the entity ID again here as it's from the entities we have already loaded based on the bulk form key. The bulk form key is just for this plugin really.

It's not: if we don't use the bulk form key we cannot tell which translations were selected, because we get only one entity for each translation set. I removed the explicit reference in the user form code though.

I don't know all the implications of implementing CacheablePluginInterface here but it sounds dangerous for a form like this.

Good point again, I'll ask Wim or Fabian to chime in.

@webchick / @dawehner:

Incidentally, I don't know that that needs to be done as part of this issue since it's a "pre-existing condition," but figured I'd mention it since it sounds like it might be easy to fix.

Let's keep that for #2473989: Lack of dynamic language field / filter makes shipping core views hard to be both compatible with mono and multilingual: hiding the column on monolingual sites involves some non-trivial code and we also need test coverage for that.

@YesCT:

Did some manual testing on operations other than delete. [...]

Thanks, I'll tackle this next.

I can see that point of view also. If we decide we want to not change it here, without also changing it on the single delete form, we can make a separate (normal) issue to change it on both after this critical goes in. It was an idea so that #53 did not have to be scoped into this issue. (which I still think is a separate issue either way.)

Yep, we should either postpone this to a separate issue, or wait for the UX team to tell us how to reconcile the two forms.

plach’s picture

Issue summary: View changes
plach’s picture

Issue summary: View changes
webchick’s picture

Yeah I think we're all in "violent agreement" ;) that #53 can be handled in #2473989: Lack of dynamic language field / filter makes shipping core views hard to be both compatible with mono and multilingual. I just wanted to capture that the discussion happened, since the point came up here in review.

So then it sounds like what I should actually be UX reviewing is what comes after that screenshot, which are the confirm form and dsm() screenshots:

But essentially, same feedback: I do not understand how a user will understand why one of these triggers a "cascading delete" and others don't (and that's even assuming we fix the previous screen to include the language column in that other issue). I do like the idea of keeping symmetry with the other confirm form, but "The following content translations" is not enough of a clue stick IMO, because "Test IT" and "Test FR" are also content translations and do not trigger the "cascading delete."

So, can we do a similar thing, which is append "(Original translation)" or some such string to the node title in that case? Then it would be:

  • Test 2 EN (Original translation) - The following content translations will be deleted:
    • English
    • French
  • Page 3
  • Page 1
  • Test FR
  • Test IT

That at least provides some bit of clue.

(Nitpick: Would be great if we could squeeze an "also" between "will" and "be" but can do that in a follow-up for all such forms.)

On this one, I feel that breaking these apart is much more confusing than not. First of all because I actually deleted four content translations, did I not? (English, French, Test FR, Test IT)

From a user POV, all I'm really deleting here are rows in a table. So can you just say "Deleted 7 items"? (Or "posts" or whatever the current language is.)

damiankloip’s picture

It's not: if we don't use the bulk form key we cannot tell which translations were selected, because we get only one entity for each translation set. I removed the explicit reference in the user form code though.

Ah, so the same entity is/could be referenced many times. I forgot this was already added to loadEntityFromBulkFormKey (to return the translation if needed). Nice!

Gábor Hojtsy’s picture

@plach: I agree with the UX of the people form re #49. Looks like this was not commented on much by others in the meantime so wanted to jot down my support :) Makes sense given there are no translatable things in the user view unlike the content view where everything is translatable except the type by default at least.

plach’s picture

@webchick:

Nitpick: Would be great if we could squeeze an "also" between "will" and "be" but can do that in a follow-up for all such forms.

This makes me suspect you didn't notice that all languages are listed, including the default translation language, not only the ones of the regular translations. Again this is the behavior currently implemented in the single deletion confirmation form. IMHO it's ok as it is right now, in fact if we listed only the translations, it would make more sense to list their titles, for consistency. But this would probably clutter the from, and may not be informative to a person having no familiarity with the related languages.

Gábor Hojtsy’s picture

@plach: well, you know that, the user may not :) Maybe "This is the original content, so all translations will be deleted:" may be a shorter summary for the message? I think while it is still confusing that some rows work different (and that is due to currently not supporting removal of the default language variant to designate another default language variant), the message does not make it clear that all the translations are removed. Making that connection may make it easier to understand why that deletes more.

plach’s picture

This should fix all the other operations, which were suffering from a loss of the translation internal references, similar to the one above.

I also fixed this, from #52:

I don't know all the implications of implementing CacheablePluginInterface here but it sounds dangerous for a form like this.

after discussing it with Fabian, Damian and Daniel (unfortunately Gandalf was missing ;)

plach’s picture

@webchick's review still to be addressed.

plach’s picture

Btw, this is why things were not working: the bulk form plugin always used EntityStorageInterface::loadRevision() to retrieve the selected entities, which, combined with #2503025: EntityStorageInterface::loadRevision() implementations are not statically cached, caused one entity translation to override the other.

plach’s picture

plach’s picture

Addressed #67 but kept separate log messages for entity and translation deletions, which IMO can be useful. I also skipped the nitpick, see #70.

plach’s picture

Added test coverage for the various operations. Now working on a specific test for deletion. The interdiff and the test-only version are identical.

dawehner’s picture

Great tets coverage

+++ b/core/modules/node/src/Tests/Views/BulkFormTest.php
@@ -102,22 +167,53 @@ public function testBulkForm() {
+      'node_bulk_form[8]' => TRUE, // Untranslated.
+      'action' => 'node_unpublish_action',

I'm curious, can you explicitly select node_bulk_form[9] => FALSE ? And document which translation that is? I guess its the italian one?

The last submitted patch, 77: views-multilingual_vbo-2484037-77-test.patch, failed testing.

plach’s picture

This completes test coverage and addresses #78. We should be done here, aside from addressing upcoming reviews.

The last submitted patch, 80: views-multilingual_vbo-2484037-80-test.patch, failed testing.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Looks great for me now!

  1. +++ b/core/modules/node/src/Form/DeleteMultiple.php
    @@ -97,16 +96,48 @@ public function getConfirmText() {
       public function buildForm(array $form, FormStateInterface $form_state) {
    -    $this->nodes = $this->tempStoreFactory->get('node_multiple_delete_confirm')->get(\Drupal::currentUser()->id());
    -    if (empty($this->nodes)) {
    +    $this->nodeInfo = $this->tempStoreFactory->get('node_multiple_delete_confirm')->get(\Drupal::currentUser()->id());
    +    if (empty($this->nodeInfo)) {
           return new RedirectResponse($this->getCancelUrl()->setAbsolute()->toString());
         }
    +    /** @var \Drupal\node\NodeInterface[] $nodes */
    +    $nodes = $this->storage->loadMultiple(array_keys($this->nodeInfo));
    +
    +    $items = [];
    

    Did you considered to adapt the code to also support deleting revisions? Is this even a concept which exists in Drupal?

  2. +++ b/core/modules/node/src/Plugin/Action/SaveNode.php
    @@ -25,6 +25,9 @@ class SaveNode extends ActionBase {
       public function execute($entity = NULL) {
    +    // We need to change at least one value, otherwise the changed timestamp
    +    // will not be updated.
    +    $entity->changed = 0;
         $entity->save();
       }
    

    Oh tricky!

damiankloip’s picture

+++ b/core/modules/system/src/Plugin/views/field/BulkForm.php
@@ -178,15 +242,19 @@ public function getValue(ResultRow $row, $field = NULL) {
+    // Make sure we do not accidentally cache this form, see ::isCacheable().

Do you think we should mirror the @todo in isCacheable() here too? As this is just a failsafe, related to that, right?

plach’s picture

Let's make sure @webchick has a chance to review this once more.

@dawehner:

1: Honestly I didn't. We definitely support deleting revisions although we currently have no UI to delete them in bulk. This would certainly make sense as a follow-up, together with a generalization making the logic available to any entity type.

@damiankloip:

Done :)

Gábor Hojtsy’s picture

Gábor Hojtsy’s picture

Looks like d.o removed the files with my crosspost with @plach's crosspost...

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Went through the same steps as earlier, and confirmed the screenshots match expectations. I also fiddled around with e.g. setting "Sticky" on both an original and regular translation, and the settings took to only the intended rows, as expected. Awesome!! Finally, I tried creating my own content bulk operations view from scratch (in French, no less, which was a lot of "fun") and verified that the behaviour works fine there too.

I did find one small issue: I can't see anywhere in the patch that would cause this, but the Cancel button on the bulk operations delete confirm leads me back to "/content" (404), not "/admin/content". Not worth holding up a critical for this, though. Can we get a follow-up? (Maybe it's even a novice issue?)

Actually, I lied, I found another small issue: If I go to /fr/admin/structure/views, and hit "Edit" (well, "Modifier" ;)) on one of the views in that table, I'm taken to /admin/structure/views/view/content (it switches me back into English UI text translation), not /fr/admin/structure/views/view/content. (Actually, several places in the Views UI seemed to do this "popping back to English" behaviour, but I wasn't completely paying attention so needs more testing.) So it looks like we might still have some spots to clean-up? But at any rate, also not part of this issue.

It looks like code-wise this patch has been reviewed by multiple folks extremely knowledgeable about this area of the code. Therefore.....!

Committed and pushed to 8.0.x. YEAH! :D

Great work on this folks!!! :D

  • webchick committed 05af46b on 8.0.x
    Issue #2484037 by plach, Gábor Hojtsy, YesCT, dawehner: Make Views bulk...
Gábor Hojtsy’s picture

Status: Fixed » Closed (fixed)

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