Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 3rdparty
Submodule 3rdparty updated 166 files
2 changes: 2 additions & 0 deletions core/AppInfo/ConfigLexicon.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class ConfigLexicon implements ILexicon {
public const USER_TIMEZONE = 'timezone';

public const UNIFIED_SEARCH_MIN_SEARCH_LENGTH = 'unified_search_min_search_length';
public const UNIFIED_SEARCH_MAX_RESULTS_PER_REQUEST = 'unified_search_max_results_per_request';

public const LASTCRON_TIMESTAMP = 'lastcron';

Expand Down Expand Up @@ -93,6 +94,7 @@ public function getAppConfigs(): array {
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'),
new Entry(self::UNIFIED_SEARCH_MAX_RESULTS_PER_REQUEST, ValueType::INT, 25, 'Maximum results returned per search request', lazy: false, rename: 'unified-search.max-results-per-request'),
];
}

Expand Down
14 changes: 11 additions & 3 deletions core/Controller/UnifiedSearchController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
namespace OC\Core\Controller;

use InvalidArgumentException;
use OC\Core\AppInfo\Application;
use OC\Core\AppInfo\ConfigLexicon;
use OC\Core\ResponseDefinitions;
use OC\Search\SearchComposer;
use OC\Search\SearchQuery;
Expand All @@ -19,6 +21,7 @@
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IAppConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
Expand All @@ -39,6 +42,7 @@ public function __construct(
private IRouter $router,
private IURLGenerator $urlGenerator,
private IL10N $l10n,
private IAppConfig $appConfig,
) {
parent::__construct('core', $request);
}
Expand Down Expand Up @@ -72,8 +76,9 @@ public function getProviders(string $from = ''): DataResponse {
* @param string $providerId ID of the provider
* @param string $term Term to search
* @param int|null $sortOrder Order of entries
* @param int|null $limit Maximum amount of entries, limited to 25
* @param int|null $limit Maximum amount of entries (capped by configurable unified-search.max-results-per-request, default: 25)
* @param int|string|null $cursor Offset for searching
* @param int|null $offset Skip this many results
* @param string $from The current user URL
*
* @return DataResponse<Http::STATUS_OK, CoreUnifiedSearchResult, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, string, array{}>
Expand All @@ -91,12 +96,14 @@ public function search(
?int $sortOrder = null,
?int $limit = null,
$cursor = null,
?int $offset = null,
string $from = '',
): DataResponse {
[$route, $routeParameters] = $this->getRouteInformation($from);

$limit ??= SearchQuery::LIMIT_DEFAULT;
$limit = max(1, min($limit, 25));
$maxLimit = $this->appConfig->getValueInt(Application::APP_ID, ConfigLexicon::UNIFIED_SEARCH_MAX_RESULTS_PER_REQUEST);
$limit = max(1, min($limit, $maxLimit));

try {
$filters = $this->composer->buildFilterList($providerId, $this->request->getParams());
Expand All @@ -118,7 +125,8 @@ public function search(
$limit,
$cursor,
$route,
$routeParameters
$routeParameters,
$offset
)
)->jsonSerialize()
);
Expand Down
2 changes: 1 addition & 1 deletion core/openapi-full.json
Original file line number Diff line number Diff line change
Expand Up @@ -8511,7 +8511,7 @@
{
"name": "limit",
"in": "query",
"description": "Maximum amount of entries, limited to 25",
"description": "Maximum amount of entries (capped by configurable unified-search.max-results-per-request, default: 25)",
"schema": {
"type": "integer",
"format": "int64",
Expand Down
2 changes: 1 addition & 1 deletion core/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -8511,7 +8511,7 @@
{
"name": "limit",
"in": "query",
"description": "Maximum amount of entries, limited to 25",
"description": "Maximum amount of entries (capped by configurable unified-search.max-results-per-request, default: 25)",
"schema": {
"type": "integer",
"format": "int64",
Expand Down
90 changes: 79 additions & 11 deletions core/src/components/UnifiedSearch/UnifiedSearchModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,6 @@ export default defineComponent({
providers: [],
providerActionMenuIsOpen: false,
dateActionMenuIsOpen: false,
providerResultLimit: 5,
dateFilter: {
id: 'date',
type: 'date',
Expand Down Expand Up @@ -414,9 +413,9 @@ export default defineComponent({
return
}

// Reset the provider result limit when performing a new search
// Reset results when performing a new search
if (query !== this.lastSearchQuery) {
this.providerResultLimit = 5
this.results = []
}
this.lastSearchQuery = query

Expand Down Expand Up @@ -453,10 +452,7 @@ export default defineComponent({
}
})

if (this.providerResultLimit > 5) {
params.limit = this.providerResultLimit
unifiedSearchLogger.debug('Limiting search to', params.limit)
}
params.limit = 5

const shouldSkipSearch = !this.searchExternalResources && provider.isExternalProvider
const wasManuallySelected = this.filteredProviders.some(filteredProvider => filteredProvider.id === provider.id)
Expand All @@ -472,7 +468,7 @@ export default defineComponent({
newResults.push({
...provider,
results: response.data.ocs.data.entries,
limit: params.limit ?? 5,
limit: 5,
})

unifiedSearchLogger.debug('Unified search results:', { results: this.results, newResults })
Expand All @@ -484,6 +480,76 @@ export default defineComponent({

providersToSearch.forEach(searchProvider)
},
findWithOffset(query: string, providersToSearch, offset: number, limit: number) {
if (this.isSearchQueryTooShort) {
return
}

this.searching = true
const newResults = []
const searchProvider = (provider) => {
const params = {
type: provider.searchFrom ?? provider.id,
query,
cursor: null,
offset,
limit,
extraQueries: provider.extraParams,
}

// Apply filters same as regular find method
const activeFilters = this.filters.filter(filter => {
return filter.type !== 'provider' && this.providerIsCompatibleWithFilters(provider, [filter.type])
})

activeFilters.forEach(filter => {
switch (filter.type) {
case 'date':
if (provider.filters?.since && provider.filters?.until) {
params.since = this.dateFilter.startFrom
params.until = this.dateFilter.endAt
}
break
case 'person':
if (provider.filters?.person) {
params.person = this.personFilter.user
}
break
}
})

const shouldSkipSearch = !this.searchExternalResources && provider.isExternalProvider
const wasManuallySelected = this.filteredProviders.some(filteredProvider => filteredProvider.id === provider.id)
if (shouldSkipSearch && !wasManuallySelected) {
this.searching = false
return
}

const request = unifiedSearch(params).request

request().then((response) => {
// For offset-based pagination, we need to append results to existing ones
const existingResults = this.results.find(result => result.id === provider.id)
if (existingResults) {
existingResults.results.push(...response.data.ocs.data.entries)
existingResults.limit = existingResults.results.length
} else {
newResults.push({
...provider,
results: response.data.ocs.data.entries,
limit: response.data.ocs.data.entries.length,
})
}

if (newResults.length > 0) {
this.updateResults(newResults)
}
this.searching = false
})
}

providersToSearch.forEach(searchProvider)
},
updateResults(newResults) {
let updatedResults = [...this.results]
// If filters are applied, remove any previous results for providers that are not in current filters
Expand Down Expand Up @@ -561,8 +627,11 @@ export default defineComponent({
unifiedSearchLogger.debug('Person filter applied', { person })
},
async loadMoreResultsForProvider(provider) {
this.providerResultLimit += 5
this.find(this.searchQuery, [provider])
// Calculate offset based on current results count for this provider
const currentResults = this.results.find(result => result.id === provider.id)
const offset = currentResults ? currentResults.results.length : 0
// Use fixed limit of 5 with proper offset
this.findWithOffset(this.searchQuery, [provider], offset, 5)
},
addProviderFilter(providerFilter, loadMoreResultsForProvider = false) {
unifiedSearchLogger.debug('Applying provider filter', { providerFilter, loadMoreResultsForProvider })
Expand All @@ -575,7 +644,6 @@ export default defineComponent({
const isProviderFilterApplied = this.filteredProviders.some(provider => provider.id === providerFilter.id)
providerFilter.callback(!isProviderFilterApplied)
}
this.providerResultLimit = loadMoreResultsForProvider ? this.providerResultLimit : 5
this.providerActionMenuIsOpen = false
// With the possibility for other apps to add new filters
// Resulting in a possible id/provider collision
Expand Down
4 changes: 3 additions & 1 deletion core/src/services/UnifiedSearchService.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ export async function getProviders() {
* @param {string} options.limit the search
* @param {string} options.person the search
* @param {object} options.extraQueries additional queries to filter search results
* @param options.offset
* @return {object} {request: Promise, cancel: Promise}
*/
export function search({ type, query, cursor, since, until, limit, person, extraQueries = {} }) {
export function search({ type, query, cursor, since, until, limit, person, offset, extraQueries = {} }) {
/**
* Generate an axios cancel token
*/
Expand All @@ -66,6 +67,7 @@ export function search({ type, query, cursor, since, until, limit, person, extra
until,
limit,
person,
offset,
// Sending which location we're currently at
from: window.location.pathname.replace('/index.php', '') + window.location.search,
...extraQueries,
Expand Down
16 changes: 16 additions & 0 deletions lib/private/Search/SearchQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function __construct(
private int|string|null $cursor = null,
private string $route = '',
private array $routeParameters = [],
private ?int $offset = null,
) {
}

Expand Down Expand Up @@ -55,6 +56,21 @@ public function getCursor(): int|string|null {
return $this->cursor;
}

public function getOffset(): ?int {
return $this->offset;
}

public function getEffectiveOffset(): int {
// Prefer explicit offset, fall back to cursor if numeric, otherwise 0
if ($this->offset !== null) {
return $this->offset;
}
if (is_numeric($this->cursor)) {
return (int)$this->cursor;
}
return 0;
}

public function getRoute(): string {
return $this->route;
}
Expand Down
17 changes: 17 additions & 0 deletions lib/public/Search/ISearchQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ public function getLimit(): int;
*/
public function getCursor();

/**
* Get the offset for pagination (number of results to skip)
*
* @return int|null
* @since 33.0.0
*/
public function getOffset(): ?int;

/**
* Get the effective offset for pagination (offset or cursor as fallback)
* This method helps with backward compatibility for providers that use cursor as offset
*
* @return int
* @since 33.0.0
*/
public function getEffectiveOffset(): int;

/**
* @return string
* @since 20.0.0
Expand Down
2 changes: 1 addition & 1 deletion openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -12020,7 +12020,7 @@
{
"name": "limit",
"in": "query",
"description": "Maximum amount of entries, limited to 25",
"description": "Maximum amount of entries (capped by configurable unified-search.max-results-per-request, default: 25)",
"schema": {
"type": "integer",
"format": "int64",
Expand Down
Loading