MOBILE-3934 competency: Add swipe navigation

main
Noel De Martin 2021-12-23 17:10:17 +01:00
parent c549e733fb
commit d2b716da8d
11 changed files with 639 additions and 253 deletions

View File

@ -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<AddonCompetencyDataForCourseCompetenciesPageCompetency> {
/**
* @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<void> {
if (this.dirty || !this.courseCompetencies) {
await this.loadCourseCompetencies();
}
await super.load();
}
/**
* Invalidate course cache.
*/
async invalidateCache(): Promise<void> {
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<void> {
[this.courseCompetencies, this.user] = await Promise.all([
AddonCompetency.getCourseCompetencies(this.COURSE_ID, this.USER_ID),
AddonCompetencyHelper.getProfile(this.USER_ID),
]);
}
}

View File

@ -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<AddonCompetencyDataForPlanPageCompetency> {
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<void> {
if (this.dirty || !this.plan) {
await this.loadLearningPlan();
}
await super.load();
}
/**
* Invalidate plan cache.
*/
async invalidateCache(): Promise<void> {
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<void> {
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);
}
}

View File

@ -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<AddonCompetencyPlanFormatted> {
/**
* @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<void> {
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.
};

View File

@ -10,7 +10,7 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content [core-swipe-navigation]="competencies">
<ion-refresher slot="fixed" [disabled]="!competencyLoaded" (ionRefresh)="refreshCompetency($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
@ -36,9 +36,7 @@
<ion-label>
<p class="item-heading">{{ 'addon.competency.path' | translate }}</p>
<p>
<a *ngIf="competency.competency.comppath.showlinks" [href]="competency.competency.comppath.pluginbaseurl + '/competencies.php?competencyframeworkid=' +
competency.competency.comppath.framework.id + '&pagecontextid=' +
competency.competency.comppath.pagecontextid" core-link>
<a *ngIf="competency.competency.comppath.showlinks" [href]="competencyFrameworkUrl" core-link>
{{ competency.competency.comppath.framework.name }}
</a>
<ng-container *ngIf="!competency.competency.comppath.showlinks">
@ -79,7 +77,8 @@
</p>
<ion-item class="ion-text-wrap" *ngFor="let activity of coursemodules" [href]="activity.url"
[attr.aria-label]="activity.name" core-link capture="true">
<core-mod-icon slot="start" [modicon]="activity.iconurl" [showAlt]="false" *ngIf="activity.iconurl"></core-mod-icon>
<core-mod-icon slot="start" [modicon]="activity.iconurl" [showAlt]="false" *ngIf="activity.iconurl">
</core-mod-icon>
<ion-label>
<core-format-text [text]="activity.name" contextLevel="module" [contextInstanceId]="activity.id"
[courseId]="courseId">

View File

@ -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<void> {
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<void> {
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<void> {
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<void> {
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<AddonCompetencyDataForUserCompetencySummaryWSResponse> {
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<AddonCompetencyDataForUserCompetencySummaryWSResponse> {
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;
}
}

View File

@ -9,35 +9,35 @@
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!competenciesLoaded" (ionRefresh)="refreshCourseCompetencies($event.target)">
<ion-refresher slot="fixed" [disabled]="!competencies.loaded" (ionRefresh)="refreshCourseCompetencies($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="competenciesLoaded">
<ion-card *ngIf="!user && competencies && competencies.statistics.competencycount > 0">
<ng-container *ngIf="competencies.cangradecompetencies">
<ion-item class="ion-text-wrap" *ngIf="competencies.settings.pushratingstouserplans">
<core-loading [hideUntil]="competencies.loaded">
<ion-card *ngIf="!user && courseCompetencies && courseCompetencies.statistics.competencycount > 0">
<ng-container *ngIf="courseCompetencies.cangradecompetencies">
<ion-item class="ion-text-wrap" *ngIf="courseCompetencies.settings.pushratingstouserplans">
<ion-label>{{ 'addon.competency.coursecompetencyratingsarepushedtouserplans' | translate }}</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="!competencies.settings.pushratingstouserplans" color="danger">
<ion-item class="ion-text-wrap" *ngIf="!courseCompetencies.settings.pushratingstouserplans" color="danger">
<ion-label>{{ 'addon.competency.coursecompetencyratingsarenotpushedtouserplans' | translate }}</ion-label>
</ion-item>
</ng-container>
<ion-item class="ion-text-wrap" *ngIf="competencies.statistics.canbegradedincourse">
<ion-item class="ion-text-wrap" *ngIf="courseCompetencies.statistics.canbegradedincourse">
<ion-label>
<span id="addon-competency-course-{{courseId}}-progress">
{{ 'addon.competency.xcompetenciesproficientoutofyincourse' | translate: {$a:
{x: competencies.statistics.proficientcompetencycount, y: competencies.statistics.competencycount} } }}
{x: courseCompetencies.statistics.proficientcompetencycount, y: courseCompetencies.statistics.competencycount} } }}
</span>
<core-progress-bar [progress]="competencies.statistics.proficientcompetencypercentage"
<core-progress-bar [progress]="courseCompetencies.statistics.proficientcompetencypercentage"
ariaDescribedBy="addon-competency-course-{{courseId}}-progress">
</core-progress-bar>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap"
*ngIf="competencies.statistics.canmanagecoursecompetencies && competencies.statistics.leastproficientcount > 0">
*ngIf="courseCompetencies.statistics.canmanagecoursecompetencies && courseCompetencies.statistics.leastproficientcount > 0">
<ion-label>
<p class="item-heading">{{ 'addon.competency.competenciesmostoftennotproficientincourse' | translate }}</p>
<p *ngFor="let comp of competencies.statistics.leastproficient">
<p *ngFor="let comp of courseCompetencies.statistics.leastproficient">
<button class="as-link" (click)="openCompetencySummary(comp.id)">
{{ comp.shortname }} - {{ comp.idnumber }}
</button>
@ -46,7 +46,7 @@
</ion-item>
</ion-card>
<h2 class="ion-margin-horizontal" *ngIf="competencies && competencies.statistics.competencycount > 0">
<h2 class="ion-margin-horizontal" *ngIf="courseCompetencies && courseCompetencies.statistics.competencycount > 0">
{{ 'addon.competency.coursecompetencies' | translate }}
</h2>
<ion-card *ngIf="user">
@ -57,13 +57,13 @@
</ion-label>
</ion-item>
</ion-card>
<core-empty-box *ngIf="competencies && competencies.statistics.competencycount == 0" icon="fas-award"
<core-empty-box *ngIf="courseCompetencies && courseCompetencies.statistics.competencycount == 0" icon="fas-award"
message="{{ 'addon.competency.nocompetenciesincourse' | translate }}">
</core-empty-box>
<div *ngIf="competencies">
<ion-card *ngFor="let competency of competencies.competencies">
<ion-item class="ion-text-wrap" (click)="openCompetency(competency.competency.id)"
<div *ngIf="competencies.loaded">
<ion-card *ngFor="let competency of competencies.items">
<ion-item class="ion-text-wrap" (click)="competencies.select(competency)"
[attr.aria-label]="competency.competency.shortname" detail="true" button>
<ion-label>
<p class="item-heading">
@ -85,8 +85,7 @@
<div>
<p class="item-heading">{{ 'addon.competency.path' | translate }}</p>
<p>
<a *ngIf="competency.comppath.showlinks" [href]="competency.comppath.pluginbaseurl + '/competencies.php?competencyframeworkid=' +
competency.comppath.framework.id + '&pagecontextid=' + competency.comppath.pagecontextid" core-link
<a *ngIf="competency.comppath.showlinks" [href]="getCompetencyFrameworkUrl(competency)" core-link
[title]="competency.comppath.framework.name">
{{ competency.comppath.framework.name }}
</a>
@ -104,7 +103,7 @@
</ng-container>
</p>
</div>
<div *ngIf="competencies.statistics.canmanagecoursecompetencies">
<div *ngIf="courseCompetencies?.statistics.canmanagecoursecompetencies">
<p class="item-heading">{{ 'addon.competency.uponcoursecompletion' | translate }}</p>
<ng-container *ngFor="let ruleoutcome of competency.ruleoutcomeoptions">
<span *ngIf="ruleoutcome.selected">{{ ruleoutcome.text }}</span>

View File

@ -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<void> {
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<void> {
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<void> {
await this.competencies.getSource().invalidateCache();
this.fetchCourseCompetencies().finally(() => {
refresher?.complete();
});
}

View File

@ -8,11 +8,11 @@
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshLearningPlan($event.target)">
<ion-content [core-swipe-navigation]="plans">
<ion-refresher slot="fixed" [disabled]="!competencies.loaded" (ionRefresh)="refreshLearningPlan($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<core-loading [hideUntil]="competencies.loaded">
<ion-card *ngIf="user">
<ion-item class="ion-text-wrap">
<ion-label>
@ -74,9 +74,8 @@
<p>{{ 'addon.competency.nocompetencies' | translate }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngFor="let competency of plan.competencies"
(click)="openCompetency(competency.competency.id)" [attr.aria-label]="competency.competency.shortname" detail="true"
button>
<ion-item class="ion-text-wrap" *ngFor="let competency of competencies.items" (click)="competencies.select(competency)"
[attr.aria-label]="competency.competency.shortname" detail="true" button>
<ion-label>
<p class="item-heading">{{competency.competency.shortname}} <em>{{competency.competency.idnumber}}</em></p>
</ion-label>

View File

@ -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<AddonCompetencyDataForPlanPageCompetency, AddonCompetencyPlanCompetenciesSource>;
/**
* @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<void> {
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<void> {
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<void> {
await this.competencies.getSource().invalidateCache();
this.fetchLearningPlan().finally(() => {
refresher?.complete();
});
}

View File

@ -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<AddonCompetencyPlanFormatted, AddonCompetencyPlansSource>;
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<void> {
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<void> {
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<AddonCompetencyPlanFormatted> {
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 {};
}
}

View File

@ -64,8 +64,10 @@ export class CoreListItemsManager<
*
* @param splitView Split view component.
*/
async start(splitView: CoreSplitViewComponent): Promise<void> {
this.watchSplitViewOutlet(splitView);
async start(splitView?: CoreSplitViewComponent): Promise<void> {
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;
}