454 lines
14 KiB
TypeScript
454 lines
14 KiB
TypeScript
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import { Injectable } from '@angular/core';
|
|
import { makeSingleton } from '@singletons';
|
|
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
|
import { CoreWSExternalWarning } from '@services/ws';
|
|
import { CoreCourseListItem, CoreCourses } from '@features/courses/services/courses';
|
|
import { CoreUserWithAvatar } from '@components/user-avatar/user-avatar';
|
|
import { CoreUser } from '@features/user/services/user';
|
|
import { CoreSite } from '@classes/site';
|
|
|
|
declare module '@singletons/events' {
|
|
|
|
/**
|
|
* Augment CoreEventsData interface with events specific to this service.
|
|
*
|
|
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
|
*/
|
|
export interface CoreEventsData {
|
|
[CORE_SEARCH_GLOBAL_SEARCH_FILTERS_UPDATED]: CoreSearchGlobalSearchFilters;
|
|
}
|
|
|
|
}
|
|
|
|
export const CORE_SEARCH_GLOBAL_SEARCH_PAGE_LENGTH = 10;
|
|
export const CORE_SEARCH_GLOBAL_SEARCH_FILTERS_UPDATED = 'core-search-global-search-filters-updated';
|
|
|
|
export type CoreSearchGlobalSearchResult = {
|
|
id: number;
|
|
title: string;
|
|
url: string;
|
|
content?: string;
|
|
context?: CoreSearchGlobalSearchResultContext;
|
|
module?: CoreSearchGlobalSearchResultModule;
|
|
component?: CoreSearchGlobalSearchResultComponent;
|
|
course?: CoreCourseListItem;
|
|
user?: CoreUserWithAvatar;
|
|
};
|
|
|
|
export type CoreSearchGlobalSearchResultContext = {
|
|
userName?: string;
|
|
courseName?: string;
|
|
};
|
|
|
|
export type CoreSearchGlobalSearchResultModule = {
|
|
name: string;
|
|
iconurl: string;
|
|
area: string;
|
|
};
|
|
|
|
export type CoreSearchGlobalSearchResultComponent = {
|
|
name: string;
|
|
iconurl: string;
|
|
};
|
|
|
|
export type CoreSearchGlobalSearchSearchAreaCategory = {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
|
|
export type CoreSearchGlobalSearchSearchArea = {
|
|
id: string;
|
|
name: string;
|
|
category: CoreSearchGlobalSearchSearchAreaCategory;
|
|
};
|
|
|
|
export interface CoreSearchGlobalSearchFilters {
|
|
searchAreaCategoryIds?: string[];
|
|
searchAreaIds?: string[];
|
|
courseIds?: number[];
|
|
contextIds?: number[];
|
|
}
|
|
|
|
/**
|
|
* Service to perform global searches.
|
|
*/
|
|
@Injectable({ providedIn: 'root' })
|
|
export class CoreSearchGlobalSearchService {
|
|
|
|
private static readonly SEARCH_AREAS_CACHE_KEY = 'CoreSearchGlobalSearch:SearchAreas';
|
|
|
|
/**
|
|
* Check whether global search is enabled or not.
|
|
*
|
|
* @returns Whether global search is enabled or not.
|
|
*/
|
|
async isEnabled(siteId?: string): Promise<boolean> {
|
|
const site = siteId
|
|
? await CoreSites.getSite(siteId)
|
|
: CoreSites.getRequiredCurrentSite();
|
|
|
|
return !site?.isFeatureDisabled('NoDelegate_GlobalSearch')
|
|
&& site?.wsAvailable('core_search_get_results') // @since 4.3
|
|
&& site?.canUseAdvancedFeature('enableglobalsearch');
|
|
}
|
|
|
|
/**
|
|
* Get results.
|
|
*
|
|
* @param query Search query.
|
|
* @param filters Search filters.
|
|
* @param page Page.
|
|
* @returns Search results.
|
|
*/
|
|
async getResults(
|
|
query: string,
|
|
filters: CoreSearchGlobalSearchFilters,
|
|
page: number,
|
|
): Promise<{ results: CoreSearchGlobalSearchResult[]; total: number; canLoadMore: boolean }> {
|
|
if (this.filtersYieldEmptyResults(filters)) {
|
|
return {
|
|
results: [],
|
|
total: 0,
|
|
canLoadMore: false,
|
|
};
|
|
}
|
|
|
|
const site = CoreSites.getRequiredCurrentSite();
|
|
const params: CoreSearchGetResultsWSParams = {
|
|
query,
|
|
page,
|
|
filters: await this.prepareAdvancedWSFilters(filters),
|
|
};
|
|
const preSets = CoreSites.getReadingStrategyPreSets(CoreSitesReadingStrategy.PREFER_NETWORK);
|
|
|
|
const { totalcount, results } = await site.read<CoreSearchGetResultsWSResponse>('core_search_get_results', params, preSets);
|
|
|
|
return {
|
|
results: await Promise.all((results ?? []).map(result => this.formatWSResult(result))),
|
|
total: totalcount,
|
|
canLoadMore: totalcount > (page + 1) * CORE_SEARCH_GLOBAL_SEARCH_PAGE_LENGTH,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get top results.
|
|
*
|
|
* @param query Search query.
|
|
* @param filters Search filters.
|
|
* @returns Top search results.
|
|
*/
|
|
async getTopResults(query: string, filters: CoreSearchGlobalSearchFilters): Promise<CoreSearchGlobalSearchResult[]> {
|
|
if (this.filtersYieldEmptyResults(filters)) {
|
|
return [];
|
|
}
|
|
|
|
const site = CoreSites.getRequiredCurrentSite();
|
|
const params: CoreSearchGetTopResultsWSParams = {
|
|
query,
|
|
filters: await this.prepareAdvancedWSFilters(filters),
|
|
};
|
|
const preSets = CoreSites.getReadingStrategyPreSets(CoreSitesReadingStrategy.PREFER_NETWORK);
|
|
|
|
const { results } = await site.read<CoreSearchGetTopResultsWSResponse>('core_search_get_top_results', params, preSets);
|
|
|
|
return await Promise.all((results ?? []).map(result => this.formatWSResult(result)));
|
|
}
|
|
|
|
/**
|
|
* Get available search areas.
|
|
*
|
|
* @returns Search areas.
|
|
*/
|
|
async getSearchAreas(): Promise<CoreSearchGlobalSearchSearchArea[]> {
|
|
const site = CoreSites.getRequiredCurrentSite();
|
|
const params: CoreSearchGetSearchAreasListWSParams = {};
|
|
|
|
const { areas } = await site.read<CoreSearchGetSearchAreasListWSResponse>('core_search_get_search_areas_list', params, {
|
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
|
cacheKey: CoreSearchGlobalSearchService.SEARCH_AREAS_CACHE_KEY,
|
|
});
|
|
|
|
return areas.map(area => ({
|
|
id: area.id,
|
|
name: area.name,
|
|
category: {
|
|
id: area.categoryid,
|
|
name: area.categoryname,
|
|
},
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Invalidate search areas cache.
|
|
*/
|
|
async invalidateSearchAreas(): Promise<void> {
|
|
const site = CoreSites.getRequiredCurrentSite();
|
|
|
|
await site.invalidateWsCacheForKey(CoreSearchGlobalSearchService.SEARCH_AREAS_CACHE_KEY);
|
|
}
|
|
|
|
/**
|
|
* Log event for viewing results.
|
|
*
|
|
* @param query Search query.
|
|
* @param filters Search filters.
|
|
*/
|
|
async logViewResults(query: string, filters: CoreSearchGlobalSearchFilters): Promise<void> {
|
|
const site = CoreSites.getRequiredCurrentSite();
|
|
const params: CoreSearchViewResultsWSParams = {
|
|
query,
|
|
filters: await this.prepareBasicWSFilters(filters),
|
|
};
|
|
|
|
await site.write<CoreSearchViewResultsWSResponse>('core_search_view_results', params);
|
|
}
|
|
|
|
/**
|
|
* Format a WS result to be used in the app.
|
|
*
|
|
* @param wsResult WS result.
|
|
* @returns App result.
|
|
*/
|
|
protected async formatWSResult(wsResult: CoreSearchWSResult): Promise<CoreSearchGlobalSearchResult> {
|
|
const result: CoreSearchGlobalSearchResult = {
|
|
id: wsResult.itemid,
|
|
title: wsResult.title,
|
|
url: wsResult.docurl,
|
|
content: wsResult.content,
|
|
};
|
|
|
|
if (wsResult.componentname === 'core_user') {
|
|
const user = await CoreUser.getProfile(wsResult.itemid);
|
|
|
|
result.user = user;
|
|
} else if (wsResult.componentname === 'core_course' && wsResult.areaname === 'course') {
|
|
const course = await CoreCourses.getCourseByField('id', wsResult.itemid);
|
|
|
|
result.course = course;
|
|
} else {
|
|
if (wsResult.userfullname || wsResult.coursefullname) {
|
|
result.context = {
|
|
userName: wsResult.userfullname,
|
|
courseName: wsResult.coursefullname,
|
|
};
|
|
}
|
|
|
|
if (wsResult.iconurl) {
|
|
if (wsResult.componentname.startsWith('mod_')) {
|
|
result.module = {
|
|
name: wsResult.componentname.substring(4),
|
|
iconurl: wsResult.iconurl,
|
|
area: wsResult.areaname,
|
|
};
|
|
} else {
|
|
result.component = {
|
|
name: wsResult.componentname,
|
|
iconurl: wsResult.iconurl,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Check whether the given filter will necessarily yield an empty list of results.
|
|
*
|
|
* @param filters Filters.
|
|
* @returns Whether the given filters will return 0 results.
|
|
*/
|
|
protected filtersYieldEmptyResults(filters: CoreSearchGlobalSearchFilters): boolean {
|
|
return filters.courseIds?.length === 0
|
|
|| filters.contextIds?.length === 0
|
|
|| filters.searchAreaIds?.length === 0
|
|
|| filters.searchAreaCategoryIds?.length === 0;
|
|
}
|
|
|
|
/**
|
|
* Prepare basic search filters before sending to WS.
|
|
*
|
|
* @param filters App filters.
|
|
* @returns Basic WS filters.
|
|
*/
|
|
protected async prepareBasicWSFilters(filters: CoreSearchGlobalSearchFilters): Promise<CoreSearchBasicWSFilters> {
|
|
const wsFilters: CoreSearchBasicWSFilters = {};
|
|
|
|
if (filters.courseIds) {
|
|
wsFilters.courseids = filters.courseIds;
|
|
}
|
|
|
|
if (filters.searchAreaIds) {
|
|
wsFilters.areaids = filters.searchAreaIds;
|
|
}
|
|
|
|
if (filters.searchAreaCategoryIds) {
|
|
const searchAreas = await this.getSearchAreas();
|
|
|
|
wsFilters.areaids = searchAreas
|
|
.filter(({ id, category }) => {
|
|
if (filters.searchAreaIds && !filters.searchAreaIds.includes(id)) {
|
|
return false;
|
|
}
|
|
|
|
return filters.searchAreaCategoryIds?.includes(category.id);
|
|
})
|
|
.map(({ id }) => id);
|
|
}
|
|
|
|
return wsFilters;
|
|
}
|
|
|
|
/**
|
|
* Prepare advanced search filters before sending to WS.
|
|
*
|
|
* @param filters App filters.
|
|
* @returns Advanced WS filters.
|
|
*/
|
|
protected async prepareAdvancedWSFilters(filters: CoreSearchGlobalSearchFilters): Promise<CoreSearchAdvancedWSFilters> {
|
|
const wsFilters: CoreSearchAdvancedWSFilters = await this.prepareBasicWSFilters(filters);
|
|
|
|
if (filters.contextIds) {
|
|
wsFilters.contextids = filters.contextIds;
|
|
}
|
|
|
|
return wsFilters;
|
|
}
|
|
|
|
}
|
|
|
|
export const CoreSearchGlobalSearch = makeSingleton(CoreSearchGlobalSearchService);
|
|
|
|
/**
|
|
* Params of core_search_get_results WS.
|
|
*/
|
|
type CoreSearchGetResultsWSParams = {
|
|
query: string; // The search query.
|
|
filters?: CoreSearchAdvancedWSFilters; // Filters to apply.
|
|
page?: number; // Results page number starting from 0, defaults to the first page.
|
|
};
|
|
|
|
/**
|
|
* Params of core_search_get_search_areas_list WS.
|
|
*/
|
|
type CoreSearchGetSearchAreasListWSParams = {
|
|
cat?: string; // Category to filter areas.
|
|
};
|
|
|
|
/**
|
|
* Params of core_search_view_results WS.
|
|
*/
|
|
type CoreSearchViewResultsWSParams = {
|
|
query: string; // The search query.
|
|
filters?: CoreSearchBasicWSFilters; // Filters to apply.
|
|
page?: number; // Results page number starting from 0, defaults to the first page.
|
|
};
|
|
|
|
/**
|
|
* Params of core_search_get_top_results WS.
|
|
*/
|
|
type CoreSearchGetTopResultsWSParams = {
|
|
query: string; // The search query.
|
|
filters?: CoreSearchAdvancedWSFilters; // Filters to apply.
|
|
};
|
|
|
|
/**
|
|
* Search result returned in WS.
|
|
*/
|
|
type CoreSearchWSResult = { // Search results.
|
|
itemid: number; // Unique id in the search area scope.
|
|
componentname: string; // Component name.
|
|
areaname: string; // Search area name.
|
|
courseurl: string; // Result course url.
|
|
coursefullname: string; // Result course fullname.
|
|
timemodified: number; // Result modified time.
|
|
title: string; // Result title.
|
|
docurl: string; // Result url.
|
|
iconurl?: string; // Icon url.
|
|
content?: string; // Result contents.
|
|
contextid: number; // Result context id.
|
|
contexturl: string; // Result context url.
|
|
description1?: string; // Extra result contents, depends on the search area.
|
|
description2?: string; // Extra result contents, depends on the search area.
|
|
multiplefiles?: number; // Whether multiple files are returned or not.
|
|
filenames?: string[]; // Result file names if present.
|
|
filename?: string; // Result file name if present.
|
|
userid?: number; // User id.
|
|
userurl?: string; // User url.
|
|
userfullname?: string; // User fullname.
|
|
textformat: number; // Text fields format, it is the same for all of them.
|
|
};
|
|
|
|
/**
|
|
* Basic search filters used in WS.
|
|
*/
|
|
type CoreSearchBasicWSFilters = {
|
|
title?: string; // Result title.
|
|
areaids?: string[]; // Restrict results to these areas.
|
|
courseids?: number[]; // Restrict results to these courses.
|
|
timestart?: number; // Docs modified after this date.
|
|
timeend?: number; // Docs modified before this date.
|
|
};
|
|
|
|
/**
|
|
* Advanced search filters used in WS.
|
|
*/
|
|
type CoreSearchAdvancedWSFilters = CoreSearchBasicWSFilters & {
|
|
contextids?: number[]; // Restrict results to these contexts.
|
|
cat?: string; // Category to filter areas.
|
|
userids?: number[]; // Restrict results to these users.
|
|
groupids?: number[]; // Restrict results to these groups.
|
|
mycoursesonly?: boolean; // Only results from my courses.
|
|
order?: string; // How to order.
|
|
};
|
|
|
|
/**
|
|
* Data returned by core_search_get_results WS.
|
|
*/
|
|
type CoreSearchGetResultsWSResponse = {
|
|
totalcount: number; // Total number of results.
|
|
results?: CoreSearchWSResult[];
|
|
};
|
|
|
|
/**
|
|
* Data returned by core_search_get_search_areas_list WS.
|
|
*/
|
|
type CoreSearchGetSearchAreasListWSResponse = {
|
|
areas: { // Search areas.
|
|
id: string; // Search area id.
|
|
categoryid: string; // Category id.
|
|
categoryname: string; // Category name.
|
|
name: string; // Search area name.
|
|
}[];
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Data returned by core_search_view_results WS.
|
|
*/
|
|
type CoreSearchViewResultsWSResponse = {
|
|
status: boolean; // Status: true if success.
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Data returned by core_search_get_top_results WS.
|
|
*/
|
|
type CoreSearchGetTopResultsWSResponse = {
|
|
results?: CoreSearchWSResult[];
|
|
};
|