diff --git a/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts index a8c52eb53..1c240c1c6 100644 --- a/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts +++ b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts @@ -70,44 +70,41 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i let modFullNames: Record = {}; const brandedIcons: Record = {}; - sections.forEach((section) => { - if (!section.modules) { + const modules = CoreCourse.getSectionsModules(sections, { + ignoreSection: section => !CoreCourseHelper.canUserViewSection(section), + ignoreModule: module => !CoreCourseHelper.canUserViewModule(module) || !CoreCourse.moduleHasView(module), + }); + + modules.forEach((mod) => { + if (archetypes[mod.modname] !== undefined) { return; } - section.modules.forEach((mod) => { - if (archetypes[mod.modname] !== undefined || - !CoreCourseHelper.canUserViewModule(mod, section) || - !CoreCourse.moduleHasView(mod)) { - // Ignore this module. - return; + // Get the archetype of the module type. + archetypes[mod.modname] = CoreCourseModuleDelegate.supportsFeature( + mod.modname, + CoreConstants.FEATURE_MOD_ARCHETYPE, + CoreConstants.MOD_ARCHETYPE_OTHER, + ); + + // Get the full name of the module type. + if (archetypes[mod.modname] === CoreConstants.MOD_ARCHETYPE_RESOURCE) { + // All resources are gathered in a single "Resources" option. + if (!modFullNames['resources']) { + modFullNames['resources'] = Translate.instant('core.resources'); } + } else { + modFullNames[mod.modname] = mod.modplural; + } - // Get the archetype of the module type. - archetypes[mod.modname] = CoreCourseModuleDelegate.supportsFeature( - mod.modname, - CoreConstants.FEATURE_MOD_ARCHETYPE, - CoreConstants.MOD_ARCHETYPE_OTHER, - ); + brandedIcons[mod.modname] = mod.branded; - // Get the full name of the module type. - if (archetypes[mod.modname] === CoreConstants.MOD_ARCHETYPE_RESOURCE) { - // All resources are gathered in a single "Resources" option. - if (!modFullNames['resources']) { - modFullNames['resources'] = Translate.instant('core.resources'); - } - } else { - modFullNames[mod.modname] = mod.modplural; - } - - brandedIcons[mod.modname] = mod.branded; - - // If this is not a theme image, leave it undefined to avoid having specific activity icons. - if (CoreUrl.isThemeImageUrl(mod.modicon)) { - modIcons[mod.modname] = mod.modicon; - } - }); + // If this is not a theme image, leave it undefined to avoid having specific activity icons. + if (CoreUrl.isThemeImageUrl(mod.modicon)) { + modIcons[mod.modname] = mod.modicon; + } }); + // Sort the modnames alphabetically. modFullNames = CoreUtils.sortValues(modFullNames); for (const modName in modFullNames) { diff --git a/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html b/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html index 79c4f4af5..56c6d540d 100644 --- a/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html +++ b/src/addons/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html @@ -12,6 +12,8 @@ - + + + diff --git a/src/addons/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts b/src/addons/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts index 41569c651..a4323648e 100644 --- a/src/addons/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts +++ b/src/addons/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts @@ -14,7 +14,7 @@ import { Component, OnInit } from '@angular/core'; import { CoreSites } from '@services/sites'; -import { CoreCourse } from '@features/course/services/course'; +import { CoreCourse, sectionContentIsModule } from '@features/course/services/course'; import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper'; import { CoreSiteHome, FrontPageItemNames } from '@features/sitehome/services/sitehome'; import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; @@ -39,6 +39,7 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl component = 'AddonBlockSiteMainMenu'; mainMenuBlock?: CoreCourseSection; siteHomeId = 1; + isModule = sectionContentIsModule; protected fetchContentDefaultError = 'Error getting main menu data.'; @@ -66,9 +67,12 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl promises.push(CoreCourse.invalidateSections(this.siteHomeId)); promises.push(CoreSiteHome.invalidateNewsForum(this.siteHomeId)); - if (this.mainMenuBlock && this.mainMenuBlock.modules) { + if (this.mainMenuBlock?.contents.length) { // Invalidate modules prefetch data. - promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(this.mainMenuBlock.modules, this.siteHomeId)); + promises.push(CoreCourseModulePrefetchDelegate.invalidateModules( + CoreCourse.getSectionsModules([this.mainMenuBlock]), + this.siteHomeId, + )); } await Promise.all(promises); @@ -114,11 +118,11 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl try { const forum = await CoreSiteHome.getNewsForum(this.siteHomeId); // Search the module that belongs to site news. - const forumIndex = - this.mainMenuBlock.modules.findIndex((mod) => mod.modname == 'forum' && mod.instance == forum.id); + const forumIndex = this.mainMenuBlock.contents.findIndex((mod) => + sectionContentIsModule(mod) && mod.modname == 'forum' && mod.instance == forum.id); if (forumIndex >= 0) { - this.mainMenuBlock.modules.splice(forumIndex, 1); + this.mainMenuBlock.contents.splice(forumIndex, 1); } } catch { // Ignore errors. diff --git a/src/addons/mod/subsection/services/handlers/index-link.ts b/src/addons/mod/subsection/services/handlers/index-link.ts index f8962eb5d..18455aca7 100644 --- a/src/addons/mod/subsection/services/handlers/index-link.ts +++ b/src/addons/mod/subsection/services/handlers/index-link.ts @@ -19,7 +19,8 @@ import { CoreCourse } from '@features/course/services/course'; import { CoreLoadings } from '@services/loadings'; import { CoreDomUtils } from '@services/utils/dom'; import { makeSingleton } from '@singletons'; -import { AddonModSubsection } from '../subsection'; +import { CoreSites } from '@services/sites'; +import { CoreCourseHelper } from '@features/course/services/course-helper'; /** * Handler to treat links to subsection. @@ -33,6 +34,29 @@ export class AddonModSubsectionIndexLinkHandlerService extends CoreContentLinksM super('AddonModSubsection', 'subsection', 'id'); } + /** + * Open a subsection. + * + * @param sectionId Section ID. + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @returns Promise resolved when done. + */ + async openSubsection(sectionId: number, courseId: number, siteId?: string): Promise { + const pageParams = { + sectionId, + }; + + if ( + (!siteId || siteId === CoreSites.getCurrentSiteId()) && + CoreCourse.currentViewIsCourse(courseId) + ) { + CoreCourse.selectCourseTab('', pageParams); + } else { + await CoreCourseHelper.getAndOpenCourse(courseId, pageParams, siteId); + } + } + /** * @inheritdoc */ @@ -51,7 +75,7 @@ export class AddonModSubsectionIndexLinkHandlerService extends CoreContentLinksM // Get the module. const module = await CoreCourse.getModule(moduleId, courseId, undefined, true, false, siteId); - await AddonModSubsection.openSubsection(module.section, module.course, siteId); + await this.openSubsection(module.section, module.course, siteId); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error opening link.'); } finally { diff --git a/src/addons/mod/subsection/services/handlers/module.ts b/src/addons/mod/subsection/services/handlers/module.ts deleted file mode 100644 index c9266d0d5..000000000 --- a/src/addons/mod/subsection/services/handlers/module.ts +++ /dev/null @@ -1,88 +0,0 @@ -// (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 { CoreConstants, ModPurpose } from '@/core/constants'; -import { Injectable } from '@angular/core'; -import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler'; -import { CoreCourseModuleData } from '@features/course/services/course-helper'; -import { - CoreCourseModuleDelegate, - CoreCourseModuleHandler, - CoreCourseModuleHandlerData, -} from '@features/course/services/module-delegate'; -import { CoreDomUtils } from '@services/utils/dom'; -import { makeSingleton } from '@singletons'; -import { AddonModSubsection } from '../subsection'; - -/** - * Handler to support subsection modules. - * - * This is merely to disable the siteplugin. - */ -@Injectable({ providedIn: 'root' }) -export class AddonModSubsectionModuleHandlerService extends CoreModuleHandlerBase implements CoreCourseModuleHandler { - - name = 'AddonModSubsection'; - modName = 'subsection'; - - supportedFeatures = { - [CoreConstants.FEATURE_MOD_ARCHETYPE]: CoreConstants.MOD_ARCHETYPE_RESOURCE, - [CoreConstants.FEATURE_GROUPS]: false, - [CoreConstants.FEATURE_GROUPINGS]: false, - [CoreConstants.FEATURE_MOD_INTRO]: false, - [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, - [CoreConstants.FEATURE_GRADE_HAS_GRADE]: false, - [CoreConstants.FEATURE_GRADE_OUTCOMES]: false, - [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, - [CoreConstants.FEATURE_SHOW_DESCRIPTION]: false, - [CoreConstants.FEATURE_MOD_PURPOSE]: ModPurpose.MOD_PURPOSE_CONTENT, - }; - - /** - * @inheritdoc - */ - getData(module: CoreCourseModuleData): CoreCourseModuleHandlerData { - return { - icon: CoreCourseModuleDelegate.getModuleIconSrc(module.modname, module.modicon), - title: module.name, - a11yTitle: '', - class: 'addon-mod-subsection-handler', - hasCustomCmListItem: true, - action: async(event, module) => { - try { - await AddonModSubsection.openSubsection(module.section, module.course); - } catch (error) { - CoreDomUtils.showErrorModalDefault(error, 'Error opening subsection.'); - } - }, - }; - } - - /** - * @inheritdoc - */ - async getMainComponent(): Promise { - // There's no need to implement this because subsection cannot be used in singleactivity course format. - return; - } - - /** - * @inheritdoc - */ - getIconSrc(): string { - return ''; - } - -} -export const AddonModSubsectionModuleHandler = makeSingleton(AddonModSubsectionModuleHandlerService); diff --git a/src/addons/mod/subsection/services/subsection.ts b/src/addons/mod/subsection/services/subsection.ts deleted file mode 100644 index ac62b952f..000000000 --- a/src/addons/mod/subsection/services/subsection.ts +++ /dev/null @@ -1,51 +0,0 @@ -// (C) Copyright 2015 Moodle Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { Injectable } from '@angular/core'; -import { CoreCourse } from '@features/course/services/course'; -import { CoreCourseHelper } from '@features/course/services/course-helper'; -import { CoreSites } from '@services/sites'; -import { makeSingleton } from '@singletons'; - -/** - * Service that provides some features for subsections. - */ -@Injectable({ providedIn: 'root' }) -export class AddonModSubsectionProvider { - - /** - * Open a subsection. - * - * @param sectionId Section ID. - * @param courseId Course ID. - * @param siteId Site ID. If not defined, current site. - * @returns Promise resolved when done. - */ - async openSubsection(sectionId: number, courseId: number, siteId?: string): Promise { - const pageParams = { - sectionId, - }; - - if ( - (!siteId || siteId === CoreSites.getCurrentSiteId()) && - CoreCourse.currentViewIsCourse(courseId) - ) { - CoreCourse.selectCourseTab('', pageParams); - } else { - await CoreCourseHelper.getAndOpenCourse(courseId, pageParams, siteId); - } - } - -} -export const AddonModSubsection = makeSingleton(AddonModSubsectionProvider); diff --git a/src/addons/mod/subsection/subsection.module.ts b/src/addons/mod/subsection/subsection.module.ts index bfd1f2f09..a917ddf0e 100644 --- a/src/addons/mod/subsection/subsection.module.ts +++ b/src/addons/mod/subsection/subsection.module.ts @@ -14,9 +14,7 @@ import { APP_INITIALIZER, NgModule } from '@angular/core'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; -import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { AddonModSubsectionIndexLinkHandler } from './services/handlers/index-link'; -import { AddonModSubsectionModuleHandler } from './services/handlers/module'; @NgModule({ providers: [ @@ -24,7 +22,6 @@ import { AddonModSubsectionModuleHandler } from './services/handlers/module'; provide: APP_INITIALIZER, multi: true, useValue: () => { - CoreCourseModuleDelegate.registerHandler(AddonModSubsectionModuleHandler.instance); CoreContentLinksDelegate.registerHandler(AddonModSubsectionIndexLinkHandler.instance); }, }, diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.html b/src/addons/storagemanager/pages/course-storage/course-storage.html index e3d2cf0dc..eb06b7744 100644 --- a/src/addons/storagemanager/pages/course-storage/course-storage.html +++ b/src/addons/storagemanager/pages/course-storage/course-storage.html @@ -16,7 +16,6 @@ - @@ -46,101 +45,111 @@ - + - - - - -

- -

- - {{ section.totalSize | coreBytesToSize }} - - - {{ 'core.calculating' | translate }} - - -

- -

-
-
-
- - - - {{section.count}} / {{section.total}} - -
- - - -
-
- - - - - - -

- -

- - {{ module.totalSize | - coreBytesToSize }} - - - {{ 'core.calculating' | translate }} - -
- -
- - - - -

- {{ 'core.notdownloadable' | translate }} -

-
-
-
-
-
-
-
+
+ + + + + + + +

+ +

+ + {{ section.totalSize | coreBytesToSize }} + + + {{ 'core.calculating' | translate }} + + +

+ +

+
+
+
+ + + + {{section.count}} / {{section.total}} + +
+ + + +
+
+ + + + @if (!isModule(modOrSubsection)) { + + } @else { + + + +

+ +

+ + {{ modOrSubsection.totalSize | + coreBytesToSize }} + + + {{ 'core.calculating' | translate }} + +
+ +
+ + + + +

+ {{ 'core.notdownloadable' | translate }} +

+
+
+ } +
+
+
+
+
+
diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.scss b/src/addons/storagemanager/pages/course-storage/course-storage.scss index e8690d5ac..49255af47 100644 --- a/src/addons/storagemanager/pages/course-storage/course-storage.scss +++ b/src/addons/storagemanager/pages/course-storage/course-storage.scss @@ -17,11 +17,6 @@ } } - .accordion-expanded { - ion-item.card-header { - --border-width: 0 0 1px 0; - } - } ion-card-content { padding: 0; diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.ts b/src/addons/storagemanager/pages/course-storage/course-storage.ts index 6573b2cb0..ebbdf01f5 100644 --- a/src/addons/storagemanager/pages/course-storage/course-storage.ts +++ b/src/addons/storagemanager/pages/course-storage/course-storage.ts @@ -13,11 +13,12 @@ // limitations under the License. import { CoreConstants, DownloadStatus } from '@/core/constants'; -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { CoreCourse, CoreCourseProvider } from '@features/course/services/course'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit } from '@angular/core'; +import { CoreCourse, CoreCourseProvider, sectionContentIsModule } from '@features/course/services/course'; import { CoreCourseHelper, CoreCourseModuleData, + CoreCourseSection, CoreCourseSectionWithStatus, CorePrefetchStatusInfo, } from '@features/course/services/course-helper'; @@ -25,12 +26,11 @@ import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchHandler } from '@features/course/services/module-prefetch-delegate'; import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses'; -import { AccordionGroupChangeEventDetail, IonAccordionGroup } from '@ionic/angular'; +import { AccordionGroupChangeEventDetail } from '@ionic/angular'; import { CoreLoadings } from '@services/loadings'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreDom } from '@singletons/dom'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -47,14 +47,13 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events'; }) export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { - @ViewChild('accordionGroup', { static: true }) accordionGroup!: IonAccordionGroup; - courseId!: number; title = ''; loaded = false; sections: AddonStorageManagerCourseSection[] = []; totalSize = 0; calculatingSize = true; + accordionMultipleValue: string[] = []; downloadEnabled = false; downloadCourseEnabled = false; @@ -67,6 +66,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { }; statusDownloaded = DownloadStatus.DOWNLOADED; + isModule = sectionContentIsModule; protected siteUpdatedObserver?: CoreEventObserver; protected courseStatusObserver?: CoreEventObserver; @@ -116,27 +116,25 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { const sections = (await CoreCourse.getSections(this.courseId, false, true)) .filter((section) => !CoreCourseHelper.isSectionStealth(section)); + this.sections = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections - .map(section => ({ - ...section, - totalSize: 0, - calculatingSize: true, - expanded: section.id === initialSectionId, - modules: section.modules.map(module => ({ - ...module, - calculatingSize: true, - })), - })); + .map(section => this.formatSection(section)); this.loaded = true; - this.accordionGroup.value = String(initialSectionId); + if (initialSectionId !== undefined && initialSectionId > 0) { + this.accordionMultipleValue.push(initialSectionId.toString()); + this.accordionGroupChange(); - CoreDom.scrollToElement( - this.elementRef.nativeElement, - '.accordion-expanded', - { addYAxis: -10 }, - ); + CoreDom.scrollToElement( + this.elementRef.nativeElement, + `#addons-course-storage-${initialSectionId}`, + { addYAxis: -10 }, + ); + } else { + this.accordionMultipleValue.push(this.sections[0].id.toString()); + this.accordionGroupChange(); + } await Promise.all([ this.initSizes(), @@ -147,9 +145,34 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { } /** - * Init course prefetch information. + * Format a section. * - * @returns Promise resolved when done. + * @param section Section to format. + * @param expanded Whether section should be expanded. + * @returns Formatted section, + */ + protected formatSection(section: CoreCourseSection, expanded = false): AddonStorageManagerCourseSection { + return { + ...section, + totalSize: 0, + calculatingSize: true, + expanded: expanded, + contents: section.contents.map(modOrSubsection => { + if (sectionContentIsModule(modOrSubsection)) { + return { + ...modOrSubsection, + totalSize: 0, + calculatingSize: false, + }; + } + + return this.formatSection(modOrSubsection, expanded); + }), + }; + } + + /** + * Init course prefetch information. */ protected async initCoursePrefetch(): Promise { if (!this.downloadCourseEnabled || this.courseStatusObserver) { @@ -158,7 +181,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { // Listen for changes in course status. this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => { - if (data.courseId == this.courseId || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) { + if (data.courseId === this.courseId || data.courseId === CoreCourseProvider.ALL_COURSES_CLEARED) { this.updateCourseStatus(data.status); } }, CoreSites.getCurrentSiteId()); @@ -166,7 +189,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { // Determine the course prefetch status. await this.determineCoursePrefetchIcon(); - if (this.prefetchCourseData.icon != CoreConstants.ICON_LOADING) { + if (this.prefetchCourseData.icon !== CoreConstants.ICON_LOADING) { return; } @@ -189,8 +212,6 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { /** * Init module prefetch information. - * - * @returns Promise resolved when done. */ protected async initModulePrefetch(): Promise { if (!this.downloadEnabled || this.sectionStatusObserver) { @@ -205,154 +226,113 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { return; } - // Check if the affected section is being downloaded. - // If so, we don't update section status because it'll already be updated when the download finishes. - const downloadId = CoreCourseHelper.getSectionDownloadId({ id: data.sectionId }); - if (CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) { - return; - } - // Get the affected section. - const section = this.sections.find(section => section.id == data.sectionId); + const { section } = CoreCourseHelper.findSection(this.sections, { id: data.sectionId }); if (!section) { return; } + // @todo: Handle parents too? It seems the SECTION_STATUS_CHANGED event is never triggered. + + // Check if the affected section is being downloaded. + // If so, we don't update section status because it'll already be updated when the download finishes. + const downloadId = CoreCourseHelper.getSectionDownloadId({ id: section.id }); + if (CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) { + return; + } + // Recalculate the status. - await CoreCourseHelper.calculateSectionStatus(section, this.courseId, false); + await this.updateSizes([section]); if (section.isDownloading && !CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) { // All the modules are now downloading, set a download all promise. - this.prefecthSection(section); + this.prefetchSection(section); } }, CoreSites.getCurrentSiteId(), ); - // The download status of a section might have been changed from within a module page. - CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false); - - this.sections.forEach((section) => { - section.modules.forEach((module) => { - if (module.handlerData?.showDownloadButton) { - - module.spinner = true; - // Listen for changes on this module status, even if download isn't enabled. - module.prefetchHandler = CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor(module.modname); - this.calculateModuleStatus(module); - } - }); - }); - this.moduleStatusObserver = CoreEvents.on(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => { - let module: AddonStorageManagerModule | undefined; + const modules = CoreCourse.getSectionsModules(this.sections); + const moduleFound = modules.find(module => module.id === data.componentId && module.prefetchHandler && + data.component === module.prefetchHandler?.component); - this.sections.some((section) => { - module = section.modules.find((module) => - module.id == data.componentId && module.prefetchHandler && data.component == module.prefetchHandler?.component); - - return !!module; - }); - - if (!module) { + if (!moduleFound) { return; } // Call determineModuleStatus to get the right status to display. - const status = CoreCourseModulePrefetchDelegate.determineModuleStatus(module, data.status); + const status = CoreCourseModulePrefetchDelegate.determineModuleStatus(moduleFound, data.status); // Update the status. - this.updateModuleStatus(module, status); + this.updateModuleStatus(moduleFound, status); }, CoreSites.getCurrentSiteId()); + + // The download status of a section might have been changed from within a module page. + this.updateSizes(this.sections); } /** * Init section, course and modules sizes. */ protected async initSizes(): Promise { - await Promise.all(this.sections.map(async (section) => { - await Promise.all(section.modules.map(async (module) => { - // Note: This function only gets the size for modules which are downloadable. - // For other modules it always returns 0, even if they have downloaded some files. - // However there is no 100% reliable way to actually track the files in this case. - // You can maybe guess it based on the component and componentid. - // But these aren't necessarily consistent, for example mod_frog vs mmaModFrog. - // There is nothing enforcing correct values. - // Most modules which have large files are downloadable, so I think this is sufficient. - const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId); - - // There are some cases where the return from this is not a valid number. - if (!isNaN(size)) { - module.totalSize = Number(size); - section.totalSize += size; - this.totalSize += size; - } - - module.calculatingSize = false; - })); - - section.calculatingSize = false; - })); - - this.calculatingSize = false; - - // Mark course as not downloaded if course size is 0. - if (this.totalSize == 0) { - this.markCourseAsNotDownloaded(); - } + await this.updateSizes(this.sections); } /** - * Update the sizes of some modules. + * Update the sizes of some sections and modules. * - * @param modules Modules. - * @param section Section the modules belong to. - * @returns Promise resolved when done. + * @param sections Modules. */ - protected async updateModulesSizes( - modules: AddonStorageManagerModule[], - section?: AddonStorageManagerCourseSection, - ): Promise { + protected async updateSizes(sections: AddonStorageManagerCourseSection[]): Promise { this.calculatingSize = true; + CoreCourseHelper.flattenSections(sections).forEach((section) => { + section.calculatingSize = true; + }); + this.changeDetectorRef.markForCheck(); + + // Update only affected module sections. + const modules = CoreCourse.getSectionsModules(sections); await Promise.all(modules.map(async (module) => { - if (module.calculatingSize) { - return; - } - - module.calculatingSize = true; - this.changeDetectorRef.markForCheck(); - - if (!section) { - section = this.sections.find((section) => section.modules.some((mod) => mod.id === module.id)); - if (section) { - section.calculatingSize = true; - this.changeDetectorRef.markForCheck(); - } - } - - try { - const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId); - - const diff = (isNaN(size) ? 0 : size) - (module.totalSize ?? 0); - - module.totalSize = Number(size); - this.totalSize += diff; - if (section) { - section.totalSize += diff; - } - } catch { - // Ignore errors, it shouldn't happen. - } finally { - module.calculatingSize = false; - this.changeDetectorRef.markForCheck(); - } + await this.calculateModuleSize(module); })); - this.calculatingSize = false; - if (section) { + const updateSectionSize = (section: AddonStorageManagerCourseSection): void => { + section.totalSize = 0; + section.calculatingSize = true; + + this.changeDetectorRef.markForCheck(); + + section.contents.forEach((modOrSubsection) => { + if (!sectionContentIsModule(modOrSubsection)) { + updateSectionSize(modOrSubsection); + } + + section.totalSize += modOrSubsection.totalSize ?? 0; + this.changeDetectorRef.markForCheck(); + }); section.calculatingSize = false; + + this.changeDetectorRef.markForCheck(); + }; + + // Update section and total sizes. + this.totalSize = 0; + this.sections.forEach((section) => { + updateSectionSize(section); + this.totalSize += section.totalSize; + }); + this.calculatingSize = false; + + // Mark course as not downloaded if course size is 0. + if (this.totalSize === 0) { + this.markCourseAsNotDownloaded(); } + + this.changeDetectorRef.markForCheck(); + + await this.calculateSectionsStatus(sections); this.changeDetectorRef.markForCheck(); } @@ -380,16 +360,10 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { return; } - const modules: AddonStorageManagerModule[] = []; - this.sections.forEach((section) => { - section.modules.forEach((module) => { - if (module.totalSize && module.totalSize > 0) { - modules.push(module); - } - }); - }); + const modules = CoreCourse.getSectionsModules(this.sections) + .filter((module) => module.totalSize && module.totalSize > 0); - this.deleteModules(modules); + await this.deleteModules(modules); } /** @@ -417,14 +391,9 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { return; } - const modules: AddonStorageManagerModule[] = []; - section.modules.forEach((module) => { - if (module.totalSize && module.totalSize > 0) { - modules.push(module); - } - }); + const modules = CoreCourse.getSectionsModules([section]).filter((module) => module.totalSize && module.totalSize > 0); - this.deleteModules(modules, section); + await this.deleteModules(modules); } /** @@ -432,12 +401,10 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { * * @param event Event object. * @param module Module details - * @param section Section the module belongs to. */ async deleteForModule( event: Event, module: AddonStorageManagerModule, - section: AddonStorageManagerCourseSection, ): Promise { event.stopPropagation(); event.preventDefault(); @@ -459,35 +426,29 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { return; } - this.deleteModules([module], section); + await this.deleteModules([module]); } /** * Deletes the specified modules, showing the loading overlay while it happens. * * @param modules Modules to delete - * @param section Section the modules belong to. - * @returns Promise Once deleting has finished */ - protected async deleteModules(modules: AddonStorageManagerModule[], section?: AddonStorageManagerCourseSection): Promise { + protected async deleteModules(modules: AddonStorageManagerModule[]): Promise { const modal = await CoreLoadings.show('core.deleting', true); - const promises: Promise[] = []; - modules.forEach((module) => { + const sections = new Set(); + const promises = modules.map(async (module) => { // Remove the files. - const promise = CoreCourseHelper.removeModuleStoredData(module, this.courseId).then(() => { - const moduleSize = module.totalSize || 0; - // When the files and cache are removed, update the size. - if (section) { - section.totalSize -= moduleSize; - } - this.totalSize -= moduleSize; - module.totalSize = 0; + await CoreCourseHelper.removeModuleStoredData(module, this.courseId); - return; - }); + module.totalSize = 0; - promises.push(promise); + const { section, parents } = CoreCourseHelper.findSection(this.sections, { id: module.section }); + const rootSection = parents[0] ?? section; + if (rootSection && !sections.has(rootSection)) { + sections.add(rootSection); + } }); try { @@ -497,13 +458,8 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { } finally { modal.dismiss(); - await this.updateModulesSizes(modules, section); - CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false); + await this.updateSizes(Array.from(sections)); - // For delete all, reset all section sizes so icons are updated. - if (this.totalSize === 0) { - this.sections.map(section => section.totalSize = 0); - } this.changeDetectorRef.markForCheck(); } } @@ -519,40 +475,26 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { CoreCourse.setCourseStatus(this.courseId, DownloadStatus.DOWNLOADABLE_NOT_DOWNLOADED); } - /** - * Calculate the status of sections. - * - * @param refresh If refresh or not. - */ - protected calculateSectionsStatus(refresh?: boolean): void { - if (!this.sections) { - return; - } - - CoreUtils.ignoreErrors(CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, refresh)); - } - /** * Confirm and prefetch a section. If the section is "all sections", prefetch all the sections. * * @param section Section to download. */ - async prefecthSection(section: AddonStorageManagerCourseSection): Promise { + async prefetchSection(section: AddonStorageManagerCourseSection): Promise { section.isCalculating = true; this.changeDetectorRef.markForCheck(); try { - await CoreCourseHelper.confirmDownloadSizeSection(this.courseId, section, this.sections); + await CoreCourseHelper.confirmDownloadSizeSection(this.courseId, [section]); try { - await CoreCourseHelper.prefetchSection(section, this.courseId, this.sections); + await CoreCourseHelper.prefetchSections([section], this.courseId); } catch (error) { if (!this.isDestroyed) { CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingsection', true); } } finally { - await this.updateModulesSizes(section.modules, section); - this.changeDetectorRef.markForCheck(); + await this.updateSizes([section]); } } catch (error) { // User cancelled or there was an error calculating the size. @@ -588,12 +530,9 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { try { // Get download size to ask for confirm if it's high. - const size = await module.prefetchHandler.getDownloadSize(module, module.course, true); await CoreCourseHelper.prefetchModule(module.prefetchHandler, module, size, module.course, refresh); - - CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false); } catch (error) { if (!this.isDestroyed) { CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true); @@ -601,8 +540,11 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { } finally { module.spinner = false; - await this.updateModulesSizes([module]); - + const { section, parents } = CoreCourseHelper.findSection(this.sections, { id: module.section }); + const rootSection = parents[0] ?? section; + if (rootSection) { + await this.updateSizes([rootSection]); + } } } @@ -625,25 +567,31 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { } /** - * Calculate and show module status. + * Calculate all modules status on a section. * - * @param module Module to update. - * @returns Promise resolved when done. + * @param section Section to check. */ - protected async calculateModuleStatus(module: AddonStorageManagerModule): Promise { - if (!module) { - return; - } + protected async calculateModulesStatusOnSection(section: AddonStorageManagerCourseSection): Promise { + await Promise.all(section.contents.map(async (modOrSubsection) => { + if (!sectionContentIsModule(modOrSubsection)) { + await this.calculateModulesStatusOnSection(modOrSubsection); - const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(module, this.courseId); + return; + } - this.updateModuleStatus(module, status); + if (modOrSubsection.handlerData?.showDownloadButton) { + modOrSubsection.spinner = true; + // Listen for changes on this module status, even if download isn't enabled. + modOrSubsection.prefetchHandler = CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor(modOrSubsection.modname); + const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(modOrSubsection, this.courseId); + + this.updateModuleStatus(modOrSubsection, status); + } + })); } /** * Determines the prefetch icon of the course. - * - * @returns Promise resolved when done. */ protected async determineCoursePrefetchIcon(): Promise { this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.courseId); @@ -664,6 +612,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { this.changeDetectorRef.markForCheck(); } + /** + * Get the course object. + * + * @param courseId Course ID. + * @returns Promise resolved with the course object if found. + */ protected async getCourse(courseId: number): Promise { try { // Check if user is enrolled. If enrolled, no guest access. @@ -709,8 +663,8 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { isGuest: this.isGuest, }, ); - await Promise.all(this.sections.map(section => this.updateModulesSizes(section.modules, section))); - this.changeDetectorRef.markForCheck(); + + await this.updateSizes(this.sections); } catch (error) { if (this.isDestroyed) { return; @@ -720,17 +674,47 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { } } + /** + * Calculate the size of the modules. + * + * @param module Module to calculate. + */ + protected async calculateModuleSize(module: AddonStorageManagerModule): Promise { + if (module.calculatingSize) { + return; + } + + module.calculatingSize = true; + this.changeDetectorRef.markForCheck(); + + // Note: This function only gets the size for modules which are downloadable. + // For other modules it always returns 0, even if they have downloaded some files. + // However there is no 100% reliable way to actually track the files in this case. + // You can maybe guess it based on the component and componentid. + // But these aren't necessarily consistent, for example mod_frog vs mmaModFrog. + // There is nothing enforcing correct values. + // Most modules which have large files are downloadable, so I think this is sufficient. + module.totalSize = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId); + + module.calculatingSize = false; + this.changeDetectorRef.markForCheck(); + } + /** * Toggle expand status. * - * @param event Event object. + * @param event Event object. If not defined, use the current value. */ - accordionGroupChange(event: AccordionGroupChangeEventDetail): void { - this.sections.forEach((section) => { + accordionGroupChange(event?: AccordionGroupChangeEventDetail): void { + const sectionIds = event?.value as string[] ?? this.accordionMultipleValue; + const allSections = CoreCourseHelper.flattenSections(this.sections); + allSections.forEach((section) => { section.expanded = false; }); - event.value.forEach((sectionId) => { - const section = this.sections.find((section) => section.id === Number(sectionId)); + + sectionIds.forEach((sectionId) => { + const section = allSections.find((section) => section.id === Number(sectionId)); + if (section) { section.expanded = true; } @@ -746,21 +730,45 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { this.moduleStatusObserver?.off(); this.siteUpdatedObserver?.off(); - this.sections.forEach((section) => { - section.modules.forEach((module) => { - module.handlerData?.onDestroy?.(); - }); + CoreCourse.getSectionsModules(this.sections).forEach((module) => { + module.handlerData?.onDestroy?.(); }); + this.isDestroyed = true; } + /** + * Calculate the status of a list of sections, setting attributes to determine the icons/data to be shown. + * + * @param sections Sections to calculate their status. + */ + protected async calculateSectionsStatus(sections: AddonStorageManagerCourseSection[]): Promise { + if (!sections) { + return; + } + + await Promise.all(sections.map(async (section) => { + if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) { + return; + } + + try { + section.isCalculating = true; + await this.calculateModulesStatusOnSection(section); + await CoreCourseHelper.calculateSectionStatus(section, this.courseId, false, false); + } finally { + section.isCalculating = false; + } + })); + } + } -type AddonStorageManagerCourseSection = Omit & { +type AddonStorageManagerCourseSection = Omit & { totalSize: number; calculatingSize: boolean; expanded: boolean; - modules: AddonStorageManagerModule[]; + contents: (AddonStorageManagerCourseSection | AddonStorageManagerModule)[]; }; type AddonStorageManagerModule = CoreCourseModuleData & { diff --git a/src/addons/storagemanager/pages/courses-storage/courses-storage.ts b/src/addons/storagemanager/pages/courses-storage/courses-storage.ts index 6e44a0c85..c09367c1d 100644 --- a/src/addons/storagemanager/pages/courses-storage/courses-storage.ts +++ b/src/addons/storagemanager/pages/courses-storage/courses-storage.ts @@ -17,7 +17,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { CoreQueueRunner } from '@classes/queue-runner'; import { CoreCourse, CoreCourseProvider } from '@features/course/services/course'; import { CoreCourseHelper } from '@features/course/services/course-helper'; -import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses'; import { CoreSettingsHelper, CoreSiteSpaceUsage } from '@features/settings/services/settings-helper'; import { CoreSiteHome } from '@features/sitehome/services/sitehome'; @@ -240,15 +239,9 @@ export class AddonStorageManagerCoursesStoragePage implements OnInit, OnDestroy */ private async calculateDownloadedCourseSize(courseId: number): Promise { const sections = await CoreCourse.getSections(courseId); - const modules = sections.map((section) => section.modules).flat(); - const promisedModuleSizes = modules.map(async (module) => { - const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId); + const modules = CoreCourse.getSectionsModules(sections); - return isNaN(size) ? 0 : size; - }); - const moduleSizes = await Promise.all(promisedModuleSizes); - - return moduleSizes.reduce((totalSize, moduleSize) => totalSize + moduleSize, 0); + return CoreCourseHelper.getModulesDownloadedSize(modules, courseId); } /** diff --git a/src/core/features/course/components/course-format/course-format.html b/src/core/features/course/components/course-format/course-format.html index 733821a0f..ecc0b6bb6 100644 --- a/src/core/features/course/components/course-format/course-format.html +++ b/src/core/features/course/components/course-format/course-format.html @@ -12,9 +12,9 @@ - + @@ -27,12 +27,11 @@ @for (section of sections; track section.id) { - @if ($index <= lastShownSectionIndex && !section.hiddenbynumsections && section.id !== allSectionsId && - section.id !== stealthModulesSectionId && !section.component) { - - } + @if ($index + <= lastShownSectionIndex && !section.hiddenbynumsections && section.id !==allSectionsId && section.id + !==stealthModulesSectionId) { + } } 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 ca9c989f6..ca29c70fe 100644 --- a/src/core/features/course/components/course-format/course-format.ts +++ b/src/core/features/course/components/course-format/course-format.ts @@ -31,9 +31,11 @@ import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; import { CoreCourse, CoreCourseProvider, + sectionContentIsModule, } from '@features/course/services/course'; import { CoreCourseHelper, + CoreCourseModuleData, CoreCourseSection, } from '@features/course/services/course-helper'; import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; @@ -124,7 +126,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { displayCourseIndex = false; displayBlocks = false; hasBlocks = false; - subSections: CoreCourseSectionToDisplay[] = []; // List of course subsections. selectedSection?: CoreCourseSectionToDisplay; previousSection?: CoreCourseSectionToDisplay; nextSection?: CoreCourseSectionToDisplay; @@ -184,27 +185,25 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { } }); - this.modViewedObserver = CoreEvents.on(CoreEvents.COURSE_MODULE_VIEWED, (data) => { - if (data.courseId !== this.course.id) { + this.modViewedObserver = CoreEvents.on(CoreEvents.COURSE_MODULE_VIEWED, (lastModuleViewed) => { + if (lastModuleViewed.courseId !== this.course.id) { return; } - this.viewedModules[data.cmId] = true; - if (!this.lastModuleViewed || data.timeaccess > this.lastModuleViewed.timeaccess) { - this.lastModuleViewed = data; + this.viewedModules[lastModuleViewed.cmId] = true; + if (!this.lastModuleViewed || lastModuleViewed.timeaccess > this.lastModuleViewed.timeaccess) { + this.lastModuleViewed = lastModuleViewed; if (this.selectedSection && this.selectedSection.id !== this.allSectionsId) { // Change section to display the one with the last viewed module - const lastViewedSection = this.getViewedModuleSection(this.sections, data); + const lastViewedSection = this.getViewedModuleSection(); if (lastViewedSection && lastViewedSection.id !== this.selectedSection?.id) { - this.sectionChanged(lastViewedSection, data.cmId); + this.sectionChanged(lastViewedSection, this.lastModuleViewed.cmId); } } } this.changeDetectorRef.markForCheck(); }); - - this.initializeExpandedSections(); } /** @@ -226,11 +225,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { } if (changes.sections && this.sections) { - this.subSections = this.sections.filter((section) => section.component === 'mod_subsection'); - this.sections = this.sections.filter((section) => section.component !== 'mod_subsection'); + await this.initializeExpandedSections(); - this.treatSections(this.sections); + await this.treatSections(this.sections); } + this.changeDetectorRef.markForCheck(); } @@ -249,7 +248,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * Get the components classes. */ protected async getComponents(): Promise { - if (!this.course || this.course.format == this.lastCourseFormat) { + if (!this.course || this.course.format === this.lastCourseFormat) { return; } @@ -326,35 +325,25 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.loaded = true; this.sectionChanged(sections[0]); } else if (this.initialSectionId || this.initialSectionNumber !== undefined) { - const subSection = this.subSections.find((section) => section.id === this.initialSectionId || - (section.section !== undefined && section.section === this.initialSectionNumber)); - if (subSection) { - // The section is a subsection, load the parent section. - this.sections.some((section) => { - const module = section.modules.find((module) => - subSection.itemid === module.instance && module.modname === 'subsection'); - if (module) { - this.initialSectionId = module.section; - this.initialSectionNumber = undefined; + // We have an input indicating the section ID to load. Search the section. + const { section, parents } = CoreCourseHelper.findSection(this.sections, { + id: this.initialSectionId, + num: this.initialSectionNumber, + }); - return true; - } - - return false; - }); + if (parents.length) { + // The section is a subsection, load the root section. + this.initialSectionId = parents[0].id; + this.initialSectionNumber = undefined; this.setInputData(); } - // We have an input indicating the section ID to load. Search the section. - const section = sections.find((section) => - 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)) { + const sectionToLoad = parents[0] ?? section; + if (sectionToLoad && this.canViewSection(sectionToLoad)) { this.loaded = true; - this.sectionChanged(section); + this.sectionChanged(sectionToLoad); } } else if (this.initialBlockInstanceId && this.displayBlocks && this.hasBlocks) { const { CoreBlockSideBlocksComponent } = await import('@features/block/components/side-blocks/side-blocks'); @@ -374,21 +363,22 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { // No section specified, not found or not visible, load current section or the section with last module viewed. const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections); - const lastModuleViewed = this.lastModuleViewed; let section = currentSectionData.section; let moduleId: number | undefined; // If all sections is not preferred, load the last viewed module section. - if (!allSectionsPreferred && lastModuleViewed) { + if (!allSectionsPreferred && this.lastModuleViewed) { if (!currentSectionData.forceSelected) { // Search the section with the last module viewed. - const lastModuleSection = this.getViewedModuleSection(sections, lastModuleViewed); - + const lastModuleSection = this.getViewedModuleSection(); section = lastModuleSection || section; - moduleId = lastModuleSection ? lastModuleViewed?.cmId : undefined; - } else if (currentSectionData.section.modules.some(module => module.id === lastModuleViewed.cmId)) { - // Last module viewed is inside the highlighted section. - moduleId = lastModuleViewed.cmId; + moduleId = lastModuleSection ? this.lastModuleViewed.cmId : undefined; + } else { + const modules = CoreCourse.getSectionsModules([currentSectionData.section]); + if (modules.some(module => module.id === this.lastModuleViewed?.cmId)) { + // Last module viewed is inside the highlighted section. + moduleId = this.lastModuleViewed.cmId; + } } } @@ -413,37 +403,29 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.viewedModules[entry.cmId] = true; }); - if (this.lastModuleViewed) { - const section = this.getViewedModuleSection(this.sections, this.lastModuleViewed); - if (section) { - this.setSectionExpanded(section); - } + const lastViewedSection = this.getViewedModuleSection(); + if (lastViewedSection) { + this.setSectionExpanded(lastViewedSection); } } /** - * Get the section of a viewed module. + * Get the section of a viewed module. If the module is in a subsection, returns the root section. * - * @param sections List of sections. - * @param viewedModule Viewed module. * @returns Section, undefined if not found. */ - protected getViewedModuleSection( - sections: CoreCourseSection[], - viewedModule: CoreCourseViewedModulesDBRecord, - ): CoreCourseSection | undefined { - let lastModuleSection: CoreCourseSection | undefined; - - if (viewedModule.sectionId) { - lastModuleSection = sections.find(section => section.id === viewedModule.sectionId); + protected getViewedModuleSection(): CoreCourseSection | undefined { + if (!this.lastModuleViewed) { + return; } - if (!lastModuleSection) { - // No sectionId or section not found. Search the module. - lastModuleSection = sections.find(section => section.modules.some(module => module.id === viewedModule.cmId)); - } + const { section, parents } = CoreCourseHelper.findSection(this.sections, { + id: this.lastModuleViewed.sectionId, + moduleId: this.lastModuleViewed.cmId, + }); + const lastModuleSection: CoreCourseSection | undefined = parents[0] ?? section; - return lastModuleSection && lastModuleSection.id !== this.stealthModulesSectionId ? lastModuleSection : undefined; + return lastModuleSection?.id !== this.stealthModulesSectionId ? lastModuleSection : undefined; } /** @@ -488,7 +470,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { componentProps: { course: this.course, sections: this.sections, - subSections: this.subSections, selectedId: selectedId, }, }); @@ -496,29 +477,32 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { if (!data) { return; } - let section = this.sections.find((section) => section.id === data.sectionId); - if (!section) { - return; - } - this.sectionChanged(section); - if (data.subSectionId) { - section = this.subSections.find((section) => section.id === data.subSectionId); - if (!section) { - return; + const { section, parents } = CoreCourseHelper.findSection(this.sections, { + moduleId: data.moduleId, + id: data.moduleId === undefined ? data.sectionId : undefined, + }); + + // Select the root section. + this.sectionChanged(parents[0] ?? section); + + if (parents.length && section) { + // It's a subsection. Expand all the parents and the subsection. + for (let i = 1; i < parents.length; i++) { + this.setSectionExpanded(parents[i]); } - // Use this section to find the module. this.setSectionExpanded(section); // Scroll to the subsection (later it may be scrolled to the module). this.scrollInCourse(section.id, true); } - if (!data.moduleId) { + if (!data.moduleId || !section) { return; } - const module = section.modules.find((module) => module.id === data.moduleId); + const module = + section.contents.find((module) => sectionContentIsModule(module) && module.id === data.moduleId); if (!module) { return; } @@ -541,14 +525,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * Open course downloads page. */ async gotoCourseDownloads(): Promise { - const selectedId = await this.getSelectedSectionId(); + const sectionId = this.selectedSection?.id !== this.allSectionsId ? this.selectedSection?.id : undefined; CoreNavigator.navigateToSitePath( `storage/${this.course.id}`, { params: { title: this.course.fullname, - sectionId: selectedId, + sectionId, isGuest: this.isGuest, }, }, @@ -676,12 +660,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { // Skip sections without content, with stealth modules or collapsed. if (!this.sections[this.lastShownSectionIndex].hasContent || - !this.sections[this.lastShownSectionIndex].modules || + !this.sections[this.lastShownSectionIndex].contents || !this.sections[this.lastShownSectionIndex].expanded) { continue; } - modulesLoaded += this.sections[this.lastShownSectionIndex].modules.reduce((total, module) => + const sectionModules = CoreCourse.getSectionsModules([this.sections[this.lastShownSectionIndex]]); + + modulesLoaded += sectionModules.reduce((total, module) => !CoreCourseHelper.isModuleStealth(module, this.sections[this.lastShownSectionIndex]) ? total + 1 : total, 0); } @@ -782,9 +768,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * Save expanded sections for the course. */ protected async saveExpandedSections(): Promise { - let expandedSections = this.sections.filter((section) => section.expanded && section.id > 0).map((section) => section.id); - expandedSections = - expandedSections.concat(this.subSections.filter((section) => section.expanded).map((section) => section.id)); + const expandedSections = CoreCourseHelper.flattenSections(this.sections) + .filter((section) => section.expanded && section.id > 0).map((section) => section.id); await this.currentSite?.setLocalSiteConfig( `${COURSE_EXPANDED_SECTIONS_PREFIX}${this.course.id}`, @@ -800,14 +785,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.currentSite?.getLocalSiteConfig(`${COURSE_EXPANDED_SECTIONS_PREFIX}${this.course.id}`), ); - // Expand all sections if not defined. if (expandedSections === undefined) { - this.sections.forEach((section) => { - section.expanded = true; - this.accordionMultipleValue.push(section.id.toString()); - }); + this.accordionMultipleValue = []; - this.subSections.forEach((section) => { + // Expand all sections if not defined. + CoreCourseHelper.flattenSections(this.sections).forEach((section) => { section.expanded = true; this.accordionMultipleValue.push(section.id.toString()); }); @@ -817,11 +799,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.accordionMultipleValue = expandedSections.split(','); - this.sections.forEach((section) => { - section.expanded = this.accordionMultipleValue.includes(section.id.toString()); - }); - - this.subSections.forEach((section) => { + CoreCourseHelper.flattenSections(this.sections).forEach((section) => { section.expanded = this.accordionMultipleValue.includes(section.id.toString()); }); } @@ -833,21 +811,13 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { */ accordionMultipleChange(ev: AccordionGroupChangeEventDetail): void { const sectionIds = ev.value as string[] | undefined; - this.sections.forEach((section) => { - section.expanded = false; - }); - - this.subSections.forEach((section) => { + const allSections = CoreCourseHelper.flattenSections(this.sections); + allSections.forEach((section) => { section.expanded = false; }); sectionIds?.forEach((sectionId) => { - const sId = Number(sectionId); - let section = this.sections.find((section) => section.id === sId); - if (!section) { - section = this.subSections.find((section) => section.id === sId); - } - + const section = allSections.find((section) => section.id === Number(sectionId)); if (section) { section.expanded = true; } diff --git a/src/core/features/course/components/course-index/course-index.html b/src/core/features/course/components/course-index/course-index.html index 8c46c32c8..14b611e81 100644 --- a/src/core/features/course/components/course-index/course-index.html +++ b/src/core/features/course/components/course-index/course-index.html @@ -56,30 +56,31 @@
- - @if (module.subSection) { + + @if (!isModule(modOrSubsection)) {
- +
} @else { - - } diff --git a/src/core/features/course/components/course-index/course-index.ts b/src/core/features/course/components/course-index/course-index.ts index 4de09de50..b1c3c6150 100644 --- a/src/core/features/course/components/course-index/course-index.ts +++ b/src/core/features/course/components/course-index/course-index.ts @@ -18,6 +18,7 @@ import { CoreCourse, CoreCourseModuleCompletionStatus, CoreCourseProvider, + sectionContentIsModule, } from '@features/course/services/course'; import { CoreCourseHelper, CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper'; import { CoreCourseFormatCurrentSectionData, CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; @@ -43,7 +44,6 @@ import { CoreDom } from '@singletons/dom'; export class CoreCourseCourseIndexComponent implements OnInit { @Input() sections: CoreCourseSection[] = []; - @Input() subSections: CoreCourseSection[] = []; @Input() selectedId?: number; @Input() course?: CoreCourseAnyCourseData; @@ -51,6 +51,7 @@ export class CoreCourseCourseIndexComponent implements OnInit { highlighted?: string; sectionsToRender: CourseIndexSection[] = []; loaded = false; + isModule = sectionContentIsModule; constructor( protected elementRef: ElementRef, @@ -88,7 +89,7 @@ export class CoreCourseCourseIndexComponent implements OnInit { const enableIndentation = await CoreCourse.isCourseIndentationEnabled(site, this.course.id); this.sectionsToRender = this.sections - .filter((section) => section.component !== 'mod_subsection' && !CoreCourseHelper.isSectionStealth(section)) + .filter((section) => !CoreCourseHelper.isSectionStealth(section)) .map((section) => this.mapSectionToRender(section, completionEnabled, enableIndentation, currentSectionData)); this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course); @@ -134,26 +135,7 @@ export class CoreCourseCourseIndexComponent implements OnInit { * @param moduleId Selected module id, if any. */ selectSectionOrModule(event: Event, sectionId: number, moduleId?: number): void { - let subSectionId: number | undefined; - this.sectionsToRender.some((section) => { - if (section.id === sectionId) { - return true; - } - - return section.modules.some((module) => { - if (module.subSection?.id === sectionId) { - // Always use the parent section. - subSectionId = sectionId; - sectionId = section.id; - - return true; - } - - return false; - }); - }); - - ModalController.dismiss({ event, sectionId, subSectionId, moduleId }); + ModalController.dismiss( { event, sectionId, moduleId }); } /** @@ -192,37 +174,26 @@ export class CoreCourseCourseIndexComponent implements OnInit { enableIndentation: boolean, currentSectionData?: CoreCourseFormatCurrentSectionData, ): CourseIndexSection { - const modules = section.modules - .filter((module) => module.modname === 'subsection' || this.renderModule(section, module)) - .map((module) => { - if (module.modname === 'subsection') { - const subSectionFound = this.subSections.find((subSection) => subSection.itemid === module.instance); - const subSection = subSectionFound - ? this.mapSectionToRender(subSectionFound, completionEnabled, enableIndentation) - : undefined; - - return { - id: module.id, - name: module.name, - course: module.course, - visible: !!module.visible, - uservisible: CoreCourseHelper.canUserViewModule(module, section), - indented: true, - subSection, - }; + const contents = section.contents + .filter((modOrSubsection) => + !sectionContentIsModule(modOrSubsection) || this.renderModule(section, modOrSubsection)) + .map((modOrSubsection) => { + if (!sectionContentIsModule(modOrSubsection)) { + return this.mapSectionToRender(modOrSubsection, completionEnabled, enableIndentation); } const completionStatus = completionEnabled - ? CoreCourseHelper.getCompletionStatus(module.completiondata) + ? CoreCourseHelper.getCompletionStatus(modOrSubsection.completiondata) : undefined; return { - id: module.id, - name: module.name, - course: module.course, - visible: !!module.visible, - uservisible: CoreCourseHelper.canUserViewModule(module, section), - indented: enableIndentation && module.indent > 0, + id: modOrSubsection.id, + name: modOrSubsection.name, + modname: modOrSubsection.modname, + course: modOrSubsection.course, + visible: !!modOrSubsection.visible, + uservisible: CoreCourseHelper.canUserViewModule(modOrSubsection, section), + indented: enableIndentation && modOrSubsection.indent > 0, completionStatus, }; }); @@ -235,8 +206,8 @@ export class CoreCourseCourseIndexComponent implements OnInit { uservisible: CoreCourseHelper.canUserViewSection(section), expanded: section.id === this.selectedId, highlighted: currentSectionData?.section.id === section.id, - hasVisibleModules: modules.length > 0, - modules, + hasVisibleModules: contents.length > 0, + contents, }; } @@ -251,20 +222,21 @@ type CourseIndexSection = { availabilityinfo: boolean; visible: boolean; uservisible: boolean; - modules: { - id: number; - course: number; - visible: boolean; - indented: boolean; - uservisible: boolean; - completionStatus?: CoreCourseModuleCompletionStatus; - subSection?: CourseIndexSection; - }[]; + contents: (CourseIndexSection | CourseIndexModule)[]; +}; + +type CourseIndexModule = { + id: number; + modname: string; + course: number; + visible: boolean; + indented: boolean; + uservisible: boolean; + completionStatus?: CoreCourseModuleCompletionStatus; }; export type CoreCourseIndexSectionWithModule = { event: Event; sectionId: number; - subSectionId?: number; moduleId?: number; }; diff --git a/src/core/features/course/components/course-section/course-section.html b/src/core/features/course/components/course-section/course-section.html index c9b9f89ac..b64ac8617 100644 --- a/src/core/features/course/components/course-section/course-section.html +++ b/src/core/features/course/components/course-section/course-section.html @@ -53,16 +53,16 @@ - - @if (module.subSection) { - + @if (!isModule(modOrSubsection)) { + } @else { - + [isLastViewed]="lastModuleViewed && lastModuleViewed.cmId === modOrSubsection.id" [class.core-course-module-not-viewed]=" + !viewedModules[modOrSubsection.id] && + (!modOrSubsection.completiondata || modOrSubsection.completiondata.state === completionStatusIncomplete)" /> } diff --git a/src/core/features/course/components/course-section/course-section.ts b/src/core/features/course/components/course-section/course-section.ts index 1c3b9ecae..ff7968d84 100644 --- a/src/core/features/course/components/course-section/course-section.ts +++ b/src/core/features/course/components/course-section/course-section.ts @@ -15,12 +15,9 @@ import { Component, HostBinding, Input, - OnChanges, OnInit, - SimpleChange, } from '@angular/core'; import { - CoreCourseModuleData, CoreCourseSection, } from '@features/course/services/course-helper'; import { CoreSharedModule } from '@/core/shared.module'; @@ -28,7 +25,7 @@ import { CoreCourseComponentsModule } from '../components.module'; import { toBoolean } from '@/core/transforms/boolean'; import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course'; -import { CoreCourseModuleCompletionStatus } from '@features/course/services/course'; +import { CoreCourseModuleCompletionStatus, sectionContentIsModule } from '@features/course/services/course'; import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; /** @@ -44,11 +41,10 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg CoreCourseComponentsModule, ], }) -export class CoreCourseSectionComponent implements OnInit, OnChanges { +export class CoreCourseSectionComponent implements OnInit { @Input({ required: true }) course!: CoreCourseAnyCourseData; // The course to render. @Input({ required: true }) section!: CoreCourseSectionToDisplay; - @Input() subSections: CoreCourseSectionToDisplay[] = []; // List of subsections in the course. @Input({ transform: toBoolean }) collapsible = true; // Whether the section can be collapsed. @Input() lastModuleViewed?: CoreCourseViewedModulesDBRecord; @Input() viewedModules: Record = {}; @@ -58,9 +54,9 @@ export class CoreCourseSectionComponent implements OnInit, OnChanges { return this.collapsible ? 'collapsible' : 'non-collapsible'; } - modules: CoreCourseModuleToDisplay[] = []; completionStatusIncomplete = CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE; highlightedName?: string; // Name to highlight. + isModule = sectionContentIsModule; /** * @inheritdoc @@ -71,28 +67,8 @@ export class CoreCourseSectionComponent implements OnInit, OnChanges { : undefined; } - /** - * @inheritdoc - */ - ngOnChanges(changes: { [name: string]: SimpleChange }): void { - if (changes.section && this.section) { - this.modules = this.section.modules; - - this.modules.forEach((module) => { - if (module.modname === 'subsection') { - module.subSection = this.subSections.find((section) => - section.component === 'mod_subsection' && section.itemid === module.instance); - } - }); - } - } - } -type CoreCourseModuleToDisplay = CoreCourseModuleData & { - subSection?: CoreCourseSectionToDisplay; -}; - export type CoreCourseSectionToDisplay = CoreCourseSection & { highlighted?: boolean; expanded?: boolean; // The aim of this property is to avoid DOM overloading. diff --git a/src/core/features/course/components/module-navigation/module-navigation.ts b/src/core/features/course/components/module-navigation/module-navigation.ts index 1023ccfda..75aa69089 100644 --- a/src/core/features/course/components/module-navigation/module-navigation.ts +++ b/src/core/features/course/components/module-navigation/module-navigation.ts @@ -42,8 +42,6 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy { nextModule?: CoreCourseModuleData; previousModule?: CoreCourseModuleData; - nextModuleSection?: CoreCourseWSSection; - previousModuleSection?: CoreCourseWSSection; loaded = false; element: HTMLElement; @@ -104,46 +102,23 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy { const sections = await CoreCourse.getSections(this.courseId, false, true, preSets); - // Search the next module. - let currentModuleIndex = -1; - - const currentSectionIndex = sections.findIndex((section) => { - if (!this.isSectionAvailable(section)) { - // User cannot view the section, skip it. - return false; - } - - currentModuleIndex = section.modules.findIndex((module: CoreCourseModuleData) => module.id == this.currentModuleId); - - return currentModuleIndex >= 0; + const modules = await CoreCourse.getSectionsModules(sections, { + ignoreSection: (section) => !this.isSectionAvailable(section), }); - if (currentSectionIndex < 0) { - // Nothing found. Return. - + const currentModuleIndex = modules.findIndex((module) => module.id === this.currentModuleId); + if (currentModuleIndex < 0) { + // Current module found. Return. return; } if (checkNext) { // Find next Module. this.nextModule = undefined; - for (let i = currentSectionIndex; i < sections.length && this.nextModule == undefined; i++) { - const section = sections[i]; - - if (!this.isSectionAvailable(section)) { - // User cannot view the section, skip it. - continue; - } - - const startModule = i == currentSectionIndex ? currentModuleIndex + 1 : 0; - for (let j = startModule; j < section.modules.length && this.nextModule == undefined; j++) { - const module = section.modules[j]; - - const found = await this.isModuleAvailable(module); - if (found) { - this.nextModule = module; - this.nextModuleSection = section; - } + for (let i = currentModuleIndex + 1; i < modules.length && this.nextModule === undefined; i++) { + const module = modules[i]; + if (this.isModuleAvailable(module)) { + this.nextModule = module; } } } @@ -151,23 +126,10 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy { if (checkPrevious) { // Find previous Module. this.previousModule = undefined; - for (let i = currentSectionIndex; i >= 0 && this.previousModule == undefined; i--) { - const section = sections[i]; - - if (!this.isSectionAvailable(section)) { - // User cannot view the section, skip it. - continue; - } - - const startModule = i == currentSectionIndex ? currentModuleIndex - 1 : section.modules.length - 1; - for (let j = startModule; j >= 0 && this.previousModule == undefined; j--) { - const module = section.modules[j]; - - const found = await this.isModuleAvailable(module); - if (found) { - this.previousModule = module; - this.previousModuleSection = section; - } + for (let i = currentModuleIndex - 1; i >= 0 && this.previousModule === undefined; i--) { + const module = modules[i]; + if (this.isModuleAvailable(module)) { + this.previousModule = module; } } } @@ -181,8 +143,8 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy { * @param module Module to check. * @returns Wether the module is available to the user or not. */ - protected async isModuleAvailable(module: CoreCourseModuleData): Promise { - return !CoreCourseHelper.isModuleStealth(module) && CoreCourse.instance.moduleHasView(module); + protected isModuleAvailable(module: CoreCourseModuleData): boolean { + return !CoreCourseHelper.isModuleStealth(module) && CoreCourse.moduleHasView(module); } /** @@ -229,10 +191,8 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy { }; if (!CoreCourseHelper.canUserViewModule(module)) { - const section = next ? this.nextModuleSection : this.previousModuleSection; options.params = { module, - section, }; CoreNavigator.navigateToSitePath('course/' + this.courseId + '/' + module.id +'/module-preview', options); } else { diff --git a/src/core/features/course/components/module-summary/module-summary.ts b/src/core/features/course/components/module-summary/module-summary.ts index a14798a75..910a892de 100644 --- a/src/core/features/course/components/module-summary/module-summary.ts +++ b/src/core/features/course/components/module-summary/module-summary.ts @@ -142,11 +142,7 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy { return; } - const moduleSize = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(this.module, this.courseId); - - if (moduleSize) { - this.size = moduleSize; - } + this.size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(this.module, this.courseId); }, 1000); this.fileStatusObserver = CoreEvents.on( diff --git a/src/core/features/course/format/singleactivity/components/singleactivity.ts b/src/core/features/course/format/singleactivity/components/singleactivity.ts index 0cfe37c73..0ee615b82 100644 --- a/src/core/features/course/format/singleactivity/components/singleactivity.ts +++ b/src/core/features/course/format/singleactivity/components/singleactivity.ts @@ -18,7 +18,7 @@ import { CoreCourseModuleDelegate } from '@features/course/services/module-deleg import { CoreCourseUnsupportedModuleComponent } from '@features/course/components/unsupported-module/unsupported-module'; import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; -import { CoreCourseModuleCompletionData, CoreCourseSection } from '@features/course/services/course-helper'; +import { CoreCourseModuleCompletionData, CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper'; import { CoreCourse } from '@features/course/services/course'; import type { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component'; @@ -59,7 +59,7 @@ export class CoreCourseFormatSingleActivityComponent implements OnChanges { } // In single activity the module should only have 1 section and 1 module. Get the module. - const module = this.sections?.[0].modules?.[0]; + const module = this.sections?.[0].contents?.[0] as (CoreCourseModuleData | undefined); this.data.courseId = this.course.id; this.data.module = module; diff --git a/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts b/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts index 999a1c08c..2f6d98401 100644 --- a/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts +++ b/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts @@ -55,8 +55,8 @@ export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseF * @inheritdoc */ getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseWSSection[]): string { - if (sections?.[0]?.modules?.[0]) { - return sections[0].modules[0].name; + if (sections?.[0]?.contents?.[0]) { + return sections[0].contents[0].name; } return course.fullname || ''; @@ -73,8 +73,8 @@ export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseF * @inheritdoc */ displayRefresher(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): boolean { - if (sections?.[0]?.modules?.[0]) { - return CoreCourseModuleDelegate.displayRefresherInSingleActivity(sections[0].modules[0].modname); + if (sections?.[0]?.contents?.[0] && 'modname' in sections[0].contents[0]) { + return CoreCourseModuleDelegate.displayRefresherInSingleActivity(sections[0].contents[0].modname); } else { return true; } diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts index b4e5c50d7..99148c0e9 100644 --- a/src/core/features/course/pages/contents/contents.ts +++ b/src/core/features/course/pages/contents/contents.ts @@ -216,10 +216,11 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon protected async loadSections(refresh?: boolean): Promise { // Get all the sections. const sections = await CoreCourse.getSections(this.course.id, false, true); + let modules: CoreCourseModuleData[] | undefined; if (refresh) { // Invalidate the recently downloaded module list. To ensure info can be prefetched. - const modules = CoreCourse.getSectionsModules(sections); + modules = CoreCourse.getSectionsModules(sections); await CoreCourseModulePrefetchDelegate.invalidateModules(modules, this.course.id); } @@ -228,9 +229,11 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon // Get the completion status. if (CoreCoursesHelper.isCompletionEnabledInCourse(this.course)) { - const sectionWithModules = sections.find((section) => section.modules.length > 0); + if (!modules) { + modules = CoreCourse.getSectionsModules(sections); + } - if (sectionWithModules && sectionWithModules.modules[0].completion !== undefined) { + if (modules[0]?.completion !== undefined) { // The module already has completion (3.6 onwards). Load the offline completion. this.modulesHaveCompletion = true; @@ -335,8 +338,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon if (this.sections) { // If the completion value is not used, the page won't be reloaded, so update the progress bar. - const completionModules = ( []) - .concat(...this.sections.map((section) => section.modules)) + const completionModules = CoreCourse.getSectionsModules(this.sections) .map((module) => module.completion && module.completion > 0 ? 1 : module.completion) .reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0); diff --git a/src/core/features/course/pages/list-mod-type/list-mod-type.html b/src/core/features/course/pages/list-mod-type/list-mod-type.html index b7358a4d1..ab5a61b1e 100644 --- a/src/core/features/course/pages/list-mod-type/list-mod-type.html +++ b/src/core/features/course/pages/list-mod-type/list-mod-type.html @@ -18,21 +18,31 @@ - - - -

- -

-
-
- - - -
+ + +
+ + + + + +

+ +

+
+
+ + @if (isModule(modOrSubsection)) { + + } @else { + + } + +
+
diff --git a/src/core/features/course/pages/list-mod-type/list-mod-type.ts b/src/core/features/course/pages/list-mod-type/list-mod-type.ts index d93fa4788..d4626e320 100644 --- a/src/core/features/course/pages/list-mod-type/list-mod-type.ts +++ b/src/core/features/course/pages/list-mod-type/list-mod-type.ts @@ -15,7 +15,7 @@ import { Component, OnInit } from '@angular/core'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreCourse } from '@features/course/services/course'; +import { CoreCourse, CoreCourseWSSection, sectionContentIsModule } from '@features/course/services/course'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper'; import { CoreNavigator } from '@services/navigator'; @@ -30,6 +30,10 @@ import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; @Component({ selector: 'page-core-course-list-mod-type', templateUrl: 'list-mod-type.html', + styles: `core-course-module:last-child { + --activity-border: 0px; + --card-padding-bottom: 0px; + }`, }) export class CoreCourseListModTypePage implements OnInit { @@ -41,6 +45,7 @@ export class CoreCourseListModTypePage implements OnInit { courseId = 0; canLoadMore = false; lastShownSectionIndex = -1; + isModule = sectionContentIsModule; protected modName?: string; protected archetypes: Record = {}; // To speed up the check of modules. @@ -97,40 +102,7 @@ export class CoreCourseListModTypePage implements OnInit { // Get all the modules in the course. let sections = await CoreCourse.getSections(this.courseId, false, true); - sections = sections.filter((section) => { - if (!section.modules.length || section.hiddenbynumsections) { - return false; - } - - section.modules = section.modules.filter((mod) => { - if (!CoreCourseHelper.canUserViewModule(mod, section) || - !CoreCourse.moduleHasView(mod) || - mod.visibleoncoursepage === 0) { - // Ignore this module. - return false; - } - - if (this.modName === 'resources') { - // Check that the module is a resource. - if (this.archetypes[mod.modname] === undefined) { - this.archetypes[mod.modname] = CoreCourseModuleDelegate.supportsFeature( - mod.modname, - CoreConstants.FEATURE_MOD_ARCHETYPE, - CoreConstants.MOD_ARCHETYPE_OTHER, - ); - } - - if (this.archetypes[mod.modname] === CoreConstants.MOD_ARCHETYPE_RESOURCE) { - return true; - } - - } else if (mod.modname === this.modName) { - return true; - } - }); - - return section.modules.length > 0; - }); + sections = this.filterSectionsAndContents(sections); const result = await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId); @@ -143,6 +115,56 @@ export class CoreCourseListModTypePage implements OnInit { } } + /** + * Given a list of sections, return only those with contents to display. Also filter the contents to only include + * the ones that should be displayed. + * + * @param sections Sections. + * @returns Filtered sections. + */ + protected filterSectionsAndContents(sections: CoreCourseWSSection[]): CoreCourseWSSection[] { + return sections.filter((section) => { + if (!section.contents.length || section.hiddenbynumsections) { + return false; + } + + section.contents = section.contents.filter((modOrSubsection) => { + if (!sectionContentIsModule(modOrSubsection)) { + const formattedSections = this.filterSectionsAndContents([modOrSubsection]); + + return !!formattedSections.length; + } + + if (!CoreCourseHelper.canUserViewModule(modOrSubsection, section) || + !CoreCourse.moduleHasView(modOrSubsection) || + modOrSubsection.visibleoncoursepage === 0) { + // Ignore this module. + return false; + } + + if (this.modName === 'resources') { + // Check that the module is a resource. + if (this.archetypes[modOrSubsection.modname] === undefined) { + this.archetypes[modOrSubsection.modname] = CoreCourseModuleDelegate.supportsFeature( + modOrSubsection.modname, + CoreConstants.FEATURE_MOD_ARCHETYPE, + CoreConstants.MOD_ARCHETYPE_OTHER, + ); + } + + if (this.archetypes[modOrSubsection.modname] === CoreConstants.MOD_ARCHETYPE_RESOURCE) { + return true; + } + + } else if (modOrSubsection.modname === this.modName) { + return true; + } + }); + + return section.contents.length > 0; + }); + } + /** * Show more activities. * @@ -153,7 +175,8 @@ export class CoreCourseListModTypePage implements OnInit { while (this.lastShownSectionIndex < this.sections.length - 1 && modulesLoaded < CoreCourseListModTypePage.PAGE_LENGTH) { this.lastShownSectionIndex++; - modulesLoaded += this.sections[this.lastShownSectionIndex].modules.length; + const sectionModules = CoreCourse.getSectionsModules([this.sections[this.lastShownSectionIndex]]); + modulesLoaded += sectionModules.length; } this.canLoadMore = this.lastShownSectionIndex < this.sections.length - 1; diff --git a/src/core/features/course/pages/module-preview/module-preview.ts b/src/core/features/course/pages/module-preview/module-preview.ts index c47db58f3..c78607e06 100644 --- a/src/core/features/course/pages/module-preview/module-preview.ts +++ b/src/core/features/course/pages/module-preview/module-preview.ts @@ -15,7 +15,7 @@ import { Component, OnInit } from '@angular/core'; import { CoreCourseModuleSummaryResult } from '@features/course/components/module-summary/module-summary'; import { CoreCourse } from '@features/course/services/course'; -import { CoreCourseHelper, CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper'; +import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { CoreModals } from '@services/modals'; import { CoreNavigator } from '@services/navigator'; @@ -35,7 +35,6 @@ export class CoreCourseModulePreviewPage implements OnInit { title!: string; module!: CoreCourseModuleData; - section?: CoreCourseSection; // The section the module belongs to. courseId!: number; loaded = false; unsupported = false; @@ -52,7 +51,6 @@ export class CoreCourseModulePreviewPage implements OnInit { try { this.module = CoreNavigator.getRequiredRouteParam('module'); this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); - this.section = CoreNavigator.getRouteParam('section'); } catch (error) { CoreDomUtils.showErrorModal(error); diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 11ae811e7..026fb7e88 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -27,6 +27,8 @@ import { CoreCourseModuleCompletionTracking, CoreCourseModuleCompletionStatus, CoreCourseGetContentsWSModule, + sectionContentIsModule, + CoreCourseAnyModuleData, } from './course'; import { CoreConstants, DownloadStatus, ContextLevel } from '@/core/constants'; import { CoreLogger } from '@singletons/logger'; @@ -166,50 +168,56 @@ export class CoreCourseHelperProvider { let hasContent = false; - const formattedSections = await Promise.all( - sections.map>(async (courseSection) => { - const section = { - ...courseSection, - hasContent: this.sectionHasContent(courseSection), - }; + const treatSection = async (sectionToTreat: CoreCourseWSSection): Promise => { + const section = { + ...sectionToTreat, + hasContent: this.sectionHasContent(sectionToTreat), + }; - if (!section.hasContent) { - return section; + if (!section.hasContent) { + return section; + } + + hasContent = true; + + section.contents = await Promise.all(section.contents.map(async (module) => { + if (!sectionContentIsModule(module)) { + return await treatSection(module); } - hasContent = true; + module.handlerData = await CoreCourseModuleDelegate.getModuleDataFor( + module.modname, + module, + courseId, + section.id, + forCoursePage, + ); - await Promise.all(section.modules.map(async (module) => { - module.handlerData = await CoreCourseModuleDelegate.getModuleDataFor( - module.modname, - module, + if (!module.completiondata && completionStatus && completionStatus[module.id] !== undefined) { + // Should not happen on > 3.6. Check if activity has completions and if it's marked. + const activityStatus = completionStatus[module.id]; + + module.completiondata = { + state: activityStatus.state, + timecompleted: activityStatus.timecompleted, + overrideby: activityStatus.overrideby || 0, + valueused: activityStatus.valueused, + tracking: activityStatus.tracking, courseId, - section.id, - forCoursePage, - ); + cmid: module.id, + }; + } - if (!module.completiondata && completionStatus && completionStatus[module.id] !== undefined) { - // Should not happen on > 3.6. Check if activity has completions and if it's marked. - const activityStatus = completionStatus[module.id]; + // Check if the module is stealth. + module.isStealth = CoreCourseHelper.isModuleStealth(module, section); - module.completiondata = { - state: activityStatus.state, - timecompleted: activityStatus.timecompleted, - overrideby: activityStatus.overrideby || 0, - valueused: activityStatus.valueused, - tracking: activityStatus.tracking, - courseId, - cmid: module.id, - }; - } + return module; + })); - // Check if the module is stealth. - module.isStealth = CoreCourseHelper.isModuleStealth(module, section); - })); + return section; + }; - return section; - }), - ); + const formattedSections = await Promise.all(sections.map((courseSection) => treatSection(courseSection))); return { hasContent, sections: formattedSections }; } @@ -218,7 +226,8 @@ export class CoreCourseHelperProvider { * Module is stealth. * * @param module Module to check. - * @param section Section to check. + * @param section Section to check. If the module belongs to a subsection, you can pass either the subsection or the parent + * section. Subsections inherit the visibility from their parent section. * @returns Wether the module is stealth. */ isModuleStealth(module: CoreCourseModuleData, section?: CoreCourseWSSection): boolean { @@ -230,7 +239,8 @@ export class CoreCourseHelperProvider { * Module is visible by the user. * * @param module Module to check. - * @param section Section to check. Omitted if not defined. + * @param section Section to check. Omitted if not defined. If the module belongs to a subsection, you can pass either the + * subsection or the parent section. Subsections inherit the visibility from their parent section. * @returns Wether the section is visible by the user. */ canUserViewModule(module: CoreCourseModuleData, section?: CoreCourseWSSection): boolean { @@ -273,15 +283,15 @@ export class CoreCourseHelperProvider { refresh?: boolean, checkUpdates: boolean = true, ): Promise<{statusData: CoreCourseModulesStatus; section: CoreCourseSectionWithStatus}> { - if (section.id == CoreCourseProvider.ALL_SECTIONS_ID) { + if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) { throw new CoreError('Invalid section'); } - const sectionWithStatus = section; + // Get the status of this section based on their modules. + const { modules, subsections } = CoreCourse.classifyContents(section.contents); - // Get the status of this section. - const result = await CoreCourseModulePrefetchDelegate.getModulesStatus( - section.modules, + const statusData = await CoreCourseModulePrefetchDelegate.getModulesStatus( + modules, courseId, section.id, refresh, @@ -289,83 +299,35 @@ export class CoreCourseHelperProvider { checkUpdates, ); + // Now calculate status of subsections, and add them to the status data. Each subsection counts as 1 item in the section. + await Promise.all(subsections.map(async (subsection) => { + const subsectionStatus = await this.calculateSectionStatus(subsection, courseId, refresh, checkUpdates); + statusData.total++; + statusData.status = CoreFilepool.determinePackagesStatus(statusData.status, subsectionStatus.statusData.status); + })); + // Check if it's being downloaded. const downloadId = this.getSectionDownloadId(section); if (CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) { - result.status = DownloadStatus.DOWNLOADING; + statusData.status = DownloadStatus.DOWNLOADING; } - sectionWithStatus.downloadStatus = result.status; + const sectionWithStatus = section; + sectionWithStatus.downloadStatus = statusData.status; // Set this section data. - if (result.status !== DownloadStatus.DOWNLOADING) { + if (statusData.status !== DownloadStatus.DOWNLOADING) { sectionWithStatus.isDownloading = false; - sectionWithStatus.total = 0; + this.resetSectionDownloadCount(section); } else { // Section is being downloaded. sectionWithStatus.isDownloading = true; CoreCourseModulePrefetchDelegate.setOnProgress(downloadId, (data) => { - sectionWithStatus.count = data.count; - sectionWithStatus.total = data.total; + this.setSectionDownloadCount(sectionWithStatus, data.count, data.total); }); } - return { statusData: result, section: sectionWithStatus }; - } - - /** - * Calculate the status of a list of sections, setting attributes to determine the icons/data to be shown. - * - * @param sections Sections to calculate their status. - * @param courseId Course ID the sections belong to. - * @param refresh True if it shouldn't use module status cache (slower). - * @param checkUpdates Whether to use the WS to check updates. Defaults to true. - * @returns Promise resolved when the states are calculated. - */ - async calculateSectionsStatus( - sections: CoreCourseSection[], - courseId: number, - refresh?: boolean, - checkUpdates: boolean = true, - ): Promise { - let allSectionsSection: CoreCourseSectionWithStatus | undefined; - let allSectionsStatus = DownloadStatus.NOT_DOWNLOADABLE as DownloadStatus; - - const promises = sections.map(async (section: CoreCourseSectionWithStatus) => { - section.isCalculating = true; - - if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) { - // "All sections" section status is calculated using the status of the rest of sections. - allSectionsSection = section; - - return; - } - - try { - const result = await this.calculateSectionStatus(section, courseId, refresh, checkUpdates); - - // Calculate "All sections" status. - allSectionsStatus = CoreFilepool.determinePackagesStatus(allSectionsStatus, result.statusData.status); - } finally { - section.isCalculating = false; - } - }); - - try { - await Promise.all(promises); - - if (allSectionsSection) { - // Set "All sections" data. - allSectionsSection.downloadStatus = allSectionsStatus; - allSectionsSection.isDownloading = allSectionsStatus === DownloadStatus.DOWNLOADING; - } - - return sections; - } finally { - if (allSectionsSection) { - allSectionsSection.isCalculating = false; - } - } + return { statusData, section: sectionWithStatus }; } /** @@ -401,7 +363,7 @@ export class CoreCourseHelperProvider { } // Confirm the download. - await this.confirmDownloadSizeSection(course.id, undefined, options.sections, true); + await this.confirmDownloadSizeSection(course.id, options.sections, true); // User confirmed, get the course handlers if needed. if (!options.courseHandlers) { @@ -448,42 +410,28 @@ export class CoreCourseHelperProvider { let count = 0; const promises = courses.map(async (course) => { - const subPromises: Promise[] = []; - let sections: CoreCourseWSSection[]; - let handlers: CoreCourseOptionsHandlerToDisplay[] = []; - let menuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = []; let success = true; // Get the sections and the handlers. - subPromises.push(CoreCourse.getSections(course.id, false, true).then((courseSections) => { - sections = courseSections; + const [sections, handlers, menuHandlers] = await Promise.all([ + CoreCourse.getSections(course.id, false, true), + CoreCourseOptionsDelegate.getHandlersToDisplay(course, false), + CoreCourseOptionsDelegate.getMenuHandlersToDisplay(course, false), + ]); - return; - })); + try { + await this.prefetchCourse(course, sections, handlers, menuHandlers, siteId); + } catch (error) { + success = false; - subPromises.push(CoreCourseOptionsDelegate.getHandlersToDisplay(course, false).then((cHandlers) => { - handlers = cHandlers; - - return; - })); - subPromises.push(CoreCourseOptionsDelegate.getMenuHandlersToDisplay(course, false).then((mHandlers) => { - menuHandlers = mHandlers; - - return; - })); - - return Promise.all(subPromises).then(() => this.prefetchCourse(course, sections, handlers, menuHandlers, siteId)) - .catch((error) => { - success = false; - - throw error; - }).finally(() => { + throw error; + } finally { // Course downloaded or failed, notify the progress. - count++; - if (options.onProgress) { - options.onProgress({ count: count, total: total, courseId: course.id, success: success }); - } - }); + count++; + if (options.onProgress) { + options.onProgress({ count: count, total: total, courseId: course.id, success: success }); + } + } }); if (options.onProgress) { @@ -498,48 +446,47 @@ export class CoreCourseHelperProvider { * Calculate the size to download a section and show a confirm modal if needed. * * @param courseId Course ID the section belongs to. - * @param section Section. If not provided, all sections. - * @param sections List of sections. Used when downloading all the sections. + * @param sections List of sections to download * @param alwaysConfirm True to show a confirm even if the size isn't high, false otherwise. * @returns Promise resolved if the user confirms or there's no need to confirm. */ async confirmDownloadSizeSection( courseId: number, - section?: CoreCourseWSSection, - sections?: CoreCourseWSSection[], - alwaysConfirm?: boolean, + sections: CoreCourseWSSection[] = [], + alwaysConfirm = false, ): Promise { let hasEmbeddedFiles = false; - let sizeSum: CoreFileSizeSum = { + const sizeSum: CoreFileSizeSum = { size: 0, total: true, }; - // Calculate the size of the download. - if (section && section.id != CoreCourseProvider.ALL_SECTIONS_ID) { - sizeSum = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.modules, courseId); + const getSectionSize = async (section: CoreCourseWSSection): Promise => { + if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) { + return { size: 0, total: true }; + } + + const { modules, subsections } = CoreCourse.classifyContents(section.contents); + + const [modulesSize, subsectionsSizes] = await Promise.all([ + CoreCourseModulePrefetchDelegate.getDownloadSize(modules, courseId), + Promise.all(subsections.map((modOrSubsection) => getSectionSize(modOrSubsection))), + ]); // Check if the section has embedded files in the description. - hasEmbeddedFiles = CoreFilepool.extractDownloadableFilesFromHtml(section.summary).length > 0; - } else if (sections) { - await Promise.all(sections.map(async (section) => { - if (section.id == CoreCourseProvider.ALL_SECTIONS_ID) { - return; - } + if (!hasEmbeddedFiles && CoreFilepool.extractDownloadableFilesFromHtml(section.summary).length > 0) { + hasEmbeddedFiles = true; + } - const sectionSize = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.modules, courseId); + return subsectionsSizes.concat(modulesSize).reduce((sizeSum, contentSize) => ({ + size: sizeSum.size + contentSize.size, + total: sizeSum.total && contentSize.total, + }), { size: 0, total: true }); + }; - sizeSum.total = sizeSum.total && sectionSize.total; - sizeSum.size += sectionSize.size; - - // Check if the section has embedded files in the description. - if (!hasEmbeddedFiles && CoreFilepool.extractDownloadableFilesFromHtml(section.summary).length > 0) { - hasEmbeddedFiles = true; - } - })); - } else { - throw new CoreError('Either section or list of sections needs to be supplied.'); - } + await Promise.all(sections.map(async (section) => { + await getSectionSize(section); + })); if (hasEmbeddedFiles) { sizeSum.total = false; @@ -549,6 +496,20 @@ export class CoreCourseHelperProvider { await CoreDomUtils.confirmDownloadSize(sizeSum, undefined, undefined, undefined, undefined, alwaysConfirm); } + /** + * Sums the stored module sizes. + * + * @param modules List of modules. + * @param courseId Course ID. + * @returns Promise resolved with the sum of the stored sizes. + */ + async getModulesDownloadedSize(modules: CoreCourseAnyModuleData[], courseId: number): Promise { + const moduleSizes = await Promise.all(modules.map(async (module) => + await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId))); + + return moduleSizes.reduce((totalSize, moduleSize) => totalSize + moduleSize, 0); + } + /** * Check whether a course is accessed using guest access and if it requires user input to enter. * @@ -624,6 +585,7 @@ export class CoreCourseHelperProvider { summary: '', summaryformat: 1, modules: [], + contents: [], }; } @@ -1121,31 +1083,36 @@ export class CoreCourseHelperProvider { const totalOffline = offlineCompletions.length; let loaded = 0; const offlineCompletionsMap = CoreUtils.arrayToObject(offlineCompletions, 'cmid'); - // Load the offline data in the modules. - for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - if (!section.modules || !section.modules.length) { - // Section has no modules, ignore it. - continue; + + const loadSectionOfflineCompletion = (section: CoreCourseWSSection): void => { + if (!section.contents || !section.contents.length) { + return; } - for (let j = 0; j < section.modules.length; j++) { - const module = section.modules[j]; - const offlineCompletion = offlineCompletionsMap[module.id]; + for (let j = 0; j < section.contents.length && loaded < totalOffline; j++) { + const modOrSubsection = section.contents[j]; + if (!sectionContentIsModule(modOrSubsection)) { + loadSectionOfflineCompletion(modOrSubsection); - if (offlineCompletion && module.completiondata !== undefined && - offlineCompletion.timecompleted >= module.completiondata.timecompleted * 1000) { + continue; + } + + const offlineCompletion = offlineCompletionsMap[modOrSubsection.id]; + + if (offlineCompletion && modOrSubsection.completiondata !== undefined && + offlineCompletion.timecompleted >= modOrSubsection.completiondata.timecompleted * 1000) { // The module has offline completion. Load it. - module.completiondata.state = offlineCompletion.completed; - module.completiondata.offline = true; + modOrSubsection.completiondata.state = offlineCompletion.completed; + modOrSubsection.completiondata.offline = true; - // If all completions have been loaded, stop. loaded++; - if (loaded == totalOffline) { - break; - } } } + }; + + // Load the offline data in the modules. + for (let i = 0; i < sections.length && loaded < totalOffline; i++) { + loadSectionOfflineCompletion(sections[i]); } } @@ -1334,20 +1301,18 @@ export class CoreCourseHelperProvider { await CoreUtils.ignoreErrors(CoreCourseModulePrefetchDelegate.invalidateCourseUpdates(courseId)); } - const results = await Promise.all([ + const [size, status, packageData] = await Promise.all([ CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId), CoreCourseModulePrefetchDelegate.getModuleStatus(module, courseId), this.getModulePackageLastDownloaded(module, component), ]); // Treat stored size. - const size = results[0]; - const sizeReadable = CoreText.bytesToSize(results[0], 2); + const sizeReadable = CoreText.bytesToSize(size, 2); // Treat module status. - const status = results[1]; let statusIcon: string | undefined; - switch (results[1]) { + switch (status) { case DownloadStatus.DOWNLOADABLE_NOT_DOWNLOADED: statusIcon = CoreConstants.ICON_NOT_DOWNLOADED; break; @@ -1364,8 +1329,6 @@ export class CoreCourseHelperProvider { break; } - const packageData = results[2]; - return { size, sizeReadable, @@ -1584,7 +1547,7 @@ export class CoreCourseHelperProvider { * @param siteId Site ID. If not defined, current site. * @returns Promise resolved when the download finishes. */ - async prefetchCourse( + protected async prefetchCourse( course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[], courseHandlers: CoreCourseOptionsHandlerToDisplay[], @@ -1609,12 +1572,7 @@ export class CoreCourseHelperProvider { const promises: Promise[] = []; - // Prefetch all the sections. If the first section is "All sections", use it. Otherwise, use a fake "All sections". - let allSectionsSection: CoreCourseWSSection = sections[0]; - if (sections[0].id != CoreCourseProvider.ALL_SECTIONS_ID) { - allSectionsSection = this.createAllSectionsSection(); - } - promises.push(this.prefetchSection(allSectionsSection, course.id, sections)); + promises.push(this.prefetchSections(sections, course.id, true)); // Prefetch course options. courseHandlers.forEach((handler) => { @@ -1631,8 +1589,8 @@ export class CoreCourseHelperProvider { // Prefetch other data needed to render the course. promises.push(CoreCourses.getCoursesByField('id', course.id)); - const sectionWithModules = sections.find((section) => section.modules && section.modules.length > 0); - if (!sectionWithModules || sectionWithModules.modules[0].completion === undefined) { + const modules = CoreCourse.getSectionsModules(sections); + if (!modules.length || modules[0].completion === undefined) { promises.push(CoreCourse.getActivitiesCompletionStatus(course.id)); } @@ -1684,41 +1642,32 @@ export class CoreCourseHelperProvider { } /** - * Prefetch one section or all the sections. - * If the section is "All sections" it will prefetch all the sections. + * Prefetch some sections * - * @param section Section. + * @param sections List of sections. . * @param courseId Course ID the section belongs to. - * @param sections List of sections. Used when downloading all the sections. - * @returns Promise resolved when the prefetch is finished. + * @param updateAllSections Update all sections status */ - async prefetchSection( - section: CoreCourseSectionWithStatus, + async prefetchSections( + sections: CoreCourseSectionWithStatus[], courseId: number, - sections?: CoreCourseSectionWithStatus[], + updateAllSections = false, ): Promise { - if (section.id != CoreCourseProvider.ALL_SECTIONS_ID) { - try { - // Download only this section. - await this.prefetchSingleSectionIfNeeded(section, courseId); - } finally { - // Calculate the status of the section that finished. - await this.calculateSectionStatus(section, courseId, false, false); - } - return; - } - - if (!sections) { - throw new CoreError('List of sections is required when downloading all sections.'); - } - - // Download all the sections except "All sections". let allSectionsStatus = DownloadStatus.NOT_DOWNLOADABLE as DownloadStatus; + let allSectionsSection: (CoreCourseSectionWithStatus) | undefined; + if (updateAllSections) { + // Prefetch all the sections. If the first section is "All sections", use it. Otherwise, use a fake "All sections". + allSectionsSection = sections[0]; + if (sections[0].id !== CoreCourseProvider.ALL_SECTIONS_ID) { + allSectionsSection = this.createAllSectionsSection(); + } + allSectionsSection.isDownloading = true; + } - section.isDownloading = true; const promises = sections.map(async (section) => { - if (section.id == CoreCourseProvider.ALL_SECTIONS_ID) { + // Download all the sections except "All sections". + if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) { return; } @@ -1737,10 +1686,14 @@ export class CoreCourseHelperProvider { await CoreUtils.allPromises(promises); // Set "All sections" data. - section.downloadStatus = allSectionsStatus; - section.isDownloading = allSectionsStatus === DownloadStatus.DOWNLOADING; + if (allSectionsSection) { + allSectionsSection.downloadStatus = allSectionsStatus; + allSectionsSection.isDownloading = allSectionsStatus === DownloadStatus.DOWNLOADING; + } } finally { - section.isDownloading = false; + if (allSectionsSection) { + allSectionsSection.isDownloading = false; + } } } @@ -1753,7 +1706,7 @@ export class CoreCourseHelperProvider { * @returns Promise resolved when the section is prefetched. */ protected async prefetchSingleSectionIfNeeded(section: CoreCourseSectionWithStatus, courseId: number): Promise { - if (section.id == CoreCourseProvider.ALL_SECTIONS_ID || section.hiddenbynumsections) { + if (section.id === CoreCourseProvider.ALL_SECTIONS_ID || section.hiddenbynumsections) { return; } @@ -1786,18 +1739,33 @@ export class CoreCourseHelperProvider { * @returns Promise resolved when the section is prefetched. */ protected async syncModulesAndPrefetchSection(section: CoreCourseSectionWithStatus, courseId: number): Promise { - // Sync the modules first. - await CoreCourseModulePrefetchDelegate.syncModules(section.modules, courseId); + const { modules, subsections } = CoreCourse.classifyContents(section.contents); - // Validate the section needs to be downloaded and calculate amount of modules that need to be downloaded. - const result = await CoreCourseModulePrefetchDelegate.getModulesStatus(section.modules, courseId, section.id); + const syncAndPrefetchModules = async () => { + // Sync the modules first. + await CoreCourseModulePrefetchDelegate.syncModules(modules, courseId); - if (result.status === DownloadStatus.DOWNLOADED || result.status === DownloadStatus.NOT_DOWNLOADABLE) { - // Section is downloaded or not downloadable, nothing to do. - return ; - } + // Validate the section needs to be downloaded and calculate amount of modules that need to be downloaded. + const result = await CoreCourseModulePrefetchDelegate.getModulesStatus(modules, courseId, section.id); - await this.prefetchSingleSection(section, result, courseId); + if (result.status === DownloadStatus.DOWNLOADED || result.status === DownloadStatus.NOT_DOWNLOADABLE) { + // Section is downloaded or not downloadable, nothing to do. + return ; + } + + await this.prefetchSingleSection(section, result, courseId); + }; + + this.setSectionDownloadCount(section, 0, subsections.length, true); + + await Promise.all([ + syncAndPrefetchModules(), + Promise.all(subsections.map(async (subsection) => { + await this.prefetchSingleSectionIfNeeded(subsection, courseId); + + this.setSectionDownloadCount(section, (section.subsectionCount ?? 0) + 1, subsections.length, true); + })), + ]); } /** @@ -1814,11 +1782,11 @@ export class CoreCourseHelperProvider { result: CoreCourseModulesStatus, courseId: number, ): Promise { - if (section.id == CoreCourseProvider.ALL_SECTIONS_ID) { + if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) { return; } - if (section.total && section.total > 0) { + if (section.moduleTotal && section.moduleTotal > 0) { // Already being downloaded. return ; } @@ -1832,8 +1800,7 @@ export class CoreCourseHelperProvider { // Prefetch all modules to prevent incoeherences in download count and to download stale data not marked as outdated. await CoreCourseModulePrefetchDelegate.prefetchModules(downloadId, modules, courseId, (data) => { - section.count = data.count; - section.total = data.total; + this.setSectionDownloadCount(section, data.count, data.total); }); } @@ -1844,16 +1811,12 @@ export class CoreCourseHelperProvider { * @returns Whether the section has content. */ sectionHasContent(section: CoreCourseWSSection): boolean { - if (!section.modules) { - return false; - } - if (section.hiddenbynumsections) { return false; } return (section.availabilityinfo !== undefined && section.availabilityinfo != '') || - section.summary != '' || (section.modules && section.modules.length > 0); + section.summary != '' || section.contents.length > 0; } /** @@ -1914,7 +1877,7 @@ export class CoreCourseHelperProvider { async deleteCourseFiles(courseId: number): Promise { const siteId = CoreSites.getCurrentSiteId(); const sections = await CoreCourse.getSections(courseId); - const modules = sections.map((section) => section.modules).flat(); + const modules = CoreCourse.getSectionsModules(sections); await Promise.all([ ...modules.map((module) => this.removeModuleStoredData(module, courseId)), @@ -2112,6 +2075,130 @@ export class CoreCourseHelperProvider { return completion.state; } + /** + * Find a section by id. + * + * @param sections List of sections, with subsections included in the contents. + * @param searchValue Value to search. If moduleId, returns the section that contains the module. + * @returns Section object, list of parents (if any) from top to bottom. + */ + findSection( + sections: T[], + searchValue: { id?: number; num?: number; moduleId?: number}, + ): {section: T | undefined; parents: T[]} { + if (searchValue.id === undefined && searchValue.num === undefined && searchValue.moduleId === undefined) { + return { section: undefined, parents: [] }; + } + + let foundSection: T | undefined; + const parents: T[] = []; + + const findInSection = (section: T): T | undefined => { + if (section.id === searchValue.id || (section.section !== undefined && section.section === searchValue.num)) { + return section; + } + + let foundSection: T | undefined; + + section.contents.some(modOrSubsection => { + if (sectionContentIsModule(modOrSubsection)) { + if (searchValue.moduleId !== undefined && modOrSubsection.id === searchValue.moduleId) { + foundSection = section; + + return true; + } + + return false; + } + + foundSection = findInSection(modOrSubsection as T); + if (!foundSection) { + return false; + } + + parents.push(section); + + return true; + }); + + return foundSection; + }; + + sections.some(section => { + foundSection = findInSection(section); + + return !!foundSection; + }); + + return { section: foundSection, parents: parents.reverse() }; + } + + /** + * Given a list of sections, returns the list of sections and subsections. + * + * @param sections Sections. + * @returns All sections, including subsections. + */ + flattenSections(sections: T[]): T[] { + const subsections: T[] = []; + + const getSubsections = (section: T): void => { + section.contents.forEach((modOrSubsection) => { + if (!sectionContentIsModule(modOrSubsection)) { + subsections.push(modOrSubsection as T); + getSubsections(modOrSubsection as T); + } + }); + }; + + sections.forEach((section) => { + getSubsections(section); + }); + + return sections.concat(subsections); + } + + /** + * Reset download counts of a section. + * + * @param section Section. + */ + protected resetSectionDownloadCount(section: CoreCourseSectionWithStatus): void { + section.moduleTotal = undefined; + section.subsectionTotal = undefined; + section.moduleCount = undefined; + section.subsectionCount = undefined; + section.total = undefined; + } + + /** + * Set download counts of a section. + * + * @param section Section. + * @param count Count value. + * @param total Total value. + * @param isSubsectionCount True to set subsection count, false to set module count. + */ + protected setSectionDownloadCount( + section: CoreCourseSectionWithStatus, + count: number, + total: number, + isSubsectionCount = false, + ): void { + if (isSubsectionCount) { + section.subsectionCount = count; + section.subsectionTotal = total; + } else { + section.moduleCount = count; + section.moduleTotal = total; + } + + section.count = section.moduleCount !== undefined && section.subsectionCount !== undefined ? + section.moduleCount + section.subsectionCount : undefined; + section.total = section.moduleTotal !== undefined && section.subsectionTotal !== undefined ? + section.moduleTotal + section.subsectionTotal : undefined; + } + } export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider); @@ -2119,8 +2206,9 @@ export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider); /** * Section with calculated data. */ -export type CoreCourseSection = CoreCourseWSSection & { +export type CoreCourseSection = Omit & { hasContent?: boolean; + contents: (CoreCourseModuleData | CoreCourseSection)[]; }; /** @@ -2129,8 +2217,12 @@ export type CoreCourseSection = CoreCourseWSSection & { export type CoreCourseSectionWithStatus = CoreCourseSection & { downloadStatus?: DownloadStatus; // Section status. isDownloading?: boolean; // Whether section is being downloaded. - total?: number; // Total of modules being downloaded. - count?: number; // Number of downloaded modules. + total?: number; // Total of modules and subsections being downloaded. + count?: number; // Number of downloaded modules and subsections. + moduleTotal?: number; // Total of modules being downloaded. + moduleCount?: number; // Number of downloaded modules. + subsectionTotal?: number; // Total of subsections being downloaded. + subsectionCount?: number; // Number of downloaded subsections. isCalculating?: boolean; // Whether status is being calculated. }; diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 34ae1d688..25ae38048 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -62,6 +62,9 @@ import { firstValueFrom } from 'rxjs'; import { map } from 'rxjs/operators'; import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site'; import { CoreLoadings } from '@services/loadings'; +import { CoreArray } from '@singletons/array'; +import { CoreText } from '@singletons/text'; +import { ArrayElement } from '@/core/utils/types'; const ROOT_CACHE_KEY = 'mmCourse:'; @@ -639,9 +642,9 @@ export class CoreCourseProvider { sectionId = module.section; } + const site = await CoreSites.getSite(siteId); let sections: CoreCourseGetContentsWSSection[]; try { - const site = await CoreSites.getSite(siteId); // We have courseId, we can use core_course_get_contents for compatibility. this.logger.debug(`Getting module ${moduleId} in course ${courseId}`); @@ -657,7 +660,11 @@ export class CoreCourseProvider { preSets.emergencyCache = false; } - sections = await this.getSections(courseId, false, false, preSets, siteId); + sections = await firstValueFrom(this.callGetSectionsWS(site, courseId, { + excludeModules: false, + excludeContents: false, + preSets, + })); } let foundModule: CoreCourseGetContentsWSModule | undefined; @@ -953,40 +960,10 @@ export class CoreCourseProvider { courseId: number, options: CoreCourseGetSectionsOptions = {}, ): WSObservable { - options.includeStealthModules = options.includeStealthModules ?? true; - return asyncObservable(async () => { const site = await CoreSites.getSite(options.siteId); - const preSets: CoreSiteWSPreSets = { - ...options.preSets, - cacheKey: this.getSectionsCacheKey(courseId), - updateFrequency: CoreSite.FREQUENCY_RARELY, - ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), - }; - - const params: CoreCourseGetContentsParams = { - courseid: courseId, - }; - params.options = [ - { - name: 'excludemodules', - value: !!options.excludeModules, - }, - { - name: 'excludecontents', - value: !!options.excludeContents, - }, - ]; - - if (this.canRequestStealthModules(site)) { - params.options.push({ - name: 'includestealthmodules', - value: !!options.includeStealthModules, - }); - } - - return site.readObservable('core_course_get_contents', params, preSets).pipe( + return this.callGetSectionsWS(site, courseId, options).pipe( map(sections => { const siteHomeId = site.getSiteHomeId(); let showSections = true; @@ -1000,17 +977,92 @@ export class CoreCourseProvider { sections.pop(); } - // Add course to all modules. - return sections.map((section) => ({ + // First format all the sections and their modules. + const formattedSections: CoreCourseWSSection[] = sections.map((section) => ({ ...section, availabilityinfo: this.treatAvailablityInfo(section.availabilityinfo), modules: section.modules.map((module) => this.addAdditionalModuleData(module, courseId, section.id)), + contents: [], })); + + // Only return the root sections, subsections are included in section contents. + return this.addSectionsContents(formattedSections).filter((section) => !section.component); }), ); }); } + /** + * Call the WS to get the course sections. + * + * @param site Site. + * @param courseId The course ID. + * @param options Options. + * @returns Observable that returns the sections. + */ + protected callGetSectionsWS( + site: CoreSite, + courseId: number, + options: CoreCourseGetSectionsOptions = {}, + ): WSObservable { + const preSets: CoreSiteWSPreSets = { + ...options.preSets, + cacheKey: this.getSectionsCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), + }; + + const params: CoreCourseGetContentsParams = { + courseid: courseId, + }; + params.options = [ + { + name: 'excludemodules', + value: !!options.excludeModules, + }, + { + name: 'excludecontents', + value: !!options.excludeContents, + }, + ]; + + if (this.canRequestStealthModules(site)) { + params.options.push({ + name: 'includestealthmodules', + value: !!(options.includeStealthModules ?? true), + }); + } + + return site.readObservable('core_course_get_contents', params, preSets); + } + + /** + * Calculate and add the section contents. Section contents include modules and subsections. + * + * @param sections Sections to calculate. + * @returns Sections with contents. + */ + protected addSectionsContents(sections: CoreCourseWSSection[]): CoreCourseWSSection[] { + const subsections = sections.filter((section) => !!section.component); + const subsectionsComponents = CoreArray.unique(subsections.map(section => (section.component ?? '').replace('mod_', ''))); + + sections.forEach(section => { + // eslint-disable-next-line deprecation/deprecation + section.contents = section.modules.map(module => { + if (!subsectionsComponents.includes(module.modname)) { + return module; + } + + // Replace the module with the subsection. If subsection not found, the module will be removed from the list. + const customData = CoreText.parseJSON<{ sectionid?: string | number }>(module.customdata ?? '{}', {}); + + return subsections.find(subsection => subsection.id === Number(customData.sectionid)); + }).filter((content): content is (CoreCourseWSSection | CoreCourseModuleData) => content !== undefined); + }); + + return sections; + } + /** * Get cache key for section WS call. * @@ -1023,16 +1075,40 @@ export class CoreCourseProvider { /** * Given a list of sections, returns the list of modules in the sections. + * The modules are ordered in the order of appearance in the course. * * @param sections Sections. + * @param options Other options. * @returns Modules. */ - getSectionsModules(sections: CoreCourseWSSection[]): CoreCourseModuleData[] { - if (!sections || !sections.length) { - return []; - } + getSectionsModules< + Section extends CoreCourseWSSection, + Module = Extract, CoreCourseModuleData> + >( + sections: Section[], + options: CoreCourseGetSectionsModulesOptions = {}, + ): Module[] { + let modules: Module[] = []; - return sections.reduce((previous: CoreCourseModuleData[], section) => previous.concat(section.modules || []), []); + sections.forEach((section) => { + if (options.ignoreSection && options.ignoreSection(section)) { + return; + } + + section.contents.forEach((modOrSubsection) => { + if (sectionContentIsModule(modOrSubsection)) { + if (options.ignoreModule && options.ignoreModule(modOrSubsection as Module)) { + return; + } + + modules.push(modOrSubsection as Module); + } else { + modules = modules.concat(this.getSectionsModules([modOrSubsection], options)); + } + }); + }); + + return modules; } /** @@ -1587,10 +1663,47 @@ export class CoreCourseProvider { return CoreDomUtils.removeElementFromHtml(availabilityInfo, 'li[data-action="showmore"]'); } + /** + * Given section contents, classify them into modules and sections. + * + * @param contents Contents. + * @returns Classified contents. + */ + classifyContents< + Contents extends CoreCourseModuleOrSection, + Module = Extract, + Section = Extract, + >(contents: Contents[]): { modules: Module[]; subsections: Section[] } { + const modules: Module[] = []; + const subsections: Section[] = []; + + contents.forEach((content) => { + if (sectionContentIsModule(content)) { + modules.push(content as Module); + } else { + subsections.push(content as unknown as Section); + } + }); + + return { modules, subsections }; + } + } export const CoreCourse = makeSingleton(CoreCourseProvider); +/** + * Type guard to detect if a section content (module or subsection) is a module. + * + * @param content Section module or subsection. + * @returns Whether section content is a module. + */ +export function sectionContentIsModule
( + content: Module | Section, +): content is Module { + return 'modname' in content; +} + /** * Common options used by modules when calling a WS through CoreSite. */ @@ -1821,9 +1934,21 @@ export type CoreCourseGetContentsWSModule = { * Data returned by core_course_get_contents WS. */ export type CoreCourseWSSection = Omit & { - modules: CoreCourseModuleData[]; // List of module. + contents: CoreCourseModuleOrSection[]; // List of modules and subsections. + + /** + * List of modules + * + * @deprecated since 4.5. Use contents instead. + */ + modules: CoreCourseModuleData[]; }; +/** + * Module or subsection. + */ +export type CoreCourseModuleOrSection = CoreCourseModuleData | CoreCourseWSSection; + /** * Params of core_course_get_course_module WS. */ @@ -1997,3 +2122,11 @@ export type CoreCourseGetSectionsOptions = CoreSitesCommonWSOptions & { includeStealthModules?: boolean; // Defaults to true. preSets?: CoreSiteWSPreSets; }; + +/** + * Options for get sections modules. + */ +export type CoreCourseGetSectionsModulesOptions = { + ignoreSection?: (section: Section) => boolean; // Function to filter sections. Return true to ignore it, false to use it. + ignoreModule?: (module: Module) => boolean; // Function to filter module. Return true to ignore it, false to use it. +}; diff --git a/src/core/features/course/services/module-prefetch-delegate.ts b/src/core/features/course/services/module-prefetch-delegate.ts index 9fc20ea79..330de8d0a 100644 --- a/src/core/features/course/services/module-prefetch-delegate.ts +++ b/src/core/features/course/services/module-prefetch-delegate.ts @@ -472,17 +472,21 @@ export class CoreCourseModulePrefetchDelegateService extends CoreDelegate { - const site = CoreSites.getCurrentSite(); - const handler = this.getPrefetchHandlerFor(module.modname); + try { + const site = CoreSites.getCurrentSite(); + const handler = this.getPrefetchHandlerFor(module.modname); - const [downloadedSize, cachedSize] = await Promise.all([ - this.getModuleDownloadedSize(module, courseId), - handler && site ? site.getComponentCacheSize(handler.component, module.id) : 0, - ]); + const [downloadedSize, cachedSize] = await Promise.all([ + this.getModuleDownloadedSize(module, courseId), + handler && site ? site.getComponentCacheSize(handler.component, module.id) : 0, + ]); - const totalSize = cachedSize + downloadedSize; + const totalSize = cachedSize + downloadedSize; - return isNaN(totalSize) ? 0 : totalSize; + return isNaN(totalSize) ? 0 : totalSize; + } catch { + return 0; + } } /** diff --git a/src/core/features/filter/services/filter-helper.ts b/src/core/features/filter/services/filter-helper.ts index 0cdf2acbe..5d84c55ab 100644 --- a/src/core/features/filter/services/filter-helper.ts +++ b/src/core/features/filter/services/filter-helper.ts @@ -26,7 +26,7 @@ import { CoreFilterStateValue, CoreFilterAllStates, } from './filter'; -import { CoreCourse } from '@features/course/services/course'; +import { CoreCourse, sectionContentIsModule } from '@features/course/services/course'; import { CoreCourses } from '@features/courses/services/courses'; import { makeSingleton } from '@singletons'; import { CoreEvents, CoreEventSiteData } from '@singletons/events'; @@ -180,16 +180,18 @@ export class CoreFilterHelperProvider { const contexts: CoreFiltersGetAvailableInContextWSParamContext[] = []; sections.forEach((section) => { - if (section.modules) { - section.modules.forEach((module) => { - if (CoreCourseHelper.canUserViewModule(module, section)) { - contexts.push({ - contextlevel: ContextLevel.MODULE, - instanceid: module.id, - }); - } - }); - } + section.contents.forEach((modOrSubsection) => { + if (!sectionContentIsModule(modOrSubsection)) { + return; + } + + if (CoreCourseHelper.canUserViewModule(modOrSubsection, section)) { + contexts.push({ + contextlevel: ContextLevel.MODULE, + instanceid: modOrSubsection.id, + }); + } + }); }); return contexts; diff --git a/src/core/features/mainmenu/tests/behat/mainmenu.feature b/src/core/features/mainmenu/tests/behat/mainmenu.feature index 9c5fc73cb..875f389e5 100644 --- a/src/core/features/mainmenu/tests/behat/mainmenu.feature +++ b/src/core/features/mainmenu/tests/behat/mainmenu.feature @@ -41,7 +41,7 @@ Feature: Main Menu opens the right page @lms_from4.5 Scenario: Opens right main menu tab when defaulthomepage is set to a custom URL that belongs to a tab Given the following config values are set as admin: - | defaulthomepage | #wwwroot#/message/index.php | + | defaulthomepage | /message/index.php | And I entered the app as "student" Then "Messages" "ion-tab-button" should be selected in the app And I should find "Contacts" in the app @@ -49,7 +49,7 @@ Feature: Main Menu opens the right page @lms_from4.5 Scenario: Opens new page when defaulthomepage is set to a custom URL Given the following config values are set as admin: - | defaulthomepage | #wwwroot#/badges/mybadges.php | + | defaulthomepage | /badges/mybadges.php | And I entered the app as "student" Then I should find "Badges" in the app And I should find "There are currently no badges" in the app @@ -61,7 +61,7 @@ Feature: Main Menu opens the right page @lms_from4.5 Scenario: defaulthomepage ignored if it's set to a custom URL not supported by the app Given the following config values are set as admin: - | defaulthomepage | #wwwroot#/foo/bar.php | + | defaulthomepage | /foo/bar.php | And I entered the app as "student" Then "My courses" "ion-tab-button" should be selected in the app And I should find "Course 1" in the app diff --git a/src/core/features/sitehome/pages/index/index.html b/src/core/features/sitehome/pages/index/index.html index 168648f1c..a4660153e 100644 --- a/src/core/features/sitehome/pages/index/index.html +++ b/src/core/features/sitehome/pages/index/index.html @@ -45,7 +45,9 @@ - + + +
diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts index 8e116aabf..96b773d14 100644 --- a/src/core/features/sitehome/pages/index/index.ts +++ b/src/core/features/sitehome/pages/index/index.ts @@ -16,7 +16,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { CoreSite, CoreSiteConfig } from '@classes/sites/site'; -import { CoreCourse, CoreCourseWSSection } from '@features/course/services/course'; +import { CoreCourse, CoreCourseWSSection, sectionContentIsModule } from '@features/course/services/course'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreSites } from '@services/sites'; import { CoreSiteHome } from '@features/sitehome/services/sitehome'; @@ -55,6 +55,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { currentSite!: CoreSite; searchEnabled = false; newsForumModule?: CoreCourseModuleData; + isModule = sectionContentIsModule; protected updateSiteObserver: CoreEventObserver; protected logView: () => void; @@ -177,9 +178,12 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { promises.push(CoreCourse.invalidateCourseBlocks(this.siteHomeId)); - if (this.section && this.section.modules) { + if (this.section?.contents.length) { // Invalidate modules prefetch data. - promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId)); + promises.push(CoreCourseModulePrefetchDelegate.invalidateModules( + CoreCourse.getSectionsModules([this.section]), + this.siteHomeId, + )); } Promise.all(promises).finally(async () => { diff --git a/src/core/features/sitehome/services/sitehome.ts b/src/core/features/sitehome/services/sitehome.ts index cc87271ed..3dcf98555 100644 --- a/src/core/features/sitehome/services/sitehome.ts +++ b/src/core/features/sitehome/services/sitehome.ts @@ -99,7 +99,7 @@ export class CoreSiteHomeProvider { throw Error('No sections found'); } - const hasContent = sections.some((section) => section.summary || (section.modules && section.modules.length)); + const hasContent = sections.some((section) => section.summary || section.contents.length); const hasCourseBlocks = await CoreBlockHelper.hasCourseBlocks(siteHomeId); if (hasContent || hasCourseBlocks) { diff --git a/src/core/utils/types.ts b/src/core/utils/types.ts index 8241d0bd6..04c055bd2 100644 --- a/src/core/utils/types.ts +++ b/src/core/utils/types.ts @@ -148,3 +148,15 @@ export function safeNumber(value?: unknown): SafeNumber | undefined { return value; } + +/** + * Helper type to extract the type of each item of an array. + * + * @example + * ``` + * type Result = ArrayElement<(A|B)[]>; + * // ^? type Result = A|B; + * ``` + */ +export type ArrayElement = ArrayType extends readonly (infer ElementType)[] ? + ElementType : never;