From 393883eab4508c25485911129aac8732b94730d5 Mon Sep 17 00:00:00 2001 From: Louis Chemineau Date: Wed, 24 Sep 2025 12:03:52 +0200 Subject: [PATCH] feat(unified-search): Use existing min search length config This setting existed already for the legacy unified search. This commit expose that setting to the new front-end, and also ignore non valid requests in the backend. We also take the opportunity to register the config in the lexicon. Signed-off-by: Louis Chemineau --- core/AppInfo/ConfigLexicon.php | 38 +++++++++++++++++++ core/Application.php | 3 ++ core/Controller/UnifiedSearchController.php | 7 ++++ .../UnifiedSearch/UnifiedSearchModal.vue | 19 ++++++++-- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + lib/private/Search/FilterCollection.php | 4 ++ lib/private/Search/SearchComposer.php | 8 ++++ lib/private/TemplateLayout.php | 7 +++- lib/public/Search/IFilterCollection.php | 7 ++++ 10 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 core/AppInfo/ConfigLexicon.php diff --git a/core/AppInfo/ConfigLexicon.php b/core/AppInfo/ConfigLexicon.php new file mode 100644 index 00000000000..8307acbc7eb --- /dev/null +++ b/core/AppInfo/ConfigLexicon.php @@ -0,0 +1,38 @@ +getMessage(), Http::STATUS_BAD_REQUEST); } + + if ($filters->count() === 0) { + return new DataResponse($this->l10n->t('No valid filters provided'), Http::STATUS_BAD_REQUEST); + } + return new DataResponse( $this->composer->search( $this->userSession->getUser(), diff --git a/core/src/components/UnifiedSearch/UnifiedSearchModal.vue b/core/src/components/UnifiedSearch/UnifiedSearchModal.vue index 151997e617e..9a973c2c85c 100644 --- a/core/src/components/UnifiedSearch/UnifiedSearchModal.vue +++ b/core/src/components/UnifiedSearch/UnifiedSearchModal.vue @@ -173,6 +173,7 @@ import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js' import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' +import { loadState } from '@nextcloud/initial-state' import CustomDateRangeModal from './CustomDateRangeModal.vue' import FilterChip from './SearchFilterChip.vue' @@ -271,6 +272,7 @@ export default defineComponent({ showDateRangeModal: false, internalIsVisible: this.open, initialized: false, + minSearchLength: loadState('unified-search', 'min-search-length', 1), } }, @@ -283,6 +285,10 @@ export default defineComponent({ return !this.isEmptySearch && this.results.length === 0 }, + isSearchQueryTooShort() { + return this.searchQuery.length < this.minSearchLength + }, + showEmptyContentInfo() { return this.isEmptySearch || this.hasNoResults }, @@ -291,9 +297,16 @@ export default defineComponent({ if (this.searching && this.hasNoResults) { return t('core', 'Searching …') } - if (this.isEmptySearch) { - return t('core', 'Start typing to search') + + if (this.isSearchQueryTooShort) { + switch (this.minSearchLength) { + case 1: + return t('core', 'Start typing to search') + default: + return t('core', 'Minimum search length is {minSearchLength} characters', { minSearchLength: this.minSearchLength }) + } } + return t('core', 'No matching results') }, @@ -375,7 +388,7 @@ export default defineComponent({ }) }, find(query: string, providersToSearchOverride = null) { - if (query.length === 0) { + if (this.isSearchQueryTooShort) { this.results = [] this.searching = false return diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 1c23c763bc1..75c10943036 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1196,6 +1196,7 @@ return array( 'OC\\Contacts\\ContactsMenu\\Providers\\EMailProvider' => $baseDir . '/lib/private/Contacts/ContactsMenu/Providers/EMailProvider.php', 'OC\\Contacts\\ContactsMenu\\Providers\\LocalTimeProvider' => $baseDir . '/lib/private/Contacts/ContactsMenu/Providers/LocalTimeProvider.php', 'OC\\Contacts\\ContactsMenu\\Providers\\ProfileProvider' => $baseDir . '/lib/private/Contacts/ContactsMenu/Providers/ProfileProvider.php', + 'OC\\Core\\AppInfo\\ConfigLexicon' => $baseDir . '/core/AppInfo/ConfigLexicon.php', 'OC\\Core\\Application' => $baseDir . '/core/Application.php', 'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => $baseDir . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php', 'OC\\Core\\BackgroundJobs\\CheckForUserCertificates' => $baseDir . '/core/BackgroundJobs/CheckForUserCertificates.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index c3b87cc93d5..c690df2fd04 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1245,6 +1245,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Contacts\\ContactsMenu\\Providers\\EMailProvider' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/Providers/EMailProvider.php', 'OC\\Contacts\\ContactsMenu\\Providers\\LocalTimeProvider' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/Providers/LocalTimeProvider.php', 'OC\\Contacts\\ContactsMenu\\Providers\\ProfileProvider' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/Providers/ProfileProvider.php', + 'OC\\Core\\AppInfo\\ConfigLexicon' => __DIR__ . '/../../..' . '/core/AppInfo/ConfigLexicon.php', 'OC\\Core\\Application' => __DIR__ . '/../../..' . '/core/Application.php', 'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php', 'OC\\Core\\BackgroundJobs\\CheckForUserCertificates' => __DIR__ . '/../../..' . '/core/BackgroundJobs/CheckForUserCertificates.php', diff --git a/lib/private/Search/FilterCollection.php b/lib/private/Search/FilterCollection.php index 173c967245a..030564db7de 100644 --- a/lib/private/Search/FilterCollection.php +++ b/lib/private/Search/FilterCollection.php @@ -40,4 +40,8 @@ class FilterCollection implements IFilterCollection { yield $k => $v; } } + + public function count(): int { + return count($this->filters); + } } diff --git a/lib/private/Search/SearchComposer.php b/lib/private/Search/SearchComposer.php index a33b7d53251..496cf59e7d2 100644 --- a/lib/private/Search/SearchComposer.php +++ b/lib/private/Search/SearchComposer.php @@ -10,6 +10,8 @@ namespace OC\Search; use InvalidArgumentException; use OC\AppFramework\Bootstrap\Coordinator; +use OC\Core\AppInfo\ConfigLexicon; +use OC\Core\Application; use OC\Core\ResponseDefinitions; use OCP\IAppConfig; use OCP\IURLGenerator; @@ -312,6 +314,12 @@ class SearchComposer { throw new UnsupportedFilter($name, $providerId); } + $minSearchLength = $this->appConfig->getValueInt(Application::APP_ID, ConfigLexicon::UNIFIED_SEARCH_MIN_SEARCH_LENGTH); + if ($filterDefinition->name() === 'term' && mb_strlen(trim($value)) < $minSearchLength) { + // Ignore term values that are not long enough + return null; + } + return FilterFactory::get($filterDefinition->type(), $value); } diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php index 42d5bc2200e..34535f845c6 100644 --- a/lib/private/TemplateLayout.php +++ b/lib/private/TemplateLayout.php @@ -10,6 +10,8 @@ namespace OC; use bantu\IniGetWrapper\IniGetWrapper; use OC\AppFramework\Http\Request; use OC\Authentication\Token\IProvider; +use OC\Core\AppInfo\ConfigLexicon; +use OC\Core\Application; use OC\Files\FilenameValidator; use OC\Search\SearchQuery; use OC\Template\CSSResourceLocator; @@ -18,6 +20,7 @@ use OC\Template\JSResourceLocator; use OCP\App\IAppManager; use OCP\AppFramework\Http\TemplateResponse; use OCP\Defaults; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IInitialStateService; use OCP\INavigationManager; @@ -41,6 +44,7 @@ class TemplateLayout extends \OC_Template { public static $jsLocator = null; private IConfig $config; + private IAppConfig $appConfig; private IAppManager $appManager; private InitialStateService $initialState; private INavigationManager $navigationManager; @@ -51,6 +55,7 @@ class TemplateLayout extends \OC_Template { */ public function __construct($renderAs, $appId = '') { $this->config = \OCP\Server::get(IConfig::class); + $this->appConfig = \OCP\Server::get(IAppConfig::class); $this->appManager = \OCP\Server::get(IAppManager::class); $this->initialState = \OCP\Server::get(InitialStateService::class); $this->navigationManager = \OCP\Server::get(INavigationManager::class); @@ -73,9 +78,9 @@ class TemplateLayout extends \OC_Template { $this->initialState->provideInitialState('core', 'active-app', $this->navigationManager->getActiveEntry()); $this->initialState->provideInitialState('core', 'apps', array_values($this->navigationManager->getAll())); + $this->initialState->provideInitialState('unified-search', 'min-search-length', $this->appConfig->getValueInt(Application::APP_ID, ConfigLexicon::UNIFIED_SEARCH_MIN_SEARCH_LENGTH)); if ($this->config->getSystemValueBool('unified_search.enabled', false) || !$this->config->getSystemValueBool('enable_non-accessible_features', true)) { $this->initialState->provideInitialState('unified-search', 'limit-default', (int)$this->config->getAppValue('core', 'unified-search.limit-default', (string)SearchQuery::LIMIT_DEFAULT)); - $this->initialState->provideInitialState('unified-search', 'min-search-length', (int)$this->config->getAppValue('core', 'unified-search.min-search-length', (string)1)); $this->initialState->provideInitialState('unified-search', 'live-search', $this->config->getAppValue('core', 'unified-search.live-search', 'yes') === 'yes'); Util::addScript('core', 'legacy-unified-search', 'core'); } else { diff --git a/lib/public/Search/IFilterCollection.php b/lib/public/Search/IFilterCollection.php index 45e5e196d7f..1c40465fcbb 100644 --- a/lib/public/Search/IFilterCollection.php +++ b/lib/public/Search/IFilterCollection.php @@ -36,4 +36,11 @@ interface IFilterCollection extends IteratorAggregate { * @since 28.0.0 */ public function getIterator(): \Traversable; + + /** + * Return the number of filters + * + * @since 31.0.10 + */ + public function count(): int; }