mirror of
https://github.com/werkraum-media/events.git
synced 2024-11-22 03:16:11 +01:00
Add grouping of locations. (#64)
It is now possible to group locations. Each location can have arbitrary children. That can be used for editorial structuring. Filtering for a location will always find all dates where the location or one of the child locations is assigned. One use case can be to group imported locations and provide a grouped location for filtering in frontend. Relates: #11233
This commit is contained in:
parent
a02435b909
commit
39dfc0b42a
9 changed files with 274 additions and 16 deletions
|
@ -7,6 +7,7 @@ namespace WerkraumMedia\Events\Domain\Repository;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use DateTimeZone;
|
use DateTimeZone;
|
||||||
use TYPO3\CMS\Core\Context\Context;
|
use TYPO3\CMS\Core\Context\Context;
|
||||||
|
use TYPO3\CMS\Core\Database\Connection;
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface;
|
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface;
|
||||||
|
@ -20,7 +21,8 @@ use WerkraumMedia\Events\Service\CategoryService;
|
||||||
final class DateRepository extends Repository
|
final class DateRepository extends Repository
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Context $context
|
private readonly Context $context,
|
||||||
|
private readonly ConnectionPool $connectionPool,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
@ -58,7 +60,7 @@ final class DateRepository extends Repository
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($demand->getLocations() !== []) {
|
if ($demand->getLocations() !== []) {
|
||||||
$constraints['locations'] = $query->in('event.location', $demand->getLocations());
|
$constraints['locations'] = $this->createLocationConstraint($query, $demand);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($demand->getOrganizers() !== []) {
|
if ($demand->getOrganizers() !== []) {
|
||||||
|
@ -144,9 +146,7 @@ final class DateRepository extends Repository
|
||||||
$wordsToSearch[] = $demand->getSearchword();
|
$wordsToSearch[] = $demand->getSearchword();
|
||||||
$constraints = [];
|
$constraints = [];
|
||||||
|
|
||||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_events_domain_model_date');
|
||||||
->getQueryBuilderForTable('tx_events_domain_model_date')
|
|
||||||
;
|
|
||||||
|
|
||||||
foreach ($wordsToSearch as $word) {
|
foreach ($wordsToSearch as $word) {
|
||||||
foreach ($fieldsToSearch as $field) {
|
foreach ($fieldsToSearch as $field) {
|
||||||
|
@ -250,11 +250,41 @@ final class DateRepository extends Repository
|
||||||
return $query->logicalAnd(... $constraints);
|
return $query->logicalAnd(... $constraints);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function createLocationConstraint(
|
||||||
|
QueryInterface $query,
|
||||||
|
DateDemand $demand
|
||||||
|
): ConstraintInterface {
|
||||||
|
$locations = $demand->getLocations();
|
||||||
|
$uidsToResolve = $locations;
|
||||||
|
|
||||||
|
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_events_domain_model_location');
|
||||||
|
$queryBuilder->select('children');
|
||||||
|
$queryBuilder->from('tx_events_domain_model_location');
|
||||||
|
|
||||||
|
// Loop as resolved uids might have further children which need to be resolved as well.
|
||||||
|
do {
|
||||||
|
$concreteQueryBuilder = clone $queryBuilder;
|
||||||
|
$concreteQueryBuilder->where($concreteQueryBuilder->expr()->in(
|
||||||
|
'uid',
|
||||||
|
$concreteQueryBuilder->createNamedParameter($uidsToResolve, Connection::PARAM_INT_ARRAY)
|
||||||
|
));
|
||||||
|
|
||||||
|
foreach ($concreteQueryBuilder->executeQuery()->fetchFirstColumn() as $newUids) {
|
||||||
|
if (is_string($newUids) === false) {
|
||||||
|
$newUids = '';
|
||||||
|
}
|
||||||
|
$newUids = GeneralUtility::intExplode(',', $newUids, true);
|
||||||
|
$uidsToResolve = array_diff($newUids, $locations);
|
||||||
|
$locations = array_merge($locations, $uidsToResolve);
|
||||||
|
}
|
||||||
|
} while ($uidsToResolve !== []);
|
||||||
|
|
||||||
|
return $query->in('event.location', $locations);
|
||||||
|
}
|
||||||
|
|
||||||
public function findSearchWord(string $search): array
|
public function findSearchWord(string $search): array
|
||||||
{
|
{
|
||||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
|
$connection = $this->connectionPool->getConnectionForTable('tx_events_domain_model_date');
|
||||||
->getConnectionForTable('tx_events_domain_model_date')
|
|
||||||
;
|
|
||||||
|
|
||||||
$queryBuilder = $connection->createQueryBuilder();
|
$queryBuilder = $connection->createQueryBuilder();
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ return [
|
||||||
'starttime' => 'starttime',
|
'starttime' => 'starttime',
|
||||||
'endtime' => 'endtime',
|
'endtime' => 'endtime',
|
||||||
],
|
],
|
||||||
|
'default_sortby' => 'name',
|
||||||
'searchFields' => 'name',
|
'searchFields' => 'name',
|
||||||
'iconfile' => 'EXT:events/Resources/Public/Icons/tx_events_domain_model_location.svg',
|
'iconfile' => 'EXT:events/Resources/Public/Icons/tx_events_domain_model_location.svg',
|
||||||
],
|
],
|
||||||
|
@ -32,7 +33,6 @@ return [
|
||||||
l10n_diffsource,
|
l10n_diffsource,
|
||||||
hidden,
|
hidden,
|
||||||
name,
|
name,
|
||||||
global_id,
|
|
||||||
|
|
||||||
street,
|
street,
|
||||||
district,
|
district,
|
||||||
|
@ -42,6 +42,10 @@ return [
|
||||||
phone,
|
phone,
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
|
--div--;' . $l10nPath . ':tabs.grouping,
|
||||||
|
children,
|
||||||
|
--div--;' . $l10nPath . ':tabs.tech,
|
||||||
|
global_id,
|
||||||
--div--;' . $l10nPath . ':tabs.access,
|
--div--;' . $l10nPath . ':tabs.access,
|
||||||
starttime,
|
starttime,
|
||||||
endtime',
|
endtime',
|
||||||
|
@ -127,6 +131,20 @@ return [
|
||||||
'eval' => 'trim',
|
'eval' => 'trim',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'children' => [
|
||||||
|
'exclude' => true,
|
||||||
|
'label' => $l10nPath . ':tx_events_domain_model_location.children',
|
||||||
|
'config' => [
|
||||||
|
'type' => 'select',
|
||||||
|
'renderType' => 'selectMultipleSideBySide',
|
||||||
|
'foreign_table' => 'tx_events_domain_model_location',
|
||||||
|
'fieldControl' => [
|
||||||
|
'editPopup' => [
|
||||||
|
'disabled' => false,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
'name' => [
|
'name' => [
|
||||||
'exclude' => true,
|
'exclude' => true,
|
||||||
'label' => $l10nPath . ':tx_events_domain_model_location.name',
|
'label' => $l10nPath . ':tx_events_domain_model_location.name',
|
||||||
|
|
45
Documentation/Changelog/3.9.0.rst
Normal file
45
Documentation/Changelog/3.9.0.rst
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
3.9.0
|
||||||
|
=====
|
||||||
|
|
||||||
|
Breaking
|
||||||
|
--------
|
||||||
|
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
* Add grouping of locations.
|
||||||
|
It is now possible to group locations.
|
||||||
|
Each location can have arbitrary children.
|
||||||
|
|
||||||
|
That can be used for editorial structuring.
|
||||||
|
Filtering for a location will always find all dates where the location or one of the child locations is assigned.
|
||||||
|
|
||||||
|
One use case can be to group imported locations and provide a grouped location for filtering in frontend.
|
||||||
|
|
||||||
|
Backport of 4.0.0 features:
|
||||||
|
|
||||||
|
* Add meta tags.
|
||||||
|
A new class is added which will add meta tags for dates and events.
|
||||||
|
The class has an interface which allows it to be replaced via DI to alter behaviour.
|
||||||
|
|
||||||
|
* Import keywords for events from destination.one.
|
||||||
|
That way keywords are available for usage in meta tags.
|
||||||
|
|
||||||
|
* Add page title provider. That way it is possible to alter the TYPO3 page title when showing a date or event.
|
||||||
|
|
||||||
|
Fixes
|
||||||
|
-----
|
||||||
|
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
Tasks
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Add image handling support in nix shell.
|
||||||
|
|
||||||
|
Deprecation
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Nothing
|
|
@ -1,4 +1,4 @@
|
||||||
4.1.9
|
4.1.0
|
||||||
=====
|
=====
|
||||||
|
|
||||||
Breaking
|
Breaking
|
||||||
|
@ -9,7 +9,16 @@ Nothing
|
||||||
Features
|
Features
|
||||||
--------
|
--------
|
||||||
|
|
||||||
Nothing
|
Forward ported from 3.9.0:
|
||||||
|
|
||||||
|
* Add grouping of locations.
|
||||||
|
It is now possible to group locations.
|
||||||
|
Each location can have arbitrary children.
|
||||||
|
|
||||||
|
That can be used for editorial structuring.
|
||||||
|
Filtering for a location will always find all dates where the location or one of the child locations is assigned.
|
||||||
|
|
||||||
|
One use case can be to group imported locations and provide a grouped location for filtering in frontend.
|
||||||
|
|
||||||
Fixes
|
Fixes
|
||||||
-----
|
-----
|
||||||
|
|
|
@ -7,6 +7,18 @@
|
||||||
<source>Location</source>
|
<source>Location</source>
|
||||||
<target>Veranstaltungsort</target>
|
<target>Veranstaltungsort</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="tabs.grouping" xml:space="preserve">
|
||||||
|
<source>Grouping</source>
|
||||||
|
<target>Gruppierung</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="tabs.tech" xml:space="preserve">
|
||||||
|
<source>Tech</source>
|
||||||
|
<target>Technik</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="tabs.access" xml:space="preserve">
|
||||||
|
<source>Access</source>
|
||||||
|
<target>Zugriff</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="tx_events_domain_model_location.global_id" xml:space="preserve">
|
<trans-unit id="tx_events_domain_model_location.global_id" xml:space="preserve">
|
||||||
<source>Global UID</source>
|
<source>Global UID</source>
|
||||||
<target>Globale UID</target>
|
<target>Globale UID</target>
|
||||||
|
@ -15,9 +27,9 @@
|
||||||
<source>Auto generated from the values.</source>
|
<source>Auto generated from the values.</source>
|
||||||
<target>Wird automatisch aus den Werten generiert.</target>
|
<target>Wird automatisch aus den Werten generiert.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="tx_events_domain_model_location.slug" xml:space="preserve">
|
<trans-unit id="tx_events_domain_model_location.children" xml:space="preserve">
|
||||||
<source>Slug</source>
|
<source>Children</source>
|
||||||
<target>URL-Segment</target>
|
<target>Kinder</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="tx_events_domain_model_location.name" xml:space="preserve">
|
<trans-unit id="tx_events_domain_model_location.name" xml:space="preserve">
|
||||||
<source>Name</source>
|
<source>Name</source>
|
||||||
|
|
|
@ -6,14 +6,23 @@
|
||||||
<trans-unit id="tx_events_domain_model_location" xml:space="preserve">
|
<trans-unit id="tx_events_domain_model_location" xml:space="preserve">
|
||||||
<source>Location</source>
|
<source>Location</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="tabs.grouping" xml:space="preserve">
|
||||||
|
<source>Grouping</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="tabs.tech" xml:space="preserve">
|
||||||
|
<source>Tech</source>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="tabs.access" xml:space="preserve">
|
||||||
|
<source>Access</source>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="tx_events_domain_model_location.global_id" xml:space="preserve">
|
<trans-unit id="tx_events_domain_model_location.global_id" xml:space="preserve">
|
||||||
<source>Global UID</source>
|
<source>Global UID</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="tx_events_domain_model_location.global_id.description" xml:space="preserve">
|
<trans-unit id="tx_events_domain_model_location.global_id.description" xml:space="preserve">
|
||||||
<source>Auto generated from the values.</source>
|
<source>Auto generated from the values.</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="tx_events_domain_model_location.slug" xml:space="preserve">
|
<trans-unit id="tx_events_domain_model_location.children" xml:space="preserve">
|
||||||
<source>Slug</source>
|
<source>Children</source>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="tx_events_domain_model_location.name" xml:space="preserve">
|
<trans-unit id="tx_events_domain_model_location.name" xml:space="preserve">
|
||||||
<source>Name</source>
|
<source>Name</source>
|
||||||
|
|
|
@ -49,4 +49,20 @@ class FilterTest extends AbstractFunctionalTestCase
|
||||||
self::assertStringContainsString('Lotte in Weimar', $html);
|
self::assertStringContainsString('Lotte in Weimar', $html);
|
||||||
self::assertStringContainsString('Was hat das Universum mit mir zu tun?', $html);
|
self::assertStringContainsString('Was hat das Universum mit mir zu tun?', $html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function canFilterDatesByParentLocationViaFlexform(): void
|
||||||
|
{
|
||||||
|
$this->importPHPDataSet(__DIR__ . '/Fixtures/Database/FilterDatesByParentLocationViaFlexform.php');
|
||||||
|
|
||||||
|
$request = new InternalRequest('https://example.com/');
|
||||||
|
$request = $request->withPageId(1);
|
||||||
|
$response = $this->executeFrontendSubRequest($request);
|
||||||
|
|
||||||
|
self::assertSame(200, $response->getStatusCode());
|
||||||
|
$html = (string)$response->getBody();
|
||||||
|
|
||||||
|
self::assertStringContainsString('Lotte in Weimar', $html);
|
||||||
|
self::assertStringContainsString('Was hat das Universum mit mir zu tun?', $html);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'tt_content' => [
|
||||||
|
[
|
||||||
|
'pid' => '1',
|
||||||
|
'uid' => '1',
|
||||||
|
'CType' => 'list',
|
||||||
|
'list_type' => 'events_datelist',
|
||||||
|
'header' => 'Kino Events',
|
||||||
|
'pi_flexform' => '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
|
||||||
|
<T3FlexForms>
|
||||||
|
<data>
|
||||||
|
<sheet index="sDEF">
|
||||||
|
<language index="lDEF">
|
||||||
|
<field index="settings.locations">
|
||||||
|
<value index="vDEF">1</value>
|
||||||
|
</field>
|
||||||
|
</language>
|
||||||
|
</sheet>
|
||||||
|
</data>
|
||||||
|
</T3FlexForms>
|
||||||
|
',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'tx_events_domain_model_location' => [
|
||||||
|
[
|
||||||
|
'uid' => '1',
|
||||||
|
'pid' => '2',
|
||||||
|
'name' => 'Parent',
|
||||||
|
'street' => '',
|
||||||
|
'city' => '',
|
||||||
|
'zip' => '',
|
||||||
|
'country' => '',
|
||||||
|
'longitude' => '',
|
||||||
|
'latitude' => '',
|
||||||
|
'children' => '2,3',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uid' => '2',
|
||||||
|
'pid' => '2',
|
||||||
|
'name' => 'Child',
|
||||||
|
'street' => 'Theaterplatz 4',
|
||||||
|
'city' => 'Weimar',
|
||||||
|
'zip' => '99423',
|
||||||
|
'country' => 'Deutschland',
|
||||||
|
'longitude' => '11.3262489',
|
||||||
|
'latitude' => '50.9800023',
|
||||||
|
'district' => 'Zentrum',
|
||||||
|
// Validate we don't end in endless recursion
|
||||||
|
'children' => '1',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uid' => '3',
|
||||||
|
'pid' => '2',
|
||||||
|
'name' => 'Child 2',
|
||||||
|
'street' => 'Cranach-Haus Markt 11/12',
|
||||||
|
'city' => 'Weimar',
|
||||||
|
'zip' => '99423',
|
||||||
|
'country' => 'Deutschland',
|
||||||
|
'longitude' => '11.330248',
|
||||||
|
'latitude' => '50.979349',
|
||||||
|
'children' => '',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'tx_events_domain_model_event' => [
|
||||||
|
[
|
||||||
|
'uid' => '1',
|
||||||
|
'pid' => '2',
|
||||||
|
'title' => 'Was hat das Universum mit mir zu tun?',
|
||||||
|
'global_id' => 'e_100478529',
|
||||||
|
'teaser' => '„WAS HAT DAS UNIVERSUM MIT MIR ZU TUN?“
|
||||||
|
Ein Abend mit Prof. Dr. Harald Lesch',
|
||||||
|
'details' => '„WAS HAT DAS UNIVERSUM MIT MIR ZU TUN?“
|
||||||
|
Ein Abend mit Prof. Dr. Harald Lesch
|
||||||
|
Auf den Spuren von Goethes Naturphilosophie ist der Astrophysiker und Wissenschaftsjournalist Prof. Dr. Harald Lesch in Weimar schon mehrmals präsent gewesen. Jetzt hält er einen Vortrag zu keiner geringeren Frage als „Was hat das Universum mit mir zu tun?“ Ob Goethe darauf eine pointierte Antwort eingefallen wäre? Sein Faust wollte die Spur seiner Erdentage nicht in Äonen untergehen sehen. Harald Lesch behauptet: Wir sind und bleiben stets Teil der Äonen - denn „wir sind alle Sternenstaub. Vor einer halben Ewigkeit ist ein Stern explodiert und hat alle Stoffe aus denen wir bestehen hervorgebracht. Und wenn das bei uns geklappt hat, könnte es auch noch woanders passiert sein.“ Erleben Sie einen faszinierenden Mix aus Rednerkunst und virtuoser musikalischer Begleitung. Neben Prof. Dr. Harald Lesch begibt sich der Musiker Hans Raths (Bayon) mit auf die Reise ins theatralische und philosophische Universum. Eine Veranstaltung nicht nur für Science-Fiction-Freaks, sondern für alle Kosmopoliten!',
|
||||||
|
'price_info' => 'Preis inklusive Platzierung mit Namensschild und einem Pausengetränk Ihrer Wahl',
|
||||||
|
'location' => '3',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uid' => '2',
|
||||||
|
'pid' => '2',
|
||||||
|
'title' => 'Lotte in Weimar',
|
||||||
|
'global_id' => 'e_100453137',
|
||||||
|
'teaser' => 'Ein „Goethe-Götter-Lustspiel“ nach dem gleichnamigen Roman von Thomas Mann',
|
||||||
|
'details' => 'LOTTE IN WEIMAR
|
||||||
|
Ein „Goethe-Götter-Lustspiel“ nach dem gleichnamigen Roman von Thomas Mann
|
||||||
|
„Welch buchenswertes Ereignis!“, ruft der Kellner Mager aus, als er erfährt, wer da in seinem Gasthaus „Zum Elephanten“ abgestiegen ist: Die berühmte Heldin aus Goethes „Die Leiden des jungen Werthers“, Charlotte Kestner, geborene Buff aus Wetzlar, – das „Urbild“ der Lotte sozusagen! Eine heiter-ironische Abrechnung mit dem Starkult anno 1816 fast am Originalschauplatz. Mit Regine Heintze, Heike Meyer und Detlef Heintze. Inszenierung: Michael Kliefert/ Detlef Heintze.',
|
||||||
|
'price_info' => 'Preise inklusive Platzierung mit Namensschild und einem Pausengetränk Ihrer Wahl (ermäßigt alkoholfrei)',
|
||||||
|
'location' => '2',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'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',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
|
@ -98,6 +98,7 @@ CREATE TABLE tx_events_domain_model_location (
|
||||||
phone varchar(255) DEFAULT '' NOT NULL,
|
phone varchar(255) DEFAULT '' NOT NULL,
|
||||||
latitude varchar(255) DEFAULT '' NOT NULL,
|
latitude varchar(255) DEFAULT '' NOT NULL,
|
||||||
longitude varchar(255) DEFAULT '' NOT NULL,
|
longitude varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
children text,
|
||||||
|
|
||||||
KEY global_id (global_id)
|
KEY global_id (global_id)
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue