MOBILE-3371 search: Filter global search
parent
65a4cc98f7
commit
6cf958eb8a
|
@ -7,7 +7,7 @@
|
|||
"",
|
||||
"@Component({",
|
||||
" selector: '$2${TM_FILENAME_BASE}',",
|
||||
" templateUrl: '$2${TM_FILENAME_BASE}.html',",
|
||||
" templateUrl: '${TM_FILENAME_BASE}.html',",
|
||||
"})",
|
||||
"export class ${1:${TM_FILENAME_BASE}}Component {",
|
||||
"",
|
||||
|
@ -110,6 +110,24 @@
|
|||
],
|
||||
"description": "[Moodle] Create a Pure Singleton"
|
||||
},
|
||||
"[Moodle] Events": {
|
||||
"prefix": "maeventsdeclaration",
|
||||
"body": [
|
||||
"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 {",
|
||||
" [$1]: $2;",
|
||||
" }",
|
||||
"",
|
||||
"}"
|
||||
],
|
||||
"description": ""
|
||||
},
|
||||
"Innherit doc": {
|
||||
"prefix": "inheritdoc",
|
||||
"body": [
|
||||
|
|
|
@ -2310,7 +2310,12 @@
|
|||
"core.scanqr": "local_moodlemobileapp",
|
||||
"core.scrollbackward": "local_moodlemobileapp",
|
||||
"core.scrollforward": "local_moodlemobileapp",
|
||||
"core.search.allcourses": "search",
|
||||
"core.search.allcategories": "local_moodlemobileapp",
|
||||
"core.search.empty": "local_moodlemobileapp",
|
||||
"core.search.filtercategories": "local_moodlemobileapp",
|
||||
"core.search.filtercourses": "local_moodlemobileapp",
|
||||
"core.search.filterheader": "search",
|
||||
"core.search.globalsearch": "search",
|
||||
"core.search.noresults": "local_moodlemobileapp",
|
||||
"core.search.noresultshelp": "local_moodlemobileapp",
|
||||
|
|
|
@ -0,0 +1,267 @@
|
|||
// (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 { Component, OnInit, Input } from '@angular/core';
|
||||
import { CoreEnrolledCourseData, CoreCourses } from '@features/courses/services/courses';
|
||||
import {
|
||||
CoreSearchGlobalSearchFilters,
|
||||
CoreSearchGlobalSearch,
|
||||
CoreSearchGlobalSearchSearchAreaCategory,
|
||||
CORE_SEARCH_GLOBAL_SEARCH_FILTERS_UPDATED,
|
||||
} from '@features/search/services/global-search';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { ModalController } from '@singletons';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
||||
type Filter<T=unknown> = T & { checked: boolean };
|
||||
|
||||
@Component({
|
||||
selector: 'core-search-global-search-filters',
|
||||
templateUrl: 'global-search-filters.html',
|
||||
styleUrls: ['./global-search-filters.scss'],
|
||||
})
|
||||
export class CoreSearchGlobalSearchFiltersComponent implements OnInit {
|
||||
|
||||
allSearchAreaCategories: boolean | null = true;
|
||||
searchAreaCategories: Filter<CoreSearchGlobalSearchSearchAreaCategory>[] = [];
|
||||
allCourses: boolean | null = true;
|
||||
courses: Filter<CoreEnrolledCourseData>[] = [];
|
||||
|
||||
@Input() filters?: CoreSearchGlobalSearchFilters;
|
||||
|
||||
private newFilters: CoreSearchGlobalSearchFilters = {};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.newFilters = this.filters ?? {};
|
||||
|
||||
await this.updateSearchAreaCategories();
|
||||
await this.updateCourses();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close popover.
|
||||
*/
|
||||
close(): void {
|
||||
ModalController.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox for all search area categories has been updated.
|
||||
*/
|
||||
allSearchAreaCategoriesUpdated(): void {
|
||||
if (this.allSearchAreaCategories === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checked = this.allSearchAreaCategories;
|
||||
|
||||
this.searchAreaCategories.forEach(searchAreaCategory => {
|
||||
if (searchAreaCategory.checked === checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
searchAreaCategory.checked = checked;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox for one search area category has been updated.
|
||||
*
|
||||
* @param searchAreaCategory Filter status.
|
||||
*/
|
||||
onSearchAreaCategoryInputChanged(searchAreaCategory: Filter<CoreSearchGlobalSearchSearchAreaCategory>): void {
|
||||
if (
|
||||
!searchAreaCategory.checked &&
|
||||
this.newFilters.searchAreaCategoryIds &&
|
||||
!this.newFilters.searchAreaCategoryIds.includes(searchAreaCategory.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
searchAreaCategory.checked &&
|
||||
(!this.newFilters.searchAreaCategoryIds || this.newFilters.searchAreaCategoryIds.includes(searchAreaCategory.id))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.searchAreaCategoryUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox for all courses has been updated.
|
||||
*/
|
||||
allCoursesUpdated(): void {
|
||||
if (this.allCourses === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checked = this.allCourses;
|
||||
|
||||
this.courses.forEach(course => {
|
||||
if (course.checked === checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
course.checked = checked;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox for one course has been updated.
|
||||
*
|
||||
* @param course Filter status.
|
||||
*/
|
||||
onCourseInputChanged(course: Filter<CoreEnrolledCourseData>): void {
|
||||
if (!course.checked && this.newFilters.courseIds && !this.newFilters.courseIds.includes(course.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (course.checked && (!this.newFilters.courseIds || this.newFilters.courseIds.includes(course.id))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.courseUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh filters.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
async refreshFilters(refresher?: IonRefresher): Promise<void> {
|
||||
await CoreUtils.ignoreErrors(Promise.all([
|
||||
CoreSearchGlobalSearch.invalidateSearchAreas(),
|
||||
CoreCourses.invalidateUserCourses(),
|
||||
]));
|
||||
|
||||
await this.updateSearchAreaCategories();
|
||||
await this.updateCourses();
|
||||
|
||||
refresher?.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update search area categories.
|
||||
*/
|
||||
private async updateSearchAreaCategories(): Promise<void> {
|
||||
const searchAreas = await CoreSearchGlobalSearch.getSearchAreas();
|
||||
const searchAreaCategoryIds = new Set();
|
||||
|
||||
this.searchAreaCategories = [];
|
||||
|
||||
for (const searchArea of searchAreas) {
|
||||
if (searchAreaCategoryIds.has(searchArea.category.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
searchAreaCategoryIds.add(searchArea.category.id);
|
||||
this.searchAreaCategories.push({
|
||||
...searchArea.category,
|
||||
checked: this.filters?.searchAreaCategoryIds?.includes(searchArea.category.id) ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
this.allSearchAreaCategories = this.getGroupFilterStatus(this.searchAreaCategories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update courses.
|
||||
*/
|
||||
private async updateCourses(): Promise<void> {
|
||||
const courses = await CoreCourses.getUserCourses();
|
||||
|
||||
this.courses = courses
|
||||
.sort((a, b) => (a.shortname?.toLowerCase() ?? '').localeCompare(b.shortname?.toLowerCase() ?? ''))
|
||||
.map(course => ({
|
||||
...course,
|
||||
checked: this.filters?.courseIds?.includes(course.id) ?? true,
|
||||
}));
|
||||
|
||||
this.allCourses = this.getGroupFilterStatus(this.courses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox for one search area category has been updated.
|
||||
*/
|
||||
private searchAreaCategoryUpdated(): void {
|
||||
const filterStatus = this.getGroupFilterStatus(this.searchAreaCategories);
|
||||
|
||||
if (filterStatus !== this.allSearchAreaCategories) {
|
||||
this.allSearchAreaCategories = filterStatus;
|
||||
}
|
||||
|
||||
this.emitFiltersUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Course filter status has been updated.
|
||||
*/
|
||||
private courseUpdated(): void {
|
||||
const filterStatus = this.getGroupFilterStatus(this.courses);
|
||||
|
||||
if (filterStatus !== this.allCourses) {
|
||||
this.allCourses = filterStatus;
|
||||
}
|
||||
|
||||
this.emitFiltersUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status for a filter representing a group of filters.
|
||||
*
|
||||
* @param filters Filters in the group.
|
||||
* @returns Group filter status. This will be true if all filters are checked, false if all filters are unchecked,
|
||||
* or null if filters have mixed states.
|
||||
*/
|
||||
private getGroupFilterStatus(filters: Filter[]): boolean | null {
|
||||
if (filters.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstChecked = filters[0].checked;
|
||||
|
||||
for (const filter of filters) {
|
||||
if (filter.checked === firstChecked) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return firstChecked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit filters updated event.
|
||||
*/
|
||||
private emitFiltersUpdated(): void {
|
||||
this.newFilters = {};
|
||||
|
||||
if (!this.allSearchAreaCategories) {
|
||||
this.newFilters.searchAreaCategoryIds = this.searchAreaCategories.filter(({ checked }) => checked).map(({ id }) => id);
|
||||
}
|
||||
|
||||
if (!this.allCourses) {
|
||||
this.newFilters.courseIds = this.courses.filter(({ checked }) => checked).map(({ id }) => id);
|
||||
}
|
||||
|
||||
CoreEvents.trigger(CORE_SEARCH_GLOBAL_SEARCH_FILTERS_UPDATED, this.newFilters);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
<h1>{{ 'core.search.filterheader' | translate }}</h1>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" (click)="close()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon name="fas-xmark" slot="icon-only" aria-hidden=true></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content [fullscreen]="true">
|
||||
<ion-refresher slot="fixed" (ionRefresh)="refreshFilters($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<ion-list>
|
||||
<ng-container *ngIf="searchAreaCategories.length > 0">
|
||||
<core-spacer></core-spacer>
|
||||
<ion-item class="ion-text-wrap help">
|
||||
<ion-label>
|
||||
{{ 'core.search.filtercategories' | translate }}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>{{ 'core.search.allcategories' | translate }}</ion-label>
|
||||
<ion-checkbox slot="end" [(ngModel)]="allSearchAreaCategories" [indeterminate]="allSearchAreaCategories === null"
|
||||
(ionChange)="allSearchAreaCategoriesUpdated()"></ion-checkbox>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let searchAreaCategory of searchAreaCategories">
|
||||
<ion-label>
|
||||
<core-format-text [text]="searchAreaCategory.name"></core-format-text>
|
||||
</ion-label>
|
||||
<ion-checkbox slot="end" [(ngModel)]="searchAreaCategory.checked"
|
||||
(ionChange)="onSearchAreaCategoryInputChanged(searchAreaCategory)"></ion-checkbox>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="courses.length > 0">
|
||||
<core-spacer></core-spacer>
|
||||
<ion-item class="ion-text-wrap help">
|
||||
<ion-label>
|
||||
{{ 'core.search.filtercourses' | translate }}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>{{ 'core.search.allcourses' | translate }}</ion-label>
|
||||
<ion-checkbox slot="end" [(ngModel)]="allCourses" [indeterminate]="allCourses === null" (ionChange)="allCoursesUpdated()">
|
||||
</ion-checkbox>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let course of courses">
|
||||
<ion-label>
|
||||
<core-format-text [text]="course.shortname"></core-format-text>
|
||||
</ion-label>
|
||||
<ion-checkbox slot="end" [(ngModel)]="course.checked" (ionChange)="onCourseInputChanged(course)"></ion-checkbox>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</ion-content>
|
|
@ -0,0 +1,30 @@
|
|||
// (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 { CoreSharedModule } from '@/core/shared.module';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { CoreSearchGlobalSearchFiltersComponent } from './global-search-filters.component';
|
||||
|
||||
export { CoreSearchGlobalSearchFiltersComponent };
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreSearchGlobalSearchFiltersComponent,
|
||||
],
|
||||
})
|
||||
export class CoreSearchGlobalSearchFiltersComponentModule {}
|
|
@ -0,0 +1,21 @@
|
|||
:host {
|
||||
--help-text-color: var(--gray-700);
|
||||
|
||||
ion-item.help {
|
||||
color: var(--help-text-color);
|
||||
|
||||
ion-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ion-item:not(.help) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
:host-context(html.dark) {
|
||||
--help-text-color: var(--gray-400);
|
||||
}
|
|
@ -1,5 +1,10 @@
|
|||
{
|
||||
"allcourses": "All courses",
|
||||
"allcategories": "All categories",
|
||||
"empty": "What are you searching for?",
|
||||
"filtercategories": "Filter results by",
|
||||
"filtercourses": "Search in",
|
||||
"filterheader": "Filter",
|
||||
"globalsearch": "Global search",
|
||||
"noresults": "No results for \"{{$a}}\"",
|
||||
"noresultshelp": "Check for typos or try using different keywords",
|
||||
|
|
|
@ -42,5 +42,11 @@
|
|||
<p><small>{{ 'core.search.noresultshelp' | translate }}</small></p>
|
||||
</ng-container>
|
||||
</core-empty-box>
|
||||
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
|
||||
<ion-fab-button (click)="openFilters()" [attr.aria-label]="'core.filter' | translate">
|
||||
<ion-icon name="fas-filter" aria-hidden="true"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
|
|
@ -12,26 +12,33 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
||||
import { CoreSearchGlobalSearchResultsSource } from '@features/search/classes/global-search-results-source';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreSearchGlobalSearchResult, CoreSearchGlobalSearch } from '@features/search/services/global-search';
|
||||
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreUrlUtils } from '@services/utils/url';
|
||||
import { CoreEvents, CoreEventObserver } from '@singletons/events';
|
||||
import {
|
||||
CoreSearchGlobalSearchResult,
|
||||
CoreSearchGlobalSearchFilters,
|
||||
CORE_SEARCH_GLOBAL_SEARCH_FILTERS_UPDATED,
|
||||
CoreSearchGlobalSearch,
|
||||
} from '@features/search/services/global-search';
|
||||
|
||||
@Component({
|
||||
selector: 'page-core-search-global-search',
|
||||
templateUrl: 'global-search.html',
|
||||
})
|
||||
export class CoreSearchGlobalSearchPage implements OnInit {
|
||||
export class CoreSearchGlobalSearchPage implements OnInit, OnDestroy {
|
||||
|
||||
loadMoreError: string | null = null;
|
||||
searchBanner: string | null = null;
|
||||
resultsSource = new CoreSearchGlobalSearchResultsSource('', {});
|
||||
private filtersObserver?: CoreEventObserver;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
|
@ -43,6 +50,18 @@ export class CoreSearchGlobalSearchPage implements OnInit {
|
|||
if (CoreUtils.isTrueOrOne(site.config?.searchbannerenable) && searchBanner.length > 0) {
|
||||
this.searchBanner = searchBanner;
|
||||
}
|
||||
|
||||
this.filtersObserver = CoreEvents.on(
|
||||
CORE_SEARCH_GLOBAL_SEARCH_FILTERS_UPDATED,
|
||||
filters => this.resultsSource.setFilters(filters),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.filtersObserver?.off();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,6 +107,23 @@ export class CoreSearchGlobalSearchPage implements OnInit {
|
|||
this.resultsSource.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open filters.
|
||||
*/
|
||||
async openFilters(): Promise<void> {
|
||||
const { CoreSearchGlobalSearchFiltersComponent } =
|
||||
await import('@features/search/components/global-search-filters/global-search-filters.module');
|
||||
|
||||
await CoreDomUtils.openSideModal<CoreSearchGlobalSearchFilters>({
|
||||
component: CoreSearchGlobalSearchFiltersComponent,
|
||||
componentProps: { filters: this.resultsSource.getFilters() },
|
||||
});
|
||||
|
||||
if (!this.resultsSource.hasEmptyQuery() && this.resultsSource.isDirty()) {
|
||||
await CoreDomUtils.showOperationModals('core.searching', true, () => this.resultsSource.reload());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit a result's origin.
|
||||
*
|
||||
|
|
|
@ -19,8 +19,23 @@ 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;
|
||||
|
@ -66,6 +81,8 @@ export interface CoreSearchGlobalSearchFilters {
|
|||
@Injectable({ providedIn: 'root' })
|
||||
export class CoreSearchGlobalSearchService {
|
||||
|
||||
private static readonly SEARCH_AREAS_CACHE_KEY = 'CoreSearchGlobalSearch:SearchAreas';
|
||||
|
||||
/**
|
||||
* Get results.
|
||||
*
|
||||
|
@ -79,6 +96,13 @@ export class CoreSearchGlobalSearchService {
|
|||
filters: CoreSearchGlobalSearchFilters,
|
||||
page: number,
|
||||
): Promise<{ results: CoreSearchGlobalSearchResult[]; canLoadMore: boolean }> {
|
||||
if (this.filtersYieldEmptyResults(filters)) {
|
||||
return {
|
||||
results: [],
|
||||
canLoadMore: false,
|
||||
};
|
||||
}
|
||||
|
||||
const site = CoreSites.getRequiredCurrentSite();
|
||||
const params: CoreSearchGetResultsWSParams = {
|
||||
query,
|
||||
|
@ -103,6 +127,10 @@ export class CoreSearchGlobalSearchService {
|
|||
* @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,
|
||||
|
@ -124,7 +152,10 @@ export class CoreSearchGlobalSearchService {
|
|||
const site = CoreSites.getRequiredCurrentSite();
|
||||
const params: CoreSearchGetSearchAreasListWSParams = {};
|
||||
|
||||
const { areas } = await site.read<CoreSearchGetSearchAreasListWSResponse>('core_search_get_search_areas_list', params);
|
||||
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,
|
||||
|
@ -136,6 +167,15 @@ export class CoreSearchGlobalSearchService {
|
|||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -194,6 +234,16 @@ export class CoreSearchGlobalSearchService {
|
|||
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.searchAreaCategoryIds?.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare search filters before sending to WS.
|
||||
*
|
||||
|
|
|
@ -100,6 +100,27 @@ Feature: Test Global Search
|
|||
|
||||
# TODO test other results like course, user, and messages (global search generator not supported)
|
||||
|
||||
Scenario: Filter results
|
||||
Given global search expects the query "page" and will return:
|
||||
| type | idnumber |
|
||||
| activity | page01 |
|
||||
And I entered the app as "student1"
|
||||
When I press the more menu button in the app
|
||||
And I press "Global search" in the app
|
||||
And I set the field "Search" to "page" in the app
|
||||
And I press "Search" "button" in the app
|
||||
Then I should find "Test page 01" in the app
|
||||
|
||||
When I press "Filter" in the app
|
||||
And I press "C1" in the app
|
||||
And I press "Users" in the app
|
||||
And global search expects the query "page" and will return:
|
||||
| type | idnumber |
|
||||
| activity | page02 |
|
||||
And I press "Close" in the app
|
||||
Then I should find "Test page 02" in the app
|
||||
But I should not find "Test page 01" in the app
|
||||
|
||||
Scenario: See search banner
|
||||
Given the following config values are set as admin:
|
||||
| searchbannerenable | 1 |
|
||||
|
|
Loading…
Reference in New Issue