MOBILE-3371 search: Support blocks and sections
parent
e4755af9c6
commit
254dcb3daa
|
@ -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}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
@ -45,6 +45,7 @@ describe('Site Home link handlers', () => {
|
|||
expect(CoreNavigator.navigateToSitePath).toHaveBeenCalledWith('/home/site', {
|
||||
siteId,
|
||||
preferCurrentTab: false,
|
||||
params: {},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue