From 4e878b6e27fe9c6fd76b0346ab0763974b15e1f7 Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Mon, 28 Oct 2024 11:41:06 +0100 Subject: [PATCH 1/2] [FIX] UI: stop fetching from async Progress\Bar endpoint on failure. --- components/ILIAS/UI/resources/js/Progress/dist/progress.min.js | 2 +- .../UI/resources/js/Progress/src/ProgressBarAsyncDecorator.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/UI/resources/js/Progress/dist/progress.min.js b/components/ILIAS/UI/resources/js/Progress/dist/progress.min.js index 29176d1818ba..c277d4a50016 100644 --- a/components/ILIAS/UI/resources/js/Progress/dist/progress.min.js +++ b/components/ILIAS/UI/resources/js/Progress/dist/progress.min.js @@ -12,4 +12,4 @@ * https://www.ilias.de * https://github.com/ILIAS-eLearning */ -!function(e,t){"use strict";class s{#e;#t;constructor(e,t){this.#e=e,this.#t=t}indeterminate(e=null){this.#e.hasAttribute("value")&&this.#e.removeAttribute("value"),null!==e&&this.#s(e)}determinate(e,t=null){if(!Number.isInteger(e)||e<0||e>=this.#e.max)throw new Error(`Progress value must be a whole number between 0 and ${this.#e.max}.`);this.#e.value=e,null!==t&&this.#s(t)}success(e){this.#e.value!==this.#e.max&&this.#r(e,"success")}failure(e){this.#e.value!==this.#e.max&&this.#r(e,"failure")}reset(){this.#e.labels.forEach((e=>{e.querySelectorAll("span[data-status]").forEach((e=>{e.classList.remove("visible"),e.classList.add("hidden")}))})),this.#e.classList.remove("c-progress-bar--success"),this.#e.classList.remove("c-progress-bar--failure"),this.#e.value=0,this.#n()}#r(e,t){this.#e.labels.forEach((e=>{e.parentElement.querySelectorAll("span[data-status]").forEach((e=>{e.getAttribute("data-status")===t?(e.classList.remove("hidden"),e.classList.add("visible")):(e.classList.remove("visible"),e.classList.add("hidden"))}))})),this.#e.value=this.#e.max,this.#e.classList.add(`c-progress-bar--${t}`),this.#s(e)}#s(e){this.#t.classList.remove("invisible"),this.#t.classList.add("visible"),this.#t.textContent=e}#n(){this.#t.classList.remove("visible"),this.#t.classList.add("invisible"),this.#t.textContent=""}}function r(e,t){if(!(t instanceof e.defaultView.HTMLProgressElement))throw new Error("Progress bar must have a element.");const r=t.parentElement.querySelector(".c-progress-bar__message");if(null===r)throw new Error("Could not find progress bar message element.");return new s(t,r)}class n{#i;#a;#l;#o;#c=null;#h=!1;constructor(e,t,s,r){this.#i=e,this.#a=t,this.#l=s,this.#o=r}#u(e){const t=function(e){const t=e.querySelector('section[data-section="status"]');return null!==t&&t.hasAttribute("data-status")?t.getAttribute("data-status"):null}(e),s=function(e){const t=e.querySelector('section[data-section="progress"] > progress');return t instanceof e.ownerDocument.defaultView.HTMLProgressElement?parseInt(t.value,10):null}(e),r=function(e){const t=e.querySelector('section[data-section="message"]');return null!==t&&t.innerText?t.innerText:null}(e);"determinate"===t?this.determinate(s??-1,r):"indeterminate"===t?this.indeterminate(r):"success"===t?this.success(r):this.failure(r)}#m(){null===this.#c&&(this.#c=setInterval((()=>this.#g()),this.#l))}#d(){null!==this.#c&&(clearInterval(this.#c),this.#c=null)}async#g(){if(!this.#h)try{this.#h=!0;const e=await this.#i.loadContent(this.#o);this.#u(e)}catch(e){this.#a.failure(e.message)}finally{this.#h=!1}}indeterminate(e=null){this.#a.indeterminate(e),this.#m()}determinate(e,t=null){this.#a.determinate(e,t)}success(e){this.#a.success(e),this.#d()}failure(e){this.#a.failure(e),this.#d()}reset(){this.#a.reset(),this.#d()}}const i=new class{#p;constructor(e){this.#p=e}loadContent(e){return fetch(e.toString()).then((e=>e.text())).then((e=>this.#f(e))).then((e=>this.#E(e))).catch((t=>{throw new Error(`Could not render element(s) from '${e}': ${t.message}`)}))}#b(e){const t=this.#p.createElement("script");return e.hasAttribute("type")&&t.setAttribute("type",e.getAttribute("type")),e.hasAttribute("src")&&t.setAttribute("src",e.getAttribute("src")),e.textContent.length>0&&(t.textContent=e.textContent),t}#f(e){const t=this.#p.createElement("div");return t.innerHTML=e.trim(),t.querySelectorAll("script").forEach((e=>{const t=this.#b(e);e.replaceWith(t)})),t.children}#E(e){const t=this.#p.createDocumentFragment();return t.append(...e),t}}(t),a=new class{#v=new Map;register(e,t){this.#v.has(t)||this.#v.set(t,e)}indeterminate(e,t=null){this.#w(e).indeterminate(t)}determinate(e,t,s=null){this.#w(e).determinate(t,s)}success(e,t){this.#w(e).success(t)}failure(e,t){this.#w(e).failure(t)}#w(e){if(!this.#v.has(e))throw new Error(`Could not find progress bar component for signal '${e}'`);return this.#v.get(e)}};e.UI=e.UI||{},e.UI.Progress={},e.UI.Progress.Bar={indeterminate:(e,t)=>a.indeterminate(e,t),success:(e,t)=>a.success(e,t),failure:(e,t)=>a.failure(e,t),determinate:(e,t,s)=>a.determinate(e,t,s),createAsync:(e,s,l)=>{const o=function(e,t,s,r){if(!s.hasAttribute("data-url"))throw new Error('Async progress bar must provide a "data-url" attribute.');return new n(e,t,r,s.getAttribute("data-url"))}(i,r(t,e),e,l);return a.register(o,s),o},create:(e,s)=>{const n=r(t,e);return a.register(n,s),n}}}(il,document); +!function(e,t){"use strict";class s{#e;#t;constructor(e,t){this.#e=e,this.#t=t}indeterminate(e=null){this.#e.hasAttribute("value")&&this.#e.removeAttribute("value"),null!==e&&this.#s(e)}determinate(e,t=null){if(!Number.isInteger(e)||e<0||e>=this.#e.max)throw new Error(`Progress value must be a whole number between 0 and ${this.#e.max}.`);this.#e.value=e,null!==t&&this.#s(t)}success(e){this.#e.value!==this.#e.max&&this.#r(e,"success")}failure(e){this.#e.value!==this.#e.max&&this.#r(e,"failure")}reset(){this.#e.labels.forEach((e=>{e.querySelectorAll("span[data-status]").forEach((e=>{e.classList.remove("visible"),e.classList.add("hidden")}))})),this.#e.classList.remove("c-progress-bar--success"),this.#e.classList.remove("c-progress-bar--failure"),this.#e.value=0,this.#n()}#r(e,t){this.#e.labels.forEach((e=>{e.parentElement.querySelectorAll("span[data-status]").forEach((e=>{e.getAttribute("data-status")===t?(e.classList.remove("hidden"),e.classList.add("visible")):(e.classList.remove("visible"),e.classList.add("hidden"))}))})),this.#e.value=this.#e.max,this.#e.classList.add(`c-progress-bar--${t}`),this.#s(e)}#s(e){this.#t.classList.remove("invisible"),this.#t.classList.add("visible"),this.#t.textContent=e}#n(){this.#t.classList.remove("visible"),this.#t.classList.add("invisible"),this.#t.textContent=""}}function r(e,t){if(!(t instanceof e.defaultView.HTMLProgressElement))throw new Error("Progress bar must have a element.");const r=t.parentElement.querySelector(".c-progress-bar__message");if(null===r)throw new Error("Could not find progress bar message element.");return new s(t,r)}class n{#i;#a;#l;#o;#c=null;#h=!1;constructor(e,t,s,r){this.#i=e,this.#a=t,this.#l=s,this.#o=r}#u(e){const t=function(e){const t=e.querySelector('section[data-section="status"]');return null!==t&&t.hasAttribute("data-status")?t.getAttribute("data-status"):null}(e),s=function(e){const t=e.querySelector('section[data-section="progress"] > progress');return t instanceof e.ownerDocument.defaultView.HTMLProgressElement?parseInt(t.value,10):null}(e),r=function(e){const t=e.querySelector('section[data-section="message"]');return null!==t&&t.innerText?t.innerText:null}(e);"determinate"===t?this.determinate(s??-1,r):"indeterminate"===t?this.indeterminate(r):"success"===t?this.success(r):this.failure(r)}#m(){null===this.#c&&(this.#c=setInterval((()=>this.#g()),this.#l))}#d(){null!==this.#c&&(clearInterval(this.#c),this.#c=null)}async#g(){if(!this.#h)try{this.#h=!0;const e=await this.#i.loadContent(this.#o);this.#u(e)}catch(e){this.failure(e.message)}finally{this.#h=!1}}indeterminate(e=null){this.#a.indeterminate(e),this.#m()}determinate(e,t=null){this.#a.determinate(e,t)}success(e){this.#a.success(e),this.#d()}failure(e){this.#a.failure(e),this.#d()}reset(){this.#a.reset(),this.#d()}}const i=new class{#p;constructor(e){this.#p=e}loadContent(e){return fetch(e.toString()).then((e=>e.text())).then((e=>this.#f(e))).then((e=>this.#E(e))).catch((t=>{throw new Error(`Could not render element(s) from '${e}': ${t.message}`)}))}#b(e){const t=this.#p.createElement("script");return e.hasAttribute("type")&&t.setAttribute("type",e.getAttribute("type")),e.hasAttribute("src")&&t.setAttribute("src",e.getAttribute("src")),e.textContent.length>0&&(t.textContent=e.textContent),t}#f(e){const t=this.#p.createElement("div");return t.innerHTML=e.trim(),t.querySelectorAll("script").forEach((e=>{const t=this.#b(e);e.replaceWith(t)})),t.children}#E(e){const t=this.#p.createDocumentFragment();return t.append(...e),t}}(t),a=new class{#v=new Map;register(e,t){this.#v.has(t)||this.#v.set(t,e)}indeterminate(e,t=null){this.#w(e).indeterminate(t)}determinate(e,t,s=null){this.#w(e).determinate(t,s)}success(e,t){this.#w(e).success(t)}failure(e,t){this.#w(e).failure(t)}#w(e){if(!this.#v.has(e))throw new Error(`Could not find progress bar component for signal '${e}'`);return this.#v.get(e)}};e.UI=e.UI||{},e.UI.Progress={},e.UI.Progress.Bar={indeterminate:(e,t)=>a.indeterminate(e,t),success:(e,t)=>a.success(e,t),failure:(e,t)=>a.failure(e,t),determinate:(e,t,s)=>a.determinate(e,t,s),createAsync:(e,s,l)=>{const o=function(e,t,s,r){if(!s.hasAttribute("data-url"))throw new Error('Async progress bar must provide a "data-url" attribute.');return new n(e,t,r,s.getAttribute("data-url"))}(i,r(t,e),e,l);return a.register(o,s),o},create:(e,s)=>{const n=r(t,e);return a.register(n,s),n}}}(il,document); diff --git a/components/ILIAS/UI/resources/js/Progress/src/ProgressBarAsyncDecorator.js b/components/ILIAS/UI/resources/js/Progress/src/ProgressBarAsyncDecorator.js index 9e271c1bb752..4c394dc37219 100644 --- a/components/ILIAS/UI/resources/js/Progress/src/ProgressBarAsyncDecorator.js +++ b/components/ILIAS/UI/resources/js/Progress/src/ProgressBarAsyncDecorator.js @@ -132,7 +132,7 @@ export default class ProgressBarAsyncDecorator { const state = await this.#asyncRenderer.loadContent(this.#asyncUrl); this.#handleState(state); } catch (error) { - this.#progressBar.failure(error.message); + this.failure(error.message); } finally { this.#isFetching = false; } From d788c928ffc1c22838cafbbe8e3743c01561b890 Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Mon, 28 Oct 2024 13:44:39 +0100 Subject: [PATCH 2/2] [FEATURE] ILIASObject: remove ilProgressBar usage in progress table. --- .../classes/class.ilObjectCopyGUI.php | 44 +++++++++---------- .../class.ilObjectCopyProgressTableGUI.php | 36 +++++++++------ lang/ilias_de.lang | 1 + lang/ilias_en.lang | 1 + 4 files changed, 47 insertions(+), 35 deletions(-) diff --git a/components/ILIAS/ILIASObject/classes/class.ilObjectCopyGUI.php b/components/ILIAS/ILIASObject/classes/class.ilObjectCopyGUI.php index 114231555938..c39ce7b938b1 100755 --- a/components/ILIAS/ILIASObject/classes/class.ilObjectCopyGUI.php +++ b/components/ILIAS/ILIASObject/classes/class.ilObjectCopyGUI.php @@ -29,6 +29,7 @@ use ILIAS\UI\Renderer as UIRenderer; use ILIAS\UI\Component\Input\Container\Form\Standard; use ILIAS\Object\ImplementsCreationCallback; +use ILIAS\HTTP\GlobalHttpState; /** * GUI class for the workflow of copying objects @@ -73,6 +74,7 @@ class ilObjectCopyGUI protected ServerRequestInterface $request; protected UIFactory $ui_factory; protected UIRenderer $ui_renderer; + protected GlobalHttpState $http; protected ContainerDBRepository $container_repo; @@ -112,6 +114,7 @@ public function __construct(ImplementsCreationCallback $parent_gui) $this->ui_factory = $DIC['ui.factory']; $this->ui_renderer = $DIC['ui.renderer']; $this->retriever = new ilObjectRequestRetriever($DIC->http()->wrapper(), $this->refinery); + $this->http = $DIC->http(); $this->container_repo = new ContainerDBRepository($DIC['ilDB']); @@ -1017,34 +1020,31 @@ protected function showCopyProgress(): void protected function updateProgress(): void { - $json = new stdClass(); - $json->percentage = null; - $json->performed_steps = null; - + $max_steps = $this->retriever->getMaybeInt('_max_steps'); $copy_id = $this->retriever->getMaybeInt('_copy_id'); $options = ilCopyWizardOptions::_getInstance($copy_id); - $node = $options->fetchFirstNode(); - $json->current_node_id = 0; - $json->current_node_title = ""; - $json->in_dependencies = false; - if (is_array($node)) { - $json->current_node_id = $node['obj_id']; - $json->current_node_title = $node['title']; + $required_steps = $options->getRequiredSteps(); + + if (0 === $required_steps) { + $state = $this->ui_factory->progress()->state()->bar()->success($this->lng->txt('copied')); } else { - $node = $options->fetchFirstDependenciesNode(); - if (is_array($node)) { - $json->current_node_id = $node['obj_id']; - $json->current_node_title = $node['title']; - $json->in_dependencies = true; - } + $completed_steps = $max_steps - $required_steps; + $percentage = floor(($completed_steps / $max_steps) * 100); + $percentage = min($percentage, 99); // cap value to 99 + $percentage = (int) $percentage; + + $state = $this->ui_factory->progress()->state()->bar()->determinate($percentage); } - $json->required_steps = $options->getRequiredSteps(); - $json->id = $copy_id; - $this->log->debug('Update copy progress: ' . json_encode($json)); + $html = $this->ui_renderer->renderAsync($state); - echo json_encode($json); - exit; + $this->http->saveResponse( + $this->http->response() + ->withHeader('Content-Type', 'text/html; charset=utf-8') + ->withBody(\ILIAS\Filesystem\Stream\Streams::ofString($html)) + ); + $this->http->sendResponse(); + $this->http->close(); } protected function copyContainer(int $target_ref_id): array diff --git a/components/ILIAS/ILIASObject/classes/class.ilObjectCopyProgressTableGUI.php b/components/ILIAS/ILIASObject/classes/class.ilObjectCopyProgressTableGUI.php index 5773a68d148f..d539dcd027af 100755 --- a/components/ILIAS/ILIASObject/classes/class.ilObjectCopyProgressTableGUI.php +++ b/components/ILIAS/ILIASObject/classes/class.ilObjectCopyProgressTableGUI.php @@ -1,7 +1,5 @@ setId('obj_cp_prog_tbl_' . $id); + $this->ui_factory = $DIC->ui()->factory(); + $this->ui_renderer = $DIC->ui()->renderer(); + $this->data_factory = new \ILIAS\Data\Factory(); parent::__construct($parent_obj, $parent_cmd); } @@ -73,16 +81,11 @@ protected function fillRow(array $set): void $this->tpl->setVariable('TYPE_IMG', ilObject::_getIcon($set['obj_id'], "small", $set['type'])); $this->tpl->setVariable('TYPE_STR', $this->lng->txt('obj_' . $set['type'])); - $progress = ilProgressBar::getInstance(); - $progress->setType(ilProgressBar::TYPE_SUCCESS); - $progress->setMin(0); - $progress->setMax($set['max_steps']); - $progress->setCurrent(0); - $progress->setAnimated(true); - $progress->setId((string) $set['copy_id']); - $this->ctrl->setParameter($this->getParentObject(), '_copy_id', $set['copy_id']); - $progress->setAsyncStatusUrl( + $this->ctrl->setParameter($this->getParentObject(), '_max_steps', $set['max_steps']); + + $progress_endpoint = $this->data_factory->uri( + ILIAS_HTTP_PATH . '/' . $this->ctrl->getLinkTarget( $this->getParentObject(), 'updateProgress', @@ -91,8 +94,15 @@ protected function fillRow(array $set): void ) ); - $progress->setAsynStatusTimeout(1); - $this->tpl->setVariable('PROGRESS_BAR', $progress->render()); + $progress_bar = $this->ui_factory->progress()->bar( + $this->lng->txt('copy_of') . ' ' . $set['title'], + $progress_endpoint + ); + + $this->tpl->setVariable('PROGRESS_BAR', $this->ui_renderer->render($progress_bar)); + + // start pulling progress from $endpoint once as soon as the page has loaded. + $this->main_tpl->addOnLoadCode("il.UI.Progress.Bar.indeterminate('{$progress_bar->getUpdateSignal()}')"); } public function parse(): void diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 695adf12e773..95c0f9b58e2d 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -3837,6 +3837,7 @@ common#:#continue#:#Weiter common#:#continue_work#:#Fortsetzen common#:#contra#:#Contra common#:#copa#:#Inhaltsseite +common#:#copied#:#Kopiert common#:#copy#:#Kopieren common#:#copyChapter#:#Kopieren common#:#copyPage#:#Kopieren diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 0f75163fff10..a525b91d9d99 100755 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -3837,6 +3837,7 @@ common#:#continue#:#Continue common#:#continue_work#:#Continue common#:#contra#:#Contra common#:#copa#:#Content Page +common#:#copied#:#Copied common#:#copy#:#Copy common#:#copyChapter#:#Copy common#:#copyPage#:#Copy