From 95c4e0e225e8d2c5583389630157618026fb0d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 13 Sep 2024 15:43:26 +0200 Subject: [PATCH] MOBILE-4442 subsection: Create subsection activity module --- src/addons/mod/mod.module.ts | 2 + .../services/handlers/index-link.ts | 65 ++++++++++++++ .../subsection/services/handlers/module.ts | 88 +++++++++++++++++++ .../mod/subsection/services/subsection.ts | 50 +++++++++++ .../mod/subsection/subsection.module.ts | 33 +++++++ .../course-format/course-format.html | 21 ++--- .../components/course-format/course-format.ts | 32 ++++++- .../course-section/course-section.scss | 8 -- 8 files changed, 278 insertions(+), 21 deletions(-) create mode 100644 src/addons/mod/subsection/services/handlers/index-link.ts create mode 100644 src/addons/mod/subsection/services/handlers/module.ts create mode 100644 src/addons/mod/subsection/services/subsection.ts create mode 100644 src/addons/mod/subsection/subsection.module.ts diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts index 1e3d73622..49a0fd1ec 100644 --- a/src/addons/mod/mod.module.ts +++ b/src/addons/mod/mod.module.ts @@ -33,6 +33,7 @@ import { AddonModPageModule } from './page/page.module'; import { AddonModQuizModule } from './quiz/quiz.module'; import { AddonModResourceModule } from './resource/resource.module'; import { AddonModScormModule } from './scorm/scorm.module'; +import { AddonModSubsectionModule } from './subsection/subsection.module'; import { AddonModSurveyModule } from './survey/survey.module'; import { AddonModUrlModule } from './url/url.module'; import { AddonModWikiModule } from './wiki/wiki.module'; @@ -59,6 +60,7 @@ import { AddonModWorkshopModule } from './workshop/workshop.module'; AddonModQuizModule, AddonModResourceModule, AddonModScormModule, + AddonModSubsectionModule, AddonModSurveyModule, AddonModUrlModule, AddonModWikiModule, diff --git a/src/addons/mod/subsection/services/handlers/index-link.ts b/src/addons/mod/subsection/services/handlers/index-link.ts new file mode 100644 index 000000000..240f55b7c --- /dev/null +++ b/src/addons/mod/subsection/services/handlers/index-link.ts @@ -0,0 +1,65 @@ +// (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 { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +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'; + +/** + * Handler to treat links to subsection. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModSubsectionIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler { + + name = 'AddonModSubsectionLinkHandler'; + + constructor() { + super('AddonModSubsection', 'subsection', 'id'); + } + + /** + * @inheritdoc + */ + getActions( + siteIds: string[], + url: string, + params: Record, + courseId?: number, + ): CoreContentLinksAction[] | Promise { + return [{ + action: async(siteId) => { + const modal = await CoreLoadings.show(); + const moduleId = Number(params.id); + + try { + // Get the module. + const module = await CoreCourse.getModule(moduleId, courseId, undefined, true, false, siteId); + + await AddonModSubsection.openSubsection(module, module.course, siteId); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error opening link.'); + } finally { + modal.dismiss(); + } + }, + }]; + } + +} +export const AddonModSubsectionIndexLinkHandler = makeSingleton(AddonModSubsectionIndexLinkHandlerService); diff --git a/src/addons/mod/subsection/services/handlers/module.ts b/src/addons/mod/subsection/services/handlers/module.ts new file mode 100644 index 000000000..e4d9ed11f --- /dev/null +++ b/src/addons/mod/subsection/services/handlers/module.ts @@ -0,0 +1,88 @@ +// (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, courseId) => { + try { + await AddonModSubsection.openSubsection(module, courseId); + } 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 new file mode 100644 index 000000000..7ef0293fd --- /dev/null +++ b/src/addons/mod/subsection/services/subsection.ts @@ -0,0 +1,50 @@ +// (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 { CoreCourseModuleData, 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. + */ + async openSubsection(module: CoreCourseModuleData , courseId?: number, siteId?: string): Promise { + if (!courseId) { + courseId = module.course; + } + + const pageParams = { + sectionId: module.section, + }; + + 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 new file mode 100644 index 000000000..bfd1f2f09 --- /dev/null +++ b/src/addons/mod/subsection/subsection.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { 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: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreCourseModuleDelegate.registerHandler(AddonModSubsectionModuleHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModSubsectionIndexLinkHandler.instance); + }, + }, + ], +}) +export class AddonModSubsectionModule {} 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 add8466bd..6d62db904 100644 --- a/src/core/features/course/components/course-format/course-format.html +++ b/src/core/features/course/components/course-format/course-format.html @@ -10,10 +10,11 @@
- - + + @@ -23,14 +24,14 @@
- + @for (section of sections; track section.id) { - @if ($index <= lastShownSectionIndex) { + @if ($index <= lastShownSectionIndex && !section.hiddenbynumsections && section.id !== allSectionsId && + section.id !== stealthModulesSectionId && !section.component) { + [course]="course" [section]="section" [lastModuleViewed]="lastModuleViewed" [viewedModules]="viewedModules" + [collapsible]="true" [sections]="subSections" /> } } 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 a1ac16558..a029d20c3 100644 --- a/src/core/features/course/components/course-format/course-format.ts +++ b/src/core/features/course/components/course-format/course-format.ts @@ -88,6 +88,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { @Input({ required: true }) course!: CoreCourseAnyCourseData; // The course to render. @Input() sections: CoreCourseSectionToDisplay[] = []; // List of course sections. + @Input() subSections: CoreCourseSectionToDisplay[] = []; // List of course subsections. @Input() initialSectionId?: number; // The section to load first (by ID). @Input() initialSectionNumber?: number; // The section to load first (by number). @Input() initialBlockInstanceId?: number; // The instance to focus. @@ -225,6 +226,9 @@ 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(); @@ -746,9 +750,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * Save expanded sections for the course. */ protected async saveExpandedSections(): Promise { - const expandedSections = this.sections.filter((section) => section.expanded).map((section) => section.id).join(','); + 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)); - await this.currentSite?.setLocalSiteConfig(`${COURSE_EXPANDED_SECTIONS_PREFIX}${this.course.id}`, expandedSections); + await this.currentSite?.setLocalSiteConfig( + `${COURSE_EXPANDED_SECTIONS_PREFIX}${this.course.id}`, + expandedSections.join(','), + ); } /** @@ -766,6 +775,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.accordionMultipleValue.push(section.id.toString()); }); + this.subSections.forEach((section) => { + section.expanded = true; + this.accordionMultipleValue.push(section.id.toString()); + }); + return; } @@ -774,6 +788,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.sections.forEach((section) => { section.expanded = this.accordionMultipleValue.includes(section.id.toString()); }); + + this.subSections.forEach((section) => { + section.expanded = this.accordionMultipleValue.includes(section.id.toString()); + }); } /** @@ -787,9 +805,17 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { section.expanded = false; }); + this.subSections.forEach((section) => { + section.expanded = false; + }); + sectionIds?.forEach((sectionId) => { const sId = Number(sectionId); - const section = this.sections.find((section) => section.id === sId); + let section = this.sections.find((section) => section.id === sId); + if (!section) { + section = this.subSections.find((section) => section.id === sId); + } + if (section) { section.expanded = true; } diff --git a/src/core/features/course/components/course-section/course-section.scss b/src/core/features/course/components/course-section/course-section.scss index 0f47146f9..70995dab5 100644 --- a/src/core/features/course/components/course-section/course-section.scss +++ b/src/core/features/course/components/course-section/course-section.scss @@ -19,12 +19,4 @@ } } } - - &.non-collapsible ::ng-deep { - ion-item.divider.course-section { - ion-icon.ion-accordion-toggle-icon { - display: none; - } - } - } }