diff --git a/app/components/TeamForm.php b/app/components/TeamForm.php index b184771..09a95dc 100644 --- a/app/components/TeamForm.php +++ b/app/components/TeamForm.php @@ -6,7 +6,6 @@ use Contributte\Translation\Wrappers\NotTranslate; use Nette\Application\UI; -use Nette\ComponentModel\IContainer; use Nette\Forms\Container; use Nette\Forms\Controls; use Nette\Utils\Json; @@ -24,33 +23,13 @@ class TeamForm extends UI\Form { /** @var string */ private $locale; - public function __construct(array $countries, array $parameters, string $locale, IContainer $parent = null, string $name = null) { - parent::__construct($parent, $name); + public function __construct(array $countries, array $parameters, string $locale) { + parent::__construct(); $this->countries = $countries; $this->parameters = $parameters; $this->locale = $locale; } - public function onRender(): void { - /** @var \Kdyby\Replicator\Container */ - $persons = $this['persons']; - $count = iterator_count($persons->getContainers()); - $minMembers = $this->parameters['minMembers']; - $maxMembers = $this->parameters['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 { $locale = $this->locale; diff --git a/app/config/CustomInputModifier.php b/app/config/CustomInputModifier.php index 3f385fd..bc93c10 100644 --- a/app/config/CustomInputModifier.php +++ b/app/config/CustomInputModifier.php @@ -16,14 +16,16 @@ class CustomInputModifier { public static function modify(Control $input, IContainer $container): void { // we also have some inputs that are based on Nextras\FormComponents\Fragments\UIComponent\BaseControl if ($input instanceof BaseControl && $input->getName() === 'registry_address') { - $pairId = 'pair-' . $input->htmlId; - $input->setOption('id', $pairId); + $input->monitor(Form::class, function(Form $form) use ($input, $container): void { + $pairId = 'pair-' . $input->htmlId; + $input->setOption('id', $pairId); - /** @var BaseControl */ - $country = $container->getComponent('country'); - $country->addCondition(Form::NOT_EQUAL, 46)->toggle($pairId, false); - $input->setRequired(false); - $input->addConditionOn($country, Form::EQUAL, 46)->setRequired(); + /** @var BaseControl */ + $country = $container->getComponent('country'); + $country->addCondition(Form::NOT_EQUAL, 46)->toggle($pairId, false); + $input->setRequired(false); + $input->addConditionOn($country, Form::EQUAL, 46)->setRequired(); + }); } } } diff --git a/app/config/config.neon b/app/config/config.neon index 30b73ec..00b4936 100644 --- a/app/config/config.neon +++ b/app/config/config.neon @@ -38,7 +38,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 3ab140d..4cfc967 100644 --- a/app/forms/TeamFormFactory.php +++ b/app/forms/TeamFormFactory.php @@ -7,16 +7,14 @@ use App\Components\CategoryEntry; use App\Components\TeamForm; use App\Model\CategoryData; +use Contributte\FormMultiplier\Multiplier; use Contributte\Translation\Wrappers\Message; -use Kdyby\Replicator\Container as ReplicatorContainer; use Nette; -use Nette\ComponentModel\IContainer; 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; -use function nspl\a\last; final class TeamFormFactory { use Nette\SmartObject; @@ -36,8 +34,8 @@ public function __construct(CategoryData $categories, Nette\DI\Container $contex $this->translator = $translator; } - public function create(array $countries, string $locale, bool $editing = false, IContainer $parent = null, string $name = null): TeamForm { - $form = new TeamForm($countries, $this->parameters, $locale, $parent, $name); + public function create(array $countries, string $locale, bool $editing = false): TeamForm { + $form = new TeamForm($countries, $this->parameters, $locale); $form->setTranslator($this->translator); $renderer = new Bs5FormRenderer(); @@ -47,7 +45,14 @@ public function create(array $countries, string $locale, bool $editing = false, $defaultMinMembers = $this->parameters['minMembers']; $defaultMaxMembers = $this->parameters['maxMembers']; - $initialMembers = $form->isSubmitted() || $editing ? $defaultMinMembers : ($this->parameters['initialMembers'] ?? $defaultMinMembers); + $initialMembers = $this->parameters['initialMembers'] ?? $defaultMinMembers; + + // 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'); @@ -69,20 +74,20 @@ public function create(array $countries, string $locale, bool $editing = false, $rule->addRule(function(CategoryEntry $entry) use ($defaultMaxMembers): bool { $category = $entry->getValue(); $maxMembers = $this->categories->getCategoryData()[$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->categories->getCategoryData()[$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->parameters['fields']['team']; @@ -91,37 +96,11 @@ public function create(array $countries, string $locale, bool $editing = false, $form->addTextArea('message', 'messages.team.message.label'); $form->setCurrentGroup(); - $form->addSubmit('save', '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->categories->getCategoryData()[$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->categories->getCategoryData()[$category]['minMembers'] ?? $defaultMinMembers; - /** @var ReplicatorContainer */ // For PHPStan. - $replicator = $button->form['persons']; - if (iterator_count($replicator->getContainers()) > $minMembers) { - /** @var ?Container */ // For PHPStan. - $lastPerson = last($replicator->getContainers()); - if ($lastPerson) { - $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', 'messages.team.action.register'); $fields = $this->parameters['fields']['person']; $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)); @@ -144,7 +123,10 @@ public function create(array $countries, string $locale, bool $editing = false, $email->setRequired(); $group->setOption('description', 'messages.team.person.isContact'); } - }, $initialMembers, true); + }, $initialMembers, $defaultMaxMembers); + $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 c2e45e0..0db5b2c 100644 --- a/app/presenters/TeamPresenter.php +++ b/app/presenters/TeamPresenter.php @@ -202,11 +202,11 @@ public function actionExport(string $type = 'csv'): void { } } - protected function createComponentTeamForm(string $name): Form { + protected function createComponentTeamForm(): Form { $idParam = $this->getParameter('id'); $editing = $idParam !== null; - $form = $this->teamFormFactory->create($this->countries->fetchIdNamePairs(), $this->locale, $editing, $this, $name); - if ($editing && !$form->isSubmitted()) { + $form = $this->teamFormFactory->create($this->countries->fetchIdNamePairs(), $this->locale, $editing); + if ($editing) { \assert(\is_string($idParam)); // For PHPStan. $id = (int) $idParam; $team = $this->teams->getById($id); @@ -252,21 +252,20 @@ protected function createComponentTeamForm(string $name): Form { $default['persons'][] = $personDefault; } - $form->setValues($default); + $form->setDefaults($default); } /** @var \Nette\Forms\Controls\SubmitButton */ $save = $form['save']; if ($this->getParameter('id')) { $save->caption = 'messages.team.action.edit'; } - /** @var callable(Nette\Forms\Controls\SubmitButton): void */ $processTeamForm = Closure::fromCallable([$this, 'processTeamForm']); - $save->onClick[] = $processTeamForm; + $form->onSuccess[] = $processTeamForm; return $form; } - private function processTeamForm(Nette\Forms\Controls\SubmitButton $button): void { + private function processTeamForm(App\Components\TeamForm $form): void { if (!$this->user->isInRole('admin')) { if ($this->context->parameters['entries']['closing']->diff(new DateTime())->invert === 0) { throw new App\TooLateForAccessException(); @@ -275,8 +274,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 */ diff --git a/composer.json b/composer.json index 49cde5a..aec3ac6 100644 --- a/composer.json +++ b/composer.json @@ -12,10 +12,10 @@ ], "require": { "php": ">= 8.0", + "contributte/forms-multiplier": "^3.2", "contributte/mail": "^0.6.0", "contributte/translation": "^1.0", "ihor/nspl": "^1.3", - "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 f8db827..62f20a0 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": "1156fad39db1ce5defb7ca4a10c0fce9", + "content-hash": "28791aec1018df56fa77984d8b50f1ec", "packages": [ + { + "name": "contributte/forms-multiplier", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/contributte/forms-multiplier.git", + "reference": "d7854344b3bc6c363a0dd31daa36bc7c9dfa4265" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/contributte/forms-multiplier/zipball/d7854344b3bc6c363a0dd31daa36bc7c9dfa4265", + "reference": "d7854344b3bc6c363a0dd31daa36bc7c9dfa4265", + "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": "^2.7.0", + "nette/application": "^3.0.0", + "nette/di": "^3.0.0", + "ninjify/qa": "^0.12", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-nette": "^0.12", + "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.2.0" + }, + "funding": [ + { + "url": "https://contributte.org/partners.html", + "type": "custom" + }, + { + "url": "https://github.com/f3l1x", + "type": "github" + } + ], + "time": "2021-03-09T08:05:53+00:00" + }, { "name": "contributte/mail", "version": "v0.6.0", @@ -231,79 +297,6 @@ }, "time": "2019-03-21T20:38:30+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.1", diff --git a/utils/phpstan.neon b/utils/phpstan.neon index c8b1dc3..e287ed2 100644 --- a/utils/phpstan.neon +++ b/utils/phpstan.neon @@ -12,4 +12,4 @@ parameters: treatPhpDocTypesAsCertain: false checkGenericClassInNonGenericObjectType: false ignoreErrors: - - '(Call to an undefined method App\\Components\\TeamForm::addDynamic\(\))' + - '(Call to an undefined method App\\Components\\TeamForm::addMultiplier\(\))'