Commit 308b6e5b authored by Andreas Fernandez's avatar Andreas Fernandez
Browse files

[BUGFIX] Improve performance of "View Upgrade Documentation"

This patch greatly improves the performance of the
"View Upgrade Documentation" module in the Install Tool.

The following optimizations are done:

* The tag filter is initialized only once now to avoid double-filtering
  documents
* Document tags are now rendered directly in Fluid, removing one
  expensive document manipulation task
* Composition of the tag list for the tag filter is sped up by a factor
  of 1.5 - 2 by using Set() to filter most duplicates
* When emptying a search, internal upgrade doc classes are removed once
  all panels are hidden
* When filtering, matching version panels are expanded with a delay of
  20ms, relaxing re-layouts and repaints in the browser


Resolves: #98405
Releases: main
Change-Id: I2e6b68c009652a23d088b27714b925b0e809aaa0
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/75840


Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Susanne Moog's avatarSusanne Moog <look@susi.dev>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
parent 22082c47
......@@ -36,20 +36,6 @@ class UpgradeDocs extends AbstractInteractableModule {
private chosenField: JQuery;
private fulltextSearchField: JQuery;
private static trimExplodeAndUnique(delimiter: string, string: string): Array<string> {
const result: Array<string> = [];
const items = string.split(delimiter);
for (let i = 0; i < items.length; i++) {
const item = items[i].trim();
if (item.length > 0) {
if ($.inArray(item, result) === -1) {
result.push(item);
}
}
}
return result;
}
public initialize(currentModal: JQuery): void {
this.currentModal = currentModal;
const isInIframe = (window.location !== window.parent.location);
......@@ -81,9 +67,7 @@ class UpgradeDocs extends AbstractInteractableModule {
private getContent(): void {
const modalContent = this.getModalBody();
modalContent.on('show.bs.collapse', this.selectorUpgradeDoc, (e: JQueryEventObject): void => {
this.renderTags($(e.currentTarget));
});
(new AjaxRequest(Router.getUrl('upgradeDocsGetContent')))
.get({cache: 'no-cache'})
.then(
......@@ -104,7 +88,7 @@ class UpgradeDocs extends AbstractInteractableModule {
}
private loadChangelogs(): void {
const promises: Array<Promise<any>> = [];
const promises: Array<Promise<AjaxRequest>> = [];
const modalContent = this.getModalBody();
this.findInModal(this.selectorChangeLogsForVersionContainer).each((index: number, el: any): void => {
const request = (new AjaxRequest(Router.getUrl('upgradeDocsGetChangelogForVersion')))
......@@ -153,8 +137,6 @@ class UpgradeDocs extends AbstractInteractableModule {
});
searchInput.focus();
this.initializeChosenSelector();
new DebounceEvent('keyup', (): void => {
this.combinedFilterSearch();
}).bindTo(searchInput);
......@@ -181,59 +163,64 @@ class UpgradeDocs extends AbstractInteractableModule {
}
/**
* Appends tags to the chosen selector
* Appends tags to the chosen selector in multiple steps:
*
* 1. create a flat CSV of tags
* 2. create a Set() with those tags, automatically filtering duplicates
* 3. reduce remaining duplicates due to the case sensitivity behavior of Set(), while keeping the original case of
* the first item of a set of dupes
* 4. sort tags
*/
private appendItemsToChosenSelector(): void {
let tagString = '';
$(this.findInModal(this.selectorUpgradeDoc)).each((index: number, element: any): void => {
tagString += $(element).data('item-tags') + ',';
});
const tagArray = UpgradeDocs.trimExplodeAndUnique(',', tagString).sort((a: string, b: string): number => {
let tagSet = new Set(tagString.slice(0, -1).split(','));
const uniqueTags = [...tagSet.values()].reduce((tagList: string[], tag: string): string[] => {
const normalizedTag = tag.toLowerCase();
if (tagList.every(otherElement => otherElement.toLowerCase() !== normalizedTag)) {
tagList.push(tag);
}
return tagList;
}, []).sort((a: string, b: string): number => {
// Sort case-insensitive by name
return a.toLowerCase().localeCompare(b.toLowerCase());
});
this.chosenField.prop('disabled', false);
$.each(tagArray, (i: number, tag: any): void => {
for (let tag of uniqueTags) {
this.chosenField.append($('<option>').text(tag));
});
}
this.chosenField.trigger('chosen:updated');
}
private combinedFilterSearch(): boolean {
private combinedFilterSearch(): void {
const modalContent = this.getModalBody();
const $items = modalContent.find(this.selectorUpgradeDoc);
if (this.chosenField.val().length < 1 && this.fulltextSearchField.val().length < 1) {
this.currentModal.find('.panel-version .panel-collapse.show').collapse('hide');
$items.removeClass('hidden searchhit filterhit');
return false;
const $expandedPanels = this.currentModal.find('.panel-version .panel-collapse.show');
$expandedPanels.one('hidden.bs.collapse', (): void => {
if (this.currentModal.find('.panel-version .panel-collapse.collapsing').length === 0) {
// Bootstrap doesn't offer promises to check whether all panels are collapsed, so we need a helper to do
// something similar
$items.removeClass('searchhit filterhit');
}
});
$expandedPanels.collapse('hide');
return;
}
$items.addClass('hidden').removeClass('searchhit filterhit');
$items.removeClass('searchhit filterhit');
// apply tags
if (this.chosenField.val().length > 0) {
$items
.addClass('hidden')
.removeClass('filterhit');
const orTags: Array<string> = [];
const andTags: Array<string> = [];
$.each(this.chosenField.val(), (index: number, item: any): void => {
const tagFilter = '[data-item-tags*="' + item + '"]';
if (item.includes(':', 1)) {
orTags.push(tagFilter);
} else {
andTags.push(tagFilter);
}
});
const andString = andTags.join('');
const tags = [];
if (orTags.length) {
for (let orTag of orTags) {
tags.push(andString + orTag);
}
} else {
tags.push(andString);
}
const tagSelection = tags.join(',');
const tagSelection = this.chosenField.val().map((tag: string) => '[data-item-tags*="' + tag + '"]').join('');
modalContent.find(tagSelection)
.removeClass('hidden')
.addClass('searchhit filterhit');
......@@ -253,7 +240,12 @@ class UpgradeDocs extends AbstractInteractableModule {
}
});
modalContent.find('.searchhit').closest('.panel-collapse').collapse('show');
modalContent.find('.searchhit').closest('.panel-collapse').each((index: number, item: Element): void => {
// This is a workaround to improve the browser performance as the panels are not expanded at once
window.setTimeout((): void => {
$(item).collapse('show');
}, 20)
});
// Check for empty panels
modalContent.find('.panel-version').each((index: number, element: any): void => {
......@@ -262,18 +254,6 @@ class UpgradeDocs extends AbstractInteractableModule {
$element.find(' > .panel-collapse').collapse('hide');
}
});
return true;
}
private renderTags($upgradeDocumentContainer: any): void {
const $tagContainer = $upgradeDocumentContainer.find('.t3js-tags');
if ($tagContainer.children().length === 0) {
const tags = $upgradeDocumentContainer.data('item-tags').split(',');
tags.forEach((value: string): void => {
$tagContainer.append($('<span />', {'class': 'badge'}).text(value));
});
}
}
/**
......
......@@ -145,6 +145,7 @@ class DocumentationFile
}
}
$entry['tagList'] = implode(',', $entry['tags']);
$entry['tagArray'] = $entry['tags'];
$entry['content'] = (string)file_get_contents($file);
$entry['parsedContent'] = $this->parseContent($entry['content']);
$entry['file_hash'] = md5($entry['content']);
......
......@@ -27,7 +27,11 @@
</h3>
</div>
<div id="collapse{id}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading{id}">
<div class="rst-tags t3js-tags"></div>
<div class="rst-tags">
<f:for each="{fileArray.tags}" as="tag">
<span class="badge">{tag}</span>
</f:for>
</div>
<div class="rst-links">
<f:if condition="{fileArray.url.issue}">
<a href="{fileArray.url.issue}" target="_blank" rel="noreferrer" class="nowrap"><core:icon identifier="actions-window-open" /> Open issue</a>
......
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
import"bootstrap";import $ from"jquery";import"@typo3/install/renderable/clearable.js";import{AbstractInteractableModule}from"@typo3/install/module/abstract-interactable-module.js";import Notification from"@typo3/backend/notification.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import{topLevelModuleImport}from"@typo3/backend/utility/top-level-module-import.js";import Router from"@typo3/install/router.js";import DebounceEvent from"@typo3/core/event/debounce-event.js";import"@typo3/backend/element/icon-element.js";class UpgradeDocs extends AbstractInteractableModule{constructor(){super(...arguments),this.selectorFulltextSearch=".t3js-upgradeDocs-fulltext-search",this.selectorChosenField=".t3js-upgradeDocs-chosen-select",this.selectorChangeLogsForVersionContainer=".t3js-version-changes",this.selectorChangeLogsForVersion=".t3js-changelog-list",this.selectorUpgradeDoc=".t3js-upgrade-doc"}static trimExplodeAndUnique(e,t){const o=[],s=t.split(e);for(let e=0;e<s.length;e++){const t=s[e].trim();t.length>0&&-1===$.inArray(t,o)&&o.push(t)}return o}initialize(e){this.currentModal=e;window.location!==window.parent.location?topLevelModuleImport("@typo3/install/chosen.jquery.min.js").then((()=>{this.getContent()})):import("@typo3/install/chosen.jquery.min.js").then((()=>{this.getContent()})),e.on("click",".t3js-upgradeDocs-markRead",(e=>{this.markRead(e.target)})),e.on("click",".t3js-upgradeDocs-unmarkRead",(e=>{this.unmarkRead(e.target)})),$.expr[":"].contains=$.expr.createPseudo((e=>t=>$(t).text().toUpperCase().includes(e.toUpperCase())))}getContent(){const e=this.getModalBody();e.on("show.bs.collapse",this.selectorUpgradeDoc,(e=>{this.renderTags($(e.currentTarget))})),new AjaxRequest(Router.getUrl("upgradeDocsGetContent")).get({cache:"no-cache"}).then((async t=>{const o=await t.resolve();!0===o.success&&"undefined"!==o.html&&o.html.length>0&&(e.empty().append(o.html),this.initializeFullTextSearch(),this.initializeChosenSelector(),this.loadChangelogs())}),(t=>{Router.handleAjaxError(t,e)}))}loadChangelogs(){const e=[],t=this.getModalBody();this.findInModal(this.selectorChangeLogsForVersionContainer).each(((o,s)=>{const a=new AjaxRequest(Router.getUrl("upgradeDocsGetChangelogForVersion")).withQueryArguments({install:{version:s.dataset.version}}).get({cache:"no-cache"}).then((async e=>{const t=await e.resolve();if(!0===t.success){const e=$(s),o=e.find(this.selectorChangeLogsForVersion);o.html(t.html),this.moveNotRelevantDocuments(o),e.find(".t3js-panel-loading").remove()}else Notification.error("Something went wrong","The request was not processed successfully. Please check the browser's console and TYPO3's log.")}),(e=>{Router.handleAjaxError(e,t)}));e.push(a)})),Promise.all(e).then((()=>{this.fulltextSearchField.prop("disabled",!1),this.appendItemsToChosenSelector()}))}initializeFullTextSearch(){this.fulltextSearchField=this.findInModal(this.selectorFulltextSearch);const e=this.fulltextSearchField.get(0);e.clearable({onClear:()=>{this.combinedFilterSearch()}}),e.focus(),this.initializeChosenSelector(),new DebounceEvent("keyup",(()=>{this.combinedFilterSearch()})).bindTo(e)}initializeChosenSelector(){this.chosenField=this.getModalBody().find(this.selectorChosenField);const e={".chosen-select":{width:"100%",placeholder_text_multiple:"tags"},".chosen-select-deselect":{allow_single_deselect:!0},".chosen-select-no-single":{disable_search_threshold:10},".chosen-select-no-results":{no_results_text:"Oops, nothing found!"},".chosen-select-width":{width:"100%"}};for(const t in e)e.hasOwnProperty(t)&&this.findInModal(t).chosen(e[t]);this.chosenField.on("change",(()=>{this.combinedFilterSearch()}))}appendItemsToChosenSelector(){let e="";$(this.findInModal(this.selectorUpgradeDoc)).each(((t,o)=>{e+=$(o).data("item-tags")+","}));const t=UpgradeDocs.trimExplodeAndUnique(",",e).sort(((e,t)=>e.toLowerCase().localeCompare(t.toLowerCase())));this.chosenField.prop("disabled",!1),$.each(t,((e,t)=>{this.chosenField.append($("<option>").text(t))})),this.chosenField.trigger("chosen:updated")}combinedFilterSearch(){const e=this.getModalBody(),t=e.find(this.selectorUpgradeDoc);if(this.chosenField.val().length<1&&this.fulltextSearchField.val().length<1)return this.currentModal.find(".panel-version .panel-collapse.show").collapse("hide"),t.removeClass("hidden searchhit filterhit"),!1;if(t.addClass("hidden").removeClass("searchhit filterhit"),this.chosenField.val().length>0){t.addClass("hidden").removeClass("filterhit");const o=[],s=[];$.each(this.chosenField.val(),((e,t)=>{const a='[data-item-tags*="'+t+'"]';t.includes(":",1)?o.push(a):s.push(a)}));const a=s.join(""),n=[];if(o.length)for(let e of o)n.push(a+e);else n.push(a);const i=n.join(",");e.find(i).removeClass("hidden").addClass("searchhit filterhit")}else t.addClass("filterhit").removeClass("hidden");const o=this.fulltextSearchField.val();return e.find(".filterhit").each(((e,t)=>{const s=$(t);$(":contains("+o+")",s).length>0||$('input[value*="'+o+'"]',s).length>0?s.removeClass("hidden").addClass("searchhit"):s.removeClass("searchhit").addClass("hidden")})),e.find(".searchhit").closest(".panel-collapse").collapse("show"),e.find(".panel-version").each(((e,t)=>{const o=$(t);o.find(".searchhit",".filterhit").length<1&&o.find(" > .panel-collapse").collapse("hide")})),!0}renderTags(e){const t=e.find(".t3js-tags");if(0===t.children().length){e.data("item-tags").split(",").forEach((e=>{t.append($("<span />",{class:"badge"}).text(e))}))}}moveNotRelevantDocuments(e){e.find('[data-item-state="read"]').appendTo(this.findInModal(".panel-body-read")),e.find('[data-item-state="notAffected"]').appendTo(this.findInModal(".panel-body-not-affected"))}markRead(e){const t=this.getModalBody(),o=this.getModuleContent().data("upgrade-docs-mark-read-token"),s=$(e).closest("button");s.toggleClass("t3js-upgradeDocs-unmarkRead t3js-upgradeDocs-markRead"),s.find("typo3-backend-icon,.t3js-icon").replaceWith('<typo3-backend-icon identifier="actions-ban" size="small"></typo3-backend-icon>'),s.closest(".panel").appendTo(this.findInModal(".panel-body-read")),new AjaxRequest(Router.getUrl()).post({install:{ignoreFile:s.data("filepath"),token:o,action:"upgradeDocsMarkRead"}}).catch((e=>{Router.handleAjaxError(e,t)}))}unmarkRead(e){const t=this.getModalBody(),o=this.getModuleContent().data("upgrade-docs-unmark-read-token"),s=$(e).closest("button"),a=s.closest(".panel").data("item-version");s.toggleClass("t3js-upgradeDocs-markRead t3js-upgradeDocs-unmarkRead"),s.find("typo3-backend-icon,.t3js-icon").replaceWith('<typo3-backend-icon identifier="actions-check" size="small"></typo3-backend-icon>'),s.closest(".panel").appendTo(this.findInModal('*[data-group-version="'+a+'"] .panel-body')),new AjaxRequest(Router.getUrl()).post({install:{ignoreFile:s.data("filepath"),token:o,action:"upgradeDocsUnmarkRead"}}).catch((e=>{Router.handleAjaxError(e,t)}))}}export default new UpgradeDocs;
\ No newline at end of file
import"bootstrap";import $ from"jquery";import"@typo3/install/renderable/clearable.js";import{AbstractInteractableModule}from"@typo3/install/module/abstract-interactable-module.js";import Notification from"@typo3/backend/notification.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import{topLevelModuleImport}from"@typo3/backend/utility/top-level-module-import.js";import Router from"@typo3/install/router.js";import DebounceEvent from"@typo3/core/event/debounce-event.js";import"@typo3/backend/element/icon-element.js";class UpgradeDocs extends AbstractInteractableModule{constructor(){super(...arguments),this.selectorFulltextSearch=".t3js-upgradeDocs-fulltext-search",this.selectorChosenField=".t3js-upgradeDocs-chosen-select",this.selectorChangeLogsForVersionContainer=".t3js-version-changes",this.selectorChangeLogsForVersion=".t3js-changelog-list",this.selectorUpgradeDoc=".t3js-upgrade-doc"}initialize(e){this.currentModal=e;window.location!==window.parent.location?topLevelModuleImport("@typo3/install/chosen.jquery.min.js").then((()=>{this.getContent()})):import("@typo3/install/chosen.jquery.min.js").then((()=>{this.getContent()})),e.on("click",".t3js-upgradeDocs-markRead",(e=>{this.markRead(e.target)})),e.on("click",".t3js-upgradeDocs-unmarkRead",(e=>{this.unmarkRead(e.target)})),$.expr[":"].contains=$.expr.createPseudo((e=>t=>$(t).text().toUpperCase().includes(e.toUpperCase())))}getContent(){const e=this.getModalBody();new AjaxRequest(Router.getUrl("upgradeDocsGetContent")).get({cache:"no-cache"}).then((async t=>{const o=await t.resolve();!0===o.success&&"undefined"!==o.html&&o.html.length>0&&(e.empty().append(o.html),this.initializeFullTextSearch(),this.initializeChosenSelector(),this.loadChangelogs())}),(t=>{Router.handleAjaxError(t,e)}))}loadChangelogs(){const e=[],t=this.getModalBody();this.findInModal(this.selectorChangeLogsForVersionContainer).each(((o,s)=>{const a=new AjaxRequest(Router.getUrl("upgradeDocsGetChangelogForVersion")).withQueryArguments({install:{version:s.dataset.version}}).get({cache:"no-cache"}).then((async e=>{const t=await e.resolve();if(!0===t.success){const e=$(s),o=e.find(this.selectorChangeLogsForVersion);o.html(t.html),this.moveNotRelevantDocuments(o),e.find(".t3js-panel-loading").remove()}else Notification.error("Something went wrong","The request was not processed successfully. Please check the browser's console and TYPO3's log.")}),(e=>{Router.handleAjaxError(e,t)}));e.push(a)})),Promise.all(e).then((()=>{this.fulltextSearchField.prop("disabled",!1),this.appendItemsToChosenSelector()}))}initializeFullTextSearch(){this.fulltextSearchField=this.findInModal(this.selectorFulltextSearch);const e=this.fulltextSearchField.get(0);e.clearable({onClear:()=>{this.combinedFilterSearch()}}),e.focus(),new DebounceEvent("keyup",(()=>{this.combinedFilterSearch()})).bindTo(e)}initializeChosenSelector(){this.chosenField=this.getModalBody().find(this.selectorChosenField);const e={".chosen-select":{width:"100%",placeholder_text_multiple:"tags"},".chosen-select-deselect":{allow_single_deselect:!0},".chosen-select-no-single":{disable_search_threshold:10},".chosen-select-no-results":{no_results_text:"Oops, nothing found!"},".chosen-select-width":{width:"100%"}};for(const t in e)e.hasOwnProperty(t)&&this.findInModal(t).chosen(e[t]);this.chosenField.on("change",(()=>{this.combinedFilterSearch()}))}appendItemsToChosenSelector(){let e="";$(this.findInModal(this.selectorUpgradeDoc)).each(((t,o)=>{e+=$(o).data("item-tags")+","}));const t=[...new Set(e.slice(0,-1).split(",")).values()].reduce(((e,t)=>{const o=t.toLowerCase();return e.every((e=>e.toLowerCase()!==o))&&e.push(t),e}),[]).sort(((e,t)=>e.toLowerCase().localeCompare(t.toLowerCase())));this.chosenField.prop("disabled",!1);for(let e of t)this.chosenField.append($("<option>").text(e));this.chosenField.trigger("chosen:updated")}combinedFilterSearch(){const e=this.getModalBody(),t=e.find(this.selectorUpgradeDoc);if(this.chosenField.val().length<1&&this.fulltextSearchField.val().length<1){const e=this.currentModal.find(".panel-version .panel-collapse.show");return e.one("hidden.bs.collapse",(()=>{0===this.currentModal.find(".panel-version .panel-collapse.collapsing").length&&t.removeClass("searchhit filterhit")})),void e.collapse("hide")}if(t.removeClass("searchhit filterhit"),this.chosenField.val().length>0){t.addClass("hidden").removeClass("filterhit");const o=this.chosenField.val().map((e=>'[data-item-tags*="'+e+'"]')).join("");e.find(o).removeClass("hidden").addClass("searchhit filterhit")}else t.addClass("filterhit").removeClass("hidden");const o=this.fulltextSearchField.val();e.find(".filterhit").each(((e,t)=>{const s=$(t);$(":contains("+o+")",s).length>0||$('input[value*="'+o+'"]',s).length>0?s.removeClass("hidden").addClass("searchhit"):s.removeClass("searchhit").addClass("hidden")})),e.find(".searchhit").closest(".panel-collapse").each(((e,t)=>{window.setTimeout((()=>{$(t).collapse("show")}),20)})),e.find(".panel-version").each(((e,t)=>{const o=$(t);o.find(".searchhit",".filterhit").length<1&&o.find(" > .panel-collapse").collapse("hide")}))}moveNotRelevantDocuments(e){e.find('[data-item-state="read"]').appendTo(this.findInModal(".panel-body-read")),e.find('[data-item-state="notAffected"]').appendTo(this.findInModal(".panel-body-not-affected"))}markRead(e){const t=this.getModalBody(),o=this.getModuleContent().data("upgrade-docs-mark-read-token"),s=$(e).closest("button");s.toggleClass("t3js-upgradeDocs-unmarkRead t3js-upgradeDocs-markRead"),s.find("typo3-backend-icon,.t3js-icon").replaceWith('<typo3-backend-icon identifier="actions-ban" size="small"></typo3-backend-icon>'),s.closest(".panel").appendTo(this.findInModal(".panel-body-read")),new AjaxRequest(Router.getUrl()).post({install:{ignoreFile:s.data("filepath"),token:o,action:"upgradeDocsMarkRead"}}).catch((e=>{Router.handleAjaxError(e,t)}))}unmarkRead(e){const t=this.getModalBody(),o=this.getModuleContent().data("upgrade-docs-unmark-read-token"),s=$(e).closest("button"),a=s.closest(".panel").data("item-version");s.toggleClass("t3js-upgradeDocs-markRead t3js-upgradeDocs-unmarkRead"),s.find("typo3-backend-icon,.t3js-icon").replaceWith('<typo3-backend-icon identifier="actions-check" size="small"></typo3-backend-icon>'),s.closest(".panel").appendTo(this.findInModal('*[data-group-version="'+a+'"] .panel-body')),new AjaxRequest(Router.getUrl()).post({install:{ignoreFile:s.data("filepath"),token:o,action:"upgradeDocsUnmarkRead"}}).catch((e=>{Router.handleAjaxError(e,t)}))}}export default new UpgradeDocs;
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment