From a92dcdbbfabeb24fc9a50f178a0a6b2eb23af429 Mon Sep 17 00:00:00 2001
From: Stefan Galinski <stefan@sgalinski.de>
Date: Tue, 21 Sep 2021 19:30:10 +0200
Subject: [PATCH] [FEATURE] Improve backend behaviour to be in line with all
 other extensions, Heavy Cleanup

---
 Classes/Controller/BackendController.php      | 117 ++++++-------
 .../ViewHelpers/Backend/ControlViewHelper.php |  23 +--
 .../ViewHelpers/Backend/IconViewHelper.php    |  71 ++++++++
 .../Widget/Controller/PaginateController.php  |  98 +++++++++++
 .../Backend/Widget/PaginateViewHelper.php     |  81 +++++++++
 Classes/ViewHelpers/Widget/UriViewHelper.php  | 157 ++++++++++++++++++
 .../Private/Backend/Layouts/Default.html      |   2 +-
 .../Private/Backend/Partials/JobList.html     |  36 ----
 .../Private/Backend/Templates/Index.html      |  89 +++++-----
 .../Backend/Widget/Paginate/Index.html        | 125 ++++++++++++++
 10 files changed, 638 insertions(+), 161 deletions(-)
 create mode 100644 Classes/ViewHelpers/Backend/IconViewHelper.php
 create mode 100644 Classes/ViewHelpers/Backend/Widget/Controller/PaginateController.php
 create mode 100644 Classes/ViewHelpers/Backend/Widget/PaginateViewHelper.php
 create mode 100644 Classes/ViewHelpers/Widget/UriViewHelper.php
 delete mode 100644 Resources/Private/Backend/Partials/JobList.html
 create mode 100644 Resources/Private/Backend/Templates/ViewHelpers/Backend/Widget/Paginate/Index.html

diff --git a/Classes/Controller/BackendController.php b/Classes/Controller/BackendController.php
index eef539ba..4bad991e 100644
--- a/Classes/Controller/BackendController.php
+++ b/Classes/Controller/BackendController.php
@@ -86,70 +86,59 @@ class BackendController extends ActionController {
 	 * @return void
 	 */
 	public function indexAction(array $filters = []): void {
-		try {
-			$pageUid = (int) GeneralUtility::_GP('id');
-
-			// create docheader + buttons
-			$pageInfo = BackendUtility::readPageAccess($pageUid, $GLOBALS['BE_USER']->getPagePermsClause(1));
-			if ($pageInfo === FALSE) {
-				$pageInfo = ['uid' => $pageUid];
-			}
-
-			$this->docHeaderComponent = GeneralUtility::makeInstance(DocHeaderComponent::class);
-			$this->docHeaderComponent->setMetaInformation($pageInfo);
-			BackendService::makeButtons($this->docHeaderComponent, $this->request);
-			$this->view->assign('docHeader', $this->docHeaderComponent->docHeaderContent());
-			// get all jobs
-			/** @var ObjectStorage $jobs */
-			$jobs = $this->jobRepository->findBackendJobs($pageUid, $filters);
-
-			$totalJobCount = \count($jobs);
-
-			$this->view->assign('pages', BackendService::getPagesWithJobRecords());
-			$this->view->assign('pageUid', $pageUid);
-
-			$sortingData = [];
-			if ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sg_jobs']['allowManualSorting'] && count($filters) <= 0) {
-
-				/**
-				 *
-				 * Somehow the pId is changed so data gets lost.
-				 *
-				 *
-				 * $previousUid = 0;
-				 * $sortingData = [];
-				 * foreach ($jobs as $job) {
-				 * if ($previousUid) {
-				 * $sortingData['prev'][$job->getUid()] = $previousUid;
-				 * $sortingData['next'][$previousUid] = $job->getUid();
-				 * }
-				 * $previousUid = $job->getUid();
-				 * }
-				 */
-				$this->view->assign('manualSortingDestroysEverything', TRUE);
-			}
-
-			$this->view->assign('sortingData', $sortingData);
-
-			// get all Locations
-			/** @noinspection PhpUndefinedMethodInspection */
-			/** @var QueryResultInterface $companies */
-			$companies = $this->companyRepository->findByPid($pageUid);
-			$this->view->assign('locationOptions', $companies);
-			if ($totalJobCount || $companies->count()) {
-				$this->view->assign('jobs', $jobs);
-				$this->view->assign('filters', $filters);
-			} else {
-				$this->view->assign('noRecords', 1);
-				$this->view->assign('isAdmin', $GLOBALS['BE_USER']->isAdmin());
-				$this->addFlashMessage(
-					LocalizationUtility::translate('backend.notice.noRecords', 'SgJobs'), '', FlashMessage::INFO
-				);
-			}
-
-		} catch (\Exception $exception) {
-			// check for NULL value in view and render an error message
-			$this->view->assign('docHeader', NULL);
+		$pageUid = (int) GeneralUtility::_GP('id');
+
+		// create docheader + buttons
+		$pageInfo = BackendUtility::readPageAccess($pageUid, $GLOBALS['BE_USER']->getPagePermsClause(1));
+		if ($pageInfo === FALSE) {
+			$pageInfo = ['uid' => $pageUid];
+		}
+
+		$this->docHeaderComponent = GeneralUtility::makeInstance(DocHeaderComponent::class);
+		$this->docHeaderComponent->setMetaInformation($pageInfo);
+		BackendService::makeButtons($this->docHeaderComponent, $this->request);
+		$this->view->assign('docHeader', $this->docHeaderComponent->docHeaderContent());
+
+		$this->view->assign('pageUid', $pageUid);
+		$this->view->assign('pages', BackendService::getPagesWithJobRecords());
+
+		$sortingData = [];
+		if ($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sg_jobs']['allowManualSorting'] && count($filters) <= 0) {
+			/**
+			 * Somehow the pId is changed so data gets lost.
+			 *
+			 *
+			 * $previousUid = 0;
+			 * $sortingData = [];
+			 * foreach ($jobs as $job) {
+			 * if ($previousUid) {
+			 * $sortingData['prev'][$job->getUid()] = $previousUid;
+			 * $sortingData['next'][$previousUid] = $job->getUid();
+			 * }
+			 * $previousUid = $job->getUid();
+			 * }
+			 */
+			$this->view->assign('manualSortingDestroysEverything', TRUE);
+		}
+		$this->view->assign('sortingData', $sortingData);
+
+		/** @var ObjectStorage $jobs */
+		$jobs = $this->jobRepository->findBackendJobs($pageUid, $filters);
+		$totalJobCount = \count($jobs);
+
+		// get all Locations
+		/** @noinspection PhpUndefinedMethodInspection */
+		/** @var QueryResultInterface $companies */
+		$companies = $this->companyRepository->findByPid($pageUid);
+		$this->view->assign('locationOptions', $companies);
+
+		$this->view->assign('isAdmin', $GLOBALS['BE_USER']->isAdmin());
+		$this->view->assign('filters', $filters);
+		$this->view->assign('jobs', $jobs);
+		if (!$totalJobCount && $pageUid) {
+			$this->addFlashMessage(
+				LocalizationUtility::translate('backend.notice.noRecords', 'SgJobs'), '', FlashMessage::INFO
+			);
 		}
 	}
 }
diff --git a/Classes/ViewHelpers/Backend/ControlViewHelper.php b/Classes/ViewHelpers/Backend/ControlViewHelper.php
index eac8f911..75b0026a 100644
--- a/Classes/ViewHelpers/Backend/ControlViewHelper.php
+++ b/Classes/ViewHelpers/Backend/ControlViewHelper.php
@@ -26,6 +26,7 @@ namespace SGalinski\SgJobs\ViewHelpers\Backend;
  *  This copyright notice MUST APPEAR in all copies of the script!
  ***************************************************************/
 
+use SGalinski\SgJobs\Domain\Model\Job;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -40,41 +41,31 @@ class ControlViewHelper extends AbstractViewHelper {
 	/**
 	 * Initialize the ViewHelper arguments
 	 */
-	public function initializeArguments() {
+	public function initializeArguments(): void {
 		parent::initializeArguments();
 		$this->registerArgument('table', 'string', 'The table to control', TRUE);
-		$this->registerArgument('row', 'mixed', 'The row of the record', TRUE);
-		$this->registerArgument('sortingData', 'array', 'The sorting data', FALSE, []);
+		$this->registerArgument('row', 'object', 'The row of the record', TRUE);
 	}
 
 	/**
 	 * Renders the control buttons for the specified record
 	 *
 	 * @return string
-	 * @throws \InvalidArgumentException
-	 * @throws \UnexpectedValueException
 	 */
 	public function render(): string {
-		$row = $this->arguments['row'];
 		$table = $this->arguments['table'];
-		$sortingData = $this->arguments['sortingData'];
+		/** @var Job $row */
+		$row = $this->arguments['row'];
+		$row = BackendUtility::getRecord('tx_sgjobs_domain_model_job', $row->getUid());
 
-		/** @var DatabaseRecordList $databaseRecordList */
 		$databaseRecordList = GeneralUtility::makeInstance(DatabaseRecordList::class);
-
-		if (!\is_array($row)) {
-			$row = BackendUtility::getRecord($table, $row->getUid());
-		}
-
 		$pageInfo = BackendUtility::readPageAccess($row['pid'], $GLOBALS['BE_USER']->getPagePermsClause(1));
+		$databaseRecordList->calcPerms = $GLOBALS['BE_USER']->calcPerms($pageInfo);
 		$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
 		$pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/AjaxDataHandler');
 		$pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
 		$languageService = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Localization\LanguageService::class);
 		$languageService->includeLLFile('EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf');
-		$databaseRecordList->calcPerms = $GLOBALS['BE_USER']->calcPerms($pageInfo);
-		$databaseRecordList->currentTable = $sortingData;
 		return $databaseRecordList->makeControl($table, $row);
 	}
-
 }
diff --git a/Classes/ViewHelpers/Backend/IconViewHelper.php b/Classes/ViewHelpers/Backend/IconViewHelper.php
new file mode 100644
index 00000000..beef6cc6
--- /dev/null
+++ b/Classes/ViewHelpers/Backend/IconViewHelper.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace SGalinski\SgJobs\ViewHelpers\Backend;
+
+/***************************************************************
+ *  Copyright notice
+ *
+ *  (c) sgalinski Internet Services (https://www.sgalinski.de)
+ *
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Imaging\Icon;
+use TYPO3\CMS\Core\Imaging\IconFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
+
+/**
+ * Class EditLink
+ **/
+class IconViewHelper extends AbstractViewHelper {
+	/**
+	 * Register the ViewHelper arguments
+	 */
+	public function initializeArguments(): void {
+		parent::initializeArguments();
+		$this->registerArgument('table', 'string', 'The table for the icon', TRUE);
+		$this->registerArgument('row', 'object', 'The row of the record', TRUE);
+		$this->registerArgument('clickMenu', 'bool', 'Render a clickMenu around the icon', FALSE, TRUE);
+	}
+
+	/**
+	 * Renders the icon for the specified record
+	 *
+	 * @return string
+	 */
+	public function render(): string {
+		$row = $this->arguments['row'];
+		$row = BackendUtility::getRecord('tx_sgjobs_domain_model_job', $row->getUid());
+
+		$table = $this->arguments['table'];
+		$clickMenu = $this->arguments['clickMenu'];
+		$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
+		$toolTip = BackendUtility::getRecordToolTip($row, $table);
+		$iconImg = '<span ' . $toolTip . '>'
+			. $iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render()
+			. '</span>';
+		if ($clickMenu) {
+			return BackendUtility::wrapClickMenuOnIcon($iconImg, $table, $row['uid']);
+		}
+		return $iconImg;
+	}
+
+}
diff --git a/Classes/ViewHelpers/Backend/Widget/Controller/PaginateController.php b/Classes/ViewHelpers/Backend/Widget/Controller/PaginateController.php
new file mode 100644
index 00000000..47265580
--- /dev/null
+++ b/Classes/ViewHelpers/Backend/Widget/Controller/PaginateController.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace SGalinski\SgJobs\ViewHelpers\Backend\Widget\Controller;
+
+/***************************************************************
+ *  Copyright notice
+ *
+ *  (c) sgalinski Internet Services (https://www.sgalinski.de)
+ *
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+/**
+ * PaginateController
+ */
+class PaginateController extends \TYPO3\CMS\Fluid\ViewHelpers\Be\Widget\Controller\PaginateController {
+
+	/**
+	 * @var mixed
+	 */
+	protected $objects;
+
+	/**
+	 * Renders the paginator
+	 *
+	 * @param int $currentPage
+	 * @return void
+	 */
+	public function indexAction($currentPage = 1) {
+		// set current page
+		$this->currentPage = (int) $currentPage;
+		if ($this->currentPage < 1) {
+			$this->currentPage = 1;
+		}
+
+		if ($this->currentPage > $this->numberOfPages) {
+			// set $modifiedObjects to NULL if the page does not exist
+			$modifiedObjects = NULL;
+		} else {
+			// modify query
+			$this->itemsPerPage = (int) $this->configuration['itemsPerPage'];
+			$this->offset = $this->itemsPerPage * ($this->currentPage - 1);
+			if (\is_array($this->objects)) {
+				$modifiedObjects = [];
+				for ($index = $this->offset; $index < $this->offset + $this->itemsPerPage; $index++) {
+					if (isset($this->objects[$index])) {
+						$modifiedObjects[] = $this->objects[$index];
+					} else {
+						break;
+					}
+				}
+			} else {
+				$query = $this->objects->getQuery();
+				$query->setLimit($this->itemsPerPage);
+				if ($this->currentPage > 1) {
+					$query->setOffset($this->offset);
+				}
+
+				$modifiedObjects = $query->execute();
+			}
+		}
+		$this->view->assign(
+			'contentArguments', [
+				$this->widgetConfiguration['as'] => $modifiedObjects
+			]
+		);
+		$this->view->assign('configuration', $this->configuration);
+		$this->view->assign('pagination', $this->buildPagination());
+	}
+
+	/**
+	 * Returns an array with the keys "pages", "current", "numberOfPages",
+	 * "nextPage" & "previousPage"
+	 *
+	 * @return array
+	 */
+	protected function buildPagination() {
+		$pagination = parent::buildPagination();
+		$pagination['totalObjects'] = \count($this->objects);
+		return $pagination;
+	}
+}
diff --git a/Classes/ViewHelpers/Backend/Widget/PaginateViewHelper.php b/Classes/ViewHelpers/Backend/Widget/PaginateViewHelper.php
new file mode 100644
index 00000000..db62e1b0
--- /dev/null
+++ b/Classes/ViewHelpers/Backend/Widget/PaginateViewHelper.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace SGalinski\SgJobs\ViewHelpers\Backend\Widget;
+
+/***************************************************************
+ *  Copyright notice
+ *
+ *  (c) sgalinski Internet Services (https://www.sgalinski.de)
+ *
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+use SGalinski\SgJobs\ViewHelpers\Backend\Widget\Controller\PaginateController;
+use TYPO3\CMS\Fluid\Core\Widget\AbstractWidgetViewHelper;
+
+/**
+ * Class PaginateViewHelper
+ *
+ * @package SGalinski\SgJobs\ViewHelpers\Backend\Widget
+ */
+class PaginateViewHelper extends AbstractWidgetViewHelper {
+	/**
+	 * @var PaginateController
+	 */
+	protected $controller;
+
+	/**
+	 * Initializes the controller
+	 *
+	 * @param PaginateController $controller
+	 */
+	public function injectPaginateController(PaginateController $controller): void {
+		$this->controller = $controller;
+	}
+
+	/**
+	 * Register the ViewHelper arguments
+	 */
+	public function initializeArguments(): void {
+		parent::initializeArguments();
+		$this->registerArgument('objects', 'array', 'The objects to paginate', TRUE);
+		$this->registerArgument('as', 'string', 'The name of the variable inside the pagination', TRUE);
+		$this->registerArgument(
+			'configuration',
+			'array',
+			'The configuration of the pagination',
+			FALSE,
+			[
+				'itemsPerPage' => 10,
+				'insertAbove' => FALSE,
+				'insertBelow' => TRUE,
+				'recordsLabel' => ''
+			]
+		);
+	}
+
+	/**
+	 * Renders the paginator
+	 *
+	 * @return string
+	 */
+	public function render(): string {
+		return $this->initiateSubRequest();
+	}
+}
diff --git a/Classes/ViewHelpers/Widget/UriViewHelper.php b/Classes/ViewHelpers/Widget/UriViewHelper.php
new file mode 100644
index 00000000..e33ba7e3
--- /dev/null
+++ b/Classes/ViewHelpers/Widget/UriViewHelper.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace SGalinski\SgJobs\ViewHelpers\Widget;
+
+/***************************************************************
+ *  Copyright notice
+ *
+ *  (c) sgalinski Internet Services (https://www.sgalinski.de)
+ *
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext;
+use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
+use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
+use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
+
+/**
+ * Class UriViewHelper
+ */
+class UriViewHelper extends AbstractViewHelper {
+
+	use CompileWithRenderStatic;
+
+	/**
+	 * @var boolean
+	 */
+	protected $escapeOutput = FALSE;
+
+	/**
+	 * @var boolean
+	 */
+	protected $escapeChildren = FALSE;
+
+	/**
+	 * Initialize arguments
+	 */
+	public function initializeArguments(): void {
+		$this->registerArgument('addQueryStringMethod', 'string', 'Method to be used for query string');
+		$this->registerArgument('action', 'string', 'Target action');
+		$this->registerArgument('arguments', 'array', 'Arguments', FALSE, []);
+		$this->registerArgument('section', 'string', 'The anchor to be added to the URI', FALSE, '');
+		$this->registerArgument('format', 'string', 'The requested format, e.g. ".html', FALSE, '');
+		$this->registerArgument(
+			'ajax', 'bool', 'TRUE if the URI should be to an AJAX widget, FALSE otherwise.', FALSE, FALSE
+		);
+	}
+
+	/**
+	 * @param array $arguments
+	 * @param \Closure $renderChildrenClosure
+	 * @param RenderingContextInterface $renderingContext
+	 * @return string
+	 */
+	public static function renderStatic(
+		array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext
+	): string {
+		$ajax = $arguments['ajax'];
+		if ($ajax === TRUE) {
+			return static::getAjaxUri($renderingContext, $arguments);
+		}
+		return static::getWidgetUri($renderingContext, $arguments);
+	}
+
+	/**
+	 * Render the Uri.
+	 *
+	 * @return string The rendered link
+	 * @api
+	 */
+	public function render(): string {
+		$ajax = $this->arguments['ajax'];
+
+		if ($ajax === TRUE) {
+			return static::getAjaxUri($this->renderingContext, $this->arguments);
+		}
+		return static::getWidgetUri($this->renderingContext, $this->arguments);
+	}
+
+	/**
+	 * Get the URI for an AJAX Request.
+	 *
+	 * @param RenderingContextInterface $renderingContext
+	 * @param array $arguments
+	 * @return string the AJAX URI
+	 */
+	protected static function getAjaxUri(RenderingContextInterface $renderingContext, array $arguments): string {
+		/** @var ControllerContext $controllerContext */
+		$controllerContext = $renderingContext->getControllerContext();
+		$action = $arguments['action'];
+		$arguments = $arguments['arguments'];
+		if ($action === NULL) {
+			$action = $controllerContext->getRequest()->getControllerActionName();
+		}
+
+		$arguments['id'] = $GLOBALS['TSFE']->id;
+		// @todo page type should be configurable
+		$arguments['type'] = 7076;
+		$arguments['fluid-widget-id'] = $controllerContext->getRequest()->getWidgetContext()->getAjaxWidgetIdentifier();
+		$arguments['action'] = $action;
+		return '?' . http_build_query($arguments, NULL, '&');
+	}
+
+	/**
+	 * Get the URI for a non-AJAX Request.
+	 *
+	 * @param RenderingContextInterface $renderingContext
+	 * @param array $arguments
+	 * @return string the Widget URI
+	 */
+	protected static function getWidgetUri(RenderingContextInterface $renderingContext, array $arguments): string {
+		/** @var ControllerContext $controllerContext */
+		$controllerContext = $renderingContext->getControllerContext();
+		$uriBuilder = $controllerContext->getUriBuilder();
+		$argumentPrefix = $controllerContext->getRequest()->getArgumentPrefix();
+		$parentNamespace = $controllerContext->getRequest()->getWidgetContext()->getParentPluginNamespace();
+		$parentArguments = GeneralUtility::_GP($parentNamespace);
+		$allArguments = [$argumentPrefix => $arguments['arguments']] ?? [];
+		if ($parentArguments && isset($parentArguments['filters'])) {
+			$allArguments[$parentNamespace . '[filters]'] = $parentArguments['filters'];
+		}
+
+		if ($arguments['action'] ?? FALSE) {
+			$allArguments[$argumentPrefix]['action'] = $arguments['action'];
+		}
+
+		if (($arguments['format'] ?? '') !== '') {
+			$allArguments[$argumentPrefix]['format'] = $arguments['format'];
+		}
+
+		return $uriBuilder->reset()
+			->setArguments($allArguments)
+			->setSection($arguments['section'])
+			->setAddQueryString(TRUE)
+			->setAddQueryStringMethod($arguments['addQueryStringMethod'] ?? '')
+			->setArgumentsToBeExcludedFromQueryString([$argumentPrefix, 'cHash'])
+			->setFormat($arguments['format'])
+			->build();
+	}
+}
diff --git a/Resources/Private/Backend/Layouts/Default.html b/Resources/Private/Backend/Layouts/Default.html
index b9a09ad4..fa390fae 100644
--- a/Resources/Private/Backend/Layouts/Default.html
+++ b/Resources/Private/Backend/Layouts/Default.html
@@ -35,7 +35,7 @@
 			<h1>
 				<f:render section="headline" />
 			</h1>
-			<f:render section="content" />
+			<f:render section="main" />
 		</div>
 	</div>
 </f:be.container>
diff --git a/Resources/Private/Backend/Partials/JobList.html b/Resources/Private/Backend/Partials/JobList.html
deleted file mode 100644
index ccaf4f27..00000000
--- a/Resources/Private/Backend/Partials/JobList.html
+++ /dev/null
@@ -1,36 +0,0 @@
-{namespace sg=SGalinski\SgJobs\ViewHelpers}
-{namespace be=TYPO3\CMS\Backend\ViewHelpers}
-
-<p>
-	<f:translate key="backend.message.sorting" />
-</p>
-
-<f:if condition="{manualSortingDestroysEverything}">
-	<h2><f:translate key="backend.manualSortingBug" /></h2>
-</f:if>
-
-<div class="panel panel-default recordlist">
-	<div class="table-fit">
-		<f:be.widget.paginate objects="{jobs}" as="paginatedJobs" configuration="{insertAbove: 1, itemsPerPage: 20}">
-			<table data-table="tx_sgjobs_domain_model_job" class="table table-striped table-hover">
-				<tbody>
-					<f:for each="{paginatedJobs}" as="job">
-						<tr data-uid="{job.uid}">
-							<td nowrap="nowrap" class="col-icon">
-								<core:icon identifier="tcarecords-tx_sgjobs_domain_model_job-default"></core:icon>
-							</td>
-							<td style="white-space: normal;">
-								<be:link.editRecord uid="{job.uid}" table="tx_sgjobs_domain_model_job">
-									<span>{job.title} -  {job.company.name}, {job.company.city}</span>
-								</be:link.editRecord>
-							</td>
-							<td nowrap="nowrap" class="col-control">
-								<f:format.raw><sg:backend.control table="tx_sgjobs_domain_model_job" row="{job}" sortingData="{sortingData}"/></f:format.raw>
-							</td>
-						</tr>
-					</f:for>
-				</tbody>
-			</table>
-		</f:be.widget.paginate>
-	</div>
-</div>
diff --git a/Resources/Private/Backend/Templates/Index.html b/Resources/Private/Backend/Templates/Index.html
index fbfb7918..55343fa2 100644
--- a/Resources/Private/Backend/Templates/Index.html
+++ b/Resources/Private/Backend/Templates/Index.html
@@ -1,62 +1,63 @@
 {namespace sg=SGalinski\SgJobs\ViewHelpers}
+{namespace be=TYPO3\CMS\Backend\ViewHelpers}
 
-<f:layout name="Default" />
+<f:layout name="Default"/>
 
 <f:section name="iconButtons">
 </f:section>
 
 <f:section name="headline">
-	<f:translate key="backend.jobs.header" />
+	<f:translate key="backend.jobs.header"/>
 </f:section>
-<f:section name="content">
+
+<f:section name="main">
 	<f:flashMessages />
-	<f:if condition="{pageUid}">
+	<f:if condition="{jobs}">
 		<f:then>
-			<f:if condition="{docHeader}">
-				<f:then>
-					<f:if condition="{noRecords}">
-						<f:then>
-							<f:if condition="{locationOptions}">
-								<f:then>
-									<f:render partial="Filter" arguments="{filters: filters, locationOptions: locationOptions}" />
-									<f:render partial="CreateJob" arguments="{pageUid:pageUid}" />
-									<p>
-										<f:translate key="backend.noJobsMessage" />
-									</p>
-								</f:then>
-								<f:else>
-									<f:render partial="SelectRoot" arguments="{pages: pages}" />
-									<f:render partial="CreateJob" arguments="{pageUid:pageUid}" />
-								</f:else>
-							</f:if>
-						</f:then>
-						<f:else>
-							<f:render partial="Filter" arguments="{filters: filters, locationOptions: locationOptions}" />
-							<f:render partial="CreateJob" arguments="{pageUid:pageUid}" />
-							<f:if condition="{jobs}">
-								<f:then>
-									<f:render partial="JobList" arguments="{jobs: jobs, manualSortingDestroysEverything: manualSortingDestroysEverything}" />
-								</f:then>
-								<f:else>
-									<p>
-										<f:translate key="backend.noJobsMessage" />
-									</p>
-								</f:else>
-							</f:if>
-						</f:else>
-					</f:if>
-				</f:then>
-				<f:else>
-					<f:render partial="Error" />
-				</f:else>
+			<f:render partial="Filter" arguments="{_all}"/>
+			<f:render partial="CreateJob" arguments="{_all}"/>
+
+			<p>
+				<f:translate key="backend.message.sorting"/>
+			</p>
+
+			<f:if condition="{manualSortingDestroysEverything}">
+				<h2>
+					<f:translate key="backend.manualSortingBug"/>
+				</h2>
 			</f:if>
+
+			<div class="panel panel-default recordlist">
+				<div class="table-fit">
+					<table data-table="tx_sgjobs_domain_model_job" class="table table-striped table-hover">
+						<sg:backend.widget.paginate objects="{jobs}" as="paginatedJobs" configuration="{insertAbove: 1, itemsPerPage: 20}">
+							<tbody>
+							<f:for each="{paginatedJobs}" as="job">
+								<tr data-uid="{job.uid}">
+									<td nowrap="nowrap" class="col-icon">
+										<f:format.raw><sg:backend.icon table="tx_sgjobs_domain_model_job" row="{job}" /></f:format.raw>
+									</td>
+									<td style="white-space: normal;">
+										<be:link.editRecord uid="{job.uid}" table="tx_sgjobs_domain_model_job">
+											<span>{job.title} -  {job.company.name}, {job.company.city}</span>
+										</be:link.editRecord>
+									</td>
+									<td nowrap="nowrap" class="col-control">
+										<f:format.raw><sg:backend.control table="tx_sgjobs_domain_model_job" row="{job}" /></f:format.raw>
+									</td>
+								</tr>
+							</f:for>
+							</tbody>
+						</sg:backend.widget.paginate>
+					</table>
+				</div>
+			</div>
 		</f:then>
 		<f:else>
-			<f:render partial="SelectRoot" arguments="{pages: pages}" />
+			<f:render partial="SelectRoot" arguments="{pages: pages}"/>
 			<f:if condition="{isAdmin}">
-				<f:render partial="CreateJob" arguments="{pageUid:pageUid}" />
+				<f:render partial="CreateJob" arguments="{pageUid:pageUid}"/>
 			</f:if>
 		</f:else>
 	</f:if>
-
 </f:section>
diff --git a/Resources/Private/Backend/Templates/ViewHelpers/Backend/Widget/Paginate/Index.html b/Resources/Private/Backend/Templates/ViewHelpers/Backend/Widget/Paginate/Index.html
new file mode 100644
index 00000000..1dd3b4ac
--- /dev/null
+++ b/Resources/Private/Backend/Templates/ViewHelpers/Backend/Widget/Paginate/Index.html
@@ -0,0 +1,125 @@
+{namespace core=TYPO3\CMS\Core\ViewHelpers}
+{namespace sg=SGalinski\SgJobs\ViewHelpers}
+
+<f:if condition="{configuration.insertAbove}">
+	<thead>
+		<tr>
+			<td colspan="3">
+				<f:render section="paginator" arguments="{pagination: pagination, position:'top', recordsLabel: configuration.recordsLabel}" />
+			</td>
+		</tr>
+	</thead>
+</f:if>
+
+<f:renderChildren arguments="{contentArguments}" />
+
+<f:if condition="{configuration.insertBelow}">
+	<tfoot>
+		<tr>
+			<td colspan="3">
+				<f:render section="paginator" arguments="{pagination: pagination, position:'bottom', recordsLabel: configuration.recordsLabel}" />
+			</td>
+		</tr>
+	</tfoot>
+</f:if>
+
+<f:section name="paginator">
+	<nav class="pagination-wrap">
+		<ul class="pagination pagination-block">
+			<f:if condition="{pagination.hasLessPages}">
+				<f:then>
+					<li>
+						<a href="{sg:widget.uri(arguments:{currentPage: 1})}" title="{f:translate(key:'widget.pagination.first')}">
+							<core:icon identifier="actions-view-paging-first" />
+						</a>
+					</li>
+					<li>
+						<a href="{sg:widget.uri(arguments:{currentPage: pagination.previousPage})}" title="{f:translate(key:'widget.pagination.previous')}">
+							<core:icon identifier="actions-view-paging-previous" />
+						</a>
+					</li>
+				</f:then>
+				<f:else>
+					<li class="disabled">
+						<span>
+							<core:icon identifier="actions-view-paging-first" />
+						</span>
+					</li>
+					<li class="disabled">
+						<span>
+							<core:icon identifier="actions-view-paging-previous" />
+						</span>
+					</li>
+				</f:else>
+			</f:if>
+			<li>
+				<span>
+					<f:if condition="{recordsLabel}">
+						<f:then>
+							{recordsLabel}
+						</f:then>
+						<f:else>
+							<f:translate key="widget.pagination.records" />
+						</f:else>
+					</f:if>
+					{pagination.startRecord} - {pagination.endRecord} / {pagination.totalObjects}
+				</span>
+			</li>
+			<li>
+				<span>
+					<f:translate key="widget.pagination.page" />
+
+					<form id="paginator-form-{position}" onsubmit="goToPage{position}(this); return false;" style="display:inline;">
+					<script type="text/javascript">
+						function goToPage{position}(formObject) {
+							var url = '{sg:widget.uri(arguments:{currentPage: 987654321}) -> f:format.raw()}';
+							var page = formObject.elements['paginator-target-page'].value;
+							if (page > {pagination.numberOfPages}) {
+								page = {pagination.numberOfPages};
+							} else if (page < 1) {
+								page = 1;
+							}
+							url = url.replace('987654321', page);
+							self.location.href= url;
+						}
+					</script>
+					<f:form.textfield id="paginator-{position}" name="paginator-target-page" additionalAttributes="{min: '1'}" class="form-control input-sm paginator-input" size="5" value="{pagination.current}" type="number" />
+					</form>
+
+					/ {pagination.numberOfPages}
+				</span>
+			</li>
+			<f:if condition="{pagination.hasMorePages}">
+				<f:then>
+					<li>
+						<a href="{sg:widget.uri(arguments:{currentPage: pagination.nextPage})}" title="{f:translate(key:'widget.pagination.next')}">
+							<core:icon identifier="actions-view-paging-next" />
+						</a>
+					</li>
+					<li>
+						<a href="{sg:widget.uri(arguments:{currentPage: pagination.numberOfPages})}" title="{f:translate(key:'widget.pagination.last')}">
+							<core:icon identifier="actions-view-paging-last" />
+						</a>
+					</li>
+				</f:then>
+				<f:else>
+					<li class="disabled">
+						<span>
+							<core:icon identifier="actions-view-paging-next" />
+						</span>
+					</li>
+					<li class="disabled">
+						<span>
+							<core:icon identifier="actions-view-paging-last" />
+						</span>
+					</li>
+				</f:else>
+			</f:if>
+			<li>
+				<a href="{sg:widget.uri(arguments:{currentPage: pagination.current})}" title="{f:translate(key:'widget.pagination.refresh')}">
+					<core:icon identifier="actions-refresh" />
+				</a>
+			</li>
+		</ul>
+	</nav>
+</f:section>
-- 
GitLab