Skip to content

Commit fdc44a0

Browse files
committed
Write event rule form from scratch again
1 parent 47bccc0 commit fdc44a0

23 files changed

+2295
-1052
lines changed

application/controllers/EventRuleController.php

Lines changed: 122 additions & 186 deletions
Large diffs are not rendered by default.

application/controllers/EventRulesController.php

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
use Icinga\Module\Notifications\Widget\ItemList\ObjectList;
1414
use Icinga\Web\Session;
1515
use ipl\Html\Form;
16-
use ipl\Html\Html;
17-
use ipl\Stdlib\Filter;
1816
use ipl\Web\Compat\CompatController;
1917
use ipl\Web\Compat\SearchControls;
2018
use ipl\Web\Control\LimitControl;
@@ -23,19 +21,14 @@
2321
use ipl\Web\Layout\DetailedItemLayout;
2422
use ipl\Web\Url;
2523
use ipl\Web\Widget\ButtonLink;
26-
use ipl\Web\Widget\Link;
2724

2825
class EventRulesController extends CompatController
2926
{
3027
use SearchControls;
3128

32-
/** @var Session\SessionNamespace */
33-
private Session\SessionNamespace $sessionNamespace;
34-
3529
public function init()
3630
{
3731
$this->assertPermission('notifications/config/event-rules');
38-
$this->sessionNamespace = Session::getSession()->getNamespace('notifications');
3932
}
4033

4134
public function indexAction(): void
@@ -47,8 +40,8 @@ public function indexAction(): void
4740
$sortControl = $this->createSortControl(
4841
$eventRules,
4942
[
50-
'name' => t('Name'),
51-
'changed_at' => t('Changed At')
43+
'name' => $this->translate('Name'),
44+
'changed_at' => $this->translate('Changed At')
5245
]
5346
);
5447

@@ -79,23 +72,14 @@ public function indexAction(): void
7972
$this->addControl($limitControl);
8073
$this->addControl($searchBar);
8174

82-
$addButton =
75+
$this->addContent(
8376
(new ButtonLink(
84-
t('Add Event Rule'),
77+
$this->translate('Add Event Rule'),
8578
Url::fromPath('notifications/event-rules/add'),
86-
'plus'
87-
))->openInModal();
88-
if (isset($this->sessionNamespace->{-1})) {
89-
$this->addContent(Html::tag('div', ['class' => 'add-new-component'], [
90-
$addButton->disable($this->translate(
91-
'You have unsaved changes. Please save or discard them first.'
92-
)),
93-
(new Link($this->translate('Continue Editing'), Links::eventRule(-1)))
94-
->setBaseTarget('_next')
95-
]));
96-
} else {
97-
$this->addContent($addButton->addAttributes(['class' => 'add-new-component']));
98-
}
79+
'plus',
80+
['class' => 'add-new-component']
81+
))->openInModal()
82+
);
9983

10084
$this->addContent(
10185
(new ObjectList($eventRules, new EventRuleRenderer()))
@@ -116,12 +100,12 @@ public function addAction(): void
116100

117101
$eventRuleForm = (new EventRuleForm())
118102
->populate(['id' => -1])
103+
->setCsrfCounterMeasureId(Session::getSession()->getId())
119104
->setAction(Url::fromRequest()->getAbsoluteUrl())
120105
->on(Form::ON_SUCCESS, function ($form) {
121-
$this->sessionNamespace->set(-1, ['id' => -1, 'name' => $form->getValue('name')]);
122106
$this->sendExtraUpdates(['#col1']);
123107
$this->getResponse()->setHeader('X-Icinga-Container', 'col2');
124-
$this->redirectNow(Links::eventRule(-1));
108+
$this->redirectNow(Links::eventRule(-1)->addParams(['name' => $form->getValue('name')]));
125109
})->handleRequest($this->getServerRequest());
126110

127111
$this->addContent($eventRuleForm);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/* Icinga Notifications Web | (c) 2025 Icinga GmbH | GPLv2 */
4+
5+
namespace Icinga\Module\Notifications\Forms\EventRuleConfigElements;
6+
7+
use ipl\Html\Attributes;
8+
9+
/**
10+
* @internal This trait is only intended for use by the {@see EventRuleConfigForm} classes.
11+
*/
12+
trait ConfigProvider
13+
{
14+
/** @var ?ConfigProviderInterface The config provider */
15+
protected ?ConfigProviderInterface $provider = null;
16+
17+
/**
18+
* Set the config provider to use
19+
*
20+
* @param ConfigProviderInterface $provider
21+
*
22+
* @return void
23+
*/
24+
public function setProvider(ConfigProviderInterface $provider): void
25+
{
26+
$this->provider = $provider;
27+
}
28+
29+
protected function registerAttributeCallbacks(Attributes $attributes): void
30+
{
31+
$attributes->registerAttributeCallback('provider', null, $this->setProvider(...));
32+
33+
parent::registerAttributeCallbacks($attributes);
34+
}
35+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/* Icinga Notifications Web | (c) 2025 Icinga GmbH | GPLv2 */
4+
5+
namespace Icinga\Module\Notifications\Forms\EventRuleConfigElements;
6+
7+
use Icinga\Module\Notifications\Model\Channel;
8+
use Icinga\Module\Notifications\Model\Contact;
9+
use Icinga\Module\Notifications\Model\Contactgroup;
10+
use Icinga\Module\Notifications\Model\Schedule;
11+
12+
interface ConfigProviderInterface
13+
{
14+
/**
15+
* Get a list of contacts to choose as part of a {@see EscalationRecipient}
16+
*
17+
* @return iterable<Contact> Properties {@see Contact::$id} and {@see Contact::$full_name} are required.
18+
*/
19+
public function fetchContacts(): iterable;
20+
21+
/**
22+
* Get a list of contact groups to choose as part of a {@see EscalationRecipient}
23+
*
24+
* @return iterable<Contactgroup> Properties {@see Contactgroup::$id} and {@see Contactgroup::$name} are required.
25+
*/
26+
public function fetchContactGroups(): iterable;
27+
28+
/**
29+
* Get a list of schedules to choose as part of a {@see EscalationRecipient}
30+
*
31+
* @return iterable<Schedule> Properties {@see Schedule::$id} and {@see Schedule::$name} are required.
32+
*/
33+
public function fetchSchedules(): iterable;
34+
35+
/**
36+
* Get a list of channels to choose as part of a {@see EscalationRecipient}
37+
*
38+
* @return iterable<Channel> Properties {@see Channel::$id} and {@see Channel::$name} are required.
39+
*/
40+
public function fetchChannels(): iterable;
41+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
/* Icinga Notifications Web | (c) 2025 Icinga GmbH | GPLv2 */
4+
5+
namespace Icinga\Module\Notifications\Forms\EventRuleConfigElements;
6+
7+
use ipl\Html\Contract\FormElement;
8+
use ipl\Html\FormElement\SubmitButtonElement;
9+
10+
trait DynamicElements
11+
{
12+
/**
13+
* Create a button to add a new element
14+
*
15+
* @return SubmitButtonElement
16+
*/
17+
abstract protected function createAddButton(): SubmitButtonElement;
18+
19+
/**
20+
* Create a new element
21+
*
22+
* The given remove button must be rendered in the element's content.
23+
*
24+
* @param int $no The position of the element in the list
25+
* @param ?SubmitButtonElement $removeButton The button to remove the element, if any
26+
*
27+
* @return FormElement
28+
*/
29+
abstract protected function createDynamicElement(int $no, ?SubmitButtonElement $removeButton): FormElement;
30+
31+
/**
32+
* Create a button to remove the given element
33+
*
34+
* Note that the button will only be registered in this set but not added to its content.
35+
* {@see DynamicElements::createDynamicElement()} is responsible for that.
36+
*
37+
* @param int $no The position of the element in the list
38+
*
39+
* @return SubmitButtonElement
40+
*/
41+
protected function createRemoveButton(int $no): SubmitButtonElement
42+
{
43+
/** @var SubmitButtonElement $remove */
44+
$remove = $this->createElement('submitButton', sprintf('remove_%d', $no), [
45+
'formnovalidate' => true
46+
]);
47+
48+
$this->registerElement($remove);
49+
50+
return $remove;
51+
}
52+
53+
public function populate($values): static
54+
{
55+
if (! isset($values['count'])) {
56+
// Ensure a count is set upon the initial population
57+
$values['count'] = count($values);
58+
}
59+
60+
return parent::populate($values);
61+
}
62+
63+
protected function assemble(): void
64+
{
65+
$expectedCount = (int) $this->getPopulatedValue('count', $this->isRequired() ? 1 : 0);
66+
67+
$count = 0; // Increases until $expectedCount is reached, ensuring proper association with form data
68+
$newCount = 0; // The actual number of restored elements, minus the one that has been removed
69+
while ($count < $expectedCount) {
70+
$remove = $this->createRemoveButton($count);
71+
if ($remove->hasBeenPressed()) {
72+
$this->clearPopulatedValue($remove->getName());
73+
$this->clearPopulatedValue($count);
74+
75+
// Re-index populated values to ensure proper association with form data
76+
foreach (range($count + 1, $expectedCount - 1) as $i) {
77+
$expectedValue = $this->getPopulatedValue($i);
78+
if ($expectedValue !== null) {
79+
$this->populate([$i - 1 => $expectedValue]);
80+
}
81+
}
82+
} else {
83+
$newCount++;
84+
}
85+
86+
$count++;
87+
}
88+
89+
$add = $this->createAddButton()->addAttributes(['formnovalidate' => true]);
90+
$this->registerElement($add);
91+
if ($add->hasBeenPressed()) {
92+
$this->createRemoveButton($newCount);
93+
$newCount++;
94+
}
95+
96+
if ($newCount === 1 && $this->isRequired()) {
97+
$this->addElement(
98+
$this->createDynamicElement(0, null)
99+
->addAttributes(['class' => 'dynamic-item'])
100+
);
101+
} else {
102+
for ($i = 0; $i < $newCount; $i++) {
103+
/** @var SubmitButtonElement $remove */
104+
$remove = $this->getElement(sprintf('remove_%d', $i));
105+
$this->addElement(
106+
$this->createDynamicElement($i, $remove)
107+
->addAttributes(['class' => 'dynamic-item'])
108+
);
109+
}
110+
}
111+
112+
$this->addElement($add);
113+
114+
$this->clearPopulatedValue('count');
115+
$this->addElement('hidden', 'count', ['ignore' => true, 'value' => $newCount]);
116+
117+
$this->addAttributes(['class' => ['dynamic-list', $newCount === 0 ? 'empty' : '']]);
118+
}
119+
}

0 commit comments

Comments
 (0)