ILIAS  trunk Revision v12.0_alpha-377-g3641b37b9db
class.ConfigurationGUI.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
23use ILIAS\User\RedirectOnMissingWrite;
27use ILIAS\UI\Factory as UIFactory;
28use ILIAS\UI\Renderer as UIRenderer;
31use ILIAS\UI\Component\Modal\RoundTrip as RoundTripModal;
32use ILIAS\UI\Component\Modal\Interruptive as InterruptiveModal;
33use ILIAS\UI\Component\Listing\Descriptive as DescriptiveListing;
34use ILIAS\UI\Component\Table\Data as DataTable;
42use ILIAS\Refinery\Factory as Refinery;
44use ILIAS\HTTP\Services as HttpService;
46use Psr\Http\Message\ServerRequestInterface;
47
49{
50 use RedirectOnMissingWrite;
51
52 private const string CHANGED_ATTRIBUTES_PARAMETER = 'ca';
53
54 private const string DEFAULT_CMD = 'show';
55 private const string CMD_SAVE = 'save';
56 private const string CMD_CREATE = 'create';
57 private const string CMD_SAVE_AFTER_LISTENER_CONFIRMATION = 'saveAfterListenerConfirmation';
58 private const string CMD_DELETE = 'delete';
59 private const string CMD_PERFORM_TABLE_ACTION = 'action';
60
61 private const string ACTION_EDIT = 'edit';
62 private const string ACTION_DELETE = 'delete';
63
64 private readonly URLBuilder $url_builder;
67
68 private array $available_fields;
69
70 public function __construct(
71 private readonly Language $lng,
72 private readonly \ilCtrl $ctrl,
73 private readonly \ilAppEventHandler $event,
74 private readonly \ilAccess $access,
75 private readonly \ilToolbarGUI $toolbar,
76 private readonly \ilGlobalTemplateInterface $tpl,
77 private readonly UIFactory $ui_factory,
78 private readonly UIRenderer $ui_renderer,
79 private readonly Refinery $refinery,
80 private readonly ServerRequestInterface $request,
81 private readonly RequestWrapper $request_wrapper,
82 private readonly RequestWrapper $post_wrapper,
83 private readonly HttpService $http,
84 private readonly array $available_change_listeners,
85 private readonly ConfigurationRepository $repository
86 ) {
87 $this->available_fields = $this->repository->get();
88
89 $url_builder = new URLBuilder(
90 new URI(
91 ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTargetByClass(
92 [\ilAdministrationGUI::class, \ilObjUserFolderGUI::class, self::class],
93 self::CMD_PERFORM_TABLE_ACTION
94 )
95 )
96 );
97 [
102 ['profile', 'fields'],
103 'table_action',
104 'field'
105 );
106 }
107
108 public function executeCommand(): void
109 {
110 $this->redirectOnMissingWrite($this->access, $this->ctrl, $this->tpl, $this->lng);
111 $cmd = $this->ctrl->getCmd() . 'Cmd';
112 $this->$cmd();
113 }
114
115 public function showCmd(?RoundTripModal $modal = null): void
116 {
117 if (!$this->repository->hasMigrationBeenRun()) {
118 $this->tpl->setOnScreenMessage('info', $this->lng->txt('missing_migration'));
119 return;
120 }
121
122 $create_modal = $this->buildCreateModal();
123 $this->toolbar->addComponent(
124 $this->ui_factory->button()->standard(
125 $this->lng->txt('add_user_defined_field'),
126 $create_modal->getShowSignal()
127 )
128 );
129 $content = [
130 $create_modal,
131 $this->buildTable()
132 ];
133
134 if ($modal !== null) {
135 $content[] = $modal;
136 }
137
138 $this->tpl->setContent(
139 $this->ui_renderer->render($content)
140 );
141 }
142
143 public function actionCmd(): void
144 {
145 $action = $this->request_wrapper->retrieve(
146 $this->action_token->getName(),
147 $this->refinery->kindlyTo()->string()
148 );
149 $this->http->saveResponse(
150 $this->http->response()->withBody(
152 $this->ui_renderer->renderAsync(
153 $this->buildActionModal($action)
154 )
155 )
156 )
157 );
158 $this->http->sendResponse();
159 $this->http->close();
160 }
161
162 public function saveCmd(): void
163 {
164 $field = $this->repository->getByIdentifier(
166 );
167 $modal = $this->buildEditModal($field)->withRequest($this->request);
168 $data = $modal->getData();
169 if ($data === null) {
170 $this->showCmd($modal->withOnLoad($modal->getShowSignal()));
171 return;
172 }
173
174 $listeners_to_notify = $this->getListenersToNotifyByChangedValues($field, $data['field']);
175 if ($listeners_to_notify !== []) {
176 $this->showChangeListenerConfirmationModal($listeners_to_notify, $data['field']);
177 return;
178 }
179
180 $this->storeField($data['field']);
181 $this->ctrl->redirectByClass(
182 [\ilAdministrationGUI::class, \ilObjUserFolderGUI::class, self::class],
183 self::DEFAULT_CMD
184 );
185 }
186
187 public function createCmd(): void
188 {
189 $modal = $this->buildCreateModal()->withRequest($this->request);
190 $data = $modal->getData();
191 if ($data === null) {
192 $this->showCmd($modal->withOnLoad($modal->getShowSignal()));
193 return;
194 }
195
196 $listeners_to_notify = $this->getListenersToNotifyByChangedValues(
197 $this->repository->getUnspecifiedCustomField(),
198 $data['field']
199 );
200
201 if ($listeners_to_notify !== []) {
202 $this->showChangeListenerConfirmationModal($listeners_to_notify, $data['field']);
203 return;
204 }
205
206 $this->storeField($data['field']);
207 $this->ctrl->redirectByClass(
208 [\ilAdministrationGUI::class, \ilObjUserFolderGUI::class, self::class],
209 self::DEFAULT_CMD
210 );
211 }
212
213 public function saveAfterListenerConfirmationCmd(): void
214 {
215 $field = $this->repository->getByIdentifier(
217 );
218
221 $field,
223 ),
224 $field,
225 )->withRequest($this->request)->getData();
226
227 if ($data === null || $data['field']->isRequired() && !$data['field']->isVisibleInRegistration()) {
228 $this->showCmd();
229 return;
230 }
231
232 $this->storeField($data['field']);
233 $this->event->raise(
234 'components/ILIAS/User',
235 'onUserFieldAttributesChanged',
236 $field->getChangedAttributes($data['field'])
237 );
238 $this->showCmd();
239 }
240
241 public function deleteCmd(): void
242 {
243 $identifier = $this->post_wrapper->retrieve(
244 'interruptive_items',
245 $this->refinery->byTrying([
246 $this->refinery->kindlyTo()->listOf(
247 $this->refinery->kindlyTo()->string()
248 ),
249 $this->refinery->always(null)
250 ])
251 );
252 if ($identifier === null) {
253 $this->showCmd();
254 }
255 $this->repository->deleteCustomField(
256 $this->repository->getByIdentifier($identifier[0])
257 );
258 $this->available_fields = $this->repository->get();
259 $this->tpl->setOnScreenMessage('success', $this->lng->txt('udf_field_deleted'), true);
260 $this->ctrl->redirectByClass(
261 [\ilAdministrationGUI::class, \ilObjUserFolderGUI::class, self::class],
262 self::DEFAULT_CMD
263 );
264 }
265
266 public function getRows(
267 DataRowBuilder $row_builder,
268 array $visible_column_ids,
270 Order $order,
271 mixed $additional_viewcontrol_data,
272 mixed $filter_data,
273 mixed $additional_parameters
274 ): \Generator {
275 $this->orderRows($order);
276 foreach ($this->available_fields as $field) {
277 yield $field->getTableRow(
278 $row_builder,
279 $this->lng,
280 $this->ui_factory,
281 $this->ui_renderer
282 );
283 }
284 }
285
286 public function getTotalRowCount(
287 mixed $additional_viewcontrol_data,
288 mixed $filter_data,
289 mixed $additional_parameters
290 ): ?int {
291 return count($this->available_fields);
292 }
293
294 private function buildTable(): DataTable
295 {
296 return $this->ui_factory->table()->data(
297 $this,
298 $this->lng->txt('profile_fields'),
299 $this->getColumns()
300 )->withActions(
301 $this->getActions()
302 )->withRequest($this->request);
303 }
304
305 private function getColumns(): array
306 {
307 $cf = $this->ui_factory->table()->column();
308 $icon_checked = $this->ui_factory->symbol()->icon()
309 ->custom('assets/images/standard/icon_checked.svg', '', 'small');
310 $icon_unchecked = $this->ui_factory->symbol()->icon()
311 ->custom('assets/images/standard/icon_unchecked.svg', '', 'small');
312 return [
313 'field' => $cf->text($this->lng->txt('user_field'))->withIsSortable(true),
314 'type' => $cf->text($this->lng->txt('type'))
315 ->withIsOptional(true, true)
316 ->withIsSortable(true),
317 'section' => $cf->text($this->lng->txt('profile_section'))
318 ->withIsOptional(true, false)
319 ->withIsSortable(true),
320 'access' => $cf->text($this->lng->txt('access'))->withIsSortable(false),
321 'required' => $cf->boolean(
322 $this->lng->txt(
323 PropertyAttributes::Required->value
324 ),
325 $icon_checked,
326 $icon_unchecked
327 )->withIsSortable(true),
328 'export' => $cf->boolean(
329 $this->lng->txt(
330 PropertyAttributes::Export->value
331 ),
332 $icon_checked,
333 $icon_unchecked
334 )->withIsSortable(true),
335 'searchable' => $cf->boolean(
336 $this->lng->txt(
337 PropertyAttributes::Searchable->value
338 ),
339 $icon_checked,
340 $icon_unchecked
341 )->withIsSortable(true),
342 'available_in_certificates' => $cf->boolean(
343 $this->lng->txt(
344 PropertyAttributes::AvailableInCertificates->value
345 ),
346 $icon_checked,
347 $icon_unchecked
348 )->withIsSortable(true)
349 ];
350 }
351
352 private function getActions(): array
353 {
354 return [
355 self::ACTION_EDIT => $this->ui_factory->table()->action()->single(
356 $this->lng->txt('edit_field'),
357 $this->url_builder->withParameter(
358 $this->action_token,
359 self::ACTION_EDIT
360 ),
361 $this->field_id_token
362 )->withAsync(true),
363 self::ACTION_DELETE => $this->ui_factory->table()->action()->single(
364 $this->lng->txt('delete'),
365 $this->url_builder->withParameter(
366 $this->action_token,
367 self::ACTION_DELETE
368 ),
369 $this->field_id_token
370 )->withAsync(true)
371 ];
372 }
373
374 private function orderRows(Order $order): void
375 {
376 $order_array = $order->get();
377 $key = array_key_first($order_array);
378 $factor = array_shift($order_array) === 'ASC' ? 1 : -1;
379 if ($key === 'field') {
380 usort(
381 $this->available_fields,
382 fn(Field $v1, Field $v2): int =>
383 $factor * ($this->lng->txt($v1->getLabel($this->lng)) <=> $this->lng->txt($v2->getLabel($this->lng)))
384 );
385 return;
386 }
387
388 if ($key === 'type') {
389 usort(
390 $this->available_fields,
391 fn(Field $v1, Field $v2): int =>
392 $factor * ($this->lng->txt($v1->isCustom() ? 'custom' : 'default') <=> $this->lng->txt($v2->isCustom() ? 'custom' : 'default'))
393 );
394 return;
395 }
396
397 if ($key === 'section') {
398 usort(
399 $this->available_fields,
400 fn(Field $v1, Field $v2): int =>
401 $factor * ($this->lng->txt($v1->getSection()->value) <=> $this->lng->txt($v2->getSection()->value))
402 );
403 return;
404 }
405
406 if ($key === 'export') {
407 usort(
408 $this->available_fields,
409 fn(Field $v1, Field $v2): int =>
410 $factor * ($v1->export() <=> $v2->export())
411 );
412 return;
413 }
414
415 if ($key === 'required') {
416 usort(
417 $this->available_fields,
418 fn(Field $v1, Field $v2): int =>
419 $factor * ($v1->isRequired() <=> $v2->isRequired())
420 );
421 return;
422 }
423
424 if ($key === 'searchable') {
425 usort(
426 $this->available_fields,
427 fn(Field $v1, Field $v2): int =>
428 $factor * ($v1->isSearchable() <=> $v2->isSearchable())
429 );
430 return;
431 }
432
433 if ($key === 'available_in_certificates') {
434 usort(
435 $this->available_fields,
436 fn(Field $v1, Field $v2): int =>
437 $factor * ($v1->isAvailableInCertificates() <=> $v2->isAvailableInCertificates())
438 );
439 return;
440 }
441 }
442
443 private function buildActionModal(
444 ?string $action
445 ): Modal|MessageBox {
446 $field = $this->repository->getByIdentifier(
447 $this->retrieveIdentifierFromQuery()
448 );
449 return match ($action) {
450 self::ACTION_EDIT => $this->buildEditModal($field),
451 self::ACTION_DELETE => $this->buildDeleteConfirmationModal($field),
452 default => $this->ui_factory->messageBox()->failure(
453 $this->lng->txt('msg_cancel')
454 )
455 };
456 }
457
458 private function buildEditModal(
459 Field $field
460 ): RoundTripModal {
461 $identifier = $this->retrieveIdentifierFromQuery();
462 $this->ctrl->setParameterByClass(self::class, $this->field_id_token->getName(), $identifier);
463 return $this->ui_factory->modal()->roundtrip(
464 "{$this->lng->txt('edit_field')}: {$field->getLabel($this->lng)}",
465 null,
466 $field->getEditForm(
467 $this->lng,
468 $this->ui_factory->input()->field(),
469 $this->refinery,
470 $this->repository->getCustomFieldTypes(),
471 array_filter(
472 $this->available_fields,
473 static fn(Field $v): bool => $v->isCustom()
474 )
475 ),
476 $this->ctrl->getFormActionByClass(
477 [\ilAdministrationGUI::class, \ilObjUserFolderGUI::class, self::class],
478 self::CMD_SAVE
479 )
480 );
481 }
482
483 private function buildCreateModal(): RoundTripModal
484 {
485 return $this->ui_factory->modal()->roundtrip(
486 $this->lng->txt('add_user_defined_field'),
487 null,
488 $this->repository->getUnspecifiedCustomField()->getCreateCustomFieldForm(
489 $this->lng,
490 $this->ui_factory->input()->field(),
491 $this->refinery,
492 $this->repository->getCustomFieldTypes(),
493 array_filter(
494 $this->available_fields,
495 static fn(Field $v): bool => $v->isCustom()
496 )
497 ),
498 $this->ctrl->getFormActionByClass(
499 [\ilAdministrationGUI::class, \ilObjUserFolderGUI::class, self::class],
500 self::CMD_CREATE
501 )
502 );
503 }
504
506 array $listeners_to_notify,
507 Field $new
508 ): void {
509 $this->setChangedAttributesParameter($listeners_to_notify);
510 $modal = $this->buildChangeListenerConfirmationModal(
511 $listeners_to_notify,
512 $new
513 );
514 $this->showCmd($modal->withOnLoad($modal->getShowSignal()));
515 }
516
518 array $listeners_to_notify,
519 Field $field
520 ): RoundTripModal {
521 return $this->ui_factory->modal()->roundtrip(
522 $this->lng->txt('usr_field_change_components_listening'),
523 $this->ui_factory->messageBox()->confirmation(
524 $this->ui_renderer->render(
525 $this->buildListingOfListeners($listeners_to_notify, $field->getLabel($this->lng))
526 )
527 ),
528 $field->getHiddenForm(
529 $this->lng,
530 $this->ui_factory->input()->field(),
531 $this->refinery,
532 $this->repository->getCustomFieldTypes(),
533 array_filter(
534 $this->available_fields,
535 static fn(Field $v): bool => $v->isCustom()
536 )
537 ),
538 $this->ctrl->getFormActionByClass(
539 [\ilAdministrationGUI::class, \ilObjUserFolderGUI::class, self::class],
540 self::CMD_SAVE_AFTER_LISTENER_CONFIRMATION
541 )
542 );
543 }
544
546 Field $field
547 ): InterruptiveModal {
548 return $this->ui_factory->modal()->interruptive(
549 $this->lng->txt('confirm'),
550 $this->lng->txt('udf_delete_sure'),
551 $this->ctrl->getFormActionByClass(
552 [\ilAdministrationGUI::class, \ilObjUserFolderGUI::class, self::class],
553 self::CMD_DELETE
554 )
555 )->withAffectedItems([
556 $this->ui_factory->modal()->interruptiveItem()->standard(
557 $field->getIdentifier(),
558 $field->getLabel($this->lng)
559 )
560 ]);
561 }
562
563 private function retrieveIdentifierFromQuery(): string
564 {
565 $identifier = $this->request_wrapper->retrieve(
566 $this->field_id_token->getName(),
567 $this->refinery->byTrying([
568 $this->refinery->kindlyTo()->string(),
569 $this->refinery->kindlyTo()->listOf(
570 $this->refinery->kindlyTo()->string()
571 )
572 ])
573 );
574
575 if (is_array($identifier)) {
576 return $identifier[0];
577 }
578 return $identifier;
579 }
580
581 private function buildListingOfListeners(
582 array $listeners_to_notify,
583 string $field_name
584 ): DescriptiveListing {
585 return $this->ui_factory->listing()->descriptive(
586 array_reduce(
587 $listeners_to_notify,
588 function (array $c, UserFieldAttributesChangeListener $v) use ($field_name): array {
589 $c[$v->getComponentName()] = $v->getDescriptionForField(
590 $this->lng,
591 $field_name,
592 $this->lng->txt($v->isInterestedInAttribute()->value)
593 );
594 return $c;
595 },
596 []
597 )
598 );
599 }
600
602 Field $old_field,
603 Field $new_field
604 ): array {
605 return array_reduce(
606 $this->available_change_listeners,
607 static function (
608 array $c,
609 string $listener_class
610 ) use ($old_field, $new_field): array {
611 $listener = new $listener_class();
612 $field_definition_class = $listener->isInterestedInField();
613
614 if ($old_field->getIdentifier() === (new $field_definition_class())->getIdentifier()
615 && $old_field->retrieveValueByPropertyAttribute($listener->isInterestedInAttribute())
616 !== $new_field->retrieveValueByPropertyAttribute($listener->isInterestedInAttribute())) {
617 $c[] = $listener;
618 }
619
620 return $c;
621 },
622 []
623 );
624 }
625
631 Field $field,
632 array $attributes
633 ): array {
634 return array_reduce(
635 $this->available_change_listeners,
636 function (
637 array $c,
638 string $listener_class
639 ) use ($field, $attributes): array {
640 $listener = new $listener_class();
641 if ($field->getIdentifier() === $this->repository->getByClass(
642 $listener->isInterestedInField()
643 )->getIdentifier()
644 && in_array($listener->isInterestedInAttribute(), $attributes)) {
645 $c[] = $listener;
646 }
647
648 return $c;
649 },
650 []
651 );
652 }
653
654 private function setChangedAttributesParameter(array $listeners_to_notify): void
655 {
656 $this->ctrl->setParameterByClass(
657 self::class,
658 self::CHANGED_ATTRIBUTES_PARAMETER,
659 implode(
660 ',',
661 array_map(
662 fn(UserFieldAttributesChangeListener $v): string => $v->isInterestedInAttribute()->value,
663 $listeners_to_notify
664 )
665 )
666 );
667 }
668
669 private function retrieveChangedAttributesFromQuery(): array
670 {
671 if (!$this->request_wrapper->has(self::CHANGED_ATTRIBUTES_PARAMETER)) {
672 return [];
673 }
674
675 return $this->request_wrapper->retrieve(
676 self::CHANGED_ATTRIBUTES_PARAMETER,
677 $this->refinery->custom()->transformation(
678 fn(string $v): array => array_reduce(
679 explode(',', $v),
680 static function (array $c, string $v): array {
681 $a = PropertyAttributes::tryFrom($v);
682 if ($a !== null) {
683 $c[] = $a;
684 }
685 return $c;
686 },
687 []
688 )
689 )
690 );
691 }
692
693 private function storeField(Field $field): void
694 {
695 $this->repository->storeConfiguration($field);
696 $this->available_fields = $this->repository->get();
698 $this->tpl->setOnScreenMessage('success', $this->lng->txt('usr_settings_saved'), true);
699 }
700}
Builds a Color from either hex- or rgb values.
Definition: Factory.php:31
Builds data types.
Definition: Factory.php:36
Both the subject and the direction need to be specified when expressing an order.
Definition: Order.php:29
A simple class to express a naive range of whole positive numbers.
Definition: Range.php:29
The scope of this class is split ilias-conform URI's into components.
Definition: URI.php:35
Stream factory which enables the user to create streams without the knowledge of the concrete class.
Definition: Streams.php:32
static ofString(string $string)
Creates a new stream with an initial value.
Definition: Streams.php:41
Class Services.
Definition: Services.php:38
acquireParameters(array $namespace, string ... $names)
Definition: URLBuilder.php:138
getRows(DataRowBuilder $row_builder, array $visible_column_ids, Range $range, Order $order, mixed $additional_viewcontrol_data, mixed $filter_data, mixed $additional_parameters)
This is called by the table to retrieve rows; map data-records to rows using the $row_builder e....
setChangedAttributesParameter(array $listeners_to_notify)
getListenersToNotifyByChangedValues(Field $old_field, Field $new_field)
buildChangeListenerConfirmationModal(array $listeners_to_notify, Field $field)
getListenersToNotifyByInterests(Field $field, array $attributes)
showChangeListenerConfirmationModal(array $listeners_to_notify, Field $new)
buildListingOfListeners(array $listeners_to_notify, string $field_name)
__construct(private readonly Language $lng, private readonly \ilCtrl $ctrl, private readonly \ilAppEventHandler $event, private readonly \ilAccess $access, private readonly \ilToolbarGUI $toolbar, private readonly \ilGlobalTemplateInterface $tpl, private readonly UIFactory $ui_factory, private readonly UIRenderer $ui_renderer, private readonly Refinery $refinery, private readonly ServerRequestInterface $request, private readonly RequestWrapper $request_wrapper, private readonly RequestWrapper $post_wrapper, private readonly HttpService $http, private readonly array $available_change_listeners, private readonly ConfigurationRepository $repository)
getTotalRowCount(mixed $additional_viewcontrol_data, mixed $filter_data, mixed $additional_parameters)
Mainly for the purpose of pagination-support, it is important to know about the total number of recor...
getLabel(Language $lng)
Definition: Field.php:73
retrieveValueByPropertyAttribute(PropertyAttributes $attribute)
Definition: Field.php:299
getEditForm(Language $lng, FieldFactory $ff, Refinery $refinery, array $custom_field_types, array $available_custom_fields)
Definition: Field.php:188
Class ilAccessHandler Checks access for ILIAS objects.
Class ilAdministrationGUI.
Global event handler.
Class ilCtrl provides processing control methods.
static _reset()
Reset all.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$http
Definition: deliver.php:30
$c
Definition: deliver.php:25
return['delivery_method'=> 'php',]
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Interface RequestWrapper.
This describes commonalities between the different modals.
Definition: Modal.php:35
This describes a Data Table.
Definition: Data.php:33
An entity that renders components to a string output.
Definition: Renderer.php:31
static http()
Fetches the global http state from ILIAS.
modal(string $title="", string $cancel_label="")
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
global $lng
Definition: privfeed.php:31