FriendsOfREDAXO / tricks

Tipps und Tricks rund um REDAXO 5
https://friendsofredaxo.github.io/tricks/
MIT License
105 stars 33 forks source link

YForm: Datensatz klonen #309

Closed christophboecker closed 1 year ago

christophboecker commented 1 year ago

Ziel: einen Datensatz per Button in der Liste aufrufen und beim Speichern als neuen Satz ablegen.

Der Lösungsweg:

Speichert man das Formular ab, wird es wie ein neuer Datensatz behandelt. Bendet man ohne zu speichern gibt es keine Reste in der Tabelle.

Wir packen alle Komponenten in einen eigenen rex_yform_manager_dataset:

class Dataset extends rex_yform_manager_dataset
{
    /**
     * Anpassungen für das Clone-Formular ausführen.
     * - Das Clone-Formular erhält eine erweiterte Callback-Funktion, die
     *   unmittelbar vor der Anzeige aus Edit Add macht
     * - Titeltext ändern (geht leider nur so).
     */
    public function executeForm(rex_yform $yform, callable $afterFieldsExecuted = null): string
    {
        if (1 === rex_request('clone', 'integer', 0)) {
            $callback = $afterFieldsExecuted;
            $afterFieldsExecuted = static function (rex_yform $yform) use ($callback) {
                self::changeEditToAdd($yform);
                if (is_callable($callback)) {
                    $callback($yform);
                }
            };
            rex_i18n::addMsg('yform_editdata', 'Datensatz klonen [Original: {0}]');
        }

        return parent::executeForm($yform, $afterFieldsExecuted);
    }

    /**
     * Fügt hinter der Edit-Spalte eine Kopie der Editspalte ein.
     * Unterschiede:
     * - Clone-Icon
     * - kein + in der Kopfspalte
     * - Zusätzlich clone=1 als Parameter
     */
    public static function YFORM_DATA_LIST(rex_extension_point $ep)
    {
        // nur ausführen für diese Tabelle
        $tableClass = self::getModelClass($ep->getParam('table')->getTableName());
        if( $tableClass !== static::class) {
            return;
        }

        /** @var rex_yform_list $list */
        $list = $ep->getSubject();

        $position = 1;
        $name = $list->getColumnName(0);
        if (str_contains($name, ' href="index.php?func=add')) {
            $list->addColumn('clone', '<i class="fa fa-clone"></i>', $position);
            $list->setColumnLayout('clone', ['<th></th>', '<td class="rex-table-icon">###VALUE###</td>']);
            $params = $list->getColumnParams($name);
            $params['clone'] = 1;
            $list->setColumnParams('clone', $params);
        }
    }

    /**
     * Ändert ein EDIT-Formular auf ADD.
     *
     * Die Daten bleiben erhalten, aber alle Datensatz-Referenzen werden
     * entfernt etc.
     * 
     * Auch Inline-Formulare von be_manager_relation/Typ5 werden umgebaut,
     * nicht aber Inline-Formulate in Inline-Formularen.
     */
    public static function changeEditToAdd(rex_yform $yform): void
    {
        // Für das Formular an sich: Auf "Add" umschalten
        $yform->objparams['form_hiddenfields']['func'] = 'add';
        unset($yform->objparams['form_hiddenfields']['data_id']);

        // In den Feldern Anpassungen vornehmen
        foreach ($yform->objparams['values'] as $k => $v) {
            // Submit-Buttons von "Edit" auf "Add" zurückstellen
            if ($v instanceof rex_yform_value_submit) {
                $yform->objparams['form_output'][$k] = str_replace(
                    [rex_i18n::msg('yform_save').'</button', rex_i18n::msg('yform_save_apply').'</button'],
                    [rex_i18n::msg('yform_add').'</button', rex_i18n::msg('yform_add_apply').'</button'],
                    $yform->objparams['form_output'][$k]
                );
                continue;
            }

            // im Feldtyp be_manager_relation / Typ 5 (inline) ebenfalls die Datensatz-ID der
            // verbundenen Sätze entfernen. Nur "inline" ist problematisch
            if ($v instanceof rex_yform_value_be_manager_relation && 5 == $v->getElement('type')) {
                $fieldName = preg_quote($v->getFieldName());
                $pattern = '/<input type="hidden" name="'.$fieldName.'(\[\d+\])*\[id\]" value="\d+" \/>/';
                $yform->objparams['form_output'][$k] = preg_replace($pattern, '', $yform->objparams['form_output'][$k]);
            }
        }
    }
}

Die Tabelle wird mit der eigenen Dataset-Klasse verbunden:

rex_yform_manager_dataset::setModelClass('rex_meine_tabelle', Dataset::class);

Und der Klon-Button wird per EP in die Tabelle eingefügt:

rex_extension::register('YFORM_DATA_LIST', 'Dataset::YFORM_DATA_LIST');
grafik
skerbis commented 1 year ago

Würde das auch das Kopieren von Datensätzen mit Relationen ermöglichen?

christophboecker commented 1 year ago

Ich bin da sehr zuversichtlich. Bei 1-n inline nutze ich es intensiv ohne Probleme. Bei den normalen Varianten 1 bis 4 (select/popup) sollte es für kein 1-n-Relationeneh kein Problem geben, denn die haben ja keine Relationen-Tabelle. Bei n-m-Relationen via Relationentabelle bin ich mir nicht sicher; ich meine es mal ausprobiert zu haben. Vermutung: ja

skerbis commented 1 year ago

Wenn ich das so eingebe wie von Dir hier beschrieben bekomme ich:

Too few arguments to function Dataset::YFORM_DATA_LIST(), 0 passed in …and exactly 1 expected

Das passiert wenn ich rex_extension::register('rex_extension_point', Dataset::YFORM_DATA_LIST()); einsetze.

tbaddade commented 1 year ago

Es gibt bereits 2 Tricks in diese Richtung. Vielleicht machen wir aus diesen drei einen Trick mit allen möglichen Varianten oder besser noch einen PR für YForm selbst?

christophboecker commented 1 year ago

Wenn ich das so eingebe wie von Dir hier beschrieben bekomme ich: ...

Danke für den Hinweis, hab es geändert.

rex_extension::register('YFORM_DATA_LIST', 'Dataset::YFORM_DATA_LIST');
skerbis commented 1 year ago

GETESTET UND SUPER GEIL. @tbaddade ich würde diese Lösung favorisieren. 1-n Relationen wurden übernommen. Klasse. @christophboecker PR für YForm wäre super und das Klonen bitte in das Dropdown.

Habe gleichzeitig das Yform_usability angehabt. Dabei verschwand mal das Prio-Feld oder plötzlich gab es wieder den Bleistift zu sehen.

olien commented 1 year ago

Super coooll! DANKE!

xong commented 1 year ago

Ich hab einen leicht anderen Ansatz über einen Trait gewählt. Das hat den Vorteil, dass man den Trait einfach bei den Model-Klassen hinzufügen kann und keine abgeleiteten Klassen braucht.

<?php

namespace Project\Trait;

use rex_i18n
    , rex_yform
    , rex_yform_value_submit
    , rex_yform_value_be_manager_relation;

trait Clonable
{
    /**
     * @param null|callable(rex_yform):void $afterFieldsExecuted
     */
    public
    function executeForm(rex_yform $yform, callable $afterFieldsExecuted = null): string
    {
        // clone angefordert? Wenn nein: normale Bearbeitung
        if (1 !== rex_request('clone', 'integer', 0)) {
            return parent::executeForm($yform, $afterFieldsExecuted);
        }

        // clone angefordert! afterFieldsExecuted wird durch ein eigenes Callback ersetzt,

        $callback = function (rex_yform $yform) use ($afterFieldsExecuted) {
            // Titelzeile frisieren: mangels EP wird die i18n-Tabelle geändert.
            rex_i18n::addMsg('yform_editdata', 'Datensatz klonen [Original: {0}]');

            // Für das Formular an sich: Auf "Add" umschalten, indem func auf "add" gesetzt und
            // die Datensatznummer entfernt wird.
            $yform->objparams['form_hiddenfields']['func'] = 'add';
            unset($yform->objparams['form_hiddenfields']['data_id']);

            // Änderungen in den Values: jeweils den vorgenerierten HTML-Code ändern
            foreach ($yform->objparams['values'] as $k => $v) {
                // Submit-Buttons von "Edit" auf "Add" zurückstellen
                if ($v instanceof rex_yform_value_submit) {
                    $yform->objparams['form_output'][$k] = str_replace(
                        [rex_i18n::msg('yform_save') . '</button', rex_i18n::msg('yform_save_apply') . '</button'],
                        [rex_i18n::msg('yform_add') . '</button', rex_i18n::msg('yform_add_apply') . '</button'],
                        $yform->objparams['form_output'][$k]
                    );
                    continue;
                }

                // im Feldtyp be_manager_relation / Typ 5 (=inline) ebenfalls die hidden-inputs mit
                // der Datensatz-ID der verbundenen Sätze entfernen
                // Nur "inline" ist problematisch;
                if ($v instanceof rex_yform_value_be_manager_relation && 5 == $v->getElement('type')) {
                    $fieldName                           = preg_quote($v->getFieldName());
                    $pattern                             = '/<input type="hidden" name="' . $fieldName . '(\[\d+\])*\[id\]" value="\d+" \/>/';
                    $yform->objparams['form_output'][$k] = preg_replace($pattern, '', $yform->objparams['form_output'][$k]);
                }
            }

            call_user_func($afterFieldsExecuted, $yform);
        };

        return parent::executeForm($yform, $callback);
    }
}

project/boot.php

\rex_extension::register('YFORM_DATA_LIST', function (\rex_extension_point $ep) {
    /** @var \rex_list $list */
    $list = $ep->getSubject();

    if (rex_get('rex_yform_manager_opener', 'bool')) {
        return $list;
    }

    // clone link
    if ($className = \rex_yform_manager_dataset::getModelClass($ep->getParam('table')->getTableName())
        and in_array(Clonable::class, class_uses($className))) {
        $name = $list->getColumnNames()[0];
        if (str_contains($name, 'href="index.php?func=add')) {
            $list->addColumn('clone', '<i class="rex-icon rex-icon-duplicate"></i>', 1);
            $list->setColumnLayout('clone', ['<th></th>', '<td class="rex-table-icon">###VALUE###</td>']);
            $params          = $list->getColumnParams($name);
            $params['clone'] = 1;
            $list->setColumnParams('clone', $params);
        }
    }

    return $list;
});

Model-Klasse

<?php
namespace Project\Model;

use Project\Trait\Clonable;

class ModelClass extends \rex_yform_manager_dataset
{
    use Clonable;
}
christophboecker commented 1 year ago

@xong Was passiert denn, wenn ich in der Klasse ModelClass die Methode executeForm erweitern will? Kann man das; ich dachte bisher, dass das zu Konflikten führt?

xong commented 1 year ago

Dann müsstest du den Trait anders einbinden:

use Clonable {
    executeForm as cloneExecuteForm;
}

public function executeForm(rex_yform $yform, callable $afterFieldsExecuted = null): string
{
    return $this->cloneExecuteForm($yform, $afterFieldsExecuted);
}

Hab ich aber nicht getestet. Wollte hier nur mal meinen Weg darstellen.

christophboecker commented 1 year ago

Es gibt bereits 2 Tricks in diese Richtung.

@tbaddade Hast natürlich recht. Und einer der beiden ist ziemlich genau diese Lösung - kein Wunder, ist ja von mir. Autsch. Ich hab auch sofort das New-Trick-Label entfernt.

skerbis commented 1 year ago

Aber hier hast du es einfacher erklärt @christophboecker :-)

tbaddade commented 1 year ago

Was wir vermeiden sollten, dass zum Schluss vier Tricks auf der Seite sind und keiner weiß welchen er nehmen soll.

christophboecker commented 1 year ago

PR für YForm wäre super

Wenn das so einfach wäre, ich hab´s mal probiert, aber das ist mir insgesamt zu heikel.

das Klonen bitte in das Dropdown.

In der neuen Version #311 ist es so gebaut, dass man beide Varianten wählen kann. Al Gusto

Habe gleichzeitig das Yform_usability angehabt. Dabei verschwand mal das Prio-Feld oder plötzlich gab es wieder den Bleistift zu sehen.

Hm, könnte mit fest verdrahteten Spaltennummer zu tun haben (0 = Edit, 1 = Clone). Baue ich aber nix für ein