commit
17305cbac6
|
@ -40,11 +40,13 @@
|
||||||
"addon.block_learningplans.pluginname": "block_lp",
|
"addon.block_learningplans.pluginname": "block_lp",
|
||||||
"addon.block_myoverview.all": "block_myoverview",
|
"addon.block_myoverview.all": "block_myoverview",
|
||||||
"addon.block_myoverview.allincludinghidden": "block_myoverview",
|
"addon.block_myoverview.allincludinghidden": "block_myoverview",
|
||||||
"addon.block_myoverview.favourites": "block_myoverview",
|
"addon.block_myoverview.aria:hiddencourses": "block_myoverview",
|
||||||
|
"addon.block_myoverview.card": "block_myoverview",
|
||||||
|
"addon.block_myoverview.favouritesonly": "local_moodlemobileapp",
|
||||||
"addon.block_myoverview.future": "block_myoverview",
|
"addon.block_myoverview.future": "block_myoverview",
|
||||||
"addon.block_myoverview.hiddencourses": "block_myoverview",
|
|
||||||
"addon.block_myoverview.inprogress": "block_myoverview",
|
"addon.block_myoverview.inprogress": "block_myoverview",
|
||||||
"addon.block_myoverview.lastaccessed": "block_myoverview",
|
"addon.block_myoverview.lastaccessed": "block_myoverview",
|
||||||
|
"addon.block_myoverview.list": "block_myoverview",
|
||||||
"addon.block_myoverview.nocourses": "block_myoverview",
|
"addon.block_myoverview.nocourses": "block_myoverview",
|
||||||
"addon.block_myoverview.past": "block_myoverview",
|
"addon.block_myoverview.past": "block_myoverview",
|
||||||
"addon.block_myoverview.pluginname": "block_myoverview",
|
"addon.block_myoverview.pluginname": "block_myoverview",
|
||||||
|
@ -1400,6 +1402,7 @@
|
||||||
"core.allparticipants": "moodle",
|
"core.allparticipants": "moodle",
|
||||||
"core.answer": "moodle",
|
"core.answer": "moodle",
|
||||||
"core.answered": "quiz",
|
"core.answered": "quiz",
|
||||||
|
"core.applyfilters": "user",
|
||||||
"core.areyousure": "moodle",
|
"core.areyousure": "moodle",
|
||||||
"core.back": "moodle",
|
"core.back": "moodle",
|
||||||
"core.block.blocks": "moodle",
|
"core.block.blocks": "moodle",
|
||||||
|
@ -1553,6 +1556,7 @@
|
||||||
"core.courses.password": "local_moodlemobileapp",
|
"core.courses.password": "local_moodlemobileapp",
|
||||||
"core.courses.paymentrequired": "moodle",
|
"core.courses.paymentrequired": "moodle",
|
||||||
"core.courses.paypalaccepted": "enrol_paypal",
|
"core.courses.paypalaccepted": "enrol_paypal",
|
||||||
|
"core.courses.refreshcourses": "local_moodlemobileapp",
|
||||||
"core.courses.reload": "moodle",
|
"core.courses.reload": "moodle",
|
||||||
"core.courses.removefromfavourites": "block_myoverview",
|
"core.courses.removefromfavourites": "block_myoverview",
|
||||||
"core.courses.search": "moodle",
|
"core.courses.search": "moodle",
|
||||||
|
|
|
@ -17,10 +17,12 @@ import { NgModule } from '@angular/core';
|
||||||
import { CoreSharedModule } from '@/core/shared.module';
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
import { CoreCoursesComponentsModule } from '@features/courses/components/components.module';
|
import { CoreCoursesComponentsModule } from '@features/courses/components/components.module';
|
||||||
import { AddonBlockMyOverviewComponent } from './myoverview/myoverview';
|
import { AddonBlockMyOverviewComponent } from './myoverview/myoverview';
|
||||||
|
import { AddonBlockMyOverviewFilterOptionsComponent } from './filteroptions/filteroptions';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AddonBlockMyOverviewComponent,
|
AddonBlockMyOverviewComponent,
|
||||||
|
AddonBlockMyOverviewFilterOptionsComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CoreSharedModule,
|
CoreSharedModule,
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<h1>{{ 'core.courses.filtermycourses' | translate }}</h1>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||||
|
<ion-icon name="fas-times" slot="icon-only" aria-hidden=true></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-list>
|
||||||
|
<ion-radio-group [(ngModel)]="options.timeFilterSelected">
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="options.show.all">
|
||||||
|
<ion-label>{{'addon.block_myoverview.allincludinghidden' | translate}}</ion-label>
|
||||||
|
<ion-radio slot="end" value="all"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="options.show.inprogress">
|
||||||
|
<ion-label>{{'addon.block_myoverview.inprogress' | translate}}</ion-label>
|
||||||
|
<ion-radio slot="end" value="inprogress"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="options.show.future">
|
||||||
|
<ion-label>{{'addon.block_myoverview.future' | translate}}</ion-label>
|
||||||
|
<ion-radio slot="end" value="future"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="options.show.past">
|
||||||
|
<ion-label>{{'addon.block_myoverview.past' | translate}}</ion-label>
|
||||||
|
<ion-radio slot="end" value="past"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
|
||||||
|
<core-spacer *ngIf="options.show.custom"></core-spacer>
|
||||||
|
<ion-radio-group [(ngModel)]="options.customSelected" *ngIf="options.show.custom" allowEmptySelection="true">
|
||||||
|
<ion-item class="ion-text-wrap" *ngFor="let customOption of options.customFilters">
|
||||||
|
<ion-label>{{customOption.name}}</ion-label>
|
||||||
|
<ion-radio slot="end" [value]="customOption.value"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
|
||||||
|
<core-spacer></core-spacer>
|
||||||
|
<ion-item *ngIf="options.show.favourite">
|
||||||
|
<ion-label>{{ 'addon.block_myoverview.favouritesonly' | translate }}</ion-label>
|
||||||
|
<ion-toggle [(ngModel)]="options.favouriteSelected"></ion-toggle>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item *ngIf="options.show.hidden">
|
||||||
|
<ion-label>{{ 'addon.block_myoverview.aria:hiddencourses' | translate }}</ion-label>
|
||||||
|
<ion-toggle [(ngModel)]="options.hiddenSelected"></ion-toggle>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</ion-content>
|
||||||
|
<ion-footer class="ion-padding">
|
||||||
|
<ion-button (click)="apply()" expand="block" [attr.aria-label]="'core.applyfilters' | translate" class="ion-text-wrap">
|
||||||
|
{{ 'core.applyfilters' | translate }}
|
||||||
|
</ion-button>
|
||||||
|
</ion-footer>
|
|
@ -0,0 +1,44 @@
|
||||||
|
// (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, Input } from '@angular/core';
|
||||||
|
import { ModalController } from '@singletons';
|
||||||
|
import { AddonBlockMyOverviewFilterOptions } from '../myoverview/myoverview';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render a my overview filter options.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-block-myoverview-filter-options',
|
||||||
|
templateUrl: 'filteroptions.html',
|
||||||
|
})
|
||||||
|
export class AddonBlockMyOverviewFilterOptionsComponent {
|
||||||
|
|
||||||
|
@Input() options!: AddonBlockMyOverviewFilterOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appl filters.
|
||||||
|
*/
|
||||||
|
apply(): void {
|
||||||
|
ModalController.dismiss(this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close modal.
|
||||||
|
*/
|
||||||
|
closeModal(): void {
|
||||||
|
ModalController.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,97 +4,82 @@
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<div slot="end" class="flex-row">
|
<div slot="end" class="flex-row">
|
||||||
<!-- Download all courses. -->
|
<!-- Download all courses. -->
|
||||||
<div *ngIf="downloadCoursesEnabled && downloadEnabled && filteredCourses.length > 1 && !showFilter"
|
<div *ngIf="downloadCoursesEnabled && downloadEnabled && filteredCourses.length > 1" class="core-button-spinner">
|
||||||
class="core-button-spinner">
|
<ion-button *ngIf="!prefetchCoursesData.loading" fill="clear" color="dark" (click)="prefetchCourses()"
|
||||||
<ion-button *ngIf="!prefetchCoursesData[selectedFilter].loading" fill="clear" color="dark" (click)="prefetchCourses()"
|
[attr.aria-label]="prefetchCoursesData.statusTranslatable | translate">
|
||||||
[attr.aria-label]="'core.courses.downloadcourses' | translate">
|
<ion-icon [name]="prefetchCoursesData.icon" slot="icon-only" aria-hidden="true">
|
||||||
<ion-icon [name]="prefetchCoursesData[selectedFilter].icon" slot="icon-only" aria-hidden="true">
|
|
||||||
</ion-icon>
|
</ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData[selectedFilter].badge"
|
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge" role="progressbar"
|
||||||
role="progressbar" [attr.aria-valuemax]="prefetchCoursesData[selectedFilter].total"
|
[attr.aria-valuemax]="prefetchCoursesData.total" [attr.aria-valuenow]="prefetchCoursesData.count"
|
||||||
[attr.aria-valuenow]="prefetchCoursesData[selectedFilter].count"
|
[attr.aria-valuetext]="prefetchCoursesData.badgeA11yText">
|
||||||
[attr.aria-valuetext]="prefetchCoursesData[selectedFilter].badgeA11yText">
|
{{prefetchCoursesData.badge}}
|
||||||
{{prefetchCoursesData[selectedFilter].badge}}
|
|
||||||
</ion-badge>
|
</ion-badge>
|
||||||
<ion-spinner *ngIf="prefetchCoursesData[selectedFilter].loading" [attr.aria-label]="'core.loading' | translate">
|
<ion-spinner *ngIf="prefetchCoursesData.loading" [attr.aria-label]="'core.loading' | translate">
|
||||||
</ion-spinner>
|
</ion-spinner>
|
||||||
</div>
|
</div>
|
||||||
<core-context-menu>
|
|
||||||
<core-context-menu-item *ngIf="loaded && showFilterSwitchButton()" [priority]="1000"
|
|
||||||
[content]="'core.courses.filtermycourses' | translate" (action)="switchFilter()" iconAction="fas-filter"
|
|
||||||
(onClosed)="switchFilterClosed()"></core-context-menu-item>
|
|
||||||
<core-context-menu-item *ngIf="loaded && showSortFilter" [priority]="900"
|
|
||||||
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.title' | translate)}}"
|
|
||||||
(action)="switchSort('fullname')" [iconAction]="sort == 'fullname' ? 'far-dot-circle' : 'far-circle'">
|
|
||||||
</core-context-menu-item>
|
|
||||||
<core-context-menu-item *ngIf="loaded && showSortFilter && showSortByShortName" [priority]="800"
|
|
||||||
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.shortname' | translate)}}"
|
|
||||||
(action)="switchSort('shortname')" [iconAction]="sort == 'shortname' ? 'far-dot-circle' : 'far-circle'">
|
|
||||||
</core-context-menu-item>
|
|
||||||
<core-context-menu-item *ngIf="loaded && showSortFilter" [priority]="700"
|
|
||||||
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.lastaccessed' | translate)}}"
|
|
||||||
(action)="switchSort('lastaccess')" [iconAction]="sort == 'lastaccess' ? 'far-dot-circle' : 'far-circle'">
|
|
||||||
</core-context-menu-item>
|
|
||||||
</core-context-menu>
|
|
||||||
</div>
|
</div>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<core-loading [hideUntil]="loaded" [fullscreen]="false">
|
<core-loading [hideUntil]="loaded" [fullscreen]="false">
|
||||||
<div class="safe-area-padding-horizontal" [hidden]="showFilter || !showSelectorFilter">
|
|
||||||
<!-- "Time" selector. -->
|
<ion-row class="ion-no-padding ion-justify-content-between" *ngIf="hasCourses">
|
||||||
<core-combobox [label]="'core.show' | translate" [selection]="selectedFilter" (onChange)="selectedChanged($event)">
|
<ion-col size="auto" class="ion-no-padding" *ngIf="filters.enabled">
|
||||||
<ion-select-option class="ion-text-wrap" value="allincludinghidden" *ngIf="showFilters.allincludinghidden != 'hidden'">
|
<core-combobox interface="modal" [label]="'core.courses.filtermycourses' | translate" (onChange)="filterOptionsChanged($event)"
|
||||||
{{ 'addon.block_myoverview.allincludinghidden' | translate }}
|
icon="fas-filter" [badge]="filters.count" [modalOptions]="filterModalOptions">
|
||||||
|
</core-combobox>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col class="ion-no-padding">
|
||||||
|
<!-- Filter courses. -->
|
||||||
|
<ion-searchbar class="ion-hide-md-down" [(ngModel)]="textFilter" (ionInput)="filterTextChanged($event.target)"
|
||||||
|
(ionCancel)="filterTextChanged($event.target)" [placeholder]="'core.filter' | translate">
|
||||||
|
</ion-searchbar>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col size="auto" class="ion-no-padding" *ngIf="sort.enabled">
|
||||||
|
<core-combobox [label]="'core.sortby' | translate" [selection]="sort.selected" (onChange)="sortCourses($event)"
|
||||||
|
icon="fas-sort-amount-down-alt">
|
||||||
|
<ion-select-option class="ion-text-wrap" value="fullname">
|
||||||
|
{{'addon.block_myoverview.title' | translate}}
|
||||||
</ion-select-option>
|
</ion-select-option>
|
||||||
<ion-select-option class="ion-text-wrap" value="all" *ngIf="showFilters.all != 'hidden'">
|
<ion-select-option class="ion-text-wrap" value="shortname" *ngIf="sort.shortnameEnabled">
|
||||||
{{ 'addon.block_myoverview.all' | translate }}
|
{{'addon.block_myoverview.shortname' | translate}}
|
||||||
</ion-select-option>
|
</ion-select-option>
|
||||||
<ion-select-option class="ion-text-wrap" value="inprogress" *ngIf="showFilters.inprogress != 'hidden'"
|
<ion-select-option class="ion-text-wrap" value="lastaccess">
|
||||||
[disabled]="showFilters.inprogress == 'disabled'">
|
{{'addon.block_myoverview.lastaccessed' | translate}}
|
||||||
{{ 'addon.block_myoverview.inprogress' | translate }}
|
|
||||||
</ion-select-option>
|
|
||||||
<ion-select-option class="ion-text-wrap" value="future" *ngIf="showFilters.future != 'hidden'"
|
|
||||||
[disabled]="showFilters.future == 'disabled'">
|
|
||||||
{{ 'addon.block_myoverview.future' | translate }}
|
|
||||||
</ion-select-option>
|
|
||||||
<ion-select-option class="ion-text-wrap" value="past" *ngIf="showFilters.past != 'hidden'"
|
|
||||||
[disabled]="showFilters.past == 'disabled'">
|
|
||||||
{{ 'addon.block_myoverview.past' | translate }}
|
|
||||||
</ion-select-option>
|
|
||||||
<ng-container *ngIf="showFilters.custom != 'hidden'">
|
|
||||||
<ng-container *ngFor="let customOption of customFilter; let index = index">
|
|
||||||
<ion-select-option class="ion-text-wrap" value="custom-{{index}}">{{ customOption.name }}</ion-select-option>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
<ion-select-option class="ion-text-wrap" value="favourite" *ngIf="showFilters.favourite != 'hidden'"
|
|
||||||
[disabled]="showFilters.favourite == 'disabled'">
|
|
||||||
{{ 'addon.block_myoverview.favourites' | translate }}
|
|
||||||
</ion-select-option>
|
|
||||||
<ion-select-option class="ion-text-wrap" value="hidden" *ngIf="showFilters.hidden != 'hidden'"
|
|
||||||
[disabled]="showFilters.hidden == 'disabled'">
|
|
||||||
{{ 'addon.block_myoverview.hiddencourses' | translate }}
|
|
||||||
</ion-select-option>
|
</ion-select-option>
|
||||||
</core-combobox>
|
</core-combobox>
|
||||||
</div>
|
</ion-col>
|
||||||
|
<ion-col size="auto" class="ion-no-padding" *ngIf="layouts.options.length > 1">
|
||||||
|
<!-- "Layouts" selector. -->
|
||||||
|
<core-combobox [label]="'core.show' | translate" [selection]="layouts.selected" (onChange)="saveLayout($event)" icon="fas-th">
|
||||||
|
<ng-container *ngFor="let layout of layouts.options">
|
||||||
|
<ion-select-option class="ion-text-wrap" [value]="layout">{{ 'addon.block_myoverview.'+layout | translate }}
|
||||||
|
</ion-select-option>
|
||||||
|
</ng-container>
|
||||||
|
</core-combobox>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
<ion-row class="ion-no-padding ion-hide-md-up" *ngIf="hasCourses">
|
||||||
|
<ion-col class="ion-no-padding">
|
||||||
<!-- Filter courses. -->
|
<!-- Filter courses. -->
|
||||||
<ion-searchbar #searchbar *ngIf="showFilter" [(ngModel)]="courses.filter" (ionInput)="filterChanged($event)"
|
<ion-searchbar [(ngModel)]="textFilter" (ionInput)="filterTextChanged($event.target)"
|
||||||
(ionCancel)="filterChanged($event)" [placeholder]="'core.courses.filtermycourses' | translate">
|
(ionCancel)="filterTextChanged($event.target)" [placeholder]="'core.filter' | translate">
|
||||||
</ion-searchbar>
|
</ion-searchbar>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
|
||||||
<core-empty-box *ngIf="filteredCourses.length == 0" image="assets/img/icons/courses.svg"
|
<core-empty-box *ngIf="filteredCourses.length == 0" image="assets/img/icons/courses.svg"
|
||||||
[message]="'addon.block_myoverview.nocourses' | translate" inline="true">
|
[message]="'addon.block_myoverview.nocourses' | translate" inline="true">
|
||||||
</core-empty-box>
|
</core-empty-box>
|
||||||
|
|
||||||
<!-- List of courses. -->
|
<!-- List of courses. -->
|
||||||
<div class="safe-area-padding">
|
<div class="safe-area-padding" *ngIf="hasCourses">
|
||||||
<ion-grid class="ion-no-padding">
|
<ion-grid class="ion-no-padding" [class.core-no-grid]="layouts.selected != 'card'">
|
||||||
<ion-row class="ion-no-padding">
|
<ion-row class="ion-no-padding">
|
||||||
<ion-col *ngFor="let course of filteredCourses" 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="12" size-sm="6" size-md="6" size-lg="4" size-xl="3">
|
size-xl="3">
|
||||||
<core-courses-course-progress [course]="course" class="core-courseoverview" showAll="true"
|
<core-courses-course-list-item [course]="course" class="core-courseoverview"
|
||||||
[showDownload]="downloadCourseEnabled && downloadEnabled">
|
[showDownload]="downloadCourseEnabled && downloadEnabled" [layout]="layouts.selected">
|
||||||
</core-courses-course-progress>
|
</core-courses-course-list-item>
|
||||||
</ion-col>
|
</ion-col>
|
||||||
</ion-row>
|
</ion-row>
|
||||||
</ion-grid>
|
</ion-grid>
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit, Input, OnDestroy, ViewChild, OnChanges, SimpleChange } from '@angular/core';
|
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChange } from '@angular/core';
|
||||||
import { IonSearchbar } from '@ionic/angular';
|
import { ModalOptions } from '@ionic/core';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreTimeUtils } from '@services/utils/time';
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||||
|
@ -27,8 +27,10 @@ import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion';
|
import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion';
|
||||||
|
import { AddonBlockMyOverviewFilterOptionsComponent } from '../filteroptions/filteroptions';
|
||||||
|
import { IonSearchbar } from '@ionic/angular';
|
||||||
|
|
||||||
const FILTER_PRIORITY = ['all', 'allincludinghidden', 'inprogress', 'future', 'past', 'favourite', 'hidden', 'custom'];
|
const FILTER_PRIORITY: AddonBlockMyOverviewTimeFilters[] = ['all', 'inprogress', 'future', 'past'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to render a my overview block.
|
* Component to render a my overview block.
|
||||||
|
@ -39,104 +41,62 @@ const FILTER_PRIORITY = ['all', 'allincludinghidden', 'inprogress', 'future', 'p
|
||||||
})
|
})
|
||||||
export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy {
|
export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
|
|
||||||
@ViewChild('searchbar') searchbar?: IonSearchbar;
|
|
||||||
@Input() downloadEnabled = false;
|
@Input() downloadEnabled = false;
|
||||||
|
|
||||||
courses = {
|
|
||||||
filter: '',
|
|
||||||
all: <CoreEnrolledCourseDataWithOptions[]> [],
|
|
||||||
allincludinghidden: <CoreEnrolledCourseDataWithOptions[]> [],
|
|
||||||
past: <CoreEnrolledCourseDataWithOptions[]> [],
|
|
||||||
inprogress: <CoreEnrolledCourseDataWithOptions[]> [],
|
|
||||||
future: <CoreEnrolledCourseDataWithOptions[]> [],
|
|
||||||
favourite: <CoreEnrolledCourseDataWithOptions[]> [],
|
|
||||||
hidden: <CoreEnrolledCourseDataWithOptions[]> [],
|
|
||||||
custom: <CoreEnrolledCourseDataWithOptions[]> [], // Leave it empty to avoid download all those courses.
|
|
||||||
};
|
|
||||||
|
|
||||||
customFilter: {
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
selectedFilter = 'inprogress';
|
|
||||||
sort = 'fullname';
|
|
||||||
currentSite?: CoreSite;
|
|
||||||
filteredCourses: CoreEnrolledCourseDataWithOptions[] = [];
|
filteredCourses: CoreEnrolledCourseDataWithOptions[] = [];
|
||||||
prefetchCoursesData = {
|
|
||||||
all: <CorePrefetchStatusInfo> {
|
prefetchCoursesData: CorePrefetchStatusInfo = {
|
||||||
icon: '',
|
icon: '',
|
||||||
statusTranslatable: 'core.loading',
|
statusTranslatable: 'core.loading',
|
||||||
status: '',
|
status: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
},
|
|
||||||
allincludinghidden: <CorePrefetchStatusInfo> {
|
|
||||||
icon: '',
|
|
||||||
statusTranslatable: 'core.loading',
|
|
||||||
status: '',
|
|
||||||
loading: true,
|
|
||||||
},
|
|
||||||
inprogress: <CorePrefetchStatusInfo> {
|
|
||||||
icon: '',
|
|
||||||
statusTranslatable: 'core.loading',
|
|
||||||
status: '',
|
|
||||||
loading: true,
|
|
||||||
},
|
|
||||||
past: <CorePrefetchStatusInfo> {
|
|
||||||
icon: '',
|
|
||||||
statusTranslatable: 'core.loading',
|
|
||||||
status: '',
|
|
||||||
loading: true,
|
|
||||||
},
|
|
||||||
future: <CorePrefetchStatusInfo> {
|
|
||||||
icon: '',
|
|
||||||
statusTranslatable: 'core.loading',
|
|
||||||
status: '',
|
|
||||||
loading: true,
|
|
||||||
},
|
|
||||||
favourite: <CorePrefetchStatusInfo> {
|
|
||||||
icon: '',
|
|
||||||
statusTranslatable: 'core.loading',
|
|
||||||
status: '',
|
|
||||||
loading: true,
|
|
||||||
},
|
|
||||||
hidden: <CorePrefetchStatusInfo> {
|
|
||||||
icon: '',
|
|
||||||
statusTranslatable: 'core.loading',
|
|
||||||
status: '',
|
|
||||||
loading: true,
|
|
||||||
},
|
|
||||||
custom: <CorePrefetchStatusInfo> {
|
|
||||||
icon: '',
|
|
||||||
statusTranslatable: '',
|
|
||||||
status: '',
|
|
||||||
loading: false,
|
|
||||||
}, // Leave it empty to avoid download all those courses.
|
|
||||||
};
|
};
|
||||||
|
|
||||||
showFilters = { // Options are show, disabled, hidden.
|
|
||||||
all: 'show',
|
|
||||||
allincludinghidden: 'show',
|
|
||||||
past: 'show',
|
|
||||||
inprogress: 'show',
|
|
||||||
future: 'show',
|
|
||||||
favourite: 'show',
|
|
||||||
hidden: 'show',
|
|
||||||
custom: 'hidden',
|
|
||||||
};
|
|
||||||
|
|
||||||
showFilter = false;
|
|
||||||
showSelectorFilter = false;
|
|
||||||
showSortFilter = false;
|
|
||||||
downloadCourseEnabled = false;
|
downloadCourseEnabled = false;
|
||||||
downloadCoursesEnabled = false;
|
downloadCoursesEnabled = false;
|
||||||
showSortByShortName = false;
|
|
||||||
|
|
||||||
|
filters: AddonBlockMyOverviewFilterOptions = {
|
||||||
|
enabled: false,
|
||||||
|
show: { // Options are visible, disabled, hidden.
|
||||||
|
all: true,
|
||||||
|
past: true,
|
||||||
|
inprogress: true,
|
||||||
|
future: true,
|
||||||
|
favourite: true,
|
||||||
|
hidden: true,
|
||||||
|
custom: false,
|
||||||
|
},
|
||||||
|
timeFilterSelected: 'inprogress',
|
||||||
|
favouriteSelected: false,
|
||||||
|
hiddenSelected: false,
|
||||||
|
customFilters: [],
|
||||||
|
count: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
filterModalOptions: ModalOptions = {
|
||||||
|
component: AddonBlockMyOverviewFilterOptionsComponent,
|
||||||
|
};
|
||||||
|
|
||||||
|
layouts: AddonBlockMyOverviewLayoutOptions = {
|
||||||
|
options: [],
|
||||||
|
selected: 'card',
|
||||||
|
};
|
||||||
|
|
||||||
|
sort: AddonBlockMyOverviewSortOptions = {
|
||||||
|
shortnameEnabled: false,
|
||||||
|
selected: 'fullname',
|
||||||
|
enabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
textFilter = '';
|
||||||
|
hasCourses = false;
|
||||||
|
|
||||||
|
protected currentSite!: CoreSite;
|
||||||
|
protected allCourses: CoreEnrolledCourseDataWithOptions[] = [];
|
||||||
protected prefetchIconsInitialized = false;
|
protected prefetchIconsInitialized = false;
|
||||||
protected isDestroyed = false;
|
protected isDestroyed = false;
|
||||||
protected coursesObserver?: CoreEventObserver;
|
protected coursesObserver?: CoreEventObserver;
|
||||||
protected updateSiteObserver?: CoreEventObserver;
|
protected updateSiteObserver?: CoreEventObserver;
|
||||||
protected courseIds: number[] = [];
|
|
||||||
protected fetchContentDefaultError = 'Error getting my overview data.';
|
protected fetchContentDefaultError = 'Error getting my overview data.';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -144,7 +104,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
// Refresh the enabled flags if enabled.
|
// Refresh the enabled flags if enabled.
|
||||||
|
@ -161,37 +121,81 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
this.coursesObserver = CoreEvents.on(
|
this.coursesObserver = CoreEvents.on(
|
||||||
CoreCoursesProvider.EVENT_MY_COURSES_UPDATED,
|
CoreCoursesProvider.EVENT_MY_COURSES_UPDATED,
|
||||||
(data) => {
|
(data) => {
|
||||||
|
this.refreshCourseList(data);
|
||||||
if (this.shouldRefreshOnUpdatedEvent(data)) {
|
|
||||||
this.refreshCourseList();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
CoreSites.getCurrentSiteId(),
|
CoreSites.getCurrentSiteId(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.currentSite = CoreSites.getCurrentSite();
|
this.currentSite = CoreSites.getRequiredCurrentSite();
|
||||||
|
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
if (this.currentSite) {
|
|
||||||
promises.push(this.currentSite.getLocalSiteConfig('AddonBlockMyOverviewSort', this.sort).then((value) => {
|
promises.push(this.currentSite.getLocalSiteConfig(
|
||||||
this.sort = value;
|
'AddonBlockMyOverviewSort',
|
||||||
|
this.sort.selected,
|
||||||
|
).then((value) => {
|
||||||
|
this.sort.selected = value;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}));
|
}));
|
||||||
promises.push(this.currentSite.getLocalSiteConfig('AddonBlockMyOverviewFilter', this.selectedFilter).then((value) => {
|
|
||||||
this.selectedFilter = value;
|
promises.push(this.currentSite.getLocalSiteConfig(
|
||||||
|
'AddonBlockMyOverviewLayout',
|
||||||
|
this.layouts.selected,
|
||||||
|
).then((value) => {
|
||||||
|
this.layouts.selected = value;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Wait for the migration.
|
||||||
|
await this.currentSite.getLocalSiteConfig<string>(
|
||||||
|
'AddonBlockMyOverviewFilter',
|
||||||
|
this.filters.timeFilterSelected,
|
||||||
|
).then(async (value) => {
|
||||||
|
if (FILTER_PRIORITY.includes(value as AddonBlockMyOverviewTimeFilters)) {
|
||||||
|
this.filters.timeFilterSelected = value as AddonBlockMyOverviewTimeFilters;
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migrate setting.
|
||||||
|
this.filters.hiddenSelected = value == 'allincludinghidden' || value == 'hidden';
|
||||||
|
|
||||||
|
if (value == 'favourite') {
|
||||||
|
this.filters.favouriteSelected = true;
|
||||||
|
} else {
|
||||||
|
this.filters.favouriteSelected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.saveFilters('all');
|
||||||
|
});
|
||||||
|
|
||||||
|
promises.push(this.currentSite.getLocalSiteConfig(
|
||||||
|
'AddonBlockMyOverviewFavouriteFilter',
|
||||||
|
this.filters.favouriteSelected ? 1 : 0,
|
||||||
|
).then((value) => {
|
||||||
|
this.filters.favouriteSelected = value == 1;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}));
|
||||||
|
|
||||||
|
promises.push(this.currentSite.getLocalSiteConfig(
|
||||||
|
'AddonBlockMyOverviewHiddenFilter',
|
||||||
|
this.filters.hiddenSelected ? 1 : 0,
|
||||||
|
).then((value) => {
|
||||||
|
this.filters.hiddenSelected = value == 1;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}));
|
||||||
|
|
||||||
Promise.all(promises).finally(() => {
|
Promise.all(promises).finally(() => {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect changes on input properties.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
|
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
|
||||||
if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) {
|
if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) {
|
||||||
|
@ -201,21 +205,35 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform the invalidate content function.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @return Resolved when done.
|
|
||||||
*/
|
*/
|
||||||
protected async invalidateContent(): Promise<void> {
|
protected async invalidateContent(): Promise<void> {
|
||||||
|
const courseIds = this.allCourses.map((course) => course.id);
|
||||||
|
|
||||||
|
await this.invalidateCourses(courseIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to invalidate only selected courses.
|
||||||
|
*
|
||||||
|
* @param courseIds Course Id array.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async invalidateCourses(courseIds: number[]): Promise<void> {
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
// Invalidate course completion data.
|
// Invalidate course completion data.
|
||||||
promises.push(CoreCourses.invalidateUserCourses().finally(() =>
|
promises.push(CoreCourses.invalidateUserCourses().finally(() =>
|
||||||
CoreUtils.allPromises(this.courseIds.map((courseId) =>
|
CoreUtils.allPromises(courseIds.map((courseId) =>
|
||||||
AddonCourseCompletion.invalidateCourseCompletion(courseId)))));
|
AddonCourseCompletion.invalidateCourseCompletion(courseId)))));
|
||||||
|
|
||||||
|
if (courseIds.length == 1) {
|
||||||
|
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(courseIds[0]));
|
||||||
|
} else {
|
||||||
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions());
|
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions());
|
||||||
if (this.courseIds.length > 0) {
|
}
|
||||||
promises.push(CoreCourses.invalidateCoursesByField('ids', this.courseIds.join(',')));
|
if (courseIds.length > 0) {
|
||||||
|
promises.push(CoreCourses.invalidateCoursesByField('ids', courseIds.join(',')));
|
||||||
}
|
}
|
||||||
|
|
||||||
await CoreUtils.allPromises(promises).finally(() => {
|
await CoreUtils.allPromises(promises).finally(() => {
|
||||||
|
@ -231,173 +249,173 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
|
|
||||||
const showCategories = config?.displaycategories?.value == '1';
|
const showCategories = config?.displaycategories?.value == '1';
|
||||||
|
|
||||||
const courses = await CoreCoursesHelper.getUserCoursesWithOptions(this.sort, undefined, undefined, showCategories, {
|
this.allCourses = await CoreCoursesHelper.getUserCoursesWithOptions(
|
||||||
|
this.sort.selected,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
showCategories,
|
||||||
|
{
|
||||||
readingStrategy: refresh ? CoreSitesReadingStrategy.PREFER_NETWORK : undefined,
|
readingStrategy: refresh ? CoreSitesReadingStrategy.PREFER_NETWORK : undefined,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.hasCourses = this.allCourses.length > 0;
|
||||||
|
|
||||||
|
this.loadSort();
|
||||||
|
this.loadLayouts(config?.layouts?.value.split(','));
|
||||||
|
this.loadFilters(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load sort.
|
||||||
|
*/
|
||||||
|
protected loadSort(): void {
|
||||||
|
const sampleCourse = this.allCourses[0];
|
||||||
|
|
||||||
// Check to show sort by short name only if the text is visible.
|
// Check to show sort by short name only if the text is visible.
|
||||||
if (courses.length > 0) {
|
this.sort.shortnameEnabled = !!sampleCourse?.displayname && !!sampleCourse?.shortname &&
|
||||||
const sampleCourse = courses[0];
|
sampleCourse?.fullname != sampleCourse?.displayname;
|
||||||
this.showSortByShortName = !!sampleCourse.displayname && !!sampleCourse.shortname &&
|
|
||||||
sampleCourse.fullname != sampleCourse.displayname;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rollback to sort by full name if user is sorting by short name then Moodle web change the config.
|
// Rollback to sort by full name if user is sorting by short name then Moodle web change the config.
|
||||||
if (!this.showSortByShortName && this.sort === 'shortname') {
|
if (!this.sort.shortnameEnabled && this.sort.selected === 'shortname') {
|
||||||
this.switchSort('fullname');
|
this.saveSort('fullname');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.courseIds = courses.map((course) => course.id);
|
this.sort.enabled = sampleCourse?.lastaccess !== undefined;
|
||||||
|
|
||||||
this.showSortFilter = courses.length > 0 && typeof courses[0].lastaccess != 'undefined';
|
|
||||||
|
|
||||||
this.initCourseFilters(courses);
|
|
||||||
|
|
||||||
this.courses.filter = '';
|
|
||||||
this.showFilter = false;
|
|
||||||
|
|
||||||
this.showFilters.all = this.getShowFilterValue(
|
|
||||||
!config || config.displaygroupingall?.value == '1',
|
|
||||||
this.courses.all.length === 0,
|
|
||||||
);
|
|
||||||
// Do not show allincludinghiddenif config it's not present (before 3.8).
|
|
||||||
this.showFilters.allincludinghidden =
|
|
||||||
this.getShowFilterValue(
|
|
||||||
config?.displaygroupingallincludinghidden?.value == '1',
|
|
||||||
this.courses.allincludinghidden.length === 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.showFilters.inprogress = this.getShowFilterValue(
|
|
||||||
!config || config.displaygroupinginprogress?.value == '1',
|
|
||||||
this.courses.inprogress.length === 0,
|
|
||||||
);
|
|
||||||
this.showFilters.past = this.getShowFilterValue(
|
|
||||||
!config || config.displaygroupingpast?.value == '1',
|
|
||||||
this.courses.past.length === 0,
|
|
||||||
);
|
|
||||||
this.showFilters.future = this.getShowFilterValue(
|
|
||||||
!config || config.displaygroupingfuture?.value == '1',
|
|
||||||
this.courses.future.length === 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.showSelectorFilter = courses.length > 0 && (this.courses.past.length > 0 || this.courses.future.length > 0 ||
|
|
||||||
typeof courses[0].enddate != 'undefined');
|
|
||||||
|
|
||||||
this.showFilters.hidden = this.getShowFilterValue(
|
|
||||||
this.showSelectorFilter && typeof courses[0].hidden != 'undefined' &&
|
|
||||||
(!config || config.displaygroupinghidden?.value == '1'),
|
|
||||||
this.courses.hidden.length === 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.showFilters.favourite = this.getShowFilterValue(
|
|
||||||
this.showSelectorFilter && typeof courses[0].isfavourite != 'undefined' &&
|
|
||||||
(!config || config.displaygroupingstarred?.value == '1' || config.displaygroupingfavourites?.value == '1'),
|
|
||||||
this.courses.favourite.length === 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.showFilters.custom = this.getShowFilterValue(
|
|
||||||
this.showSelectorFilter && config?.displaygroupingcustomfield?.value == '1' && !!config?.customfieldsexport?.value,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
if (this.showFilters.custom == 'show') {
|
|
||||||
this.customFilter = CoreTextUtils.parseJSON(config?.customfieldsexport?.value || '[]', []);
|
|
||||||
} else {
|
|
||||||
this.customFilter = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.showSelectorFilter) {
|
|
||||||
// Check if any selector is shown and not disabled.
|
|
||||||
this.showSelectorFilter = Object.keys(this.showFilters).some((key) => this.showFilters[key] == 'show');
|
|
||||||
|
|
||||||
if (!this.showSelectorFilter) {
|
|
||||||
// All filters disabled, display all the courses.
|
|
||||||
this.showFilters.all = 'show';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.showSelectorFilter) {
|
|
||||||
// No selector, display all the courses.
|
|
||||||
this.selectedFilter = 'all';
|
|
||||||
}
|
|
||||||
this.setCourseFilter(this.selectedFilter);
|
|
||||||
|
|
||||||
this.initPrefetchCoursesIcons();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to help with filter values.
|
* Load filters.
|
||||||
*
|
*
|
||||||
* @param showCondition If true, filter will be shown.
|
* @param config Block configuration.
|
||||||
* @param disabledCondition If true, and showCondition is also met, it will be shown as disabled.
|
|
||||||
* @return show / disabled / hidden value.
|
|
||||||
*/
|
*/
|
||||||
protected getShowFilterValue(showCondition: boolean, disabledCondition: boolean): string {
|
protected loadFilters(
|
||||||
return showCondition ? (disabledCondition ? 'disabled' : 'show') : 'hidden';
|
config?: Record<string, { name: string; value: string; type: string }>,
|
||||||
|
): void {
|
||||||
|
this.textFilter = '';
|
||||||
|
|
||||||
|
const sampleCourse = this.allCourses[0];
|
||||||
|
|
||||||
|
// Do not show hidden if config it's not present (before 3.8) but if hidden is enabled.
|
||||||
|
this.filters.show.hidden =
|
||||||
|
config?.displaygroupingallincludinghidden?.value == '1' ||
|
||||||
|
sampleCourse.hidden !== undefined && (!config || config.displaygroupinghidden?.value == '1');
|
||||||
|
|
||||||
|
this.filters.show.all = !config || config.displaygroupingall?.value == '1';
|
||||||
|
this.filters.show.inprogress = !config || config.displaygroupinginprogress?.value == '1';
|
||||||
|
this.filters.show.past = !config || config.displaygroupingpast?.value == '1';
|
||||||
|
this.filters.show.future = !config || config.displaygroupingfuture?.value == '1';
|
||||||
|
|
||||||
|
this.filters.show.favourite = sampleCourse.isfavourite !== undefined &&
|
||||||
|
(!config || config.displaygroupingstarred?.value == '1' || config.displaygroupingfavourites?.value == '1');
|
||||||
|
|
||||||
|
this.filters.show.custom = config?.displaygroupingcustomfield?.value == '1' && !!config?.customfieldsexport?.value;
|
||||||
|
|
||||||
|
this.filters.customFilters = this.filters.show.custom
|
||||||
|
? CoreTextUtils.parseJSON(config?.customfieldsexport?.value || '[]', [])
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Check if any selector is shown and not disabled.
|
||||||
|
this.filters.enabled = Object.keys(this.filters.show).some((key) => this.filters.show[key]);
|
||||||
|
|
||||||
|
if (!this.filters.enabled) {
|
||||||
|
// All filters disabled, display all the courses.
|
||||||
|
this.filters.show.all = true;
|
||||||
|
this.saveFilters('all');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filterModalOptions.componentProps = {
|
||||||
|
options: Object.assign({}, this.filters),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.filterCourses();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether list should be refreshed based on a EVENT_MY_COURSES_UPDATED event.
|
* Load block layouts.
|
||||||
|
*
|
||||||
|
* @param layouts Config available layouts.
|
||||||
|
*/
|
||||||
|
protected loadLayouts(layouts?: string[]): void {
|
||||||
|
this.layouts.options = [];
|
||||||
|
|
||||||
|
if (layouts === undefined) {
|
||||||
|
this.layouts.options = ['card', 'list'];
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
layouts.forEach((layout) => {
|
||||||
|
if (layout == '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validLayout: AddonBlockMyOverviewLayouts = layout == 'summary' ? 'list' : layout as AddonBlockMyOverviewLayouts;
|
||||||
|
if (!this.layouts.options.includes(validLayout)) {
|
||||||
|
this.layouts.options.push(validLayout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no layout is available use card.
|
||||||
|
if (this.layouts.options.length == 0) {
|
||||||
|
this.layouts.options = ['card'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.layouts.options.includes(this.layouts.selected)) {
|
||||||
|
this.layouts.selected = this.layouts.options[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh course list based on a EVENT_MY_COURSES_UPDATED event.
|
||||||
*
|
*
|
||||||
* @param data Event data.
|
* @param data Event data.
|
||||||
* @return Whether to refresh.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected shouldRefreshOnUpdatedEvent(data: CoreCoursesMyCoursesUpdatedEventData): boolean {
|
protected async refreshCourseList(data: CoreCoursesMyCoursesUpdatedEventData): Promise<void> {
|
||||||
if (data.action == CoreCoursesProvider.ACTION_ENROL) {
|
if (data.action == CoreCoursesProvider.ACTION_ENROL) {
|
||||||
// Always update if user enrolled in a course.
|
// Always update if user enrolled in a course.
|
||||||
return true;
|
return await this.refreshContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const course = this.allCourses.find((course) => course.id == data.courseId);
|
||||||
if (data.action == CoreCoursesProvider.ACTION_STATE_CHANGED) {
|
if (data.action == CoreCoursesProvider.ACTION_STATE_CHANGED) {
|
||||||
// Update list when course state changes (favourite, hidden).
|
if (!course) {
|
||||||
return true;
|
// Not found, use WS update.
|
||||||
|
return await this.refreshContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.state == CoreCoursesProvider.STATE_FAVOURITE) {
|
||||||
|
course.isfavourite = !!data.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.state == CoreCoursesProvider.STATE_HIDDEN) {
|
||||||
|
course.hidden = !!data.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.invalidateCourses([course.id]);
|
||||||
|
await this.filterCourses();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.action == CoreCoursesProvider.ACTION_VIEW && data.courseId != CoreSites.getCurrentSiteHomeId()) {
|
if (data.action == CoreCoursesProvider.ACTION_VIEW && data.courseId != CoreSites.getCurrentSiteHomeId()) {
|
||||||
// User viewed a course. If it isn't the most recent accessed course, update the list.
|
if (!course) {
|
||||||
let recentAccessedCourse: CoreEnrolledCourseDataWithOptions | undefined;
|
// Not found, use WS update.
|
||||||
if (this.sort == 'lastaccess') {
|
return await this.refreshContent();
|
||||||
recentAccessedCourse = this.courses.allincludinghidden[0];
|
|
||||||
} else {
|
|
||||||
recentAccessedCourse = Array.from(this.courses.allincludinghidden)
|
|
||||||
.sort((a, b) => (b.lastaccess || 0) - (a.lastaccess || 0))[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recentAccessedCourse && data.courseId != recentAccessedCourse.id) {
|
course.lastaccess = CoreTimeUtils.timestamp();
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
await this.invalidateCourses([course.id]);
|
||||||
}
|
await this.filterCourses();
|
||||||
|
|
||||||
/**
|
|
||||||
* The filter has changed.
|
|
||||||
*
|
|
||||||
* @param Received Event.
|
|
||||||
*/
|
|
||||||
filterChanged(event: Event): void {
|
|
||||||
const target = <HTMLInputElement>event?.target || null;
|
|
||||||
|
|
||||||
const newValue = target?.value.trim().toLowerCase();
|
|
||||||
if (!newValue || this.courses.allincludinghidden.length <= 0) {
|
|
||||||
this.filteredCourses = this.courses.allincludinghidden;
|
|
||||||
} else {
|
|
||||||
// Use displayname if available, or fullname if not.
|
|
||||||
if (this.courses.allincludinghidden.length > 0 &&
|
|
||||||
typeof this.courses.allincludinghidden[0].displayname != 'undefined') {
|
|
||||||
this.filteredCourses = this.courses.allincludinghidden.filter((course) =>
|
|
||||||
course.displayname && course.displayname.toLowerCase().indexOf(newValue) > -1);
|
|
||||||
} else {
|
|
||||||
this.filteredCourses = this.courses.allincludinghidden.filter((course) =>
|
|
||||||
course.fullname.toLowerCase().indexOf(newValue) > -1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the prefetch icon for selected courses.
|
* Initialize the prefetch icon for selected courses.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected initPrefetchCoursesIcons(): void {
|
async initPrefetchCoursesIcons(): Promise<void> {
|
||||||
if (this.prefetchIconsInitialized || !this.downloadEnabled) {
|
if (this.prefetchIconsInitialized || !this.downloadEnabled) {
|
||||||
// Already initialized.
|
// Already initialized.
|
||||||
return;
|
return;
|
||||||
|
@ -405,10 +423,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
|
|
||||||
this.prefetchIconsInitialized = true;
|
this.prefetchIconsInitialized = true;
|
||||||
|
|
||||||
Object.keys(this.prefetchCoursesData).forEach(async (filter) => {
|
this.prefetchCoursesData = await CoreCourseHelper.initPrefetchCoursesIcons(this.filteredCourses, this.prefetchCoursesData);
|
||||||
this.prefetchCoursesData[filter] =
|
|
||||||
await CoreCourseHelper.initPrefetchCoursesIcons(this.courses[filter], this.prefetchCoursesData[filter]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -417,106 +432,153 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async prefetchCourses(): Promise<void> {
|
async prefetchCourses(): Promise<void> {
|
||||||
const selected = this.selectedFilter;
|
const initialIcon = this.prefetchCoursesData.icon;
|
||||||
const initialIcon = this.prefetchCoursesData[selected].icon;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await CoreCourseHelper.prefetchCourses(this.courses[selected], this.prefetchCoursesData[selected]);
|
await CoreCourseHelper.prefetchCourses(this.filteredCourses, this.prefetchCoursesData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!this.isDestroyed) {
|
if (!this.isDestroyed) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||||
this.prefetchCoursesData[selected].icon = initialIcon;
|
this.prefetchCoursesData.icon = initialIcon;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh the list of courses.
|
* Text filter changed.
|
||||||
*
|
*
|
||||||
* @return Promise resolved when done.
|
* @param target Searchbar element.
|
||||||
*/
|
*/
|
||||||
protected async refreshCourseList(): Promise<void> {
|
filterTextChanged(target: IonSearchbar): void {
|
||||||
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED);
|
this.textFilter = target.value || '';
|
||||||
|
|
||||||
await this.loadContent(true);
|
this.filterCourses();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The selected courses filter have changed.
|
|
||||||
*
|
|
||||||
* @param filter New filter
|
|
||||||
*/
|
|
||||||
selectedChanged(filter: string): void {
|
|
||||||
this.selectedFilter = filter;
|
|
||||||
this.setCourseFilter(this.selectedFilter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set selected courses filter.
|
* Set selected courses filter.
|
||||||
*
|
|
||||||
* @param filter Filter name to set.
|
|
||||||
*/
|
*/
|
||||||
protected async setCourseFilter(filter: string): Promise<void> {
|
protected async filterCourses(): Promise<void> {
|
||||||
this.selectedFilter = filter;
|
this.filters.count = 0;
|
||||||
|
|
||||||
if (this.showFilters.custom == 'show' && filter.startsWith('custom-') &&
|
let timeFilter = this.filters.timeFilterSelected;
|
||||||
typeof this.customFilter[filter.substr(7)] != 'undefined') {
|
|
||||||
|
|
||||||
const filterName = this.block.configsRecord!.customfiltergrouping.value;
|
// Filter is not active, take the first active or all.
|
||||||
const filterValue = this.customFilter[filter.substr(7)].value;
|
if (!this.filters.show[timeFilter]) {
|
||||||
|
timeFilter = FILTER_PRIORITY.find((name) => this.filters.show[name]) || 'all';
|
||||||
|
|
||||||
|
this.saveFilters(timeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeFilter !== 'all') {
|
||||||
|
this.filters.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filteredCourses = this.allCourses;
|
||||||
|
|
||||||
|
const customFilterName = this.block.configsRecord?.customfiltergrouping.value;
|
||||||
|
const customFilterValue = this.filters.customSelected;
|
||||||
|
if (customFilterName && this.filters.show.custom && customFilterValue !== undefined) {
|
||||||
|
this.filters.count++;
|
||||||
|
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
try {
|
try {
|
||||||
const courses = await CoreCourses.getEnrolledCoursesByCustomField(filterName, filterValue);
|
const courses = await CoreCourses.getEnrolledCoursesByCustomField(customFilterName, customFilterValue);
|
||||||
|
|
||||||
// Get the courses information from allincludinghidden to get the max info about the course.
|
|
||||||
const courseIds = courses.map((course) => course.id);
|
const courseIds = courses.map((course) => course.id);
|
||||||
|
|
||||||
this.filteredCourses = this.courses.allincludinghidden.filter((allCourse) =>
|
this.filteredCourses = this.filteredCourses.filter((course) => courseIds.includes(course.id));
|
||||||
courseIds.indexOf(allCourse.id) !== -1);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError);
|
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError);
|
||||||
} finally {
|
} finally {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only save the filter if not a custom one.
|
const onlyFavourite = this.filters.show.favourite && this.filters.favouriteSelected;
|
||||||
this.currentSite?.setLocalSiteConfig('AddonBlockMyOverviewFilter', filter);
|
if (onlyFavourite) {
|
||||||
|
this.filters.count++;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.showFilters[filter] == 'show') {
|
const showHidden = this.filters.show.hidden && this.filters.hiddenSelected;
|
||||||
this.filteredCourses = this.courses[filter];
|
if (showHidden) {
|
||||||
|
this.filters.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time filter, favourite and hidden.
|
||||||
|
const today = CoreTimeUtils.timestamp();
|
||||||
|
this.filteredCourses = this.filteredCourses.filter((course) => {
|
||||||
|
let include = timeFilter == 'all';
|
||||||
|
|
||||||
|
if (!include) {
|
||||||
|
if ((course.enddate && course.enddate < today) || course.completed) {
|
||||||
|
// Courses that have already ended.
|
||||||
|
include = timeFilter == 'past';
|
||||||
|
} else if (course.startdate && course.startdate > today) {
|
||||||
|
// Courses that have not started yet.
|
||||||
|
include = timeFilter == 'future';
|
||||||
} else {
|
} else {
|
||||||
const activeFilter = FILTER_PRIORITY.find((name) => this.showFilters[name] == 'show');
|
// Courses still in progress.
|
||||||
|
include = timeFilter == 'inprogress';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (activeFilter) {
|
if (onlyFavourite) {
|
||||||
this.setCourseFilter(activeFilter);
|
include = include && !!course.isfavourite;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!showHidden) {
|
||||||
|
include = include && !course.hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
return include;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Text filter.
|
||||||
|
const value = this.textFilter.trim().toLowerCase();
|
||||||
|
if (value != '' && this.filteredCourses.length > 0) {
|
||||||
|
// Use displayname if available, or fullname if not.
|
||||||
|
if (this.filteredCourses[0].displayname !== undefined) {
|
||||||
|
this.filteredCourses = this.filteredCourses.filter((course) =>
|
||||||
|
course.displayname && course.displayname.toLowerCase().indexOf(value) > -1);
|
||||||
|
} else {
|
||||||
|
this.filteredCourses = this.filteredCourses.filter((course) =>
|
||||||
|
course.fullname.toLowerCase().indexOf(value) > -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.sortCourses(this.sort.selected);
|
||||||
|
|
||||||
|
// Refresh prefetch data (if enabled).
|
||||||
|
this.prefetchIconsInitialized = false;
|
||||||
|
this.initPrefetchCoursesIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init courses filters.
|
* Sort courses
|
||||||
*
|
*
|
||||||
* @param courses Courses to filter.
|
* @param sort Sort by value.
|
||||||
*/
|
*/
|
||||||
initCourseFilters(courses: CoreEnrolledCourseDataWithOptions[]): void {
|
sortCourses(sort: string): void {
|
||||||
this.courses.allincludinghidden = courses;
|
if (!this.sort.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.showSortFilter) {
|
if (this.sort.selected != sort) {
|
||||||
if (this.sort == 'lastaccess') {
|
this.saveSort(sort);
|
||||||
courses.sort((a, b) => (b.lastaccess || 0) - (a.lastaccess || 0));
|
}
|
||||||
} else if (this.sort == 'fullname') {
|
|
||||||
courses.sort((a, b) => {
|
if (this.sort.selected == 'lastaccess') {
|
||||||
|
this.filteredCourses.sort((a, b) => (b.lastaccess || 0) - (a.lastaccess || 0));
|
||||||
|
} else if (this.sort.selected == 'fullname') {
|
||||||
|
this.filteredCourses.sort((a, b) => {
|
||||||
const compareA = a.fullname.toLowerCase();
|
const compareA = a.fullname.toLowerCase();
|
||||||
const compareB = b.fullname.toLowerCase();
|
const compareB = b.fullname.toLowerCase();
|
||||||
|
|
||||||
return compareA.localeCompare(compareB);
|
return compareA.localeCompare(compareB);
|
||||||
});
|
});
|
||||||
} else if (this.sort == 'shortname') {
|
} else if (this.sort.selected == 'shortname') {
|
||||||
courses.sort((a, b) => {
|
this.filteredCourses.sort((a, b) => {
|
||||||
const compareA = a.shortname.toLowerCase();
|
const compareA = a.shortname.toLowerCase();
|
||||||
const compareB = b.shortname.toLowerCase();
|
const compareB = b.shortname.toLowerCase();
|
||||||
|
|
||||||
|
@ -525,87 +587,63 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.courses.all = [];
|
/**
|
||||||
this.courses.past = [];
|
* Saves filters value.
|
||||||
this.courses.inprogress = [];
|
*
|
||||||
this.courses.future = [];
|
* @param timeFilter New time filter.
|
||||||
this.courses.favourite = [];
|
* @return Promise resolved when done.
|
||||||
this.courses.hidden = [];
|
*/
|
||||||
|
async saveFilters(timeFilter: AddonBlockMyOverviewTimeFilters): Promise<void> {
|
||||||
|
this.filters.timeFilterSelected = timeFilter;
|
||||||
|
|
||||||
const today = CoreTimeUtils.timestamp();
|
this.filterModalOptions.componentProps = {
|
||||||
courses.forEach((course) => {
|
options: Object.assign({}, this.filters),
|
||||||
if (course.hidden) {
|
};
|
||||||
this.courses.hidden.push(course);
|
|
||||||
} else {
|
|
||||||
this.courses.all.push(course);
|
|
||||||
|
|
||||||
if ((course.enddate && course.enddate < today) || course.completed) {
|
await Promise.all([
|
||||||
// Courses that have already ended.
|
this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewFilter', this.filters.timeFilterSelected),
|
||||||
this.courses.past.push(course);
|
this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewFavouriteFilter', this.filters.favouriteSelected ? 1 : 0),
|
||||||
} else if (course.startdate && course.startdate > today) {
|
this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewHiddenFilter', this.filters.hiddenSelected ? 1 : 0),
|
||||||
// Courses that have not started yet.
|
]);
|
||||||
this.courses.future.push(course);
|
|
||||||
} else {
|
|
||||||
// Courses still in progress.
|
|
||||||
this.courses.inprogress.push(course);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (course.isfavourite) {
|
|
||||||
this.courses.favourite.push(course);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setCourseFilter(this.selectedFilter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The selected courses sort filter have changed.
|
* Saves layout value.
|
||||||
|
*
|
||||||
|
* @param layout New layout.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async saveLayout(layout: AddonBlockMyOverviewLayouts): Promise<void> {
|
||||||
|
this.layouts.selected = layout;
|
||||||
|
|
||||||
|
await this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewLayout', this.layouts.selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves sort courses value.
|
||||||
*
|
*
|
||||||
* @param sort New sorting.
|
* @param sort New sorting.
|
||||||
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
switchSort(sort: string): void {
|
async saveSort(sort: string): Promise<void> {
|
||||||
this.sort = sort;
|
this.sort.selected = sort;
|
||||||
this.currentSite?.setLocalSiteConfig('AddonBlockMyOverviewSort', this.sort);
|
|
||||||
this.initCourseFilters(this.courses.allincludinghidden);
|
await this.currentSite.setLocalSiteConfig('AddonBlockMyOverviewSort', this.sort.selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show or hide the filter.
|
* Opens display Options modal.
|
||||||
*/
|
|
||||||
switchFilter(): void {
|
|
||||||
this.showFilter = !this.showFilter;
|
|
||||||
this.courses.filter = '';
|
|
||||||
|
|
||||||
if (this.showFilter) {
|
|
||||||
this.filteredCourses = this.courses.allincludinghidden;
|
|
||||||
} else {
|
|
||||||
this.setCourseFilter(this.selectedFilter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Popover closed after clicking switch filter.
|
|
||||||
*/
|
|
||||||
switchFilterClosed(): void {
|
|
||||||
if (this.showFilter) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.searchbar?.setFocus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If switch button that enables the filter input is shown or not.
|
|
||||||
*
|
*
|
||||||
* @return If switch button that enables the filter input is shown or not.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
showFilterSwitchButton(): boolean {
|
filterOptionsChanged(modalData: AddonBlockMyOverviewFilterOptions): void {
|
||||||
return this.loaded && this.courses.allincludinghidden && this.courses.allincludinghidden.length > 5;
|
this.filters = modalData;
|
||||||
|
this.saveFilters(this.filters.timeFilterSelected);
|
||||||
|
this.filterCourses();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being destroyed.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.isDestroyed = true;
|
this.isDestroyed = true;
|
||||||
|
@ -614,3 +652,39 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AddonBlockMyOverviewLayouts = 'card'|'list';
|
||||||
|
type AddonBlockMyOverviewTimeFilters = 'all'|'inprogress'|'future'|'past';
|
||||||
|
|
||||||
|
export type AddonBlockMyOverviewFilterOptions = {
|
||||||
|
enabled: boolean;
|
||||||
|
show: {
|
||||||
|
all: boolean;
|
||||||
|
inprogress: boolean;
|
||||||
|
future: boolean;
|
||||||
|
past: boolean;
|
||||||
|
favourite: boolean;
|
||||||
|
hidden: boolean;
|
||||||
|
custom: boolean;
|
||||||
|
};
|
||||||
|
timeFilterSelected: AddonBlockMyOverviewTimeFilters;
|
||||||
|
favouriteSelected: boolean;
|
||||||
|
hiddenSelected: boolean;
|
||||||
|
customFilters: {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
customSelected?: string;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AddonBlockMyOverviewLayoutOptions = {
|
||||||
|
options: AddonBlockMyOverviewLayouts[];
|
||||||
|
selected: AddonBlockMyOverviewLayouts;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AddonBlockMyOverviewSortOptions = {
|
||||||
|
shortnameEnabled: boolean;
|
||||||
|
selected: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
{
|
{
|
||||||
"all": "All (except removed from view)",
|
"all": "All (except removed from view)",
|
||||||
"allincludinghidden": "All",
|
"allincludinghidden": "All",
|
||||||
"favourites": "Starred",
|
"aria:hiddencourses": "Show courses removed from view",
|
||||||
|
"card": "Card",
|
||||||
|
"favouritesonly": "Show starred courses only",
|
||||||
"future": "Future",
|
"future": "Future",
|
||||||
"hiddencourses": "Removed from view",
|
|
||||||
"inprogress": "In progress",
|
"inprogress": "In progress",
|
||||||
"lastaccessed": "Last accessed",
|
"lastaccessed": "Last accessed",
|
||||||
|
"list": "List",
|
||||||
"nocourses": "No courses",
|
"nocourses": "No courses",
|
||||||
"past": "Past",
|
"past": "Past",
|
||||||
"pluginname": "Course overview",
|
"pluginname": "Course overview",
|
||||||
|
|
|
@ -3,20 +3,6 @@
|
||||||
<h2>{{ 'addon.block_recentlyaccessedcourses.pluginname' | translate }}</h2>
|
<h2>{{ 'addon.block_recentlyaccessedcourses.pluginname' | translate }}</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<div slot="end" class="flex-row">
|
<div slot="end" class="flex-row">
|
||||||
<div *ngIf="downloadCoursesEnabled && downloadEnabled && courses && courses.length > 1" class="core-button-spinner">
|
|
||||||
<ion-button *ngIf="prefetchCoursesData.icon && !prefetchCoursesData.loading" fill="clear" color="dark"
|
|
||||||
(click)="prefetchCourses()" [attr.aria-label]="'core.courses.downloadcourses' | translate">
|
|
||||||
<ion-icon [name]="prefetchCoursesData.icon" slot="icon-only" aria-hidden="true"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge"
|
|
||||||
role="progressbar" [attr.aria-valuemax]="prefetchCoursesData.total"
|
|
||||||
[attr.aria-valuenow]="prefetchCoursesData.count" [attr.aria-valuetext]="prefetchCoursesData.badgeA11yText">
|
|
||||||
{{prefetchCoursesData.badge}}
|
|
||||||
</ion-badge>
|
|
||||||
<ion-spinner *ngIf="!prefetchCoursesData.icon || prefetchCoursesData.loading"
|
|
||||||
[attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId">
|
<core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId">
|
||||||
</core-horizontal-scroll-controls>
|
</core-horizontal-scroll-controls>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,16 +11,13 @@
|
||||||
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg" inline="true"
|
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg" inline="true"
|
||||||
[message]="'addon.block_recentlyaccessedcourses.nocourses' | translate"></core-empty-box>
|
[message]="'addon.block_recentlyaccessedcourses.nocourses' | translate"></core-empty-box>
|
||||||
<!-- List of courses. -->
|
<!-- List of courses. -->
|
||||||
<div
|
<div [hidden]="courses.length === 0" [id]="scrollElementId" class="core-horizontal-scroll"
|
||||||
[id]="scrollElementId"
|
(scroll)="scrollControls.updateScrollPosition()">
|
||||||
class="core-horizontal-scroll"
|
|
||||||
(scroll)="scrollControls.updateScrollPosition()"
|
|
||||||
>
|
|
||||||
<div (onResize)="scrollControls.updateScrollPosition()" class="flex-row">
|
<div (onResize)="scrollControls.updateScrollPosition()" class="flex-row">
|
||||||
<div class="safe-area-pseudo-padding-start"></div>
|
<div class="safe-area-pseudo-padding-start"></div>
|
||||||
<ng-container *ngFor="let course of courses">
|
<ng-container *ngFor="let course of courses">
|
||||||
<core-courses-course-progress [course]="course" class="core-recentlyaccessedcourses"
|
<core-courses-course-list-item [course]="course" class="core-recentlyaccessedcourses" layout="summarycard"
|
||||||
[showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-progress>
|
[showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-list-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="safe-area-pseudo-padding-end"></div>
|
<div class="safe-area-pseudo-padding-end"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit, OnDestroy, Input, OnChanges, SimpleChange } from '@angular/core';
|
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import {
|
import {
|
||||||
|
@ -21,13 +21,11 @@ import {
|
||||||
CoreCourses,
|
CoreCourses,
|
||||||
CoreCourseSummaryData,
|
CoreCourseSummaryData,
|
||||||
} from '@features/courses/services/courses';
|
} from '@features/courses/services/courses';
|
||||||
import { CoreCourseSearchedDataWithExtraInfoAndOptions, CoreCoursesHelper } from '@features/courses/services/courses-helper';
|
import { CoreCourseSearchedDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper';
|
||||||
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper';
|
|
||||||
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
||||||
import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion';
|
import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion';
|
||||||
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
|
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to render a recent courses block.
|
* Component to render a recent courses block.
|
||||||
|
@ -36,24 +34,15 @@ import { CoreDomUtils } from '@services/utils/dom';
|
||||||
selector: 'addon-block-recentlyaccessedcourses',
|
selector: 'addon-block-recentlyaccessedcourses',
|
||||||
templateUrl: 'addon-block-recentlyaccessedcourses.html',
|
templateUrl: 'addon-block-recentlyaccessedcourses.html',
|
||||||
})
|
})
|
||||||
export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy {
|
export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@Input() downloadEnabled = false;
|
@Input() downloadEnabled = false;
|
||||||
|
|
||||||
courses: (Omit<CoreCourseSummaryData, 'visible'> & CoreCourseSearchedDataWithExtraInfoAndOptions)[] = [];
|
courses: (Omit<CoreCourseSummaryData, 'visible'> & CoreCourseSearchedDataWithExtraInfoAndOptions)[] = [];
|
||||||
prefetchCoursesData: CorePrefetchStatusInfo = {
|
|
||||||
icon: '',
|
|
||||||
statusTranslatable: 'core.loading',
|
|
||||||
status: '',
|
|
||||||
loading: true,
|
|
||||||
badge: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
downloadCourseEnabled = false;
|
downloadCourseEnabled = false;
|
||||||
downloadCoursesEnabled = false;
|
|
||||||
scrollElementId!: string;
|
scrollElementId!: string;
|
||||||
|
|
||||||
protected prefetchIconsInitialized = false;
|
|
||||||
protected isDestroyed = false;
|
protected isDestroyed = false;
|
||||||
protected coursesObserver?: CoreEventObserver;
|
protected coursesObserver?: CoreEventObserver;
|
||||||
protected updateSiteObserver?: CoreEventObserver;
|
protected updateSiteObserver?: CoreEventObserver;
|
||||||
|
@ -74,12 +63,10 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom
|
||||||
|
|
||||||
// Refresh the enabled flags if enabled.
|
// Refresh the enabled flags if enabled.
|
||||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||||
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
|
||||||
|
|
||||||
// Refresh the enabled flags if site is updated.
|
// Refresh the enabled flags if site is updated.
|
||||||
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||||
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
|
||||||
|
|
||||||
}, CoreSites.getCurrentSiteId());
|
}, CoreSites.getCurrentSiteId());
|
||||||
|
|
||||||
|
@ -94,16 +81,6 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
|
|
||||||
if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) {
|
|
||||||
// Download all courses is enabled now, initialize it.
|
|
||||||
this.initPrefetchCoursesIcons();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
@ -136,9 +113,7 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom
|
||||||
promises.push(CoreCourses.invalidateCoursesByField('ids', courseIds.join(',')));
|
promises.push(CoreCourses.invalidateCoursesByField('ids', courseIds.join(',')));
|
||||||
}
|
}
|
||||||
|
|
||||||
await CoreUtils.allPromises(promises).finally(() => {
|
await CoreUtils.allPromises(promises);
|
||||||
this.prefetchIconsInitialized = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -170,24 +145,6 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom
|
||||||
course.categoryname = '';
|
course.categoryname = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await CoreCoursesHelper.loadCoursesColorAndImage(courses);
|
|
||||||
|
|
||||||
this.initPrefetchCoursesIcons();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the prefetch icon for selected courses.
|
|
||||||
*/
|
|
||||||
protected async initPrefetchCoursesIcons(): Promise<void> {
|
|
||||||
if (this.prefetchIconsInitialized || !this.downloadEnabled) {
|
|
||||||
// Already initialized.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.prefetchIconsInitialized = true;
|
|
||||||
|
|
||||||
this.prefetchCoursesData = await CoreCourseHelper.initPrefetchCoursesIcons(this.courses, this.prefetchCoursesData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -221,26 +178,6 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom
|
||||||
data.state == CoreCoursesProvider.STATE_FAVOURITE && course) {
|
data.state == CoreCoursesProvider.STATE_FAVOURITE && course) {
|
||||||
course.isfavourite = !!data.value;
|
course.isfavourite = !!data.value;
|
||||||
await this.invalidateCourses([course.id]);
|
await this.invalidateCourses([course.id]);
|
||||||
|
|
||||||
this.initPrefetchCoursesIcons();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prefetch all the shown courses.
|
|
||||||
*
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
async prefetchCourses(): Promise<void> {
|
|
||||||
const initialIcon = this.prefetchCoursesData.icon;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await CoreCourseHelper.prefetchCourses(this.courses, this.prefetchCoursesData);
|
|
||||||
} catch (error) {
|
|
||||||
if (!this.isDestroyed) {
|
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
|
||||||
this.prefetchCoursesData.icon = initialIcon;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,20 +3,6 @@
|
||||||
<h2>{{ 'addon.block_starredcourses.pluginname' | translate }}</h2>
|
<h2>{{ 'addon.block_starredcourses.pluginname' | translate }}</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<div slot="end" class="flex-row">
|
<div slot="end" class="flex-row">
|
||||||
<div *ngIf="downloadCoursesEnabled && downloadEnabled && courses && courses.length > 1" class="core-button-spinner">
|
|
||||||
<ion-button *ngIf="prefetchCoursesData.icon && !prefetchCoursesData.loading" fill="clear" color="dark"
|
|
||||||
(click)="prefetchCourses()" [attr.aria-label]="'core.courses.downloadcourses' | translate">
|
|
||||||
<ion-icon [name]="prefetchCoursesData.icon" slot="icon-only" aria-hidden="true"></ion-icon>
|
|
||||||
</ion-button>
|
|
||||||
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge"
|
|
||||||
role="progressbar" [attr.aria-valuemax]="prefetchCoursesData.total"
|
|
||||||
[attr.aria-valuenow]="prefetchCoursesData.count" [attr.aria-valuetext]="prefetchCoursesData.badgeA11yText">
|
|
||||||
{{prefetchCoursesData.badge}}
|
|
||||||
</ion-badge>
|
|
||||||
<ion-spinner *ngIf="!prefetchCoursesData.icon || prefetchCoursesData.loading"
|
|
||||||
[attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId">
|
<core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId">
|
||||||
</core-horizontal-scroll-controls>
|
</core-horizontal-scroll-controls>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,17 +11,13 @@
|
||||||
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg" inline="true"
|
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg" inline="true"
|
||||||
[message]="'addon.block_starredcourses.nocourses' | translate"></core-empty-box>
|
[message]="'addon.block_starredcourses.nocourses' | translate"></core-empty-box>
|
||||||
<!-- List of courses. -->
|
<!-- List of courses. -->
|
||||||
<div
|
<div [hidden]="courses.length === 0" [id]="scrollElementId" class="core-horizontal-scroll"
|
||||||
[hidden]="courses.length === 0"
|
(scroll)="scrollControls.updateScrollPosition()">
|
||||||
[id]="scrollElementId"
|
|
||||||
class="core-horizontal-scroll"
|
|
||||||
(scroll)="scrollControls.updateScrollPosition()"
|
|
||||||
>
|
|
||||||
<div (onResize)="scrollControls.updateScrollPosition()" class="flex-row">
|
<div (onResize)="scrollControls.updateScrollPosition()" class="flex-row">
|
||||||
<div class="safe-area-pseudo-padding-start"></div>
|
<div class="safe-area-pseudo-padding-start"></div>
|
||||||
<ng-container *ngFor="let course of courses">
|
<ng-container *ngFor="let course of courses">
|
||||||
<core-courses-course-progress [course]="course" class="core-block_starredcourses"
|
<core-courses-course-list-item [course]="course" class="core-block_starredcourses" layout="summarycard"
|
||||||
[showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-progress>
|
[showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-list-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="safe-area-pseudo-padding-end"></div>
|
<div class="safe-area-pseudo-padding-end"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,17 +12,15 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit, OnDestroy, Input, OnChanges, SimpleChange } from '@angular/core';
|
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData, CoreCourses } from '@features/courses/services/courses';
|
import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData, CoreCourses } from '@features/courses/services/courses';
|
||||||
import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
|
import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
|
||||||
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper';
|
|
||||||
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
||||||
import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion';
|
import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion';
|
||||||
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
|
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to render a starred courses block.
|
* Component to render a starred courses block.
|
||||||
|
@ -31,24 +29,15 @@ import { CoreDomUtils } from '@services/utils/dom';
|
||||||
selector: 'addon-block-starredcourses',
|
selector: 'addon-block-starredcourses',
|
||||||
templateUrl: 'addon-block-starredcourses.html',
|
templateUrl: 'addon-block-starredcourses.html',
|
||||||
})
|
})
|
||||||
export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy {
|
export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@Input() downloadEnabled = false;
|
@Input() downloadEnabled = false;
|
||||||
|
|
||||||
courses: CoreEnrolledCourseDataWithOptions [] = [];
|
courses: CoreEnrolledCourseDataWithOptions [] = [];
|
||||||
prefetchCoursesData: CorePrefetchStatusInfo = {
|
|
||||||
icon: '',
|
|
||||||
statusTranslatable: 'core.loading',
|
|
||||||
status: '',
|
|
||||||
loading: true,
|
|
||||||
badge: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
downloadCourseEnabled = false;
|
downloadCourseEnabled = false;
|
||||||
downloadCoursesEnabled = false;
|
|
||||||
scrollElementId!: string;
|
scrollElementId!: string;
|
||||||
|
|
||||||
protected prefetchIconsInitialized = false;
|
|
||||||
protected isDestroyed = false;
|
protected isDestroyed = false;
|
||||||
protected coursesObserver?: CoreEventObserver;
|
protected coursesObserver?: CoreEventObserver;
|
||||||
protected updateSiteObserver?: CoreEventObserver;
|
protected updateSiteObserver?: CoreEventObserver;
|
||||||
|
@ -69,12 +58,10 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im
|
||||||
|
|
||||||
// Refresh the enabled flags if enabled.
|
// Refresh the enabled flags if enabled.
|
||||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||||
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
|
||||||
|
|
||||||
// Refresh the enabled flags if site is updated.
|
// Refresh the enabled flags if site is updated.
|
||||||
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||||
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
|
||||||
}, CoreSites.getCurrentSiteId());
|
}, CoreSites.getCurrentSiteId());
|
||||||
|
|
||||||
this.coursesObserver = CoreEvents.on(
|
this.coursesObserver = CoreEvents.on(
|
||||||
|
@ -89,16 +76,6 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
|
|
||||||
if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) {
|
|
||||||
// Download all courses is enabled now, initialize it.
|
|
||||||
this.initPrefetchCoursesIcons();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
@ -131,9 +108,7 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im
|
||||||
promises.push(CoreCourses.invalidateCoursesByField('ids', courseIds.join(',')));
|
promises.push(CoreCourses.invalidateCoursesByField('ids', courseIds.join(',')));
|
||||||
}
|
}
|
||||||
|
|
||||||
await CoreUtils.allPromises(promises).finally(() => {
|
await CoreUtils.allPromises(promises);
|
||||||
this.prefetchIconsInitialized = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -145,8 +120,6 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im
|
||||||
|
|
||||||
// @TODO: Sort won't coincide with website because timemodified is not informed.
|
// @TODO: Sort won't coincide with website because timemodified is not informed.
|
||||||
this.courses = await CoreCoursesHelper.getUserCoursesWithOptions('timemodified', 0, 'isfavourite', showCategories);
|
this.courses = await CoreCoursesHelper.getUserCoursesWithOptions('timemodified', 0, 'isfavourite', showCategories);
|
||||||
|
|
||||||
this.initPrefetchCoursesIcons();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -182,43 +155,10 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.invalidateCourses([course.id]);
|
await this.invalidateCourses([course.id]);
|
||||||
this.initPrefetchCoursesIcons();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the prefetch icon for selected courses.
|
|
||||||
*/
|
|
||||||
protected async initPrefetchCoursesIcons(): Promise<void> {
|
|
||||||
if (this.prefetchIconsInitialized || !this.downloadEnabled) {
|
|
||||||
// Already initialized.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.prefetchIconsInitialized = true;
|
|
||||||
|
|
||||||
this.prefetchCoursesData = await CoreCourseHelper.initPrefetchCoursesIcons(this.courses, this.prefetchCoursesData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prefetch all the shown courses.
|
|
||||||
*
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
async prefetchCourses(): Promise<void> {
|
|
||||||
const initialIcon = this.prefetchCoursesData.icon;
|
|
||||||
|
|
||||||
try {
|
|
||||||
return CoreCourseHelper.prefetchCourses(this.courses, this.prefetchCoursesData);
|
|
||||||
} catch (error) {
|
|
||||||
if (!this.isDestroyed) {
|
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
|
||||||
this.prefetchCoursesData.icon = initialIcon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -429,7 +429,7 @@ export class CoreCourseHelperProvider {
|
||||||
const siteId = CoreSites.getCurrentSiteId();
|
const siteId = CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
// Confirm the download without checking size because it could take a while.
|
// Confirm the download without checking size because it could take a while.
|
||||||
await CoreDomUtils.showConfirm(Translate.instant('core.areyousure'));
|
await CoreDomUtils.showConfirm(Translate.instant('core.areyousure'), Translate.instant('core.courses.downloadcourses'));
|
||||||
|
|
||||||
const total = courses.length;
|
const total = courses.length;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
@ -1209,7 +1209,7 @@ export class CoreCourseHelperProvider {
|
||||||
|
|
||||||
const status = await this.determineCoursesStatus(courses);
|
const status = await this.determineCoursesStatus(courses);
|
||||||
|
|
||||||
prefetch = this.getCoursePrefetchStatusInfo(status);
|
prefetch = this.getCoursesPrefetchStatusInfo(status);
|
||||||
|
|
||||||
if (prefetch.loading) {
|
if (prefetch.loading) {
|
||||||
// It seems all courses are being downloaded, show a download button instead.
|
// It seems all courses are being downloaded, show a download button instead.
|
||||||
|
@ -1381,6 +1381,33 @@ export class CoreCourseHelperProvider {
|
||||||
return prefetchStatus;
|
return prefetchStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a courses status icon and the langkey to use as a title from status.
|
||||||
|
*
|
||||||
|
* @param status Courses status.
|
||||||
|
* @return Prefetch status info.
|
||||||
|
*/
|
||||||
|
getCoursesPrefetchStatusInfo(status: string): CorePrefetchStatusInfo {
|
||||||
|
const prefetchStatus: CorePrefetchStatusInfo = {
|
||||||
|
status: status,
|
||||||
|
icon: this.getPrefetchStatusIcon(status, false),
|
||||||
|
statusTranslatable: '',
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status == CoreConstants.DOWNLOADED) {
|
||||||
|
// Always show refresh icon, we cannot know if there's anything new in course options.
|
||||||
|
prefetchStatus.statusTranslatable = 'core.courses.refreshcourses';
|
||||||
|
} else if (status == CoreConstants.DOWNLOADING) {
|
||||||
|
prefetchStatus.statusTranslatable = 'core.downloading';
|
||||||
|
prefetchStatus.loading = true;
|
||||||
|
} else {
|
||||||
|
prefetchStatus.statusTranslatable = 'core.courses.downloadcourses';
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefetchStatus;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the icon given the status and if trust the download status.
|
* Get the icon given the status and if trust the download status.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,47 +1,106 @@
|
||||||
<ion-item class="ion-text-wrap" (click)="openCourse()" [class.item-disabled]="course.visible == 0"
|
<ion-item class="ion-text-wrap core-course-list-item" (click)="openCourse()" [class.item-disabled]="course.visible == 0"
|
||||||
[attr.aria-label]="course.displayname || course.fullname" detail="true" button>
|
[class.item-dimmed]="course.hidden" [attr.aria-label]="course.displayname || course.fullname" detail="true" button
|
||||||
<ion-icon *ngIf="!course.courseImage" name="fas-graduation-cap" slot="start" class="course-icon"
|
*ngIf="layout == 'list' || layout == 'listwithenrol'">
|
||||||
[attr.course-color]="course.color ? null : course.colorNumber" [style.color]="course.color"></ion-icon>
|
<ion-icon *ngIf="!course.courseImage" name="fas-graduation-cap" slot="start" class="course-icon core-course-thumb"
|
||||||
<ion-avatar *ngIf="course.courseImage" slot="start">
|
[attr.course-color]="course.color ? null : course.colorNumber" [style.color]="course.color">
|
||||||
|
</ion-icon>
|
||||||
|
<ion-avatar *ngIf="course.courseImage" slot="start" class="core-course-thumb">
|
||||||
<img [src]="course.courseImage" core-external-content alt="" />
|
<img [src]="course.courseImage" core-external-content alt="" />
|
||||||
</ion-avatar>
|
</ion-avatar>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>
|
<ion-row>
|
||||||
<core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="course.id">
|
<ion-col class="ion-align-self-center">
|
||||||
</core-format-text>
|
<ng-container *ngTemplateOutlet="mainInfo"></ng-container>
|
||||||
</h2>
|
</ion-col>
|
||||||
|
<ion-col size="auto" class="ion-align-self-center">
|
||||||
|
<ng-container *ngIf="!isEnrolled">
|
||||||
|
<ion-icon *ngFor="let icon of enrolmentIcons" color="dark" size="small" [name]="icon.icon"
|
||||||
|
[title]="icon.label | translate" [attr.aria-label]="icon.label | translate">
|
||||||
|
</ion-icon>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="isEnrolled">
|
||||||
|
<ng-container *ngTemplateOutlet="download"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
<p *ngIf="isEnrolled && progress! >= 0 && completionUserTracked !== false">
|
||||||
|
<core-progress-bar [progress]="progress" a11yText="core.courses.aria:courseprogress"></core-progress-bar>
|
||||||
|
</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-card [attr.course-color]="course.color ? null : course.colorNumber" *ngIf="layout == 'card' || layout == 'summarycard'"
|
||||||
|
class="core-course-list-card" [class.item-dimmed]="course.hidden" [attr.aria-label]="course.displayname || course.fullname">
|
||||||
|
<div (click)="openCourse()" class="core-course-thumb" [class.core-course-color-img]="course.courseImage"
|
||||||
|
[style.background-color]="course.color">
|
||||||
|
<img *ngIf="course.courseImage" [src]="course.courseImage" core-external-content alt="" />
|
||||||
|
</div>
|
||||||
|
<ion-item button lines="none" (click)="openCourse()" [attr.aria-label]="course.displayname || course.fullname"
|
||||||
|
class="core-course-header" [class.item-disabled]="course.visible == 0"
|
||||||
|
[class.core-course-only-title]="layout == 'summarycard' || progress < 0 && completionUserTracked === false" detail="false">
|
||||||
|
<ion-label class="ion-text-wrap core-course-title">
|
||||||
|
<ion-row>
|
||||||
|
<ion-col>
|
||||||
|
<ng-container *ngTemplateOutlet="mainInfo"></ng-container>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col size="auto"
|
||||||
|
*ngIf="isEnrolled && ((downloadCourseEnabled && !courseOptionMenuEnabled && showDownload) || courseOptionMenuEnabled)">
|
||||||
|
<ng-container *ngTemplateOutlet="download"></ng-container>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
<div *ngIf="layout == 'card' && progress >= 0 && completionUserTracked !== false" lines="none" class="core-course-progress">
|
||||||
|
<core-progress-bar [progress]="progress" a11yText="core.courses.aria:courseprogress"></core-progress-bar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template #download>
|
||||||
|
<div class="core-button-spinner" *ngIf="downloadCourseEnabled && !courseOptionMenuEnabled && showDownload">
|
||||||
|
<core-download-refresh [status]="prefetchCourseData.status" [enabled]="downloadCourseEnabled"
|
||||||
|
[statusTranslatable]="prefetchCourseData.statusTranslatable" canTrustDownload="false" [loading]="prefetchCourseData.loading"
|
||||||
|
(action)="prefetchCourse()"></core-download-refresh>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="core-button-spinner" *ngIf="courseOptionMenuEnabled">
|
||||||
|
<!-- Download course spinner. -->
|
||||||
|
<ion-spinner *ngIf="(downloadCourseEnabled && prefetchCourseData.icon == 'spinner') || showSpinner"
|
||||||
|
[attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||||
|
|
||||||
|
<!-- Downloaded icon. -->
|
||||||
|
<ion-icon *ngIf="downloadCourseEnabled && prefetchCourseData.downloadSucceeded && !showSpinner" class="core-icon-downloaded"
|
||||||
|
name="cloud-done" color="success" role="status" [attr.aria-label]="'core.downloaded' | translate"></ion-icon>
|
||||||
|
|
||||||
|
<!-- Options menu. -->
|
||||||
|
<ion-button fill="clear" color="dark" (click)="showCourseOptionsMenu($event)" *ngIf="!showSpinner"
|
||||||
|
[attr.aria-label]="('core.displayoptions' | translate)">
|
||||||
|
<ion-icon name="ellipsis-vertical" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #mainInfo>
|
||||||
<p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)"
|
<p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)"
|
||||||
class="core-course-additional-info">
|
class="core-course-additional-info">
|
||||||
<span *ngIf="course.categoryname" class="core-course-category">
|
<span *ngIf="course.categoryname" class="core-course-category">
|
||||||
|
<span class="sr-only">{{ 'core.courses.aria:coursecategory' | translate }}</span>
|
||||||
<core-format-text [text]="course.categoryname"></core-format-text>
|
<core-format-text [text]="course.categoryname"></core-format-text>
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="course.categoryname && course.displayname && course.shortname && course.fullname != course.displayname"
|
<span *ngIf="course.categoryname && course.displayname && course.shortname && course.fullname != course.displayname"
|
||||||
class="core-course-category"> | </span>
|
class="core-course-category"> | </span>
|
||||||
<span *ngIf="course.displayname && course.shortname && course.fullname != course.displayname"
|
<span *ngIf="course.displayname && course.shortname && course.fullname != course.displayname" class="core-course-shortname">
|
||||||
class="core-course-shortname">
|
|
||||||
<core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id">
|
<core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id">
|
||||||
</core-format-text>
|
</core-format-text>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p *ngIf="isEnrolled && course.progress! >= 0 && course.completionusertracked !== false">
|
<p class="item-heading">
|
||||||
<core-progress-bar [progress]="course.progress" a11yText="core.courses.aria:courseprogress"></core-progress-bar>
|
<ion-icon name="fas-star" *ngIf="course.isfavourite" [attr.aria-label]="'core.courses.favourite' | translate">
|
||||||
</p>
|
|
||||||
</ion-label>
|
|
||||||
<ng-container *ngIf="!isEnrolled">
|
|
||||||
<ion-icon *ngFor="let icon of icons" color="dark" size="small" [name]="icon.icon"
|
|
||||||
[title]="icon.label | translate"
|
|
||||||
[attr.aria-label]="icon.label | translate"
|
|
||||||
slot="end">
|
|
||||||
</ion-icon>
|
</ion-icon>
|
||||||
</ng-container>
|
<span class="sr-only" *ngIf="course.isfavourite">{{ 'core.courses.aria:favourite' | translate }}</span>
|
||||||
|
<span class="sr-only">{{ 'core.courses.aria:coursename' | translate }}</span>
|
||||||
<div class="core-button-spinner" *ngIf="isEnrolled && showDownload" slot="end">
|
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id">
|
||||||
<core-download-refresh
|
</core-format-text>
|
||||||
[status]="prefetchCourseData.status"
|
</p>
|
||||||
[statusTranslatable]="prefetchCourseData.statusTranslatable"
|
</ng-template>
|
||||||
[enabled]="true"
|
|
||||||
canTrustDownload="false"
|
|
||||||
[loading]="prefetchCourseData.loading"
|
|
||||||
(action)="prefetchCourse()"></core-download-refresh>
|
|
||||||
</div>
|
|
||||||
</ion-item>
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
@import "~theme/globals";
|
@import "~theme/globals";
|
||||||
|
|
||||||
:host {
|
.core-course-list-item {
|
||||||
.course-icon {
|
.course-icon {
|
||||||
color: white;
|
color: white;
|
||||||
background: var(--gray-light);
|
background: var(--gray-light);
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-inline-end: 16px;
|
|
||||||
-webkit-transition: all 50ms ease-in-out;
|
-webkit-transition: all 50ms ease-in-out;
|
||||||
transition: all 50ms ease-in-out;
|
transition: all 50ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
@ -22,4 +21,177 @@
|
||||||
-webkit-transition: all 50ms ease-in-out;
|
-webkit-transition: all 50ms ease-in-out;
|
||||||
transition: all 50ms ease-in-out;
|
transition: all 50ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.core-course-thumb {
|
||||||
|
@include margin(12px, 16px, 12px, null);
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-course-summary {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-heading ion-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: var(--core-star-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-card {
|
||||||
|
--vertical-margin: 12px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-self: stretch;
|
||||||
|
height: calc(100% - var(--vertical-margin) - var(--vertical-margin));
|
||||||
|
margin-top: var(--vertical-margin);
|
||||||
|
margin-bottom: var(--vertical-margin);
|
||||||
|
|
||||||
|
@for $i from 0 to length($core-course-image-background) {
|
||||||
|
&[course-color="#{$i}"] .core-course-thumb {
|
||||||
|
background: var(--core-course-color-#{$i});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-row {
|
||||||
|
min-height: var(--a11y-min-target-size);
|
||||||
|
ion-col .core-button-spinner {
|
||||||
|
min-width: calc(var(--a11y-min-target-size) + 16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-course-thumb {
|
||||||
|
padding-top: 40%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
position: relative;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
-webkit-transition: all 50ms ease-in-out;
|
||||||
|
transition: all 50ms ease-in-out;
|
||||||
|
|
||||||
|
&.core-course-color-img {
|
||||||
|
background: var(--ion-item-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if ($core-course-hide-thumb-on-cards) {
|
||||||
|
.core-course-thumb {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if ($core-course-thumb-on-cards-background) {
|
||||||
|
.core-course-thumb {
|
||||||
|
background: $core-course-thumb-on-cards-background !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-course-additional-info {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-course-header {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
--inner-padding-end: 0px;
|
||||||
|
|
||||||
|
&::part(native) {
|
||||||
|
flex-grow: 1;
|
||||||
|
align-items: self-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.core-course-only-title {
|
||||||
|
&::part(native) {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-course-title {
|
||||||
|
margin: 12px 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-button-spinner {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.core-button-spinner ion-spinner {
|
||||||
|
vertical-align: top; // the better option for most scenarios
|
||||||
|
vertical-align: -webkit-baseline-middle; // the best for those that support it
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-button-spinner .core-icon-downloaded {
|
||||||
|
font-size: 28.8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-button[icon-only] {
|
||||||
|
min-width: 50px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if ($core-course-hide-progress-on-cards) {
|
||||||
|
.core-course-progress {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.core-horizontal-scroll) {
|
||||||
|
@include horizontal_scroll_item(80%, 250px, 300px);
|
||||||
|
|
||||||
|
ion-card {
|
||||||
|
.core-course-thumb {
|
||||||
|
padding-top: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-item.core-course-header {
|
||||||
|
--padding-start: 4px;
|
||||||
|
|
||||||
|
.core-course-title {
|
||||||
|
margin: 7px 0;
|
||||||
|
|
||||||
|
.item-heading ion-icon {
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.core-button-spinner {
|
||||||
|
min-height: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
|
||||||
|
ion-spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.item-button[icon-only] {
|
||||||
|
min-width: 40px;
|
||||||
|
width: 40px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,15 +12,19 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { CoreConstants } from '@/core/constants';
|
||||||
import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
|
import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { CoreCourseProvider, CoreCourse } from '@features/course/services/course';
|
import { CoreCourseProvider, CoreCourse } from '@features/course/services/course';
|
||||||
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper';
|
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper';
|
||||||
|
import { CoreUser } from '@features/user/services/user';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreCourseListItem, CoreCourses } from '../../services/courses';
|
import { CoreCourseListItem, CoreCourses, CoreCoursesProvider } from '../../services/courses';
|
||||||
import { CoreCoursesHelper } from '../../services/courses-helper';
|
import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper';
|
||||||
|
import { CoreCoursesCourseOptionsMenuComponent } from '../course-options-menu/course-options-menu';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This directive is meant to display an item for a list of courses.
|
* This directive is meant to display an item for a list of courses.
|
||||||
|
@ -37,10 +41,10 @@ import { CoreCoursesHelper } from '../../services/courses-helper';
|
||||||
export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, OnChanges {
|
export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, OnChanges {
|
||||||
|
|
||||||
@Input() course!: CoreCourseListItem; // The course to render.
|
@Input() course!: CoreCourseListItem; // The course to render.
|
||||||
|
|
||||||
@Input() showDownload = false; // If true, will show download button.
|
@Input() showDownload = false; // If true, will show download button.
|
||||||
|
@Input() layout: 'listwithenrol'|'summarycard'|'list'|'card' = 'listwithenrol';
|
||||||
|
|
||||||
icons: CoreCoursesEnrolmentIcons[] = [];
|
enrolmentIcons: CoreCoursesEnrolmentIcons[] = [];
|
||||||
isEnrolled = false;
|
isEnrolled = false;
|
||||||
prefetchCourseData: CorePrefetchStatusInfo = {
|
prefetchCourseData: CorePrefetchStatusInfo = {
|
||||||
icon: '',
|
icon: '',
|
||||||
|
@ -49,8 +53,16 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
|
||||||
loading: true,
|
loading: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected courseStatusObserver?: CoreEventObserver;
|
showSpinner = false;
|
||||||
|
downloadCourseEnabled = false;
|
||||||
|
courseOptionMenuEnabled = false;
|
||||||
|
progress = -1;
|
||||||
|
completionUserTracked: boolean | undefined = false;
|
||||||
|
|
||||||
|
protected courseStatus = CoreConstants.NOT_DOWNLOADED;
|
||||||
protected isDestroyed = false;
|
protected isDestroyed = false;
|
||||||
|
protected courseStatusObserver?: CoreEventObserver;
|
||||||
|
protected siteUpdatedObserver?: CoreEventObserver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
|
@ -58,48 +70,71 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
CoreCoursesHelper.loadCourseColorAndImage(this.course);
|
CoreCoursesHelper.loadCourseColorAndImage(this.course);
|
||||||
|
|
||||||
this.isEnrolled = this.course.progress !== undefined;
|
// Assume is enroled if mode is not listwithenrol.
|
||||||
|
this.isEnrolled = this.layout != 'listwithenrol' || this.course.progress !== undefined;
|
||||||
|
|
||||||
if (!this.isEnrolled) {
|
if (!this.isEnrolled) {
|
||||||
try {
|
try {
|
||||||
const course = await CoreCourses.getUserCourse(this.course.id);
|
const course = await CoreCourses.getUserCourse(this.course.id);
|
||||||
this.course.progress = course.progress;
|
this.course = Object.assign(this.course, course);
|
||||||
this.course.completionusertracked = course.completionusertracked;
|
this.updateCourseFields();
|
||||||
|
|
||||||
this.isEnrolled = true;
|
this.isEnrolled = true;
|
||||||
|
|
||||||
if (this.showDownload) {
|
|
||||||
this.initPrefetchCourse();
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
this.isEnrolled = false;
|
this.isEnrolled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isEnrolled) {
|
if (this.isEnrolled) {
|
||||||
this.icons = [];
|
if (this.showDownload) {
|
||||||
|
this.initPrefetchCourse();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||||
|
|
||||||
|
if (this.downloadCourseEnabled) {
|
||||||
|
this.initPrefetchCourse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This field is only available from 3.6 onwards.
|
||||||
|
this.courseOptionMenuEnabled = (this.layout != 'listwithenrol' && this.layout != 'summarycard') &&
|
||||||
|
this.course.isfavourite !== undefined;
|
||||||
|
|
||||||
|
// Refresh the enabled flag if site is updated.
|
||||||
|
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||||
|
const wasEnabled = this.downloadCourseEnabled;
|
||||||
|
|
||||||
|
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||||
|
|
||||||
|
if (!wasEnabled && this.downloadCourseEnabled) {
|
||||||
|
// Download course is enabled now, initialize it.
|
||||||
|
this.initPrefetchCourse();
|
||||||
|
}
|
||||||
|
}, CoreSites.getCurrentSiteId());
|
||||||
|
} else if ('enrollmentmethods' in this.course) {
|
||||||
|
this.enrolmentIcons = [];
|
||||||
|
|
||||||
this.course.enrollmentmethods.forEach((instance) => {
|
this.course.enrollmentmethods.forEach((instance) => {
|
||||||
if (instance === 'self') {
|
if (instance === 'self') {
|
||||||
this.icons.push({
|
this.enrolmentIcons.push({
|
||||||
label: 'core.courses.selfenrolment',
|
label: 'core.courses.selfenrolment',
|
||||||
icon: 'fas-key',
|
icon: 'fas-key',
|
||||||
});
|
});
|
||||||
} else if (instance === 'guest') {
|
} else if (instance === 'guest') {
|
||||||
this.icons.push({
|
this.enrolmentIcons.push({
|
||||||
label: 'core.courses.allowguests',
|
label: 'core.courses.allowguests',
|
||||||
icon: 'fas-unlock',
|
icon: 'fas-unlock',
|
||||||
});
|
});
|
||||||
} else if (instance === 'paypal') {
|
} else if (instance === 'paypal') {
|
||||||
this.icons.push({
|
this.enrolmentIcons.push({
|
||||||
label: 'core.courses.paypalaccepted',
|
label: 'core.courses.paypalaccepted',
|
||||||
icon: 'fab-paypal',
|
icon: 'fab-paypal',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.icons.length == 0) {
|
if (this.enrolmentIcons.length == 0) {
|
||||||
this.icons.push({
|
this.enrolmentIcons.push({
|
||||||
label: 'core.courses.notenrollable',
|
label: 'core.courses.notenrollable',
|
||||||
icon: 'fas-lock',
|
icon: 'fas-lock',
|
||||||
});
|
});
|
||||||
|
@ -114,6 +149,16 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
|
||||||
if (this.showDownload && this.isEnrolled) {
|
if (this.showDownload && this.isEnrolled) {
|
||||||
this.initPrefetchCourse();
|
this.initPrefetchCourse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateCourseFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to update course fields.
|
||||||
|
*/
|
||||||
|
protected updateCourseFields(): void {
|
||||||
|
this.progress = 'progress' in this.course ? this.course.progress || -1 : -1;
|
||||||
|
this.completionUserTracked = 'completionusertracked' in this.course && this.course.completionusertracked;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -179,6 +224,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
|
||||||
protected updateCourseStatus(status: string): void {
|
protected updateCourseStatus(status: string): void {
|
||||||
const statusData = CoreCourseHelper.getCoursePrefetchStatusInfo(status);
|
const statusData = CoreCourseHelper.getCoursePrefetchStatusInfo(status);
|
||||||
|
|
||||||
|
this.courseStatus = status;
|
||||||
this.prefetchCourseData.status = statusData.status;
|
this.prefetchCourseData.status = statusData.status;
|
||||||
this.prefetchCourseData.icon = statusData.icon;
|
this.prefetchCourseData.icon = statusData.icon;
|
||||||
this.prefetchCourseData.statusTranslatable = statusData.statusTranslatable;
|
this.prefetchCourseData.statusTranslatable = statusData.statusTranslatable;
|
||||||
|
@ -188,11 +234,11 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
|
||||||
/**
|
/**
|
||||||
* Prefetch the course.
|
* Prefetch the course.
|
||||||
*
|
*
|
||||||
* @param e Click event.
|
* @param event Click event.
|
||||||
*/
|
*/
|
||||||
async prefetchCourse(e?: Event): Promise<void> {
|
async prefetchCourse(event?: Event): Promise<void> {
|
||||||
e?.preventDefault();
|
event?.preventDefault();
|
||||||
e?.stopPropagation();
|
event?.stopPropagation();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course);
|
await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course);
|
||||||
|
@ -203,12 +249,149 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete course stored data.
|
||||||
|
*/
|
||||||
|
async deleteCourseStoredData(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await CoreDomUtils.showDeleteConfirm('core.course.confirmdeletestoreddata');
|
||||||
|
} catch (error) {
|
||||||
|
if (!CoreDomUtils.isCanceledError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = await CoreDomUtils.showModalLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreCourseHelper.deleteCourseFiles(this.course.id);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, Translate.instant('core.errordeletefile'));
|
||||||
|
} finally {
|
||||||
|
modal.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the context menu.
|
||||||
|
*
|
||||||
|
* @param event Click Event.
|
||||||
|
*/
|
||||||
|
async showCourseOptionsMenu(event: Event): Promise<void> {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const popoverData = await CoreDomUtils.openPopover<string>({
|
||||||
|
component: CoreCoursesCourseOptionsMenuComponent,
|
||||||
|
componentProps: {
|
||||||
|
course: this.course,
|
||||||
|
prefetch: this.prefetchCourseData,
|
||||||
|
},
|
||||||
|
event: event,
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (popoverData) {
|
||||||
|
case 'download':
|
||||||
|
if (!this.prefetchCourseData.loading) {
|
||||||
|
this.prefetchCourse(event);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
if (this.courseStatus == CoreConstants.DOWNLOADED || this.courseStatus == CoreConstants.OUTDATED) {
|
||||||
|
this.deleteCourseStoredData();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'hide':
|
||||||
|
this.setCourseHidden(true);
|
||||||
|
break;
|
||||||
|
case 'show':
|
||||||
|
this.setCourseHidden(false);
|
||||||
|
break;
|
||||||
|
case 'favourite':
|
||||||
|
this.setCourseFavourite(true);
|
||||||
|
break;
|
||||||
|
case 'unfavourite':
|
||||||
|
this.setCourseFavourite(false);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide/Unhide the course from the course list.
|
||||||
|
*
|
||||||
|
* @param hide True to hide and false to show.
|
||||||
|
*/
|
||||||
|
protected async setCourseHidden(hide: boolean): Promise<void> {
|
||||||
|
this.showSpinner = true;
|
||||||
|
|
||||||
|
// We should use null to unset the preference.
|
||||||
|
try {
|
||||||
|
await CoreUser.updateUserPreference(
|
||||||
|
'block_myoverview_hidden_course_' + this.course.id,
|
||||||
|
hide ? '1' : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.course.hidden = hide;
|
||||||
|
|
||||||
|
(<CoreEnrolledCourseDataWithExtraInfoAndOptions> this.course).hidden = hide;
|
||||||
|
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
|
||||||
|
courseId: this.course.id,
|
||||||
|
course: this.course,
|
||||||
|
action: CoreCoursesProvider.ACTION_STATE_CHANGED,
|
||||||
|
state: CoreCoursesProvider.STATE_HIDDEN,
|
||||||
|
value: hide,
|
||||||
|
}, CoreSites.getCurrentSiteId());
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (!this.isDestroyed) {
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'Error changing course visibility.');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.showSpinner = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Favourite/Unfavourite the course from the course list.
|
||||||
|
*
|
||||||
|
* @param favourite True to favourite and false to unfavourite.
|
||||||
|
*/
|
||||||
|
protected async setCourseFavourite(favourite: boolean): Promise<void> {
|
||||||
|
this.showSpinner = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreCourses.setFavouriteCourse(this.course.id, favourite);
|
||||||
|
|
||||||
|
this.course.isfavourite = favourite;
|
||||||
|
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
|
||||||
|
courseId: this.course.id,
|
||||||
|
course: this.course,
|
||||||
|
action: CoreCoursesProvider.ACTION_STATE_CHANGED,
|
||||||
|
state: CoreCoursesProvider.STATE_FAVOURITE,
|
||||||
|
value: favourite,
|
||||||
|
}, CoreSites.getCurrentSiteId());
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (!this.isDestroyed) {
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'Error changing course favourite attribute.');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.showSpinner = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.isDestroyed = true;
|
this.isDestroyed = true;
|
||||||
this.courseStatusObserver?.off();
|
this.courseStatusObserver?.off();
|
||||||
|
this.siteUpdatedObserver?.off();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,9 +151,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(body.version-3-1) {
|
|
||||||
.core-course-thumb{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -35,6 +35,8 @@ import { CoreUser } from '@features/user/services/user';
|
||||||
*
|
*
|
||||||
* <core-courses-course-progress [course]="course">
|
* <core-courses-course-progress [course]="course">
|
||||||
* </core-courses-course-progress>
|
* </core-courses-course-progress>
|
||||||
|
*
|
||||||
|
* @deprecated since 4.0 Use core-courses-course-list-item instead.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'core-courses-course-progress',
|
selector: 'core-courses-course-progress',
|
||||||
|
@ -175,7 +177,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy, On
|
||||||
try {
|
try {
|
||||||
await CoreDomUtils.showDeleteConfirm('core.course.confirmdeletestoreddata');
|
await CoreDomUtils.showDeleteConfirm('core.course.confirmdeletestoreddata');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (CoreDomUtils.isCanceledError(error)) {
|
if (!CoreDomUtils.isCanceledError(error)) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,7 +223,6 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy, On
|
||||||
component: CoreCoursesCourseOptionsMenuComponent,
|
component: CoreCoursesCourseOptionsMenuComponent,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
course: this.course,
|
course: this.course,
|
||||||
courseStatus: this.courseStatus,
|
|
||||||
prefetch: this.prefetchCourseData,
|
prefetch: this.prefetchCourseData,
|
||||||
},
|
},
|
||||||
event: e,
|
event: e,
|
||||||
|
@ -234,7 +235,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy, On
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
if (this.courseStatus == 'downloaded' || this.courseStatus == 'outdated') {
|
if (this.courseStatus == CoreConstants.DOWNLOADED || this.courseStatus == CoreConstants.OUTDATED) {
|
||||||
this.deleteCourse();
|
this.deleteCourse();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -60,7 +60,7 @@ const mainMenuHomeSiblingRoutes: Routes = [
|
||||||
const mainMenuTabRoutes: Routes = [
|
const mainMenuTabRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: CoreCoursesMyCoursesMainMenuHandlerService.PAGE_NAME,
|
path: CoreCoursesMyCoursesMainMenuHandlerService.PAGE_NAME,
|
||||||
loadChildren: () => import('./pages/list/list.module').then(m => m.CoreCoursesListPageModule),
|
loadChildren: () => import('./pages/my/my.module').then(m => m.CoreCoursesMyCoursesPageModule),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"password": "Enrolment key",
|
"password": "Enrolment key",
|
||||||
"paymentrequired": "This course requires a payment for entry.",
|
"paymentrequired": "This course requires a payment for entry.",
|
||||||
"paypalaccepted": "PayPal payments accepted",
|
"paypalaccepted": "PayPal payments accepted",
|
||||||
|
"refreshcourses": "Refresh courses",
|
||||||
"reload": "Reload",
|
"reload": "Reload",
|
||||||
"removefromfavourites": "Unstar this course",
|
"removefromfavourites": "Unstar this course",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
|
|
|
@ -10,10 +10,9 @@
|
||||||
<ion-buttons slot="end">
|
<ion-buttons slot="end">
|
||||||
<core-context-menu>
|
<core-context-menu>
|
||||||
<core-context-menu-item *ngIf="downloadCourseEnabled || downloadCoursesEnabled" [priority]="1000"
|
<core-context-menu-item *ngIf="downloadCourseEnabled || downloadCoursesEnabled" [priority]="1000"
|
||||||
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()"
|
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()" iconAction="toggle"
|
||||||
iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item>
|
[(toggle)]="downloadEnabled"></core-context-menu-item>
|
||||||
<core-context-menu-item [priority]="900"
|
<core-context-menu-item [priority]="900" [content]="'core.courses.showonlyenrolled' | translate" (action)="filterEnrolled()"
|
||||||
[content]="'core.courses.showonlyenrolled' | translate" (action)="filterEnrolled()"
|
|
||||||
iconAction="toggle" [(toggle)]="showOnlyEnrolled"></core-context-menu-item>
|
iconAction="toggle" [(toggle)]="showOnlyEnrolled"></core-context-menu-item>
|
||||||
</core-context-menu>
|
</core-context-menu>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
|
@ -28,8 +27,8 @@
|
||||||
<ion-icon name="fas-folder" slot="start" [attr.aria-label]="'core.category' | translate"></ion-icon>
|
<ion-icon name="fas-folder" slot="start" [attr.aria-label]="'core.category' | translate"></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<p class="item-heading">
|
<p class="item-heading">
|
||||||
<core-format-text [text]="currentCategory.name" contextLevel="coursecat"
|
<core-format-text [text]="currentCategory.name" contextLevel="coursecat" [contextInstanceId]="currentCategory.id">
|
||||||
[contextInstanceId]="currentCategory.id"></core-format-text>
|
</core-format-text>
|
||||||
</p>
|
</p>
|
||||||
<p *ngIf="currentCategory.description">
|
<p *ngIf="currentCategory.description">
|
||||||
<core-format-text [text]="currentCategory.description" maxHeight="60" contextLevel="coursecat"
|
<core-format-text [text]="currentCategory.description" maxHeight="60" contextLevel="coursecat"
|
||||||
|
@ -45,8 +44,7 @@
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<section *ngFor="let category of categories">
|
<section *ngFor="let category of categories">
|
||||||
<ion-item button class="ion-text-wrap" (click)="openCategory(category.id)" [attr.aria-label]="category.name"
|
<ion-item button class="ion-text-wrap" (click)="openCategory(category.id)" [attr.aria-label]="category.name" detail="true">
|
||||||
detail="true">
|
|
||||||
<ion-icon name="fas-folder" slot="start" [attr.aria-label]="'core.category' | translate"></ion-icon>
|
<ion-icon name="fas-folder" slot="start" [attr.aria-label]="'core.category' | translate"></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>
|
<h2>
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
<core-context-menu>
|
||||||
|
<core-context-menu-item *ngIf="downloadCoursesEnabled && myOverviewBlock && myOverviewBlock.filteredCourses.length > 1"
|
||||||
|
[priority]="1000" [content]="myOverviewBlock?.prefetchCoursesData.statusTranslatable | translate"
|
||||||
|
(action)="myOverviewBlock?.prefetchCourses()"
|
||||||
|
[iconAction]="myOverviewBlock?.prefetchCoursesData.loading ? 'spinner' : myOverviewBlock?.prefetchCoursesData.icon"
|
||||||
|
[badge]="myOverviewBlock?.prefetchCoursesData.badge"
|
||||||
|
[badgeA11yText]="myOverviewBlock?.prefetchCoursesData.badgeA11yText">
|
||||||
|
</core-context-menu-item>
|
||||||
|
<core-context-menu-item [priority]="500" [content]="'addon.storagemanager.managestorage' | translate"
|
||||||
|
(action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item>
|
||||||
|
</core-context-menu>
|
||||||
|
<core-user-menu-button></core-user-menu-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher slot="fixed" (ionRefresh)="refresh($event.target)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<ion-list>
|
||||||
|
<!-- My courses blocks. -->
|
||||||
|
<core-block [block]="{name: 'myoverview', visible: true}" contextLevel="user" [instanceId]="userId"
|
||||||
|
[extraData]="{'downloadEnabled': true}">
|
||||||
|
</core-block>
|
||||||
|
</ion-list>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,63 @@
|
||||||
|
// (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 { Injector, NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, ROUTES, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { CoreBlockComponentsModule } from '@features/block/components/components.module';
|
||||||
|
|
||||||
|
import { CoreCoursesMyCoursesPage } from './my';
|
||||||
|
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
|
||||||
|
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||||
|
|
||||||
|
function buildRoutes(injector: Injector): Routes {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CoreCoursesMyCoursesPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('../list/list.module')
|
||||||
|
.then(m => m.CoreCoursesListPageModule),
|
||||||
|
},
|
||||||
|
...buildTabMainRoutes(injector, {
|
||||||
|
redirectTo: '',
|
||||||
|
pathMatch: 'full',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CoreSharedModule,
|
||||||
|
CoreBlockComponentsModule,
|
||||||
|
CoreMainMenuComponentsModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ROUTES,
|
||||||
|
multi: true,
|
||||||
|
deps: [Injector],
|
||||||
|
useFactory: buildRoutes,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
CoreCoursesMyCoursesPage,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class CoreCoursesMyCoursesPageModule { }
|
|
@ -0,0 +1,3 @@
|
||||||
|
:host ::ng-deep ion-item-divider {
|
||||||
|
display: none !important;
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
// (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 { AddonBlockMyOverviewComponent } from '@addons/block/myoverview/components/myoverview/myoverview';
|
||||||
|
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
|
import { CoreBlockComponent } from '@features/block/components/block/block';
|
||||||
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
import { CoreCourses } from '../../services/courses';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that shows a my courses.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-core-courses-my',
|
||||||
|
templateUrl: 'my.html',
|
||||||
|
styleUrls: ['my.scss'],
|
||||||
|
})
|
||||||
|
export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
@ViewChild(CoreBlockComponent) block!: CoreBlockComponent;
|
||||||
|
|
||||||
|
searchEnabled = false;
|
||||||
|
downloadCoursesEnabled = false;
|
||||||
|
userId: number;
|
||||||
|
myOverviewBlock?: AddonBlockMyOverviewComponent;
|
||||||
|
|
||||||
|
protected updateSiteObserver: CoreEventObserver;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Refresh the enabled flags if site is updated.
|
||||||
|
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||||
|
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
|
||||||
|
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
||||||
|
}, CoreSites.getCurrentSiteId());
|
||||||
|
|
||||||
|
this.userId = CoreSites.getCurrentSiteUserId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
|
||||||
|
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
||||||
|
|
||||||
|
this.loadBlock();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load my overview block instance.
|
||||||
|
*/
|
||||||
|
protected loadBlock(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.block) {
|
||||||
|
return this.loadBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.myOverviewBlock = this.block?.dynamicComponent?.instance as AddonBlockMyOverviewComponent;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open page to manage courses storage.
|
||||||
|
*/
|
||||||
|
manageCoursesStorage(): void {
|
||||||
|
CoreNavigator.navigateToSitePath('/storage');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to search courses.
|
||||||
|
*/
|
||||||
|
async openSearch(): Promise<void> {
|
||||||
|
CoreNavigator.navigateToSitePath('/list', { params : { mode: 'search' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
*/
|
||||||
|
async refresh(refresher?: IonRefresher): Promise<void> {
|
||||||
|
if (this.block) {
|
||||||
|
await CoreUtils.ignoreErrors(this.block.doRefresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
refresher?.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.updateSiteObserver?.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -32,6 +32,8 @@ import { AddonCourseCompletion } from '@/addons/coursecompletion/services/course
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class CoreCoursesHelperProvider {
|
export class CoreCoursesHelperProvider {
|
||||||
|
|
||||||
|
protected courseSiteColors: Record<string, (string | undefined)[]> = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the courses to display the course picker popover. If a courseId is specified, it will also return its categoryId.
|
* Get the courses to display the course picker popover. If a courseId is specified, it will also return its categoryId.
|
||||||
*
|
*
|
||||||
|
@ -72,11 +74,10 @@ export class CoreCoursesHelperProvider {
|
||||||
* @param courseByField Course returned by core_course_get_courses_by_field.
|
* @param courseByField Course returned by core_course_get_courses_by_field.
|
||||||
* @param addCategoryName Whether add category name or not.
|
* @param addCategoryName Whether add category name or not.
|
||||||
*/
|
*/
|
||||||
loadCourseExtraInfo(
|
protected loadCourseExtraInfo(
|
||||||
course: CoreEnrolledCourseDataWithExtraInfo,
|
course: CoreEnrolledCourseDataWithExtraInfo,
|
||||||
courseByField: CoreCourseSearchedData,
|
courseByField: CoreCourseSearchedData,
|
||||||
addCategoryName: boolean = false,
|
addCategoryName: boolean = false,
|
||||||
colors?: (string | undefined)[],
|
|
||||||
): void {
|
): void {
|
||||||
if (courseByField) {
|
if (courseByField) {
|
||||||
course.displayname = courseByField.displayname;
|
course.displayname = courseByField.displayname;
|
||||||
|
@ -85,18 +86,8 @@ export class CoreCoursesHelperProvider {
|
||||||
} else {
|
} else {
|
||||||
delete course.displayname;
|
delete course.displayname;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadCourseColorAndImage(course, colors);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a list of courses returned by core_enrol_get_users_courses, load some extra data using the WebService
|
|
||||||
* core_course_get_courses_by_field if available.
|
|
||||||
*
|
|
||||||
* @param courses List of courses.
|
|
||||||
* @param loadCategoryNames Whether load category names or not.
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* Loads the color of courses or the thumb image.
|
* Loads the color of courses or the thumb image.
|
||||||
*
|
*
|
||||||
|
@ -107,11 +98,8 @@ export class CoreCoursesHelperProvider {
|
||||||
if (!courses.length) {
|
if (!courses.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const colors = await this.loadCourseSiteColors();
|
|
||||||
|
|
||||||
courses.forEach((course) => {
|
await Promise.all(courses.map((course) => this.loadCourseColorAndImage(course)));
|
||||||
this.loadCourseColorAndImage(course, colors);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -131,32 +119,19 @@ export class CoreCoursesHelperProvider {
|
||||||
let coursesInfo = {};
|
let coursesInfo = {};
|
||||||
let courseInfoAvailable = false;
|
let courseInfoAvailable = false;
|
||||||
|
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
let colors: (string | undefined)[] = [];
|
|
||||||
|
|
||||||
promises.push(this.loadCourseSiteColors().then((loadedColors) => {
|
|
||||||
colors = loadedColors;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (loadCategoryNames || (courses[0].overviewfiles === undefined && courses[0].displayname === undefined)) {
|
if (loadCategoryNames || (courses[0].overviewfiles === undefined && courses[0].displayname === undefined)) {
|
||||||
const courseIds = courses.map((course) => course.id).join(',');
|
const courseIds = courses.map((course) => course.id).join(',');
|
||||||
|
|
||||||
courseInfoAvailable = true;
|
courseInfoAvailable = true;
|
||||||
|
|
||||||
// Get the extra data for the courses.
|
// Get the extra data for the courses.
|
||||||
promises.push(CoreCourses.getCoursesByField('ids', courseIds).then((coursesInfos) => {
|
const coursesInfosArray = await CoreCourses.getCoursesByField('ids', courseIds);
|
||||||
coursesInfo = CoreUtils.arrayToObject(coursesInfos, 'id');
|
|
||||||
|
|
||||||
return;
|
coursesInfo = CoreUtils.arrayToObject(coursesInfosArray, 'id');
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
|
|
||||||
courses.forEach((course) => {
|
courses.forEach((course) => {
|
||||||
this.loadCourseExtraInfo(course, courseInfoAvailable ? coursesInfo[course.id] : course, loadCategoryNames, colors);
|
this.loadCourseExtraInfo(course, courseInfoAvailable ? coursesInfo[course.id] : course, loadCategoryNames);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,19 +141,31 @@ export class CoreCoursesHelperProvider {
|
||||||
* @return course colors RGB.
|
* @return course colors RGB.
|
||||||
*/
|
*/
|
||||||
protected async loadCourseSiteColors(): Promise<(string | undefined)[]> {
|
protected async loadCourseSiteColors(): Promise<(string | undefined)[]> {
|
||||||
const site = CoreSites.getCurrentSite();
|
const site = CoreSites.getRequiredCurrentSite();
|
||||||
|
const siteId = site.getId();
|
||||||
|
|
||||||
|
if (this.courseSiteColors[siteId] !== undefined) {
|
||||||
|
return this.courseSiteColors[siteId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!site.isVersionGreaterEqualThan('3.8')) {
|
||||||
|
this.courseSiteColors[siteId] = [];
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const colors: (string | undefined)[] = [];
|
const colors: (string | undefined)[] = [];
|
||||||
|
|
||||||
if (site?.isVersionGreaterEqualThan('3.8')) {
|
|
||||||
try {
|
try {
|
||||||
const configs = await site.getConfig();
|
const configs = await site.getConfig();
|
||||||
for (let x = 0; x < 10; x++) {
|
for (let x = 0; x < 10; x++) {
|
||||||
colors[x] = configs['core_admin_coursecolor' + (x + 1)] || undefined;
|
colors[x] = configs['core_admin_coursecolor' + (x + 1)] || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.courseSiteColors[siteId] = colors;
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore errors.
|
// Ignore errors.
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return colors;
|
return colors;
|
||||||
}
|
}
|
||||||
|
@ -187,20 +174,19 @@ export class CoreCoursesHelperProvider {
|
||||||
* Loads the color of the course or the thumb image.
|
* Loads the color of the course or the thumb image.
|
||||||
*
|
*
|
||||||
* @param course Course data.
|
* @param course Course data.
|
||||||
* @param colors Colors loaded.
|
|
||||||
*/
|
*/
|
||||||
async loadCourseColorAndImage(course: CoreCourseWithImageAndColor, colors?: (string | undefined)[]): Promise<void> {
|
async loadCourseColorAndImage(course: CoreCourseWithImageAndColor): Promise<void> {
|
||||||
if (!colors) {
|
|
||||||
colors = await this.loadCourseSiteColors();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (course.overviewfiles && course.overviewfiles[0]) {
|
if (course.overviewfiles && course.overviewfiles[0]) {
|
||||||
course.courseImage = course.overviewfiles[0].fileurl;
|
course.courseImage = course.overviewfiles[0].fileurl;
|
||||||
} else {
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors = await this.loadCourseSiteColors();
|
||||||
|
|
||||||
course.colorNumber = course.id % 10;
|
course.colorNumber = course.id % 10;
|
||||||
course.color = colors.length ? colors[course.colorNumber] : undefined;
|
course.color = colors.length ? colors[course.colorNumber] : undefined;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user courses with admin and nav options.
|
* Get user courses with admin and nav options.
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { makeSingleton } from '@singletons';
|
||||||
import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
||||||
import { CoreEvents } from '@singletons/events';
|
import { CoreEvents } from '@singletons/events';
|
||||||
import { CoreWSError } from '@classes/errors/wserror';
|
import { CoreWSError } from '@classes/errors/wserror';
|
||||||
import { CoreCourseWithImageAndColor } from './courses-helper';
|
import { CoreCourseAnyCourseDataWithExtraInfoAndOptions, CoreCourseWithImageAndColor } from './courses-helper';
|
||||||
|
|
||||||
const ROOT_CACHE_KEY = 'mmCourses:';
|
const ROOT_CACHE_KEY = 'mmCourses:';
|
||||||
|
|
||||||
|
@ -1384,7 +1384,10 @@ export type CoreCourseSearchedData = CoreCourseBasicSearchedData & {
|
||||||
/**
|
/**
|
||||||
* Course to render as list item.
|
* Course to render as list item.
|
||||||
*/
|
*/
|
||||||
export type CoreCourseListItem = CoreCourseSearchedData & CoreCourseWithImageAndColor & {
|
export type CoreCourseListItem = ((CoreCourseSearchedData & CoreCourseWithImageAndColor) |
|
||||||
|
CoreCourseAnyCourseDataWithExtraInfoAndOptions) & {
|
||||||
|
isfavourite?: boolean; // If the user marked this course a favourite.
|
||||||
|
hidden?: boolean; // If the user hide the course from the dashboard.
|
||||||
completionusertracked?: boolean; // If the user is completion tracked.
|
completionusertracked?: boolean; // If the user is completion tracked.
|
||||||
progress?: number | null; // Progress percentage.
|
progress?: number | null; // Progress percentage.
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,7 +26,7 @@ import { CoreDashboardHomeHandler } from './dashboard-home';
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class CoreCoursesMyCoursesMainMenuHandlerService implements CoreMainMenuHandler {
|
export class CoreCoursesMyCoursesMainMenuHandlerService implements CoreMainMenuHandler {
|
||||||
|
|
||||||
static readonly PAGE_NAME = 'courses';
|
static readonly PAGE_NAME = 'my';
|
||||||
|
|
||||||
name = 'CoreCoursesMyCourses';
|
name = 'CoreCoursesMyCourses';
|
||||||
priority = 900;
|
priority = 900;
|
||||||
|
@ -35,13 +35,20 @@ export class CoreCoursesMyCoursesMainMenuHandlerService implements CoreMainMenuH
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async isEnabled(): Promise<boolean> {
|
async isEnabled(): Promise<boolean> {
|
||||||
const siteId = CoreSites.getCurrentSiteId();
|
const site = CoreSites.getRequiredCurrentSite();
|
||||||
|
|
||||||
|
const siteId = site.getId();
|
||||||
const disabled = await CoreCourses.isMyCoursesDisabled(siteId);
|
const disabled = await CoreCourses.isMyCoursesDisabled(siteId);
|
||||||
|
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (site.isVersionGreaterEqualThan('4.0')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dashboard cannot be disabled on 3.5 or 3.6 so it will never show this tab.
|
||||||
const dashboardEnabled = await CoreDashboardHomeHandler.isEnabledForSite(siteId);
|
const dashboardEnabled = await CoreDashboardHomeHandler.isEnabledForSite(siteId);
|
||||||
const siteHomeEnabled = await CoreSiteHomeHomeHandler.isEnabledForSite(siteId);
|
const siteHomeEnabled = await CoreSiteHomeHomeHandler.isEnabledForSite(siteId);
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"allgroups": "All groups",
|
"allgroups": "All groups",
|
||||||
"allparticipants": "All participants",
|
"allparticipants": "All participants",
|
||||||
|
"applyfilters": "Apply filters",
|
||||||
"answer": "Answer",
|
"answer": "Answer",
|
||||||
"answered": "Answered",
|
"answered": "Answered",
|
||||||
"areyousure": "Are you sure?",
|
"areyousure": "Are you sure?",
|
||||||
|
|
|
@ -1163,3 +1163,7 @@ iframe {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-grid.core-no-grid > ion-row {
|
||||||
|
display: block;
|
||||||
|
}
|
Loading…
Reference in New Issue