Skip to content

Commit d0e3fe3

Browse files
ContactForm: Show suggestions while typing in user element
Introduce IcingaWebUserSuggestions class ContactControler: Add suggestion action and remove dead code
1 parent 4812a81 commit d0e3fe3

File tree

4 files changed

+182
-6
lines changed

4 files changed

+182
-6
lines changed

application/controllers/ContactController.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@
55
namespace Icinga\Module\Notifications\Controllers;
66

77
use Icinga\Module\Notifications\Common\Database;
8-
use Icinga\Module\Notifications\Model\Contact;
98
use Icinga\Module\Notifications\Web\Form\ContactForm;
9+
use Icinga\Module\Notifications\Widget\IcingaWebUserSuggestions;
1010
use Icinga\Web\Notification;
11-
use ipl\Html\FormElement\FieldsetElement;
12-
use ipl\Sql\Connection;
13-
use ipl\Stdlib\Filter;
1411
use ipl\Web\Compat\CompatController;
1512

1613
class ContactController extends CompatController
@@ -48,4 +45,12 @@ public function indexAction(): void
4845

4946
$this->addContent($form);
5047
}
48+
49+
public function suggestIcingaWebUserAction(): void
50+
{
51+
$users = new IcingaWebUserSuggestions();
52+
$users->forRequest($this->getServerRequest());
53+
54+
$this->getDocument()->addHtml($users);
55+
}
5156
}

library/Notifications/Web/Form/ContactForm.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use ipl\Validator\StringLengthValidator;
2525
use ipl\Web\Common\CsrfCounterMeasure;
2626
use ipl\Web\Compat\CompatForm;
27+
use ipl\Web\Url;
2728

2829
class ContactForm extends CompatForm
2930
{
@@ -93,7 +94,12 @@ protected function assemble()
9394
'label' => $this->translate('Contact Name'),
9495
'required' => true
9596
]
96-
)->addElement(
97+
);
98+
99+
$suggestionsId = 'icinga-user-suggestions';
100+
$contact
101+
->addHtml(new HtmlElement('div', new Attributes(['id' => $suggestionsId, 'class' => 'search-suggestions'])))
102+
->addElement(
97103
'text',
98104
'username',
99105
[
@@ -117,7 +123,14 @@ protected function assemble()
117123

118124
return true;
119125
})
120-
]
126+
],
127+
'placeholder' => $this->translate('Start typing to see suggestions ...'),
128+
'autocomplete' => 'off',
129+
'class' => 'search',
130+
'data-enrichment-type' => 'completion',
131+
'data-term-suggestions' => '#' . $suggestionsId,
132+
'data-suggest-url' => Url::fromPath('notifications/contact/suggest-icinga-web-user')
133+
->with(['showCompact' => true, '_disableLayout' => 1]),
121134
]
122135
)->addHtml(new HtmlElement(
123136
'p',
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
/* Icinga Notifications Web | (c) 2024 Icinga GmbH | GPLv2 */
4+
5+
namespace Icinga\Module\Notifications\Widget;
6+
7+
use Exception;
8+
use Icinga\Application\Config;
9+
use Icinga\Authentication\User\DomainAwareInterface;
10+
use Icinga\Authentication\User\UserBackend;
11+
use Icinga\Data\Selectable;
12+
use Icinga\Repository\Repository;
13+
use ipl\Html\Attributes;
14+
use ipl\Html\BaseHtmlElement;
15+
use ipl\Html\HtmlElement;
16+
use ipl\Html\Text;
17+
use ipl\Web\Control\SearchBar\Suggestions;
18+
use Psr\Http\Message\ServerRequestInterface;
19+
20+
class IcingaWebUserSuggestions extends BaseHtmlElement
21+
{
22+
protected $tag = 'ul';
23+
24+
/** @var string */
25+
protected $searchTerm;
26+
27+
/** @var string */
28+
protected $originalValue;
29+
30+
public function setSearchTerm(string $term): self
31+
{
32+
$this->searchTerm = $term;
33+
34+
return $this;
35+
}
36+
37+
public function setOriginalValue(string $term): self
38+
{
39+
$this->originalValue = $term;
40+
41+
return $this;
42+
}
43+
44+
/**
45+
* Load suggestions as requested by the client
46+
*
47+
* @param ServerRequestInterface $request
48+
*
49+
* @return $this
50+
*/
51+
public function forRequest(ServerRequestInterface $request): self
52+
{
53+
if ($request->getMethod() !== 'POST') {
54+
return $this;
55+
}
56+
57+
$requestData = json_decode($request->getBody()->read(8192), true);
58+
if (empty($requestData)) {
59+
return $this;
60+
}
61+
62+
$this->setSearchTerm($requestData['term']['label']);
63+
$this->setOriginalValue($requestData['term']['search']);
64+
65+
return $this;
66+
}
67+
68+
protected function assemble(): void
69+
{
70+
$userBackends = [];
71+
foreach (Config::app('authentication') as $backendName => $backendConfig) {
72+
$candidate = UserBackend::create($backendName, $backendConfig);
73+
if ($candidate instanceof Selectable) {
74+
$userBackends[] = $candidate;
75+
}
76+
}
77+
78+
$limit = 10;
79+
while ($limit > 0 && ! empty($userBackends)) {
80+
/** @var Repository $backend */
81+
$backend = array_shift($userBackends);
82+
$query = $backend->select()
83+
->from('user', ['user_name'])
84+
->where('user_name', $this->searchTerm)
85+
->limit($limit);
86+
87+
try {
88+
$names = $query->fetchColumn();
89+
} catch (Exception $e) {
90+
continue;
91+
}
92+
93+
if (empty($names)) {
94+
continue;
95+
}
96+
97+
if ($backend instanceof DomainAwareInterface) {
98+
$names = array_map(function($name) use ($backend) {
99+
return $name . '@' . $backend->getDomain();
100+
}, $names);
101+
}
102+
103+
$this->addHtml(
104+
new HtmlElement(
105+
'li',
106+
new Attributes(['class' => Suggestions::SUGGESTION_TITLE_CLASS]),
107+
new Text($backend->getName())
108+
)
109+
);
110+
111+
foreach ($names as $name) {
112+
$this->addHtml(
113+
new HtmlElement(
114+
'li',
115+
null,
116+
new HtmlElement(
117+
'input',
118+
Attributes::create([
119+
'type' => 'button',
120+
'value' => $name,
121+
'data-label' => $name,
122+
'data-search' => $name,
123+
'data-class' => 'icinga-web-user',
124+
])
125+
)
126+
)
127+
);
128+
}
129+
130+
$limit -= count($names);
131+
}
132+
133+
if ($this->isEmpty()) {
134+
$this->addHtml(
135+
new HtmlElement(
136+
'li',
137+
Attributes::create(['class' => 'nothing-to-suggest']),
138+
new HtmlElement('em', null, Text::create(t('Nothing to suggest')))
139+
)
140+
);
141+
}
142+
}
143+
144+
public function renderUnwrapped(): string
145+
{
146+
$this->ensureAssembled();
147+
148+
if ($this->isEmpty()) {
149+
return '';
150+
}
151+
152+
return parent::renderUnwrapped();
153+
}
154+
}

public/css/form.less

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,8 @@
122122
padding-bottom: 1em;
123123
border-bottom: 1px solid @gray-light;
124124
}
125+
126+
input.search {
127+
padding-left: 1.5em; // icinga-controls changes it
128+
}
125129
}

0 commit comments

Comments
 (0)