MOBILE-2302 courses: Implement My Courses

main
Dani Palou 2017-12-12 10:49:13 +01:00
parent e778448824
commit 2d29cc2da6
18 changed files with 528 additions and 18 deletions

View File

@ -113,3 +113,105 @@ ion-avatar ion-img, ion-avatar img {
font-style: italic;
}
/** Format Text */
core-format-text[maxHeight], *[core-format-text][maxHeight] {
display: block;
position: relative;
width: 100%;
overflow: hidden;
/* Force display inline */
&.inline {
display: inline-block;
width: auto;
}
// This is to allow clicks in radio/checkbox content.
&.mm-text-formatted {
cursor: pointer;
.mm-show-more {
display: none;
}
&:not(.mm-shortened) {
max-height: none !important;
}
&.mm-shortened {
color: $gray-darker;
overflow: hidden;
min-height: 50px;
.mm-show-more {
color: color($colors, dark);
text-align: right;
font-size: 14px;
display: block;
position: absolute;
bottom: 0;
right: 0;
z-index: 1001;
background-color: $white;
padding-left: 10px;
/* @todo
&:after {
@extend .ion;
content: $ionicon-var-chevron-down;
margin-left: 10px;
color: $item-icon-accessory-color;
}
*/
}
&.mm-expand-in-fullview .mm-show-more:after {
// content: $ionicon-var-chevron-right; @todo
}
&:before {
content: '';
height: 100%;
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: -moz-linear-gradient(top, rgba(255, 255, 255, 0) calc(100% - 50px), white calc(100% - 15px));
background: -webkit-gradient(left top, left bottom, color-stop(calc(100% - 50px), rgba(255, 255, 255, 0)), color-stop(calc(100% - 15px), white));
background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) calc(100% - 50px), white calc(100% - 15px));
background: -o-linear-gradient(top, rgba(255, 255, 255, 0) calc(100% - 50px), white calc(100% - 15px));
background: -ms-linear-gradient(top, rgba(255, 255, 255, 0) calc(100% - 50px), white calc(100% - 15px));
background: linear-gradient(to bottom, rgba(255, 255, 255, 0) calc(100% - 50px), white calc(100% - 15px));
z-index: 1000;
}
}
}
}
core-format-text, *[core-format-text] {
audio, video, a, iframe {
pointer-events: auto;
}
// Fix lists styles in core-format-text.
ul, ol {
-webkit-padding-start: 40px;
}
ul {
list-style: disc;
}
ol {
list-style: decimal;
}
.badge {
position: initial !important;
}
}
// Message item.
.item-message {
core-format-text > p:only-child {
display: inline;
}
}

View File

@ -482,7 +482,14 @@ export class CoreSite {
// We pass back a clone of the original object, this may
// prevent errors if in the callback the object is modified.
return Object.assign({}, response);
if (typeof response == 'object') {
if (Array.isArray(response)) {
return Array.from(response);
} else {
return Object.assign({}, response);
}
}
return response;
}).catch((error) => {
if (error.errorcode == 'invalidtoken' ||
(error.errorcode == 'accessexception' && error.message.indexOf('Invalid token - token expired') > -1)) {

View File

@ -22,6 +22,7 @@ import { CoreInputErrorsComponent } from './input-errors/input-errors';
import { CoreShowPasswordComponent } from './show-password/show-password';
import { CoreIframeComponent } from './iframe/iframe';
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
@NgModule({
declarations: [
@ -30,7 +31,8 @@ import { CoreProgressBarComponent } from './progress-bar/progress-bar';
CoreInputErrorsComponent,
CoreShowPasswordComponent,
CoreIframeComponent,
CoreProgressBarComponent
CoreProgressBarComponent,
CoreEmptyBoxComponent
],
imports: [
IonicModule,
@ -43,7 +45,8 @@ import { CoreProgressBarComponent } from './progress-bar/progress-bar';
CoreInputErrorsComponent,
CoreShowPasswordComponent,
CoreIframeComponent,
CoreProgressBarComponent
CoreProgressBarComponent,
CoreEmptyBoxComponent
]
})
export class CoreComponentsModule {}

View File

@ -0,0 +1,8 @@
<div class="mm-empty-box" [class.mm-empty-box-inline]="!image && !icon">
<div class="mm-empty-box-content">
<img *ngIf="image && !icon" [src]="image" role="presentation">
<ion-icon *ngIf="icon" name="icon"></ion-icon>
<p *ngIf="message">{{ message }}</p>
<ng-content></ng-content>
</div>
</div>

View File

@ -0,0 +1,3 @@
core-empty-box {
}

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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';
/**
* Component to show an empty box message. It will show an optional icon or image and a text centered on page.
*
* Usage:
* <core-empty-box *ngIf="empty" icon="bell" [message]="'core.emptymessage' | translate"></core-empty-box>
*/
@Component({
selector: 'core-empty-box',
templateUrl: 'empty-box.html'
})
export class CoreEmptyBoxComponent {
@Input() message: string; // Message to display.
@Input() icon?: string; // Name of the icon to use.
@Input() image?: string; // Image source. If an icon is provided, image won't be used.
constructor() {}
}

View File

@ -0,0 +1,40 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '../../../components/components.module';
import { CoreDirectivesModule } from '../../../directives/directives.module';
import { CoreCoursesCourseProgressComponent } from '../components/course-progress/course-progress';
@NgModule({
declarations: [
CoreCoursesCourseProgressComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule
],
providers: [
],
exports: [
CoreCoursesCourseProgressComponent
]
})
export class CoreCoursesComponentsModule {}

View File

@ -14,19 +14,21 @@
import { NgModule } from '@angular/core';
import { CoreCoursesProvider } from './providers/courses';
import { CoreCoursesCourseProgressComponent } from './components/course-progress/course-progress';
import { CoreCoursesMainMenuHandler } from './providers/handlers';
import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate';
@NgModule({
declarations: [
CoreCoursesCourseProgressComponent
],
declarations: [],
imports: [
],
providers: [
CoreCoursesProvider
CoreCoursesProvider,
CoreCoursesMainMenuHandler
],
exports: [
CoreCoursesCourseProgressComponent
]
exports: []
})
export class CoreCoursesModule {}
export class CoreCoursesModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, mainMenuHandler: CoreCoursesMainMenuHandler) {
mainMenuDelegate.registerHandler(mainMenuHandler);
}
}

View File

@ -0,0 +1,28 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'core.courses.mycourses' | translate }}</ion-title>
<ion-buttons end>
<button *ngIf="searchEnabled" ion-button icon-only (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate">
<ion-icon name="search"></ion-icon>
</button>
<!-- @todo: Context menu. -->
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="coursesLoaded" (ionRefresh)="refreshCourses($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="coursesLoaded">
<ion-item *ngIf="showFilter" class="item-transparent">
<ion-icon name="funnel" class="placeholder-icon" item-start></ion-icon>
<ion-input type="text" name="filter" placeholder="{{ 'core.courses.filtermycourses' | translate }}" [(ngModel)]="filter" (ngModelChange)="filterChanged($event)"></ion-input>
</ion-item>
<core-courses-course-progress *ngFor="let course of filteredCourses" [course]="course" showSummary="true"></core-courses-course-progress>
<core-empty-box *ngIf="!courses || !courses.length" icon="ionic" [message]="'core.courses.nocourses' | translate">
<p *ngIf="searchEnabled">{{ 'core.courses.searchcoursesadvice' | translate }}</p>
</core-empty-box>
</core-loading>
</ion-content>

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreCoursesMyCoursesPage } from './my-courses';
import { CoreComponentsModule } from '../../../../components/components.module';
import { CoreCoursesComponentsModule } from '../../components/components.module';
@NgModule({
declarations: [
CoreCoursesMyCoursesPage,
],
imports: [
CoreComponentsModule,
CoreCoursesComponentsModule,
IonicPageModule.forChild(CoreCoursesMyCoursesPage),
TranslateModule.forChild()
],
})
export class CoreCoursesMyCoursesPageModule {}

View File

@ -0,0 +1,3 @@
page-core-courses-my-courses {
}

View File

@ -0,0 +1,154 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 } from '@angular/core';
import { IonicPage, NavController } from 'ionic-angular';
import { CoreEventsProvider } from '../../../../providers/events';
import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreCoursesProvider } from '../../providers/courses';
/**
* Page that displays the list of courses the user is enrolled in.
*/
@IonicPage()
@Component({
selector: 'page-core-courses-my-courses',
templateUrl: 'my-courses.html',
})
export class CoreCoursesMyCoursesPage {
courses: any[];
filteredCourses: any[];
searchEnabled: boolean;
filter = '';
showFilter = false;
coursesLoaded = false;
protected prefetchIconInitialized = false;
protected myCoursesObserver;
protected siteUpdatedObserver;
constructor(private navCtrl: NavController, private coursesProvider: CoreCoursesProvider,
private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider,
private sitesProvider: CoreSitesProvider) {}
/**
* View loaded.
*/
ionViewDidLoad() {
this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite();
this.fetchCourses().finally(() => {
this.coursesLoaded = true;
});
this.myCoursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, (data) => {
if (data.siteId == this.sitesProvider.getCurrentSiteId()) {
this.fetchCourses();
}
});
this.siteUpdatedObserver = this.eventsProvider.on(CoreEventsProvider.SITE_UPDATED, (data) => {
if (data.siteId == this.sitesProvider.getCurrentSiteId()) {
this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite();
}
});
}
/**
* Fetch the user courses.
*/
protected fetchCourses() {
return this.coursesProvider.getUserCourses().then((courses) => {
const courseIds = courses.map((course) => {
return course.id;
});
return this.coursesProvider.getCoursesOptions(courseIds).then((options) => {
courses.forEach((course) => {
course.progress = isNaN(parseInt(course.progress, 10)) ? false : parseInt(course.progress, 10);
course.navOptions = options.navOptions[course.id];
course.admOptions = options.admOptions[course.id];
});
this.courses = courses;
this.filteredCourses = this.courses;
this.filter = '';
// this.initPrefetchCoursesIcon();
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
});
}
/**
* Refresh the courses.
*
* @param {any} refresher Refresher.
*/
refreshCourses(refresher: any) {
let promises = [];
promises.push(this.coursesProvider.invalidateUserCourses());
// promises.push($mmCoursesDelegate.clearAndInvalidateCoursesOptions());
Promise.all(promises).finally(() => {
this.prefetchIconInitialized = false;
this.fetchCourses().finally(() => {
refresher.complete();
});
});
}
/**
* Show or hide the filter.
*/
switchFilter() {
this.filter = '';
this.showFilter = !this.showFilter;
this.filteredCourses = this.courses;
}
/**
* Go to search courses.
*/
openSearch() {
this.navCtrl.push('CoreCoursesSearchPage');
}
/**
* The filter has changed.
*
* @param {string} newValue New filter value.
*/
filterChanged(newValue: string) {
if (!newValue || !this.courses) {
this.filteredCourses = this.courses;
} else {
this.filteredCourses = this.courses.filter((course) => {
return course.fullname.indexOf(newValue) > -1;
});
}
}
/**
* Page destroyed.
*/
ngOnDestroy() {
this.myCoursesObserver && this.myCoursesObserver.off();
this.siteUpdatedObserver && this.siteUpdatedObserver.off();
}
}

View File

@ -144,7 +144,7 @@ export class CoreCoursesProvider {
* @param {CoreSite} [site] Site. If not defined, use current site.
* @return {boolean} Whether it's disabled.
*/
isMyCoursesDisabledInSite(site: CoreSite) : boolean {
isMyCoursesDisabledInSite(site?: CoreSite) : boolean {
site = site || this.sitesProvider.getCurrentSite();
return site.isFeatureDisabled('$mmSideMenuDelegate_mmCourses');
}
@ -167,7 +167,7 @@ export class CoreCoursesProvider {
* @param {CoreSite} [site] Site. If not defined, use current site.
* @return {boolean} Whether it's disabled.
*/
isSearchCoursesDisabledInSite(site: CoreSite) : boolean {
isSearchCoursesDisabledInSite(site?: CoreSite) : boolean {
site = site || this.sitesProvider.getCurrentSite();
return site.isFeatureDisabled('$mmCoursesDelegate_search');
}

View File

@ -0,0 +1,65 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreCoursesProvider } from './courses';
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/providers/delegate';
/**
* Handler to inject an option into main menu.
*/
@Injectable()
export class CoreCoursesMainMenuHandler implements CoreMainMenuHandler {
name = 'mmCourses';
priority = 1100;
constructor(private coursesProvider: CoreCoursesProvider) {}
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean|Promise<boolean> {
let myCoursesDisabled = this.coursesProvider.isMyCoursesDisabledInSite();
// Check if overview side menu is available, so it won't show My courses.
// var $mmaMyOverview = $mmAddonManager.get('$mmaMyOverview');
// if ($mmaMyOverview) {
// return $mmaMyOverview.isSideMenuAvailable().then(function(enabled) {
// if (enabled) {
// return false;
// }
// // Addon not enabled, check my courses.
// return !myCoursesDisabled;
// });
// }
// Addon not present, check my courses.
return !myCoursesDisabled;
}
/**
* Returns the data needed to render the handler.
*
* @return {CoreMainMenuHandlerData} Data needed to render the handler.
*/
getDisplayData(): CoreMainMenuHandlerData {
return {
icon: 'ionic',
title: 'core.courses.mycourses',
page: 'CoreCoursesMyCoursesPage',
class: 'mm-mycourses-handler'
};
}
}

View File

@ -28,7 +28,7 @@ import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../providers/d
templateUrl: 'menu.html',
})
export class CoreMainMenuPage implements OnDestroy {
tabs: CoreMainMenuHandlerData[];
tabs: CoreMainMenuHandlerData[] = [];
loaded: boolean;
protected subscription;
protected moreTabData = {
@ -36,6 +36,7 @@ export class CoreMainMenuPage implements OnDestroy {
title: 'core.more',
icon: 'more'
};
protected moreTabAdded = false;
protected logoutObserver;
constructor(private menuDelegate: CoreMainMenuDelegate, private sitesProvider: CoreSitesProvider,
@ -58,7 +59,31 @@ export class CoreMainMenuPage implements OnDestroy {
this.subscription = this.menuDelegate.getHandlers().subscribe((handlers) => {
this.tabs = handlers.slice(0, CoreMainMenuProvider.NUM_MAIN_HANDLERS); // Get main handlers.
this.tabs.push(this.moreTabData); // Add "More" tab.
// Check if handlers are already in tabs. Add the ones that aren't.
// @todo: https://github.com/ionic-team/ionic/issues/13633
for (let i in handlers) {
let handler = handlers[i],
found = false;
for (let j in this.tabs) {
let tab = this.tabs[j];
if (tab.title == handler.title && tab.icon == handler.icon) {
found = true;
break;
}
}
if (!found) {
this.tabs.push(handler);
}
}
if (!this.moreTabAdded) {
this.moreTabAdded = true;
this.tabs.push(this.moreTabData); // Add "More" tab.
}
this.loaded = this.menuDelegate.areHandlersLoaded();
});
}

View File

@ -1,6 +1,6 @@
<ion-header>
<ion-navbar>
<ion-title>{{ siteInfo.sitename }}</ion-title>
<ion-title><core-format-text [text]="siteInfo.sitename"></core-format-text></ion-title>
</ion-navbar>
</ion-header>
<ion-content>

View File

@ -177,7 +177,8 @@ export class CoreFormatTextDirective implements OnChanges {
if (expandInFullview) {
this.element.classList.add('mm-expand-in-fullview');
}
this.element.classList.add('mm-text-formatted mm-shortened');
this.element.classList.add('mm-text-formatted');
this.element.classList.add('mm-shortened');
this.element.style.maxHeight = this.maxHeight + 'px';
this.element.addEventListener('click', (e) => {

View File

@ -726,6 +726,9 @@ export class CoreDomUtilsProvider {
*/
showErrorModalDefault(error: any, defaultError: any, needsTranslate?: boolean, autocloseTime?: number) : Alert {
if (error != CoreConstants.dontShowError) {
if (error && typeof error != 'string') {
error = error.message || error.error;
}
error = typeof error == 'string' ? error : defaultError;
return this.showErrorModal(error, needsTranslate, autocloseTime);
}