Merge pull request #3046 from NoelDeMartin/MOBILE-3934

MOBILE-3934: Refactor pages with nested split views
main
Dani Palou 2022-01-17 13:06:56 +01:00 committed by GitHub
commit 1b43cb4076
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1913 additions and 1645 deletions

View File

@ -17,7 +17,7 @@ import { CoreBlockHandlerData } from '@features/block/services/block-delegate';
import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block';
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
import { makeSingleton } from '@singletons';
import { ADDON_COMPETENCY_MAIN_PAGE_NAME } from '@addons/competency/competency.module';
import { ADDON_COMPETENCY_LEARNING_PLANS_PAGE } from '@addons/competency/competency.module';
/**
* Block handler.
@ -38,7 +38,7 @@ export class AddonBlockLearningPlansHandlerService extends CoreBlockBaseHandler
title: 'addon.block_learningplans.pluginname',
class: 'addon-block-learning-plans',
component: CoreBlockOnlyTitleComponent,
link: ADDON_COMPETENCY_MAIN_PAGE_NAME,
link: ADDON_COMPETENCY_LEARNING_PLANS_PAGE,
navOptions: {
preferCurrentTab: false,
},

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

@ -15,8 +15,8 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AddonCompetencyCourseCompetenciesPageModule } from './pages/coursecompetencies/coursecompetencies.module';
import { AddonCompetencyCourseCompetenciesPage } from './pages/coursecompetencies/coursecompetencies.page';
import { AddonCompetencyCourseCompetenciesPageModule } from './pages/coursecompetencies/coursecompetencies.module';
const routes: Routes = [
{
@ -31,4 +31,4 @@ const routes: Routes = [
AddonCompetencyCourseCompetenciesPageModule,
],
})
export class AddonCompetencyCourseLazyModule {}
export class AddonCompetencyCourseContentsLazyModule {}

View File

@ -0,0 +1,72 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AddonCompetencyCompetencyPage } from './pages/competency/competency.page';
import { AddonCompetencyCompetencySummaryPage } from './pages/competencysummary/competencysummary.page';
import { ADDON_COMPETENCY_SUMMARY_PAGE } from './competency.module';
import { AddonCompetencyCompetencyPageModule } from './pages/competency/competency.module';
import { AddonCompetencyCompetencySummaryPageModule } from './pages/competencysummary/competencysummary.module';
import { AddonCompetencyCourseCompetenciesPage } from './pages/coursecompetencies/coursecompetencies.page';
import { AddonCompetencyCourseCompetenciesPageModule } from './pages/coursecompetencies/coursecompetencies.module';
import { AddonCompetencyCompetenciesPage } from './pages/competencies/competencies.page';
import { conditionalRoutes } from '@/app/app-routing.module';
import { CoreScreen } from '@services/screen';
import { AddonCompetencyCompetenciesPageModule } from './pages/competencies/competencies.module';
const mobileRoutes: Routes = [
{
path: '',
component: AddonCompetencyCourseCompetenciesPage,
},
{
path: ':competencyId',
component: AddonCompetencyCompetencyPage,
},
];
const tabletRoutes: Routes = [
{
path: '',
component: AddonCompetencyCompetenciesPage,
children: [
{
path: ':competencyId',
component: AddonCompetencyCompetencyPage,
},
],
},
];
const routes: Routes = [
...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile),
...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet),
{
path: `:competencyId/${ADDON_COMPETENCY_SUMMARY_PAGE}`,
component: AddonCompetencyCompetencySummaryPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
AddonCompetencyCourseCompetenciesPageModule,
AddonCompetencyCompetenciesPageModule,
AddonCompetencyCompetencyPageModule,
AddonCompetencyCompetencySummaryPageModule,
],
})
export class AddonCompetencyCourseDetailsLazyModule {}

View File

@ -20,11 +20,13 @@ import { CoreScreen } from '@services/screen';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonCompetencyPlanPage } from './pages/plan/plan';
import { AddonCompetencyPlanListPage } from './pages/planlist/planlist';
import { AddonCompetencyCompetenciesPage } from './pages/competencies/competencies';
import { AddonCompetencyCompetencyPage } from './pages/competency/competency';
import { AddonCompetencyCompetencySummaryPage } from './pages/competencysummary/competencysummary';
import { AddonCompetencyCourseCompetenciesPage } from './pages/coursecompetencies/coursecompetencies.page';
import { AddonCompetencyCourseCompetenciesPageModule } from './pages/coursecompetencies/coursecompetencies.module';
import { AddonCompetencyCompetencyPage } from './pages/competency/competency.page';
import { AddonCompetencyCompetencySummaryPage } from './pages/competencysummary/competencysummary.page';
import { ADDON_COMPETENCY_COMPETENCIES_PAGE, ADDON_COMPETENCY_SUMMARY_PAGE } from './competency.module';
import { AddonCompetencyCompetencyPageModule } from './pages/competency/competency.module';
import { AddonCompetencyCompetencySummaryPageModule } from './pages/competencysummary/competencysummary.module';
import { AddonCompetencyCompetenciesPage } from './pages/competencies/competencies.page';
import { AddonCompetencyCompetenciesPageModule } from './pages/competencies/competencies.module';
const mobileRoutes: Routes = [
{
@ -33,34 +35,28 @@ const mobileRoutes: Routes = [
component: AddonCompetencyPlanListPage,
},
{
path: 'competencies',
component: AddonCompetencyCompetenciesPage,
},
{
path: 'competencies/:competencyId',
component: AddonCompetencyCompetencyPage,
},
{
path: 'course/:courseId',
component: AddonCompetencyCourseCompetenciesPage,
},
{
path: 'summary/:competencyId',
component: AddonCompetencyCompetencySummaryPage,
},
{
path: ':planId',
path: `:planId/${ADDON_COMPETENCY_COMPETENCIES_PAGE}`,
component: AddonCompetencyPlanPage,
},
{
path: `:planId/${ADDON_COMPETENCY_COMPETENCIES_PAGE}/:competencyId`,
component: AddonCompetencyCompetencyPage,
},
];
const tabletRoutes: Routes = [
{
path: 'summary/:competencyId',
component: AddonCompetencyCompetencySummaryPage,
path: '',
component: AddonCompetencyPlanListPage,
children: [
{
path: `:planId/${ADDON_COMPETENCY_COMPETENCIES_PAGE}`,
component: AddonCompetencyPlanPage,
},
],
},
{
path: 'competencies',
path: `:planId/${ADDON_COMPETENCY_COMPETENCIES_PAGE}`,
component: AddonCompetencyCompetenciesPage,
children: [
{
@ -69,40 +65,28 @@ const tabletRoutes: Routes = [
},
],
},
{
path: 'course/:courseId',
component: AddonCompetencyCourseCompetenciesPage,
},
{
path: '',
component: AddonCompetencyPlanListPage,
children: [
{
path: ':planId',
component: AddonCompetencyPlanPage,
},
],
},
];
const routes: Routes = [
...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile),
...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet),
{
path: `:planId/${ADDON_COMPETENCY_COMPETENCIES_PAGE}/:competencyId/${ADDON_COMPETENCY_SUMMARY_PAGE}`,
component: AddonCompetencyCompetencySummaryPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CoreSharedModule,
AddonCompetencyCourseCompetenciesPageModule,
AddonCompetencyCompetenciesPageModule,
AddonCompetencyCompetencyPageModule,
AddonCompetencyCompetencySummaryPageModule,
],
declarations: [
AddonCompetencyPlanPage,
AddonCompetencyPlanListPage,
AddonCompetencyCompetenciesPage,
AddonCompetencyCompetencyPage,
AddonCompetencyCompetencySummaryPage,
],
})
export class AddonCompetencyLazyModule {}
export class AddonCompetencyLearningPlansLazyModule {}

View File

@ -30,6 +30,8 @@ import { Routes } from '@angular/router';
import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index-routing.module';
import { COURSE_PAGE_NAME } from '@features/course/course.module';
import { PARTICIPANTS_PAGE_NAME } from '@features/user/user.module';
// List of providers (without handlers).
export const ADDON_COMPETENCY_SERVICES: Type<unknown>[] = [
@ -37,19 +39,29 @@ export const ADDON_COMPETENCY_SERVICES: Type<unknown>[] = [
AddonCompetencyHelperProvider,
];
export const ADDON_COMPETENCY_MAIN_PAGE_NAME = 'competency';
export const ADDON_COMPETENCY_LEARNING_PLANS_PAGE = 'learning-plans';
export const ADDON_COMPETENCY_COMPETENCIES_PAGE = 'competencies';
export const ADDON_COMPETENCY_SUMMARY_PAGE = 'summary';
const mainMenuChildrenRoutes: Routes = [
{
path: ADDON_COMPETENCY_MAIN_PAGE_NAME,
loadChildren: () => import('./competency-lazy.module').then(m => m.AddonCompetencyLazyModule),
path: ADDON_COMPETENCY_LEARNING_PLANS_PAGE,
loadChildren: () => import('./competency-learning-plans-lazy.module').then(m => m.AddonCompetencyLearningPlansLazyModule),
},
{
path: `${COURSE_PAGE_NAME}/:courseId/${ADDON_COMPETENCY_COMPETENCIES_PAGE}`,
loadChildren: () => import('./competency-course-details-lazy.module').then(m => m.AddonCompetencyCourseDetailsLazyModule),
},
{
path: `${COURSE_PAGE_NAME}/:courseId/${PARTICIPANTS_PAGE_NAME}/:userId/${ADDON_COMPETENCY_COMPETENCIES_PAGE}`,
loadChildren: () => import('./competency-course-details-lazy.module').then(m => m.AddonCompetencyCourseDetailsLazyModule),
},
];
const courseIndexRoutes: Routes = [
{
path: ADDON_COMPETENCY_MAIN_PAGE_NAME,
loadChildren: () => import('@addons/competency/competency-course-lazy.module').then(m => m.AddonCompetencyCourseLazyModule),
path: ADDON_COMPETENCY_COMPETENCIES_PAGE,
loadChildren: () => import('./competency-course-contents-lazy.module').then(m => m.AddonCompetencyCourseContentsLazyModule),
},
];

View File

@ -0,0 +1,28 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { AddonCompetencyCompetenciesPage } from './competencies.page';
@NgModule({
imports: [
CoreSharedModule,
],
declarations: [
AddonCompetencyCompetenciesPage,
],
})
export class AddonCompetencyCompetenciesPageModule {}

View File

@ -0,0 +1,125 @@
// (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 { CoreDomUtils } from '@services/utils/dom';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import {
AddonCompetencyDataForPlanPageCompetency,
AddonCompetencyDataForCourseCompetenciesPageCompetency,
} from '../../services/competency';
import { Translate } from '@singletons';
import { CoreNavigator } from '@services/navigator';
import { CoreError } from '@classes/errors/error';
import { AddonCompetencyPlanCompetenciesSource } from '@addons/competency/classes/competency-plan-competencies-source';
import { AddonCompetencyCourseCompetenciesSource } from '@addons/competency/classes/competency-course-competencies-source';
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
/**
* Page that displays the list of competencies of a learning plan.
*/
@Component({
selector: 'page-addon-competency-competencies',
templateUrl: 'competencies.html',
})
export class AddonCompetencyCompetenciesPage implements AfterViewInit, OnDestroy {
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
competencies: CoreListItemsManager<
AddonCompetencyDataForPlanPageCompetency | AddonCompetencyDataForCourseCompetenciesPageCompetency,
AddonCompetencyPlanCompetenciesSource | AddonCompetencyCourseCompetenciesSource
>;
title = '';
constructor() {
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 CoreListItemsManager(source, AddonCompetencyCompetenciesPage);
return;
}
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(AddonCompetencyPlanCompetenciesSource, [planId]);
this.competencies = new CoreListItemsManager(source, AddonCompetencyCompetenciesPage);
}
/**
* @inheritdoc
*/
async ngAfterViewInit(): Promise<void> {
await this.fetchCompetencies();
this.competencies.start(this.splitView);
}
/**
* Fetches the competencies and updates the view.
*
* @return Promise resolved when done.
*/
protected async fetchCompetencies(): Promise<void> {
try {
const source = this.competencies.getSource();
await this.competencies.load();
if (source instanceof AddonCompetencyPlanCompetenciesSource) {
if (!source.plan || source.plan && source.plan.competencycount <= 0) {
throw new CoreError(Translate.instant('addon.competency.errornocompetenciesfound'));
}
this.title = source.plan.plan.name;
} else {
this.title = Translate.instant('addon.competency.coursecompetencies');
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error getting competencies data.');
}
}
/**
* Refreshes the competencies.
*
* @param refresher Refresher.
*/
async refreshCompetencies(refresher?: IonRefresher): Promise<void> {
await this.competencies.getSource().invalidateCache();
this.competencies.getSource().setDirty(true);
this.fetchCompetencies().finally(() => {
refresher?.complete();
});
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.competencies.destroy();
}
}

View File

@ -1,165 +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 { 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 {
AddonCompetencyDataForPlanPageCompetency, AddonCompetencyDataForCourseCompetenciesPageCompetency, AddonCompetency,
} from '../../services/competency';
import { Params, ActivatedRoute } from '@angular/router';
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
import { Translate } from '@singletons';
import { CoreNavigator } from '@services/navigator';
import { CoreError } from '@classes/errors/error';
/**
* Page that displays the list of competencies of a learning plan.
*/
@Component({
selector: 'page-addon-competency-competencies',
templateUrl: 'competencies.html',
})
export class AddonCompetencyCompetenciesPage implements AfterViewInit, OnDestroy {
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
protected planId?: number;
protected courseId?: number;
protected userId?: number;
competenciesLoaded = false;
competencies: AddonCompetencyListManager;
title = '';
constructor(protected route: ActivatedRoute) {
this.planId = CoreNavigator.getRouteNumberParam('planId', { route });
if (!this.planId) {
this.courseId = CoreNavigator.getRouteNumberParam('courseId', { route });
this.userId = CoreNavigator.getRouteNumberParam('userId', { route });
}
this.competencies =
new AddonCompetencyListManager(AddonCompetencyCompetenciesPage, this.planId, this.courseId, this.userId);
}
/**
* @inheritdoc
*/
async ngAfterViewInit(): Promise<void> {
await this.fetchCompetencies();
this.competencies.start(this.splitView);
}
/**
* Fetches the competencies and updates the view.
*
* @return Promise resolved when done.
*/
protected async fetchCompetencies(): Promise<void> {
try {
if (this.planId) {
const response = await AddonCompetency.getLearningPlan(this.planId);
if (response.competencycount <= 0) {
throw new CoreError(Translate.instant('addon.competency.errornocompetenciesfound'));
}
this.title = response.plan.name;
this.userId = response.plan.userid;
this.competencies.setItems(response.competencies);
} else if (this.courseId) {
const response = await AddonCompetency.getCourseCompetencies(this.courseId, this.userId);
this.title = Translate.instant('addon.competency.coursecompetencies');
this.competencies.setItems(response.competencies);
} else {
throw null;
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error getting competencies data.');
}
}
/**
* Refreshes the competencies.
*
* @param refresher Refresher.
*/
async refreshCompetencies(refresher?: IonRefresher): Promise<void> {
try {
if (this.planId) {
await AddonCompetency.invalidateLearningPlan(this.planId);
} else {
await AddonCompetency.invalidateCourseCompetencies(this.courseId!, this.userId);
}
} finally {
this.fetchCompetencies().finally(() => {
refresher?.complete();
});
}
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.competencies.destroy();
}
}
type AddonCompetencyDataForPlanPageCompetencyFormatted =
AddonCompetencyDataForPlanPageCompetency | AddonCompetencyDataForCourseCompetenciesPageCompetency;
/**
* Helper class to manage competencies list.
*/
class AddonCompetencyListManager extends CorePageItemsListManager<AddonCompetencyDataForPlanPageCompetencyFormatted> {
planId?: number;
courseId?: number;
userId?: number;
constructor(pageComponent: unknown, planId?: number, courseId?: number, userId?: number) {
super(pageComponent);
this.planId = planId;
this.courseId = courseId;
this.userId = userId;
}
/**
* @inheritdoc
*/
protected getItemPath(competency: AddonCompetencyDataForPlanPageCompetencyFormatted): string {
return String(competency.competency.id);
}
/**
* @inheritdoc
*/
protected getItemQueryParams(): Params {
if (this.planId) {
return { planId: this.planId };
} else {
return { courseId: this.courseId, userId: this.userId };
}
}
}

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

@ -0,0 +1,28 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { AddonCompetencyCompetencyPage } from './competency.page';
@NgModule({
imports: [
CoreSharedModule,
],
declarations: [
AddonCompetencyCompetencyPage,
],
})
export class AddonCompetencyCompetencyPageModule {}

View File

@ -0,0 +1,308 @@
// (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 { AddonCompetencyHelper } from '@addons/competency/services/competency-helper';
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';
import { CoreDomUtils } from '@services/utils/dom';
import { Translate } from '@singletons';
import {
AddonCompetencyDataForUserCompetencySummaryWSResponse,
AddonCompetencyUserCompetencyPlan,
AddonCompetencyUserCompetency,
AddonCompetencyUserCompetencyCourse,
AddonCompetency,
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.
*/
@Component({
selector: 'page-addon-competency-competency',
templateUrl: 'competency.html',
})
export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy {
competencyLoaded = false;
competencies!: AddonCompetencyCompetenciesSwipeManager;
planStatus?: number;
coursemodules?: CoreCourseModuleSummary[];
user?: CoreUserSummary;
competency?: AddonCompetencyDataForUserCompetencySummaryWSResponse;
userCompetency?: AddonCompetencyUserCompetencyPlan | AddonCompetencyUserCompetency | AddonCompetencyUserCompetencyCourse;
contextLevel?: string;
contextInstanceId?: number;
constructor() {
try {
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);
CoreNavigator.back();
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();
if (!this.competency) {
return;
}
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 {
await CoreUtils.ignoreErrors(
AddonCompetency.logCompetencyInCourseView(
source.COURSE_ID,
this.requireCompetencyId(),
name,
source.USER_ID,
),
);
}
} finally {
this.competencyLoaded = true;
}
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.competencies.destroy();
}
/**
* Fetches the competency and updates the view.
*
* @return Promise resolved when done.
*/
protected async fetchCompetency(): Promise<void> {
try {
const source = this.competencies.getSource();
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.
this.user = this.competency.user;
}
this.competency.evidence.forEach((evidence) => {
if (evidence.descidentifier) {
const key = 'addon.competency.' + evidence.descidentifier;
evidence.description = Translate.instant(key, { $a: evidence.desca });
}
});
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error getting competency data.');
}
}
/**
* Refreshes the competency.
*
* @param refresher Refresher.
*/
async refreshCompetency(refresher: IonRefresher): Promise<void> {
const source = this.competencies.getSource();
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();
});
}
/**
* Opens the summary of a competency.
*
* @param competencyId
*/
openCompetencySummary(competencyId: number): void {
CoreNavigator.navigate(
`../${competencyId}/${ADDON_COMPETENCY_SUMMARY_PAGE}`,
{
params: { contextLevel: this.contextLevel, contextInstanceId: this.contextInstanceId },
},
);
}
/**
* 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

@ -1,194 +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 { AddonCompetencyHelper } from '@addons/competency/services/competency-helper';
import { Component, OnInit } from '@angular/core';
import { CoreCourseModuleSummary } from '@features/course/services/course';
import { CoreUserSummary } from '@features/user/services/user';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { Translate } from '@singletons';
import {
AddonCompetencyDataForUserCompetencySummaryWSResponse,
AddonCompetencyUserCompetencyPlan,
AddonCompetencyUserCompetency,
AddonCompetencyUserCompetencyCourse,
AddonCompetency,
AddonCompetencyDataForUserCompetencySummaryInPlanWSResponse,
AddonCompetencyDataForUserCompetencySummaryInCourseWSResponse,
} 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_MAIN_PAGE_NAME } from '@addons/competency/competency.module';
/**
* Page that displays the competency information.
*/
@Component({
selector: 'page-addon-competency-competency',
templateUrl: 'competency.html',
})
export class AddonCompetencyCompetencyPage implements OnInit {
competencyLoaded = false;
competencyId!: number;
planId?: number;
courseId?: number;
userId?: number;
planStatus?: number;
coursemodules?: CoreCourseModuleSummary[];
user?: CoreUserSummary;
competency?: AddonCompetencyDataForUserCompetencySummaryWSResponse;
userCompetency?: AddonCompetencyUserCompetencyPlan | AddonCompetencyUserCompetency | AddonCompetencyUserCompetencyCourse;
contextLevel?: string;
contextInstanceId?: number;
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
try {
this.competencyId = CoreNavigator.getRequiredRouteNumberParam('competencyId');
this.planId = CoreNavigator.getRouteNumberParam('planId');
if (!this.planId) {
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.userId = CoreNavigator.getRouteNumberParam('userId');
}
} catch (error) {
CoreDomUtils.showErrorModal(error);
CoreNavigator.back();
return;
}
try {
await this.fetchCompetency();
const name = this.competency && this.competency.competency && this.competency.competency.competency &&
this.competency.competency.competency.shortname;
if (this.planId) {
CoreUtils.ignoreErrors(AddonCompetency.logCompetencyInPlanView(
this.planId,
this.competencyId,
this.planStatus!,
name,
this.userId,
));
} else {
CoreUtils.ignoreErrors(
AddonCompetency.logCompetencyInCourseView(this.courseId!, this.competencyId, name, this.userId),
);
}
} finally {
this.competencyLoaded = true;
}
}
/**
* Fetches the competency and updates the view.
*
* @return Promise resolved when done.
*/
protected async fetchCompetency(): Promise<void> {
try {
let competency: AddonCompetencyDataForUserCompetencySummaryInPlanWSResponse |
AddonCompetencyDataForUserCompetencySummaryInCourseWSResponse;
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;
}
if (this.competency.user.id != CoreSites.getCurrentSiteUserId()) {
// Get the user profile from the returned object.
this.user = this.competency.user;
}
this.competency.evidence.forEach((evidence) => {
if (evidence.descidentifier) {
const key = 'addon.competency.' + evidence.descidentifier;
evidence.description = Translate.instant(key, { $a: evidence.desca });
}
});
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error getting competency data.');
}
}
/**
* Refreshes the competency.
*
* @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);
}
} finally {
this.fetchCompetency().finally(() => {
refresher?.complete();
});
}
}
/**
* Opens the summary of a competency.
*
* @param competencyId
*/
openCompetencySummary(competencyId: number): void {
CoreNavigator.navigateToSitePath(
ADDON_COMPETENCY_MAIN_PAGE_NAME + '/summary/' + competencyId,
{
params: { contextLevel: this.contextLevel, contextInstanceId: this.contextInstanceId },
},
);
}
}

View File

@ -0,0 +1,28 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { AddonCompetencyCompetencySummaryPage } from './competencysummary.page';
@NgModule({
imports: [
CoreSharedModule,
],
declarations: [
AddonCompetencyCompetencySummaryPage,
],
})
export class AddonCompetencyCompetencySummaryPageModule {}

View File

@ -19,7 +19,7 @@ import { IonRefresher } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { ADDON_COMPETENCY_MAIN_PAGE_NAME } from '@addons/competency/competency.module';
import { ADDON_COMPETENCY_SUMMARY_PAGE } from '@addons/competency/competency.module';
/**
* Page that displays the competency summary.
@ -101,8 +101,8 @@ export class AddonCompetencyCompetencySummaryPage implements OnInit {
* @param competencyId
*/
openCompetencySummary(competencyId: number): void {
CoreNavigator.navigateToSitePath(
ADDON_COMPETENCY_MAIN_PAGE_NAME + '/summary/' + competencyId,
CoreNavigator.navigate(
`../../${competencyId}/${ADDON_COMPETENCY_SUMMARY_PAGE}`,
{
params: { contextLevel: this.contextLevel, contextInstanceId: this.contextInstanceId },
},

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_MAIN_PAGE_NAME } from '@addons/competency/competency.module';
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_MAIN_PAGE_NAME } from '@addons/competency/competency.m
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,40 +111,27 @@ 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.navigateToSitePath(
ADDON_COMPETENCY_MAIN_PAGE_NAME + '/competencies/' + competencyId,
{
params: { courseId: this.courseId, userId: this.userId },
},
);
}
/**
* Opens the summary of a competency.
*
* @param competencyId
*/
openCompetencySummary(competencyId: number): void {
CoreNavigator.navigateToSitePath(ADDON_COMPETENCY_MAIN_PAGE_NAME + '/summary/' + competencyId, {
params: {
contextLevel: ContextLevel.COURSE,
contextInstanceId: this.courseId,
} });
CoreNavigator.navigateToSitePath(
`./${competencyId}/${ADDON_COMPETENCY_SUMMARY_PAGE}`,
{
params: {
contextLevel: ContextLevel.COURSE,
contextInstanceId: this.courseId,
},
},
);
}
/**
@ -106,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,14 +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 { ADDON_COMPETENCY_MAIN_PAGE_NAME } 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 { 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.
@ -28,19 +31,26 @@ import { ADDON_COMPETENCY_MAIN_PAGE_NAME } from '@addons/competency/competency.m
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);
@ -48,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();
}
/**
@ -61,40 +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.navigateToSitePath(
ADDON_COMPETENCY_MAIN_PAGE_NAME + '/competencies/' + competencyId,
{ params: { planId: this.planId } },
);
}
/**
* 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

@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
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 { 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.
@ -28,22 +28,17 @@ import { CorePageItemsListManager } from '@classes/page-items-list-manager';
selector: 'page-addon-competency-planlist',
templateUrl: 'planlist.html',
})
export class AddonCompetencyPlanListPage implements OnInit, AfterViewInit, OnDestroy {
export class AddonCompetencyPlanListPage implements AfterViewInit, OnDestroy {
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
protected userId?: number;
plans: AddonCompetencyPlanListManager;
plans: CoreListItemsManager<AddonCompetencyPlanFormatted, AddonCompetencyPlansSource>;
constructor() {
this.plans = new AddonCompetencyPlanListManager(AddonCompetencyPlanListPage);
}
const userId = CoreNavigator.getRouteNumberParam('userId');
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(AddonCompetencyPlansSource, [userId]);
/**
* @inheritdoc
*/
ngOnInit(): void {
this.userId = CoreNavigator.getRouteNumberParam('userId');
this.plans = new CoreListItemsManager(source, AddonCompetencyPlanListPage);
}
/**
@ -62,23 +57,7 @@ export class AddonCompetencyPlanListPage implements OnInit, AfterViewInit, OnDes
*/
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.');
}
@ -89,11 +68,12 @@ export class AddonCompetencyPlanListPage implements OnInit, AfterViewInit, OnDes
*
* @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.plans.getSource().setDirty(true);
this.fetchLearningPlans().finally(() => {
refresher?.complete();
});
}
@ -105,28 +85,3 @@ export class AddonCompetencyPlanListPage implements OnInit, AfterViewInit, OnDes
}
}
/**
* 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> {
constructor(pageComponent: unknown) {
super(pageComponent);
}
/**
* @inheritdoc
*/
protected getItemPath(plan: AddonCompetencyPlanFormatted): string {
return String(plan.id);
}
}

View File

@ -12,10 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { ADDON_COMPETENCY_MAIN_PAGE_NAME } from '@addons/competency/competency.module';
import { ADDON_COMPETENCY_COMPETENCIES_PAGE, ADDON_COMPETENCY_LEARNING_PLANS_PAGE } from '@addons/competency/competency.module';
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { COURSE_PAGE_NAME } from '@features/course/course.module';
import { CoreNavigator } from '@services/navigator';
import { makeSingleton } from '@singletons';
import { AddonCompetency } from '../competency';
@ -37,17 +38,29 @@ export class AddonCompetencyCompetencyLinkHandlerService extends CoreContentLink
return [{
action: (siteId: string): void => {
const pageParams = {
planId: params.planid,
courseId: courseId,
userId: params.userid,
};
if (courseId) {
CoreNavigator.navigateToSitePath(
`${COURSE_PAGE_NAME}/${courseId}/${ADDON_COMPETENCY_COMPETENCIES_PAGE}`,
{
params: { userId: params.userid },
siteId,
},
);
CoreNavigator.navigateToSitePath(
ADDON_COMPETENCY_MAIN_PAGE_NAME + '/competencies/' + params.competencyid,
{ params: pageParams, siteId },
);
return;
}
if (params.planid) {
CoreNavigator.navigateToSitePath(
`${ADDON_COMPETENCY_LEARNING_PLANS_PAGE}/competencies/${params.planid}`,
{
params: { userId: params.userid },
siteId,
},
);
return;
}
},
}];
}

View File

@ -25,7 +25,7 @@ import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/service
import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper';
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
import { ContextLevel } from '@/core/constants';
import { ADDON_COMPETENCY_MAIN_PAGE_NAME } from '@addons/competency/competency.module';
import { ADDON_COMPETENCY_COMPETENCIES_PAGE } from '@addons/competency/competency.module';
/**
* Course nav handler.
@ -75,7 +75,7 @@ export class AddonCompetencyCourseOptionHandlerService implements CoreCourseOpti
return {
title: 'addon.competency.competencies',
class: 'addon-competency-course-handler',
page: ADDON_COMPETENCY_MAIN_PAGE_NAME,
page: ADDON_COMPETENCY_COMPETENCIES_PAGE,
};
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { ADDON_COMPETENCY_MAIN_PAGE_NAME } from '@addons/competency/competency.module';
import { ADDON_COMPETENCY_LEARNING_PLANS_PAGE } from '@addons/competency/competency.module';
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
@ -36,7 +36,7 @@ export class AddonCompetencyPlanLinkHandlerService extends CoreContentLinksHandl
return [{
action: (siteId: string): void => {
CoreNavigator.navigateToSitePath(
ADDON_COMPETENCY_MAIN_PAGE_NAME + '/' + params.id,
`${ADDON_COMPETENCY_LEARNING_PLANS_PAGE}/${params.id}`,
{ siteId },
);
},

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { ADDON_COMPETENCY_MAIN_PAGE_NAME } from '@addons/competency/competency.module';
import { ADDON_COMPETENCY_LEARNING_PLANS_PAGE } from '@addons/competency/competency.module';
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
@ -36,7 +36,7 @@ export class AddonCompetencyPlansLinkHandlerService extends CoreContentLinksHand
return [{
action: (siteId: string): void => {
CoreNavigator.navigateToSitePath(
ADDON_COMPETENCY_MAIN_PAGE_NAME,
ADDON_COMPETENCY_LEARNING_PLANS_PAGE,
{ params: { userId: params.userid }, siteId },
);

View File

@ -12,8 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { ADDON_COMPETENCY_MAIN_PAGE_NAME } from '@addons/competency/competency.module';
import { ADDON_COMPETENCY_COMPETENCIES_PAGE, ADDON_COMPETENCY_LEARNING_PLANS_PAGE } from '@addons/competency/competency.module';
import { Injectable } from '@angular/core';
import { COURSE_PAGE_NAME } from '@features/course/course.module';
import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate';
import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications';
import { CoreNavigator } from '@services/navigator';
@ -56,7 +57,7 @@ export class AddonCompetencyPushClickHandlerService implements CorePushNotificat
await CoreUtils.ignoreErrors(AddonCompetency.invalidateLearningPlan(planId, notification.site));
await CoreNavigator.navigateToSitePath(ADDON_COMPETENCY_MAIN_PAGE_NAME + '/' + planId, {
await CoreNavigator.navigateToSitePath(`${ADDON_COMPETENCY_LEARNING_PLANS_PAGE}/${planId}`, {
siteId: notification.site,
});
@ -71,15 +72,30 @@ export class AddonCompetencyPushClickHandlerService implements CorePushNotificat
const userId = Number(contextUrlParams.userid);
await CoreUtils.ignoreErrors(AddonCompetency.invalidateCompetencyInPlan(planId, competencyId, notification.site));
await CoreNavigator.navigateToSitePath(
ADDON_COMPETENCY_MAIN_PAGE_NAME + '/competencies/' + competencyId,
{
params: { planId, courseId, userId },
siteId: notification.site,
},
);
return;
if (courseId) {
await CoreNavigator.navigateToSitePath(
`${COURSE_PAGE_NAME}/${courseId}/${ADDON_COMPETENCY_COMPETENCIES_PAGE}/${competencyId}`,
{
params: { userId },
siteId: notification.site,
},
);
return;
}
if (planId) {
await CoreNavigator.navigateToSitePath(
`${ADDON_COMPETENCY_LEARNING_PLANS_PAGE}/competencies/${planId}/${competencyId}`,
{
params: { userId },
siteId: notification.site,
},
);
return;
}
}
// Open the list of plans.
@ -87,7 +103,7 @@ export class AddonCompetencyPushClickHandlerService implements CorePushNotificat
await CoreUtils.ignoreErrors(AddonCompetency.invalidateLearningPlans(userId, notification.site));
await CoreNavigator.navigateToSitePath(ADDON_COMPETENCY_MAIN_PAGE_NAME, {
await CoreNavigator.navigateToSitePath(ADDON_COMPETENCY_LEARNING_PLANS_PAGE, {
params: { userId },
siteId: notification.site,
});

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { ADDON_COMPETENCY_MAIN_PAGE_NAME } from '@addons/competency/competency.module';
import { ADDON_COMPETENCY_COMPETENCIES_PAGE, ADDON_COMPETENCY_SUMMARY_PAGE } from '@addons/competency/competency.module';
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
@ -33,14 +33,12 @@ export class AddonCompetencyUserCompetencyLinkHandlerService extends CoreContent
* @inheritdoc
*/
getActions(siteIds: string[], url: string, params: Record<string, string>): CoreContentLinksAction[] {
return [{
action: (siteId: string): void => {
CoreNavigator.navigateToSitePath(
ADDON_COMPETENCY_MAIN_PAGE_NAME + '/summary/' + params.id,
`${ADDON_COMPETENCY_COMPETENCIES_PAGE}/${params.id}/${ADDON_COMPETENCY_SUMMARY_PAGE}`,
{ siteId },
);
},
}];
}

View File

@ -12,10 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { ADDON_COMPETENCY_MAIN_PAGE_NAME } from '@addons/competency/competency.module';
import { ADDON_COMPETENCY_COMPETENCIES_PAGE, ADDON_COMPETENCY_LEARNING_PLANS_PAGE } from '@addons/competency/competency.module';
import { Injectable } from '@angular/core';
import { COURSE_PAGE_NAME } from '@features/course/course.module';
import { CoreUserProfile } from '@features/user/services/user';
import { CoreUserProfileHandler, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
import { PARTICIPANTS_PAGE_NAME } from '@features/user/user.module';
import { CoreNavigator } from '@services/navigator';
import { makeSingleton } from '@singletons';
import { AddonCompetency } from '../competency';
@ -69,12 +71,8 @@ export class AddonCompetencyUserHandlerService implements CoreUserProfileHandler
event.preventDefault();
event.stopPropagation();
CoreNavigator.navigateToSitePath(
ADDON_COMPETENCY_MAIN_PAGE_NAME + '/course/' + courseId,
{
params: { userId: user.id },
},
[COURSE_PAGE_NAME, courseId, PARTICIPANTS_PAGE_NAME, user.id, ADDON_COMPETENCY_COMPETENCIES_PAGE].join('/'),
);
},
};
} else {
@ -85,7 +83,7 @@ export class AddonCompetencyUserHandlerService implements CoreUserProfileHandler
action: (event, user): void => {
event.preventDefault();
event.stopPropagation();
CoreNavigator.navigateToSitePath(ADDON_COMPETENCY_MAIN_PAGE_NAME, {
CoreNavigator.navigateToSitePath(ADDON_COMPETENCY_LEARNING_PLANS_PAGE, {
params: { userId: user.id },
});
},

View File

@ -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 { Params } from '@angular/router';
import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source';
import { CoreUser } from '@features/user/services/user';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { AddonModChat, AddonModChatSession, AddonModChatSessionUser } from '../services/chat';
/**
* Provides a collection of sessions.
*/
export class AddonModChatSessionsSource extends CoreRoutedItemsManagerSource<AddonModChatSessionFormatted> {
readonly COURSE_ID: number;
readonly CHAT_ID: number;
readonly CM_ID: number;
showAll = false;
groupId = 0;
groupInfo?: CoreGroupInfo;
constructor(courseId: number, chatId: number, cmId: number) {
super();
this.COURSE_ID = courseId;
this.CHAT_ID = chatId;
this.CM_ID = cmId;
}
/**
* Invalidate chat cache.
*/
async invalidateCache(): Promise<void> {
await CoreUtils.ignoreErrors(CoreUtils.allPromises([
CoreGroups.invalidateActivityGroupInfo(this.CM_ID),
AddonModChat.invalidateSessions(this.CHAT_ID, this.groupId, this.showAll),
]));
}
/**
* @inheritdoc
*/
protected async loadPageItems(): Promise<{ items: AddonModChatSessionFormatted[] }> {
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.CM_ID, false);
this.groupId = CoreGroups.validateGroupId(this.groupId, this.groupInfo);
const sessions = await AddonModChat.getSessions(this.CHAT_ID, this.groupId, this.showAll, { cmId: this.CM_ID });
// Fetch user profiles.
const promises: Promise<unknown>[] = [];
const formattedSessions = sessions.map((session: AddonModChatSessionFormatted) => {
session.duration = session.sessionend - session.sessionstart;
session.sessionusers.forEach((sessionUser) => {
// The WS does not return the user name, fetch user profile.
promises.push(this.loadUserFullname(sessionUser));
});
// If session has more than 4 users we display a "Show more" link.
session.allsessionusers = session.sessionusers;
if (session.sessionusers.length > 4) {
session.sessionusers = session.allsessionusers.slice(0, 3);
}
return session;
});
await Promise.all(promises);
return { items: formattedSessions };
}
/**
* @inheritdoc
*/
getItemPath(session: AddonModChatSessionFormatted): string {
return `${session.sessionstart}/${session.sessionend}`;
}
/**
* @inheritdoc
*/
getItemQueryParams(): Params {
return {
chatId: this.CHAT_ID,
groupId: this.groupId,
};
}
/**
* Load the fullname of a user.
*
* @param id User ID.
* @return Promise resolved when done.
*/
protected async loadUserFullname(sessionUser: AddonModChatUserSessionFormatted): Promise<void> {
if (sessionUser.userfullname) {
return;
}
try {
const user = await CoreUser.getProfile(sessionUser.userid, this.COURSE_ID, true);
sessionUser.userfullname = user.fullname;
} catch {
// Error getting profile, most probably the user is deleted.
sessionUser.userfullname = Translate.instant('core.deleteduser') + ' ' + sessionUser.userid;
}
}
}
/**
* Fields added to chat session in this view.
*/
export type AddonModChatSessionFormatted = Omit<AddonModChatSession, 'sessionusers'> & {
duration?: number; // Session duration.
sessionusers: AddonModChatUserSessionFormatted[];
allsessionusers?: AddonModChatUserSessionFormatted[]; // All session users.
};
/**
* Fields added to user session in this view.
*/
export type AddonModChatUserSessionFormatted = AddonModChatSessionUser & {
userfullname?: string; // User full name.
};

View File

@ -19,7 +19,7 @@
<ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container>
</ion-label>
<ion-select [(ngModel)]="groupId" (ionChange)="fetchSessions(true)" aria-labelledby="addon-chat-groupslabel"
<ion-select [(ngModel)]="groupId" (ionChange)="reloadSessions()" aria-labelledby="addon-chat-groupslabel"
interface="action-sheet" [interfaceOptions]="{header: 'core.group' | translate}">
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
{{groupOpt.name}}
@ -29,7 +29,7 @@
<ion-item>
<ion-label>{{ 'addon.mod_chat.showincompletesessions' | translate }}</ion-label>
<ion-toggle [(ngModel)]="showAll" (ionChange)="fetchSessions(true)"></ion-toggle>
<ion-toggle [(ngModel)]="showAll" (ionChange)="reloadSessions()"></ion-toggle>
</ion-item>
<ion-card *ngFor="let session of sessions.items" (click)="sessions.select(session)" button

View File

@ -13,17 +13,14 @@
// limitations under the License.
import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
import { Params } from '@angular/router';
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreUser } from '@features/user/services/user';
import { IonRefresher } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreGroupInfo } from '@services/groups';
import { CoreNavigator } from '@services/navigator';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { AddonModChat, AddonModChatSession, AddonModChatSessionUser } from '../../services/chat';
import { AddonModChatSessionFormatted, AddonModChatSessionsSource } from '../../classes/chat-sessions-source';
/**
* Page that displays list of chat sessions.
@ -36,28 +33,19 @@ export class AddonModChatSessionsPage implements AfterViewInit, OnDestroy {
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
sessions!: AddonChatSessionsManager;
showAll = false;
groupId = 0;
groupInfo?: CoreGroupInfo;
protected courseId!: number;
protected cmId!: number;
protected chatId!: number;
sessions!: CoreListItemsManager<AddonModChatSessionFormatted, AddonModChatSessionsSource>;
constructor() {
this.sessions = new AddonChatSessionsManager(AddonModChatSessionsPage);
}
/**
* @inheritdoc
*/
async ngAfterViewInit(): Promise<void> {
try {
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
this.chatId = CoreNavigator.getRequiredRouteNumberParam('chatId');
this.sessions.setChatId(this.chatId);
const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
const chatId = CoreNavigator.getRequiredRouteNumberParam('chatId');
const cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
AddonModChatSessionsSource,
[courseId, chatId, cmId],
);
this.sessions = new CoreListItemsManager(source, AddonModChatSessionsPage);
} catch (error) {
CoreDomUtils.showErrorModal(error);
@ -65,7 +53,32 @@ export class AddonModChatSessionsPage implements AfterViewInit, OnDestroy {
return;
}
}
get groupId(): number {
return this.sessions.getSource().groupId;
}
set groupId(value: number) {
this.sessions.getSource().groupId = value;
}
get showAll(): boolean {
return this.sessions.getSource().showAll;
}
set showAll(value: boolean) {
this.sessions.getSource().showAll = value;
}
get groupInfo(): CoreGroupInfo | undefined {
return this.sessions.getSource().groupInfo;
}
/**
* @inheritdoc
*/
async ngAfterViewInit(): Promise<void> {
await this.fetchSessions();
this.sessions.start(this.splitView);
@ -75,66 +88,27 @@ export class AddonModChatSessionsPage implements AfterViewInit, OnDestroy {
* Fetch chat sessions.
*
* @param showLoading Display a loading modal.
* @return Promise resolved when done.
*/
async fetchSessions(showLoading?: boolean): Promise<void> {
const modal = showLoading ? await CoreDomUtils.showModalLoading() : null;
async fetchSessions(): Promise<void> {
try {
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.cmId, false);
this.groupId = CoreGroups.validateGroupId(this.groupId, this.groupInfo);
this.sessions.setGroupId(this.groupId);
const sessions = await AddonModChat.getSessions(this.chatId, this.groupId, this.showAll, { cmId: this.cmId });
// Fetch user profiles.
const promises: Promise<unknown>[] = [];
const formattedSessions = sessions.map((session: AddonModChatSessionFormatted) => {
session.duration = session.sessionend - session.sessionstart;
session.sessionusers.forEach((sessionUser) => {
// The WS does not return the user name, fetch user profile.
promises.push(this.loadUserFullname(sessionUser));
});
// If session has more than 4 users we display a "Show more" link.
session.allsessionusers = session.sessionusers;
if (session.sessionusers.length > 4) {
session.sessionusers = session.allsessionusers.slice(0, 3);
}
return session;
});
await Promise.all(promises);
this.sessions.setItems(formattedSessions);
await this.sessions.load();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true);
} finally {
modal?.dismiss();
}
}
/**
* Load the fullname of a user.
*
* @param id User ID.
* @return Promise resolved when done.
* Reload chat sessions.
*/
protected async loadUserFullname(sessionUser: AddonModChatUserSessionFormatted): Promise<void> {
if (sessionUser.userfullname) {
return;
}
async reloadSessions(): Promise<void> {
const modal = await CoreDomUtils.showModalLoading();
try {
const user = await CoreUser.getProfile(sessionUser.userid, this.courseId, true);
sessionUser.userfullname = user.fullname;
} catch {
// Error getting profile, most probably the user is deleted.
sessionUser.userfullname = Translate.instant('core.deleteduser') + ' ' + sessionUser.userid;
await this.sessions.reload();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true);
} finally {
modal.dismiss();
}
}
@ -145,11 +119,9 @@ export class AddonModChatSessionsPage implements AfterViewInit, OnDestroy {
*/
async refreshSessions(refresher: IonRefresher): Promise<void> {
try {
await CoreUtils.ignoreErrors(CoreUtils.allPromises([
CoreGroups.invalidateActivityGroupInfo(this.cmId),
AddonModChat.invalidateSessions(this.chatId, this.groupId, this.showAll),
]));
this.sessions.getSource().setDirty(true);
await this.sessions.getSource().invalidateCache();
await this.fetchSessions();
} finally {
refresher.complete();
@ -163,7 +135,10 @@ export class AddonModChatSessionsPage implements AfterViewInit, OnDestroy {
* @param event The event.
*/
showMoreUsers(session: AddonModChatSessionFormatted, event: Event): void {
session.sessionusers = session.allsessionusers!;
if (session.allsessionusers) {
session.sessionusers = session.allsessionusers;
}
event.stopPropagation();
}
@ -175,68 +150,3 @@ export class AddonModChatSessionsPage implements AfterViewInit, OnDestroy {
}
}
/**
* Helper class to manage sessions.
*/
class AddonChatSessionsManager extends CorePageItemsListManager<AddonModChatSessionFormatted> {
chatId = -1;
groupId = 0;
constructor(pageComponent: unknown) {
super(pageComponent);
}
/**
* Set chat ID.
*
* @param chatId Chat ID.
*/
setChatId(chatId: number): void {
this.chatId = chatId;
}
/**
* Set group ID.
*
* @param groupId Group ID.
*/
setGroupId(groupId: number): void {
this.groupId = groupId;
}
/**
* @inheritdoc
*/
protected getItemPath(session: AddonModChatSessionFormatted): string {
return `${session.sessionstart}/${session.sessionend}`;
}
/**
* @inheritdoc
*/
protected getItemQueryParams(): Params {
return {
chatId: this.chatId,
groupId: this.groupId,
};
}
}
/**
* Fields added to chat session in this view.
*/
type AddonModChatSessionFormatted = Omit<AddonModChatSession, 'sessionusers'> & {
duration?: number; // Session duration.
sessionusers: AddonModChatUserSessionFormatted[];
allsessionusers?: AddonModChatUserSessionFormatted[]; // All session users.
};
/**
* Fields added to user session in this view.
*/
type AddonModChatUserSessionFormatted = AddonModChatSessionUser & {
userfullname?: string; // User full name.
};

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();
@ -121,7 +123,13 @@ export class CoreListItemsManager<
*
* @param item Item.
*/
async select(item: Item): Promise<void> {
async select(item: Item | null): Promise<void> {
if (!item) {
await this.navigateToIndex({ reset: this.resetNavigation() });
return;
}
await this.navigateToItem(item, { reset: this.resetNavigation() });
}
@ -172,17 +180,9 @@ export class CoreListItemsManager<
protected updateSelectedItem(route: ActivatedRouteSnapshot | null = null): void {
super.updateSelectedItem(route);
if (CoreScreen.isMobile || this.selectedItem !== null || this.splitView?.isNested) {
return;
}
const selectDefault = CoreScreen.isTablet && this.selectedItem === null && this.splitView && !this.splitView.isNested;
const defaultItem = this.getDefaultItem();
if (!defaultItem) {
return;
}
this.select(defaultItem);
this.select(selectDefault ? this.getDefaultItem() : this.selectedItem);
}
/**

View File

@ -119,6 +119,34 @@ export abstract class CoreRoutedItemsManager<
await CoreNavigator.navigate(pathPrefix + itemPath, { params, ...options });
}
/**
* Navigate to the index page.
*
* @param options Navigation options.
*/
protected async navigateToIndex(
options: Pick<CoreNavigationOptions, 'reset' | 'replace' | 'animationDirection'> = {},
): Promise<void> {
// Get current route in the page.
const route = this.getCurrentPageRoute();
if (route === null) {
return;
}
// If the current page is already the index, do nothing.
const selectedItemPath = this.getSelectedItemPath(route.snapshot);
if (selectedItemPath === null) {
return;
}
// Navigate to index.
const indexPath = selectedItemPath ? selectedItemPath.split('/').fill('../').join('') : '';
await CoreNavigator.navigate(indexPath, options);
}
/**
* @inheritdoc
*/

View File

@ -1,300 +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 { ActivatedRoute, ActivatedRouteSnapshot, Params, UrlSegment } 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.
*
* @deprecated use CoreListItemsManager instead.
*/
export abstract class CorePageItemsListManager<Item> {
protected itemsList: Item[] | null = null;
protected itemsMap: Record<string, Item> | null = null;
protected hasMoreItems = true;
protected selectedItem: Item | null = null;
protected pageRouteLocator?: unknown | ActivatedRoute;
protected splitView?: CoreSplitViewComponent;
protected splitViewOutletSubscription?: Subscription;
constructor(pageRouteLocator: unknown | ActivatedRoute) {
this.pageRouteLocator = pageRouteLocator;
}
get items(): Item[] {
return this.itemsList || [];
}
get loaded(): boolean {
return this.itemsMap !== null;
}
get completed(): boolean {
return !this.hasMoreItems;
}
get empty(): boolean {
return this.itemsList === null || this.itemsList.length === 0;
}
/**
* Process page started operations.
*
* @param splitView Split view component.
*/
async start(splitView: CoreSplitViewComponent): Promise<void> {
this.watchSplitViewOutlet(splitView);
// Calculate current selected item.
const route = this.getCurrentPageRoute();
this.updateSelectedItem(route?.snapshot ?? null);
// Select default item if none is selected on a non-mobile layout.
if (!CoreScreen.isMobile && this.selectedItem === null && !splitView.isNested) {
const defaultItem = this.getDefaultItem();
if (defaultItem) {
this.select(defaultItem);
}
}
// Log activity.
await CoreUtils.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(this.getPageRouteFromSplitViewOutlet(route)),
);
this.updateSelectedItem(this.getPageRouteFromSplitViewOutlet(splitView.outletRoute) ?? null);
}
/**
* Reset items data.
*/
resetItems(): void {
this.itemsList = null;
this.itemsMap = null;
this.hasMoreItems = true;
this.selectedItem = null;
}
/**
* 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;
}
/**
* Return the current aria value.
*
* @param item Item.
* @return Will return the current value of the item if selected, false otherwise.
*/
getItemAriaCurrent(item: Item): string {
return this.isSelected(item) ? 'page' : 'false';
}
/**
* Select an item.
*
* @param item Item.
*/
async select(item: Item): Promise<void> {
// Get current route in the page.
const route = this.getCurrentPageRoute();
if (route === null) {
return;
}
// If this item is already selected, do nothing.
const itemPath = this.getItemPath(item);
const selectedItemPath = this.getSelectedItemPath(route.snapshot);
if (selectedItemPath === itemPath) {
return;
}
// Navigate to item.
const params = this.getItemQueryParams(item);
const reset = this.resetNavigation();
const pathPrefix = selectedItemPath ? selectedItemPath.split('/').fill('../').join('') : '';
await CoreNavigator.navigate(pathPrefix + itemPath, { params, reset });
}
/**
* Set the list of items.
*
* @param items Items.
* @param hasMoreItems Whether the list has more items that haven't been loaded.
*/
setItems(items: Item[], hasMoreItems: boolean = false): void {
this.hasMoreItems = hasMoreItems;
this.itemsList = items.slice(0);
this.itemsMap = items.reduce((map, item) => {
map[this.getItemPath(item)] = item;
return map;
}, {});
this.updateSelectedItem(this.getPageRouteFromSplitViewOutlet(this.splitView?.outletRoute ?? null));
}
/**
* Log activity when the page starts.
*/
protected async logActivity(): Promise<void> {
//
}
/**
* Update the selected item given the current route.
*
* @param route Current route.
*/
protected updateSelectedItem(route: ActivatedRouteSnapshot | null): void {
const selectedItemPath = this.getSelectedItemPath(route);
this.selectedItem = selectedItemPath
? this.itemsMap?.[selectedItemPath] ?? null
: null;
}
/**
* Check whether to reset navigation when selecting an item.
*
* @returns boolean Whether navigation should be reset.
*/
protected resetNavigation(): boolean {
if (!CoreScreen.isTablet) {
return false;
}
return !!this.splitView && !this.splitView?.isNested;
}
/**
* 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 Page route.
* @return Path of the selected item in the given route.
*/
protected getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null {
const segments: UrlSegment[] = [];
while ((route = route?.firstChild)) {
segments.push(...route.url);
}
return segments.map(segment => segment.path).join('/').replace(/\/+/, '/').trim() || null;
}
/**
* Get page route.
*
* @returns Current page route, if any.
*/
private getCurrentPageRoute(): ActivatedRoute | null {
if (this.pageRouteLocator instanceof ActivatedRoute) {
return CoreNavigator.isRouteActive(this.pageRouteLocator) ? this.pageRouteLocator : null;
}
return CoreNavigator.getCurrentRoute({ pageComponent: this.pageRouteLocator });
}
/**
* Get the page route given a child route on the splitview outlet.
*
* @param route Child route.
* @return Page route.
*/
private getPageRouteFromSplitViewOutlet(route: ActivatedRouteSnapshot | null): ActivatedRouteSnapshot | null {
const isPageRoute = this.buildRouteMatcher();
while (route && !isPageRoute(route)) {
route = route.parent;
}
return route;
}
/**
* Build a function to check whether the given snapshot belongs to the page.
*
* @returns Route matcher.
*/
private buildRouteMatcher(): (route: ActivatedRouteSnapshot) => boolean {
if (this.pageRouteLocator instanceof ActivatedRoute) {
const pageRoutePath = CoreNavigator.getRouteFullPath(this.pageRouteLocator.snapshot);
return route => CoreNavigator.getRouteFullPath(route) === pageRoutePath;
}
return route => route.component === this.pageRouteLocator;
}
}

View File

@ -0,0 +1,41 @@
// (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 { CoreGrades } from '../services/grades';
import { CoreGradesGradeOverviewWithCourseData, CoreGradesHelper } from '../services/grades-helper';
/**
* Provides a collection of courses.
*/
export class CoreGradesCoursesSource extends CoreRoutedItemsManagerSource<CoreGradesGradeOverviewWithCourseData> {
/**
* @inheritdoc
*/
protected async loadPageItems(): Promise<{ items: CoreGradesGradeOverviewWithCourseData[] }> {
const grades = await CoreGrades.getCoursesGrades();
const courses = await CoreGradesHelper.getGradesCourseData(grades);
return { items: courses };
}
/**
* @inheritdoc
*/
getItemPath(course: CoreGradesGradeOverviewWithCourseData): string {
return course.courseid.toString();
}
}

View File

@ -22,10 +22,7 @@ const routes: Routes = [
{
path: '',
component: CoreGradesCoursePage,
data: {
useSplitView: false,
outsideGradesTab: true,
},
data: { swipeEnabled: false },
},
];

View File

@ -22,33 +22,21 @@ import { CoreSharedModule } from '@/core/shared.module';
import { CoreGradesCoursePage } from './pages/course/course.page';
import { CoreGradesCoursePageModule } from './pages/course/course.module';
import { CoreGradesCoursesPage } from './pages/courses/courses.page';
import { CoreGradesGradePage } from './pages/grade/grade.page';
import { CoreGradesUserHandlerService } from './services/handlers/user';
const mobileRoutes: Routes = [
{
path: '',
data: {
mainMenuTabRoot: CoreGradesUserHandlerService.PAGE_NAME,
},
component: CoreGradesCoursesPage,
},
{
path: ':courseId',
component: CoreGradesCoursePage,
},
{
path: ':courseId/:gradeId',
component: CoreGradesGradePage,
},
];
const tabletRoutes: Routes = [
{
path: '',
data: {
mainMenuTabRoot: CoreGradesUserHandlerService.PAGE_NAME,
},
component: CoreGradesCoursesPage,
children: [
{
@ -57,16 +45,6 @@ const tabletRoutes: Routes = [
},
],
},
{
path: ':courseId',
component: CoreGradesCoursePage,
children: [
{
path: ':gradeId',
component: CoreGradesGradePage,
},
],
},
];
const routes: Routes = [
@ -82,7 +60,6 @@ const routes: Routes = [
],
declarations: [
CoreGradesCoursesPage,
CoreGradesGradePage,
],
})
export class CoreGradesLazyModule {}
export class CoreGradesCoursesLazyModule {}

View File

@ -15,16 +15,17 @@
import { APP_INITIALIZER, NgModule, Type } from '@angular/core';
import { Routes } from '@angular/router';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { COURSE_PAGE_NAME } from '@features/course/course.module';
import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index-routing.module';
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CoreUserDelegate } from '@features/user/services/user-delegate';
import { PARTICIPANTS_PAGE_NAME } from '@features/user/user.module';
import { CoreGradesProvider } from './services/grades';
import { CoreGradesHelperProvider } from './services/grades-helper';
import { CoreGradesCourseOptionHandler } from './services/handlers/course-option';
import { CoreGradesOverviewLinkHandler } from './services/handlers/overview-link';
import { CoreGradesUserHandler, CoreGradesUserHandlerService } from './services/handlers/user';
import { CoreGradesUserHandler } from './services/handlers/user';
import { CoreGradesUserLinkHandler } from './services/handlers/user-link';
export const CORE_GRADES_SERVICES: Type<unknown>[] = [
@ -32,28 +33,29 @@ export const CORE_GRADES_SERVICES: Type<unknown>[] = [
CoreGradesHelperProvider,
];
const routes: Routes = [
export const GRADES_PAGE_NAME = 'grades';
const mainMenuChildrenRoutes: Routes = [
{
path: CoreGradesUserHandlerService.PAGE_NAME,
loadChildren: () => import('@features/grades/grades-lazy.module').then(m => m.CoreGradesLazyModule),
path: GRADES_PAGE_NAME,
loadChildren: () => import('./grades-courses-lazy.module').then(m => m.CoreGradesCoursesLazyModule),
},
{
path: 'user-grades/:courseId',
loadChildren: () => import('@features/grades/grades-course-lazy.module').then(m => m.CoreGradesCourseLazyModule),
path: `${COURSE_PAGE_NAME}/:courseId/${PARTICIPANTS_PAGE_NAME}/:userId/${GRADES_PAGE_NAME}`,
loadChildren: () => import('./grades-course-lazy.module').then(m => m.CoreGradesCourseLazyModule),
},
];
const courseIndexRoutes: Routes = [
{
path: 'grades',
loadChildren: () => import('@features/grades/grades-course-lazy.module').then(m => m.CoreGradesCourseLazyModule),
path: GRADES_PAGE_NAME,
loadChildren: () => import('./grades-course-lazy.module').then(m => m.CoreGradesCourseLazyModule),
},
];
@NgModule({
imports: [
CoreMainMenuTabRoutingModule.forChild(routes),
CoreMainMenuRoutingModule.forChild({ children: routes }),
CoreMainMenuTabRoutingModule.forChild(mainMenuChildrenRoutes),
CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }),
],
providers: [

View File

@ -4,36 +4,41 @@
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<h1>{{ 'core.grades.grades' | translate }}</h1>
<h1>{{ title }}</h1>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-split-view [mode]="splitViewMode">
<ion-refresher slot="fixed" [disabled]="!grades.loaded" (ionRefresh)="refreshGrades($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="grades.loaded" class="safe-area-padding">
<core-empty-box *ngIf="grades.empty" icon="fas-chart-bar" [message]="'core.grades.nogradesreturned' | translate">
</core-empty-box>
<div *ngIf="!grades.empty" class="core-grades-container">
<table cellspacing="0" cellpadding="0" class="core-grades-table">
<thead>
<tr>
<th *ngFor="let column of grades.columns" id="{{column.name}}" class="ion-text-start"
[class.ion-hide-md-down]="column.hiddenPhone" [attr.colspan]="column.colspan">
{{ 'core.grades.' + column.name | translate }}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of grades.rows" role="button row" [attr.tabindex]="row.itemtype != 'category' ? 0 : null"
(ariaButtonClick)="row.itemtype != 'category' && grades.select(row)" [class]="row.rowclass"
[ngClass]='{"core-grades-grade-clickable": row.itemtype != "category"}'>
<ion-content [core-swipe-navigation]="courses">
<ion-refresher slot="fixed" [disabled]="!columns || !rows" (ionRefresh)="refreshGrades($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="columns && rows" class="safe-area-padding">
<core-empty-box *ngIf="rows && rows.length === 0" icon="fas-chart-bar" [message]="'core.grades.nogradesreturned' | translate">
</core-empty-box>
<div *ngIf="rows && rows.length > 0" class="core-grades-container">
<table cellspacing="0" cellpadding="0" class="core-grades-table" [class.summary]="showSummary">
<thead>
<tr>
<th *ngFor="let column of columns" id="{{column.name}}" class="ion-text-start"
[class.ion-hide-md-down]="column.hiddenPhone" [attr.colspan]="column.colspan">
{{ 'core.grades.' + column.name | translate }}
</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let row of rows">
<tr [attr.role]="row.expandable && showSummary ? 'button row' : 'row'"
[attr.tabindex]="row.expandable && showSummary && 0" [attr.aria-expanded]="row.expanded"
[attr.aria-label]="rowAriaLabel(row)" [attr.aria-controls]="row.detailsid"
(ariaButtonClick)="row.expandable && showSummary && toggleRow(row)" [class]="row.rowclass"
[class.core-grades-grade-clickable]="row.expandable && showSummary">
<ng-container *ngIf="row.itemtype">
<td *ngIf="row.itemtype == 'category'" class="core-grades-table-category" [attr.rowspan]="row.rowspan"></td>
<th class="core-grades-table-gradeitem ion-text-start" [class.column-itemname]="row.itemtype == 'category'"
[attr.aria-current]="grades.getItemAriaCurrent(row)" [attr.colspan]="row.colspan">
<td *ngIf="row.itemtype == 'category'" class="core-grades-table-category" [attr.rowspan]="row.rowspan">
</td>
<th class="core-grades-table-gradeitem ion-text-start" [attr.colspan]="row.colspan">
<ion-icon *ngIf="row.expandable && showSummary" aria-hidden="true" slot="start"
[name]="row.expanded ? 'fas-caret-down' : 'fas-caret-right'">
</ion-icon>
<ion-icon *ngIf="row.icon" name="{{row.icon}}" slot="start" [attr.aria-label]="row.iconAlt">
</ion-icon>
<img *ngIf="row.image && !row.itemmodule" [src]="row.image" slot="start" class="core-module-icon"
@ -43,16 +48,129 @@
</core-mod-icon>
<span [innerHTML]="row.gradeitem"></span>
</th>
<ng-container *ngFor="let column of grades.columns">
<td *ngIf="column.name != 'gradeitem' && row[column.name] != undefined"
<ng-container *ngFor="let column of columns">
<td *ngIf="column.name !== 'gradeitem' && column.name !== 'feedback' && row[column.name] != undefined"
[class]="'ion-text-start core-grades-table-' + column.name"
[class.ion-hide-md-down]="column.hiddenPhone" [innerHTML]="row[column.name]"></td>
[class.ion-hide-md-down]="column.hiddenPhone" [innerHTML]="row[column.name]">
</td>
<td *ngIf="column.name === 'feedback' && row.feedback !== undefined"
class="ion-text-start core-grades-table-feedback" [class.ion-hide-md-down]="column.hiddenPhone">
<core-format-text [maxHeight]="120" [text]="row.feedback" contextLevel="course"
[contextInstanceId]="courseId">
</core-format-text>
</td>
</ng-container>
</ng-container>
</tr>
</tbody>
</table>
</div>
</core-loading>
</core-split-view>
<tr *ngIf="row.expandable" [id]="row.detailsid" [class]="row.rowclass" [hidden]="!row.expanded">
<td [attr.colspan]="totalColumnsSpan">
<ion-list>
<ion-item *ngIf="row.itemname && row.link" class="ion-text-wrap" detail="true" [href]="row.link"
core-link capture="true">
<ion-icon *ngIf="row.icon" name="{{row.icon}}" slot="start" [attr.aria-label]="row.iconAlt">
</ion-icon>
<img *ngIf="row.image && !row.itemmodule" [src]="row.image && row.itemmodule" slot="start"
[alt]="row.iconAlt" />
<core-mod-icon *ngIf="row.image && row.itemmodule" [modicon]="row.image" slot="start"
[modname]="row.itemmodule">
</core-mod-icon>
<ion-label>
<h2>
<core-format-text [text]="row.itemname" contextLevel="course"
[contextInstanceId]="courseId">
</core-format-text>
</h2>
</ion-label>
</ion-item>
<ion-item *ngIf="row.itemname && !row.link" class="ion-text-wrap">
<ion-icon *ngIf="row.icon" name="{{row.icon}}" slot="start" [attr.aria-label]="row.iconAlt">
</ion-icon>
<img *ngIf="row.image && !row.itemmodule" [src]="row.image" slot="start" [alt]="row.iconAlt" />
<core-mod-icon *ngIf="row.image && row.itemmodule" [modicon]="row.image" slot="start"
[modname]="row.itemmodule">
</core-mod-icon>
<ion-label>
<h2>
<core-format-text [text]="row.itemname" contextLevel="course"
[contextInstanceId]="courseId">
</core-format-text>
</h2>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="row.weight">
<ion-label>
<h2>{{ 'core.grades.weight' | translate}}</h2>
<p [innerHTML]="row.weight"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="row.grade">
<ion-label>
<h2>{{ 'core.grades.grade' | translate}}</h2>
<p [innerHTML]="row.grade"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="row.range">
<ion-label>
<h2>{{ 'core.grades.range' | translate}}</h2>
<p [innerHTML]="row.range"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="row.percentage">
<ion-label>
<h2>{{ 'core.grades.percentage' | translate}}</h2>
<p [innerHTML]="row.percentage"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="row.lettergrade">
<ion-label>
<h2>{{ 'core.grades.lettergrade' | translate}}</h2>
<p [innerHTML]="row.lettergrade"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="row.rank">
<ion-label>
<h2>{{ 'core.grades.rank' | translate}}</h2>
<p [innerHTML]="row.rank"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="row.average">
<ion-label>
<h2>{{ 'core.grades.average' | translate}}</h2>
<p [innerHTML]="row.average"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="row.feedback">
<ion-label>
<h2>{{ 'core.grades.feedback' | translate}}</h2>
<p>
<core-format-text [maxHeight]="120" [text]="row.feedback" contextLevel="course"
[contextInstanceId]="courseId">
</core-format-text>
</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="row.contributiontocoursetotal">
<ion-label>
<h2>{{ 'core.grades.contributiontocoursetotal' | translate}}</h2>
<p [innerHTML]="row.contributiontocoursetotal"></p>
</ion-label>
</ion-item>
</ion-list>
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>
</core-loading>
</ion-content>

View File

@ -12,23 +12,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { ActivatedRoute, Params } from '@angular/router';
import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { AfterViewInit, Component, ElementRef, OnDestroy } from '@angular/core';
import { IonRefresher } from '@ionic/angular';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreGrades } from '@features/grades/services/grades';
import {
CoreGradesFormattedTable,
CoreGradesFormattedTableColumn,
CoreGradesFormattedTableRow,
CoreGradesHelper,
} from '@features/grades/services/grades-helper';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { CoreSplitViewComponent, CoreSplitViewMode } from '@components/split-view/split-view';
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
import { CoreNavigator } from '@services/navigator';
import { CoreScreen } from '@services/screen';
import { Translate } from '@singletons';
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreGradesCoursesSource } from '@features/grades/classes/grades-courses-source';
/**
* Page that displays a course grades.
@ -40,18 +42,29 @@ import { CoreNavigator } from '@services/navigator';
})
export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
grades!: CoreGradesCourseManager;
splitViewMode?: CoreSplitViewMode;
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
constructor(protected route: ActivatedRoute) {
let courseId: number;
let userId: number;
courseId!: number;
userId!: number;
expandLabel!: string;
collapseLabel!: string;
title?: string;
courses?: CoreSwipeNavigationItemsManager;
columns?: CoreGradesFormattedTableColumn[];
rows?: CoreGradesFormattedTableRow[];
totalColumnsSpan?: number;
withinSplitView?: boolean;
constructor(protected route: ActivatedRoute, protected element: ElementRef<HTMLElement>) {
try {
courseId = CoreNavigator.getRequiredRouteNumberParam('courseId', { route });
userId = CoreNavigator.getRouteNumberParam('userId', { route }) ?? CoreSites.getCurrentSiteUserId();
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId', { route });
this.userId = CoreNavigator.getRouteNumberParam('userId', { route }) ?? CoreSites.getCurrentSiteUserId();
this.expandLabel = Translate.instant('core.expand');
this.collapseLabel = Translate.instant('core.collapse');
if (route.snapshot.data.swipeEnabled ?? true) {
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreGradesCoursesSource, []);
this.courses = new CoreSwipeNavigationItemsManager(source);
}
} catch (error) {
CoreDomUtils.showErrorModal(error);
@ -59,28 +72,69 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
return;
}
}
const useSplitView = route.snapshot.data.useSplitView ?? true;
const outsideGradesTab = route.snapshot.data.outsideGradesTab ?? false;
this.splitViewMode = useSplitView ? undefined : CoreSplitViewMode.MENU_ONLY;
this.grades = new CoreGradesCourseManager(CoreGradesCoursePage, courseId, userId, outsideGradesTab);
get showSummary(): boolean {
return CoreScreen.isMobile || !!this.withinSplitView;
}
/**
* @inheritdoc
*/
async ngAfterViewInit(): Promise<void> {
await this.fetchInitialGrades();
this.withinSplitView = !!this.element.nativeElement.parentElement?.closest('core-split-view');
this.grades.start(this.splitView);
await this.courses?.start();
await this.fetchInitialGrades();
await CoreGrades.logCourseGradesView(this.courseId, this.userId);
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.grades.destroy();
this.courses?.destroy();
}
/**
* Get aria label for row.
*
* @param row Row.
* @returns Aria label, if applicable.
*/
rowAriaLabel(row: CoreGradesFormattedTableRow): string | undefined {
if (!row.expandable || !this.showSummary) {
return;
}
const actionLabel = row.expanded ? this.collapseLabel : this.expandLabel;
return `${actionLabel} ${row.ariaLabel}`;
}
/**
* Toggle whether a row is expanded or collapsed.
*
* @param row Row.
*/
toggleRow(row: CoreGradesFormattedTableRow): void {
if (!this.rows || !this.columns) {
return;
}
row.expanded = !row.expanded;
let colspan: number = this.columns.length + (row.colspan ?? 0) - 1;
for (let i = this.rows.indexOf(row) - 1; i >= 0; i--) {
const previousRow = this.rows[i];
if (previousRow.expandable || !previousRow.colspan || !previousRow.rowspan || previousRow.colspan !== colspan) {
continue;
}
colspan++;
previousRow.rowspan += row.expanded ? 1 : -1;
}
}
/**
@ -89,9 +143,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
* @param refresher Refresher.
*/
async refreshGrades(refresher: IonRefresher): Promise<void> {
const { courseId, userId } = this.grades;
await CoreUtils.ignoreErrors(CoreGrades.invalidateCourseGradesData(courseId, userId));
await CoreUtils.ignoreErrors(CoreGrades.invalidateCourseGradesData(this.courseId, this.userId));
await CoreUtils.ignoreErrors(this.fetchGrades());
refresher?.complete();
@ -106,7 +158,8 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading course');
this.grades.setTable({ columns: [], rows: [] });
this.columns = [];
this.rows = [];
}
}
@ -114,99 +167,13 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
* Update the table of grades.
*/
private async fetchGrades(): Promise<void> {
const table = await CoreGrades.getCourseGradesTable(this.grades.courseId, this.grades.userId);
const table = await CoreGrades.getCourseGradesTable(this.courseId, this.userId);
const formattedTable = await CoreGradesHelper.formatGradesTable(table);
this.grades.setTable(formattedTable);
this.title = formattedTable.rows[0]?.gradeitem ?? Translate.instant('core.grades.grades');
this.columns = formattedTable.columns;
this.rows = formattedTable.rows;
this.totalColumnsSpan = formattedTable.columns.reduce((total, column) => total + column.colspan, 0);
}
}
/**
* Helper to manage the table of grades.
*/
class CoreGradesCourseManager extends CorePageItemsListManager<CoreGradesFormattedTableRowFilled> {
courseId: number;
userId: number;
columns?: CoreGradesFormattedTableColumn[];
rows?: CoreGradesFormattedTableRow[];
private outsideGradesTab: boolean;
constructor(pageComponent: unknown, courseId: number, userId: number, outsideGradesTab: boolean) {
super(pageComponent);
this.courseId = courseId;
this.userId = userId;
this.outsideGradesTab = outsideGradesTab;
}
/**
* 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
*/
async select(row: CoreGradesFormattedTableRowFilled): Promise<void> {
if (this.outsideGradesTab) {
await CoreNavigator.navigateToSitePath(`/grades/${this.courseId}/${row.id}`);
return;
}
return super.select(row);
}
/**
* @inheritdoc
*/
protected getDefaultItem(): CoreGradesFormattedTableRowFilled | null {
return null;
}
/**
* @inheritdoc
*/
protected getItemPath(row: CoreGradesFormattedTableRowFilled): string {
return row.id.toString();
}
/**
* @inheritdoc
*/
protected getItemQueryParams(): Params {
return { userId: this.userId };
}
/**
* @inheritdoc
*/
protected async logActivity(): Promise<void> {
await CoreGrades.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;
}
}
export type CoreGradesFormattedTableRowFilled = Omit<CoreGradesFormattedTableRow, 'id'> & {
id: number;
};

View File

@ -61,11 +61,15 @@
background-color: var(--header-background);
}
thead #gradeitem {
@include padding(null, null, null, 23px);
}
tbody th {
font-weight: normal;
}
#gradeitem {
tbody #gradeitem {
@include padding(null, null, null, 5px);
}
@ -131,12 +135,15 @@
}
}
}
ion-list, ion-item::part(native) {
background-color: transparent;
}
&.summary .ion-hide-md-down {
display: none;
opacity: 0;
}
core-split-view.nested .core-grades-table .ion-hide-md-down,
core-split-view.menu-and-content .core-grades-table .ion-hide-md-down {
display: none;
opacity: 0;
}
@include media-breakpoint-down(md) {

View File

@ -13,11 +13,12 @@
// limitations under the License.
import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreGradesCoursesSource } from '@features/grades/classes/grades-courses-source';
import { CoreGrades } from '@features/grades/services/grades';
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';
@ -31,10 +32,16 @@ import { CoreUtils } from '@services/utils/utils';
})
export class CoreGradesCoursesPage implements OnDestroy, AfterViewInit {
courses: CoreGradesCoursesManager = new CoreGradesCoursesManager(CoreGradesCoursesPage);
courses: CoreGradesCoursesManager;
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
constructor() {
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreGradesCoursesSource, []);
this.courses = new CoreGradesCoursesManager(source, CoreGradesCoursesPage);
}
/**
* @inheritdoc
*/
@ -58,7 +65,7 @@ export class CoreGradesCoursesPage implements OnDestroy, AfterViewInit {
*/
async refreshCourses(refresher: IonRefresher): Promise<void> {
await CoreUtils.ignoreErrors(CoreGrades.invalidateCoursesGradesData());
await CoreUtils.ignoreErrors(this.fetchCourses());
await CoreUtils.ignoreErrors(this.courses.reload());
refresher?.complete();
}
@ -68,37 +75,18 @@ export class CoreGradesCoursesPage implements OnDestroy, AfterViewInit {
*/
private async fetchInitialCourses(): Promise<void> {
try {
await this.fetchCourses();
await this.courses.load();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading courses');
this.courses.setItems([]);
}
}
/**
* Update the list of courses.
*/
private async fetchCourses(): Promise<void> {
const grades = await CoreGrades.getCoursesGrades();
const courses = await CoreGradesHelper.getGradesCourseData(grades);
this.courses.setItems(courses);
}
}
/**
* Helper class to manage courses.
*/
class CoreGradesCoursesManager extends CorePageItemsListManager<CoreGradesGradeOverviewWithCourseData> {
/**
* @inheritdoc
*/
protected getItemPath(courseGrade: CoreGradesGradeOverviewWithCourseData): string {
return courseGrade.courseid.toString();
}
class CoreGradesCoursesManager extends CoreListItemsManager {
/**
* @inheritdoc

View File

@ -1,112 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<h1>{{ 'core.grades.grade' | translate }}</h1>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!gradeLoaded" (ionRefresh)="refreshGrade($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="gradeLoaded">
<core-empty-box *ngIf="!grade" icon="fas-chart-bar" [message]="'core.grades.nogradesreturned' | translate"></core-empty-box>
<ion-list *ngIf="grade">
<ion-item *ngIf="grade.itemname && grade.link" class="ion-text-wrap" detail="true" [href]="grade.link" core-link capture="true">
<ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="start" [attr.aria-label]="grade.iconAlt"></ion-icon>
<img *ngIf="grade.image && !grade.itemmodule" [src]="grade.image && grade.itemmodule" slot="start" [alt]="grade.iconAlt" />
<core-mod-icon *ngIf="grade.image && grade.itemmodule" [modicon]="grade.image" slot="start" [modname]="grade.itemmodule">
</core-mod-icon>
<ion-label>
<h2>
<core-format-text [text]="grade.itemname" contextLevel="course" [contextInstanceId]="courseId">
</core-format-text>
</h2>
</ion-label>
</ion-item>
<ion-item *ngIf="grade.itemname && !grade.link" class="ion-text-wrap">
<ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="start" [attr.aria-label]="grade.iconAlt"></ion-icon>
<img *ngIf="grade.image && !grade.itemmodule" [src]="grade.image" slot="start" [alt]="grade.iconAlt" />
<core-mod-icon *ngIf="grade.image && grade.itemmodule" [modicon]="grade.image" slot="start" [modname]="grade.itemmodule">
</core-mod-icon>
<ion-label>
<h2>
<core-format-text [text]="grade.itemname" contextLevel="course" [contextInstanceId]="courseId">
</core-format-text>
</h2>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.weight">
<ion-label>
<h2>{{ 'core.grades.weight' | translate}}</h2>
<p [innerHTML]="grade.weight"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.grade">
<ion-label>
<h2>{{ 'core.grades.grade' | translate}}</h2>
<p [innerHTML]="grade.grade"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.range">
<ion-label>
<h2>{{ 'core.grades.range' | translate}}</h2>
<p [innerHTML]="grade.range"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.percentage">
<ion-label>
<h2>{{ 'core.grades.percentage' | translate}}</h2>
<p [innerHTML]="grade.percentage"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.lettergrade">
<ion-label>
<h2>{{ 'core.grades.lettergrade' | translate}}</h2>
<p [innerHTML]="grade.lettergrade"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.rank">
<ion-label>
<h2>{{ 'core.grades.rank' | translate}}</h2>
<p [innerHTML]="grade.rank"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.average">
<ion-label>
<h2>{{ 'core.grades.average' | translate}}</h2>
<p [innerHTML]="grade.average"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.feedback">
<ion-label>
<h2>{{ 'core.grades.feedback' | translate}}</h2>
<p>
<core-format-text [maxHeight]="120" [text]="grade.feedback" contextLevel="course" [contextInstanceId]="courseId">
</core-format-text>
</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.contributiontocoursetotal">
<ion-label>
<h2>{{ 'core.grades.contributiontocoursetotal' | translate}}</h2>
<p [innerHTML]="grade.contributiontocoursetotal"></p>
</ion-label>
</ion-item>
</ion-list>
</core-loading>
</ion-content>

View File

@ -1,85 +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 { CoreDomUtils } from '@services/utils/dom';
import { CoreGrades } from '@features/grades/services/grades';
import { CoreGradesFormattedRow, CoreGradesHelper } from '@features/grades/services/grades-helper';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { CoreNavigator } from '@services/navigator';
/**
* Page that displays activity grade.
*/
@Component({
selector: 'page-core-grades-grade',
templateUrl: 'grade.html',
})
export class CoreGradesGradePage implements OnInit {
courseId!: number;
userId!: number;
gradeId!: number;
grade?: CoreGradesFormattedRow | null;
gradeLoaded = false;
constructor() {
try {
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.gradeId = CoreNavigator.getRequiredRouteNumberParam('gradeId');
this.userId = CoreNavigator.getRouteNumberParam('userId') ?? CoreSites.getCurrentSiteUserId();
} catch (error) {
CoreDomUtils.showErrorModal(error);
CoreNavigator.back();
return;
}
}
/**
* @inheritdoc
*/
ngOnInit(): void {
this.fetchGrade();
}
/**
* Fetch all the data required for the view.
*/
async fetchGrade(): Promise<void> {
try {
this.grade = await CoreGradesHelper.getGradeItem(this.courseId, this.gradeId, this.userId);
this.gradeLoaded = true;
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading grade item');
}
}
/**
* Refresh data.
*
* @param refresher Refresher.
*/
async refreshGrade(refresher: IonRefresher): Promise<void> {
await CoreUtils.ignoreErrors(CoreGrades.invalidateCourseGradesData(this.courseId, this.userId));
await CoreUtils.ignoreErrors(this.fetchGrade());
refresher.complete();
}
}

View File

@ -35,6 +35,7 @@ import { CoreNavigator } from '@services/navigator';
import { makeSingleton, Translate } from '@singletons';
import { CoreError } from '@classes/errors/error';
import { CoreCourseHelper } from '@features/course/services/course-helper';
import { GRADES_PAGE_NAME } from '../grades.module';
/**
* Service that provides some features regarding grades information.
@ -53,6 +54,7 @@ export class CoreGradesHelperProvider {
*
* @param tableRow JSON object representing row of grades table data.
* @return Formatted row object.
* @deprecated since app 4.0
*/
protected async formatGradeRow(tableRow: CoreGradesTableRow): Promise<CoreGradesFormattedRow> {
const row: CoreGradesFormattedRow = {
@ -125,6 +127,13 @@ export class CoreGradesHelperProvider {
content = CoreTextUtils.replaceNewLines(content, '<br>');
}
if (row.itemtype !== 'category') {
row.expandable = true;
row.expanded = false;
row.detailsid = `grade-item-${row.id}-details`;
row.ariaLabel = `${row.gradeitem} (${row.grade})`;
}
if (content == '&nbsp;') {
content = '';
}
@ -279,6 +288,7 @@ export class CoreGradesHelperProvider {
* @param siteId Site ID. If not defined, current site.
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
* @return Promise to be resolved when the grades are retrieved.
* @deprecated since app 4.0
*/
async getGradeItem(
courseId: number,
@ -381,6 +391,7 @@ export class CoreGradesHelperProvider {
* @param table JSON object representing a table with data.
* @param gradeId Grade Object identifier.
* @return Formatted HTML table.
* @deprecated since app 4.0
*/
async getGradesTableRow(table: CoreGradesTable, gradeId: number): Promise<CoreGradesFormattedRow | null> {
if (table.tabledata) {
@ -472,10 +483,7 @@ export class CoreGradesHelperProvider {
const gradeId = item.id;
await CoreUtils.ignoreErrors(
CoreNavigator.navigateToSitePath(`/grades/${courseId}/${gradeId}`, {
siteId,
params: { userId },
}),
CoreNavigator.navigateToSitePath(`/${GRADES_PAGE_NAME}/${courseId}/${gradeId}`, { siteId }),
);
} catch (error) {
try {
@ -483,10 +491,7 @@ export class CoreGradesHelperProvider {
if (userId && userId != currentUserId) {
// View another user grades. Open the grades page directly.
await CoreUtils.ignoreErrors(
CoreNavigator.navigateToSitePath(`/grades/${courseId}`, {
siteId,
params: { userId },
}),
CoreNavigator.navigateToSitePath(`/${GRADES_PAGE_NAME}/${courseId}`, { siteId }),
);
}
@ -502,7 +507,7 @@ export class CoreGradesHelperProvider {
await CoreCourseHelper.getAndOpenCourse(courseId, { selectedTab: 'CoreGrades' }, siteId);
} catch (error) {
// Cannot get course for some reason, just open the grades page.
await CoreNavigator.navigateToSitePath(`/grades/${courseId}`, { siteId });
await CoreNavigator.navigateToSitePath(`/${GRADES_PAGE_NAME}/${courseId}`, { siteId });
}
} finally {
modal.dismiss();
@ -710,8 +715,12 @@ export type CoreGradesFormattedTable = {
export type CoreGradesFormattedTableRow = CoreGradesFormattedRowCommonData & {
id?: number;
detailsid?: string;
colspan?: number;
gradeitem?: string; // The item returned data.
ariaLabel?: string;
expandable?: boolean;
expanded?: boolean;
};
export type CoreGradesFormattedTableColumn = {

View File

@ -15,6 +15,7 @@
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { GRADES_PAGE_NAME } from '@features/grades/grades.module';
import { CoreNavigator } from '@services/navigator';
import { makeSingleton } from '@singletons';
import { CoreGrades } from '../grades';
@ -36,10 +37,7 @@ export class CoreGradesOverviewLinkHandlerService extends CoreContentLinksHandle
getActions(): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [{
action: siteId => {
CoreNavigator.navigateToSitePath('/grades', {
siteId,
preferCurrentTab: false,
});
CoreNavigator.navigateToSitePath(GRADES_PAGE_NAME, { siteId });
},
}];
}

View File

@ -13,6 +13,8 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { COURSE_PAGE_NAME } from '@features/course/course.module';
import { GRADES_PAGE_NAME } from '@features/grades/grades.module';
import { CoreGrades } from '@features/grades/services/grades';
import { CoreUserProfile } from '@features/user/services/user';
@ -21,6 +23,7 @@ import {
CoreUserProfileHandler,
CoreUserProfileHandlerData,
} from '@features/user/services/user-delegate';
import { PARTICIPANTS_PAGE_NAME } from '@features/user/user.module';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
@ -32,8 +35,6 @@ import { makeSingleton } from '@singletons';
@Injectable({ providedIn: 'root' })
export class CoreGradesUserHandlerService implements CoreUserProfileHandler {
static readonly PAGE_NAME = 'grades';
name = 'CoreGrades:viewGrades';
priority = 400;
type = CoreUserDelegateService.TYPE_NEW_PAGE;
@ -81,9 +82,9 @@ export class CoreGradesUserHandlerService implements CoreUserProfileHandler {
action: (event, user, courseId): void => {
event.preventDefault();
event.stopPropagation();
CoreNavigator.navigateToSitePath(`/user-grades/${courseId}`, {
params: { userId: user.id },
});
CoreNavigator.navigateToSitePath(
[COURSE_PAGE_NAME, courseId, PARTICIPANTS_PAGE_NAME, user.id, GRADES_PAGE_NAME].join('/'),
);
},
};
} else {
@ -94,7 +95,7 @@ export class CoreGradesUserHandlerService implements CoreUserProfileHandler {
action: (event): void => {
event.preventDefault();
event.stopPropagation();
CoreNavigator.navigateToSitePath(CoreGradesUserHandlerService.PAGE_NAME);
CoreNavigator.navigateToSitePath(GRADES_PAGE_NAME);
},
};
}

View File

@ -0,0 +1,45 @@
// (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 { CoreSettingsDelegate, CoreSettingsHandlerToDisplay } from '../services/settings-delegate';
/**
* Provides a collection of site settings.
*/
export class CoreSettingsHandlersSource extends CoreRoutedItemsManagerSource<CoreSettingsHandlerToDisplay> {
/**
* @inheritdoc
*/
protected async loadPageItems(): Promise<{ items: CoreSettingsHandlerToDisplay[] }> {
return { items: CoreSettingsDelegate.getHandlers() };
}
/**
* @inheritdoc
*/
getItemPath(handler: CoreSettingsHandlerToDisplay): string {
return handler.page;
}
/**
* @inheritdoc
*/
getItemQueryParams(handler: CoreSettingsHandlerToDisplay): Params {
return handler.params || {};
}
}

View File

@ -0,0 +1,90 @@
// (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 { CoreConstants } from '@/core/constants';
import { Params } from '@angular/router';
import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source';
import { SHAREDFILES_PAGE_NAME } from '@features/sharedfiles/sharedfiles.module';
import { CoreApp } from '@services/app';
/**
* Provides a collection of setting sections.
*/
export class CoreSettingsSectionsSource extends CoreRoutedItemsManagerSource<CoreSettingsSection> {
/**
* @inheritdoc
*/
protected async loadPageItems(): Promise<{ items: CoreSettingsSection[] }> {
const sections: CoreSettingsSection[] = [
{
name: 'core.settings.general',
path: 'general',
icon: 'fas-wrench',
},
{
name: 'core.settings.spaceusage',
path: 'spaceusage',
icon: 'fas-tasks',
},
{
name: 'core.settings.synchronization',
path: 'sync',
icon: CoreConstants.ICON_SYNC,
},
];
if (CoreApp.isIOS()) {
sections.push({
name: 'core.sharedfiles.sharedfiles',
path: SHAREDFILES_PAGE_NAME + '/list/root',
icon: 'fas-folder',
params: { manage: true },
});
}
sections.push({
name: 'core.settings.about',
path: 'about',
icon: 'fas-id-card',
});
return { items: sections };
}
/**
* @inheritdoc
*/
getItemPath(section: CoreSettingsSection): string {
return section.path;
}
/**
* @inheritdoc
*/
getItemQueryParams(section: CoreSettingsSection): Params {
return section.params || {};
}
}
/**
* Settings section.
*/
export type CoreSettingsSection = {
name: string;
path: string;
icon: string;
params?: Params;
};

View File

@ -13,13 +13,11 @@
// limitations under the License.
import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
import { Params } from '@angular/router';
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreConstants } from '@/core/constants';
import { SHAREDFILES_PAGE_NAME } from '@features/sharedfiles/sharedfiles.module';
import { CoreApp } from '@services/app';
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
import { CoreSettingsSection, CoreSettingsSectionsSource } from '@features/settings/classes/settings-sections-source';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
@Component({
selector: 'page-core-settings-index',
@ -27,16 +25,22 @@ import { CoreApp } from '@services/app';
})
export class CoreSettingsIndexPage implements AfterViewInit, OnDestroy {
sections: CoreSettingsSectionsManager = new CoreSettingsSectionsManager(CoreSettingsIndexPage);
sections: CoreListItemsManager<CoreSettingsSection>;
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
constructor() {
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreSettingsSectionsSource, []);
this.sections = new CoreListItemsManager(source, CoreSettingsIndexPage);
}
/**
* @inheritdoc
*/
ngAfterViewInit(): void {
this.sections.setItems(this.getSections());
this.sections.start(this.splitView);
async ngAfterViewInit(): Promise<void> {
await this.sections.load();
await this.sections.start(this.splitView);
}
/**
@ -46,77 +50,4 @@ export class CoreSettingsIndexPage implements AfterViewInit, OnDestroy {
this.sections.destroy();
}
/**
* Get the sections.
*
* @returns Sections.
*/
protected getSections(): CoreSettingsSection[] {
const sections: CoreSettingsSection[] = [
{
name: 'core.settings.general',
path: 'general',
icon: 'fas-wrench',
},
{
name: 'core.settings.spaceusage',
path: 'spaceusage',
icon: 'fas-tasks',
},
{
name: 'core.settings.synchronization',
path: 'sync',
icon: CoreConstants.ICON_SYNC,
},
];
if (CoreApp.isIOS()) {
sections.push({
name: 'core.sharedfiles.sharedfiles',
path: SHAREDFILES_PAGE_NAME + '/list/root',
icon: 'fas-folder',
params: { manage: true },
});
}
sections.push({
name: 'core.settings.about',
path: 'about',
icon: 'fas-id-card',
});
return sections;
}
}
/**
* Helper class to manage sections.
*/
class CoreSettingsSectionsManager extends CorePageItemsListManager<CoreSettingsSection> {
/**
* @inheritdoc
*/
protected getItemPath(section: CoreSettingsSection): string {
return section.path;
}
/**
* @inheritdoc
*/
protected getItemQueryParams(section: CoreSettingsSection): Params {
return section.params || {};
}
}
/**
* Settings section.
*/
export type CoreSettingsSection = {
name: string;
path: string;
icon: string;
params?: Params;
};

View File

@ -13,10 +13,9 @@
// limitations under the License.
import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
import { Params } from '@angular/router';
import { IonRefresher } from '@ionic/angular';
import { CoreSettingsDelegate, CoreSettingsHandlerToDisplay } from '../../services/settings-delegate';
import { CoreSettingsHandlerToDisplay } from '../../services/settings-delegate';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
@ -24,8 +23,10 @@ import { CoreSettingsHelper, CoreSiteSpaceUsage } from '../../services/settings-
import { CoreApp } from '@services/app';
import { Translate } from '@singletons';
import { CoreNavigator } from '@services/navigator';
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreSettingsHandlersSource } from '@features/settings/classes/settings-handlers-source';
/**
* Page that displays the list of site settings pages.
@ -38,7 +39,7 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
handlers: CoreSettingsSitePreferencesManager;
handlers: CoreListItemsManager<CoreSettingsHandlerToDisplay>;
isIOS: boolean;
siteId: string;
@ -51,10 +52,12 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
protected isDestroyed = false;
constructor() {
this.isIOS = CoreApp.isIOS();
this.siteId = CoreSites.getCurrentSiteId();
this.handlers = new CoreSettingsSitePreferencesManager(CoreSitePreferencesPage);
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreSettingsHandlersSource, []);
this.handlers = new CoreListItemsManager(source, CoreSitePreferencesPage);
this.sitesObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
this.refreshData();
@ -70,14 +73,14 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
try {
await this.fetchData();
} finally {
const handler = pageToOpen ? this.handlers.items.find(handler => handler.page == pageToOpen) : undefined;
if (handler) {
this.handlers.select(handler);
this.handlers.watchSplitViewOutlet(this.splitView);
await this.handlers.select(handler);
} else {
this.handlers.start(this.splitView);
await this.handlers.start(this.splitView);
}
}
}
@ -86,7 +89,7 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
* Fetch Data.
*/
protected async fetchData(): Promise<void> {
this.handlers.setItems(CoreSettingsDelegate.getHandlers());
await this.handlers.load();
this.spaceUsage = await CoreSettingsHelper.getSiteSpaceUsage(this.siteId);
}
@ -122,6 +125,7 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
* @param refresher Refresher.
*/
refreshData(refresher?: IonRefresher): void {
this.handlers.getSource().setDirty(true);
this.fetchData().finally(() => {
refresher?.complete();
});
@ -171,24 +175,3 @@ export class CoreSitePreferencesPage implements AfterViewInit, OnDestroy {
}
}
/**
* Helper class to manage sections.
*/
class CoreSettingsSitePreferencesManager extends CorePageItemsListManager<CoreSettingsHandlerToDisplay> {
/**
* @inheritdoc
*/
protected getItemPath(handler: CoreSettingsHandlerToDisplay): string {
return handler.page;
}
/**
* @inheritdoc
*/
protected getItemQueryParams(handler: CoreSettingsHandlerToDisplay): Params {
return handler.params || {};
}
}