From 2e73698915da09a47abd4bc4c61520e4eee848ad Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Sun, 10 Apr 2022 18:01:42 +0200 Subject: [PATCH] TeamForm: Switch to contributte/forms-multiplier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `kdyby/replicator` served us well for a long time but unfortunately, it has been unmaintained for a while now. Let’s switch to an actively maintained library. Multiplier manages adding and removing for us and handles default values correctly. Currently, we cannot control per-category person limits at submission. --- app/Components/TeamForm.php | 21 ----- app/Config/common.neon | 2 +- app/Forms/TeamFormFactory.php | 70 ++++++++------- app/Presenters/TeamPresenter.php | 90 +++++++++----------- composer.json | 2 +- composer.lock | 141 +++++++++++++++---------------- utils/phpstan.neon | 2 +- 7 files changed, 145 insertions(+), 183 deletions(-) diff --git a/app/Components/TeamForm.php b/app/Components/TeamForm.php index fb00d6a..b238a49 100644 --- a/app/Components/TeamForm.php +++ b/app/Components/TeamForm.php @@ -12,7 +12,6 @@ use Contributte\Translation\Wrappers\NotTranslate; use Nette\Application\UI; use Nette\Forms\Container; -use Nette\Forms\Controls; use Nette\Utils\Json; /** @@ -30,26 +29,6 @@ public function __construct( parent::__construct(); } - public function onRender(): void { - /** @var \Kdyby\Replicator\Container */ - $persons = $this['persons']; - $count = iterator_count($persons->getContainers()); - $minMembers = $this->entries->minMembers; - $maxMembers = $this->entries->maxMembers; - - if ($count >= $maxMembers) { - /** @var Controls\SubmitButton */ - $add = $this['add']; - $add->setDisabled(); - } - - if ($count <= $minMembers) { - /** @var Controls\SubmitButton */ - $remove = $this['remove']; - $remove->setDisabled(); - } - } - public function addCustomFields(array $fields, Container $container): void { foreach ($fields as $field) { $name = $field->name; diff --git a/app/Config/common.neon b/app/Config/common.neon index b2a0d18..c410246 100644 --- a/app/Config/common.neon +++ b/app/Config/common.neon @@ -42,7 +42,7 @@ extensions: translation: Contributte\Translation\DI\TranslationExtension orm: Nextras\Orm\Bridges\NetteDI\OrmExtension dbal: Nextras\Dbal\Bridges\NetteDI\DbalExtension - replicator: Kdyby\Replicator\DI\ReplicatorExtension + multiplier: Contributte\FormMultiplier\DI\MultiplierExtension contribMail: Contributte\Mail\DI\MailExtension contribMail: diff --git a/app/Forms/TeamFormFactory.php b/app/Forms/TeamFormFactory.php index 7044583..290b0ff 100644 --- a/app/Forms/TeamFormFactory.php +++ b/app/Forms/TeamFormFactory.php @@ -6,13 +6,12 @@ use App\Components\CategoryEntry; use App\Components\TeamForm; -use App\Helpers\Iter; use App\Model\Configuration\Entries; +use Contributte\FormMultiplier\Multiplier; use Contributte\Translation\Wrappers\Message; -use Kdyby\Replicator\Container as ReplicatorContainer; use Nette; use Nette\Forms\Container; -use Nette\Forms\Controls\SubmitButton; +use Nette\Forms\Form; use Nette\Localization\Translator; use Nextras\FormComponents\Controls\DateControl; use Nextras\FormsRendering\Renderers\Bs5FormRenderer; @@ -54,6 +53,13 @@ public function create( // Handled in TeamPresenter::renderCreate. $initialMembers = $this->entries->minMembers; + // Group for top submit button, since DefaultFormRenderer renders group before all ungrouped Controls. + $group = $form->addGroup(); + $group->setOption('container', 'div aria-hidden="true" class="visually-hidden"'); + // Browsers consider the first submit button a default submit button for use when submitting the form using Enter key. + // Let’s add the save button to the top, to prevent the remove button of the first container from being picked. + $form->addSubmit('save_default_submit', 'messages.team.action.register')->getControlPrototype()->setHtmlAttribute('aria-hidden', 'true')->setHtmlAttribute('tabindex', '-1'); + $form->addProtection(); $form->addGroup('messages.team.info.label'); $form->addText('name', 'messages.team.name.label')->setRequired(); @@ -77,20 +83,20 @@ public function create( $rule->addRule(function(CategoryEntry $entry) use ($defaultMaxMembers): bool { $category = $entry->getValue(); $maxMembers = $this->entries->categories->allCategories[$category]->maxMembers ?? $defaultMaxMembers; - /** @var ReplicatorContainer */ - $replicator = $entry->form['persons']; + /** @var Multiplier */ // For PHPStan. + $multiplier = $entry->form['persons']; - return iterator_count($replicator->getContainers()) <= $maxMembers; + return $multiplier->getCopyNumber() <= $maxMembers; }, 'messages.team.error.too_many_members_simple'); // TODO: add params like in add/remove buttons $rule = $category->addCondition(true); // not to block the export of rules to JS $rule->addRule(function(CategoryEntry $entry) use ($defaultMinMembers): bool { $category = $entry->getValue(); $minMembers = $this->entries->categories->allCategories[$category]->minMembers ?? $defaultMinMembers; - /** @var ReplicatorContainer */ - $replicator = $entry->form['persons']; + /** @var Multiplier */ // For PHPStan. + $multiplier = $entry->form['persons']; - return iterator_count($replicator->getContainers()) >= $minMembers; + return $multiplier->getCopyNumber() >= $minMembers; }, 'messages.team.error.too_few_members_simple'); $fields = $this->entries->teamFields; @@ -99,36 +105,11 @@ public function create( $form->addTextArea('message', 'messages.team.message.label'); $form->setCurrentGroup(); - $form->addSubmit('save', $isEditing ? 'messages.team.action.edit' : 'messages.team.action.register'); - $form->addSubmit('add', 'messages.team.action.add')->setValidationScope([])->onClick[] = function(SubmitButton $button) use ($defaultMaxMembers): void { - $category = $button->form->getUnsafeValues(null)['category']; - $maxMembers = $this->entries->categories->allCategories[$category]->maxMembers ?? $defaultMaxMembers; - /** @var ReplicatorContainer */ - $replicator = $button->form['persons']; - if (iterator_count($replicator->getContainers()) < $maxMembers) { - $replicator->createOne(); - } else { - $button->form->addError($this->translator->translate('messages.team.error.too_many_members', $maxMembers, ['category' => $category]), false); - } - }; - $form->addSubmit('remove', 'messages.team.action.remove')->setValidationScope([])->onClick[] = function(SubmitButton $button) use ($defaultMinMembers): void { - $category = $button->form->getUnsafeValues(null)['category']; - $minMembers = $this->entries->categories->allCategories[$category]->minMembers ?? $defaultMinMembers; - /** @var ReplicatorContainer */ // For PHPStan. - $replicator = $button->form['persons']; - if (iterator_count($replicator->getContainers()) > $minMembers) { - $lastPerson = Iter::last($replicator->getContainers()); - if ($lastPerson !== null) { - $replicator->remove($lastPerson, true); - } - } else { - $button->form->addError($this->translator->translate('messages.team.error.too_few_members', $minMembers, ['category' => $category]), false); - } - }; + $renderer->primaryButton = $form->addSubmit('save', $isEditing ? 'messages.team.action.edit' : 'messages.team.action.register'); $fields = $this->entries->personFields; $i = 0; - $form->addDynamic('persons', function(Container $container) use (&$i, $fields, $form): void { + $multiplier = $form->addMultiplier('persons', function(Container $container, TeamForm $form) use (&$i, $fields): void { ++$i; $group = $form->addGroup(); $group->setOption('label', new Message('messages.team.person.label', $i)); @@ -151,7 +132,22 @@ public function create( $email->setRequired(); $group->setOption('description', 'messages.team.person.isContact'); } - }, $initialMembers, true); + }, $initialMembers, $defaultMaxMembers); + $multiplier->onCreateComponents[] = function(Multiplier $multiplier) use ($form, $defaultMaxMembers): void { + if (!$form->isSubmitted()) { + return; + } + + $category = $form->getUnsafeValues(null)['category']; + $maxMembers = $this->entries->categories->allCategories[$category]->maxMembers ?? $defaultMaxMembers; + $count = iterator_count($multiplier->getContainers()); + if ($count >= $maxMembers) { + $form->addError($this->translator->translate('messages.team.error.too_many_members', $maxMembers, ['category' => $category]), false); + } + }; + $multiplier->setMinCopies($defaultMinMembers); + $multiplier->addCreateButton('messages.team.action.add')->setNoValidate(); + $multiplier->addRemoveButton('messages.team.action.remove'); return $form; } diff --git a/app/Presenters/TeamPresenter.php b/app/Presenters/TeamPresenter.php index 56515b8..9e52c09 100644 --- a/app/Presenters/TeamPresenter.php +++ b/app/Presenters/TeamPresenter.php @@ -13,9 +13,9 @@ use App\Model\Orm\Invoice\Invoice; use App\Model\Orm\ItemReservation\ItemReservation; use App\Model\Orm\Team\Team; +use Contributte\FormMultiplier\Multiplier; use DateTimeImmutable; use Exception; -use Kdyby\Replicator\Container as ReplicatorContainer; use Latte; use Nette; use Nette\Application\ForbiddenRequestException; @@ -122,10 +122,10 @@ public function renderRegister(): void { if (!$form->isSubmitted()) { // Create sufficient number of person subforms for the most common team size (when it is greater than minimum team size). $remainingMembers = $this->entries->initialMembers - $this->entries->minMembers; - /** @var ReplicatorContainer */ - $replicator = $form['persons']; + /** @var Multiplier */ // For PHPStan. + $multiplier = $form['persons']; for ($i = $remainingMembers; $i > 0; --$i) { - $replicator->createOne(); + $multiplier->addCopy(); } } } @@ -155,50 +155,48 @@ public function renderEdit(int $id = null): void { } $form = $this->getComponent('teamForm'); - if (!$form->isSubmitted()) { - $default = []; - $default['name'] = $team->name; - $default['category'] = $team->category; - $default['message'] = $team->message; - $default['persons'] = []; - - $fields = $this->entries->teamFields; + $default = []; + $default['name'] = $team->name; + $default['category'] = $team->category; + $default['message'] = $team->message; + $default['persons'] = []; + + $fields = $this->entries->teamFields; + foreach ($fields as $field) { + $name = $field->name; + if (isset($team->getJsonData()->$name)) { + $default[$name] = $team->getJsonData()->$name; + } elseif ($field instanceof Fields\SportidentField) { + $default[$name] = [ + SportidentControl::NAME_NEEDED => true, + ]; + } + } + + $fields = $this->entries->personFields; + foreach ($team->persons as $person) { + $personDefault = [ + 'firstname' => $person->firstname, + 'lastname' => $person->lastname, + 'gender' => $person->gender, + 'email' => $person->email, + 'birth' => $person->birth, + ]; + foreach ($fields as $field) { $name = $field->name; - if (isset($team->getJsonData()->$name)) { - $default[$name] = $team->getJsonData()->$name; + if (isset($person->getJsonData()->$name)) { + $personDefault[$name] = $person->getJsonData()->$name; } elseif ($field instanceof Fields\SportidentField) { - $default[$name] = [ + $personDefault[$name] = [ SportidentControl::NAME_NEEDED => true, ]; } } - $fields = $this->entries->personFields; - foreach ($team->persons as $person) { - $personDefault = [ - 'firstname' => $person->firstname, - 'lastname' => $person->lastname, - 'gender' => $person->gender, - 'email' => $person->email, - 'birth' => $person->birth, - ]; - - foreach ($fields as $field) { - $name = $field->name; - if (isset($person->getJsonData()->$name)) { - $personDefault[$name] = $person->getJsonData()->$name; - } elseif ($field instanceof Fields\SportidentField) { - $personDefault[$name] = [ - SportidentControl::NAME_NEEDED => true, - ]; - } - } - - $default['persons'][] = $personDefault; - } - $form->setValues($default); + $default['persons'][] = $personDefault; } + $form->setDefaults($default); } } @@ -296,14 +294,12 @@ protected function createComponentTeamForm(): Form { isEditing: $isEditing, ); - /** @var \Nette\Forms\Controls\SubmitButton */ - $save = $form['save']; - $save->onClick[] = $this->processTeamForm(...); + $form->onSuccess[] = $this->processTeamForm(...); return $form; } - private function processTeamForm(Nette\Forms\Controls\SubmitButton $button): void { + private function processTeamForm(App\Components\TeamForm $form): void { $today = new DateTimeImmutable(); if (!$this->user->isInRole('admin')) { if ($this->entries->closing !== null && $this->entries->closing < $today) { @@ -313,8 +309,6 @@ private function processTeamForm(Nette\Forms\Controls\SubmitButton $button): voi } } - /** @var App\Components\TeamForm $form */ - $form = $button->form; /** @var array */ // actually \ArrayAccess but PHPStan does not handle that very well. $values = $form->getValues(); /** @var string $password */ @@ -485,9 +479,9 @@ private function processTeamForm(Nette\Forms\Controls\SubmitButton $button): voi /** @var ?string $firstMemberName */ $firstMemberName = null; - /** @var ReplicatorContainer */ - $replicator = $form['persons']; - $personContainers = iterator_to_array($replicator->getContainers()); + /** @var Multiplier */ // For PHPStan. + $multiplier = $form['persons']; + $personContainers = iterator_to_array($multiplier->getContainers()); foreach ($values['persons'] as $personKey => $member) { $personContainer = $personContainers[$personKey]; $firstname = $member['firstname']; diff --git a/composer.json b/composer.json index 546f2d1..d9aee5b 100644 --- a/composer.json +++ b/composer.json @@ -12,9 +12,9 @@ ], "require": { "php": ">= 8.1", + "contributte/forms-multiplier": "^3.3", "contributte/mail": "^0.6.0", "contributte/translation": "^2.0", - "kdyby/forms-replicator": "^2.0.0", "latte/latte": "~2.5", "moneyphp/money": "^4.0", "nette/application": "~3.0", diff --git a/composer.lock b/composer.lock index be82155..9111d90 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,74 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a73afe89d6375cd457980c51689578c2", + "content-hash": "827f792d6a2d4cde712b3c735f5b6241", "packages": [ + { + "name": "contributte/forms-multiplier", + "version": "v3.3.1", + "source": { + "type": "git", + "url": "https://github.com/contributte/forms-multiplier.git", + "reference": "80e15b6515a372c46e564decc7608f2f548d42d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/contributte/forms-multiplier/zipball/80e15b6515a372c46e564decc7608f2f548d42d6", + "reference": "80e15b6515a372c46e564decc7608f2f548d42d6", + "shasum": "" + }, + "require": { + "nette/forms": "^3.1.0", + "php": ">=7.2" + }, + "require-dev": { + "codeception/codeception": "^4.0.0", + "codeception/module-asserts": "^1.3", + "codeception/module-phpbrowser": "^1.0", + "latte/latte": "^3.0.0", + "nette/application": "^3.0.0", + "nette/di": "^3.0.0", + "ninjify/qa": "^0.12", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-nette": "^1.0", + "webchemistry/testing-helpers": "~2.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Contributte\\FormMultiplier\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "description": "Multiplier for nette forms", + "keywords": [ + "Forms", + "contributte", + "multiplier", + "nette" + ], + "support": { + "issues": "https://github.com/contributte/forms-multiplier/issues", + "source": "https://github.com/contributte/forms-multiplier/tree/v3.3.1" + }, + "funding": [ + { + "url": "https://contributte.org/partners.html", + "type": "custom" + }, + { + "url": "https://github.com/f3l1x", + "type": "github" + } + ], + "time": "2022-12-03T18:36:23+00:00" + }, { "name": "contributte/mail", "version": "v0.6.0", @@ -159,79 +225,6 @@ ], "time": "2022-11-25T10:43:13+00:00" }, - { - "name": "kdyby/forms-replicator", - "version": "v2.0.0", - "source": { - "type": "git", - "url": "https://github.com/Kdyby/FormsReplicator.git", - "reference": "cd6c9ccfaade43b85d181a8041ec5a7f842969b1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Kdyby/FormsReplicator/zipball/cd6c9ccfaade43b85d181a8041ec5a7f842969b1", - "reference": "cd6c9ccfaade43b85d181a8041ec5a7f842969b1", - "shasum": "" - }, - "require": { - "nette/forms": "^3.0", - "nette/utils": "^3.0", - "php": ">=7.1" - }, - "require-dev": { - "nette/application": "^3.0@rc", - "nette/bootstrap": "^3.0@rc", - "nette/di": "^3.0@rc", - "nette/tester": "^2.2", - "tracy/tracy": "^2.6" - }, - "suggest": { - "nette/di": "to use ReplicatorExtension[CompilerExtension]" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "psr-4": { - "Kdyby\\Replicator\\": "src/Replicator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause", - "GPL-2.0", - "GPL-3.0" - ], - "authors": [ - { - "name": "Filip Procházka", - "email": "filip@prochazka.su", - "homepage": "http://filip-prochazka.com" - }, - { - "name": "David Šolc", - "email": "solcik@gmail.com", - "homepage": "https://solc.dev" - } - ], - "description": "Nette forms container replicator aka addDynamic", - "homepage": "http://kdyby.org", - "keywords": [ - "Forms", - "addDynamic", - "kdyby", - "nette", - "replicator" - ], - "support": { - "issues": "https://github.com/Kdyby/FormsReplicator/issues", - "source": "https://github.com/Kdyby/FormsReplicator/tree/master" - }, - "time": "2019-03-18T16:16:28+00:00" - }, { "name": "latte/latte", "version": "v2.11.6", diff --git a/utils/phpstan.neon b/utils/phpstan.neon index 9b4ab9a..9eb1cc2 100644 --- a/utils/phpstan.neon +++ b/utils/phpstan.neon @@ -12,7 +12,7 @@ parameters: treatPhpDocTypesAsCertain: false checkGenericClassInNonGenericObjectType: false ignoreErrors: - - '(Call to an undefined method App\\Components\\TeamForm::addDynamic\(\))' + - '(Call to an undefined method App\\Components\\TeamForm::addMultiplier\(\))' # https://github.com/phpstan/phpstan/issues/6402 - '(Readonly property App\\Model\\Configuration\\CategoryData::\$allCategories is already assigned.)' - '(Method App\\Model\\Configuration\\Helpers::ensureSubclassOf\(\) should return class-string\|null but returns class-string.)'