MOBILE-3686 courses: Merge available courses and search courses pages

main
Pau Ferrer Ocaña 2021-10-13 16:18:29 +02:00
parent 55b6d9f76a
commit 5ac62106bf
13 changed files with 263 additions and 280 deletions

View File

@ -33,16 +33,10 @@ const routes: Routes = [
.then(m => m.CoreCoursesCategoriesPageModule),
},
{
path: 'all',
path: 'list',
loadChildren: () =>
import('./pages/available-courses/available-courses.module')
.then(m => m.CoreCoursesAvailableCoursesPageModule),
},
{
path: 'search',
loadChildren: () =>
import('./pages/search/search.module')
.then(m => m.CoreCoursesSearchPageModule),
import('./pages/list/list.module')
.then(m => m.CoreCoursesListPageModule),
},
{
path: 'my',

View File

@ -1,19 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<h1>{{ 'core.courses.availablecourses' | translate }}</h1>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!coursesLoaded" (ionRefresh)="refreshCourses($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="coursesLoaded">
<ng-container *ngIf="courses.length > 0">
<core-courses-course-list-item *ngFor="let course of courses" [course]="course"></core-courses-course-list-item>
</ng-container>
<core-empty-box *ngIf="!courses.length" icon="fas-graduation-cap" [message]="'core.courses.nocourses' | translate"></core-empty-box>
</core-loading>
</ion-content>

View File

@ -1,41 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreCoursesComponentsModule } from '../../components/components.module';
import { CoreCoursesAvailableCoursesPage } from './available-courses';
const routes: Routes = [
{
path: '',
component: CoreCoursesAvailableCoursesPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CoreSharedModule,
CoreCoursesComponentsModule,
],
declarations: [
CoreCoursesAvailableCoursesPage,
],
exports: [RouterModule],
})
export class CoreCoursesAvailableCoursesPageModule { }

View File

@ -1,78 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { IonRefresher } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourses, CoreCourseSearchedData } from '../../services/courses';
/**
* Page that displays available courses in current site.
*/
@Component({
selector: 'page-core-courses-available-courses',
templateUrl: 'available-courses.html',
})
export class CoreCoursesAvailableCoursesPage implements OnInit {
courses: CoreCourseSearchedData[] = [];
coursesLoaded = false;
/**
* View loaded.
*/
ngOnInit(): void {
this.loadCourses().finally(() => {
this.coursesLoaded = true;
});
}
/**
* Load the courses.
*
* @return Promise resolved when done.
*/
protected async loadCourses(): Promise<void> {
const frontpageCourseId = CoreSites.getCurrentSiteHomeId();
try {
const courses = await CoreCourses.getCoursesByField();
this.courses = courses.filter((course) => course.id != frontpageCourseId);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
}
}
/**
* Refresh the courses.
*
* @param refresher Refresher.
*/
refreshCourses(refresher: IonRefresher): void {
const promises: Promise<void>[] = [];
promises.push(CoreCourses.invalidateUserCourses());
promises.push(CoreCourses.invalidateCoursesByField());
Promise.all(promises).finally(() => {
this.loadCourses().finally(() => {
refresher?.complete();
});
});
}
}

View File

@ -167,7 +167,7 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy {
* Go to search courses.
*/
async openSearch(): Promise<void> {
CoreNavigator.navigateToSitePath('/courses/search');
CoreNavigator.navigateToSitePath('/courses/list', { params : { mode: 'search' } });
}
/**

View File

@ -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.availablecourses' | translate }}</h1>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!coursesLoaded" (ionRefresh)="refreshCourses($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-search-box *ngIf="searchEnabled" (onSubmit)="search($event)" (onClear)="clearSearch()"
[placeholder]="'core.courses.search' | translate" [searchLabel]="'core.courses.search' | translate" [autoFocus]="searchMode"
searchArea="CoreCoursesSearch"></core-search-box>
<core-loading [hideUntil]="coursesLoaded">
<ng-container *ngIf="searchMode && searchTotal > 0">
<ion-item-divider>
<ion-label><h2>{{ 'core.courses.totalcoursesearchresults' | translate:{$a: searchTotal} }}</h2></ion-label>
</ion-item-divider>
</ng-container>
<core-courses-course-list-item *ngFor="let course of courses" [course]="course"></core-courses-course-list-item>
<core-infinite-loading [enabled]="searchMode && searchCanLoadMore" (action)="loadMoreResults($event)" [error]="searchLoadMoreError">
</core-infinite-loading>
<core-empty-box *ngIf="searchMode && !courses.length" icon="fas-search" [message]="'core.courses.nosearchresults' | translate">
</core-empty-box>
<core-empty-box *ngIf="!searchMode && !courses.length" icon="fas-graduation-cap" [message]="'core.courses.nocourses' | translate">
</core-empty-box>
</core-loading>
</ion-content>

View File

@ -19,12 +19,12 @@ import { CoreSharedModule } from '@/core/shared.module';
import { CoreCoursesComponentsModule } from '../../components/components.module';
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
import { CoreCoursesSearchPage } from './search';
import { CoreCoursesListPage } from './list';
const routes: Routes = [
{
path: '',
component: CoreCoursesSearchPage,
component: CoreCoursesListPage,
},
];
@ -36,8 +36,8 @@ const routes: Routes = [
CoreSearchComponentsModule,
],
declarations: [
CoreCoursesSearchPage,
CoreCoursesListPage,
],
exports: [RouterModule],
})
export class CoreCoursesSearchPageModule { }
export class CoreCoursesListPageModule { }

View File

@ -0,0 +1,211 @@
// (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, OnDestroy, OnInit } from '@angular/core';
import { IonRefresher } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreCourseBasicSearchedData, CoreCourses } from '../../services/courses';
type CoreCoursesListMode = 'search' | 'all';
/**
* Page that shows a list of courses.
*/
@Component({
selector: 'page-core-courses-list',
templateUrl: 'list.html',
})
export class CoreCoursesListPage implements OnInit, OnDestroy {
searchEnabled = false;
searchMode = false;
searchCanLoadMore = false;
searchLoadMoreError = false;
searchTotal = 0;
mode: CoreCoursesListMode = 'all';
courses: CoreCourseBasicSearchedData[] = [];
coursesLoaded = false;
protected currentSiteId: string;
protected frontpageCourseId: number;
protected searchPage = 0;
protected searchText = '';
protected siteUpdatedObserver: CoreEventObserver;
constructor() {
this.currentSiteId = CoreSites.getRequiredCurrentSite().getId();
this.frontpageCourseId = CoreSites.getRequiredCurrentSite().getSiteHomeId();
// Refresh the enabled flags if site is updated.
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
if (!this.searchEnabled) {
this.searchMode = false;
this.fetchCourses();
}
}, this.currentSiteId);
}
/**
* @inheritdoc
*/
ngOnInit(): void {
this.mode = CoreNavigator.getRouteParam<CoreCoursesListMode>('mode') || this.mode;
if (this.mode == 'search') {
this.searchMode = true;
}
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
if (!this.searchEnabled) {
this.searchMode = false;
}
this.fetchCourses();
}
/**
* Load the course list.
*
* @return Promise resolved when done.
*/
protected async fetchCourses(): Promise<void> {
try {
if (this.searchMode && this.searchText) {
await this.search(this.searchText);
} else {
await this.loadAvailableCourses();
}
} finally {
this.coursesLoaded = true;
}
}
/**
* Load the courses.
*
* @return Promise resolved when done.
*/
protected async loadAvailableCourses(): Promise<void> {
try {
const courses = await CoreCourses.getCoursesByField();
this.courses = courses.filter((course) => course.id != this.frontpageCourseId);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
}
}
/**
* Refresh the courses.
*
* @param refresher Refresher.
*/
refreshCourses(refresher: IonRefresher): void {
const promises: Promise<void>[] = [];
promises.push(CoreCourses.invalidateUserCourses());
promises.push(CoreCourses.invalidateCoursesByField());
Promise.all(promises).finally(() => {
this.fetchCourses().finally(() => {
refresher?.complete();
});
});
}
/**
* Search a new text.
*
* @param text The text to search.
*/
async search(text: string): Promise<void> {
this.searchMode = true;
this.searchText = text;
this.courses = [];
this.searchPage = 0;
this.searchTotal = 0;
const modal = await CoreDomUtils.showModalLoading('core.searching', true);
this.searchCourses().finally(() => {
modal.dismiss();
});
}
/**
* Clear search box.
*/
clearSearch(): void {
this.searchText = '';
this.courses = [];
this.searchPage = 0;
this.searchTotal = 0;
this.searchMode = false;
this.coursesLoaded = false;
this.fetchCourses();
}
/**
* Load more results.
*
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
*/
loadMoreResults(infiniteComplete?: () => void ): void {
this.searchCourses().finally(() => {
infiniteComplete && infiniteComplete();
});
}
/**
* Search courses or load the next page of current search.
*
* @return Promise resolved when done.
*/
protected async searchCourses(): Promise<void> {
this.searchLoadMoreError = false;
try {
const response = await CoreCourses.search(this.searchText, this.searchPage);
if (this.searchPage === 0) {
this.courses = response.courses;
} else {
this.courses = this.courses.concat(response.courses);
}
this.searchTotal = response.total;
this.searchPage++;
this.searchCanLoadMore = this.courses.length < this.searchTotal;
} catch (error) {
this.searchLoadMoreError = true; // Set to prevent infinite calls with infinite-loading.
CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorsearching', true);
}
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.siteUpdatedObserver?.off();
}
}

View File

@ -204,7 +204,7 @@ export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy {
* Go to search courses.
*/
openSearch(): void {
CoreNavigator.navigateToSitePath('courses/search');
CoreNavigator.navigateToSitePath('courses/list', { params : { mode: 'search' } });
}
/**

View File

@ -1,23 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<h1>{{ 'core.courses.searchcourses' | translate }}</h1>
</ion-toolbar>
</ion-header>
<ion-content>
<core-search-box (onSubmit)="search($event)" (onClear)="clearSearch()"
[placeholder]="'core.courses.search' | translate" [searchLabel]="'core.courses.search' | translate" autoFocus="true"
searchArea="CoreCoursesSearch"></core-search-box>
<ng-container *ngIf="total > 0">
<ion-item-divider>
<ion-label><h2>{{ 'core.courses.totalcoursesearchresults' | translate:{$a: total} }}</h2></ion-label>
</ion-item-divider>
<core-courses-course-list-item *ngFor="let course of courses" [course]="course"></core-courses-course-list-item>
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMoreResults($event)" [error]="loadMoreError">
</core-infinite-loading>
</ng-container>
<core-empty-box *ngIf="total == 0" icon="search" [message]="'core.courses.nosearchresults' | translate"></core-empty-box>
</ion-content>

View File

@ -1,100 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourseBasicSearchedData, CoreCourses } from '../../services/courses';
/**
* Page that allows searching for courses.
*/
@Component({
selector: 'page-core-courses-search',
templateUrl: 'search.html',
})
export class CoreCoursesSearchPage {
total = 0;
courses: CoreCourseBasicSearchedData[] = [];
canLoadMore = false;
loadMoreError = false;
protected page = 0;
protected currentSearch = '';
/**
* Search a new text.
*
* @param text The text to search.
*/
async search(text: string): Promise<void> {
this.currentSearch = text;
this.courses = [];
this.page = 0;
this.total = 0;
const modal = await CoreDomUtils.showModalLoading('core.searching', true);
this.searchCourses().finally(() => {
modal.dismiss();
});
}
/**
* Clear search box.
*/
clearSearch(): void {
this.currentSearch = '';
this.courses = [];
this.page = 0;
this.total = 0;
}
/**
* Load more results.
*
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
*/
loadMoreResults(infiniteComplete?: () => void ): void {
this.searchCourses().finally(() => {
infiniteComplete && infiniteComplete();
});
}
/**
* Search courses or load the next page of current search.
*
* @return Promise resolved when done.
*/
protected async searchCourses(): Promise<void> {
this.loadMoreError = false;
try {
const response = await CoreCourses.search(this.currentSearch, this.page);
if (this.page === 0) {
this.courses = response.courses;
} else {
this.courses = this.courses.concat(response.courses);
}
this.total = response.total;
this.page++;
this.canLoadMore = this.courses.length < this.total;
} catch (error) {
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorsearching', true);
}
}
}

View File

@ -42,14 +42,17 @@ export class CoreCoursesIndexLinkHandlerService extends CoreContentLinksHandlerB
return [{
action: (siteId): void => {
let pageName = CoreCoursesMyCoursesHomeHandlerService.PAGE_NAME;
const pageParams: Params = {};
if (params.categoryid) {
pageName += '/categories/' + params.categoryid;
} else {
pageName += '/all';
pageName += '/list';
pageParams.mode = 'all';
}
CoreNavigator.navigateToSitePath(pageName, { siteId });
CoreNavigator.navigateToSitePath(pageName, { params: pageParams, siteId });
},
}];
}

View File

@ -218,14 +218,14 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
* Go to search courses.
*/
openSearch(): void {
CoreNavigator.navigateToSitePath('courses/search');
CoreNavigator.navigateToSitePath('courses/list', { params : { mode: 'search' } });
}
/**
* Go to available courses.
*/
openAvailableCourses(): void {
CoreNavigator.navigateToSitePath('courses/all');
CoreNavigator.navigateToSitePath('courses/list', { params : { mode: 'all' } });
}
/**