From f320c19c853d74acde60b59ad610cb37f65daeee 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 | 3 +++ core/Controller/UnifiedSearchController.php | 7 +++++++ .../UnifiedSearch/UnifiedSearchModal.vue | 19 ++++++++++++++++--- lib/private/Search/FilterCollection.php | 4 ++++ lib/private/Search/SearchComposer.php | 8 ++++++++ lib/private/TemplateLayout.php | 4 +++- lib/public/Search/IFilterCollection.php | 7 +++++++ 7 files changed, 48 insertions(+), 4 deletions(-) diff --git a/core/AppInfo/ConfigLexicon.php b/core/AppInfo/ConfigLexicon.php index c0ebd3b53bf..4fd4eb490ba 100644 --- a/core/AppInfo/ConfigLexicon.php +++ b/core/AppInfo/ConfigLexicon.php @@ -33,6 +33,8 @@ class ConfigLexicon implements ILexicon { public const USER_LOCALE = 'locale'; public const USER_TIMEZONE = 'timezone'; + public const UNIFIED_SEARCH_MIN_SEARCH_LENGTH = 'unified_search_min_search_length'; + public const LASTCRON_TIMESTAMP = 'lastcron'; public function getStrictness(): Strictness { @@ -90,6 +92,7 @@ class ConfigLexicon implements ILexicon { new Entry(self::LASTCRON_TIMESTAMP, ValueType::INT, 0, 'timestamp of last cron execution'), new Entry(self::OCM_DISCOVERY_ENABLED, ValueType::BOOL, true, 'enable/disable OCM', lazy: true), new Entry(self::OCM_INVITE_ACCEPT_DIALOG, ValueType::STRING, '', 'route to local invite accept dialog', lazy: true, note: 'set as empty string to disable feature'), + new Entry(self::UNIFIED_SEARCH_MIN_SEARCH_LENGTH, ValueType::INT, 1, 'Minimum search length to trigger the request', lazy: false, rename: 'unified-search.min-search-length'), ]; } diff --git a/core/Controller/UnifiedSearchController.php b/core/Controller/UnifiedSearchController.php index c770c6240df..169c1f348dc 100644 --- a/core/Controller/UnifiedSearchController.php +++ b/core/Controller/UnifiedSearchController.php @@ -19,6 +19,7 @@ use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; +use OCP\IL10N; use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUserSession; @@ -37,6 +38,7 @@ class UnifiedSearchController extends OCSController { private SearchComposer $composer, private IRouter $router, private IURLGenerator $urlGenerator, + private IL10N $l10n, ) { parent::__construct('core', $request); } @@ -101,6 +103,11 @@ class UnifiedSearchController extends OCSController { } catch (UnsupportedFilter|InvalidArgumentException $e) { return new DataResponse($e->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 1f92a17269c..d799a797de2 100644 --- a/core/src/components/UnifiedSearch/UnifiedSearchModal.vue +++ b/core/src/components/UnifiedSearch/UnifiedSearchModal.vue @@ -181,6 +181,7 @@ import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' import NcInputField from '@nextcloud/vue/components/NcInputField' import NcDialog from '@nextcloud/vue/components/NcDialog' import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' +import { loadState } from '@nextcloud/initial-state' import CustomDateRangeModal from './CustomDateRangeModal.vue' import FilterChip from './SearchFilterChip.vue' @@ -281,6 +282,7 @@ export default defineComponent({ internalIsVisible: this.open, initialized: false, searchExternalResources: false, + minSearchLength: loadState('unified-search', 'min-search-length', 1), } }, @@ -293,6 +295,10 @@ export default defineComponent({ return !this.isEmptySearch && this.results.length === 0 }, + isSearchQueryTooShort() { + return this.searchQuery.length < this.minSearchLength + }, + showEmptyContentInfo() { return this.isEmptySearch || this.hasNoResults }, @@ -301,9 +307,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') }, @@ -395,7 +408,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/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 be366e8ba6c..c017d1ec9a3 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\Application; +use OC\Core\AppInfo\ConfigLexicon; use OC\Core\ResponseDefinitions; use OCP\IAppConfig; use OCP\IURLGenerator; @@ -315,6 +317,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 42861eddc0d..b37f919abe7 100644 --- a/lib/private/TemplateLayout.php +++ b/lib/private/TemplateLayout.php @@ -12,6 +12,8 @@ namespace OC; use bantu\IniGetWrapper\IniGetWrapper; use OC\AppFramework\Http\Request; use OC\Authentication\Token\IProvider; +use OC\Core\AppInfo\Application; +use OC\Core\AppInfo\ConfigLexicon; use OC\Files\FilenameValidator; use OC\Search\SearchQuery; use OC\Template\CSSResourceLocator; @@ -74,9 +76,9 @@ class TemplateLayout { $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..2ed22d27ae4 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 32.0.1 + */ + public function count(): int; }