Extract list items management from grades

main
Noel De Martin 2021-02-04 19:55:55 +01:00
parent 98910cf465
commit adc026cd50
6 changed files with 219 additions and 194 deletions

View File

@ -8,18 +8,18 @@
</ion-header> </ion-header>
<ion-content> <ion-content>
<core-split-view> <core-split-view>
<ion-refresher slot="fixed" [disabled]="!gradesTableLoaded" (ionRefresh)="refreshGradesTable($event.target)"> <ion-refresher slot="fixed" [disabled]="!grades.loaded" (ionRefresh)="refreshGrades($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<core-loading [hideUntil]="gradesTableLoaded" class="safe-area-page"> <core-loading [hideUntil]="grades.loaded" class="safe-area-page">
<core-empty-box *ngIf="!gradesTable" icon="stats" [message]="'core.grades.nogradesreturned' | translate"> <core-empty-box *ngIf="grades.empty" icon="stats" [message]="'core.grades.nogradesreturned' | translate">
</core-empty-box> </core-empty-box>
<div *ngIf="gradesTable" class="core-grades-container"> <div *ngIf="!grades.empty" class="core-grades-container">
<table cellspacing="0" cellpadding="0" class="core-grades-table"> <table cellspacing="0" cellpadding="0" class="core-grades-table">
<thead> <thead>
<tr> <tr>
<th <th
*ngFor="let column of gradesTable.columns" *ngFor="let column of grades.columns"
id="{{column.name}}" id="{{column.name}}"
class="ion-text-start" class="ion-text-start"
[class.ion-hide-md-down]="column.hiddenPhone" [class.ion-hide-md-down]="column.hiddenPhone"
@ -31,8 +31,8 @@
</thead> </thead>
<tbody> <tbody>
<tr <tr
*ngFor="let row of gradesTable.rows" *ngFor="let row of grades.rows"
(click)="row.itemtype != 'category' && gotoGrade(row.id)" (click)="row.itemtype != 'category' && grades.select(row)"
[class]="row.rowclass" [class]="row.rowclass"
[ngClass]='{"core-grades-grade-clickable": row.itemtype != "category"}' [ngClass]='{"core-grades-grade-clickable": row.itemtype != "category"}'
> >
@ -45,14 +45,14 @@
<th <th
class="core-grades-table-gradeitem ion-text-start" class="core-grades-table-gradeitem ion-text-start"
[class.column-itemname]="row.itemtype == 'category'" [class.column-itemname]="row.itemtype == 'category'"
[class.core-selected-item]="activeGradeId == row.id" [class.core-selected-item]="grades.isSelected(row)"
[attr.colspan]="row.colspan" [attr.colspan]="row.colspan"
> >
<ion-icon *ngIf="row.icon" name="{{row.icon}}" slot="start"></ion-icon> <ion-icon *ngIf="row.icon" name="{{row.icon}}" slot="start"></ion-icon>
<img *ngIf="row.image" [src]="row.image" slot="start" /> <img *ngIf="row.image" [src]="row.image" slot="start" />
<span [innerHTML]="row.gradeitem"></span> <span [innerHTML]="row.gradeitem"></span>
</th> </th>
<ng-container *ngFor="let column of gradesTable.columns"> <ng-container *ngFor="let column of grades.columns">
<td <td
*ngIf="column.name != 'gradeitem' && row[column.name] != undefined" *ngIf="column.name != 'gradeitem' && row[column.name] != undefined"
[class]="'ion-text-start core-grades-table-' + column.name" [class]="'ion-text-start core-grades-table-' + column.name"

View File

@ -12,20 +12,24 @@
// 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 { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router';
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
import { IonRefresher } from '@ionic/angular'; import { IonRefresher } from '@ionic/angular';
import { Subscription } from 'rxjs';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreGrades } from '@features/grades/services/grades'; import { CoreGrades } from '@features/grades/services/grades';
import { CoreGradesFormattedTable, CoreGradesHelper } from '@features/grades/services/grades-helper'; import {
import { CoreNavigator } from '@services/navigator'; CoreGradesFormattedTable,
CoreGradesFormattedTableColumn,
CoreGradesFormattedTableRow,
CoreGradesFormattedTableRowFilled,
CoreGradesHelper,
} from '@features/grades/services/grades-helper';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreScreen } from '@services/screen';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreObject } from '@singletons/object'; import { CoreObject } from '@singletons/object';
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
/** /**
* Page that displays a course grades. * Page that displays a course grades.
@ -35,116 +39,147 @@ import { CoreObject } from '@singletons/object';
templateUrl: 'course.html', templateUrl: 'course.html',
styleUrls: ['course.scss'], styleUrls: ['course.scss'],
}) })
export class CoreGradesCoursePage implements OnInit, OnDestroy { export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
courseId: number; grades: CoreGradesCourseManager;
userId: number;
gradesTable?: CoreGradesFormattedTable;
gradesTableLoaded = false;
activeGradeId?: number;
layoutSubscription?: Subscription;
@ViewChild(CoreSplitViewComponent) splitView?: CoreSplitViewComponent; @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
constructor(private route: ActivatedRoute) { constructor(route: ActivatedRoute) {
this.courseId = route.snapshot.params.courseId; const courseId = parseInt(route.snapshot.params.courseId);
this.userId = route.snapshot.queryParams.userId ?? CoreSites.instance.getCurrentSiteUserId(); const userId = parseInt(route.snapshot.queryParams.userId ?? CoreSites.instance.getCurrentSiteUserId());
this.grades = new CoreGradesCourseManager(CoreGradesCoursePage, courseId, userId);
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
async ngOnInit(): Promise<void> { async ngAfterViewInit(): Promise<void> {
this.layoutSubscription = CoreScreen.instance.layoutObservable.subscribe(() => this.updateActiveGrade()); await this.fetchInitialGrades();
await this.fetchGradesTable(); this.grades.watchSplitViewOutlet(this.splitView);
this.grades.start();
// Add log in Moodle.
await CoreUtils.instance.ignoreErrors(CoreGrades.instance.logCourseGradesView(this.courseId, this.userId));
}
/**
* @inheritdoc
*/
ionViewWillEnter(): void {
this.updateActiveGrade();
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.layoutSubscription?.unsubscribe(); this.grades.destroy();
} }
/** /**
* Fetch all the data required for the view. * Refresh grades.
*/
async fetchGradesTable(): Promise<void> {
try {
const table = await CoreGrades.instance.getCourseGradesTable(this.courseId, this.userId);
this.gradesTable = CoreGradesHelper.instance.formatGradesTable(table);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading grades');
this.gradesTable = { rows: [], columns: [] };
} finally {
this.gradesTableLoaded = true;
}
}
/**
* Refresh data.
* *
* @param refresher Refresher. * @param refresher Refresher.
*/ */
async refreshGradesTable(refresher: IonRefresher): Promise<void> { async refreshGrades(refresher: IonRefresher): Promise<void> {
await CoreUtils.instance.ignoreErrors(CoreGrades.instance.invalidateCourseGradesData(this.courseId, this.userId)); const { courseId, userId } = this.grades;
await CoreUtils.instance.ignoreErrors(this.fetchGradesTable());
refresher.complete(); await CoreUtils.instance.ignoreErrors(CoreGrades.instance.invalidateCourseGradesData(courseId, userId));
await CoreUtils.instance.ignoreErrors(this.fetchGrades());
refresher?.complete();
} }
/** /**
* Navigate to the grade of the selected item. * Obtain the initial table of grades.
*
* @param gradeId Grade item ID where to navigate.
*/ */
async gotoGrade(gradeId: number): Promise<void> { private async fetchInitialGrades(): Promise<void> {
const path = this.activeGradeId ? `../${gradeId}` : gradeId.toString(); try {
await this.fetchGrades();
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading course');
await CoreNavigator.instance.navigate(path, { this.grades.setTable({ columns: [], rows: [] });
params: CoreObject.withoutEmpty({ userId: this.userId }), }
});
this.updateActiveGrade(gradeId);
} }
/** /**
* Update active grade. * Update the table of grades.
*
* @param activeGradeId Active grade id.
*/ */
private updateActiveGrade(activeGradeId?: number): void { private async fetchGrades(): Promise<void> {
if (CoreScreen.instance.isMobile || this.splitView?.isNested) { const table = await CoreGrades.instance.getCourseGradesTable(this.grades.courseId!, this.grades.userId);
delete this.activeGradeId; const formattedTable = await CoreGradesHelper.instance.formatGradesTable(table);
return; this.grades.setTable(formattedTable);
} }
this.activeGradeId = activeGradeId ?? this.guessActiveGrade();
} }
/** /**
* Guess active grade looking at the current route. * Helper to manage the table of grades.
*
* @return Active grade id.
*/ */
private guessActiveGrade(): number | undefined { class CoreGradesCourseManager extends CorePageItemsListManager<CoreGradesFormattedTableRowFilled> {
const gradeId = parseInt(this.route.snapshot?.firstChild?.params.gradeId);
return isNaN(gradeId) ? undefined : gradeId; courseId: number;
userId: number;
columns?: CoreGradesFormattedTableColumn[];
rows?: CoreGradesFormattedTableRow[];
constructor(pageComponent: unknown, courseId: number, userId: number) {
super(pageComponent);
this.courseId = courseId;
this.userId = userId;
}
/**
* Set grades table.
*
* @param table Grades table.
*/
setTable(table: CoreGradesFormattedTable): void {
this.columns = table.columns;
this.rows = table.rows;
this.setItems(table.rows.filter(this.isFilledRow));
}
/**
* @inheritdoc
*/
protected getDefaultItem(): CoreGradesFormattedTableRowFilled | null {
return null;
}
/**
* @inheritdoc
*/
protected getItemPath(row: CoreGradesFormattedTableRowFilled): string {
return row.id.toString();
}
/**
* @inheritdoc
*/
protected getItemQueryParams(): Params {
return CoreObject.withoutEmpty({ userId: this.userId });
}
/**
* @inheritdoc
*/
protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null {
return route.params.gradeId ?? null;
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
await CoreGrades.instance.logCourseGradesView(this.courseId!, this.userId!);
}
/**
* Check whether the given row is filled or not.
*
* @param row Grades table row.
* @return Whether the given row is filled or not.
*/
private isFilledRow(row: CoreGradesFormattedTableRow): row is CoreGradesFormattedTableRowFilled {
return 'id' in row;
} }
} }

View File

@ -8,34 +8,34 @@
</ion-header> </ion-header>
<ion-content> <ion-content>
<core-split-view> <core-split-view>
<ion-refresher slot="fixed" [disabled]="!gradesLoaded" (ionRefresh)="refreshGrades($event.target)"> <ion-refresher slot="fixed" [disabled]="!courses.loaded" (ionRefresh)="refreshCourses($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<core-loading [hideUntil]="gradesLoaded"> <core-loading [hideUntil]="courses.loaded">
<core-empty-box <core-empty-box
*ngIf="grades && grades.length == 0" *ngIf="courses.empty"
icon="stats" icon="stats"
[message]="'core.grades.nogradesreturned' | translate" [message]="'core.grades.nogradesreturned' | translate"
></core-empty-box> ></core-empty-box>
<ion-list *ngIf="grades && grades.length > 0"> <ion-list *ngIf="!courses.empty">
<ion-item <ion-item
*ngFor="let grade of grades" *ngFor="let course of courses.items"
[title]="grade.courseFullName" [title]="course.courseFullName"
[class.core-selected-item]="grade.courseid === this.activeCourseId" [class.core-selected-item]="courses.isSelected(course)"
class="ion-text-wrap" class="ion-text-wrap"
button button
detail detail
(click)="openCourse(grade.courseid)" (click)="courses.select(course)"
> >
<ion-label> <ion-label>
<core-format-text <core-format-text
[text]="grade.courseFullName" [text]="course.courseFullName"
[contextInstanceId]="grade.courseid" [contextInstanceId]="course.courseid"
contextLevel="course" contextLevel="course"
></core-format-text> ></core-format-text>
</ion-label> </ion-label>
<ion-badge slot="end" color="light">{{grade.grade}}</ion-badge> <ion-badge slot="end" color="light">{{course.grade}}</ion-badge>
</ion-item> </ion-item>
</ion-list> </ion-list>
</core-loading> </core-loading>

View File

@ -12,17 +12,16 @@
// 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, OnDestroy, OnInit } from '@angular/core'; import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
import { IonRefresher } from '@ionic/angular'; import { ActivatedRouteSnapshot } from '@angular/router';
import { Subscription } from 'rxjs'; import { CorePageItemsListManager } from '@classes/page-items-list-manager';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreGrades } from '@features/grades/services/grades'; import { CoreGrades } from '@features/grades/services/grades';
import { CoreGradesHelper, CoreGradesGradeOverviewWithCourseData } from '@features/grades/services/grades-helper'; import { CoreGradesGradeOverviewWithCourseData, CoreGradesHelper } from '@features/grades/services/grades-helper';
import { CoreNavigator } from '@services/navigator'; import { IonRefresher } from '@ionic/angular';
import { CoreScreen } from '@services/screen'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { ActivatedRoute } from '@angular/router';
/** /**
* Page that displays courses grades (main menu option). * Page that displays courses grades (main menu option).
@ -31,113 +30,92 @@ import { ActivatedRoute } from '@angular/router';
selector: 'page-core-grades-courses', selector: 'page-core-grades-courses',
templateUrl: 'courses.html', templateUrl: 'courses.html',
}) })
export class CoreGradesCoursesPage implements OnInit, OnDestroy { export class CoreGradesCoursesPage implements OnDestroy, AfterViewInit {
grades?: CoreGradesGradeOverviewWithCourseData[]; courses: CoreGradesCoursesManager = new CoreGradesCoursesManager(CoreGradesCoursesPage);
gradesLoaded = false;
activeCourseId?: number;
layoutSubscription?: Subscription;
constructor(private route: ActivatedRoute) {} @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
/** /**
* @inheritdoc * @inheritdoc
*/ */
async ngOnInit(): Promise<void> { async ngAfterViewInit(): Promise<void> {
this.layoutSubscription = CoreScreen.instance.layoutObservable.subscribe(() => this.updateActiveCourse()); await this.fetchInitialCourses();
this.updateActiveCourse();
await this.fetchGrades(); this.courses.watchSplitViewOutlet(this.splitView);
this.courses.start();
if (!CoreScreen.instance.isMobile && !this.activeCourseId && this.grades && this.grades.length > 0) {
this.openCourse(this.grades[0].courseid);
}
// Add log in Moodle.
await CoreUtils.instance.ignoreErrors(CoreGrades.instance.logCoursesGradesView());
}
/**
* @inheritdoc
*/
ionViewWillEnter(): void {
this.updateActiveCourse();
} }
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.layoutSubscription?.unsubscribe(); this.courses.destroy();
} }
/** /**
* Fetch all the data required for the view. * Refresh courses.
*/
async fetchGrades(): Promise<void> {
try {
const grades = await CoreGrades.instance.getCoursesGrades();
const gradesWithCourseData = await CoreGradesHelper.instance.getGradesCourseData(grades);
this.grades = gradesWithCourseData;
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading grades');
this.grades = [];
} finally {
this.gradesLoaded = true;
}
}
/**
* Refresh data.
* *
* @param refresher Refresher. * @param refresher Refresher.
*/ */
async refreshGrades(refresher: IonRefresher): Promise<void> { async refreshCourses(refresher: IonRefresher): Promise<void> {
await CoreUtils.instance.ignoreErrors(CoreGrades.instance.invalidateCoursesGradesData()); await CoreUtils.instance.ignoreErrors(CoreGrades.instance.invalidateCoursesGradesData());
await CoreUtils.instance.ignoreErrors(this.fetchGrades()); await CoreUtils.instance.ignoreErrors(this.fetchCourses());
refresher.complete(); refresher?.complete();
} }
/** /**
* Navigate to the grades of the selected course. * Obtain the initial list of courses.
*
* @param courseId Course Id where to navigate.
*/ */
async openCourse(courseId: number): Promise<void> { private async fetchInitialCourses(): Promise<void> {
const path = this.activeCourseId ? `../${courseId}` : courseId.toString(); try {
await this.fetchCourses();
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading courses');
await CoreNavigator.instance.navigate(path); this.courses.setItems([]);
}
this.updateActiveCourse(courseId);
} }
/** /**
* Update active course. * Update the list of courses.
*
* @param activeCourseId Active course id.
*/ */
private updateActiveCourse(activeCourseId?: number): void { private async fetchCourses(): Promise<void> {
if (CoreScreen.instance.isMobile) { const grades = await CoreGrades.instance.getCoursesGrades();
delete this.activeCourseId; const courses = await CoreGradesHelper.instance.getGradesCourseData(grades);
return; this.courses.setItems(courses);
} }
this.activeCourseId = activeCourseId ?? this.guessActiveCourse();
} }
/** /**
* Guess active course looking at the current route. * Helper class to manage courses.
*
* @return Active course id.
*/ */
private guessActiveCourse(): number | undefined { class CoreGradesCoursesManager extends CorePageItemsListManager<CoreGradesGradeOverviewWithCourseData> {
const courseId = parseInt(this.route.snapshot?.firstChild?.params.courseId);
return isNaN(courseId) ? undefined : courseId; /**
* @inheritdoc
*/
protected getItemPath(courseGrade: CoreGradesGradeOverviewWithCourseData): string {
return courseGrade.courseid.toString();
}
/**
* @inheritdoc
*/
protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null {
const courseId = parseInt(route?.params.courseId);
return isNaN(courseId) ? null : courseId.toString();
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
await CoreGrades.instance.logCoursesGradesView();
} }
} }

View File

@ -38,9 +38,9 @@ export class CoreGradesGradePage implements OnInit {
gradeLoaded = false; gradeLoaded = false;
constructor(route: ActivatedRoute) { constructor(route: ActivatedRoute) {
this.courseId = route.snapshot.params.courseId ?? route.snapshot.parent?.params.courseId; this.courseId = parseInt(route.snapshot.params.courseId ?? route.snapshot.parent?.params.courseId);
this.gradeId = route.snapshot.params.gradeId; this.gradeId = parseInt(route.snapshot.params.gradeId);
this.userId = route.snapshot.queryParams.userId ?? CoreSites.instance.getCurrentSiteUserId(); this.userId = parseInt(route.snapshot.queryParams.userId ?? CoreSites.instance.getCurrentSiteUserId());
} }
/** /**

View File

@ -144,9 +144,9 @@ export class CoreGradesHelperProvider {
*/ */
formatGradesTable(table: CoreGradesTable): CoreGradesFormattedTable { formatGradesTable(table: CoreGradesTable): CoreGradesFormattedTable {
const maxDepth = table.maxdepth; const maxDepth = table.maxdepth;
const formatted: CoreGradesFormattedTable = { const formatted = {
columns: [], columns: [] as any[],
rows: [], rows: [] as any[],
}; };
// Columns, in order. // Columns, in order.
@ -673,9 +673,21 @@ export class CoreGradesHelper extends makeSingleton(CoreGradesHelperProvider) {}
export type CoreGradesFormattedRow = any; export type CoreGradesFormattedRow = any;
export type CoreGradesFormattedRowForTable = any; export type CoreGradesFormattedRowForTable = any;
export type CoreGradesFormattedItem = any; export type CoreGradesFormattedItem = any;
export type CoreGradesFormattedTableColumn = any;
export type CoreGradesFormattedTableRow = CoreGradesFormattedTableRowFilled | CoreGradesFormattedTableRowEmpty;
export type CoreGradesFormattedTable = { export type CoreGradesFormattedTable = {
columns: any[]; columns: CoreGradesFormattedTableColumn[];
rows: any[]; rows: CoreGradesFormattedTableRow[];
};
export type CoreGradesFormattedTableRowFilled = {
// @todo complete types.
id: number;
itemtype: 'category' | 'leader';
grade: unknown;
percentage: unknown;
};
type CoreGradesFormattedTableRowEmpty ={
//
}; };
/** /**