VBO breaks other modules functionality (potentially core)

Created on 30 October 2024, about 1 month ago

Problem/Motivation

In \Drupal\views_bulk_operations\Plugin\views\field\ViewsBulkOperationsBulkForm::viewsForm, if the "Display selectable actions as buttons" has been enabled, the class is removing the default (provided by core in \Drupal\views\Form\ViewsFormMainForm::buildForm) actions submit button(s) and replaces them with the enabled VBO actions:

      if ($this->options['buttons']) {
        unset($form['actions']['submit']);
        foreach ($action_options as $id => $label) {
          $form['actions'][$id] = [
            '#type' => 'submit',
...

As a result, the VBO is "taking over" the whole form, thus not allowing other modules to provide their form-based functionalities in the same view.

For example the Views Entity Form Field module provides the ability to add form field widgets to a view to edit multiple entities at one time. It is mentioned in the module's main page that:

There is an issue with Views that use Bulk Form Operations - BFO takes over the submit button and will throw validation errors if there are no BFO checkboxes checked. It's suggested to not use both modules on the same View for now.

(relevant issue https://www.drupal.org/project/views_entity_form_field/issues/3126328 )

Steps to reproduce

  • Install both modules: VBO and views_entity_form_field
  • Create a page view
  • Add any form field provided by views_entity_form_field
  • Save the view, and open the view's page
  • Here you can see the "Save" button, that will submit the form and update the fields' values
  • Edit the view and add the VBO field, with the "Display selectable actions as buttons" option selected
  • Open the view's page: The "save" button has disappeared.

A similar problem comes up if the "Display selectable actions as buttons" is not enabled. Here, javascript disables the (single one) save button, unless some VBO action has been selected.

Proposed resolution

I don't have a concrete proposal yet, as I would like to first discuss with VBO's maintainers, who have much better understanding of the history and the limitations of the module.
I'd be happy to contribute to any good solution that we might agree on.
As a first thought, very draft, I thought of 2 different approaches:

  1. Add some extra option like "Prevent overriding other form submit handlers".
  2. Rewrite the VBO whole submit system (form elements and handlers) in a more isolated/modular way.

Remaining tasks

  • Discuss with maintainers about possible approaches, limitations and challenges
  • Decide on a solution
  • Code the solution

User interface changes

TBD

API changes

TBD

Data model changes

TBD

🐛 Bug report
Status

Active

Version

4.3

Component

Core

Created by

🇳🇴Norway efpapado

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @efpapado
  • 🇵🇱Poland Graber
    1. Views Entity Form Field has 5k usage while VBO has 60k so I think the former should rather have an issue.
    2. VBO unsets the default `submit` button only, if Views Entity Form Field defined its own button instead (`veff_submit` or whatever, it wouldn't be affected by VBO.

    In any case, happy to help to solve this somehow.

  • 🇵🇱Poland Graber

    I see Views adds a "Save" button by default.. I think every contrib module should unset that and set one of its own.. and core Views shouldn't provide that default button at all in the first place.

  • 🇳🇴Norway efpapado

    @graber Thanks for your quick reply. I started digging the code, here is what I found:

    Both modules use the core class \Drupal\views\Form\ViewsForm that adds the form functionality to the view.
    For both modules, when the form is submitted this function is called: \Drupal\views\Form\ViewsForm::submitForm which then calls the \Drupal\views\Form\ViewsFormMainForm::submitForm, which then does this:

    foreach ($view->field as $field) {
      if (method_exists($field, 'viewsFormSubmit')) {
        $field->viewsFormSubmit($form, $form_state);
      }
    }
    

    Similar logic is followed on the validation step, in \Drupal\views\Form\ViewsFormMainForm::validateForm

    By looking at this, I understand that the core's logic is that all the possible fields that contain a form are supposed to have their forms validated/submitted, and then each module's validation/submit handler is responsible to figure out whether it should do something or not. So I think that what you wrote:

    I see Views adds a "Save" button by default.. I think every contrib module should unset that and set one of its own.. and core Views shouldn't provide that default button at all in the first place.

    is basically correct as approach.

    I experimented a bit with having VEFF adding its own button, and calling its own submit handlers independently. It worked more or less, but new problems arose. When a VEFF field is added in the form, the VBO's validation breaks:

      public function viewsFormValidate(&$form, FormStateInterface $form_state) {
        if ($this->options['buttons']) {
          $trigger = $form_state->getTriggeringElement();
          $action_delta = \end($trigger['#parents']);
          $form_state->setValue('action', $action_delta);
        }
        else {
          $action_delta = $form_state->getValue('action');
        }
    

    In case of VBO's non-buttons configuration, the last line $action_delta = $form_state->getValue('action'); expects a flat values array.
    But claro is moving the VBO stuff in another container:
    https://git.drupalcode.org/project/drupal/-/blob/11.x/core/themes/claro/...

    function claro_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
    ...
        // Determine if the Views form includes a bulk operations form. If it does,
        // move it to the bottom and remove the second bulk operations submit.
        foreach (Element::children($form['header']) as $key) {
          if (str_contains($key, '_bulk_form')) {
            // Move the bulk actions form from the header to its own container.
            $form['bulk_actions_container'] = $form['header'][$key];
            unset($form['header'][$key]);
    
            // Remove the supplementary bulk operations submit button as it appears
            // in the same location the form was moved to.
            unset($form['actions']);
    ...
    

    and because VEFF is adding to the form a #tree => TRUE, the $action_delta = $form_state->getValue('action'); returns NULL.
    This last part must become $action_delta = $form_state->getValue(['bulk_actions_container', 'action']); in order to return the correct delta.

    Even when bypassing this, because of the core class's behaviour described above, whatever button triggers the submission, all the submit handlers will be called. So if the extra button offered by VEFF is clicked, VBO's submit handler will also be called (and also the opposite).

    I suggest we add on both viewsFormValidate and viewsFormSubmit

    $trigger = $form_state->getTriggeringElement();
    // some logic that will return, if the button that submitted the form was not the VBO one
    

    This of course must be done also on VEFF. And possibly we need to document it on the core's class.

    Also, we need to make VBO not change its behaviour based on the theme's or other modules' changes. Thus find a more concrete way to understand the context of the form submission (#tree or not, own container or not).

    Before proceeding with the above, I'd like to ask your opinion based on your insight.

Production build 0.71.5 2024