diff --git a/src/core/components/tabs/tabs.ts b/src/core/components/tabs/tabs.ts index 760e93b54..e05fc8939 100644 --- a/src/core/components/tabs/tabs.ts +++ b/src/core/components/tabs/tabs.ts @@ -106,7 +106,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe protected unregisterBackButtonAction: any; protected languageChangedSubscription: Subscription; protected isInTransition = false; // Weather Slides is in transition. - protected slidesSwiper: any; + protected slidesSwiper: any; // eslint-disable-line @typescript-eslint/no-explicit-any protected slidesSwiperLoaded = false; protected stackEventsSubscription?: Subscription; @@ -338,7 +338,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe return; } - this.firstSelectedTab = selectedTab.id; + this.firstSelectedTab = selectedTab.id!; this.selectTab(this.firstSelectedTab); // Setup tab scrolling. @@ -548,18 +548,31 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe } /** - * Tab selected. + * Select a tab by ID. * - * @param tabId Selected tab index. + * @param tabId Tab ID. * @param e Event. + * @return Promise resolved when done. */ async selectTab(tabId: string, e?: Event): Promise { - let index = this.tabs.findIndex((tab) => tabId == tab.id); + const index = this.tabs.findIndex((tab) => tabId == tab.id); + + return this.selectByIndex(index, e); + } + + /** + * Select a tab by index. + * + * @param index Index to select. + * @param e Event. + * @return Promise resolved when done. + */ + async selectByIndex(index: number, e?: Event): Promise { if (index < 0 || index >= this.tabs.length) { if (this.selected) { // Invalid index do not change tab. - e && e.preventDefault(); - e && e.stopPropagation(); + e?.preventDefault(); + e?.stopPropagation(); return; } @@ -568,12 +581,11 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe index = 0; } - const selectedTab = this.tabs[index]; - if (tabId == this.selected || !selectedTab || !selectedTab.enabled) { + const tabToSelect = this.tabs[index]; + if (!tabToSelect || !tabToSelect.enabled || tabToSelect.id == this.selected) { // Already selected or not enabled. - - e && e.preventDefault(); - e && e.stopPropagation(); + e?.preventDefault(); + e?.stopPropagation(); return; } @@ -583,17 +595,17 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe } const pageParams: NavigationOptions = {}; - if (selectedTab.pageParams) { - pageParams.queryParams = selectedTab.pageParams; + if (tabToSelect.pageParams) { + pageParams.queryParams = tabToSelect.pageParams; } - const ok = await this.navCtrl.navigateForward(selectedTab.page, pageParams); + const ok = await this.navCtrl.navigateForward(tabToSelect.page, pageParams); if (ok !== false) { - this.selectHistory.push(tabId); - this.selected = tabId; + this.selectHistory.push(tabToSelect.id!); + this.selected = tabToSelect.id; this.selectedIndex = index; - this.ionChange.emit(selectedTab); + this.ionChange.emit(tabToSelect); } } @@ -644,16 +656,14 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe /** * Core Tab class. */ -class CoreTab { - - id = ''; // Unique tab id. - class = ''; // Class, if needed. - title = ''; // The translatable tab title. +export type CoreTab = { + page: string; // Page to navigate to. + title: string; // The translatable tab title. + id?: string; // Unique tab id. + class?: string; // Class, if needed. icon?: string; // The tab icon. badge?: string; // A badge to add in the tab. badgeStyle?: string; // The badge color. - enabled = true; // Whether the tab is enabled. - page = ''; // Page to navigate to. + enabled?: boolean; // Whether the tab is enabled. pageParams?: Params; // Page params. - -} +}; diff --git a/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html b/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html index c871d803a..4afcc17da 100644 --- a/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html +++ b/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html @@ -1,6 +1,6 @@
- +

{{ 'core.whoops' | translate }}

diff --git a/src/core/features/course/course-lazy.module.ts b/src/core/features/course/course-lazy.module.ts new file mode 100644 index 000000000..6431088a2 --- /dev/null +++ b/src/core/features/course/course-lazy.module.ts @@ -0,0 +1,33 @@ +// (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 { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: '', + redirectTo: 'index', + pathMatch: 'full', + }, + { + path: 'index', + loadChildren: () => import('./pages/index/index.module').then( m => m.CoreCourseIndexPageModule), + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], +}) +export class CoreCourseLazyModule {} diff --git a/src/core/features/course/course.module.ts b/src/core/features/course/course.module.ts index 6960585bc..09a5ede90 100644 --- a/src/core/features/course/course.module.ts +++ b/src/core/features/course/course.module.ts @@ -13,18 +13,38 @@ // limitations under the License. import { NgModule } from '@angular/core'; +import { Routes } from '@angular/router'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CORE_SITE_SCHEMAS } from '@services/sites'; import { CoreCourseComponentsModule } from './components/components.module'; import { CoreCourseFormatModule } from './format/formats.module'; import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/course'; import { SITE_SCHEMA as LOG_SITE_SCHEMA } from './services/database/log'; +import { CoreCourseIndexRoutingModule } from './pages/index/index-routing.module'; + +const routes: Routes = [ + { + path: 'course', + loadChildren: () => import('@features/course/course-lazy.module').then(m => m.CoreCourseLazyModule), + }, +]; + +const courseIndexRoutes: Routes = [ + { + path: 'contents', + loadChildren: () => import('./pages/contents/contents.module').then(m => m.CoreCourseContentsPageModule), + }, +]; @NgModule({ imports: [ + CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }), + CoreMainMenuTabRoutingModule.forChild(routes), CoreCourseFormatModule, CoreCourseComponentsModule, ], + exports: [CoreCourseIndexRoutingModule], providers: [ { provide: CORE_SITE_SCHEMAS, diff --git a/src/core/features/course/pages/contents/contents.html b/src/core/features/course/pages/contents/contents.html new file mode 100644 index 000000000..fb0570236 --- /dev/null +++ b/src/core/features/course/pages/contents/contents.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/core/features/course/pages/contents/contents.module.ts b/src/core/features/course/pages/contents/contents.module.ts new file mode 100644 index 000000000..0c386db0e --- /dev/null +++ b/src/core/features/course/pages/contents/contents.module.ts @@ -0,0 +1,46 @@ +// (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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreCourseContentsPage } from './contents'; +import { CoreCourseComponentsModule } from '@features/course/components/components.module'; + +const routes: Routes = [ + { + path: '', + component: CoreCourseContentsPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreSharedModule, + CoreCourseComponentsModule, + ], + declarations: [ + CoreCourseContentsPage, + ], + exports: [RouterModule], +}) +export class CoreCourseContentsPageModule {} diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts new file mode 100644 index 000000000..32a7e6149 --- /dev/null +++ b/src/core/features/course/pages/contents/contents.ts @@ -0,0 +1,533 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { IonContent, IonRefresher, NavController } from '@ionic/angular'; + +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreCourses, CoreCourseAnyCourseData } from '@features/courses/services/courses'; +import { + CoreCourse, + CoreCourseCompletionActivityStatus, + CoreCourseModuleCompletionData, + CoreCourseProvider, +} from '@features/course/services/course'; +import { CoreCourseHelper, CoreCourseSectionFormatted, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; +import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; +// import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { + CoreCourseOptionsDelegate, + CoreCourseOptionsMenuHandlerToDisplay, +} from '@features/course/services/course-options-delegate'; +// import { CoreCourseSyncProvider } from '../../providers/sync'; +// import { CoreCourseFormatComponent } from '../../components/format/format'; +import { CoreFilterHelper } from '@features/filter/services/filter-helper'; +import { + CoreEvents, + CoreEventObserver, + CoreEventCourseStatusChanged, + CoreEventCompletionModuleViewedData, +} from '@singletons/events'; +import { Translate } from '@singletons'; +import { CoreNavHelper } from '@services/nav-helper'; + +/** + * Page that displays the contents of a course. + */ +@Component({ + selector: 'page-core-course-contents', + templateUrl: 'contents.html', +}) +export class CoreCourseContentsPage implements OnInit, OnDestroy { + + @ViewChild(IonContent) content?: IonContent; + // @ViewChild(CoreCourseFormatComponent) formatComponent: CoreCourseFormatComponent; + + course!: CoreCourseAnyCourseData; + sections?: Section[]; + sectionId?: number; + sectionNumber?: number; + courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = []; + dataLoaded = false; + downloadEnabled = false; + downloadEnabledIcon = 'far-square'; // Disabled by default. + downloadCourseEnabled = false; + moduleId?: number; + displayEnableDownload = false; + displayRefresher = false; + prefetchCourseData: CorePrefetchStatusInfo = { + icon: 'spinner', + statusTranslatable: 'core.course.downloadcourse', + status: '', + loading: true, + }; + + protected formatOptions?: Record; + protected completionObserver?: CoreEventObserver; + protected courseStatusObserver?: CoreEventObserver; + protected syncObserver?: CoreEventObserver; + protected isDestroyed = false; + + constructor( + protected route: ActivatedRoute, + protected navCtrl: NavController, + ) { } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + // Get params. + this.course = this.route.snapshot.queryParams['course']; + this.sectionId = this.route.snapshot.queryParams['sectionId']; + this.sectionNumber = this.route.snapshot.queryParams['sectionNumber']; + this.moduleId = this.route.snapshot.queryParams['moduleId']; + + if (!this.course) { + CoreDomUtils.instance.showErrorModal('Missing required course parameter.'); + this.navCtrl.pop(); + + return; + } + + this.displayEnableDownload = !CoreSites.instance.getCurrentSite()?.isOfflineDisabled() && + CoreCourseFormatDelegate.instance.displayEnableDownload(this.course); + this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); + + this.initListeners(); + + await this.loadData(false, true); + + this.dataLoaded = true; + + this.initPrefetch(); + } + + /** + * Init listeners. + * + * @return Promise resolved when done. + */ + protected async initListeners(): Promise { + if (this.downloadCourseEnabled) { + // Listen for changes in course status. + this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => { + if (data.courseId == this.course.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) { + this.updateCourseStatus(data.status); + } + }, CoreSites.instance.getCurrentSiteId()); + } + + // Check if the course format requires the view to be refreshed when completion changes. + const shouldRefresh = await CoreCourseFormatDelegate.instance.shouldRefreshWhenCompletionChanges(this.course); + if (!shouldRefresh) { + return; + } + + this.completionObserver = CoreEvents.on( + CoreEvents.COMPLETION_MODULE_VIEWED, + (data) => { + if (data && data.courseId == this.course.id) { + this.refreshAfterCompletionChange(true); + } + }, + ); + + // @todo this.syncObserver = CoreEvents.on(CoreCourseSyncProvider.AUTO_SYNCED, (data) => { + // if (data && data.courseId == this.course.id) { + // this.refreshAfterCompletionChange(false); + + // if (data.warnings && data.warnings[0]) { + // CoreDomUtils.instance.showErrorModal(data.warnings[0]); + // } + // } + // }); + } + + /** + * Init prefetch data if needed. + * + * @return Promise resolved when done. + */ + protected async initPrefetch(): Promise { + if (!this.downloadCourseEnabled) { + // Cannot download the whole course, stop. + return; + } + + // Determine the course prefetch status. + await this.determineCoursePrefetchIcon(); + + if (this.prefetchCourseData.icon != 'spinner') { + return; + } + + // Course is being downloaded. Get the download promise. + const promise = CoreCourseHelper.instance.getCourseDownloadPromise(this.course.id); + if (promise) { + // There is a download promise. Show an error if it fails. + promise.catch((error) => { + if (!this.isDestroyed) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); + } + }); + } else { + // No download, this probably means that the app was closed while downloading. Set previous status. + const status = await CoreCourse.instance.setCoursePreviousStatus(this.course.id); + + this.updateCourseStatus(status); + } + } + + /** + * Fetch and load all the data required for the view. + * + * @param refresh If it's refreshing content. + * @param sync If it should try to sync. + * @return Promise resolved when done. + */ + protected async loadData(refresh?: boolean, sync?: boolean): Promise { + // First of all, get the course because the data might have changed. + const result = await CoreUtils.instance.ignoreErrors(CoreCourseHelper.instance.getCourse(this.course.id)); + + if (result) { + if (this.course.id === result.course.id && 'displayname' in this.course && !('displayname' in result.course)) { + result.course.displayname = this.course.displayname; + } + this.course = result.course; + } + + // @todo: Get the overview files. Maybe move it to format component? + // if ('overviewfiles' in this.course && this.course.overviewfiles) { + // this.course.imageThumb = this.course.overviewfiles[0] && this.course.overviewfiles[0].fileurl; + // } + + if (sync) { + // Try to synchronize the course data. + // @todo return this.syncProvider.syncCourse(this.course.id).then((result) => { + // if (result.warnings && result.warnings.length) { + // CoreDomUtils.instance.showErrorModal(result.warnings[0]); + // } + // }).catch(() => { + // // For now we don't allow manual syncing, so ignore errors. + // }); + } + + try { + await Promise.all([ + this.loadSections(refresh), + this.loadMenuHandlers(refresh), + this.loadCourseFormatOptions(), + ]); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true); + } + } + + /** + * Load course sections. + * + * @param refresh If it's refreshing content. + * @return Promise resolved when done. + */ + protected async loadSections(refresh?: boolean): Promise { + // Get all the sections. + const sections = await CoreCourse.instance.getSections(this.course.id, false, true); + + if (refresh) { + // Invalidate the recently downloaded module list. To ensure info can be prefetched. + // const modules = CoreCourse.instance.getSectionsModules(sections); + + // @todo await this.prefetchDelegate.invalidateModules(modules, this.course.id); + } + + let completionStatus: Record = {}; + + // Get the completion status. + if (this.course.enablecompletion !== false) { + const sectionWithModules = sections.find((section) => section.modules.length > 0); + + if (sectionWithModules && typeof sectionWithModules.modules[0].completion != 'undefined') { + // The module already has completion (3.6 onwards). Load the offline completion. + await CoreUtils.instance.ignoreErrors(CoreCourseHelper.instance.loadOfflineCompletion(this.course.id, sections)); + } else { + const fetchedData = await CoreUtils.instance.ignoreErrors( + CoreCourse.instance.getActivitiesCompletionStatus(this.course.id), + ); + + completionStatus = fetchedData || completionStatus; + } + } + + // Add handlers + const result = CoreCourseHelper.instance.addHandlerDataForModules( + sections, + this.course.id, + completionStatus, + this.course.fullname, + true, + ); + + // Format the name of each section. + result.sections.forEach(async (section: Section) => { + const result = await CoreFilterHelper.instance.getFiltersAndFormatText( + section.name.trim(), + 'course', + this.course.id, + { clean: true, singleLine: true }, + ); + + section.formattedName = result.text; + }); + + this.sections = result.sections; + + if (CoreCourseFormatDelegate.instance.canViewAllSections(this.course)) { + // Add a fake first section (all sections). + this.sections.unshift({ + id: CoreCourseProvider.ALL_SECTIONS_ID, + name: Translate.instance.instant('core.course.allsections'), + hasContent: true, + summary: '', + summaryformat: 1, + modules: [], + }); + } + + // Get whether to show the refresher now that we have sections. + this.displayRefresher = CoreCourseFormatDelegate.instance.displayRefresher(this.course, this.sections); + } + + /** + * Load the course menu handlers. + * + * @param refresh If it's refreshing content. + * @return Promise resolved when done. + */ + protected async loadMenuHandlers(refresh?: boolean): Promise { + this.courseMenuHandlers = await CoreCourseOptionsDelegate.instance.getMenuHandlersToDisplay(this.course, refresh); + } + + /** + * Load course format options if needed. + * + * @return Promise resolved when done. + */ + protected async loadCourseFormatOptions(): Promise { + + // Load the course format options when course completion is enabled to show completion progress on sections. + if (!this.course.enablecompletion || !CoreCourses.instance.isGetCoursesByFieldAvailable()) { + return; + } + + if ('courseformatoptions' in this.course && this.course.courseformatoptions) { + // Already loaded. + this.formatOptions = CoreUtils.instance.objectToKeyValueMap(this.course.courseformatoptions, 'name', 'value'); + + return; + } + + const course = await CoreUtils.instance.ignoreErrors(CoreCourses.instance.getCourseByField('id', this.course.id)); + + course && Object.assign(this.course, course); + + if (course?.courseformatoptions) { + this.formatOptions = CoreUtils.instance.objectToKeyValueMap(course.courseformatoptions, 'name', 'value'); + } + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + * @return Promise resolved when done. + */ + async doRefresh(refresher?: CustomEvent): Promise { + await CoreUtils.instance.ignoreErrors(this.invalidateData()); + + try { + await this.loadData(true, true); + } finally { + // Do not call doRefresh on the format component if the refresher is defined in the format component + // to prevent an inifinite loop. + if (this.displayRefresher) { + // @todo await CoreUtils.instance.ignoreErrors(this.formatComponent.doRefresh(refresher)); + } + + refresher?.detail.complete(); + } + } + + /** + * The completion of any of the modules has changed. + * + * @param completionData Completion data. + * @return Promise resolved when done. + */ + async onCompletionChange(completionData: CoreCourseModuleCompletionData): Promise { + const shouldReload = typeof completionData.valueused == 'undefined' || completionData.valueused; + + if (!shouldReload) { + return; + } + + await CoreUtils.instance.ignoreErrors(this.invalidateData()); + + await this.refreshAfterCompletionChange(true); + } + + /** + * Invalidate the data. + * + * @return Promise resolved when done. + */ + protected async invalidateData(): Promise { + const promises: Promise[] = []; + + promises.push(CoreCourse.instance.invalidateSections(this.course.id)); + promises.push(CoreCourses.instance.invalidateUserCourses()); + promises.push(CoreCourseFormatDelegate.instance.invalidateData(this.course, this.sections || [])); + + if (this.sections) { + // @todo promises.push(this.prefetchDelegate.invalidateCourseUpdates(this.course.id)); + } + + await Promise.all(promises); + } + + /** + * Refresh list after a completion change since there could be new activities. + * + * @param sync If it should try to sync. + * @return Promise resolved when done. + */ + protected async refreshAfterCompletionChange(sync?: boolean): Promise { + // Save scroll position to restore it once done. + const scrollElement = await this.content?.getScrollElement(); + const scrollTop = scrollElement?.scrollTop || 0; + const scrollLeft = scrollElement?.scrollLeft || 0; + + this.dataLoaded = false; + this.content?.scrollToTop(0); // Scroll top so the spinner is seen. + + try { + await this.loadData(true, sync); + + // @todo await this.formatComponent.doRefresh(undefined, undefined, true); + } finally { + this.dataLoaded = true; + + // Wait for new content height to be calculated and scroll without animation. + setTimeout(() => { + this.content?.scrollToPoint(scrollLeft, scrollTop, 0); + }); + } + } + + /** + * Determines the prefetch icon of the course. + * + * @return Promise resolved when done. + */ + protected async determineCoursePrefetchIcon(): Promise { + this.prefetchCourseData = await CoreCourseHelper.instance.getCourseStatusIconAndTitle(this.course.id); + } + + /** + * Prefetch the whole course. + */ + prefetchCourse(): void { + try { + // @todo await CoreCourseHelper.instance.confirmAndPrefetchCourse( + // this.prefetchCourseData, + // this.course, + // this.sections, + // this.courseHandlers, + // this.courseMenuHandlers, + // ); + } catch (error) { + if (this.isDestroyed) { + return; + } + + CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); + } + } + + /** + * Toggle download enabled. + */ + toggleDownload(): void { + this.downloadEnabled = !this.downloadEnabled; + this.downloadEnabledIcon = this.downloadEnabled ? 'far-check-square' : 'far-square'; + } + + /** + * Update the course status icon and title. + * + * @param status Status to show. + */ + protected updateCourseStatus(status: string): void { + this.prefetchCourseData = CoreCourseHelper.instance.getCourseStatusIconAndTitleFromStatus(status); + } + + /** + * Open the course summary + */ + openCourseSummary(): void { + CoreNavHelper.instance.goInCurrentMainMenuTab('/courses/preview', { course: this.course, avoidOpenCourse: true }); + } + + /** + * Opens a menu item registered to the delegate. + * + * @param item Item to open + */ + openMenuItem(item: CoreCourseOptionsMenuHandlerToDisplay): void { + const params = Object.assign({ course: this.course }, item.data.pageParams); + CoreNavHelper.instance.goInCurrentMainMenuTab(item.data.page, params); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.isDestroyed = true; + this.completionObserver?.off(); + this.courseStatusObserver?.off(); + this.syncObserver?.off(); + } + + /** + * User entered the page. + */ + ionViewDidEnter(): void { + // @todo this.formatComponent?.ionViewDidEnter(); + } + + /** + * User left the page. + */ + ionViewDidLeave(): void { + // @todo this.formatComponent?.ionViewDidLeave(); + } + +} + +type Section = CoreCourseSectionFormatted & { + formattedName?: string; +}; diff --git a/src/core/features/course/pages/index/index-routing.module.ts b/src/core/features/course/pages/index/index-routing.module.ts new file mode 100644 index 000000000..993f6f662 --- /dev/null +++ b/src/core/features/course/pages/index/index-routing.module.ts @@ -0,0 +1,33 @@ +// (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 { InjectionToken, ModuleWithProviders, NgModule } from '@angular/core'; + +import { ModuleRoutesConfig } from '@/app/app-routing.module'; + +export const COURSE_INDEX_ROUTES = new InjectionToken('COURSE_INDEX_ROUTES'); + +@NgModule() +export class CoreCourseIndexRoutingModule { + + static forChild(routes: ModuleRoutesConfig): ModuleWithProviders { + return { + ngModule: CoreCourseIndexRoutingModule, + providers: [ + { provide: COURSE_INDEX_ROUTES, multi: true, useValue: routes }, + ], + }; + } + +} diff --git a/src/core/features/course/pages/index/index.html b/src/core/features/course/pages/index/index.html new file mode 100644 index 000000000..221b8b63d --- /dev/null +++ b/src/core/features/course/pages/index/index.html @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/core/features/course/pages/index/index.module.ts b/src/core/features/course/pages/index/index.module.ts new file mode 100644 index 000000000..b21a450c2 --- /dev/null +++ b/src/core/features/course/pages/index/index.module.ts @@ -0,0 +1,54 @@ +// (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 { Injector, NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, ROUTES, Routes } from '@angular/router'; +import { IonicModule } from '@ionic/angular'; +import { TranslateModule } from '@ngx-translate/core'; + +import { resolveModuleRoutes } from '@/app/app-routing.module'; +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreCourseIndexPage } from './index'; +import { COURSE_INDEX_ROUTES } from './index-routing.module'; + +function buildRoutes(injector: Injector): Routes { + const routes = resolveModuleRoutes(injector, COURSE_INDEX_ROUTES); + + return [ + { + path: '', + component: CoreCourseIndexPage, + children: routes.children, + }, + ...routes.siblings, + ]; +} + +@NgModule({ + providers: [ + { provide: ROUTES, multi: true, useFactory: buildRoutes, deps: [Injector] }, + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreSharedModule, + ], + declarations: [ + CoreCourseIndexPage, + ], + exports: [RouterModule], +}) +export class CoreCourseIndexPageModule {} diff --git a/src/core/features/course/pages/index/index.ts b/src/core/features/course/pages/index/index.ts new file mode 100644 index 000000000..b74859f77 --- /dev/null +++ b/src/core/features/course/pages/index/index.ts @@ -0,0 +1,191 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, ViewChild, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; + +import { CoreTab, CoreTabsComponent } from '@components/tabs/tabs'; +import { CoreCourseFormatDelegate } from '../../services/format-delegate'; +import { CoreCourseOptionsDelegate } from '../../services/course-options-delegate'; +import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; +import { CoreEventObserver, CoreEvents, CoreEventSelectCourseTabData } from '@singletons/events'; +import { CoreCourse, CoreCourseModuleData } from '@features/course/services/course'; +import { CoreCourseHelper } from '@features/course/services/course-helper'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreNavHelper } from '@services/nav-helper'; +import { CoreObject } from '@singletons/object'; + +/** + * Page that displays the list of courses the user is enrolled in. + */ +@Component({ + selector: 'page-core-course-index', + templateUrl: 'index.html', +}) +export class CoreCourseIndexPage implements OnInit, OnDestroy { + + @ViewChild(CoreTabsComponent) tabsComponent?: CoreTabsComponent; + + title?: string; + course?: CoreCourseAnyCourseData; + tabs: CourseTab[] = []; + loaded = false; + + protected currentPagePath = ''; + protected selectTabObserver: CoreEventObserver; + protected firstTabName?: string; + protected contentsTab: CoreTab = { + page: 'contents', + title: 'core.course.contents', + pageParams: {}, + }; + + constructor( + protected route: ActivatedRoute, + ) { + this.selectTabObserver = CoreEvents.on(CoreEvents.SELECT_COURSE_TAB, (data) => { + if (!data.name) { + // If needed, set sectionId and sectionNumber. They'll only be used if the content tabs hasn't been loaded yet. + if (data.sectionId) { + this.contentsTab.pageParams!.sectionId = data.sectionId; + } + if (data.sectionNumber) { + this.contentsTab.pageParams!.sectionNumber = data.sectionNumber; + } + + // Select course contents. + this.tabsComponent?.selectByIndex(0); + } else if (this.tabs) { + const index = this.tabs.findIndex((tab) => tab.name == data.name); + + if (index >= 0) { + this.tabsComponent?.selectByIndex(index + 1); + } + } + }); + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + // Get params. + this.course = this.route.snapshot.queryParams['course']; + this.firstTabName = this.route.snapshot.queryParams['selectedTab']; + const module: CoreCourseModuleData | undefined = this.route.snapshot.queryParams['module']; + const modParams: Params | undefined = this.route.snapshot.queryParams['modParams']; + + this.currentPagePath = CoreNavHelper.instance.getCurrentPage(); + this.contentsTab.page = CoreTextUtils.instance.concatenatePaths(this.currentPagePath, this.contentsTab.page); + this.contentsTab.pageParams = CoreObject.removeUndefined({ + course: this.course, + sectionId: this.route.snapshot.queryParams['sectionId'], + sectionNumber: this.route.snapshot.queryParams['sectionNumber'], + }); + + if (module) { + this.contentsTab.pageParams!.moduleId = module.id; + CoreCourseHelper.instance.openModule(module, this.course!.id, this.contentsTab.pageParams!.sectionId, modParams); + } + + this.tabs.push(this.contentsTab); + this.loaded = true; + + await Promise.all([ + this.loadCourseHandlers(), + this.loadTitle(), + ]); + } + + /** + * Load course option handlers. + * + * @return Promise resolved when done. + */ + protected async loadCourseHandlers(): Promise { + // Load the course handlers. + const handlers = await CoreCourseOptionsDelegate.instance.getHandlersToDisplay(this.course!, false, false); + + this.tabs.concat(handlers.map(handler => handler.data)); + + let tabToLoad: number | undefined; + + // Add the courseId to the handler component data. + handlers.forEach((handler, index) => { + handler.data.page = CoreTextUtils.instance.concatenatePaths(this.currentPagePath, handler.data.page); + handler.data.pageParams = handler.data.pageParams || {}; + handler.data.pageParams.courseId = this.course!.id; + + // Check if this handler should be the first selected tab. + if (this.firstTabName && handler.name == this.firstTabName) { + tabToLoad = index + 1; + } + }); + + // Select the tab if needed. + this.firstTabName = undefined; + if (tabToLoad) { + setTimeout(() => { + this.tabsComponent?.selectByIndex(tabToLoad!); + }); + } + } + + /** + * Load title for the page. + * + * @return Promise resolved when done. + */ + protected async loadTitle(): Promise { + // Get the title to display initially. + this.title = CoreCourseFormatDelegate.instance.getCourseTitle(this.course!); + + // Load sections. + const sections = await CoreUtils.instance.ignoreErrors(CoreCourse.instance.getSections(this.course!.id, false, true)); + + if (!sections) { + return; + } + + // Get the title again now that we have sections. + this.title = CoreCourseFormatDelegate.instance.getCourseTitle(this.course!, sections); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.selectTabObserver?.off(); + } + + /** + * User entered the page. + */ + ionViewDidEnter(): void { + this.tabsComponent?.ionViewDidEnter(); + } + + /** + * User left the page. + */ + ionViewDidLeave(): void { + this.tabsComponent?.ionViewDidLeave(); + } + +} + +type CourseTab = CoreTab & { + name?: string; +}; diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index f13ff9f8a..17b8bc3e9 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -122,7 +122,6 @@ export class CoreCourseHelperProvider { protected logger: CoreLogger; constructor() { - this.logger = CoreLogger.getInstance('CoreCourseHelperProvider'); } @@ -138,17 +137,24 @@ export class CoreCourseHelperProvider { * @return Whether the sections have content. */ addHandlerDataForModules( - sections: CoreCourseSectionFormatted[], + sections: CoreCourseSection[], courseId: number, completionStatus?: Record, courseName?: string, forCoursePage = false, - ): boolean { + ): { hasContent: boolean; sections: CoreCourseSectionFormatted[] } { + const formattedSections: CoreCourseSectionFormatted[] = sections; let hasContent = false; - sections.forEach((section) => { - if (!section || !this.sectionHasContent(section) || !section.modules) { + formattedSections.forEach((section) => { + if (!section || !section.modules) { + return; + } + + section.hasContent = this.sectionHasContent(section); + + if (!section.hasContent) { return; } @@ -189,7 +195,7 @@ export class CoreCourseHelperProvider { }); }); - return hasContent; + return { hasContent, sections: formattedSections }; } /** @@ -821,8 +827,24 @@ export class CoreCourseHelperProvider { * @param modParams Params to pass to the module * @param True if module can be opened, false otherwise. */ - openModule(): void { - // @todo params and logic + openModule(module: CoreCourseModuleDataFormatted, courseId: number, sectionId?: number, modParams?: Params): boolean { + if (!module.handlerData) { + module.handlerData = CoreCourseModuleDelegate.instance.getModuleDataFor( + module.modname, + module, + courseId, + sectionId, + false, + ); + } + + if (module.handlerData?.action) { + module.handlerData.action(new Event('click'), module, courseId, { animated: false }, modParams); + + return true; + } + + return false; } /** @@ -1054,6 +1076,7 @@ export class CoreCourseHelper extends makeSingleton(CoreCourseHelperProvider) {} * Section with calculated data. */ export type CoreCourseSectionFormatted = Omit & { + hasContent?: boolean; modules: CoreCourseModuleDataFormatted[]; }; diff --git a/src/core/features/course/services/course-options-delegate.ts b/src/core/features/course/services/course-options-delegate.ts index b878c692a..30d0bac44 100644 --- a/src/core/features/course/services/course-options-delegate.ts +++ b/src/core/features/course/services/course-options-delegate.ts @@ -13,12 +13,18 @@ // limitations under the License. // @todo test delegate -import { Injectable, Type } from '@angular/core'; -import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { Injectable } from '@angular/core'; +import { CoreDelegate, CoreDelegateHandler, CoreDelegateToDisplay } from '@classes/delegate'; import { CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; import { CoreUtils, PromiseDefer } from '@services/utils/utils'; -import { CoreCourses, CoreCoursesProvider, CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; +import { + CoreCourseAnyCourseData, + CoreCourseAnyCourseDataWithOptions, + CoreCourses, + CoreCoursesProvider, + CoreCourseUserAdminOrNavOptionIndexed, +} from '@features/courses/services/courses'; import { CoreCourseProvider } from './course'; import { Params } from '@angular/router'; import { makeSingleton } from '@singletons'; @@ -61,7 +67,7 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler { * @return Data or promise resolved with the data. */ getDisplayData?( - course: CoreEnrolledCourseDataWithExtraInfoAndOptions, + course: CoreCourseAnyCourseDataWithOptions, ): CoreCourseOptionsHandlerData | Promise; /** @@ -98,7 +104,7 @@ export interface CoreCourseOptionsMenuHandler extends CoreCourseOptionsHandler { * @return Data or promise resolved with data. */ getMenuDisplayData( - course: CoreEnrolledCourseDataWithExtraInfoAndOptions, + course: CoreCourseAnyCourseDataWithOptions, ): CoreCourseOptionsMenuHandlerData | Promise; } @@ -117,15 +123,14 @@ export interface CoreCourseOptionsHandlerData { class?: string; /** - * The component to render the handler. It must be the component class, not the name or an instance. - * When the component is created, it will receive the courseId as input. + * Path of the page to load for the handler. */ - component: Type; + page: string; /** - * Data to pass to the component. All the properties in this object will be passed to the component as inputs. + * Params to pass to the page (other than 'courseId' which is always sent). */ - componentData?: Record; + pageParams?: Params; } /** @@ -143,7 +148,7 @@ export interface CoreCourseOptionsMenuHandlerData { class?: string; /** - * Name of the page to load for the handler. + * Path of the page to load for the handler. */ page: string; @@ -161,22 +166,12 @@ export interface CoreCourseOptionsMenuHandlerData { /** * Data returned by the delegate for each handler. */ -export interface CoreCourseOptionsHandlerToDisplay { +export interface CoreCourseOptionsHandlerToDisplay extends CoreDelegateToDisplay { /** * Data to display. */ data: CoreCourseOptionsHandlerData; - /** - * Name of the handler, or name and sub context (AddonMessages, AddonMessages:blockContact, ...). - */ - name: string; - - /** - * The highest priority is displayed first. - */ - priority?: number; - /** * Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline. * @@ -368,7 +363,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate { + const courseWithOptions: CoreCourseAnyCourseDataWithOptions = course; const accessData = { type: isGuest ? CoreCourseProvider.ACCESS_GUEST : CoreCourseProvider.ACCESS_DEFAULT, }; const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[] = []; if (navOptions) { - course.navOptions = navOptions; + courseWithOptions.navOptions = navOptions; } if (admOptions) { - course.admOptions = admOptions; + courseWithOptions.admOptions = admOptions; } - await this.loadCourseOptions(course, refresh); + await this.loadCourseOptions(courseWithOptions, refresh); + // Call getHandlersForAccess to make sure the handlers have been loaded. - await this.getHandlersForAccess(course.id, refresh, accessData, course.navOptions, course.admOptions); + await this.getHandlersForAccess(course.id, refresh, accessData, courseWithOptions.navOptions, courseWithOptions.admOptions); + const promises: Promise[] = []; let handlerList: CoreCourseOptionsMenuHandler[] | CoreCourseOptionsHandler[]; @@ -450,7 +448,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate { + promises.push(Promise.resolve(getFunction!.call(handler, courseWithOptions)).then((data) => { handlersToDisplay.push({ data: data, priority: handler.priority, @@ -587,7 +585,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate { + protected async loadCourseOptions(course: CoreCourseAnyCourseDataWithOptions, refresh = false): Promise { if (CoreCourses.instance.canGetAdminAndNavOptions() && (typeof course.navOptions == 'undefined' || typeof course.admOptions == 'undefined' || refresh)) { diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index f4fcff57f..1969a8333 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -981,7 +981,7 @@ export class CoreCourseProvider { * @return Promise resolved when done. */ async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params): Promise { - // @todo const loading = await CoreDomUtils.instance.showModalLoading(); + const loading = await CoreDomUtils.instance.showModalLoading(); // Wait for site plugins to be fetched. // @todo await this.sitePluginsProvider.waitFetchPlugins(); @@ -992,14 +992,13 @@ export class CoreCourseProvider { course = result.course; } - /* @todo - if (!this.sitePluginsProvider.sitePluginPromiseExists('format_' + course.format)) { + if (course) { // @todo Replace with: if (!this.sitePluginsProvider.sitePluginPromiseExists('format_' + course.format)) { // No custom format plugin. We don't need to wait for anything. - await CoreCourseFormatDelegate.instance.openCourse(course, params); + await CoreCourseFormatDelegate.instance.openCourse( course, params); loading.dismiss(); return; - } */ + } // This course uses a custom format plugin, wait for the format plugin to finish loading. try { diff --git a/src/core/features/course/services/handlers/default-format.ts b/src/core/features/course/services/handlers/default-format.ts index 6bdd96bf0..194f584ae 100644 --- a/src/core/features/course/services/handlers/default-format.ts +++ b/src/core/features/course/services/handlers/default-format.ts @@ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; import { Params } from '@angular/router'; import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses'; +import { CoreNavHelper } from '@services/nav-helper'; import { CoreUtils } from '@services/utils/utils'; import { CoreCourseSection } from '../course'; import { CoreCourseFormatHandler } from '../format-delegate'; @@ -175,7 +176,7 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { Object.assign(params, { course: course }); // Don't return the .push promise, we don't want to display a loading modal during the page transition. - // @todo navCtrl.push('CoreCourseSectionPage', params); + CoreNavHelper.instance.goInCurrentMainMenuTab('course', params); } /** diff --git a/src/core/features/course/services/handlers/default-module.ts b/src/core/features/course/services/handlers/default-module.ts index 8966a92ea..10c272e9c 100644 --- a/src/core/features/course/services/handlers/default-module.ts +++ b/src/core/features/course/services/handlers/default-module.ts @@ -49,8 +49,8 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler { getData( module: CoreCourseModuleData | CoreCourseModuleBasicInfo, courseId: number, // eslint-disable-line @typescript-eslint/no-unused-vars - sectionId: number, // eslint-disable-line @typescript-eslint/no-unused-vars - forCoursePage: boolean, // eslint-disable-line @typescript-eslint/no-unused-vars + sectionId?: number, // eslint-disable-line @typescript-eslint/no-unused-vars + forCoursePage?: boolean, // eslint-disable-line @typescript-eslint/no-unused-vars ): CoreCourseModuleHandlerData { // Return the default data. const defaultData: CoreCourseModuleHandlerData = { diff --git a/src/core/features/course/services/module-delegate.ts b/src/core/features/course/services/module-delegate.ts index 905800baa..9dbd5cd24 100644 --- a/src/core/features/course/services/module-delegate.ts +++ b/src/core/features/course/services/module-delegate.ts @@ -54,8 +54,8 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler { getData( module: CoreCourseModuleData | CoreCourseModuleBasicInfo, courseId: number, - sectionId: number, - forCoursePage: boolean, + sectionId?: number, + forCoursePage?: boolean, ): CoreCourseModuleHandlerData; /** @@ -270,7 +270,7 @@ export class CoreCourseModuleDelegateService extends CoreDelegate( diff --git a/src/core/features/courses/services/courses.ts b/src/core/features/courses/services/courses.ts index f2126f01a..54e64f496 100644 --- a/src/core/features/courses/services/courses.ts +++ b/src/core/features/courses/services/courses.ts @@ -1589,4 +1589,15 @@ type CoreCourseSetFavouriteCoursesWSParams = { }[]; }; +/** + * Any of the possible course data. + */ export type CoreCourseAnyCourseData = CoreEnrolledCourseData | CoreCourseSearchedData | CoreCourseGetCoursesData; + +/** + * Course data with admin and navigation option availability. + */ +export type CoreCourseAnyCourseDataWithOptions = CoreCourseAnyCourseData & { + navOptions?: CoreCourseUserAdminOrNavOptionIndexed; + admOptions?: CoreCourseUserAdminOrNavOptionIndexed; +}; diff --git a/src/core/features/mainmenu/pages/home/home.ts b/src/core/features/mainmenu/pages/home/home.ts index ab24ce8aa..005951458 100644 --- a/src/core/features/mainmenu/pages/home/home.ts +++ b/src/core/features/mainmenu/pages/home/home.ts @@ -17,7 +17,7 @@ import { Subscription } from 'rxjs'; import { CoreSites } from '@services/sites'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; -import { CoreTabsComponent } from '@components/tabs/tabs'; +import { CoreTab, CoreTabsComponent } from '@components/tabs/tabs'; import { CoreMainMenuHomeDelegate, CoreMainMenuHomeHandlerToDisplay } from '../../services/home-delegate'; /** @@ -33,7 +33,7 @@ export class CoreMainMenuHomePage implements OnInit { @ViewChild(CoreTabsComponent) tabsComponent?: CoreTabsComponent; siteName!: string; - tabs: CoreMainMenuHomeHandlerToDisplay[] = []; + tabs: CoreTab[] = []; loaded = false; selectedTab?: number; @@ -68,9 +68,10 @@ export class CoreMainMenuHomePage implements OnInit { const tab = this.tabs.find((tab) => tab.title == handler.title); return tab || handler; - }) + }); + // Sort them by priority so new handlers are in the right position. - .sort((a, b) => (b.priority || 0) - (a.priority || 0)); + newTabs.sort((a, b) => (b.priority || 0) - (a.priority || 0)); if (typeof this.selectedTab == 'undefined' && newTabs.length > 0) { let maxPriority = 0; diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts index 4a310b554..6c9f5f626 100644 --- a/src/core/features/sitehome/pages/index/index.ts +++ b/src/core/features/sitehome/pages/index/index.ts @@ -131,15 +131,14 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { // Check "Include a topic section" setting from numsections. this.section = config.numsections ? sections.find((section) => section.section == 1) : undefined; if (this.section) { - this.section.hasContent = false; - this.section.hasContent = CoreCourseHelper.instance.sectionHasContent(this.section); - this.hasContent = CoreCourseHelper.instance.addHandlerDataForModules( + const result = CoreCourseHelper.instance.addHandlerDataForModules( [this.section], this.siteHomeId, undefined, undefined, true, - ) || this.hasContent; + ); + this.hasContent = result.hasContent || this.hasContent; } // Add log in Moodle. diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts index 6c6029688..317b879f7 100644 --- a/src/core/singletons/events.ts +++ b/src/core/singletons/events.ts @@ -264,10 +264,25 @@ export type CoreEventFormActionData = CoreEventSiteData & { online?: boolean; // Whether the data was sent to server or not. Only when submitting. }; - /** * Data passed to NOTIFICATION_SOUND_CHANGED event. */ export type CoreEventNotificationSoundChangedData = CoreEventSiteData & { enabled: boolean; }; + +/** + * Data passed to SELECT_COURSE_TAB event. + */ +export type CoreEventSelectCourseTabData = CoreEventSiteData & { + name?: string; // Name of the tab's handler. If not set, load course contents. + sectionId?: number; + sectionNumber?: number; +}; + +/** + * Data passed to COMPLETION_MODULE_VIEWED event. + */ +export type CoreEventCompletionModuleViewedData = CoreEventSiteData & { + courseId?: number; +};