MOBILE-3934 grades: Move details into accordion
parent
803ad32ec8
commit
275991d9ef
|
@ -22,10 +22,6 @@ const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: CoreGradesCoursePage,
|
component: CoreGradesCoursePage,
|
||||||
data: {
|
|
||||||
useSplitView: false,
|
|
||||||
outsideGradesTab: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ import { CoreSharedModule } from '@/core/shared.module';
|
||||||
import { CoreGradesCoursePage } from './pages/course/course.page';
|
import { CoreGradesCoursePage } from './pages/course/course.page';
|
||||||
import { CoreGradesCoursePageModule } from './pages/course/course.module';
|
import { CoreGradesCoursePageModule } from './pages/course/course.module';
|
||||||
import { CoreGradesCoursesPage } from './pages/courses/courses.page';
|
import { CoreGradesCoursesPage } from './pages/courses/courses.page';
|
||||||
import { CoreGradesGradePage } from './pages/grade/grade.page';
|
|
||||||
|
|
||||||
const mobileRoutes: Routes = [
|
const mobileRoutes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -33,10 +32,6 @@ const mobileRoutes: Routes = [
|
||||||
path: ':courseId',
|
path: ':courseId',
|
||||||
component: CoreGradesCoursePage,
|
component: CoreGradesCoursePage,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: ':courseId/:gradeId',
|
|
||||||
component: CoreGradesGradePage,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const tabletRoutes: Routes = [
|
const tabletRoutes: Routes = [
|
||||||
|
@ -50,16 +45,6 @@ const tabletRoutes: Routes = [
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: ':courseId',
|
|
||||||
component: CoreGradesCoursePage,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: ':gradeId',
|
|
||||||
component: CoreGradesGradePage,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
|
@ -75,7 +60,6 @@ const routes: Routes = [
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CoreGradesCoursesPage,
|
CoreGradesCoursesPage,
|
||||||
CoreGradesGradePage,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreGradesCoursesLazyModule {}
|
export class CoreGradesCoursesLazyModule {}
|
||||||
|
|
|
@ -9,31 +9,36 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<core-split-view [mode]="splitViewMode">
|
<ion-refresher slot="fixed" [disabled]="!columns || !rows" (ionRefresh)="refreshGrades($event.target)">
|
||||||
<ion-refresher slot="fixed" [disabled]="!grades.loaded" (ionRefresh)="refreshGrades($event.target)">
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
</ion-refresher>
|
||||||
</ion-refresher>
|
<core-loading [hideUntil]="columns && rows" class="safe-area-padding">
|
||||||
<core-loading [hideUntil]="grades.loaded" class="safe-area-padding">
|
<core-empty-box *ngIf="rows && rows.length === 0" icon="fas-chart-bar" [message]="'core.grades.nogradesreturned' | translate">
|
||||||
<core-empty-box *ngIf="grades.empty" icon="fas-chart-bar" [message]="'core.grades.nogradesreturned' | translate">
|
</core-empty-box>
|
||||||
</core-empty-box>
|
<div *ngIf="rows && rows.length > 0" class="core-grades-container">
|
||||||
<div *ngIf="!grades.empty" class="core-grades-container">
|
<table cellspacing="0" cellpadding="0" class="core-grades-table" [class.summary]="showSummary">
|
||||||
<table cellspacing="0" cellpadding="0" class="core-grades-table">
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th *ngFor="let column of columns" id="{{column.name}}" class="ion-text-start"
|
||||||
<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">
|
||||||
[class.ion-hide-md-down]="column.hiddenPhone" [attr.colspan]="column.colspan">
|
{{ 'core.grades.' + column.name | translate }}
|
||||||
{{ 'core.grades.' + column.name | translate }}
|
</th>
|
||||||
</th>
|
</tr>
|
||||||
</tr>
|
</thead>
|
||||||
</thead>
|
<tbody>
|
||||||
<tbody>
|
<ng-container *ngFor="let row of rows">
|
||||||
<tr *ngFor="let row of grades.rows" role="button row" [attr.tabindex]="row.itemtype != 'category' ? 0 : null"
|
<tr [attr.role]="row.expandable && showSummary ? 'button row' : 'row'"
|
||||||
(ariaButtonClick)="row.itemtype != 'category' && grades.select(row)" [class]="row.rowclass"
|
[attr.tabindex]="row.expandable && showSummary && 0" [attr.aria-expanded]="row.expanded"
|
||||||
[ngClass]='{"core-grades-grade-clickable": row.itemtype != "category"}'>
|
[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">
|
<ng-container *ngIf="row.itemtype">
|
||||||
<td *ngIf="row.itemtype == 'category'" class="core-grades-table-category" [attr.rowspan]="row.rowspan"></td>
|
<td *ngIf="row.itemtype == 'category'" class="core-grades-table-category" [attr.rowspan]="row.rowspan">
|
||||||
<th class="core-grades-table-gradeitem ion-text-start" [class.column-itemname]="row.itemtype == 'category'"
|
</td>
|
||||||
[attr.aria-current]="grades.getItemAriaCurrent(row)" [attr.colspan]="row.colspan">
|
<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 *ngIf="row.icon" name="{{row.icon}}" slot="start" [attr.aria-label]="row.iconAlt">
|
||||||
</ion-icon>
|
</ion-icon>
|
||||||
<img *ngIf="row.image && !row.itemmodule" [src]="row.image" slot="start" class="core-module-icon"
|
<img *ngIf="row.image && !row.itemmodule" [src]="row.image" slot="start" class="core-module-icon"
|
||||||
|
@ -43,16 +48,122 @@
|
||||||
</core-mod-icon>
|
</core-mod-icon>
|
||||||
<span [innerHTML]="row.gradeitem"></span>
|
<span [innerHTML]="row.gradeitem"></span>
|
||||||
</th>
|
</th>
|
||||||
<ng-container *ngFor="let column of grades.columns">
|
<ng-container *ngFor="let column of columns">
|
||||||
<td *ngIf="column.name != 'gradeitem' && row[column.name] != undefined"
|
<td *ngIf="column.name != 'gradeitem' && row[column.name] != undefined"
|
||||||
[class]="'ion-text-start core-grades-table-' + column.name"
|
[class]="'ion-text-start core-grades-table-' + column.name"
|
||||||
[class.ion-hide-md-down]="column.hiddenPhone" [innerHTML]="row[column.name]"></td>
|
[class.ion-hide-md-down]="column.hiddenPhone" [innerHTML]="row[column.name]"></td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
<tr *ngIf="row.expandable" [id]="row.detailsid" [class]="row.rowclass" [hidden]="!row.expanded">
|
||||||
</table>
|
<td [attr.colspan]="totalColumnsSpan">
|
||||||
</div>
|
<ion-list>
|
||||||
</core-loading>
|
<ion-item *ngIf="row.itemname && row.link" class="ion-text-wrap" detail="true" [href]="row.link"
|
||||||
</core-split-view>
|
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>
|
</ion-content>
|
||||||
|
|
|
@ -12,23 +12,22 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
|
import { AfterViewInit, Component, ElementRef } from '@angular/core';
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreGrades } from '@features/grades/services/grades';
|
import { CoreGrades } from '@features/grades/services/grades';
|
||||||
import {
|
import {
|
||||||
CoreGradesFormattedTable,
|
|
||||||
CoreGradesFormattedTableColumn,
|
CoreGradesFormattedTableColumn,
|
||||||
CoreGradesFormattedTableRow,
|
CoreGradesFormattedTableRow,
|
||||||
CoreGradesHelper,
|
CoreGradesHelper,
|
||||||
} from '@features/grades/services/grades-helper';
|
} from '@features/grades/services/grades-helper';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreSplitViewComponent, CoreSplitViewMode } from '@components/split-view/split-view';
|
|
||||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreScreen } from '@services/screen';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays a course grades.
|
* Page that displays a course grades.
|
||||||
|
@ -38,20 +37,23 @@ import { CoreNavigator } from '@services/navigator';
|
||||||
templateUrl: 'course.html',
|
templateUrl: 'course.html',
|
||||||
styleUrls: ['course.scss'],
|
styleUrls: ['course.scss'],
|
||||||
})
|
})
|
||||||
export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
|
export class CoreGradesCoursePage implements AfterViewInit {
|
||||||
|
|
||||||
grades!: CoreGradesCourseManager;
|
courseId!: number;
|
||||||
splitViewMode?: CoreSplitViewMode;
|
userId!: number;
|
||||||
|
expandLabel!: string;
|
||||||
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
collapseLabel!: string;
|
||||||
|
columns?: CoreGradesFormattedTableColumn[];
|
||||||
constructor(protected route: ActivatedRoute) {
|
rows?: CoreGradesFormattedTableRow[];
|
||||||
let courseId: number;
|
totalColumnsSpan?: number;
|
||||||
let userId: number;
|
withinSplitView?: boolean;
|
||||||
|
|
||||||
|
constructor(protected route: ActivatedRoute, protected element: ElementRef<HTMLElement>) {
|
||||||
try {
|
try {
|
||||||
courseId = CoreNavigator.getRequiredRouteNumberParam('courseId', { route });
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId', { route });
|
||||||
userId = CoreNavigator.getRouteNumberParam('userId', { route }) ?? CoreSites.getCurrentSiteUserId();
|
this.userId = CoreNavigator.getRouteNumberParam('userId', { route }) ?? CoreSites.getCurrentSiteUserId();
|
||||||
|
this.expandLabel = Translate.instant('core.expand');
|
||||||
|
this.collapseLabel = Translate.instant('core.collapse');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
@ -59,28 +61,61 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const useSplitView = route.snapshot.data.useSplitView ?? true;
|
get showSummary(): boolean {
|
||||||
const outsideGradesTab = route.snapshot.data.outsideGradesTab ?? false;
|
return CoreScreen.isMobile || !!this.withinSplitView;
|
||||||
|
|
||||||
this.splitViewMode = useSplitView ? undefined : CoreSplitViewMode.MENU_ONLY;
|
|
||||||
this.grades = new CoreGradesCourseManager(CoreGradesCoursePage, courseId, userId, outsideGradesTab);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngAfterViewInit(): Promise<void> {
|
async ngAfterViewInit(): Promise<void> {
|
||||||
await this.fetchInitialGrades();
|
this.withinSplitView = !!this.element.nativeElement.parentElement?.closest('core-split-view');
|
||||||
|
|
||||||
this.grades.start(this.splitView);
|
await this.fetchInitialGrades();
|
||||||
|
await CoreGrades.logCourseGradesView(this.courseId, this.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* Get aria label for row.
|
||||||
|
*
|
||||||
|
* @param row Row.
|
||||||
|
* @returns Aria label, if applicable.
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
rowAriaLabel(row: CoreGradesFormattedTableRow): string | undefined {
|
||||||
this.grades.destroy();
|
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 +124,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
|
||||||
* @param refresher Refresher.
|
* @param refresher Refresher.
|
||||||
*/
|
*/
|
||||||
async refreshGrades(refresher: IonRefresher): Promise<void> {
|
async refreshGrades(refresher: IonRefresher): Promise<void> {
|
||||||
const { courseId, userId } = this.grades;
|
await CoreUtils.ignoreErrors(CoreGrades.invalidateCourseGradesData(this.courseId, this.userId));
|
||||||
|
|
||||||
await CoreUtils.ignoreErrors(CoreGrades.invalidateCourseGradesData(courseId, userId));
|
|
||||||
await CoreUtils.ignoreErrors(this.fetchGrades());
|
await CoreUtils.ignoreErrors(this.fetchGrades());
|
||||||
|
|
||||||
refresher?.complete();
|
refresher?.complete();
|
||||||
|
@ -106,7 +139,8 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'Error loading course');
|
CoreDomUtils.showErrorModalDefault(error, 'Error loading course');
|
||||||
|
|
||||||
this.grades.setTable({ columns: [], rows: [] });
|
this.columns = [];
|
||||||
|
this.rows = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,103 +148,12 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
|
||||||
* Update the table of grades.
|
* Update the table of grades.
|
||||||
*/
|
*/
|
||||||
private async fetchGrades(): Promise<void> {
|
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);
|
const formattedTable = await CoreGradesHelper.formatGradesTable(table);
|
||||||
|
|
||||||
this.grades.setTable(formattedTable);
|
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}`, {
|
|
||||||
params: {
|
|
||||||
userId: this.userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
|
@ -61,11 +61,15 @@
|
||||||
background-color: var(--header-background);
|
background-color: var(--header-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
thead #gradeitem {
|
||||||
|
@include padding(null, null, null, 23px);
|
||||||
|
}
|
||||||
|
|
||||||
tbody th {
|
tbody th {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
#gradeitem {
|
tbody #gradeitem {
|
||||||
@include padding(null, null, null, 5px);
|
@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) {
|
@include media-breakpoint-down(md) {
|
||||||
|
|
|
@ -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>
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -54,6 +54,7 @@ export class CoreGradesHelperProvider {
|
||||||
*
|
*
|
||||||
* @param tableRow JSON object representing row of grades table data.
|
* @param tableRow JSON object representing row of grades table data.
|
||||||
* @return Formatted row object.
|
* @return Formatted row object.
|
||||||
|
* @deprecated since app 4.0
|
||||||
*/
|
*/
|
||||||
protected async formatGradeRow(tableRow: CoreGradesTableRow): Promise<CoreGradesFormattedRow> {
|
protected async formatGradeRow(tableRow: CoreGradesTableRow): Promise<CoreGradesFormattedRow> {
|
||||||
const row: CoreGradesFormattedRow = {
|
const row: CoreGradesFormattedRow = {
|
||||||
|
@ -126,6 +127,13 @@ export class CoreGradesHelperProvider {
|
||||||
content = CoreTextUtils.replaceNewLines(content, '<br>');
|
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 == ' ') {
|
if (content == ' ') {
|
||||||
content = '';
|
content = '';
|
||||||
}
|
}
|
||||||
|
@ -280,6 +288,7 @@ export class CoreGradesHelperProvider {
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @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).
|
* @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.
|
* @return Promise to be resolved when the grades are retrieved.
|
||||||
|
* @deprecated since app 4.0
|
||||||
*/
|
*/
|
||||||
async getGradeItem(
|
async getGradeItem(
|
||||||
courseId: number,
|
courseId: number,
|
||||||
|
@ -382,6 +391,7 @@ export class CoreGradesHelperProvider {
|
||||||
* @param table JSON object representing a table with data.
|
* @param table JSON object representing a table with data.
|
||||||
* @param gradeId Grade Object identifier.
|
* @param gradeId Grade Object identifier.
|
||||||
* @return Formatted HTML table.
|
* @return Formatted HTML table.
|
||||||
|
* @deprecated since app 4.0
|
||||||
*/
|
*/
|
||||||
async getGradesTableRow(table: CoreGradesTable, gradeId: number): Promise<CoreGradesFormattedRow | null> {
|
async getGradesTableRow(table: CoreGradesTable, gradeId: number): Promise<CoreGradesFormattedRow | null> {
|
||||||
if (table.tabledata) {
|
if (table.tabledata) {
|
||||||
|
@ -705,8 +715,12 @@ export type CoreGradesFormattedTable = {
|
||||||
|
|
||||||
export type CoreGradesFormattedTableRow = CoreGradesFormattedRowCommonData & {
|
export type CoreGradesFormattedTableRow = CoreGradesFormattedRowCommonData & {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
detailsid?: string;
|
||||||
colspan?: number;
|
colspan?: number;
|
||||||
gradeitem?: string; // The item returned data.
|
gradeitem?: string; // The item returned data.
|
||||||
|
ariaLabel?: string;
|
||||||
|
expandable?: boolean;
|
||||||
|
expanded?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CoreGradesFormattedTableColumn = {
|
export type CoreGradesFormattedTableColumn = {
|
||||||
|
|
Loading…
Reference in New Issue