MOBILE-3371 search: Support blocks and sections

main
Noel De Martin 2023-09-14 13:12:42 +02:00
parent e4755af9c6
commit 254dcb3daa
15 changed files with 153 additions and 25 deletions

View File

@ -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}`;
}
}

View File

@ -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<CoreBlockComponent>;
@ -42,12 +44,16 @@ export class CoreBlockSideBlocksComponent implements OnInit {
loaded = false;
blocks: CoreCourseBlock[] = [];
constructor(protected elementRef: ElementRef<HTMLElement>) {}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
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<void> {
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 });
}
}

View File

@ -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 ? `&section=${sectionNumber}` : '';
if (firstLoad && sectionNumber) {
let extraParams = sectionNumber !== undefined ? `&section=${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');

View File

@ -5,7 +5,8 @@
<core-loading [hideUntil]="dataLoaded && !updatingData">
<core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId" [initialSectionNumber]="sectionNumber"
[moduleId]="moduleId" class="core-course-format-{{course.format}}" *ngIf="dataLoaded && sections" [isGuest]="isGuest">
[initialBlockInstanceId]="blockInstanceId" [moduleId]="moduleId" class="core-course-format-{{course.format}}"
*ngIf="dataLoaded && sections" [isGuest]="isGuest">
</core-course-format>
</core-loading>
</ion-content>

View File

@ -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');

View File

@ -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;
}

View File

@ -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);

View File

@ -6,6 +6,8 @@
<ion-icon *ngIf="renderedIcon" [name]="renderedIcon" aria-hidden="true"></ion-icon>
<core-mod-icon *ngIf="!renderedIcon && result.module" [modicon]="result.module.iconurl"
[modname]="result.module.name"></core-mod-icon>
<img *ngIf="!renderedIcon && !result.module && result.component" [src]="result.component.iconurl" alt="" class="result-icon"
core-external-content [component]="result.component.name">
<core-format-text [text]="result.title"></core-format-text>
</h3>
<core-format-text *ngIf="result.content && !result.course && !result.user" [text]="result.content"></core-format-text>

View File

@ -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;

View File

@ -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,
};
}
}
}

View File

@ -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,

View File

@ -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',
},
},
];
/**

View File

@ -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,
},
});
}
}
}

View File

@ -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<CoreContentLinksAction[]> {
getActions(siteIds: string[], url: string): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
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,
},
);
},

View File

@ -45,6 +45,7 @@ describe('Site Home link handlers', () => {
expect(CoreNavigator.navigateToSitePath).toHaveBeenCalledWith('/home/site', {
siteId,
preferCurrentTab: false,
params: {},
});
});