atk4 / ui

Robust and easy to use PHP Framework for Web Apps
https://atk4-ui.readthedocs.io
MIT License
440 stars 104 forks source link

States of GRID checkboxes aren't saved for further use #2081

Open bedengler opened 1 year ago

bedengler commented 1 year ago

The issue

As per discussion on Discord, there's currently no implemented way of saving the state of checkboxes in a GRID. That means, you add checkboxes via "addSelection();", then you get checkboxes. That's it.

If you check some checkboxes and click on another page in paginator (below the grid), all selections get lost. It's currently impossible to go through all items of a grid and make selections on several pages. Yes, you could increase the IPP, but that's not user friendly or convenient.

My current workaround is to call an AJAX file every time when a checkbox is clicked. I save the state of the checkbox in a session array then.

When the grid reloads, it reads the selected IDs from the session variable and pre-selects them. That's working fine so far (code below) until it comes to pagination. As soon as you click on another page, the selections get lost again (although the previously selected checkboxes are still in the session variable - so they still can be used, but aren't there visually which is bad for the UX).

(Partial) Workaround

Now let's discuss ways of how we can achieve the following:

  1. Save the state of checkboxes the atk4 way (e.g. using memorize) - so that we can use the selection for actions.
  2. Check previously checked checkboxes on reload of the GRID (also page reload)
  3. Make sure that selections are maintained when paginating

I added the following method into my extended GRID:

public function setupGrid() {

   $sel = $this->addSelection();

    // Load the selected checkboxes
    // Session variable name is too specific currently for general use - must be something like "$this->id->checkboxes" or "$this->memorize"
    if(isset($_SESSION['checkboxesChecked']))
      $session = $_SESSION[checkboxesChecked];
    else
      $session = NULL;

    // pre-select checkboxes, that have been selected before and are saved in the session
    if (isset($session)) {
      $jsCode = [];

      foreach ($session as $id) {
        $lineID = "#".$this->table->name." [data-id=".$id."] .checkbox";
        $jsCode[] = "$('" . $lineID . "').checkbox('check');";
      }
    }

    if (!empty($jsCode)) {
      $this->js(true, new \Atk4\Ui\JsExpression(implode("\n", $jsCode)));

      // This should maintain selections over pagination, but doesn't work currently
      $this->paginator->on('click', '.item', new \Atk4\Ui\JsExpression(implode("\n", $jsCode)));

    // Save clicked checkboxes into session as soon as clicked
    $this->on('click', '.checkbox', new \Atk4\Ui\JsExpression('
      var id = $(this).closest("tr").data("id");
      var checked = $(this).is(".checked");

      $.ajax({
        url: "ajax/saveToSession.php",
        method: "POST",
        data: { 
          idToChange: id, 
          saveWfSelection: 1,
          active: checked,
        }
      });
    '));
  }

use it like this:

$grid = myGrid::addTo($app);
$gridModel = new myModel($app->db);
$grid->setModel($gridModel);
$grid->setupGrid();

The saveToSession.php looks like this:

/** 
 * Function that gets a value and an array. If the value is in the array, it removes it from the array. If the value isn't present in the array, it adds it
 * Function is placed in an external file - I'm just adding it here for the sake of logic
 * Returns the new array
 */
function addRemoveValueFromArray($value, $array, $status = NULL) {
    // $status determines, if the given value should be activated or deactivated
    // Must be true or false, otherwise it will be ignored

    if (in_array($value, $array)) {
        // If $value exists in the array, remove it
        $array = array_filter($array, function($item) use ($value) {
            return $item !== $value;
        });
        //$arrayNew = array_values($array);  // Re-index the array
    } 
    else{
        // If $value does not exist in the array, add it
        $array[] = $value;
    }

    // Now force the action according to the status
    if ($status == "false") {
        $array = array_filter($array, function($item) use ($value) {
            return $item !== $value;
        });
    }
    elseif($status == "true" AND !in_array($value, $array)) {
        $array[] = $value;
    }

    return $array;
}

// Save divers data into session variables
session_start();

$idToChange = $_POST['idToChange'];

if(!isset($_SESSION['checkboxesChecked']))
        $_SESSION['checkboxesChecked'] = [];

$selectionOld = $_SESSION['checkboxesChecked'];

$selectionNew = addRemoveValueFromArray($idToChange, $selectionOld, $_POST['active']);

$_SESSION['checkboxesChecked'] = $selectionNew;

This should be ment as a basis for discussion on how to implement the functionalities described above but in a more atk4 way.

Let's brainstorm first here and then do it. I am happy to support and implement it, but I'm not so deep in atk4 core development, so I need your support / ideas on how to bring it into the atk4 core and create a pr please πŸ™

mvorisek commented 1 year ago

So if you have a table with 100 entries (20 per page) and you want to go through all of them and select some of them, that's currently impossible.

This will lead to a hidden selection. This is not wanted nor standard. We do not want to allow selection (and actions) on non-displayed entities. The solution is however easy, use larger IPP (items per page) and optimionally narrow the entities by search.

Am I right is issue is solely about persisting the selection? Across what? Grid reload? page reload? Search? -> please comment and edit the description a) to be shorter :), b) to narrow it's scope.

bedengler commented 1 year ago

More details on the issues:

1) Usability

I understand it's a matter of taste and learning / habits. Nevertheless I believe the more natural and convenient way is to keep selection over page switching.

Probably we can add an option to addSelected that keeps this flexible? Something like addSelection($preserveSelectionOnAllPages = false); If set to true, checkboxes remain saved in the state, even if user switches to another page.

2) Persisting The Selection

Example:

$arr = (0 => "1", 1 => "5");
$grid-> addSelection($array);

Expected behaviour:

In my case I need to keep the state of checked checkboxes over a 4 step wizard. So whenever the user jumps back to the step with the grid, where he made some selections before, these selections should remain checked. addSelection($array); could solve that as well...

Example: $grid->addSelection($wizard->recall($checkedCheckboxes));

3) Save The Selection

4) Reset The Selection

A button to reset selection. If "$preserveSelectionOnAllPages == true;" just unsetting the variable that contains the selection.

bedengler commented 1 year ago

Steps to do:

mvorisek commented 1 year ago

Imagine, there are 115 entries and IPP is at 100 and the user realises that after making all selections. Paginating now would mean losing the selection. Not paginating means he needs to do the action twice.

This is legit point. IPP change should not reset the selection.

Memorizing the selection across (whole) page load is however probably not wanted, as there can be multiple actions, so the memorized selection can be for another/previous action and can no longer make sense.

Also I am strongly againts any hidden selection, so page (in sense of paginator) change should probably not hold the selection from another no longer visible page.

API for setting an array of selection/getting the current selection might be good.

What are your usecases? Maybe you might want to store the checkebox state within entity and use inline/in-cell edit.

bedengler commented 1 year ago

This is legit point. IPP change should not reset the selection.

Not only that, what, if the max. selectable IPP is 100 and you have 115 entries. No chance to get that done in a user friendly way...

Memorizing the selection across (whole) page load is however probably not wanted, as there can be multiple actions, so the memorized selection can be for another/previous action and can no longer make sense.

Probably yes. Although in some cases, it makes things easier. So we should be flexible (that's what I love about atk4: it's flexibility)...

What are your usecases? Maybe you might want to store the checkebox state within entity and use inline/in-cell edit.

I have a client who has 4.000 adspaces. In 2006 I have written for them an entire management system including invoicing etc. We adapted this system continuously. Nevertheless this system is a bit outdated now, so we decided to relaunch it, put in all the experience we gathered within the last 15 years about their workflow and what they want, which processes work best for them etc. I decided to go with atk4.

To create offers for their clients, they want a list of adspaces that's filterable and searchable. atk4's wizard is awesome for that process. In the first step they get a list (grid) of all adspaces. They filter / search e.g. the location, make some selections of adspaces that should be part of the offer. Then they change the filters (probably another city) and continue making selections.

When they believe, they are ready for the next step, they go to the next step in the wizard. There they can make adaptions to each of the selected entries (e.g. change the price, date etc.).

It happens, that they miss an entry, so they go one step back to the list again, search for another adspace and add it to the selection.

If the selection is reset every time they search, switch page or go to the next step, it would be a productivity and usability nightmare.

And the other way: if they could just go seamlessly back and forth that would make things easy and user friendly (in this case).

I hope you see my points now 😊