From 87cd9e6e305310bd14941a53689d49e6b92b49d3 Mon Sep 17 00:00:00 2001
From: Andreas Nedbal <andy@pixelde.su>
Date: Wed, 12 Jun 2024 10:58:45 +0200
Subject: [PATCH] [FEATURE] Provide backend modules in LiveSearch

Backend users can now search for the backend modules they have
access to in the LiveSearch.

Resolves: #92009
Releases: main
Change-Id: I28d9593655989f064b7a7fac0ab80415eee4c6fe
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/84654
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Andreas Kienast <a.fernandez@scripting-base.de>
Tested-by: Andreas Kienast <a.fernandez@scripting-base.de>
---
 .../backend-module-result-type.ts             |   8 ++
 .../AfterBackendPageRenderEventListener.php   |   5 +
 .../LiveSearch/BackendModuleProvider.php      | 110 ++++++++++++++++++
 .../Resources/Private/Language/locallang.xlf  |   9 ++
 .../backend-module-result-type.js             |  13 +++
 ...2009-ProvideBackendModulesInLiveSearch.rst |  23 ++++
 6 files changed, 168 insertions(+)
 create mode 100644 Build/Sources/TypeScript/backend/live-search/result-types/backend-module-result-type.ts
 create mode 100644 typo3/sysext/backend/Classes/Search/LiveSearch/BackendModuleProvider.php
 create mode 100644 typo3/sysext/backend/Resources/Public/JavaScript/live-search/result-types/backend-module-result-type.js
 create mode 100644 typo3/sysext/core/Documentation/Changelog/13.2/Feature-92009-ProvideBackendModulesInLiveSearch.rst

diff --git a/Build/Sources/TypeScript/backend/live-search/result-types/backend-module-result-type.ts b/Build/Sources/TypeScript/backend/live-search/result-types/backend-module-result-type.ts
new file mode 100644
index 000000000000..fe8e2b48ce14
--- /dev/null
+++ b/Build/Sources/TypeScript/backend/live-search/result-types/backend-module-result-type.ts
@@ -0,0 +1,8 @@
+import LiveSearchConfigurator from '@typo3/backend/live-search/live-search-configurator';
+import { ResultItemInterface } from '@typo3/backend/live-search/element/result/item/item';
+
+export function registerType(type: string) {
+  LiveSearchConfigurator.addInvokeHandler(type, 'open_module', (resultItem: ResultItemInterface): void => {
+    TYPO3.ModuleMenu.App.showModule(resultItem.extraData.moduleIdentifier);
+  });
+}
diff --git a/typo3/sysext/backend/Classes/EventListener/AfterBackendPageRenderEventListener.php b/typo3/sysext/backend/Classes/EventListener/AfterBackendPageRenderEventListener.php
index 07e0d6ebd4b2..0cd3cf648133 100644
--- a/typo3/sysext/backend/Classes/EventListener/AfterBackendPageRenderEventListener.php
+++ b/typo3/sysext/backend/Classes/EventListener/AfterBackendPageRenderEventListener.php
@@ -18,6 +18,7 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Backend\EventListener;
 
 use TYPO3\CMS\Backend\Controller\Event\AfterBackendPageRenderEvent;
+use TYPO3\CMS\Backend\Search\LiveSearch\BackendModuleProvider;
 use TYPO3\CMS\Backend\Search\LiveSearch\DatabaseRecordProvider;
 use TYPO3\CMS\Backend\Search\LiveSearch\PageRecordProvider;
 use TYPO3\CMS\Core\Attribute\AsEventListener;
@@ -43,5 +44,9 @@ final readonly class AfterBackendPageRenderEventListener
             JavaScriptModuleInstruction::create('@typo3/backend/live-search/result-types/page-result-type.js', 'registerRenderer')
                 ->invoke(null, PageRecordProvider::class)
         );
+        $javaScriptRenderer->addJavaScriptModuleInstruction(
+            JavaScriptModuleInstruction::create('@typo3/backend/live-search/result-types/backend-module-result-type.js', 'registerType')
+                ->invoke(null, BackendModuleProvider::class)
+        );
     }
 }
diff --git a/typo3/sysext/backend/Classes/Search/LiveSearch/BackendModuleProvider.php b/typo3/sysext/backend/Classes/Search/LiveSearch/BackendModuleProvider.php
new file mode 100644
index 000000000000..4f8a6ab66ea7
--- /dev/null
+++ b/typo3/sysext/backend/Classes/Search/LiveSearch/BackendModuleProvider.php
@@ -0,0 +1,110 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3\CMS\Backend\Search\LiveSearch;
+
+use TYPO3\CMS\Backend\Module\ModuleInterface;
+use TYPO3\CMS\Backend\Module\ModuleProvider;
+use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
+use TYPO3\CMS\Backend\Routing\UriBuilder;
+use TYPO3\CMS\Backend\Search\LiveSearch\SearchDemand\SearchDemand;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Imaging\IconFactory;
+use TYPO3\CMS\Core\Imaging\IconSize;
+use TYPO3\CMS\Core\Localization\LanguageService;
+use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
+
+class BackendModuleProvider implements SearchProviderInterface
+{
+    private LanguageService $languageService;
+
+    public function __construct(
+        private readonly LanguageServiceFactory $languageServiceFactory,
+        private readonly UriBuilder $uriBuilder,
+        private readonly IconFactory $iconFactory,
+        private readonly ModuleProvider $moduleProvider
+    ) {
+        $this->languageService = $this->languageServiceFactory->createFromUserPreferences($this->getBackendUser());
+    }
+
+    public function getFilterLabel(): string
+    {
+        return $this->languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:liveSearch.backendModuleProvider.filterLabel');
+    }
+
+    public function find(SearchDemand $searchDemand): array
+    {
+        $items = [];
+
+        foreach ($this->getFilteredModules($searchDemand) as $module) {
+            // we can't generate accessible URLs for all modules by their identifier
+            // if URL generation fails, we don't create an action to open a module
+            // and if no actions exist, we skip result item creation altogether
+            try {
+                $moduleUrl = (string)$this->uriBuilder->buildUriFromRoute($module->getIdentifier());
+            } catch (RouteNotFoundException) {
+                continue;
+            }
+
+            $action = (new ResultItemAction('open_module'))
+                ->setLabel($this->languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:resultItem.backendModuleProvider.openModule'))
+                ->setUrl($moduleUrl);
+
+            $iconIdentifier = $module->getIconIdentifier();
+            if ($iconIdentifier === '' && $module->hasParentModule()) {
+                $iconIdentifier = $module->getParentModule()->getIconIdentifier();
+            }
+
+            $items[] = (new ResultItem(self::class))
+                ->setItemTitle($this->languageService->sL($module->getTitle()))
+                ->setTypeLabel($this->languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:liveSearch.backendModuleProvider.typeLabel'))
+                ->setIcon($this->iconFactory->getIcon($iconIdentifier, IconSize::SMALL))
+                ->setActions($action)
+                ->setExtraData([
+                    'moduleIdentifier' => $module->getIdentifier(),
+                ]);
+        }
+
+        return $items;
+    }
+
+    public function count(SearchDemand $searchDemand): int
+    {
+        return count($this->getFilteredModules($searchDemand));
+    }
+
+    /**
+     * @return list<ModuleInterface>
+     */
+    private function getFilteredModules(SearchDemand $searchDemand): array
+    {
+        $filteredModules = array_filter(
+            $this->moduleProvider->getModules($this->getBackendUser(), true, false),
+            fn(ModuleInterface $module) => str_contains(mb_strtolower($this->languageService->sL($module->getTitle())), mb_strtolower($searchDemand->getQuery()))
+        );
+
+        $firstResult = $searchDemand->getOffset();
+        $remainingItems = $searchDemand->getLimit();
+
+        return array_slice($filteredModules, $firstResult, $remainingItems, true);
+    }
+
+    private function getBackendUser(): BackendUserAuthentication
+    {
+        return $GLOBALS['BE_USER'];
+    }
+}
diff --git a/typo3/sysext/backend/Resources/Private/Language/locallang.xlf b/typo3/sysext/backend/Resources/Private/Language/locallang.xlf
index de99a6997fa6..9d8405b57ec2 100644
--- a/typo3/sysext/backend/Resources/Private/Language/locallang.xlf
+++ b/typo3/sysext/backend/Resources/Private/Language/locallang.xlf
@@ -190,6 +190,15 @@ Have a nice day.</source>
 			<trans-unit id="liveSearch.pageRecordProvider.typeLabel" resname="liveSearch.pageRecordProvider.typeLabel">
 				<source>Page</source>
 			</trans-unit>
+			<trans-unit id="liveSearch.backendModuleProvider.filterLabel" resname="liveSearch.backendModuleProvider.filterLabel">
+				<source>Backend modules</source>
+			</trans-unit>
+			<trans-unit id="liveSearch.backendModuleProvider.typeLabel" resname="liveSearch.backendModuleProvider.typeLabel">
+				<source>Backend module</source>
+			</trans-unit>
+			<trans-unit id="resultItem.backendModuleProvider.openModule" resname="resultItem.backendModuleProvider.openModule">
+				<source>Open module</source>
+			</trans-unit>
 			<trans-unit id="moduleMenu.dropdown.label" resname="moduleMenu.dropdown.label">
 				<source>Module action</source>
 			</trans-unit>
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/live-search/result-types/backend-module-result-type.js b/typo3/sysext/backend/Resources/Public/JavaScript/live-search/result-types/backend-module-result-type.js
new file mode 100644
index 000000000000..42329614f0d8
--- /dev/null
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/live-search/result-types/backend-module-result-type.js
@@ -0,0 +1,13 @@
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+import LiveSearchConfigurator from"@typo3/backend/live-search/live-search-configurator.js";export function registerType(e){LiveSearchConfigurator.addInvokeHandler(e,"open_module",(e=>{TYPO3.ModuleMenu.App.showModule(e.extraData.moduleIdentifier)}))}
\ No newline at end of file
diff --git a/typo3/sysext/core/Documentation/Changelog/13.2/Feature-92009-ProvideBackendModulesInLiveSearch.rst b/typo3/sysext/core/Documentation/Changelog/13.2/Feature-92009-ProvideBackendModulesInLiveSearch.rst
new file mode 100644
index 000000000000..ee92789d3a83
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/13.2/Feature-92009-ProvideBackendModulesInLiveSearch.rst
@@ -0,0 +1,23 @@
+.. include:: /Includes.rst.txt
+
+.. _feature-92009-1718182575:
+
+=======================================================
+Feature: #92009 - Provide backend modules in LiveSearch
+=======================================================
+
+See :issue:`92009`
+
+Description
+===========
+
+The backend LiveSearch is now capable of listing backend modules, a user has
+access to, offering the possibility of alternative navigation to different
+parts of the backend.
+
+Impact
+======
+
+Backend users now have another possibility to quickly access a backend module.
+
+.. index:: Backend, ext:backend
-- 
GitLab