From 1458a847059000b8adf260b13474f3c58c349ec6 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Fri, 4 Apr 2025 15:28:56 +0200 Subject: [PATCH 01/18] 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 5e54741176261a31cfa2d8e08cd755e1515f5426 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Mon, 7 Apr 2025 15:34:10 +0200 Subject: [PATCH 02/18] 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 41931f7551bf63a1f546db025e9a7c1a99a9dad1 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Thu, 10 Apr 2025 09:53:35 +0200 Subject: [PATCH 03/18] New Renderer for Entities inside MessageBox --- components/ILIAS/UI/UI.php | 10 ++++ .../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, 118 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 1fd13c1e888f..ce627e49bf85 100644 --- a/components/ILIAS/UI/UI.php +++ b/components/ILIAS/UI/UI.php @@ -551,6 +551,16 @@ 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], + ), new UI\Implementation\Component\Listing\ListingRendererFactory( $use[UI\Implementation\FactoryInternal::class], $internal[UI\Implementation\Render\TemplateFactory::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 851679081989..33e86641ca20 100755 --- a/components/ILIAS/UI/src/Implementation/Render/FSLoader.php +++ b/components/ILIAS/UI/src/Implementation/Render/FSLoader.php @@ -29,6 +29,7 @@ use ILIAS\UI\Implementation\Component\Input\Container\Form\Form; use ILIAS\UI\Implementation\Component\Menu\Menu; use ILIAS\UI\Implementation\Component\Listing\Listing; +use ILIAS\UI\Component\Entity\Entity; /** * Loads renderers for components from the file system. @@ -52,6 +53,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, private RendererFactory $listing_renderer_factory, ) { } @@ -77,6 +79,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 114b28c714cd4151157ab54c6ed14ca0e884b8be Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Tue, 6 May 2025 13:33:45 +0200 Subject: [PATCH 04/18] 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 0412bff2ac42..5bf1f5466c2b 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 { @@ -480,6 +481,16 @@ public function getDefaultRenderer( $help_text_retriever, $this->getUploadLimitResolver(), ), + new EntityRendererFactory( + $ui_factory, + $tpl_factory, + $lng, + $js_binding, + $image_path_resolver, + $data_factory, + $help_text_retriever, + $this->getUploadLimitResolver(), + ), new I\Listing\ListingRendererFactory( $ui_factory, $tpl_factory, diff --git a/components/ILIAS/UI/tests/InitUIFramework.php b/components/ILIAS/UI/tests/InitUIFramework.php index b5425805726e..11d0994e8d3a 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"], + ), new ILIAS\UI\Implementation\Component\Listing\ListingRendererFactory( $c["ui.factory"], $c["ui.template_factory"], diff --git a/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php b/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php index 634360242a54..62902ec1a023 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 RendererFactory & MockObject $list_renderer_factory; protected FSLoader $fs_loader; @@ -53,6 +55,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->list_renderer_factory = $this->createMock(RendererFactory::class); $this->fs_loader = new FSLoader( @@ -62,6 +65,7 @@ protected function setUp(): void $this->message_box_renderer_factory, $this->form_renderer_factory, $this->menu_renderer_factory, + $this->entity_renderer_factory, $this->list_renderer_factory, ); @@ -142,6 +146,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 4a09376273c0243ccf043f750241feab72ca3e34 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Wed, 27 Aug 2025 11:00:02 +0200 Subject: [PATCH 05/18] Remove copyright from ConfirmationModal examples --- .../Confirmation/confirmationWithEntityList.php | 16 ---------------- .../Prompt/Standard/confirmation_prompt.php | 16 ---------------- 2 files changed, 32 deletions(-) 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/18] 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/18] [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 33886e94266c755f46ce1520689f234d86c4c5a7 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Thu, 2 Jul 2026 12:04:11 +0200 Subject: [PATCH 08/18] [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 ce627e49bf85..4ee42fbb96d9 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 11d0994e8d3a..36e1d0eeb458 100755 --- a/components/ILIAS/UI/tests/InitUIFramework.php +++ b/components/ILIAS/UI/tests/InitUIFramework.php @@ -439,8 +439,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 b8171cdf898a3bb0fd740330028870522b761616 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Thu, 2 Jul 2026 12:17:31 +0200 Subject: [PATCH 09/18] [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 a3ffa460ce9056011af42a6d72c58918cd88ab2e Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Tue, 14 Jul 2026 10:09:18 +0200 Subject: [PATCH 10/18] [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 695a6e7c2cc87fdd2afcb875b90ccd17525682fe Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Tue, 14 Jul 2026 14:12:08 +0200 Subject: [PATCH 11/18] [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 0a24e094684887da1b850adf477940f56f1dd30d Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Tue, 14 Jul 2026 14:16:03 +0200 Subject: [PATCH 12/18] [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 fd846e3f60c74cb7f9bb3e5bcbd298ff1804ff93 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Thu, 16 Jul 2026 13:23:58 +0200 Subject: [PATCH 13/18] [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()) !== ''; From 0748e1d7436eff04fcb6aac1b67309f3b225a4b8 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Mon, 24 Aug 2026 11:40:53 +0200 Subject: [PATCH 14/18] [Feature] UI: ConfirmationPrompt - fix entity ids in query instead of post --- .../UI/src/Component/Prompt/State/Factory.php | 10 +- .../Component/Prompt/State/Confirmation.php | 6 +- .../Component/Prompt/State/Factory.php | 13 +- .../examples/Prompt/Standard/confirmation.php | 59 ++++++- .../examples/Prompt/State/Confirm/base.php | 57 ++++++- .../Prompt/State/ConfirmStateTest.php | 145 ++++++++++++++++++ 6 files changed, 272 insertions(+), 18 deletions(-) create mode 100644 components/ILIAS/UI/tests/Component/Prompt/State/ConfirmStateTest.php diff --git a/components/ILIAS/UI/src/Component/Prompt/State/Factory.php b/components/ILIAS/UI/src/Component/Prompt/State/Factory.php index bb67ac369694..d9a7993a48f6 100644 --- a/components/ILIAS/UI/src/Component/Prompt/State/Factory.php +++ b/components/ILIAS/UI/src/Component/Prompt/State/Factory.php @@ -60,11 +60,17 @@ 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 below it and a form with hidden fields for the entity ids internally. + * listing below it and a Standard Form with Kitchen Sink Hidden Inputs + * (one per entity id, grouped under the URLBuilderToken name). + * IDs are never attached to the form action URL. + * The Prompt renders the form without its own submit buttons and places + * Confirm/Cancel in the Prompt footer via the form submit signal. * 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. + * and posts the entity ids in the request body when confirmed. + * Consumers retrieve them from the Standard Form via withRequest/getData, + * keyed by the URLBuilderToken name. * * context: * - The Prompt State is used for Prompts. 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 85373305e77d..7ee650de7f4b 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php @@ -21,10 +21,10 @@ namespace ILIAS\UI\Implementation\Component\Prompt\State; 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\Input\Container\Form\Standard as StandardForm; use ILIAS\UI\Implementation\Component\Prompt\IsPromptContentInternal; class Confirmation implements IsPromptContentInternal @@ -34,7 +34,7 @@ class Confirmation implements IsPromptContentInternal public function __construct( private readonly MessageBox $message_box, private readonly EntityListing $entity_listing, - private readonly Form $form, + private readonly StandardForm $form, private readonly string $title, ) { } @@ -49,7 +49,7 @@ public function getEntityListing(): EntityListing return $this->entity_listing; } - public function getForm(): Form + public function getForm(): StandardForm { 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 79da39b58363..e2fc9493a30b 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php @@ -56,9 +56,18 @@ public function confirm( $message_box = $this->messagebox_factory->confirmation($question); + $hidden_inputs = []; + foreach (array_values($entity_ids) as $index => $entity_id) { + $hidden_inputs[(string) $index] = $this->input_factory->field() + ->hidden() + ->withValue((string) $entity_id); + } + $form = $this->input_factory->container()->form()->standard( - (string) $post_url->withParameter($post_parameter, $entity_ids)->buildURI(), - [] + (string) $post_url->deleteParameter($post_parameter)->buildURI(), + [ + $post_parameter->getName() => $this->input_factory->field()->group($hidden_inputs), + ] ); $content = new Confirmation( diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php index 334756dafab7..0a536ca2e5d6 100644 --- a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php @@ -31,11 +31,12 @@ * --- * description: > * A confirmation prompt for deleting or changing multiple entities. - * The UI framework composes message box, entity listing and hidden form fields. + * The UI framework composes message box, entity listing and a Standard Form + * with Kitchen Sink Hidden Inputs. * * 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. + * Submitting posts the entity ids via the Standard Form (POST body); the result is shown in the prompt. * The prompt can be closed. * --- */ @@ -87,7 +88,13 @@ function confirmation(): string 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) ?? []; + $entity_ids = retrievePostedEntityIds( + $factory, + $http->request(), + $entities_token->getName(), + $demo_entity_ids, + $refinery, + ); $message = $factory->messageBox()->success( 'Submitted entity ids: ' . implode(', ', array_map('strval', $entity_ids)) ); @@ -116,15 +123,15 @@ function confirmation(): string * @return array|null */ function retrieveEntityIds( - \ILIAS\HTTP\Wrapper\RequestWrapper $query, + \ILIAS\HTTP\Wrapper\RequestWrapper $wrapper, string $parameter_name, \ILIAS\Refinery\Factory $refinery, ): ?array { - if (!$query->has($parameter_name)) { + if (!$wrapper->has($parameter_name)) { return null; } - $raw = $query->retrieve( + $raw = $wrapper->retrieve( $parameter_name, $refinery->custom()->transformation(static fn(mixed $value): mixed => $value) ); @@ -145,6 +152,46 @@ function retrieveEntityIds( return $refinery->kindlyTo()->listOf($refinery->kindlyTo()->int())->transform($raw); } +/** + * Rebuild the same Standard Form shape as confirm() (group of Hidden Inputs), + * then read posted ids via withRequest/getData. + * + * @param array $ids_for_form_shape + * @return list + */ +function retrievePostedEntityIds( + \ILIAS\UI\Factory $ui_factory, + \Psr\Http\Message\ServerRequestInterface $request, + string $token_name, + array $ids_for_form_shape, + \ILIAS\Refinery\Factory $refinery, +): array { + $hiddens = []; + foreach (array_values($ids_for_form_shape) as $index => $_) { + $hiddens[(string) $index] = $ui_factory->input()->field()->hidden(); + } + + $form = $ui_factory->input()->container()->form()->standard('#', [ + $token_name => $ui_factory->input()->field()->group($hiddens), + ])->withRequest($request); + + $raw = $form->getData()[$token_name] ?? null; + if (!is_array($raw)) { + return []; + } + + $raw = array_values(array_filter( + $raw, + static fn(mixed $value): bool => $value !== '' && $value !== null + )); + + if ($raw === []) { + return []; + } + + return $refinery->kindlyTo()->listOf($refinery->kindlyTo()->int())->transform($raw); +} + class ConfirmationEntityRetrieval implements EntityRetrieval { protected array $data = [ 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 5d97a069bafc..cd2a3bb2f0ae 100644 --- a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php +++ b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php @@ -34,7 +34,8 @@ * * 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. + * Confirming posts the entity ids via the Standard Form (Kitchen Sink Hidden Inputs); + * a success message is shown inside the prompt. * --- */ function base(): string @@ -82,7 +83,13 @@ function base(): string 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) ?? []; + $entity_ids = retrievePostedEntityIds( + $factory, + $http->request(), + $entities_token->getName(), + $demo_entity_ids, + $refinery, + ); $message = $factory->messageBox()->success( 'Submitted entity ids: ' . implode(', ', array_map('strval', $entity_ids)) ); @@ -111,15 +118,15 @@ function base(): string * @return array|null */ function retrieveEntityIds( - \ILIAS\HTTP\Wrapper\RequestWrapper $query, + \ILIAS\HTTP\Wrapper\RequestWrapper $wrapper, string $parameter_name, \ILIAS\Refinery\Factory $refinery, ): ?array { - if (!$query->has($parameter_name)) { + if (!$wrapper->has($parameter_name)) { return null; } - $raw = $query->retrieve( + $raw = $wrapper->retrieve( $parameter_name, $refinery->custom()->transformation(static fn(mixed $value): mixed => $value) ); @@ -140,6 +147,46 @@ function retrieveEntityIds( return $refinery->kindlyTo()->listOf($refinery->kindlyTo()->int())->transform($raw); } +/** + * Rebuild the same Standard Form shape as confirm() (group of Hidden Inputs), + * then read posted ids via withRequest/getData. + * + * @param array $ids_for_form_shape + * @return list + */ +function retrievePostedEntityIds( + \ILIAS\UI\Factory $ui_factory, + \Psr\Http\Message\ServerRequestInterface $request, + string $token_name, + array $ids_for_form_shape, + \ILIAS\Refinery\Factory $refinery, +): array { + $hiddens = []; + foreach (array_values($ids_for_form_shape) as $index => $_) { + $hiddens[(string) $index] = $ui_factory->input()->field()->hidden(); + } + + $form = $ui_factory->input()->container()->form()->standard('#', [ + $token_name => $ui_factory->input()->field()->group($hiddens), + ])->withRequest($request); + + $raw = $form->getData()[$token_name] ?? null; + if (!is_array($raw)) { + return []; + } + + $raw = array_values(array_filter( + $raw, + static fn(mixed $value): bool => $value !== '' && $value !== null + )); + + if ($raw === []) { + return []; + } + + return $refinery->kindlyTo()->listOf($refinery->kindlyTo()->int())->transform($raw); +} + class ConfirmStateEntityRetrieval implements EntityRetrieval { public function getEntities( diff --git a/components/ILIAS/UI/tests/Component/Prompt/State/ConfirmStateTest.php b/components/ILIAS/UI/tests/Component/Prompt/State/ConfirmStateTest.php new file mode 100644 index 000000000000..201bdde07050 --- /dev/null +++ b/components/ILIAS/UI/tests/Component/Prompt/State/ConfirmStateTest.php @@ -0,0 +1,145 @@ +getFormMock('https://example.com/go'); + + $form_factory = $this->createMock(C\Input\Container\Form\Factory::class); + $form_factory->method('standard')->willReturnCallback( + function (string $url, array $inputs) use (&$captured_url, &$captured_inputs, $form) { + $captured_url = $url; + $captured_inputs = $inputs; + + return $form; + } + ); + + $container_factory = $this->createMock(C\Input\Container\Factory::class); + $container_factory->method('form')->willReturn($form_factory); + + $input_factory = $this->createMock(C\Input\Factory::class); + $input_factory->method('container')->willReturn($container_factory); + $input_factory->method('field')->willReturn($this->getFieldFactory()); + + $factory = new PromptState\Factory( + new ListingEntity\Factory(), + new MessageBox\Factory(), + $input_factory + ); + + $url_builder = new URLBuilder(new URI('https://example.com/go')); + [$url_builder, $token] = $url_builder->acquireParameters(['test'], 'entities'); + + $state = $factory->confirm( + $this->getEntityRetrieval(), + $url_builder, + $token, + [11, 22], + 'question', + 'title', + ); + + $this->assertIsString($captured_url); + $this->assertStringNotContainsString('11', $captured_url); + $this->assertStringNotContainsString('22', $captured_url); + $this->assertStringNotContainsString($token->getName(), $captured_url); + + $this->assertIsArray($captured_inputs); + $this->assertArrayHasKey($token->getName(), $captured_inputs); + $group = $captured_inputs[$token->getName()]; + $this->assertInstanceOf(C\Input\Field\Group::class, $group); + $this->assertSame(['11', '22'], array_values($group->getValue())); + + $content = $state->getContent(); + $this->assertInstanceOf(PromptState\Confirmation::class, $content); + $this->assertSame($form, $content->getForm()); + } + + private function getFormMock(string $post_url): I\Input\Container\Form\Standard + { + $form = $this->getMockBuilder(I\Input\Container\Form\Standard::class) + ->disableOriginalConstructor() + ->onlyMethods(['getPostURL', 'getSubmitSignal']) + ->getMock(); + $form->method('getPostURL')->willReturn($post_url); + $form->method('getSubmitSignal')->willReturn(new Signal('submit_signal')); + + return $form; + } + + private function getEntityRetrieval(): C\Entity\EntityRetrieval + { + return new class () implements C\Entity\EntityRetrieval { + public function getEntities( + ILIAS\UI\Factory $ui_factory, + ILIAS\Data\Range $range, + ILIAS\Data\Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters, + ): Generator { + yield from []; + } + + public function getEntitiesByIds( + ILIAS\UI\Factory $ui_factory, + ILIAS\Data\Order $order, + array $entity_ids, + ): Generator { + yield from []; + } + }; + } + + public function getUIFactory(): NoUIFactory + { + return new class () extends NoUIFactory { + public function messageBox(): MessageBox\Factory + { + return new MessageBox\Factory(); + } + + public function button(): I\Button\Factory + { + return new I\Button\Factory(); + } + }; + } +} From 21ff9c04e7973642649bf16d037ff91cce1d1ea5 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Tue, 25 Aug 2026 11:42:01 +0200 Subject: [PATCH 15/18] [Feature] UI: ConfirmationPrompt - getConfirmedData --- .../Entity/{Entity.php => EntityListing.php} | 4 +- .../UI/src/Component/Listing/Entity/Grid.php | 2 +- .../src/Component/Listing/Entity/Standard.php | 2 +- .../UI/src/Component/Prompt/State/Factory.php | 3 +- .../UI/src/Component/Prompt/State/State.php | 8 ++ .../Listing/Entity/EntityListing.php | 2 +- .../Component/Prompt/State/Confirmation.php | 26 ++++++- .../Component/Prompt/State/Factory.php | 42 ++++++++--- .../Component/Prompt/State/State.php | 14 ++++ .../examples/Prompt/Standard/confirmation.php | 52 ++----------- .../examples/Prompt/State/Confirm/base.php | 52 ++----------- .../Listing/Entity/EntityListingTest.php | 2 +- .../Listing/Entity/GridEntityListingTest.php | 2 +- .../Prompt/State/ConfirmStateTest.php | 75 +++++++++++++++++++ 14 files changed, 174 insertions(+), 112 deletions(-) rename components/ILIAS/UI/src/Component/Listing/Entity/{Entity.php => EntityListing.php} (88%) diff --git a/components/ILIAS/UI/src/Component/Listing/Entity/Entity.php b/components/ILIAS/UI/src/Component/Listing/Entity/EntityListing.php similarity index 88% rename from components/ILIAS/UI/src/Component/Listing/Entity/Entity.php rename to components/ILIAS/UI/src/Component/Listing/Entity/EntityListing.php index be71e6311bbe..d92cedebf9f1 100644 --- a/components/ILIAS/UI/src/Component/Listing/Entity/Entity.php +++ b/components/ILIAS/UI/src/Component/Listing/Entity/EntityListing.php @@ -23,8 +23,8 @@ use ILIAS\UI\Component\Component; /** - * This is what an EntityListings looks like + * This is what an Entity Listing looks like. */ -interface Entity extends Component +interface EntityListing extends Component { } diff --git a/components/ILIAS/UI/src/Component/Listing/Entity/Grid.php b/components/ILIAS/UI/src/Component/Listing/Entity/Grid.php index 0f7312a9b0cb..b9e060d93529 100644 --- a/components/ILIAS/UI/src/Component/Listing/Entity/Grid.php +++ b/components/ILIAS/UI/src/Component/Listing/Entity/Grid.php @@ -20,6 +20,6 @@ namespace ILIAS\UI\Component\Listing\Entity; -interface Grid extends Entity +interface Grid extends EntityListing { } diff --git a/components/ILIAS/UI/src/Component/Listing/Entity/Standard.php b/components/ILIAS/UI/src/Component/Listing/Entity/Standard.php index 2cafc24c3127..2b6c6546d7c5 100755 --- a/components/ILIAS/UI/src/Component/Listing/Entity/Standard.php +++ b/components/ILIAS/UI/src/Component/Listing/Entity/Standard.php @@ -23,6 +23,6 @@ /** * This is what a Standard EntityListings looks like */ -interface Standard extends Entity +interface Standard extends EntityListing { } diff --git a/components/ILIAS/UI/src/Component/Prompt/State/Factory.php b/components/ILIAS/UI/src/Component/Prompt/State/Factory.php index d9a7993a48f6..ab888f885d55 100644 --- a/components/ILIAS/UI/src/Component/Prompt/State/Factory.php +++ b/components/ILIAS/UI/src/Component/Prompt/State/Factory.php @@ -69,8 +69,7 @@ public function show( * effect: > * The Prompt shows the confirmation question, lists affected entities * and posts the entity ids in the request body when confirmed. - * Consumers retrieve them from the Standard Form via withRequest/getData, - * keyed by the URLBuilderToken name. + * Consumers retrieve them via getConfirmedData() on this state. * * context: * - The Prompt State is used for Prompts. diff --git a/components/ILIAS/UI/src/Component/Prompt/State/State.php b/components/ILIAS/UI/src/Component/Prompt/State/State.php index f97128ceb789..0aae8b39f00b 100644 --- a/components/ILIAS/UI/src/Component/Prompt/State/State.php +++ b/components/ILIAS/UI/src/Component/Prompt/State/State.php @@ -21,6 +21,7 @@ namespace ILIAS\UI\Component\Prompt\State; use ILIAS\UI\Component\Component; +use Psr\Http\Message\ServerRequestInterface; interface State extends Component { @@ -28,4 +29,11 @@ interface State extends Component * Get a Prompts State like this, but provide it with an explicit title. */ public function withTitle(string $title): self; + + /** + * Entity ids posted by a confirmation form. Only available on confirm states. + * + * @return array + */ + public function getConfirmedData(ServerRequestInterface $request): array; } 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 f2e61bfacc41..06df12bac733 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/EntityListing.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Entity/EntityListing.php @@ -26,7 +26,7 @@ use ILIAS\Data\Range; use ILIAS\Data\Order; -abstract class EntityListing implements I\Entity +abstract class EntityListing implements I\EntityListing { use ComponentHelper; 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 7ee650de7f4b..c200159faa9c 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php @@ -21,11 +21,12 @@ namespace ILIAS\UI\Implementation\Component\Prompt\State; use ILIAS\UI\Component\Button\Button; -use ILIAS\UI\Component\Listing\Entity\Entity as EntityListing; +use ILIAS\UI\Component\Listing\Entity\EntityListing; use ILIAS\UI\Component\MessageBox\MessageBox; use ILIAS\UI\Implementation\Component\ComponentHelper; use ILIAS\UI\Implementation\Component\Input\Container\Form\Standard as StandardForm; use ILIAS\UI\Implementation\Component\Prompt\IsPromptContentInternal; +use Psr\Http\Message\ServerRequestInterface; class Confirmation implements IsPromptContentInternal { @@ -36,6 +37,7 @@ public function __construct( private readonly EntityListing $entity_listing, private readonly StandardForm $form, private readonly string $title, + private readonly string $post_parameter_name, ) { } @@ -54,6 +56,28 @@ public function getForm(): StandardForm return $this->form; } + /** + * @return array + */ + public function getPostedData(ServerRequestInterface $request): array + { + $data = $this->form->withRequest($request)->getData(); + $raw = is_array($data) ? ($data[$this->post_parameter_name] ?? null) : null; + if (!is_array($raw)) { + return []; + } + + $ids = []; + foreach ($raw as $value) { + if ($value === null || $value === '') { + continue; + } + $ids[] = (string) $value; + } + + return $ids; + } + public function getPromptTitle(): string { return $this->title; 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 e2fc9493a30b..fb32902cb5da 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php @@ -28,6 +28,7 @@ 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; +use ILIAS\UI\Implementation\Component\Input\Container\Form\Standard as StandardForm; class Factory implements I\State\Factory { @@ -55,19 +56,10 @@ public function confirm( $listing = $this->listing_entity_factory->standard($listing_retrieval); $message_box = $this->messagebox_factory->confirmation($question); - - $hidden_inputs = []; - foreach (array_values($entity_ids) as $index => $entity_id) { - $hidden_inputs[(string) $index] = $this->input_factory->field() - ->hidden() - ->withValue((string) $entity_id); - } - - $form = $this->input_factory->container()->form()->standard( + $form = $this->entityIdsForm( (string) $post_url->deleteParameter($post_parameter)->buildURI(), - [ - $post_parameter->getName() => $this->input_factory->field()->group($hidden_inputs), - ] + $post_parameter, + $entity_ids, ); $content = new Confirmation( @@ -75,6 +67,7 @@ public function confirm( $listing, $form, $title, + $post_parameter->getName(), ); return (new State($content))->withTitle($title); @@ -91,4 +84,29 @@ public function redirect(URI $redirect): State return (new State(null)) ->withRedirect($redirect); } + + /** + * @param array $entity_ids + */ + private function entityIdsForm( + string $post_url, + URLBuilderToken $post_parameter, + array $entity_ids, + ): StandardForm { + $hidden_inputs = []; + foreach (array_values($entity_ids) as $index => $entity_id) { + $hidden_inputs[(string) $index] = $this->input_factory->field() + ->hidden() + ->withValue((string) $entity_id); + } + + return $this->input_factory->container()->form()->standard( + $post_url, + [ + $post_parameter->getName() => $this->input_factory->field() + ->group($hidden_inputs) + ->withDedicatedName($post_parameter->getName()), + ] + ); + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/State.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/State.php index 36e85c616040..54e494f37f25 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/State.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/State.php @@ -25,6 +25,7 @@ use ILIAS\UI\Component\Prompt as I; use ILIAS\UI\Implementation\Component\ComponentHelper; use ILIAS\Data\URI; +use Psr\Http\Message\ServerRequestInterface; /** * @@ -58,6 +59,19 @@ public function withTitle(string $title): self return $clone; } + /** + * @return array + */ + public function getConfirmedData(ServerRequestInterface $request): array + { + $content = $this->content; + if (!$content instanceof Confirmation) { + throw new \LogicException('Confirmed data is only available on confirm states.'); + } + + return $content->getPostedData($request); + } + public function withContent(I\IsPromptContent $content): self { $clone = clone $this; diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php index 0a536ca2e5d6..9d0e89d73791 100644 --- a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php @@ -88,13 +88,15 @@ function confirmation(): string if ($http->request()->getMethod() === 'POST' && $query->has($process_token->getName())) { $process = $query->retrieve($process_token->getName(), $refinery->kindlyTo()->string()); if ($process !== '') { - $entity_ids = retrievePostedEntityIds( - $factory, - $http->request(), - $entities_token->getName(), + $state = $factory->prompt()->state()->confirm( + new ConfirmationEntityRetrieval(), + $url_builder->withParameter($process_token, '1'), + $entities_token, $demo_entity_ids, - $refinery, + 'Are you sure you want to perform this action?', + 'Performing some action', ); + $entity_ids = $state->getConfirmedData($http->request()); $message = $factory->messageBox()->success( 'Submitted entity ids: ' . implode(', ', array_map('strval', $entity_ids)) ); @@ -152,46 +154,6 @@ function retrieveEntityIds( return $refinery->kindlyTo()->listOf($refinery->kindlyTo()->int())->transform($raw); } -/** - * Rebuild the same Standard Form shape as confirm() (group of Hidden Inputs), - * then read posted ids via withRequest/getData. - * - * @param array $ids_for_form_shape - * @return list - */ -function retrievePostedEntityIds( - \ILIAS\UI\Factory $ui_factory, - \Psr\Http\Message\ServerRequestInterface $request, - string $token_name, - array $ids_for_form_shape, - \ILIAS\Refinery\Factory $refinery, -): array { - $hiddens = []; - foreach (array_values($ids_for_form_shape) as $index => $_) { - $hiddens[(string) $index] = $ui_factory->input()->field()->hidden(); - } - - $form = $ui_factory->input()->container()->form()->standard('#', [ - $token_name => $ui_factory->input()->field()->group($hiddens), - ])->withRequest($request); - - $raw = $form->getData()[$token_name] ?? null; - if (!is_array($raw)) { - return []; - } - - $raw = array_values(array_filter( - $raw, - static fn(mixed $value): bool => $value !== '' && $value !== null - )); - - if ($raw === []) { - return []; - } - - return $refinery->kindlyTo()->listOf($refinery->kindlyTo()->int())->transform($raw); -} - class ConfirmationEntityRetrieval implements EntityRetrieval { protected array $data = [ 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 cd2a3bb2f0ae..6cbdb8623715 100644 --- a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php +++ b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php @@ -83,13 +83,15 @@ function base(): string if ($http->request()->getMethod() === 'POST' && $query->has($process_token->getName())) { $process = $query->retrieve($process_token->getName(), $refinery->kindlyTo()->string()); if ($process !== '') { - $entity_ids = retrievePostedEntityIds( - $factory, - $http->request(), - $entities_token->getName(), + $state = $factory->prompt()->state()->confirm( + new ConfirmStateEntityRetrieval(), + $url_builder->withParameter($process_token, '1'), + $entities_token, $demo_entity_ids, - $refinery, + 'Are you sure you want to perform this action?', + 'Performing some action', ); + $entity_ids = $state->getConfirmedData($http->request()); $message = $factory->messageBox()->success( 'Submitted entity ids: ' . implode(', ', array_map('strval', $entity_ids)) ); @@ -147,46 +149,6 @@ function retrieveEntityIds( return $refinery->kindlyTo()->listOf($refinery->kindlyTo()->int())->transform($raw); } -/** - * Rebuild the same Standard Form shape as confirm() (group of Hidden Inputs), - * then read posted ids via withRequest/getData. - * - * @param array $ids_for_form_shape - * @return list - */ -function retrievePostedEntityIds( - \ILIAS\UI\Factory $ui_factory, - \Psr\Http\Message\ServerRequestInterface $request, - string $token_name, - array $ids_for_form_shape, - \ILIAS\Refinery\Factory $refinery, -): array { - $hiddens = []; - foreach (array_values($ids_for_form_shape) as $index => $_) { - $hiddens[(string) $index] = $ui_factory->input()->field()->hidden(); - } - - $form = $ui_factory->input()->container()->form()->standard('#', [ - $token_name => $ui_factory->input()->field()->group($hiddens), - ])->withRequest($request); - - $raw = $form->getData()[$token_name] ?? null; - if (!is_array($raw)) { - return []; - } - - $raw = array_values(array_filter( - $raw, - static fn(mixed $value): bool => $value !== '' && $value !== null - )); - - if ($raw === []) { - return []; - } - - return $refinery->kindlyTo()->listOf($refinery->kindlyTo()->int())->transform($raw); -} - class ConfirmStateEntityRetrieval implements EntityRetrieval { public function getEntities( diff --git a/components/ILIAS/UI/tests/Component/Listing/Entity/EntityListingTest.php b/components/ILIAS/UI/tests/Component/Listing/Entity/EntityListingTest.php index e8894746fff4..8bbc42ab3cba 100755 --- a/components/ILIAS/UI/tests/Component/Listing/Entity/EntityListingTest.php +++ b/components/ILIAS/UI/tests/Component/Listing/Entity/EntityListingTest.php @@ -76,7 +76,7 @@ public function entity(): Entity\Factory public function testEntityListingFactory(): void { $this->assertInstanceOf( - I\Listing\Entity\Entity::class, + I\Listing\Entity\EntityListing::class, $this->getUIFactory()->listing()->entity()->standard($this->getEntityRetrieval()) ); } diff --git a/components/ILIAS/UI/tests/Component/Listing/Entity/GridEntityListingTest.php b/components/ILIAS/UI/tests/Component/Listing/Entity/GridEntityListingTest.php index bde28b60e32f..ac217842b461 100644 --- a/components/ILIAS/UI/tests/Component/Listing/Entity/GridEntityListingTest.php +++ b/components/ILIAS/UI/tests/Component/Listing/Entity/GridEntityListingTest.php @@ -84,7 +84,7 @@ public function entity(): Entity\Factory public function testGridEntityListingFactory(): void { $this->assertInstanceOf( - C\Listing\Entity\Entity::class, + C\Listing\Entity\EntityListing::class, $this->getUIFactory()->listing()->entity()->grid($this->getEntityRetrieval()) ); } diff --git a/components/ILIAS/UI/tests/Component/Prompt/State/ConfirmStateTest.php b/components/ILIAS/UI/tests/Component/Prompt/State/ConfirmStateTest.php index 201bdde07050..9feb282d0b2b 100644 --- a/components/ILIAS/UI/tests/Component/Prompt/State/ConfirmStateTest.php +++ b/components/ILIAS/UI/tests/Component/Prompt/State/ConfirmStateTest.php @@ -26,6 +26,7 @@ use ILIAS\UI\URLBuilder; use ILIAS\UI\Component as C; use ILIAS\UI\Implementation\Component as I; +use Psr\Http\Message\ServerRequestInterface; use ILIAS\UI\Implementation\Component\Signal; use ILIAS\UI\Implementation\Component\MessageBox; use ILIAS\UI\Implementation\Component\Prompt\State as PromptState; @@ -92,6 +93,80 @@ function (string $url, array $inputs) use (&$captured_url, &$captured_inputs, $f $this->assertSame($form, $content->getForm()); } + public function testGetConfirmedDataReadsEntityIdsFromRequest(): void + { + $factory = $this->getStateFactoryWithRealForms(); + + $url_builder = new URLBuilder(new URI('https://example.com/go')); + [$url_builder, $token] = $url_builder->acquireParameters(['test'], 'entities'); + + $state = $factory->confirm( + $this->getEntityRetrieval(), + $url_builder, + $token, + [11, 22], + 'question', + 'title', + ); + + $content = $state->getContent(); + $this->assertInstanceOf(PromptState\Confirmation::class, $content); + + $group = $content->getForm()->getInputs()[$token->getName()]; + $this->assertInstanceOf(C\Input\Field\Group::class, $group); + + $body = []; + foreach ($group->getInputs() as $hidden) { + $body[$hidden->getName()] = $hidden->getValue(); + } + + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getParsedBody')->willReturn($body); + + $this->assertInstanceOf(PromptState\State::class, $state); + $this->assertSame(['11', '22'], $state->getConfirmedData($request)); + } + + public function testGetConfirmedDataReturnsEmptyArrayWhenRequestHasNoIds(): void + { + $factory = $this->getStateFactoryWithRealForms(); + + $url_builder = new URLBuilder(new URI('https://example.com/go')); + [$url_builder, $token] = $url_builder->acquireParameters(['test'], 'entities'); + + $state = $factory->confirm( + $this->getEntityRetrieval(), + $url_builder, + $token, + [11, 22], + 'question', + 'title', + ); + + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getParsedBody')->willReturn([]); + + $this->assertSame([], $state->getConfirmedData($request)); + } + + private function getStateFactoryWithRealForms(): PromptState\Factory + { + $field_factory = $this->getFieldFactory(); + $form_factory = new I\Input\Container\Form\Factory($field_factory, new I\SignalGenerator()); + $container_factory = $this->createMock(C\Input\Container\Factory::class); + $container_factory->method('form')->willReturn($form_factory); + + $input_factory = $this->createMock(C\Input\Factory::class); + $input_factory->method('field')->willReturn($field_factory); + $input_factory->method('container')->willReturn($container_factory); + + return new PromptState\Factory( + new ListingEntity\Factory(), + new MessageBox\Factory(), + $input_factory + ); + } + private function getFormMock(string $post_url): I\Input\Container\Form\Standard { $form = $this->getMockBuilder(I\Input\Container\Form\Standard::class) From f1d6a6623478b5b3d4476b6cadc5c7c4d2d74e42 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Wed, 26 Aug 2026 09:39:02 +0200 Subject: [PATCH 16/18] [Feature] UI: ConfirmationPrompt - move confirmation from state to prompt --- components/ILIAS/UI/UI.php | 9 ++- .../UI/src/Component/Prompt/Confirmation.php | 38 +++++++++++ .../ILIAS/UI/src/Component/Prompt/Factory.php | 53 ++++++++++++++- .../UI/src/Component/Prompt/State/Factory.php | 40 ----------- .../UI/src/Component/Prompt/State/State.php | 8 --- .../Prompt/{State => }/Confirmation.php | 26 ++++--- .../Component/Prompt/Factory.php | 62 ++++++++++++++++- .../Component/Prompt/Renderer.php | 11 +++ .../Component/Prompt/State/Factory.php | 68 ------------------- .../Component/Prompt/State/Renderer.php | 14 +--- .../Component/Prompt/State/State.php | 14 ---- .../{State => }/SubsetEntityRetrieval.php | 2 +- .../{State/Confirm => Confirmation}/base.php | 18 ++--- .../{State/Confirm => Confirmation}/close.php | 10 +-- .../Confirm => Confirmation}/redirect.php | 20 +++--- .../examples/Prompt/Standard/confirmation.php | 12 ++-- ...firmStateTest.php => ConfirmationTest.php} | 68 ++++++++++--------- components/ILIAS/UI/tests/InitUIFramework.php | 11 ++- 18 files changed, 256 insertions(+), 228 deletions(-) create mode 100644 components/ILIAS/UI/src/Component/Prompt/Confirmation.php rename components/ILIAS/UI/src/Implementation/Component/Prompt/{State => }/Confirmation.php (79%) rename components/ILIAS/UI/src/Implementation/Component/Prompt/{State => }/SubsetEntityRetrieval.php (96%) rename components/ILIAS/UI/src/examples/Prompt/{State/Confirm => Confirmation}/base.php (90%) rename components/ILIAS/UI/src/examples/Prompt/{State/Confirm => Confirmation}/close.php (90%) rename components/ILIAS/UI/src/examples/Prompt/{State/Confirm => Confirmation}/redirect.php (87%) rename components/ILIAS/UI/tests/Component/Prompt/{State/ConfirmStateTest.php => ConfirmationTest.php} (77%) diff --git a/components/ILIAS/UI/UI.php b/components/ILIAS/UI/UI.php index 4ee42fbb96d9..531da10be9a8 100644 --- a/components/ILIAS/UI/UI.php +++ b/components/ILIAS/UI/UI.php @@ -467,15 +467,14 @@ public function init( 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], - ); + new UI\Implementation\Component\Prompt\State\Factory(); $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\Listing\Entity\Factory::class], + $internal[UI\Implementation\Component\MessageBox\Factory::class], + $internal[UI\Implementation\Component\Input\Factory::class], ); $internal[UI\Implementation\Component\Navigation\Factory::class] = static fn() => diff --git a/components/ILIAS/UI/src/Component/Prompt/Confirmation.php b/components/ILIAS/UI/src/Component/Prompt/Confirmation.php new file mode 100644 index 000000000000..3d9366116c2e --- /dev/null +++ b/components/ILIAS/UI/src/Component/Prompt/Confirmation.php @@ -0,0 +1,38 @@ + + */ + public function getData(): array; +} diff --git a/components/ILIAS/UI/src/Component/Prompt/Factory.php b/components/ILIAS/UI/src/Component/Prompt/Factory.php index 51433ca275dd..3d8a5c67e002 100644 --- a/components/ILIAS/UI/src/Component/Prompt/Factory.php +++ b/components/ILIAS/UI/src/Component/Prompt/Factory.php @@ -20,8 +20,10 @@ namespace ILIAS\UI\Component\Prompt; -use ILIAS\UI\Component; use ILIAS\Data\URI; +use ILIAS\UI\Component\Entity\EntityRetrieval; +use ILIAS\UI\URLBuilder; +use ILIAS\UI\URLBuilderToken; interface Factory { @@ -53,6 +55,55 @@ interface Factory */ public function standard(URI $async_url): Prompt; + /** + * --- + * description: + * purpose: > + * Confirmation content asks the user to confirm an action on a set of + * entities without leaving the current context. It is shown inside a + * Prompt via a show-state. + * composition: > + * A confirmation message box, an entity listing below it and a Standard + * Form with Kitchen Sink Hidden Inputs (one per entity id, grouped under + * the URLBuilderToken name). IDs are never attached to the form action URL. + * The Prompt renders the form without its own submit buttons and places + * Confirm/Cancel in the Prompt footer via the form submit signal. + * effect: > + * The Prompt shows the confirmation question, lists affected entities + * and posts the entity ids in the request body when confirmed. + * Consumers retrieve them via withRequest() and getData() on this content. + * rivals: + * Form in Prompt: A Form collects user input. Confirmation only transports already selected entity ids and does not ask for further fields. + * Interruptive Modal: A synchronous modal for a single interruptive action, not an async Prompt roundtrip with an entity listing. + * Message Box: A confirmation Message Box states the question only; it does not list entities or post their ids. + * + * context: + * - Shown in a Prompt via state()->show(). + * + * rules: + * usage: + * 1: > + * Confirmation MUST be returned to the Prompt as content of a show-state. + * 2: > + * After POST, consumers MUST call withRequest() and getData() on the + * Confirmation, then answer with show, close or redirect. + * 3: > + * Entity ids MUST be posted in the request body, never attached to the + * form action URL. + * + * --- + * @param array $entity_ids + * @return \ILIAS\UI\Component\Prompt\Confirmation + */ + public function confirmation( + EntityRetrieval $entity_retrieval, + URLBuilder $post_url, + URLBuilderToken $post_parameter, + array $entity_ids, + string $question, + string $title, + ): Confirmation; + /** * --- * description: diff --git a/components/ILIAS/UI/src/Component/Prompt/State/Factory.php b/components/ILIAS/UI/src/Component/Prompt/State/Factory.php index ab888f885d55..a0b9441074e5 100644 --- a/components/ILIAS/UI/src/Component/Prompt/State/Factory.php +++ b/components/ILIAS/UI/src/Component/Prompt/State/Factory.php @@ -20,9 +20,6 @@ namespace ILIAS\UI\Component\Prompt\State; -use ILIAS\UI\URLBuilderToken; -use ILIAS\UI\URLBuilder; -use ILIAS\UI\Component; use ILIAS\Data\URI; interface Factory @@ -53,43 +50,6 @@ 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 below it and a Standard Form with Kitchen Sink Hidden Inputs - * (one per entity id, grouped under the URLBuilderToken name). - * IDs are never attached to the form action URL. - * The Prompt renders the form without its own submit buttons and places - * Confirm/Cancel in the Prompt footer via the form submit signal. - * Consumers provide an EntityRetrieval and the ids to confirm. - * effect: > - * The Prompt shows the confirmation question, lists affected entities - * and posts the entity ids in the request body when confirmed. - * Consumers retrieve them via getConfirmedData() on this state. - * - * 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 0aae8b39f00b..f97128ceb789 100644 --- a/components/ILIAS/UI/src/Component/Prompt/State/State.php +++ b/components/ILIAS/UI/src/Component/Prompt/State/State.php @@ -21,7 +21,6 @@ namespace ILIAS\UI\Component\Prompt\State; use ILIAS\UI\Component\Component; -use Psr\Http\Message\ServerRequestInterface; interface State extends Component { @@ -29,11 +28,4 @@ interface State extends Component * Get a Prompts State like this, but provide it with an explicit title. */ public function withTitle(string $title): self; - - /** - * Entity ids posted by a confirmation form. Only available on confirm states. - * - * @return array - */ - public function getConfirmedData(ServerRequestInterface $request): array; } diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/Confirmation.php similarity index 79% rename from components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php rename to components/ILIAS/UI/src/Implementation/Component/Prompt/Confirmation.php index c200159faa9c..cb52c6ed3215 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/Confirmation.php @@ -18,24 +18,24 @@ declare(strict_types=1); -namespace ILIAS\UI\Implementation\Component\Prompt\State; +namespace ILIAS\UI\Implementation\Component\Prompt; use ILIAS\UI\Component\Button\Button; -use ILIAS\UI\Component\Listing\Entity\EntityListing; +use Psr\Http\Message\ServerRequestInterface; use ILIAS\UI\Component\MessageBox\MessageBox; +use ILIAS\UI\Component\Listing\Entity\EntityListing; use ILIAS\UI\Implementation\Component\ComponentHelper; +use ILIAS\UI\Component\Prompt\Confirmation as IConfirmation; use ILIAS\UI\Implementation\Component\Input\Container\Form\Standard as StandardForm; -use ILIAS\UI\Implementation\Component\Prompt\IsPromptContentInternal; -use Psr\Http\Message\ServerRequestInterface; -class Confirmation implements IsPromptContentInternal +class Confirmation implements IConfirmation, IsPromptContentInternal { use ComponentHelper; public function __construct( private readonly MessageBox $message_box, private readonly EntityListing $entity_listing, - private readonly StandardForm $form, + private StandardForm $form, private readonly string $title, private readonly string $post_parameter_name, ) { @@ -56,12 +56,20 @@ public function getForm(): StandardForm return $this->form; } + public function withRequest(ServerRequestInterface $request): self + { + $clone = clone $this; + $clone->form = $this->form->withRequest($request); + + return $clone; + } + /** - * @return array + * @return list */ - public function getPostedData(ServerRequestInterface $request): array + public function getData(): array { - $data = $this->form->withRequest($request)->getData(); + $data = $this->form->getData(); $raw = is_array($data) ? ($data[$this->post_parameter_name] ?? null) : null; if (!is_array($raw)) { return []; diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php index b9ff4d62a322..20fcf9308a99 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php @@ -20,15 +20,25 @@ namespace ILIAS\UI\Implementation\Component\Prompt; +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\Implementation\Component\SignalGeneratorInterface; -use ILIAS\Data\URI; +use ILIAS\UI\Component\Listing\Entity\Factory as ListingEntityFactory; +use ILIAS\UI\Implementation\Component\Input\Container\Form\Standard as StandardForm; class Factory implements I\Factory { public function __construct( protected SignalGeneratorInterface $signal_generator, protected State\Factory $state_factory, + private readonly ListingEntityFactory $listing_entity_factory, + private readonly MessageBoxFactory $messagebox_factory, + private readonly InputFactory $input_factory, ) { } @@ -37,8 +47,58 @@ public function standard(URI $async_url): Standard return new Standard($this->signal_generator, $async_url); } + public function confirmation( + EntityRetrieval $entity_retrieval, + URLBuilder $post_url, + URLBuilderToken $post_parameter, + array $entity_ids, + string $question, + string $title, + ): Confirmation { + $listing = $this->listing_entity_factory->standard( + new SubsetEntityRetrieval($entity_retrieval, $entity_ids) + ); + + return new Confirmation( + $this->messagebox_factory->confirmation($question), + $listing, + $this->entityIdsForm( + (string) $post_url->deleteParameter($post_parameter)->buildURI(), + $post_parameter, + $entity_ids, + ), + $title, + $post_parameter->getName(), + ); + } + public function state(): State\Factory { return $this->state_factory; } + + /** + * @param array $entity_ids + */ + private function entityIdsForm( + string $post_url, + URLBuilderToken $post_parameter, + array $entity_ids, + ): StandardForm { + $hidden_inputs = []; + foreach (array_values($entity_ids) as $index => $entity_id) { + $hidden_inputs[(string) $index] = $this->input_factory->field() + ->hidden() + ->withValue((string) $entity_id); + } + + return $this->input_factory->container()->form()->standard( + $post_url, + [ + $post_parameter->getName() => $this->input_factory->field() + ->group($hidden_inputs) + ->withDedicatedName($post_parameter->getName()), + ] + ); + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/Renderer.php index df4fcc5f922f..b824ca46b84c 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/Renderer.php @@ -37,6 +37,10 @@ public function render(Component\Component $component, RendererInterface $defaul return $this->renderPrompt($component, $default_renderer); } + if ($component instanceof Confirmation) { + return $this->renderConfirmation($component, $default_renderer); + } + $this->cannotHandleComponent($component); } @@ -70,4 +74,11 @@ protected function renderPrompt(Component\Prompt\Prompt $component, RendererInte return $tpl->get(); } + 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/Implementation/Component/Prompt/State/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php index fb32902cb5da..cc6d898ab241 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Factory.php @@ -21,58 +21,15 @@ namespace ILIAS\UI\Implementation\Component\Prompt\State; 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; -use ILIAS\UI\Implementation\Component\Input\Container\Form\Standard as StandardForm; 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); - $form = $this->entityIdsForm( - (string) $post_url->deleteParameter($post_parameter)->buildURI(), - $post_parameter, - $entity_ids, - ); - - $content = new Confirmation( - $message_box, - $listing, - $form, - $title, - $post_parameter->getName(), - ); - - return (new State($content))->withTitle($title); - } - public function close(): State { return (new State(null)) @@ -84,29 +41,4 @@ public function redirect(URI $redirect): State return (new State(null)) ->withRedirect($redirect); } - - /** - * @param array $entity_ids - */ - private function entityIdsForm( - string $post_url, - URLBuilderToken $post_parameter, - array $entity_ids, - ): StandardForm { - $hidden_inputs = []; - foreach (array_values($entity_ids) as $index => $entity_id) { - $hidden_inputs[(string) $index] = $this->input_factory->field() - ->hidden() - ->withValue((string) $entity_id); - } - - return $this->input_factory->container()->form()->standard( - $post_url, - [ - $post_parameter->getName() => $this->input_factory->field() - ->group($hidden_inputs) - ->withDedicatedName($post_parameter->getName()), - ] - ); - } } 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 5d5949dd9b9d..c1b974147cba 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Renderer.php @@ -20,6 +20,7 @@ namespace ILIAS\UI\Implementation\Component\Prompt\State; +use ILIAS\UI\Implementation\Component\Prompt\Confirmation; use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; use ILIAS\UI\Renderer as RendererInterface; use ILIAS\UI\Component; @@ -35,20 +36,9 @@ 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->getEntityListing()) - . $default_renderer->render($component->getForm()); - } - protected function renderState(State $component, RendererInterface $default_renderer): string { $tpl = $this->getTemplate('tpl.promptstate.html', true, true); @@ -67,7 +57,7 @@ protected function renderState(State $component, RendererInterface $default_rend } if ($content_component instanceof Confirmation) { - $tpl->setVariable('CONTENT', $this->renderConfirmation($content_component, $default_renderer)); + $tpl->setVariable('CONTENT', $default_renderer->render($content_component)); $buttons = $this->getConfirmationButtons($content_component); } else { $tpl->setVariable('CONTENT', $default_renderer->render($content_component)); diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/State.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/State.php index 54e494f37f25..36e85c616040 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/State.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/State.php @@ -25,7 +25,6 @@ use ILIAS\UI\Component\Prompt as I; use ILIAS\UI\Implementation\Component\ComponentHelper; use ILIAS\Data\URI; -use Psr\Http\Message\ServerRequestInterface; /** * @@ -59,19 +58,6 @@ public function withTitle(string $title): self return $clone; } - /** - * @return array - */ - public function getConfirmedData(ServerRequestInterface $request): array - { - $content = $this->content; - if (!$content instanceof Confirmation) { - throw new \LogicException('Confirmed data is only available on confirm states.'); - } - - return $content->getPostedData($request); - } - public function withContent(I\IsPromptContent $content): self { $clone = clone $this; diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/SubsetEntityRetrieval.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/SubsetEntityRetrieval.php similarity index 96% rename from components/ILIAS/UI/src/Implementation/Component/Prompt/State/SubsetEntityRetrieval.php rename to components/ILIAS/UI/src/Implementation/Component/Prompt/SubsetEntityRetrieval.php index 866fe71fa850..aaecfa6d8610 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/SubsetEntityRetrieval.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/SubsetEntityRetrieval.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\UI\Implementation\Component\Prompt\State; +namespace ILIAS\UI\Implementation\Component\Prompt; use Generator; use ILIAS\Data\Order; diff --git a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php b/components/ILIAS/UI/src/examples/Prompt/Confirmation/base.php similarity index 90% rename from components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php rename to components/ILIAS/UI/src/examples/Prompt/Confirmation/base.php index 6cbdb8623715..9f292b8b4518 100644 --- a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php +++ b/components/ILIAS/UI/src/examples/Prompt/Confirmation/base.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\UI\examples\Prompt\State\Confirm; +namespace ILIAS\UI\examples\Prompt\Confirmation; use Generator; use ILIAS\Data\Order; @@ -52,7 +52,7 @@ function base(): string $here_uri = $data_factory->uri((string) $http->request()->getUri()); $url_builder = new URLBuilder($here_uri); - $example_namespace = ['prompt', 'state', 'confirm']; + $example_namespace = ['prompt', 'confirmation']; $demo_entity_ids = [1, 2, 3]; [$url_builder, $endpoint_token] = $url_builder->acquireParameters($example_namespace, 'endpoint'); @@ -66,8 +66,8 @@ function base(): string $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(), + $confirmation = $factory->prompt()->confirmation( + new ConfirmationExampleEntityRetrieval(), $post_url, $entities_token, $entity_ids, @@ -75,7 +75,7 @@ function base(): string 'Performing some action', ); - echo $renderer->renderAsync($state); + echo $renderer->renderAsync($factory->prompt()->state()->show($confirmation)); exit; } } @@ -83,15 +83,15 @@ function base(): string if ($http->request()->getMethod() === 'POST' && $query->has($process_token->getName())) { $process = $query->retrieve($process_token->getName(), $refinery->kindlyTo()->string()); if ($process !== '') { - $state = $factory->prompt()->state()->confirm( - new ConfirmStateEntityRetrieval(), + $confirmation = $factory->prompt()->confirmation( + new ConfirmationExampleEntityRetrieval(), $url_builder->withParameter($process_token, '1'), $entities_token, $demo_entity_ids, 'Are you sure you want to perform this action?', 'Performing some action', ); - $entity_ids = $state->getConfirmedData($http->request()); + $entity_ids = $confirmation->withRequest($http->request())->getData(); $message = $factory->messageBox()->success( 'Submitted entity ids: ' . implode(', ', array_map('strval', $entity_ids)) ); @@ -149,7 +149,7 @@ function retrieveEntityIds( return $refinery->kindlyTo()->listOf($refinery->kindlyTo()->int())->transform($raw); } -class ConfirmStateEntityRetrieval implements EntityRetrieval +class ConfirmationExampleEntityRetrieval implements EntityRetrieval { public function getEntities( \ILIAS\UI\Factory $ui_factory, diff --git a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/close.php b/components/ILIAS/UI/src/examples/Prompt/Confirmation/close.php similarity index 90% rename from components/ILIAS/UI/src/examples/Prompt/State/Confirm/close.php rename to components/ILIAS/UI/src/examples/Prompt/Confirmation/close.php index 5e10d0d92c58..72362343afe6 100644 --- a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/close.php +++ b/components/ILIAS/UI/src/examples/Prompt/Confirmation/close.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\UI\examples\Prompt\State\Confirm; +namespace ILIAS\UI\examples\Prompt\Confirmation; use ILIAS\UI\URLBuilder; @@ -49,7 +49,7 @@ function close(): string $here_uri = $data_factory->uri((string) $http->request()->getUri()); $url_builder = new URLBuilder($here_uri); - $example_namespace = ['prompt', 'state', 'confirm', 'close']; + $example_namespace = ['prompt', 'confirmation', 'close']; $demo_entity_ids = [1, 2, 3]; [$url_builder, $endpoint_token] = $url_builder->acquireParameters($example_namespace, 'endpoint'); @@ -63,8 +63,8 @@ function close(): string $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(), + $confirmation = $factory->prompt()->confirmation( + new ConfirmationExampleEntityRetrieval(), $post_url, $entities_token, $entity_ids, @@ -72,7 +72,7 @@ function close(): string 'Performing some action', ); - echo $renderer->renderAsync($state); + echo $renderer->renderAsync($factory->prompt()->state()->show($confirmation)); exit; } } diff --git a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php b/components/ILIAS/UI/src/examples/Prompt/Confirmation/redirect.php similarity index 87% rename from components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php rename to components/ILIAS/UI/src/examples/Prompt/Confirmation/redirect.php index bbb80bac6fc4..e3a16b2bc256 100644 --- a/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php +++ b/components/ILIAS/UI/src/examples/Prompt/Confirmation/redirect.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\UI\examples\Prompt\State\Confirm; +namespace ILIAS\UI\examples\Prompt\Confirmation; use ILIAS\UI\URLBuilder; @@ -50,7 +50,7 @@ function redirect(): string $here_uri = $data_factory->uri((string) $http->request()->getUri()); $url_builder = new URLBuilder($here_uri); - $example_namespace = ['prompt', 'state', 'confirm', 'redirect']; + $example_namespace = ['prompt', 'confirmation', 'redirect']; $demo_entity_ids = [1, 2, 3]; [$url_builder, $endpoint_token] = $url_builder->acquireParameters($example_namespace, 'endpoint'); @@ -66,8 +66,8 @@ function redirect(): string $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(), + $confirmation = $factory->prompt()->confirmation( + new ConfirmationExampleEntityRetrieval(), $post_url, $entities_token, $entity_ids, @@ -75,7 +75,7 @@ function redirect(): string 'Performing some action', ); - echo $renderer->renderAsync($state); + echo $renderer->renderAsync($factory->prompt()->state()->show($confirmation)); exit; } } @@ -103,8 +103,8 @@ function redirect(): string $prompt = $factory->prompt()->standard($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()) !== ''; + $has_success_feedback = $query->has($success_token->getName()) && + $query->retrieve($success_token->getName(), $refinery->kindlyTo()->string()) !== ''; $components = [$trigger, $prompt]; if ($has_success_feedback) { @@ -114,9 +114,9 @@ function redirect(): string ); } - $is_async = !$has_success_feedback - && $query->has($endpoint_token->getName()) - && $query->retrieve($endpoint_token->getName(), $refinery->kindlyTo()->string()) === 'true'; + $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); diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php index 9d0e89d73791..197ee8894ddc 100644 --- a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php @@ -31,8 +31,8 @@ * --- * description: > * A confirmation prompt for deleting or changing multiple entities. - * The UI framework composes message box, entity listing and a Standard Form - * with Kitchen Sink Hidden Inputs. + * prompt()->confirmation() composes message box, entity listing and a Standard + * Form with Kitchen Sink Hidden Inputs; it is shown via state()->show(). * * expected output: > * A button opens a prompt listing selected entities with a confirmation question. @@ -70,7 +70,7 @@ function confirmation(): string $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( + $confirmation = $factory->prompt()->confirmation( new ConfirmationEntityRetrieval(), $post_url, $entities_token, @@ -79,7 +79,7 @@ function confirmation(): string 'Performing some action', ); - echo $renderer->renderAsync($state); + echo $renderer->renderAsync($factory->prompt()->state()->show($confirmation)); exit; } } @@ -88,7 +88,7 @@ function confirmation(): string if ($http->request()->getMethod() === 'POST' && $query->has($process_token->getName())) { $process = $query->retrieve($process_token->getName(), $refinery->kindlyTo()->string()); if ($process !== '') { - $state = $factory->prompt()->state()->confirm( + $confirmation = $factory->prompt()->confirmation( new ConfirmationEntityRetrieval(), $url_builder->withParameter($process_token, '1'), $entities_token, @@ -96,7 +96,7 @@ function confirmation(): string 'Are you sure you want to perform this action?', 'Performing some action', ); - $entity_ids = $state->getConfirmedData($http->request()); + $entity_ids = $confirmation->withRequest($http->request())->getData(); $message = $factory->messageBox()->success( 'Submitted entity ids: ' . implode(', ', array_map('strval', $entity_ids)) ); diff --git a/components/ILIAS/UI/tests/Component/Prompt/State/ConfirmStateTest.php b/components/ILIAS/UI/tests/Component/Prompt/ConfirmationTest.php similarity index 77% rename from components/ILIAS/UI/tests/Component/Prompt/State/ConfirmStateTest.php rename to components/ILIAS/UI/tests/Component/Prompt/ConfirmationTest.php index 9feb282d0b2b..5bf82f98876b 100644 --- a/components/ILIAS/UI/tests/Component/Prompt/State/ConfirmStateTest.php +++ b/components/ILIAS/UI/tests/Component/Prompt/ConfirmationTest.php @@ -18,25 +18,25 @@ declare(strict_types=1); -require_once __DIR__ . '/../../../../../../../vendor/composer/vendor/autoload.php'; -require_once __DIR__ . '/../../../Base.php'; -require_once __DIR__ . '/../../Input/Field/CommonFieldRendering.php'; +require_once __DIR__ . '/../../../../../../vendor/composer/vendor/autoload.php'; +require_once __DIR__ . '/../../Base.php'; +require_once __DIR__ . '/../Input/Field/CommonFieldRendering.php'; use ILIAS\Data\URI; use ILIAS\UI\URLBuilder; use ILIAS\UI\Component as C; use ILIAS\UI\Implementation\Component as I; use Psr\Http\Message\ServerRequestInterface; +use ILIAS\UI\Implementation\Component\Prompt; use ILIAS\UI\Implementation\Component\Signal; use ILIAS\UI\Implementation\Component\MessageBox; -use ILIAS\UI\Implementation\Component\Prompt\State as PromptState; use ILIAS\UI\Implementation\Component\Listing\Entity as ListingEntity; -class ConfirmStateTest extends ILIAS_UI_TestBase +class ConfirmationTest extends ILIAS_UI_TestBase { use CommonFieldRendering; - public function testConfirmPutsEntityIdsIntoHiddenFormInputs(): void + public function testConfirmationPutsEntityIdsIntoHiddenFormInputs(): void { $captured_url = null; $captured_inputs = null; @@ -59,16 +59,12 @@ function (string $url, array $inputs) use (&$captured_url, &$captured_inputs, $f $input_factory->method('container')->willReturn($container_factory); $input_factory->method('field')->willReturn($this->getFieldFactory()); - $factory = new PromptState\Factory( - new ListingEntity\Factory(), - new MessageBox\Factory(), - $input_factory - ); + $factory = $this->getPromptFactory($input_factory); $url_builder = new URLBuilder(new URI('https://example.com/go')); [$url_builder, $token] = $url_builder->acquireParameters(['test'], 'entities'); - $state = $factory->confirm( + $confirmation = $factory->confirmation( $this->getEntityRetrieval(), $url_builder, $token, @@ -77,6 +73,7 @@ function (string $url, array $inputs) use (&$captured_url, &$captured_inputs, $f 'title', ); + $this->assertInstanceOf(C\Prompt\Confirmation::class, $confirmation); $this->assertIsString($captured_url); $this->assertStringNotContainsString('11', $captured_url); $this->assertStringNotContainsString('22', $captured_url); @@ -88,19 +85,18 @@ function (string $url, array $inputs) use (&$captured_url, &$captured_inputs, $f $this->assertInstanceOf(C\Input\Field\Group::class, $group); $this->assertSame(['11', '22'], array_values($group->getValue())); - $content = $state->getContent(); - $this->assertInstanceOf(PromptState\Confirmation::class, $content); - $this->assertSame($form, $content->getForm()); + $this->assertInstanceOf(Prompt\Confirmation::class, $confirmation); + $this->assertSame($form, $confirmation->getForm()); } - public function testGetConfirmedDataReadsEntityIdsFromRequest(): void + public function testGetDataReadsEntityIdsFromRequest(): void { - $factory = $this->getStateFactoryWithRealForms(); + $factory = $this->getPromptFactoryWithRealForms(); $url_builder = new URLBuilder(new URI('https://example.com/go')); [$url_builder, $token] = $url_builder->acquireParameters(['test'], 'entities'); - $state = $factory->confirm( + $confirmation = $factory->confirmation( $this->getEntityRetrieval(), $url_builder, $token, @@ -109,10 +105,9 @@ public function testGetConfirmedDataReadsEntityIdsFromRequest(): void 'title', ); - $content = $state->getContent(); - $this->assertInstanceOf(PromptState\Confirmation::class, $content); + $this->assertInstanceOf(Prompt\Confirmation::class, $confirmation); - $group = $content->getForm()->getInputs()[$token->getName()]; + $group = $confirmation->getForm()->getInputs()[$token->getName()]; $this->assertInstanceOf(C\Input\Field\Group::class, $group); $body = []; @@ -123,18 +118,18 @@ public function testGetConfirmedDataReadsEntityIdsFromRequest(): void $request = $this->createMock(ServerRequestInterface::class); $request->method('getParsedBody')->willReturn($body); - $this->assertInstanceOf(PromptState\State::class, $state); - $this->assertSame(['11', '22'], $state->getConfirmedData($request)); + $this->assertSame(['11', '22'], $confirmation->withRequest($request)->getData()); + $this->assertSame([], $confirmation->getData()); } - public function testGetConfirmedDataReturnsEmptyArrayWhenRequestHasNoIds(): void + public function testGetDataReturnsEmptyArrayWhenRequestHasNoIds(): void { - $factory = $this->getStateFactoryWithRealForms(); + $factory = $this->getPromptFactoryWithRealForms(); $url_builder = new URLBuilder(new URI('https://example.com/go')); [$url_builder, $token] = $url_builder->acquireParameters(['test'], 'entities'); - $state = $factory->confirm( + $confirmation = $factory->confirmation( $this->getEntityRetrieval(), $url_builder, $token, @@ -146,10 +141,21 @@ public function testGetConfirmedDataReturnsEmptyArrayWhenRequestHasNoIds(): void $request = $this->createMock(ServerRequestInterface::class); $request->method('getParsedBody')->willReturn([]); - $this->assertSame([], $state->getConfirmedData($request)); + $this->assertSame([], $confirmation->withRequest($request)->getData()); } - private function getStateFactoryWithRealForms(): PromptState\Factory + private function getPromptFactory(C\Input\Factory $input_factory): Prompt\Factory + { + return new Prompt\Factory( + new I\SignalGenerator(), + new Prompt\State\Factory(), + new ListingEntity\Factory(), + new MessageBox\Factory(), + $input_factory + ); + } + + private function getPromptFactoryWithRealForms(): Prompt\Factory { $field_factory = $this->getFieldFactory(); $form_factory = new I\Input\Container\Form\Factory($field_factory, new I\SignalGenerator()); @@ -160,11 +166,7 @@ private function getStateFactoryWithRealForms(): PromptState\Factory $input_factory->method('field')->willReturn($field_factory); $input_factory->method('container')->willReturn($container_factory); - return new PromptState\Factory( - new ListingEntity\Factory(), - new MessageBox\Factory(), - $input_factory - ); + return $this->getPromptFactory($input_factory); } private function getFormMock(string $post_url): I\Input\Container\Form\Standard diff --git a/components/ILIAS/UI/tests/InitUIFramework.php b/components/ILIAS/UI/tests/InitUIFramework.php index 36e1d0eeb458..064b8ed6448a 100755 --- a/components/ILIAS/UI/tests/InitUIFramework.php +++ b/components/ILIAS/UI/tests/InitUIFramework.php @@ -440,17 +440,16 @@ public function getRefreshIntervalInMs(): int }; $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"], - ); + return new ILIAS\UI\Implementation\Component\Prompt\State\Factory(); }; $c["ui.factory.prompt"] = function ($c) { return new ILIAS\UI\Implementation\Component\Prompt\Factory( $c["ui.signal_generator"], - $c["ui.factory.prompt.state"] + $c["ui.factory.prompt.state"], + $c["ui.factory.listing"]->entity(), + $c["ui.factory.messagebox"], + $c["ui.factory.input"], ); }; From 6a4acdb56047130bf4b4f5839af9923764227e57 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Wed, 26 Aug 2026 14:04:21 +0200 Subject: [PATCH 17/18] [Feature] UI: ConfirmationPrompt - Entity as Input Field --- components/ILIAS/UI/UI.php | 3 - .../Field/Entity.php} | 18 +- .../UI/src/Component/Input/Field/Factory.php | 43 ++++ .../ILIAS/UI/src/Component/Prompt/Factory.php | 52 ---- .../Component/Entity/Entity.php | 19 -- ...rer.php => EntityFieldContextRenderer.php} | 29 ++- .../Entity/EntityRendererFactory.php | 9 +- .../Component/Input/Field/Entity.php | 86 +++++++ .../Component/Input/Field/Factory.php | 13 + .../Component/Input/Field/Renderer.php | 26 ++ .../Component/Prompt/Confirmation.php | 101 -------- .../Component/Prompt/Factory.php | 60 ----- .../Component/Prompt/Renderer.php | 11 - .../Component/Prompt/State/Renderer.php | 32 +-- .../Prompt/SubsetEntityRetrieval.php | 60 ----- .../src/examples/Input/Field/Entity/base.php | 85 +++++++ .../Input/Field/Entity/with_details.php | 115 +++++++++ .../src/examples/Prompt/Confirmation/base.php | 181 -------------- .../examples/Prompt/Confirmation/close.php | 101 -------- .../examples/Prompt/Confirmation/redirect.php | 140 ----------- .../examples/Prompt/Standard/confirmation.php | 55 +++-- .../Standard/confirmation_type_title.php | 112 +++++++++ .../templates/default/Input/tpl.entity.html | 8 + .../Component/Input/Field/EntityInputTest.php | 99 ++++++++ .../Component/Prompt/ConfirmationTest.php | 222 ------------------ components/ILIAS/UI/tests/InitUIFramework.php | 5 +- .../Input/_ui-component_entity.scss | 37 +++ .../Input/_ui-component_input.scss | 5 +- templates/default/delos.css | 36 ++- 29 files changed, 730 insertions(+), 1033 deletions(-) rename components/ILIAS/UI/src/Component/{Prompt/Confirmation.php => Input/Field/Entity.php} (56%) rename components/ILIAS/UI/src/Implementation/Component/Entity/{BriefEntityRenderer.php => EntityFieldContextRenderer.php} (54%) create mode 100644 components/ILIAS/UI/src/Implementation/Component/Input/Field/Entity.php delete mode 100644 components/ILIAS/UI/src/Implementation/Component/Prompt/Confirmation.php delete mode 100644 components/ILIAS/UI/src/Implementation/Component/Prompt/SubsetEntityRetrieval.php create mode 100644 components/ILIAS/UI/src/examples/Input/Field/Entity/base.php create mode 100644 components/ILIAS/UI/src/examples/Input/Field/Entity/with_details.php delete mode 100644 components/ILIAS/UI/src/examples/Prompt/Confirmation/base.php delete mode 100644 components/ILIAS/UI/src/examples/Prompt/Confirmation/close.php delete mode 100644 components/ILIAS/UI/src/examples/Prompt/Confirmation/redirect.php create mode 100644 components/ILIAS/UI/src/examples/Prompt/Standard/confirmation_type_title.php create mode 100644 components/ILIAS/UI/src/templates/default/Input/tpl.entity.html create mode 100644 components/ILIAS/UI/tests/Component/Input/Field/EntityInputTest.php delete mode 100644 components/ILIAS/UI/tests/Component/Prompt/ConfirmationTest.php create mode 100644 templates/default/070-components/UI-framework/Input/_ui-component_entity.scss diff --git a/components/ILIAS/UI/UI.php b/components/ILIAS/UI/UI.php index 531da10be9a8..e3a901931017 100644 --- a/components/ILIAS/UI/UI.php +++ b/components/ILIAS/UI/UI.php @@ -472,9 +472,6 @@ public function init( new UI\Implementation\Component\Prompt\Factory( $internal[UI\Implementation\Component\SignalGeneratorInterface::class], $internal[UI\Implementation\Component\Prompt\State\Factory::class], - $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\Navigation\Factory::class] = static fn() => diff --git a/components/ILIAS/UI/src/Component/Prompt/Confirmation.php b/components/ILIAS/UI/src/Component/Input/Field/Entity.php similarity index 56% rename from components/ILIAS/UI/src/Component/Prompt/Confirmation.php rename to components/ILIAS/UI/src/Component/Input/Field/Entity.php index 3d9366116c2e..14b47a0abf7d 100644 --- a/components/ILIAS/UI/src/Component/Prompt/Confirmation.php +++ b/components/ILIAS/UI/src/Component/Input/Field/Entity.php @@ -18,21 +18,15 @@ declare(strict_types=1); -namespace ILIAS\UI\Component\Prompt; +namespace ILIAS\UI\Component\Input\Field; -use Psr\Http\Message\ServerRequestInterface; +use ILIAS\UI\Component\Entity\EntityRetrieval; +use ILIAS\UI\Component\Input\Container\Form\FormInput; /** - * Confirmation content for a Prompt: question, affected entities, posted ids. + * Carries entity identifiers through a form roundtrip and visualises them. */ -interface Confirmation extends IsPromptContent +interface Entity extends FormInput { - public function withRequest(ServerRequestInterface $request): self; - - /** - * Entity ids from the confirmation form after withRequest(). - * - * @return list - */ - public function getData(): array; + public function getEntityRetrieval(): EntityRetrieval; } diff --git a/components/ILIAS/UI/src/Component/Input/Field/Factory.php b/components/ILIAS/UI/src/Component/Input/Field/Factory.php index 3c9c4294d376..535396894aba 100755 --- a/components/ILIAS/UI/src/Component/Input/Field/Factory.php +++ b/components/ILIAS/UI/src/Component/Input/Field/Factory.php @@ -22,6 +22,7 @@ use ILIAS\UI\Component\Input\Container\Form\FormInput; use ILIAS\UI\Component\Input\Field\Node\NodeRetrieval; +use ILIAS\UI\Component\Entity\EntityRetrieval; use ILIAS\Data\ImagePurpose; /** @@ -809,6 +810,48 @@ public function link(string $label, ?string $byline = null): Link; */ public function hidden(): Hidden; + /** + * --- + * description: + * purpose: > + * The Entity Input carries technical identifiers of entities through a + * form roundtrip and visualises those entities. Users must not operate + * this input. Typical context is a confirmation prompt: a Standard Form + * in a Prompt State contains this input (withValue of the affected ids) + * so the posted payload and the shown entities cannot diverge. Additional + * operable fields (e.g. type the object title) can sit in the same form. + * A confirmation Message Box as sibling Prompt content depends on + * secondary Prompt content (follow-up to GitHub issue 11105: + * show($primary, ...$secondary)). + * composition: > + * An unordered list of Entity components, each paired with a Hidden Input + * generated via withValue() (HasDynamicInputs). There is no user control. + * effect: > + * Submitting the enclosing form posts the entity ids in the request body. + * Consumers read them from the form getData() for this field. + * rivals: + * Hidden: Hidden transmits a value without visualisation. Entity Input also shows the entities. + * Entity Listing: Entity Listing is a listing, not a form field, and has its own retrieval lifecycle. + * + * context: + * - The Entity Input is used in UI-forms, especially confirmation prompts. + * + * rules: + * usage: + * 1: > + * The Entity Input MUST NOT be operated by the user. + * 2: > + * Consumers MUST set the identifiers with withValue() before rendering + * and MUST read them from the enclosing form getData() after withRequest(). + * 3: > + * Entity ids MUST be posted in the request body, never attached to the + * form action URL. + * + * --- + * @return \ILIAS\UI\Component\Input\Field\Entity + */ + public function entity(EntityRetrieval $entity_retrieval): Entity; + /** * --- * description: diff --git a/components/ILIAS/UI/src/Component/Prompt/Factory.php b/components/ILIAS/UI/src/Component/Prompt/Factory.php index 3d8a5c67e002..036db44c7625 100644 --- a/components/ILIAS/UI/src/Component/Prompt/Factory.php +++ b/components/ILIAS/UI/src/Component/Prompt/Factory.php @@ -21,9 +21,6 @@ namespace ILIAS\UI\Component\Prompt; use ILIAS\Data\URI; -use ILIAS\UI\Component\Entity\EntityRetrieval; -use ILIAS\UI\URLBuilder; -use ILIAS\UI\URLBuilderToken; interface Factory { @@ -55,55 +52,6 @@ interface Factory */ public function standard(URI $async_url): Prompt; - /** - * --- - * description: - * purpose: > - * Confirmation content asks the user to confirm an action on a set of - * entities without leaving the current context. It is shown inside a - * Prompt via a show-state. - * composition: > - * A confirmation message box, an entity listing below it and a Standard - * Form with Kitchen Sink Hidden Inputs (one per entity id, grouped under - * the URLBuilderToken name). IDs are never attached to the form action URL. - * The Prompt renders the form without its own submit buttons and places - * Confirm/Cancel in the Prompt footer via the form submit signal. - * effect: > - * The Prompt shows the confirmation question, lists affected entities - * and posts the entity ids in the request body when confirmed. - * Consumers retrieve them via withRequest() and getData() on this content. - * rivals: - * Form in Prompt: A Form collects user input. Confirmation only transports already selected entity ids and does not ask for further fields. - * Interruptive Modal: A synchronous modal for a single interruptive action, not an async Prompt roundtrip with an entity listing. - * Message Box: A confirmation Message Box states the question only; it does not list entities or post their ids. - * - * context: - * - Shown in a Prompt via state()->show(). - * - * rules: - * usage: - * 1: > - * Confirmation MUST be returned to the Prompt as content of a show-state. - * 2: > - * After POST, consumers MUST call withRequest() and getData() on the - * Confirmation, then answer with show, close or redirect. - * 3: > - * Entity ids MUST be posted in the request body, never attached to the - * form action URL. - * - * --- - * @param array $entity_ids - * @return \ILIAS\UI\Component\Prompt\Confirmation - */ - public function confirmation( - EntityRetrieval $entity_retrieval, - URLBuilder $post_url, - URLBuilderToken $post_parameter, - array $entity_ids, - string $question, - string $title, - ): Confirmation; - /** * --- * description: diff --git a/components/ILIAS/UI/src/Implementation/Component/Entity/Entity.php b/components/ILIAS/UI/src/Implementation/Component/Entity/Entity.php index d6a43fe56f6a..3f7d99a6017a 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Entity/Entity.php +++ b/components/ILIAS/UI/src/Implementation/Component/Entity/Entity.php @@ -297,23 +297,4 @@ 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/BriefEntityRenderer.php b/components/ILIAS/UI/src/Implementation/Component/Entity/EntityFieldContextRenderer.php similarity index 54% rename from components/ILIAS/UI/src/Implementation/Component/Entity/BriefEntityRenderer.php rename to components/ILIAS/UI/src/Implementation/Component/Entity/EntityFieldContextRenderer.php index 86d125ff8da0..964df58bb22c 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Entity/BriefEntityRenderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Entity/EntityFieldContextRenderer.php @@ -22,22 +22,22 @@ use ILIAS\UI\Component; use ILIAS\UI\Renderer as RendererInterface; -use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; /** - * Renders compact entities with primary visual only (e.g. in confirmation prompts). + * Compact entity rendering inside an Entity Input (form context). */ -class BriefEntityRenderer extends AbstractComponentRenderer +class EntityFieldContextRenderer extends Renderer { public function render(Component\Component $component, RendererInterface $default_renderer): string { - if ($component instanceof Component\Entity\Entity) { - return $this->renderEntity($component, $default_renderer); + if ($component instanceof Entity && $this->isCompactListItem($component)) { + return $this->renderCompactEntity($component, $default_renderer); } - $this->cannotHandleComponent($component); + + return parent::render($component, $default_renderer); } - protected function renderEntity( + protected function renderCompactEntity( Component\Entity\Entity $component, RendererInterface $default_renderer ): string { @@ -52,4 +52,19 @@ protected function renderEntity( return $tpl->get(); } + + protected function isCompactListItem(Entity $component): bool + { + return $component->getBlockingAvailabilityConditions() === [] && + $component->getFeaturedProperties() === [] && + $component->getMainDetails() === [] && + $component->getDetails() === [] && + $component->getAvailability() === [] && + $component->getPersonalStatus() === [] && + $component->getPrioritizedReactions() === [] && + $component->getReactions() === [] && + $component->getManagingActions() === [] && + $component->getWorkflow() === null && + $component->getSecondaryIdentifier() === ''; + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Entity/EntityRendererFactory.php b/components/ILIAS/UI/src/Implementation/Component/Entity/EntityRendererFactory.php index 7b360038d67b..790ff3ee75aa 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Entity/EntityRendererFactory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Entity/EntityRendererFactory.php @@ -25,17 +25,14 @@ class EntityRendererFactory extends Render\DefaultRendererFactory { - private const PROMPT_STATE_CONTEXT = 'StateStatePrompt'; + private const ENTITY_FIELD_CONTEXT = 'EntityFieldInput'; public function getRendererInContext( Component\Component $component, array $contexts ): Render\AbstractComponentRenderer { - if ($component instanceof Entity - && in_array(self::PROMPT_STATE_CONTEXT, $contexts, true) - && $component->isCompactListItem() - ) { - return new BriefEntityRenderer( + if (in_array(self::ENTITY_FIELD_CONTEXT, $contexts, true)) { + return new EntityFieldContextRenderer( $this->ui_factory, $this->tpl_factory, $this->lng, diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Entity.php b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Entity.php new file mode 100644 index 000000000000..baa01b8639f0 --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Entity.php @@ -0,0 +1,86 @@ +entity_retrieval; + } + + public function getUpdateOnLoadCode(): Closure + { + return static fn($id) => ''; + } + + protected function getConstraintForRequirement(): ?Constraint + { + if ($this->requirement_constraint !== null) { + return $this->requirement_constraint; + } + + return $this->refinery->custom()->constraint( + static fn($value) => is_array($value) && 1 <= count($value), + sprintf($this->language->txt('not_greater_than_or_equal'), 1) + ); + } + + protected function isClientSideValueOk($value): bool + { + if (!is_array($value)) { + return false; + } + foreach ($value as $_value) { + if (!is_string($_value) && !is_int($_value)) { + return false; + } + } + + return parent::isClientSideValueOk($value); + } +} diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Factory.php index 71fed74d932c..1a3f39fce13f 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Factory.php @@ -179,6 +179,19 @@ public function hidden(): Hidden return new Hidden($this->data_factory, $this->refinery); } + public function entity(\ILIAS\UI\Component\Entity\EntityRetrieval $entity_retrieval): Entity + { + return new Entity( + $this->lng, + $this->data_factory, + $this->refinery, + $this->hidden(), + $entity_retrieval, + '', + null, + ); + } + public function colorSelect(string $label, ?string $byline = null): ColorSelect { return new ColorSelect($this->data_factory, $this->refinery, $label, $byline); diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Renderer.php index a3f6c9a6ea6f..760123da5971 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Input/Field/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Field/Renderer.php @@ -38,6 +38,7 @@ use ILIAS\Data\FiveStarRatingScale; use ILIAS\UI\Implementation\Component\Input\Container\Filter\ProxyFilterField; use ILIAS\Data\URI; +use ILIAS\Data\Order; use ILIAS\UI\Implementation\Component\ComponentHelper; /** @@ -157,6 +158,9 @@ public function render(Component\Component $component, RendererInterface $defaul case ($component instanceof F\Hidden): return $this->renderHiddenField($component); + case ($component instanceof F\Entity): + return $this->renderEntityField($component, $default_renderer); + case ($component instanceof F\ColorSelect): return $this->renderColorSelectField($component, $default_renderer); @@ -934,6 +938,28 @@ protected function renderHiddenField(F\Hidden $input): string return $template->get(); } + protected function renderEntityField(F\Entity $component, RendererInterface $default_renderer): string + { + $template = $this->getTemplate('tpl.entity.html', true, true); + + $dynamic_inputs = (static fn() => yield from $component->getGeneratedDynamicInputs())(); + $entities = $component->getEntityRetrieval()->getEntitiesByIds( + $this->getUIFactory(), + new Order('id', Order::ASC), + array_values($component->getValue()), + ); + + foreach ($this->iterateGeneratorsInLockstep($entities, $dynamic_inputs) as [$entity, $dynamic_input]) { + $this->checkArgInstanceOf('entity', $entity, Component\Entity\Entity::class); + $template->setCurrentBlock('entity'); + $template->setVariable('ENTITY', $default_renderer->render($entity)); + $template->setVariable('INPUT', $default_renderer->render($dynamic_input)); + $template->parseCurrentBlock(); + } + + return $this->wrapInFormContext($component, $component->getLabel(), $template->get()); + } + /** * @inheritdoc */ diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/Confirmation.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/Confirmation.php deleted file mode 100644 index cb52c6ed3215..000000000000 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/Confirmation.php +++ /dev/null @@ -1,101 +0,0 @@ -message_box; - } - - public function getEntityListing(): EntityListing - { - return $this->entity_listing; - } - - public function getForm(): StandardForm - { - return $this->form; - } - - public function withRequest(ServerRequestInterface $request): self - { - $clone = clone $this; - $clone->form = $this->form->withRequest($request); - - return $clone; - } - - /** - * @return list - */ - public function getData(): array - { - $data = $this->form->getData(); - $raw = is_array($data) ? ($data[$this->post_parameter_name] ?? null) : null; - if (!is_array($raw)) { - return []; - } - - $ids = []; - foreach ($raw as $value) { - if ($value === null || $value === '') { - continue; - } - $ids[] = (string) $value; - } - - return $ids; - } - - public function getPromptTitle(): string - { - return $this->title; - } - - /** - * @return Button[] - */ - public function getPromptButtons(): array - { - return []; - } -} diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php index 20fcf9308a99..158f77df5d13 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php @@ -21,24 +21,14 @@ namespace ILIAS\UI\Implementation\Component\Prompt; 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\Implementation\Component\SignalGeneratorInterface; -use ILIAS\UI\Component\Listing\Entity\Factory as ListingEntityFactory; -use ILIAS\UI\Implementation\Component\Input\Container\Form\Standard as StandardForm; class Factory implements I\Factory { public function __construct( protected SignalGeneratorInterface $signal_generator, protected State\Factory $state_factory, - private readonly ListingEntityFactory $listing_entity_factory, - private readonly MessageBoxFactory $messagebox_factory, - private readonly InputFactory $input_factory, ) { } @@ -47,58 +37,8 @@ public function standard(URI $async_url): Standard return new Standard($this->signal_generator, $async_url); } - public function confirmation( - EntityRetrieval $entity_retrieval, - URLBuilder $post_url, - URLBuilderToken $post_parameter, - array $entity_ids, - string $question, - string $title, - ): Confirmation { - $listing = $this->listing_entity_factory->standard( - new SubsetEntityRetrieval($entity_retrieval, $entity_ids) - ); - - return new Confirmation( - $this->messagebox_factory->confirmation($question), - $listing, - $this->entityIdsForm( - (string) $post_url->deleteParameter($post_parameter)->buildURI(), - $post_parameter, - $entity_ids, - ), - $title, - $post_parameter->getName(), - ); - } - public function state(): State\Factory { return $this->state_factory; } - - /** - * @param array $entity_ids - */ - private function entityIdsForm( - string $post_url, - URLBuilderToken $post_parameter, - array $entity_ids, - ): StandardForm { - $hidden_inputs = []; - foreach (array_values($entity_ids) as $index => $entity_id) { - $hidden_inputs[(string) $index] = $this->input_factory->field() - ->hidden() - ->withValue((string) $entity_id); - } - - return $this->input_factory->container()->form()->standard( - $post_url, - [ - $post_parameter->getName() => $this->input_factory->field() - ->group($hidden_inputs) - ->withDedicatedName($post_parameter->getName()), - ] - ); - } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/Renderer.php index b824ca46b84c..df4fcc5f922f 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/Renderer.php @@ -37,10 +37,6 @@ public function render(Component\Component $component, RendererInterface $defaul return $this->renderPrompt($component, $default_renderer); } - if ($component instanceof Confirmation) { - return $this->renderConfirmation($component, $default_renderer); - } - $this->cannotHandleComponent($component); } @@ -74,11 +70,4 @@ protected function renderPrompt(Component\Prompt\Prompt $component, RendererInte return $tpl->get(); } - 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/Implementation/Component/Prompt/State/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Renderer.php index c1b974147cba..be8c26a9ff93 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Renderer.php @@ -20,7 +20,6 @@ namespace ILIAS\UI\Implementation\Component\Prompt\State; -use ILIAS\UI\Implementation\Component\Prompt\Confirmation; use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; use ILIAS\UI\Renderer as RendererInterface; use ILIAS\UI\Component; @@ -56,39 +55,20 @@ protected function renderState(State $component, RendererInterface $default_rend return $tpl->get(); } - if ($content_component instanceof Confirmation) { - $tpl->setVariable('CONTENT', $default_renderer->render($content_component)); - $buttons = $this->getConfirmationButtons($content_component); - } else { - $tpl->setVariable('CONTENT', $default_renderer->render($content_component)); - $buttons = $component->getButtons(); + $tpl->setVariable('CONTENT', $default_renderer->render($content_component)); + $buttons = $component->getButtons(); - if ($content_component instanceof \ILIAS\UI\Component\Input\Container\Form\Form) { - $buttons[] = $this->getFormSubmitButton($content_component); - } - - $buttons[] = $this->getPromptCloseButton(); + if ($content_component instanceof \ILIAS\UI\Component\Input\Container\Form\Form) { + $buttons[] = $this->getFormSubmitButton($content_component); } + $buttons[] = $this->getPromptCloseButton(); + $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 { diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/SubsetEntityRetrieval.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/SubsetEntityRetrieval.php deleted file mode 100644 index aaecfa6d8610..000000000000 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/SubsetEntityRetrieval.php +++ /dev/null @@ -1,60 +0,0 @@ - $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/Input/Field/Entity/base.php b/components/ILIAS/UI/src/examples/Input/Field/Entity/base.php new file mode 100644 index 000000000000..3e831002e2ff --- /dev/null +++ b/components/ILIAS/UI/src/examples/Input/Field/Entity/base.php @@ -0,0 +1,85 @@ + + * Example of the Entity Input in a Standard Form with compact (brief) + * entities: only primary identifier, empty secondary identifier, no further + * sections. The field posts entity ids and is not operated by the user. + * + * expected output: > + * A form shows a compact list of entity titles. Submitting the form displays the posted ids. + * --- + */ +function base(): string +{ + global $DIC; + $factory = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + $request = $DIC->http()->request(); + + $form = $factory->input()->container()->form()->standard('#', [ + $factory->input()->field()->entity(new EntityInputExampleRetrieval()) + ->withValue([0, 1]), + ]); + + if ($request->getMethod() === 'POST') { + $form = $form->withRequest($request); + $data = $form->getData(); + + return $renderer->render($form) + . '
' . htmlspecialchars(print_r($data, true), ENT_QUOTES, 'UTF-8') . '
'; + } + + return $renderer->render($form); +} + +class EntityInputExampleRetrieval 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 { + yield from []; + } + + 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, 'Entity ' . $entity_id, ''); + } + } +} diff --git a/components/ILIAS/UI/src/examples/Input/Field/Entity/with_details.php b/components/ILIAS/UI/src/examples/Input/Field/Entity/with_details.php new file mode 100644 index 000000000000..a4e7260af66c --- /dev/null +++ b/components/ILIAS/UI/src/examples/Input/Field/Entity/with_details.php @@ -0,0 +1,115 @@ + + * Entity Input with fully populated Entity components (avatar, main details, + * details). Unlike the base example, these are not rendered in the compact + * brief layout. + * + * expected output: > + * A form shows a list of rich entity cards. Submitting the form displays the posted ids. + * --- + */ +function with_details(): string +{ + global $DIC; + $factory = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + $request = $DIC->http()->request(); + + $form = $factory->input()->container()->form()->standard('#', [ + $factory->input()->field()->entity(new EntityInputDetailedRetrieval()) + ->withValue([0, 1, 2]), + ]); + + if ($request->getMethod() === 'POST') { + $form = $form->withRequest($request); + $data = $form->getData(); + + return $renderer->render($form) + . '
' . htmlspecialchars(print_r($data, true), ENT_QUOTES, 'UTF-8') . '
'; + } + + return $renderer->render($form); +} + +class EntityInputDetailedRetrieval 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], + ]; + + 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 []; + } + + 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])) { + throw new InvalidArgumentException('Unknown entity id: ' . $entity_id); + } + 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, $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/Confirmation/base.php b/components/ILIAS/UI/src/examples/Prompt/Confirmation/base.php deleted file mode 100644 index 9f292b8b4518..000000000000 --- a/components/ILIAS/UI/src/examples/Prompt/Confirmation/base.php +++ /dev/null @@ -1,181 +0,0 @@ - - * 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 via the Standard Form (Kitchen Sink Hidden Inputs); - * 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', 'confirmation']; - $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'); - $confirmation = $factory->prompt()->confirmation( - new ConfirmationExampleEntityRetrieval(), - $post_url, - $entities_token, - $entity_ids, - 'Are you sure you want to perform this action?', - 'Performing some action', - ); - - echo $renderer->renderAsync($factory->prompt()->state()->show($confirmation)); - exit; - } - } - - if ($http->request()->getMethod() === 'POST' && $query->has($process_token->getName())) { - $process = $query->retrieve($process_token->getName(), $refinery->kindlyTo()->string()); - if ($process !== '') { - $confirmation = $factory->prompt()->confirmation( - new ConfirmationExampleEntityRetrieval(), - $url_builder->withParameter($process_token, '1'), - $entities_token, - $demo_entity_ids, - 'Are you sure you want to perform this action?', - 'Performing some action', - ); - $entity_ids = $confirmation->withRequest($http->request())->getData(); - $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 (And 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 $wrapper, - string $parameter_name, - \ILIAS\Refinery\Factory $refinery, -): ?array { - if (!$wrapper->has($parameter_name)) { - return null; - } - - $raw = $wrapper->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 ConfirmationExampleEntityRetrieval 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/Confirmation/close.php b/components/ILIAS/UI/src/examples/Prompt/Confirmation/close.php deleted file mode 100644 index 72362343afe6..000000000000 --- a/components/ILIAS/UI/src/examples/Prompt/Confirmation/close.php +++ /dev/null @@ -1,101 +0,0 @@ - - * 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. - * No further information is displayed. - * The prompt can also be closed. - * --- - */ -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', 'confirmation', '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'); - $confirmation = $factory->prompt()->confirmation( - new ConfirmationExampleEntityRetrieval(), - $post_url, - $entities_token, - $entity_ids, - 'Are you sure you want to perform this action?', - 'Performing some action', - ); - - echo $renderer->renderAsync($factory->prompt()->state()->show($confirmation)); - 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 Confirmation (And 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/Confirmation/redirect.php b/components/ILIAS/UI/src/examples/Prompt/Confirmation/redirect.php deleted file mode 100644 index e3a16b2bc256..000000000000 --- a/components/ILIAS/UI/src/examples/Prompt/Confirmation/redirect.php +++ /dev/null @@ -1,140 +0,0 @@ - - * 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 above the trigger button. - * The prompt can also be closed. - * --- - */ -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', 'confirmation', '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'); - $confirmation = $factory->prompt()->confirmation( - new ConfirmationExampleEntityRetrieval(), - $post_url, - $entities_token, - $entity_ids, - 'Are you sure you want to perform this action?', - 'Performing some action', - ); - - echo $renderer->renderAsync($factory->prompt()->state()->show($confirmation)); - exit; - } - } - - if ($http->request()->getMethod() === 'POST' && $query->has($process_token->getName())) { - $process = $query->retrieve($process_token->getName(), $refinery->kindlyTo()->string()); - if ($process !== '') { - $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; - } - } - - $open_uri = $async_url_builder - ->withParameter($confirm_token, $demo_entity_ids) - ->buildURI(); - - $prompt = $factory->prompt()->standard($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()) !== ''; - - $components = [$trigger, $prompt]; - if ($has_success_feedback) { - array_unshift( - $components, - $factory->messageBox()->success('Action confirmed. Entity ids were processed.') - ); - } - - $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/examples/Prompt/Standard/confirmation.php b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php index 197ee8894ddc..616a84f171d6 100644 --- a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php @@ -24,6 +24,7 @@ use ILIAS\Data\Order; use ILIAS\Data\Range; use ILIAS\UI\URLBuilder; +use InvalidArgumentException; use ILIAS\UI\Component\Entity\Entity; use ILIAS\UI\Component\Entity\EntityRetrieval; @@ -31,11 +32,15 @@ * --- * description: > * A confirmation prompt for deleting or changing multiple entities. - * prompt()->confirmation() composes message box, entity listing and a Standard - * Form with Kitchen Sink Hidden Inputs; it is shown via state()->show(). + * Consumers compose a Standard Form with field()->entity() (ids via withValue) + * and show it as the primary Prompt State content. + * A confirmation Message Box next to that Form depends on Tim Schmitz' + * follow-up to GitHub #11105 ("Secondary Prompt Content": + * show($primary, ...$secondary)). Until that lands, this example uses + * show($form) only; adding the Message Box is then a one-argument change. * * expected output: > - * A button opens a prompt listing selected entities with a confirmation question. + * A button opens a prompt listing selected entities. * Submitting posts the entity ids via the Standard Form (POST body); the result is shown in the prompt. * The prompt can be closed. * --- @@ -63,23 +68,26 @@ function confirmation(): string [$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'); - $confirmation = $factory->prompt()->confirmation( - new ConfirmationEntityRetrieval(), - $post_url, - $entities_token, - $entity_ids, - 'Are you sure you want to perform this action?', - 'Performing some action', - ); - - echo $renderer->renderAsync($factory->prompt()->state()->show($confirmation)); + $form = $factory->input()->container()->form()->standard( + (string) $post_url->buildURI(), + [ + $factory->input()->field()->entity(new ConfirmationEntityRetrieval()) + ->withValue($entity_ids), + ] + )->withSubmitLabel('Confirm'); + + // Ready for secondary Prompt content (#11105 follow-up): + // $message = $factory->messageBox()->confirmation('Really perform this action?'); + // $state = $factory->prompt()->state()->show($form, $message)->withTitle(...); + $state = $factory->prompt()->state()->show($form)->withTitle('Performing some action'); + + echo $renderer->renderAsync($state); exit; } } @@ -88,15 +96,15 @@ function confirmation(): string if ($http->request()->getMethod() === 'POST' && $query->has($process_token->getName())) { $process = $query->retrieve($process_token->getName(), $refinery->kindlyTo()->string()); if ($process !== '') { - $confirmation = $factory->prompt()->confirmation( - new ConfirmationEntityRetrieval(), - $url_builder->withParameter($process_token, '1'), - $entities_token, - $demo_entity_ids, - 'Are you sure you want to perform this action?', - 'Performing some action', + $form = $factory->input()->container()->form()->standard( + (string) $url_builder->withParameter($process_token, '1')->buildURI(), + [ + $factory->input()->field()->entity(new ConfirmationEntityRetrieval()) + ->withValue($demo_entity_ids), + ] ); - $entity_ids = $confirmation->withRequest($http->request())->getData(); + $data = $form->withRequest($http->request())->getData(); + $entity_ids = array_values((array) (is_array($data) ? array_values($data)[0] ?? [] : [])); $message = $factory->messageBox()->success( 'Submitted entity ids: ' . implode(', ', array_map('strval', $entity_ids)) ); @@ -181,9 +189,10 @@ public function getEntitiesByIds( Order $order, array $entity_ids, ): Generator { + // Must yield one entity per id in the same order (Entity Input renderer lockstep). foreach ($entity_ids as $entity_id) { if (!isset($this->data[$entity_id])) { - continue; + throw new InvalidArgumentException('Unknown entity id: ' . $entity_id); } yield $this->mapRecord($ui_factory, $entity_id, $this->data[$entity_id]); } diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation_type_title.php b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation_type_title.php new file mode 100644 index 000000000000..bef7fa5c7a3c --- /dev/null +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation_type_title.php @@ -0,0 +1,112 @@ + + * A confirmation prompt for deleting a single object without Entity Input. + * The user must re-type the object title in a Text Input to confirm. + * A confirmation Message Box as sibling Prompt content can be added once + * secondary Prompt content is available (follow-up to GitHub issue 11105). + * + * expected output: > + * A button opens a prompt with a form asking to type the object title. + * Submitting with a wrong or empty title keeps the prompt open and shows + * an error on the text input. + * Typing the exact title and submitting closes the prompt. + * --- + */ +function confirmation_type_title(): 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']; + + $object_title = 'Introduction to ILIAS'; + + $here_uri = $data_factory->uri((string) $http->request()->getUri()); + $url_builder = new URLBuilder($here_uri); + + $example_namespace = ['prompt', 'confirmation_type_title']; + [$url_builder, $endpoint_token] = $url_builder->acquireParameters($example_namespace, 'endpoint'); + $url_builder = $url_builder->withParameter($endpoint_token, 'true'); + + [$url_builder, $action_token] = $url_builder->acquireParameters($example_namespace, 'action'); + $form_uri = $url_builder->withParameter($action_token, 'form')->buildURI(); + + $build_form = static function (string $post_url) use ($factory, $refinery, $object_title) { + $title_field = $factory->input()->field()->text( + 'Object title', + sprintf('Type "%s" to confirm deletion.', $object_title) + )->withRequired(true) + ->withAdditionalTransformation( + $refinery->custom()->constraint( + static fn(mixed $value): bool => is_string($value) && $value === $object_title, + sprintf('The title must match "%s" exactly.', $object_title) + ) + ); + + return $factory->input()->container()->form()->standard( + $post_url, + [$title_field] + )->withSubmitLabel('Delete'); + }; + + if ($query->has($action_token->getName()) && + $query->retrieve($action_token->getName(), $refinery->kindlyTo()->string()) === 'form' + ) { + $form = $build_form((string) $form_uri); + $title = sprintf('Delete "%s"?', $object_title); + + if ($http->request()->getMethod() === 'POST') { + $form = $form->withRequest($http->request()); + if ($form->getData() !== null) { + echo $renderer->renderAsync($factory->prompt()->state()->close()); + exit; + } + } + + echo $renderer->renderAsync( + $factory->prompt()->state()->show($form)->withTitle($title) + ); + exit; + } + + $prompt = $factory->prompt()->standard($form_uri); + $trigger = $factory->button()->primary( + 'Open delete confirmation (type title)', + $prompt->getShowSignal($form_uri) + ); + + if (!$query->has($endpoint_token->getName())) { + return $renderer->render([$prompt, $trigger]); + } + + return ''; +} diff --git a/components/ILIAS/UI/src/templates/default/Input/tpl.entity.html b/components/ILIAS/UI/src/templates/default/Input/tpl.entity.html new file mode 100644 index 000000000000..0f37665b5f9e --- /dev/null +++ b/components/ILIAS/UI/src/templates/default/Input/tpl.entity.html @@ -0,0 +1,8 @@ +
    + +
  • + {ENTITY} + {INPUT} +
  • + +
diff --git a/components/ILIAS/UI/tests/Component/Input/Field/EntityInputTest.php b/components/ILIAS/UI/tests/Component/Input/Field/EntityInputTest.php new file mode 100644 index 000000000000..bb69f7bb1ed2 --- /dev/null +++ b/components/ILIAS/UI/tests/Component/Input/Field/EntityInputTest.php @@ -0,0 +1,99 @@ +getEntityRetrieval(); + $field = $this->getFieldFactory()->entity($retrieval)->withValue([11, 22]); + + $this->assertInstanceOf(C\Input\Field\Entity::class, $field); + $this->assertCount(2, $field->getGeneratedDynamicInputs()); + $this->assertSame([11, 22], array_values($field->getValue())); + $this->assertSame($retrieval, $field->getEntityRetrieval()); + } + + public function testEntityWithValueRejectsNonArray(): void + { + $this->expectException(InvalidArgumentException::class); + $this->getFieldFactory()->entity($this->getEntityRetrieval())->withValue('11'); + } + + public function testEntityFormGetDataReadsIdsFromRequest(): void + { + $field_factory = $this->getFieldFactory(); + $form_factory = new I\Input\Container\Form\Factory($field_factory, new I\SignalGenerator()); + $form = $form_factory->standard('https://example.com/go', [ + $field_factory->entity($this->getEntityRetrieval())->withValue([11, 22]), + ]); + + $entity_input = array_values($form->getInputs())[0]; + $this->assertInstanceOf(C\Input\Field\Entity::class, $entity_input); + + $parent_name = $entity_input->getName(); + $this->assertNotNull($parent_name); + + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getParsedBody')->willReturn([ + $parent_name => [ + 'input_0' => ['11', '22'], + ], + ]); + + $data = $form->withRequest($request)->getData(); + $this->assertIsArray($data); + $this->assertSame(['11', '22'], array_values((array) array_values($data)[0])); + } + + private function getEntityRetrieval(): C\Entity\EntityRetrieval + { + return new class () implements C\Entity\EntityRetrieval { + public function getEntities( + ILIAS\UI\Factory $ui_factory, + ILIAS\Data\Range $range, + ILIAS\Data\Order $order, + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters, + ): Generator { + yield from []; + } + + public function getEntitiesByIds( + ILIAS\UI\Factory $ui_factory, + ILIAS\Data\Order $order, + array $entity_ids, + ): Generator { + yield from []; + } + }; + } +} diff --git a/components/ILIAS/UI/tests/Component/Prompt/ConfirmationTest.php b/components/ILIAS/UI/tests/Component/Prompt/ConfirmationTest.php deleted file mode 100644 index 5bf82f98876b..000000000000 --- a/components/ILIAS/UI/tests/Component/Prompt/ConfirmationTest.php +++ /dev/null @@ -1,222 +0,0 @@ -getFormMock('https://example.com/go'); - - $form_factory = $this->createMock(C\Input\Container\Form\Factory::class); - $form_factory->method('standard')->willReturnCallback( - function (string $url, array $inputs) use (&$captured_url, &$captured_inputs, $form) { - $captured_url = $url; - $captured_inputs = $inputs; - - return $form; - } - ); - - $container_factory = $this->createMock(C\Input\Container\Factory::class); - $container_factory->method('form')->willReturn($form_factory); - - $input_factory = $this->createMock(C\Input\Factory::class); - $input_factory->method('container')->willReturn($container_factory); - $input_factory->method('field')->willReturn($this->getFieldFactory()); - - $factory = $this->getPromptFactory($input_factory); - - $url_builder = new URLBuilder(new URI('https://example.com/go')); - [$url_builder, $token] = $url_builder->acquireParameters(['test'], 'entities'); - - $confirmation = $factory->confirmation( - $this->getEntityRetrieval(), - $url_builder, - $token, - [11, 22], - 'question', - 'title', - ); - - $this->assertInstanceOf(C\Prompt\Confirmation::class, $confirmation); - $this->assertIsString($captured_url); - $this->assertStringNotContainsString('11', $captured_url); - $this->assertStringNotContainsString('22', $captured_url); - $this->assertStringNotContainsString($token->getName(), $captured_url); - - $this->assertIsArray($captured_inputs); - $this->assertArrayHasKey($token->getName(), $captured_inputs); - $group = $captured_inputs[$token->getName()]; - $this->assertInstanceOf(C\Input\Field\Group::class, $group); - $this->assertSame(['11', '22'], array_values($group->getValue())); - - $this->assertInstanceOf(Prompt\Confirmation::class, $confirmation); - $this->assertSame($form, $confirmation->getForm()); - } - - public function testGetDataReadsEntityIdsFromRequest(): void - { - $factory = $this->getPromptFactoryWithRealForms(); - - $url_builder = new URLBuilder(new URI('https://example.com/go')); - [$url_builder, $token] = $url_builder->acquireParameters(['test'], 'entities'); - - $confirmation = $factory->confirmation( - $this->getEntityRetrieval(), - $url_builder, - $token, - [11, 22], - 'question', - 'title', - ); - - $this->assertInstanceOf(Prompt\Confirmation::class, $confirmation); - - $group = $confirmation->getForm()->getInputs()[$token->getName()]; - $this->assertInstanceOf(C\Input\Field\Group::class, $group); - - $body = []; - foreach ($group->getInputs() as $hidden) { - $body[$hidden->getName()] = $hidden->getValue(); - } - - $request = $this->createMock(ServerRequestInterface::class); - $request->method('getParsedBody')->willReturn($body); - - $this->assertSame(['11', '22'], $confirmation->withRequest($request)->getData()); - $this->assertSame([], $confirmation->getData()); - } - - public function testGetDataReturnsEmptyArrayWhenRequestHasNoIds(): void - { - $factory = $this->getPromptFactoryWithRealForms(); - - $url_builder = new URLBuilder(new URI('https://example.com/go')); - [$url_builder, $token] = $url_builder->acquireParameters(['test'], 'entities'); - - $confirmation = $factory->confirmation( - $this->getEntityRetrieval(), - $url_builder, - $token, - [11, 22], - 'question', - 'title', - ); - - $request = $this->createMock(ServerRequestInterface::class); - $request->method('getParsedBody')->willReturn([]); - - $this->assertSame([], $confirmation->withRequest($request)->getData()); - } - - private function getPromptFactory(C\Input\Factory $input_factory): Prompt\Factory - { - return new Prompt\Factory( - new I\SignalGenerator(), - new Prompt\State\Factory(), - new ListingEntity\Factory(), - new MessageBox\Factory(), - $input_factory - ); - } - - private function getPromptFactoryWithRealForms(): Prompt\Factory - { - $field_factory = $this->getFieldFactory(); - $form_factory = new I\Input\Container\Form\Factory($field_factory, new I\SignalGenerator()); - $container_factory = $this->createMock(C\Input\Container\Factory::class); - $container_factory->method('form')->willReturn($form_factory); - - $input_factory = $this->createMock(C\Input\Factory::class); - $input_factory->method('field')->willReturn($field_factory); - $input_factory->method('container')->willReturn($container_factory); - - return $this->getPromptFactory($input_factory); - } - - private function getFormMock(string $post_url): I\Input\Container\Form\Standard - { - $form = $this->getMockBuilder(I\Input\Container\Form\Standard::class) - ->disableOriginalConstructor() - ->onlyMethods(['getPostURL', 'getSubmitSignal']) - ->getMock(); - $form->method('getPostURL')->willReturn($post_url); - $form->method('getSubmitSignal')->willReturn(new Signal('submit_signal')); - - return $form; - } - - private function getEntityRetrieval(): C\Entity\EntityRetrieval - { - return new class () implements C\Entity\EntityRetrieval { - public function getEntities( - ILIAS\UI\Factory $ui_factory, - ILIAS\Data\Range $range, - ILIAS\Data\Order $order, - mixed $additional_viewcontrol_data, - mixed $filter_data, - mixed $additional_parameters, - ): Generator { - yield from []; - } - - public function getEntitiesByIds( - ILIAS\UI\Factory $ui_factory, - ILIAS\Data\Order $order, - array $entity_ids, - ): Generator { - yield from []; - } - }; - } - - public function getUIFactory(): NoUIFactory - { - return new class () extends NoUIFactory { - public function messageBox(): MessageBox\Factory - { - return new MessageBox\Factory(); - } - - public function button(): I\Button\Factory - { - return new I\Button\Factory(); - } - }; - } -} diff --git a/components/ILIAS/UI/tests/InitUIFramework.php b/components/ILIAS/UI/tests/InitUIFramework.php index 064b8ed6448a..6d8611c301bb 100755 --- a/components/ILIAS/UI/tests/InitUIFramework.php +++ b/components/ILIAS/UI/tests/InitUIFramework.php @@ -446,10 +446,7 @@ public function getRefreshIntervalInMs(): int $c["ui.factory.prompt"] = function ($c) { return new ILIAS\UI\Implementation\Component\Prompt\Factory( $c["ui.signal_generator"], - $c["ui.factory.prompt.state"], - $c["ui.factory.listing"]->entity(), - $c["ui.factory.messagebox"], - $c["ui.factory.input"], + $c["ui.factory.prompt.state"] ); }; diff --git a/templates/default/070-components/UI-framework/Input/_ui-component_entity.scss b/templates/default/070-components/UI-framework/Input/_ui-component_entity.scss new file mode 100644 index 000000000000..519c1ba0f22f --- /dev/null +++ b/templates/default/070-components/UI-framework/Input/_ui-component_entity.scss @@ -0,0 +1,37 @@ +@use "../../../050-layout/basics/" as l; + +// Entity Input is not user-operated; render full-width without the form label column. +.c-form .c-input[data-il-ui-component="entity-field-input"].c-input { + display: flex; + flex-direction: column; + + > label:empty { + display: none; + } + + > label:not(:empty) { + width: 100%; + padding-right: 0; + margin-bottom: l.$il-margin-base-vertical; + } + + > .c-input__help-byline, + > .c-input__error-msg, + > .c-input__field { + width: 100%; + } +} + +.c-form ul.c-input-entity { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: l.$il-margin-large-vertical; + + > li { + margin: 0; + padding: 0; + } +} diff --git a/templates/default/070-components/UI-framework/Input/_ui-component_input.scss b/templates/default/070-components/UI-framework/Input/_ui-component_input.scss index 3cfe33a42477..b6889311bb30 100755 --- a/templates/default/070-components/UI-framework/Input/_ui-component_input.scss +++ b/templates/default/070-components/UI-framework/Input/_ui-component_input.scss @@ -29,6 +29,7 @@ @use "_ui-component_password.scss"; @use "_ui-component_radio.scss"; @use "_ui-component_multiselect.scss"; +@use "_ui-component_entity.scss"; @use "_ui-component_textarea.scss"; @use "_ui-component_filter.scss"; @use "_ui-component_duration.scss"; @@ -115,7 +116,7 @@ /// styling shared by multiple input elements // hover/focus extends beyond box without pushing it -.c-input:not([data-il-ui-component="section-field-input"]) { +.c-input:not([data-il-ui-component="section-field-input"]):not([data-il-ui-component="entity-field-input"]) { &:hover { box-shadow: 0px 0px 0px l.$il-margin-base-horizontal $il-main-dark-bg; background-color: $il-main-dark-bg; @@ -146,7 +147,7 @@ } margin-bottom: 0; // overrides alert default } - .c-input[data-il-ui-component]:not([data-il-ui-component="section-field-input"]):has(> .c-input__error-msg) { + .c-input[data-il-ui-component]:not([data-il-ui-component="section-field-input"]):not([data-il-ui-component="entity-field-input"]):has(> .c-input__error-msg) { border-inline-start: 6px solid $il-alert-danger-border; padding-left: l.$il-padding-base-horizontal; } diff --git a/templates/default/delos.css b/templates/default/delos.css index 3db6ac03ec63..9ae1000e01c9 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -8571,6 +8571,36 @@ button .minimize, button .close { width: 100%; } +.c-form .c-input[data-il-ui-component=entity-field-input].c-input { + display: flex; + flex-direction: column; +} +.c-form .c-input[data-il-ui-component=entity-field-input].c-input > label:empty { + display: none; +} +.c-form .c-input[data-il-ui-component=entity-field-input].c-input > label:not(:empty) { + width: 100%; + padding-right: 0; + margin-bottom: 3px; +} +.c-form .c-input[data-il-ui-component=entity-field-input].c-input > .c-input__help-byline, +.c-form .c-input[data-il-ui-component=entity-field-input].c-input > .c-input__error-msg, +.c-form .c-input[data-il-ui-component=entity-field-input].c-input > .c-input__field { + width: 100%; +} +.c-form ul.c-input-entity { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 12px; +} +.c-form ul.c-input-entity > li { + margin: 0; + padding: 0; +} + .c-form ul.c-field-multiselect { list-style: none; padding: 0; @@ -9765,11 +9795,11 @@ ul.c-input--has-mustache-variables__definitions > li { } } -.c-input:not([data-il-ui-component=section-field-input]):hover { +.c-input:not([data-il-ui-component=section-field-input]):not([data-il-ui-component=entity-field-input]):hover { box-shadow: 0px 0px 0px 9px #f9f9f9; background-color: #f9f9f9; } -.c-input:not([data-il-ui-component=section-field-input]):has(:focus-visible) { +.c-input:not([data-il-ui-component=section-field-input]):not([data-il-ui-component=entity-field-input]):has(:focus-visible) { box-shadow: 0px 0px 0px 9px rgb(226.2857142857, 231.6428571429, 238.7142857143); background-color: rgb(226.2857142857, 231.6428571429, 238.7142857143); } @@ -9795,7 +9825,7 @@ ul.c-input--has-mustache-variables__definitions > li { .c-form .c-input__error-msg { margin-bottom: 0; } -.c-form .c-input[data-il-ui-component]:not([data-il-ui-component=section-field-input]):has(> .c-input__error-msg) { +.c-form .c-input[data-il-ui-component]:not([data-il-ui-component=section-field-input]):not([data-il-ui-component=entity-field-input]):has(> .c-input__error-msg) { border-inline-start: 6px solid rgb(255, 214.54, 214.54); padding-left: 9px; } From 71e03d4578db6e0e88adc98438d4edf285241d92 Mon Sep 17 00:00:00 2001 From: Fabian Helfer Date: Thu, 27 Aug 2026 09:26:06 +0200 Subject: [PATCH 18/18] [Feature] UI: ConfirmationPrompt - copyright --- .../examples/Entity/Standard/video_object.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 bfbf74a933a1..899a84c1cdb1 100644 --- a/components/ILIAS/UI/src/examples/Entity/Standard/video_object.php +++ b/components/ILIAS/UI/src/examples/Entity/Standard/video_object.php @@ -1,5 +1,21 @@