Merge pull request #3012 from NoelDeMartin/MOBILE-3926
MOBILE-3926: Add swipe navigation to pages with split-viewmain
commit
ad6c7367ff
|
@ -0,0 +1,60 @@
|
||||||
|
// (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 { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
|
||||||
|
import { AddonBadges, AddonBadgesUserBadge } from '../services/badges';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a collection of user badges.
|
||||||
|
*/
|
||||||
|
export class AddonBadgesUserBadgesSource extends CoreItemsManagerSource<AddonBadgesUserBadge> {
|
||||||
|
|
||||||
|
readonly COURSE_ID: number;
|
||||||
|
readonly USER_ID: number;
|
||||||
|
|
||||||
|
constructor(courseId: number, userId: number) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.COURSE_ID = courseId;
|
||||||
|
this.USER_ID = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemPath(badge: AddonBadgesUserBadge): string {
|
||||||
|
return badge.uniquehash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemQueryParams(): Params {
|
||||||
|
return {
|
||||||
|
courseId: this.COURSE_ID,
|
||||||
|
userId: this.USER_ID,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async loadPageItems(): Promise<{ items: AddonBadgesUserBadge[] }> {
|
||||||
|
const badges = await AddonBadges.getUserBadges(this.COURSE_ID, this.USER_ID);
|
||||||
|
|
||||||
|
return { items: badges };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,6 +10,7 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
|
<core-swipe-navigation [manager]="badges">
|
||||||
<ion-refresher slot="fixed" [disabled]="!badgeLoaded" (ionRefresh)="refreshBadges($event.target)">
|
<ion-refresher slot="fixed" [disabled]="!badgeLoaded" (ionRefresh)="refreshBadges($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>
|
||||||
|
@ -245,4 +246,5 @@
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
</core-swipe-navigation>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -23,6 +23,9 @@ import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
|
import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
|
||||||
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
|
import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-source';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays the list of calendar events.
|
* Page that displays the list of calendar events.
|
||||||
|
@ -40,12 +43,11 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
|
||||||
user?: CoreUserProfile;
|
user?: CoreUserProfile;
|
||||||
course?: CoreEnrolledCourseData;
|
course?: CoreEnrolledCourseData;
|
||||||
badge?: AddonBadgesUserBadge;
|
badge?: AddonBadgesUserBadge;
|
||||||
|
badges?: CoreSwipeItemsManager;
|
||||||
badgeLoaded = false;
|
badgeLoaded = false;
|
||||||
currentTime = 0;
|
currentTime = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(protected route: ActivatedRoute) { }
|
||||||
protected route: ActivatedRoute,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View loaded.
|
* View loaded.
|
||||||
|
@ -58,6 +60,11 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
|
||||||
this.fetchIssuedBadge().finally(() => {
|
this.fetchIssuedBadge().finally(() => {
|
||||||
this.badgeLoaded = true;
|
this.badgeLoaded = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(AddonBadgesUserBadgesSource, [this.courseId, this.userId]);
|
||||||
|
this.badges = new CoreSwipeItemsManager(source);
|
||||||
|
|
||||||
|
this.badges.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -19,10 +19,11 @@ import { CoreTimeUtils } from '@services/utils/time';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
|
||||||
import { Params } from '@angular/router';
|
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||||
|
import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-source';
|
||||||
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays the list of calendar events.
|
* Page that displays the list of calendar events.
|
||||||
|
@ -34,7 +35,7 @@ import { CoreNavigator } from '@services/navigator';
|
||||||
export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
|
export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
currentTime = 0;
|
currentTime = 0;
|
||||||
badges: AddonBadgesUserBadgesManager;
|
badges: CoreListItemsManager<AddonBadgesUserBadge, AddonBadgesUserBadgesSource>;
|
||||||
|
|
||||||
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||||
|
|
||||||
|
@ -47,7 +48,10 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
|
||||||
courseId = 0;
|
courseId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.badges = new AddonBadgesUserBadgesManager(AddonBadgesUserBadgesPage, courseId, userId);
|
this.badges = new CoreListItemsManager(
|
||||||
|
CoreItemsManagerSourcesTracker.getOrCreateSource(AddonBadgesUserBadgesSource, [courseId, userId]),
|
||||||
|
AddonBadgesUserBadgesPage,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -72,8 +76,13 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
|
||||||
* @param refresher Refresher.
|
* @param refresher Refresher.
|
||||||
*/
|
*/
|
||||||
async refreshBadges(refresher?: IonRefresher): Promise<void> {
|
async refreshBadges(refresher?: IonRefresher): Promise<void> {
|
||||||
await CoreUtils.ignoreErrors(AddonBadges.invalidateUserBadges(this.badges.courseId, this.badges.userId));
|
await CoreUtils.ignoreErrors(
|
||||||
await CoreUtils.ignoreErrors(this.fetchBadges());
|
AddonBadges.invalidateUserBadges(
|
||||||
|
this.badges.getSource().COURSE_ID,
|
||||||
|
this.badges.getSource().USER_ID,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await CoreUtils.ignoreErrors(this.badges.reload());
|
||||||
|
|
||||||
refresher?.complete();
|
refresher?.complete();
|
||||||
}
|
}
|
||||||
|
@ -85,55 +94,12 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
|
||||||
this.currentTime = CoreTimeUtils.timestamp();
|
this.currentTime = CoreTimeUtils.timestamp();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.fetchBadges();
|
await this.badges.reload();
|
||||||
} catch (message) {
|
} catch (message) {
|
||||||
CoreDomUtils.showErrorModalDefault(message, 'Error loading badges');
|
CoreDomUtils.showErrorModalDefault(message, 'Error loading badges');
|
||||||
|
|
||||||
this.badges.setItems([]);
|
this.badges.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the list of badges.
|
|
||||||
*/
|
|
||||||
private async fetchBadges(): Promise<void> {
|
|
||||||
const badges = await AddonBadges.getUserBadges(this.badges.courseId, this.badges.userId);
|
|
||||||
|
|
||||||
this.badges.setItems(badges);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class to manage badges.
|
|
||||||
*/
|
|
||||||
class AddonBadgesUserBadgesManager extends CorePageItemsListManager<AddonBadgesUserBadge> {
|
|
||||||
|
|
||||||
courseId: number;
|
|
||||||
userId: number;
|
|
||||||
|
|
||||||
constructor(pageComponent: unknown, courseId: number, userId: number) {
|
|
||||||
super(pageComponent);
|
|
||||||
|
|
||||||
this.courseId = courseId;
|
|
||||||
this.userId = userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
protected getItemPath(badge: AddonBadgesUserBadge): string {
|
|
||||||
return badge.uniquehash;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
protected getItemQueryParams(): Params {
|
|
||||||
return {
|
|
||||||
courseId: this.courseId,
|
|
||||||
userId: this.userId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,257 @@
|
||||||
|
// (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 { 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemPath(submission: AddonModAssignSubmissionForList): string {
|
||||||
|
return String(submission.submitid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemQueryParams(submission: AddonModAssignSubmissionForList): Params {
|
||||||
|
return {
|
||||||
|
blindId: submission.blindid,
|
||||||
|
groupId: this.groupId,
|
||||||
|
selectedStatus: this.SELECTED_STATUS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
};
|
|
@ -16,10 +16,10 @@
|
||||||
|
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<core-split-view>
|
<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-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
</ion-refresher>
|
</ion-refresher>
|
||||||
<core-loading [hideUntil]="loaded && submissions.loaded">
|
<core-loading [hideUntil]="submissions.loaded">
|
||||||
<core-empty-box *ngIf="!submissions || submissions.empty" icon="fas-file-signature"
|
<core-empty-box *ngIf="!submissions || submissions.empty" icon="fas-file-signature"
|
||||||
[message]="'addon.mod_assign.submissionstatus_' | translate">
|
[message]="'addon.mod_assign.submissionstatus_' | translate">
|
||||||
</core-empty-box>
|
</core-empty-box>
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<ion-label id="addon-assign-groupslabel" *ngIf="groupInfo.visibleGroups">
|
<ion-label id="addon-assign-groupslabel" *ngIf="groupInfo.visibleGroups">
|
||||||
{{ 'core.groupsvisible' | translate }}
|
{{ 'core.groupsvisible' | translate }}
|
||||||
</ion-label>
|
</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}">
|
interface="action-sheet" slot="end" [interfaceOptions]="{header: 'core.group' | translate}">
|
||||||
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
|
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
|
||||||
{{groupOpt.name}}
|
{{groupOpt.name}}
|
||||||
|
|
|
@ -13,29 +13,20 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
|
import { Component, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
|
||||||
import { Params } from '@angular/router';
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
|
import { CoreGroupInfo } from '@services/groups';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import {
|
import { AddonModAssignSubmissionForList, AddonModAssignSubmissionsSource } from '../../classes/submissions-source';
|
||||||
AddonModAssignAssign,
|
import { AddonModAssignAssign, AddonModAssignProvider } from '../../services/assign';
|
||||||
AddonModAssignSubmission,
|
|
||||||
AddonModAssignProvider,
|
|
||||||
AddonModAssign,
|
|
||||||
AddonModAssignGrade,
|
|
||||||
} from '../../services/assign';
|
|
||||||
import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../../services/assign-helper';
|
|
||||||
import { AddonModAssignOffline } from '../../services/assign-offline';
|
|
||||||
import {
|
import {
|
||||||
AddonModAssignSyncProvider,
|
AddonModAssignSyncProvider,
|
||||||
AddonModAssignSync,
|
|
||||||
AddonModAssignManualSyncData,
|
AddonModAssignManualSyncData,
|
||||||
AddonModAssignAutoSyncData,
|
AddonModAssignAutoSyncData,
|
||||||
} from '../../services/assign-sync';
|
} from '../../services/assign-sync';
|
||||||
|
@ -51,47 +42,26 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
||||||
|
|
||||||
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||||
|
|
||||||
title = ''; // Title to display.
|
title = '';
|
||||||
assign?: AddonModAssignAssign; // Assignment.
|
submissions!: CoreListItemsManager<AddonModAssignSubmissionForList, AddonModAssignSubmissionsSource>; // List of submissions
|
||||||
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.
|
|
||||||
|
|
||||||
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 gradedObserver: CoreEventObserver; // Observer to refresh data when a grade changes.
|
||||||
protected syncObserver: CoreEventObserver; // Observer to refresh data when the async is synchronized.
|
protected syncObserver: CoreEventObserver; // Observer to refresh data when the async is synchronized.
|
||||||
protected submissionsData: { canviewsubmissions: boolean; submissions?: AddonModAssignSubmission[] } = {
|
protected sourceUnsubscribe?: () => void;
|
||||||
canviewsubmissions: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.submissions = new AddonModAssignSubmissionListManager(AddonModAssignSubmissionListPage);
|
|
||||||
|
|
||||||
// Update data if some grade changes.
|
// Update data if some grade changes.
|
||||||
this.gradedObserver = CoreEvents.on(
|
this.gradedObserver = CoreEvents.on(
|
||||||
AddonModAssignProvider.GRADED_EVENT,
|
AddonModAssignProvider.GRADED_EVENT,
|
||||||
(data) => {
|
(data) => {
|
||||||
if (
|
if (
|
||||||
this.loaded &&
|
this.submissions.loaded &&
|
||||||
this.assign &&
|
this.submissions.getSource().assign &&
|
||||||
data.assignmentId == this.assign.id &&
|
data.assignmentId == this.submissions.getSource().assign?.id &&
|
||||||
data.userId == CoreSites.getCurrentSiteUserId()
|
data.userId == CoreSites.getCurrentSiteUserId()
|
||||||
) {
|
) {
|
||||||
// Grade changed, refresh the data.
|
// Grade changed, refresh the data.
|
||||||
this.loaded = false;
|
this.refreshAllData(true);
|
||||||
|
|
||||||
this.refreshAllData(true).finally(() => {
|
|
||||||
this.loaded = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
CoreSites.getCurrentSiteId(),
|
CoreSites.getCurrentSiteId(),
|
||||||
|
@ -102,29 +72,36 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
||||||
this.syncObserver = CoreEvents.onMultiple<AddonModAssignAutoSyncData | AddonModAssignManualSyncData>(
|
this.syncObserver = CoreEvents.onMultiple<AddonModAssignAutoSyncData | AddonModAssignManualSyncData>(
|
||||||
events,
|
events,
|
||||||
(data) => {
|
(data) => {
|
||||||
if (!this.loaded || ('context' in data && data.context == 'submission-list')) {
|
if (!this.submissions.loaded || ('context' in data && data.context == 'submission-list')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loaded = false;
|
this.refreshAllData(false);
|
||||||
|
|
||||||
this.refreshAllData(false).finally(() => {
|
|
||||||
this.loaded = true;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
CoreSites.getCurrentSiteId(),
|
CoreSites.getCurrentSiteId(),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component being initialized.
|
|
||||||
*/
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
try {
|
try {
|
||||||
this.moduleId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
const moduleId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
this.groupId = CoreNavigator.getRouteNumberParam('groupId') || 0;
|
const groupId = CoreNavigator.getRouteNumberParam('groupId') || 0;
|
||||||
this.selectedStatus = CoreNavigator.getRouteParam('status');
|
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 CoreListItemsManager(
|
||||||
|
submissionsSource,
|
||||||
|
AddonModAssignSubmissionListPage,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
@ -132,18 +109,48 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.selectedStatus) {
|
get assign(): AddonModAssignAssign | undefined {
|
||||||
if (this.selectedStatus == AddonModAssignProvider.NEED_GRADING) {
|
return this.submissions.getSource().assign;
|
||||||
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');
|
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',
|
||||||
|
);
|
||||||
|
|
||||||
this.fetchAssignment(true).finally(() => {
|
this.fetchAssignment(true).finally(() => {
|
||||||
this.loaded = true;
|
|
||||||
this.submissions.start(this.splitView);
|
this.submissions.start(this.splitView);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -156,148 +163,12 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
||||||
*/
|
*/
|
||||||
protected async fetchAssignment(sync = false): Promise<void> {
|
protected async fetchAssignment(sync = false): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Get assignment data.
|
await this.submissions.getSource().loadAssignment(sync);
|
||||||
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));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'Error getting assigment data.');
|
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.
|
* Refresh all the data.
|
||||||
*
|
*
|
||||||
|
@ -305,18 +176,8 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async refreshAllData(sync?: boolean): Promise<void> {
|
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 {
|
try {
|
||||||
await Promise.all(promises);
|
await this.submissions.getSource().invalidateCache();
|
||||||
} finally {
|
} finally {
|
||||||
this.fetchAssignment(sync);
|
this.fetchAssignment(sync);
|
||||||
}
|
}
|
||||||
|
@ -333,6 +194,13 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload submissions list.
|
||||||
|
*/
|
||||||
|
async reloadSubmissions(): Promise<void> {
|
||||||
|
await this.submissions.reload();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being destroyed.
|
* Component being destroyed.
|
||||||
*/
|
*/
|
||||||
|
@ -340,43 +208,7 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
||||||
this.gradedObserver?.off();
|
this.gradedObserver?.off();
|
||||||
this.syncObserver?.off();
|
this.syncObserver?.off();
|
||||||
this.submissions.destroy();
|
this.submissions.destroy();
|
||||||
|
this.sourceUnsubscribe && this.sourceUnsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class to manage submissions.
|
|
||||||
*/
|
|
||||||
class AddonModAssignSubmissionListManager extends CorePageItemsListManager<AddonModAssignSubmissionForList> {
|
|
||||||
|
|
||||||
constructor(pageComponent: unknown) {
|
|
||||||
super(pageComponent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
protected getItemPath(submission: AddonModAssignSubmissionForList): string {
|
|
||||||
return String(submission.submitid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
protected getItemQueryParams(submission: AddonModAssignSubmissionForList): Params {
|
|
||||||
return {
|
|
||||||
blindId: submission.blindid,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
};
|
|
||||||
|
|
|
@ -20,12 +20,14 @@
|
||||||
</core-navbar-buttons>
|
</core-navbar-buttons>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
|
<core-swipe-navigation [manager]="submissions">
|
||||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event.target)">
|
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($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]="loaded">
|
<core-loading [hideUntil]="loaded">
|
||||||
<addon-mod-assign-submission [courseId]="courseId" [moduleId]="moduleId" [submitId]="submitId" [blindId]="blindId">
|
<addon-mod-assign-submission *ngIf="loaded"
|
||||||
|
[courseId]="courseId" [moduleId]="moduleId" [submitId]="submitId" [blindId]="blindId">
|
||||||
</addon-mod-assign-submission>
|
</addon-mod-assign-submission>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
</core-swipe-navigation>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -12,14 +12,17 @@
|
||||||
// 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 { Component, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, ActivatedRouteSnapshot } 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 { CoreCourse } from '@features/course/services/course';
|
||||||
import { CanLeave } from '@guards/can-leave';
|
import { CanLeave } from '@guards/can-leave';
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreScreen } from '@services/screen';
|
import { CoreScreen } from '@services/screen';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { AddonModAssignSubmissionsSource } from '../../classes/submissions-source';
|
||||||
import { AddonModAssignSubmissionComponent } from '../../components/submission/submission';
|
import { AddonModAssignSubmissionComponent } from '../../components/submission/submission';
|
||||||
import { AddonModAssign, AddonModAssignAssign } from '../../services/assign';
|
import { AddonModAssign, AddonModAssignAssign } from '../../services/assign';
|
||||||
|
|
||||||
|
@ -30,11 +33,12 @@ import { AddonModAssign, AddonModAssignAssign } from '../../services/assign';
|
||||||
selector: 'page-addon-mod-assign-submission-review',
|
selector: 'page-addon-mod-assign-submission-review',
|
||||||
templateUrl: 'submission-review.html',
|
templateUrl: 'submission-review.html',
|
||||||
})
|
})
|
||||||
export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave {
|
export class AddonModAssignSubmissionReviewPage implements OnInit, OnDestroy, CanLeave {
|
||||||
|
|
||||||
@ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent;
|
@ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent;
|
||||||
|
|
||||||
title = ''; // Title to display.
|
title = ''; // Title to display.
|
||||||
|
submissions?: AddonModAssignSubmissionSwipeItemsManager;
|
||||||
moduleId!: number; // Module ID the submission belongs to.
|
moduleId!: number; // Module ID the submission belongs to.
|
||||||
courseId!: number; // Course ID the assignment belongs to.
|
courseId!: number; // Course ID the assignment belongs to.
|
||||||
submitId!: number; // User that did the submission.
|
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 blindMarking = false; // Whether it uses blind marking.
|
||||||
protected forceLeave = false; // To allow leaving the page without checking for changes.
|
protected forceLeave = false; // To allow leaving the page without checking for changes.
|
||||||
|
|
||||||
constructor(
|
constructor(protected route: ActivatedRoute) { }
|
||||||
protected route: ActivatedRoute,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
|
@ -60,6 +62,19 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave {
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
this.submitId = CoreNavigator.getRouteNumberParam('submitId') || 0;
|
this.submitId = CoreNavigator.getRouteNumberParam('submitId') || 0;
|
||||||
this.blindId = CoreNavigator.getRouteNumberParam('blindId', { params });
|
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) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(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.
|
* Check if we can leave the page or not.
|
||||||
*
|
*
|
||||||
|
@ -190,3 +212,17 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within a collection of submissions.
|
||||||
|
*/
|
||||||
|
class AddonModAssignSubmissionSwipeItemsManager extends CoreSwipeItemsManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||||
|
return route.params.submitId;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,265 @@
|
||||||
|
// (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 { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
|
||||||
|
import { CoreUser } from '@features/user/services/user';
|
||||||
|
import {
|
||||||
|
AddonModForum,
|
||||||
|
AddonModForumData,
|
||||||
|
AddonModForumDiscussion,
|
||||||
|
AddonModForumProvider,
|
||||||
|
AddonModForumSortOrder,
|
||||||
|
} from '../services/forum';
|
||||||
|
import { AddonModForumOffline, AddonModForumOfflineDiscussion } from '../services/forum-offline';
|
||||||
|
|
||||||
|
export class AddonModForumDiscussionsSource extends CoreItemsManagerSource<AddonModForumDiscussionItem> {
|
||||||
|
|
||||||
|
static readonly NEW_DISCUSSION: AddonModForumNewDiscussionForm = { newDiscussion: true };
|
||||||
|
|
||||||
|
readonly DISCUSSIONS_PATH_PREFIX: string;
|
||||||
|
readonly COURSE_ID: number;
|
||||||
|
readonly CM_ID: number;
|
||||||
|
|
||||||
|
forum?: AddonModForumData;
|
||||||
|
trackPosts = false;
|
||||||
|
usesGroups = false;
|
||||||
|
selectedSortOrder: AddonModForumSortOrder | null = null;
|
||||||
|
|
||||||
|
constructor(courseId: number, cmId: number, discussionsPathPrefix: string) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.DISCUSSIONS_PATH_PREFIX = discussionsPathPrefix;
|
||||||
|
this.COURSE_ID = courseId;
|
||||||
|
this.CM_ID = cmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to infer NewDiscussionForm objects.
|
||||||
|
*
|
||||||
|
* @param discussion Item to check.
|
||||||
|
* @return Whether the item is a new discussion form.
|
||||||
|
*/
|
||||||
|
isNewDiscussionForm(discussion: AddonModForumDiscussionItem): discussion is AddonModForumNewDiscussionForm {
|
||||||
|
return 'newDiscussion' in discussion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to infer AddonModForumDiscussion objects.
|
||||||
|
*
|
||||||
|
* @param discussion Item to check.
|
||||||
|
* @return Whether the item is an online discussion.
|
||||||
|
*/
|
||||||
|
isOfflineDiscussion(discussion: AddonModForumDiscussionItem): discussion is AddonModForumOfflineDiscussion {
|
||||||
|
return !this.isNewDiscussionForm(discussion) && !this.isOnlineDiscussion(discussion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to infer AddonModForumDiscussion objects.
|
||||||
|
*
|
||||||
|
* @param discussion Item to check.
|
||||||
|
* @return Whether the item is an online discussion.
|
||||||
|
*/
|
||||||
|
isOnlineDiscussion(discussion: AddonModForumDiscussionItem): discussion is AddonModForumDiscussion {
|
||||||
|
return 'id' in discussion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemPath(discussion: AddonModForumDiscussionItem): string {
|
||||||
|
if (this.isOnlineDiscussion(discussion)) {
|
||||||
|
return this.DISCUSSIONS_PATH_PREFIX + discussion.discussion;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isOfflineDiscussion(discussion)) {
|
||||||
|
return `${this.DISCUSSIONS_PATH_PREFIX}new/${discussion.timecreated}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${this.DISCUSSIONS_PATH_PREFIX}new/0`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemQueryParams(discussion: AddonModForumDiscussionItem): Params {
|
||||||
|
return {
|
||||||
|
courseId: this.COURSE_ID,
|
||||||
|
cmId: this.CM_ID,
|
||||||
|
forumId: this.forum?.id,
|
||||||
|
...(this.isOnlineDiscussion(discussion) ? { discussion, trackPosts: this.trackPosts } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getPagesLoaded(): number {
|
||||||
|
if (this.items === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onlineEntries = this.items.filter(item => this.isOnlineDiscussion(item));
|
||||||
|
|
||||||
|
return Math.ceil(onlineEntries.length / this.getPageLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getPageLength(): number {
|
||||||
|
return AddonModForumProvider.DISCUSSIONS_PER_PAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load forum.
|
||||||
|
*/
|
||||||
|
async loadForum(): Promise<void> {
|
||||||
|
this.forum = await AddonModForum.getForum(this.COURSE_ID, this.CM_ID);
|
||||||
|
|
||||||
|
if (typeof this.forum.istracked != 'undefined') {
|
||||||
|
this.trackPosts = this.forum.istracked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async loadPageItems(page: number): Promise<{ items: AddonModForumDiscussionItem[]; hasMoreItems: boolean }> {
|
||||||
|
const discussions: AddonModForumDiscussionItem[] = [];
|
||||||
|
|
||||||
|
if (page === 0) {
|
||||||
|
const offlineDiscussions = await this.loadOfflineDiscussions();
|
||||||
|
|
||||||
|
discussions.push(AddonModForumDiscussionsSource.NEW_DISCUSSION);
|
||||||
|
discussions.push(...offlineDiscussions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { discussions: onlineDiscussions, canLoadMore } = await this.loadOnlineDiscussions(page);
|
||||||
|
|
||||||
|
discussions.push(...onlineDiscussions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: discussions,
|
||||||
|
hasMoreItems: canLoadMore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load online discussions for the given page.
|
||||||
|
*
|
||||||
|
* @param page Page.
|
||||||
|
* @returns Online discussions info.
|
||||||
|
*/
|
||||||
|
private async loadOnlineDiscussions(page: number): Promise<{
|
||||||
|
discussions: AddonModForumDiscussionItem[];
|
||||||
|
canLoadMore: boolean;
|
||||||
|
}> {
|
||||||
|
if (!this.forum || !this.selectedSortOrder) {
|
||||||
|
throw new Error('Can\'t load discussions without a forum or selected sort order');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await AddonModForum.getDiscussions(this.forum.id, {
|
||||||
|
cmId: this.forum.cmid,
|
||||||
|
sortOrder: this.selectedSortOrder.value,
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
let discussions = response.discussions;
|
||||||
|
|
||||||
|
if (this.usesGroups) {
|
||||||
|
discussions = await AddonModForum.formatDiscussionsGroups(this.forum.cmid, discussions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide author for first post and type single.
|
||||||
|
if (this.forum.type === 'single') {
|
||||||
|
for (const discussion of discussions) {
|
||||||
|
if (discussion.userfullname && discussion.parent === 0) {
|
||||||
|
discussion.userfullname = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any discussion has unread posts, the whole forum is being tracked.
|
||||||
|
if (typeof this.forum.istracked === 'undefined' && !this.trackPosts) {
|
||||||
|
for (const discussion of discussions) {
|
||||||
|
if (discussion.numunread > 0) {
|
||||||
|
this.trackPosts = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { discussions, canLoadMore: response.canLoadMore };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load offline discussions.
|
||||||
|
*
|
||||||
|
* @returns Offline discussions.
|
||||||
|
*/
|
||||||
|
private async loadOfflineDiscussions(): Promise<AddonModForumOfflineDiscussion[]> {
|
||||||
|
if (!this.forum) {
|
||||||
|
throw new Error('Can\'t load discussions without a forum');
|
||||||
|
}
|
||||||
|
|
||||||
|
const forum = this.forum;
|
||||||
|
let offlineDiscussions = await AddonModForumOffline.getNewDiscussions(forum.id);
|
||||||
|
|
||||||
|
if (offlineDiscussions.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.usesGroups) {
|
||||||
|
offlineDiscussions = await AddonModForum.formatDiscussionsGroups(forum.cmid, offlineDiscussions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill user data for Offline discussions (should be already cached).
|
||||||
|
const promises = offlineDiscussions.map(async (offlineDiscussion) => {
|
||||||
|
const discussion = offlineDiscussion as unknown as AddonModForumDiscussion;
|
||||||
|
|
||||||
|
if (discussion.parent === 0 || forum.type === 'single') {
|
||||||
|
// Do not show author for first post and type single.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await CoreUser.getProfile(discussion.userid, this.COURSE_ID, true);
|
||||||
|
|
||||||
|
discussion.userfullname = user.fullname;
|
||||||
|
discussion.userpictureurl = user.profileimageurl;
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
// Sort discussion by time (newer first).
|
||||||
|
offlineDiscussions.sort((a, b) => b.timecreated - a.timecreated);
|
||||||
|
|
||||||
|
return offlineDiscussions;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type to select the new discussion form.
|
||||||
|
*/
|
||||||
|
export type AddonModForumNewDiscussionForm = { newDiscussion: true };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of items that can be held by the discussions manager.
|
||||||
|
*/
|
||||||
|
export type AddonModForumDiscussionItem = AddonModForumDiscussion | AddonModForumOfflineDiscussion | AddonModForumNewDiscussionForm;
|
|
@ -0,0 +1,52 @@
|
||||||
|
// (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 { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
|
||||||
|
import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from './forum-discussions-source';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within a collection of discussions.
|
||||||
|
*/
|
||||||
|
export class AddonModForumDiscussionsSwipeManager
|
||||||
|
extends CoreSwipeItemsManager<AddonModForumDiscussionItem, AddonModForumDiscussionsSource> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async navigateToNextItem(): Promise<void> {
|
||||||
|
let delta = -1;
|
||||||
|
const item = await this.getItemBy(-1);
|
||||||
|
|
||||||
|
if (item && this.getSource().isNewDiscussionForm(item)) {
|
||||||
|
delta--;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.navigateToItemBy(delta, 'back');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async navigateToPreviousItem(): Promise<void> {
|
||||||
|
let delta = 1;
|
||||||
|
const item = await this.getItemBy(1);
|
||||||
|
|
||||||
|
if (item && this.getSource().isNewDiscussionForm(item)) {
|
||||||
|
delta++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.navigateToItemBy(delta, 'forward');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,11 +10,11 @@
|
||||||
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper"
|
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" iconAction="far-newspaper"
|
||||||
(action)="gotoBlog()">
|
(action)="gotoBlog()">
|
||||||
</core-context-menu-item>
|
</core-context-menu-item>
|
||||||
<core-context-menu-item *ngIf="discussions.loaded && !(hasOffline || hasOfflineRatings) && isOnline" [priority]="700"
|
<core-context-menu-item *ngIf="discussions && discussions.loaded && !(hasOffline || hasOfflineRatings) && isOnline" [priority]="700"
|
||||||
[content]="'addon.mod_forum.refreshdiscussions' | translate" [iconAction]="refreshIcon" [closeOnClick]="false"
|
[content]="'addon.mod_forum.refreshdiscussions' | translate" [iconAction]="refreshIcon" [closeOnClick]="false"
|
||||||
(action)="doRefresh(null, $event)">
|
(action)="doRefresh(null, $event)">
|
||||||
</core-context-menu-item>
|
</core-context-menu-item>
|
||||||
<core-context-menu-item *ngIf="discussions.loaded && (hasOffline || hasOfflineRatings) && isOnline" [priority]="600"
|
<core-context-menu-item *ngIf="discussions && discussions.loaded && (hasOffline || hasOfflineRatings) && isOnline" [priority]="600"
|
||||||
[content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false"
|
[content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false"
|
||||||
(action)="doRefresh(null, $event, true)">
|
(action)="doRefresh(null, $event, true)">
|
||||||
</core-context-menu-item>
|
</core-context-menu-item>
|
||||||
|
@ -32,11 +32,11 @@
|
||||||
|
|
||||||
<!-- Content. -->
|
<!-- Content. -->
|
||||||
<core-split-view>
|
<core-split-view>
|
||||||
<ion-refresher slot="fixed" [disabled]="!discussions.loaded" (ionRefresh)="doRefresh($event.target)">
|
<ion-refresher slot="fixed" [disabled]="discussions && !discussions.loaded" (ionRefresh)="doRefresh($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]="discussions.loaded">
|
<core-loading [hideUntil]="discussions && discussions.loaded">
|
||||||
<!-- Activity info. -->
|
<!-- Activity info. -->
|
||||||
<core-course-module-info [module]="module" (completionChanged)="onCompletionChange()"
|
<core-course-module-info [module]="module" (completionChanged)="onCompletionChange()"
|
||||||
[description]="forum && forum.type != 'single' && description" [component]="component" [componentId]="componentId"
|
[description]="forum && forum.type != 'single' && description" [component]="component" [componentId]="componentId"
|
||||||
|
@ -57,17 +57,18 @@
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
|
||||||
<ng-container *ngIf="forum">
|
<ng-container *ngIf="forum">
|
||||||
<core-empty-box *ngIf="discussions.empty" icon="far-comments" [message]="'addon.mod_forum.forumnodiscussionsyet' | translate">
|
<core-empty-box *ngIf="!discussions || discussions.empty" icon="far-comments"
|
||||||
|
[message]="'addon.mod_forum.forumnodiscussionsyet' | translate">
|
||||||
</core-empty-box>
|
</core-empty-box>
|
||||||
|
|
||||||
<div *ngIf="!discussions.empty && sortingAvailable && selectedSortOrder" class="ion-text-wrap">
|
<div *ngIf="discussions && !discussions.empty && sortingAvailable && selectedSortOrder" class="ion-text-wrap">
|
||||||
<core-combobox [modalOptions]="sortOrderSelectorModalOptions" listboxId="addon-mod-forum-sort-selector"
|
<core-combobox [modalOptions]="sortOrderSelectorModalOptions" listboxId="addon-mod-forum-sort-selector"
|
||||||
[label]="('core.sort' | translate)" (onChange)="setSortOrder($event)" [selection]="selectedSortOrder.label | translate"
|
[label]="('core.sort' | translate)" (onChange)="setSortOrder($event)" [selection]="selectedSortOrder.label | translate"
|
||||||
interface="modal">
|
interface="modal">
|
||||||
</core-combobox>
|
</core-combobox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ion-item *ngFor="let discussion of discussions.items" class="addon-mod-forum-discussion" detail="true"
|
<ion-item *ngFor="let discussion of discussionsItems" class="addon-mod-forum-discussion" detail="true"
|
||||||
[lines]="discussion.groupname && 'none'" [attr.aria-current]="discussions.getItemAriaCurrent(discussion)"
|
[lines]="discussion.groupname && 'none'" [attr.aria-current]="discussions.getItemAriaCurrent(discussion)"
|
||||||
(click)="discussions.select(discussion)" button>
|
(click)="discussions.select(discussion)" button>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
@ -96,17 +97,16 @@
|
||||||
<ion-icon name="fas-users" [attr.aria-label]="'addon.mod_forum.group' | translate">
|
<ion-icon name="fas-users" [attr.aria-label]="'addon.mod_forum.group' | translate">
|
||||||
</ion-icon> {{ discussion.groupname }}
|
</ion-icon> {{ discussion.groupname }}
|
||||||
</p>
|
</p>
|
||||||
<p *ngIf="discussions.isOnlineDiscussion(discussion)">
|
<p *ngIf="isOnlineDiscussion(discussion)">
|
||||||
{{discussion.created * 1000 | coreFormatDate: "strftimerecentfull"}}
|
{{discussion.created * 1000 | coreFormatDate: "strftimerecentfull"}}
|
||||||
</p>
|
</p>
|
||||||
<p *ngIf="discussions.isOfflineDiscussion(discussion)">
|
<p *ngIf="isOfflineDiscussion(discussion)">
|
||||||
<ion-icon name="fas-clock" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-clock" aria-hidden="true"></ion-icon>
|
||||||
{{ 'core.notsent' | translate }}
|
{{ 'core.notsent' | translate }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ion-row *ngIf="discussions.isOnlineDiscussion(discussion)"
|
<ion-row *ngIf="isOnlineDiscussion(discussion)" class="ion-text-center addon-mod-forum-discussion-more-info">
|
||||||
class="ion-text-center addon-mod-forum-discussion-more-info">
|
|
||||||
<ion-col class="ion-text-start">
|
<ion-col class="ion-text-start">
|
||||||
<ion-note>
|
<ion-note>
|
||||||
<ion-icon name="fas-clock" aria-hidden="true"></ion-icon> {{ 'addon.mod_forum.lastpost' | translate }}
|
<ion-icon name="fas-clock" aria-hidden="true"></ion-icon> {{ 'addon.mod_forum.lastpost' | translate }}
|
||||||
|
@ -134,7 +134,7 @@
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<core-infinite-loading [enabled]="discussions.onlineLoaded && !discussions.completed" [error]="discussions.fetchFailed"
|
<core-infinite-loading [enabled]="discussions && discussions.loaded && !discussions.completed" [error]="fetchFailed"
|
||||||
(action)="fetchMoreDiscussions($event)">
|
(action)="fetchMoreDiscussions($event)">
|
||||||
</core-infinite-loading>
|
</core-infinite-loading>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, Optional, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core';
|
import { Component, Optional, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { IonContent } from '@ionic/angular';
|
import { IonContent } from '@ionic/angular';
|
||||||
import { ModalOptions } from '@ionic/core';
|
import { ModalOptions } from '@ionic/core';
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ import {
|
||||||
AddonModForumNewDiscussionData,
|
AddonModForumNewDiscussionData,
|
||||||
AddonModForumReplyDiscussionData,
|
AddonModForumReplyDiscussionData,
|
||||||
} from '@addons/mod/forum/services/forum';
|
} from '@addons/mod/forum/services/forum';
|
||||||
import { AddonModForumOffline, AddonModForumOfflineDiscussion } from '@addons/mod/forum/services/forum-offline';
|
import { AddonModForumOffline } from '@addons/mod/forum/services/forum-offline';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||||
import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper';
|
import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper';
|
||||||
|
@ -44,7 +44,6 @@ import { CoreUser } from '@features/user/services/user';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreCourse } from '@features/course/services/course';
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { AddonModForumDiscussionOptionsMenuComponent } from '../discussion-options-menu/discussion-options-menu';
|
import { AddonModForumDiscussionOptionsMenuComponent } from '../discussion-options-menu/discussion-options-menu';
|
||||||
import { AddonModForumSortOrderSelectorComponent } from '../sort-order-selector/sort-order-selector';
|
import { AddonModForumSortOrderSelectorComponent } from '../sort-order-selector/sort-order-selector';
|
||||||
|
@ -56,6 +55,9 @@ import { CoreRatingProvider } from '@features/rating/services/rating';
|
||||||
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
|
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
|
||||||
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
||||||
import { ContextLevel } from '@/core/constants';
|
import { ContextLevel } from '@/core/constants';
|
||||||
|
import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
||||||
|
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||||
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays a forum entry page.
|
* Component that displays a forum entry page.
|
||||||
|
@ -72,24 +74,21 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
component = AddonModForumProvider.COMPONENT;
|
component = AddonModForumProvider.COMPONENT;
|
||||||
moduleName = 'forum';
|
moduleName = 'forum';
|
||||||
descriptionNote?: string;
|
descriptionNote?: string;
|
||||||
forum?: AddonModForumData;
|
discussions!: AddonModForumDiscussionsManager;
|
||||||
discussions: AddonModForumDiscussionsManager;
|
discussionsItems: AddonModForumDiscussionItem[] = [];
|
||||||
|
fetchFailed = false;
|
||||||
canAddDiscussion = false;
|
canAddDiscussion = false;
|
||||||
addDiscussionText!: string;
|
addDiscussionText!: string;
|
||||||
availabilityMessage: string | null = null;
|
availabilityMessage: string | null = null;
|
||||||
sortingAvailable!: boolean;
|
sortingAvailable!: boolean;
|
||||||
sortOrders: AddonModForumSortOrder[] = [];
|
sortOrders: AddonModForumSortOrder[] = [];
|
||||||
selectedSortOrder: AddonModForumSortOrder | null = null;
|
|
||||||
canPin = false;
|
canPin = false;
|
||||||
trackPosts = false;
|
|
||||||
hasOfflineRatings = false;
|
hasOfflineRatings = false;
|
||||||
sortOrderSelectorModalOptions: ModalOptions = {
|
sortOrderSelectorModalOptions: ModalOptions = {
|
||||||
component: AddonModForumSortOrderSelectorComponent,
|
component: AddonModForumSortOrderSelectorComponent,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected syncEventName = AddonModForumSyncProvider.AUTO_SYNCED;
|
protected syncEventName = AddonModForumSyncProvider.AUTO_SYNCED;
|
||||||
protected page = 0;
|
|
||||||
protected usesGroups = false;
|
|
||||||
protected syncManualObserver?: CoreEventObserver; // It will observe the sync manual event.
|
protected syncManualObserver?: CoreEventObserver; // It will observe the sync manual event.
|
||||||
protected replyObserver?: CoreEventObserver;
|
protected replyObserver?: CoreEventObserver;
|
||||||
protected newDiscObserver?: CoreEventObserver;
|
protected newDiscObserver?: CoreEventObserver;
|
||||||
|
@ -97,19 +96,42 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
protected changeDiscObserver?: CoreEventObserver;
|
protected changeDiscObserver?: CoreEventObserver;
|
||||||
protected ratingOfflineObserver?: CoreEventObserver;
|
protected ratingOfflineObserver?: CoreEventObserver;
|
||||||
protected ratingSyncObserver?: CoreEventObserver;
|
protected ratingSyncObserver?: CoreEventObserver;
|
||||||
|
protected sourceUnsubscribe?: () => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
route: ActivatedRoute,
|
public route: ActivatedRoute,
|
||||||
@Optional() protected content?: IonContent,
|
@Optional() protected content?: IonContent,
|
||||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||||
) {
|
) {
|
||||||
super('AddonModForumIndexComponent', content, courseContentsPage);
|
super('AddonModForumIndexComponent', content, courseContentsPage);
|
||||||
|
}
|
||||||
|
|
||||||
this.discussions = new AddonModForumDiscussionsManager(
|
get forum(): AddonModForumData | undefined {
|
||||||
route.component,
|
return this.discussions?.getSource().forum;
|
||||||
this,
|
}
|
||||||
courseContentsPage ? `${AddonModForumModuleHandlerService.PAGE_NAME}/` : '',
|
|
||||||
);
|
get selectedSortOrder(): AddonModForumSortOrder | undefined {
|
||||||
|
return this.discussions?.getSource().selectedSortOrder ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a discussion is online.
|
||||||
|
*
|
||||||
|
* @param discussion Discussion
|
||||||
|
* @return Whether the discussion is online.
|
||||||
|
*/
|
||||||
|
isOnlineDiscussion(discussion: AddonModForumDiscussionItem): boolean {
|
||||||
|
return this.discussions && this.discussions.getSource().isOnlineDiscussion(discussion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a discussion is offline.
|
||||||
|
*
|
||||||
|
* @param discussion Discussion
|
||||||
|
* @return Whether the discussion is offline.
|
||||||
|
*/
|
||||||
|
isOfflineDiscussion(discussion: AddonModForumDiscussionItem): boolean {
|
||||||
|
return this.discussions && this.discussions.getSource().isOfflineDiscussion(discussion);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,6 +148,48 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
|
|
||||||
await super.ngOnInit();
|
await super.ngOnInit();
|
||||||
|
|
||||||
|
// Initialize discussions manager.
|
||||||
|
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
AddonModForumDiscussionsSource,
|
||||||
|
[this.courseId, this.module.id, this.courseContentsPage ? `${AddonModForumModuleHandlerService.PAGE_NAME}/` : ''],
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sourceUnsubscribe = source.addListener({
|
||||||
|
onItemsUpdated: async discussions => {
|
||||||
|
this.discussionsItems = discussions.filter(discussion => !source.isNewDiscussionForm(discussion));
|
||||||
|
|
||||||
|
if (!this.forum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are replies for discussions stored in offline.
|
||||||
|
const hasOffline = await AddonModForumOffline.hasForumReplies(this.forum.id);
|
||||||
|
|
||||||
|
this.hasOffline = this.hasOffline || hasOffline;
|
||||||
|
|
||||||
|
if (hasOffline) {
|
||||||
|
// Only update new fetched discussions.
|
||||||
|
const promises = discussions.map(async (discussion) => {
|
||||||
|
if (!this.discussions.getSource().isOnlineDiscussion(discussion)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get offline discussions.
|
||||||
|
const replies = await AddonModForumOffline.getDiscussionReplies(discussion.discussion);
|
||||||
|
|
||||||
|
discussion.numreplies = Number(discussion.numreplies) + replies.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onReset: () => {
|
||||||
|
this.discussionsItems = [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.discussions = new AddonModForumDiscussionsManager(source, this);
|
||||||
|
|
||||||
// Refresh data if this forum discussion is synchronized from discussions list.
|
// Refresh data if this forum discussion is synchronized from discussions list.
|
||||||
this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, (data) => {
|
this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, (data) => {
|
||||||
this.autoSyncEventReceived(data);
|
this.autoSyncEventReceived(data);
|
||||||
|
@ -141,12 +205,16 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
this.eventReceived.bind(this, false),
|
this.eventReceived.bind(this, false),
|
||||||
);
|
);
|
||||||
this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data => {
|
this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data => {
|
||||||
if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module.id) {
|
if (!this.forum) {
|
||||||
AddonModForum.invalidateDiscussionsList(this.forum!.id).finally(() => {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.forum.id === data.forumId || data.cmId === this.module.id) {
|
||||||
|
AddonModForum.invalidateDiscussionsList(this.forum.id).finally(() => {
|
||||||
if (data.discussionId) {
|
if (data.discussionId) {
|
||||||
// Discussion changed, search it in the list of discussions.
|
// Discussion changed, search it in the list of discussions.
|
||||||
const discussion = this.discussions.items.find(
|
const discussion = this.discussions.items.find(
|
||||||
(disc) => this.discussions.isOnlineDiscussion(disc) && data.discussionId == disc.discussion,
|
(disc) => this.discussions.getSource().isOnlineDiscussion(disc) && data.discussionId == disc.discussion,
|
||||||
) as AddonModForumDiscussion;
|
) as AddonModForumDiscussion;
|
||||||
|
|
||||||
if (discussion) {
|
if (discussion) {
|
||||||
|
@ -196,20 +264,6 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
async ngAfterViewInit(): Promise<void> {
|
async ngAfterViewInit(): Promise<void> {
|
||||||
await this.loadContent(false, true);
|
await this.loadContent(false, true);
|
||||||
|
|
||||||
if (!this.forum) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CoreUtils.ignoreErrors(
|
|
||||||
AddonModForum.instance
|
|
||||||
.logView(this.forum.id, this.forum.name)
|
|
||||||
.then(async () => {
|
|
||||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.discussions.start(this.splitView);
|
this.discussions.start(this.splitView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,6 +280,8 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
this.changeDiscObserver && this.changeDiscObserver.off();
|
this.changeDiscObserver && this.changeDiscObserver.off();
|
||||||
this.ratingOfflineObserver && this.ratingOfflineObserver.off();
|
this.ratingOfflineObserver && this.ratingOfflineObserver.off();
|
||||||
this.ratingSyncObserver && this.ratingSyncObserver.off();
|
this.ratingSyncObserver && this.ratingSyncObserver.off();
|
||||||
|
this.sourceUnsubscribe && this.sourceUnsubscribe();
|
||||||
|
this.discussions.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -236,19 +292,21 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @param showErrors Wether to show errors to the user or hide them.
|
* @param showErrors Wether to show errors to the user or hide them.
|
||||||
*/
|
*/
|
||||||
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
this.discussions.fetchFailed = false;
|
this.fetchFailed = false;
|
||||||
|
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
|
|
||||||
promises.push(this.fetchForum(sync, showErrors));
|
|
||||||
promises.push(this.fetchSortOrderPreference());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(promises);
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.fetchOfflineDiscussions(),
|
this.fetchForum(sync, showErrors),
|
||||||
this.fetchDiscussions(refresh),
|
this.fetchSortOrderPreference(),
|
||||||
CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.forum!.cmid).then((hasRatings) => {
|
]);
|
||||||
|
|
||||||
|
if (!this.forum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
refresh ? this.discussions.reload() : this.discussions.load(),
|
||||||
|
CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.forum.cmid).then((hasRatings) => {
|
||||||
this.hasOfflineRatings = hasRatings;
|
this.hasOfflineRatings = hasRatings;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -258,7 +316,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true);
|
||||||
|
|
||||||
this.discussions.fetchFailed = true; // Set to prevent infinite calls with infinite-loading.
|
this.fetchFailed = true; // Set to prevent infinite calls with infinite-loading.
|
||||||
} else {
|
} else {
|
||||||
// Get forum failed, retry without using cache since it might be a new activity.
|
// Get forum failed, retry without using cache since it might be a new activity.
|
||||||
await this.refreshContent(sync);
|
await this.refreshContent(sync);
|
||||||
|
@ -273,19 +331,19 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const forum = await AddonModForum.getForum(this.courseId, this.module.id);
|
await this.discussions.getSource().loadForum();
|
||||||
|
|
||||||
this.forum = forum;
|
if (!this.forum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forum = this.forum;
|
||||||
this.description = forum.intro || this.description;
|
this.description = forum.intro || this.description;
|
||||||
this.availabilityMessage = AddonModForumHelper.getAvailabilityMessage(forum);
|
this.availabilityMessage = AddonModForumHelper.getAvailabilityMessage(forum);
|
||||||
this.descriptionNote = Translate.instant('addon.mod_forum.numdiscussions', {
|
this.descriptionNote = Translate.instant('addon.mod_forum.numdiscussions', {
|
||||||
numdiscussions: forum.numdiscussions,
|
numdiscussions: forum.numdiscussions,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof forum.istracked != 'undefined') {
|
|
||||||
this.trackPosts = forum.istracked;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dataRetrieved.emit(forum);
|
this.dataRetrieved.emit(forum);
|
||||||
|
|
||||||
switch (forum.type) {
|
switch (forum.type) {
|
||||||
|
@ -319,10 +377,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
// Check if the activity uses groups.
|
// Check if the activity uses groups.
|
||||||
promises.push(
|
promises.push(
|
||||||
CoreGroups.instance
|
CoreGroups.instance
|
||||||
.getActivityGroupMode(this.forum.cmid)
|
.getActivityGroupMode(forum.cmid)
|
||||||
.then(async mode => {
|
.then(async mode => {
|
||||||
this.usesGroups = mode === CoreGroupsProvider.SEPARATEGROUPS
|
this.discussions.getSource().usesGroups =
|
||||||
|| mode === CoreGroupsProvider.VISIBLEGROUPS;
|
mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}),
|
}),
|
||||||
|
@ -330,14 +388,14 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
|
|
||||||
promises.push(
|
promises.push(
|
||||||
AddonModForum.instance
|
AddonModForum.instance
|
||||||
.getAccessInformation(this.forum.id, { cmId: this.module.id })
|
.getAccessInformation(forum.id, { cmId: this.module.id })
|
||||||
.then(async accessInfo => {
|
.then(async accessInfo => {
|
||||||
// Disallow adding discussions if cut-off date is reached and the user has not the
|
// Disallow adding discussions if cut-off date is reached and the user has not the
|
||||||
// capability to override it.
|
// capability to override it.
|
||||||
// Just in case the forum was fetched from WS when the cut-off date was not reached but it is now.
|
// Just in case the forum was fetched from WS when the cut-off date was not reached but it is now.
|
||||||
const cutoffDateReached = AddonModForumHelper.isCutoffDateReached(this.forum!)
|
const cutoffDateReached = AddonModForumHelper.isCutoffDateReached(forum)
|
||||||
&& !accessInfo.cancanoverridecutoff;
|
&& !accessInfo.cancanoverridecutoff;
|
||||||
this.canAddDiscussion = !!this.forum?.cancreatediscussions && !cutoffDateReached;
|
this.canAddDiscussion = !!forum.cancreatediscussions && !cutoffDateReached;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}),
|
}),
|
||||||
|
@ -347,7 +405,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
// Use the canAddDiscussion WS to check if the user can pin discussions.
|
// Use the canAddDiscussion WS to check if the user can pin discussions.
|
||||||
promises.push(
|
promises.push(
|
||||||
AddonModForum.instance
|
AddonModForum.instance
|
||||||
.canAddDiscussionToAll(this.forum.id, { cmId: this.module.id })
|
.canAddDiscussionToAll(forum.id, { cmId: this.module.id })
|
||||||
.then(async response => {
|
.then(async response => {
|
||||||
this.canPin = !!response.canpindiscussions;
|
this.canPin = !!response.canpindiscussions;
|
||||||
|
|
||||||
|
@ -366,124 +424,6 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience function to fetch offline discussions.
|
|
||||||
*
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
protected async fetchOfflineDiscussions(): Promise<void> {
|
|
||||||
const forum = this.forum!;
|
|
||||||
let offlineDiscussions = await AddonModForumOffline.getNewDiscussions(forum.id);
|
|
||||||
this.hasOffline = !!offlineDiscussions.length;
|
|
||||||
|
|
||||||
if (!this.hasOffline) {
|
|
||||||
this.discussions.setOfflineDiscussions([]);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.usesGroups) {
|
|
||||||
offlineDiscussions = await AddonModForum.formatDiscussionsGroups(forum.cmid, offlineDiscussions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill user data for Offline discussions (should be already cached).
|
|
||||||
const promises = offlineDiscussions.map(async (offlineDiscussion) => {
|
|
||||||
const discussion = offlineDiscussion as unknown as AddonModForumDiscussion;
|
|
||||||
|
|
||||||
if (discussion.parent === 0 || forum.type === 'single') {
|
|
||||||
// Do not show author for first post and type single.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = await CoreUser.getProfile(discussion.userid, this.courseId, true);
|
|
||||||
|
|
||||||
discussion.userfullname = user.fullname;
|
|
||||||
discussion.userpictureurl = user.profileimageurl;
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore errors.
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
|
|
||||||
// Sort discussion by time (newer first).
|
|
||||||
offlineDiscussions.sort((a, b) => b.timecreated - a.timecreated);
|
|
||||||
|
|
||||||
this.discussions.setOfflineDiscussions(offlineDiscussions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience function to get forum discussions.
|
|
||||||
*
|
|
||||||
* @param refresh Whether we're refreshing data.
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
protected async fetchDiscussions(refresh: boolean): Promise<void> {
|
|
||||||
const forum = this.forum!;
|
|
||||||
this.discussions.fetchFailed = false;
|
|
||||||
|
|
||||||
if (refresh) {
|
|
||||||
this.page = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await AddonModForum.getDiscussions(forum.id, {
|
|
||||||
cmId: forum.cmid,
|
|
||||||
sortOrder: this.selectedSortOrder!.value,
|
|
||||||
page: this.page,
|
|
||||||
});
|
|
||||||
let discussions = response.discussions;
|
|
||||||
|
|
||||||
if (this.usesGroups) {
|
|
||||||
discussions = await AddonModForum.formatDiscussionsGroups(forum.cmid, discussions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide author for first post and type single.
|
|
||||||
if (forum.type === 'single') {
|
|
||||||
for (const discussion of discussions) {
|
|
||||||
if (discussion.userfullname && discussion.parent === 0) {
|
|
||||||
discussion.userfullname = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If any discussion has unread posts, the whole forum is being tracked.
|
|
||||||
if (typeof forum.istracked === 'undefined' && !this.trackPosts) {
|
|
||||||
for (const discussion of discussions) {
|
|
||||||
if (discussion.numunread > 0) {
|
|
||||||
this.trackPosts = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.page === 0) {
|
|
||||||
this.discussions.setOnlineDiscussions(discussions, response.canLoadMore);
|
|
||||||
} else {
|
|
||||||
this.discussions.setItems(this.discussions.items.concat(discussions), response.canLoadMore);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.page++;
|
|
||||||
|
|
||||||
// Check if there are replies for discussions stored in offline.
|
|
||||||
const hasOffline = await AddonModForumOffline.hasForumReplies(forum.id);
|
|
||||||
|
|
||||||
this.hasOffline = this.hasOffline || hasOffline;
|
|
||||||
|
|
||||||
if (hasOffline) {
|
|
||||||
// Only update new fetched discussions.
|
|
||||||
const promises = discussions.map(async (discussion) => {
|
|
||||||
// Get offline discussions.
|
|
||||||
const replies = await AddonModForumOffline.getDiscussionReplies(discussion.discussion);
|
|
||||||
|
|
||||||
discussion.numreplies = Number(discussion.numreplies) + replies.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience function to load more forum discussions.
|
* Convenience function to load more forum discussions.
|
||||||
*
|
*
|
||||||
|
@ -492,11 +432,13 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
*/
|
*/
|
||||||
async fetchMoreDiscussions(complete: () => void): Promise<void> {
|
async fetchMoreDiscussions(complete: () => void): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.fetchDiscussions(false);
|
this.fetchFailed = false;
|
||||||
|
|
||||||
|
await this.discussions.load();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true);
|
||||||
|
|
||||||
this.discussions.fetchFailed = true;
|
this.fetchFailed = true;
|
||||||
} finally {
|
} finally {
|
||||||
complete();
|
complete();
|
||||||
}
|
}
|
||||||
|
@ -521,9 +463,13 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = await getSortOrder();
|
const value = await getSortOrder();
|
||||||
|
const selectedOrder = this.sortOrders.find(sortOrder => sortOrder.value === value) || this.sortOrders[0];
|
||||||
|
|
||||||
this.selectedSortOrder = this.sortOrders.find(sortOrder => sortOrder.value === value) || this.sortOrders[0];
|
this.discussions.getSource().selectedSortOrder = selectedOrder;
|
||||||
this.sortOrderSelectorModalOptions.componentProps!.selected = this.selectedSortOrder.value;
|
|
||||||
|
if (this.sortOrderSelectorModalOptions.componentProps) {
|
||||||
|
this.sortOrderSelectorModalOptions.componentProps.selected = selectedOrder.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -597,11 +543,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
if (isNewDiscussion && CoreScreen.isTablet) {
|
if (isNewDiscussion && CoreScreen.isTablet) {
|
||||||
const newDiscussionData = data as AddonModForumNewDiscussionData;
|
const newDiscussionData = data as AddonModForumNewDiscussionData;
|
||||||
const discussion = this.discussions.items.find(disc => {
|
const discussion = this.discussions.items.find(disc => {
|
||||||
if (this.discussions.isOfflineDiscussion(disc)) {
|
if (this.discussions.getSource().isOfflineDiscussion(disc)) {
|
||||||
return disc.timecreated === newDiscussionData.discTimecreated;
|
return disc.timecreated === newDiscussionData.discTimecreated;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.discussions.isOnlineDiscussion(disc)) {
|
if (this.discussions.getSource().isOnlineDiscussion(disc)) {
|
||||||
return CoreArray.contains(newDiscussionData.discussionIds ?? [], disc.discussion);
|
return CoreArray.contains(newDiscussionData.discussionIds ?? [], disc.discussion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -625,7 +571,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @param timeCreated Creation time of the offline discussion.
|
* @param timeCreated Creation time of the offline discussion.
|
||||||
*/
|
*/
|
||||||
openNewDiscussion(): void {
|
openNewDiscussion(): void {
|
||||||
this.discussions.select({ newDiscussion: true });
|
this.discussions.select(AddonModForumDiscussionsSource.NEW_DISCUSSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -634,10 +580,13 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @param sortOrder Sort order new data.
|
* @param sortOrder Sort order new data.
|
||||||
*/
|
*/
|
||||||
async setSortOrder(sortOrder: AddonModForumSortOrder): Promise<void> {
|
async setSortOrder(sortOrder: AddonModForumSortOrder): Promise<void> {
|
||||||
if (sortOrder.value != this.selectedSortOrder?.value) {
|
if (sortOrder.value != this.discussions.getSource().selectedSortOrder?.value) {
|
||||||
this.selectedSortOrder = sortOrder;
|
this.discussions.getSource().selectedSortOrder = sortOrder;
|
||||||
this.sortOrderSelectorModalOptions.componentProps!.selected = this.selectedSortOrder.value;
|
this.discussions.getSource().setDirty(true);
|
||||||
this.page = 0;
|
|
||||||
|
if (this.sortOrderSelectorModalOptions.componentProps) {
|
||||||
|
this.sortOrderSelectorModalOptions.componentProps.selected = sortOrder.value;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await CoreUser.setUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, sortOrder.value.toFixed(0));
|
await CoreUser.setUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, sortOrder.value.toFixed(0));
|
||||||
|
@ -666,6 +615,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @param discussion Discussion.
|
* @param discussion Discussion.
|
||||||
*/
|
*/
|
||||||
async showOptionsMenu(event: Event, discussion: AddonModForumDiscussion): Promise<void> {
|
async showOptionsMenu(event: Event, discussion: AddonModForumDiscussion): Promise<void> {
|
||||||
|
if (!this.forum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
|
@ -673,7 +626,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
component: AddonModForumDiscussionOptionsMenuComponent,
|
component: AddonModForumDiscussionOptionsMenuComponent,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
discussion,
|
discussion,
|
||||||
forumId: this.forum!.id,
|
forumId: this.forum.id,
|
||||||
cmId: this.module.id,
|
cmId: this.module.id,
|
||||||
},
|
},
|
||||||
event,
|
event,
|
||||||
|
@ -698,125 +651,47 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Type to select the new discussion form.
|
|
||||||
*/
|
|
||||||
type NewDiscussionForm = { newDiscussion: true };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of items that can be held by the discussions manager.
|
|
||||||
*/
|
|
||||||
type DiscussionItem = AddonModForumDiscussion | AddonModForumOfflineDiscussion | NewDiscussionForm;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discussions manager.
|
* Discussions manager.
|
||||||
*/
|
*/
|
||||||
class AddonModForumDiscussionsManager extends CorePageItemsListManager<DiscussionItem> {
|
class AddonModForumDiscussionsManager extends CoreListItemsManager<AddonModForumDiscussionItem, AddonModForumDiscussionsSource> {
|
||||||
|
|
||||||
onlineLoaded = false;
|
page: AddonModForumIndexComponent;
|
||||||
fetchFailed = false;
|
|
||||||
|
|
||||||
private discussionsPathPrefix: string;
|
constructor(source: AddonModForumDiscussionsSource, page: AddonModForumIndexComponent) {
|
||||||
private component: AddonModForumIndexComponent;
|
super(source, page.route.component);
|
||||||
|
|
||||||
constructor(pageComponent: unknown, component: AddonModForumIndexComponent, discussionsPathPrefix: string) {
|
this.page = page;
|
||||||
super(pageComponent);
|
|
||||||
|
|
||||||
this.component = component;
|
|
||||||
this.discussionsPathPrefix = discussionsPathPrefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
get loaded(): boolean {
|
|
||||||
return super.loaded && (this.onlineLoaded || this.fetchFailed);
|
|
||||||
}
|
|
||||||
|
|
||||||
get onlineDiscussions(): AddonModForumDiscussion[] {
|
|
||||||
return this.items.filter(discussion => this.isOnlineDiscussion(discussion)) as AddonModForumDiscussion[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
getItemQueryParams(discussion: DiscussionItem): Params {
|
protected getDefaultItem(): AddonModForumDiscussionItem | null {
|
||||||
return {
|
const source = this.getSource();
|
||||||
courseId: this.component.courseId,
|
|
||||||
cmId: this.component.module.id,
|
|
||||||
forumId: this.component.forum!.id,
|
|
||||||
...(this.isOnlineDiscussion(discussion) ? { discussion, trackPosts: this.component.trackPosts } : {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return this.items.find(discussion => !source.isNewDiscussionForm(discussion)) || null;
|
||||||
* Type guard to infer NewDiscussionForm objects.
|
|
||||||
*
|
|
||||||
* @param discussion Item to check.
|
|
||||||
* @return Whether the item is a new discussion form.
|
|
||||||
*/
|
|
||||||
isNewDiscussionForm(discussion: DiscussionItem): discussion is NewDiscussionForm {
|
|
||||||
return 'newDiscussion' in discussion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to infer AddonModForumDiscussion objects.
|
|
||||||
*
|
|
||||||
* @param discussion Item to check.
|
|
||||||
* @return Whether the item is an online discussion.
|
|
||||||
*/
|
|
||||||
isOfflineDiscussion(discussion: DiscussionItem): discussion is AddonModForumOfflineDiscussion {
|
|
||||||
return !this.isNewDiscussionForm(discussion)
|
|
||||||
&& !this.isOnlineDiscussion(discussion);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to infer AddonModForumDiscussion objects.
|
|
||||||
*
|
|
||||||
* @param discussion Item to check.
|
|
||||||
* @return Whether the item is an online discussion.
|
|
||||||
*/
|
|
||||||
isOnlineDiscussion(discussion: DiscussionItem): discussion is AddonModForumDiscussion {
|
|
||||||
return 'id' in discussion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update online discussion items.
|
|
||||||
*
|
|
||||||
* @param onlineDiscussions Online discussions
|
|
||||||
*/
|
|
||||||
setOnlineDiscussions(onlineDiscussions: AddonModForumDiscussion[], hasMoreItems: boolean = false): void {
|
|
||||||
const otherDiscussions = this.items.filter(discussion => !this.isOnlineDiscussion(discussion));
|
|
||||||
|
|
||||||
this.setItems(otherDiscussions.concat(onlineDiscussions), hasMoreItems);
|
|
||||||
this.onlineLoaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update offline discussion items.
|
|
||||||
*
|
|
||||||
* @param offlineDiscussions Offline discussions
|
|
||||||
*/
|
|
||||||
setOfflineDiscussions(offlineDiscussions: AddonModForumOfflineDiscussion[]): void {
|
|
||||||
const otherDiscussions = this.items.filter(discussion => !this.isOfflineDiscussion(discussion));
|
|
||||||
|
|
||||||
this.setItems((offlineDiscussions as DiscussionItem[]).concat(otherDiscussions), this.hasMoreItems);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected getItemPath(discussion: DiscussionItem): string {
|
protected async logActivity(): Promise<void> {
|
||||||
const getRelativePath = () => {
|
const forum = this.getSource().forum;
|
||||||
if (this.isOnlineDiscussion(discussion)) {
|
|
||||||
return discussion.discussion;
|
if (!forum) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isOfflineDiscussion(discussion)) {
|
CoreUtils.ignoreErrors(
|
||||||
return `new/${discussion.timecreated}`;
|
AddonModForum.instance
|
||||||
}
|
.logView(forum.id, forum.name)
|
||||||
|
.then(async () => {
|
||||||
|
CoreCourse.checkModuleCompletion(this.page.courseId, this.page.module.completiondata);
|
||||||
|
|
||||||
return 'new/0';
|
return;
|
||||||
};
|
}),
|
||||||
|
);
|
||||||
return this.discussionsPathPrefix + getRelativePath();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ const mainMenuRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/discussion/:discussionId`,
|
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/discussion/:discussionId`,
|
||||||
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
||||||
|
data: { swipeEnabled: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: AddonModForumModuleHandlerService.PAGE_NAME,
|
path: AddonModForumModuleHandlerService.PAGE_NAME,
|
||||||
|
@ -66,10 +67,12 @@ const mainMenuRoutes: Routes = [
|
||||||
path: `${COURSE_CONTENTS_PATH}/${AddonModForumModuleHandlerService.PAGE_NAME}/new/:timeCreated`,
|
path: `${COURSE_CONTENTS_PATH}/${AddonModForumModuleHandlerService.PAGE_NAME}/new/:timeCreated`,
|
||||||
loadChildren: () => import('./pages/new-discussion/new-discussion.module')
|
loadChildren: () => import('./pages/new-discussion/new-discussion.module')
|
||||||
.then(m => m.AddonForumNewDiscussionPageModule),
|
.then(m => m.AddonForumNewDiscussionPageModule),
|
||||||
|
data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${COURSE_CONTENTS_PATH}/${AddonModForumModuleHandlerService.PAGE_NAME}/:discussionId`,
|
path: `${COURSE_CONTENTS_PATH}/${AddonModForumModuleHandlerService.PAGE_NAME}/:discussionId`,
|
||||||
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
||||||
|
data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
() => CoreScreen.isMobile,
|
() => CoreScreen.isMobile,
|
||||||
|
@ -82,10 +85,12 @@ const courseContentsRoutes: Routes = conditionalRoutes(
|
||||||
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/new/:timeCreated`,
|
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/new/:timeCreated`,
|
||||||
loadChildren: () => import('./pages/new-discussion/new-discussion.module')
|
loadChildren: () => import('./pages/new-discussion/new-discussion.module')
|
||||||
.then(m => m.AddonForumNewDiscussionPageModule),
|
.then(m => m.AddonForumNewDiscussionPageModule),
|
||||||
|
data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/:discussionId`,
|
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/:discussionId`,
|
||||||
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
||||||
|
data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
() => CoreScreen.isTablet,
|
() => CoreScreen.isTablet,
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
</core-context-menu>
|
</core-context-menu>
|
||||||
</core-navbar-buttons>
|
</core-navbar-buttons>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
|
<core-swipe-navigation [manager]="discussions">
|
||||||
<ion-refresher slot="fixed" [disabled]="!discussionLoaded" (ionRefresh)="doRefresh($event.target)">
|
<ion-refresher slot="fixed" [disabled]="!discussionLoaded" (ionRefresh)="doRefresh($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>
|
||||||
|
@ -87,8 +88,8 @@
|
||||||
<div *ngIf="startingPost" class="ion-margin-bottom">
|
<div *ngIf="startingPost" class="ion-margin-bottom">
|
||||||
<addon-mod-forum-post [post]="startingPost" [discussion]="discussion" [courseId]="courseId" [highlight]="true"
|
<addon-mod-forum-post [post]="startingPost" [discussion]="discussion" [courseId]="courseId" [highlight]="true"
|
||||||
[discussionId]="discussionId" [component]="component" [componentId]="cmId" [formData]="formData"
|
[discussionId]="discussionId" [component]="component" [componentId]="cmId" [formData]="formData"
|
||||||
[originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo"
|
[originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts"
|
||||||
[leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
[ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
||||||
</addon-mod-forum-post>
|
</addon-mod-forum-post>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -96,9 +97,9 @@
|
||||||
<ng-container *ngFor="let post of posts; first as first">
|
<ng-container *ngFor="let post of posts; first as first">
|
||||||
<core-spacer *ngIf="!first"></core-spacer>
|
<core-spacer *ngIf="!first"></core-spacer>
|
||||||
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component"
|
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component"
|
||||||
[componentId]="cmId" [formData]="formData" [originalData]="originalData" [parentSubject]="postSubjects[post.parentid]"
|
[componentId]="cmId" [formData]="formData" [originalData]="originalData"
|
||||||
[forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo"
|
[parentSubject]="postSubjects[post.parentid]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts"
|
||||||
[leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
[ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
||||||
</addon-mod-forum-post>
|
</addon-mod-forum-post>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
|
@ -112,9 +113,9 @@
|
||||||
<ng-template #nestedPosts let-post="post">
|
<ng-template #nestedPosts let-post="post">
|
||||||
<ion-card>
|
<ion-card>
|
||||||
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component"
|
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component"
|
||||||
[componentId]="cmId" [formData]="formData" [originalData]="originalData" [parentSubject]="postSubjects[post.parentid]"
|
[componentId]="cmId" [formData]="formData" [originalData]="originalData"
|
||||||
[forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo"
|
[parentSubject]="postSubjects[post.parentid]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts"
|
||||||
[leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
[ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
||||||
</addon-mod-forum-post>
|
</addon-mod-forum-post>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
<div class="ion-padding-start" *ngIf="post.children && post.children.length && post.children[0].subject">
|
<div class="ion-padding-start" *ngIf="post.children && post.children.length && post.children[0].subject">
|
||||||
|
@ -124,4 +125,5 @@
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
</core-swipe-navigation>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
|
|
||||||
import { ContextLevel, CoreConstants } from '@/core/constants';
|
import { ContextLevel, CoreConstants } from '@/core/constants';
|
||||||
import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Optional } from '@angular/core';
|
import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Optional } from '@angular/core';
|
||||||
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||||
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
|
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
|
||||||
|
@ -32,6 +34,8 @@ import { Network, NgZone, Translate } from '@singletons';
|
||||||
import { CoreArray } from '@singletons/array';
|
import { CoreArray } from '@singletons/array';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
||||||
|
import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discussions-swipe-manager';
|
||||||
import {
|
import {
|
||||||
AddonModForum,
|
AddonModForum,
|
||||||
AddonModForumAccessInformation,
|
AddonModForumAccessInformation,
|
||||||
|
@ -68,6 +72,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
||||||
forum: Partial<AddonModForumData> = {};
|
forum: Partial<AddonModForumData> = {};
|
||||||
accessInfo: AddonModForumAccessInformation = {};
|
accessInfo: AddonModForumAccessInformation = {};
|
||||||
discussion?: AddonModForumDiscussion;
|
discussion?: AddonModForumDiscussion;
|
||||||
|
discussions?: AddonModForumDiscussionDiscussionsSwipeManager;
|
||||||
startingPost?: Post;
|
startingPost?: Post;
|
||||||
posts!: Post[];
|
posts!: Post[];
|
||||||
discussionLoaded = false;
|
discussionLoaded = false;
|
||||||
|
@ -117,14 +122,16 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
||||||
constructor(
|
constructor(
|
||||||
@Optional() protected splitView: CoreSplitViewComponent,
|
@Optional() protected splitView: CoreSplitViewComponent,
|
||||||
protected elementRef: ElementRef,
|
protected elementRef: ElementRef,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get isMobile(): boolean {
|
get isMobile(): boolean {
|
||||||
return CoreScreen.isMobile;
|
return CoreScreen.isMobile;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
async ngOnInit(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const routeData = this.route.snapshot.data;
|
||||||
this.courseId = CoreNavigator.getRouteNumberParam('courseId');
|
this.courseId = CoreNavigator.getRouteNumberParam('courseId');
|
||||||
this.cmId = CoreNavigator.getRouteNumberParam('cmId');
|
this.cmId = CoreNavigator.getRouteNumberParam('cmId');
|
||||||
this.forumId = CoreNavigator.getRouteNumberParam('forumId');
|
this.forumId = CoreNavigator.getRouteNumberParam('forumId');
|
||||||
|
@ -136,6 +143,16 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
||||||
this.postId = CoreNavigator.getRouteNumberParam('postId');
|
this.postId = CoreNavigator.getRouteNumberParam('postId');
|
||||||
this.parent = CoreNavigator.getRouteNumberParam('parent');
|
this.parent = CoreNavigator.getRouteNumberParam('parent');
|
||||||
|
|
||||||
|
if (this.courseId && this.cmId && (routeData.swipeEnabled ?? true)) {
|
||||||
|
this.discussions = new AddonModForumDiscussionDiscussionsSwipeManager(
|
||||||
|
CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
AddonModForumDiscussionsSource,
|
||||||
|
[this.courseId, this.cmId, routeData.discussionsPathPrefix ?? ''],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.discussions.start();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
@ -311,6 +328,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.onlineObserver && this.onlineObserver.unsubscribe();
|
this.onlineObserver && this.onlineObserver.unsubscribe();
|
||||||
|
this.discussions && this.discussions.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -839,3 +857,17 @@ export type AddonModForumSharedPostFormData = Omit<AddonModForumPostFormData, 'i
|
||||||
id?: number; // ID when editing an online reply.
|
id?: number; // ID when editing an online reply.
|
||||||
syncId?: string; // Sync ID if some post has blocked synchronization.
|
syncId?: string; // Sync ID if some post has blocked synchronization.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within a collection of discussions.
|
||||||
|
*/
|
||||||
|
class AddonModForumDiscussionDiscussionsSwipeManager extends AddonModForumDiscussionsSwipeManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||||
|
return this.getSource().DISCUSSIONS_PATH_PREFIX + route.params.discussionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -12,10 +12,10 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
|
<core-swipe-navigation [manager]="discussions">
|
||||||
<ion-refresher slot="fixed" [disabled]="!groupsLoaded" (ionRefresh)="refreshGroups($event.target)">
|
<ion-refresher slot="fixed" [disabled]="!groupsLoaded" (ionRefresh)="refreshGroups($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]="groupsLoaded">
|
<core-loading [hideUntil]="groupsLoaded">
|
||||||
<form *ngIf="showForm" #newDiscFormEl>
|
<form *ngIf="showForm" #newDiscFormEl>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
|
@ -63,8 +63,8 @@
|
||||||
<ion-toggle [(ngModel)]="newDiscussion.pin" name="pin"></ion-toggle>
|
<ion-toggle [(ngModel)]="newDiscussion.pin" name="pin"></ion-toggle>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<core-attachments *ngIf="canCreateAttachments && forum && forum.maxattachments > 0" [files]="newDiscussion.files"
|
<core-attachments *ngIf="canCreateAttachments && forum && forum.maxattachments > 0" [files]="newDiscussion.files"
|
||||||
[maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid"
|
[maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component"
|
||||||
[allowOffline]="true" [courseId]="courseId">
|
[componentId]="forum.cmid" [allowOffline]="true" [courseId]="courseId">
|
||||||
</core-attachments>
|
</core-attachments>
|
||||||
</div>
|
</div>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
|
@ -84,4 +84,5 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</form>
|
</form>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
</core-swipe-navigation>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -40,6 +40,10 @@ import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CanLeave } from '@guards/can-leave';
|
import { CanLeave } from '@guards/can-leave';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreForms } from '@singletons/form';
|
import { CoreForms } from '@singletons/form';
|
||||||
|
import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discussions-swipe-manager';
|
||||||
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
|
import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
||||||
|
|
||||||
type NewDiscussionData = {
|
type NewDiscussionData = {
|
||||||
subject: string;
|
subject: string;
|
||||||
|
@ -88,6 +92,8 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
|
||||||
accessInfo: AddonModForumAccessInformation = {};
|
accessInfo: AddonModForumAccessInformation = {};
|
||||||
courseId!: number;
|
courseId!: number;
|
||||||
|
|
||||||
|
discussions?: AddonModForumNewDiscussionDiscussionsSwipeManager;
|
||||||
|
|
||||||
protected cmId!: number;
|
protected cmId!: number;
|
||||||
protected forumId!: number;
|
protected forumId!: number;
|
||||||
protected timeCreated!: number;
|
protected timeCreated!: number;
|
||||||
|
@ -97,17 +103,29 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
|
||||||
protected originalData?: Partial<NewDiscussionData>;
|
protected originalData?: Partial<NewDiscussionData>;
|
||||||
protected forceLeave = false;
|
protected forceLeave = false;
|
||||||
|
|
||||||
constructor(@Optional() protected splitView: CoreSplitViewComponent) {}
|
constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
async ngOnInit(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const routeData = this.route.snapshot.data;
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
this.forumId = CoreNavigator.getRequiredRouteNumberParam('forumId');
|
this.forumId = CoreNavigator.getRequiredRouteNumberParam('forumId');
|
||||||
this.timeCreated = CoreNavigator.getRequiredRouteNumberParam('timeCreated');
|
this.timeCreated = CoreNavigator.getRequiredRouteNumberParam('timeCreated');
|
||||||
|
|
||||||
|
if (this.timeCreated !== 0 && (routeData.swipeEnabled ?? true)) {
|
||||||
|
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
AddonModForumDiscussionsSource,
|
||||||
|
[this.courseId, this.cmId, routeData.discussionsPathPrefix ?? ''],
|
||||||
|
);
|
||||||
|
|
||||||
|
this.discussions = new AddonModForumNewDiscussionDiscussionsSwipeManager(source);
|
||||||
|
|
||||||
|
await this.discussions.start();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
@ -625,3 +643,17 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within a collection of discussions.
|
||||||
|
*/
|
||||||
|
class AddonModForumNewDiscussionDiscussionsSwipeManager extends AddonModForumDiscussionsSwipeManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||||
|
return `${this.getSource().DISCUSSIONS_PATH_PREFIX}new/${route.params.timeCreated}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,381 @@
|
||||||
|
// (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 { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
|
||||||
|
import {
|
||||||
|
AddonModGlossary,
|
||||||
|
AddonModGlossaryEntry,
|
||||||
|
AddonModGlossaryGetEntriesOptions,
|
||||||
|
AddonModGlossaryGetEntriesWSResponse,
|
||||||
|
AddonModGlossaryGlossary,
|
||||||
|
AddonModGlossaryProvider,
|
||||||
|
} from '../services/glossary';
|
||||||
|
import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '../services/glossary-offline';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a collection of glossary entries.
|
||||||
|
*/
|
||||||
|
export class AddonModGlossaryEntriesSource extends CoreItemsManagerSource<AddonModGlossaryEntryItem> {
|
||||||
|
|
||||||
|
static readonly NEW_ENTRY: AddonModGlossaryNewEntryForm = { newEntry: true };
|
||||||
|
|
||||||
|
readonly COURSE_ID: number;
|
||||||
|
readonly CM_ID: number;
|
||||||
|
readonly GLOSSARY_PATH_PREFIX: string;
|
||||||
|
|
||||||
|
isSearch = false;
|
||||||
|
hasSearched = false;
|
||||||
|
fetchMode?: AddonModGlossaryFetchMode;
|
||||||
|
viewMode?: string;
|
||||||
|
glossary?: AddonModGlossaryGlossary;
|
||||||
|
onlineEntries: AddonModGlossaryEntry[] = [];
|
||||||
|
offlineEntries: AddonModGlossaryOfflineEntry[] = [];
|
||||||
|
|
||||||
|
protected fetchFunction?: (options?: AddonModGlossaryGetEntriesOptions) => AddonModGlossaryGetEntriesWSResponse;
|
||||||
|
protected fetchInvalidate?: () => Promise<void>;
|
||||||
|
|
||||||
|
constructor(courseId: number, cmId: number, glossaryPathPrefix: string) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.COURSE_ID = courseId;
|
||||||
|
this.CM_ID = cmId;
|
||||||
|
this.GLOSSARY_PATH_PREFIX = glossaryPathPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to infer NewEntryForm objects.
|
||||||
|
*
|
||||||
|
* @param entry Item to check.
|
||||||
|
* @return Whether the item is a new entry form.
|
||||||
|
*/
|
||||||
|
isNewEntryForm(entry: AddonModGlossaryEntryItem): entry is AddonModGlossaryNewEntryForm {
|
||||||
|
return 'newEntry' in entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to infer entry objects.
|
||||||
|
*
|
||||||
|
* @param entry Item to check.
|
||||||
|
* @return Whether the item is an offline entry.
|
||||||
|
*/
|
||||||
|
isOnlineEntry(entry: AddonModGlossaryEntryItem): entry is AddonModGlossaryEntry {
|
||||||
|
return 'id' in entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to infer entry objects.
|
||||||
|
*
|
||||||
|
* @param entry Item to check.
|
||||||
|
* @return Whether the item is an offline entry.
|
||||||
|
*/
|
||||||
|
isOfflineEntry(entry: AddonModGlossaryEntryItem): entry is AddonModGlossaryOfflineEntry {
|
||||||
|
return !this.isNewEntryForm(entry) && !this.isOnlineEntry(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemPath(entry: AddonModGlossaryEntryItem): string {
|
||||||
|
if (this.isOnlineEntry(entry)) {
|
||||||
|
return `${this.GLOSSARY_PATH_PREFIX}entry/${entry.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isOfflineEntry(entry)) {
|
||||||
|
return `${this.GLOSSARY_PATH_PREFIX}edit/${entry.timecreated}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${this.GLOSSARY_PATH_PREFIX}edit/0`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemQueryParams(entry: AddonModGlossaryEntryItem): Params {
|
||||||
|
const params: Params = {
|
||||||
|
cmId: this.CM_ID,
|
||||||
|
courseId: this.COURSE_ID,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.isOfflineEntry(entry)) {
|
||||||
|
params.concept = entry.concept;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getPagesLoaded(): number {
|
||||||
|
if (this.items === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.ceil(this.onlineEntries.length / this.getPageLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start searching.
|
||||||
|
*/
|
||||||
|
startSearch(): void {
|
||||||
|
this.isSearch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop searching and restore unfiltered collection.
|
||||||
|
*
|
||||||
|
* @param cachedOnlineEntries Cached online entries.
|
||||||
|
* @param hasMoreOnlineEntries Whether there were more online entries.
|
||||||
|
*/
|
||||||
|
stopSearch(cachedOnlineEntries: AddonModGlossaryEntry[], hasMoreOnlineEntries: boolean): void {
|
||||||
|
if (!this.fetchMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isSearch = false;
|
||||||
|
this.hasSearched = false;
|
||||||
|
this.onlineEntries = cachedOnlineEntries;
|
||||||
|
this.hasMoreItems = hasMoreOnlineEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set search query.
|
||||||
|
*
|
||||||
|
* @param query Search query.
|
||||||
|
*/
|
||||||
|
search(query: string): void {
|
||||||
|
if (!this.glossary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchFunction = AddonModGlossary.getEntriesBySearch.bind(
|
||||||
|
AddonModGlossary.instance,
|
||||||
|
this.glossary.id,
|
||||||
|
query,
|
||||||
|
true,
|
||||||
|
'CONCEPT',
|
||||||
|
'ASC',
|
||||||
|
);
|
||||||
|
this.fetchInvalidate = AddonModGlossary.invalidateEntriesBySearch.bind(
|
||||||
|
AddonModGlossary.instance,
|
||||||
|
this.glossary.id,
|
||||||
|
query,
|
||||||
|
true,
|
||||||
|
'CONCEPT',
|
||||||
|
'ASC',
|
||||||
|
);
|
||||||
|
this.hasSearched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load glossary.
|
||||||
|
*/
|
||||||
|
async loadGlossary(): Promise<void> {
|
||||||
|
this.glossary = await AddonModGlossary.getGlossary(this.COURSE_ID, this.CM_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate glossary cache.
|
||||||
|
*/
|
||||||
|
async invalidateCache(): Promise<void> {
|
||||||
|
await Promise.all([
|
||||||
|
AddonModGlossary.invalidateCourseGlossaries(this.COURSE_ID),
|
||||||
|
this.fetchInvalidate && this.fetchInvalidate(),
|
||||||
|
this.glossary && AddonModGlossary.invalidateCategories(this.glossary.id),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change fetch mode.
|
||||||
|
*
|
||||||
|
* @param mode New mode.
|
||||||
|
*/
|
||||||
|
switchMode(mode: AddonModGlossaryFetchMode): void {
|
||||||
|
if (!this.glossary) {
|
||||||
|
throw new Error('Can\'t switch entries mode without a glossary!');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchMode = mode;
|
||||||
|
this.isSearch = false;
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case 'author_all':
|
||||||
|
// Browse by author.
|
||||||
|
this.viewMode = 'author';
|
||||||
|
this.fetchFunction = AddonModGlossary.getEntriesByAuthor.bind(
|
||||||
|
AddonModGlossary.instance,
|
||||||
|
this.glossary.id,
|
||||||
|
'ALL',
|
||||||
|
'LASTNAME',
|
||||||
|
'ASC',
|
||||||
|
);
|
||||||
|
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByAuthor.bind(
|
||||||
|
AddonModGlossary.instance,
|
||||||
|
this.glossary.id,
|
||||||
|
'ALL',
|
||||||
|
'LASTNAME',
|
||||||
|
'ASC',
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cat_all':
|
||||||
|
// Browse by category.
|
||||||
|
this.viewMode = 'cat';
|
||||||
|
this.fetchFunction = AddonModGlossary.getEntriesByCategory.bind(
|
||||||
|
AddonModGlossary.instance,
|
||||||
|
this.glossary.id,
|
||||||
|
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
|
||||||
|
);
|
||||||
|
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByCategory.bind(
|
||||||
|
AddonModGlossary.instance,
|
||||||
|
this.glossary.id,
|
||||||
|
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'newest_first':
|
||||||
|
// Newest first.
|
||||||
|
this.viewMode = 'date';
|
||||||
|
this.fetchFunction = AddonModGlossary.getEntriesByDate.bind(
|
||||||
|
AddonModGlossary.instance,
|
||||||
|
this.glossary.id,
|
||||||
|
'CREATION',
|
||||||
|
'DESC',
|
||||||
|
);
|
||||||
|
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByDate.bind(
|
||||||
|
AddonModGlossary.instance,
|
||||||
|
this.glossary.id,
|
||||||
|
'CREATION',
|
||||||
|
'DESC',
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'recently_updated':
|
||||||
|
// Recently updated.
|
||||||
|
this.viewMode = 'date';
|
||||||
|
this.fetchFunction = AddonModGlossary.getEntriesByDate.bind(
|
||||||
|
AddonModGlossary.instance,
|
||||||
|
this.glossary.id,
|
||||||
|
'UPDATE',
|
||||||
|
'DESC',
|
||||||
|
);
|
||||||
|
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByDate.bind(
|
||||||
|
AddonModGlossary.instance,
|
||||||
|
this.glossary.id,
|
||||||
|
'UPDATE',
|
||||||
|
'DESC',
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'letter_all':
|
||||||
|
default:
|
||||||
|
// Consider it is 'letter_all'.
|
||||||
|
this.viewMode = 'letter';
|
||||||
|
this.fetchMode = 'letter_all';
|
||||||
|
this.fetchFunction = AddonModGlossary.getEntriesByLetter.bind(
|
||||||
|
AddonModGlossary.instance,
|
||||||
|
this.glossary.id,
|
||||||
|
'ALL',
|
||||||
|
);
|
||||||
|
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByLetter.bind(
|
||||||
|
AddonModGlossary.instance,
|
||||||
|
this.glossary.id,
|
||||||
|
'ALL',
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async loadPageItems(page: number): Promise<{ items: AddonModGlossaryEntryItem[]; hasMoreItems: boolean }> {
|
||||||
|
const glossary = this.glossary;
|
||||||
|
const fetchFunction = this.fetchFunction;
|
||||||
|
|
||||||
|
if (!glossary || !fetchFunction) {
|
||||||
|
throw new Error('Can\'t load entries without glossary or fetch function');
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries: AddonModGlossaryEntryItem[] = [];
|
||||||
|
|
||||||
|
if (page === 0) {
|
||||||
|
const offlineEntries = await AddonModGlossaryOffline.getGlossaryNewEntries(glossary.id);
|
||||||
|
|
||||||
|
offlineEntries.sort((a, b) => a.concept.localeCompare(b.concept));
|
||||||
|
|
||||||
|
entries.push(AddonModGlossaryEntriesSource.NEW_ENTRY);
|
||||||
|
entries.push(...offlineEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
const from = page * this.getPageLength();
|
||||||
|
const pageEntries = await fetchFunction({ from, cmId: this.CM_ID });
|
||||||
|
|
||||||
|
entries.push(...pageEntries.entries);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: entries,
|
||||||
|
hasMoreItems: from + pageEntries.entries.length < pageEntries.count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getPageLength(): number {
|
||||||
|
return AddonModGlossaryProvider.LIMIT_ENTRIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected setItems(entries: AddonModGlossaryEntryItem[], hasMoreItems: boolean): void {
|
||||||
|
this.onlineEntries = [];
|
||||||
|
this.offlineEntries = [];
|
||||||
|
|
||||||
|
entries.forEach(entry => {
|
||||||
|
this.isOnlineEntry(entry) && this.onlineEntries.push(entry);
|
||||||
|
this.isOfflineEntry(entry) && this.offlineEntries.push(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
super.setItems(entries, hasMoreItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
reset(): void {
|
||||||
|
this.onlineEntries = [];
|
||||||
|
this.offlineEntries = [];
|
||||||
|
|
||||||
|
super.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of items that can be held by the entries manager.
|
||||||
|
*/
|
||||||
|
export type AddonModGlossaryEntryItem = AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | AddonModGlossaryNewEntryForm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type to select the new entry form.
|
||||||
|
*/
|
||||||
|
export type AddonModGlossaryNewEntryForm = { newEntry: true };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch mode to sort entries.
|
||||||
|
*/
|
||||||
|
export type AddonModGlossaryFetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'letter_all';
|
|
@ -0,0 +1,52 @@
|
||||||
|
// (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 { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
|
||||||
|
import { AddonModGlossaryEntriesSource, AddonModGlossaryEntryItem } from './glossary-entries-source';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within a collection of glossary entries.
|
||||||
|
*/
|
||||||
|
export abstract class AddonModGlossaryEntriesSwipeManager
|
||||||
|
extends CoreSwipeItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async navigateToNextItem(): Promise<void> {
|
||||||
|
let delta = -1;
|
||||||
|
const item = await this.getItemBy(-1);
|
||||||
|
|
||||||
|
if (item && this.getSource().isNewEntryForm(item)) {
|
||||||
|
delta--;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.navigateToItemBy(delta, 'back');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async navigateToPreviousItem(): Promise<void> {
|
||||||
|
let delta = 1;
|
||||||
|
const item = await this.getItemBy(1);
|
||||||
|
|
||||||
|
if (item && this.getSource().isNewEntryForm(item)) {
|
||||||
|
delta++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.navigateToItemBy(delta, 'forward');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -54,7 +54,7 @@
|
||||||
[component]="component" [componentId]="componentId" [courseId]="courseId" [hasDataToSync]="hasOffline || hasOfflineRatings">
|
[component]="component" [componentId]="componentId" [courseId]="courseId" [hasDataToSync]="hasOffline || hasOfflineRatings">
|
||||||
</core-course-module-info>
|
</core-course-module-info>
|
||||||
|
|
||||||
<ion-list *ngIf="!isSearch && entries.offlineEntries.length > 0">
|
<ion-list *ngIf="!isSearch && entries && entries.offlineEntries.length > 0">
|
||||||
<ion-item-divider>
|
<ion-item-divider>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ 'addon.mod_glossary.entriestobesynced' | translate }}</h2>
|
<h2>{{ 'addon.mod_glossary.entriestobesynced' | translate }}</h2>
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
||||||
<ion-list *ngIf="entries.onlineEntries.length > 0">
|
<ion-list *ngIf="entries && entries.onlineEntries.length > 0">
|
||||||
<ng-container *ngFor="let entry of entries.onlineEntries; let index = index">
|
<ng-container *ngFor="let entry of entries.onlineEntries; let index = index">
|
||||||
<ion-item-divider *ngIf="getDivider && showDivider(entry, entries.onlineEntries[index - 1])">
|
<ion-item-divider *ngIf="getDivider && showDivider(entry, entries.onlineEntries[index - 1])">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
@ -88,11 +88,11 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
||||||
<core-empty-box *ngIf="entries.empty && (!isSearch || hasSearched)" icon="fas-list"
|
<core-empty-box *ngIf="(!entries || entries.empty) && (!isSearch || hasSearched)" icon="fas-list"
|
||||||
[message]="'addon.mod_glossary.noentriesfound' | translate">
|
[message]="'addon.mod_glossary.noentriesfound' | translate">
|
||||||
</core-empty-box>
|
</core-empty-box>
|
||||||
|
|
||||||
<core-infinite-loading [enabled]="!entries.completed" [error]="loadMoreError" (action)="loadMoreEntries($event)">
|
<core-infinite-loading [enabled]="entries && !entries.completed" [error]="loadMoreError" (action)="loadMoreEntries($event)">
|
||||||
</core-infinite-loading>
|
</core-infinite-loading>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,9 @@
|
||||||
|
|
||||||
import { ContextLevel } from '@/core/constants';
|
import { ContextLevel } from '@/core/constants';
|
||||||
import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
|
import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
import { ActivatedRoute } 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 { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||||
|
@ -29,16 +30,19 @@ import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
import {
|
||||||
|
AddonModGlossaryEntriesSource,
|
||||||
|
AddonModGlossaryEntryItem,
|
||||||
|
AddonModGlossaryFetchMode,
|
||||||
|
} from '../../classes/glossary-entries-source';
|
||||||
import {
|
import {
|
||||||
AddonModGlossary,
|
AddonModGlossary,
|
||||||
AddonModGlossaryEntry,
|
AddonModGlossaryEntry,
|
||||||
AddonModGlossaryEntryWithCategory,
|
AddonModGlossaryEntryWithCategory,
|
||||||
AddonModGlossaryGetEntriesOptions,
|
|
||||||
AddonModGlossaryGetEntriesWSResponse,
|
|
||||||
AddonModGlossaryGlossary,
|
AddonModGlossaryGlossary,
|
||||||
AddonModGlossaryProvider,
|
AddonModGlossaryProvider,
|
||||||
} from '../../services/glossary';
|
} from '../../services/glossary';
|
||||||
import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
|
import { AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
|
||||||
import {
|
import {
|
||||||
AddonModGlossaryAutoSyncData,
|
AddonModGlossaryAutoSyncData,
|
||||||
AddonModGlossarySyncProvider,
|
AddonModGlossarySyncProvider,
|
||||||
|
@ -63,23 +67,17 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
component = AddonModGlossaryProvider.COMPONENT;
|
component = AddonModGlossaryProvider.COMPONENT;
|
||||||
moduleName = 'glossary';
|
moduleName = 'glossary';
|
||||||
|
|
||||||
isSearch = false;
|
|
||||||
hasSearched = false;
|
|
||||||
canAdd = false;
|
canAdd = false;
|
||||||
loadMoreError = false;
|
loadMoreError = false;
|
||||||
loadingMessage?: string;
|
loadingMessage: string;
|
||||||
entries: AddonModGlossaryEntriesManager;
|
entries!: AddonModGlossaryEntriesManager;
|
||||||
hasOfflineRatings = false;
|
hasOfflineRatings = false;
|
||||||
glossary?: AddonModGlossaryGlossary;
|
|
||||||
|
|
||||||
protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
|
protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
|
||||||
protected fetchFunction?: (options?: AddonModGlossaryGetEntriesOptions) => AddonModGlossaryGetEntriesWSResponse;
|
|
||||||
protected fetchInvalidate?: () => Promise<void>;
|
|
||||||
protected addEntryObserver?: CoreEventObserver;
|
protected addEntryObserver?: CoreEventObserver;
|
||||||
protected fetchMode?: AddonModGlossaryFetchMode;
|
|
||||||
protected viewMode?: string;
|
|
||||||
protected fetchedEntriesCanLoadMore = false;
|
protected fetchedEntriesCanLoadMore = false;
|
||||||
protected fetchedEntries: AddonModGlossaryEntry[] = [];
|
protected fetchedEntries: AddonModGlossaryEntry[] = [];
|
||||||
|
protected sourceUnsubscribe?: () => void;
|
||||||
protected ratingOfflineObserver?: CoreEventObserver;
|
protected ratingOfflineObserver?: CoreEventObserver;
|
||||||
protected ratingSyncObserver?: CoreEventObserver;
|
protected ratingSyncObserver?: CoreEventObserver;
|
||||||
|
|
||||||
|
@ -87,26 +85,47 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false;
|
showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected content?: IonContent,
|
protected content?: IonContent,
|
||||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
@Optional() protected courseContentsPage?: CoreCourseContentsPage,
|
||||||
) {
|
) {
|
||||||
super('AddonModGlossaryIndexComponent', content, courseContentsPage);
|
super('AddonModGlossaryIndexComponent', content, courseContentsPage);
|
||||||
|
|
||||||
this.entries = new AddonModGlossaryEntriesManager(
|
this.loadingMessage = Translate.instant('core.loading');
|
||||||
route.component,
|
}
|
||||||
this,
|
|
||||||
courseContentsPage ? `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` : '',
|
get glossary(): AddonModGlossaryGlossary | undefined {
|
||||||
);
|
return this.entries.getSource().glossary;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isSearch(): boolean {
|
||||||
|
return this.entries.getSource().isSearch;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasSearched(): boolean {
|
||||||
|
return this.entries.getSource().hasSearched;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
super.ngOnInit();
|
await super.ngOnInit();
|
||||||
|
|
||||||
this.loadingMessage = Translate.instant('core.loading');
|
// Initialize entries manager.
|
||||||
|
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
AddonModGlossaryEntriesSource,
|
||||||
|
[this.courseId, this.module.id, this.courseContentsPage ? `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` : ''],
|
||||||
|
);
|
||||||
|
|
||||||
|
this.entries = new AddonModGlossaryEntriesManager(
|
||||||
|
source,
|
||||||
|
this.route.component,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sourceUnsubscribe = source.addListener({
|
||||||
|
onItemsUpdated: items => this.hasOffline = !!items.find(item => source.isOfflineEntry(item)),
|
||||||
|
});
|
||||||
|
|
||||||
// When an entry is added, we reload the data.
|
// When an entry is added, we reload the data.
|
||||||
this.addEntryObserver = CoreEvents.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, (data) => {
|
this.addEntryObserver = CoreEvents.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, (data) => {
|
||||||
|
@ -143,11 +162,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.entries.start(this.splitView);
|
await this.entries.start(this.splitView);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await AddonModGlossary.logView(this.glossary.id, this.viewMode!, this.glossary.name);
|
|
||||||
|
|
||||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore errors.
|
// Ignore errors.
|
||||||
|
@ -159,14 +176,18 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
*/
|
*/
|
||||||
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.module.id);
|
await this.entries.getSource().loadGlossary();
|
||||||
|
|
||||||
|
if (!this.glossary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.description = this.glossary.intro || this.description;
|
this.description = this.glossary.intro || this.description;
|
||||||
this.canAdd = !!this.glossary.canaddentry || false;
|
this.canAdd = !!this.glossary.canaddentry || false;
|
||||||
|
|
||||||
this.dataRetrieved.emit(this.glossary);
|
this.dataRetrieved.emit(this.glossary);
|
||||||
|
|
||||||
if (!this.fetchMode) {
|
if (!this.entries.getSource().fetchMode) {
|
||||||
this.switchMode('letter_all');
|
this.switchMode('letter_all');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +198,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
|
|
||||||
const [hasOfflineRatings] = await Promise.all([
|
const [hasOfflineRatings] = await Promise.all([
|
||||||
CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule),
|
CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule),
|
||||||
this.fetchEntries(),
|
refresh ? this.entries.reload() : this.entries.load(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.hasOfflineRatings = hasOfflineRatings;
|
this.hasOfflineRatings = hasOfflineRatings;
|
||||||
|
@ -186,59 +207,11 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience function to fetch entries.
|
|
||||||
*
|
|
||||||
* @param append True if fetched entries are appended to exsiting ones.
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
protected async fetchEntries(append: boolean = false): Promise<void> {
|
|
||||||
if (!this.fetchFunction) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadMoreError = false;
|
|
||||||
const from = append ? this.entries.onlineEntries.length : 0;
|
|
||||||
|
|
||||||
const result = await this.fetchFunction({
|
|
||||||
from: from,
|
|
||||||
cmId: this.module.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasMoreEntries = from + result.entries.length < result.count;
|
|
||||||
|
|
||||||
if (append) {
|
|
||||||
this.entries.setItems(this.entries.items.concat(result.entries), hasMoreEntries);
|
|
||||||
} else {
|
|
||||||
this.entries.setOnlineEntries(result.entries, hasMoreEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now get the ofline entries.
|
|
||||||
// Check if there are responses stored in offline.
|
|
||||||
const offlineEntries = await AddonModGlossaryOffline.getGlossaryNewEntries(this.glossary!.id);
|
|
||||||
|
|
||||||
offlineEntries.sort((a, b) => a.concept.localeCompare(b.concept));
|
|
||||||
this.hasOffline = !!offlineEntries.length;
|
|
||||||
this.entries.setOfflineEntries(offlineEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected async invalidateContent(): Promise<void> {
|
protected async invalidateContent(): Promise<void> {
|
||||||
const promises: Promise<void>[] = [];
|
await this.entries.getSource().invalidateCache();
|
||||||
|
|
||||||
if (this.fetchInvalidate) {
|
|
||||||
promises.push(this.fetchInvalidate());
|
|
||||||
}
|
|
||||||
|
|
||||||
promises.push(AddonModGlossary.invalidateCourseGlossaries(this.courseId));
|
|
||||||
|
|
||||||
if (this.glossary) {
|
|
||||||
promises.push(AddonModGlossary.invalidateCategories(this.glossary.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -277,111 +250,52 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* @param mode New mode.
|
* @param mode New mode.
|
||||||
*/
|
*/
|
||||||
protected switchMode(mode: AddonModGlossaryFetchMode): void {
|
protected switchMode(mode: AddonModGlossaryFetchMode): void {
|
||||||
this.fetchMode = mode;
|
this.entries.getSource().switchMode(mode);
|
||||||
this.isSearch = false;
|
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'author_all':
|
case 'author_all':
|
||||||
// Browse by author.
|
// Browse by author.
|
||||||
this.viewMode = 'author';
|
|
||||||
this.fetchFunction = AddonModGlossary.getEntriesByAuthor.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'ALL',
|
|
||||||
'LASTNAME',
|
|
||||||
'ASC',
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByAuthor.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'ALL',
|
|
||||||
'LASTNAME',
|
|
||||||
'ASC',
|
|
||||||
);
|
|
||||||
this.getDivider = (entry) => entry.userfullname;
|
this.getDivider = (entry) => entry.userfullname;
|
||||||
this.showDivider = (entry, previous) => !previous || entry.userid != previous.userid;
|
this.showDivider = (entry, previous) => !previous || entry.userid != previous.userid;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'cat_all':
|
case 'cat_all': {
|
||||||
// Browse by category.
|
// Browse by category.
|
||||||
this.viewMode = 'cat';
|
const getDivider = (entry: AddonModGlossaryEntryWithCategory) => entry.categoryname || '';
|
||||||
this.fetchFunction = AddonModGlossary.getEntriesByCategory.bind(
|
|
||||||
AddonModGlossary.instance,
|
this.getDivider = getDivider;
|
||||||
this.glossary!.id,
|
this.showDivider = (entry, previous) => !previous || getDivider(entry) != getDivider(previous);
|
||||||
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByCategory.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
|
|
||||||
);
|
|
||||||
this.getDivider = (entry: AddonModGlossaryEntryWithCategory) => entry.categoryname || '';
|
|
||||||
this.showDivider = (entry, previous) => !previous || this.getDivider!(entry) != this.getDivider!(previous);
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'newest_first':
|
case 'newest_first':
|
||||||
// Newest first.
|
// Newest first.
|
||||||
this.viewMode = 'date';
|
|
||||||
this.fetchFunction = AddonModGlossary.getEntriesByDate.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'CREATION',
|
|
||||||
'DESC',
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByDate.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'CREATION',
|
|
||||||
'DESC',
|
|
||||||
);
|
|
||||||
this.getDivider = undefined;
|
this.getDivider = undefined;
|
||||||
this.showDivider = () => false;
|
this.showDivider = () => false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'recently_updated':
|
case 'recently_updated':
|
||||||
// Recently updated.
|
// Recently updated.
|
||||||
this.viewMode = 'date';
|
|
||||||
this.fetchFunction = AddonModGlossary.getEntriesByDate.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'UPDATE',
|
|
||||||
'DESC',
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByDate.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'UPDATE',
|
|
||||||
'DESC',
|
|
||||||
);
|
|
||||||
this.getDivider = undefined;
|
this.getDivider = undefined;
|
||||||
this.showDivider = () => false;
|
this.showDivider = () => false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'letter_all':
|
case 'letter_all':
|
||||||
default:
|
default: {
|
||||||
// Consider it is 'letter_all'.
|
// Consider it is 'letter_all'.
|
||||||
this.viewMode = 'letter';
|
const getDivider = (entry) => {
|
||||||
this.fetchMode = 'letter_all';
|
|
||||||
this.fetchFunction = AddonModGlossary.getEntriesByLetter.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'ALL',
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByLetter.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
'ALL',
|
|
||||||
);
|
|
||||||
this.getDivider = (entry) => {
|
|
||||||
// Try to get the first letter without HTML tags.
|
// Try to get the first letter without HTML tags.
|
||||||
const noTags = CoreTextUtils.cleanTags(entry.concept);
|
const noTags = CoreTextUtils.cleanTags(entry.concept);
|
||||||
|
|
||||||
return (noTags || entry.concept).substr(0, 1).toUpperCase();
|
return (noTags || entry.concept).substr(0, 1).toUpperCase();
|
||||||
};
|
};
|
||||||
this.showDivider = (entry, previous) => !previous || this.getDivider!(entry) != this.getDivider!(previous);
|
|
||||||
|
this.getDivider = getDivider;
|
||||||
|
this.showDivider = (entry, previous) => !previous || getDivider(entry) != getDivider(previous);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience function to load more entries.
|
* Convenience function to load more entries.
|
||||||
|
@ -391,7 +305,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
*/
|
*/
|
||||||
async loadMoreEntries(infiniteComplete?: () => void): Promise<void> {
|
async loadMoreEntries(infiniteComplete?: () => void): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.fetchEntries(true);
|
this.loadMoreError = false;
|
||||||
|
|
||||||
|
await this.entries.load();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.loadMoreError = true;
|
this.loadMoreError = true;
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
|
||||||
|
@ -406,21 +322,34 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* @param event Event.
|
* @param event Event.
|
||||||
*/
|
*/
|
||||||
async openModePicker(event: MouseEvent): Promise<void> {
|
async openModePicker(event: MouseEvent): Promise<void> {
|
||||||
const mode = await CoreDomUtils.openPopover<AddonModGlossaryFetchMode>({
|
if (!this.glossary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousMode = this.entries.getSource().fetchMode;
|
||||||
|
const newMode = await CoreDomUtils.openPopover<AddonModGlossaryFetchMode>({
|
||||||
component: AddonModGlossaryModePickerPopoverComponent,
|
component: AddonModGlossaryModePickerPopoverComponent,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
browseModes: this.glossary!.browsemodes,
|
browseModes: this.glossary.browsemodes,
|
||||||
selectedMode: this.isSearch ? '' : this.fetchMode,
|
selectedMode: this.isSearch ? '' : previousMode,
|
||||||
},
|
},
|
||||||
event,
|
event,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mode) {
|
if (!newMode) {
|
||||||
if (mode !== this.fetchMode) {
|
return;
|
||||||
this.changeFetchMode(mode);
|
|
||||||
} else if (this.isSearch) {
|
|
||||||
this.toggleSearch();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newMode !== previousMode) {
|
||||||
|
this.changeFetchMode(newMode);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isSearch) {
|
||||||
|
this.toggleSearch();
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,20 +358,22 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
*/
|
*/
|
||||||
toggleSearch(): void {
|
toggleSearch(): void {
|
||||||
if (this.isSearch) {
|
if (this.isSearch) {
|
||||||
this.isSearch = false;
|
const fetchMode = this.entries.getSource().fetchMode;
|
||||||
this.hasSearched = false;
|
|
||||||
this.entries.setOnlineEntries(this.fetchedEntries, this.fetchedEntriesCanLoadMore);
|
fetchMode && this.switchMode(fetchMode);
|
||||||
this.switchMode(this.fetchMode!);
|
this.entries.getSource().stopSearch(this.fetchedEntries, this.fetchedEntriesCanLoadMore);
|
||||||
} else {
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Search for entries. The fetch function will be set when searching.
|
// Search for entries. The fetch function will be set when searching.
|
||||||
|
this.fetchedEntries = this.entries.getSource().onlineEntries;
|
||||||
|
this.fetchedEntriesCanLoadMore = !this.entries.completed;
|
||||||
this.getDivider = undefined;
|
this.getDivider = undefined;
|
||||||
this.showDivider = () => false;
|
this.showDivider = () => false;
|
||||||
this.isSearch = true;
|
|
||||||
|
|
||||||
this.fetchedEntries = this.entries.onlineEntries;
|
this.entries.reset();
|
||||||
this.fetchedEntriesCanLoadMore = !this.entries.completed;
|
this.entries.getSource().startSearch();
|
||||||
this.entries.setItems([], false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -451,7 +382,6 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* @param mode Mode.
|
* @param mode Mode.
|
||||||
*/
|
*/
|
||||||
changeFetchMode(mode: AddonModGlossaryFetchMode): void {
|
changeFetchMode(mode: AddonModGlossaryFetchMode): void {
|
||||||
this.isSearch = false;
|
|
||||||
this.loadingMessage = Translate.instant('core.loading');
|
this.loadingMessage = Translate.instant('core.loading');
|
||||||
this.content?.scrollToTop();
|
this.content?.scrollToTop();
|
||||||
this.switchMode(mode);
|
this.switchMode(mode);
|
||||||
|
@ -463,7 +393,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* Opens new entry editor.
|
* Opens new entry editor.
|
||||||
*/
|
*/
|
||||||
openNewEntry(): void {
|
openNewEntry(): void {
|
||||||
this.entries.select({ newEntry: true });
|
this.entries.select(AddonModGlossaryEntriesSource.NEW_ENTRY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -473,24 +403,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
*/
|
*/
|
||||||
search(query: string): void {
|
search(query: string): void {
|
||||||
this.loadingMessage = Translate.instant('core.searching');
|
this.loadingMessage = Translate.instant('core.searching');
|
||||||
this.fetchFunction = AddonModGlossary.getEntriesBySearch.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
query,
|
|
||||||
true,
|
|
||||||
'CONCEPT',
|
|
||||||
'ASC',
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = AddonModGlossary.invalidateEntriesBySearch.bind(
|
|
||||||
AddonModGlossary.instance,
|
|
||||||
this.glossary!.id,
|
|
||||||
query,
|
|
||||||
true,
|
|
||||||
'CONCEPT',
|
|
||||||
'ASC',
|
|
||||||
);
|
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.hasSearched = true;
|
|
||||||
|
this.entries.getSource().search(query);
|
||||||
this.loadContent();
|
this.loadContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -503,154 +418,44 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
this.addEntryObserver?.off();
|
this.addEntryObserver?.off();
|
||||||
this.ratingOfflineObserver?.off();
|
this.ratingOfflineObserver?.off();
|
||||||
this.ratingSyncObserver?.off();
|
this.ratingSyncObserver?.off();
|
||||||
|
this.sourceUnsubscribe?.call(null);
|
||||||
|
this.entries.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Type to select the new entry form.
|
|
||||||
*/
|
|
||||||
type NewEntryForm = { newEntry: true };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of items that can be held by the entries manager.
|
|
||||||
*/
|
|
||||||
type EntryItem = AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | NewEntryForm;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entries manager.
|
* Entries manager.
|
||||||
*/
|
*/
|
||||||
class AddonModGlossaryEntriesManager extends CorePageItemsListManager<EntryItem> {
|
class AddonModGlossaryEntriesManager extends CoreListItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> {
|
||||||
|
|
||||||
onlineEntries: AddonModGlossaryEntry[] = [];
|
get offlineEntries(): AddonModGlossaryOfflineEntry[] {
|
||||||
offlineEntries: AddonModGlossaryOfflineEntry[] = [];
|
return this.getSource().offlineEntries;
|
||||||
|
|
||||||
protected glossaryPathPrefix: string;
|
|
||||||
protected component: AddonModGlossaryIndexComponent;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
pageComponent: unknown,
|
|
||||||
component: AddonModGlossaryIndexComponent,
|
|
||||||
glossaryPathPrefix: string,
|
|
||||||
) {
|
|
||||||
super(pageComponent);
|
|
||||||
|
|
||||||
this.component = component;
|
|
||||||
this.glossaryPathPrefix = glossaryPathPrefix;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
get onlineEntries(): AddonModGlossaryEntry[] {
|
||||||
* Type guard to infer NewEntryForm objects.
|
return this.getSource().onlineEntries;
|
||||||
*
|
|
||||||
* @param entry Item to check.
|
|
||||||
* @return Whether the item is a new entry form.
|
|
||||||
*/
|
|
||||||
isNewEntryForm(entry: EntryItem): entry is NewEntryForm {
|
|
||||||
return 'newEntry' in entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to infer entry objects.
|
|
||||||
*
|
|
||||||
* @param entry Item to check.
|
|
||||||
* @return Whether the item is an offline entry.
|
|
||||||
*/
|
|
||||||
isOfflineEntry(entry: EntryItem): entry is AddonModGlossaryOfflineEntry {
|
|
||||||
return !this.isNewEntryForm(entry) && !this.isOnlineEntry(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to infer entry objects.
|
|
||||||
*
|
|
||||||
* @param entry Item to check.
|
|
||||||
* @return Whether the item is an offline entry.
|
|
||||||
*/
|
|
||||||
isOnlineEntry(entry: EntryItem): entry is AddonModGlossaryEntry {
|
|
||||||
return 'id' in entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update online entries items.
|
|
||||||
*
|
|
||||||
* @param onlineEntries Online entries.
|
|
||||||
*/
|
|
||||||
setOnlineEntries(onlineEntries: AddonModGlossaryEntry[], hasMoreItems: boolean = false): void {
|
|
||||||
this.setItems((<EntryItem[]> this.offlineEntries).concat(onlineEntries), hasMoreItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update offline entries items.
|
|
||||||
*
|
|
||||||
* @param offlineEntries Offline entries.
|
|
||||||
*/
|
|
||||||
setOfflineEntries(offlineEntries: AddonModGlossaryOfflineEntry[]): void {
|
|
||||||
this.setItems((<EntryItem[]> offlineEntries).concat(this.onlineEntries), this.hasMoreItems);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
setItems(entries: EntryItem[], hasMoreItems: boolean = false): void {
|
protected getDefaultItem(): AddonModGlossaryEntryItem | null {
|
||||||
super.setItems(entries, hasMoreItems);
|
return this.getSource().onlineEntries[0] || null;
|
||||||
|
|
||||||
this.onlineEntries = [];
|
|
||||||
this.offlineEntries = [];
|
|
||||||
this.items.forEach(entry => {
|
|
||||||
if (this.isOfflineEntry(entry)) {
|
|
||||||
this.offlineEntries.push(entry);
|
|
||||||
} else if (this.isOnlineEntry(entry)) {
|
|
||||||
this.onlineEntries.push(entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
resetItems(): void {
|
protected async logActivity(): Promise<void> {
|
||||||
super.resetItems();
|
const glossary = this.getSource().glossary;
|
||||||
this.onlineEntries = [];
|
const viewMode = this.getSource().viewMode;
|
||||||
this.offlineEntries = [];
|
|
||||||
|
if (!glossary || !viewMode) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
await AddonModGlossary.logView(glossary.id, viewMode, glossary.name);
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
protected getItemPath(entry: EntryItem): string {
|
|
||||||
if (this.isOnlineEntry(entry)) {
|
|
||||||
return `${this.glossaryPathPrefix}entry/${entry.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isOfflineEntry(entry)) {
|
|
||||||
return `${this.glossaryPathPrefix}edit/${entry.timecreated}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${this.glossaryPathPrefix}edit/0`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
getItemQueryParams(entry: EntryItem): Params {
|
|
||||||
const params: Params = {
|
|
||||||
cmId: this.component.module.id,
|
|
||||||
courseId: this.component.courseId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.isOfflineEntry(entry)) {
|
|
||||||
params.concept = entry.concept;
|
|
||||||
}
|
|
||||||
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
protected getDefaultItem(): EntryItem | null {
|
|
||||||
return this.onlineEntries[0] || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AddonModGlossaryFetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'letter_all';
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { PopoverController } from '@singletons';
|
import { PopoverController } from '@singletons';
|
||||||
import { AddonModGlossaryFetchMode } from '../index';
|
import { AddonModGlossaryFetchMode } from '../../classes/glossary-entries-source';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to display the mode picker.
|
* Component to display the mode picker.
|
||||||
|
|
|
@ -51,10 +51,12 @@ const mainMenuRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
||||||
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
||||||
|
data: { swipeEnabled: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
||||||
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
||||||
|
data: { swipeEnabled: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: AddonModGlossaryModuleHandlerService.PAGE_NAME,
|
path: AddonModGlossaryModuleHandlerService.PAGE_NAME,
|
||||||
|
@ -65,10 +67,12 @@ const mainMenuRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
||||||
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
||||||
|
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
||||||
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
||||||
|
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
() => CoreScreen.isMobile,
|
() => CoreScreen.isMobile,
|
||||||
|
@ -80,10 +84,12 @@ const courseContentsRoutes: Routes = conditionalRoutes(
|
||||||
{
|
{
|
||||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
||||||
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
||||||
|
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
||||||
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
||||||
|
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
() => CoreScreen.isTablet,
|
() => CoreScreen.isTablet,
|
||||||
|
|
|
@ -12,19 +12,21 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
|
<core-swipe-navigation [manager]="entries">
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-loading [hideUntil]="loaded">
|
||||||
<form #editFormEl *ngIf="glossary">
|
<form #editFormEl *ngIf="glossary">
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label position="stacked">{{ 'addon.mod_glossary.concept' | translate }}</ion-label>
|
<ion-label position="stacked">{{ 'addon.mod_glossary.concept' | translate }}</ion-label>
|
||||||
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="entry.concept" name="concept">
|
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="entry.concept"
|
||||||
|
name="concept">
|
||||||
</ion-input>
|
</ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label position="stacked">{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
|
<ion-label position="stacked">{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
|
||||||
<core-rich-text-editor [control]="definitionControl" (contentChanged)="onDefinitionChange($event)"
|
<core-rich-text-editor [control]="definitionControl" (contentChanged)="onDefinitionChange($event)"
|
||||||
[placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit" [component]="component"
|
[placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit" [component]="component"
|
||||||
[componentId]="cmId" [autoSave]="true" contextLevel="module" [contextInstanceId]="cmId" elementId="definition_editor"
|
[componentId]="cmId" [autoSave]="true" contextLevel="module" [contextInstanceId]="cmId"
|
||||||
[draftExtraParams]="editorExtraParams">
|
elementId="definition_editor" [draftExtraParams]="editorExtraParams">
|
||||||
</core-rich-text-editor>
|
</core-rich-text-editor>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item *ngIf="categories.length > 0">
|
<ion-item *ngIf="categories.length > 0">
|
||||||
|
@ -80,4 +82,5 @@
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</form>
|
</form>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
</core-swipe-navigation>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -12,9 +12,11 @@
|
||||||
// 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 { Component, OnInit, ViewChild, ElementRef, Optional } from '@angular/core';
|
import { Component, OnInit, ViewChild, ElementRef, Optional, OnDestroy } from '@angular/core';
|
||||||
import { FormControl } from '@angular/forms';
|
import { FormControl } from '@angular/forms';
|
||||||
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||||
import { CanLeave } from '@guards/can-leave';
|
import { CanLeave } from '@guards/can-leave';
|
||||||
|
@ -26,6 +28,8 @@ import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreForms } from '@singletons/form';
|
import { CoreForms } from '@singletons/form';
|
||||||
|
import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source';
|
||||||
|
import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager';
|
||||||
import {
|
import {
|
||||||
AddonModGlossary,
|
AddonModGlossary,
|
||||||
AddonModGlossaryCategory,
|
AddonModGlossaryCategory,
|
||||||
|
@ -45,7 +49,7 @@ import { AddonModGlossaryOffline } from '../../services/glossary-offline';
|
||||||
selector: 'page-addon-mod-glossary-edit',
|
selector: 'page-addon-mod-glossary-edit',
|
||||||
templateUrl: 'edit.html',
|
templateUrl: 'edit.html',
|
||||||
})
|
})
|
||||||
export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
|
|
||||||
@ViewChild('editFormEl') formElement?: ElementRef;
|
@ViewChild('editFormEl') formElement?: ElementRef;
|
||||||
|
|
||||||
|
@ -64,6 +68,8 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
timecreated: 0,
|
timecreated: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
entries?: AddonModGlossaryEditEntriesSwipeManager;
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
categories: <string[]> [],
|
categories: <string[]> [],
|
||||||
aliases: '',
|
aliases: '',
|
||||||
|
@ -80,18 +86,30 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
protected originalData?: AddonModGlossaryNewEntryWithFiles;
|
protected originalData?: AddonModGlossaryNewEntryWithFiles;
|
||||||
protected saved = false;
|
protected saved = false;
|
||||||
|
|
||||||
constructor(@Optional() protected splitView: CoreSplitViewComponent) {}
|
constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
async ngOnInit(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const routeData = this.route.snapshot.data;
|
||||||
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
this.timecreated = CoreNavigator.getRequiredRouteNumberParam('timecreated');
|
this.timecreated = CoreNavigator.getRequiredRouteNumberParam('timecreated');
|
||||||
this.concept = CoreNavigator.getRouteParam<string>('concept') || '';
|
this.concept = CoreNavigator.getRouteParam<string>('concept') || '';
|
||||||
this.editorExtraParams.timecreated = this.timecreated;
|
this.editorExtraParams.timecreated = this.timecreated;
|
||||||
|
|
||||||
|
if (this.timecreated !== 0 && (routeData.swipeEnabled ?? true)) {
|
||||||
|
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
AddonModGlossaryEntriesSource,
|
||||||
|
[this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
|
||||||
|
);
|
||||||
|
|
||||||
|
this.entries = new AddonModGlossaryEditEntriesSwipeManager(source);
|
||||||
|
|
||||||
|
await this.entries.start();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
@ -103,6 +121,13 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.entries?.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch required data.
|
* Fetch required data.
|
||||||
*
|
*
|
||||||
|
@ -134,7 +159,11 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async loadOfflineData(): Promise<void> {
|
protected async loadOfflineData(): Promise<void> {
|
||||||
const entry = await AddonModGlossaryOffline.getNewEntry(this.glossary!.id, this.concept, this.timecreated);
|
if (!this.glossary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = await AddonModGlossaryOffline.getNewEntry(this.glossary.id, this.concept, this.timecreated);
|
||||||
|
|
||||||
this.entry.concept = entry.concept || '';
|
this.entry.concept = entry.concept || '';
|
||||||
this.entry.definition = entry.definition || '';
|
this.entry.definition = entry.definition || '';
|
||||||
|
@ -159,7 +188,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
|
|
||||||
// Treat offline attachments if any.
|
// Treat offline attachments if any.
|
||||||
if (entry.attachments?.offline) {
|
if (entry.attachments?.offline) {
|
||||||
this.attachments = await AddonModGlossaryHelper.getStoredFiles(this.glossary!.id, entry.concept, entry.timecreated);
|
this.attachments = await AddonModGlossaryHelper.getStoredFiles(this.glossary.id, entry.concept, entry.timecreated);
|
||||||
|
|
||||||
this.originalData.files = this.attachments.slice();
|
this.originalData.files = this.attachments.slice();
|
||||||
}
|
}
|
||||||
|
@ -236,6 +265,10 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
definition = CoreTextUtils.formatHtmlLines(definition);
|
definition = CoreTextUtils.formatHtmlLines(definition);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!this.glossary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Upload attachments first if any.
|
// Upload attachments first if any.
|
||||||
const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated);
|
const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated);
|
||||||
|
|
||||||
|
@ -244,7 +277,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
categories: this.options.categories.join(','),
|
categories: this.options.categories.join(','),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.glossary!.usedynalink) {
|
if (this.glossary.usedynalink) {
|
||||||
options.usedynalink = this.options.usedynalink ? 1 : 0;
|
options.usedynalink = this.options.usedynalink ? 1 : 0;
|
||||||
if (this.options.usedynalink) {
|
if (this.options.usedynalink) {
|
||||||
options.casesensitive = this.options.casesensitive ? 1 : 0;
|
options.casesensitive = this.options.casesensitive ? 1 : 0;
|
||||||
|
@ -253,9 +286,9 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveOffline) {
|
if (saveOffline) {
|
||||||
if (this.entry && !this.glossary!.allowduplicatedentries) {
|
if (this.entry && !this.glossary.allowduplicatedentries) {
|
||||||
// Check if the entry is duplicated in online or offline mode.
|
// Check if the entry is duplicated in online or offline mode.
|
||||||
const isUsed = await AddonModGlossary.isConceptUsed(this.glossary!.id, this.entry.concept, {
|
const isUsed = await AddonModGlossary.isConceptUsed(this.glossary.id, this.entry.concept, {
|
||||||
timeCreated: this.entry.timecreated,
|
timeCreated: this.entry.timecreated,
|
||||||
cmId: this.cmId,
|
cmId: this.cmId,
|
||||||
});
|
});
|
||||||
|
@ -268,7 +301,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
|
|
||||||
// Save entry in offline.
|
// Save entry in offline.
|
||||||
await AddonModGlossaryOffline.addNewEntry(
|
await AddonModGlossaryOffline.addNewEntry(
|
||||||
this.glossary!.id,
|
this.glossary.id,
|
||||||
this.entry.concept,
|
this.entry.concept,
|
||||||
definition,
|
definition,
|
||||||
this.courseId,
|
this.courseId,
|
||||||
|
@ -283,7 +316,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
// Try to send it to server.
|
// Try to send it to server.
|
||||||
// Don't allow offline if there are attachments since they were uploaded fine.
|
// Don't allow offline if there are attachments since they were uploaded fine.
|
||||||
await AddonModGlossary.addEntry(
|
await AddonModGlossary.addEntry(
|
||||||
this.glossary!.id,
|
this.glossary.id,
|
||||||
this.entry.concept,
|
this.entry.concept,
|
||||||
definition,
|
definition,
|
||||||
this.courseId,
|
this.courseId,
|
||||||
|
@ -293,7 +326,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
timeCreated: timecreated,
|
timeCreated: timecreated,
|
||||||
discardEntry: this.entry,
|
discardEntry: this.entry,
|
||||||
allowOffline: !this.attachments.length,
|
allowOffline: !this.attachments.length,
|
||||||
checkDuplicates: !this.glossary!.allowduplicatedentries,
|
checkDuplicates: !this.glossary.allowduplicatedentries,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -303,12 +336,12 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
|
|
||||||
if (entryId) {
|
if (entryId) {
|
||||||
// Data sent to server, delete stored files (if any).
|
// Data sent to server, delete stored files (if any).
|
||||||
AddonModGlossaryHelper.deleteStoredFiles(this.glossary!.id, this.entry.concept, timecreated);
|
AddonModGlossaryHelper.deleteStoredFiles(this.glossary.id, this.entry.concept, timecreated);
|
||||||
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' });
|
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' });
|
||||||
}
|
}
|
||||||
|
|
||||||
CoreEvents.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, {
|
CoreEvents.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, {
|
||||||
glossaryId: this.glossary!.id,
|
glossaryId: this.glossary.id,
|
||||||
entryId: entryId,
|
entryId: entryId,
|
||||||
}, CoreSites.getCurrentSiteId());
|
}, CoreSites.getCurrentSiteId());
|
||||||
|
|
||||||
|
@ -342,7 +375,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
protected async uploadAttachments(
|
protected async uploadAttachments(
|
||||||
timecreated: number,
|
timecreated: number,
|
||||||
): Promise<{saveOffline: boolean; attachmentsResult?: number | CoreFileUploaderStoreFilesResult}> {
|
): Promise<{saveOffline: boolean; attachmentsResult?: number | CoreFileUploaderStoreFilesResult}> {
|
||||||
if (!this.attachments.length) {
|
if (!this.attachments.length || !this.glossary) {
|
||||||
return {
|
return {
|
||||||
saveOffline: false,
|
saveOffline: false,
|
||||||
};
|
};
|
||||||
|
@ -352,7 +385,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles(
|
const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles(
|
||||||
this.attachments,
|
this.attachments,
|
||||||
AddonModGlossaryProvider.COMPONENT,
|
AddonModGlossaryProvider.COMPONENT,
|
||||||
this.glossary!.id,
|
this.glossary.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -362,7 +395,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
} catch {
|
} catch {
|
||||||
// Cannot upload them in online, save them in offline.
|
// Cannot upload them in online, save them in offline.
|
||||||
const attachmentsResult = await AddonModGlossaryHelper.storeFiles(
|
const attachmentsResult = await AddonModGlossaryHelper.storeFiles(
|
||||||
this.glossary!.id,
|
this.glossary.id,
|
||||||
this.entry.concept,
|
this.entry.concept,
|
||||||
timecreated,
|
timecreated,
|
||||||
this.attachments,
|
this.attachments,
|
||||||
|
@ -387,3 +420,17 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within a collection of glossary entries.
|
||||||
|
*/
|
||||||
|
class AddonModGlossaryEditEntriesSwipeManager extends AddonModGlossaryEntriesSwipeManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||||
|
return `${this.getSource().GLOSSARY_PATH_PREFIX}edit/${route.params.timecreated}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
|
<core-swipe-navigation [manager]="entries">
|
||||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
|
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($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>
|
||||||
|
@ -41,8 +42,8 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item class="ion-text-wrap">
|
<ion-item class="ion-text-wrap">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition" contextLevel="module"
|
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition"
|
||||||
[contextInstanceId]="componentId" [courseId]="courseId">
|
contextLevel="module" [contextInstanceId]="componentId" [courseId]="courseId">
|
||||||
</core-format-text>
|
</core-format-text>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
@ -70,8 +71,8 @@
|
||||||
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" [userId]="entry.userid" (onUpdate)="ratingUpdated()">
|
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" [userId]="entry.userid" (onUpdate)="ratingUpdated()">
|
||||||
</core-rating-rate>
|
</core-rating-rate>
|
||||||
<core-rating-aggregate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
|
<core-rating-aggregate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
|
||||||
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [courseId]="glossary.course" [aggregateMethod]="glossary.assessed"
|
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [courseId]="glossary.course"
|
||||||
[scaleId]="glossary.scale">
|
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale">
|
||||||
</core-rating-aggregate>
|
</core-rating-aggregate>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -81,4 +82,5 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
</core-swipe-navigation>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -12,7 +12,9 @@
|
||||||
// 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 { Component, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
|
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
|
||||||
import { CoreComments } from '@features/comments/services/comments';
|
import { CoreComments } from '@features/comments/services/comments';
|
||||||
import { CoreRatingInfo } from '@features/rating/services/rating';
|
import { CoreRatingInfo } from '@features/rating/services/rating';
|
||||||
|
@ -21,6 +23,8 @@ import { IonRefresher } from '@ionic/angular';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source';
|
||||||
|
import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager';
|
||||||
import {
|
import {
|
||||||
AddonModGlossary,
|
AddonModGlossary,
|
||||||
AddonModGlossaryEntry,
|
AddonModGlossaryEntry,
|
||||||
|
@ -35,13 +39,14 @@ import {
|
||||||
selector: 'page-addon-mod-glossary-entry',
|
selector: 'page-addon-mod-glossary-entry',
|
||||||
templateUrl: 'entry.html',
|
templateUrl: 'entry.html',
|
||||||
})
|
})
|
||||||
export class AddonModGlossaryEntryPage implements OnInit {
|
export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@ViewChild(CoreCommentsCommentsComponent) comments?: CoreCommentsCommentsComponent;
|
@ViewChild(CoreCommentsCommentsComponent) comments?: CoreCommentsCommentsComponent;
|
||||||
|
|
||||||
component = AddonModGlossaryProvider.COMPONENT;
|
component = AddonModGlossaryProvider.COMPONENT;
|
||||||
componentId?: number;
|
componentId?: number;
|
||||||
entry?: AddonModGlossaryEntry;
|
entry?: AddonModGlossaryEntry;
|
||||||
|
entries?: AddonModGlossaryEntryEntriesSwipeManager;
|
||||||
glossary?: AddonModGlossaryGlossary;
|
glossary?: AddonModGlossaryGlossary;
|
||||||
loaded = false;
|
loaded = false;
|
||||||
showAuthor = false;
|
showAuthor = false;
|
||||||
|
@ -53,15 +58,30 @@ export class AddonModGlossaryEntryPage implements OnInit {
|
||||||
|
|
||||||
protected entryId!: number;
|
protected entryId!: number;
|
||||||
|
|
||||||
|
constructor(protected route: ActivatedRoute) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const routeData = this.route.snapshot.data;
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
this.entryId = CoreNavigator.getRequiredRouteNumberParam('entryId');
|
this.entryId = CoreNavigator.getRequiredRouteNumberParam('entryId');
|
||||||
this.tagsEnabled = CoreTag.areTagsAvailableInSite();
|
this.tagsEnabled = CoreTag.areTagsAvailableInSite();
|
||||||
this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
|
this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
|
||||||
|
|
||||||
|
if (routeData.swipeEnabled ?? true) {
|
||||||
|
const cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
|
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
AddonModGlossaryEntriesSource,
|
||||||
|
[this.courseId, cmId, routeData.glossaryPathPrefix ?? ''],
|
||||||
|
);
|
||||||
|
|
||||||
|
this.entries = new AddonModGlossaryEntryEntriesSwipeManager(source);
|
||||||
|
|
||||||
|
await this.entries.start();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
@ -73,16 +93,23 @@ export class AddonModGlossaryEntryPage implements OnInit {
|
||||||
try {
|
try {
|
||||||
await this.fetchEntry();
|
await this.fetchEntry();
|
||||||
|
|
||||||
if (!this.glossary) {
|
if (!this.glossary || !this.componentId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.entryId, this.componentId!, this.glossary.name));
|
await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.entryId, this.componentId, this.glossary.name));
|
||||||
} finally {
|
} finally {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.entries?.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh the data.
|
* Refresh the data.
|
||||||
*
|
*
|
||||||
|
@ -152,3 +179,17 @@ export class AddonModGlossaryEntryPage implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within a collection of glossary entries.
|
||||||
|
*/
|
||||||
|
class AddonModGlossaryEntryEntriesSwipeManager extends AddonModGlossaryEntriesSwipeManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||||
|
return `${this.getSource().GLOSSARY_PATH_PREFIX}entry/${route.params.entryId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -12,12 +12,14 @@
|
||||||
// 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 { Params } from '@angular/router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates listener.
|
* Updates listener.
|
||||||
*/
|
*/
|
||||||
export interface CoreItemsListSourceListener<Item> {
|
export interface CoreItemsListSourceListener<Item> {
|
||||||
onItemsUpdated(items: Item[], hasMoreItems: boolean): void;
|
onItemsUpdated?(items: Item[], hasMoreItems: boolean): void;
|
||||||
onReset(): void;
|
onReset?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,9 +37,10 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
return args.map(argument => String(argument)).join('-');
|
return args.map(argument => String(argument)).join('-');
|
||||||
}
|
}
|
||||||
|
|
||||||
private items: Item[] | null = null;
|
protected items: Item[] | null = null;
|
||||||
private hasMoreItems = true;
|
protected hasMoreItems = true;
|
||||||
private listeners: CoreItemsListSourceListener<Item>[] = [];
|
protected listeners: CoreItemsListSourceListener<Item>[] = [];
|
||||||
|
protected dirty = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether any page has been loaded.
|
* Check whether any page has been loaded.
|
||||||
|
@ -57,6 +60,17 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
return !this.hasMoreItems;
|
return !this.hasMoreItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether the source as dirty.
|
||||||
|
*
|
||||||
|
* When a source is dirty, the next load request will reload items from the beginning.
|
||||||
|
*
|
||||||
|
* @param dirty Whether source should be marked as dirty or not.
|
||||||
|
*/
|
||||||
|
setDirty(dirty: boolean): void {
|
||||||
|
this.dirty = dirty;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get collection items.
|
* Get collection items.
|
||||||
*
|
*
|
||||||
|
@ -76,7 +90,12 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.ceil(this.items.length / this.getPageLength());
|
const pageLength = this.getPageLength();
|
||||||
|
if (pageLength === null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.ceil(this.items.length / pageLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,8 +104,9 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.items = null;
|
this.items = null;
|
||||||
this.hasMoreItems = true;
|
this.hasMoreItems = true;
|
||||||
|
this.dirty = false;
|
||||||
|
|
||||||
this.listeners.forEach(listener => listener.onReset());
|
this.listeners.forEach(listener => listener.onReset?.call(listener));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,36 +142,67 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
async reload(): Promise<void> {
|
async reload(): Promise<void> {
|
||||||
const { items, hasMoreItems } = await this.loadPageItems(0);
|
const { items, hasMoreItems } = await this.loadPageItems(0);
|
||||||
|
|
||||||
this.setItems(items, hasMoreItems);
|
this.dirty = false;
|
||||||
|
this.setItems(items, hasMoreItems ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load items for the next page, if any.
|
* Load more items, if any.
|
||||||
*/
|
*/
|
||||||
async loadNextPage(): Promise<void> {
|
async load(): Promise<void> {
|
||||||
|
if (this.dirty) {
|
||||||
|
const { items, hasMoreItems } = await this.loadPageItems(0);
|
||||||
|
|
||||||
|
this.dirty = false;
|
||||||
|
this.setItems(items, hasMoreItems ?? false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.hasMoreItems) {
|
if (!this.hasMoreItems) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { items, hasMoreItems } = await this.loadPageItems(this.getPagesLoaded());
|
const { items, hasMoreItems } = await this.loadPageItems(this.getPagesLoaded());
|
||||||
|
|
||||||
this.setItems((this.items ?? []).concat(items), hasMoreItems);
|
this.setItems((this.items ?? []).concat(items), hasMoreItems ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
abstract getItemPath(item: Item): string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load page items.
|
* Load page items.
|
||||||
*
|
*
|
||||||
* @param page Page number (starting at 0).
|
* @param page Page number (starting at 0).
|
||||||
* @return Page items data.
|
* @return Page items data.
|
||||||
*/
|
*/
|
||||||
protected abstract loadPageItems(page: number): Promise<{ items: Item[]; hasMoreItems: boolean }>;
|
protected abstract loadPageItems(page: number): Promise<{ items: Item[]; hasMoreItems?: boolean }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the length of each page in the collection.
|
* Get the length of each page in the collection.
|
||||||
*
|
*
|
||||||
* @return Page length.
|
* @return Page length; null for collections that don't support pagination.
|
||||||
*/
|
*/
|
||||||
protected abstract getPageLength(): number;
|
protected getPageLength(): number | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the collection items.
|
* Update the collection items.
|
||||||
|
@ -163,7 +214,7 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
||||||
this.items = items;
|
this.items = items;
|
||||||
this.hasMoreItems = hasMoreItems;
|
this.hasMoreItems = hasMoreItems;
|
||||||
|
|
||||||
this.listeners.forEach(listener => listener.onItemsUpdated(items, hasMoreItems));
|
this.listeners.forEach(listener => listener.onItemsUpdated?.call(listener, items, hasMoreItems));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ type SourceConstructor<T extends CoreItemsManagerSource = CoreItemsManagerSource
|
||||||
getSourceId(...args: unknown[]): string;
|
getSourceId(...args: unknown[]): string;
|
||||||
new (...args: unknown[]): T;
|
new (...args: unknown[]): T;
|
||||||
};
|
};
|
||||||
|
type SourceConstuctorInstance<T> = T extends { new(...args: unknown[]): infer P } ? P : never;
|
||||||
type InstanceTracking = { instance: CoreItemsManagerSource; references: unknown[] };
|
type InstanceTracking = { instance: CoreItemsManagerSource; references: unknown[] };
|
||||||
type Instances = Record<string, InstanceTracking>;
|
type Instances = Record<string, InstanceTracking>;
|
||||||
|
|
||||||
|
@ -36,14 +37,14 @@ export class CoreItemsManagerSourcesTracker {
|
||||||
* @param constructorArguments Arguments to create a new instance, used to find out if an instance already exists.
|
* @param constructorArguments Arguments to create a new instance, used to find out if an instance already exists.
|
||||||
* @returns Source.
|
* @returns Source.
|
||||||
*/
|
*/
|
||||||
static getOrCreateSource<T extends CoreItemsManagerSource>(
|
static getOrCreateSource<T extends CoreItemsManagerSource, C extends SourceConstructor<T>>(
|
||||||
constructor: SourceConstructor<T>,
|
constructor: C,
|
||||||
constructorArguments: ConstructorParameters<SourceConstructor<T>>,
|
constructorArguments: ConstructorParameters<C>,
|
||||||
): T {
|
): SourceConstuctorInstance<C> {
|
||||||
const id = constructor.getSourceId(...constructorArguments);
|
const id = constructor.getSourceId(...constructorArguments);
|
||||||
const constructorInstances = this.getConstructorInstances(constructor);
|
const constructorInstances = this.getConstructorInstances(constructor);
|
||||||
|
|
||||||
return constructorInstances[id]?.instance as T
|
return constructorInstances[id]?.instance as SourceConstuctorInstance<C>
|
||||||
?? this.createInstance(id, constructor, constructorArguments);
|
?? this.createInstance(id, constructor, constructorArguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +58,7 @@ export class CoreItemsManagerSourcesTracker {
|
||||||
const constructorInstances = this.getConstructorInstances(source.constructor as SourceConstructor);
|
const constructorInstances = this.getConstructorInstances(source.constructor as SourceConstructor);
|
||||||
const instanceId = this.instanceIds.get(source);
|
const instanceId = this.instanceIds.get(source);
|
||||||
|
|
||||||
if (!instanceId) {
|
if (instanceId === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +83,7 @@ export class CoreItemsManagerSourcesTracker {
|
||||||
const instanceId = this.instanceIds.get(source);
|
const instanceId = this.instanceIds.get(source);
|
||||||
const index = constructorInstances?.[instanceId ?? '']?.references.indexOf(reference) ?? -1;
|
const index = constructorInstances?.[instanceId ?? '']?.references.indexOf(reference) ?? -1;
|
||||||
|
|
||||||
if (!constructorInstances || !instanceId || index === -1) {
|
if (!constructorInstances || instanceId === undefined || index === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
// 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, ActivatedRouteSnapshot, Params } from '@angular/router';
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
import { CoreItemsManagerSource } from './items-manager-source';
|
import { CoreItemsManagerSource } from './items-manager-source';
|
||||||
|
@ -21,13 +21,13 @@ import { CoreItemsManagerSourcesTracker } from './items-manager-sources-tracker'
|
||||||
/**
|
/**
|
||||||
* Helper to manage a collection of items in a page.
|
* Helper to manage a collection of items in a page.
|
||||||
*/
|
*/
|
||||||
export abstract class CoreItemsManager<Item = unknown> {
|
export abstract class CoreItemsManager<Item = unknown, Source extends CoreItemsManagerSource<Item> = CoreItemsManagerSource<Item>> {
|
||||||
|
|
||||||
protected source?: { instance: CoreItemsManagerSource<Item>; unsubscribe: () => void };
|
protected source?: { instance: Source; unsubscribe: () => void };
|
||||||
protected itemsMap: Record<string, Item> | null = null;
|
protected itemsMap: Record<string, Item> | null = null;
|
||||||
protected selectedItem: Item | null = null;
|
protected selectedItem: Item | null = null;
|
||||||
|
|
||||||
constructor(source: CoreItemsManagerSource<Item>) {
|
constructor(source: Source) {
|
||||||
this.setSource(source);
|
this.setSource(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ export abstract class CoreItemsManager<Item = unknown> {
|
||||||
*
|
*
|
||||||
* @returns Source.
|
* @returns Source.
|
||||||
*/
|
*/
|
||||||
getSource(): CoreItemsManagerSource<Item> {
|
getSource(): Source {
|
||||||
if (!this.source) {
|
if (!this.source) {
|
||||||
throw new Error('Source is missing from items manager');
|
throw new Error('Source is missing from items manager');
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ export abstract class CoreItemsManager<Item = unknown> {
|
||||||
*
|
*
|
||||||
* @param newSource New source.
|
* @param newSource New source.
|
||||||
*/
|
*/
|
||||||
setSource(newSource: CoreItemsManagerSource<Item> | null): void {
|
setSource(newSource: Source | null): void {
|
||||||
if (this.source) {
|
if (this.source) {
|
||||||
CoreItemsManagerSourcesTracker.removeReference(this.source.instance, this);
|
CoreItemsManagerSourcesTracker.removeReference(this.source.instance, this);
|
||||||
|
|
||||||
|
@ -92,31 +92,26 @@ export abstract class CoreItemsManager<Item = unknown> {
|
||||||
*/
|
*/
|
||||||
protected abstract getCurrentPageRoute(): ActivatedRoute | null;
|
protected abstract getCurrentPageRoute(): ActivatedRoute | null;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
* Get the path of the selected item given the current route.
|
||||||
*
|
*
|
||||||
* @param route Page route.
|
* @param route Page route.
|
||||||
* @return Path of the selected item in the given route.
|
* @return Path of the selected item in the given route.
|
||||||
*/
|
*/
|
||||||
protected abstract getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null;
|
protected abstract getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the query parameters to use when navigating to an item page.
|
* Get the path of the selected item.
|
||||||
*
|
*
|
||||||
* @param item Item.
|
* @param route Page route, if any.
|
||||||
* @return Query parameters to use when navigating to the item page.
|
* @return Path of the selected item.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
protected getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null {
|
||||||
protected getItemQueryParams(item: Item): Params {
|
if (!route) {
|
||||||
return {};
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getSelectedItemPathFromRoute(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -152,7 +147,7 @@ export abstract class CoreItemsManager<Item = unknown> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this item is already selected, do nothing.
|
// If this item is already selected, do nothing.
|
||||||
const itemPath = this.getItemPath(item);
|
const itemPath = this.getSource().getItemPath(item);
|
||||||
const selectedItemPath = this.getSelectedItemPath(route.snapshot);
|
const selectedItemPath = this.getSelectedItemPath(route.snapshot);
|
||||||
|
|
||||||
if (selectedItemPath === itemPath) {
|
if (selectedItemPath === itemPath) {
|
||||||
|
@ -160,7 +155,7 @@ export abstract class CoreItemsManager<Item = unknown> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to item.
|
// Navigate to item.
|
||||||
const params = this.getItemQueryParams(item);
|
const params = this.getSource().getItemQueryParams(item);
|
||||||
const pathPrefix = selectedItemPath ? selectedItemPath.split('/').fill('../').join('') : '';
|
const pathPrefix = selectedItemPath ? selectedItemPath.split('/').fill('../').join('') : '';
|
||||||
|
|
||||||
await CoreNavigator.navigate(pathPrefix + itemPath, { params, ...options });
|
await CoreNavigator.navigate(pathPrefix + itemPath, { params, ...options });
|
||||||
|
@ -173,7 +168,7 @@ export abstract class CoreItemsManager<Item = unknown> {
|
||||||
*/
|
*/
|
||||||
protected onSourceItemsUpdated(items: Item[]): void {
|
protected onSourceItemsUpdated(items: Item[]): void {
|
||||||
this.itemsMap = items.reduce((map, item) => {
|
this.itemsMap = items.reduce((map, item) => {
|
||||||
map[this.getItemPath(item)] = item;
|
map[this.getSource().getItemPath(item)] = item;
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
|
@ -26,13 +26,16 @@ import { CoreItemsManagerSource } from './items-manager-source';
|
||||||
/**
|
/**
|
||||||
* Helper class to manage the state and routing of a list of items in a page.
|
* Helper class to manage the state and routing of a list of items in a page.
|
||||||
*/
|
*/
|
||||||
export abstract class CoreListItemsManager<Item = unknown> extends CoreItemsManager<Item> {
|
export class CoreListItemsManager<
|
||||||
|
Item = unknown,
|
||||||
|
Source extends CoreItemsManagerSource<Item> = CoreItemsManagerSource<Item>
|
||||||
|
> extends CoreItemsManager<Item, Source> {
|
||||||
|
|
||||||
protected pageRouteLocator?: unknown | ActivatedRoute;
|
protected pageRouteLocator?: unknown | ActivatedRoute;
|
||||||
protected splitView?: CoreSplitViewComponent;
|
protected splitView?: CoreSplitViewComponent;
|
||||||
protected splitViewOutletSubscription?: Subscription;
|
protected splitViewOutletSubscription?: Subscription;
|
||||||
|
|
||||||
constructor(source: CoreItemsManagerSource<Item>, pageRouteLocator: unknown | ActivatedRoute) {
|
constructor(source: Source, pageRouteLocator: unknown | ActivatedRoute) {
|
||||||
super(source);
|
super(source);
|
||||||
|
|
||||||
this.pageRouteLocator = pageRouteLocator;
|
this.pageRouteLocator = pageRouteLocator;
|
||||||
|
@ -67,15 +70,6 @@ export abstract class CoreListItemsManager<Item = unknown> extends CoreItemsMana
|
||||||
// Calculate current selected item.
|
// Calculate current selected item.
|
||||||
this.updateSelectedItem();
|
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.
|
// Log activity.
|
||||||
await CoreUtils.ignoreErrors(this.logActivity());
|
await CoreUtils.ignoreErrors(this.logActivity());
|
||||||
}
|
}
|
||||||
|
@ -146,10 +140,10 @@ export abstract class CoreListItemsManager<Item = unknown> extends CoreItemsMana
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load items for the next page, if any.
|
* Load more items, if any.
|
||||||
*/
|
*/
|
||||||
async loadNextPage(): Promise<void> {
|
async load(): Promise<void> {
|
||||||
await this.getSource().loadNextPage();
|
await this.getSource().load();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -172,6 +166,25 @@ export abstract class CoreListItemsManager<Item = unknown> extends CoreItemsMana
|
||||||
return !!this.splitView && !this.splitView?.isNested;
|
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.
|
* Get the item that should be selected by default.
|
||||||
*/
|
*/
|
||||||
|
@ -193,10 +206,12 @@ export abstract class CoreListItemsManager<Item = unknown> extends CoreItemsMana
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null {
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||||
const segments: UrlSegment[] = [];
|
const segments: UrlSegment[] = [];
|
||||||
|
|
||||||
while ((route = route?.firstChild)) {
|
while (route.firstChild) {
|
||||||
|
route = route.firstChild;
|
||||||
|
|
||||||
segments.push(...route.url);
|
segments.push(...route.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,16 +12,21 @@
|
||||||
// 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 } from '@angular/router';
|
import { ActivatedRoute, ActivatedRouteSnapshot, UrlSegment } from '@angular/router';
|
||||||
|
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
import { CoreItemsManager } from './items-manager';
|
import { CoreItemsManager } from './items-manager';
|
||||||
|
import { CoreItemsManagerSource } from './items-manager-source';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper class to manage the state and routing of a swipeable page.
|
* Helper class to manage the state and routing of a swipeable page.
|
||||||
*/
|
*/
|
||||||
export abstract class CoreSwipeItemsManager<Item = unknown> extends CoreItemsManager<Item> {
|
export class CoreSwipeItemsManager<
|
||||||
|
Item = unknown,
|
||||||
|
Source extends CoreItemsManagerSource<Item> = CoreItemsManagerSource<Item>
|
||||||
|
>
|
||||||
|
extends CoreItemsManager<Item, Source> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process page started operations.
|
* Process page started operations.
|
||||||
|
@ -51,6 +56,25 @@ export abstract class CoreSwipeItemsManager<Item = unknown> extends CoreItemsMan
|
||||||
return CoreNavigator.getCurrentRoute();
|
return CoreNavigator.getCurrentRoute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||||
|
const segments: UrlSegment[] = [];
|
||||||
|
|
||||||
|
while (route) {
|
||||||
|
segments.push(...route.url);
|
||||||
|
|
||||||
|
if (!route.firstChild) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
route = route.firstChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments.map(segment => segment.path).join('/').replace(/\/+/, '/').trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to an item by an offset.
|
* Navigate to an item by an offset.
|
||||||
*
|
*
|
||||||
|
@ -86,7 +110,7 @@ export abstract class CoreSwipeItemsManager<Item = unknown> extends CoreItemsMan
|
||||||
const item = items?.[index + delta] ?? null;
|
const item = items?.[index + delta] ?? null;
|
||||||
|
|
||||||
if (!item && !this.getSource().isCompleted()) {
|
if (!item && !this.getSource().isCompleted()) {
|
||||||
await this.getSource().loadNextPage();
|
await this.getSource().load();
|
||||||
|
|
||||||
return this.getItemBy(delta);
|
return this.getItemBy(delta);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<ion-slides [options]="{ allowTouchMove: !!manager }" (swipeleft)="swipeLeft()" (swiperight)="swipeRight()">
|
<ion-slides [options]="{ allowTouchMove: enabled }" (swipeleft)="swipeLeft()" (swiperight)="swipeRight()">
|
||||||
<ion-slide>
|
<ion-slide>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</ion-slide>
|
</ion-slide>
|
||||||
|
|
|
@ -5,3 +5,15 @@ ion-slides {
|
||||||
ion-slide {
|
ion-slide {
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
|
||||||
|
core-loading .core-loading-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-refresher.refresher-native {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
|
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
|
||||||
|
import { CoreScreen } from '@services/screen';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'core-swipe-navigation',
|
selector: 'core-swipe-navigation',
|
||||||
|
@ -24,10 +25,18 @@ export class CoreSwipeNavigationComponent {
|
||||||
|
|
||||||
@Input() manager?: CoreSwipeItemsManager;
|
@Input() manager?: CoreSwipeItemsManager;
|
||||||
|
|
||||||
|
get enabled(): boolean {
|
||||||
|
return CoreScreen.isMobile && !!this.manager;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Swipe to previous item.
|
* Swipe to previous item.
|
||||||
*/
|
*/
|
||||||
swipeLeft(): void {
|
swipeLeft(): void {
|
||||||
|
if (!this.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.manager?.navigateToPreviousItem();
|
this.manager?.navigateToPreviousItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +44,10 @@ export class CoreSwipeNavigationComponent {
|
||||||
* Swipe to next item.
|
* Swipe to next item.
|
||||||
*/
|
*/
|
||||||
swipeRight(): void {
|
swipeRight(): void {
|
||||||
|
if (!this.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.manager?.navigateToNextItem();
|
this.manager?.navigateToNextItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
// 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 { Params } from '@angular/router';
|
||||||
import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
|
import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
|
||||||
|
|
||||||
import { CoreUser, CoreUserData, CoreUserParticipant, CoreUserProvider } from '../services/user';
|
import { CoreUser, CoreUserData, CoreUserParticipant, CoreUserProvider } from '../services/user';
|
||||||
|
@ -40,6 +41,20 @@ export class CoreUserParticipantsSource extends CoreItemsManagerSource<CoreUserP
|
||||||
this.SEARCH_QUERY = searchQuery;
|
this.SEARCH_QUERY = searchQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemPath(user: CoreUserParticipant | CoreUserData): string {
|
||||||
|
return user.id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemQueryParams(): Params {
|
||||||
|
return { search: this.SEARCH_QUERY };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
import { Params } from '@angular/router';
|
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
|
||||||
import { CoreApp } from '@services/app';
|
import { CoreApp } from '@services/app';
|
||||||
|
@ -50,7 +49,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
this.participants = new CoreUserParticipantsManager(
|
this.participants = new CoreUserParticipantsManager(
|
||||||
CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]),
|
CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]),
|
||||||
this,
|
CoreUserParticipantsPage,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
@ -186,7 +185,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
||||||
private async fetchParticipants(reload: boolean): Promise<void> {
|
private async fetchParticipants(reload: boolean): Promise<void> {
|
||||||
reload
|
reload
|
||||||
? await this.participants.reload()
|
? await this.participants.reload()
|
||||||
: await this.participants.loadNextPage();
|
: await this.participants.load();
|
||||||
|
|
||||||
this.fetchMoreParticipantsFailed = false;
|
this.fetchMoreParticipantsFailed = false;
|
||||||
}
|
}
|
||||||
|
@ -196,35 +195,13 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
||||||
/**
|
/**
|
||||||
* Helper to manage the list of participants.
|
* Helper to manage the list of participants.
|
||||||
*/
|
*/
|
||||||
class CoreUserParticipantsManager extends CoreListItemsManager<CoreUserParticipant | CoreUserData> {
|
class CoreUserParticipantsManager extends CoreListItemsManager<CoreUserParticipant | CoreUserData, CoreUserParticipantsSource> {
|
||||||
|
|
||||||
page: CoreUserParticipantsPage;
|
|
||||||
|
|
||||||
constructor(source: CoreUserParticipantsSource, page: CoreUserParticipantsPage) {
|
|
||||||
super(source, CoreUserParticipantsPage);
|
|
||||||
|
|
||||||
this.page = page;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
protected getItemPath(participant: CoreUserParticipant | CoreUserData): string {
|
|
||||||
return participant.id.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
protected getItemQueryParams(): Params {
|
|
||||||
return { search: this.page.searchQuery };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected async logActivity(): Promise<void> {
|
protected async logActivity(): Promise<void> {
|
||||||
await CoreUser.logParticipantsView(this.page.courseId);
|
await CoreUser.logParticipantsView(this.getSource().COURSE_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
// 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, ActivatedRouteSnapshot, Params } from '@angular/router';
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
@ -21,7 +21,7 @@ import { CoreSite } from '@classes/site';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreUser, CoreUserBasicData, CoreUserProfile, CoreUserProvider } from '@features/user/services/user';
|
import { CoreUser, CoreUserProfile, CoreUserProvider } from '@features/user/services/user';
|
||||||
import { CoreUserHelper } from '@features/user/services/user-helper';
|
import { CoreUserHelper } from '@features/user/services/user-helper';
|
||||||
import { CoreUserDelegate, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
|
import { CoreUserDelegate, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
@ -30,7 +30,6 @@ import { CoreCourses } from '@features/courses/services/courses';
|
||||||
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
|
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
|
||||||
import { CoreUserParticipantsSource } from '@features/user/classes/participants-source';
|
import { CoreUserParticipantsSource } from '@features/user/classes/participants-source';
|
||||||
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||||
import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'page-core-user-profile',
|
selector: 'page-core-user-profile',
|
||||||
|
@ -57,7 +56,6 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
|
||||||
communicationHandlers: CoreUserProfileHandlerData[] = [];
|
communicationHandlers: CoreUserProfileHandlerData[] = [];
|
||||||
|
|
||||||
users?: CoreUserSwipeItemsManager;
|
users?: CoreUserSwipeItemsManager;
|
||||||
usersQueryParams: Params = {};
|
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute) {
|
constructor(private route: ActivatedRoute) {
|
||||||
this.obsProfileRefreshed = CoreEvents.on(CoreUserProvider.PROFILE_REFRESHED, (data) => {
|
this.obsProfileRefreshed = CoreEvents.on(CoreUserProvider.PROFILE_REFRESHED, (data) => {
|
||||||
|
@ -93,9 +91,8 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
|
||||||
if (this.courseId && this.route.snapshot.data.swipeManagerSource === 'participants') {
|
if (this.courseId && this.route.snapshot.data.swipeManagerSource === 'participants') {
|
||||||
const search = CoreNavigator.getRouteParam('search');
|
const search = CoreNavigator.getRouteParam('search');
|
||||||
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId, search]);
|
const source = CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId, search]);
|
||||||
this.users = new CoreUserSwipeItemsManager(source, this);
|
this.users = new CoreUserSwipeItemsManager(source);
|
||||||
|
|
||||||
this.usersQueryParams.search = search;
|
|
||||||
this.users.start();
|
this.users.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,38 +224,12 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* Helper to manage swiping within a collection of users.
|
* Helper to manage swiping within a collection of users.
|
||||||
*/
|
*/
|
||||||
class CoreUserSwipeItemsManager extends CoreSwipeItemsManager<CoreUserBasicData> {
|
class CoreUserSwipeItemsManager extends CoreSwipeItemsManager {
|
||||||
|
|
||||||
page: CoreUserProfilePage;
|
|
||||||
|
|
||||||
constructor(source: CoreItemsManagerSource<CoreUserBasicData>, page: CoreUserProfilePage) {
|
|
||||||
super(source);
|
|
||||||
|
|
||||||
this.page = page;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected getItemPath(item: CoreUserBasicData): string {
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||||
return String(item.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
protected getItemQueryParams(): Params {
|
|
||||||
return this.page.usersQueryParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
protected getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null {
|
|
||||||
if (!route) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return route.params.userId;
|
return route.params.userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,6 @@
|
||||||
right: calc(50% - 12px - var(--core-avatar-size) / 2) !important;
|
right: calc(50% - 12px - var(--core-avatar-size) / 2) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
core-loading .core-loading-content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -397,8 +397,14 @@ export class CoreUserDelegateService extends CoreDelegate<CoreUserProfileHandler
|
||||||
*/
|
*/
|
||||||
protected clearHandlerCache(courseId?: number, userId?: number): void {
|
protected clearHandlerCache(courseId?: number, userId?: number): void {
|
||||||
if (courseId && userId) {
|
if (courseId && userId) {
|
||||||
|
const cacheKey = this.getCacheKey(courseId, userId);
|
||||||
|
|
||||||
Object.keys(this.enabledHandlers).forEach((name) => {
|
Object.keys(this.enabledHandlers).forEach((name) => {
|
||||||
delete this.enabledForUserCache[name][this.getCacheKey(courseId, userId)];
|
const cache = this.enabledForUserCache[name];
|
||||||
|
|
||||||
|
if (cache) {
|
||||||
|
delete cache[cacheKey];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.enabledForUserCache = {};
|
this.enabledForUserCache = {};
|
||||||
|
|
|
@ -290,7 +290,7 @@ export class CoreNavigatorService {
|
||||||
* @param routeOptions Optional routeOptions to get the params or route value from. If missing, it will autodetect.
|
* @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.
|
* @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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let value: any;
|
let value: any;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue