MOBILE-4660 storagemanager: Manage subsections on storage manager
parent
a169d9301a
commit
a2c6a5b578
|
@ -46,101 +46,109 @@
|
||||||
</ion-card-header>
|
</ion-card-header>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
<ion-accordion-group [multiple]="true" (ionChange)="accordionGroupChange($event.detail)" #accordionGroup>
|
<ion-accordion-group [multiple]="true" (ionChange)="accordionGroupChange($event.detail)" [value]="accordionMultipleValue">
|
||||||
<ng-container *ngFor="let section of sections">
|
<ng-container *ngFor="let section of sections">
|
||||||
<ion-card class="section" *ngIf="section.modules.length > 0">
|
<ng-container *ngTemplateOutlet="sectionCard; context: { section }" />
|
||||||
<ion-accordion [value]="section.id" toggleIconSlot="start">
|
|
||||||
<ion-item [detail]="false" slot="header" class="card-header">
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading ion-text-wrap">
|
|
||||||
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="section.course"
|
|
||||||
[adaptImg]="false" />
|
|
||||||
</p>
|
|
||||||
<ion-badge [color]="section.downloadStatus === statusDownloaded ? 'success' : 'light'"
|
|
||||||
*ngIf="!section.calculatingSize && section.totalSize > 0">
|
|
||||||
<ion-icon name="fam-cloud-done" *ngIf="section.downloadStatus === statusDownloaded"
|
|
||||||
[attr.aria-label]="'core.downloaded' | translate" />{{ section.totalSize | coreBytesToSize }}
|
|
||||||
</ion-badge>
|
|
||||||
<ion-badge color="light" *ngIf="section.calculatingSize">
|
|
||||||
{{ 'core.calculating' | translate }}
|
|
||||||
</ion-badge>
|
|
||||||
<!-- Download progress. -->
|
|
||||||
<p *ngIf="downloadEnabled && section.isDownloading">
|
|
||||||
<core-progress-bar [progress]="section.total === 0 ? -1 : (section.count / section.total) * 100" />
|
|
||||||
</p>
|
|
||||||
</ion-label>
|
|
||||||
<div class="storage-buttons" slot="end"
|
|
||||||
*ngIf="(!section.calculatingSize && section.totalSize > 0) || downloadEnabled">
|
|
||||||
<div *ngIf="downloadEnabled" slot="end" class="core-button-spinner">
|
|
||||||
<core-download-refresh *ngIf="!section.isDownloading && section.downloadStatus !== statusDownloaded"
|
|
||||||
[status]="section.downloadStatus" [enabled]="true" (action)="prefecthSection(section)"
|
|
||||||
[loading]="section.isDownloading || section.isCalculating" [canTrustDownload]="true"
|
|
||||||
[statusesTranslatable]="{notdownloaded: 'addon.storagemanager.downloaddatafrom' }"
|
|
||||||
[statusSubject]="section.name" />
|
|
||||||
|
|
||||||
<ion-badge class="core-course-download-section-progress"
|
|
||||||
*ngIf="section.isDownloading && section.count < section.total" role="progressbar"
|
|
||||||
[attr.aria-valuemax]="section.total" [attr.aria-valuenow]="section.count"
|
|
||||||
[attr.aria-valuetext]="'core.course.downloadsectionprogressdescription' | translate:section">
|
|
||||||
{{section.count}} / {{section.total}}
|
|
||||||
</ion-badge>
|
|
||||||
</div>
|
|
||||||
<ion-button (click)="deleteForSection($event, section)"
|
|
||||||
*ngIf="!section.calculatingSize && section.totalSize > 0" color="danger" fill="clear">
|
|
||||||
<ion-icon name="fas-trash" slot="icon-only"
|
|
||||||
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: section.name }" />
|
|
||||||
</ion-button>
|
|
||||||
</div>
|
|
||||||
</ion-item>
|
|
||||||
<ion-card-content slot="content">
|
|
||||||
<ng-container *ngIf="section.expanded">
|
|
||||||
<ng-container *ngFor="let module of section.modules">
|
|
||||||
<ion-item class="core-course-storage-activity"
|
|
||||||
*ngIf="downloadEnabled || (!module.calculatingSize && module.totalSize > 0)">
|
|
||||||
<core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon"
|
|
||||||
[modname]="module.modname" [componentId]="module.instance"
|
|
||||||
[fallbackTranslation]="module.modplural" [isBranded]="module.branded" />
|
|
||||||
<ion-label class="ion-text-wrap">
|
|
||||||
<p class="item-heading {{module.handlerData!.class}} addon-storagemanager-module-size">
|
|
||||||
<core-format-text [text]="module.handlerData.title" [courseId]="module.course"
|
|
||||||
contextLevel="module" [contextInstanceId]="module.id" [adaptImg]="false" />
|
|
||||||
</p>
|
|
||||||
<ion-badge [color]="module.downloadStatus === statusDownloaded ? 'success' : 'light'"
|
|
||||||
*ngIf="!module.calculatingSize && module.totalSize > 0">
|
|
||||||
<ion-icon name="fam-cloud-done" *ngIf="module.downloadStatus === statusDownloaded"
|
|
||||||
[attr.aria-label]="'core.downloaded' | translate" />{{ module.totalSize |
|
|
||||||
coreBytesToSize }}
|
|
||||||
</ion-badge>
|
|
||||||
<ion-badge color="light" *ngIf="module.calculatingSize ||
|
|
||||||
(section.isDownloading && module.downloadStatus === statusDownloaded)">
|
|
||||||
{{ 'core.calculating' | translate }}
|
|
||||||
</ion-badge>
|
|
||||||
</ion-label>
|
|
||||||
|
|
||||||
<div class="storage-buttons" slot="end">
|
|
||||||
<core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton &&
|
|
||||||
module.downloadStatus !== statusDownloaded" [status]="module.downloadStatus" [enabled]="true"
|
|
||||||
[canTrustDownload]="true" [loading]="module.spinner || module.handlerData.spinner"
|
|
||||||
(action)="prefetchModule(module)"
|
|
||||||
[statusesTranslatable]="{notdownloaded: 'addon.storagemanager.downloaddatafrom' }"
|
|
||||||
[statusSubject]="module.name" />
|
|
||||||
<ion-button fill="clear" (click)="deleteForModule($event, module, section)"
|
|
||||||
*ngIf="!module.calculatingSize && module.totalSize > 0" color="danger">
|
|
||||||
<ion-icon name="fas-trash" slot="icon-only" [attr.aria-label]="
|
|
||||||
'addon.storagemanager.deletedatafrom' | translate: { name: module.name }" />
|
|
||||||
</ion-button>
|
|
||||||
<p *ngIf="!downloadEnabled || !module.handlerData?.showDownloadButton" class="sr-only">
|
|
||||||
{{ 'core.notdownloadable' | translate }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</ion-item>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
</ion-card-content>
|
|
||||||
</ion-accordion>
|
|
||||||
</ion-card>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-accordion-group>
|
</ion-accordion-group>
|
||||||
|
|
||||||
</core-loading>
|
</core-loading>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template #sectionCard let-section="section">
|
||||||
|
<ion-card class="section" *ngIf="section.modules.length > 0" [id]="'addons-course-storage-'+section.id">
|
||||||
|
<ion-accordion [value]="section.id" toggleIconSlot="start">
|
||||||
|
<ion-item [detail]="false" slot="header" class="card-header">
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading ion-text-wrap">
|
||||||
|
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="section.course"
|
||||||
|
[adaptImg]="false" />
|
||||||
|
</p>
|
||||||
|
<ion-badge [color]="section.downloadStatus === statusDownloaded ? 'success' : 'light'"
|
||||||
|
*ngIf="!section.calculatingSize && section.totalSize > 0">
|
||||||
|
<ion-icon name="fam-cloud-done" *ngIf="section.downloadStatus === statusDownloaded"
|
||||||
|
[attr.aria-label]="'core.downloaded' | translate" />{{ section.totalSize | coreBytesToSize }}
|
||||||
|
</ion-badge>
|
||||||
|
<ion-badge color="light" *ngIf="section.calculatingSize">
|
||||||
|
{{ 'core.calculating' | translate }}
|
||||||
|
</ion-badge>
|
||||||
|
<!-- Download progress. -->
|
||||||
|
<p *ngIf="downloadEnabled && section.isDownloading">
|
||||||
|
<core-progress-bar [progress]="section.total === 0 ? -1 : (section.count / section.total) * 100" />
|
||||||
|
</p>
|
||||||
|
</ion-label>
|
||||||
|
<div class="storage-buttons" slot="end" *ngIf="(!section.calculatingSize && section.totalSize > 0) || downloadEnabled">
|
||||||
|
<div *ngIf="downloadEnabled" slot="end" class="core-button-spinner">
|
||||||
|
<core-download-refresh *ngIf="!section.isDownloading && section.downloadStatus !== statusDownloaded"
|
||||||
|
[status]="section.downloadStatus" [enabled]="true" (action)="prefecthSection(section)"
|
||||||
|
[loading]="section.isDownloading || section.isCalculating" [canTrustDownload]="true"
|
||||||
|
[statusesTranslatable]="{notdownloaded: 'addon.storagemanager.downloaddatafrom' }"
|
||||||
|
[statusSubject]="section.name" />
|
||||||
|
|
||||||
|
<ion-badge class="core-course-download-section-progress"
|
||||||
|
*ngIf="section.isDownloading && section.count < section.total" role="progressbar"
|
||||||
|
[attr.aria-valuemax]="section.total" [attr.aria-valuenow]="section.count"
|
||||||
|
[attr.aria-valuetext]="'core.course.downloadsectionprogressdescription' | translate:section">
|
||||||
|
{{section.count}} / {{section.total}}
|
||||||
|
</ion-badge>
|
||||||
|
</div>
|
||||||
|
<ion-button (click)="deleteForSection($event, section)" *ngIf="!section.calculatingSize && section.totalSize > 0"
|
||||||
|
color="danger" fill="clear">
|
||||||
|
<ion-icon name="fas-trash" slot="icon-only"
|
||||||
|
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: section.name }" />
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</ion-item>
|
||||||
|
<ion-card-content slot="content">
|
||||||
|
<ng-container *ngIf="section.expanded">
|
||||||
|
<ng-container *ngFor="let module of section.modules">
|
||||||
|
@if (module.subSection) {
|
||||||
|
<ng-container *ngTemplateOutlet="sectionCard; context: { section: module.subSection }" />
|
||||||
|
} @else {
|
||||||
|
<ion-item class="core-course-storage-activity"
|
||||||
|
*ngIf="downloadEnabled || (!module.calculatingSize && module.totalSize > 0)">
|
||||||
|
<core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon"
|
||||||
|
[modname]="module.modname" [componentId]="module.instance" [fallbackTranslation]="module.modplural"
|
||||||
|
[isBranded]="module.branded" />
|
||||||
|
<ion-label class="ion-text-wrap">
|
||||||
|
<p class="item-heading {{module.handlerData!.class}} addon-storagemanager-module-size">
|
||||||
|
<core-format-text [text]="module.handlerData.title" [courseId]="module.course" contextLevel="module"
|
||||||
|
[contextInstanceId]="module.id" [adaptImg]="false" />
|
||||||
|
</p>
|
||||||
|
<ion-badge [color]="module.downloadStatus === statusDownloaded ? 'success' : 'light'"
|
||||||
|
*ngIf="!module.calculatingSize && module.totalSize > 0">
|
||||||
|
<ion-icon name="fam-cloud-done" *ngIf="module.downloadStatus === statusDownloaded"
|
||||||
|
[attr.aria-label]="'core.downloaded' | translate" />{{ module.totalSize |
|
||||||
|
coreBytesToSize }}
|
||||||
|
</ion-badge>
|
||||||
|
<ion-badge color="light" *ngIf="module.calculatingSize ||
|
||||||
|
(section.isDownloading && module.downloadStatus === statusDownloaded)">
|
||||||
|
{{ 'core.calculating' | translate }}
|
||||||
|
</ion-badge>
|
||||||
|
</ion-label>
|
||||||
|
|
||||||
|
<div class="storage-buttons" slot="end">
|
||||||
|
<core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton &&
|
||||||
|
module.downloadStatus !== statusDownloaded" [status]="module.downloadStatus" [enabled]="true"
|
||||||
|
[canTrustDownload]="true" [loading]="module.spinner || module.handlerData.spinner"
|
||||||
|
(action)="prefetchModule(module)"
|
||||||
|
[statusesTranslatable]="{notdownloaded: 'addon.storagemanager.downloaddatafrom' }"
|
||||||
|
[statusSubject]="module.name" />
|
||||||
|
<ion-button fill="clear" (click)="deleteForModule($event, module)"
|
||||||
|
*ngIf="!module.calculatingSize && module.totalSize > 0" color="danger">
|
||||||
|
<ion-icon name="fas-trash" slot="icon-only" [attr.aria-label]="
|
||||||
|
'addon.storagemanager.deletedatafrom' | translate: { name: module.name }" />
|
||||||
|
</ion-button>
|
||||||
|
<p *ngIf="!downloadEnabled || !module.handlerData?.showDownloadButton" class="sr-only">
|
||||||
|
{{ 'core.notdownloadable' | translate }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</ion-item>
|
||||||
|
}
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ion-card-content>
|
||||||
|
</ion-accordion>
|
||||||
|
</ion-card>
|
||||||
|
</ng-template>
|
||||||
|
|
|
@ -17,11 +17,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-expanded {
|
|
||||||
ion-item.card-header {
|
|
||||||
--border-width: 0 0 1px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ion-card-content {
|
ion-card-content {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { CoreConstants, DownloadStatus } from '@/core/constants';
|
import { CoreConstants, DownloadStatus } from '@/core/constants';
|
||||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
|
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
|
||||||
import {
|
import {
|
||||||
CoreCourseHelper,
|
CoreCourseHelper,
|
||||||
|
@ -25,7 +25,7 @@ import {
|
||||||
CoreCourseModulePrefetchDelegate,
|
CoreCourseModulePrefetchDelegate,
|
||||||
CoreCourseModulePrefetchHandler } from '@features/course/services/module-prefetch-delegate';
|
CoreCourseModulePrefetchHandler } from '@features/course/services/module-prefetch-delegate';
|
||||||
import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses';
|
import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses';
|
||||||
import { AccordionGroupChangeEventDetail, IonAccordionGroup } from '@ionic/angular';
|
import { AccordionGroupChangeEventDetail } from '@ionic/angular';
|
||||||
import { CoreLoadings } from '@services/loadings';
|
import { CoreLoadings } from '@services/loadings';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
|
@ -47,14 +47,13 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
})
|
})
|
||||||
export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@ViewChild('accordionGroup', { static: true }) accordionGroup!: IonAccordionGroup;
|
|
||||||
|
|
||||||
courseId!: number;
|
courseId!: number;
|
||||||
title = '';
|
title = '';
|
||||||
loaded = false;
|
loaded = false;
|
||||||
sections: AddonStorageManagerCourseSection[] = [];
|
sections: AddonStorageManagerCourseSection[] = [];
|
||||||
totalSize = 0;
|
totalSize = 0;
|
||||||
calculatingSize = true;
|
calculatingSize = true;
|
||||||
|
accordionMultipleValue: string[] = [];
|
||||||
|
|
||||||
downloadEnabled = false;
|
downloadEnabled = false;
|
||||||
downloadCourseEnabled = false;
|
downloadCourseEnabled = false;
|
||||||
|
@ -116,7 +115,8 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
const sections = (await CoreCourse.getSections(this.courseId, false, true))
|
const sections = (await CoreCourse.getSections(this.courseId, false, true))
|
||||||
.filter((section) => !CoreCourseHelper.isSectionStealth(section));
|
.filter((section) => !CoreCourseHelper.isSectionStealth(section));
|
||||||
this.sections = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
|
|
||||||
|
const sectionsToRender = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
|
||||||
.map(section => ({
|
.map(section => ({
|
||||||
...section,
|
...section,
|
||||||
totalSize: 0,
|
totalSize: 0,
|
||||||
|
@ -128,15 +128,29 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const subSections = sectionsToRender.filter((section) => section.component === 'mod_subsection');
|
||||||
|
|
||||||
|
this.sections = sectionsToRender.filter((section) => section.component !== 'mod_subsection');
|
||||||
|
this.sections.forEach((section) => {
|
||||||
|
section.modules.forEach((module) => {
|
||||||
|
if (module.modname === 'subsection') {
|
||||||
|
module.subSection = subSections.find((section) =>
|
||||||
|
section.component === 'mod_subsection' && section.itemid === module.instance);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
|
|
||||||
this.accordionGroup.value = String(initialSectionId);
|
if (initialSectionId !== undefined) {
|
||||||
|
this.accordionMultipleValue.push(initialSectionId.toString());
|
||||||
|
|
||||||
CoreDom.scrollToElement(
|
CoreDom.scrollToElement(
|
||||||
this.elementRef.nativeElement,
|
this.elementRef.nativeElement,
|
||||||
'.accordion-expanded',
|
`#addons-course-storage-${initialSectionId}`,
|
||||||
{ addYAxis: -10 },
|
{ addYAxis: -10 },
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.initSizes(),
|
this.initSizes(),
|
||||||
|
@ -158,7 +172,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
// Listen for changes in course status.
|
// Listen for changes in course status.
|
||||||
this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => {
|
this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => {
|
||||||
if (data.courseId == this.courseId || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
|
if (data.courseId === this.courseId || data.courseId === CoreCourseProvider.ALL_COURSES_CLEARED) {
|
||||||
this.updateCourseStatus(data.status);
|
this.updateCourseStatus(data.status);
|
||||||
}
|
}
|
||||||
}, CoreSites.getCurrentSiteId());
|
}, CoreSites.getCurrentSiteId());
|
||||||
|
@ -213,17 +227,20 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the affected section.
|
// Get the affected section.
|
||||||
const section = this.sections.find(section => section.id == data.sectionId);
|
const sectionFinder = CoreCourseHelper.findSectionWithSubsection(this.sections, data.sectionId);
|
||||||
if (!section) {
|
if (!sectionFinder?.section) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recalculate the status.
|
// Recalculate the status.
|
||||||
await CoreCourseHelper.calculateSectionStatus(section, this.courseId, false);
|
await CoreCourseHelper.calculateSectionStatus(sectionFinder.section, this.courseId, false);
|
||||||
|
if (sectionFinder.subSection) {
|
||||||
|
await CoreCourseHelper.calculateSectionStatus(sectionFinder.subSection, this.courseId, false);
|
||||||
|
}
|
||||||
|
|
||||||
if (section.isDownloading && !CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) {
|
if (sectionFinder.section.isDownloading && !CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) {
|
||||||
// All the modules are now downloading, set a download all promise.
|
// All the modules are now downloading, set a download all promise.
|
||||||
this.prefecthSection(section);
|
this.prefecthSection(sectionFinder.section);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
CoreSites.getCurrentSiteId(),
|
CoreSites.getCurrentSiteId(),
|
||||||
|
@ -233,36 +250,46 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);
|
CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);
|
||||||
|
|
||||||
this.sections.forEach((section) => {
|
this.sections.forEach((section) => {
|
||||||
section.modules.forEach((module) => {
|
this.calculateModulesStatusOnSection(section);
|
||||||
if (module.handlerData?.showDownloadButton) {
|
|
||||||
|
|
||||||
module.spinner = true;
|
|
||||||
// Listen for changes on this module status, even if download isn't enabled.
|
|
||||||
module.prefetchHandler = CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor(module.modname);
|
|
||||||
this.calculateModuleStatus(module);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.moduleStatusObserver = CoreEvents.on(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => {
|
this.moduleStatusObserver = CoreEvents.on(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => {
|
||||||
let module: AddonStorageManagerModule | undefined;
|
let moduleFound: AddonStorageManagerModule | undefined;
|
||||||
|
|
||||||
this.sections.some((section) => {
|
this.sections.some((section) =>
|
||||||
module = section.modules.find((module) =>
|
section.modules.some((module) => {
|
||||||
module.id == data.componentId && module.prefetchHandler && data.component == module.prefetchHandler?.component);
|
if (module.subSection) {
|
||||||
|
return module.subSection.modules.some((module) => {
|
||||||
|
if (module.id === data.componentId &&
|
||||||
|
module.prefetchHandler &&
|
||||||
|
data.component === module.prefetchHandler?.component) {
|
||||||
|
moduleFound = module;
|
||||||
|
|
||||||
return !!module;
|
return true;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (module.id === data.componentId &&
|
||||||
|
module.prefetchHandler &&
|
||||||
|
data.component === module.prefetchHandler?.component) {
|
||||||
|
moduleFound = module;
|
||||||
|
|
||||||
if (!module) {
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!moduleFound) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call determineModuleStatus to get the right status to display.
|
// Call determineModuleStatus to get the right status to display.
|
||||||
const status = CoreCourseModulePrefetchDelegate.determineModuleStatus(module, data.status);
|
const status = CoreCourseModulePrefetchDelegate.determineModuleStatus(moduleFound, data.status);
|
||||||
|
|
||||||
// Update the status.
|
// Update the status.
|
||||||
this.updateModuleStatus(module, status);
|
this.updateModuleStatus(moduleFound, status);
|
||||||
}, CoreSites.getCurrentSiteId());
|
}, CoreSites.getCurrentSiteId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,50 +297,24 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
* Init section, course and modules sizes.
|
* Init section, course and modules sizes.
|
||||||
*/
|
*/
|
||||||
protected async initSizes(): Promise<void> {
|
protected async initSizes(): Promise<void> {
|
||||||
await Promise.all(this.sections.map(async (section) => {
|
const modules = this.getAllModulesList();
|
||||||
await Promise.all(section.modules.map(async (module) => {
|
await Promise.all(modules.map(async (module) => {
|
||||||
// Note: This function only gets the size for modules which are downloadable.
|
await this.calculateModuleSize(module);
|
||||||
// For other modules it always returns 0, even if they have downloaded some files.
|
|
||||||
// However there is no 100% reliable way to actually track the files in this case.
|
|
||||||
// You can maybe guess it based on the component and componentid.
|
|
||||||
// But these aren't necessarily consistent, for example mod_frog vs mmaModFrog.
|
|
||||||
// There is nothing enforcing correct values.
|
|
||||||
// Most modules which have large files are downloadable, so I think this is sufficient.
|
|
||||||
const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId);
|
|
||||||
|
|
||||||
// There are some cases where the return from this is not a valid number.
|
|
||||||
if (!isNaN(size)) {
|
|
||||||
module.totalSize = Number(size);
|
|
||||||
section.totalSize += size;
|
|
||||||
this.totalSize += size;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.calculatingSize = false;
|
|
||||||
}));
|
|
||||||
|
|
||||||
section.calculatingSize = false;
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.calculatingSize = false;
|
await this.updateModulesSizes(modules);
|
||||||
|
|
||||||
// Mark course as not downloaded if course size is 0.
|
|
||||||
if (this.totalSize == 0) {
|
|
||||||
this.markCourseAsNotDownloaded();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the sizes of some modules.
|
* Update the sizes of some modules.
|
||||||
*
|
*
|
||||||
* @param modules Modules.
|
* @param modules Modules.
|
||||||
* @param section Section the modules belong to.
|
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async updateModulesSizes(
|
protected async updateModulesSizes(modules: AddonStorageManagerModule[]): Promise<void> {
|
||||||
modules: AddonStorageManagerModule[],
|
|
||||||
section?: AddonStorageManagerCourseSection,
|
|
||||||
): Promise<void> {
|
|
||||||
this.calculatingSize = true;
|
this.calculatingSize = true;
|
||||||
|
let section: AddonStorageManagerCourseSection | undefined;
|
||||||
|
let subSection: AddonStorageManagerCourseSection | undefined;
|
||||||
|
|
||||||
await Promise.all(modules.map(async (module) => {
|
await Promise.all(modules.map(async (module) => {
|
||||||
if (module.calculatingSize) {
|
if (module.calculatingSize) {
|
||||||
|
@ -321,38 +322,59 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.calculatingSize = true;
|
module.calculatingSize = true;
|
||||||
|
|
||||||
|
const sectionFinder = CoreCourseHelper.findSectionWithSubsection(this.sections, module.section);
|
||||||
|
section = sectionFinder?.section;
|
||||||
|
if (section) {
|
||||||
|
section.calculatingSize = true;
|
||||||
|
|
||||||
|
subSection = sectionFinder?.subSection;
|
||||||
|
if (subSection) {
|
||||||
|
subSection.calculatingSize = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
|
|
||||||
if (!section) {
|
await this.calculateModuleSize(module);
|
||||||
section = this.sections.find((section) => section.modules.some((mod) => mod.id === module.id));
|
|
||||||
if (section) {
|
|
||||||
section.calculatingSize = true;
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId);
|
|
||||||
|
|
||||||
const diff = (isNaN(size) ? 0 : size) - (module.totalSize ?? 0);
|
|
||||||
|
|
||||||
module.totalSize = Number(size);
|
|
||||||
this.totalSize += diff;
|
|
||||||
if (section) {
|
|
||||||
section.totalSize += diff;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore errors, it shouldn't happen.
|
|
||||||
} finally {
|
|
||||||
module.calculatingSize = false;
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.calculatingSize = false;
|
// Update section and total sizes.
|
||||||
if (section) {
|
this.totalSize = 0;
|
||||||
|
this.sections.forEach((section) => {
|
||||||
|
section.totalSize = 0;
|
||||||
|
section.modules.forEach((module) => {
|
||||||
|
if (module.subSection) {
|
||||||
|
const subSection = module.subSection;
|
||||||
|
|
||||||
|
subSection.totalSize = 0;
|
||||||
|
subSection.modules.forEach((module) => {
|
||||||
|
if (module.totalSize && module.totalSize > 0) {
|
||||||
|
subSection.totalSize += module.totalSize;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
subSection.calculatingSize = false;
|
||||||
|
|
||||||
|
section.totalSize += module.subSection.totalSize;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module.totalSize && module.totalSize > 0) {
|
||||||
|
section.totalSize += module.totalSize;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
section.calculatingSize = false;
|
section.calculatingSize = false;
|
||||||
|
this.totalSize += section.totalSize;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.calculatingSize = false;
|
||||||
|
|
||||||
|
// Mark course as not downloaded if course size is 0.
|
||||||
|
if (this.totalSize === 0) {
|
||||||
|
this.markCourseAsNotDownloaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -380,16 +402,9 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modules: AddonStorageManagerModule[] = [];
|
const modules = this.getAllModulesList().filter((module) => module.totalSize && module.totalSize > 0);
|
||||||
this.sections.forEach((section) => {
|
|
||||||
section.modules.forEach((module) => {
|
|
||||||
if (module.totalSize && module.totalSize > 0) {
|
|
||||||
modules.push(module);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.deleteModules(modules);
|
await this.deleteModules(modules);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -419,12 +434,22 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
const modules: AddonStorageManagerModule[] = [];
|
const modules: AddonStorageManagerModule[] = [];
|
||||||
section.modules.forEach((module) => {
|
section.modules.forEach((module) => {
|
||||||
|
if (module.subSection) {
|
||||||
|
module.subSection.modules.forEach((module) => {
|
||||||
|
if (module.totalSize && module.totalSize > 0) {
|
||||||
|
modules.push(module);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (module.totalSize && module.totalSize > 0) {
|
if (module.totalSize && module.totalSize > 0) {
|
||||||
modules.push(module);
|
modules.push(module);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.deleteModules(modules, section);
|
await this.deleteModules(modules);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -432,12 +457,10 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
*
|
*
|
||||||
* @param event Event object.
|
* @param event Event object.
|
||||||
* @param module Module details
|
* @param module Module details
|
||||||
* @param section Section the module belongs to.
|
|
||||||
*/
|
*/
|
||||||
async deleteForModule(
|
async deleteForModule(
|
||||||
event: Event,
|
event: Event,
|
||||||
module: AddonStorageManagerModule,
|
module: AddonStorageManagerModule,
|
||||||
section: AddonStorageManagerCourseSection,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -459,35 +482,23 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deleteModules([module], section);
|
await this.deleteModules([module]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the specified modules, showing the loading overlay while it happens.
|
* Deletes the specified modules, showing the loading overlay while it happens.
|
||||||
*
|
*
|
||||||
* @param modules Modules to delete
|
* @param modules Modules to delete
|
||||||
* @param section Section the modules belong to.
|
|
||||||
* @returns Promise<void> Once deleting has finished
|
* @returns Promise<void> Once deleting has finished
|
||||||
*/
|
*/
|
||||||
protected async deleteModules(modules: AddonStorageManagerModule[], section?: AddonStorageManagerCourseSection): Promise<void> {
|
protected async deleteModules(modules: AddonStorageManagerModule[]): Promise<void> {
|
||||||
const modal = await CoreLoadings.show('core.deleting', true);
|
const modal = await CoreLoadings.show('core.deleting', true);
|
||||||
|
|
||||||
const promises: Promise<void>[] = [];
|
const promises = modules.map(async (module) => {
|
||||||
modules.forEach((module) => {
|
|
||||||
// Remove the files.
|
// Remove the files.
|
||||||
const promise = CoreCourseHelper.removeModuleStoredData(module, this.courseId).then(() => {
|
await CoreCourseHelper.removeModuleStoredData(module, this.courseId);
|
||||||
const moduleSize = module.totalSize || 0;
|
|
||||||
// When the files and cache are removed, update the size.
|
|
||||||
if (section) {
|
|
||||||
section.totalSize -= moduleSize;
|
|
||||||
}
|
|
||||||
this.totalSize -= moduleSize;
|
|
||||||
module.totalSize = 0;
|
|
||||||
|
|
||||||
return;
|
module.totalSize = 0;
|
||||||
});
|
|
||||||
|
|
||||||
promises.push(promise);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -497,13 +508,9 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
} finally {
|
} finally {
|
||||||
modal.dismiss();
|
modal.dismiss();
|
||||||
|
|
||||||
await this.updateModulesSizes(modules, section);
|
await this.updateModulesSizes(modules);
|
||||||
CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);
|
CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);
|
||||||
|
|
||||||
// For delete all, reset all section sizes so icons are updated.
|
|
||||||
if (this.totalSize === 0) {
|
|
||||||
this.sections.map(section => section.totalSize = 0);
|
|
||||||
}
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -551,8 +558,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingsection', true);
|
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingsection', true);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await this.updateModulesSizes(section.modules, section);
|
await this.updateModulesSizes(section.modules);
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// User cancelled or there was an error calculating the size.
|
// User cancelled or there was an error calculating the size.
|
||||||
|
@ -602,7 +608,6 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
module.spinner = false;
|
module.spinner = false;
|
||||||
|
|
||||||
await this.updateModulesSizes([module]);
|
await this.updateModulesSizes([module]);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -624,6 +629,24 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate all modules status on a section.
|
||||||
|
*
|
||||||
|
* @param section Section to check.
|
||||||
|
*/
|
||||||
|
protected async calculateModulesStatusOnSection(section: AddonStorageManagerCourseSection): Promise<void> {
|
||||||
|
await Promise.all(section.modules.map(async (module) => {
|
||||||
|
if (module.subSection) {
|
||||||
|
await this.calculateModulesStatusOnSection(module.subSection);
|
||||||
|
} else if (module.handlerData?.showDownloadButton) {
|
||||||
|
module.spinner = true;
|
||||||
|
// Listen for changes on this module status, even if download isn't enabled.
|
||||||
|
module.prefetchHandler = CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor(module.modname);
|
||||||
|
await this.calculateModuleStatus(module);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate and show module status.
|
* Calculate and show module status.
|
||||||
*
|
*
|
||||||
|
@ -664,6 +687,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the course object.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @returns Promise resolved with the course object if found.
|
||||||
|
*/
|
||||||
protected async getCourse(courseId: number): Promise<CoreCourseAnyCourseData | undefined> {
|
protected async getCourse(courseId: number): Promise<CoreCourseAnyCourseData | undefined> {
|
||||||
try {
|
try {
|
||||||
// Check if user is enrolled. If enrolled, no guest access.
|
// Check if user is enrolled. If enrolled, no guest access.
|
||||||
|
@ -709,8 +738,9 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
isGuest: this.isGuest,
|
isGuest: this.isGuest,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await Promise.all(this.sections.map(section => this.updateModulesSizes(section.modules, section)));
|
|
||||||
this.changeDetectorRef.markForCheck();
|
const modules = this.getAllModulesList();
|
||||||
|
await this.updateModulesSizes(modules);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.isDestroyed) {
|
if (this.isDestroyed) {
|
||||||
return;
|
return;
|
||||||
|
@ -720,19 +750,76 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all modules list.
|
||||||
|
*
|
||||||
|
* @returns All modules list.
|
||||||
|
*/
|
||||||
|
protected getAllModulesList(): AddonStorageManagerModule[] {
|
||||||
|
const modules: AddonStorageManagerModule[] = [];
|
||||||
|
this.sections.forEach((section) => {
|
||||||
|
section.modules.forEach((module) => {
|
||||||
|
if (module.subSection) {
|
||||||
|
module.subSection.modules.forEach((module) => {
|
||||||
|
modules.push(module);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modules.push(module);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the size of a module.
|
||||||
|
*
|
||||||
|
* @param module Module to calculate.
|
||||||
|
*/
|
||||||
|
protected async calculateModuleSize(module: AddonStorageManagerModule): Promise<void> {
|
||||||
|
module.calculatingSize = true;
|
||||||
|
|
||||||
|
// Note: This function only gets the size for modules which are downloadable.
|
||||||
|
// For other modules it always returns 0, even if they have downloaded some files.
|
||||||
|
// However there is no 100% reliable way to actually track the files in this case.
|
||||||
|
// You can maybe guess it based on the component and componentid.
|
||||||
|
// But these aren't necessarily consistent, for example mod_frog vs mmaModFrog.
|
||||||
|
// There is nothing enforcing correct values.
|
||||||
|
// Most modules which have large files are downloadable, so I think this is sufficient.
|
||||||
|
const size = await CoreUtils.ignoreErrors(CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId));
|
||||||
|
|
||||||
|
if (size !== undefined) {
|
||||||
|
// There are some cases where the return from this is not a valid number.
|
||||||
|
module.totalSize = !isNaN(size) ? Number(size) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
module.calculatingSize = false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle expand status.
|
* Toggle expand status.
|
||||||
*
|
*
|
||||||
* @param event Event object.
|
* @param event Event object.
|
||||||
*/
|
*/
|
||||||
accordionGroupChange(event: AccordionGroupChangeEventDetail): void {
|
accordionGroupChange(event: AccordionGroupChangeEventDetail): void {
|
||||||
|
const sectionIds = event.value as string[] | [];
|
||||||
this.sections.forEach((section) => {
|
this.sections.forEach((section) => {
|
||||||
section.expanded = false;
|
section.expanded = false;
|
||||||
|
section.modules.forEach((section) => {
|
||||||
|
if (section.subSection) {
|
||||||
|
section.subSection.expanded = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
event.value.forEach((sectionId) => {
|
|
||||||
const section = this.sections.find((section) => section.id === Number(sectionId));
|
sectionIds.forEach((sectionId) => {
|
||||||
if (section) {
|
const sectionToExpand = CoreCourseHelper.findSectionById(this.sections, Number(sectionId));
|
||||||
section.expanded = true;
|
if (sectionToExpand) {
|
||||||
|
sectionToExpand.expanded = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -748,6 +835,10 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
this.sections.forEach((section) => {
|
this.sections.forEach((section) => {
|
||||||
section.modules.forEach((module) => {
|
section.modules.forEach((module) => {
|
||||||
|
module.subSection?.modules.forEach((module) => {
|
||||||
|
module.handlerData?.onDestroy?.();
|
||||||
|
});
|
||||||
|
|
||||||
module.handlerData?.onDestroy?.();
|
module.handlerData?.onDestroy?.();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -769,4 +860,5 @@ type AddonStorageManagerModule = CoreCourseModuleData & {
|
||||||
prefetchHandler?: CoreCourseModulePrefetchHandler;
|
prefetchHandler?: CoreCourseModulePrefetchHandler;
|
||||||
spinner?: boolean;
|
spinner?: boolean;
|
||||||
downloadStatus?: DownloadStatus;
|
downloadStatus?: DownloadStatus;
|
||||||
|
subSection?: AddonStorageManagerCourseSection;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1600,7 +1600,7 @@ export class CoreCourseHelperProvider {
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Promise resolved when the download finishes.
|
* @returns Promise resolved when the download finishes.
|
||||||
*/
|
*/
|
||||||
async prefetchCourse(
|
protected async prefetchCourse(
|
||||||
course: CoreCourseAnyCourseData,
|
course: CoreCourseAnyCourseData,
|
||||||
sections: CoreCourseWSSection[],
|
sections: CoreCourseWSSection[],
|
||||||
courseHandlers: CoreCourseOptionsHandlerToDisplay[],
|
courseHandlers: CoreCourseOptionsHandlerToDisplay[],
|
||||||
|
|
Loading…
Reference in New Issue