From d2b716da8d8730af79f5a9f2dbab02098b1d3ac6 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 23 Dec 2021 17:10:17 +0100 Subject: [PATCH] MOBILE-3934 competency: Add swipe navigation --- .../competency-course-competencies-source.ts | 97 +++++++ .../competency-plan-competencies-source.ts | 88 ++++++ .../classes/competency-plans-source.ts | 97 +++++++ .../pages/competency/competency.html | 9 +- .../pages/competency/competency.page.ts | 260 +++++++++++++----- .../coursecompetencies.html | 37 ++- .../coursecompetencies.page.ts | 108 +++++--- src/addons/competency/pages/plan/plan.html | 11 +- src/addons/competency/pages/plan/plan.ts | 93 ++++--- .../competency/pages/planlist/planlist.ts | 84 +----- .../items-management/list-items-manager.ts | 8 +- 11 files changed, 639 insertions(+), 253 deletions(-) create mode 100644 src/addons/competency/classes/competency-course-competencies-source.ts create mode 100644 src/addons/competency/classes/competency-plan-competencies-source.ts create mode 100644 src/addons/competency/classes/competency-plans-source.ts diff --git a/src/addons/competency/classes/competency-course-competencies-source.ts b/src/addons/competency/classes/competency-course-competencies-source.ts new file mode 100644 index 000000000..363514900 --- /dev/null +++ b/src/addons/competency/classes/competency-course-competencies-source.ts @@ -0,0 +1,97 @@ +// (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 { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source'; +import { CoreUserProfile } from '@features/user/services/user'; +import { CoreUtils } from '@services/utils/utils'; +import { + AddonCompetency, + AddonCompetencyDataForCourseCompetenciesPageCompetency, + AddonCompetencyDataForCourseCompetenciesPageWSResponse, +} from '../services/competency'; +import { AddonCompetencyHelper } from '../services/competency-helper'; + +/** + * Provides a collection of course competencies. + */ +export class AddonCompetencyCourseCompetenciesSource + extends CoreRoutedItemsManagerSource { + + /** + * @inheritdoc + */ + static getSourceId(courseId: number, userId?: number): string { + return `${courseId}-${userId || 'current-user'}`; + } + + readonly COURSE_ID: number; + readonly USER_ID?: number; + + courseCompetencies?: AddonCompetencyDataForCourseCompetenciesPageWSResponse; + user?: CoreUserProfile; + + constructor(courseId: number, userId?: number) { + super(); + + this.COURSE_ID = courseId; + this.USER_ID = userId; + } + + /** + * @inheritdoc + */ + getItemPath(competency: AddonCompetencyDataForCourseCompetenciesPageCompetency): string { + return String(competency.competency.id); + } + + /** + * @inheritdoc + */ + async load(): Promise { + if (this.dirty || !this.courseCompetencies) { + await this.loadCourseCompetencies(); + } + + await super.load(); + } + + /** + * Invalidate course cache. + */ + async invalidateCache(): Promise { + await CoreUtils.ignoreErrors(AddonCompetency.invalidateCourseCompetencies(this.COURSE_ID, this.USER_ID)); + } + + /** + * @inheritdoc + */ + protected async loadPageItems(): Promise<{ items: AddonCompetencyDataForCourseCompetenciesPageCompetency[] }> { + if (!this.courseCompetencies) { + throw new Error('Can\'t load competencies without course data'); + } + + return { items: this.courseCompetencies.competencies }; + } + + /** + * Load competencies. + */ + private async loadCourseCompetencies(): Promise { + [this.courseCompetencies, this.user] = await Promise.all([ + AddonCompetency.getCourseCompetencies(this.COURSE_ID, this.USER_ID), + AddonCompetencyHelper.getProfile(this.USER_ID), + ]); + } + +} diff --git a/src/addons/competency/classes/competency-plan-competencies-source.ts b/src/addons/competency/classes/competency-plan-competencies-source.ts new file mode 100644 index 000000000..a674f4c1e --- /dev/null +++ b/src/addons/competency/classes/competency-plan-competencies-source.ts @@ -0,0 +1,88 @@ +// (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 { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source'; +import { CoreUserProfile } from '@features/user/services/user'; +import { CoreUtils } from '@services/utils/utils'; +import { + AddonCompetency, + AddonCompetencyDataForPlanPageCompetency, + AddonCompetencyDataForPlanPageWSResponse, +} from '../services/competency'; +import { AddonCompetencyHelper } from '../services/competency-helper'; + +/** + * Provides a collection of plan competencies. + */ +export class AddonCompetencyPlanCompetenciesSource extends CoreRoutedItemsManagerSource { + + readonly PLAN_ID: number; + + plan?: AddonCompetencyDataForPlanPageWSResponse; + user?: CoreUserProfile; + + constructor(planId: number) { + super(); + + this.PLAN_ID = planId; + } + + /** + * @inheritdoc + */ + getItemPath(competency: AddonCompetencyDataForPlanPageCompetency): string { + return String(competency.competency.id); + } + + /** + * @inheritdoc + */ + async load(): Promise { + if (this.dirty || !this.plan) { + await this.loadLearningPlan(); + } + + await super.load(); + } + + /** + * Invalidate plan cache. + */ + async invalidateCache(): Promise { + await CoreUtils.ignoreErrors(AddonCompetency.invalidateLearningPlan(this.PLAN_ID)); + } + + /** + * @inheritdoc + */ + protected async loadPageItems(): Promise<{ items: AddonCompetencyDataForPlanPageCompetency[] }> { + if (!this.plan) { + throw new Error('Can\'t load competencies without plan!'); + } + + return { items: this.plan.competencies }; + } + + /** + * Load learning plan. + */ + private async loadLearningPlan(): Promise { + this.plan = await AddonCompetency.getLearningPlan(this.PLAN_ID); + this.plan.plan.statusname = AddonCompetencyHelper.getPlanStatusName(this.plan.plan.status); + + // Get the user profile image. + this.user = await AddonCompetencyHelper.getProfile(this.plan.plan.userid); + } + +} diff --git a/src/addons/competency/classes/competency-plans-source.ts b/src/addons/competency/classes/competency-plans-source.ts new file mode 100644 index 000000000..471db5ba5 --- /dev/null +++ b/src/addons/competency/classes/competency-plans-source.ts @@ -0,0 +1,97 @@ +// (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 { Params } from '@angular/router'; +import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source'; +import { ADDON_COMPETENCY_COMPETENCIES_PAGE } from '../competency.module'; +import { AddonCompetency, AddonCompetencyPlan, AddonCompetencyProvider } from '../services/competency'; +import { AddonCompetencyHelper } from '../services/competency-helper'; + +/** + * Provides a collection of learning plans. + */ +export class AddonCompetencyPlansSource extends CoreRoutedItemsManagerSource { + + /** + * @inheritdoc + */ + static getSourceId(userId?: number): string { + return userId ? String(userId) : 'current-user'; + } + + readonly USER_ID?: number; + + constructor(userId?: number) { + super(); + + this.USER_ID = userId; + } + + /** + * @inheritdoc + */ + getItemPath(plan: AddonCompetencyPlanFormatted): string { + return `${plan.id}/${ADDON_COMPETENCY_COMPETENCIES_PAGE}`; + } + + /** + * @inheritdoc + */ + getItemQueryParams(): Params { + if (this.USER_ID) { + return { userId: this.USER_ID }; + } + + return {}; + } + + /** + * Invalidate learning plans cache. + */ + async invalidateCache(): Promise { + await AddonCompetency.invalidateLearningPlans(this.USER_ID); + } + + /** + * @inheritdoc + */ + protected async loadPageItems(): Promise<{ items: AddonCompetencyPlanFormatted[] }> { + const plans = await AddonCompetency.getLearningPlans(this.USER_ID); + + plans.forEach((plan: AddonCompetencyPlanFormatted) => { + plan.statusname = AddonCompetencyHelper.getPlanStatusName(plan.status); + switch (plan.status) { + case AddonCompetencyProvider.STATUS_ACTIVE: + plan.statuscolor = 'success'; + break; + case AddonCompetencyProvider.STATUS_COMPLETE: + plan.statuscolor = 'danger'; + break; + default: + plan.statuscolor = 'warning'; + break; + } + }); + + return { items: plans }; + } + +} + +/** + * Competency plan with some calculated data. + */ +export type AddonCompetencyPlanFormatted = AddonCompetencyPlan & { + statuscolor?: string; // Calculated in the app. Color of the plan's status. +}; diff --git a/src/addons/competency/pages/competency/competency.html b/src/addons/competency/pages/competency/competency.html index d70e9e806..a73a97e2c 100644 --- a/src/addons/competency/pages/competency/competency.html +++ b/src/addons/competency/pages/competency/competency.html @@ -10,7 +10,7 @@ - + @@ -36,9 +36,7 @@

{{ 'addon.competency.path' | translate }}

- + {{ competency.competency.comppath.framework.name }} @@ -79,7 +77,8 @@

- + + diff --git a/src/addons/competency/pages/competency/competency.page.ts b/src/addons/competency/pages/competency/competency.page.ts index 94c08b897..e2c2a41a1 100644 --- a/src/addons/competency/pages/competency/competency.page.ts +++ b/src/addons/competency/pages/competency/competency.page.ts @@ -13,7 +13,7 @@ // limitations under the License. import { AddonCompetencyHelper } from '@addons/competency/services/competency-helper'; -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { CoreCourseModuleSummary } from '@features/course/services/course'; import { CoreUserSummary } from '@features/user/services/user'; import { CoreSites } from '@services/sites'; @@ -25,14 +25,19 @@ import { AddonCompetencyUserCompetency, AddonCompetencyUserCompetencyCourse, AddonCompetency, - AddonCompetencyDataForUserCompetencySummaryInPlanWSResponse, - AddonCompetencyDataForUserCompetencySummaryInCourseWSResponse, + AddonCompetencyDataForPlanPageCompetency, + AddonCompetencyDataForCourseCompetenciesPageCompetency, } from '@addons/competency/services/competency'; import { CoreNavigator } from '@services/navigator'; import { IonRefresher } from '@ionic/angular'; import { ContextLevel } from '@/core/constants'; import { CoreUtils } from '@services/utils/utils'; import { ADDON_COMPETENCY_SUMMARY_PAGE } from '@addons/competency/competency.module'; +import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; +import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { AddonCompetencyPlanCompetenciesSource } from '@addons/competency/classes/competency-plan-competencies-source'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source'; /** * Page that displays the competency information. @@ -41,13 +46,10 @@ import { ADDON_COMPETENCY_SUMMARY_PAGE } from '@addons/competency/competency.mod selector: 'page-addon-competency-competency', templateUrl: 'competency.html', }) -export class AddonCompetencyCompetencyPage implements OnInit { +export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy { competencyLoaded = false; - competencyId!: number; - planId?: number; - courseId?: number; - userId?: number; + competencies!: AddonCompetencyCompetenciesSwipeManager; planStatus?: number; coursemodules?: CoreCourseModuleSummary[]; user?: CoreUserSummary; @@ -56,17 +58,26 @@ export class AddonCompetencyCompetencyPage implements OnInit { contextLevel?: string; contextInstanceId?: number; - /** - * @inheritdoc - */ - async ngOnInit(): Promise { + constructor() { try { - this.competencyId = CoreNavigator.getRequiredRouteNumberParam('competencyId'); - this.planId = CoreNavigator.getRouteNumberParam('planId'); - if (!this.planId) { - this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); - this.userId = CoreNavigator.getRouteNumberParam('userId'); + const planId = CoreNavigator.getRouteNumberParam('planId'); + + if (!planId) { + const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); + const userId = CoreNavigator.getRouteNumberParam('userId'); + const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( + AddonCompetencyCourseCompetenciesSource, + [courseId, userId], + ); + + this.competencies = new AddonCompetencyCompetenciesSwipeManager(source); + + return; } + + const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(AddonCompetencyPlanCompetenciesSource, [planId]); + + this.competencies = new AddonCompetencyCompetenciesSwipeManager(source); } catch (error) { CoreDomUtils.showErrorModal(error); @@ -74,24 +85,63 @@ export class AddonCompetencyCompetencyPage implements OnInit { return; } + } + get competencyFrameworkUrl(): string | undefined { + if (!this.competency) { + return; + } + + const { pluginbaseurl, framework, pagecontextid } = this.competency.competency.comppath; + + return `${pluginbaseurl}/competencies.php?competencyframeworkid=${framework.id}&pagecontextid=${pagecontextid}`; + } + + get courseId(): number | undefined { + const source = this.competencies.getSource(); + + if (!(source instanceof AddonCompetencyCourseCompetenciesSource)) { + return; + } + + return source.COURSE_ID; + } + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { try { + const source = this.competencies.getSource(); + + await source.reload(); + await this.competencies.start(); await this.fetchCompetency(); - const name = this.competency && this.competency.competency && this.competency.competency.competency && - this.competency.competency.competency.shortname; + if (!this.competency) { + return; + } - if (this.planId) { - CoreUtils.ignoreErrors(AddonCompetency.logCompetencyInPlanView( - this.planId, - this.competencyId, - this.planStatus!, - name, - this.userId, - )); + const name = this.competency.competency.competency.shortname; + + if (source instanceof AddonCompetencyPlanCompetenciesSource) { + this.planStatus && await CoreUtils.ignoreErrors( + AddonCompetency.logCompetencyInPlanView( + source.PLAN_ID, + this.requireCompetencyId(), + this.planStatus, + name, + source.user?.id, + ), + ); } else { - CoreUtils.ignoreErrors( - AddonCompetency.logCompetencyInCourseView(this.courseId!, this.competencyId, name, this.userId), + await CoreUtils.ignoreErrors( + AddonCompetency.logCompetencyInCourseView( + source.COURSE_ID, + this.requireCompetencyId(), + name, + source.USER_ID, + ), ); } } finally { @@ -99,47 +149,25 @@ export class AddonCompetencyCompetencyPage implements OnInit { } } + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.competencies.destroy(); + } + /** * Fetches the competency and updates the view. * * @return Promise resolved when done. */ protected async fetchCompetency(): Promise { - try { - let competency: AddonCompetencyDataForUserCompetencySummaryInPlanWSResponse | - AddonCompetencyDataForUserCompetencySummaryInCourseWSResponse; + const source = this.competencies.getSource(); - if (this.planId) { - this.planStatus = undefined; - - competency = await AddonCompetency.getCompetencyInPlan(this.planId, this.competencyId); - } else if (this.courseId) { - competency = await AddonCompetency.getCompetencyInCourse(this.courseId, this.competencyId, this.userId); - } else { - throw null; - } - - // Calculate the context. - if (this.courseId) { - this.contextLevel = ContextLevel.COURSE; - this.contextInstanceId = this.courseId; - } else { - this.contextLevel = ContextLevel.USER; - this.contextInstanceId = this.userId || competency.usercompetencysummary.user.id; - } - - this.competency = competency.usercompetencysummary; - this.userCompetency = this.competency.usercompetencyplan || this.competency.usercompetency; - - if ('plan' in competency) { - this.planStatus = competency.plan.status; - this.competency.usercompetency!.statusname = - AddonCompetencyHelper.getCompetencyStatusName(this.competency.usercompetency!.status); - } else { - this.userCompetency = this.competency.usercompetencycourse; - this.coursemodules = competency.coursemodules; - } + this.competency = source instanceof AddonCompetencyPlanCompetenciesSource + ? await this.fetchCompetencySummaryFromPlan(source) + : await this.fetchCompetencySummaryFromCourse(source); if (this.competency.user.id != CoreSites.getCurrentSiteUserId()) { // Get the user profile from the returned object. @@ -163,18 +191,17 @@ export class AddonCompetencyCompetencyPage implements OnInit { * @param refresher Refresher. */ async refreshCompetency(refresher: IonRefresher): Promise { - try { - if (this.planId) { - await AddonCompetency.invalidateCompetencyInPlan(this.planId, this.competencyId); - } else { - await AddonCompetency.invalidateCompetencyInCourse(this.courseId!, this.competencyId); - } + const source = this.competencies.getSource(); - } finally { - this.fetchCompetency().finally(() => { - refresher?.complete(); - }); - } + await CoreUtils.ignoreErrors( + source instanceof AddonCompetencyPlanCompetenciesSource + ? AddonCompetency.invalidateCompetencyInPlan(source.PLAN_ID, this.requireCompetencyId()) + : AddonCompetency.invalidateCompetencyInCourse(source.COURSE_ID, this.requireCompetencyId(), source.USER_ID), + ); + + this.fetchCompetency().finally(() => { + refresher?.complete(); + }); } /** @@ -191,4 +218,91 @@ export class AddonCompetencyCompetencyPage implements OnInit { ); } + /** + * Get competency id or fail. + * + * @returns Competency id. + */ + private requireCompetencyId(): number { + const selectedItem = this.competencies.getSelectedItem(); + + if (!selectedItem) { + throw new Error('Failed to get competency id from selected item'); + } + + return selectedItem.competency.id; + } + + /** + * Fetch competency summary from a plan source. + * + * @param source Plan competencies source. + * @returns Competency summary. + */ + private async fetchCompetencySummaryFromPlan( + source: AddonCompetencyPlanCompetenciesSource, + ): Promise { + const competency = await AddonCompetency.getCompetencyInPlan( + source.PLAN_ID, + this.requireCompetencyId(), + ); + + this.planStatus = competency.plan.status; + + if (competency.usercompetencysummary.usercompetency) { + competency.usercompetencysummary.usercompetency.statusname = + AddonCompetencyHelper.getCompetencyStatusName(competency.usercompetencysummary.usercompetency.status); + } + + this.contextLevel = ContextLevel.USER; + this.contextInstanceId = source.user?.id || competency.usercompetencysummary.user.id; + this.userCompetency = competency.usercompetencysummary.usercompetencyplan + || competency.usercompetencysummary.usercompetency; + + return competency.usercompetencysummary; + } + + /** + * Fetch competency summary from a course source. + * + * @param source Course competencies source. + * @returns Competency summary. + */ + private async fetchCompetencySummaryFromCourse( + source: AddonCompetencyCourseCompetenciesSource, + ): Promise { + const competency = await AddonCompetency.getCompetencyInCourse( + source.COURSE_ID, + this.requireCompetencyId(), + source.USER_ID, + ); + + this.coursemodules = competency.coursemodules; + + this.contextLevel = ContextLevel.COURSE; + this.contextInstanceId = source.COURSE_ID; + this.userCompetency = competency.usercompetencysummary.usercompetencycourse + || competency.usercompetencysummary.usercompetency; + + return competency.usercompetencysummary; + } + +} + +/** + * Helper to manage swiping within a collection of competencies. + */ +class AddonCompetencyCompetenciesSwipeManager + extends CoreSwipeNavigationItemsManager< + AddonCompetencyDataForPlanPageCompetency | AddonCompetencyDataForCourseCompetenciesPageCompetency, + AddonCompetencyPlanCompetenciesSource | AddonCompetencyCourseCompetenciesSource + > { + + /** + * @inheritdoc + */ + protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { + return route.params.competencyId; + } + } diff --git a/src/addons/competency/pages/coursecompetencies/coursecompetencies.html b/src/addons/competency/pages/coursecompetencies/coursecompetencies.html index fa1cf08bc..d8f6cb615 100644 --- a/src/addons/competency/pages/coursecompetencies/coursecompetencies.html +++ b/src/addons/competency/pages/coursecompetencies/coursecompetencies.html @@ -9,35 +9,35 @@ - + - - - - + + + + {{ 'addon.competency.coursecompetencyratingsarepushedtouserplans' | translate }} - + {{ 'addon.competency.coursecompetencyratingsarenotpushedtouserplans' | translate }} - + {{ 'addon.competency.xcompetenciesproficientoutofyincourse' | translate: {$a: - {x: competencies.statistics.proficientcompetencycount, y: competencies.statistics.competencycount} } }} + {x: courseCompetencies.statistics.proficientcompetencycount, y: courseCompetencies.statistics.competencycount} } }} - + *ngIf="courseCompetencies.statistics.canmanagecoursecompetencies && courseCompetencies.statistics.leastproficientcount > 0">

{{ 'addon.competency.competenciesmostoftennotproficientincourse' | translate }}

-

+

@@ -46,7 +46,7 @@ -

+

{{ 'addon.competency.coursecompetencies' | translate }}

@@ -57,13 +57,13 @@
- -
- - + +

@@ -85,8 +85,7 @@

{{ 'addon.competency.path' | translate }}

- {{ competency.comppath.framework.name }} @@ -104,7 +103,7 @@

-
+

{{ 'addon.competency.uponcoursecompletion' | translate }}

{{ ruleoutcome.text }} diff --git a/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts b/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts index 4ad29a386..79924d2a1 100644 --- a/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts +++ b/src/addons/competency/pages/coursecompetencies/coursecompetencies.page.ts @@ -12,15 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit } from '@angular/core'; -import { AddonCompetencyDataForCourseCompetenciesPageWSResponse, AddonCompetency } from '@addons/competency/services/competency'; -import { AddonCompetencyHelper } from '@addons/competency/services/competency-helper'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { + AddonCompetencyDataForCourseCompetenciesPageWSResponse, + AddonCompetencyDataForCourseCompetenciesPageCompetency, +} from '@addons/competency/services/competency'; import { CoreUserProfile } from '@features/user/services/user'; import { IonRefresher } from '@ionic/angular'; import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; import { ContextLevel } from '@/core/constants'; import { ADDON_COMPETENCY_SUMMARY_PAGE } from '@addons/competency/competency.module'; +import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; +import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source'; /** * Page that displays the list of competencies of a course. @@ -29,22 +34,23 @@ import { ADDON_COMPETENCY_SUMMARY_PAGE } from '@addons/competency/competency.mod selector: 'page-addon-competency-coursecompetencies', templateUrl: 'coursecompetencies.html', }) -export class AddonCompetencyCourseCompetenciesPage implements OnInit { +export class AddonCompetencyCourseCompetenciesPage implements OnInit, OnDestroy { - competenciesLoaded = false; - competencies?: AddonCompetencyDataForCourseCompetenciesPageWSResponse; - user?: CoreUserProfile; - courseId!: number; + competencies!: CoreListItemsManager< + AddonCompetencyDataForCourseCompetenciesPageCompetency, + AddonCompetencyCourseCompetenciesSource + >; - protected userId?: number; - - /** - * View loaded. - */ - ngOnInit(): void { + constructor() { try { - this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); - this.userId = CoreNavigator.getRouteNumberParam('userId'); + const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); + const userId = CoreNavigator.getRouteNumberParam('userId'); + const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( + AddonCompetencyCourseCompetenciesSource, + [courseId, userId], + ); + + this.competencies = new CoreListItemsManager(source, AddonCompetencyCourseCompetenciesPage); } catch (error) { CoreDomUtils.showErrorModal(error); @@ -52,10 +58,50 @@ export class AddonCompetencyCourseCompetenciesPage implements OnInit { return; } + } - this.fetchCourseCompetencies().finally(() => { - this.competenciesLoaded = true; - }); + get courseCompetencies(): AddonCompetencyDataForCourseCompetenciesPageWSResponse | undefined { + return this.competencies.getSource().courseCompetencies; + } + + get courseId(): number { + return this.competencies.getSource().COURSE_ID; + } + + get user(): CoreUserProfile | undefined { + return this.competencies.getSource().user; + } + + get showLeastProficientCompetencies(): boolean { + return !!this.courseCompetencies?.statistics.canmanagecoursecompetencies + && this.courseCompetencies?.statistics.leastproficientcount > 0; + } + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + await this.fetchCourseCompetencies(); + await this.competencies.start(); + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.competencies.destroy(); + } + + /** + * Get competency framework url. + * + * @param competency Competency. + * @returns Competency framework url. + */ + getCompetencyFrameworkUrl(competency: AddonCompetencyDataForCourseCompetenciesPageCompetency): string { + const { pluginbaseurl, framework, pagecontextid } = competency.comppath; + + return `${pluginbaseurl}/competencies.php?competencyframeworkid=${framework.id}&pagecontextid=${pagecontextid}`; } /** @@ -65,24 +111,12 @@ export class AddonCompetencyCourseCompetenciesPage implements OnInit { */ protected async fetchCourseCompetencies(): Promise { try { - this.competencies = await AddonCompetency.getCourseCompetencies(this.courseId, this.userId); - - // Get the user profile image. - this.user = await AddonCompetencyHelper.getProfile(this.userId); + await this.competencies.getSource().reload(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting course competencies data.'); } } - /** - * Opens a competency. - * - * @param competencyId - */ - openCompetency(competencyId: number): void { - CoreNavigator.navigate('./' + competencyId); - } - /** * Opens the summary of a competency. * @@ -105,11 +139,11 @@ export class AddonCompetencyCourseCompetenciesPage implements OnInit { * * @param refresher Refresher. */ - refreshCourseCompetencies(refresher?: IonRefresher): void { - AddonCompetency.invalidateCourseCompetencies(this.courseId, this.userId).finally(() => { - this.fetchCourseCompetencies().finally(() => { - refresher?.complete(); - }); + async refreshCourseCompetencies(refresher?: IonRefresher): Promise { + await this.competencies.getSource().invalidateCache(); + + this.fetchCourseCompetencies().finally(() => { + refresher?.complete(); }); } diff --git a/src/addons/competency/pages/plan/plan.html b/src/addons/competency/pages/plan/plan.html index ff5035a62..4c55d843d 100644 --- a/src/addons/competency/pages/plan/plan.html +++ b/src/addons/competency/pages/plan/plan.html @@ -8,11 +8,11 @@ - - + + - + @@ -74,9 +74,8 @@

{{ 'addon.competency.nocompetencies' | translate }}

- +

{{competency.competency.shortname}} {{competency.competency.idnumber}}

diff --git a/src/addons/competency/pages/plan/plan.ts b/src/addons/competency/pages/plan/plan.ts index e154da006..ee1c9a11f 100644 --- a/src/addons/competency/pages/plan/plan.ts +++ b/src/addons/competency/pages/plan/plan.ts @@ -12,13 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { CoreDomUtils } from '@services/utils/dom'; -import { AddonCompetencyDataForPlanPageWSResponse, AddonCompetency } from '../../services/competency'; -import { AddonCompetencyHelper } from '../../services/competency-helper'; +import { AddonCompetencyDataForPlanPageCompetency, AddonCompetencyDataForPlanPageWSResponse } from '../../services/competency'; import { CoreNavigator } from '@services/navigator'; import { CoreUserProfile } from '@features/user/services/user'; import { IonRefresher } from '@ionic/angular'; +import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; +import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { AddonCompetencyPlansSource } from '@addons/competency/classes/competency-plans-source'; +import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; +import { AddonCompetencyPlanCompetenciesSource } from '@addons/competency/classes/competency-plan-competencies-source'; /** * Page that displays a learning plan. @@ -27,19 +31,26 @@ import { IonRefresher } from '@ionic/angular'; selector: 'page-addon-competency-plan', templateUrl: 'plan.html', }) -export class AddonCompetencyPlanPage implements OnInit { +export class AddonCompetencyPlanPage implements OnInit, OnDestroy { - protected planId!: number; - loaded = false; - plan?: AddonCompetencyDataForPlanPageWSResponse; - user?: CoreUserProfile; + plans!: CoreSwipeNavigationItemsManager; + competencies!: CoreListItemsManager; - /** - * @inheritdoc - */ - ngOnInit(): void { + constructor() { try { - this.planId = CoreNavigator.getRequiredRouteNumberParam('planId'); + const planId = CoreNavigator.getRequiredRouteNumberParam('planId'); + const userId = CoreNavigator.getRouteNumberParam('userId'); + const plansSource = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( + AddonCompetencyPlansSource, + [userId], + ); + const competenciesSource = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( + AddonCompetencyPlanCompetenciesSource, + [planId], + ); + + this.competencies = new CoreListItemsManager(competenciesSource, AddonCompetencyPlanPage); + this.plans = new CoreSwipeNavigationItemsManager(plansSource); } catch (error) { CoreDomUtils.showErrorModal(error); @@ -47,10 +58,31 @@ export class AddonCompetencyPlanPage implements OnInit { return; } + } - this.fetchLearningPlan().finally(() => { - this.loaded = true; - }); + get plan(): AddonCompetencyDataForPlanPageWSResponse | undefined { + return this.competencies.getSource().plan; + } + + get user(): CoreUserProfile | undefined { + return this.competencies.getSource().user; + } + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + await this.fetchLearningPlan(); + await this.plans.start(); + await this.competencies.start(); + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.plans.destroy(); + this.competencies.destroy(); } /** @@ -60,39 +92,22 @@ export class AddonCompetencyPlanPage implements OnInit { */ protected async fetchLearningPlan(): Promise { try { - const plan = await AddonCompetency.getLearningPlan(this.planId); - plan.plan.statusname = AddonCompetencyHelper.getPlanStatusName(plan.plan.status); - - // Get the user profile image. - this.user = await AddonCompetencyHelper.getProfile(plan.plan.userid); - - this.plan = plan; + await this.competencies.getSource().reload(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting learning plan data.'); } } - /** - * Navigates to a particular competency. - * - * @param competencyId - */ - openCompetency(competencyId: number): void { - CoreNavigator.navigate('./' + competencyId, { - params: { userId: this.user?.id }, - }); - } - /** * Refreshes the learning plan. * * @param refresher Refresher. */ - refreshLearningPlan(refresher: IonRefresher): void { - AddonCompetency.invalidateLearningPlan(this.planId).finally(() => { - this.fetchLearningPlan().finally(() => { - refresher?.complete(); - }); + async refreshLearningPlan(refresher: IonRefresher): Promise { + await this.competencies.getSource().invalidateCache(); + + this.fetchLearningPlan().finally(() => { + refresher?.complete(); }); } diff --git a/src/addons/competency/pages/planlist/planlist.ts b/src/addons/competency/pages/planlist/planlist.ts index 59f878ca7..f5603431a 100644 --- a/src/addons/competency/pages/planlist/planlist.ts +++ b/src/addons/competency/pages/planlist/planlist.ts @@ -16,12 +16,10 @@ import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; import { IonRefresher } from '@ionic/angular'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; -import { AddonCompetencyProvider, AddonCompetencyPlan, AddonCompetency } from '../../services/competency'; -import { AddonCompetencyHelper } from '../../services/competency-helper'; import { CoreNavigator } from '@services/navigator'; -import { CorePageItemsListManager } from '@classes/page-items-list-manager'; -import { ADDON_COMPETENCY_COMPETENCIES_PAGE } from '@addons/competency/competency.module'; -import { Params } from '@angular/router'; +import { AddonCompetencyPlanFormatted, AddonCompetencyPlansSource } from '@addons/competency/classes/competency-plans-source'; +import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; /** * Page that displays the list of learning plans. @@ -34,13 +32,13 @@ export class AddonCompetencyPlanListPage implements AfterViewInit, OnDestroy { @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; - protected userId?: number; - plans: AddonCompetencyPlanListManager; + plans: CoreListItemsManager; constructor() { - this.userId = CoreNavigator.getRouteNumberParam('userId'); + const userId = CoreNavigator.getRouteNumberParam('userId'); + const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(AddonCompetencyPlansSource, [userId]); - this.plans = new AddonCompetencyPlanListManager(AddonCompetencyPlanListPage, this.userId); + this.plans = new CoreListItemsManager(source, AddonCompetencyPlanListPage); } /** @@ -59,23 +57,7 @@ export class AddonCompetencyPlanListPage implements AfterViewInit, OnDestroy { */ protected async fetchLearningPlans(): Promise { try { - const plans = await AddonCompetency.getLearningPlans(this.userId); - plans.forEach((plan: AddonCompetencyPlanFormatted) => { - plan.statusname = AddonCompetencyHelper.getPlanStatusName(plan.status); - switch (plan.status) { - case AddonCompetencyProvider.STATUS_ACTIVE: - plan.statuscolor = 'success'; - break; - case AddonCompetencyProvider.STATUS_COMPLETE: - plan.statuscolor = 'danger'; - break; - default: - plan.statuscolor = 'warning'; - break; - } - }); - this.plans.setItems(plans); - + await this.plans.load(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Error getting learning plans data.'); } @@ -86,11 +68,11 @@ export class AddonCompetencyPlanListPage implements AfterViewInit, OnDestroy { * * @param refresher Refresher. */ - refreshLearningPlans(refresher: IonRefresher): void { - AddonCompetency.invalidateLearningPlans(this.userId).finally(() => { - this.fetchLearningPlans().finally(() => { - refresher?.complete(); - }); + async refreshLearningPlans(refresher: IonRefresher): Promise { + await this.plans.getSource().invalidateCache(); + + this.fetchLearningPlans().finally(() => { + refresher?.complete(); }); } @@ -102,43 +84,3 @@ export class AddonCompetencyPlanListPage implements AfterViewInit, OnDestroy { } } - -/** - * Competency plan with some calculated data. - */ -type AddonCompetencyPlanFormatted = AddonCompetencyPlan & { - statuscolor?: string; // Calculated in the app. Color of the plan's status. -}; - -/** - * Helper class to manage plan list. - */ -class AddonCompetencyPlanListManager extends CorePageItemsListManager { - - private userId?: number; - - constructor(pageComponent: unknown, userId?: number) { - super(pageComponent); - - this.userId = userId; - } - - /** - * @inheritdoc - */ - protected getItemPath(plan: AddonCompetencyPlanFormatted): string { - return `${plan.id}/${ADDON_COMPETENCY_COMPETENCIES_PAGE}`; - } - - /** - * @inheritdoc - */ - protected getItemQueryParams(): Params { - if (this.userId) { - return { userId: this.userId }; - } - - return {}; - } - -} diff --git a/src/core/classes/items-management/list-items-manager.ts b/src/core/classes/items-management/list-items-manager.ts index 9229b1de4..174a93efe 100644 --- a/src/core/classes/items-management/list-items-manager.ts +++ b/src/core/classes/items-management/list-items-manager.ts @@ -64,8 +64,10 @@ export class CoreListItemsManager< * * @param splitView Split view component. */ - async start(splitView: CoreSplitViewComponent): Promise { - this.watchSplitViewOutlet(splitView); + async start(splitView?: CoreSplitViewComponent): Promise { + if (splitView) { + this.watchSplitViewOutlet(splitView); + } // Calculate current selected item. this.updateSelectedItem(); @@ -172,7 +174,7 @@ export class CoreListItemsManager< protected updateSelectedItem(route: ActivatedRouteSnapshot | null = null): void { super.updateSelectedItem(route); - if (CoreScreen.isMobile || this.selectedItem !== null || this.splitView?.isNested) { + if (CoreScreen.isMobile || this.selectedItem !== null || !this.splitView || this.splitView.isNested) { return; }