diff --git a/scripts/langindex.json b/scripts/langindex.json index 90906b272..709e101f2 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -66,6 +66,7 @@ "addon.block_recentlyaccesseditems.noitems": "block_recentlyaccesseditems", "addon.block_recentlyaccesseditems.pluginname": "block_recentlyaccesseditems", "addon.block_rssclient.pluginname": "block_rss_client", + "addon.block_searchforums.pluginname": "block_search_forums", "addon.block_selfcompletion.pluginname": "block_selfcompletion", "addon.block_sitemainmenu.pluginname": "block_site_main_menu", "addon.block_starredcourses.nocourses": "block_starredcourses", @@ -684,6 +685,7 @@ "addon.mod_forum.removefromfavourites": "forum", "addon.mod_forum.reply": "forum", "addon.mod_forum.replyplaceholder": "forum", + "addon.mod_forum.searchresults": "course", "addon.mod_forum.subject": "forum", "addon.mod_forum.tagarea_forum_posts": "forum", "addon.mod_forum.thisforumhasduedate": "forum", diff --git a/src/addons/block/block.module.ts b/src/addons/block/block.module.ts index 0989bedf9..3e353219d 100644 --- a/src/addons/block/block.module.ts +++ b/src/addons/block/block.module.ts @@ -42,6 +42,7 @@ import { AddonBlockStarredCoursesModule } from './starredcourses/starredcourses. import { AddonBlockTagsModule } from './tags/tags.module'; import { AddonBlockTimelineModule } from './timeline/timeline.module'; import { AddonBlockGlobalSearchModule } from '@addons/block/globalsearch/globalsearch.module'; +import { AddonBlockSearchForumsModule } from '@addons/block/searchforums/searchforums.module'; @NgModule({ imports: [ @@ -68,6 +69,7 @@ import { AddonBlockGlobalSearchModule } from '@addons/block/globalsearch/globals AddonBlockRecentlyAccessedCoursesModule, AddonBlockRecentlyAccessedItemsModule, AddonBlockRssClientModule, + AddonBlockSearchForumsModule, AddonBlockSelfCompletionModule, AddonBlockSiteMainMenuModule, AddonBlockStarredCoursesModule, diff --git a/src/addons/block/searchforums/lang.json b/src/addons/block/searchforums/lang.json new file mode 100644 index 000000000..39f2d3380 --- /dev/null +++ b/src/addons/block/searchforums/lang.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Search forums" +} diff --git a/src/addons/block/searchforums/searchforums.module.ts b/src/addons/block/searchforums/searchforums.module.ts new file mode 100644 index 000000000..d2299d28b --- /dev/null +++ b/src/addons/block/searchforums/searchforums.module.ts @@ -0,0 +1,38 @@ +// (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 { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@features/block/services/block-delegate'; +import { AddonBlockSearchForumsHandler } from './services/block-handler'; +import { CoreBlockComponentsModule } from '@features/block/components/components.module'; + +@NgModule({ + imports: [ + IonicModule, + CoreBlockComponentsModule, + TranslateModule.forChild(), + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreBlockDelegate.registerHandler(AddonBlockSearchForumsHandler.instance); + }, + }, + ], +}) +export class AddonBlockSearchForumsModule {} diff --git a/src/addons/block/searchforums/services/block-handler.ts b/src/addons/block/searchforums/services/block-handler.ts new file mode 100644 index 000000000..c21aa4112 --- /dev/null +++ b/src/addons/block/searchforums/services/block-handler.ts @@ -0,0 +1,68 @@ +// (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 { CoreBlockHandlerData } from '@features/block/services/block-delegate'; +import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block'; +import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; +import { makeSingleton } from '@singletons'; +import { FORUM_SEARCH_PAGE_NAME } from '@addons/mod/forum/forum.module'; +import { CoreCourseBlock } from '@features/course/services/course'; +import { CoreSearchGlobalSearch } from '@features/search/services/global-search'; + +/** + * Block handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlockSearchForumsHandlerService extends CoreBlockBaseHandler { + + name = 'AddonBlockSearchForums'; + blockName = 'search_forums'; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + const enabled = await CoreSearchGlobalSearch.isEnabled(); + + if (!enabled) { + return false; + } + + const forumSearchAreas = ['mod_forum-activity', 'mod_forum-post']; + const searchAreas = await CoreSearchGlobalSearch.getSearchAreas(); + + return searchAreas.some(({ id }) => forumSearchAreas.includes(id)); + } + + /** + * @inheritdoc + */ + getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData | undefined { + if (contextLevel !== 'course') { + return; + } + + return { + title: 'addon.block_searchforums.pluginname', + class: 'addon-block-search-forums', + component: CoreBlockOnlyTitleComponent, + link: FORUM_SEARCH_PAGE_NAME, + linkParams: { courseId: instanceId }, + }; + } + +} + +export const AddonBlockSearchForumsHandler = makeSingleton(AddonBlockSearchForumsHandlerService); diff --git a/src/addons/mod/forum/components/index/index.html b/src/addons/mod/forum/components/index/index.html index 9d9fb1bcf..63fcc9497 100644 --- a/src/addons/mod/forum/components/index/index.html +++ b/src/addons/mod/forum/components/index/index.html @@ -1,5 +1,8 @@ + + + diff --git a/src/addons/mod/forum/components/index/index.ts b/src/addons/mod/forum/components/index/index.ts index 3283edc33..eb52118e8 100644 --- a/src/addons/mod/forum/components/index/index.ts +++ b/src/addons/mod/forum/components/index/index.ts @@ -57,6 +57,8 @@ import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from '../ import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CorePromisedValue } from '@classes/promised-value'; +import { CoreNavigator } from '@services/navigator'; +import { FORUM_SEARCH_PAGE_NAME } from '@addons/mod/forum/forum.module'; /** * Component that displays a forum entry page. @@ -332,6 +334,22 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom this.discussions?.destroy(); } + /** + * Open search page. + */ + async openSearch(): Promise { + if (!this.forum) { + return; + } + + await CoreNavigator.navigateToSitePath(FORUM_SEARCH_PAGE_NAME, { + params: { + courseId: this.courseId, + forumId: this.forum.id, + }, + }); + } + /** * @inheritdoc */ diff --git a/src/addons/mod/forum/forum-discussion-lazy.module.ts b/src/addons/mod/forum/forum-discussion-lazy.module.ts index beb00dbb2..697816702 100644 --- a/src/addons/mod/forum/forum-discussion-lazy.module.ts +++ b/src/addons/mod/forum/forum-discussion-lazy.module.ts @@ -36,4 +36,4 @@ const routes: Routes = [{ AddonModForumDiscussionPage, ], }) -export class AddonForumDiscussionLazyModule {} +export class AddonModForumDiscussionLazyModule {} diff --git a/src/addons/mod/forum/forum-lazy.module.ts b/src/addons/mod/forum/forum-lazy.module.ts index 9acda2d6e..8610f26b5 100644 --- a/src/addons/mod/forum/forum-lazy.module.ts +++ b/src/addons/mod/forum/forum-lazy.module.ts @@ -29,15 +29,15 @@ const mobileRoutes: Routes = [ }, { path: ':courseId/:cmId/new/:timeCreated', - loadChildren: () => import('./forum-new-discussion-lazy.module').then(m => m.AddonForumNewDiscussionLazyModule), + loadChildren: () => import('./forum-new-discussion-lazy.module').then(m => m.AddonModForumNewDiscussionLazyModule), }, { path: ':courseId/:cmId/:discussionId', - loadChildren: () => import('./forum-discussion-lazy.module').then(m => m.AddonForumDiscussionLazyModule), + loadChildren: () => import('./forum-discussion-lazy.module').then(m => m.AddonModForumDiscussionLazyModule), }, { path: 'discussion/:discussionId', // Only for discussion link handling. - loadChildren: () => import('./forum-discussion-lazy.module').then(m => m.AddonForumDiscussionLazyModule), + loadChildren: () => import('./forum-discussion-lazy.module').then(m => m.AddonModForumDiscussionLazyModule), }, ]; @@ -48,11 +48,11 @@ const tabletRoutes: Routes = [ children: [ { path: 'new/:timeCreated', - loadChildren: () => import('./forum-new-discussion-lazy.module').then(m => m.AddonForumNewDiscussionLazyModule), + loadChildren: () => import('./forum-new-discussion-lazy.module').then(m => m.AddonModForumNewDiscussionLazyModule), }, { path: ':discussionId', - loadChildren: () => import('./forum-discussion-lazy.module').then(m => m.AddonForumDiscussionLazyModule), + loadChildren: () => import('./forum-discussion-lazy.module').then(m => m.AddonModForumDiscussionLazyModule), }, ], }, diff --git a/src/addons/mod/forum/forum-new-discussion-lazy.module.ts b/src/addons/mod/forum/forum-new-discussion-lazy.module.ts index 07024b705..e0007654b 100644 --- a/src/addons/mod/forum/forum-new-discussion-lazy.module.ts +++ b/src/addons/mod/forum/forum-new-discussion-lazy.module.ts @@ -38,4 +38,4 @@ const routes: Routes = [{ AddonModForumNewDiscussionPage, ], }) -export class AddonForumNewDiscussionLazyModule {} +export class AddonModForumNewDiscussionLazyModule {} diff --git a/src/addons/mod/forum/forum-search-lazy.module.ts b/src/addons/mod/forum/forum-search-lazy.module.ts new file mode 100644 index 000000000..64e02c6ba --- /dev/null +++ b/src/addons/mod/forum/forum-search-lazy.module.ts @@ -0,0 +1,38 @@ +// (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 { AddonModForumSearchPage } from '@addons/mod/forum/pages/search/search'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; +import { CoreSearchComponentsModule } from '@features/search/components/components.module'; + +const routes: Routes = [{ + path: '', + component: AddonModForumSearchPage, +}]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + CoreSearchComponentsModule, + CoreMainMenuComponentsModule, + ], + declarations: [ + AddonModForumSearchPage, + ], +}) +export class AddonModForumSearchLazyModule {} diff --git a/src/addons/mod/forum/forum.module.ts b/src/addons/mod/forum/forum.module.ts index ce2f0533b..b0a4a78b0 100644 --- a/src/addons/mod/forum/forum.module.ts +++ b/src/addons/mod/forum/forum.module.ts @@ -52,10 +52,16 @@ export const ADDON_MOD_FORUM_SERVICES: Type[] = [ AddonModForumSyncProvider, ]; +export const FORUM_SEARCH_PAGE_NAME = 'forum/search'; + const mainMenuRoutes: Routes = [ + { + path: FORUM_SEARCH_PAGE_NAME, + loadChildren: () => import('./forum-search-lazy.module').then(m => m.AddonModForumSearchLazyModule), + }, { path: `${AddonModForumModuleHandlerService.PAGE_NAME}/discussion/:discussionId`, - loadChildren: () => import('./forum-discussion-lazy.module').then(m => m.AddonForumDiscussionLazyModule), + loadChildren: () => import('./forum-discussion-lazy.module').then(m => m.AddonModForumDiscussionLazyModule), data: { swipeEnabled: false }, }, { @@ -66,12 +72,12 @@ const mainMenuRoutes: Routes = [ [ { path: `${COURSE_CONTENTS_PATH}/${AddonModForumModuleHandlerService.PAGE_NAME}/new/:timeCreated`, - loadChildren: () => import('./forum-new-discussion-lazy.module').then(m => m.AddonForumNewDiscussionLazyModule), + loadChildren: () => import('./forum-new-discussion-lazy.module').then(m => m.AddonModForumNewDiscussionLazyModule), data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` }, }, { path: `${COURSE_CONTENTS_PATH}/${AddonModForumModuleHandlerService.PAGE_NAME}/:discussionId`, - loadChildren: () => import('./forum-discussion-lazy.module').then(m => m.AddonForumDiscussionLazyModule), + loadChildren: () => import('./forum-discussion-lazy.module').then(m => m.AddonModForumDiscussionLazyModule), data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` }, }, ], @@ -83,12 +89,12 @@ const courseContentsRoutes: Routes = conditionalRoutes( [ { path: `${AddonModForumModuleHandlerService.PAGE_NAME}/new/:timeCreated`, - loadChildren: () => import('./forum-new-discussion-lazy.module').then(m => m.AddonForumNewDiscussionLazyModule), + loadChildren: () => import('./forum-new-discussion-lazy.module').then(m => m.AddonModForumNewDiscussionLazyModule), data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` }, }, { path: `${AddonModForumModuleHandlerService.PAGE_NAME}/:discussionId`, - loadChildren: () => import('./forum-discussion-lazy.module').then(m => m.AddonForumDiscussionLazyModule), + loadChildren: () => import('./forum-discussion-lazy.module').then(m => m.AddonModForumDiscussionLazyModule), data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` }, }, ], diff --git a/src/addons/mod/forum/lang.json b/src/addons/mod/forum/lang.json index 46277f2df..42c33b92a 100644 --- a/src/addons/mod/forum/lang.json +++ b/src/addons/mod/forum/lang.json @@ -59,6 +59,7 @@ "removefromfavourites": "Unstar this discussion", "reply": "Reply", "replyplaceholder": "Write your reply...", + "searchresults": "Search results: {{$a}}", "subject": "Subject", "tagarea_forum_posts": "Forum posts", "thisforumhasduedate": "The due date for posting to this forum is {{$a}}.", diff --git a/src/addons/mod/forum/pages/search/search.html b/src/addons/mod/forum/pages/search/search.html new file mode 100644 index 000000000..24274aa3c --- /dev/null +++ b/src/addons/mod/forum/pages/search/search.html @@ -0,0 +1,48 @@ + + + + + + +

{{ forum.name }}

+

{{ 'addon.block_searchforums.pluginname' | translate }}

+
+
+
+ +
+ + + + + + + + + + + +
+ {{ 'addon.mod_forum.searchresults' | translate: { $a: resultsSource.getTotalResults() } }} +
+ + + + + + + + + + +

{{ 'core.search.empty' | translate }}

+ +

{{ 'core.search.noresults' | translate: { $a: resultsSource.getQuery() } }}

+

{{ 'core.search.noresultshelp' | translate }}

+
+
+
+
diff --git a/src/addons/mod/forum/pages/search/search.scss b/src/addons/mod/forum/pages/search/search.scss new file mode 100644 index 000000000..99577941a --- /dev/null +++ b/src/addons/mod/forum/pages/search/search.scss @@ -0,0 +1,15 @@ +:host { + --results-count-text-color: var(--gray-700); + + .results-count { + color: var(--results-count-text-color); + min-height: 0px; + margin: 8px 16px; + font-size: 14px; + } + +} + +:host-context(html.dark) { + --results-count-text-color: var(--gray-400); +} diff --git a/src/addons/mod/forum/pages/search/search.ts b/src/addons/mod/forum/pages/search/search.ts new file mode 100644 index 000000000..9fa207c70 --- /dev/null +++ b/src/addons/mod/forum/pages/search/search.ts @@ -0,0 +1,161 @@ +// (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 { AddonModForum, AddonModForumData } from '@addons/mod/forum/services/forum'; +import { Component, OnInit } from '@angular/core'; +import { CorePromisedValue } from '@classes/promised-value'; +import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreSearchGlobalSearchResultsSource } from '@features/search/classes/global-search-results-source'; +import { + CoreSearchGlobalSearch, + CoreSearchGlobalSearchFilters, + CoreSearchGlobalSearchResult, +} from '@features/search/services/global-search'; +import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; + +@Component({ + selector: 'page-addon-mod-forum-search', + templateUrl: 'search.html', + styleUrls: ['search.scss'], +}) +export class AddonModForumSearchPage implements OnInit { + + loadMoreError: string | null = null; + searchBanner: string | null = null; + resultsSource = new CoreSearchGlobalSearchResultsSource('', {}); + forum?: AddonModForumData; + searchAreaId?: string; + + private ready = new CorePromisedValue(); + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + try { + const site = CoreSites.getRequiredCurrentSite(); + const searchBanner = site.config?.searchbanner?.trim() ?? ''; + const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); + const forumId = CoreNavigator.getRouteNumberParam('forumId'); + const filters: CoreSearchGlobalSearchFilters = { + courseIds: [courseId], + }; + + if (CoreUtils.isTrueOrOne(site.config?.searchbannerenable) && searchBanner.length > 0) { + this.searchBanner = searchBanner; + } + + if (forumId) { + this.forum = await AddonModForum.getForumById(courseId, forumId); + const module = await CoreCourse.getModule(this.forum.cmid, courseId); + + filters.searchAreaIds = ['mod_forum-post']; + + if (module.contextid) { + filters.contextIds = [module.contextid]; + } + + this.searchAreaId = `AddonModForumSearch-${courseId}-${this.forum.id}`; + } else { + filters.searchAreaIds = ['mod_forum-activity', 'mod_forum-post']; + this.searchAreaId = `AddonModForumSearch-${courseId}`; + } + + this.resultsSource.setFilters(filters); + this.ready.resolve(); + } catch (error) { + CoreDomUtils.showErrorModal(error); + CoreNavigator.back(); + + return; + } + } + + /** + * Perform a new search. + * + * @param query Search query. + */ + async search(query: string): Promise { + await this.ready; + + this.resultsSource.setQuery(query); + + if (this.resultsSource.hasEmptyQuery()) { + return; + } + + await CoreDomUtils.showOperationModals('core.searching', true, async () => { + await this.resultsSource.reload(); + await CoreUtils.ignoreErrors( + CoreSearchGlobalSearch.logViewResults(this.resultsSource.getQuery(), this.resultsSource.getFilters()), + ); + + CoreAnalytics.logEvent({ + type: CoreAnalyticsEventType.VIEW_ITEM_LIST, + ws: 'core_search_view_results', + name: Translate.instant('core.search.globalsearch'), + data: { + query, + filters: JSON.stringify(this.resultsSource.getFilters()), + }, + url: CoreUrlUtils.addParamsToUrl('/search/index.php', { + q: query, + }), + }); + }); + } + + /** + * Clear search results. + */ + clearSearch(): void { + this.loadMoreError = null; + + this.resultsSource.setQuery(''); + this.resultsSource.reset(); + } + + /** + * Visit a result's origin. + * + * @param result Result to visit. + */ + async visitResult(result: CoreSearchGlobalSearchResult): Promise { + await CoreContentLinksHelper.handleLink(result.url); + } + + /** + * Load more results. + * + * @param complete Notify completion. + */ + async loadMoreResults(complete: () => void ): Promise { + try { + await this.resultsSource?.load(); + } catch (error) { + this.loadMoreError = CoreDomUtils.getErrorMessage(error); + } finally { + complete(); + } + } + +} diff --git a/src/addons/mod/forum/tests/behat/search.feature b/src/addons/mod/forum/tests/behat/search.feature new file mode 100644 index 000000000..f9cde8dc2 --- /dev/null +++ b/src/addons/mod/forum/tests/behat/search.feature @@ -0,0 +1,51 @@ +@mod @mod_forum @app @javascript @lms_from4.3 +Feature: Test Forum Search + + Background: + Given solr is installed + And the following config values are set as admin: + | enableglobalsearch | 1 | + | searchengine | solr | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + | Course 2 | C2 | + And the following "users" exist: + | username | + | student1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student1 | C2 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | forum | Test forum 1 | Test forum 1 intro | C1 | forum1 | + | forum | Test forum 2 | Test forum 2 intro | C1 | forum2 | + | forum | Test forum 3 | Test forum 3 intro | C2 | forum3 | + And the following "mod_forum > discussions" exist: + | forum | name | subject | message | + | forum1 | Initial discussion 1 | Initial discussion 1 | Initial discussion message 1 | + | forum2 | Initial discussion 2 | Initial discussion 2 | Initial discussion message 2 | + | forum3 | Initial discussion 3 | Initial discussion 3 | Initial discussion message 3 | + + # TODO test single forum search (lacking generators for post search results) + + Scenario: Search in side block + Given global search expects the query "message" and will return: + | type | idnumber | + | activity | forum1 | + | activity | forum2 | + And the following "blocks" exist: + | blockname | contextlevel | reference | + | search_forums | Course | C1 | + And I entered the course "Course 1" as "student1" in the app + When I press "Open block drawer" in the app + And I press "Search forums" in the app + Then I should find "What are you searching for?" in the app + And I should find "Search forums" in the app + + When I set the field "Search" to "message" in the app + And I press "Search" "button" in the app + Then I should find "Search results: 2" in the app + And I should find "Test forum 1" in the app + And I should find "Test forum 2" in the app diff --git a/src/core/features/search/classes/global-search-results-source.ts b/src/core/features/search/classes/global-search-results-source.ts index 183f0c4af..26caf6be3 100644 --- a/src/core/features/search/classes/global-search-results-source.ts +++ b/src/core/features/search/classes/global-search-results-source.ts @@ -28,6 +28,7 @@ export class CoreSearchGlobalSearchResultsSource extends CorePaginatedItemsManag private query: string; private filters: CoreSearchGlobalSearchFilters; private pagesLoaded = 0; + private totalResults?: number; private topResultsIds?: number[]; constructor(query: string, filters: CoreSearchGlobalSearchFilters) { @@ -93,6 +94,15 @@ export class CoreSearchGlobalSearchResultsSource extends CorePaginatedItemsManag return this.pagesLoaded; } + /** + * Get total results with the given filter. + * + * @returns Total results. + */ + getTotalResults(): number | null { + return this.totalResults ?? null; + } + /** * @inheritdoc */ @@ -107,6 +117,7 @@ export class CoreSearchGlobalSearchResultsSource extends CorePaginatedItemsManag */ reset(): void { this.pagesLoaded = 0; + delete this.totalResults; delete this.topResultsIds; super.reset(); @@ -130,6 +141,8 @@ export class CoreSearchGlobalSearchResultsSource extends CorePaginatedItemsManag const pageResults = await CoreSearchGlobalSearch.getResults(this.query, this.filters, page); + this.totalResults = pageResults.total; + results.push(...pageResults.results.filter(result => !this.topResultsIds?.includes(result.id))); return { items: results, hasMoreItems: pageResults.canLoadMore }; diff --git a/src/core/features/search/components/components.module.ts b/src/core/features/search/components/components.module.ts index 2c935312a..730dc349b 100644 --- a/src/core/features/search/components/components.module.ts +++ b/src/core/features/search/components/components.module.ts @@ -16,16 +16,19 @@ import { NgModule } from '@angular/core'; import { CoreSharedModule } from '@/core/shared.module'; import { CoreSearchBoxComponent } from './search-box/search-box'; +import { CoreSearchGlobalSearchResultComponent } from '@features/search/components/global-search-result/global-search-result'; @NgModule({ declarations: [ CoreSearchBoxComponent, + CoreSearchGlobalSearchResultComponent, ], imports: [ CoreSharedModule, ], exports: [ CoreSearchBoxComponent, + CoreSearchGlobalSearchResultComponent, ], }) export class CoreSearchComponentsModule {} diff --git a/src/core/features/search/components/global-search-result/global-search-result.html b/src/core/features/search/components/global-search-result/global-search-result.html index 2e6304296..46c186d82 100644 --- a/src/core/features/search/components/global-search-result/global-search-result.html +++ b/src/core/features/search/components/global-search-result/global-search-result.html @@ -11,14 +11,14 @@ -
-
+
+
- +
-
+
- {{ 'core.search.resultby' | translate: { $a: result.context.userName } }} + {{ 'core.search.resultby' | translate: { $a: renderedContext.userName } }}
diff --git a/src/core/features/search/components/global-search-result/global-search-result.ts b/src/core/features/search/components/global-search-result/global-search-result.ts index 8b952b009..7e60f15e6 100644 --- a/src/core/features/search/components/global-search-result/global-search-result.ts +++ b/src/core/features/search/components/global-search-result/global-search-result.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, Input, Output, EventEmitter, OnChanges } from '@angular/core'; -import { CoreSearchGlobalSearchResult } from '@features/search/services/global-search'; +import { CoreSearchGlobalSearchResult, CoreSearchGlobalSearchResultContext } from '@features/search/services/global-search'; @Component({ selector: 'core-search-global-search-result', @@ -23,6 +23,9 @@ import { CoreSearchGlobalSearchResult } from '@features/search/services/global-s export class CoreSearchGlobalSearchResultComponent implements OnChanges { @Input() result!: CoreSearchGlobalSearchResult; + @Input() showCourse?: boolean; + + renderedContext: CoreSearchGlobalSearchResultContext | null = null; renderedIcon: string | null = null; @Output() onClick = new EventEmitter(); @@ -31,9 +34,25 @@ export class CoreSearchGlobalSearchResultComponent implements OnChanges { * @inheritdoc */ ngOnChanges(): void { + this.renderedContext = this.computeRenderedContext(); this.renderedIcon = this.computeRenderedIcon(); } + /** + * Calculate the value of the context to render. + * + * @returns Rendered context. + */ + private computeRenderedContext(): CoreSearchGlobalSearchResultContext | null { + const context = { ...this.result.context } ?? {}; + + if (this.showCourse === false) { + delete context.courseName; + } + + return Object.keys(context).length > 0 ? context : null; + } + /** * Calculate the value of the icon to render. * diff --git a/src/core/features/search/search-lazy.module.ts b/src/core/features/search/search-lazy.module.ts index 61c70cce0..bc07b9618 100644 --- a/src/core/features/search/search-lazy.module.ts +++ b/src/core/features/search/search-lazy.module.ts @@ -19,7 +19,6 @@ import { CoreSearchGlobalSearchPage } from './pages/global-search/global-search' import { CoreSearchComponentsModule } from '@features/search/components/components.module'; import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; -import { CoreSearchGlobalSearchResultComponent } from '@features/search/components/global-search-result/global-search-result'; /** * Build module routes. @@ -42,7 +41,6 @@ function buildRoutes(injector: Injector): Routes { exports: [RouterModule], declarations: [ CoreSearchGlobalSearchPage, - CoreSearchGlobalSearchResultComponent, ], providers: [ { diff --git a/src/core/features/search/services/global-search.ts b/src/core/features/search/services/global-search.ts index c4cbbe0e9..b7728e195 100644 --- a/src/core/features/search/services/global-search.ts +++ b/src/core/features/search/services/global-search.ts @@ -78,7 +78,9 @@ export type CoreSearchGlobalSearchSearchArea = { export interface CoreSearchGlobalSearchFilters { searchAreaCategoryIds?: string[]; + searchAreaIds?: string[]; courseIds?: number[]; + contextIds?: number[]; } /** @@ -116,10 +118,11 @@ export class CoreSearchGlobalSearchService { query: string, filters: CoreSearchGlobalSearchFilters, page: number, - ): Promise<{ results: CoreSearchGlobalSearchResult[]; canLoadMore: boolean }> { + ): Promise<{ results: CoreSearchGlobalSearchResult[]; total: number; canLoadMore: boolean }> { if (this.filtersYieldEmptyResults(filters)) { return { results: [], + total: 0, canLoadMore: false, }; } @@ -128,7 +131,7 @@ export class CoreSearchGlobalSearchService { const params: CoreSearchGetResultsWSParams = { query, page, - filters: await this.prepareWSFilters(filters), + filters: await this.prepareAdvancedWSFilters(filters), }; const preSets = CoreSites.getReadingStrategyPreSets(CoreSitesReadingStrategy.PREFER_NETWORK); @@ -136,6 +139,7 @@ export class CoreSearchGlobalSearchService { return { results: await Promise.all((results ?? []).map(result => this.formatWSResult(result))), + total: totalcount, canLoadMore: totalcount > (page + 1) * CORE_SEARCH_GLOBAL_SEARCH_PAGE_LENGTH, }; } @@ -155,7 +159,7 @@ export class CoreSearchGlobalSearchService { const site = CoreSites.getRequiredCurrentSite(); const params: CoreSearchGetTopResultsWSParams = { query, - filters: await this.prepareWSFilters(filters), + filters: await this.prepareAdvancedWSFilters(filters), }; const preSets = CoreSites.getReadingStrategyPreSets(CoreSitesReadingStrategy.PREFER_NETWORK); @@ -207,7 +211,7 @@ export class CoreSearchGlobalSearchService { const site = CoreSites.getRequiredCurrentSite(); const params: CoreSearchViewResultsWSParams = { query, - filters: await this.prepareWSFilters(filters), + filters: await this.prepareBasicWSFilters(filters), }; await site.write('core_search_view_results', params); @@ -269,33 +273,62 @@ export class CoreSearchGlobalSearchService { * @returns Whether the given filters will return 0 results. */ protected filtersYieldEmptyResults(filters: CoreSearchGlobalSearchFilters): boolean { - return filters.courseIds?.length === 0 || filters.searchAreaCategoryIds?.length === 0; + return filters.courseIds?.length === 0 + || filters.contextIds?.length === 0 + || filters.searchAreaIds?.length === 0 + || filters.searchAreaCategoryIds?.length === 0; } /** - * Prepare search filters before sending to WS. + * Prepare basic search filters before sending to WS. * * @param filters App filters. - * @returns WS filters. + * @returns Basic WS filters. */ - protected async prepareWSFilters(filters: CoreSearchGlobalSearchFilters): Promise { + protected async prepareBasicWSFilters(filters: CoreSearchGlobalSearchFilters): Promise { 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(({ category }) => filters.searchAreaCategoryIds?.includes(category.id)) + .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 { + const wsFilters: CoreSearchAdvancedWSFilters = await this.prepareBasicWSFilters(filters); + + if (filters.contextIds) { + wsFilters.contextids = filters.contextIds; + } + + return wsFilters; + } + } export const CoreSearchGlobalSearch = makeSingleton(CoreSearchGlobalSearchService); diff --git a/src/core/features/search/stories/global-search-result.stories.ts b/src/core/features/search/stories/global-search-result.stories.ts index 125f4666b..8bc43c2bd 100644 --- a/src/core/features/search/stories/global-search-result.stories.ts +++ b/src/core/features/search/stories/global-search-result.stories.ts @@ -37,6 +37,7 @@ interface Args { module: 'forum-activity' | 'forum-post' | 'assign' | 'none'; courseContext: boolean; userContext: boolean; + showCourse: boolean; } export default > { @@ -79,6 +80,7 @@ export default > { module: 'none', courseContext: false, userContext: false, + showCourse: true, }, parameters: { design: { @@ -88,7 +90,7 @@ export default > { }, }; -const Template = story(({ image, courseContext, userContext, module, ...args }) => { +const Template = story(({ image, courseContext, userContext, module, showCourse, ...args }) => { const result: CoreSearchGlobalSearchResult = { ...args, id: 1, @@ -126,7 +128,7 @@ const Template = story(({ image, courseContext, userContext, module, ...ar return { component: CoreSearchGlobalSearchResultComponent, - props: { result }, + props: { result, showCourse }, }; });