MOBILE-3926 assign: Submissions swipe navigation

main
Noel De Martin 2021-11-22 15:08:31 +01:00
parent c041e2a314
commit f45f984d8e
8 changed files with 424 additions and 260 deletions

View File

@ -0,0 +1,238 @@
// (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 { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
import {
AddonModAssign,
AddonModAssignAssign,
AddonModAssignGrade,
AddonModAssignProvider,
AddonModAssignSubmission,
} from '../services/assign';
import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../services/assign-helper';
import { AddonModAssignOffline } from '../services/assign-offline';
import { AddonModAssignSync, AddonModAssignSyncProvider } from '../services/assign-sync';
/**
* Provides a collection of assignment submissions.
*/
export class AddonModAssignSubmissionsSource extends CoreItemsManagerSource<AddonModAssignSubmissionForList> {
/**
* @inheritdoc
*/
static getSourceId(courseId: number, moduleId: number, selectedStatus?: string): string {
selectedStatus = selectedStatus ?? '__empty__';
return `submissions-${courseId}-${moduleId}-${selectedStatus}`;
}
readonly COURSE_ID: number;
readonly MODULE_ID: number;
readonly SELECTED_STATUS: string | undefined;
assign?: AddonModAssignAssign;
groupId = 0;
groupInfo: CoreGroupInfo = {
groups: [],
separateGroups: false,
visibleGroups: false,
defaultGroupId: 0,
};
protected submissionsData: { canviewsubmissions: boolean; submissions?: AddonModAssignSubmission[] } = {
canviewsubmissions: false,
};
constructor(courseId: number, moduleId: number, selectedStatus?: string) {
super();
this.COURSE_ID = courseId;
this.MODULE_ID = moduleId;
this.SELECTED_STATUS = selectedStatus;
}
/**
* Invalidate assignment cache.
*/
async invalidateCache(): Promise<void> {
await Promise.all([
AddonModAssign.invalidateAssignmentData(this.COURSE_ID),
this.assign && AddonModAssign.invalidateAllSubmissionData(this.assign.id),
this.assign && AddonModAssign.invalidateAssignmentUserMappingsData(this.assign.id),
this.assign && AddonModAssign.invalidateAssignmentGradesData(this.assign.id),
this.assign && AddonModAssign.invalidateListParticipantsData(this.assign.id),
]);
}
/**
* Load assignment.
*/
async loadAssignment(sync: boolean = false): Promise<void> {
// Get assignment data.
this.assign = await AddonModAssign.getAssignment(this.COURSE_ID, this.MODULE_ID);
if (sync) {
try {
// Try to synchronize data.
const result = await AddonModAssignSync.syncAssign(this.assign.id);
if (result && result.updated) {
CoreEvents.trigger(
AddonModAssignSyncProvider.MANUAL_SYNCED,
{
assignId: this.assign.id,
warnings: result.warnings,
gradesBlocked: result.gradesBlocked,
context: 'submission-list',
},
CoreSites.getCurrentSiteId(),
);
}
} catch (error) {
// Ignore errors, probably user is offline or sync is blocked.
}
}
// Get assignment submissions.
this.submissionsData = await AddonModAssign.getSubmissions(this.assign.id, { cmId: this.assign.cmid });
if (!this.submissionsData.canviewsubmissions) {
// User shouldn't be able to reach here.
throw new Error('Cannot view submissions.');
}
// Check if groupmode is enabled to avoid showing wrong numbers.
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.assign.cmid, false);
this.groupId = CoreGroups.validateGroupId(this.groupId, this.groupInfo);
await this.reload();
}
/**
* @inheritdoc
*/
protected async loadPageItems(): Promise<{ items: AddonModAssignSubmissionForList[] }> {
const assign = this.assign;
if (!assign) {
throw new Error('Can\'t load submissions without assignment');
}
// Fetch submissions and grades.
const submissions =
await AddonModAssignHelper.getSubmissionsUserData(
assign,
this.submissionsData.submissions,
this.groupId,
);
// Get assignment grades only if workflow is not enabled to check grading date.
const grades = !assign.markingworkflow
? await AddonModAssign.getAssignmentGrades(assign.id, { cmId: assign.cmid })
: [];
// Filter the submissions to get only the ones with the right status and add some extra data.
const getNeedGrading = this.SELECTED_STATUS == AddonModAssignProvider.NEED_GRADING;
const searchStatus = getNeedGrading ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED : this.SELECTED_STATUS;
const promises: Promise<void>[] = [];
const showSubmissions: AddonModAssignSubmissionForList[] = [];
submissions.forEach((submission: AddonModAssignSubmissionForList) => {
if (!searchStatus || searchStatus == submission.status) {
promises.push(
CoreUtils.ignoreErrors(
AddonModAssignOffline.getSubmissionGrade(assign.id, submission.userid),
).then(async (data) => {
if (getNeedGrading) {
// Only show the submissions that need to be graded.
const add = await AddonModAssign.needsSubmissionToBeGraded(submission, assign.id);
if (!add) {
return;
}
}
// Load offline grades.
const notSynced = !!data && submission.timemodified < data.timemodified;
if (submission.gradingstatus == 'graded' && !assign.markingworkflow) {
// Get the last grade of the submission.
const grade = grades
.filter((grade) => grade.userid == submission.userid)
.reduce(
(a, b) => (a && a.timemodified > b.timemodified ? a : b),
<AddonModAssignGrade | undefined> undefined,
);
if (grade && grade.timemodified < submission.timemodified) {
submission.gradingstatus = AddonModAssignProvider.GRADED_FOLLOWUP_SUBMIT;
}
}
submission.statusColor = AddonModAssign.getSubmissionStatusColor(submission.status);
submission.gradingColor = AddonModAssign.getSubmissionGradingStatusColor(
submission.gradingstatus,
);
// Show submission status if not submitted for grading.
if (submission.statusColor != 'success' || !submission.gradingstatus) {
submission.statusTranslated = Translate.instant(
'addon.mod_assign.submissionstatus_' + submission.status,
);
} else {
submission.statusTranslated = '';
}
if (notSynced) {
submission.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced';
submission.gradingColor = '';
} else if (submission.statusColor != 'danger' || submission.gradingColor != 'danger') {
// Show grading status if one of the statuses is not done.
submission.gradingStatusTranslationId = AddonModAssign.getSubmissionGradingStatusTranslationId(
submission.gradingstatus,
);
} else {
submission.gradingStatusTranslationId = '';
}
showSubmissions.push(submission);
return;
}),
);
}
});
await Promise.all(promises);
return { items: showSubmissions };
}
}
/**
* Calculated data for an assign submission.
*/
export type AddonModAssignSubmissionForList = AddonModAssignSubmissionFormatted & {
statusColor?: string; // Calculated in the app. Color of the submission status.
gradingColor?: string; // Calculated in the app. Color of the submission grading status.
statusTranslated?: string; // Calculated in the app. Translated text of the submission status.
gradingStatusTranslationId?: string; // Calculated in the app. Key of the text of the submission grading status.
};

View File

@ -16,10 +16,10 @@
<ion-content>
<core-split-view>
<ion-refresher slot="fixed" [disabled]="!loaded || !submissions.loaded" (ionRefresh)="refreshList($event.target)">
<ion-refresher slot="fixed" [disabled]="!submissions.loaded" (ionRefresh)="refreshList($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded && submissions.loaded">
<core-loading [hideUntil]="submissions.loaded">
<core-empty-box *ngIf="!submissions || submissions.empty" icon="fas-file-signature"
[message]="'addon.mod_assign.submissionstatus_' | translate">
</core-empty-box>
@ -32,7 +32,7 @@
<ion-label id="addon-assign-groupslabel" *ngIf="groupInfo.visibleGroups">
{{ 'core.groupsvisible' | translate }}
</ion-label>
<ion-select [(ngModel)]="groupId" (ionChange)="setGroup(groupId)" aria-labelledby="addon-assign-groupslabel"
<ion-select [(ngModel)]="groupId" (ionChange)="reloadSubmissions()" aria-labelledby="addon-assign-groupslabel"
interface="action-sheet" slot="end" [interfaceOptions]="{header: 'core.group' | translate}">
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
{{groupOpt.name}}

View File

@ -14,28 +14,20 @@
import { Component, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
import { Params } from '@angular/router';
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { IonRefresher } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreGroupInfo } from '@services/groups';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import {
AddonModAssignAssign,
AddonModAssignSubmission,
AddonModAssignProvider,
AddonModAssign,
AddonModAssignGrade,
} from '../../services/assign';
import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../../services/assign-helper';
import { AddonModAssignOffline } from '../../services/assign-offline';
import { AddonModAssignSubmissionForList, AddonModAssignSubmissionsSource } from '../../classes/submissions-source';
import { AddonModAssignAssign, AddonModAssignProvider } from '../../services/assign';
import {
AddonModAssignSyncProvider,
AddonModAssignSync,
AddonModAssignManualSyncData,
AddonModAssignAutoSyncData,
} from '../../services/assign-sync';
@ -51,47 +43,26 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
title = ''; // Title to display.
assign?: AddonModAssignAssign; // Assignment.
submissions: AddonModAssignSubmissionListManager; // List of submissions
loaded = false; // Whether data has been loaded.
groupId = 0; // Group ID to show.
courseId!: number; // Course ID the assignment belongs to.
moduleId!: number; // Module ID the submission belongs to.
title = '';
submissions!: AddonModAssignSubmissionListManager; // List of submissions
groupInfo: CoreGroupInfo = {
groups: [],
separateGroups: false,
visibleGroups: false,
defaultGroupId: 0,
};
protected selectedStatus?: string; // The status to see.
protected gradedObserver: CoreEventObserver; // Observer to refresh data when a grade changes.
protected syncObserver: CoreEventObserver; // Observer to refresh data when the async is synchronized.
protected submissionsData: { canviewsubmissions: boolean; submissions?: AddonModAssignSubmission[] } = {
canviewsubmissions: false,
};
protected sourceUnsubscribe?: () => void;
constructor() {
this.submissions = new AddonModAssignSubmissionListManager(AddonModAssignSubmissionListPage);
// Update data if some grade changes.
this.gradedObserver = CoreEvents.on(
AddonModAssignProvider.GRADED_EVENT,
(data) => {
if (
this.loaded &&
this.assign &&
data.assignmentId == this.assign.id &&
this.submissions.loaded &&
this.submissions.getSource().assign &&
data.assignmentId == this.submissions.getSource().assign?.id &&
data.userId == CoreSites.getCurrentSiteUserId()
) {
// Grade changed, refresh the data.
this.loaded = false;
this.refreshAllData(true).finally(() => {
this.loaded = true;
});
this.refreshAllData(true);
}
},
CoreSites.getCurrentSiteId(),
@ -102,29 +73,36 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
this.syncObserver = CoreEvents.onMultiple<AddonModAssignAutoSyncData | AddonModAssignManualSyncData>(
events,
(data) => {
if (!this.loaded || ('context' in data && data.context == 'submission-list')) {
if (!this.submissions.loaded || ('context' in data && data.context == 'submission-list')) {
return;
}
this.loaded = false;
this.refreshAllData(false).finally(() => {
this.loaded = true;
});
this.refreshAllData(false);
},
CoreSites.getCurrentSiteId(),
);
}
/**
* Component being initialized.
*/
ngAfterViewInit(): void {
try {
this.moduleId = CoreNavigator.getRequiredRouteNumberParam('cmId');
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.groupId = CoreNavigator.getRouteNumberParam('groupId') || 0;
this.selectedStatus = CoreNavigator.getRouteParam('status');
const moduleId = CoreNavigator.getRequiredRouteNumberParam('cmId');
const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
const groupId = CoreNavigator.getRouteNumberParam('groupId') || 0;
const selectedStatus = CoreNavigator.getRouteParam('status');
const submissionsSource = CoreItemsManagerSourcesTracker.getOrCreateSource(
AddonModAssignSubmissionsSource,
[courseId, moduleId, selectedStatus],
);
submissionsSource.groupId = groupId;
this.sourceUnsubscribe = submissionsSource.addListener({
onItemsUpdated: () => {
this.title = this.submissions.getSource().assign?.name || this.title;
},
});
this.submissions = new AddonModAssignSubmissionListManager(
submissionsSource,
AddonModAssignSubmissionListPage,
);
} catch (error) {
CoreDomUtils.showErrorModal(error);
@ -132,18 +110,48 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
return;
}
}
get assign(): AddonModAssignAssign | undefined {
return this.submissions.getSource().assign;
}
get groupInfo(): CoreGroupInfo {
return this.submissions.getSource().groupInfo;
}
get moduleId(): number {
return this.submissions.getSource().MODULE_ID;
}
get courseId(): number {
return this.submissions.getSource().COURSE_ID;
}
get groupId(): number {
return this.submissions.getSource().groupId;
}
set groupId(value: number) {
this.submissions.getSource().groupId = value;
}
/**
* @inheritdoc
*/
ngAfterViewInit(): void {
const selectedStatus = this.submissions.getSource().SELECTED_STATUS;
this.title = Translate.instant(
selectedStatus
? (
selectedStatus === AddonModAssignProvider.NEED_GRADING
? 'addon.mod_assign.numberofsubmissionsneedgrading'
: `addon.mod_assign.submissionstatus_${selectedStatus}`
)
: 'addon.mod_assign.numberofparticipants',
);
if (this.selectedStatus) {
if (this.selectedStatus == AddonModAssignProvider.NEED_GRADING) {
this.title = Translate.instant('addon.mod_assign.numberofsubmissionsneedgrading');
} else {
this.title = Translate.instant('addon.mod_assign.submissionstatus_' + this.selectedStatus);
}
} else {
this.title = Translate.instant('addon.mod_assign.numberofparticipants');
}
this.fetchAssignment(true).finally(() => {
this.loaded = true;
this.submissions.start(this.splitView);
});
}
@ -156,148 +164,12 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
*/
protected async fetchAssignment(sync = false): Promise<void> {
try {
// Get assignment data.
this.assign = await AddonModAssign.getAssignment(this.courseId, this.moduleId);
this.title = this.assign.name || this.title;
if (sync) {
try {
// Try to synchronize data.
const result = await AddonModAssignSync.syncAssign(this.assign.id);
if (result && result.updated) {
CoreEvents.trigger(
AddonModAssignSyncProvider.MANUAL_SYNCED,
{
assignId: this.assign.id,
warnings: result.warnings,
gradesBlocked: result.gradesBlocked,
context: 'submission-list',
},
CoreSites.getCurrentSiteId(),
);
}
} catch (error) {
// Ignore errors, probably user is offline or sync is blocked.
}
}
// Get assignment submissions.
this.submissionsData = await AddonModAssign.getSubmissions(this.assign.id, { cmId: this.assign.cmid });
if (!this.submissionsData.canviewsubmissions) {
// User shouldn't be able to reach here.
throw new Error('Cannot view submissions.');
}
// Check if groupmode is enabled to avoid showing wrong numbers.
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.assign.cmid, false);
await this.setGroup(CoreGroups.validateGroupId(this.groupId, this.groupInfo));
await this.submissions.getSource().loadAssignment(sync);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error getting assigment data.');
}
}
/**
* Set group to see the summary.
*
* @param groupId Group ID.
* @return Resolved when done.
*/
async setGroup(groupId: number): Promise<void> {
this.groupId = groupId;
// Fetch submissions and grades.
const submissions =
await AddonModAssignHelper.getSubmissionsUserData(
this.assign!,
this.submissionsData.submissions,
this.groupId,
);
// Get assignment grades only if workflow is not enabled to check grading date.
const grades = !this.assign!.markingworkflow
? await AddonModAssign.getAssignmentGrades(this.assign!.id, { cmId: this.assign!.cmid })
: [];
// Filter the submissions to get only the ones with the right status and add some extra data.
const getNeedGrading = this.selectedStatus == AddonModAssignProvider.NEED_GRADING;
const searchStatus = getNeedGrading ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED : this.selectedStatus;
const promises: Promise<void>[] = [];
const showSubmissions: AddonModAssignSubmissionForList[] = [];
submissions.forEach((submission: AddonModAssignSubmissionForList) => {
if (!searchStatus || searchStatus == submission.status) {
promises.push(
CoreUtils.ignoreErrors(
AddonModAssignOffline.getSubmissionGrade(this.assign!.id, submission.userid),
).then(async (data) => {
if (getNeedGrading) {
// Only show the submissions that need to be graded.
const add = await AddonModAssign.needsSubmissionToBeGraded(submission, this.assign!.id);
if (!add) {
return;
}
}
// Load offline grades.
const notSynced = !!data && submission.timemodified < data.timemodified;
if (submission.gradingstatus == 'graded' && !this.assign!.markingworkflow) {
// Get the last grade of the submission.
const grade = grades
.filter((grade) => grade.userid == submission.userid)
.reduce(
(a, b) => (a && a.timemodified > b.timemodified ? a : b),
<AddonModAssignGrade | undefined> undefined,
);
if (grade && grade.timemodified < submission.timemodified) {
submission.gradingstatus = AddonModAssignProvider.GRADED_FOLLOWUP_SUBMIT;
}
}
submission.statusColor = AddonModAssign.getSubmissionStatusColor(submission.status);
submission.gradingColor = AddonModAssign.getSubmissionGradingStatusColor(
submission.gradingstatus,
);
// Show submission status if not submitted for grading.
if (submission.statusColor != 'success' || !submission.gradingstatus) {
submission.statusTranslated = Translate.instant(
'addon.mod_assign.submissionstatus_' + submission.status,
);
} else {
submission.statusTranslated = '';
}
if (notSynced) {
submission.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced';
submission.gradingColor = '';
} else if (submission.statusColor != 'danger' || submission.gradingColor != 'danger') {
// Show grading status if one of the statuses is not done.
submission.gradingStatusTranslationId = AddonModAssign.getSubmissionGradingStatusTranslationId(
submission.gradingstatus,
);
} else {
submission.gradingStatusTranslationId = '';
}
showSubmissions.push(submission);
return;
}),
);
}
});
await Promise.all(promises);
this.submissions.setItems(showSubmissions);
}
/**
* Refresh all the data.
*
@ -305,18 +177,8 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
* @return Promise resolved when done.
*/
protected async refreshAllData(sync?: boolean): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModAssign.invalidateAssignmentData(this.courseId));
if (this.assign) {
promises.push(AddonModAssign.invalidateAllSubmissionData(this.assign.id));
promises.push(AddonModAssign.invalidateAssignmentUserMappingsData(this.assign.id));
promises.push(AddonModAssign.invalidateAssignmentGradesData(this.assign.id));
promises.push(AddonModAssign.invalidateListParticipantsData(this.assign.id));
}
try {
await Promise.all(promises);
await this.submissions.getSource().invalidateCache();
} finally {
this.fetchAssignment(sync);
}
@ -333,6 +195,13 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
});
}
/**
* Reload submissions list.
*/
async reloadSubmissions(): Promise<void> {
await this.submissions.reload();
}
/**
* Component being destroyed.
*/
@ -340,6 +209,7 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
this.gradedObserver?.off();
this.syncObserver?.off();
this.submissions.destroy();
this.sourceUnsubscribe && this.sourceUnsubscribe();
}
}
@ -347,11 +217,8 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
/**
* Helper class to manage submissions.
*/
class AddonModAssignSubmissionListManager extends CorePageItemsListManager<AddonModAssignSubmissionForList> {
constructor(pageComponent: unknown) {
super(pageComponent);
}
class AddonModAssignSubmissionListManager
extends CoreListItemsManager<AddonModAssignSubmissionForList, AddonModAssignSubmissionsSource> {
/**
* @inheritdoc
@ -366,17 +233,9 @@ class AddonModAssignSubmissionListManager extends CorePageItemsListManager<Addon
protected getItemQueryParams(submission: AddonModAssignSubmissionForList): Params {
return {
blindId: submission.blindid,
groupId: this.getSource().groupId,
selectedStatus: this.getSource().SELECTED_STATUS,
};
}
}
/**
* Calculated data for an assign submission.
*/
type AddonModAssignSubmissionForList = AddonModAssignSubmissionFormatted & {
statusColor?: string; // Calculated in the app. Color of the submission status.
gradingColor?: string; // Calculated in the app. Color of the submission grading status.
statusTranslated?: string; // Calculated in the app. Translated text of the submission status.
gradingStatusTranslationId?: string; // Calculated in the app. Key of the text of the submission grading status.
};

View File

@ -20,12 +20,14 @@
</core-navbar-buttons>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<addon-mod-assign-submission [courseId]="courseId" [moduleId]="moduleId" [submitId]="submitId" [blindId]="blindId">
</addon-mod-assign-submission>
</core-loading>
<core-swipe-navigation [manager]="submissions">
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<addon-mod-assign-submission *ngIf="loaded"
[courseId]="courseId" [moduleId]="moduleId" [submitId]="submitId" [blindId]="blindId">
</addon-mod-assign-submission>
</core-loading>
</core-swipe-navigation>
</ion-content>

View File

@ -12,14 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router';
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
import { CoreCourse } from '@features/course/services/course';
import { CanLeave } from '@guards/can-leave';
import { IonRefresher } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
import { CoreScreen } from '@services/screen';
import { CoreDomUtils } from '@services/utils/dom';
import { AddonModAssignSubmissionForList, AddonModAssignSubmissionsSource } from '../../classes/submissions-source';
import { AddonModAssignSubmissionComponent } from '../../components/submission/submission';
import { AddonModAssign, AddonModAssignAssign } from '../../services/assign';
@ -30,11 +33,12 @@ import { AddonModAssign, AddonModAssignAssign } from '../../services/assign';
selector: 'page-addon-mod-assign-submission-review',
templateUrl: 'submission-review.html',
})
export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave {
export class AddonModAssignSubmissionReviewPage implements OnInit, OnDestroy, CanLeave {
@ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent;
title = ''; // Title to display.
submissions?: AddonModAssignSubmissionSwipeItemsManager;
moduleId!: number; // Module ID the submission belongs to.
courseId!: number; // Course ID the assignment belongs to.
submitId!: number; // User that did the submission.
@ -46,9 +50,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave {
protected blindMarking = false; // Whether it uses blind marking.
protected forceLeave = false; // To allow leaving the page without checking for changes.
constructor(
protected route: ActivatedRoute,
) { }
constructor(protected route: ActivatedRoute) { }
/**
* Component being initialized.
@ -60,6 +62,19 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave {
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.submitId = CoreNavigator.getRouteNumberParam('submitId') || 0;
this.blindId = CoreNavigator.getRouteNumberParam('blindId', { params });
const groupId = CoreNavigator.getRequiredRouteNumberParam('groupId');
const selectedStatus = CoreNavigator.getRouteParam('selectedStatus');
const submissionsSource = CoreItemsManagerSourcesTracker.getOrCreateSource(
AddonModAssignSubmissionsSource,
[this.courseId, this.moduleId, selectedStatus],
);
this.submissions?.destroy();
submissionsSource.groupId = groupId;
this.submissions = new AddonModAssignSubmissionSwipeItemsManager(submissionsSource);
this.submissions.start();
} catch (error) {
CoreDomUtils.showErrorModal(error);
@ -74,6 +89,13 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave {
});
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.submissions?.destroy();
}
/**
* Check if we can leave the page or not.
*
@ -190,3 +212,36 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave {
}
}
/**
* Helper to manage swiping within a collection of submissions.
*/
class AddonModAssignSubmissionSwipeItemsManager
extends CoreSwipeItemsManager<AddonModAssignSubmissionForList, AddonModAssignSubmissionsSource> {
/**
* @inheritdoc
*/
protected getItemPath(submission: AddonModAssignSubmissionForList): string {
return String(submission.submitid);
}
/**
* @inheritdoc
*/
protected getItemQueryParams(submission: AddonModAssignSubmissionForList): Params {
return {
blindId: submission.blindid,
groupId: this.getSource().groupId,
selectedStatus: this.getSource().SELECTED_STATUS,
};
}
/**
* @inheritdoc
*/
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
return route.params.submitId;
}
}

View File

@ -16,8 +16,8 @@
* Updates listener.
*/
export interface CoreItemsListSourceListener<Item> {
onItemsUpdated(items: Item[], hasMoreItems: boolean): void;
onReset(): void;
onItemsUpdated?(items: Item[], hasMoreItems: boolean): void;
onReset?(): void;
}
/**
@ -91,7 +91,7 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
this.items = null;
this.hasMoreItems = true;
this.listeners.forEach(listener => listener.onReset());
this.listeners.forEach(listener => listener.onReset?.call(listener));
}
/**
@ -170,7 +170,7 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
this.items = items;
this.hasMoreItems = hasMoreItems;
this.listeners.forEach(listener => listener.onItemsUpdated(items, hasMoreItems));
this.listeners.forEach(listener => listener.onItemsUpdated?.call(listener, items, hasMoreItems));
}
}

View File

@ -70,15 +70,6 @@ export abstract class CoreListItemsManager<
// Calculate current selected item.
this.updateSelectedItem();
// 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());
}
@ -175,6 +166,25 @@ export abstract class CoreListItemsManager<
return !!this.splitView && !this.splitView?.isNested;
}
/**
* @inheritdoc
*/
protected updateSelectedItem(route: ActivatedRouteSnapshot | null = null): void {
super.updateSelectedItem(route);
if (CoreScreen.isMobile || this.selectedItem !== null || this.splitView?.isNested) {
return;
}
const defaultItem = this.getDefaultItem();
if (!defaultItem) {
return;
}
this.select(defaultItem);
}
/**
* Get the item that should be selected by default.
*/

View File

@ -290,7 +290,7 @@ export class CoreNavigatorService {
* @param routeOptions Optional routeOptions to get the params or route value from. If missing, it will autodetect.
* @return Value of the parameter, undefined if not found.
*/
getRouteParam<T = unknown>(name: string, routeOptions: CoreNavigatorCurrentRouteOptions = {}): T | undefined {
getRouteParam<T = string>(name: string, routeOptions: CoreNavigatorCurrentRouteOptions = {}): T | undefined {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let value: any;