diff --git a/components/ILIAS/UI/UI.php b/components/ILIAS/UI/UI.php index ca489d6225c0..548ec5d318a9 100644 --- a/components/ILIAS/UI/UI.php +++ b/components/ILIAS/UI/UI.php @@ -466,12 +466,17 @@ public function init( $internal[UI\Implementation\Component\Entity\Factory::class] = static fn() => new UI\Implementation\Component\Entity\Factory(); + $internal[UI\Implementation\Component\Prompt\State\Factory::class] = static fn() => + new UI\Implementation\Component\Prompt\State\Factory( + $internal[UI\Implementation\Component\Listing\Entity\Factory::class], + $internal[UI\Implementation\Component\MessageBox\Factory::class], + $internal[UI\Implementation\Component\Input\Factory::class], + ); $internal[UI\Implementation\Component\Prompt\Factory::class] = static fn() => new UI\Implementation\Component\Prompt\Factory( $internal[UI\Implementation\Component\SignalGeneratorInterface::class], + $internal[UI\Implementation\Component\Prompt\State\Factory::class], ); - $internal[UI\Implementation\Component\Prompt\State\Factory::class] = static fn() => - new UI\Implementation\Component\Prompt\State\Factory(); $internal[UI\Implementation\Component\Navigation\Factory::class] = static fn() => new UI\Implementation\Component\Navigation\Factory( @@ -551,6 +556,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], + ) ) ) ); diff --git a/components/ILIAS/UI/resources/js/Prompt/dist/prompt.min.js b/components/ILIAS/UI/resources/js/Prompt/dist/prompt.min.js index 788838ffbdba..7248d4887457 100644 --- a/components/ILIAS/UI/resources/js/Prompt/dist/prompt.min.js +++ b/components/ILIAS/UI/resources/js/Prompt/dist/prompt.min.js @@ -12,4 +12,4 @@ * https://www.ilias.de * https://github.com/ILIAS-eLearning */ -!function(t,e){"use strict";class r{#t;#e;#r;constructor(t,e){if(this.#t=t,this.#e=document.getElementById(e),null===this.#e)throw new Error(`Could not find a Prompt for id '${e}'.`);this.#r=this.#e.getElementsByTagName("dialog").item(0)}show(t){this.load(t),this.#r.showModal()}close(){this.#r.close()}async load(t,e={}){await fetch(t,e).then((t=>t.text())).then((t=>{const e=(new this.#t).parseFromString(t,"text/html"),r=e.querySelector('section[data-section="il-prompt-state__title"]'),n=e.querySelector('section[data-section="il-prompt-state__contents"]'),o=e.querySelector('section[data-section="il-prompt-state__buttons"]'),i=e.querySelector('section[data-section="il-prompt-state__command"]'),s=e.querySelector('section[data-section="il-prompt-state__parameters"]'),a=e.querySelector("script"),c=this.#r.querySelector("span.il-prompt__title"),p=this.#r.querySelector("div.il-prompt__contents"),l=this.#r.querySelector("div.il-prompt__buttons");c.innerHTML=r.innerHTML,p.innerHTML=n.innerHTML,l.innerHTML=o.innerHTML,this.#n(p),this.#o(p),a&&this.#i(a.text);const m=[];return s.querySelectorAll("data").forEach((t=>{m[t.innerHTML.trim()]=t.getAttribute("value")})),{cmd:i.innerHTML.trim(),params:m}})).then((t=>{const e=t.cmd,{params:r}=t;"close"===e&&this.close(),"redirect"===e&&window.location.replace(r.redirect)}))}#i(t){const e=this.#e.querySelector("section.il-prompt__scripts"),r=document.createElement("script");r.text=t,e.innerHTML="",e.appendChild(r)}#n(t){t.getElementsByTagName("form").forEach((t=>{t.addEventListener("submit",(e=>{e.preventDefault(),this.load(t.action,{method:t.method,body:new FormData(t)})}))}))}#o(t){t.getElementsByTagName("a").forEach((t=>{const{target:e}=t;if("_blank"!==e){const e=t.href;t.addEventListener("click",(()=>this.load(e))),t.removeAttribute("href")}}))}}t.UI=t.UI||{},t.UI.prompt=new class{#t;#s=[];constructor(t){this.#t=t}init(t){if(void 0!==this.#s[t])throw new Error(`Prompt with id '${t}' has already been initialized.`);this.#s[t]=new r(this.#t,t)}get(t){return this.#s[t]??null}}(e)}(il,DOMParser); +!function(t,e){"use strict";class r{#t;#e;#r;constructor(t,e){if(this.#t=t,this.#e=document.getElementById(e),null===this.#e)throw new Error(`Could not find a Prompt for id '${e}'.`);this.#r=this.#e.getElementsByTagName("dialog").item(0)}show(t){this.load(t),this.#r.showModal()}close(){this.#r.close()}async load(t,e={}){await fetch(t,e).then((t=>t.text())).then((t=>{const e=(new this.#t).parseFromString(t,"text/html"),r=e.querySelector('section[data-section="il-prompt-state__title"]'),n=e.querySelector('section[data-section="il-prompt-state__contents"]'),o=e.querySelector('section[data-section="il-prompt-state__buttons"]'),i=e.querySelector('section[data-section="il-prompt-state__command"]'),s=e.querySelector('section[data-section="il-prompt-state__parameters"]'),a=e.querySelector("script[data-replace-marker=\"script\"]"),c=this.#r.querySelector("span.il-prompt__title"),p=this.#r.querySelector("div.il-prompt__contents"),l=this.#r.querySelector("div.il-prompt__buttons");r&&(c.innerHTML=r.innerHTML),n&&(p.innerHTML=n.innerHTML),o&&(l.innerHTML=o.innerHTML),this.#n(p),this.#o(p),a&&this.#i(a.text);const m=[];return s&&s.querySelectorAll("data").forEach((t=>{m[t.innerHTML.trim()]=t.getAttribute("value")})),{cmd:i?i.innerHTML.trim():"",params:m}})).then((t=>{const e=t.cmd,{params:r}=t;"close"===e&&this.close(),"redirect"===e&&window.location.replace(r.redirect)}))}#i(t){const e=this.#e.querySelector("section.il-prompt__scripts"),r=document.createElement("script");r.text=t.replace(/il\.UI\.prompt\.init\([^)]*\);?/g,"").replace(/\$\(this\)\.trigger\('(il_signal_[^']+)'/g,"$(document).trigger('$1'"),e.innerHTML="",e.appendChild(r)}#n(t){t.querySelectorAll("form").forEach((t=>{t.addEventListener("submit",(e=>{e.preventDefault(),this.load(t.action,{method:t.method,body:new FormData(t)})}))}))}#o(t){t.querySelectorAll("a").forEach((t=>{const{target:e}=t;if("_blank"!==e){const e=t.href;t.addEventListener("click",(()=>this.load(e))),t.removeAttribute("href")}}))}}t.UI=t.UI||{},t.UI.prompt=new class{#t;#s=[];constructor(t){this.#t=t}init(t){if(void 0!==this.#s[t])return;try{this.#s[t]=new r(this.#t,t)}catch(t){console.warn(t)}}get(t){return this.#s[t]??null}}(e)}(il,DOMParser); diff --git a/components/ILIAS/UI/resources/js/Prompt/src/prompt.class.js b/components/ILIAS/UI/resources/js/Prompt/src/prompt.class.js index e34ae4065d9a..e467888ca22b 100644 --- a/components/ILIAS/UI/resources/js/Prompt/src/prompt.class.js +++ b/components/ILIAS/UI/resources/js/Prompt/src/prompt.class.js @@ -70,13 +70,19 @@ export default class Prompt { const buttons = doc.querySelector('section[data-section="il-prompt-state__buttons"]'); const command = doc.querySelector('section[data-section="il-prompt-state__command"]'); const parameters = doc.querySelector('section[data-section="il-prompt-state__parameters"]'); - const scripts = doc.querySelector('script'); + const scripts = doc.querySelector('script[data-replace-marker="script"]'); const dialogTitle = this.#prompt.querySelector('span.il-prompt__title'); const dialogContents = this.#prompt.querySelector('div.il-prompt__contents'); const dialogButtons = this.#prompt.querySelector('div.il-prompt__buttons'); - dialogTitle.innerHTML = title.innerHTML; - dialogContents.innerHTML = contents.innerHTML; - dialogButtons.innerHTML = buttons.innerHTML; + if (title) { + dialogTitle.innerHTML = title.innerHTML; + } + if (contents) { + dialogContents.innerHTML = contents.innerHTML; + } + if (buttons) { + dialogButtons.innerHTML = buttons.innerHTML; + } this.#captureForms(dialogContents); this.#captureLinks(dialogContents); @@ -85,14 +91,16 @@ export default class Prompt { } const params = []; - parameters.querySelectorAll('data').forEach( - (data) => { - params[data.innerHTML.trim()] = data.getAttribute('value'); - }, - ); + if (parameters) { + parameters.querySelectorAll('data').forEach( + (data) => { + params[data.innerHTML.trim()] = data.getAttribute('value'); + }, + ); + } return { - cmd: command.innerHTML.trim(), + cmd: command ? command.innerHTML.trim() : '', params, }; }) @@ -117,7 +125,12 @@ export default class Prompt { #appendScript(js) { const dialogScript = this.#component.querySelector('section.il-prompt__scripts'); const script = document.createElement('script'); - script.text = js; + script.text = js + .replace(/il\.UI\.prompt\.init\([^)]*\);?/g, '') + .replace( + /\$\(this\)\.trigger\('(il_signal_[^']+)'/g, + "$(document).trigger('$1'", + ); dialogScript.innerHTML = ''; dialogScript.appendChild(script); } @@ -127,8 +140,7 @@ export default class Prompt { * @return {void} */ #captureForms(doc) { - const forms = doc.getElementsByTagName('form'); - forms.forEach( + doc.querySelectorAll('form').forEach( (form) => { form.addEventListener('submit', (e) => { e.preventDefault(); @@ -146,8 +158,7 @@ export default class Prompt { * @return {void} */ #captureLinks(doc) { - const links = doc.getElementsByTagName('a'); - links.forEach( + doc.querySelectorAll('a').forEach( (lnk) => { const { target } = lnk; if (target !== '_blank') { diff --git a/components/ILIAS/UI/resources/js/Prompt/src/prompt.factory.js b/components/ILIAS/UI/resources/js/Prompt/src/prompt.factory.js index e8daa037cdc4..9a5c2644e156 100644 --- a/components/ILIAS/UI/resources/js/Prompt/src/prompt.factory.js +++ b/components/ILIAS/UI/resources/js/Prompt/src/prompt.factory.js @@ -13,7 +13,7 @@ * https://github.com/ILIAS-eLearning */ -import Prompt from './prompt.class'; +import Prompt from './prompt.class.js'; export default class PromptFactory { /** @@ -40,10 +40,14 @@ export default class PromptFactory { */ init(id) { if (this.#instances[id] !== undefined) { - throw new Error(`Prompt with id '${id}' has already been initialized.`); + return; } - this.#instances[id] = new Prompt(this.#DOMParser, id); + try { + this.#instances[id] = new Prompt(this.#DOMParser, id); + } catch (error) { + // Prompt element may not exist yet during async content replacement. + } } /** 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 @@ - + * 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. + * 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/Entity/BriefEntityRenderer.php b/components/ILIAS/UI/src/Implementation/Component/Entity/BriefEntityRenderer.php new file mode 100644 index 000000000000..86d125ff8da0 --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Entity/BriefEntityRenderer.php @@ -0,0 +1,55 @@ +renderEntity($component, $default_renderer); + } + $this->cannotHandleComponent($component); + } + + 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 04ae7352e2b2..d6a43fe56f6a 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Entity/Entity.php +++ b/components/ILIAS/UI/src/Implementation/Component/Entity/Entity.php @@ -77,11 +77,17 @@ abstract class Entity implements I\Entity protected ?Workflow\Linear $workflow = null; public function __construct( + protected string|int $id, protected Symbol | Image | Shy | StandardLink | string $primary_identifier, protected Symbol | Image | Shy | StandardLink | string $secondary_identifier ) { } + public function getId(): string|int + { + return $this->id; + } + public function withPrimaryIdentifier(Symbol | Image | Shy | StandardLink | string $primary_identifier): self { $clone = clone $this; @@ -291,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 new file mode 100644 index 000000000000..7b360038d67b --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Entity/EntityRendererFactory.php @@ -0,0 +1,61 @@ +isCompactListItem() + ) { + return new BriefEntityRenderer( + $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 + ); + } + + 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/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/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..85373305e77d --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/State/Confirmation.php @@ -0,0 +1,69 @@ +message_box; + } + + public function getEntityListing(): EntityListing + { + return $this->entity_listing; + } + + 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..79da39b58363 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); + + $form = $this->input_factory->container()->form()->standard( + (string) $post_url->withParameter($post_parameter, $entity_ids)->buildURI(), + [] + ); + + $content = new Confirmation( + $message_box, + $listing, + $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..5d5949dd9b9d 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,19 @@ 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 { @@ -58,29 +66,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/Implementation/Render/FSLoader.php b/components/ILIAS/UI/src/Implementation/Render/FSLoader.php index e7e5ff088705..fca01c3d86df 100755 --- a/components/ILIAS/UI/src/Implementation/Render/FSLoader.php +++ b/components/ILIAS/UI/src/Implementation/Render/FSLoader.php @@ -28,6 +28,7 @@ use ILIAS\UI\Implementation\Component\MessageBox\MessageBox; use ILIAS\UI\Implementation\Component\Input\Container\Form\Form; use ILIAS\UI\Implementation\Component\Menu\Menu; +use ILIAS\UI\Component\Entity\Entity; /** * Loads renderers for components from the file system. @@ -51,6 +52,7 @@ public function __construct( private RendererFactory $message_box_renderer_factory, private RendererFactory $form_renderer_factory, private RendererFactory $menu_renderer_factory, + private RendererFactory $entity_renderer_factory, ) { } @@ -75,6 +77,9 @@ public function getRendererFactoryFor(Component $component): RendererFactory if ($component instanceof MessageBox) { return $this->message_box_renderer_factory; } + if ($component instanceof Entity) { + return $this->entity_renderer_factory; + } if ($component instanceof Form) { return $this->form_renderer_factory; } diff --git a/components/ILIAS/UI/src/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/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/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/Prompt/Standard/confirmation.php b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php new file mode 100644 index 000000000000..334756dafab7 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/confirmation.php @@ -0,0 +1,202 @@ + + * 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. + * The prompt can be closed. + * --- + */ +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 +{ + 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 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, $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/base.php b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/base.php new file mode 100644 index 000000000000..5d97a069bafc --- /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 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 $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..5e10d0d92c58 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/close.php @@ -0,0 +1,101 @@ + + * 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', '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 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/State/Confirm/redirect.php b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php new file mode 100644 index 000000000000..bbb80bac6fc4 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Prompt/State/Confirm/redirect.php @@ -0,0 +1,140 @@ + + * 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', '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 !== '') { + $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/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/Base.php b/components/ILIAS/UI/tests/Base.php index a82b6f9d7ad7..4639291ce7ea 100755 --- a/components/ILIAS/UI/tests/Base.php +++ b/components/ILIAS/UI/tests/Base.php @@ -46,6 +46,7 @@ use ILIAS\UI\Implementation\Component\Input\UploadLimitResolver; use ILIAS\UI\Implementation\Component\MessageBox\MessageBoxRendererFactory; use ILIAS\UI\Implementation\Component\Input\Container\Form\FormRendererFactory; +use ILIAS\UI\Implementation\Component\Entity\EntityRendererFactory; class ilIndependentTemplateFactory implements TemplateFactory { @@ -479,6 +480,16 @@ public function getDefaultRenderer( $data_factory, $help_text_retriever, $this->getUploadLimitResolver(), + ), + new EntityRendererFactory( + $ui_factory, + $tpl_factory, + $lng, + $js_binding, + $image_path_resolver, + $data_factory, + $help_text_retriever, + $this->getUploadLimitResolver() ) ) ) diff --git a/components/ILIAS/UI/tests/Component/Entity/EntityTest.php b/components/ILIAS/UI/tests/Component/Entity/EntityTest.php index a195d16a4102..6308ba5cc6d3 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); @@ -130,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); @@ -164,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(), ); @@ -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) 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)); } } 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())); diff --git a/components/ILIAS/UI/tests/InitUIFramework.php b/components/ILIAS/UI/tests/InitUIFramework.php index 0ba2a1c745e5..f54074a17921 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"] + ), ) ) ); @@ -419,8 +429,19 @@ public function getRefreshIntervalInMs(): int return new ILIAS\UI\Implementation\Component\Entity\Factory(); }; + $c["ui.factory.prompt.state"] = function ($c) { + return new ILIAS\UI\Implementation\Component\Prompt\State\Factory( + $c["ui.factory.listing"]->entity(), + $c["ui.factory.messagebox"], + $c["ui.factory.input"], + ); + }; + $c["ui.factory.prompt"] = function ($c) { - return new ILIAS\UI\Implementation\Component\Prompt\Factory($c["ui.signal_generator"]); + return new ILIAS\UI\Implementation\Component\Prompt\Factory( + $c["ui.signal_generator"], + $c["ui.factory.prompt.state"] + ); }; $c["ui.factory.navigation"] = function ($c) { diff --git a/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php b/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php index 689fbfd80d76..26f328cdb6cb 100755 --- a/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php +++ b/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php @@ -28,6 +28,7 @@ use ILIAS\UI\Implementation\Component\MessageBox\MessageBox; use ILIAS\UI\Implementation\Component\Input\Container\Form\Form; use ILIAS\UI\Implementation\Component\Menu\Menu; +use ILIAS\UI\Component\Entity\Entity; use ILIAS\UI\Component\Component; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -41,6 +42,7 @@ class FSLoaderTest extends TestCase protected RendererFactory & MockObject $message_box_renderer_factory; protected RendererFactory & MockObject $form_renderer_factory; protected RendererFactory & MockObject $menu_renderer_factory; + protected RendererFactory & MockObject $entity_renderer_factory; protected FSLoader $fs_loader; @@ -52,6 +54,7 @@ protected function setUp(): void $this->message_box_renderer_factory = $this->createMock(RendererFactory::class); $this->form_renderer_factory = $this->createMock(RendererFactory::class); $this->menu_renderer_factory = $this->createMock(RendererFactory::class); + $this->entity_renderer_factory = $this->createMock(RendererFactory::class); $this->fs_loader = new FSLoader( $this->default_renderer_factory, @@ -60,6 +63,7 @@ protected function setUp(): void $this->message_box_renderer_factory, $this->form_renderer_factory, $this->menu_renderer_factory, + $this->entity_renderer_factory, ); parent::setUp(); @@ -139,6 +143,13 @@ public function testGetRendererFactoryForButton(): void $this->assertSame($factory, $this->button_renderer_factory); } + public function testGetRendererFactoryForEntity(): void + { + $component_mock = $this->createMock(Entity::class); + $factory = $this->fs_loader->getRendererFactoryFor($component_mock); + $this->assertSame($factory, $this->entity_renderer_factory); + } + public function testGetRendererFactoryForOther(): void { $component_mock = $this->createMock(Component::class);