From 98910cf46580da08b77ae81b9d0f80ea8d5817d6 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 4 Feb 2021 19:55:26 +0100 Subject: [PATCH 1/3] Extract list items management from settings --- src/core/classes/page-items-list-manager.ts | 202 ++++++++++++++++++ src/core/components/split-view/split-view.ts | 22 +- .../features/settings/pages/index/index.html | 6 +- .../features/settings/pages/index/index.ts | 94 +++----- src/core/services/navigator.ts | 29 ++- 5 files changed, 283 insertions(+), 70 deletions(-) create mode 100644 src/core/classes/page-items-list-manager.ts diff --git a/src/core/classes/page-items-list-manager.ts b/src/core/classes/page-items-list-manager.ts new file mode 100644 index 000000000..0984b31e8 --- /dev/null +++ b/src/core/classes/page-items-list-manager.ts @@ -0,0 +1,202 @@ +// (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 { ActivatedRouteSnapshot, Params } from '@angular/router'; +import { Subscription } from 'rxjs'; + +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreNavigator } from '@services/navigator'; +import { CoreScreen } from '@services/screen'; +import { CoreUtils } from '@services/utils/utils'; + +/** + * Helper class to manage the state and routing of a list of items in a page, for example on pages using a split view. + */ +export abstract class CorePageItemsListManager { + + protected itemsList: Item[] | null = null; + protected itemsMap: Record | null = null; + protected selectedItem: Item | null = null; + protected pageComponent: unknown; + protected splitView?: CoreSplitViewComponent; + protected splitViewOutletSubscription?: Subscription; + + constructor(pageComponent: unknown) { + this.pageComponent = pageComponent; + } + + get items(): Item[] { + return this.itemsList || []; + } + + get loaded(): boolean { + return this.itemsMap !== null; + } + + get empty(): boolean { + return this.itemsList === null || this.itemsList.length === 0; + } + + /** + * Process page started operations. + */ + async start(): Promise { + // Calculate current selected item. + const route = CoreNavigator.instance.getCurrentRoute({ pageComponent: this.pageComponent }); + if (route !== null && route.firstChild) { + this.updateSelectedItem(route.firstChild.snapshot); + } + + // Select default item if none is selected on a non-mobile layout. + if (!CoreScreen.instance.isMobile && this.selectedItem === null) { + const defaultItem = this.getDefaultItem(); + + if (defaultItem) { + this.select(defaultItem); + } + } + + // Log activity. + await CoreUtils.instance.ignoreErrors(this.logActivity()); + } + + /** + * Process page destroyed operations. + */ + destroy(): void { + this.splitViewOutletSubscription?.unsubscribe(); + } + + /** + * Watch a split view outlet to keep track of the selected item. + * + * @param splitView Split view component. + */ + watchSplitViewOutlet(splitView: CoreSplitViewComponent): void { + this.splitView = splitView; + this.splitViewOutletSubscription = splitView.outletRouteObservable.subscribe(route => this.updateSelectedItem(route)); + + this.updateSelectedItem(splitView.outletRoute); + } + + // @todo Implement watchResize. + + /** + * Check whether the given item is selected or not. + * + * @param item Item. + * @return Whether the given item is selected. + */ + isSelected(item: Item): boolean { + return this.selectedItem === item; + } + + /** + * Select an item. + * + * @param item Item. + */ + async select(item: Item): Promise { + // Get current route in the page. + const route = CoreNavigator.instance.getCurrentRoute({ pageComponent: this.pageComponent }); + + if (route === null) { + return; + } + + // If this item is already selected, do nothing. + const itemPath = this.getItemPath(item); + + if (route.firstChild?.routeConfig?.path === itemPath) { + return; + } + + // Navigate to item. + const path = route.firstChild ? `../${itemPath}` : itemPath; + const params = this.getItemQueryParams(item); + + await CoreNavigator.instance.navigate(path, { params }); + } + + /** + * Set the list of items. + * + * @param items Items. + */ + setItems(items: Item[]): void { + this.itemsList = items.slice(0); + this.itemsMap = items.reduce((map, item) => { + map[this.getItemPath(item)] = item; + + return map; + }, {}); + + this.updateSelectedItem(this.splitView?.outletRoute); + } + + /** + * Log activity when the page starts. + */ + protected async logActivity(): Promise { + // + } + + /** + * Update the selected item given the current route. + * + * @param route Current route. + */ + protected updateSelectedItem(route?: ActivatedRouteSnapshot | null): void { + const selectedItemPath = route ? this.getSelectedItemPath(route) : null; + + this.selectedItem = selectedItemPath + ? this.itemsMap?.[selectedItemPath] ?? null + : null; + } + + /** + * Get the item that should be selected by default. + */ + protected getDefaultItem(): Item | null { + return this.itemsList?.[0] || null; + } + + /** + * Get the query parameters to use when navigating to an item page. + * + * @param item Item. + * @return Query parameters to use when navigating to the item page. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected getItemQueryParams(item: Item): Params { + return {}; + } + + /** + * Get the path to use when navigating to an item page. + * + * @param item Item. + * @return Path to use when navigating to the item page. + */ + protected abstract getItemPath(item: Item): string; + + /** + * Get the path of the selected item given the current route. + * + * @param route Current route. + * @return Path of the selected item in the given route. + */ + protected abstract getSelectedItemPath(route: ActivatedRouteSnapshot): string | null; + +} diff --git a/src/core/components/split-view/split-view.ts b/src/core/components/split-view/split-view.ts index c9ab27d97..101719d7d 100644 --- a/src/core/components/split-view/split-view.ts +++ b/src/core/components/split-view/split-view.ts @@ -13,9 +13,10 @@ // limitations under the License. import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; import { IonRouterOutlet } from '@ionic/angular'; import { CoreScreen } from '@services/screen'; -import { Subscription } from 'rxjs'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; enum CoreSplitViewMode { MenuOnly = 'menu-only', // Hides content. @@ -35,18 +36,33 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy { @Input() placeholderText = 'core.emptysplit'; isNested = false; + private outletRouteSubject: BehaviorSubject = new BehaviorSubject(null); private subscriptions?: Subscription[]; constructor(private element: ElementRef) {} + get outletRoute(): ActivatedRouteSnapshot | null { + return this.outletRouteSubject.value; + } + + get outletRouteObservable(): Observable { + return this.outletRouteSubject.asObservable(); + } + /** * @inheritdoc */ ngAfterViewInit(): void { this.isNested = !!this.element.nativeElement.parentElement?.closest('core-split-view'); this.subscriptions = [ - this.outlet.activateEvents.subscribe(() => this.updateClasses()), - this.outlet.deactivateEvents.subscribe(() => this.updateClasses()), + this.outlet.activateEvents.subscribe(() => { + this.updateClasses(); + this.outletRouteSubject.next(this.outlet.activatedRoute.snapshot); + }), + this.outlet.deactivateEvents.subscribe(() => { + this.updateClasses(); + this.outletRouteSubject.next(null); + }), CoreScreen.instance.layoutObservable.subscribe(() => this.updateClasses()), ]; diff --git a/src/core/features/settings/pages/index/index.html b/src/core/features/settings/pages/index/index.html index dc2d95829..2552bd392 100644 --- a/src/core/features/settings/pages/index/index.html +++ b/src/core/features/settings/pages/index/index.html @@ -11,11 +11,11 @@ {{ 'core.settings.' + section.name | translate }} diff --git a/src/core/features/settings/pages/index/index.ts b/src/core/features/settings/pages/index/index.ts index 0d40b0c11..d373ad9a2 100644 --- a/src/core/features/settings/pages/index/index.ts +++ b/src/core/features/settings/pages/index/index.ts @@ -12,83 +12,57 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Subscription } from 'rxjs'; - -import { CoreNavigator } from '@services/navigator'; -import { CoreScreen } from '@services/screen'; +import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; import { CoreSettingsConstants, CoreSettingsSection } from '@features/settings/constants'; +import { CorePageItemsListManager } from '@classes/page-items-list-manager'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; @Component({ selector: 'page-core-settings-index', templateUrl: 'index.html', }) -export class CoreSettingsIndexPage implements OnInit, OnDestroy { +export class CoreSettingsIndexPage implements AfterViewInit, OnDestroy { - sections = CoreSettingsConstants.SECTIONS; - activeSection?: string; - layoutSubscription?: Subscription; + sections: CoreSettingsSectionsManager = new CoreSettingsSectionsManager(CoreSettingsIndexPage); + + @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; /** * @inheritdoc */ - ngOnInit(): void { - this.layoutSubscription = CoreScreen.instance.layoutObservable.subscribe(() => this.updateActiveSection()); - } - - /** - * @inheritdoc - */ - ionViewWillEnter(): void { - this.updateActiveSection(); + ngAfterViewInit(): void { + this.sections.setItems(CoreSettingsConstants.SECTIONS); + this.sections.watchSplitViewOutlet(this.splitView); + this.sections.start(); } /** * @inheritdoc */ ngOnDestroy(): void { - this.layoutSubscription?.unsubscribe(); - } - - /** - * Open a section page. - * - * @param section Section to open. - */ - openSection(section: CoreSettingsSection): void { - const path = this.activeSection ? `../${section.path}` : section.path; - - CoreNavigator.instance.navigate(path); - - this.updateActiveSection(section.name); - } - - /** - * Update active section. - * - * @param activeSection Active section. - */ - private updateActiveSection(activeSection?: string): void { - if (CoreScreen.instance.isMobile) { - delete this.activeSection; - - return; - } - - this.activeSection = activeSection ?? this.guessActiveSection(); - } - - /** - * Guess active section looking at the current route. - * - * @return Active section. - */ - private guessActiveSection(): string | undefined { - const activeSection = this.sections.find( - section => CoreNavigator.instance.isCurrent(`**/settings/${section.path}`), - ); - - return activeSection?.name; + this.sections.destroy(); + } + +} + +/** + * Helper class to manage sections. + */ +class CoreSettingsSectionsManager extends CorePageItemsListManager { + + /** + * @inheritdoc + */ + protected getItemPath(section: CoreSettingsSection): string { + return section.path; + } + + /** + * @inheritdoc + */ + protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null { + return route.parent?.routeConfig?.path ?? null; } } diff --git a/src/core/services/navigator.ts b/src/core/services/navigator.ts index 1789848fa..473807261 100644 --- a/src/core/services/navigator.ts +++ b/src/core/services/navigator.ts @@ -50,6 +50,14 @@ export type CoreNavigationOptions = { reset?: boolean; }; +/** + * Options for CoreNavigatorService#getCurrentRoute method. + */ +type GetCurrentRouteOptions = Partial<{ + parentRoute: ActivatedRoute; + pageComponent: unknown; +}>; + /** * Service to provide some helper functions regarding navigation. */ @@ -310,13 +318,26 @@ export class CoreNavigatorService { /** * Get current activated route. * - * @param route Parent route. + * @param options + * - parent: Parent route, if this isn't provided the current active route will be used. + * - pageComponent: Page component of the route to find, if this isn't provided the deepest route in the hierarchy + * will be returned. * @return Current activated route. */ - protected getCurrentRoute(route?: ActivatedRoute): ActivatedRoute { - route = route ?? Router.instance.routerState.root; + getCurrentRoute(): ActivatedRoute; + getCurrentRoute(options: GetCurrentRouteOptions): ActivatedRoute | null; + getCurrentRoute({ parentRoute, pageComponent }: GetCurrentRouteOptions = {}): ActivatedRoute | null { + parentRoute = parentRoute ?? Router.instance.routerState.root; - return route.firstChild ? this.getCurrentRoute(route.firstChild) : route; + if (pageComponent && parentRoute.component === pageComponent) { + return parentRoute; + } + + if (parentRoute.firstChild) { + return this.getCurrentRoute({ parentRoute: parentRoute.firstChild, pageComponent }); + } + + return pageComponent ? null : parentRoute; } /** From adc026cd50beeec94c24237bbae955c859c9723d Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 4 Feb 2021 19:55:55 +0100 Subject: [PATCH 2/3] Extract list items management from grades --- .../features/grades/pages/course/course.html | 18 +- .../features/grades/pages/course/course.ts | 201 ++++++++++-------- .../grades/pages/courses/courses.html | 22 +- .../features/grades/pages/courses/courses.ts | 144 ++++++------- src/core/features/grades/pages/grade/grade.ts | 6 +- .../features/grades/services/grades-helper.ts | 22 +- 6 files changed, 219 insertions(+), 194 deletions(-) diff --git a/src/core/features/grades/pages/course/course.html b/src/core/features/grades/pages/course/course.html index a7ba31495..f732a8470 100644 --- a/src/core/features/grades/pages/course/course.html +++ b/src/core/features/grades/pages/course/course.html @@ -8,18 +8,18 @@ - + - - + + -
+
@@ -45,14 +45,14 @@ - +
{ - this.layoutSubscription = CoreScreen.instance.layoutObservable.subscribe(() => this.updateActiveGrade()); + async ngAfterViewInit(): Promise { + await this.fetchInitialGrades(); - await this.fetchGradesTable(); - - // Add log in Moodle. - await CoreUtils.instance.ignoreErrors(CoreGrades.instance.logCourseGradesView(this.courseId, this.userId)); - } - - /** - * @inheritdoc - */ - ionViewWillEnter(): void { - this.updateActiveGrade(); + this.grades.watchSplitViewOutlet(this.splitView); + this.grades.start(); } /** * @inheritdoc */ ngOnDestroy(): void { - this.layoutSubscription?.unsubscribe(); + this.grades.destroy(); } /** - * Fetch all the data required for the view. - */ - async fetchGradesTable(): Promise { - 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. + * Refresh grades. * * @param refresher Refresher. */ - async refreshGradesTable(refresher: IonRefresher): Promise { - await CoreUtils.instance.ignoreErrors(CoreGrades.instance.invalidateCourseGradesData(this.courseId, this.userId)); - await CoreUtils.instance.ignoreErrors(this.fetchGradesTable()); + async refreshGrades(refresher: IonRefresher): Promise { + const { courseId, userId } = this.grades; - 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. - * - * @param gradeId Grade item ID where to navigate. + * Obtain the initial table of grades. */ - async gotoGrade(gradeId: number): Promise { - const path = this.activeGradeId ? `../${gradeId}` : gradeId.toString(); + private async fetchInitialGrades(): Promise { + try { + await this.fetchGrades(); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading course'); - await CoreNavigator.instance.navigate(path, { - params: CoreObject.withoutEmpty({ userId: this.userId }), - }); - - this.updateActiveGrade(gradeId); - } - - /** - * Update active grade. - * - * @param activeGradeId Active grade id. - */ - private updateActiveGrade(activeGradeId?: number): void { - if (CoreScreen.instance.isMobile || this.splitView?.isNested) { - delete this.activeGradeId; - - return; + this.grades.setTable({ columns: [], rows: [] }); } - - this.activeGradeId = activeGradeId ?? this.guessActiveGrade(); } /** - * Guess active grade looking at the current route. - * - * @return Active grade id. + * Update the table of grades. */ - private guessActiveGrade(): number | undefined { - const gradeId = parseInt(this.route.snapshot?.firstChild?.params.gradeId); + private async fetchGrades(): Promise { + const table = await CoreGrades.instance.getCourseGradesTable(this.grades.courseId!, this.grades.userId); + const formattedTable = await CoreGradesHelper.instance.formatGradesTable(table); - return isNaN(gradeId) ? undefined : gradeId; + this.grades.setTable(formattedTable); + } + +} + +/** + * Helper to manage the table of grades. + */ +class CoreGradesCourseManager extends CorePageItemsListManager { + + 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 { + 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; } } diff --git a/src/core/features/grades/pages/courses/courses.html b/src/core/features/grades/pages/courses/courses.html index 64b6a39a4..c18910932 100644 --- a/src/core/features/grades/pages/courses/courses.html +++ b/src/core/features/grades/pages/courses/courses.html @@ -8,34 +8,34 @@ - + - + - + - {{grade.grade}} + {{course.grade}} diff --git a/src/core/features/grades/pages/courses/courses.ts b/src/core/features/grades/pages/courses/courses.ts index e25404996..013614f43 100644 --- a/src/core/features/grades/pages/courses/courses.ts +++ b/src/core/features/grades/pages/courses/courses.ts @@ -12,17 +12,16 @@ // 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 { Subscription } from 'rxjs'; +import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; +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 { CoreGradesHelper, CoreGradesGradeOverviewWithCourseData } from '@features/grades/services/grades-helper'; -import { CoreNavigator } from '@services/navigator'; -import { CoreScreen } from '@services/screen'; +import { CoreGradesGradeOverviewWithCourseData, CoreGradesHelper } from '@features/grades/services/grades-helper'; +import { IonRefresher } from '@ionic/angular'; +import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; -import { ActivatedRoute } from '@angular/router'; /** * Page that displays courses grades (main menu option). @@ -31,113 +30,92 @@ import { ActivatedRoute } from '@angular/router'; selector: 'page-core-grades-courses', templateUrl: 'courses.html', }) -export class CoreGradesCoursesPage implements OnInit, OnDestroy { +export class CoreGradesCoursesPage implements OnDestroy, AfterViewInit { - grades?: CoreGradesGradeOverviewWithCourseData[]; - gradesLoaded = false; - activeCourseId?: number; - layoutSubscription?: Subscription; + courses: CoreGradesCoursesManager = new CoreGradesCoursesManager(CoreGradesCoursesPage); - constructor(private route: ActivatedRoute) {} + @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; /** * @inheritdoc */ - async ngOnInit(): Promise { - this.layoutSubscription = CoreScreen.instance.layoutObservable.subscribe(() => this.updateActiveCourse()); - this.updateActiveCourse(); + async ngAfterViewInit(): Promise { + await this.fetchInitialCourses(); - await this.fetchGrades(); - - 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(); + this.courses.watchSplitViewOutlet(this.splitView); + this.courses.start(); } /** * @inheritdoc */ ngOnDestroy(): void { - this.layoutSubscription?.unsubscribe(); + this.courses.destroy(); } /** - * Fetch all the data required for the view. - */ - async fetchGrades(): Promise { - 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. + * Refresh courses. * * @param refresher Refresher. */ - async refreshGrades(refresher: IonRefresher): Promise { + async refreshCourses(refresher: IonRefresher): Promise { 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. - * - * @param courseId Course Id where to navigate. + * Obtain the initial list of courses. */ - async openCourse(courseId: number): Promise { - const path = this.activeCourseId ? `../${courseId}` : courseId.toString(); + private async fetchInitialCourses(): Promise { + try { + await this.fetchCourses(); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading courses'); - await CoreNavigator.instance.navigate(path); - - this.updateActiveCourse(courseId); - } - - /** - * Update active course. - * - * @param activeCourseId Active course id. - */ - private updateActiveCourse(activeCourseId?: number): void { - if (CoreScreen.instance.isMobile) { - delete this.activeCourseId; - - return; + this.courses.setItems([]); } - - this.activeCourseId = activeCourseId ?? this.guessActiveCourse(); } /** - * Guess active course looking at the current route. - * - * @return Active course id. + * Update the list of courses. */ - private guessActiveCourse(): number | undefined { - const courseId = parseInt(this.route.snapshot?.firstChild?.params.courseId); + private async fetchCourses(): Promise { + const grades = await CoreGrades.instance.getCoursesGrades(); + const courses = await CoreGradesHelper.instance.getGradesCourseData(grades); - return isNaN(courseId) ? undefined : courseId; + this.courses.setItems(courses); + } + +} + +/** + * Helper class to manage courses. + */ +class CoreGradesCoursesManager extends CorePageItemsListManager { + + /** + * @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 { + await CoreGrades.instance.logCoursesGradesView(); } } diff --git a/src/core/features/grades/pages/grade/grade.ts b/src/core/features/grades/pages/grade/grade.ts index 3d41e0b7f..efec61258 100644 --- a/src/core/features/grades/pages/grade/grade.ts +++ b/src/core/features/grades/pages/grade/grade.ts @@ -38,9 +38,9 @@ export class CoreGradesGradePage implements OnInit { gradeLoaded = false; constructor(route: ActivatedRoute) { - this.courseId = route.snapshot.params.courseId ?? route.snapshot.parent?.params.courseId; - this.gradeId = route.snapshot.params.gradeId; - this.userId = route.snapshot.queryParams.userId ?? CoreSites.instance.getCurrentSiteUserId(); + this.courseId = parseInt(route.snapshot.params.courseId ?? route.snapshot.parent?.params.courseId); + this.gradeId = parseInt(route.snapshot.params.gradeId); + this.userId = parseInt(route.snapshot.queryParams.userId ?? CoreSites.instance.getCurrentSiteUserId()); } /** diff --git a/src/core/features/grades/services/grades-helper.ts b/src/core/features/grades/services/grades-helper.ts index 5fa221650..8a27e791a 100644 --- a/src/core/features/grades/services/grades-helper.ts +++ b/src/core/features/grades/services/grades-helper.ts @@ -144,9 +144,9 @@ export class CoreGradesHelperProvider { */ formatGradesTable(table: CoreGradesTable): CoreGradesFormattedTable { const maxDepth = table.maxdepth; - const formatted: CoreGradesFormattedTable = { - columns: [], - rows: [], + const formatted = { + columns: [] as any[], + rows: [] as any[], }; // Columns, in order. @@ -673,9 +673,21 @@ export class CoreGradesHelper extends makeSingleton(CoreGradesHelperProvider) {} export type CoreGradesFormattedRow = any; export type CoreGradesFormattedRowForTable = any; export type CoreGradesFormattedItem = any; +export type CoreGradesFormattedTableColumn = any; +export type CoreGradesFormattedTableRow = CoreGradesFormattedTableRowFilled | CoreGradesFormattedTableRowEmpty; export type CoreGradesFormattedTable = { - columns: any[]; - rows: any[]; + columns: CoreGradesFormattedTableColumn[]; + rows: CoreGradesFormattedTableRow[]; +}; +export type CoreGradesFormattedTableRowFilled = { + // @todo complete types. + id: number; + itemtype: 'category' | 'leader'; + grade: unknown; + percentage: unknown; +}; +type CoreGradesFormattedTableRowEmpty ={ + // }; /** From dc1adaa95f34a939ca6cbcaa734656c55cb0c955 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 4 Feb 2021 19:57:06 +0100 Subject: [PATCH 3/3] Extract list items management from badges --- src/addons/badges/badges-lazy.module.ts | 53 +++++-- .../pages/issued-badge/issued-badge.module.ts | 44 ------ .../{issued-badge.page.ts => issued-badge.ts} | 0 .../badges/pages/user-badges/user-badges.html | 12 +- .../pages/user-badges/user-badges.module.ts | 63 -------- .../pages/user-badges/user-badges.page.ts | 106 ------------- .../badges/pages/user-badges/user-badges.ts | 142 ++++++++++++++++++ .../badges/services/handlers/badge-link.ts | 8 +- .../badges/services/handlers/mybadges-link.ts | 2 +- .../badges/services/handlers/push-click.ts | 5 +- src/addons/badges/services/handlers/user.ts | 8 +- 11 files changed, 198 insertions(+), 245 deletions(-) delete mode 100644 src/addons/badges/pages/issued-badge/issued-badge.module.ts rename src/addons/badges/pages/issued-badge/{issued-badge.page.ts => issued-badge.ts} (100%) delete mode 100644 src/addons/badges/pages/user-badges/user-badges.module.ts delete mode 100644 src/addons/badges/pages/user-badges/user-badges.page.ts create mode 100644 src/addons/badges/pages/user-badges/user-badges.ts diff --git a/src/addons/badges/badges-lazy.module.ts b/src/addons/badges/badges-lazy.module.ts index f88a84958..2245f6fdb 100644 --- a/src/addons/badges/badges-lazy.module.ts +++ b/src/addons/badges/badges-lazy.module.ts @@ -12,29 +12,60 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CommonModule } from '@angular/common'; +import { IonicModule } from '@ionic/angular'; import { NgModule } from '@angular/core'; -import { Route, RouterModule, Routes } from '@angular/router'; +import { RouterModule, Routes } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { conditionalRoutes } from '@/app/app-routing.module'; +import { CoreScreen } from '@services/screen'; +import { CoreSharedModule } from '@/core/shared.module'; -export const AddonBadgesIssueRoute: Route = { - path: 'issue', - loadChildren: () => import('./pages/issued-badge/issued-badge.module').then( m => m.AddonBadgesIssuedBadgePageModule), -}; +import { AddonBadgesIssuedBadgePage } from './pages/issued-badge/issued-badge'; +import { AddonBadgesUserBadgesPage } from './pages/user-badges/user-badges'; -const routes: Routes = [ +const mobileRoutes: Routes = [ { path: '', - redirectTo: 'user', pathMatch: 'full', + component: AddonBadgesUserBadgesPage, }, - AddonBadgesIssueRoute, { - path: 'user', - loadChildren: () => import('./pages/user-badges/user-badges.module').then( m => m.AddonBadgesUserBadgesPageModule), + path: ':badgeHash', + component: AddonBadgesIssuedBadgePage, }, ]; +const tabletRoutes: Routes = [ + { + path: '', + component: AddonBadgesUserBadgesPage, + children: [ + { + path: ':badgeHash', + component: AddonBadgesIssuedBadgePage, + }, + ], + }, +]; + +const routes: Routes = [ + ...conditionalRoutes(mobileRoutes, () => CoreScreen.instance.isMobile), + ...conditionalRoutes(tabletRoutes, () => CoreScreen.instance.isTablet), +]; + @NgModule({ - imports: [RouterModule.forChild(routes)], + imports: [ + RouterModule.forChild(routes), + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreSharedModule, + ], + declarations: [ + AddonBadgesUserBadgesPage, + AddonBadgesIssuedBadgePage, + ], }) export class AddonBadgesLazyModule {} diff --git a/src/addons/badges/pages/issued-badge/issued-badge.module.ts b/src/addons/badges/pages/issued-badge/issued-badge.module.ts deleted file mode 100644 index dcf0c421c..000000000 --- a/src/addons/badges/pages/issued-badge/issued-badge.module.ts +++ /dev/null @@ -1,44 +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 { IonicModule } from '@ionic/angular'; -import { TranslateModule } from '@ngx-translate/core'; -import { RouterModule, Routes } from '@angular/router'; -import { CommonModule } from '@angular/common'; - -import { CoreSharedModule } from '@/core/shared.module'; -import { AddonBadgesIssuedBadgePage } from './issued-badge.page'; - -const routes: Routes = [ - { - path: '', - component: AddonBadgesIssuedBadgePage, - }, -]; - -@NgModule({ - imports: [ - RouterModule.forChild(routes), - CommonModule, - IonicModule, - TranslateModule.forChild(), - CoreSharedModule, - ], - declarations: [ - AddonBadgesIssuedBadgePage, - ], - exports: [RouterModule], -}) -export class AddonBadgesIssuedBadgePageModule {} diff --git a/src/addons/badges/pages/issued-badge/issued-badge.page.ts b/src/addons/badges/pages/issued-badge/issued-badge.ts similarity index 100% rename from src/addons/badges/pages/issued-badge/issued-badge.page.ts rename to src/addons/badges/pages/issued-badge/issued-badge.ts diff --git a/src/addons/badges/pages/user-badges/user-badges.html b/src/addons/badges/pages/user-badges/user-badges.html index 684182843..509496363 100644 --- a/src/addons/badges/pages/user-badges/user-badges.html +++ b/src/addons/badges/pages/user-badges/user-badges.html @@ -8,17 +8,17 @@ - + - - + - - + + diff --git a/src/addons/badges/pages/user-badges/user-badges.module.ts b/src/addons/badges/pages/user-badges/user-badges.module.ts deleted file mode 100644 index 10d3a6e86..000000000 --- a/src/addons/badges/pages/user-badges/user-badges.module.ts +++ /dev/null @@ -1,63 +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 { IonicModule } from '@ionic/angular'; -import { TranslateModule } from '@ngx-translate/core'; -import { RouterModule, Routes } from '@angular/router'; -import { CommonModule } from '@angular/common'; -import { conditionalRoutes } from '@/app/app-routing.module'; -import { CoreScreen } from '@services/screen'; - -import { CoreSharedModule } from '@/core/shared.module'; -import { AddonBadgesUserBadgesPage } from './user-badges.page'; -import { AddonBadgesIssueRoute } from '@addons/badges/badges-lazy.module'; - -const mobileRoutes: Routes = [ - { - path: '', - component: AddonBadgesUserBadgesPage, - }, - AddonBadgesIssueRoute, -]; - -const tabletRoutes: Routes = [ - { - path: '', - component: AddonBadgesUserBadgesPage, - children: [ - AddonBadgesIssueRoute, - ], - }, -]; - -const routes: Routes = [ - ...conditionalRoutes(mobileRoutes, () => CoreScreen.instance.isMobile), - ...conditionalRoutes(tabletRoutes, () => CoreScreen.instance.isTablet), -]; - -@NgModule({ - imports: [ - RouterModule.forChild(routes), - CommonModule, - IonicModule, - TranslateModule.forChild(), - CoreSharedModule, - ], - declarations: [ - AddonBadgesUserBadgesPage, - ], - exports: [RouterModule], -}) -export class AddonBadgesUserBadgesPageModule {} diff --git a/src/addons/badges/pages/user-badges/user-badges.page.ts b/src/addons/badges/pages/user-badges/user-badges.page.ts deleted file mode 100644 index 75087bfc4..000000000 --- a/src/addons/badges/pages/user-badges/user-badges.page.ts +++ /dev/null @@ -1,106 +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 { AddonBadges, AddonBadgesUserBadge } from '../../services/badges'; -import { CoreTimeUtils } from '@services/utils/time'; -import { CoreDomUtils } from '@services/utils/dom'; -import { CoreSites } from '@services/sites'; -import { CoreUtils } from '@services/utils/utils'; -import { CoreNavigator } from '@services/navigator'; -import { CoreScreen } from '@services/screen'; - -/** - * Page that displays the list of calendar events. - */ -@Component({ - selector: 'page-addon-badges-user-badges', - templateUrl: 'user-badges.html', -}) -export class AddonBadgesUserBadgesPage implements OnInit { - - courseId = 0; - userId!: number; - - badgesLoaded = false; - badges: AddonBadgesUserBadge[] = []; - currentTime = 0; - badgeHash!: string; - - /** - * View loaded. - */ - ngOnInit(): void { - - this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId') || this.courseId; // Use 0 for site badges. - this.userId = CoreNavigator.instance.getRouteNumberParam('userId') || CoreSites.instance.getCurrentSite()!.getUserId(); - - this.fetchBadges().finally(() => { - if (!this.badgeHash && CoreScreen.instance.isTablet && this.badges.length > 0) { - // Take first and load it. - this.loadIssuedBadge(this.badges[0].uniquehash); - } - this.badgesLoaded = true; - }); - } - - /** - * Fetch all the badges required for the view. - * - * @return Promise resolved when done. - */ - async fetchBadges(): Promise { - this.currentTime = CoreTimeUtils.instance.timestamp(); - - try { - this.badges = await AddonBadges.instance.getUserBadges(this.courseId, this.userId); - } catch (message) { - CoreDomUtils.instance.showErrorModalDefault(message, 'Error getting badges data.'); - } - } - - /** - * Refresh the badges. - * - * @param refresher Refresher. - */ - async refreshBadges(refresher?: CustomEvent): Promise { - await CoreUtils.instance.ignoreErrors(Promise.all([ - AddonBadges.instance.invalidateUserBadges(this.courseId, this.userId), - ])); - - await CoreUtils.instance.ignoreErrors(Promise.all([ - this.fetchBadges(), - ])); - - refresher?.detail.complete(); - } - - /** - * Navigate to a particular badge. - * - * @param badgeHash Badge to load. - */ - loadIssuedBadge(badgeHash: string): void { - this.badgeHash = badgeHash; - const params = { courseId: this.courseId, userId: this.userId, badgeHash: badgeHash }; - - const splitViewLoaded = CoreNavigator.instance.isCurrentPathInTablet('**/badges/user/issue'); - const path = (splitViewLoaded ? '../' : '') + 'issue'; - - CoreNavigator.instance.navigate(path, { params }); - } - -} diff --git a/src/addons/badges/pages/user-badges/user-badges.ts b/src/addons/badges/pages/user-badges/user-badges.ts new file mode 100644 index 000000000..7144fec28 --- /dev/null +++ b/src/addons/badges/pages/user-badges/user-badges.ts @@ -0,0 +1,142 @@ +// (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 { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; +import { IonRefresher } from '@ionic/angular'; +import { AddonBadges, AddonBadgesUserBadge } from '../../services/badges'; +import { CoreTimeUtils } from '@services/utils/time'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CorePageItemsListManager } from '@classes/page-items-list-manager'; +import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreObject } from '@singletons/object'; + +/** + * Page that displays the list of calendar events. + */ +@Component({ + selector: 'page-addon-badges-user-badges', + templateUrl: 'user-badges.html', +}) +export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy { + + currentTime = 0; + badges: AddonBadgesUserBadgesManager; + + @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; + + constructor(route: ActivatedRoute) { + const courseId = parseInt(route.snapshot.queryParams.courseId ?? 0); // Use 0 for site badges. + const userId = parseInt(route.snapshot.queryParams.userId ?? CoreSites.instance.getCurrentSiteUserId()); + + this.badges = new AddonBadgesUserBadgesManager(AddonBadgesUserBadgesPage, courseId, userId); + } + + /** + * @inheritdoc + */ + async ngAfterViewInit(): Promise { + await this.fetchInitialBadges(); + + this.badges.watchSplitViewOutlet(this.splitView); + this.badges.start(); + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.badges.destroy(); + } + + /** + * Refresh the badges. + * + * @param refresher Refresher. + */ + async refreshBadges(refresher?: IonRefresher): Promise { + await CoreUtils.instance.ignoreErrors(AddonBadges.instance.invalidateUserBadges(this.badges.courseId, this.badges.userId)); + await CoreUtils.instance.ignoreErrors(this.fetchBadges()); + + refresher?.complete(); + } + + /** + * Obtain the initial list of badges. + */ + private async fetchInitialBadges(): Promise { + this.currentTime = CoreTimeUtils.instance.timestamp(); + + try { + await this.fetchBadges(); + } catch (message) { + CoreDomUtils.instance.showErrorModalDefault(message, 'Error loading badges'); + + this.badges.setItems([]); + } + } + + /** + * Update the list of badges. + */ + private async fetchBadges(): Promise { + const badges = await AddonBadges.instance.getUserBadges(this.badges.courseId, this.badges.userId); + + this.badges.setItems(badges); + } + +} + +/** + * Helper class to manage badges. + */ +class AddonBadgesUserBadgesManager extends CorePageItemsListManager { + + courseId: number; + userId: number; + + constructor(pageComponent: unknown, courseId: number, userId: number) { + super(pageComponent); + + this.courseId = courseId; + this.userId = userId; + } + + /** + * @inheritdoc + */ + protected getItemPath(badge: AddonBadgesUserBadge): string { + return badge.uniquehash; + } + + /** + * @inheritdoc + */ + protected getItemQueryParams(): Params { + return CoreObject.withoutEmpty({ + courseId: this.courseId, + userId: this.userId, + }); + } + + /** + * @inheritdoc + */ + protected getSelectedItemPath(route: ActivatedRouteSnapshot): string | null { + return route.params.badgeHash ?? null; + } + +} diff --git a/src/addons/badges/services/handlers/badge-link.ts b/src/addons/badges/services/handlers/badge-link.ts index 617bddc50..7d2ffb80f 100644 --- a/src/addons/badges/services/handlers/badge-link.ts +++ b/src/addons/badges/services/handlers/badge-link.ts @@ -43,13 +43,7 @@ export class AddonBadgesBadgeLinkHandlerService extends CoreContentLinksHandlerB return [{ action: (siteId: string): void => { - CoreNavigator.instance.navigateToSitePath( - '/badges/issue', - { - siteId, - params: { courseId: 0, badgeHash: params.hash }, - }, - ); + CoreNavigator.instance.navigateToSitePath(`/badges/${params.hash}`, { siteId }); }, }]; } diff --git a/src/addons/badges/services/handlers/mybadges-link.ts b/src/addons/badges/services/handlers/mybadges-link.ts index 519b8990a..aab4b9a01 100644 --- a/src/addons/badges/services/handlers/mybadges-link.ts +++ b/src/addons/badges/services/handlers/mybadges-link.ts @@ -37,7 +37,7 @@ export class AddonBadgesMyBadgesLinkHandlerService extends CoreContentLinksHandl getActions(): CoreContentLinksAction[] { return [{ action: (siteId: string): void => { - CoreNavigator.instance.navigateToSitePath('/badges/user', { siteId }); + CoreNavigator.instance.navigateToSitePath('/badges', { siteId }); }, }]; } diff --git a/src/addons/badges/services/handlers/push-click.ts b/src/addons/badges/services/handlers/push-click.ts index e3a834f3c..28e829f64 100644 --- a/src/addons/badges/services/handlers/push-click.ts +++ b/src/addons/badges/services/handlers/push-click.ts @@ -59,9 +59,8 @@ export class AddonBadgesPushClickHandlerService implements CorePushNotifications if (data.hash) { // We have the hash, open the badge directly. - await CoreNavigator.instance.navigateToSitePath('/badges/issue', { + await CoreNavigator.instance.navigateToSitePath(`/badges/${data.hash}`, { siteId: notification.site, - params: { courseId: 0, badgeHash: data.hash }, }); return; @@ -76,7 +75,7 @@ export class AddonBadgesPushClickHandlerService implements CorePushNotifications ), ); - await CoreNavigator.instance.navigateToSitePath('/badges/user', { siteId: notification.site }); + await CoreNavigator.instance.navigateToSitePath('/badges', { siteId: notification.site }); } } diff --git a/src/addons/badges/services/handlers/user.ts b/src/addons/badges/services/handlers/user.ts index 0aed8ef9c..0c422af21 100644 --- a/src/addons/badges/services/handlers/user.ts +++ b/src/addons/badges/services/handlers/user.ts @@ -18,6 +18,7 @@ import { CoreUserProfile } from '@features/user/services/user'; import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; import { CoreNavigator } from '@services/navigator'; import { makeSingleton } from '@singletons'; +import { CoreObject } from '@singletons/object'; import { AddonBadges } from '../badges'; /** @@ -72,10 +73,9 @@ export class AddonBadgesUserHandlerService implements CoreUserProfileHandler { action: (event, user, courseId): void => { event.preventDefault(); event.stopPropagation(); - CoreNavigator.instance.navigateToSitePath( - '/badges/user', - { params: { courseId: courseId || 0, userId: user.id } }, - ); + CoreNavigator.instance.navigateToSitePath('/badges', { + params: CoreObject.withoutEmpty({ courseId, userId: user.id }), + }); }, }; }