atk4 / ui

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

Execute php callback on Grid->addSelection() for multiple selection #1727

Closed mhuser closed 1 year ago

mhuser commented 2 years ago

First of all, I want to thank you for this great tool that is atk4/ui ! Since some time I have a question which I believe does not find answer in the documentation (but maybe I am just blinded). Let's suppose we have two models User and Role and User hasMany Role.

My question is : how to use $grid->addSelection() to mass-attribute a Role to a selected set of User ? (Which enable to use grid filtering and sorting) In the demo there is a call to some javascript

$grid->menu->addItem('show selection')->on('click', new \Atk4\Ui\JsExpression(
    'alert("Selected: "+[])',
    [$sel->jsChecked()]
));

But I does not understand how to relies on a php callback instead that would receive the selected id list and perform some processing such at foreach the list and create the appropriate role lines or others.

mhuser commented 2 years ago

I finally managed it by using javascript to set value of an hidden form input in a static modal. Any other idea for better implementation welcome.

    $mod2 = \atk4\ui\Modal::addTo($app);
    $form2 = \atk4\ui\Form::addTo($mod2);
    $form2->addControl('selection_data', [\atk4\ui\Form\Control\Hidden::class]);
    $val2 = $form2->getControl('selection_data');
    $val2->setAttr(['type' => 'hidden']);
    $form2->addControl('role',
        [
        \atk4\ui\Form\Control\Lookup::class,
        'model' => new Role($app->db),
        'placeholder' => 'Search by name',
        'search' => ['Name'],
    ]);
    $form2->onSubmit(function ($f){
        $errors = [];
        if (!$f->model->get('role')) {
            $errors[] =  $f->error('role', 'Required');
        }
        if ($errors) {
            return $errors;
        }
        $user_ids = explode(',', $f->model->get('selection_data'));
        foreach($user_ids as $id){
            $m = new RoleLine($f->getApp()->db);
            $m->set('IdRole', $f->model->get('role'));
            $m->set('IdUser', $id);
            $m->set('department_id', $f->getApp()->department->getId());
            $m->save();
        }
        $modal_chain = new \atk4\ui\jQuery('.ui.modal');
        $modal_chain->modal('hide');
        return [$modal_chain];
    });
    $grid->menu->addItem(['Attribuer rôle', 'icon'=>'theater masks'])
    ->on('click',
        function($j, $param) use ($mod2, $val2) {
            return [$val2->jsInput()->val($param), $mod2->show()];
        },
        [$sel->jsChecked()]);
mvorisek commented 2 years ago

@mhuser is there anything to be improve in atk4/ui?

mhuser commented 2 years ago

Hello @mvorisek thank you a lot for asking!

I find atk4/ui is a really great tool!

When starting to use Grid I stumbled on this: It is easy to create a grid with a model and some conditions. When interacting with it, it is also very straightforward to

  1. use $grid->menu->addItem(...); to pop a JsModal and add a new record or perform an action on all records
  2. use $grid->addModalAction(...); to pop a modal and perform an action with a Form on a single existing record

And all of this even without writing any JavaScript, which is incredibly nice!

However, when it come to performing an action on a selection of records, things escalated quickly: The demo page introduce $grid->addSelection(); with only a JsExpression that pop up an alert. Such is also the documentation.

To be honest, I fear this is not very helpful for an individual with limited atk4/ui skills like me.

The solution above works, but the code required for it is verbose and error-prone when several actions exist. In comparison, the other atk4/ui lines look nice.

I have later discovered this documentation making an exemple of multiple selection in Grid and calling Action. But I was not able to get the full power of it yet.

In my wildest dream, there are two elements that could be improved with Atk4/Ui/Grid:

mvorisek commented 2 years ago

Thank you for the perfect analysis.

To have the ability that $grid->menu->addItem(...) would receive the $grid->addSelection() results in some transparent way (even if not using it)

If you can, please submit a PR with an improvement.

To have a checkbox in the title row that check/unchecks all the column

opened https://github.com/atk4/ui/issues/1877, it is a separate issue

mhuser commented 2 years ago

If you can, please submit a PR with an improvement.

Thank you.

Let's try it. This will enable me to better understand what is going behind the hood and I will be the first to benefits from it.

mhuser commented 2 years ago

I know that the question may sound silly, but once I have cloned atk4-ui master on a development web server, how can I get the demos to work? I guess there is something to do with composer to setup autoload.

mvorisek commented 2 years ago

run composer update in atk4/ui root directory

mhuser commented 2 years ago

Thank you so much !

mhuser commented 2 years ago

OK, I have the running demo and the sqlite db was populated thanks to your composer hint. I have no css styles yet at this stage. This is because the app tries to have stylesheets and others loaded by their full local paths instead of relative path from the server adress. This is forbidden by the server. Seems strange to me to have to modify the demos/init-app.php to force the paths. Is there some magic configuration to do or a doc I could follow without bothering you?

mvorisek commented 2 years ago

in the latest develop version it should be configured automatically in:

https://github.com/atk4/ui/blob/b1b2268862f35c730425fdb410aec5a1406ac2f7/src/App.php#L630-L655

please describe:

mhuser commented 2 years ago

Apache HTTP Server 2.4 and PHP 74 on a local NAS server. atk4-ui is a subdirectory in the http/web directory. Using version up to commit b1b2268862f35c730425fdb410aec5a1406ac2f7 $_SERVER:

USER: http
HOME: /var/services/web
SCRIPT_NAME: /atk4-ui/demos/test.php
REQUEST_URI: /atk4-ui/demos/test.php
QUERY_STRING: 
REQUEST_METHOD: GET
SERVER_PROTOCOL: HTTP/1.1
GATEWAY_INTERFACE: CGI/1.1
REMOTE_PORT: 60394
SCRIPT_FILENAME: /var/services/web/atk4-ui/demos/test.php
SERVER_ADMIN: [no address given]
CONTEXT_DOCUMENT_ROOT: /var/services/web
CONTEXT_PREFIX: 
REQUEST_SCHEME: https
DOCUMENT_ROOT: /var/services/web
REMOTE_ADDR: 2a02:aa17:327f:6380:4463:ac80:9642:44a
SERVER_PORT: 443
SERVER_ADDR: 2a02:aa17:327f:6380:211:32ff:fe80:2943
SERVER_NAME: etml-elo.diskstation.me
SERVER_SOFTWARE: Apache/2.4.43 (Unix)
SERVER_SIGNATURE: 
PATH: /sbin:/bin:/usr/sbin:/usr/bin:/usr/syno/sbin:/usr/syno/bin:/usr/local/sbin:/usr/local/bin
HTTP_SEC_FETCH_USER: ?1
HTTP_SEC_FETCH_SITE: none
HTTP_SEC_FETCH_MODE: navigate
HTTP_SEC_FETCH_DEST: document
HTTP_UPGRADE_INSECURE_REQUESTS: 1
HTTP_COOKIE: stay_login=0; smid=A6w2rEfFtRb89YYpRiAdBaohBHOfLMuH-Ej4D9SMdB3Df7fCpRt8JvaopH7tF8bX0RGcuYMyTjTT7opuYOqSuw
HTTP_ACCEPT_ENCODING: gzip, deflate, br
HTTP_ACCEPT_LANGUAGE: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
HTTP_ACCEPT: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
HTTP_USER_AGENT: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0
HTTP_CONNECTION: close
HTTP_HOST: etml-elo.diskstation.me
HTTP_X_REAL_PORT: 49908
HTTP_X_PORT: 443
HTTP_X_HTTPS: on
HTTP_X_REAL_IP: 2a02:aa17:327f:6380:4463:ac80:9642:44a
HTTP_X_FORWARDED_BY: 2a02:aa17:327f:6380:211:32ff:fe80:2943
proxy-nokeepalive: 1
MOD_X_SENDFILE_ENABLED: yes
HTTPS: on
FCGI_ROLE: RESPONDER
PHP_SELF: /atk4-ui/demos/test.php
REQUEST_TIME_FLOAT: 1666799611.7181
REQUEST_TIME: 1666799611
mvorisek commented 2 years ago

I will take a look.

for now, edit the App cdns here https://github.com/atk4/ui/blob/b1b2268862f35c730425fdb410aec5a1406ac2f7/src/App.php#L147 or anywhere where you create your App

mhuser commented 2 years ago

Thanks to you I am now up and running! I applied this bodge to demos/init-app.php

foreach ($app->cdn as $k => $v) {
    if (str_starts_with($v, '/') && !str_starts_with($v, '//')) {
        $app->cdn[$k] = str_replace('/volume1/web', 'https://etml-elo.diskstation.me', $v);
    }
}
mhuser commented 2 years ago

Draft started in #1920

mvorisek commented 2 years ago

@mhuser before https://github.com/atk4/ui/blob/b1b2268862f35c730425fdb410aec5a1406ac2f7/src/App.php#L641 line I added:

$localDirPath = '/var/services/web';
$subdir = '/atk4-ui';
$_SERVER = [
    'HOME' => $localDirPath,
    'REQUEST_METHOD' => 'GET',
    'SERVER_PROTOCOL' => 'HTTP/1.1',
    'GATEWAY_INTERFACE' => 'CGI/1.1',
    'REMOTE_PORT' => '60394',
    'SCRIPT_NAME' => $subdir . '/demos/test.php',
    'REQUEST_URI' => $subdir . '/demos/test.php',
    'QUERY_STRING' => '',
    'SCRIPT_FILENAME' => $localDirPath . $subdir . '/demos/test.php',
    'CONTEXT_DOCUMENT_ROOT' => $localDirPath,
    'CONTEXT_PREFIX' => '',
    'REQUEST_SCHEME' => 'https',
    'DOCUMENT_ROOT' => $localDirPath,
    'REMOTE_ADDR' => '2a02:aa17:327f:6380:4463:ac80:9642:44a',
    'SERVER_PORT' => '443',
    'SERVER_ADDR' => '2a02:aa17:327f:6380:211:32ff:fe80:2943',
    'SERVER_NAME' => 'etml-elo.diskstation.me',
    'HTTP_HOST' => 'etml-elo.diskstation.me',
    'HTTP_X_REAL_PORT' => '49908',
    'HTTP_X_PORT' => '443',
    'HTTP_X_HTTPS' => 'on',
    'HTTP_X_REAL_IP' => '2a02:aa17:327f:6380:4463:ac80:9642:44a',
    'HTTP_X_FORWARDED_BY' => '2a02:aa17:327f:6380:211:32ff:fe80:2943',
    'HTTPS' => 'on',
    'PHP_SELF' => $subdir . '/demos/test.php',
];

and demos/index.php demos homepage has about this content:

image

navigate to the homepage and please post here:

[1] can be analysed by linux realpath https://serverfault.com/questions/1069147/how-can-i-quickly-test-a-path-in-bash-to-determine-if-any-segment-of-it-is-a-sym

mhuser commented 2 years ago

I think it is my configuration that is the issue (the server is on a Synology NAS setup from their package)

mvorisek commented 2 years ago

Thank you a lot for the data! I will do my best to make it working. As there is no modrewrite, it should be possible :)

mvorisek commented 2 years ago

I was able to reproduce with the following filesystem configuration:

RUN mkdir /mount && mkdir /mount/html && chmod 0777 /mount/html
RUN ln -s /mount/html /var/www/ht # /var/www/html directory cannot be removed as mounted by Docker
WORKDIR /mount/html

and

foreach ($_SERVER as $k => $v) {
    if (is_string($v)) {
        $_SERVER[$k] = preg_replace('~^/mount/html~', '/var/www/ht', $v, 1);
    }
}

at the beginning of the demos/index.php file the output is then like:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Agile UI Demo v4.0-dev</title>
    <meta charset="utf-8">
    <script>'use strict'; window.__atkBundlePublicPath = '/mount/html/public';</script>
    <script src="/mount/html/public/external/jquery/dist/jquery.js"></script>
<script src="/mount/html/public/external/fomantic-ui/dist/semantic.js"></script>
<link rel="stylesheet" type="text/css" href="/mount/html/public/external/fomantic-ui/dist/semantic.css">
...

and when var_dump([$localPath, $requestLocalPath, $res]) is added to https://github.com/atk4/ui/blob/7102df96dc980ef04bc6c770897715bb3efeb25f/src/App.php#L652 it shows:

array(3) {
  [0]=>
  string(46) "/mount/html/src/../public/external/jquery/dist"
  [1]=>
  string(27) "/var/www/ht/demos/index.php"
  [2]=>
  string(40) "/mount/html/public/external/jquery/dist/"
}

more debug:

        $localPath = realpath($localPath);
        $localPathRelative = $fs->makePathRelative($localPath, dirname($requestLocalPath));
        var_dump([$localPath, dirname($requestLocalPath), $localPathRelative]);
        $res = '/' . $fs->makePathRelative($requestUrlPath . '/' . $localPathRelative, '/');
        var_dump([$requestUrlPath . '/' . $localPathRelative, $res]);

shows

array(3) {
  [0]=>
  string(18) "/mount/html/public"
  [1]=>
  string(17) "/var/www/ht/demos"
  [2]=>
  string(30) "../../../../mount/html/public/"
}
array(2) {
  [0]=>
  string(37) "/demos/../../../../mount/html/public/"
  [1]=>
  string(19) "/mount/html/public/"
}

array(3) {
  [0]=>
  string(39) "/mount/html/public/external/jquery/dist"
  [1]=>
  string(17) "/var/www/ht/demos"
  [2]=>
  string(51) "../../../../mount/html/public/external/jquery/dist/"
}
array(2) {
  [0]=>
  string(58) "/demos/../../../../mount/html/public/external/jquery/dist/"
  [1]=>
  string(40) "/mount/html/public/external/jquery/dist/"
}
mhuser commented 2 years ago

Looks similar to the issue I faced !

mvorisek commented 2 years ago

please test if adding:

        $localPath = realpath($localPath);
        $requestLocalPath = realpath($requestLocalPath);

at https://github.com/atk4/ui/blob/7102df96dc980ef04bc6c770897715bb3efeb25f/src/App.php#L651 fixes the path calculation, eg. fixes https://github.com/atk4/ui/issues/1727#issuecomment-1295960552

mhuser commented 2 years ago

Yes, it fixes for my situation. With these lines, I do not need to tweak the cdns any more! image

mvorisek commented 2 years ago

@mhuser thank you for reporting the problem and helping me to understand it, fix merged in https://github.com/atk4/ui/pull/1936, I was litte worried about realpath performance with many calls, but it turned out __DIR__ is resolved implicitly, and all inputs should be constructed using it, thus with this knowledge/assumptiom, the fix requires only one realpath per app, ❤️

mhuser commented 2 years ago

@mvorisek This sounds great! Thank you a lot for the quick fix!