Browse Source

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 <louis@chmn.me>
pull/55299/head
Louis Chemineau 4 weeks ago
committed by Louis
parent
commit
393883eab4
  1. 38
      core/AppInfo/ConfigLexicon.php
  2. 3
      core/Application.php
  3. 7
      core/Controller/UnifiedSearchController.php
  4. 19
      core/src/components/UnifiedSearch/UnifiedSearchModal.vue
  5. 1
      lib/composer/composer/autoload_classmap.php
  6. 1
      lib/composer/composer/autoload_static.php
  7. 4
      lib/private/Search/FilterCollection.php
  8. 8
      lib/private/Search/SearchComposer.php
  9. 7
      lib/private/TemplateLayout.php
  10. 7
      lib/public/Search/IFilterCollection.php

38
core/AppInfo/ConfigLexicon.php

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Core\AppInfo;
use NCU\Config\Lexicon\ConfigLexiconEntry;
use NCU\Config\Lexicon\ConfigLexiconStrictness;
use NCU\Config\Lexicon\IConfigLexicon;
use NCU\Config\ValueType;
/**
* Config Lexicon for core.
*
* Please Add & Manage your Config Keys in that file and keep the Lexicon up to date!
*/
class ConfigLexicon implements IConfigLexicon {
public const UNIFIED_SEARCH_MIN_SEARCH_LENGTH = 'unified_search_min_search_length';
public function getStrictness(): ConfigLexiconStrictness {
return ConfigLexiconStrictness::IGNORE;
}
public function getAppConfigs(): array {
return [
new ConfigLexiconEntry(self::UNIFIED_SEARCH_MIN_SEARCH_LENGTH, ValueType::INT, 1, 'Minimum search length to trigger the request', lazy: false),
];
}
public function getUserConfigs(): array {
return [
];
}
}

3
core/Application.php

@ -37,6 +37,9 @@ use OCP\Util;
* @package OC\Core
*/
class Application extends App {
public const APP_ID = 'core';
public function __construct() {
parent::__construct('core');

7
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(),

19
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

1
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',

1
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',

4
lib/private/Search/FilterCollection.php

@ -40,4 +40,8 @@ class FilterCollection implements IFilterCollection {
yield $k => $v;
}
}
public function count(): int {
return count($this->filters);
}
}

8
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);
}

7
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 {

7
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;
}
Loading…
Cancel
Save