Commit 2f2cfff8 authored by Andreas Fernandez's avatar Andreas Fernandez Committed by Susanne Moog
Browse files

[BUGFIX] Disable trigger buttons in Install Tool when actions are executed

If an action in the Install Tool is executed that is related to an
inline module or an interactable module (a.k.a "modal"), its trigger
button(s) get now properly disabled and enabled to avoid executing the
same actions consecutively while any request is still pending.

Resolves: #91076
Releases: master
Change-Id: I9a61063819f21a33ac8ede644fa8f998212b342b
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/64207


Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Jonas Eberle's avatarJonas Eberle <flightvision@googlemail.com>
Tested-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Susanne Moog's avatarSusanne Moog <look@susi.dev>
Reviewed-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Susanne Moog's avatarSusanne Moog <look@susi.dev>
parent 534e7cf7
......@@ -10,4 +10,11 @@
*
* The TYPO3 project - inspiring people to share!
*/
define(["require","exports"],(function(e,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0})}));
\ No newline at end of file
export abstract class AbstractInlineModule {
protected setButtonState(button: JQuery, interactable: boolean): void {
button.toggleClass('disabled', !interactable).prop('disabled', !interactable);
}
abstract initialize($trigger: JQuery): void;
}
......@@ -33,5 +33,15 @@ export abstract class AbstractInteractableModule {
return this.currentModal.find(selector);
}
protected setModalButtonsState(interactable: boolean): void {
this.getModalFooter().find('button').each((_: number, elem: Element): void => {
this.setModalButtonState($(elem), interactable)
});
}
protected setModalButtonState(button: JQuery, interactable: boolean): void {
button.toggleClass('disabled', !interactable).prop('disabled', !interactable);
}
public abstract initialize(currentModal: JQuery): void;
}
......@@ -45,12 +45,13 @@ class EnvironmentCheck extends AbstractInteractableModule {
}
private runTests(): void {
this.setModalButtonsState(false);
const modalContent = this.getModalBody();
const $errorBadge = $(this.selectorGridderBadge);
$errorBadge.text('').hide();
const message = ProgressBar.render(Severity.loading, 'Loading...', '');
modalContent.find(this.selectorOutputContainer).empty().append(message);
this.findInModal(this.selectorExecuteTrigger).addClass('disabled').prop('disabled', true);
(new AjaxRequest(Router.getUrl('environmentCheckGetStatus')))
.get({cache: 'no-cache'})
......
......@@ -49,7 +49,6 @@ class FolderStructure extends AbstractInteractableModule {
currentModal.on('click', this.selectorErrorFixTrigger, (e: JQueryEventObject): void => {
e.preventDefault();
$(e.currentTarget).addClass('disabled').prop('disabled', true);
this.fix();
});
}
......@@ -111,6 +110,8 @@ class FolderStructure extends AbstractInteractableModule {
}
private fix(): void {
this.setModalButtonsState(false);
const modalContent: JQuery = this.getModalBody();
const $outputContainer: JQuery = this.findInModal(this.selectorOutputContainer);
const message: any = ProgressBar.render(Severity.loading, 'Loading...', '');
......
......@@ -68,7 +68,7 @@ class ImageProcessing extends AbstractInteractableModule {
private runTests(): void {
const modalContent = this.getModalBody();
const $triggerButton = this.findInModal(this.selectorExecuteTrigger);
$triggerButton.addClass('disabled').prop('disabled', true);
this.setModalButtonsState(false);
const $twinImageTemplate = this.findInModal(this.selectorTwinImageTemplate);
const promises: Array<Promise<any>> = [];
......
......@@ -59,41 +59,43 @@ class MailTest extends AbstractInteractableModule {
},
(error: ResponseError): void => {
Router.handleAjaxError(error, modalContent);
}
},
);
}
private send(): void {
this.setModalButtonsState(false);
const executeToken: string = this.getModuleContent().data('mail-test-token');
const $outputContainer: JQuery = this.findInModal(this.selectorOutputContainer);
const message: any = ProgressBar.render(Severity.loading, 'Loading...', '');
$outputContainer.empty().html(message);
(new AjaxRequest(Router.getUrl()))
.post({
install: {
action: 'mailTest',
token: executeToken,
email: this.findInModal('.t3js-mailTest-email').val(),
},
})
.then(
async (response: AjaxResponse): Promise<any> => {
const data = await response.resolve();
$outputContainer.empty();
if (Array.isArray(data.status)) {
data.status.forEach((element: any): void => {
const aMessage: any = InfoBox.render(element.severity, element.title, element.message);
$outputContainer.html(aMessage);
});
} else {
Notification.error('Something went wrong');
}
},
(): void => {
// 500 can happen here if the mail configuration is broken
(new AjaxRequest(Router.getUrl())).post({
install: {
action: 'mailTest',
token: executeToken,
email: this.findInModal('.t3js-mailTest-email').val(),
},
}).then(
async (response: AjaxResponse): Promise<any> => {
const data = await response.resolve();
$outputContainer.empty();
if (Array.isArray(data.status)) {
data.status.forEach((element: any): void => {
const aMessage: any = InfoBox.render(element.severity, element.title, element.message);
$outputContainer.html(aMessage);
});
} else {
Notification.error('Something went wrong');
}
);
},
(): void => {
// 500 can happen here if the mail configuration is broken
Notification.error('Something went wrong');
},
).finally((): void => {
this.setModalButtonsState(true);
});
}
}
......
......@@ -15,14 +15,14 @@ import Notification = require('TYPO3/CMS/Backend/Notification');
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
import Router = require('../../Router');
import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import {InlineModuleInterface} from './../InlineModuleInterface';
import {AbstractInlineModule} from '../AbstractInlineModule';
/**
* Module: TYPO3/CMS/Install/Module/Cache
*/
class Cache implements InlineModuleInterface {
class Cache extends AbstractInlineModule {
public initialize($trigger: JQuery): void {
$trigger.addClass('disabled').prop('disabled', true);
this.setButtonState($trigger, false);
(new AjaxRequest(Router.getUrl('cacheClearAll', 'maintenance')))
.get({cache: 'no-cache'})
......@@ -45,10 +45,10 @@ class Cache implements InlineModuleInterface {
Notification.error(
'Clearing caches went wrong on the server side. Check the system for broken extensions or missing database tables and try again',
);
}
},
)
.finally((): void => {
$trigger.removeClass('disabled').prop('disabled', false);
this.setButtonState($trigger, true);
});
}
}
......
......@@ -51,6 +51,8 @@ class ClearTables extends AbstractInteractableModule {
}
private getStats(): void {
this.setModalButtonsState(false);
const modalContent: JQuery = this.getModalBody();
(new AjaxRequest(Router.getUrl('clearTablesStats')))
.get({cache: 'no-cache'})
......
......@@ -50,6 +50,8 @@ class ClearTypo3tempFiles extends AbstractInteractableModule {
}
private getStats(): void {
this.setModalButtonsState(false);
const modalContent = this.getModalBody();
(new AjaxRequest(Router.getUrl('clearTypo3tempFilesStats')))
.get({cache: 'no-cache'})
......
......@@ -56,49 +56,53 @@ class CreateAdmin extends AbstractInteractableModule {
},
(error: ResponseError): void => {
Router.handleAjaxError(error, modalContent);
}
},
);
}
private create(): void {
this.setModalButtonsState(false);
const modalContent = this.getModalBody();
const executeToken = this.getModuleContent().data('create-admin-token');
(new AjaxRequest(Router.getUrl()))
.post({
install: {
action: 'createAdmin',
token: executeToken,
userName: this.findInModal('.t3js-createAdmin-user').val(),
userPassword: this.findInModal('.t3js-createAdmin-password').val(),
userPasswordCheck: this.findInModal('.t3js-createAdmin-password-check').val(),
userEmail: this.findInModal('.t3js-createAdmin-email').val(),
userSystemMaintainer: (this.findInModal('.t3js-createAdmin-system-maintainer').is(':checked')) ? 1 : 0,
},
})
.then(
async (response: AjaxResponse): Promise<any> => {
const data = await response.resolve();
if (data.success === true && Array.isArray(data.status)) {
data.status.forEach((element: any): void => {
if (element.severity === 2) {
Notification.error(element.message);
} else {
Notification.success(element.title);
}
});
const payload = {
install: {
action: 'createAdmin',
token: executeToken,
userName: this.findInModal('.t3js-createAdmin-user').val(),
userPassword: this.findInModal('.t3js-createAdmin-password').val(),
userPasswordCheck: this.findInModal('.t3js-createAdmin-password-check').val(),
userEmail: this.findInModal('.t3js-createAdmin-email').val(),
userSystemMaintainer: (this.findInModal('.t3js-createAdmin-system-maintainer').is(':checked')) ? 1 : 0,
},
};
this.getModuleContent().find(':input').prop('disabled', true);
(new AjaxRequest(Router.getUrl())).post(payload).then(async (response: AjaxResponse): Promise<any> => {
const data = await response.resolve();
if (data.success === true && Array.isArray(data.status)) {
data.status.forEach((element: any): void => {
if (element.severity === 2) {
Notification.error(element.message);
} else {
Notification.error('Something went wrong');
Notification.success(element.title);
}
},
(error: ResponseError): void => {
Router.handleAjaxError(error, modalContent);
}
);
this.findInModal('.t3js-createAdmin-user').val('');
this.findInModal('.t3js-createAdmin-password').val('');
this.findInModal('.t3js-createAdmin-password-check').val('');
this.findInModal('.t3js-createAdmin-email').val('');
this.findInModal('.t3js-createAdmin-system-maintainer').prop('checked', false);
});
} else {
Notification.error('Something went wrong');
}
}, (error: ResponseError): void => {
Router.handleAjaxError(error, modalContent);
}).finally((): void => {
this.setModalButtonsState(true);
this.getModuleContent().find(':input').prop('disabled', false);
this.findInModal('.t3js-createAdmin-user').val('');
this.findInModal('.t3js-createAdmin-password').val('');
this.findInModal('.t3js-createAdmin-password-check').val('');
this.findInModal('.t3js-createAdmin-email').val('');
this.findInModal('.t3js-createAdmin-system-maintainer').prop('checked', false);
});
}
}
......
......@@ -75,6 +75,8 @@ class DatabaseAnalyzer extends AbstractInteractableModule {
}
private analyze(): void {
this.setModalButtonsState(false);
const modalContent = this.getModalBody();
const modalFooter = this.getModalFooter();
const outputContainer = modalContent.find(this.selectorOutputContainer);
......@@ -82,13 +84,9 @@ class DatabaseAnalyzer extends AbstractInteractableModule {
const analyzeTrigger = modalFooter.find(this.selectorAnalyzeTrigger);
outputContainer.empty().append(ProgressBar.render(Severity.loading, 'Analyzing current database schema...', ''));
analyzeTrigger.prop('disabled', true);
executeTrigger.prop('disabled', true);
outputContainer.on('change', 'input[type="checkbox"]', (): void => {
const hasCheckedCheckboxes = outputContainer.find(':checked').length > 0;
executeTrigger.prop('disabled', !hasCheckedCheckboxes);
this.setModalButtonState(executeTrigger, hasCheckedCheckboxes);
});
(new AjaxRequest(Router.getUrl('databaseAnalyzerAnalyze')))
......@@ -138,9 +136,8 @@ class DatabaseAnalyzer extends AbstractInteractableModule {
outputContainer.append(aBlock.html());
});
const isInitiallyDisabled = outputContainer.find(':checked').length === 0;
analyzeTrigger.prop('disabled', false);
executeTrigger.prop('disabled', isInitiallyDisabled);
this.setModalButtonState(analyzeTrigger, true);
this.setModalButtonState(executeTrigger, outputContainer.find(':checked').length > 0);
}
if (data.suggestions.length === 0 && data.status.length === 0) {
outputContainer.append(InfoBox.render(Severity.ok, 'Database schema is up to date. Good job!', ''));
......@@ -156,18 +153,17 @@ class DatabaseAnalyzer extends AbstractInteractableModule {
}
private execute(): void {
this.setModalButtonsState(false);
const modalContent = this.getModalBody();
const modalFooter = this.getModalFooter();
const executeToken = this.getModuleContent().data('database-analyzer-execute-token');
const outputContainer = modalContent.find(this.selectorOutputContainer);
const selectedHashes: Array<any> = [];
const selectedHashes: Array<any> = [];
outputContainer.find('.t3js-databaseAnalyzer-suggestion-line input:checked').each((index: number, element: any): void => {
selectedHashes.push($(element).data('hash'));
});
outputContainer.empty().append(ProgressBar.render(Severity.loading, 'Executing database updates...', ''));
modalFooter.find(this.selectorExecuteTrigger).prop('disabled', true);
modalFooter.find(this.selectorAnalyzeTrigger).prop('disabled', true);
(new AjaxRequest(Router.getUrl()))
.post({
......
......@@ -15,14 +15,14 @@ import Notification = require('TYPO3/CMS/Backend/Notification');
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
import Router = require('../../Router');
import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import {InlineModuleInterface} from './../InlineModuleInterface';
import {AbstractInlineModule} from '../AbstractInlineModule';
/**
* Module: TYPO3/CMS/Install/Module/DumpAutoload
*/
class DumpAutoload implements InlineModuleInterface {
class DumpAutoload extends AbstractInlineModule {
public initialize($trigger: JQuery): void {
$trigger.addClass('disabled').prop('disabled', true);
this.setButtonState($trigger, false);
(new AjaxRequest(Router.getUrl('dumpAutoload')))
.get({cache: 'no-cache'})
......@@ -46,7 +46,7 @@ class DumpAutoload implements InlineModuleInterface {
}
)
.finally((): void => {
$trigger.removeClass('disabled').prop('disabled', false);
this.setButtonState($trigger, true);
});
}
}
......
......@@ -13,16 +13,16 @@
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
import {AjaxResponse} from 'TYPO3/CMS/Core/Ajax/AjaxResponse';
import {InlineModuleInterface} from './../InlineModuleInterface';
import {AbstractInlineModule} from '../AbstractInlineModule';
import Notification = require('TYPO3/CMS/Backend/Notification');
import Router = require('../../Router');
/**
* Module: TYPO3/CMS/Install/Module/ResetBackendUserUc
*/
class ResetBackendUserUc implements InlineModuleInterface {
class ResetBackendUserUc extends AbstractInlineModule {
public initialize($trigger: JQuery): void {
$trigger.addClass('disabled').prop('disabled', true);
this.setButtonState($trigger, false);
(new AjaxRequest(Router.getUrl('resetBackendUserUc')))
.get({cache: 'no-cache'})
......@@ -46,7 +46,7 @@ class ResetBackendUserUc implements InlineModuleInterface {
}
)
.finally((): void => {
$trigger.removeClass('disabled').prop('disabled', false);
this.setButtonState($trigger, true);
});
}
}
......
......@@ -60,35 +60,32 @@ class ChangeInstallToolPassword extends AbstractInteractableModule {
}
private change(): void {
this.setModalButtonsState(false);
const modalContent = this.getModalBody();
const executeToken = this.getModuleContent().data('install-tool-token');
(new AjaxRequest(Router.getUrl()))
.post({
install: {
action: 'changeInstallToolPassword',
token: executeToken,
password: this.findInModal('.t3js-changeInstallToolPassword-password').val(),
passwordCheck: this.findInModal('.t3js-changeInstallToolPassword-password-check').val(),
},
})
.then(
async (response: AjaxResponse): Promise<any> => {
const data = await response.resolve();
if (data.success === true && Array.isArray(data.status)) {
data.status.forEach((element: any): void => {
Notification.showMessage('', element.message, element.severity);
});
} else {
Notification.error('Something went wrong');
}
},
(error: ResponseError): void => {
Router.handleAjaxError(error, modalContent);
}
)
.finally((): void => {
this.findInModal('.t3js-changeInstallToolPassword-password,.t3js-changeInstallToolPassword-password-check').val('');
});
(new AjaxRequest(Router.getUrl())).post({
install: {
action: 'changeInstallToolPassword',
token: executeToken,
password: this.findInModal('.t3js-changeInstallToolPassword-password').val(),
passwordCheck: this.findInModal('.t3js-changeInstallToolPassword-password-check').val(),
},
}).then(async (response: AjaxResponse): Promise<any> => {
const data = await response.resolve();
if (data.success === true && Array.isArray(data.status)) {
data.status.forEach((element: any): void => {
Notification.showMessage('', element.message, element.severity);
});
} else {
Notification.error('Something went wrong');
}
}, (error: ResponseError): void => {
Router.handleAjaxError(error, modalContent);
}).finally((): void => {
this.findInModal('.t3js-changeInstallToolPassword-password,.t3js-changeInstallToolPassword-password-check').val('');
this.setModalButtonsState(true);
});
}
}
......
......@@ -57,6 +57,8 @@ class Features extends AbstractInteractableModule {
}
private save(): void {
this.setModalButtonsState(false);
const modalContent = this.getModalBody();
const executeToken = this.getModuleContent().data('features-save-token');
const postData: any = {};
......
......@@ -114,6 +114,8 @@ class LocalConfiguration extends AbstractInteractableModule {
}
private write(): void {
this.setModalButtonsState(false);
const modalContent: JQuery = this.getModalBody();
const executeToken: JQuery = this.getModuleContent().data('local-configuration-write-token');
const configurationValues: any = {};
......@@ -129,29 +131,26 @@ class LocalConfiguration extends AbstractInteractableModule {
configurationValues[$element.data('path')] = $element.val();
}
});
(new AjaxRequest(Router.getUrl()))
.post({
install: {
action: 'localConfigurationWrite',
token: executeToken,
configurationValues: configurationValues,
},
})
.then(
async (response: AjaxResponse): Promise<any> => {
const data = await response.resolve();
if (data.success === true && Array.isArray(data.status)) {
data.status.forEach((element: any): void => {
Notification.showMessage(element.title, element.message, element.severity);
});
} else {
Notification.error('Something went wrong');
}
},
(error: ResponseError): void => {
Router.handleAjaxError(error, modalContent);
}
);
(new AjaxRequest(Router.getUrl())).post({
install: {
action: 'localConfigurationWrite',
token: executeToken,
configurationValues: configurationValues,
},
}).then(async (response: AjaxResponse): Promise<any> => {
const data = await response.resolve();
if (data.success === true && Array.isArray(data.status)) {
data.status.forEach((element: any): void => {
Notification.showMessage(element.title, element.message, element.severity);
});
} else {
Notification.error('Something went wrong');
}
}, (error: ResponseError): void => {
Router.handleAjaxError(error, modalContent);
}).finally((): void => {
this.setModalButtonsState(true);
});
}
}
......
......@@ -67,7 +67,7 @@ class Presets extends AbstractInteractableModule {
},
(error: ResponseError): void => {