MOBILE-3686 courses: Merge list courses and my courses page
parent
18f20dc12e
commit
8acb8b74e2
|
@ -1551,6 +1551,7 @@
|
|||
"core.courses.selfenrolment": "local_moodlemobileapp",
|
||||
"core.courses.sendpaymentbutton": "enrol_paypal",
|
||||
"core.courses.show": "block_myoverview",
|
||||
"core.courses.showonlyenrolled": "local_moodlemobileapp",
|
||||
"core.courses.therearecourses": "moodle",
|
||||
"core.courses.totalcoursesearchresults": "local_moodlemobileapp",
|
||||
"core.currentdevice": "local_moodlemobileapp",
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
<img [src]="course.courseImage" core-external-content alt=""/>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
<p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)"
|
||||
class="core-course-additional-info">
|
||||
<span *ngIf="course.categoryname" class="core-course-category">
|
||||
|
@ -19,10 +23,6 @@
|
|||
</core-format-text>
|
||||
</span>
|
||||
</p>
|
||||
<h2>
|
||||
<core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
<p *ngIf="isEnrolled && course.progress! >= 0 && course.completionusertracked !== false">
|
||||
<core-progress-bar [progress]="course.progress" a11yText="core.courses.aria:courseprogress"></core-progress-bar>
|
||||
</p>
|
||||
|
|
|
@ -15,8 +15,11 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreCourses, CoreCourseSearchedData } from '../../services/courses';
|
||||
import { CoreCoursesHelper, CoreCourseWithImageAndColor } from '../../services/courses-helper';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreCourseListItem, CoreCourses } from '../../services/courses';
|
||||
import { CoreCoursesHelper } from '../../services/courses-helper';
|
||||
|
||||
/**
|
||||
* This directive is meant to display an item for a list of courses.
|
||||
|
@ -32,10 +35,7 @@ import { CoreCoursesHelper, CoreCourseWithImageAndColor } from '../../services/c
|
|||
})
|
||||
export class CoreCoursesCourseListItemComponent implements OnInit {
|
||||
|
||||
@Input() course!: CoreCourseSearchedData & CoreCourseWithImageAndColor & {
|
||||
completionusertracked?: boolean; // If the user is completion tracked.
|
||||
progress?: number | null; // Progress percentage.
|
||||
}; // The course to render.
|
||||
@Input() course!: CoreCourseListItem; // The course to render.
|
||||
|
||||
icons: CoreCoursesEnrolmentIcons[] = [];
|
||||
isEnrolled = false;
|
||||
|
@ -46,15 +46,21 @@ export class CoreCoursesCourseListItemComponent implements OnInit {
|
|||
async ngOnInit(): Promise<void> {
|
||||
CoreCoursesHelper.loadCourseColorAndImage(this.course);
|
||||
|
||||
// Check if the user is enrolled in the course.
|
||||
try {
|
||||
const course = await CoreCourses.getUserCourse(this.course.id);
|
||||
this.course.progress = course.progress;
|
||||
this.course.completionusertracked = course.completionusertracked;
|
||||
this.isEnrolled = this.course.progress !== undefined;
|
||||
|
||||
this.isEnrolled = true;
|
||||
} catch {
|
||||
this.isEnrolled = false;
|
||||
if (!this.isEnrolled) {
|
||||
try {
|
||||
const course = await CoreCourses.getUserCourse(this.course.id);
|
||||
this.course.progress = course.progress;
|
||||
this.course.completionusertracked = course.completionusertracked;
|
||||
|
||||
this.isEnrolled = true;
|
||||
} catch {
|
||||
this.isEnrolled = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isEnrolled) {
|
||||
this.icons = [];
|
||||
|
||||
this.course.enrollmentmethods.forEach((instance) => {
|
||||
|
|
|
@ -18,7 +18,7 @@ import { RouterModule, Routes } from '@angular/router';
|
|||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'my',
|
||||
redirectTo: 'list',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
|
@ -38,12 +38,6 @@ const routes: Routes = [
|
|||
import('./pages/list/list.module')
|
||||
.then(m => m.CoreCoursesListPageModule),
|
||||
},
|
||||
{
|
||||
path: 'my',
|
||||
loadChildren: () =>
|
||||
import('./pages/my-courses/my-courses.module')
|
||||
.then(m => m.CoreCoursesMyCoursesPageModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
@ -44,7 +44,7 @@ const mainMenuHomeChildrenRoutes: Routes = [
|
|||
},
|
||||
{
|
||||
path: CoreCoursesMyCoursesHomeHandlerService.PAGE_NAME,
|
||||
loadChildren: () => import('./pages/my-courses/my-courses.module').then(m => m.CoreCoursesMyCoursesPageModule),
|
||||
loadChildren: () => import('./pages/list/list.module').then(m => m.CoreCoursesListPageModule),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
"selfenrolment": "Self enrolment",
|
||||
"sendpaymentbutton": "Send payment via PayPal",
|
||||
"show": "Restore to view",
|
||||
"showonlyenrolled": "Show only my courses",
|
||||
"therearecourses": "There are {{$a}} courses",
|
||||
"totalcoursesearchresults": "Total courses: {{$a}}"
|
||||
}
|
||||
|
|
|
@ -3,7 +3,19 @@
|
|||
<ion-buttons slot="start">
|
||||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<h1>{{ 'core.courses.availablecourses' | translate }}</h1>
|
||||
<h1 *ngIf="!showOnlyEnrolled">{{ 'core.courses.availablecourses' | translate }}</h1>
|
||||
<h1 *ngIf="showOnlyEnrolled">{{ 'core.courses.mycourses' | translate }}</h1>
|
||||
<ion-buttons slot="end"></ion-buttons>
|
||||
<core-navbar-buttons slot="end">
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="downloadCourseEnabled || downloadCoursesEnabled" [priority]="1000"
|
||||
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload(!downloadEnabled)"
|
||||
iconAction="toggle" [toggle]="downloadEnabled"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="myCoursesEnabled" [priority]="900"
|
||||
[content]="'core.courses.showonlyenrolled' | translate" (action)="toggleEnrolled(!showOnlyEnrolled)"
|
||||
iconAction="toggle" [toggle]="showOnlyEnrolled"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
|
|
|
@ -13,14 +13,15 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfo } from '@features/courses/services/courses-helper';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreCourseBasicSearchedData, CoreCourses } from '../../services/courses';
|
||||
import { CoreCourseBasicSearchedData, CoreCourses, CoreCoursesProvider } from '../../services/courses';
|
||||
|
||||
type CoreCoursesListMode = 'search' | 'all';
|
||||
type CoreCoursesListMode = 'search' | 'all' | 'my';
|
||||
|
||||
/**
|
||||
* Page that shows a list of courses.
|
||||
|
@ -31,31 +32,60 @@ type CoreCoursesListMode = 'search' | 'all';
|
|||
})
|
||||
export class CoreCoursesListPage implements OnInit, OnDestroy {
|
||||
|
||||
downloadAllCoursesEnabled = false;
|
||||
|
||||
searchEnabled = false;
|
||||
myCoursesEnabled = true;
|
||||
searchMode = false;
|
||||
searchCanLoadMore = false;
|
||||
searchLoadMoreError = false;
|
||||
searchTotal = 0;
|
||||
|
||||
mode: CoreCoursesListMode = 'all';
|
||||
downloadEnabled = false;
|
||||
downloadCourseEnabled = false;
|
||||
downloadCoursesEnabled = false;
|
||||
|
||||
courses: CoreCourseBasicSearchedData[] = [];
|
||||
mode: CoreCoursesListMode = 'my';
|
||||
|
||||
courses: (CoreCourseBasicSearchedData|CoreEnrolledCourseDataWithExtraInfo)[] = [];
|
||||
coursesLoaded = false;
|
||||
|
||||
showOnlyEnrolled = false;
|
||||
|
||||
protected currentSiteId: string;
|
||||
protected frontpageCourseId: number;
|
||||
protected searchPage = 0;
|
||||
protected searchText = '';
|
||||
protected myCoursesObserver: CoreEventObserver;
|
||||
protected siteUpdatedObserver: CoreEventObserver;
|
||||
protected courseIds = '';
|
||||
protected isDestroyed = false;
|
||||
|
||||
constructor() {
|
||||
this.currentSiteId = CoreSites.getRequiredCurrentSite().getId();
|
||||
this.frontpageCourseId = CoreSites.getRequiredCurrentSite().getSiteHomeId();
|
||||
|
||||
// Update list if user enrols in a course.
|
||||
this.myCoursesObserver = CoreEvents.on(
|
||||
CoreCoursesProvider.EVENT_MY_COURSES_UPDATED,
|
||||
(data) => {
|
||||
|
||||
if (data.action == CoreCoursesProvider.ACTION_ENROL) {
|
||||
this.fetchCourses();
|
||||
}
|
||||
},
|
||||
|
||||
this.currentSiteId,
|
||||
);
|
||||
|
||||
// Refresh the enabled flags if site is updated.
|
||||
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
|
||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
||||
this.myCoursesEnabled = !CoreCourses.isMyCoursesDisabledInSite();
|
||||
|
||||
this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && this.downloadEnabled;
|
||||
if (!this.searchEnabled) {
|
||||
this.searchMode = false;
|
||||
|
||||
|
@ -68,12 +98,25 @@ export class CoreCoursesListPage implements OnInit, OnDestroy {
|
|||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
||||
|
||||
this.mode = CoreNavigator.getRouteParam<CoreCoursesListMode>('mode') || this.mode;
|
||||
|
||||
this.myCoursesEnabled = !CoreCourses.isMyCoursesDisabledInSite();
|
||||
if (this.mode == 'my' && !this.myCoursesEnabled) {
|
||||
this.mode = 'all';
|
||||
this.showOnlyEnrolled = false;
|
||||
}
|
||||
|
||||
if (this.mode == 'search') {
|
||||
this.searchMode = true;
|
||||
}
|
||||
|
||||
if (this.mode == 'my') {
|
||||
this.showOnlyEnrolled = true;
|
||||
}
|
||||
|
||||
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
|
||||
if (!this.searchEnabled) {
|
||||
this.searchMode = false;
|
||||
|
@ -91,6 +134,8 @@ export class CoreCoursesListPage implements OnInit, OnDestroy {
|
|||
try {
|
||||
if (this.searchMode && this.searchText) {
|
||||
await this.search(this.searchText);
|
||||
} else if (this.showOnlyEnrolled) {
|
||||
await this.loadMyCourses();
|
||||
} else {
|
||||
await this.loadAvailableCourses();
|
||||
}
|
||||
|
@ -99,6 +144,24 @@ export class CoreCoursesListPage implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the user courses.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadMyCourses(): Promise<void> {
|
||||
try {
|
||||
const courses: CoreEnrolledCourseDataWithExtraInfo[] = await CoreCourses.getUserCourses();
|
||||
this.courseIds = courses.map((course) => course.id).join(',');
|
||||
|
||||
await CoreCoursesHelper.loadCoursesExtraInfo(courses, true);
|
||||
|
||||
this.courses = courses;
|
||||
} catch (error) {
|
||||
!this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the courses.
|
||||
*
|
||||
|
@ -110,7 +173,7 @@ export class CoreCoursesListPage implements OnInit, OnDestroy {
|
|||
|
||||
this.courses = courses.filter((course) => course.id != this.frontpageCourseId);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
|
||||
!this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,6 +187,9 @@ export class CoreCoursesListPage implements OnInit, OnDestroy {
|
|||
|
||||
promises.push(CoreCourses.invalidateUserCourses());
|
||||
promises.push(CoreCourses.invalidateCoursesByField());
|
||||
if (this.courseIds) {
|
||||
promises.push(CoreCourses.invalidateCoursesByField('ids', this.courseIds));
|
||||
}
|
||||
|
||||
Promise.all(promises).finally(() => {
|
||||
this.fetchCourses().finally(() => {
|
||||
|
@ -145,7 +211,7 @@ export class CoreCoursesListPage implements OnInit, OnDestroy {
|
|||
this.searchTotal = 0;
|
||||
|
||||
const modal = await CoreDomUtils.showModalLoading('core.searching', true);
|
||||
this.searchCourses().finally(() => {
|
||||
await this.searchCourses().finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}
|
||||
|
@ -184,7 +250,7 @@ export class CoreCoursesListPage implements OnInit, OnDestroy {
|
|||
this.searchLoadMoreError = false;
|
||||
|
||||
try {
|
||||
const response = await CoreCourses.search(this.searchText, this.searchPage);
|
||||
const response = await CoreCourses.search(this.searchText, this.searchPage, undefined, this.showOnlyEnrolled);
|
||||
|
||||
if (this.searchPage === 0) {
|
||||
this.courses = response.courses;
|
||||
|
@ -197,15 +263,34 @@ export class CoreCoursesListPage implements OnInit, OnDestroy {
|
|||
this.searchCanLoadMore = this.courses.length < this.searchTotal;
|
||||
} catch (error) {
|
||||
this.searchLoadMoreError = true; // Set to prevent infinite calls with infinite-loading.
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorsearching', true);
|
||||
!this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorsearching', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle show only my courses.
|
||||
*/
|
||||
toggleEnrolled(enabled: boolean): void {
|
||||
this.coursesLoaded = false;
|
||||
this.showOnlyEnrolled = enabled;
|
||||
|
||||
this.fetchCourses();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle download enabled.
|
||||
*/
|
||||
toggleDownload(enabled: boolean): void {
|
||||
this.downloadEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.myCoursesObserver?.off();
|
||||
this.siteUpdatedObserver?.off();
|
||||
this.isDestroyed = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<h1>{{ 'core.courses.mycourses' | translate }}</h1>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
<core-navbar-buttons>
|
||||
<ion-button *ngIf="searchEnabled" (click)="openSearch()"
|
||||
[attr.aria-label]="'core.courses.searchcourses' | translate">
|
||||
<ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button [hidden]="!downloadAllCoursesEnabled || !courses || courses.length < 2 || downloadAllCoursesLoading"
|
||||
(click)="prefetchCourses()" [attr.aria-label]="'core.courses.downloadcourses' | translate">
|
||||
<ion-icon [name]="downloadAllCoursesIcon" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-spinner [hidden]="!downloadAllCoursesEnabled || !courses || courses.length < 2 ||
|
||||
downloadAllCoursesBadge != '' || !downloadAllCoursesLoading"
|
||||
[attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||
<ion-badge [hidden]="!downloadAllCoursesEnabled || !courses || courses.length < 2 || !downloadAllCoursesLoading ||
|
||||
downloadAllCoursesBadge == '' || !downloadAllCoursesLoading"
|
||||
role="progressbar" [attr.aria-valuemax]="downloadAllCoursesTotal"
|
||||
[attr.aria-valuenow]="downloadAllCoursesCount" [attr.aria-valuetext]="downloadAllCoursesBadgeA11yText">
|
||||
{{downloadAllCoursesBadge}}
|
||||
</ion-badge>
|
||||
</core-navbar-buttons>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!coursesLoaded" (ionRefresh)="refreshCourses($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="coursesLoaded">
|
||||
<ion-searchbar #searchbar *ngIf="courses && courses.length > 5" [(ngModel)]="filter" (ionInput)="filterChanged($event)"
|
||||
(ionCancel)="filterChanged()" [placeholder]="'core.courses.filtermycourses' | translate">
|
||||
</ion-searchbar>
|
||||
<ion-grid class="ion-no-padding safe-area-padding">
|
||||
<ion-row class="ion-no-padding">
|
||||
<ion-col *ngFor="let course of filteredCourses" class="ion-no-padding"
|
||||
size="12" size-sm="6" size-md="6" size-lg="4" size-xl="4">
|
||||
<core-courses-course-progress [course]="course" class="core-courseoverview" showAll="true">
|
||||
</core-courses-course-progress>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
<core-empty-box *ngIf="!courses || !courses.length" icon="fas-graduation-cap"
|
||||
[message]="'core.courses.nocourses' | translate">
|
||||
<p *ngIf="searchEnabled">{{ 'core.courses.searchcoursesadvice' | translate }}</p>
|
||||
</core-empty-box>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -1,40 +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 { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreCoursesMyCoursesPage } from './my-courses';
|
||||
import { CoreCoursesComponentsModule } from '../../components/components.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreCoursesMyCoursesPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
CoreCoursesComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreCoursesMyCoursesPage,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class CoreCoursesMyCoursesPageModule { }
|
|
@ -1,219 +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 { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { IonSearchbar, IonRefresher } from '@ionic/angular';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import {
|
||||
CoreCoursesProvider,
|
||||
CoreCourses,
|
||||
} from '../../services/courses';
|
||||
import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper';
|
||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { Translate } from '@singletons';
|
||||
|
||||
/**
|
||||
* Page that displays the list of courses the user is enrolled in.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-courses-my-courses',
|
||||
templateUrl: 'my-courses.html',
|
||||
})
|
||||
export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy {
|
||||
|
||||
@ViewChild(IonSearchbar) searchbar!: IonSearchbar;
|
||||
|
||||
courses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = [];
|
||||
filteredCourses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = [];
|
||||
searchEnabled = false;
|
||||
filter = '';
|
||||
showFilter = false;
|
||||
coursesLoaded = false;
|
||||
downloadAllCoursesIcon = CoreConstants.ICON_NOT_DOWNLOADED;
|
||||
downloadAllCoursesLoading = false;
|
||||
downloadAllCoursesBadge = '';
|
||||
downloadAllCoursesEnabled = false;
|
||||
downloadAllCoursesCount?: number;
|
||||
downloadAllCoursesTotal?: number;
|
||||
downloadAllCoursesBadgeA11yText = '';
|
||||
|
||||
protected myCoursesObserver: CoreEventObserver;
|
||||
protected siteUpdatedObserver: CoreEventObserver;
|
||||
protected isDestroyed = false;
|
||||
protected courseIds = '';
|
||||
|
||||
constructor() {
|
||||
// Update list if user enrols in a course.
|
||||
this.myCoursesObserver = CoreEvents.on(
|
||||
CoreCoursesProvider.EVENT_MY_COURSES_UPDATED,
|
||||
(data) => {
|
||||
|
||||
if (data.action == CoreCoursesProvider.ACTION_ENROL) {
|
||||
this.fetchCourses();
|
||||
}
|
||||
},
|
||||
|
||||
CoreSites.getCurrentSiteId(),
|
||||
);
|
||||
|
||||
// Refresh the enabled flags if site is updated.
|
||||
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
|
||||
this.downloadAllCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
||||
}, CoreSites.getCurrentSiteId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
|
||||
this.downloadAllCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
||||
|
||||
this.fetchCourses().finally(() => {
|
||||
this.coursesLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the user courses.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchCourses(): Promise<void> {
|
||||
try {
|
||||
const courses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = await CoreCourses.getUserCourses();
|
||||
const courseIds = courses.map((course) => course.id);
|
||||
|
||||
this.courseIds = courseIds.join(',');
|
||||
|
||||
await CoreCoursesHelper.loadCoursesExtraInfo(courses);
|
||||
|
||||
const options = await CoreCourses.getCoursesAdminAndNavOptions(courseIds);
|
||||
courses.forEach((course) => {
|
||||
course.navOptions = options.navOptions[course.id];
|
||||
course.admOptions = options.admOptions[course.id];
|
||||
});
|
||||
|
||||
this.courses = courses;
|
||||
this.filteredCourses = this.courses;
|
||||
this.filter = '';
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the courses.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
refreshCourses(refresher: IonRefresher): void {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(CoreCourses.invalidateUserCourses());
|
||||
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions());
|
||||
if (this.courseIds) {
|
||||
promises.push(CoreCourses.invalidateCoursesByField('ids', this.courseIds));
|
||||
}
|
||||
|
||||
Promise.all(promises).finally(() => {
|
||||
this.fetchCourses().finally(() => {
|
||||
refresher?.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide the filter.
|
||||
*/
|
||||
switchFilter(): void {
|
||||
this.filter = '';
|
||||
this.showFilter = !this.showFilter;
|
||||
this.filteredCourses = this.courses;
|
||||
if (this.showFilter) {
|
||||
setTimeout(() => {
|
||||
this.searchbar.setFocus();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The filter has changed.
|
||||
*
|
||||
* @param Received Event.
|
||||
*/
|
||||
filterChanged(event?: Event): void {
|
||||
const target = <HTMLInputElement>event?.target || null;
|
||||
const newValue = target ? String(target.value).trim().toLowerCase() : null;
|
||||
if (!newValue || !this.courses) {
|
||||
this.filteredCourses = this.courses;
|
||||
} else {
|
||||
// Use displayname if available, or fullname if not.
|
||||
if (this.courses.length > 0 && typeof this.courses[0].displayname != 'undefined') {
|
||||
this.filteredCourses = this.courses.filter((course) => course.displayname!.toLowerCase().indexOf(newValue) > -1);
|
||||
} else {
|
||||
this.filteredCourses = this.courses.filter((course) => course.fullname.toLowerCase().indexOf(newValue) > -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch all the courses.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async prefetchCourses(): Promise<void> {
|
||||
this.downloadAllCoursesLoading = true;
|
||||
|
||||
try {
|
||||
await CoreCourseHelper.confirmAndPrefetchCourses(this.courses, { onProgress: (progress) => {
|
||||
this.downloadAllCoursesBadge = progress.count + ' / ' + progress.total;
|
||||
this.downloadAllCoursesBadgeA11yText =
|
||||
Translate.instant('core.course.downloadcoursesprogressdescription', progress);
|
||||
this.downloadAllCoursesCount = progress.count;
|
||||
this.downloadAllCoursesTotal = progress.total;
|
||||
} });
|
||||
} catch (error) {
|
||||
if (!this.isDestroyed) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||
}
|
||||
}
|
||||
|
||||
this.downloadAllCoursesBadge = '';
|
||||
this.downloadAllCoursesLoading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to search courses.
|
||||
*/
|
||||
openSearch(): void {
|
||||
CoreNavigator.navigateToSitePath('courses/list', { params : { mode: 'search' } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
this.myCoursesObserver?.off();
|
||||
this.siteUpdatedObserver?.off();
|
||||
}
|
||||
|
||||
}
|
|
@ -20,6 +20,7 @@ import { makeSingleton } from '@singletons';
|
|||
import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { CoreWSError } from '@classes/errors/wserror';
|
||||
import { CoreCourseWithImageAndColor } from './courses-helper';
|
||||
|
||||
const ROOT_CACHE_KEY = 'mmCourses:';
|
||||
|
||||
|
@ -1121,6 +1122,7 @@ export class CoreCoursesProvider {
|
|||
* @param text Text to search.
|
||||
* @param page Page to get.
|
||||
* @param perPage Number of courses per page. Defaults to CoreCoursesProvider.SEARCH_PER_PAGE.
|
||||
* @param limitToEnrolled Limit to enrolled courses.
|
||||
* @param siteId Site ID. If not defined, use current site.
|
||||
* @return Promise resolved with the courses and the total of matches.
|
||||
*/
|
||||
|
@ -1128,6 +1130,7 @@ export class CoreCoursesProvider {
|
|||
text: string,
|
||||
page: number = 0,
|
||||
perPage: number = CoreCoursesProvider.SEARCH_PER_PAGE,
|
||||
limitToEnrolled: boolean = false,
|
||||
siteId?: string,
|
||||
): Promise<{ total: number; courses: CoreCourseBasicSearchedData[] }> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
@ -1136,6 +1139,7 @@ export class CoreCoursesProvider {
|
|||
criteriavalue: text,
|
||||
page: page,
|
||||
perpage: perPage,
|
||||
limittoenrolled: limitToEnrolled,
|
||||
};
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
getFromCache: false,
|
||||
|
@ -1358,6 +1362,14 @@ export type CoreCourseSearchedData = CoreCourseBasicSearchedData & {
|
|||
courseformatoptions?: CoreCourseFormatOption[]; // Additional options for particular course format.
|
||||
};
|
||||
|
||||
/**
|
||||
* Course to render as list item.
|
||||
*/
|
||||
export type CoreCourseListItem = CoreCourseSearchedData & CoreCourseWithImageAndColor & {
|
||||
completionusertracked?: boolean; // If the user is completion tracked.
|
||||
progress?: number | null; // Progress percentage.
|
||||
};
|
||||
|
||||
export type CoreCourseGetCoursesData = CoreEnrolledCourseBasicData & {
|
||||
categoryid: number; // Category id.
|
||||
categorysortorder?: number; // Sort order into the category.
|
||||
|
|
|
@ -223,7 +223,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
|
|||
* Go to my courses.
|
||||
*/
|
||||
openMyCourses(): void {
|
||||
CoreNavigator.navigateToSitePath('courses/my');
|
||||
CoreNavigator.navigateToSitePath('courses/list', { params : { mode: 'my' } });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue