From e2ed83a6d9f86ca8bb588a6215b21d1562185c1e Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Fri, 4 Apr 2025 15:28:56 +0200 Subject: [PATCH 01/14] Init ConfirmationModals --- .../LinksAndEntityListConflictException.php | 35 +++++ .../src/Component/MessageBox/MessageBox.php | 16 +++ components/ILIAS/UI/src/Factory.php | 3 +- .../EntityListingRendererFactory.php | 27 ++++ .../Component/MessageBox/MessageBox.php | 22 ++++ .../MessageBox/PromptContextRenderer.php | 2 + .../Component/MessageBox/Renderer.php | 2 + .../confirmationWithEntityList.php | 97 ++++++++++++++ .../Prompt/Standard/confirmation_prompt.php | 120 ++++++++++++++++++ 9 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 components/ILIAS/UI/src/Component/MessageBox/Exception/LinksAndEntityListConflictException.php create mode 100644 components/ILIAS/UI/src/Implementation/Component/MessageBox/EntityListingRendererFactory.php create mode 100644 components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php create mode 100644 components/ILIAS/UI/src/examples/Prompt/Standard/confirmation_prompt.php diff --git a/components/ILIAS/UI/src/Component/MessageBox/Exception/LinksAndEntityListConflictException.php b/components/ILIAS/UI/src/Component/MessageBox/Exception/LinksAndEntityListConflictException.php new file mode 100644 index 000000000000..a548cdf0f677 --- /dev/null +++ b/components/ILIAS/UI/src/Component/MessageBox/Exception/LinksAndEntityListConflictException.php @@ -0,0 +1,35 @@ + - * Message Boxes consist of a mandatory message text, optional Buttons and an optional Unordered List of Links. + * Message Boxes consist of a mandatory message text, optional Buttons and either an optional Unordered List of Links + * OR an optional EntityList (but not both simultaneously). * There are four main types of Message Boxes, each is displayed in the according color: * 1. Failure, * 2. Success, diff --git a/components/ILIAS/UI/src/Implementation/Component/MessageBox/EntityListingRendererFactory.php b/components/ILIAS/UI/src/Implementation/Component/MessageBox/EntityListingRendererFactory.php new file mode 100644 index 000000000000..735738478da9 --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/MessageBox/EntityListingRendererFactory.php @@ -0,0 +1,27 @@ +entity_list)) { + throw new C\MessageBox\Exception\LinksAndEntityListConflictException(); + } + $types = array(C\Component::class); $this->checkArgListElements("links", $links, $types); @@ -108,6 +113,23 @@ public function withLinks(array $links): C\MessageBox\MessageBox return $clone; } + public function withEntityListing(C\Listing\Entity\EntityListing $entity_list): C\MessageBox\MessageBox + { + if (!empty($this->links)) { + throw new C\MessageBox\Exception\LinksAndEntityListConflictException(); + } + + $clone = clone $this; + $clone->entity_list = $entity_list; + + return $clone; + } + + public function getEntityListing(): ?C\Listing\Entity\EntityListing + { + return $this->entity_list; + } + /** * @inheritdoc */ diff --git a/components/ILIAS/UI/src/Implementation/Component/MessageBox/PromptContextRenderer.php b/components/ILIAS/UI/src/Implementation/Component/MessageBox/PromptContextRenderer.php index dcb3610655fb..69693cad9e0e 100644 --- a/components/ILIAS/UI/src/Implementation/Component/MessageBox/PromptContextRenderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/MessageBox/PromptContextRenderer.php @@ -60,6 +60,8 @@ public function render(Component\Component $component, RendererInterface $defaul ); $tpl->setVariable("LINK_LIST", $default_renderer->render($unordered)); + } elseif ($component->getEntityListing() !== null) { + $tpl->setVariable('LINK_LIST', $default_renderer->render($component->getEntityListing())); } $tpl->touchBlock($component->getType() . "_class"); diff --git a/components/ILIAS/UI/src/Implementation/Component/MessageBox/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/MessageBox/Renderer.php index 3423a3e8f89e..69954e8ff9d4 100755 --- a/components/ILIAS/UI/src/Implementation/Component/MessageBox/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/MessageBox/Renderer.php @@ -67,6 +67,8 @@ public function render(Component\Component $component, RendererInterface $defaul ); $tpl->setVariable("LINK_LIST", $default_renderer->render($unordered)); + } elseif ($component->getEntityListing() !== null) { + $tpl->setVariable("LINK_LIST", $default_renderer->render($component->getEntityListing())); } $tpl->touchBlock($component->getType() . "_class"); diff --git a/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php b/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php new file mode 100644 index 000000000000..8e524ec0a6fa --- /dev/null +++ b/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php @@ -0,0 +1,97 @@ + + * Example for rendering a confirmation message box with entity list. + * + * expected output: > + * ILIAS shows a yellow box with a listing of entities and two buttons. + * Clicking the buttons does not do anything. + * --- + */ +function confirmationWithEntityList(): string +{ + global $DIC; + $f = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + $buttons = [$f->button()->standard('Confirm', '#'), $f->button()->standard('Cancel', '#')]; + + $record_to_entity = new class () implements RecordToEntity { + public function map(UIFactory $ui_factory, mixed $record): Entity + { + [$abbreviation, $login, $email, $name, $last_seen, $active] = $record; + $avatar = $ui_factory->symbol()->avatar()->letter($abbreviation); + + return $ui_factory->entity()->standard($name, $avatar) + ->withMainDetails( + $ui_factory->listing()->property() + ->withProperty('login', $login) + ->withProperty('mail', $email, false) + ); + } + }; + + $data = new class () implements DataRetrieval { + protected array $data = [ + ['jw', 'jimmywilson', 'jimmywilson@example.com', 'Jimmy Wilson', '2022-03-15 13:20:10', true], + ['eb', 'emilybrown', 'emilybrown@example.com', 'Emily Brown', '2022-03-16 10:45:32', false], + ['ms', 'michaelscott', 'michaelscott@example.com', 'Michael Scott', '2022-03-14 08:15:05', true], + ['kj', 'katiejones', 'katiejones@example.com', 'Katie Jones', '2022-03-17 15:30:50', true] + ]; + + public function getEntities( + Mapping $mapping, + ?Range $range, + ?array $additional_parameters + ): Generator { + foreach ($this->data as $usr) { + yield $mapping->map($usr); + } + } + }; + + $buttons = [$f->button()->standard('Confirm', '#'), $f->button()->standard('Cancel', '#')]; + + return $renderer->render( + $f->messageBox() + ->confirmation( + 'Do you really want to delete these items' + )->withEntityListing( + $f->listing()->entity()->standard( + $record_to_entity + )->withData($data) + )->withButtons( + $buttons + ) + ); +} diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation_prompt.php b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation_prompt.php new file mode 100644 index 000000000000..a778ecaf888b --- /dev/null +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation_prompt.php @@ -0,0 +1,120 @@ + + * + * expected output: > + * --- + */ +function confirmation_prompt() +{ + global $DIC; + $factory = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + $df = new \ILIAS\Data\Factory(); + $refinery = $DIC['refinery']; + + $here_uri = $df->uri($DIC->http()->request()->getUri()->__toString()); + $url_builder = new URLBuilder($here_uri); + + $record_to_entity = new class () implements RecordToEntity { + public function map(UIFactory $ui_factory, mixed $record): Entity + { + list($abbreviation, $login, $email, $name, $last_seen, $active) = $record; + $avatar = $ui_factory->symbol()->avatar()->letter($abbreviation); + + return $ui_factory->entity()->standard($name, $avatar) + ->withMainDetails( + $ui_factory->listing()->property() + ->withProperty('login', $login) + ->withProperty('mail', $email, false) + ); + } + }; + + $data = new class () implements DataRetrieval { + protected array $data = [ + ['jw', 'jimmywilson', 'jimmywilson@example.com', 'Jimmy Wilson', '2022-03-15 13:20:10', true], + ['eb', 'emilybrown', 'emilybrown@example.com', 'Emily Brown', '2022-03-16 10:45:32', false], + ['ms', 'michaelscott', 'michaelscott@example.com', 'Michael Scott', '2022-03-14 08:15:05', true], + ['kj', 'katiejones', 'katiejones@example.com', 'Katie Jones', '2022-03-17 15:30:50', true] + ]; + + public function getEntities( + Mapping $mapping, + ?Range $range, + ?array $additional_parameters + ): Generator { + foreach ($this->data as $usr) { + yield $mapping->map($usr); + } + } + }; + + $buttons = [$factory->button()->standard('Confirm', '#'), $factory->button()->standard('Cancel', '#')]; + + $message = $factory->messageBox()->confirmation('some message box') + ->withButtons($buttons) + ->withEntityListing($factory->listing()->entity()->standard($record_to_entity)->withData($data)); + + // when expecting a state, we do not want to render other examples + $example_namespace = ['prompt', 'endpoints']; + [$url_builder, $endpointtoken] = $url_builder->acquireParameters($example_namespace, 'endpoint'); + $url_builder = $url_builder->withParameter($endpointtoken, 'true'); + + // build the prompt + $query_namespace = ['prompt', 'example_conf']; + [$url_builder, $token] = $url_builder->acquireParameters($query_namespace, 'show'); + $url_builder = $url_builder->withParameter($token, 'true'); + $prompt = $factory->prompt()->standard($url_builder->buildURI()); + + // build the endpoint returning the wrapped message + $query = $DIC->http()->wrapper()->query(); + if ($query->has($token->getName())) { + $response = $factory->prompt()->state()->show($message); + echo $renderer->renderAsync($response); + exit; + } + + // a button to open the prompt: + $show_button = $factory->button()->standard('Show Simple Prompt', $prompt->getShowSignal()); + + if (!$query->has($endpointtoken->getName())) { + return $renderer->render([ + $message, + $prompt, + $show_button + ]); + } +} From 5594723d47114a205b955e10f1117599a97bc6a6 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Mon, 7 Apr 2025 15:34:10 +0200 Subject: [PATCH 02/14] Review Exception renaming and renderer removal --- ...ksAndEntitiesMustBeExclusiveException.php} | 4 +-- .../src/Component/MessageBox/MessageBox.php | 4 +-- .../EntityListingRendererFactory.php | 27 ------------------- .../Component/MessageBox/MessageBox.php | 4 +-- 4 files changed, 6 insertions(+), 33 deletions(-) rename components/ILIAS/UI/src/Component/MessageBox/Exception/{LinksAndEntityListConflictException.php => LinksAndEntitiesMustBeExclusiveException.php} (90%) delete mode 100644 components/ILIAS/UI/src/Implementation/Component/MessageBox/EntityListingRendererFactory.php diff --git a/components/ILIAS/UI/src/Component/MessageBox/Exception/LinksAndEntityListConflictException.php b/components/ILIAS/UI/src/Component/MessageBox/Exception/LinksAndEntitiesMustBeExclusiveException.php similarity index 90% rename from components/ILIAS/UI/src/Component/MessageBox/Exception/LinksAndEntityListConflictException.php rename to components/ILIAS/UI/src/Component/MessageBox/Exception/LinksAndEntitiesMustBeExclusiveException.php index a548cdf0f677..c2baca93f169 100644 --- a/components/ILIAS/UI/src/Component/MessageBox/Exception/LinksAndEntityListConflictException.php +++ b/components/ILIAS/UI/src/Component/MessageBox/Exception/LinksAndEntitiesMustBeExclusiveException.php @@ -20,13 +20,13 @@ namespace ILIAS\UI\Component\MessageBox\Exception; -use Exception; +use DomainException; /** * This exception indicates that an UI component was accepted by the JF but is * not backed by a real implementation. */ -class LinksAndEntityListConflictException extends Exception +class LinksAndEntitiesMustBeExclusiveException extends DomainException { public function __construct() { diff --git a/components/ILIAS/UI/src/Component/MessageBox/MessageBox.php b/components/ILIAS/UI/src/Component/MessageBox/MessageBox.php index 637b87f1e5f1..ad3d3d2b61b4 100755 --- a/components/ILIAS/UI/src/Component/MessageBox/MessageBox.php +++ b/components/ILIAS/UI/src/Component/MessageBox/MessageBox.php @@ -69,7 +69,7 @@ public function withButtons(array $buttons): MessageBox; * Get a Message Box like this, but with links. * * @param \ILIAS\UI\Component\Link\Standard[] $links - * @throws Exception\LinksAndEntityListConflictException + * @throws Exception\LinksAndEntitiesMustBeExclusiveException */ public function withLinks(array $links): MessageBox; @@ -77,7 +77,7 @@ public function withLinks(array $links): MessageBox; * Get a Message Box like this, but with an entity listing. * * @param \ILIAS\UI\Component\Listing\Entity\EntityListing $entity_list - * @throws Exception\LinksAndEntityListConflictException + * @throws Exception\LinksAndEntitiesMustBeExclusiveException */ public function withEntityListing(\ILIAS\UI\Component\Listing\Entity\EntityListing $entity_list): MessageBox; diff --git a/components/ILIAS/UI/src/Implementation/Component/MessageBox/EntityListingRendererFactory.php b/components/ILIAS/UI/src/Implementation/Component/MessageBox/EntityListingRendererFactory.php deleted file mode 100644 index 735738478da9..000000000000 --- a/components/ILIAS/UI/src/Implementation/Component/MessageBox/EntityListingRendererFactory.php +++ /dev/null @@ -1,27 +0,0 @@ -entity_list)) { - throw new C\MessageBox\Exception\LinksAndEntityListConflictException(); + throw new C\MessageBox\Exception\LinksAndEntitiesMustBeExclusiveException(); } $types = array(C\Component::class); @@ -116,7 +116,7 @@ public function withLinks(array $links): C\MessageBox\MessageBox public function withEntityListing(C\Listing\Entity\EntityListing $entity_list): C\MessageBox\MessageBox { if (!empty($this->links)) { - throw new C\MessageBox\Exception\LinksAndEntityListConflictException(); + throw new C\MessageBox\Exception\LinksAndEntitiesMustBeExclusiveException(); } $clone = clone $this; From 468214308d12d29a3f34af6cabb8ef0a74196829 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Thu, 10 Apr 2025 09:53:35 +0200 Subject: [PATCH 03/14] New Renderer for Entities inside MessageBox --- components/ILIAS/UI/UI.php | 11 ++++ .../Entity/EntityRendererFactory.php | 56 +++++++++++++++++++ .../Component/Entity/MessageBoxRenderer.php | 44 +++++++++++++++ .../UI/src/Implementation/Render/FSLoader.php | 5 ++ .../default/Entity/tpl.entity-abstract.html | 3 + 5 files changed, 119 insertions(+) create mode 100644 components/ILIAS/UI/src/Implementation/Component/Entity/EntityRendererFactory.php create mode 100644 components/ILIAS/UI/src/Implementation/Component/Entity/MessageBoxRenderer.php create mode 100644 components/ILIAS/UI/src/templates/default/Entity/tpl.entity-abstract.html diff --git a/components/ILIAS/UI/UI.php b/components/ILIAS/UI/UI.php index ca489d6225c0..519d40983e01 100644 --- a/components/ILIAS/UI/UI.php +++ b/components/ILIAS/UI/UI.php @@ -551,6 +551,17 @@ public function init( $use[UI\HelpTextRetriever::class], $internal[UI\Implementation\Component\Input\UploadLimitResolver::class], ), + ), + new UI\Implementation\Component\Entity\EntityRendererFactory( + $use[UI\Implementation\FactoryInternal::class], + $internal[UI\Implementation\Render\TemplateFactory::class], + $use[Language\Language::class], + $internal[UI\Implementation\Render\JavaScriptBinding::class], + $use[UI\Implementation\Render\ImagePathResolver::class], + $pull[Data\Factory::class], + $use[UI\HelpTextRetriever::class], + $internal[UI\Implementation\Component\Input\UploadLimitResolver::class], + ) ) ) ); diff --git a/components/ILIAS/UI/src/Implementation/Component/Entity/EntityRendererFactory.php b/components/ILIAS/UI/src/Implementation/Component/Entity/EntityRendererFactory.php new file mode 100644 index 000000000000..fb013a0c08ce --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Entity/EntityRendererFactory.php @@ -0,0 +1,56 @@ +ui_factory, + $this->tpl_factory, + $this->lng, + $this->js_binding, + $this->image_path_resolver, + $this->data_factory, + $this->help_text_retriever, + $this->upload_limit_resolver + ); + } + + return new Renderer( + $this->ui_factory, + $this->tpl_factory, + $this->lng, + $this->js_binding, + $this->image_path_resolver, + $this->data_factory, + $this->help_text_retriever, + $this->upload_limit_resolver + ); + } +} diff --git a/components/ILIAS/UI/src/Implementation/Component/Entity/MessageBoxRenderer.php b/components/ILIAS/UI/src/Implementation/Component/Entity/MessageBoxRenderer.php new file mode 100644 index 000000000000..f7b9e3d1d402 --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Entity/MessageBoxRenderer.php @@ -0,0 +1,44 @@ +renderEntity($component, $default_renderer); + } + $this->cannotHandleComponent($component); + } + + protected function renderEntity(Entity $component, RendererInterface $default_renderer): string + { + $tpl = $this->getTemplate('tpl.entity-abstract.html', true, true); + $tpl->setVariable('CONTENT', $component->getPrimaryIdentifier()); + + return $tpl->get(); + } +} diff --git a/components/ILIAS/UI/src/Implementation/Render/FSLoader.php b/components/ILIAS/UI/src/Implementation/Render/FSLoader.php index e7e5ff088705..fca01c3d86df 100755 --- a/components/ILIAS/UI/src/Implementation/Render/FSLoader.php +++ b/components/ILIAS/UI/src/Implementation/Render/FSLoader.php @@ -28,6 +28,7 @@ use ILIAS\UI\Implementation\Component\MessageBox\MessageBox; use ILIAS\UI\Implementation\Component\Input\Container\Form\Form; use ILIAS\UI\Implementation\Component\Menu\Menu; +use ILIAS\UI\Component\Entity\Entity; /** * Loads renderers for components from the file system. @@ -51,6 +52,7 @@ public function __construct( private RendererFactory $message_box_renderer_factory, private RendererFactory $form_renderer_factory, private RendererFactory $menu_renderer_factory, + private RendererFactory $entity_renderer_factory, ) { } @@ -75,6 +77,9 @@ public function getRendererFactoryFor(Component $component): RendererFactory if ($component instanceof MessageBox) { return $this->message_box_renderer_factory; } + if ($component instanceof Entity) { + return $this->entity_renderer_factory; + } if ($component instanceof Form) { return $this->form_renderer_factory; } diff --git a/components/ILIAS/UI/src/templates/default/Entity/tpl.entity-abstract.html b/components/ILIAS/UI/src/templates/default/Entity/tpl.entity-abstract.html new file mode 100644 index 000000000000..3a62c7c338cc --- /dev/null +++ b/components/ILIAS/UI/src/templates/default/Entity/tpl.entity-abstract.html @@ -0,0 +1,3 @@ +
+ {CONTENT} +
\ No newline at end of file From 6861ead39a651bc96c175b5a28952b48a5ffe70f Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Tue, 6 May 2025 13:33:45 +0200 Subject: [PATCH 04/14] Fix Unit Tests for Feature ConfirmationModals --- components/ILIAS/UI/tests/Base.php | 11 +++++++++++ components/ILIAS/UI/tests/InitUIFramework.php | 10 ++++++++++ components/ILIAS/UI/tests/Renderer/FSLoaderTest.php | 11 +++++++++++ 3 files changed, 32 insertions(+) diff --git a/components/ILIAS/UI/tests/Base.php b/components/ILIAS/UI/tests/Base.php index a82b6f9d7ad7..4639291ce7ea 100755 --- a/components/ILIAS/UI/tests/Base.php +++ b/components/ILIAS/UI/tests/Base.php @@ -46,6 +46,7 @@ use ILIAS\UI\Implementation\Component\Input\UploadLimitResolver; use ILIAS\UI\Implementation\Component\MessageBox\MessageBoxRendererFactory; use ILIAS\UI\Implementation\Component\Input\Container\Form\FormRendererFactory; +use ILIAS\UI\Implementation\Component\Entity\EntityRendererFactory; class ilIndependentTemplateFactory implements TemplateFactory { @@ -479,6 +480,16 @@ public function getDefaultRenderer( $data_factory, $help_text_retriever, $this->getUploadLimitResolver(), + ), + new EntityRendererFactory( + $ui_factory, + $tpl_factory, + $lng, + $js_binding, + $image_path_resolver, + $data_factory, + $help_text_retriever, + $this->getUploadLimitResolver() ) ) ) diff --git a/components/ILIAS/UI/tests/InitUIFramework.php b/components/ILIAS/UI/tests/InitUIFramework.php index 0ba2a1c745e5..fc2bcf2b4807 100755 --- a/components/ILIAS/UI/tests/InitUIFramework.php +++ b/components/ILIAS/UI/tests/InitUIFramework.php @@ -381,6 +381,16 @@ public function getRefreshIntervalInMs(): int $c["help.text_retriever"], $c["ui.upload_limit_resolver"] ), + new ILIAS\UI\Implementation\Component\Entity\EntityRendererFactory( + $c["ui.factory"], + $c["ui.template_factory"], + $c["lng"], + $c["ui.javascript_binding"], + $c["ui.pathresolver"], + $c["ui.data_factory"], + $c["help.text_retriever"], + $c["ui.upload_limit_resolver"] + ), ) ) ); diff --git a/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php b/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php index 689fbfd80d76..26f328cdb6cb 100755 --- a/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php +++ b/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php @@ -28,6 +28,7 @@ use ILIAS\UI\Implementation\Component\MessageBox\MessageBox; use ILIAS\UI\Implementation\Component\Input\Container\Form\Form; use ILIAS\UI\Implementation\Component\Menu\Menu; +use ILIAS\UI\Component\Entity\Entity; use ILIAS\UI\Component\Component; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -41,6 +42,7 @@ class FSLoaderTest extends TestCase protected RendererFactory & MockObject $message_box_renderer_factory; protected RendererFactory & MockObject $form_renderer_factory; protected RendererFactory & MockObject $menu_renderer_factory; + protected RendererFactory & MockObject $entity_renderer_factory; protected FSLoader $fs_loader; @@ -52,6 +54,7 @@ protected function setUp(): void $this->message_box_renderer_factory = $this->createMock(RendererFactory::class); $this->form_renderer_factory = $this->createMock(RendererFactory::class); $this->menu_renderer_factory = $this->createMock(RendererFactory::class); + $this->entity_renderer_factory = $this->createMock(RendererFactory::class); $this->fs_loader = new FSLoader( $this->default_renderer_factory, @@ -60,6 +63,7 @@ protected function setUp(): void $this->message_box_renderer_factory, $this->form_renderer_factory, $this->menu_renderer_factory, + $this->entity_renderer_factory, ); parent::setUp(); @@ -139,6 +143,13 @@ public function testGetRendererFactoryForButton(): void $this->assertSame($factory, $this->button_renderer_factory); } + public function testGetRendererFactoryForEntity(): void + { + $component_mock = $this->createMock(Entity::class); + $factory = $this->fs_loader->getRendererFactoryFor($component_mock); + $this->assertSame($factory, $this->entity_renderer_factory); + } + public function testGetRendererFactoryForOther(): void { $component_mock = $this->createMock(Component::class); From 88ad8031f2a8c976fe806a431f5c1cf6090bbd6e Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Wed, 27 Aug 2025 11:00:02 +0200 Subject: [PATCH 05/14] Remove copyright from ConfirmationModal examples --- components/ILIAS/UI/UI.php | 1 - .../Confirmation/confirmationWithEntityList.php | 16 ---------------- .../Prompt/Standard/confirmation_prompt.php | 16 ---------------- 3 files changed, 33 deletions(-) diff --git a/components/ILIAS/UI/UI.php b/components/ILIAS/UI/UI.php index 519d40983e01..12d0996a8500 100644 --- a/components/ILIAS/UI/UI.php +++ b/components/ILIAS/UI/UI.php @@ -551,7 +551,6 @@ public function init( $use[UI\HelpTextRetriever::class], $internal[UI\Implementation\Component\Input\UploadLimitResolver::class], ), - ), new UI\Implementation\Component\Entity\EntityRendererFactory( $use[UI\Implementation\FactoryInternal::class], $internal[UI\Implementation\Render\TemplateFactory::class], diff --git a/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php b/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php index 8e524ec0a6fa..f17ef37f19af 100644 --- a/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php +++ b/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php @@ -1,21 +1,5 @@ Date: Thu, 28 Aug 2025 09:48:23 +0200 Subject: [PATCH 06/14] Add missing copyrights --- .../Confirmation/confirmationWithEntityList.php | 16 ++++++++++++++++ .../Prompt/Standard/confirmation_prompt.php | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php b/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php index f17ef37f19af..8e524ec0a6fa 100644 --- a/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php +++ b/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php @@ -1,5 +1,21 @@ Date: Thu, 2 Jul 2026 12:03:33 +0200 Subject: [PATCH 07/14] [Feature] UI: refactor entity listing retrieval API --- .../ILIAS/UI/src/Component/Entity/Entity.php | 5 + .../src/Component/Entity/EntityRetrieval.php | 61 +++++++++++ .../ILIAS/UI/src/Component/Entity/Factory.php | 8 +- .../Listing/Entity/DataRetrieval.php | 40 ------- .../Entity/{Mapping.php => Entity.php} | 7 +- .../Listing/Entity/EntityListing.php | 36 ------- .../src/Component/Listing/Entity/Factory.php | 10 +- .../UI/src/Component/Listing/Entity/Grid.php | 2 +- .../Listing/Entity/RecordToEntity.php | 32 ------ .../src/Component/Listing/Entity/Standard.php | 2 +- .../src/Component/MessageBox/MessageBox.php | 8 +- .../Component/Entity/Entity.php | 6 ++ .../Component/Entity/Factory.php | 7 +- .../Listing/Entity/EntityListing.php | 46 ++++---- .../Component/Listing/Entity/Factory.php | 9 +- .../Component/Listing/Entity/Renderer.php | 3 +- .../Component/MessageBox/MessageBox.php | 6 +- .../UI/src/examples/Entity/Standard/base.php | 1 + .../Entity/Standard/semantic_groups.php | 2 +- .../examples/Listing/Entity/Standard/base.php | 100 ++++++++++-------- .../confirmationWithEntityList.php | 100 +++++++++--------- .../Listing/Entity/EntityListingTest.php | 62 +++++------ 22 files changed, 264 insertions(+), 289 deletions(-) create mode 100644 components/ILIAS/UI/src/Component/Entity/EntityRetrieval.php delete mode 100755 components/ILIAS/UI/src/Component/Listing/Entity/DataRetrieval.php rename components/ILIAS/UI/src/Component/Listing/Entity/{Mapping.php => Entity.php} (78%) mode change 100755 => 100644 delete mode 100755 components/ILIAS/UI/src/Component/Listing/Entity/EntityListing.php delete mode 100755 components/ILIAS/UI/src/Component/Listing/Entity/RecordToEntity.php diff --git a/components/ILIAS/UI/src/Component/Entity/Entity.php b/components/ILIAS/UI/src/Component/Entity/Entity.php index 1ed2beaef8d5..818893344185 100755 --- a/components/ILIAS/UI/src/Component/Entity/Entity.php +++ b/components/ILIAS/UI/src/Component/Entity/Entity.php @@ -35,6 +35,11 @@ */ interface Entity extends Component { + /** + * Returns the technical identifier of this entity. + */ + public function getId(): string|int; + //Priority Areas /** diff --git a/components/ILIAS/UI/src/Component/Entity/EntityRetrieval.php b/components/ILIAS/UI/src/Component/Entity/EntityRetrieval.php new file mode 100644 index 000000000000..330a080feec7 --- /dev/null +++ b/components/ILIAS/UI/src/Component/Entity/EntityRetrieval.php @@ -0,0 +1,61 @@ + + */ + public function getEntities( + \ILIAS\UI\Factory $ui_factory, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters, + ): Generator; + + /** + * This method is used by the Prompt State to confirm a subset of entities. + * + * @param array $entity_ids + * @return Generator + */ + public function getEntitiesByIds( + \ILIAS\UI\Factory $ui_factory, + Order $order, + array $entity_ids, + ): Generator; +} diff --git a/components/ILIAS/UI/src/Component/Entity/Factory.php b/components/ILIAS/UI/src/Component/Entity/Factory.php index a85d2e8135f6..37c4d9a42905 100755 --- a/components/ILIAS/UI/src/Component/Entity/Factory.php +++ b/components/ILIAS/UI/src/Component/Entity/Factory.php @@ -37,10 +37,14 @@ interface Factory * The Standard Entity can (and should) be used to list system entities * such as repository objects, users and similar. * --- + * @param string|int $technical_identifier + * @param Symbol|Image|ShyButton|ShyLink|string $primary_visual_identifier + * @param Symbol|Image|ShyButton|ShyLink|string $secondary_visual_identifier * @return \ILIAS\UI\Component\Entity\Standard */ public function standard( - Symbol | Image | ShyButton | ShyLink | string $primary_identifier, - Symbol | Image | ShyButton | ShyLink | string $secondary_identifier + string|int $technical_identifier, + Symbol|Image|ShyButton|ShyLink|string $primary_visual_identifier, + Symbol|Image|ShyButton|ShyLink|string $secondary_visual_identifier, ): Standard; } diff --git a/components/ILIAS/UI/src/Component/Listing/Entity/DataRetrieval.php b/components/ILIAS/UI/src/Component/Listing/Entity/DataRetrieval.php deleted file mode 100755 index ff9f49f40189..000000000000 --- a/components/ILIAS/UI/src/Component/Listing/Entity/DataRetrieval.php +++ /dev/null @@ -1,40 +0,0 @@ - $additional_parameters - * @return \Generator - */ - public function getEntities( - Mapping $mapping, - ?Range $range, - ?array $additional_parameters - ): \Generator; -} diff --git a/components/ILIAS/UI/src/Component/Listing/Entity/Mapping.php b/components/ILIAS/UI/src/Component/Listing/Entity/Entity.php old mode 100755 new mode 100644 similarity index 78% rename from components/ILIAS/UI/src/Component/Listing/Entity/Mapping.php rename to components/ILIAS/UI/src/Component/Listing/Entity/Entity.php index ea0f31764cf3..be71e6311bbe --- a/components/ILIAS/UI/src/Component/Listing/Entity/Mapping.php +++ b/components/ILIAS/UI/src/Component/Listing/Entity/Entity.php @@ -20,12 +20,11 @@ namespace ILIAS\UI\Component\Listing\Entity; -use ILIAS\UI\Component\Entity\Entity; +use ILIAS\UI\Component\Component; /** - * Hand a record over to RecordToEntity and factor an Entity + * This is what an EntityListings looks like */ -interface Mapping +interface Entity extends Component { - public function map(mixed $record): Entity; } diff --git a/components/ILIAS/UI/src/Component/Listing/Entity/EntityListing.php b/components/ILIAS/UI/src/Component/Listing/Entity/EntityListing.php deleted file mode 100755 index 6f5c98022c95..000000000000 --- a/components/ILIAS/UI/src/Component/Listing/Entity/EntityListing.php +++ /dev/null @@ -1,36 +0,0 @@ -id; + } + public function withPrimaryIdentifier(Symbol | Image | Shy | StandardLink | string $primary_identifier): self { $clone = clone $this; diff --git a/components/ILIAS/UI/src/Implementation/Component/Entity/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Entity/Factory.php index 46ff666c23d8..363f4d5b6860 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Entity/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Entity/Factory.php @@ -29,9 +29,10 @@ class Factory implements I\Factory { public function standard( - Symbol | Image | ShyButton | ShyLink | string $primary_identifier, - Symbol | Image | ShyButton | ShyLink | string $secondary_identifier + string|int $technical_identifier, + Symbol|Image|ShyButton|ShyLink|string $primary_visual_identifier, + Symbol|Image|ShyButton|ShyLink|string $secondary_visual_identifier, ): Standard { - return new Standard($primary_identifier, $secondary_identifier); + return new Standard($technical_identifier, $primary_visual_identifier, $secondary_visual_identifier); } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/EntityListing.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/EntityListing.php index ff840cdb2f28..f2e61bfacc41 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/EntityListing.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/EntityListing.php @@ -24,48 +24,38 @@ use ILIAS\UI\Component\Entity as IEntity; use ILIAS\UI\Implementation\Component\ComponentHelper; use ILIAS\Data\Range; +use ILIAS\Data\Order; -abstract class EntityListing implements I\EntityListing +abstract class EntityListing implements I\Entity { use ComponentHelper; - protected I\DataRetrieval $data; - public function __construct( - protected I\RecordToEntity $entity_mapping + protected IEntity\EntityRetrieval $entity_retrieval ) { } - public function withData(I\DataRetrieval $data): self - { - $clone = clone $this; - $clone->data = $data; - return $clone; - } - /** - * @param array $additional_parameters * @return \Generator */ public function getEntities( \ILIAS\UI\Factory $ui_factory, ?Range $range = null, - ?array $additional_parameters = null + ?Order $order = null, + mixed $additional_viewcontrol_data = null, + mixed $filter_data = null, + mixed $additional_parameters = null, ): \Generator { - $mapping = new class ($this->entity_mapping, $ui_factory) implements I\Mapping { - public function __construct( - protected I\RecordToEntity $mapper, - protected \ILIAS\UI\Factory $ui_factory - ) { - } - - public function map(mixed $record): IEntity\Entity - { - return $this->mapper->map($this->ui_factory, $record); - } - }; - - $additional_parameters = null; - return $this->data->getEntities($mapping, $range, $additional_parameters); + $range = $range ?? new Range(0, PHP_INT_MAX); + $order = $order ?? new Order('id', Order::ASC); + + yield from $this->entity_retrieval->getEntities( + $ui_factory, + $range, + $order, + $additional_viewcontrol_data, + $filter_data, + $additional_parameters, + ); } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Factory.php index e8fe586bdceb..e683676c55fc 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Factory.php @@ -21,16 +21,17 @@ namespace ILIAS\UI\Implementation\Component\Listing\Entity; use ILIAS\UI\Component\Listing\Entity as I; +use ILIAS\UI\Component\Entity\EntityRetrieval; class Factory implements I\Factory { - public function standard(I\RecordToEntity $mapping): Standard + public function standard(EntityRetrieval $entity_retrieval): Standard { - return new Standard($mapping); + return new Standard($entity_retrieval); } - public function grid(I\RecordToEntity $mapping): Grid + public function grid(EntityRetrieval $entity_retrieval): Grid { - return new Grid($mapping); + return new Grid($entity_retrieval); } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Renderer.php index 192769e8091a..6068a0abc0dd 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/Renderer.php @@ -23,8 +23,6 @@ use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; use ILIAS\UI\Renderer as RendererInterface; use ILIAS\UI\Component; -use ILIAS\UI\Implementation\Render\ResourceRegistry; -use ILIAS\UI\Implementation\Render\Template; class Renderer extends AbstractComponentRenderer { @@ -55,6 +53,7 @@ protected function renderEntityListingStandard(EntityListing $component, Rendere } return $tpl->get(); } + protected function renderEntityListingGrid(EntityListing $component, RendererInterface $default_renderer): string { $tpl = $this->getTemplate('tpl.entitylistinggrid.html', true, true); diff --git a/components/ILIAS/UI/src/Implementation/Component/MessageBox/MessageBox.php b/components/ILIAS/UI/src/Implementation/Component/MessageBox/MessageBox.php index 0be62d093cb9..f3c736742828 100755 --- a/components/ILIAS/UI/src/Implementation/Component/MessageBox/MessageBox.php +++ b/components/ILIAS/UI/src/Implementation/Component/MessageBox/MessageBox.php @@ -42,7 +42,7 @@ class MessageBox implements C\MessageBox\MessageBox, IsPromptContentInternal private string $message_text; private array $buttons = []; private array $links = []; - private ?C\Listing\Entity\EntityListing $entity_list = null; + private ?C\Listing\Entity\Entity $entity_list = null; public function __construct($type, string $message_text) { @@ -113,7 +113,7 @@ public function withLinks(array $links): C\MessageBox\MessageBox return $clone; } - public function withEntityListing(C\Listing\Entity\EntityListing $entity_list): C\MessageBox\MessageBox + public function withEntityListing(C\Listing\Entity\Entity $entity_list): C\MessageBox\MessageBox { if (!empty($this->links)) { throw new C\MessageBox\Exception\LinksAndEntitiesMustBeExclusiveException(); @@ -125,7 +125,7 @@ public function withEntityListing(C\Listing\Entity\EntityListing $entity_list): return $clone; } - public function getEntityListing(): ?C\Listing\Entity\EntityListing + public function getEntityListing(): ?C\Listing\Entity\Entity { return $this->entity_list; } diff --git a/components/ILIAS/UI/src/examples/Entity/Standard/base.php b/components/ILIAS/UI/src/examples/Entity/Standard/base.php index 33b6dd521876..3fb27a80cc83 100755 --- a/components/ILIAS/UI/src/examples/Entity/Standard/base.php +++ b/components/ILIAS/UI/src/examples/Entity/Standard/base.php @@ -59,6 +59,7 @@ function base() // creating the entity object now so it can be filled in the logic section $entity = $f->entity()->standard( + 'event-demo', $primary_id, $secondary_id ); diff --git a/components/ILIAS/UI/src/examples/Entity/Standard/semantic_groups.php b/components/ILIAS/UI/src/examples/Entity/Standard/semantic_groups.php index 36ff079ce184..01035410e1d7 100755 --- a/components/ILIAS/UI/src/examples/Entity/Standard/semantic_groups.php +++ b/components/ILIAS/UI/src/examples/Entity/Standard/semantic_groups.php @@ -45,7 +45,7 @@ function semantic_groups() $f = $DIC->ui()->factory(); $renderer = $DIC->ui()->renderer(); - $entity = $f->entity()->standard('Primary Identifier', 'Secondary Identifier') + $entity = $f->entity()->standard('demo-entity', 'Primary Identifier', 'Secondary Identifier') ->withBlockingAvailabilityConditions($f->legacy()->content('Blocking Conditions')) ->withFeaturedProperties($f->legacy()->content('Featured Properties')) ->withPersonalStatus($f->legacy()->content('Personal Status')) diff --git a/components/ILIAS/UI/src/examples/Listing/Entity/Standard/base.php b/components/ILIAS/UI/src/examples/Listing/Entity/Standard/base.php index 3e445f019dcc..e7310cef38d6 100755 --- a/components/ILIAS/UI/src/examples/Listing/Entity/Standard/base.php +++ b/components/ILIAS/UI/src/examples/Listing/Entity/Standard/base.php @@ -20,12 +20,11 @@ namespace ILIAS\UI\Examples\Listing\Entity\Standard; -use ILIAS\UI\Factory as UIFactory; -use ILIAS\UI\Component\Listing\Entity\RecordToEntity; -use ILIAS\UI\Component\Listing\Entity\DataRetrieval; -use ILIAS\UI\Component\Entity\Entity; -use ILIAS\UI\Component\Listing\Entity\Mapping; +use Generator; +use ILIAS\Data\Order; use ILIAS\Data\Range; +use ILIAS\UI\Component\Entity\Entity; +use ILIAS\UI\Component\Entity\EntityRetrieval; /** * --- @@ -42,48 +41,63 @@ function base() $f = $DIC->ui()->factory(); $renderer = $DIC->ui()->renderer(); - $record_to_entity = new class () implements RecordToEntity { - public function map(UIFactory $ui_factory, mixed $record): Entity - { - list($abbreviation, $login, $email, $name, $last_seen, $active) = $record; - $avatar = $ui_factory->symbol()->avatar()->letter($abbreviation); - return $ui_factory->entity()->standard($name, $avatar) - ->withMainDetails( - $ui_factory->listing()->property() - ->withProperty('login', $login) - ->withProperty('mail', $email, false) - ) - ->withDetails( - $ui_factory->listing()->property() - ->withItems([ - ['last seen', $last_seen], - ['active', $active ? 'yes' : 'no'], - ]) - ); - } - }; + $listing = $f->listing()->entity()->standard(new DemoEntityRetrieval()); + + return $renderer->render($listing); +} - $data = new class () implements DataRetrieval { - protected $data = [ - ['jw', 'jimmywilson','jimmywilson@example.com', 'Jimmy Wilson', '2022-03-15 13:20:10', true], - ['eb', 'emilybrown','emilybrown@example.com','Emily Brown','2022-03-16 10:45:32', false], - ['ms', 'michaelscott','michaelscott@example.com','Michael Scott','2022-03-14 08:15:05', true], - ['kj', 'katiejones','katiejones@example.com','Katie Jones','2022-03-17 15:30:50',true] - ]; +class DemoEntityRetrieval implements EntityRetrieval +{ + protected array $data = [ + ['jw', 'jimmywilson', 'jimmywilson@example.com', 'Jimmy Wilson', '2022-03-15 13:20:10', true], + ['eb', 'emilybrown', 'emilybrown@example.com', 'Emily Brown', '2022-03-16 10:45:32', false], + ['ms', 'michaelscott', 'michaelscott@example.com', 'Michael Scott', '2022-03-14 08:15:05', true], + ['kj', 'katiejones', 'katiejones@example.com', 'Katie Jones', '2022-03-17 15:30:50', true], + ]; - public function getEntities( - Mapping $mapping, - ?Range $range, - ?array $additional_parameters - ): \Generator { - foreach ($this->data as $usr) { - yield $mapping->map($usr); + public function getEntities( + \ILIAS\UI\Factory $ui_factory, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters, + ): Generator { + foreach ($this->data as $index => $record) { + yield $this->mapRecord($ui_factory, $index, $record); + } + } + + public function getEntitiesByIds( + \ILIAS\UI\Factory $ui_factory, + Order $order, + array $entity_ids, + ): Generator { + foreach ($entity_ids as $entity_id) { + if (!isset($this->data[$entity_id])) { + continue; } + yield $this->mapRecord($ui_factory, $entity_id, $this->data[$entity_id]); } - }; + } - $listing = $f->listing()->entity()->standard($record_to_entity) - ->withData($data); + protected function mapRecord(\ILIAS\UI\Factory $ui_factory, int|string $id, array $record): Entity + { + [$abbreviation, $login, $email, $name, $last_seen, $active] = $record; + $avatar = $ui_factory->symbol()->avatar()->letter($abbreviation); - return $renderer->render($listing); + return $ui_factory->entity()->standard($id, $name, $avatar) + ->withMainDetails( + $ui_factory->listing()->property() + ->withProperty('login', $login) + ->withProperty('mail', $email, false) + ) + ->withDetails( + $ui_factory->listing()->property() + ->withItems([ + ['last seen', $last_seen], + ['active', $active ? 'yes' : 'no'], + ]) + ); + } } diff --git a/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php b/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php index 8e524ec0a6fa..5b9d89075809 100644 --- a/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php +++ b/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php @@ -21,24 +21,18 @@ namespace ILIAS\UI\examples\MessageBox\Confirmation; use Generator; +use ILIAS\Data\Order; use ILIAS\Data\Range; -use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Component\Entity\Entity; -use ILIAS\UI\Component\Listing\Entity\Mapping; -use ILIAS\UI\Component\Listing\Entity\DataRetrieval; -use ILIAS\UI\Component\Listing\Entity\RecordToEntity; +use ILIAS\UI\Component\Entity\EntityRetrieval; /** * --- - * description: > - * Example for rendering a confirmation message box with entity list. - * * expected output: > - * ILIAS shows a yellow box with a listing of entities and two buttons. - * Clicking the buttons does not do anything. + * ILIAS shows a confirmation message box with an entity listing. * --- */ -function confirmationWithEntityList(): string +function confirmationWithEntityList() { global $DIC; $f = $DIC->ui()->factory(); @@ -46,52 +40,56 @@ function confirmationWithEntityList(): string $buttons = [$f->button()->standard('Confirm', '#'), $f->button()->standard('Cancel', '#')]; - $record_to_entity = new class () implements RecordToEntity { - public function map(UIFactory $ui_factory, mixed $record): Entity - { - [$abbreviation, $login, $email, $name, $last_seen, $active] = $record; - $avatar = $ui_factory->symbol()->avatar()->letter($abbreviation); + return $renderer->render( + $f->messageBox()->confirmation('some message box') + ->withButtons($buttons) + ->withEntityListing($f->listing()->entity()->standard(new DemoEntityRetrieval())) + ); +} - return $ui_factory->entity()->standard($name, $avatar) - ->withMainDetails( - $ui_factory->listing()->property() - ->withProperty('login', $login) - ->withProperty('mail', $email, false) - ); - } - }; +class DemoEntityRetrieval implements EntityRetrieval +{ + protected array $data = [ + ['jw', 'jimmywilson', 'jimmywilson@example.com', 'Jimmy Wilson'], + ['eb', 'emilybrown', 'emilybrown@example.com', 'Emily Brown'], + ]; - $data = new class () implements DataRetrieval { - protected array $data = [ - ['jw', 'jimmywilson', 'jimmywilson@example.com', 'Jimmy Wilson', '2022-03-15 13:20:10', true], - ['eb', 'emilybrown', 'emilybrown@example.com', 'Emily Brown', '2022-03-16 10:45:32', false], - ['ms', 'michaelscott', 'michaelscott@example.com', 'Michael Scott', '2022-03-14 08:15:05', true], - ['kj', 'katiejones', 'katiejones@example.com', 'Katie Jones', '2022-03-17 15:30:50', true] - ]; + public function getEntities( + \ILIAS\UI\Factory $ui_factory, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters, + ): Generator { + foreach ($this->data as $index => $record) { + yield $this->mapRecord($ui_factory, $index, $record); + } + } - public function getEntities( - Mapping $mapping, - ?Range $range, - ?array $additional_parameters - ): Generator { - foreach ($this->data as $usr) { - yield $mapping->map($usr); + public function getEntitiesByIds( + \ILIAS\UI\Factory $ui_factory, + Order $order, + array $entity_ids, + ): Generator { + foreach ($entity_ids as $entity_id) { + if (!isset($this->data[$entity_id])) { + continue; } + yield $this->mapRecord($ui_factory, $entity_id, $this->data[$entity_id]); } - }; + } - $buttons = [$f->button()->standard('Confirm', '#'), $f->button()->standard('Cancel', '#')]; + protected function mapRecord(\ILIAS\UI\Factory $ui_factory, int|string $id, array $record): Entity + { + [$abbreviation, $login, $email, $name] = $record; + $avatar = $ui_factory->symbol()->avatar()->letter($abbreviation); - return $renderer->render( - $f->messageBox() - ->confirmation( - 'Do you really want to delete these items' - )->withEntityListing( - $f->listing()->entity()->standard( - $record_to_entity - )->withData($data) - )->withButtons( - $buttons - ) - ); + return $ui_factory->entity()->standard($id, $name, $avatar) + ->withMainDetails( + $ui_factory->listing()->property() + ->withProperty('login', $login) + ->withProperty('mail', $email, false) + ); + } } diff --git a/components/ILIAS/UI/tests/Component/Listing/Entity/EntityListingTest.php b/components/ILIAS/UI/tests/Component/Listing/Entity/EntityListingTest.php index 479f902e0d17..e8894746fff4 100755 --- a/components/ILIAS/UI/tests/Component/Listing/Entity/EntityListingTest.php +++ b/components/ILIAS/UI/tests/Component/Listing/Entity/EntityListingTest.php @@ -18,25 +18,42 @@ declare(strict_types=1); -use ILIAS\UI\Implementation\Component\Listing; -use ILIAS\UI\Implementation\Component\Entity; -use ILIAS\UI\Component as I; -use ILIAS\UI\Factory as UIFactory; +use ILIAS\Data\Order; use ILIAS\Data\Range; +use ILIAS\UI\Component as I; +use ILIAS\UI\Implementation\Component\Entity; +use ILIAS\UI\Implementation\Component\Listing; class EntityListingTest extends ILIAS_UI_TestBase { - public function getEntityMapping(): I\Listing\Entity\RecordToEntity + public function getEntityRetrieval(): I\Entity\EntityRetrieval { - return new class () implements I\Listing\Entity\RecordToEntity { - public function map( - UIFactory $ui_factory, - mixed $record - ): Entity\Entity { - return $ui_factory->entity()->standard('primary', 'secondary'); + return new class () implements I\Entity\EntityRetrieval { + public function getEntities( + ILIAS\UI\Factory $ui_factory, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters, + ): Generator { + for ($i = 1; $i <= 3; $i++) { + yield $ui_factory->entity()->standard($i, 'primary ' . $i, 'secondary ' . $i); + } + } + + public function getEntitiesByIds( + ILIAS\UI\Factory $ui_factory, + Order $order, + array $entity_ids, + ): Generator { + foreach ($entity_ids as $entity_id) { + yield $ui_factory->entity()->standard($entity_id, 'primary ' . $entity_id, 'secondary ' . $entity_id); + } } }; } + public function getUIFactory(): NoUIFactory { return new class () extends NoUIFactory { @@ -48,6 +65,7 @@ public function listing(): Listing\Factory new Listing\Entity\Factory(), ); } + public function entity(): Entity\Factory { return new Entity\Factory(); @@ -58,35 +76,19 @@ public function entity(): Entity\Factory public function testEntityListingFactory(): void { $this->assertInstanceOf( - I\Listing\Entity\EntityListing::class, - $this->getUIFactory()->listing()->entity()->standard($this->getEntityMapping()) + I\Listing\Entity\Entity::class, + $this->getUIFactory()->listing()->entity()->standard($this->getEntityRetrieval()) ); } public function testEntityListingYieldingEntities(): void { - $data = new class () implements I\Listing\Entity\DataRetrieval { - protected $data = [1,2,3]; - - public function getEntities( - I\Listing\Entity\Mapping $mapping, - ?Range $range, - ?array $additional_parameters - ): \Generator { - foreach ($this->data as $entry) { - yield $mapping->map($entry); - } - } - }; - $listing = $this->getUIFactory()->listing()->entity() - ->standard($this->getEntityMapping()) - ->withData($data); + ->standard($this->getEntityRetrieval()); $entities = iterator_to_array($listing->getEntities($this->getUIFactory())); $this->assertCount(3, $entities); - $this->assertInstanceOf(I\Entity\Entity::class, array_pop($entities)); } } From bcc9a82624ae7520fd0c92a28d1ee2637a14ecb8 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Thu, 2 Jul 2026 12:04:10 +0200 Subject: [PATCH 08/14] [Feature] UI: fix async prompt client handling --- .../UI/resources/js/Prompt/dist/prompt.min.js | 2 +- .../resources/js/Prompt/src/prompt.class.js | 41 ++++++++++++------- .../resources/js/Prompt/src/prompt.factory.js | 10 +++-- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/components/ILIAS/UI/resources/js/Prompt/dist/prompt.min.js b/components/ILIAS/UI/resources/js/Prompt/dist/prompt.min.js index 788838ffbdba..7248d4887457 100644 --- a/components/ILIAS/UI/resources/js/Prompt/dist/prompt.min.js +++ b/components/ILIAS/UI/resources/js/Prompt/dist/prompt.min.js @@ -12,4 +12,4 @@ * https://www.ilias.de * https://github.com/ILIAS-eLearning */ -!function(t,e){"use strict";class r{#t;#e;#r;constructor(t,e){if(this.#t=t,this.#e=document.getElementById(e),null===this.#e)throw new Error(`Could not find a Prompt for id '${e}'.`);this.#r=this.#e.getElementsByTagName("dialog").item(0)}show(t){this.load(t),this.#r.showModal()}close(){this.#r.close()}async load(t,e={}){await fetch(t,e).then((t=>t.text())).then((t=>{const e=(new this.#t).parseFromString(t,"text/html"),r=e.querySelector('section[data-section="il-prompt-state__title"]'),n=e.querySelector('section[data-section="il-prompt-state__contents"]'),o=e.querySelector('section[data-section="il-prompt-state__buttons"]'),i=e.querySelector('section[data-section="il-prompt-state__command"]'),s=e.querySelector('section[data-section="il-prompt-state__parameters"]'),a=e.querySelector("script"),c=this.#r.querySelector("span.il-prompt__title"),p=this.#r.querySelector("div.il-prompt__contents"),l=this.#r.querySelector("div.il-prompt__buttons");c.innerHTML=r.innerHTML,p.innerHTML=n.innerHTML,l.innerHTML=o.innerHTML,this.#n(p),this.#o(p),a&&this.#i(a.text);const m=[];return s.querySelectorAll("data").forEach((t=>{m[t.innerHTML.trim()]=t.getAttribute("value")})),{cmd:i.innerHTML.trim(),params:m}})).then((t=>{const e=t.cmd,{params:r}=t;"close"===e&&this.close(),"redirect"===e&&window.location.replace(r.redirect)}))}#i(t){const e=this.#e.querySelector("section.il-prompt__scripts"),r=document.createElement("script");r.text=t,e.innerHTML="",e.appendChild(r)}#n(t){t.getElementsByTagName("form").forEach((t=>{t.addEventListener("submit",(e=>{e.preventDefault(),this.load(t.action,{method:t.method,body:new FormData(t)})}))}))}#o(t){t.getElementsByTagName("a").forEach((t=>{const{target:e}=t;if("_blank"!==e){const e=t.href;t.addEventListener("click",(()=>this.load(e))),t.removeAttribute("href")}}))}}t.UI=t.UI||{},t.UI.prompt=new class{#t;#s=[];constructor(t){this.#t=t}init(t){if(void 0!==this.#s[t])throw new Error(`Prompt with id '${t}' has already been initialized.`);this.#s[t]=new r(this.#t,t)}get(t){return this.#s[t]??null}}(e)}(il,DOMParser); +!function(t,e){"use strict";class r{#t;#e;#r;constructor(t,e){if(this.#t=t,this.#e=document.getElementById(e),null===this.#e)throw new Error(`Could not find a Prompt for id '${e}'.`);this.#r=this.#e.getElementsByTagName("dialog").item(0)}show(t){this.load(t),this.#r.showModal()}close(){this.#r.close()}async load(t,e={}){await fetch(t,e).then((t=>t.text())).then((t=>{const e=(new this.#t).parseFromString(t,"text/html"),r=e.querySelector('section[data-section="il-prompt-state__title"]'),n=e.querySelector('section[data-section="il-prompt-state__contents"]'),o=e.querySelector('section[data-section="il-prompt-state__buttons"]'),i=e.querySelector('section[data-section="il-prompt-state__command"]'),s=e.querySelector('section[data-section="il-prompt-state__parameters"]'),a=e.querySelector("script[data-replace-marker=\"script\"]"),c=this.#r.querySelector("span.il-prompt__title"),p=this.#r.querySelector("div.il-prompt__contents"),l=this.#r.querySelector("div.il-prompt__buttons");r&&(c.innerHTML=r.innerHTML),n&&(p.innerHTML=n.innerHTML),o&&(l.innerHTML=o.innerHTML),this.#n(p),this.#o(p),a&&this.#i(a.text);const m=[];return s&&s.querySelectorAll("data").forEach((t=>{m[t.innerHTML.trim()]=t.getAttribute("value")})),{cmd:i?i.innerHTML.trim():"",params:m}})).then((t=>{const e=t.cmd,{params:r}=t;"close"===e&&this.close(),"redirect"===e&&window.location.replace(r.redirect)}))}#i(t){const e=this.#e.querySelector("section.il-prompt__scripts"),r=document.createElement("script");r.text=t.replace(/il\.UI\.prompt\.init\([^)]*\);?/g,"").replace(/\$\(this\)\.trigger\('(il_signal_[^']+)'/g,"$(document).trigger('$1'"),e.innerHTML="",e.appendChild(r)}#n(t){t.querySelectorAll("form").forEach((t=>{t.addEventListener("submit",(e=>{e.preventDefault(),this.load(t.action,{method:t.method,body:new FormData(t)})}))}))}#o(t){t.querySelectorAll("a").forEach((t=>{const{target:e}=t;if("_blank"!==e){const e=t.href;t.addEventListener("click",(()=>this.load(e))),t.removeAttribute("href")}}))}}t.UI=t.UI||{},t.UI.prompt=new class{#t;#s=[];constructor(t){this.#t=t}init(t){if(void 0!==this.#s[t])return;try{this.#s[t]=new r(this.#t,t)}catch(t){console.warn(t)}}get(t){return this.#s[t]??null}}(e)}(il,DOMParser); diff --git a/components/ILIAS/UI/resources/js/Prompt/src/prompt.class.js b/components/ILIAS/UI/resources/js/Prompt/src/prompt.class.js index e34ae4065d9a..e467888ca22b 100644 --- a/components/ILIAS/UI/resources/js/Prompt/src/prompt.class.js +++ b/components/ILIAS/UI/resources/js/Prompt/src/prompt.class.js @@ -70,13 +70,19 @@ export default class Prompt { const buttons = doc.querySelector('section[data-section="il-prompt-state__buttons"]'); const command = doc.querySelector('section[data-section="il-prompt-state__command"]'); const parameters = doc.querySelector('section[data-section="il-prompt-state__parameters"]'); - const scripts = doc.querySelector('script'); + const scripts = doc.querySelector('script[data-replace-marker="script"]'); const dialogTitle = this.#prompt.querySelector('span.il-prompt__title'); const dialogContents = this.#prompt.querySelector('div.il-prompt__contents'); const dialogButtons = this.#prompt.querySelector('div.il-prompt__buttons'); - dialogTitle.innerHTML = title.innerHTML; - dialogContents.innerHTML = contents.innerHTML; - dialogButtons.innerHTML = buttons.innerHTML; + if (title) { + dialogTitle.innerHTML = title.innerHTML; + } + if (contents) { + dialogContents.innerHTML = contents.innerHTML; + } + if (buttons) { + dialogButtons.innerHTML = buttons.innerHTML; + } this.#captureForms(dialogContents); this.#captureLinks(dialogContents); @@ -85,14 +91,16 @@ export default class Prompt { } const params = []; - parameters.querySelectorAll('data').forEach( - (data) => { - params[data.innerHTML.trim()] = data.getAttribute('value'); - }, - ); + if (parameters) { + parameters.querySelectorAll('data').forEach( + (data) => { + params[data.innerHTML.trim()] = data.getAttribute('value'); + }, + ); + } return { - cmd: command.innerHTML.trim(), + cmd: command ? command.innerHTML.trim() : '', params, }; }) @@ -117,7 +125,12 @@ export default class Prompt { #appendScript(js) { const dialogScript = this.#component.querySelector('section.il-prompt__scripts'); const script = document.createElement('script'); - script.text = js; + script.text = js + .replace(/il\.UI\.prompt\.init\([^)]*\);?/g, '') + .replace( + /\$\(this\)\.trigger\('(il_signal_[^']+)'/g, + "$(document).trigger('$1'", + ); dialogScript.innerHTML = ''; dialogScript.appendChild(script); } @@ -127,8 +140,7 @@ export default class Prompt { * @return {void} */ #captureForms(doc) { - const forms = doc.getElementsByTagName('form'); - forms.forEach( + doc.querySelectorAll('form').forEach( (form) => { form.addEventListener('submit', (e) => { e.preventDefault(); @@ -146,8 +158,7 @@ export default class Prompt { * @return {void} */ #captureLinks(doc) { - const links = doc.getElementsByTagName('a'); - links.forEach( + doc.querySelectorAll('a').forEach( (lnk) => { const { target } = lnk; if (target !== '_blank') { diff --git a/components/ILIAS/UI/resources/js/Prompt/src/prompt.factory.js b/components/ILIAS/UI/resources/js/Prompt/src/prompt.factory.js index e8daa037cdc4..9a5c2644e156 100644 --- a/components/ILIAS/UI/resources/js/Prompt/src/prompt.factory.js +++ b/components/ILIAS/UI/resources/js/Prompt/src/prompt.factory.js @@ -13,7 +13,7 @@ * https://github.com/ILIAS-eLearning */ -import Prompt from './prompt.class'; +import Prompt from './prompt.class.js'; export default class PromptFactory { /** @@ -40,10 +40,14 @@ export default class PromptFactory { */ init(id) { if (this.#instances[id] !== undefined) { - throw new Error(`Prompt with id '${id}' has already been initialized.`); + return; } - this.#instances[id] = new Prompt(this.#DOMParser, id); + try { + this.#instances[id] = new Prompt(this.#DOMParser, id); + } catch (error) { + // Prompt element may not exist yet during async content replacement. + } } /** From aadecdd18fa99520a470779fac8a2653605ea5ab Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Thu, 2 Jul 2026 12:04:11 +0200 Subject: [PATCH 09/14] [Feature] UI: add prompt confirmation state --- components/ILIAS/UI/UI.php | 9 +- .../UI/src/Component/Prompt/State/Factory.php | 34 ++++ .../UI/src/Component/Prompt/State/State.php | 4 + .../Component/Prompt/Factory.php | 5 +- .../Component/Prompt/State/Confirmation.php | 62 ++++++ .../Component/Prompt/State/Factory.php | 45 ++++- .../Component/Prompt/State/Renderer.php | 69 +++++-- .../Prompt/State/SubsetEntityRetrieval.php | 60 ++++++ .../examples/Prompt/Standard/confirmation.php | 176 ++++++++++++++++++ .../Prompt/Standard/confirmation_prompt.php | 120 ------------ .../examples/Prompt/State/Confirm/base.php | 172 +++++++++++++++++ .../examples/Prompt/State/Confirm/close.php | 100 ++++++++++ .../Prompt/State/Confirm/redirect.php | 112 +++++++++++ components/ILIAS/UI/tests/InitUIFramework.php | 13 +- 14 files changed, 836 insertions(+), 145 deletions(-) create mode 100644 components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php create mode 100644 components/ILIAS/UI/src/Implementation/Component/Prompt/State/SubsetEntityRetrieval.php create mode 100644 components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php delete mode 100644 components/ILIAS/UI/src/examples/Prompt/Standard/confirmation_prompt.php create mode 100644 components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php create mode 100644 components/ILIAS/UI/src/examples/Prompt/State/Confirm/close.php create mode 100644 components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php diff --git a/components/ILIAS/UI/UI.php b/components/ILIAS/UI/UI.php index 12d0996a8500..548ec5d318a9 100644 --- a/components/ILIAS/UI/UI.php +++ b/components/ILIAS/UI/UI.php @@ -466,12 +466,17 @@ public function init( $internal[UI\Implementation\Component\Entity\Factory::class] = static fn() => new UI\Implementation\Component\Entity\Factory(); + $internal[UI\Implementation\Component\Prompt\State\Factory::class] = static fn() => + new UI\Implementation\Component\Prompt\State\Factory( + $internal[UI\Implementation\Component\Listing\Entity\Factory::class], + $internal[UI\Implementation\Component\MessageBox\Factory::class], + $internal[UI\Implementation\Component\Input\Factory::class], + ); $internal[UI\Implementation\Component\Prompt\Factory::class] = static fn() => new UI\Implementation\Component\Prompt\Factory( $internal[UI\Implementation\Component\SignalGeneratorInterface::class], + $internal[UI\Implementation\Component\Prompt\State\Factory::class], ); - $internal[UI\Implementation\Component\Prompt\State\Factory::class] = static fn() => - new UI\Implementation\Component\Prompt\State\Factory(); $internal[UI\Implementation\Component\Navigation\Factory::class] = static fn() => new UI\Implementation\Component\Navigation\Factory( diff --git a/components/ILIAS/UI/src/Component/Prompt/State/Factory.php b/components/ILIAS/UI/src/Component/Prompt/State/Factory.php index 8d2aacd0553e..2b969e6ff58e 100644 --- a/components/ILIAS/UI/src/Component/Prompt/State/Factory.php +++ b/components/ILIAS/UI/src/Component/Prompt/State/Factory.php @@ -20,6 +20,8 @@ namespace ILIAS\UI\Component\Prompt\State; +use ILIAS\UI\URLBuilderToken; +use ILIAS\UI\URLBuilder; use ILIAS\UI\Component; use ILIAS\Data\URI; @@ -51,6 +53,38 @@ public function show( \ILIAS\UI\Component\Prompt\IsPromptContent $content ): State; + /** + * --- + * description: + * purpose: > + * Build a Prompt State to confirm an action on a set of entities. + * composition: > + * The UI framework composes a confirmation message box, an entity + * listing and a form with hidden fields for the entity ids internally. + * Consumers provide an EntityRetrieval and the ids to confirm. + * effect: > + * The Prompt shows the confirmation question, lists affected entities + * and posts the entity ids to the given URL when confirmed. + * + * context: + * - The Prompt State is used for Prompts. + * + * --- + * @param Component\Entity\EntityRetrieval $entity_retrieval + * @param URLBuilder $post_url + * @param URLBuilderToken $post_parameter + * @param array $entity_ids + * @return \ILIAS\UI\Component\Prompt\State\State + */ + public function confirm( + Component\Entity\EntityRetrieval $entity_retrieval, + URLBuilder $post_url, + URLBuilderToken $post_parameter, + array $entity_ids, + string $question, + string $title, + ): State; + /** * --- * description: diff --git a/components/ILIAS/UI/src/Component/Prompt/State/State.php b/components/ILIAS/UI/src/Component/Prompt/State/State.php index f6f440041f7f..f97128ceb789 100644 --- a/components/ILIAS/UI/src/Component/Prompt/State/State.php +++ b/components/ILIAS/UI/src/Component/Prompt/State/State.php @@ -24,4 +24,8 @@ interface State extends Component { + /** + * Get a Prompts State like this, but provide it with an explicit title. + */ + public function withTitle(string $title): self; } diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php index da21193aa8f7..b9ff4d62a322 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php @@ -27,7 +27,8 @@ class Factory implements I\Factory { public function __construct( - protected SignalGeneratorInterface $signal_generator + protected SignalGeneratorInterface $signal_generator, + protected State\Factory $state_factory, ) { } @@ -38,6 +39,6 @@ public function standard(URI $async_url): Standard public function state(): State\Factory { - return new State\Factory(); + return $this->state_factory; } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php new file mode 100644 index 000000000000..a7ddc19f1f37 --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php @@ -0,0 +1,62 @@ +message_box; + } + + public function getForm(): Form + { + return $this->form; + } + + public function getPromptTitle(): string + { + return $this->title; + } + + /** + * @return Button[] + */ + public function getPromptButtons(): array + { + return []; + } +} diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php index ebd99108be1e..b9302c92673c 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php @@ -20,17 +20,57 @@ namespace ILIAS\UI\Implementation\Component\Prompt\State; -use ILIAS\UI\Component\Prompt as I; -use ILIAS\UI\Implementation\Component\SignalGeneratorInterface; use ILIAS\Data\URI; +use ILIAS\UI\URLBuilder; +use ILIAS\UI\URLBuilderToken; +use ILIAS\UI\Component\Prompt as I; +use ILIAS\UI\Component\Entity\EntityRetrieval; +use ILIAS\UI\Component\Input\Factory as InputFactory; +use ILIAS\UI\Component\MessageBox\Factory as MessageBoxFactory; +use ILIAS\UI\Component\Listing\Entity\Factory as ListingEntityFactory; class Factory implements I\State\Factory { + public function __construct( + private readonly ListingEntityFactory $listing_entity_factory, + private readonly MessageBoxFactory $messagebox_factory, + private readonly InputFactory $input_factory, + ) { + } + public function show(I\IsPromptContent $content): State { return new State($content); } + public function confirm( + EntityRetrieval $entity_retrieval, + URLBuilder $post_url, + URLBuilderToken $post_parameter, + array $entity_ids, + string $question, + string $title, + ): State { + $listing_retrieval = new SubsetEntityRetrieval($entity_retrieval, $entity_ids); + $listing = $this->listing_entity_factory->standard($listing_retrieval); + + $message_box = $this->messagebox_factory->confirmation($question) + ->withEntityListing($listing); + + $form = $this->input_factory->container()->form()->standard( + (string) $post_url->withParameter($post_parameter, $entity_ids)->buildURI(), + [] + ); + + $content = new Confirmation( + $message_box, + $form, + $title, + ); + + return (new State($content))->withTitle($title); + } + public function close(): State { return (new State(null)) @@ -42,5 +82,4 @@ public function redirect(URI $redirect): State return (new State(null)) ->withRedirect($redirect); } - } diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Renderer.php index d7c22257d2b5..f5cda26e98ff 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Renderer.php @@ -21,8 +21,6 @@ namespace ILIAS\UI\Implementation\Component\Prompt\State; use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; -use ILIAS\UI\Implementation\Render\Template; -use ILIAS\UI\Implementation\Component\Prompt\State\State; use ILIAS\UI\Renderer as RendererInterface; use ILIAS\UI\Component; @@ -37,9 +35,18 @@ public function render(Component\Component $component, RendererInterface $defaul return $this->renderState($component, $default_renderer); } + if ($component instanceof Confirmation) { + return $this->renderConfirmation($component, $default_renderer); + } + $this->cannotHandleComponent($component); } + protected function renderConfirmation(Confirmation $component, RendererInterface $default_renderer): string + { + return $default_renderer->render($component->getMessageBox()) + . $default_renderer->render($component->getForm()); + } protected function renderState(State $component, RendererInterface $default_renderer): string { @@ -58,29 +65,57 @@ protected function renderState(State $component, RendererInterface $default_rend return $tpl->get(); } - $tpl->setVariable('CONTENT', $default_renderer->render($content_component)); - $tpl->setVariable('TITLE', $component->getTitle()); + if ($content_component instanceof Confirmation) { + $tpl->setVariable('CONTENT', $this->renderConfirmation($content_component, $default_renderer)); + $buttons = $this->getConfirmationButtons($content_component); + } else { + $tpl->setVariable('CONTENT', $default_renderer->render($content_component)); + $buttons = $component->getButtons(); - $buttons = $component->getButtons(); - if ($content_component instanceof \ILIAS\UI\Component\Input\Container\Form\Form) { - $submit_button = $this->getUIFactory()->button()->standard( - $content_component->getSubmitLabel() ?? $this->txt("save"), - $content_component->getSubmitSignal() - ); - $buttons[] = $submit_button; + if ($content_component instanceof \ILIAS\UI\Component\Input\Container\Form\Form) { + $buttons[] = $this->getFormSubmitButton($content_component); + } + + $buttons[] = $this->getPromptCloseButton(); } - $buttons[] = $this->getUIFactory()->button() - ->standard($this->txt('close'), '') + $tpl->setVariable('TITLE', $component->getTitle()); + $tpl->setVariable('BUTTONS', $default_renderer->render($buttons)); + return $tpl->get(); + } + + /** + * @return \ILIAS\UI\Component\Button\Button[] + */ + protected function getConfirmationButtons(Confirmation $confirmation): array + { + return [ + $this->getUIFactory()->button()->primary( + 'Confirm', + $confirmation->getForm()->getSubmitSignal() + ), + $this->getPromptCloseButton($this->txt('cancel')), + ]; + } + + protected function getFormSubmitButton( + \ILIAS\UI\Component\Input\Container\Form\Form $form + ): \ILIAS\UI\Component\Button\Button { + return $this->getUIFactory()->button()->standard( + $form->getSubmitLabel() ?? $this->txt('save'), + $form->getSubmitSignal() + ); + } + + protected function getPromptCloseButton(?string $label = null): \ILIAS\UI\Component\Button\Button + { + return $this->getUIFactory()->button() + ->standard($label ?? $this->txt('close'), '') ->withOnLoadCode( fn($id) => "$('#$id').on('click', (e)=> { let promptId = e.target.closest('dialog').parentNode.id; il.UI.prompt.get(promptId).close(); });" ); - - $tpl->setVariable('BUTTONS', $default_renderer->render($buttons)); - return $tpl->get(); } - } diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/SubsetEntityRetrieval.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/SubsetEntityRetrieval.php new file mode 100644 index 000000000000..866fe71fa850 --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/SubsetEntityRetrieval.php @@ -0,0 +1,60 @@ + $entity_ids + */ + public function __construct( + private readonly EntityRetrieval $entity_retrieval, + private readonly array $entity_ids, + ) { + } + + public function getEntities( + \ILIAS\UI\Factory $ui_factory, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters, + ): Generator { + yield from $this->entity_retrieval->getEntitiesByIds($ui_factory, $order, $this->entity_ids); + } + + public function getEntitiesByIds( + \ILIAS\UI\Factory $ui_factory, + Order $order, + array $entity_ids, + ): Generator { + yield from $this->entity_retrieval->getEntitiesByIds($ui_factory, $order, $entity_ids); + } +} diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php new file mode 100644 index 000000000000..f1e060826728 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php @@ -0,0 +1,176 @@ + + * A confirmation prompt for deleting or changing multiple entities. + * The UI framework composes message box, entity listing and hidden form fields. + * + * expected output: > + * A button opens a prompt listing selected entities with a confirmation question. + * Submitting posts the entity ids; the result is shown in the prompt. + * --- + */ +function confirmation(): string +{ + global $DIC; + + $http = $DIC->http(); + $factory = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + $query = $http->wrapper()->query(); + $data_factory = new \ILIAS\Data\Factory(); + $refinery = $DIC['refinery']; + + $here_uri = $data_factory->uri((string) $http->request()->getUri()); + $url_builder = new URLBuilder($here_uri); + + $example_namespace = ['prompt', 'confirmation']; + $demo_entity_ids = [1, 2, 3]; + + // when expecting a state, we do not want to render other examples on the same page + [$url_builder, $endpoint_token] = $url_builder->acquireParameters($example_namespace, 'endpoint'); + $url_builder = $url_builder->withParameter($endpoint_token, 'true'); + + [$url_builder, $confirm_token] = $url_builder->acquireParameters($example_namespace, 'confirm'); + [$url_builder, $process_token] = $url_builder->acquireParameters($example_namespace, 'process'); + [$url_builder, $entities_token] = $url_builder->acquireParameters($example_namespace, 'entities'); + + // async GET: load confirmation prompt state + if ($query->has($confirm_token->getName())) { + $entity_ids = retrieveEntityIds($query, $confirm_token->getName(), $refinery); + if ($entity_ids !== null) { + $post_url = $url_builder->withParameter($process_token, '1'); + $state = $factory->prompt()->state()->confirm( + new ConfirmationEntityRetrieval(), + $post_url, + $entities_token, + $entity_ids, + 'Are you sure you want to perform this action?', + 'Performing some action', + ); + + echo $renderer->renderAsync($state); + exit; + } + } + + // async POST: form submit after confirmation + if ($http->request()->getMethod() === 'POST' && $query->has($process_token->getName())) { + $process = $query->retrieve($process_token->getName(), $refinery->kindlyTo()->string()); + if ($process !== '') { + $entity_ids = retrieveEntityIds($query, $entities_token->getName(), $refinery) ?? []; + $message = $factory->messageBox()->success( + 'Submitted entity ids: ' . implode(', ', array_map('strval', $entity_ids)) + ); + echo $renderer->renderAsync( + $factory->prompt()->state()->show($message)->withTitle('Confirmation result') + ); + exit; + } + } + + $open_uri = $url_builder + ->withParameter($confirm_token, $demo_entity_ids) + ->buildURI(); + + $prompt = $factory->prompt()->standard($open_uri); + $trigger = $factory->button()->primary('Open confirmation prompt', $prompt->getShowSignal($open_uri)); + + if (!$query->has($endpoint_token->getName())) { + return $renderer->render([$prompt, $trigger]); + } + + return ''; +} + +/** + * @return array|null + */ +function retrieveEntityIds( + \ILIAS\HTTP\Wrapper\RequestWrapper $query, + string $parameter_name, + \ILIAS\Refinery\Factory $refinery, +): ?array { + if (!$query->has($parameter_name)) { + return null; + } + + $raw = $query->retrieve( + $parameter_name, + $refinery->custom()->transformation(static fn(mixed $value): mixed => $value) + ); + + if (!is_array($raw)) { + $raw = ($raw === '' || $raw === null) ? [] : [$raw]; + } + + $raw = array_values(array_filter( + $raw, + static fn(mixed $value): bool => $value !== '' && $value !== null + )); + + if ($raw === []) { + return null; + } + + return $refinery->kindlyTo()->listOf($refinery->kindlyTo()->int())->transform($raw); +} + +class ConfirmationEntityRetrieval implements EntityRetrieval +{ + public function getEntities( + \ILIAS\UI\Factory $ui_factory, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters, + ): Generator { + foreach ([1, 2, 3] as $entity_id) { + yield $this->getPseudoEntity($ui_factory, $entity_id); + } + } + + public function getEntitiesByIds( + \ILIAS\UI\Factory $ui_factory, + Order $order, + array $entity_ids, + ): Generator { + foreach ($entity_ids as $entity_id) { + yield $this->getPseudoEntity($ui_factory, (int) $entity_id); + } + } + + protected function getPseudoEntity(\ILIAS\UI\Factory $ui_factory, int $entity_id): Entity + { + return $ui_factory->entity()->standard($entity_id, "Entity $entity_id", ''); + } +} diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation_prompt.php b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation_prompt.php deleted file mode 100644 index a778ecaf888b..000000000000 --- a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation_prompt.php +++ /dev/null @@ -1,120 +0,0 @@ - - * - * expected output: > - * --- - */ -function confirmation_prompt() -{ - global $DIC; - $factory = $DIC->ui()->factory(); - $renderer = $DIC->ui()->renderer(); - - $df = new \ILIAS\Data\Factory(); - $refinery = $DIC['refinery']; - - $here_uri = $df->uri($DIC->http()->request()->getUri()->__toString()); - $url_builder = new URLBuilder($here_uri); - - $record_to_entity = new class () implements RecordToEntity { - public function map(UIFactory $ui_factory, mixed $record): Entity - { - list($abbreviation, $login, $email, $name, $last_seen, $active) = $record; - $avatar = $ui_factory->symbol()->avatar()->letter($abbreviation); - - return $ui_factory->entity()->standard($name, $avatar) - ->withMainDetails( - $ui_factory->listing()->property() - ->withProperty('login', $login) - ->withProperty('mail', $email, false) - ); - } - }; - - $data = new class () implements DataRetrieval { - protected array $data = [ - ['jw', 'jimmywilson', 'jimmywilson@example.com', 'Jimmy Wilson', '2022-03-15 13:20:10', true], - ['eb', 'emilybrown', 'emilybrown@example.com', 'Emily Brown', '2022-03-16 10:45:32', false], - ['ms', 'michaelscott', 'michaelscott@example.com', 'Michael Scott', '2022-03-14 08:15:05', true], - ['kj', 'katiejones', 'katiejones@example.com', 'Katie Jones', '2022-03-17 15:30:50', true] - ]; - - public function getEntities( - Mapping $mapping, - ?Range $range, - ?array $additional_parameters - ): Generator { - foreach ($this->data as $usr) { - yield $mapping->map($usr); - } - } - }; - - $buttons = [$factory->button()->standard('Confirm', '#'), $factory->button()->standard('Cancel', '#')]; - - $message = $factory->messageBox()->confirmation('some message box') - ->withButtons($buttons) - ->withEntityListing($factory->listing()->entity()->standard($record_to_entity)->withData($data)); - - // when expecting a state, we do not want to render other examples - $example_namespace = ['prompt', 'endpoints']; - [$url_builder, $endpointtoken] = $url_builder->acquireParameters($example_namespace, 'endpoint'); - $url_builder = $url_builder->withParameter($endpointtoken, 'true'); - - // build the prompt - $query_namespace = ['prompt', 'example_conf']; - [$url_builder, $token] = $url_builder->acquireParameters($query_namespace, 'show'); - $url_builder = $url_builder->withParameter($token, 'true'); - $prompt = $factory->prompt()->standard($url_builder->buildURI()); - - // build the endpoint returning the wrapped message - $query = $DIC->http()->wrapper()->query(); - if ($query->has($token->getName())) { - $response = $factory->prompt()->state()->show($message); - echo $renderer->renderAsync($response); - exit; - } - - // a button to open the prompt: - $show_button = $factory->button()->standard('Show Simple Prompt', $prompt->getShowSignal()); - - if (!$query->has($endpointtoken->getName())) { - return $renderer->render([ - $message, - $prompt, - $show_button - ]); - } -} diff --git a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php new file mode 100644 index 000000000000..14e938699b7d --- /dev/null +++ b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php @@ -0,0 +1,172 @@ + + * After confirming, the consumer returns a show state with new prompt content. + * + * expected output: > + * A button opens a Prompt with confirmation question, entity listing and actions. + * Confirming posts the entity ids; a success message is shown inside the prompt. + * --- + */ +function base(): string +{ + global $DIC; + + $http = $DIC->http(); + $factory = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + $query = $http->wrapper()->query(); + $refinery = $DIC['refinery']; + $data_factory = new \ILIAS\Data\Factory(); + + $here_uri = $data_factory->uri((string) $http->request()->getUri()); + $url_builder = new URLBuilder($here_uri); + + $example_namespace = ['prompt', 'state', 'confirm']; + $demo_entity_ids = [1, 2, 3]; + + [$url_builder, $endpoint_token] = $url_builder->acquireParameters($example_namespace, 'endpoint'); + $url_builder = $url_builder->withParameter($endpoint_token, 'true'); + + [$url_builder, $confirm_token] = $url_builder->acquireParameters($example_namespace, 'confirm'); + [$url_builder, $process_token] = $url_builder->acquireParameters($example_namespace, 'process'); + [$url_builder, $entities_token] = $url_builder->acquireParameters($example_namespace, 'entities'); + + if ($query->has($confirm_token->getName())) { + $entity_ids = retrieveEntityIds($query, $confirm_token->getName(), $refinery); + if ($entity_ids !== null) { + $post_url = $url_builder->withParameter($process_token, '1'); + $state = $factory->prompt()->state()->confirm( + new ConfirmStateEntityRetrieval(), + $post_url, + $entities_token, + $entity_ids, + 'Are you sure you want to perform this action?', + 'Performing some action', + ); + + echo $renderer->renderAsync($state); + exit; + } + } + + if ($http->request()->getMethod() === 'POST' && $query->has($process_token->getName())) { + $process = $query->retrieve($process_token->getName(), $refinery->kindlyTo()->string()); + if ($process !== '') { + $entity_ids = retrieveEntityIds($query, $entities_token->getName(), $refinery) ?? []; + $message = $factory->messageBox()->success( + 'Submitted entity ids: ' . implode(', ', array_map('strval', $entity_ids)) + ); + echo $renderer->renderAsync( + $factory->prompt()->state()->show($message)->withTitle('Confirmation result') + ); + exit; + } + } + + $open_uri = $url_builder + ->withParameter($confirm_token, $demo_entity_ids) + ->buildURI(); + + $prompt = $factory->prompt()->standard($open_uri); + $trigger = $factory->button()->primary('Open confirm (show result)', $prompt->getShowSignal($open_uri)); + + if (!$query->has($endpoint_token->getName())) { + return $renderer->render([$trigger, $prompt]); + } + + return ''; +} + +/** + * @return array|null + */ +function retrieveEntityIds( + \ILIAS\HTTP\Wrapper\RequestWrapper $query, + string $parameter_name, + \ILIAS\Refinery\Factory $refinery, +): ?array { + if (!$query->has($parameter_name)) { + return null; + } + + $raw = $query->retrieve( + $parameter_name, + $refinery->custom()->transformation(static fn(mixed $value): mixed => $value) + ); + + if (!is_array($raw)) { + $raw = ($raw === '' || $raw === null) ? [] : [$raw]; + } + + $raw = array_values(array_filter( + $raw, + static fn(mixed $value): bool => $value !== '' && $value !== null + )); + + if ($raw === []) { + return null; + } + + return $refinery->kindlyTo()->listOf($refinery->kindlyTo()->int())->transform($raw); +} + +class ConfirmStateEntityRetrieval implements EntityRetrieval +{ + public function getEntities( + \ILIAS\UI\Factory $ui_factory, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters, + ): Generator { + foreach ([1, 2, 3] as $entity_id) { + yield $this->getPseudoEntity($ui_factory, $entity_id); + } + } + + public function getEntitiesByIds( + \ILIAS\UI\Factory $ui_factory, + Order $order, + array $entity_ids, + ): Generator { + foreach ($entity_ids as $entity_id) { + yield $this->getPseudoEntity($ui_factory, (int) $entity_id); + } + } + + protected function getPseudoEntity(\ILIAS\UI\Factory $ui_factory, int $entity_id): Entity + { + return $ui_factory->entity()->standard($entity_id, "Entity $entity_id", ''); + } +} diff --git a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/close.php b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/close.php new file mode 100644 index 000000000000..78142a21c585 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/close.php @@ -0,0 +1,100 @@ + + * After confirming, the consumer returns a close state and the prompt is dismissed. + * + * expected output: > + * A button opens a confirmation prompt. Confirming closes the prompt without + * showing further content inside it. + * --- + */ +function close(): string +{ + global $DIC; + + $http = $DIC->http(); + $factory = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + $query = $http->wrapper()->query(); + $refinery = $DIC['refinery']; + $data_factory = new \ILIAS\Data\Factory(); + + $here_uri = $data_factory->uri((string) $http->request()->getUri()); + $url_builder = new URLBuilder($here_uri); + + $example_namespace = ['prompt', 'state', 'confirm', 'close']; + $demo_entity_ids = [1, 2, 3]; + + [$url_builder, $endpoint_token] = $url_builder->acquireParameters($example_namespace, 'endpoint'); + $url_builder = $url_builder->withParameter($endpoint_token, 'true'); + + [$url_builder, $confirm_token] = $url_builder->acquireParameters($example_namespace, 'confirm'); + [$url_builder, $process_token] = $url_builder->acquireParameters($example_namespace, 'process'); + [$url_builder, $entities_token] = $url_builder->acquireParameters($example_namespace, 'entities'); + + if ($query->has($confirm_token->getName())) { + $entity_ids = retrieveEntityIds($query, $confirm_token->getName(), $refinery); + if ($entity_ids !== null) { + $post_url = $url_builder->withParameter($process_token, '1'); + $state = $factory->prompt()->state()->confirm( + new ConfirmStateEntityRetrieval(), + $post_url, + $entities_token, + $entity_ids, + 'Are you sure you want to perform this action?', + 'Performing some action', + ); + + echo $renderer->renderAsync($state); + exit; + } + } + + if ($http->request()->getMethod() === 'POST' && $query->has($process_token->getName())) { + $process = $query->retrieve($process_token->getName(), $refinery->kindlyTo()->string()); + if ($process !== '') { + echo $renderer->renderAsync($factory->prompt()->state()->close()); + exit; + } + } + + $open_uri = $url_builder + ->withParameter($confirm_token, $demo_entity_ids) + ->buildURI(); + + $prompt = $factory->prompt()->standard($open_uri); + $trigger = $factory->button()->primary('Open confirm (close prompt)', $prompt->getShowSignal($open_uri)); + $hint = $factory->messageBox()->info('After confirming, the prompt closes.'); + + if (!$query->has($endpoint_token->getName())) { + return $renderer->render([$hint, $trigger, $prompt]); + } + + return ''; +} diff --git a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php new file mode 100644 index 000000000000..059c462626a2 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php @@ -0,0 +1,112 @@ + + * After confirming, the consumer returns a redirect state to leave the prompt + * and show feedback on the target page. + * + * expected output: > + * A button opens a confirmation prompt. Confirming redirects the page and + * shows a success message below the trigger button. + * --- + */ +function redirect(): string +{ + global $DIC; + + $http = $DIC->http(); + $factory = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + $query = $http->wrapper()->query(); + $refinery = $DIC['refinery']; + $data_factory = new \ILIAS\Data\Factory(); + + $here_uri = $data_factory->uri((string) $http->request()->getUri()); + $url_builder = new URLBuilder($here_uri); + + $example_namespace = ['prompt', 'state', 'confirm', 'redirect']; + $demo_entity_ids = [1, 2, 3]; + + [$url_builder, $endpoint_token] = $url_builder->acquireParameters($example_namespace, 'endpoint'); + [$url_builder, $success_token] = $url_builder->acquireParameters($example_namespace, 'success'); + + [$url_builder, $confirm_token] = $url_builder->acquireParameters($example_namespace, 'confirm'); + [$url_builder, $process_token] = $url_builder->acquireParameters($example_namespace, 'process'); + [$url_builder, $entities_token] = $url_builder->acquireParameters($example_namespace, 'entities'); + + $async_url_builder = $url_builder->withParameter($endpoint_token, 'true'); + + if ($query->has($confirm_token->getName())) { + $entity_ids = retrieveEntityIds($query, $confirm_token->getName(), $refinery); + if ($entity_ids !== null) { + $post_url = $async_url_builder->withParameter($process_token, '1'); + $state = $factory->prompt()->state()->confirm( + new ConfirmStateEntityRetrieval(), + $post_url, + $entities_token, + $entity_ids, + 'Are you sure you want to perform this action?', + 'Performing some action', + ); + + echo $renderer->renderAsync($state); + exit; + } + } + + if ($http->request()->getMethod() === 'POST' && $query->has($process_token->getName())) { + $process = $query->retrieve($process_token->getName(), $refinery->kindlyTo()->string()); + if ($process !== '') { + $target = $url_builder->withParameter($success_token, '1')->buildURI(); + echo $renderer->renderAsync($factory->prompt()->state()->redirect($target)); + exit; + } + } + + $open_uri = $async_url_builder + ->withParameter($confirm_token, $demo_entity_ids) + ->buildURI(); + + $prompt = $factory->prompt()->standard($open_uri); + $trigger = $factory->button()->primary('Open confirm (redirect)', $prompt->getShowSignal($open_uri)); + + $components = [$trigger, $prompt]; + if ($query->has($success_token->getName()) + && $query->retrieve($success_token->getName(), $refinery->kindlyTo()->string()) !== '') { + array_unshift( + $components, + $factory->messageBox()->success('Action confirmed. Entity ids were processed.') + ); + } + + if (!$query->has($endpoint_token->getName())) { + return $renderer->render($components); + } + + return ''; +} diff --git a/components/ILIAS/UI/tests/InitUIFramework.php b/components/ILIAS/UI/tests/InitUIFramework.php index fc2bcf2b4807..f54074a17921 100755 --- a/components/ILIAS/UI/tests/InitUIFramework.php +++ b/components/ILIAS/UI/tests/InitUIFramework.php @@ -429,8 +429,19 @@ public function getRefreshIntervalInMs(): int return new ILIAS\UI\Implementation\Component\Entity\Factory(); }; + $c["ui.factory.prompt.state"] = function ($c) { + return new ILIAS\UI\Implementation\Component\Prompt\State\Factory( + $c["ui.factory.listing"]->entity(), + $c["ui.factory.messagebox"], + $c["ui.factory.input"], + ); + }; + $c["ui.factory.prompt"] = function ($c) { - return new ILIAS\UI\Implementation\Component\Prompt\Factory($c["ui.signal_generator"]); + return new ILIAS\UI\Implementation\Component\Prompt\Factory( + $c["ui.signal_generator"], + $c["ui.factory.prompt.state"] + ); }; $c["ui.factory.navigation"] = function ($c) { From 7bf8854d9ff837ed3782a7ec4a4de8c6cd8144ab Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Thu, 2 Jul 2026 12:17:31 +0200 Subject: [PATCH 10/14] [Feature] UI: ConfirmationPrompt - fix unit tests --- .../UI/tests/Component/Entity/EntityTest.php | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/components/ILIAS/UI/tests/Component/Entity/EntityTest.php b/components/ILIAS/UI/tests/Component/Entity/EntityTest.php index a195d16a4102..8ea2951c946f 100755 --- a/components/ILIAS/UI/tests/Component/Entity/EntityTest.php +++ b/components/ILIAS/UI/tests/Component/Entity/EntityTest.php @@ -41,15 +41,16 @@ protected function legacy(string $string): Content public function testEntityFactory(): void { - $entity = $this->getEntityFactory()->standard('primary', 'secondary'); + $entity = $this->getEntityFactory()->standard(1, 'primary', 'secondary'); $this->assertInstanceOf("ILIAS\\UI\\Component\\Entity\\Standard", $entity); + $this->assertEquals(1, $entity->getId()); $this->assertEquals('primary', $entity->getPrimaryIdentifier()); $this->assertEquals('secondary', $entity->getSecondaryIdentifier()); } public function testEntityBasicProperties(): void { - $entity = $this->getEntityFactory()->standard('primary', 'secondary'); + $entity = $this->getEntityFactory()->standard(1, 'primary', 'secondary'); $this->assertEquals([$this->legacy('bc')], $entity->withBlockingAvailabilityConditions($this->legacy('bc'))->getBlockingAvailabilityConditions()); $this->assertEquals([$this->legacy('fp')], $entity->withFeaturedProperties($this->legacy('fp'))->getFeaturedProperties()); $this->assertEquals([$this->legacy('md')], $entity->withMainDetails($this->legacy('md'))->getMainDetails()); @@ -58,7 +59,7 @@ public function testEntityBasicProperties(): void $this->assertEquals([$this->legacy('d')], $entity->withDetails($this->legacy('d'))->getDetails()); } - public static function getEntityAllowedIdentiferTypes(): array + public static function getEntityAllowedVisualTypes(): array { $shy_button = new Button\Shy('the label', '#'); $shy_link = new Link\Standard('the label', '#'); @@ -73,12 +74,13 @@ public static function getEntityAllowedIdentiferTypes(): array ]; } - #[\PHPUnit\Framework\Attributes\DataProvider('getEntityAllowedIdentiferTypes')] - public function testEntityIdentifiers($identifier): void + #[\PHPUnit\Framework\Attributes\DataProvider('getEntityAllowedVisualTypes')] + public function testEntityVisualIdentifiers($visual_identifier): void { - $entity = $this->getEntityFactory()->standard($identifier, $identifier); - $this->assertEquals($identifier, $entity->getPrimaryIdentifier()); - $this->assertEquals($identifier, $entity->getSecondaryIdentifier()); + $entity = $this->getEntityFactory()->standard(1, $visual_identifier, $visual_identifier); + $this->assertEquals(1, $entity->getId()); + $this->assertEquals($visual_identifier, $entity->getPrimaryIdentifier()); + $this->assertEquals($visual_identifier, $entity->getSecondaryIdentifier()); } public function testEntityActionProperties(): void @@ -86,7 +88,7 @@ public function testEntityActionProperties(): void $glyph = new Symbol\Glyph\Glyph('laugh', 'some glyph'); $tag = new Button\Tag('tag', '#'); $shy = new Button\Shy('shy', '#'); - $entity = $this->getEntityFactory()->standard('primary', 'secondary') + $entity = $this->getEntityFactory()->standard(1, 'primary', 'secondary') ->withPrioritizedReactions($glyph, $tag) ->withReactions($glyph, $glyph, $glyph) ->withManagingActions($shy); @@ -101,7 +103,7 @@ public function testEntityComponentProperties(): void $glyph = new Symbol\Glyph\Glyph('laugh', 'some glyph'); $tag = new Button\Tag('tag', '#'); $shy = new Button\Shy('shy', '#'); - $entity = $this->getEntityFactory()->standard('primary', 'secondary') + $entity = $this->getEntityFactory()->standard(1, 'primary', 'secondary') ->withPrioritizedReactions($glyph, $tag) ->withReactions($glyph) ->withManagingActions($shy); @@ -176,7 +178,7 @@ public function testEntityRendering(): void $glyph = new Symbol\Glyph\Glyph('laugh', 'some glyph'); $tag = new Button\Tag('tag', '#'); $shy = new Button\Shy('shy', '#'); - $entity = $this->getEntityFactory()->standard('primary', 'secondary') + $entity = $this->getEntityFactory()->standard(1, 'primary', 'secondary') ->withPrioritizedReactions($glyph, $tag) ->withReactions($glyph, $glyph) ->withManagingActions($shy, $shy) From 8aac7b2779e4bcb32885b4a29e9f13fc67006a96 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Tue, 14 Jul 2026 10:09:18 +0200 Subject: [PATCH 11/14] [Feature] UI: ConfirmationPrompt - decouple EntityListing and MessageBox --- .../UI/src/Component/Prompt/State/Factory.php | 2 +- .../Component/Prompt/State/Confirmation.php | 7 + .../Component/Prompt/State/Factory.php | 4 +- .../Component/Prompt/State/Renderer.php | 1 + .../examples/Entity/Standard/video_object.php | 1 + .../src/examples/Listing/Entity/Grid/base.php | 196 ++++++++++-------- .../UI/tests/Component/Entity/EntityTest.php | 4 +- .../Listing/Entity/GridEntityListingTest.php | 74 +++---- 8 files changed, 150 insertions(+), 139 deletions(-) diff --git a/components/ILIAS/UI/src/Component/Prompt/State/Factory.php b/components/ILIAS/UI/src/Component/Prompt/State/Factory.php index 2b969e6ff58e..bb67ac369694 100644 --- a/components/ILIAS/UI/src/Component/Prompt/State/Factory.php +++ b/components/ILIAS/UI/src/Component/Prompt/State/Factory.php @@ -60,7 +60,7 @@ public function show( * Build a Prompt State to confirm an action on a set of entities. * composition: > * The UI framework composes a confirmation message box, an entity - * listing and a form with hidden fields for the entity ids internally. + * listing below it and a form with hidden fields for the entity ids internally. * Consumers provide an EntityRetrieval and the ids to confirm. * effect: > * The Prompt shows the confirmation question, lists affected entities diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php index a7ddc19f1f37..85373305e77d 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php @@ -22,6 +22,7 @@ use ILIAS\UI\Component\Button\Button; use ILIAS\UI\Component\Input\Container\Form\Form; +use ILIAS\UI\Component\Listing\Entity\Entity as EntityListing; use ILIAS\UI\Component\MessageBox\MessageBox; use ILIAS\UI\Implementation\Component\ComponentHelper; use ILIAS\UI\Implementation\Component\Prompt\IsPromptContentInternal; @@ -32,6 +33,7 @@ class Confirmation implements IsPromptContentInternal public function __construct( private readonly MessageBox $message_box, + private readonly EntityListing $entity_listing, private readonly Form $form, private readonly string $title, ) { @@ -42,6 +44,11 @@ public function getMessageBox(): MessageBox return $this->message_box; } + public function getEntityListing(): EntityListing + { + return $this->entity_listing; + } + public function getForm(): Form { return $this->form; diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php index b9302c92673c..79da39b58363 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php @@ -54,8 +54,7 @@ public function confirm( $listing_retrieval = new SubsetEntityRetrieval($entity_retrieval, $entity_ids); $listing = $this->listing_entity_factory->standard($listing_retrieval); - $message_box = $this->messagebox_factory->confirmation($question) - ->withEntityListing($listing); + $message_box = $this->messagebox_factory->confirmation($question); $form = $this->input_factory->container()->form()->standard( (string) $post_url->withParameter($post_parameter, $entity_ids)->buildURI(), @@ -64,6 +63,7 @@ public function confirm( $content = new Confirmation( $message_box, + $listing, $form, $title, ); diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Renderer.php index f5cda26e98ff..5d5949dd9b9d 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Renderer.php @@ -45,6 +45,7 @@ public function render(Component\Component $component, RendererInterface $defaul protected function renderConfirmation(Confirmation $component, RendererInterface $default_renderer): string { return $default_renderer->render($component->getMessageBox()) + . $default_renderer->render($component->getEntityListing()) . $default_renderer->render($component->getForm()); } diff --git a/components/ILIAS/UI/src/examples/Entity/Standard/video_object.php b/components/ILIAS/UI/src/examples/Entity/Standard/video_object.php index 1c53f7dadc8d..bfbf74a933a1 100644 --- a/components/ILIAS/UI/src/examples/Entity/Standard/video_object.php +++ b/components/ILIAS/UI/src/examples/Entity/Standard/video_object.php @@ -37,6 +37,7 @@ function video_object() // creating the entity object now so it can be filled in the logic section $entity = $f->entity()->standard( + 'video-demo', $primary_id, $secondary_id ); diff --git a/components/ILIAS/UI/src/examples/Listing/Entity/Grid/base.php b/components/ILIAS/UI/src/examples/Listing/Entity/Grid/base.php index ee98483ace68..39f87e3d5c33 100644 --- a/components/ILIAS/UI/src/examples/Listing/Entity/Grid/base.php +++ b/components/ILIAS/UI/src/examples/Listing/Entity/Grid/base.php @@ -4,12 +4,12 @@ namespace ILIAS\UI\Examples\Listing\Entity\Grid; +use Generator; +use ILIAS\Data\Order; +use ILIAS\Data\Range; use ILIAS\UI\Factory as UIFactory; -use ILIAS\UI\Component\Listing\Entity\RecordToEntity; -use ILIAS\UI\Component\Listing\Entity\DataRetrieval; use ILIAS\UI\Component\Entity\Entity; -use ILIAS\UI\Component\Listing\Entity\Mapping; -use ILIAS\Data\Range; +use ILIAS\UI\Component\Entity\EntityRetrieval; /** * --- @@ -28,101 +28,115 @@ function base(): string $f = $DIC->ui()->factory(); $renderer = $DIC->ui()->renderer(); - $record_to_entity = new class () implements RecordToEntity { - public function map(UIFactory $ui_factory, mixed $record): Entity - { - $glyph_user = $ui_factory->symbol()->glyph()->user() - ->withLabel("Created by"); - - $glyph_calendar = $ui_factory->symbol()->glyph()->calendar() - ->withLabel("Upload date"); - - $glyph_duration = $ui_factory->symbol()->glyph()->time() - ->withLabel("Duration"); - - list($title, $thumbnail_url, $creator, $availability, $description, $duration, $add_workflow, $date) = $record; - $managing_actions = [ - $ui_factory->button()->shy("Edit", "#"), - $ui_factory->button()->shy("Move", "#"), - ]; - $entity = $ui_factory->entity()->standard( - $ui_factory->link()->standard($title, ""), - $ui_factory->image()->responsive($thumbnail_url, $title)->withAction("#") - ) - ->withFeaturedProperties( - $ui_factory->listing()->property() - ->withProperty($glyph_user, $creator) - ) - ->withManagingActions(...$managing_actions) - ->withMainDetails( - $ui_factory->listing()->property() - ->withProperty($glyph_duration, $description, false) - ->withProperty($glyph_duration, $duration) - ->withProperty($glyph_calendar, $date) - ) - ->withPrioritizedReactions($ui_factory->button()->shy("Like", "#")->withSymbol($ui_factory->symbol()->glyph()->like())) - ; - if ($availability) { - $entity = $entity->withBlockingAvailabilityConditions( - $ui_factory->listing()->property() - ->withProperty("Status", $ui_factory->legacy()->content($availability), false) - ); - } - if ($add_workflow) { - $workflow_factory = $ui_factory->listing()->workflow(); - $dummy_step = $workflow_factory->step('', ''); - - $steps = [ - $workflow_factory->step("Upload video file", "Upload an .mp4 file or start a recording.", "#") - ->withAvailability($dummy_step::NOT_ANYMORE)->withStatus($dummy_step::SUCCESSFULLY), - $workflow_factory->step("Cut video", "Trim or remove parts of the video.", "#") - ->withAvailability($dummy_step::NOT_ANYMORE)->withStatus($dummy_step::NOT_STARTED), - $workflow_factory->step("Add subtitles", "You must upload or generate subtitles for every video.", "#") - ->withAvailability($dummy_step::AVAILABLE)->withStatus($dummy_step::SUCCESSFULLY), - $workflow_factory->step("Publish", "Set who can see this video.", "#") - ->withAvailability($dummy_step::AVAILABLE)->withStatus($dummy_step::NOT_STARTED), - ]; - - $video_workflow = $workflow_factory->linear("Video Curation", $steps); - - $entity = $entity->withWorkflow($video_workflow); - } - return $entity; - } - }; - $glyph_eye_closed = $f->symbol()->glyph()->eyeclosed(); - $glyph_with_text = $renderer->render($glyph_eye_closed) . " offline"; + $glyph_with_text = $renderer->render($glyph_eye_closed) . ' offline'; $card_data = [ - ['Snowboarding for beginners - How to avoid falling on your face', 'assets/ui-examples/images/Image/ski_widescreen-thumbnail.jpg', "Bobby's School of Snowboarding Austria", null, 'This is the perfect start for anyone wanting to get on a snowboard. We talk the best gear and the best locations for a beginner. And no worries - it is not expensive: Renting equipment will work just fine. Then we end with some first exercises to get the stability needed to tackle your first slope', '23 min', false, '01.01.2026'], - ['The History of Bridges', 'assets/ui-examples/images/Image/sanfrancisco_widescreen-thumbnail.jpg', 'BBC England', $glyph_with_text,'One of the most monumental achievements of human kind is the invention of bridges. Crossing streets, rivers and sometimes oceans became a huge pillar for our our modern infrastructure. This documentary looks at the different types of bridges and how they have been developed and engineered throughout different cultures and centuries','90 min', true, "01.11.2026"], - ['Mountains through the ages - the formation of giants', 'assets/ui-examples/images/Image/mountains_widescreen-thumbnail.jpg','ARD/ZDF, Canal Plus', null, "A fascinating look on the forces of nature that are able to move unimaginable tons of rocks. Find out how seemingly immovable landscape has transformed drastically through the incredible forces set free by earthquakes, vulcanos and water. This award-winning documentary traces the movement of the world's greatest mountain ranges throughout millions of years.", "45 min", false, "11.10.2026"], - ['Snowboarding for beginners - How to avoid falling on your face', 'assets/ui-examples/images/Image/ski_widescreen-thumbnail.jpg', "Bobby's School of Snowboarding Austria", null, 'This is the perfect start for anyone wanting to get on a snowboard. We talk the best gear and the best locations for a beginner. And no worries - it is not expensive: Renting equipment will work just fine. Then we end with some first exercises to get the stability needed to tackle your first slope', '23 min', false, "28.02.2026"], - ['The History of Bridges', 'assets/ui-examples/images/Image/sanfrancisco_widescreen-thumbnail.jpg', 'BBC England', $glyph_with_text,'One of the most monumental achievements of human kind is the invention of bridges. Crossing streets, rivers and sometimes oceans became a huge pillar for our our modern infrastructure. This documentary looks at the different types of bridges and how they have been developed and engineered throughout different cultures and centuries','90 min', true, "15.12.2025"], - ['Mountains through the ages - the formation of giants', 'assets/ui-examples/images/Image/mountains_widescreen-thumbnail.jpg','ARD/ZDF, Canal Plus', null, "A fascinating look on the forces of nature that are able to move unimaginable tons of rocks. Find out how seemingly immovable landscape has transformed drastically through the incredible forces set free by earthquakes, vulcanos and water. This award-winning documentary traces the movement of the world's greatest mountain ranges throughout millions of years.", "45 min", false, "09.06.2026"] + ['Snowboarding for beginners - How to avoid falling on your face', 'assets/ui-examples/images/Image/ski_widescreen-thumbnail.jpg', "Bobby's School of Snowboarding Austria", null, 'This is the perfect start for anyone wanting to get on a snowboard. We talk the best gear and the best locations for a beginner. And no worries - it is not expensive: Renting equipment will work just fine. Then we end with some first exercises to get the stability needed to tackle your first slope', '23 min', false, '01.01.2026'], + ['The History of Bridges', 'assets/ui-examples/images/Image/sanfrancisco_widescreen-thumbnail.jpg', 'BBC England', $glyph_with_text, 'One of the most monumental achievements of human kind is the invention of bridges. Crossing streets, rivers and sometimes oceans became a huge pillar for our our modern infrastructure. This documentary looks at the different types of bridges and how they have been developed and engineered throughout different cultures and centuries', '90 min', true, '01.11.2026'], + ['Mountains through the ages - the formation of giants', 'assets/ui-examples/images/Image/mountains_widescreen-thumbnail.jpg', 'ARD/ZDF, Canal Plus', null, "A fascinating look on the forces of nature that are able to move unimaginable tons of rocks. Find out how seemingly immovable landscape has transformed drastically through the incredible forces set free by earthquakes, vulcanos and water. This award-winning documentary traces the movement of the world's greatest mountain ranges throughout millions of years.", '45 min', false, '11.10.2026'], + ['Snowboarding for beginners - How to avoid falling on your face', 'assets/ui-examples/images/Image/ski_widescreen-thumbnail.jpg', "Bobby's School of Snowboarding Austria", null, 'This is the perfect start for anyone wanting to get on a snowboard. We talk the best gear and the best locations for a beginner. And no worries - it is not expensive: Renting equipment will work just fine. Then we end with some first exercises to get the stability needed to tackle your first slope', '23 min', false, '28.02.2026'], + ['The History of Bridges', 'assets/ui-examples/images/Image/sanfrancisco_widescreen-thumbnail.jpg', 'BBC England', $glyph_with_text, 'One of the most monumental achievements of human kind is the invention of bridges. Crossing streets, rivers and sometimes oceans became a huge pillar for our our modern infrastructure. This documentary looks at the different types of bridges and how they have been developed and engineered throughout different cultures and centuries', '90 min', true, '15.12.2025'], + ['Mountains through the ages - the formation of giants', 'assets/ui-examples/images/Image/mountains_widescreen-thumbnail.jpg', 'ARD/ZDF, Canal Plus', null, "A fascinating look on the forces of nature that are able to move unimaginable tons of rocks. Find out how seemingly immovable landscape has transformed drastically through the incredible forces set free by earthquakes, vulcanos and water. This award-winning documentary traces the movement of the world's greatest mountain ranges throughout millions of years.", '45 min', false, '09.06.2026'] ]; - $data = new class ($card_data) implements DataRetrieval { - protected array $data = []; + $listing = $f->listing()->entity()->grid(new GridEntityRetrieval($card_data)); - public function __construct($card_data) - { - $this->data = $card_data; - } + return $renderer->render($listing); +} - public function getEntities( - Mapping $mapping, - ?Range $range, - ?array $additional_parameters - ): \Generator { - foreach ($this->data as $vid) { - yield $mapping->map($vid); +class GridEntityRetrieval implements EntityRetrieval +{ + public function __construct( + private readonly array $data, + ) { + } + + public function getEntities( + UIFactory $ui_factory, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters, + ): Generator { + foreach ($this->data as $index => $record) { + yield $this->mapRecord($ui_factory, $index, $record); + } + } + + public function getEntitiesByIds( + UIFactory $ui_factory, + Order $order, + array $entity_ids, + ): Generator { + foreach ($entity_ids as $entity_id) { + if (!isset($this->data[$entity_id])) { + continue; } + yield $this->mapRecord($ui_factory, $entity_id, $this->data[$entity_id]); } - }; + } + + protected function mapRecord(UIFactory $ui_factory, int|string $id, array $record): Entity + { + $glyph_user = $ui_factory->symbol()->glyph()->user() + ->withLabel('Created by'); + + $glyph_calendar = $ui_factory->symbol()->glyph()->calendar() + ->withLabel('Upload date'); + + $glyph_duration = $ui_factory->symbol()->glyph()->time() + ->withLabel('Duration'); + + list($title, $thumbnail_url, $creator, $availability, $description, $duration, $add_workflow, $date) = $record; + $managing_actions = [ + $ui_factory->button()->shy('Edit', '#'), + $ui_factory->button()->shy('Move', '#'), + ]; + $entity = $ui_factory->entity()->standard( + $id, + $ui_factory->link()->standard($title, ''), + $ui_factory->image()->responsive($thumbnail_url, $title)->withAction('#') + ) + ->withFeaturedProperties( + $ui_factory->listing()->property() + ->withProperty($glyph_user, $creator) + ) + ->withManagingActions(...$managing_actions) + ->withMainDetails( + $ui_factory->listing()->property() + ->withProperty($glyph_duration, $description, false) + ->withProperty($glyph_duration, $duration) + ->withProperty($glyph_calendar, $date) + ) + ->withPrioritizedReactions($ui_factory->button()->shy('Like', '#')->withSymbol($ui_factory->symbol()->glyph()->like())) + ; + if ($availability) { + $entity = $entity->withBlockingAvailabilityConditions( + $ui_factory->listing()->property() + ->withProperty('Status', $ui_factory->legacy()->content($availability), false) + ); + } + if ($add_workflow) { + $workflow_factory = $ui_factory->listing()->workflow(); + $dummy_step = $workflow_factory->step('', ''); + + $steps = [ + $workflow_factory->step('Upload video file', 'Upload an .mp4 file or start a recording.', '#') + ->withAvailability($dummy_step::NOT_ANYMORE)->withStatus($dummy_step::SUCCESSFULLY), + $workflow_factory->step('Cut video', 'Trim or remove parts of the video.', '#') + ->withAvailability($dummy_step::NOT_ANYMORE)->withStatus($dummy_step::NOT_STARTED), + $workflow_factory->step('Add subtitles', 'You must upload or generate subtitles for every video.', '#') + ->withAvailability($dummy_step::AVAILABLE)->withStatus($dummy_step::SUCCESSFULLY), + $workflow_factory->step('Publish', 'Set who can see this video.', '#') + ->withAvailability($dummy_step::AVAILABLE)->withStatus($dummy_step::NOT_STARTED), + ]; - $listing = $f->listing()->entity()->grid($record_to_entity) - ->withData($data); + $video_workflow = $workflow_factory->linear('Video Curation', $steps); - return $renderer->render($listing); + $entity = $entity->withWorkflow($video_workflow); + } + + return $entity; + } } diff --git a/components/ILIAS/UI/tests/Component/Entity/EntityTest.php b/components/ILIAS/UI/tests/Component/Entity/EntityTest.php index 8ea2951c946f..6308ba5cc6d3 100755 --- a/components/ILIAS/UI/tests/Component/Entity/EntityTest.php +++ b/components/ILIAS/UI/tests/Component/Entity/EntityTest.php @@ -132,7 +132,7 @@ public function testEntityWorkflowButtons(): void $video_workflow = $workflow_factory->linear("Video Curation", $steps); - $entity = $this->getEntityFactory()->standard('primary', 'secondary') + $entity = $this->getEntityFactory()->standard(1, 'primary', 'secondary') ->withWorkflow($video_workflow); $rendered_entity = $this->getDefaultRenderer()->render($entity); @@ -166,7 +166,7 @@ public function button(): I\Button\Factory public function listing(): I\Listing\Factory { return new I\Listing\Factory( - new I\Listing\Workflow\Factory, + new I\Listing\Workflow\Factory(), $this->characteristic_value_factory, new I\Listing\Entity\Factory(), ); diff --git a/components/ILIAS/UI/tests/Component/Listing/Entity/GridEntityListingTest.php b/components/ILIAS/UI/tests/Component/Listing/Entity/GridEntityListingTest.php index 35b5a85a26fb..bde28b60e32f 100644 --- a/components/ILIAS/UI/tests/Component/Listing/Entity/GridEntityListingTest.php +++ b/components/ILIAS/UI/tests/Component/Listing/Entity/GridEntityListingTest.php @@ -18,25 +18,43 @@ declare(strict_types=1); +use ILIAS\Data\Order; +use ILIAS\Data\Range; +use ILIAS\UI\Component as C; use ILIAS\UI\Implementation\Component\Entity; use ILIAS\UI\Implementation\Component as I; -use ILIAS\UI\Component as C; use ILIAS\UI\Factory as UIFactory; -use ILIAS\Data\Range; class GridEntityListingTest extends ILIAS_UI_TestBase { - public function getEntityMapping(): C\Listing\Entity\RecordToEntity + public function getEntityRetrieval(): C\Entity\EntityRetrieval { - return new class () implements C\Listing\Entity\RecordToEntity { - public function map( + return new class () implements C\Entity\EntityRetrieval { + public function getEntities( + UIFactory $ui_factory, + Range $range, + Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters, + ): Generator { + for ($i = 1; $i <= 3; $i++) { + yield $ui_factory->entity()->standard($i, 'primary', 'secondary'); + } + } + + public function getEntitiesByIds( UIFactory $ui_factory, - mixed $record - ): Entity\Entity { - return $ui_factory->entity()->standard('primary', 'secondary'); + Order $order, + array $entity_ids, + ): Generator { + foreach ($entity_ids as $entity_id) { + yield $ui_factory->entity()->standard($entity_id, 'primary', 'secondary'); + } } }; } + public function getUIFactory(): NoUIFactory { return new class ( @@ -56,7 +74,7 @@ public function listing(): I\Listing\Factory new I\Listing\Entity\Factory(), ); } - public function entity(): I\Entity\Factory + public function entity(): Entity\Factory { return new Entity\Factory(); } @@ -66,30 +84,15 @@ public function entity(): I\Entity\Factory public function testGridEntityListingFactory(): void { $this->assertInstanceOf( - C\Listing\Entity\EntityListing::class, - $this->getUIFactory()->listing()->entity()->grid($this->getEntityMapping()) + C\Listing\Entity\Entity::class, + $this->getUIFactory()->listing()->entity()->grid($this->getEntityRetrieval()) ); } public function testGridEntityListingRendering(): void { - $data = new class () implements C\Listing\Entity\DataRetrieval { - protected $data = [1,2,3]; - - public function getEntities( - C\Listing\Entity\Mapping $mapping, - ?Range $range, - ?array $additional_parameters - ): \Generator { - foreach ($this->data as $entry) { - yield $mapping->map($entry); - } - } - }; - $listing = $this->getUIFactory()->listing()->entity() - ->grid($this->getEntityMapping()) - ->withData($data); + ->grid($this->getEntityRetrieval()); $render = $this->getDefaultRenderer()->render($listing); $expected = <<data as $entry) { - yield $mapping->map($entry); - } - } - }; - $listing = $this->getUIFactory()->listing()->entity() - ->grid($this->getEntityMapping()) - ->withData($data); + ->grid($this->getEntityRetrieval()); $entities = iterator_to_array($listing->getEntities($this->getUIFactory())); From c921ec245f3c910fc55b3640f3037a05e7326fb8 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Tue, 14 Jul 2026 14:12:08 +0200 Subject: [PATCH 12/14] [Feature] UI: ConfirmationPrompt - remove MessageBox changes & fix ConfirmationRedirect Example + BriefEntitiy --- ...nksAndEntitiesMustBeExclusiveException.php | 35 ------- .../src/Component/MessageBox/MessageBox.php | 16 ---- components/ILIAS/UI/src/Factory.php | 3 +- ...oxRenderer.php => BriefEntityRenderer.php} | 21 +++- .../Component/Entity/Entity.php | 19 ++++ .../Entity/EntityRendererFactory.php | 9 +- .../Component/MessageBox/MessageBox.php | 22 ----- .../MessageBox/PromptContextRenderer.php | 2 - .../Component/MessageBox/Renderer.php | 2 - .../confirmationWithEntityList.php | 95 ------------------- .../examples/Prompt/Standard/confirmation.php | 35 ++++++- .../Prompt/State/Confirm/redirect.php | 35 ++++++- .../default/Entity/tpl.entity-abstract.html | 3 - .../default/Entity/tpl.entity-brief.html | 3 + .../Component/MessageBox/MessageBoxTest.php | 8 -- 15 files changed, 107 insertions(+), 201 deletions(-) delete mode 100644 components/ILIAS/UI/src/Component/MessageBox/Exception/LinksAndEntitiesMustBeExclusiveException.php rename components/ILIAS/UI/src/Implementation/Component/Entity/{MessageBoxRenderer.php => BriefEntityRenderer.php} (63%) delete mode 100644 components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php delete mode 100644 components/ILIAS/UI/src/templates/default/Entity/tpl.entity-abstract.html create mode 100644 components/ILIAS/UI/src/templates/default/Entity/tpl.entity-brief.html diff --git a/components/ILIAS/UI/src/Component/MessageBox/Exception/LinksAndEntitiesMustBeExclusiveException.php b/components/ILIAS/UI/src/Component/MessageBox/Exception/LinksAndEntitiesMustBeExclusiveException.php deleted file mode 100644 index c2baca93f169..000000000000 --- a/components/ILIAS/UI/src/Component/MessageBox/Exception/LinksAndEntitiesMustBeExclusiveException.php +++ /dev/null @@ -1,35 +0,0 @@ - - * Message Boxes consist of a mandatory message text, optional Buttons and either an optional Unordered List of Links - * OR an optional EntityList (but not both simultaneously). + * Message Boxes consist of a mandatory message text, optional Buttons and an optional Unordered List of Links. * There are four main types of Message Boxes, each is displayed in the according color: * 1. Failure, * 2. Success, diff --git a/components/ILIAS/UI/src/Implementation/Component/Entity/MessageBoxRenderer.php b/components/ILIAS/UI/src/Implementation/Component/Entity/BriefEntityRenderer.php similarity index 63% rename from components/ILIAS/UI/src/Implementation/Component/Entity/MessageBoxRenderer.php rename to components/ILIAS/UI/src/Implementation/Component/Entity/BriefEntityRenderer.php index f7b9e3d1d402..86d125ff8da0 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Entity/MessageBoxRenderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Entity/BriefEntityRenderer.php @@ -24,7 +24,10 @@ use ILIAS\UI\Renderer as RendererInterface; use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; -class MessageBoxRenderer extends AbstractComponentRenderer +/** + * Renders compact entities with primary visual only (e.g. in confirmation prompts). + */ +class BriefEntityRenderer extends AbstractComponentRenderer { public function render(Component\Component $component, RendererInterface $default_renderer): string { @@ -34,10 +37,18 @@ public function render(Component\Component $component, RendererInterface $defaul $this->cannotHandleComponent($component); } - protected function renderEntity(Entity $component, RendererInterface $default_renderer): string - { - $tpl = $this->getTemplate('tpl.entity-abstract.html', true, true); - $tpl->setVariable('CONTENT', $component->getPrimaryIdentifier()); + protected function renderEntity( + Component\Entity\Entity $component, + RendererInterface $default_renderer + ): string { + $tpl = $this->getTemplate('tpl.entity-brief.html', true, true); + + $primary_identifier = $component->getPrimaryIdentifier(); + $content = is_string($primary_identifier) + ? $primary_identifier + : $default_renderer->render($primary_identifier); + + $tpl->setVariable('CONTENT', $content); return $tpl->get(); } diff --git a/components/ILIAS/UI/src/Implementation/Component/Entity/Entity.php b/components/ILIAS/UI/src/Implementation/Component/Entity/Entity.php index 3f7d99a6017a..d6a43fe56f6a 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Entity/Entity.php +++ b/components/ILIAS/UI/src/Implementation/Component/Entity/Entity.php @@ -297,4 +297,23 @@ public function getWorkflow(): ?Workflow\Linear { return $this->workflow; } + + /** + * Entities without additional sections and without secondary visual + * can be rendered in a compact form (e.g. inside confirmation prompts). + */ + public function isCompactListItem(): bool + { + return $this->blocking_conditions === [] + && $this->featured_props === [] + && $this->main_details === [] + && $this->details === [] + && $this->availability === [] + && $this->personal_status === [] + && $this->prio_reactions === [] + && $this->reactions === [] + && $this->managing_actions === [] + && $this->workflow === null + && $this->secondary_identifier === ''; + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Entity/EntityRendererFactory.php b/components/ILIAS/UI/src/Implementation/Component/Entity/EntityRendererFactory.php index fb013a0c08ce..7b360038d67b 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Entity/EntityRendererFactory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Entity/EntityRendererFactory.php @@ -25,12 +25,17 @@ class EntityRendererFactory extends Render\DefaultRendererFactory { + private const PROMPT_STATE_CONTEXT = 'StateStatePrompt'; + public function getRendererInContext( Component\Component $component, array $contexts ): Render\AbstractComponentRenderer { - if (in_array('MessageBoxMessageBox', $contexts, true)) { - return new MessageBoxRenderer( + if ($component instanceof Entity + && in_array(self::PROMPT_STATE_CONTEXT, $contexts, true) + && $component->isCompactListItem() + ) { + return new BriefEntityRenderer( $this->ui_factory, $this->tpl_factory, $this->lng, diff --git a/components/ILIAS/UI/src/Implementation/Component/MessageBox/MessageBox.php b/components/ILIAS/UI/src/Implementation/Component/MessageBox/MessageBox.php index f3c736742828..4edd6e01fb35 100755 --- a/components/ILIAS/UI/src/Implementation/Component/MessageBox/MessageBox.php +++ b/components/ILIAS/UI/src/Implementation/Component/MessageBox/MessageBox.php @@ -42,7 +42,6 @@ class MessageBox implements C\MessageBox\MessageBox, IsPromptContentInternal private string $message_text; private array $buttons = []; private array $links = []; - private ?C\Listing\Entity\Entity $entity_list = null; public function __construct($type, string $message_text) { @@ -101,10 +100,6 @@ public function withButtons(array $buttons): C\MessageBox\MessageBox */ public function withLinks(array $links): C\MessageBox\MessageBox { - if (!empty($this->entity_list)) { - throw new C\MessageBox\Exception\LinksAndEntitiesMustBeExclusiveException(); - } - $types = array(C\Component::class); $this->checkArgListElements("links", $links, $types); @@ -113,23 +108,6 @@ public function withLinks(array $links): C\MessageBox\MessageBox return $clone; } - public function withEntityListing(C\Listing\Entity\Entity $entity_list): C\MessageBox\MessageBox - { - if (!empty($this->links)) { - throw new C\MessageBox\Exception\LinksAndEntitiesMustBeExclusiveException(); - } - - $clone = clone $this; - $clone->entity_list = $entity_list; - - return $clone; - } - - public function getEntityListing(): ?C\Listing\Entity\Entity - { - return $this->entity_list; - } - /** * @inheritdoc */ diff --git a/components/ILIAS/UI/src/Implementation/Component/MessageBox/PromptContextRenderer.php b/components/ILIAS/UI/src/Implementation/Component/MessageBox/PromptContextRenderer.php index 69693cad9e0e..dcb3610655fb 100644 --- a/components/ILIAS/UI/src/Implementation/Component/MessageBox/PromptContextRenderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/MessageBox/PromptContextRenderer.php @@ -60,8 +60,6 @@ public function render(Component\Component $component, RendererInterface $defaul ); $tpl->setVariable("LINK_LIST", $default_renderer->render($unordered)); - } elseif ($component->getEntityListing() !== null) { - $tpl->setVariable('LINK_LIST', $default_renderer->render($component->getEntityListing())); } $tpl->touchBlock($component->getType() . "_class"); diff --git a/components/ILIAS/UI/src/Implementation/Component/MessageBox/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/MessageBox/Renderer.php index 69954e8ff9d4..3423a3e8f89e 100755 --- a/components/ILIAS/UI/src/Implementation/Component/MessageBox/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/MessageBox/Renderer.php @@ -67,8 +67,6 @@ public function render(Component\Component $component, RendererInterface $defaul ); $tpl->setVariable("LINK_LIST", $default_renderer->render($unordered)); - } elseif ($component->getEntityListing() !== null) { - $tpl->setVariable("LINK_LIST", $default_renderer->render($component->getEntityListing())); } $tpl->touchBlock($component->getType() . "_class"); diff --git a/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php b/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php deleted file mode 100644 index 5b9d89075809..000000000000 --- a/components/ILIAS/UI/src/examples/MessageBox/Confirmation/confirmationWithEntityList.php +++ /dev/null @@ -1,95 +0,0 @@ - - * ILIAS shows a confirmation message box with an entity listing. - * --- - */ -function confirmationWithEntityList() -{ - global $DIC; - $f = $DIC->ui()->factory(); - $renderer = $DIC->ui()->renderer(); - - $buttons = [$f->button()->standard('Confirm', '#'), $f->button()->standard('Cancel', '#')]; - - return $renderer->render( - $f->messageBox()->confirmation('some message box') - ->withButtons($buttons) - ->withEntityListing($f->listing()->entity()->standard(new DemoEntityRetrieval())) - ); -} - -class DemoEntityRetrieval implements EntityRetrieval -{ - protected array $data = [ - ['jw', 'jimmywilson', 'jimmywilson@example.com', 'Jimmy Wilson'], - ['eb', 'emilybrown', 'emilybrown@example.com', 'Emily Brown'], - ]; - - public function getEntities( - \ILIAS\UI\Factory $ui_factory, - Range $range, - Order $order, - mixed $additional_viewcontrol_data, - mixed $filter_data, - mixed $additional_parameters, - ): Generator { - foreach ($this->data as $index => $record) { - yield $this->mapRecord($ui_factory, $index, $record); - } - } - - public function getEntitiesByIds( - \ILIAS\UI\Factory $ui_factory, - Order $order, - array $entity_ids, - ): Generator { - foreach ($entity_ids as $entity_id) { - if (!isset($this->data[$entity_id])) { - continue; - } - yield $this->mapRecord($ui_factory, $entity_id, $this->data[$entity_id]); - } - } - - protected function mapRecord(\ILIAS\UI\Factory $ui_factory, int|string $id, array $record): Entity - { - [$abbreviation, $login, $email, $name] = $record; - $avatar = $ui_factory->symbol()->avatar()->letter($abbreviation); - - return $ui_factory->entity()->standard($id, $name, $avatar) - ->withMainDetails( - $ui_factory->listing()->property() - ->withProperty('login', $login) - ->withProperty('mail', $email, false) - ); - } -} diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php index f1e060826728..65408bb01bd4 100644 --- a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php @@ -146,6 +146,13 @@ function retrieveEntityIds( class ConfirmationEntityRetrieval implements EntityRetrieval { + protected array $data = [ + ['jw', 'jimmywilson', 'jimmywilson@example.com', 'Jimmy Wilson', '2022-03-15 13:20:10', true], + ['eb', 'emilybrown', 'emilybrown@example.com', 'Emily Brown', '2022-03-16 10:45:32', false], + ['ms', 'michaelscott', 'michaelscott@example.com', 'Michael Scott', '2022-03-14 08:15:05', true], + ['kj', 'katiejones', 'katiejones@example.com', 'Katie Jones', '2022-03-17 15:30:50', true], + ]; + public function getEntities( \ILIAS\UI\Factory $ui_factory, Range $range, @@ -154,8 +161,8 @@ public function getEntities( mixed $filter_data, mixed $additional_parameters, ): Generator { - foreach ([1, 2, 3] as $entity_id) { - yield $this->getPseudoEntity($ui_factory, $entity_id); + foreach ($this->data as $index => $record) { + yield $this->mapRecord($ui_factory, $index, $record); } } @@ -165,12 +172,30 @@ public function getEntitiesByIds( array $entity_ids, ): Generator { foreach ($entity_ids as $entity_id) { - yield $this->getPseudoEntity($ui_factory, (int) $entity_id); + if (!isset($this->data[$entity_id])) { + continue; + } + yield $this->mapRecord($ui_factory, $entity_id, $this->data[$entity_id]); } } - protected function getPseudoEntity(\ILIAS\UI\Factory $ui_factory, int $entity_id): Entity + protected function mapRecord(\ILIAS\UI\Factory $ui_factory, int|string $id, array $record): Entity { - return $ui_factory->entity()->standard($entity_id, "Entity $entity_id", ''); + [$abbreviation, $login, $email, $name, $last_seen, $active] = $record; + $avatar = $ui_factory->symbol()->avatar()->letter($abbreviation); + + return $ui_factory->entity()->standard($id, $name, $avatar) + ->withMainDetails( + $ui_factory->listing()->property() + ->withProperty('login', $login) + ->withProperty('mail', $email, false) + ) + ->withDetails( + $ui_factory->listing()->property() + ->withItems([ + ['last seen', $last_seen], + ['active', $active ? 'yes' : 'no'], + ]) + ); } } diff --git a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php index 059c462626a2..2444b58e477d 100644 --- a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php +++ b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php @@ -82,7 +82,14 @@ function redirect(): string if ($http->request()->getMethod() === 'POST' && $query->has($process_token->getName())) { $process = $query->retrieve($process_token->getName(), $refinery->kindlyTo()->string()); if ($process !== '') { - $target = $url_builder->withParameter($success_token, '1')->buildURI(); + $parameter_prefix = implode(URLBuilder::SEPARATOR, $example_namespace) . URLBuilder::SEPARATOR; + $clean_uri = stripExampleParameters( + $data_factory->uri((string) $http->request()->getUri()), + $parameter_prefix + ); + [$redirect_builder, $redirect_success_token] = (new URLBuilder($clean_uri)) + ->acquireParameters($example_namespace, 'success'); + $target = $redirect_builder->withParameter($redirect_success_token, '1')->buildURI(); echo $renderer->renderAsync($factory->prompt()->state()->redirect($target)); exit; } @@ -95,18 +102,38 @@ function redirect(): string $prompt = $factory->prompt()->standard($open_uri); $trigger = $factory->button()->primary('Open confirm (redirect)', $prompt->getShowSignal($open_uri)); + $has_success_feedback = $query->has($success_token->getName()) + && $query->retrieve($success_token->getName(), $refinery->kindlyTo()->string()) !== ''; + $components = [$trigger, $prompt]; - if ($query->has($success_token->getName()) - && $query->retrieve($success_token->getName(), $refinery->kindlyTo()->string()) !== '') { + if ($has_success_feedback) { array_unshift( $components, $factory->messageBox()->success('Action confirmed. Entity ids were processed.') ); } - if (!$query->has($endpoint_token->getName())) { + $is_async = !$has_success_feedback + && $query->has($endpoint_token->getName()) + && $query->retrieve($endpoint_token->getName(), $refinery->kindlyTo()->string()) === 'true'; + + if (!$is_async) { return $renderer->render($components); } return ''; } + +/** + * Remove all example-specific query parameters before building a redirect target. + */ +function stripExampleParameters(\ILIAS\Data\URI $uri, string $parameter_prefix): \ILIAS\Data\URI +{ + $parameters = array_filter( + $uri->getParameters(), + static fn(string $key): bool => !str_starts_with($key, $parameter_prefix), + ARRAY_FILTER_USE_KEY + ); + + return $uri->withParameters($parameters); +} diff --git a/components/ILIAS/UI/src/templates/default/Entity/tpl.entity-abstract.html b/components/ILIAS/UI/src/templates/default/Entity/tpl.entity-abstract.html deleted file mode 100644 index 3a62c7c338cc..000000000000 --- a/components/ILIAS/UI/src/templates/default/Entity/tpl.entity-abstract.html +++ /dev/null @@ -1,3 +0,0 @@ -
- {CONTENT} -
\ No newline at end of file diff --git a/components/ILIAS/UI/src/templates/default/Entity/tpl.entity-brief.html b/components/ILIAS/UI/src/templates/default/Entity/tpl.entity-brief.html new file mode 100644 index 000000000000..cea5d6123b0a --- /dev/null +++ b/components/ILIAS/UI/src/templates/default/Entity/tpl.entity-brief.html @@ -0,0 +1,3 @@ +
+ {CONTENT} +
diff --git a/components/ILIAS/UI/tests/Component/MessageBox/MessageBoxTest.php b/components/ILIAS/UI/tests/Component/MessageBox/MessageBoxTest.php index 3d727f2265e5..ed3e36942f89 100755 --- a/components/ILIAS/UI/tests/Component/MessageBox/MessageBoxTest.php +++ b/components/ILIAS/UI/tests/Component/MessageBox/MessageBoxTest.php @@ -66,14 +66,6 @@ public static function getMessageboxTypeProvider(): array public function getUIFactory(): NoUIFactory { return new class () extends NoUIFactory { - public function listing(): IC\Listing\Factory - { - return new IC\Listing\Factory( - new IC\Listing\Workflow\Factory(), - new IC\Listing\CharacteristicValue\Factory(), - new IC\Listing\Entity\Factory(), - ); - } }; } From 229004e2c109ee1e8963d533a8e54d44c0851370 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Tue, 14 Jul 2026 14:16:03 +0200 Subject: [PATCH 13/14] [Feature] UI: ConfirmationPrompt - fix MesasgeBox Test --- .../UI/tests/Component/MessageBox/MessageBoxTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/components/ILIAS/UI/tests/Component/MessageBox/MessageBoxTest.php b/components/ILIAS/UI/tests/Component/MessageBox/MessageBoxTest.php index ed3e36942f89..3d727f2265e5 100755 --- a/components/ILIAS/UI/tests/Component/MessageBox/MessageBoxTest.php +++ b/components/ILIAS/UI/tests/Component/MessageBox/MessageBoxTest.php @@ -66,6 +66,14 @@ public static function getMessageboxTypeProvider(): array public function getUIFactory(): NoUIFactory { return new class () extends NoUIFactory { + public function listing(): IC\Listing\Factory + { + return new IC\Listing\Factory( + new IC\Listing\Workflow\Factory(), + new IC\Listing\CharacteristicValue\Factory(), + new IC\Listing\Entity\Factory(), + ); + } }; } From 425dd0afae554846c46924d11714570a855f9389 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Thu, 16 Jul 2026 13:23:58 +0200 Subject: [PATCH 14/14] [Feature] UI: ConfirmationPrompt - fix docs wording --- .../ILIAS/UI/src/examples/Prompt/Standard/confirmation.php | 1 + .../ILIAS/UI/src/examples/Prompt/State/Confirm/base.php | 2 +- .../ILIAS/UI/src/examples/Prompt/State/Confirm/close.php | 7 ++++--- .../UI/src/examples/Prompt/State/Confirm/redirect.php | 5 +++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php index 65408bb01bd4..334756dafab7 100644 --- a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php @@ -36,6 +36,7 @@ * expected output: > * A button opens a prompt listing selected entities with a confirmation question. * Submitting posts the entity ids; the result is shown in the prompt. + * The prompt can be closed. * --- */ function confirmation(): string diff --git a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php index 14e938699b7d..5d97a069bafc 100644 --- a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php +++ b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php @@ -98,7 +98,7 @@ function base(): string ->buildURI(); $prompt = $factory->prompt()->standard($open_uri); - $trigger = $factory->button()->primary('Open confirm (show result)', $prompt->getShowSignal($open_uri)); + $trigger = $factory->button()->primary('Open Confirmation (And Show Result)', $prompt->getShowSignal($open_uri)); if (!$query->has($endpoint_token->getName())) { return $renderer->render([$trigger, $prompt]); diff --git a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/close.php b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/close.php index 78142a21c585..5e10d0d92c58 100644 --- a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/close.php +++ b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/close.php @@ -30,8 +30,9 @@ * After confirming, the consumer returns a close state and the prompt is dismissed. * * expected output: > - * A button opens a confirmation prompt. Confirming closes the prompt without - * showing further content inside it. + * A button opens a confirmation prompt. Confirming closes the prompt. + * No further information is displayed. + * The prompt can also be closed. * --- */ function close(): string @@ -89,7 +90,7 @@ function close(): string ->buildURI(); $prompt = $factory->prompt()->standard($open_uri); - $trigger = $factory->button()->primary('Open confirm (close prompt)', $prompt->getShowSignal($open_uri)); + $trigger = $factory->button()->primary('Open Confirmation (And Close Prompt)', $prompt->getShowSignal($open_uri)); $hint = $factory->messageBox()->info('After confirming, the prompt closes.'); if (!$query->has($endpoint_token->getName())) { diff --git a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php index 2444b58e477d..bbb80bac6fc4 100644 --- a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php +++ b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php @@ -32,7 +32,8 @@ * * expected output: > * A button opens a confirmation prompt. Confirming redirects the page and - * shows a success message below the trigger button. + * shows a success message above the trigger button. + * The prompt can also be closed. * --- */ function redirect(): string @@ -100,7 +101,7 @@ function redirect(): string ->buildURI(); $prompt = $factory->prompt()->standard($open_uri); - $trigger = $factory->button()->primary('Open confirm (redirect)', $prompt->getShowSignal($open_uri)); + $trigger = $factory->button()->primary('Open Confirmation (And Redirect)', $prompt->getShowSignal($open_uri)); $has_success_feedback = $query->has($success_token->getName()) && $query->retrieve($success_token->getName(), $refinery->kindlyTo()->string()) !== '';