From a169d9301a2a572218f727712700c8dd85a6395a Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 3 Oct 2024 17:13:45 +0200 Subject: [PATCH] MOBILE-4660 course: Refactor how subsections are handled This commit doesn't refactor course downloads yet, it will be done in another commit. --- .../activitymodules/activitymodules.ts | 59 ++-- .../addon-block-sitemainmenu.html | 4 +- .../components/sitemainmenu/sitemainmenu.ts | 16 +- .../services/handlers/index-link.ts | 28 +- .../subsection/services/handlers/module.ts | 88 ------ .../mod/subsection/services/subsection.ts | 51 ---- .../mod/subsection/subsection.module.ts | 3 - .../pages/courses-storage/courses-storage.ts | 2 +- .../course-format/course-format.html | 17 +- .../components/course-format/course-format.ts | 130 ++++---- .../components/course-index/course-index.html | 31 +- .../components/course-index/course-index.ts | 90 ++---- .../course-section/course-section.html | 14 +- .../course-section/course-section.ts | 30 +- .../module-navigation/module-navigation.ts | 70 +---- .../components/singleactivity.ts | 4 +- .../handlers/singleactivity-format.ts | 8 +- .../course/pages/contents/contents.ts | 10 +- .../pages/list-mod-type/list-mod-type.html | 36 ++- .../pages/list-mod-type/list-mod-type.ts | 95 +++--- .../pages/module-preview/module-preview.ts | 4 +- .../features/course/services/course-helper.ts | 278 +++++++++++++----- src/core/features/course/services/course.ts | 154 +++++++--- .../features/filter/services/filter-helper.ts | 24 +- .../features/sitehome/pages/index/index.html | 4 +- .../features/sitehome/pages/index/index.ts | 10 +- .../features/sitehome/services/sitehome.ts | 2 +- src/core/utils/types.ts | 12 + 28 files changed, 652 insertions(+), 622 deletions(-) delete mode 100644 src/addons/mod/subsection/services/handlers/module.ts delete mode 100644 src/addons/mod/subsection/services/subsection.ts diff --git a/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts b/src/addons/block/activitymodules/components/activitymodules/activitymodules.ts index a8c52eb53..1f946efba 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 = CoreCourseHelper.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/courses-storage/courses-storage.ts b/src/addons/storagemanager/pages/courses-storage/courses-storage.ts index 6e44a0c85..aa478bdcb 100644 --- a/src/addons/storagemanager/pages/courses-storage/courses-storage.ts +++ b/src/addons/storagemanager/pages/courses-storage/courses-storage.ts @@ -240,7 +240,7 @@ 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 modules = CoreCourseHelper.getSectionsModules(sections); const promisedModuleSizes = modules.map(async (module) => { const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, 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..a52f5c3eb 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; @@ -226,9 +227,6 @@ 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'); - this.treatSections(this.sections); } this.changeDetectorRef.markForCheck(); @@ -326,35 +324,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'); @@ -386,9 +374,12 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { 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; + } else { + const modules = CoreCourseHelper.getSectionsModules([currentSectionData.section]); + if (modules.some(module => module.id === lastModuleViewed.cmId)) { + // Last module viewed is inside the highlighted section. + moduleId = lastModuleViewed.cmId; + } } } @@ -422,7 +413,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { } /** - * 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. @@ -432,16 +423,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { sections: CoreCourseSection[], viewedModule: CoreCourseViewedModulesDBRecord, ): CoreCourseSection | undefined { - let lastModuleSection: CoreCourseSection | undefined; - - if (viewedModule.sectionId) { - lastModuleSection = sections.find(section => section.id === viewedModule.sectionId); - } - - 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(sections, { + id: viewedModule.sectionId, + moduleId: viewedModule.cmId, + }); + const lastModuleSection: CoreCourseSection | undefined = parents[0] ?? section; return lastModuleSection && lastModuleSection.id !== this.stealthModulesSectionId ? lastModuleSection : undefined; } @@ -488,7 +474,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { componentProps: { course: this.course, sections: this.sections, - subSections: this.subSections, selectedId: selectedId, }, }); @@ -496,29 +481,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; } @@ -676,12 +664,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 = CoreCourseHelper.getSectionsModules([this.sections[this.lastShownSectionIndex]]); + + modulesLoaded += sectionModules.reduce((total, module) => !CoreCourseHelper.isModuleStealth(module, this.sections[this.lastShownSectionIndex]) ? total + 1 : total, 0); } @@ -782,9 +772,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}`, @@ -802,12 +791,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { // Expand all sections if not defined. if (expandedSections === undefined) { - this.sections.forEach((section) => { - section.expanded = true; - this.accordionMultipleValue.push(section.id.toString()); - }); - - this.subSections.forEach((section) => { + CoreCourseHelper.flattenSections(this.sections).forEach((section) => { section.expanded = true; this.accordionMultipleValue.push(section.id.toString()); }); @@ -817,11 +801,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,20 +813,14 @@ 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 === sId); 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..53e460976 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 CoreCourseHelper.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/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..1db217366 100644 --- a/src/core/features/course/pages/contents/contents.ts +++ b/src/core/features/course/pages/contents/contents.ts @@ -27,7 +27,6 @@ import { import { CoreCourseHelper, CoreCourseModuleCompletionData, - CoreCourseModuleData, CoreCourseSection, } from '@features/course/services/course-helper'; import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; @@ -219,7 +218,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon if (refresh) { // Invalidate the recently downloaded module list. To ensure info can be prefetched. - const modules = CoreCourse.getSectionsModules(sections); + const modules = CoreCourseHelper.getSectionsModules(sections); await CoreCourseModulePrefetchDelegate.invalidateModules(modules, this.course.id); } @@ -228,9 +227,9 @@ 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); + const modules = CoreCourseHelper.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 +334,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 = CoreCourseHelper.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..01e959a87 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 = CoreCourseHelper.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..420d69396 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -27,6 +27,7 @@ import { CoreCourseModuleCompletionTracking, CoreCourseModuleCompletionStatus, CoreCourseGetContentsWSModule, + sectionContentIsModule, } from './course'; import { CoreConstants, DownloadStatus, ContextLevel } from '@/core/constants'; import { CoreLogger } from '@singletons/logger'; @@ -76,6 +77,7 @@ import { CoreEnrolAction, CoreEnrolDelegate } from '@features/enrol/services/enr import { LazyRoutesModule } from '@/app/app-routing.module'; import { CoreModals } from '@services/modals'; import { CoreLoadings } from '@services/loadings'; +import { ArrayElement } from '@/core/utils/types'; /** * Prefetch info of a module. @@ -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 { @@ -281,7 +291,7 @@ export class CoreCourseHelperProvider { // Get the status of this section. const result = await CoreCourseModulePrefetchDelegate.getModulesStatus( - section.modules, + section.contents, courseId, section.id, refresh, @@ -517,7 +527,7 @@ export class CoreCourseHelperProvider { // Calculate the size of the download. if (section && section.id != CoreCourseProvider.ALL_SECTIONS_ID) { - sizeSum = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.modules, courseId); + sizeSum = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.contents, courseId); // Check if the section has embedded files in the description. hasEmbeddedFiles = CoreFilepool.extractDownloadableFilesFromHtml(section.summary).length > 0; @@ -527,7 +537,7 @@ export class CoreCourseHelperProvider { return; } - const sectionSize = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.modules, courseId); + const sectionSize = await CoreCourseModulePrefetchDelegate.getDownloadSize(section.contents, courseId); sizeSum.total = sizeSum.total && sectionSize.total; sizeSum.size += sectionSize.size; @@ -624,6 +634,7 @@ export class CoreCourseHelperProvider { summary: '', summaryformat: 1, modules: [], + contents: [], }; } @@ -1121,31 +1132,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]); } } @@ -1631,8 +1647,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 = this.getSectionsModules(sections); + if (!modules.length || modules[0].completion === undefined) { promises.push(CoreCourse.getActivitiesCompletionStatus(course.id)); } @@ -1787,10 +1803,10 @@ export class CoreCourseHelperProvider { */ protected async syncModulesAndPrefetchSection(section: CoreCourseSectionWithStatus, courseId: number): Promise { // Sync the modules first. - await CoreCourseModulePrefetchDelegate.syncModules(section.modules, courseId); + await CoreCourseModulePrefetchDelegate.syncModules(section.contents, courseId); // 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 result = await CoreCourseModulePrefetchDelegate.getModulesStatus(section.contents, courseId, section.id); if (result.status === DownloadStatus.DOWNLOADED || result.status === DownloadStatus.NOT_DOWNLOADABLE) { // Section is downloaded or not downloadable, nothing to do. @@ -1844,16 +1860,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 +1926,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 = this.getSectionsModules(sections); await Promise.all([ ...modules.map((module) => this.removeModuleStoredData(module, courseId)), @@ -2112,6 +2124,127 @@ 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 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< + Section extends CoreCourseWSSection, + Module = Extract, CoreCourseModuleData> + >( + sections: Section[], + options: CoreCourseGetSectionsModulesOptions = {}, + ): Module[] { + let modules: Module[] = []; + + 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; + } + + /** + * 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); + } + } export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider); @@ -2119,8 +2252,9 @@ export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider); /** * Section with calculated data. */ -export type CoreCourseSection = CoreCourseWSSection & { +export type CoreCourseSection = Omit & { hasContent?: boolean; + contents: (CoreCourseModuleData | CoreCourseSection)[]; }; /** @@ -2216,3 +2350,11 @@ export type CoreCourseGuestAccessInfo = { */ passwordRequired?: boolean; }; + +/** + * 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/course.ts b/src/core/features/course/services/course.ts index 34ae1d688..5ae7baa2a 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -62,6 +62,8 @@ 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'; const ROOT_CACHE_KEY = 'mmCourse:'; @@ -639,9 +641,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 +659,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 +959,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 +976,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. * @@ -1026,13 +1077,10 @@ export class CoreCourseProvider { * * @param sections Sections. * @returns Modules. + * @deprecated since 4.5. Use CoreCourseHelper.getSectionsModules instead. */ getSectionsModules(sections: CoreCourseWSSection[]): CoreCourseModuleData[] { - if (!sections || !sections.length) { - return []; - } - - return sections.reduce((previous: CoreCourseModuleData[], section) => previous.concat(section.modules || []), []); + return CoreCourseHelper.getSectionsModules(sections); } /** @@ -1591,6 +1639,18 @@ export class CoreCourseProvider { 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 +1881,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. */ 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/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;