From 254dcb3daa65c44b9cadc8574d2b472b6bf524c3 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 14 Sep 2023 13:12:42 +0200 Subject: [PATCH] MOBILE-3371 search: Support blocks and sections --- .../pre-rendered-block/pre-rendered-block.ts | 6 +++- .../components/side-blocks/side-blocks.ts | 24 ++++++++++++++- .../components/course-format/course-format.ts | 20 ++++++++++--- .../course/pages/contents/contents.html | 3 +- .../course/pages/contents/contents.ts | 2 ++ src/core/features/course/pages/index/index.ts | 5 ++-- .../courses/services/handlers/course-link.ts | 8 ++++- .../global-search-result.html | 2 ++ .../global-search-result.scss | 12 +++++--- .../features/search/services/global-search.ts | 29 ++++++++++++++----- .../stories/components/components.module.ts | 4 +++ .../global-search-results-page.ts | 24 +++++++++++++++ .../features/sitehome/pages/index/index.ts | 27 +++++++++++++++-- .../sitehome/services/handlers/index-link.ts | 11 ++++++- .../features/sitehome/tests/links.test.ts | 1 + 15 files changed, 153 insertions(+), 25 deletions(-) diff --git a/src/core/features/block/components/pre-rendered-block/pre-rendered-block.ts b/src/core/features/block/components/pre-rendered-block/pre-rendered-block.ts index ad4889a38..fda6ef0a7 100644 --- a/src/core/features/block/components/pre-rendered-block/pre-rendered-block.ts +++ b/src/core/features/block/components/pre-rendered-block/pre-rendered-block.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { OnInit, Component } from '@angular/core'; +import { OnInit, Component, HostBinding } from '@angular/core'; import { CoreBlockBaseComponent } from '../../classes/base-block-component'; /** @@ -26,6 +26,8 @@ export class CoreBlockPreRenderedComponent extends CoreBlockBaseComponent implem courseId?: number; + @HostBinding('attr.id') id?: string; + constructor() { super('CoreBlockPreRenderedComponent'); } @@ -39,6 +41,8 @@ export class CoreBlockPreRenderedComponent extends CoreBlockBaseComponent implem this.courseId = this.contextLevel == 'course' ? this.instanceId : undefined; this.fetchContentDefaultError = 'Error getting ' + this.block.contents?.title + ' data.'; + + this.id = `block-${this.block.instanceid}`; } } diff --git a/src/core/features/block/components/side-blocks/side-blocks.ts b/src/core/features/block/components/side-blocks/side-blocks.ts index 736ad9422..e3c75876c 100644 --- a/src/core/features/block/components/side-blocks/side-blocks.ts +++ b/src/core/features/block/components/side-blocks/side-blocks.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChildren, Input, OnInit, QueryList } from '@angular/core'; +import { Component, ViewChildren, Input, OnInit, QueryList, ElementRef } from '@angular/core'; import { ModalController } from '@singletons'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreCourse, CoreCourseBlock } from '@features/course/services/course'; @@ -22,6 +22,7 @@ import { CoreUtils } from '@services/utils/utils'; import { IonRefresher } from '@ionic/angular'; import { CoreCoursesDashboard } from '@features/courses/services/dashboard'; import { CoreTextUtils } from '@services/utils/text'; +import { CoreDom } from '@singletons/dom'; /** * Component that displays the list of side blocks. @@ -35,6 +36,7 @@ export class CoreBlockSideBlocksComponent implements OnInit { @Input() contextLevel!: string; @Input() instanceId!: number; + @Input() initialBlockInstanceId?: number; @Input() myDashboardPage?: string; @ViewChildren(CoreBlockComponent) blocksComponents?: QueryList; @@ -42,12 +44,16 @@ export class CoreBlockSideBlocksComponent implements OnInit { loaded = false; blocks: CoreCourseBlock[] = []; + constructor(protected elementRef: ElementRef) {} + /** * @inheritdoc */ async ngOnInit(): Promise { this.loadContent().finally(() => { this.loaded = true; + + this.focusInitialBlock(); }); } @@ -119,4 +125,20 @@ export class CoreBlockSideBlocksComponent implements OnInit { ModalController.dismiss(); } + /** + * Focus the initial block, if any. + */ + private async focusInitialBlock(): Promise { + if (!this.initialBlockInstanceId) { + return; + } + + const selector = '#block-' + this.initialBlockInstanceId; + + await CoreUtils.waitFor(() => !!this.elementRef.nativeElement.querySelector(selector)); + await CoreUtils.wait(200); + + CoreDom.scrollToElement(this.elementRef.nativeElement, selector, { addYAxis: -10 }); + } + } diff --git a/src/core/features/course/components/course-format/course-format.ts b/src/core/features/course/components/course-format/course-format.ts index 5e71561cf..d9e925eb9 100644 --- a/src/core/features/course/components/course-format/course-format.ts +++ b/src/core/features/course/components/course-format/course-format.ts @@ -52,6 +52,7 @@ import { CoreDom } from '@singletons/dom'; import { CoreUserTourDirectiveOptions } from '@directives/user-tour'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CorePlatform } from '@services/platform'; +import { CoreBlockSideBlocksComponent } from '@features/block/components/side-blocks/side-blocks'; /** * Component to display course contents using a certain format. If the format isn't found, use default one. @@ -76,6 +77,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { @Input() sections: CoreCourseSectionToDisplay[] = []; // List of course sections. @Input() initialSectionId?: number; // The section to load first (by ID). @Input() initialSectionNumber?: number; // The section to load first (by number). + @Input() initialBlockInstanceId?: number; // The instance to focus. @Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section. @Input() isGuest?: boolean; // If user is accessing using an ACCESS_GUEST enrolment method. @@ -298,16 +300,26 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { // Always load "All sections" to display the section title. If it isn't there just load the section. this.loaded = true; this.sectionChanged(sections[0]); - } else if (this.initialSectionId || this.initialSectionNumber) { + } else if (this.initialSectionId || this.initialSectionNumber !== undefined) { // We have an input indicating the section ID to load. Search the section. const section = sections.find((section) => - section.id == this.initialSectionId || (section.section && section.section == this.initialSectionNumber)); + section.id == this.initialSectionId || + (section.section !== undefined && section.section == this.initialSectionNumber)); // Don't load the section if it cannot be viewed by the user. if (section && this.canViewSection(section)) { this.loaded = true; this.sectionChanged(section); } + } else if (this.initialBlockInstanceId && this.displayBlocks && this.hasBlocks) { + CoreDomUtils.openSideModal({ + component: CoreBlockSideBlocksComponent, + componentProps: { + contextLevel: 'course', + instanceId: this.course.id, + initialBlockInstanceId: this.initialBlockInstanceId, + }, + }); } if (!this.loaded) { @@ -666,8 +678,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { CoreCourse.logView(this.course.id, sectionNumber), ); - let extraParams = sectionNumber ? `§ion=${sectionNumber}` : ''; - if (firstLoad && sectionNumber) { + let extraParams = sectionNumber !== undefined ? `§ion=${sectionNumber}` : ''; + if (firstLoad && sectionNumber !== undefined) { // If course is configured to show all sections in one page, don't include section in URL in first load. const courseDisplay = 'courseformatoptions' in this.course && this.course.courseformatoptions?.find(option => option.name === 'coursedisplay'); diff --git a/src/core/features/course/pages/contents/contents.html b/src/core/features/course/pages/contents/contents.html index 056ab073f..30246dd99 100644 --- a/src/core/features/course/pages/contents/contents.html +++ b/src/core/features/course/pages/contents/contents.html @@ -5,7 +5,8 @@ + [initialBlockInstanceId]="blockInstanceId" [moduleId]="moduleId" class="core-course-format-{{course.format}}" + *ngIf="dataLoaded && sections" [isGuest]="isGuest"> diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts index dac4a6212..82afe676d 100644 --- a/src/core/features/course/pages/contents/contents.ts +++ b/src/core/features/course/pages/contents/contents.ts @@ -58,6 +58,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon sections?: CoreCourseSection[]; sectionId?: number; sectionNumber?: number; + blockInstanceId?: number; dataLoaded = false; updatingData = false; downloadCourseEnabled = false; @@ -92,6 +93,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon this.sectionId = CoreNavigator.getRouteNumberParam('sectionId'); this.sectionNumber = CoreNavigator.getRouteNumberParam('sectionNumber'); + this.blockInstanceId = CoreNavigator.getRouteNumberParam('blockInstanceId'); this.moduleId = CoreNavigator.getRouteNumberParam('moduleId'); this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest'); diff --git a/src/core/features/course/pages/index/index.ts b/src/core/features/course/pages/index/index.ts index b472cdaff..4176cee9d 100644 --- a/src/core/features/course/pages/index/index.ts +++ b/src/core/features/course/pages/index/index.ts @@ -74,7 +74,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { if (data.sectionId) { this.contentsTab.pageParams.sectionId = data.sectionId; } - if (data.sectionNumber) { + if (data.sectionNumber !== undefined) { this.contentsTab.pageParams.sectionNumber = data.sectionNumber; } @@ -162,12 +162,13 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { course: this.course, sectionId: CoreNavigator.getRouteNumberParam('sectionId'), sectionNumber: CoreNavigator.getRouteNumberParam('sectionNumber'), + blockInstanceId: CoreNavigator.getRouteNumberParam('blockInstanceId'), isGuest: this.isGuest, }; if (this.module) { this.contentsTab.pageParams.moduleId = this.module.id; - if (!this.contentsTab.pageParams.sectionId && !this.contentsTab.pageParams.sectionNumber) { + if (!this.contentsTab.pageParams.sectionId && this.contentsTab.pageParams.sectionNumber === undefined) { // No section specified, use module section. this.contentsTab.pageParams.sectionId = this.module.section; } diff --git a/src/core/features/courses/services/handlers/course-link.ts b/src/core/features/courses/services/handlers/course-link.ts index d29db57e6..f1e2dad2a 100644 --- a/src/core/features/courses/services/handlers/course-link.ts +++ b/src/core/features/courses/services/handlers/course-link.ts @@ -69,6 +69,12 @@ export class CoreCoursesCourseLinkHandlerService extends CoreContentLinksHandler if (!isNaN(sectionNumber)) { pageParams.sectionNumber = sectionNumber; + } else { + const matches = url.match(/#inst(\d+)/); + + if (matches && matches[1]) { + pageParams.blockInstanceId = parseInt(matches[1], 10); + } } return [{ @@ -136,7 +142,7 @@ export class CoreCoursesCourseLinkHandlerService extends CoreContentLinksHandler // Direct access. const course = await CoreUtils.ignoreErrors(CoreCourses.getUserCourse(courseId), { id: courseId }); - CoreCourseHelper.openCourse(course, pageParams); + CoreCourseHelper.openCourse(course, { params: pageParams }); } else { this.navigateCourseSummary(courseId, pageParams); 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 b5a1b7d4d..2e6304296 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 @@ -6,6 +6,8 @@ + diff --git a/src/core/features/search/components/global-search-result/global-search-result.scss b/src/core/features/search/components/global-search-result/global-search-result.scss index d508e5985..7be59d0c0 100644 --- a/src/core/features/search/components/global-search-result/global-search-result.scss +++ b/src/core/features/search/components/global-search-result/global-search-result.scss @@ -3,6 +3,7 @@ --core-global-search-result-title-color: var(--text); --core-global-search-result-content-color: var(--gray-700); --core-global-search-result-context-color: var(--gray-600); + --core-global-search-result-icon-size: 16px; --mod-icon-filter: brightness(0); h3 { @@ -12,7 +13,7 @@ color: var(--core-global-search-result-title-color); core-mod-icon { - --size: 16px; + --size: var(--core-global-search-result-icon-size); --filter: var(--mod-icon-filter); margin-inline-end: var(--spacing-2); @@ -22,9 +23,9 @@ background: transparent; } - ion-icon { - width: 16px; - height: 16px; + ion-icon, .result-icon { + width: var(--core-global-search-result-icon-size); + height: var(--core-global-search-result-icon-size); margin-inline-end: var(--spacing-2); } @@ -61,6 +62,9 @@ } .result-context { + display: flex; + justify-items: center; + align-items: center; color: var(--core-global-search-result-context-color); margin-top: var(--spacing-2); font-size: 12px; diff --git a/src/core/features/search/services/global-search.ts b/src/core/features/search/services/global-search.ts index f423e8626..c4cbbe0e9 100644 --- a/src/core/features/search/services/global-search.ts +++ b/src/core/features/search/services/global-search.ts @@ -44,6 +44,7 @@ export type CoreSearchGlobalSearchResult = { content?: string; context?: CoreSearchGlobalSearchResultContext; module?: CoreSearchGlobalSearchResultModule; + component?: CoreSearchGlobalSearchResultComponent; course?: CoreCourseListItem; user?: CoreUserWithAvatar; }; @@ -59,6 +60,11 @@ export type CoreSearchGlobalSearchResultModule = { area: string; }; +export type CoreSearchGlobalSearchResultComponent = { + name: string; + iconurl: string; +}; + export type CoreSearchGlobalSearchSearchAreaCategory = { id: string; name: string; @@ -225,8 +231,8 @@ export class CoreSearchGlobalSearchService { const user = await CoreUser.getProfile(wsResult.itemid); result.user = user; - } else if (wsResult.componentname === 'core_course') { - const course = await CoreCourses.getCourse(wsResult.itemid); + } else if (wsResult.componentname === 'core_course' && wsResult.areaname === 'course') { + const course = await CoreCourses.getCourseByField('id', wsResult.itemid); result.course = course; } else { @@ -237,12 +243,19 @@ export class CoreSearchGlobalSearchService { }; } - if (wsResult.iconurl && wsResult.componentname.startsWith('mod_')) { - result.module = { - name: wsResult.componentname.substring(4), - iconurl: wsResult.iconurl, - area: wsResult.areaname, - }; + 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, + }; + } } } diff --git a/src/core/features/search/stories/components/components.module.ts b/src/core/features/search/stories/components/components.module.ts index 5a2a88bac..bfece68ca 100644 --- a/src/core/features/search/stories/components/components.module.ts +++ b/src/core/features/search/stories/components/components.module.ts @@ -20,12 +20,16 @@ import { CommonModule } from '@angular/common'; import { CoreSearchGlobalSearchResultsPageComponent, } from '@features/search/stories/components/global-search-results-page/global-search-results-page'; +import { CoreSearchGlobalSearchResultComponent } from '@features/search/components/global-search-result/global-search-result'; +import { CoreSharedModule } from '@/core/shared.module'; @NgModule({ declarations: [ CoreSearchGlobalSearchResultsPageComponent, + CoreSearchGlobalSearchResultComponent, ], imports: [ + CoreSharedModule, CommonModule, StorybookModule, CoreComponentsModule, diff --git a/src/core/features/search/stories/components/global-search-results-page/global-search-results-page.ts b/src/core/features/search/stories/components/global-search-results-page/global-search-results-page.ts index 500de398d..8e4dd3dd3 100644 --- a/src/core/features/search/stories/components/global-search-results-page/global-search-results-page.ts +++ b/src/core/features/search/stories/components/global-search-results-page/global-search-results-page.ts @@ -83,6 +83,30 @@ export class CoreSearchGlobalSearchResultsPageComponent { area: 'post', }, }, + { + id: 6, + url: '', + title: 'Side block', + context: { + courseName: 'Moodle Site', + }, + component: { + name: 'block_html', + iconurl: 'https://master.mm.moodledemo.net/theme/image.php?theme=boost&component=core&image=e%2Fanchor', + }, + }, + { + id: 7, + url: '', + title: 'Course section', + context: { + courseName: 'Course 101', + }, + component: { + name: 'core_course', + iconurl: 'https://master.mm.moodledemo.net/theme/image.php?theme=boost&component=core&image=i%2Fsection', + }, + }, ]; /** diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts index a7f41db8a..dd12102d4 100644 --- a/src/core/features/sitehome/pages/index/index.ts +++ b/src/core/features/sitehome/pages/index/index.ts @@ -14,7 +14,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { IonRefresher } from '@ionic/angular'; -import { Params } from '@angular/router'; +import { ActivatedRoute, Params } from '@angular/router'; import { CoreSite, CoreSiteConfig } from '@classes/site'; import { CoreCourse, CoreCourseWSSection } from '@features/course/services/course'; @@ -31,6 +31,7 @@ import { CoreBlockHelper } from '@features/block/services/block-helper'; import { CoreUtils } from '@services/utils/utils'; import { CoreTime } from '@singletons/time'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; +import { CoreBlockSideBlocksComponent } from '@features/block/components/side-blocks/side-blocks'; /** * Page that displays site home index. @@ -58,7 +59,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { protected updateSiteObserver: CoreEventObserver; protected logView: () => void; - constructor() { + constructor(protected route: ActivatedRoute) { // Refresh the enabled flags if site is updated. this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite(); @@ -102,6 +103,10 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { this.loadContent().finally(() => { this.dataLoaded = true; }); + + this.openFocusedInstance(); + + this.route.queryParams.subscribe(() => this.openFocusedInstance()); } /** @@ -226,4 +231,22 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { this.updateSiteObserver.off(); } + /** + * Check whether there is a focused instance in the page parameters and open it. + */ + private openFocusedInstance() { + const blockInstanceId = CoreNavigator.getRouteNumberParam('blockInstanceId'); + + if (blockInstanceId) { + CoreDomUtils.openSideModal({ + component: CoreBlockSideBlocksComponent, + componentProps: { + contextLevel: 'course', + instanceId: this.siteHomeId, + initialBlockInstanceId: blockInstanceId, + }, + }); + } + } + } diff --git a/src/core/features/sitehome/services/handlers/index-link.ts b/src/core/features/sitehome/services/handlers/index-link.ts index ac0770424..755e14a53 100644 --- a/src/core/features/sitehome/services/handlers/index-link.ts +++ b/src/core/features/sitehome/services/handlers/index-link.ts @@ -22,6 +22,7 @@ import { makeSingleton } from '@singletons'; import { CoreNavigator } from '@services/navigator'; import { CoreSiteHomeHomeHandlerService } from './sitehome-home'; import { CoreMainMenuHomeHandlerService } from '@features/mainmenu/services/handlers/mainmenu'; +import { Params } from '@angular/router'; /** * Handler to treat links to site home index. @@ -36,7 +37,14 @@ export class CoreSiteHomeIndexLinkHandlerService extends CoreContentLinksHandler /** * @inheritdoc */ - getActions(): CoreContentLinksAction[] | Promise { + getActions(siteIds: string[], url: string): CoreContentLinksAction[] | Promise { + const pageParams: Params = {}; + const matches = url.match(/#inst(\d+)/); + + if (matches && matches[1]) { + pageParams.blockInstanceId = parseInt(matches[1], 10); + } + return [{ action: (siteId: string): void => { CoreNavigator.navigateToSitePath( @@ -44,6 +52,7 @@ export class CoreSiteHomeIndexLinkHandlerService extends CoreContentLinksHandler { preferCurrentTab: false, siteId, + params: pageParams, }, ); }, diff --git a/src/core/features/sitehome/tests/links.test.ts b/src/core/features/sitehome/tests/links.test.ts index 528e11b6e..fd25d399a 100644 --- a/src/core/features/sitehome/tests/links.test.ts +++ b/src/core/features/sitehome/tests/links.test.ts @@ -45,6 +45,7 @@ describe('Site Home link handlers', () => { expect(CoreNavigator.navigateToSitePath).toHaveBeenCalledWith('/home/site', { siteId, preferCurrentTab: false, + params: {}, }); });