mirror of
https://github.com/werkraum-media/events.git
synced 2025-03-26 15:43:48 +01:00
Support pagination with search parameters
Ensure the parameters are passed on to new generated links. Cover things with tests. Relates: #11574
This commit is contained in:
parent
56d922cb3a
commit
4e1d158923
24 changed files with 757 additions and 17 deletions
Classes
Configuration
Documentation
Tests/Functional/Frontend
Fixtures
Database
Extensions
ce_filter
Configuration/TypoScript
Resources/Private/Templates/Date
composer.jsonext_emconf.phpext_localconf.phpce_list
example
Sites/default
|
@ -7,6 +7,7 @@ namespace WerkraumMedia\Events\Controller;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
use TYPO3\CMS\Core\Http\PropagateResponseException;
|
||||||
use TYPO3\CMS\Extbase\Annotation as Extbase;
|
use TYPO3\CMS\Extbase\Annotation as Extbase;
|
||||||
use TYPO3\CMS\Extbase\Service\ExtensionService;
|
use TYPO3\CMS\Extbase\Service\ExtensionService;
|
||||||
use WerkraumMedia\Events\Domain\Model\Date;
|
use WerkraumMedia\Events\Domain\Model\Date;
|
||||||
|
@ -38,13 +39,13 @@ final class DateController extends AbstractController
|
||||||
{
|
{
|
||||||
parent::initializeAction();
|
parent::initializeAction();
|
||||||
|
|
||||||
|
$this->handlePostRequests();
|
||||||
|
|
||||||
$contentObject = $this->request->getAttribute('currentContentObject');
|
$contentObject = $this->request->getAttribute('currentContentObject');
|
||||||
if ($contentObject !== null) {
|
if ($contentObject !== null) {
|
||||||
$this->demandFactory->setContentObjectRenderer($contentObject);
|
$this->demandFactory->setContentObjectRenderer($contentObject);
|
||||||
}
|
}
|
||||||
$this->dataProcessing->setConfigurationManager($this->configurationManager);
|
$this->dataProcessing->setConfigurationManager($this->configurationManager);
|
||||||
|
|
||||||
$this->handlePostRequest();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function listAction(
|
public function listAction(
|
||||||
|
@ -127,22 +128,34 @@ final class DateController extends AbstractController
|
||||||
*
|
*
|
||||||
* @see: https://en.wikipedia.org/wiki/Post/Redirect/Get
|
* @see: https://en.wikipedia.org/wiki/Post/Redirect/Get
|
||||||
*/
|
*/
|
||||||
private function handlePostRequest(): void
|
private function handlePostRequests(): void
|
||||||
{
|
{
|
||||||
if (
|
if ($this->request->getMethod() !== 'POST') {
|
||||||
$this->request->getMethod() === 'POST'
|
return;
|
||||||
&& $this->request->hasArgument('search')
|
}
|
||||||
&& is_array($this->request->getArgument('search'))
|
|
||||||
) {
|
$searchArguments = [];
|
||||||
$namespace = $this->extensionService->getPluginNamespace(null, null);
|
if ($this->request->hasArgument('search')) {
|
||||||
|
$searchArguments = $this->request->getArgument('search');
|
||||||
|
}
|
||||||
|
if (is_array($searchArguments) === false) {
|
||||||
|
$searchArguments = [];
|
||||||
|
}
|
||||||
|
$searchArguments = array_filter($searchArguments);
|
||||||
|
|
||||||
|
$parameter = [];
|
||||||
|
if ($searchArguments !== []) {
|
||||||
|
$parameter['search'] = $searchArguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
$namespace = $this->extensionService->getPluginNamespace(null, null);
|
||||||
|
|
||||||
|
throw new PropagateResponseException(
|
||||||
$this->redirectToUri($this->request->getAttribute('currentContentObject')->typoLink_URL([
|
$this->redirectToUri($this->request->getAttribute('currentContentObject')->typoLink_URL([
|
||||||
'parameter' => 't3://page?uid=current',
|
'parameter' => 't3://page?uid=current',
|
||||||
'additionalParams' => '&' . http_build_query([
|
'additionalParams' => '&' . http_build_query([$namespace => $parameter]),
|
||||||
$namespace => [
|
])),
|
||||||
'search' => array_filter($this->request->getArgument('search')),
|
303
|
||||||
],
|
);
|
||||||
]),
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Daniel Siepmann <daniel.siepmann@codappix.com>
|
||||||
|
*
|
||||||
|
* This program 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 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||||
|
* 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WerkraumMedia\Events\Middleware;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use TYPO3\CMS\Core\Routing\PageArguments;
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Right now I couldn't find any way to add the search parameter to the pagination via addQueryString.
|
||||||
|
* The issue is that these are not part of TYPO3 routing. Nested lists like categories also seem impossible to configure for routing.
|
||||||
|
*
|
||||||
|
* See corresponding core issue: https://forge.typo3.org/issues/105941
|
||||||
|
*/
|
||||||
|
final class AddSearchArgumentsToRouteArgumentsMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $routingPath = 'events/search',
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
RequestHandlerInterface $handler
|
||||||
|
): ResponseInterface {
|
||||||
|
$previousResult = $request->getAttribute('routing', null);
|
||||||
|
if ($previousResult instanceof PageArguments) {
|
||||||
|
$request = $request->withAttribute('routing', $this->extendPageArguments($previousResult));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extendPageArguments(PageArguments $pageArguments): PageArguments
|
||||||
|
{
|
||||||
|
$dynamicArguments = $pageArguments->getDynamicArguments();
|
||||||
|
|
||||||
|
if (ArrayUtility::isValidPath($dynamicArguments, $this->routingPath) === false) {
|
||||||
|
return $pageArguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
$routeArguments = ArrayUtility::setValueByPath(
|
||||||
|
$pageArguments->getRouteArguments(),
|
||||||
|
$this->routingPath,
|
||||||
|
ArrayUtility::getValueByPath($dynamicArguments, $this->routingPath)
|
||||||
|
);
|
||||||
|
$remainingArguments = ArrayUtility::removeByPath($dynamicArguments, $this->routingPath);
|
||||||
|
|
||||||
|
return new PageArguments(
|
||||||
|
$pageArguments->getPageId(),
|
||||||
|
$pageArguments->getPageType(),
|
||||||
|
$routeArguments,
|
||||||
|
$pageArguments->getStaticArguments(),
|
||||||
|
$remainingArguments,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,9 @@ routeEnhancers:
|
||||||
EventsPagination:
|
EventsPagination:
|
||||||
type: Plugin
|
type: Plugin
|
||||||
namespace: events
|
namespace: events
|
||||||
routePath: '/{localizedPage}-{currentPage}'
|
routePath: '/{localizedPage}-{currentPage}/{controller}'
|
||||||
|
defaults:
|
||||||
|
controller: 'Date'
|
||||||
aspects:
|
aspects:
|
||||||
localizedPage:
|
localizedPage:
|
||||||
type: LocaleModifier
|
type: LocaleModifier
|
||||||
|
|
19
Configuration/RequestMiddlewares.php
Normal file
19
Configuration/RequestMiddlewares.php
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use WerkraumMedia\Events\Middleware\AddSearchArgumentsToRouteArgumentsMiddleware;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'frontend' => [
|
||||||
|
'werkraummedia/events/add-search-arguments-to-route-arguments' => [
|
||||||
|
'target' => AddSearchArgumentsToRouteArgumentsMiddleware::class,
|
||||||
|
'after' => [
|
||||||
|
'typo3/cms-frontend/page-resolver',
|
||||||
|
],
|
||||||
|
'before' => [
|
||||||
|
'typo3/cms-frontend/page-argument-validator',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
|
@ -44,6 +44,9 @@ Fixes
|
||||||
* Ensure pagination settings are provided in expected type (int).
|
* Ensure pagination settings are provided in expected type (int).
|
||||||
TypoScript settings will always be strings, so we fix this with a proper type cast.
|
TypoScript settings will always be strings, so we fix this with a proper type cast.
|
||||||
|
|
||||||
|
* Fix broken pagination routing in combination with active search for TYPO3 12.4 and onwards.
|
||||||
|
See: :ref:`searchPagination`.
|
||||||
|
|
||||||
Tasks
|
Tasks
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
25
Documentation/Features/SearchPagination.rst
Normal file
25
Documentation/Features/SearchPagination.rst
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
.. index:: single: Search
|
||||||
|
.. _searchPagination:
|
||||||
|
|
||||||
|
Search Pagination
|
||||||
|
=================
|
||||||
|
|
||||||
|
The extension supports pagination of search results.
|
||||||
|
Please properly configure the system. Add the following `TYPO3_CONF_VARS` configuration:
|
||||||
|
|
||||||
|
.. code-block:: php
|
||||||
|
|
||||||
|
'FE' => [
|
||||||
|
'cacheHash' => [
|
||||||
|
'excludedParameters' => [
|
||||||
|
'^events[search]',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
Adopt the configuration to your own setup, e.g. change the `pluginNamespace` from `events` to whatever you've configured.
|
||||||
|
And ensure the involved plugins are excluded from caching (`USER_INT`).
|
||||||
|
|
||||||
|
The extension will assume `events[search]` as default namespace for search arguments.
|
||||||
|
Please make use of Services files and Dependency Injection to configure the custom
|
||||||
|
`AddSearchArgumentsToRouteArgumentsMiddleware` middleware with your own namespace.
|
57
Tests/Functional/Frontend/Fixtures/Database/SearchSetup.php
Normal file
57
Tests/Functional/Frontend/Fixtures/Database/SearchSetup.php
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'tt_content' => [
|
||||||
|
[
|
||||||
|
'pid' => '1',
|
||||||
|
'uid' => '1',
|
||||||
|
'CType' => 'cefilter_filter',
|
||||||
|
'header' => 'Search Form',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'pid' => '1',
|
||||||
|
'uid' => '2',
|
||||||
|
'CType' => 'celist_list',
|
||||||
|
'header' => 'Search Results',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'tx_events_domain_model_event' => [
|
||||||
|
[
|
||||||
|
'uid' => '1',
|
||||||
|
'pid' => '2',
|
||||||
|
'title' => 'Event one',
|
||||||
|
'teaser' => 'Some Teaser',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uid' => '2',
|
||||||
|
'pid' => '2',
|
||||||
|
'title' => 'Event two',
|
||||||
|
'teaser' => 'Another teaser',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'tx_events_domain_model_date' => [
|
||||||
|
[
|
||||||
|
'uid' => '1',
|
||||||
|
'pid' => '2',
|
||||||
|
'event' => '1',
|
||||||
|
'start' => '1661626800',
|
||||||
|
'end' => '1661632200',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uid' => '2',
|
||||||
|
'pid' => '2',
|
||||||
|
'event' => '1',
|
||||||
|
'start' => '1660158000',
|
||||||
|
'end' => '1660163400',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uid' => '3',
|
||||||
|
'pid' => '2',
|
||||||
|
'event' => '2',
|
||||||
|
'start' => '1661194800',
|
||||||
|
'end' => '1661200200',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
|
@ -0,0 +1,9 @@
|
||||||
|
tt_content.cefilter_filter.20 {
|
||||||
|
view {
|
||||||
|
pluginNamespace = events
|
||||||
|
}
|
||||||
|
|
||||||
|
persistence {
|
||||||
|
storagePid = 2
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-8 offset-md-4">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-body">
|
||||||
|
<f:form method="post" name="search" object="{demand}">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="searchword">Searchword</label>
|
||||||
|
<f:form.textfield type="text" class="form-control" id="searchword" property="searchword" value="{searchword}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="start">From:</label>
|
||||||
|
<f:form.textfield type="date" class="form-control" id="start" property="start" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="end">To:</label>
|
||||||
|
<f:form.textfield type="date" class="form-control" id="end" property="end" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<f:if condition="{categories}">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<f:for each="{categories}" as="category">
|
||||||
|
<div class="col-md-4 d-none d-lg-block">
|
||||||
|
<div class="form-check">
|
||||||
|
<f:form.checkbox class="form-check-input" property="userCategories" value="{category.uid}" id="check_{category.uid}"/>
|
||||||
|
<label class="form-check-label" for="check_{category.uid}">{category.title} {category.amountOfEvents}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</f:for>
|
||||||
|
</div>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<f:form.submit value="Search" class="btn btn-primary" />
|
||||||
|
</div>
|
||||||
|
</f:form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "werkraummedia/ce-events-filter",
|
||||||
|
"type": "typo3-cms-extension",
|
||||||
|
"description": "Content element: EXT:events filter",
|
||||||
|
"license": "GPL-2.0-or-later",
|
||||||
|
"version": "v1.0.0",
|
||||||
|
"require": {
|
||||||
|
"typo3/cms-core": "*",
|
||||||
|
"typo3/cms-extbase": "*"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"typo3/cms": {
|
||||||
|
"extension-key": "ce_filter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$EM_CONF['ce_filter'] = [
|
||||||
|
'title' => 'Events filter',
|
||||||
|
'description' => 'Content Element for \'Events\'',
|
||||||
|
'category' => 'plugin',
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'constraints' => [
|
||||||
|
'depends' => [
|
||||||
|
'typo3' => '*',
|
||||||
|
'events' => '*',
|
||||||
|
],
|
||||||
|
'conflicts' => [
|
||||||
|
],
|
||||||
|
'suggests' => [
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptSetup(
|
||||||
|
'@import "EXT:ce_filter/Configuration/TypoScript/Setup.typoscript"'
|
||||||
|
);
|
||||||
|
|
||||||
|
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
|
||||||
|
'CeFilter',
|
||||||
|
'filter',
|
||||||
|
[
|
||||||
|
\WerkraumMedia\Events\Controller\DateController::class => 'search',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
\WerkraumMedia\Events\Controller\DateController::class => 'search',
|
||||||
|
],
|
||||||
|
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||||
|
);
|
|
@ -0,0 +1,15 @@
|
||||||
|
tt_content.celist_list.20 {
|
||||||
|
view {
|
||||||
|
pluginNamespace = events
|
||||||
|
}
|
||||||
|
|
||||||
|
persistence {
|
||||||
|
storagePid = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
sortByDate = start
|
||||||
|
sortOrder = ASC
|
||||||
|
itemsPerPage = 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
|
||||||
|
data-namespace-typo3-fluid="true">
|
||||||
|
|
||||||
|
<nav role="navigation" aria-label="Pagination Navigation">
|
||||||
|
<ul class="pagination">
|
||||||
|
<f:if condition="{pagination.previousPageNumber}">
|
||||||
|
<li class="page-item">
|
||||||
|
<f:if condition="{pagination.previousPageNumber} > 1">
|
||||||
|
<f:then>
|
||||||
|
<a class="page-link"
|
||||||
|
href="{f:uri.action(addQueryString: 1, arguments: {currentPage: pagination.previousPageNumber})}"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
|
</a>
|
||||||
|
</f:then>
|
||||||
|
<f:else>
|
||||||
|
<a class="page-link" href="{f:uri.action(addQueryString: 1)}">
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
|
</a>
|
||||||
|
</f:else>
|
||||||
|
</f:if>
|
||||||
|
</li>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<f:if condition="{pagination.displayRangeStart} > 1">
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link"
|
||||||
|
href="{f:uri.action(addQueryString: 1)}"
|
||||||
|
aria-label="Goto Page 1"
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<f:if condition="{pagination.hasLessPages}">
|
||||||
|
<li class="page-item">
|
||||||
|
<span class="page-link">…</span>
|
||||||
|
</li>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<f:for each="{pagination.allPageNumbers}" as="page">
|
||||||
|
<f:if condition="{page} == {pagination.paginator.currentPageNumber}">
|
||||||
|
<f:then>
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link"
|
||||||
|
aria-label="Current Page {page}"
|
||||||
|
aria-current="true"
|
||||||
|
>{page}</span>
|
||||||
|
</li>
|
||||||
|
</f:then>
|
||||||
|
<f:else>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link"
|
||||||
|
href="{f:uri.action(addQueryString: 1, arguments: {currentPage: page})}"
|
||||||
|
aria-label="Goto Page {page}"
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</f:else>
|
||||||
|
</f:if>
|
||||||
|
</f:for>
|
||||||
|
|
||||||
|
<f:if condition="{pagination.hasMorePages}">
|
||||||
|
<li class="page-item">
|
||||||
|
<span class="page-link">…</span>
|
||||||
|
</li>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<f:if condition="{pagination.displayRangeEnd} < {pagination.lastPageNumber}">
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link"
|
||||||
|
href="{f:uri.action(addQueryString: 1, arguments: {currentPage: pagination.lastPageNumber})}"
|
||||||
|
aria-label="Goto Page {pagination.lastPageNumber}"
|
||||||
|
>
|
||||||
|
{pagination.lastPageNumber}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
<f:if condition="{pagination.nextPageNumber}">
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link"
|
||||||
|
href="{f:uri.action(addQueryString: 1, arguments: {currentPage: pagination.nextPageNumber})}"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</f:if>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
|
||||||
|
|
||||||
|
<f:if condition="{pagination.paginator.paginatedItems}">
|
||||||
|
<section class="events-list" data-uniform="height">
|
||||||
|
|
||||||
|
<f:for each="{pagination.paginator.paginatedItems}" as="date" iteration="index">
|
||||||
|
<div class="row mb-5">
|
||||||
|
<h3>
|
||||||
|
{date.event.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<f:format.crop maxCharacters="160" respectHtml="true"
|
||||||
|
respectWordBoundaries="true">
|
||||||
|
{date.event.teaser}</f:format.crop>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</f:for>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="events-list-pagination">
|
||||||
|
{f:render(partial: 'Pagination', arguments: {
|
||||||
|
pagination: pagination
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</f:if>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "werkraummedia/ce-list",
|
||||||
|
"type": "typo3-cms-extension",
|
||||||
|
"description": "Content element: EXT:events list",
|
||||||
|
"license": "GPL-2.0-or-later",
|
||||||
|
"version": "v1.0.0",
|
||||||
|
"require": {
|
||||||
|
"typo3/cms-core": "*",
|
||||||
|
"typo3/cms-extbase": "*"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"typo3/cms": {
|
||||||
|
"extension-key": "ce_list"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$EM_CONF['ce_list'] = [
|
||||||
|
'title' => 'Events list view',
|
||||||
|
'description' => 'Content Elements for \'Events\'',
|
||||||
|
'category' => 'plugin',
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'constraints' => [
|
||||||
|
'depends' => [
|
||||||
|
'typo3' => '*',
|
||||||
|
'events' => '*',
|
||||||
|
],
|
||||||
|
'conflicts' => [
|
||||||
|
],
|
||||||
|
'suggests' => [
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScriptSetup(
|
||||||
|
'@import "EXT:ce_list/Configuration/TypoScript/Setup.typoscript"'
|
||||||
|
);
|
||||||
|
|
||||||
|
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
|
||||||
|
'CeList',
|
||||||
|
'list',
|
||||||
|
[
|
||||||
|
\WerkraumMedia\Events\Controller\DateController::class => 'list',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
\WerkraumMedia\Events\Controller\DateController::class => 'list',
|
||||||
|
],
|
||||||
|
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||||
|
);
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Daniel Siepmann <daniel.siepmann@codappix.com>
|
||||||
|
*
|
||||||
|
* This program 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 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||||
|
* 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WerkraumMedia\EventsExample;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use TYPO3\CMS\Core\Context\Context;
|
||||||
|
use TYPO3\CMS\Core\Context\DateTimeAspect;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows us to set a specific DateTimeAspect for requests within tests.
|
||||||
|
*/
|
||||||
|
final class TestingDateTimeAspectMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Context $context,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
RequestHandlerInterface $handler
|
||||||
|
): ResponseInterface {
|
||||||
|
$testingDateAspect = $request->getAttribute('testingDateAspect');
|
||||||
|
if ($testingDateAspect instanceof DateTimeAspect) {
|
||||||
|
$this->context->setAspect('date', $testingDateAspect);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use WerkraumMedia\EventsExample\TestingDateTimeAspectMiddleware;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'frontend' => [
|
||||||
|
'werkraummedia/events/testing-date-time-aspect' => [
|
||||||
|
'target' => TestingDateTimeAspectMiddleware::class,
|
||||||
|
'before' => [
|
||||||
|
'typo3/cms-frontend/timetracker',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
||||||
|
|
||||||
|
return static function (ContainerConfigurator $containerConfigurator) {
|
||||||
|
$services = $containerConfigurator->services()
|
||||||
|
->defaults()
|
||||||
|
->autowire()
|
||||||
|
->autoconfigure()
|
||||||
|
;
|
||||||
|
|
||||||
|
$services->load('WerkraumMedia\\EventsExample\\', '../Classes/*');
|
||||||
|
};
|
|
@ -14,3 +14,6 @@ languages:
|
||||||
flag: us
|
flag: us
|
||||||
websiteTitle: ''
|
websiteTitle: ''
|
||||||
rootPageId: 1
|
rootPageId: 1
|
||||||
|
imports:
|
||||||
|
-
|
||||||
|
resource: 'EXT:events/Configuration/CeRouting.yaml'
|
||||||
|
|
141
Tests/Functional/Frontend/SearchTest.php
Normal file
141
Tests/Functional/Frontend/SearchTest.php
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace WerkraumMedia\Events\Tests\Functional\Frontend;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use TYPO3\CMS\Core\Context\DateTimeAspect;
|
||||||
|
use TYPO3\CMS\Core\Http\StreamFactory;
|
||||||
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||||
|
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
|
||||||
|
|
||||||
|
final class SearchTest extends AbstractFrontendTestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->testExtensionsToLoad = [
|
||||||
|
...$this->testExtensionsToLoad,
|
||||||
|
'typo3conf/ext/events/Tests/Functional/Frontend/Fixtures/Extensions/ce_filter/',
|
||||||
|
'typo3conf/ext/events/Tests/Functional/Frontend/Fixtures/Extensions/ce_list/',
|
||||||
|
];
|
||||||
|
|
||||||
|
ArrayUtility::mergeRecursiveWithOverrule($this->configurationToUseInTestInstance, [
|
||||||
|
'FE' => [
|
||||||
|
'cacheHash' => [
|
||||||
|
'excludedParameters' => [
|
||||||
|
'^events[search]',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->importPHPDataSet(__DIR__ . '/Fixtures/Database/SearchSetup.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function submittingPostWithoutSearchArgumentsRedirectsToGet(): void
|
||||||
|
{
|
||||||
|
$request = new InternalRequest('https://example.com/');
|
||||||
|
$request = $request->withMethod('POST');
|
||||||
|
$request = $request->withPageId(1);
|
||||||
|
|
||||||
|
$response = $this->executeFrontendSubRequest($request);
|
||||||
|
|
||||||
|
self::assertSame(303, $response->getStatusCode());
|
||||||
|
self::assertSame('http://example.com/', $response->getHeaderLine('location'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function submittingPostWithSearchArgumentsRedirectsToGet(): void
|
||||||
|
{
|
||||||
|
$request = new InternalRequest('https://example.com/');
|
||||||
|
$request = $request->withMethod('POST');
|
||||||
|
$request = $request->withPageId(1);
|
||||||
|
$request = $request->withBody((new StreamFactory())->createStream(http_build_query([
|
||||||
|
'events' => [
|
||||||
|
'search' => [
|
||||||
|
'searchword' => 'Event',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])));
|
||||||
|
|
||||||
|
$response = $this->executeFrontendSubRequest($request);
|
||||||
|
|
||||||
|
self::assertSame(303, $response->getStatusCode());
|
||||||
|
self::assertSame('http://example.com/?events%5Bsearch%5D%5Bsearchword%5D=Event', $response->getHeaderLine('location'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function submittedInputShownInForm(): void
|
||||||
|
{
|
||||||
|
$request = new InternalRequest('https://example.com/');
|
||||||
|
$request = $request->withQueryParams([
|
||||||
|
'events' => [
|
||||||
|
'search' => [
|
||||||
|
'searchword' => 'Event',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$request = $request->withPageId(1);
|
||||||
|
$response = $this->executeFrontendSubRequest($request);
|
||||||
|
$html = $response->getBody()->__toString();
|
||||||
|
|
||||||
|
self::assertSame(200, $response->getStatusCode());
|
||||||
|
self::assertStringContainsString('value="Event"', $html, 'Submitted value is not rendered within form');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function submittedInputIsKeptWithinPagination(): void
|
||||||
|
{
|
||||||
|
$request = new InternalRequest('https://example.com/');
|
||||||
|
$request = $request->withAttribute('testingDateAspect', new DateTimeAspect(new DateTimeImmutable('2022-08-10')));
|
||||||
|
$request = $request->withQueryParams([
|
||||||
|
'events' => [
|
||||||
|
'search' => [
|
||||||
|
'searchword' => 'Event',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$request = $request->withPageId(1);
|
||||||
|
$response = $this->executeFrontendSubRequest($request);
|
||||||
|
$html = $response->getBody()->__toString();
|
||||||
|
|
||||||
|
self::assertSame(200, $response->getStatusCode());
|
||||||
|
self::assertStringContainsString('Event one', $html);
|
||||||
|
self::assertStringContainsString('Current Page 1', $html);
|
||||||
|
self::assertStringContainsString('/page-2?events%5Bsearch%5D%5Bsearchword%5D=Event&cHash=41711281293c1c3a3aa161e96bbd4e98', $html);
|
||||||
|
self::assertStringNotContainsString('Event two', $html);
|
||||||
|
|
||||||
|
self::assertStringContainsString('value="Event"', $html, 'Submitted value is not rendered within form');
|
||||||
|
|
||||||
|
// Ensure going to 2nd page works (make sure it is available after warming up cache for first page)
|
||||||
|
|
||||||
|
$request = new InternalRequest('https://example.com/');
|
||||||
|
$request = $request->withAttribute('testingDateAspect', new DateTimeAspect(new DateTimeImmutable('2022-08-10')));
|
||||||
|
$request = $request->withQueryParams([
|
||||||
|
'events' => [
|
||||||
|
'search' => [
|
||||||
|
'searchword' => 'Event',
|
||||||
|
],
|
||||||
|
'controller' => 'Date',
|
||||||
|
'currentPage' => '2',
|
||||||
|
],
|
||||||
|
'cHash' => '41711281293c1c3a3aa161e96bbd4e98',
|
||||||
|
]);
|
||||||
|
$request = $request->withPageId(1);
|
||||||
|
$response = $this->executeFrontendSubRequest($request);
|
||||||
|
$html = $response->getBody()->__toString();
|
||||||
|
|
||||||
|
self::assertSame(200, $response->getStatusCode());
|
||||||
|
self::assertStringContainsString('Event two', $html);
|
||||||
|
self::assertStringContainsString('Current Page 2', $html);
|
||||||
|
self::assertStringContainsString('/page-1?events%5Bsearch%5D%5Bsearchword%5D=Event&cHash=13c33adfef09ccb19da7d399ada25c4c', $html);
|
||||||
|
self::assertStringNotContainsString('Event one', $html);
|
||||||
|
|
||||||
|
self::assertStringContainsString('value="Event"', $html, 'Submitted value is not rendered within form');
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,7 @@
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"WerkraumMedia\\Events\\Tests\\": "Tests",
|
"WerkraumMedia\\Events\\Tests\\": "Tests",
|
||||||
|
"WerkraumMedia\\EventsExample\\": "Tests/Functional/Frontend/Fixtures/Extensions/example/Classes/",
|
||||||
"WerkraumMedia\\CustomCategories\\": "Tests/Functional/Psr14Events/DestinationDataImport/Fixtures/Extensions/custom_categories/Classes/",
|
"WerkraumMedia\\CustomCategories\\": "Tests/Functional/Psr14Events/DestinationDataImport/Fixtures/Extensions/custom_categories/Classes/",
|
||||||
"WerkraumMedia\\CustomEvent\\": "Tests/Functional/Psr14Events/DestinationDataImport/Fixtures/Extensions/custom_event/Classes/"
|
"WerkraumMedia\\CustomEvent\\": "Tests/Functional/Psr14Events/DestinationDataImport/Fixtures/Extensions/custom_event/Classes/"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue