[FEATURE] Add pagination and filtering to the redirects module 85/58085/8
authorDaniel Goerz <daniel.goerz@posteo.de>
Thu, 30 Aug 2018 11:34:43 +0000 (13:34 +0200)
committerBenni Mack <benni@typo3.org>
Fri, 31 Aug 2018 12:06:47 +0000 (14:06 +0200)
Some minor usability Improvements have been made as well:
- The source path now crops after 100 characters
- The destination column now shows the Page ID if the target is a page
- Redirects are now sorted by source host (1st) and source path (2nd)

Resolves: #83749
Releases: master
Change-Id: I7369814676e851b35da674cc837b094693345650
Reviewed-on: https://review.typo3.org/58085
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Frans Saris <franssaris@gmail.com>
Tested-by: Frans Saris <franssaris@gmail.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
typo3/sysext/core/Documentation/Changelog/master/Feature-83749-FilteringAndPaginationInTheRedirectsModule.rst [new file with mode: 0644]
typo3/sysext/redirects/Classes/Controller/ManagementController.php
typo3/sysext/redirects/Classes/Repository/Demand.php [new file with mode: 0644]
typo3/sysext/redirects/Classes/Repository/RedirectRepository.php [new file with mode: 0644]
typo3/sysext/redirects/Classes/Service/RedirectCacheService.php
typo3/sysext/redirects/Classes/ViewHelpers/TargetPageIdViewHelper.php [new file with mode: 0644]
typo3/sysext/redirects/Resources/Private/Language/locallang_module_redirect.xlf
typo3/sysext/redirects/Resources/Private/Partials/Pagination.html [new file with mode: 0644]
typo3/sysext/redirects/Resources/Private/Templates/Management/Overview.html

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-83749-FilteringAndPaginationInTheRedirectsModule.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-83749-FilteringAndPaginationInTheRedirectsModule.rst
new file mode 100644 (file)
index 0000000..16768e8
--- /dev/null
@@ -0,0 +1,35 @@
+.. include:: ../../Includes.txt
+
+==================================================================
+Feature: #83749 - Filtering and Pagination in the redirects module
+==================================================================
+
+See :issue:`83749`
+
+Description
+===========
+
+The backend module "Redirects" received filtering and pagination to improve the overall usability.
+
+The list of redirects can be filtered by:
+- The source host
+- The source path
+- The destination (either the path or the Page ID)
+- The target status code
+
+All filters are concatenated by a logical AND.
+
+Pagination is set to 50 records per page.
+
+Some minor usability Improvements have been made as well:
+- The source path now crops after 100 characters to keep the table from expanding too much
+- The destination column now also shows the Page ID if the target is a page
+- Redirects are now sorted by source host (1st) and source path (2nd)
+
+
+Impact
+======
+
+With these improvements it is now possible to easily manage a big amount of redirect records.
+
+.. index:: Backend, ext:redirects
\ No newline at end of file
index 01acb14..d4779d2 100644 (file)
@@ -28,7 +28,8 @@ use TYPO3\CMS\Core\Imaging\IconFactory;
 use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
-use TYPO3\CMS\Redirects\Service\RedirectCacheService;
+use TYPO3\CMS\Redirects\Repository\Demand;
+use TYPO3\CMS\Redirects\Repository\RedirectRepository;
 use TYPO3\CMS\Redirects\Service\UrlService;
 use TYPO3Fluid\Fluid\View\ViewInterface;
 
@@ -92,23 +93,59 @@ class ManagementController
 
     /**
      * Show all redirects, and add a button to create a new redirect
+     * @param ServerRequestInterface $request
      */
-    protected function overviewAction()
+    protected function overviewAction(ServerRequestInterface $request)
     {
         $this->getButtons();
-
-        $redirects = GeneralUtility::makeInstance(RedirectCacheService::class)->getAllRedirects();
-        $defaultUrl = GeneralUtility::makeInstance(UrlService::class)->getDefaultUrl();
-        $showHitCounter = GeneralUtility::makeInstance(Features::class)->isFeatureEnabled('redirects.hitCount');
+        $demand = Demand::createFromRequest($request);
+        $redirectRepository = GeneralUtility::makeInstance(RedirectRepository::class, $demand);
+        $count = $redirectRepository->countRedirectsByByDemand();
 
         $this->view->assignMultiple([
-            'redirects' => $redirects,
-            'defaultUrl' => $defaultUrl,
-            'showHitCounter' => $showHitCounter,
+            'redirects' => $redirectRepository->findRedirectsByDemand(),
+            'hosts' => $redirectRepository->findHostsOfRedirects(),
+            'statusCodes' => $redirectRepository->findStatusCodesOfRedirects(),
+            'demand' => $demand,
+            'defaultUrl' => GeneralUtility::makeInstance(UrlService::class)->getDefaultUrl(),
+            'showHitCounter' => GeneralUtility::makeInstance(Features::class)->isFeatureEnabled('redirects.hitCount'),
+            'pagination' => $this->preparePagination($demand, $count),
         ]);
     }
 
     /**
+     * Prepares information for the pagination of the module
+     *
+     * @param Demand $demand
+     * @param int $count
+     * @return array
+     */
+    protected function preparePagination(Demand $demand, int $count): array
+    {
+        $numberOfPages = ceil($count / $demand->getLimit());
+        $endRecord = $demand->getOffset() + $demand->getLimit();
+        if ($endRecord > $count) {
+            $endRecord = $count;
+        }
+
+        $pagination = [
+            'current' => $demand->getPage(),
+            'numberOfPages' => $numberOfPages,
+            'hasLessPages' => $demand->getPage() > 1,
+            'hasMorePages' => $demand->getPage() < $numberOfPages,
+            'startRecord' => $demand->getOffset() + 1,
+            'endRecord' => $endRecord
+        ];
+        if ($pagination['current'] < $pagination['numberOfPages']) {
+            $pagination['nextPage'] = $pagination['current'] + 1;
+        }
+        if ($pagination['current'] > 1) {
+            $pagination['previousPage'] = $pagination['current'] - 1;
+        }
+        return $pagination;
+    }
+
+    /**
      * @param string $templateName
      */
     protected function initializeView(string $templateName)
diff --git a/typo3/sysext/redirects/Classes/Repository/Demand.php b/typo3/sysext/redirects/Classes/Repository/Demand.php
new file mode 100644 (file)
index 0000000..918329f
--- /dev/null
@@ -0,0 +1,215 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Redirects\Repository;
+
+/*
+ * 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!
+ */
+
+use Psr\Http\Message\ServerRequestInterface;
+
+/**
+ * Demand Object for filtering redirects in the backend module
+ * @internal
+ */
+class Demand
+{
+    /**
+     * @var string
+     */
+    protected $sourceHost;
+
+    /**
+     * @var string
+     */
+    protected $sourcePath;
+
+    /**
+     * @var string
+     */
+    protected $target;
+
+    /**
+     * @var int
+     */
+    protected $statusCode;
+
+    /**
+     * @var int
+     */
+    protected $limit = 50;
+
+    /**
+     * @var int
+     */
+    protected $page;
+
+    /**
+     * Demand constructor.
+     * @param int $page
+     * @param string $sourceHost
+     * @param string $sourcePath
+     * @param string $target
+     * @param int $statusCode
+     */
+    public function __construct(int $page = 1, string $sourceHost = '', string $sourcePath = '', string $target = '', int $statusCode = 0)
+    {
+        $this->page = $page;
+        $this->sourceHost = $sourceHost;
+        $this->sourcePath = $sourcePath;
+        $this->target = $target;
+        $this->statusCode = $statusCode;
+    }
+
+    /**
+     * Creates a Demand object from the current request.
+     *
+     * @param ServerRequestInterface $request
+     * @return Demand
+     */
+    public static function createFromRequest(ServerRequestInterface $request): Demand
+    {
+        $page = (int)($request->getQueryParams()['page'] ?? $request->getParsedBody()['page'] ?? 1);
+        $demand = $request->getQueryParams()['demand'] ?? $request->getParsedBody()['demand'];
+        if (empty($demand)) {
+            return new self($page);
+        }
+        $sourceHost = $demand['source_host'] ?? '';
+        $sourcePath = $demand['source_path'] ?? '';
+        $statusCode = (int)$demand['target_statuscode'] ?? 0;
+        $target = $demand['target'] ?? '';
+        return new self($page, $sourceHost, $sourcePath, $target, $statusCode);
+    }
+
+    /**
+     * @return string
+     */
+    public function getSourceHost(): string
+    {
+        return $this->sourceHost;
+    }
+
+    /**
+     * @return string
+     */
+    public function getSourcePath(): string
+    {
+        return $this->sourcePath;
+    }
+
+    /**
+     * @return string
+     */
+    public function getTarget(): string
+    {
+        return $this->target;
+    }
+
+    /**
+     * @return int
+     */
+    public function getLimit(): int
+    {
+        return $this->limit;
+    }
+
+    /**
+     * @return int
+     */
+    public function getStatusCode(): int
+    {
+        return $this->statusCode;
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasSourceHost(): bool
+    {
+        return $this->sourceHost !== '';
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasSourcePath(): bool
+    {
+        return $this->sourcePath !== '';
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasTarget(): bool
+    {
+        return $this->target !== '';
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasStatusCode(): bool
+    {
+        return $this->statusCode !== 0;
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasConstraints(): bool
+    {
+        return $this->hasSourcePath()
+            || $this->hasSourceHost()
+            || $this->hasTarget();
+    }
+
+    /**
+     * The current Page of the paginated redirects
+     *
+     * @return int
+     */
+    public function getPage(): int
+    {
+        return $this->page;
+    }
+
+    /**
+     * Offset for the current set of records
+     *
+     * @return int
+     */
+    public function getOffset(): int
+    {
+        return ($this->page - 1) * $this->limit;
+    }
+
+    /**
+     * @return array
+     */
+    public function getParameters(): array
+    {
+        $parameters = [];
+        if ($this->hasSourcePath()) {
+            $parameters['source_path'] = $this->sourcePath;
+        }
+        if ($this->hasSourceHost()) {
+            $parameters['source_host'] = $this->sourceHost;
+        }
+        if ($this->hasTarget()) {
+            $parameters['target'] = $this->target;
+        }
+        if ($this->hasStatusCode()) {
+            $parameters['target_statuscode'] = $this->target;
+        }
+        return $parameters;
+    }
+}
diff --git a/typo3/sysext/redirects/Classes/Repository/RedirectRepository.php b/typo3/sysext/redirects/Classes/Repository/RedirectRepository.php
new file mode 100644 (file)
index 0000000..76d8598
--- /dev/null
@@ -0,0 +1,159 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Redirects\Repository;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Query\QueryBuilder;
+use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Class for accessing redirect records from the database
+ */
+class RedirectRepository
+{
+    /**
+     * @var Demand
+     */
+    protected $demand;
+
+    /**
+     * With a possible demand object
+     *
+     * @param Demand|null $demand
+     */
+    public function __construct(Demand $demand = null)
+    {
+        $this->demand = $demand ?? new Demand();
+    }
+
+    /**
+     * Used within the backend module, which also includes the hidden records, but never deleted records.
+     *
+     * @return array
+     */
+    public function findRedirectsByDemand(): array
+    {
+        return $this->getQueryBuilderForDemand()
+            ->setMaxResults($this->demand->getLimit())
+            ->setFirstResult($this->demand->getOffset())
+            ->execute()
+            ->fetchAll();
+    }
+
+    /**
+     * @return int
+     */
+    public function countRedirectsByByDemand(): int
+    {
+        return $this->getQueryBuilderForDemand()->execute()->rowCount();
+    }
+
+    /**
+     * Prepares the QueryBuilder with Constraints from the Demand
+     *
+     * @return QueryBuilder
+     */
+    protected function getQueryBuilderForDemand(): QueryBuilder
+    {
+        $queryBuilder = $this->getQueryBuilder();
+        $queryBuilder
+            ->select('*')
+            ->from('sys_redirect')
+            ->orderBy('source_host')
+            ->addOrderBy('source_path');
+
+        $constraints = [];
+        if ($this->demand->hasSourceHost()) {
+            $constraints[] =$queryBuilder->expr()->eq(
+                'source_host',
+                $queryBuilder->createNamedParameter($this->demand->getSourceHost(), \PDO::PARAM_STR)
+            );
+        }
+
+        if ($this->demand->hasSourcePath()) {
+            $escapedLikeString = '%' . $queryBuilder->escapeLikeWildcards($this->demand->getSourcePath()) . '%';
+            $constraints[] = $queryBuilder->expr()->like(
+                'source_path',
+                $queryBuilder->createNamedParameter($escapedLikeString, \PDO::PARAM_STR)
+            );
+        }
+
+        if ($this->demand->hasTarget()) {
+            $escapedLikeString = '%' . $queryBuilder->escapeLikeWildcards($this->demand->getTarget()) . '%';
+            $constraints[] = $queryBuilder->expr()->like(
+                'target',
+                $queryBuilder->createNamedParameter($escapedLikeString, \PDO::PARAM_STR)
+            );
+        }
+
+        if ($this->demand->hasStatusCode()) {
+            $constraints[] =$queryBuilder->expr()->eq(
+                'target_statuscode',
+                $queryBuilder->createNamedParameter($this->demand->getStatusCode(), \PDO::PARAM_INT)
+            );
+        }
+
+        if (!empty($constraints)) {
+            $queryBuilder->where(...$constraints);
+        }
+        return $queryBuilder;
+    }
+
+    /**
+     * Used for the filtering in the backend
+     *
+     * @return array
+     */
+    public function findHostsOfRedirects(): array
+    {
+        return $this->getQueryBuilder()
+            ->select('source_host as name')
+            ->from('sys_redirect')
+            ->orderBy('source_host')
+            ->groupBy('source_host')
+            ->execute()
+            ->fetchAll();
+    }
+
+    /**
+     * Used for the filtering in the backend
+     *
+     * @return array
+     */
+    public function findStatusCodesOfRedirects(): array
+    {
+        return $this->getQueryBuilder()
+            ->select('target_statuscode as code')
+            ->from('sys_redirect')
+            ->orderBy('target_statuscode')
+            ->groupBy('target_statuscode')
+            ->execute()
+            ->fetchAll();
+    }
+
+    /**
+     * @return QueryBuilder
+     */
+    protected function getQueryBuilder(): QueryBuilder
+    {
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_redirect');
+        $queryBuilder->getRestrictions()
+            ->removeAll()
+            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+        return $queryBuilder;
+    }
+}
index 289e3c8..ec3783a 100644 (file)
@@ -88,31 +88,6 @@ class RedirectCacheService
     }
 
     /**
-     * Used within the backend module, which also includes the hidden records
-     * @return array
-     */
-    public function getAllRedirects(): array
-    {
-        $redirects = [];
-        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_redirect');
-        $queryBuilder->getRestrictions()->removeAll()
-            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
-        $statement = $queryBuilder
-            ->select('*')
-            ->from('sys_redirect')
-            ->execute();
-        while ($row = $statement->fetch()) {
-            $host = $row['source_host'] ?: '*';
-            if ($row['is_regexp']) {
-                $redirects[$host]['regexp'][$row['source_path']][$row['uid']] = $row;
-            } else {
-                $redirects[$host]['flat'][rtrim($row['source_path'], '/') . '/'][$row['uid']] = $row;
-            }
-        }
-        return $redirects;
-    }
-
-    /**
      * Flushes all redirects from the cache
      */
     protected function flush()
diff --git a/typo3/sysext/redirects/Classes/ViewHelpers/TargetPageIdViewHelper.php b/typo3/sysext/redirects/Classes/ViewHelpers/TargetPageIdViewHelper.php
new file mode 100644 (file)
index 0000000..32b5496
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Redirects\ViewHelpers;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Core\LinkHandling\Exception\UnknownUrnException;
+use TYPO3\CMS\Core\LinkHandling\LinkService;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
+use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
+use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
+
+/**
+ * The target of a redirect can contain a t3://page link.
+ * This ViewHelper checks for such a case and returns the Page ID
+ */
+class TargetPageIdViewHelper extends AbstractViewHelper
+{
+    use CompileWithRenderStatic;
+
+    /**
+     * Initializes the arguments
+     */
+    public function initializeArguments()
+    {
+        $this->registerArgument('target', 'string', 'The target of the redirect.', true);
+    }
+
+    /**
+     * Renders the page ID
+     *
+     * @param array $arguments
+     * @param \Closure $renderChildrenClosure
+     * @param RenderingContextInterface $renderingContext
+     * @return string
+     */
+    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string
+    {
+        if (!strpos($arguments['target'], 't3://page', 0) === 0) {
+            return '';
+        }
+
+        try {
+            $linkService = GeneralUtility::makeInstance(LinkService::class);
+            $resolvedLink = $linkService->resolveByStringRepresentation($arguments['target']);
+            return $resolvedLink['pageuid'] ?? '';
+        } catch (UnknownUrnException $e) {
+            return '';
+        }
+    }
+}
index a674ba5..2ab5406 100644 (file)
                                <source>Create new redirect</source>
                        </trans-unit>
 
+                       <trans-unit id="redirect_not_found_with_filter.title">
+                               <source>No redirects found!</source>
+                       </trans-unit>
+                       <trans-unit id="redirect_not_found_with_filter.message">
+                               <source>With the current set of filters applied, no redirect could be found.</source>
+                       </trans-unit>
+                       <trans-unit id="redirect_no_filter">
+                               <source>Remove all filter</source>
+                       </trans-unit>
+                       <trans-unit id="filter.source_host.showAll">
+                               <source>Show All</source>
+                       </trans-unit>
+                       <trans-unit id="filter.destination">
+                               <source>Destination (Path or Page ID)</source>
+                       </trans-unit>
+                       <trans-unit id="filter.targetStatusCode">
+                               <source>Status Code</source>
+                       </trans-unit>
+                       <trans-unit id="filter.sendButton">
+                               <source>Send</source>
+                       </trans-unit>
+
                        <trans-unit id="record_disabled">
                                <source>Redirect is not activated!</source>
                        </trans-unit>
@@ -48,6 +70,9 @@
                        <trans-unit id="destination">
                                <source>Destination</source>
                        </trans-unit>
+                       <trans-unit id="pageID">
+                               <source>Page ID</source>
+                       </trans-unit>
                        <trans-unit id="destination_status_code">
                                <source>Status Code</source>
                        </trans-unit>
                        <trans-unit id="hit_last_never">
                                <source>Never</source>
                        </trans-unit>
+                       <trans-unit id="visit_destination">
+                               <source>Go to</source>
+                       </trans-unit>
                </body>
        </file>
 </xliff>
diff --git a/typo3/sysext/redirects/Resources/Private/Partials/Pagination.html b/typo3/sysext/redirects/Resources/Private/Partials/Pagination.html
new file mode 100644 (file)
index 0000000..0db2569
--- /dev/null
@@ -0,0 +1,107 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
+                       data-namespace-typo3-fluid="true">
+
+<script type="text/javascript">
+       function goToPage(formObject) {
+               var formField = formObject.elements['paginator-target-page'];
+               var url = formField.dataset.url;
+               var numberOfPages = parseInt(formField.dataset.numberOfPages);
+               var page = parseInt(formField.value);
+               if (page > numberOfPages) {
+                       page = numberOfPages;
+               } else if (page < 1) {
+                       page = 1;
+               }
+               url = url.replace('987654322', page);
+               self.location.href = url;
+               return false;
+       }
+</script>
+
+<nav class="pagination-wrap">
+       <ul class="pagination pagination-block">
+               <f:if condition="{pagination.hasLessPages}">
+                       <f:then>
+                               <li>
+                                       <a href="{f:be.uri(route:'site_redirects', parameters: {action: 'overview', demand: demand.parameters, page: 1})}" title="{f:translate(extensionName: 'fluid', key:'widget.pagination.first')}">
+                                               <core:icon identifier="actions-view-paging-first" />
+                                       </a>
+                               </li>
+                               <li>
+                                       <a href="{f:be.uri(route:'site_redirects', parameters: {action: 'overview', demand: demand.parameters, page: pagination.previousPage})}" title="{f:translate(extensionName: 'fluid', 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>
+                                       Redirects
+                                       {pagination.startRecord} - {pagination.endRecord}
+                               </span>
+               </li>
+               <li>
+                               <span>
+                                       <f:translate extensionName="fluid" key="widget.pagination.page" />
+                                               <f:variable name="gotToPageUrl">
+                                                       <f:be.uri route="site_redirects" parameters="{action: 'overview', demand: demand.parameters, page: 987654322}" />
+                                               </f:variable>
+                                               <form onsubmit="return goToPage(this);" style="display:inline;">
+                                                       <f:form.textfield
+                                                               additionalAttributes="{min: 1, max: pagination.numberOfPages}"
+                                                               data="{number-of-pages: pagination.numberOfPages, url: gotToPageUrl}"
+                                                               name="paginator-target-page"
+                                                               class="form-control input-sm paginator-input"
+                                                               value="{pagination.current}"
+                                                               type="number" />
+                                               </form>
+                                       / {pagination.numberOfPages}
+                               </span>
+               </li>
+               <f:if condition="{pagination.hasMorePages}">
+                       <f:then>
+                               <li>
+                                       <a href="{f:be.uri(route:'site_redirects', parameters: {action: 'overview', demand: demand.parameters, page: pagination.nextPage})}" title="{f:translate(extensionName: 'fluid', key:'widget.pagination.next')}">
+                                               <core:icon identifier="actions-view-paging-next" />
+                                       </a>
+                               </li>
+                               <li>
+                                       <a href="{f:be.uri(route:'site_redirects', parameters: {action: 'overview', demand: demand.parameters, page: pagination.numberOfPages})}" title="{f:translate(extensionName: 'fluid', 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="{f:be.uri(route:'site_redirects', parameters: {action: 'overview', demand: demand.parameters, page: demand.page})}" title="{f:translate(extensionName: 'fluid', key:'widget.pagination.refresh')}">
+                               <core:icon identifier="actions-refresh" />
+                       </a>
+               </li>
+       </ul>
+</nav>
+
+</html>
index 7a6277e..8b50cc8 100644 (file)
@@ -9,20 +9,41 @@
 </f:section>
 
 <f:section name="content">
+
        <f:if condition="{redirects -> f:count()}">
-               <f:then><f:render section="table" arguments="{_all}" /></f:then>
+               <f:then>
+                       <f:render section="filter" arguments="{_all}" />
+                       <f:render section="table" arguments="{_all}" />
+               </f:then>
                <f:else>
-                       <f:be.infobox state="-1" title="{f:translate(key: 'LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:redirect_not_found.title')}">
-                               <p><f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:redirect_not_found.message"/></p>
-                               <be:link.newRecord class="btn btn-primary" table="sys_redirect">
-                                       <f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:redirect_create"/>
-                               </be:link.newRecord>
-                       </f:be.infobox>
+                       <f:if condition="{demand.constraints}">
+                               <f:then>
+                                       <f:render section="filter" arguments="{_all}" />
+                                       <f:be.infobox state="-2" title="{f:translate(key: 'LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:redirect_not_found_with_filter.title')}">
+                                               <p><f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:redirect_not_found_with_filter.message"/></p>
+                                               <a class="btn btn-default" href="{f:be.uri(route:'site_redirects', parameters: {action: 'overview'})}">
+                                                       <f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:redirect_no_filter"/>
+                                               </a>
+                                               <be:link.newRecord class="btn btn-primary" table="sys_redirect">
+                                                       <f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:redirect_create"/>
+                                               </be:link.newRecord>
+                                       </f:be.infobox>
+                               </f:then>
+                               <f:else>
+                                       <f:be.infobox state="-1" title="{f:translate(key: 'LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:redirect_not_found.title')}">
+                                               <p><f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:redirect_not_found.message"/></p>
+                                               <be:link.newRecord class="btn btn-primary" table="sys_redirect">
+                                                       <f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:redirect_create"/>
+                                               </be:link.newRecord>
+                                       </f:be.infobox>
+                               </f:else>
+                       </f:if>
                </f:else>
        </f:if>
 </f:section>
 
 <f:section name="table">
+       <f:render partial="Pagination" arguments="{_all}" />
        <div class="table-fit">
                <table class="table table-striped table-hover">
                        <thead>
                                </tr>
                        </thead>
                        <tbody>
-                               <f:for each="{redirects}" key="domainName" as="redirectsPerDomain">
-                                       <f:for each="{redirectsPerDomain}" as="groupedRedirects">
-                                               <f:for each="{groupedRedirects}" as="redirectRecords">
-                                                       <f:for each="{redirectRecords}" as="redirect">
-                                                               <tr>
-                                                                       <td>{redirect.source_host}</td>
-                                                                       <td>
-                                                                               <f:alias map="{
-                                                                                       disable: '{f:translate(key: \'LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:record_disabled\')}',
-                                                                                       onlystart: '{f:translate(key: \'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:start\')}: {f:format.date(date: redirect.starttime, format: \'d.m.Y H:i:s\')}',
-                                                                                       onlyend: '{f:translate(key: \'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:stop\')}: {f:format.date(date: redirect.endtime, format: \'d.m.Y H:i:s\')}',
-                                                                                       startend: '{f:translate(key: \'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:start\')}: {f:format.date(date: redirect.starttime, format: \'d.m.Y H:i:s\')} - {f:translate(key: \'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:stop\')}: {f:format.date(date: redirect.endtime, format: \'d.m.Y H:i:s\')}'
-                                                                               }">
-                                                                               <f:if condition="{redirect.disabled}"><f:then><span title="{disable}"></f:then>
-                                                                               <f:else if="{redirect.starttime} && !{redirect.endtime}"><span title="{onlystart}"></f:else>
-                                                                               <f:else if="!{redirect.starttime} && {redirect.endtime}"><span title="{onlyend}"></f:else>
-                                                                               <f:else if="{redirect.starttime} && {redirect.endtime}"><span title="{startend}"></f:else>
-                                                                               <f:else><span></f:else>
-                                                                               </f:if>
-                                                                               <core:iconForRecord table="sys_redirect" row="{redirect}" /></span>
-                                                                               </f:alias>
-                                                                               <be:link.editRecord table="sys_redirect" uid="{redirect.uid}" title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:edit')}">
-                                                                                       {redirect.source_path}
-                                                                               </be:link.editRecord>
-                                                                       </td>
-                                                                       <td><f:link.typolink parameter="{redirect.target}" target="_blank"><f:uri.typolink parameter="{redirect.target}"></f:uri.typolink></f:link.typolink> (<f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:destination_status_code"/>: {redirect.target_statuscode})</td>
-                                                                       <f:if condition="{showHitCounter}">
-                                                                               <td>
-                                                                                       <f:if condition="!{redirect.disable_hitcount}">
-                                                                                                       <f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:hit{f:if(condition:'{redirect.hitcount} > 1',then:'s')}_text" arguments="{0:redirect.hitcount}"/>
-                                                                                                       <f:if condition="{redirect.hitcount} != 0">
-                                                                                                       <a class="t3js-modal-trigger"
-                                                                                                          href="{rd:editRecord(command: 'resetcounter', uid: redirect.uid)}"
-                                                                                                          title="{f:translate(key: 'LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:hit_reset')}"
-                                                                                                          data-title="{f:translate(key: 'LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:hit_reset.confirm.title')}"
-                                                                                                          data-content="{f:translate(key: 'LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:hit_reset.confirm.content')}"
-                                                                                                          data-button-close-text="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:buttons.confirm.delete_record.no')}">
-                                                                                                       <core:icon identifier="actions-edit-restore" /></a>
-                                                                                               </f:if>
-                                                                                       </f:if>
-                                                                               <td>
-                                                                                       <f:if condition="{redirect.lasthiton}">
-                                                                                               <f:then><f:format.date format="d.m.Y H:i:s">@{redirect.lasthiton}</f:format.date></f:then>
-                                                                                               <f:else><f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:hit_last_never"/></f:else>
-                                                                                       </f:if>
-                                                                               </td>
+                               <f:for each="{redirects}" key="domainName" as="redirect">
+                                       <f:variable name="pageId" value="{rd:targetPageId(target:redirect.target)}" />
+                                       <f:variable name="targetUri" value="{f:uri.typolink(parameter:redirect.target)}" />
+                                       <tr>
+                                               <td>{redirect.source_host}</td>
+                                               <td>
+                                                       <f:alias map="{
+                                                               disable: '{f:translate(key: \'LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:record_disabled\')}',
+                                                               onlystart: '{f:translate(key: \'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:start\')}: {f:format.date(date: redirect.starttime, format: \'d.m.Y H:i:s\')}',
+                                                               onlyend: '{f:translate(key: \'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:stop\')}: {f:format.date(date: redirect.endtime, format: \'d.m.Y H:i:s\')}',
+                                                               startend: '{f:translate(key: \'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:start\')}: {f:format.date(date: redirect.starttime, format: \'d.m.Y H:i:s\')} - {f:translate(key: \'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:stop\')}: {f:format.date(date: redirect.endtime, format: \'d.m.Y H:i:s\')}'
+                                                       }">
+                                                       <f:if condition="{redirect.disabled}"><f:then><span title="{disable}"></f:then>
+                                                       <f:else if="{redirect.starttime} && !{redirect.endtime}"><span title="{onlystart}"></f:else>
+                                                       <f:else if="!{redirect.starttime} && {redirect.endtime}"><span title="{onlyend}"></f:else>
+                                                       <f:else if="{redirect.starttime} && {redirect.endtime}"><span title="{startend}"></f:else>
+                                                       <f:else><span></f:else>
+                                                       </f:if>
+                                                       <core:iconForRecord table="sys_redirect" row="{redirect}" /></span>
+                                                       </f:alias>
+                                                       <be:link.editRecord table="sys_redirect" uid="{redirect.uid}" title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:edit')}: {redirect.source_path}">
+                                                               {redirect.source_path -> f:format.crop(maxCharacters:100)}
+                                                       </be:link.editRecord>
+                                               </td>
+                                               <td><f:link.typolink parameter="{redirect.target}" target="_blank" title="{f:translate(key: 'LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:visit_destination')} {targetUri}">{targetUri}</f:link.typolink>
+                                                       (<f:if condition="{pageId}"><f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:pageID"/>: {pageId},
+                                                       </f:if><f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:destination_status_code"/>: {redirect.target_statuscode})</td>
+                                               <f:if condition="{showHitCounter}">
+                                                       <td>
+                                                               <f:if condition="!{redirect.disable_hitcount}">
+                                                                               <f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:hit{f:if(condition:'{redirect.hitcount} > 1',then:'s')}_text" arguments="{0:redirect.hitcount}"/>
+                                                                               <f:if condition="{redirect.hitcount} != 0">
+                                                                               <a class="t3js-modal-trigger"
+                                                                                        href="{rd:editRecord(command: 'resetcounter', uid: redirect.uid)}"
+                                                                                        title="{f:translate(key: 'LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:hit_reset')}"
+                                                                                        data-title="{f:translate(key: 'LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:hit_reset.confirm.title')}"
+                                                                                        data-content="{f:translate(key: 'LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:hit_reset.confirm.content')}"
+                                                                                        data-button-close-text="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:buttons.confirm.delete_record.no')}">
+                                                                               <core:icon identifier="actions-edit-restore" /></a>
                                                                        </f:if>
-                                                                       <td>
-                                                                               <div class="btn-group">
-                                                                                       <f:if condition="{redirect.is_regexp}">
-                                                                                               <f:then>
-                                                                                                       <span class="btn btn-default disabled"><core:icon identifier="empty-empty" /></span>
-                                                                                               </f:then>
-                                                                                               <f:else>
-                                                                                                       <f:link.external class="btn btn-default" uri="{f:if(condition: '{redirect.source_host} == \'*\'', then: defaultUrl, else: redirect.source_host)}{redirect.source_path}" target="_blank">
-                                                                                                               <core:icon identifier="actions-view-page" />
-                                                                                                       </f:link.external>
-                                                                                               </f:else>
-                                                                                       </f:if>
-                                                                                       <be:link.editRecord class="btn btn-default" table="sys_redirect" uid="{redirect.uid}" title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:edit')}">
-                                                                                               <core:icon identifier="actions-open" />
-                                                                                       </be:link.editRecord>
-                                                                                       <f:if condition="{redirect.disabled} == 1">
-                                                                                               <f:then>
-                                                                                                       <a class="btn btn-default" href="{rd:editRecord(command: 'unhide', uid: redirect.uid)}" title="{f:translate(key:'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:unHide')}"><core:icon identifier="actions-edit-unhide" /></a>
-                                                                                               </f:then>
-                                                                                               <f:else>
-                                                                                                       <a class="btn btn-default" href="{rd:editRecord(command: 'hide', uid: redirect.uid)}" title="{f:translate(key:'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:hide')}"><core:icon identifier="actions-edit-hide" /></a>
-                                                                                               </f:else>
-                                                                                       </f:if>
-                                                                                       <a class="btn btn-default t3js-modal-trigger"
-                                                                                                href="{rd:editRecord(command: 'delete', uid: redirect.uid)}"
-                                                                                                title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:delete')}"
-                                                                                                data-severity="warning"
-                                                                                                data-title="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:label.confirm.delete_record.title')}"
-                                                                                                data-content="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:deleteWarning')}"
-                                                                                                data-button-close-text="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:buttons.confirm.delete_record.no')}">
-                                                                                               <core:icon identifier="actions-delete" />
-                                                                                       </a>
-                                                                               </div>
-                                                                       </td>
-                                                               </tr>
-                                                       </f:for>
-                                               </f:for>
-                                       </f:for>
+                                                               </f:if>
+                                                       <td>
+                                                               <f:if condition="{redirect.lasthiton}">
+                                                                       <f:then><f:format.date format="d.m.Y H:i:s">@{redirect.lasthiton}</f:format.date></f:then>
+                                                                       <f:else><f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:hit_last_never"/></f:else>
+                                                               </f:if>
+                                                       </td>
+                                               </f:if>
+                                               <td>
+                                                       <div class="btn-group">
+                                                               <f:if condition="{redirect.is_regexp}">
+                                                                       <f:then>
+                                                                               <span class="btn btn-default disabled"><core:icon identifier="empty-empty" /></span>
+                                                                       </f:then>
+                                                                       <f:else>
+                                                                               <f:link.external class="btn btn-default" uri="{f:if(condition: '{redirect.source_host} == \'*\'', then: defaultUrl, else: redirect.source_host)}{redirect.source_path}" target="_blank">
+                                                                                       <core:icon identifier="actions-view-page" />
+                                                                               </f:link.external>
+                                                                       </f:else>
+                                                               </f:if>
+                                                               <be:link.editRecord class="btn btn-default" table="sys_redirect" uid="{redirect.uid}" title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:edit')}">
+                                                                       <core:icon identifier="actions-open" />
+                                                               </be:link.editRecord>
+                                                               <f:if condition="{redirect.disabled} == 1">
+                                                                       <f:then>
+                                                                               <a class="btn btn-default" href="{rd:editRecord(command: 'unhide', uid: redirect.uid)}" title="{f:translate(key:'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:unHide')}"><core:icon identifier="actions-edit-unhide" /></a>
+                                                                       </f:then>
+                                                                       <f:else>
+                                                                               <a class="btn btn-default" href="{rd:editRecord(command: 'hide', uid: redirect.uid)}" title="{f:translate(key:'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:hide')}"><core:icon identifier="actions-edit-hide" /></a>
+                                                                       </f:else>
+                                                               </f:if>
+                                                               <a class="btn btn-default t3js-modal-trigger"
+                                                                        href="{rd:editRecord(command: 'delete', uid: redirect.uid)}"
+                                                                        title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:delete')}"
+                                                                        data-severity="warning"
+                                                                        data-title="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:label.confirm.delete_record.title')}"
+                                                                        data-content="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:deleteWarning')}"
+                                                                        data-button-close-text="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:buttons.confirm.delete_record.no')}">
+                                                                       <core:icon identifier="actions-delete" />
+                                                               </a>
+                                                       </div>
+                                               </td>
+                                       </tr>
                                </f:for>
                        </tbody>
                </table>
        </div>
+       <f:render partial="Pagination" arguments="{_all}" />
+</f:section>
+
+<f:section name="filter">
+       <form action="{f:be.uri(route:'site_redirects', parameters: {action: 'overview'})}"
+               method="post"
+               enctype="multipart/form-data"
+               name="demand"
+               class="form-inline form-inline-spaced">
+
+               <div class="form-group">
+                       <label for="demand-source-host"><f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:source_host"/></label>
+                       <select id="demand-source-host" class="form-control input-sm" name="demand[source_host]" onchange="submit()">
+                               <option value=""><f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:filter.source_host.showAll"/></option>
+                               <f:for each="{hosts}" as="host">
+                                       <f:form.select.option value="{host.name}" selected="{host.name} === {demand.sourceHost}">{host.name}</f:form.select.option>
+                               </f:for>
+                       </select>
+               </div>
+               <div class="form-group">
+                       <label for="demand-source-path"><f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:source_path"/></label>
+                       <input type="text" id="demand-source-path" class="form-control input-sm" name="demand[source_path]" value="{demand.sourcePath}"/>
+               </div>
+               <div class="form-group">
+                       <label for="demand-target"><f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:filter.destination"/></label>
+                       <input type="text" id="demand-target" class="form-control input-sm" name="demand[target]" value="{demand.target}"/>
+               </div>
+               <div class="form-group">
+                       <label for="demand-target-status-code"><f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:filter.targetStatusCode"/></label>
+                       <select id="demand-target-status-code" class="form-control input-sm" name="demand[target_statuscode]" onchange="submit()">
+                               <option value=""><f:translate key="LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:filter.source_host.showAll"/></option>
+                               <f:for each="{statusCodes}" as="statusCode">
+                                       <f:form.select.option value="{statusCode.code}" selected="{statusCode.code} === {demand.statusCode}">{statusCode.code}</f:form.select.option>
+                               </f:for>
+                       </select>
+               </div>
+               <div class="form-group">
+                       <input type="submit" value="{f:translate(key: 'LLL:EXT:redirects/Resources/Private/Language/locallang_module_redirect.xlf:filter.sendButton')}" class="btn btn-default btn-sm" />
+               </div>
+       </form>
 </f:section>
 </html>