MOBILE-4442 subsection: Create subsection activity module

main
Pau Ferrer Ocaña 2024-09-13 15:43:26 +02:00
parent 0b4c3dc88e
commit 95c4e0e225
8 changed files with 278 additions and 21 deletions

View File

@ -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,

View File

@ -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<string, string>,
courseId?: number,
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
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);

View File

@ -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<undefined> {
// 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);

View File

@ -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<void> {
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);

View File

@ -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 {}

View File

@ -10,10 +10,11 @@
<!-- Single section. -->
<div *ngIf="selectedSection && selectedSection.id !== allSectionsId" class="list-item-limited-width">
<core-dynamic-component [component]="singleSectionComponent" [data]="data">
<ion-accordion-group [readonly]="true" value="non-collapsible">
<core-course-section *ngIf="!selectedSection.hiddenbynumsections && selectedSection.id !== allSectionsId &&
selectedSection.id !== stealthModulesSectionId" [course]="course" [section]="selectedSection"
[lastModuleViewed]="lastModuleViewed" [viewedModules]="viewedModules" [collapsible]="false" />
<ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)"
[value]="accordionMultipleValue">
<core-course-section *ngIf="!selectedSection.hiddenbynumsections && selectedSection.id !== stealthModulesSectionId &&
!selectedSection.component" [course]="course" [section]="selectedSection" [lastModuleViewed]="lastModuleViewed"
[viewedModules]="viewedModules" [collapsible]="false" [sections]="subSections" />
</ion-accordion-group>
<core-empty-box *ngIf="!selectedSection.hasContent" icon="fas-table-cells-large"
[message]="'core.course.nocontentavailable' | translate" />
@ -23,14 +24,14 @@
<!-- Multiple sections. -->
<div *ngIf="selectedSection && selectedSection.id === allSectionsId" class="list-item-limited-width">
<core-dynamic-component [component]="allSectionsComponent" [data]="data">
<ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)" [value]="accordionMultipleValue"
#accordionMultiple>
<ion-accordion-group [multiple]="true" (ionChange)="accordionMultipleChange($event.detail)"
[value]="accordionMultipleValue">
@for (section of sections; track section.id) {
@if ($index <= lastShownSectionIndex) {
@if ($index <= lastShownSectionIndex && !section.hiddenbynumsections && section.id !== allSectionsId &&
section.id !== stealthModulesSectionId && !section.component) {
<core-course-section
*ngIf="!section.hiddenbynumsections && section.id !== allSectionsId && section.id !== stealthModulesSectionId"
[course]="course" [section]="section" [lastModuleViewed]="lastModuleViewed" [viewedModules]="viewedModules"
[collapsible]="true" />
[course]="course" [section]="section" [lastModuleViewed]="lastModuleViewed" [viewedModules]="viewedModules"
[collapsible]="true" [sections]="subSections" />
}
}
</ion-accordion-group>

View File

@ -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<void> {
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;
}

View File

@ -19,12 +19,4 @@
}
}
}
&.non-collapsible ::ng-deep {
ion-item.divider.course-section {
ion-icon.ion-accordion-toggle-icon {
display: none;
}
}
}
}