From 90b525d69d6979b94364b31596beb59f95a6bdca Mon Sep 17 00:00:00 2001
From: Benjamin Kott <benjamin.kott@outlook.com>
Date: Tue, 14 Mar 2023 14:25:34 +0100
Subject: [PATCH] [BUGFIX] Enable sorting for all task groups in scheduler

Releases: main
Resolves: #100163
Change-Id: Id4f8d3f871e6b2c2ae02fb6ee6ed114bc5d3da01
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/78118
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 Build/Gruntfile.js                            | 106 ++++++++++--------
 .../TypeScript/backend/sortable-table.ts      |  22 ++++
 .../TypeScript/extensionmanager/main.ts       |   8 +-
 .../TypeScript/extensionmanager/repository.ts |  10 +-
 .../Sources/TypeScript/scheduler/scheduler.ts |   9 +-
 Build/types/tablesort.d.ts                    |   1 +
 .../Public/JavaScript/sortable-table.js       |  13 +++
 .../core/Configuration/JavaScriptModules.php  |   1 +
 .../JavaScript/Contrib/tablesort.number.js    |  11 ++
 .../Application/Scheduler/TasksCest.php       |   2 +-
 .../Resources/Public/JavaScript/main.js       |   2 +-
 .../Resources/Public/JavaScript/repository.js |   2 +-
 .../Resources/Private/Partials/TaskList.html  |   4 +-
 .../Resources/Public/JavaScript/scheduler.js  |   2 +-
 14 files changed, 126 insertions(+), 67 deletions(-)
 create mode 100644 Build/Sources/TypeScript/backend/sortable-table.ts
 create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/sortable-table.js
 create mode 100644 typo3/sysext/core/Resources/Public/JavaScript/Contrib/tablesort.number.js

diff --git a/Build/Gruntfile.js b/Build/Gruntfile.js
index 0537c05f5c44..f40f443ea677 100644
--- a/Build/Gruntfile.js
+++ b/Build/Gruntfile.js
@@ -1,3 +1,6 @@
+/* eslint-env node, commonjs */
+/* eslint-disable @typescript-eslint/no-var-requires */
+
 /*
  * This file is part of the TYPO3 CMS project.
  *
@@ -20,8 +23,7 @@ module.exports = function (grunt) {
    * Grunt stylefmt task
    */
   grunt.registerMultiTask('formatsass', 'Grunt task for stylefmt', function () {
-    var options = this.options(),
-      done = this.async(),
+    let done = this.async(),
       stylefmt = require('@ronilaukkarinen/stylefmt'),
       postcss = require('postcss'),
       scss = require('postcss-scss'),
@@ -31,8 +33,8 @@ module.exports = function (grunt) {
       counter = 0;
     this.files.forEach(function (file) {
       file.src.filter(function (filepath) {
-        var content = grunt.file.read(filepath);
-        var settings = {
+        let content = grunt.file.read(filepath);
+        let settings = {
           from: filepath,
           syntax: scss
         };
@@ -40,7 +42,9 @@ module.exports = function (grunt) {
           grunt.file.write(file.dest, result.css);
           grunt.log.success('Source file "' + filepath + '" was processed.');
           counter++;
-          if (counter >= files.length) done(true);
+          if (counter >= files.length) {
+            done(true);
+          }
         });
       });
     });
@@ -50,8 +54,7 @@ module.exports = function (grunt) {
    * Grunt flag tasks
    */
   grunt.registerMultiTask('flags', 'Grunt task rendering the flags', function () {
-    var options = this.options(),
-      done = this.async(),
+    let done = this.async(),
       path = require('path'),
       sharp = require('sharp'),
       filesize = require('filesize'),
@@ -76,7 +79,9 @@ module.exports = function (grunt) {
           .then(data => {
             grunt.log.ok(`File ${targetFilename} created. ${filesize.filesize(data.size)}`)
             counter++;
-            if (counter >= files.length) done(true);
+            if (counter >= files.length) {
+              done(true);
+            }
           }).catch(function (err) {
             grunt.log.error('File "' + targetFilename + '" was not processed.');
             console.log(err)
@@ -165,37 +170,37 @@ module.exports = function (grunt) {
       },
       backend: {
         files: {
-          "<%= paths.backend %>Public/Css/backend.css": "<%= paths.sass %>backend.scss"
+          '<%= paths.backend %>Public/Css/backend.css': '<%= paths.sass %>backend.scss'
         }
       },
       form: {
         files: {
-          "<%= paths.form %>Public/Css/form.css": "<%= paths.sass %>form.scss"
+          '<%= paths.form %>Public/Css/form.css': '<%= paths.sass %>form.scss'
         }
       },
       dashboard: {
         files: {
-          "<%= paths.dashboard %>Public/Css/dashboard.css": "<%= paths.sass %>dashboard.scss"
+          '<%= paths.dashboard %>Public/Css/dashboard.css': '<%= paths.sass %>dashboard.scss'
         }
       },
       dashboard_modal: {
         files: {
-          "<%= paths.dashboard %>Public/Css/Modal/style.css": "<%= paths.sass %>dashboard_modal.scss"
+          '<%= paths.dashboard %>Public/Css/Modal/style.css': '<%= paths.sass %>dashboard_modal.scss'
         }
       },
       adminpanel: {
         files: {
-          "<%= paths.adminpanel %>Public/Css/adminpanel.css": "<%= paths.sass %>adminpanel.scss"
+          '<%= paths.adminpanel %>Public/Css/adminpanel.css': '<%= paths.sass %>adminpanel.scss'
         }
       },
       webfonts: {
         files: {
-          "<%= paths.backend %>Public/Css/webfonts.css": "<%= paths.sass %>webfonts.scss"
+          '<%= paths.backend %>Public/Css/webfonts.css': '<%= paths.sass %>webfonts.scss'
         }
       },
       workspaces: {
         files: {
-          "<%= paths.workspaces %>Public/Css/preview.css": "<%= paths.sass %>workspace.scss"
+          '<%= paths.workspaces %>Public/Css/preview.css': '<%= paths.sass %>workspace.scss'
         }
       }
     },
@@ -295,7 +300,7 @@ module.exports = function (grunt) {
       ts_files: {
         options: {
           process: (source, srcpath) => {
-            const [imports, exports] = esModuleLexer.parse(source, srcpath);
+            const [imports] = esModuleLexer.parse(source, srcpath);
 
             source = require('./util/map-import.js').mapImports(source, srcpath, imports);
 
@@ -395,7 +400,7 @@ module.exports = function (grunt) {
       },
       lit: {
         options: {
-          process: (content, srcpath) => content.replace(/\/\/# sourceMappingURL=[^ ]+/, '')
+          process: (content) => content.replace(/\/\/# sourceMappingURL=[^ ]+/, '')
         },
         files: [{
           expand: true,
@@ -571,11 +576,11 @@ module.exports = function (grunt) {
       options: {
         clean: false,
         report: false,
-        srcPrefix: "node_modules/"
+        srcPrefix: 'node_modules/'
       },
       backend: {
         options: {
-          destPrefix: "<%= paths.backend %>Public",
+          destPrefix: '<%= paths.backend %>Public',
           copyOptions: {
             process: (source, srcpath) => {
               if (srcpath.match(/.*\.js$/)) {
@@ -592,7 +597,7 @@ module.exports = function (grunt) {
       },
       dashboardToEs6: {
         options: {
-          destPrefix: "<%= paths.dashboard %>Public",
+          destPrefix: '<%= paths.dashboard %>Public',
           copyOptions: {
             process: (source, srcpath) => {
               if (srcpath.match(/.*\.js$/)) {
@@ -609,7 +614,7 @@ module.exports = function (grunt) {
       },
       dashboard: {
         options: {
-          destPrefix: "<%= paths.dashboard %>Public",
+          destPrefix: '<%= paths.dashboard %>Public',
         },
         files: {
           'JavaScript/Contrib/chartjs.js': 'chart.js/dist/chart.js',
@@ -618,7 +623,7 @@ module.exports = function (grunt) {
       },
       umdToEs6: {
         options: {
-          destPrefix: "<%= paths.core %>Public/JavaScript/Contrib",
+          destPrefix: '<%= paths.core %>Public/JavaScript/Contrib',
           copyOptions: {
             process: (source, srcpath) => {
               let imports = [], prefix = '';
@@ -631,6 +636,10 @@ module.exports = function (grunt) {
                 prefix = 'import Tablesort from "tablesort";';
               }
 
+              if (srcpath === 'node_modules/tablesort/dist/sorts/tablesort.number.min.js') {
+                prefix = 'import Tablesort from "tablesort";';
+              }
+
               return require('./util/cjs-to-esm.js').cjsToEsm(source, imports, prefix);
             }
           }
@@ -646,12 +655,13 @@ module.exports = function (grunt) {
           'sortablejs.js': 'sortablejs/dist/sortable.umd.js',
           'tablesort.js': 'tablesort/dist/tablesort.min.js',
           'tablesort.dotsep.js': 'tablesort/dist/sorts/tablesort.dotsep.min.js',
+          'tablesort.number.js': 'tablesort/dist/sorts/tablesort.number.min.js',
           'taboverride.js': 'taboverride/build/output/taboverride.js',
         }
       },
       install: {
         options: {
-          destPrefix: "<%= paths.install %>Public/JavaScript",
+          destPrefix: '<%= paths.install %>Public/JavaScript',
           copyOptions: {
             process: (source, srcpath) => {
               if (srcpath === 'node_modules/chosen-js/chosen.jquery.js') {
@@ -668,7 +678,7 @@ module.exports = function (grunt) {
       },
       jqueryUi: {
         options: {
-          destPrefix: "<%= paths.core %>Public/JavaScript/Contrib",
+          destPrefix: '<%= paths.core %>Public/JavaScript/Contrib',
           copyOptions: {
             process: (source, srcpath) => {
 
@@ -717,7 +727,7 @@ module.exports = function (grunt) {
       },
       all: {
         options: {
-          destPrefix: "<%= paths.core %>Public/JavaScript/Contrib"
+          destPrefix: '<%= paths.core %>Public/JavaScript/Contrib'
         },
         files: {
           'autosize.js': 'autosize/dist/autosize.esm.js',
@@ -737,27 +747,27 @@ module.exports = function (grunt) {
       },
       thirdparty: {
         files: {
-          "<%= paths.core %>Public/JavaScript/Contrib/es-module-shims.js": ["<%= paths.core %>Public/JavaScript/Contrib/es-module-shims.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/broadcastchannel.js": ["<%= paths.core %>Public/JavaScript/Contrib/broadcastchannel.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/cropperjs.js": ["<%= paths.core %>Public/JavaScript/Contrib/cropperjs.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/flatpickr/flatpickr.min.js": ["<%= paths.core %>Public/JavaScript/Contrib/flatpickr/flatpickr.min.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/flatpickr/locales.js": ["<%= paths.core %>Public/JavaScript/Contrib/flatpickr/locales.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/luxon.js": ["<%= paths.core %>Public/JavaScript/Contrib/luxon.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/require.js": ["<%= paths.core %>Public/JavaScript/Contrib/require.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/nprogress.js": ["<%= paths.core %>Public/JavaScript/Contrib/nprogress.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/taboverride.js": ["<%= paths.core %>Public/JavaScript/Contrib/taboverride.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/core.js": ["<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/core.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/draggable.js": ["<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/draggable.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/droppable.js": ["<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/droppable.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/mouse.js": ["<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/mouse.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/position.js": ["<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/position.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/resizable.js": ["<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/resizable.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/selectable.js": ["<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/selectable.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/sortable.js": ["<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/sortable.js"],
-          "<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/widget.js": ["<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/widget.js"],
-          "<%= paths.dashboard %>Public/JavaScript/Contrib/chartjs.js": ["<%= paths.dashboard %>Public/JavaScript/Contrib/chartjs.js"],
-          "<%= paths.dashboard %>Public/JavaScript/Contrib/chunks/helpers.segment.js": ["<%= paths.dashboard %>Public/JavaScript/Contrib/chunks/helpers.segment.js"],
-          "<%= paths.install %>Public/JavaScript/chosen.jquery.min.js": ["<%= paths.install %>Public/JavaScript/chosen.jquery.min.js"]
+          '<%= paths.core %>Public/JavaScript/Contrib/es-module-shims.js': ['<%= paths.core %>Public/JavaScript/Contrib/es-module-shims.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/broadcastchannel.js': ['<%= paths.core %>Public/JavaScript/Contrib/broadcastchannel.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/cropperjs.js': ['<%= paths.core %>Public/JavaScript/Contrib/cropperjs.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/flatpickr/flatpickr.min.js': ['<%= paths.core %>Public/JavaScript/Contrib/flatpickr/flatpickr.min.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/flatpickr/locales.js': ['<%= paths.core %>Public/JavaScript/Contrib/flatpickr/locales.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/luxon.js': ['<%= paths.core %>Public/JavaScript/Contrib/luxon.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/require.js': ['<%= paths.core %>Public/JavaScript/Contrib/require.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/nprogress.js': ['<%= paths.core %>Public/JavaScript/Contrib/nprogress.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/taboverride.js': ['<%= paths.core %>Public/JavaScript/Contrib/taboverride.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/core.js': ['<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/core.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/draggable.js': ['<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/draggable.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/droppable.js': ['<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/droppable.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/mouse.js': ['<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/mouse.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/position.js': ['<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/position.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/resizable.js': ['<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/resizable.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/selectable.js': ['<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/selectable.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/sortable.js': ['<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/sortable.js'],
+          '<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/widget.js': ['<%= paths.core %>Public/JavaScript/Contrib/jquery-ui/widget.js'],
+          '<%= paths.dashboard %>Public/JavaScript/Contrib/chartjs.js': ['<%= paths.dashboard %>Public/JavaScript/Contrib/chartjs.js'],
+          '<%= paths.dashboard %>Public/JavaScript/Contrib/chunks/helpers.segment.js': ['<%= paths.dashboard %>Public/JavaScript/Contrib/chunks/helpers.segment.js'],
+          '<%= paths.install %>Public/JavaScript/chosen.jquery.min.js': ['<%= paths.install %>Public/JavaScript/chosen.jquery.min.js']
         }
       },
       t3editor: {
@@ -909,11 +919,11 @@ module.exports = function (grunt) {
    * this task updates the tsconfig.json file with modules paths for all sysexts
    */
   grunt.task.registerTask('tsconfig', function () {
-    const config = grunt.file.readJSON("tsconfig.json");
+    const config = grunt.file.readJSON('tsconfig.json');
     const typescriptPath = grunt.config.get('paths.typescript');
     config.compilerOptions.paths = {};
     grunt.file.expand(typescriptPath + '*/').map(dir => dir.replace(typescriptPath, '')).forEach((path) => {
-      const extname = path.match(/^([^\/]+?)\//)[1].replace(/_/g, '-')
+      const extname = path.match(/^([^/]+?)\//)[1].replace(/_/g, '-')
       config.compilerOptions.paths['@typo3/' + extname + '/*'] = [path + '*'];
     });
 
diff --git a/Build/Sources/TypeScript/backend/sortable-table.ts b/Build/Sources/TypeScript/backend/sortable-table.ts
new file mode 100644
index 000000000000..1baaa8850d0c
--- /dev/null
+++ b/Build/Sources/TypeScript/backend/sortable-table.ts
@@ -0,0 +1,22 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+import Tablesort from 'tablesort';
+import 'tablesort.dotsep';
+import 'tablesort.number';
+
+export default class SortableTable {
+  constructor(table: HTMLTableElement) {
+    new Tablesort(table);
+  }
+}
diff --git a/Build/Sources/TypeScript/extensionmanager/main.ts b/Build/Sources/TypeScript/extensionmanager/main.ts
index 5f57f9653eca..8ec9dbaae5b1 100644
--- a/Build/Sources/TypeScript/extensionmanager/main.ts
+++ b/Build/Sources/TypeScript/extensionmanager/main.ts
@@ -21,13 +21,12 @@ import SecurityUtility from '@typo3/core/security-utility';
 import ExtensionManagerRepository from './repository';
 import ExtensionManagerUpdate from './update';
 import ExtensionManagerUploadForm from './upload-form';
-import Tablesort from 'tablesort';
-import 'tablesort.dotsep';
 import '@typo3/backend/input/clearable';
 import { AjaxResponse } from '@typo3/core/ajax/ajax-response';
 import AjaxRequest from '@typo3/core/ajax/ajax-request';
 import DebounceEvent from '@typo3/core/event/debounce-event';
 import RegularEvent from '@typo3/core/event/regular-event';
+import SortableTable from '@typo3/backend/sortable-table';
 
 const securityUtility = new SecurityUtility();
 
@@ -63,7 +62,10 @@ class ExtensionManager {
 
       const extensionList = document.getElementById(ExtensionManagerIdentifier.extensionlist);
       if (extensionList !== null) {
-        new Tablesort(extensionList);
+
+        if (extensionList instanceof HTMLTableElement) {
+          new SortableTable(extensionList);
+        }
 
         new RegularEvent('click', (e: Event, target: HTMLAnchorElement): void => {
           e.preventDefault();
diff --git a/Build/Sources/TypeScript/extensionmanager/repository.ts b/Build/Sources/TypeScript/extensionmanager/repository.ts
index ceb1f89cc5a1..5cb7b762d30d 100644
--- a/Build/Sources/TypeScript/extensionmanager/repository.ts
+++ b/Build/Sources/TypeScript/extensionmanager/repository.ts
@@ -16,7 +16,7 @@ import NProgress from 'nprogress';
 import Modal from '@typo3/backend/modal';
 import Notification from '@typo3/backend/notification';
 import Severity from '@typo3/backend/severity';
-import Tablesort from 'tablesort';
+import SortableTable from '@typo3/backend/sortable-table';
 import '@typo3/backend/input/clearable';
 import { AjaxResponse } from '@typo3/core/ajax/ajax-response';
 import AjaxRequest from '@typo3/core/ajax/ajax-request';
@@ -45,11 +45,11 @@ class Repository {
     const terVersionTable = document.getElementById('terVersionTable');
     const terSearchTable = document.getElementById('terSearchTable');
 
-    if (terVersionTable !== null) {
-      new Tablesort(terVersionTable);
+    if (terVersionTable instanceof HTMLTableElement) {
+      new SortableTable(terVersionTable);
     }
-    if (terSearchTable !== null) {
-      new Tablesort(terSearchTable);
+    if (terSearchTable instanceof HTMLTableElement) {
+      new SortableTable(terSearchTable);
     }
 
     this.bindDownload();
diff --git a/Build/Sources/TypeScript/scheduler/scheduler.ts b/Build/Sources/TypeScript/scheduler/scheduler.ts
index 4ba88b9d8285..781fb82036b0 100644
--- a/Build/Sources/TypeScript/scheduler/scheduler.ts
+++ b/Build/Sources/TypeScript/scheduler/scheduler.ts
@@ -12,7 +12,7 @@
  */
 
 import $ from 'jquery';
-import Tablesort from 'tablesort';
+import SortableTable from '@typo3/backend/sortable-table';
 import DocumentSaveActions from '@typo3/backend/document-save-actions';
 import RegularEvent from '@typo3/core/event/regular-event';
 import Modal from '@typo3/backend/modal';
@@ -178,10 +178,9 @@ class Scheduler {
       $target.val($target.attr('value')).trigger('blur');
     });
 
-    const taskGroupTable = document.querySelector('table.taskGroup-table');
-    if (taskGroupTable !== null) {
-      new Tablesort(taskGroupTable);
-    }
+    document.querySelectorAll('[data-scheduler-table]').forEach((table: HTMLTableElement) => {
+      new SortableTable(table);
+    });
 
     (<NodeListOf<HTMLInputElement>>document.querySelectorAll('#tx_scheduler_form .t3js-datetimepicker')).forEach(
       (dateTimePickerElement: HTMLInputElement) => DateTimePicker.initialize(dateTimePickerElement)
diff --git a/Build/types/tablesort.d.ts b/Build/types/tablesort.d.ts
index ad0d8907f7c9..81687fbeb838 100644
--- a/Build/types/tablesort.d.ts
+++ b/Build/types/tablesort.d.ts
@@ -9,6 +9,7 @@ type TablesortOptions = {
 
 declare const Tablesort: {
   new(table: Element, options?: {[key: string]: TablesortOptions}): Tablesort;
+  // eslint-disable-next-line @typescript-eslint/ban-types
   extend(name: string, pattern: Function, sort: Function): void;
 }
 
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/sortable-table.js b/typo3/sysext/backend/Resources/Public/JavaScript/sortable-table.js
new file mode 100644
index 000000000000..3ca188e2bae0
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/sortable-table.js
@@ -0,0 +1,13 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+import Tablesort from"tablesort";import"tablesort.dotsep.js";import"tablesort.number.js";export default class SortableTable{constructor(t){new Tablesort(t)}}
\ No newline at end of file
diff --git a/typo3/sysext/core/Configuration/JavaScriptModules.php b/typo3/sysext/core/Configuration/JavaScriptModules.php
index 97e09b9ee0a1..3d5cc637af72 100644
--- a/typo3/sysext/core/Configuration/JavaScriptModules.php
+++ b/typo3/sysext/core/Configuration/JavaScriptModules.php
@@ -34,6 +34,7 @@ return [
         'nprogress' => 'EXT:core/Resources/Public/JavaScript/Contrib/nprogress.js',
         'sortablejs' => 'EXT:core/Resources/Public/JavaScript/Contrib/sortablejs.js',
         'tablesort.dotsep.js' => 'EXT:core/Resources/Public/JavaScript/Contrib/tablesort.dotsep.js',
+        'tablesort.number.js' => 'EXT:core/Resources/Public/JavaScript/Contrib/tablesort.number.js',
         'tablesort' => 'EXT:core/Resources/Public/JavaScript/Contrib/tablesort.js',
         'taboverride' => 'EXT:core/Resources/Public/JavaScript/Contrib/taboverride.js',
     ],
diff --git a/typo3/sysext/core/Resources/Public/JavaScript/Contrib/tablesort.number.js b/typo3/sysext/core/Resources/Public/JavaScript/Contrib/tablesort.number.js
new file mode 100644
index 000000000000..3c8c71aee068
--- /dev/null
+++ b/typo3/sysext/core/Resources/Public/JavaScript/Contrib/tablesort.number.js
@@ -0,0 +1,11 @@
+import Tablesort from "tablesort";
+export default (new function() {
+  const module = { exports: {} }, exports = module.exports, define = null;
+/*!
+ * tablesort v5.2.1 (2020-06-02)
+ * http://tristen.ca/tablesort/demo/
+ * Copyright (c) 2020 ; Licensed MIT
+*/
+!function(){var a=function(a){return a.replace(/[^\-?0-9.]/g,"")},b=function(a,b){return a=parseFloat(a),b=parseFloat(b),a=isNaN(a)?0:a,b=isNaN(b)?0:b,a-b};Tablesort.extend("number",function(a){return a.match(/^[-+]?[£\x24Û¢´€]?\d+\s*([,\.]\d{0,2})/)||a.match(/^[-+]?\d+\s*([,\.]\d{0,2})?[£\x24Û¢´€]/)||a.match(/^[-+]?(\d)*-?([,\.]){0,1}-?(\d)+([E,e][\-+][\d]+)?%?$/)},function(c,d){return c=a(c),d=a(d),b(d,c)})}();
+  this.__default_export = module.exports;
+}).__default_export;
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Acceptance/Application/Scheduler/TasksCest.php b/typo3/sysext/core/Tests/Acceptance/Application/Scheduler/TasksCest.php
index cc3a9ef6b30c..daf0380bfbac 100644
--- a/typo3/sysext/core/Tests/Acceptance/Application/Scheduler/TasksCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Application/Scheduler/TasksCest.php
@@ -146,7 +146,7 @@ final class TasksCest
         $this->createASchedulerTask($I);
 
         $I->amGoingTo('test the new task group button on task edit view');
-        $I->click('.taskGroup-table > tbody > tr > td.nowrap > div:nth-child(1) > a:nth-child(1)');
+        $I->click('[data-scheduler-table] > tbody > tr > td.nowrap > div:nth-child(1) > a:nth-child(1)');
         $I->waitForElementNotVisible('#t3js-ui-block');
         $I->canSee('Edit scheduled task "System Status Update (reports)"');
         $I->click('#task_group_row > div > div > div > div > a');
diff --git a/typo3/sysext/extensionmanager/Resources/Public/JavaScript/main.js b/typo3/sysext/extensionmanager/Resources/Public/JavaScript/main.js
index 9e1b51e78f8e..255752cc8ff3 100644
--- a/typo3/sysext/extensionmanager/Resources/Public/JavaScript/main.js
+++ b/typo3/sysext/extensionmanager/Resources/Public/JavaScript/main.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-import DocumentService from"@typo3/core/document-service.js";import $ from"jquery";import BrowserSession from"@typo3/backend/storage/browser-session.js";import NProgress from"nprogress";import{default as Modal}from"@typo3/backend/modal.js";import Severity from"@typo3/backend/severity.js";import SecurityUtility from"@typo3/core/security-utility.js";import ExtensionManagerRepository from"@typo3/extensionmanager/repository.js";import ExtensionManagerUpdate from"@typo3/extensionmanager/update.js";import ExtensionManagerUploadForm from"@typo3/extensionmanager/upload-form.js";import Tablesort from"tablesort";import"tablesort.dotsep.js";import"@typo3/backend/input/clearable.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import DebounceEvent from"@typo3/core/event/debounce-event.js";import RegularEvent from"@typo3/core/event/regular-event.js";const securityUtility=new SecurityUtility;var ExtensionManagerIdentifier;!function(e){e.extensionlist="typo3-extension-list",e.searchField="#Tx_Extensionmanager_extensionkey"}(ExtensionManagerIdentifier||(ExtensionManagerIdentifier={}));class ExtensionManager{constructor(){this.searchFilterSessionKey="tx-extensionmanager-local-filter",DocumentService.ready().then((()=>{this.Update=new ExtensionManagerUpdate,this.UploadForm=new ExtensionManagerUploadForm,this.Repository=new ExtensionManagerRepository;const e=document.getElementById(ExtensionManagerIdentifier.extensionlist);let t;if(null!==e&&(new Tablesort(e),new RegularEvent("click",((e,t)=>{e.preventDefault(),Modal.confirm(TYPO3.lang["extensionList.removalConfirmation.title"],TYPO3.lang["extensionList.removalConfirmation.question"],Severity.error,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:()=>{Modal.dismiss()}},{text:TYPO3.lang["button.remove"],btnClass:"btn-danger",trigger:()=>{this.removeExtensionFromDisk(t),Modal.dismiss()}}])})).delegateTo(e,".removeExtension")),$(document).on("click",".onClickMaskExtensionManager",(()=>{NProgress.start()})).on("click","a[data-action=update-extension]",(e=>{e.preventDefault(),NProgress.start(),new AjaxRequest($(e.currentTarget).attr("href")).get().then(this.updateExtension)})).on("change","input[name=unlockDependencyIgnoreButton]",(e=>{$(".t3js-dependencies").toggleClass("disabled",!$(e.currentTarget).prop("checked"))})),null!==(t=document.querySelector(ExtensionManagerIdentifier.searchField))){const e=BrowserSession.get(this.searchFilterSessionKey);null!==e&&(t.value=e,this.filterExtensions(e)),new RegularEvent("submit",(e=>{e.preventDefault()})).bindTo(t.closest("form")),new DebounceEvent("input",(e=>{const t=e.target;BrowserSession.set(this.searchFilterSessionKey,t.value),this.filterExtensions(t.value)}),100).bindTo(t),t.clearable({onClear:()=>{BrowserSession.unset(this.searchFilterSessionKey),this.filterExtensions("")}})}$(document).on("click",".t3-button-action-installdistribution",(()=>{NProgress.start()})),this.Repository.initDom(),this.Update.initializeEvents(),this.UploadForm.initializeEvents()}))}filterExtensions(e){const t=document.querySelectorAll("[data-filterable]"),n=[];t.forEach((e=>{const t=Array.from(e.parentElement.children);n.push(t.indexOf(e))}));document.querySelectorAll("#typo3-extension-list tbody tr").forEach((t=>{const o=n.map((e=>t.children.item(e))),r=[];o.forEach((e=>{r.push(e.textContent.trim().replace(/\s+/g," "))})),t.classList.toggle("hidden",""!==e&&!RegExp(e,"i").test(r.join(":")))}))}removeExtensionFromDisk(e){NProgress.start(),new AjaxRequest(e.href).get().then((()=>{location.reload()})).finally((()=>{NProgress.done()}))}async updateExtension(e){let t=0;const n=await e.resolve(),o=$("<form>");for(const[e,r]of Object.entries(n.updateComments)){const n=$("<input>").attr({type:"radio",name:"version"}).val(e);0===t&&n.attr("checked","checked"),o.append([$("<h3>").append([n," "+securityUtility.encodeHtml(e)]),$("<div>").append(r.replace(/(\r\n|\n\r|\r|\n)/g,"\n").split(/\n/).map((e=>securityUtility.encodeHtml(e))).join("<br>"))]),t++}const r=$("<div>").append([$("<h1>").text(TYPO3.lang["extensionList.updateConfirmation.title"]),$("<h2>").text(TYPO3.lang["extensionList.updateConfirmation.message"]),o]);NProgress.done(),Modal.confirm(TYPO3.lang["extensionList.updateConfirmation.questionVersionComments"],r,Severity.warning,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:(e,t)=>t.hideModal()},{text:TYPO3.lang["button.updateExtension"],btnClass:"btn-warning",trigger:(e,t)=>{NProgress.start(),new AjaxRequest(n.url).withQueryArguments({version:$("input:radio[name=version]:checked",t).val()}).get().finally((()=>{location.reload()})),t.hideModal()}}])}}const extensionManagerObject=new ExtensionManager;void 0===TYPO3.ExtensionManager&&(TYPO3.ExtensionManager=extensionManagerObject);export default extensionManagerObject;
\ No newline at end of file
+import DocumentService from"@typo3/core/document-service.js";import $ from"jquery";import BrowserSession from"@typo3/backend/storage/browser-session.js";import NProgress from"nprogress";import{default as Modal}from"@typo3/backend/modal.js";import Severity from"@typo3/backend/severity.js";import SecurityUtility from"@typo3/core/security-utility.js";import ExtensionManagerRepository from"@typo3/extensionmanager/repository.js";import ExtensionManagerUpdate from"@typo3/extensionmanager/update.js";import ExtensionManagerUploadForm from"@typo3/extensionmanager/upload-form.js";import"@typo3/backend/input/clearable.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import DebounceEvent from"@typo3/core/event/debounce-event.js";import RegularEvent from"@typo3/core/event/regular-event.js";import SortableTable from"@typo3/backend/sortable-table.js";const securityUtility=new SecurityUtility;var ExtensionManagerIdentifier;!function(e){e.extensionlist="typo3-extension-list",e.searchField="#Tx_Extensionmanager_extensionkey"}(ExtensionManagerIdentifier||(ExtensionManagerIdentifier={}));class ExtensionManager{constructor(){this.searchFilterSessionKey="tx-extensionmanager-local-filter",DocumentService.ready().then((()=>{this.Update=new ExtensionManagerUpdate,this.UploadForm=new ExtensionManagerUploadForm,this.Repository=new ExtensionManagerRepository;const e=document.getElementById(ExtensionManagerIdentifier.extensionlist);let t;if(null!==e&&(e instanceof HTMLTableElement&&new SortableTable(e),new RegularEvent("click",((e,t)=>{e.preventDefault(),Modal.confirm(TYPO3.lang["extensionList.removalConfirmation.title"],TYPO3.lang["extensionList.removalConfirmation.question"],Severity.error,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:()=>{Modal.dismiss()}},{text:TYPO3.lang["button.remove"],btnClass:"btn-danger",trigger:()=>{this.removeExtensionFromDisk(t),Modal.dismiss()}}])})).delegateTo(e,".removeExtension")),$(document).on("click",".onClickMaskExtensionManager",(()=>{NProgress.start()})).on("click","a[data-action=update-extension]",(e=>{e.preventDefault(),NProgress.start(),new AjaxRequest($(e.currentTarget).attr("href")).get().then(this.updateExtension)})).on("change","input[name=unlockDependencyIgnoreButton]",(e=>{$(".t3js-dependencies").toggleClass("disabled",!$(e.currentTarget).prop("checked"))})),null!==(t=document.querySelector(ExtensionManagerIdentifier.searchField))){const e=BrowserSession.get(this.searchFilterSessionKey);null!==e&&(t.value=e,this.filterExtensions(e)),new RegularEvent("submit",(e=>{e.preventDefault()})).bindTo(t.closest("form")),new DebounceEvent("input",(e=>{const t=e.target;BrowserSession.set(this.searchFilterSessionKey,t.value),this.filterExtensions(t.value)}),100).bindTo(t),t.clearable({onClear:()=>{BrowserSession.unset(this.searchFilterSessionKey),this.filterExtensions("")}})}$(document).on("click",".t3-button-action-installdistribution",(()=>{NProgress.start()})),this.Repository.initDom(),this.Update.initializeEvents(),this.UploadForm.initializeEvents()}))}filterExtensions(e){const t=document.querySelectorAll("[data-filterable]"),n=[];t.forEach((e=>{const t=Array.from(e.parentElement.children);n.push(t.indexOf(e))}));document.querySelectorAll("#typo3-extension-list tbody tr").forEach((t=>{const o=n.map((e=>t.children.item(e))),r=[];o.forEach((e=>{r.push(e.textContent.trim().replace(/\s+/g," "))})),t.classList.toggle("hidden",""!==e&&!RegExp(e,"i").test(r.join(":")))}))}removeExtensionFromDisk(e){NProgress.start(),new AjaxRequest(e.href).get().then((()=>{location.reload()})).finally((()=>{NProgress.done()}))}async updateExtension(e){let t=0;const n=await e.resolve(),o=$("<form>");for(const[e,r]of Object.entries(n.updateComments)){const n=$("<input>").attr({type:"radio",name:"version"}).val(e);0===t&&n.attr("checked","checked"),o.append([$("<h3>").append([n," "+securityUtility.encodeHtml(e)]),$("<div>").append(r.replace(/(\r\n|\n\r|\r|\n)/g,"\n").split(/\n/).map((e=>securityUtility.encodeHtml(e))).join("<br>"))]),t++}const r=$("<div>").append([$("<h1>").text(TYPO3.lang["extensionList.updateConfirmation.title"]),$("<h2>").text(TYPO3.lang["extensionList.updateConfirmation.message"]),o]);NProgress.done(),Modal.confirm(TYPO3.lang["extensionList.updateConfirmation.questionVersionComments"],r,Severity.warning,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:(e,t)=>t.hideModal()},{text:TYPO3.lang["button.updateExtension"],btnClass:"btn-warning",trigger:(e,t)=>{NProgress.start(),new AjaxRequest(n.url).withQueryArguments({version:$("input:radio[name=version]:checked",t).val()}).get().finally((()=>{location.reload()})),t.hideModal()}}])}}const extensionManagerObject=new ExtensionManager;void 0===TYPO3.ExtensionManager&&(TYPO3.ExtensionManager=extensionManagerObject);export default extensionManagerObject;
\ No newline at end of file
diff --git a/typo3/sysext/extensionmanager/Resources/Public/JavaScript/repository.js b/typo3/sysext/extensionmanager/Resources/Public/JavaScript/repository.js
index 754c8f318009..489860a1aab3 100644
--- a/typo3/sysext/extensionmanager/Resources/Public/JavaScript/repository.js
+++ b/typo3/sysext/extensionmanager/Resources/Public/JavaScript/repository.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-import $ from"jquery";import NProgress from"nprogress";import Modal from"@typo3/backend/modal.js";import Notification from"@typo3/backend/notification.js";import Severity from"@typo3/backend/severity.js";import Tablesort from"tablesort";import"@typo3/backend/input/clearable.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import RegularEvent from"@typo3/core/event/regular-event.js";class Repository{constructor(){this.downloadPath="",this.getDependencies=async e=>{const t=await e.resolve();NProgress.done(),t.hasDependencies?Modal.confirm(t.title,$(t.message),Severity.info,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:()=>{Modal.dismiss()}},{text:TYPO3.lang["button.resolveDependencies"],btnClass:"btn-primary",trigger:()=>{this.getResolveDependenciesAndInstallResult(t.url+"&downloadPath="+this.downloadPath),Modal.dismiss()}}]):t.hasErrors?Notification.error(t.title,t.message,15):this.getResolveDependenciesAndInstallResult(t.url+"&downloadPath="+this.downloadPath)}}initDom(){NProgress.configure({parent:".module-loading-indicator",showSpinner:!1});const e=document.getElementById("terVersionTable"),t=document.getElementById("terSearchTable");null!==e&&new Tablesort(e),null!==t&&new Tablesort(t),this.bindDownload(),this.bindSearchFieldResetter()}bindDownload(){new RegularEvent("click",((e,t)=>{e.preventDefault();const n=t.closest("form"),s=n.dataset.href;this.downloadPath=n.querySelector("input.downloadPath:checked").value,NProgress.start(),new AjaxRequest(s).get().then(this.getDependencies)})).delegateTo(document,".downloadFromTer form.download button[type=submit]")}getResolveDependenciesAndInstallResult(e){NProgress.start(),new AjaxRequest(e).get().then((async e=>{const t=await e.raw().json();if(t.errorCount>0){const e=Modal.confirm(t.errorTitle,$(t.errorMessage),Severity.error,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:()=>{Modal.dismiss()}},{text:TYPO3.lang["button.resolveDependenciesIgnore"],btnClass:"btn-danger disabled t3js-dependencies",trigger:e=>{$(e.currentTarget).hasClass("disabled")||(this.getResolveDependenciesAndInstallResult(t.skipDependencyUri),Modal.dismiss())}}]);e.addEventListener("typo3-modal-shown",(()=>{const t=e.querySelector(".t3js-dependencies");e.querySelector('input[name="unlockDependencyIgnoreButton"]').addEventListener("change",(e=>{e.currentTarget.checked?t?.classList.remove("disabled"):t?.classList.add("disabled")}))}))}else{let e=TYPO3.lang["extensionList.dependenciesResolveDownloadSuccess.message"+t.installationTypeLanguageKey].replace(/\{0\}/g,t.extension);e+="\n"+TYPO3.lang["extensionList.dependenciesResolveDownloadSuccess.header"]+": ";for(const[n,s]of Object.entries(t.result)){e+="\n\n"+TYPO3.lang["extensionList.dependenciesResolveDownloadSuccess.item"]+" "+n+": ";for(const t of s)e+="\n* "+t}Notification.info(TYPO3.lang["extensionList.dependenciesResolveFlashMessage.title"+t.installationTypeLanguageKey].replace(/\{0\}/g,t.extension),e,15),top.TYPO3.ModuleMenu.App.refreshMenu()}})).finally((()=>{NProgress.done()}))}bindSearchFieldResetter(){let e;if(null!==(e=document.querySelector('.typo3-extensionmanager-searchTerForm input[type="text"]'))){const t=""!==e.value;e.clearable({onClear:e=>{t&&e.closest("form").submit()}})}}}export default Repository;
\ No newline at end of file
+import $ from"jquery";import NProgress from"nprogress";import Modal from"@typo3/backend/modal.js";import Notification from"@typo3/backend/notification.js";import Severity from"@typo3/backend/severity.js";import SortableTable from"@typo3/backend/sortable-table.js";import"@typo3/backend/input/clearable.js";import AjaxRequest from"@typo3/core/ajax/ajax-request.js";import RegularEvent from"@typo3/core/event/regular-event.js";class Repository{constructor(){this.downloadPath="",this.getDependencies=async e=>{const t=await e.resolve();NProgress.done(),t.hasDependencies?Modal.confirm(t.title,$(t.message),Severity.info,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:()=>{Modal.dismiss()}},{text:TYPO3.lang["button.resolveDependencies"],btnClass:"btn-primary",trigger:()=>{this.getResolveDependenciesAndInstallResult(t.url+"&downloadPath="+this.downloadPath),Modal.dismiss()}}]):t.hasErrors?Notification.error(t.title,t.message,15):this.getResolveDependenciesAndInstallResult(t.url+"&downloadPath="+this.downloadPath)}}initDom(){NProgress.configure({parent:".module-loading-indicator",showSpinner:!1});const e=document.getElementById("terVersionTable"),t=document.getElementById("terSearchTable");e instanceof HTMLTableElement&&new SortableTable(e),t instanceof HTMLTableElement&&new SortableTable(t),this.bindDownload(),this.bindSearchFieldResetter()}bindDownload(){new RegularEvent("click",((e,t)=>{e.preventDefault();const n=t.closest("form"),o=n.dataset.href;this.downloadPath=n.querySelector("input.downloadPath:checked").value,NProgress.start(),new AjaxRequest(o).get().then(this.getDependencies)})).delegateTo(document,".downloadFromTer form.download button[type=submit]")}getResolveDependenciesAndInstallResult(e){NProgress.start(),new AjaxRequest(e).get().then((async e=>{const t=await e.raw().json();if(t.errorCount>0){const e=Modal.confirm(t.errorTitle,$(t.errorMessage),Severity.error,[{text:TYPO3.lang["button.cancel"],active:!0,btnClass:"btn-default",trigger:()=>{Modal.dismiss()}},{text:TYPO3.lang["button.resolveDependenciesIgnore"],btnClass:"btn-danger disabled t3js-dependencies",trigger:e=>{$(e.currentTarget).hasClass("disabled")||(this.getResolveDependenciesAndInstallResult(t.skipDependencyUri),Modal.dismiss())}}]);e.addEventListener("typo3-modal-shown",(()=>{const t=e.querySelector(".t3js-dependencies");e.querySelector('input[name="unlockDependencyIgnoreButton"]').addEventListener("change",(e=>{e.currentTarget.checked?t?.classList.remove("disabled"):t?.classList.add("disabled")}))}))}else{let e=TYPO3.lang["extensionList.dependenciesResolveDownloadSuccess.message"+t.installationTypeLanguageKey].replace(/\{0\}/g,t.extension);e+="\n"+TYPO3.lang["extensionList.dependenciesResolveDownloadSuccess.header"]+": ";for(const[n,o]of Object.entries(t.result)){e+="\n\n"+TYPO3.lang["extensionList.dependenciesResolveDownloadSuccess.item"]+" "+n+": ";for(const t of o)e+="\n* "+t}Notification.info(TYPO3.lang["extensionList.dependenciesResolveFlashMessage.title"+t.installationTypeLanguageKey].replace(/\{0\}/g,t.extension),e,15),top.TYPO3.ModuleMenu.App.refreshMenu()}})).finally((()=>{NProgress.done()}))}bindSearchFieldResetter(){let e;if(null!==(e=document.querySelector('.typo3-extensionmanager-searchTerForm input[type="text"]'))){const t=""!==e.value;e.clearable({onClear:e=>{t&&e.closest("form").submit()}})}}}export default Repository;
\ No newline at end of file
diff --git a/typo3/sysext/scheduler/Resources/Private/Partials/TaskList.html b/typo3/sysext/scheduler/Resources/Private/Partials/TaskList.html
index f85213d7fbd3..a93ef95298c6 100644
--- a/typo3/sysext/scheduler/Resources/Private/Partials/TaskList.html
+++ b/typo3/sysext/scheduler/Resources/Private/Partials/TaskList.html
@@ -52,7 +52,7 @@
             </div>
             <div class="panel-collapse collapse {f:if(condition: '!{taskGroup.taskGroupCollapsed}', then: 'show')}" id="scheduler-task-group-{taskGroupId}" data-table="task-group-{taskGroupId}">
                 <div class="table-fit">
-                    <table class="table table-striped table-hover taskGroup-table">
+                    <table class="table table-striped table-hover" data-scheduler-table>
                         <thead>
                             <tr class="taskGroup_{taskGroupId}">
                                 <th data-sort-method="none">
@@ -388,7 +388,7 @@
                             <td class="nowrap-disabled"><span>{f:translate(key:'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.invalidTaskClass') -> f:format.raw()}</span></td>
                         </tr>
                     </table>
-                    <table class="table table-striped table-hover taskGroup-table">
+                    <table class="table table-striped table-hover" data-scheduler-table>
                         <thead>
                         <tr>
                             <th><f:translate key="LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.id"/></th>
diff --git a/typo3/sysext/scheduler/Resources/Public/JavaScript/scheduler.js b/typo3/sysext/scheduler/Resources/Public/JavaScript/scheduler.js
index e01b21767b64..5d788df78398 100644
--- a/typo3/sysext/scheduler/Resources/Public/JavaScript/scheduler.js
+++ b/typo3/sysext/scheduler/Resources/Public/JavaScript/scheduler.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-import $ from"jquery";import Tablesort from"tablesort";import DocumentSaveActions from"@typo3/backend/document-save-actions.js";import RegularEvent from"@typo3/core/event/regular-event.js";import Modal from"@typo3/backend/modal.js";import Icons from"@typo3/backend/icons.js";import{MessageUtility}from"@typo3/backend/utility/message-utility.js";import PersistentStorage from"@typo3/backend/storage/persistent.js";import DateTimePicker from"@typo3/backend/date-time-picker.js";import{MultiRecordSelectionSelectors}from"@typo3/backend/multi-record-selection.js";class Scheduler{constructor(){this.initializeEvents(),this.initializeDefaultStates(),DocumentSaveActions.getInstance().addPreSubmitCallback((()=>{let e=$("#task_class").val();e=e.toLowerCase().replace(/\\/g,"-"),$(".extraFields").appendTo($("#extraFieldsHidden")),$(".extra_fields_"+e).appendTo($("#extraFieldsSection"))}))}static updateElementBrowserTriggers(){document.querySelectorAll(".t3js-element-browser").forEach((e=>{const t=document.getElementById(e.dataset.triggerFor);e.dataset.params=t.name+"|||pages"}))}static resolveDefaultNumberOfDays(){const e=document.getElementById("task_tableGarbageCollection_numberOfDays");return null===e||void 0===e.dataset.defaultNumberOfDays?null:JSON.parse(e.dataset.defaultNumberOfDays)}static storeCollapseState(e,t){let a={};PersistentStorage.isset("moduleData.scheduler_manage")&&(a=PersistentStorage.get("moduleData.scheduler_manage"));const l={};l[e]=t?1:0,$.extend(a,l),PersistentStorage.set("moduleData.scheduler_manage",a)}actOnChangedTaskClass(e){let t=e.val();t=t.toLowerCase().replace(/\\/g,"-"),$(".extraFields").hide(),$(".extra_fields_"+t).show()}actOnChangedTaskType(e){this.toggleFieldsByTaskType($(e.currentTarget).val())}actOnChangeSchedulerTableGarbageCollectionAllTables(e){const t=$("#task_tableGarbageCollection_numberOfDays"),a=$("#task_tableGarbageCollection_table");if(e.prop("checked"))a.prop("disabled",!0),t.prop("disabled",!0);else{let e=parseInt(t.val(),10);if(e<1){const t=a.val(),l=Scheduler.resolveDefaultNumberOfDays();null!==l&&(e=l[t])}a.prop("disabled",!1),e>0&&t.prop("disabled",!1)}}actOnChangeSchedulerTableGarbageCollectionTable(e){const t=$("#task_tableGarbageCollection_numberOfDays"),a=Scheduler.resolveDefaultNumberOfDays();null!==a&&a[e.val()]>0?(t.prop("disabled",!1),t.val(a[e.val()])):(t.prop("disabled",!0),t.val(0))}toggleFieldsByTaskType(e){e=parseInt(e+"",10),$("#task_end_col").toggle(2===e),$("#task_frequency_row").toggle(2===e)}initializeEvents(){$("#task_class").on("change",(e=>{this.actOnChangedTaskClass($(e.currentTarget))})),$("#task_type").on("change",this.actOnChangedTaskType.bind(this)),$("#task_tableGarbageCollection_allTables").on("change",(e=>{this.actOnChangeSchedulerTableGarbageCollectionAllTables($(e.currentTarget))})),$("#task_tableGarbageCollection_table").on("change",(e=>{this.actOnChangeSchedulerTableGarbageCollectionTable($(e.currentTarget))})),$("[data-update-task-frequency]").on("change",(e=>{const t=$(e.currentTarget);$("#task_frequency").val(t.val()),t.val(t.attr("value")).trigger("blur")}));const e=document.querySelector("table.taskGroup-table");null!==e&&new Tablesort(e),document.querySelectorAll("#tx_scheduler_form .t3js-datetimepicker").forEach((e=>DateTimePicker.initialize(e))),$(document).on("click",".t3js-element-browser",(e=>{e.preventDefault();const t=e.currentTarget;Modal.advanced({type:Modal.types.iframe,content:t.href+"&mode="+t.dataset.mode+"&bparams="+t.dataset.params,size:Modal.sizes.large})})),new RegularEvent("show.bs.collapse",this.toggleCollapseIcon.bind(this)).bindTo(document),new RegularEvent("hide.bs.collapse",this.toggleCollapseIcon.bind(this)).bindTo(document),new RegularEvent("multiRecordSelection:action:go",this.executeTasks.bind(this)).bindTo(document),new RegularEvent("multiRecordSelection:action:go_cron",this.executeTasks.bind(this)).bindTo(document),window.addEventListener("message",this.listenOnElementBrowser.bind(this))}initializeDefaultStates(){const e=$("#task_type");e.length&&this.toggleFieldsByTaskType(e.val());const t=$("#task_class");t.length&&(this.actOnChangedTaskClass(t),Scheduler.updateElementBrowserTriggers())}listenOnElementBrowser(e){if(!MessageUtility.verifyOrigin(e.origin))throw"Denied message sent by "+e.origin;if("typo3:elementBrowser:elementAdded"===e.data.actionName){if(void 0===e.data.fieldName)throw"fieldName not defined in message";if(void 0===e.data.value)throw"value not defined in message";document.querySelector('input[name="'+e.data.fieldName+'"]').value=e.data.value.split("_").pop()}}toggleCollapseIcon(e){const t="hide.bs.collapse"===e.type,a=document.querySelector('.t3js-toggle-table[data-bs-target="#'+e.target.id+'"] .collapseIcon');null!==a&&Icons.getIcon(t?"actions-view-list-expand":"actions-view-list-collapse",Icons.sizes.small).then((e=>{a.innerHTML=e})),Scheduler.storeCollapseState($(e.target).data("table"),t)}executeTasks(e){const t=document.querySelector("#tx_scheduler_form");if(null===t)return;const a=[];if(e.detail.checkboxes.forEach((e=>{const t=e.closest(MultiRecordSelectionSelectors.elementSelector);null!==t&&t.dataset.taskId&&a.push(t.dataset.taskId)})),a.length){if("multiRecordSelection:action:go_cron"===e.type){const e=document.createElement("input");e.setAttribute("type","hidden"),e.setAttribute("name","scheduleCron"),e.setAttribute("value",a.join(",")),t.append(e)}else{const e=document.createElement("input");e.setAttribute("type","hidden"),e.setAttribute("name","execute"),e.setAttribute("value",a.join(",")),t.append(e)}t.submit()}}}export default new Scheduler;
\ No newline at end of file
+import $ from"jquery";import SortableTable from"@typo3/backend/sortable-table.js";import DocumentSaveActions from"@typo3/backend/document-save-actions.js";import RegularEvent from"@typo3/core/event/regular-event.js";import Modal from"@typo3/backend/modal.js";import Icons from"@typo3/backend/icons.js";import{MessageUtility}from"@typo3/backend/utility/message-utility.js";import PersistentStorage from"@typo3/backend/storage/persistent.js";import DateTimePicker from"@typo3/backend/date-time-picker.js";import{MultiRecordSelectionSelectors}from"@typo3/backend/multi-record-selection.js";class Scheduler{constructor(){this.initializeEvents(),this.initializeDefaultStates(),DocumentSaveActions.getInstance().addPreSubmitCallback((()=>{let e=$("#task_class").val();e=e.toLowerCase().replace(/\\/g,"-"),$(".extraFields").appendTo($("#extraFieldsHidden")),$(".extra_fields_"+e).appendTo($("#extraFieldsSection"))}))}static updateElementBrowserTriggers(){document.querySelectorAll(".t3js-element-browser").forEach((e=>{const t=document.getElementById(e.dataset.triggerFor);e.dataset.params=t.name+"|||pages"}))}static resolveDefaultNumberOfDays(){const e=document.getElementById("task_tableGarbageCollection_numberOfDays");return null===e||void 0===e.dataset.defaultNumberOfDays?null:JSON.parse(e.dataset.defaultNumberOfDays)}static storeCollapseState(e,t){let a={};PersistentStorage.isset("moduleData.scheduler_manage")&&(a=PersistentStorage.get("moduleData.scheduler_manage"));const l={};l[e]=t?1:0,$.extend(a,l),PersistentStorage.set("moduleData.scheduler_manage",a)}actOnChangedTaskClass(e){let t=e.val();t=t.toLowerCase().replace(/\\/g,"-"),$(".extraFields").hide(),$(".extra_fields_"+t).show()}actOnChangedTaskType(e){this.toggleFieldsByTaskType($(e.currentTarget).val())}actOnChangeSchedulerTableGarbageCollectionAllTables(e){const t=$("#task_tableGarbageCollection_numberOfDays"),a=$("#task_tableGarbageCollection_table");if(e.prop("checked"))a.prop("disabled",!0),t.prop("disabled",!0);else{let e=parseInt(t.val(),10);if(e<1){const t=a.val(),l=Scheduler.resolveDefaultNumberOfDays();null!==l&&(e=l[t])}a.prop("disabled",!1),e>0&&t.prop("disabled",!1)}}actOnChangeSchedulerTableGarbageCollectionTable(e){const t=$("#task_tableGarbageCollection_numberOfDays"),a=Scheduler.resolveDefaultNumberOfDays();null!==a&&a[e.val()]>0?(t.prop("disabled",!1),t.val(a[e.val()])):(t.prop("disabled",!0),t.val(0))}toggleFieldsByTaskType(e){e=parseInt(e+"",10),$("#task_end_col").toggle(2===e),$("#task_frequency_row").toggle(2===e)}initializeEvents(){$("#task_class").on("change",(e=>{this.actOnChangedTaskClass($(e.currentTarget))})),$("#task_type").on("change",this.actOnChangedTaskType.bind(this)),$("#task_tableGarbageCollection_allTables").on("change",(e=>{this.actOnChangeSchedulerTableGarbageCollectionAllTables($(e.currentTarget))})),$("#task_tableGarbageCollection_table").on("change",(e=>{this.actOnChangeSchedulerTableGarbageCollectionTable($(e.currentTarget))})),$("[data-update-task-frequency]").on("change",(e=>{const t=$(e.currentTarget);$("#task_frequency").val(t.val()),t.val(t.attr("value")).trigger("blur")})),document.querySelectorAll("[data-scheduler-table]").forEach((e=>{new SortableTable(e)})),document.querySelectorAll("#tx_scheduler_form .t3js-datetimepicker").forEach((e=>DateTimePicker.initialize(e))),$(document).on("click",".t3js-element-browser",(e=>{e.preventDefault();const t=e.currentTarget;Modal.advanced({type:Modal.types.iframe,content:t.href+"&mode="+t.dataset.mode+"&bparams="+t.dataset.params,size:Modal.sizes.large})})),new RegularEvent("show.bs.collapse",this.toggleCollapseIcon.bind(this)).bindTo(document),new RegularEvent("hide.bs.collapse",this.toggleCollapseIcon.bind(this)).bindTo(document),new RegularEvent("multiRecordSelection:action:go",this.executeTasks.bind(this)).bindTo(document),new RegularEvent("multiRecordSelection:action:go_cron",this.executeTasks.bind(this)).bindTo(document),window.addEventListener("message",this.listenOnElementBrowser.bind(this))}initializeDefaultStates(){const e=$("#task_type");e.length&&this.toggleFieldsByTaskType(e.val());const t=$("#task_class");t.length&&(this.actOnChangedTaskClass(t),Scheduler.updateElementBrowserTriggers())}listenOnElementBrowser(e){if(!MessageUtility.verifyOrigin(e.origin))throw"Denied message sent by "+e.origin;if("typo3:elementBrowser:elementAdded"===e.data.actionName){if(void 0===e.data.fieldName)throw"fieldName not defined in message";if(void 0===e.data.value)throw"value not defined in message";document.querySelector('input[name="'+e.data.fieldName+'"]').value=e.data.value.split("_").pop()}}toggleCollapseIcon(e){const t="hide.bs.collapse"===e.type,a=document.querySelector('.t3js-toggle-table[data-bs-target="#'+e.target.id+'"] .collapseIcon');null!==a&&Icons.getIcon(t?"actions-view-list-expand":"actions-view-list-collapse",Icons.sizes.small).then((e=>{a.innerHTML=e})),Scheduler.storeCollapseState($(e.target).data("table"),t)}executeTasks(e){const t=document.querySelector("#tx_scheduler_form");if(null===t)return;const a=[];if(e.detail.checkboxes.forEach((e=>{const t=e.closest(MultiRecordSelectionSelectors.elementSelector);null!==t&&t.dataset.taskId&&a.push(t.dataset.taskId)})),a.length){if("multiRecordSelection:action:go_cron"===e.type){const e=document.createElement("input");e.setAttribute("type","hidden"),e.setAttribute("name","scheduleCron"),e.setAttribute("value",a.join(",")),t.append(e)}else{const e=document.createElement("input");e.setAttribute("type","hidden"),e.setAttribute("name","execute"),e.setAttribute("value",a.join(",")),t.append(e)}t.submit()}}}export default new Scheduler;
\ No newline at end of file
-- 
GitLab