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,239 +10,241 @@
|
|||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!badgeLoaded" (ionRefresh)="refreshBadges($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="badgeLoaded">
|
||||
<ion-item-group *ngIf="badge">
|
||||
<ion-item class="ion-text-wrap ion-text-center">
|
||||
<ion-label>
|
||||
<img *ngIf="badge.badgeurl" class="large-avatar" [src]="badge.badgeurl" core-external-content [alt]="badge.name" />
|
||||
<ion-badge color="danger" *ngIf="badge.dateexpire && currentTime >= badge.dateexpire">
|
||||
{{ 'addon.badges.expired' | translate }}
|
||||
</ion-badge>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<ion-item-group *ngIf="user">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.recipientdetails' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.name' | translate}}</h2>
|
||||
<p>{{ user.fullname }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<ng-container *ngIf="badge">
|
||||
<ion-item-group>
|
||||
<ion-item-divider>
|
||||
<core-swipe-navigation [manager]="badges">
|
||||
<ion-refresher slot="fixed" [disabled]="!badgeLoaded" (ionRefresh)="refreshBadges($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="badgeLoaded">
|
||||
<ion-item-group *ngIf="badge">
|
||||
<ion-item class="ion-text-wrap ion-text-center">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.issuerdetails' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.issuername">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.issuername' | translate}}</h2>
|
||||
<p>{{ badge.issuername }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.issuercontact">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.contact' | translate}}</h2>
|
||||
<p><a href="mailto:{{badge.issuercontact}}" core-link auto-login="no" [showBrowserWarning]="false">
|
||||
{{ badge.issuercontact }}
|
||||
</a></p>
|
||||
<img *ngIf="badge.badgeurl" class="large-avatar" [src]="badge.badgeurl" core-external-content [alt]="badge.name" />
|
||||
<ion-badge color="danger" *ngIf="badge.dateexpire && currentTime >= badge.dateexpire">
|
||||
{{ 'addon.badges.expired' | translate }}
|
||||
</ion-badge>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item-group *ngIf="user">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.badgedetails' | translate}}</h2>
|
||||
<h2>{{ 'addon.badges.recipientdetails' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.name">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.name' | translate}}</h2>
|
||||
<p>{{ badge.name }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.version">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.version' | translate}}</h2>
|
||||
<p>{{ badge.version }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.language">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.language' | translate}}</h2>
|
||||
<p>{{ badge.language }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.description">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.description' | translate}}</h2>
|
||||
<p>{{ badge.description }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.imageauthorname">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.imageauthorname' | translate}}</h2>
|
||||
<p>{{ badge.imageauthorname }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.imageauthoremail">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.imageauthoremail' | translate}}</h2>
|
||||
<p><a href="mailto:{{badge.imageauthoremail}}" core-link auto-login="no" [showBrowserWarning]="false">
|
||||
{{ badge.imageauthoremail }}
|
||||
</a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.imageauthorurl">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.imageauthorurl' | translate}}</h2>
|
||||
<p><a [href]="badge.imageauthorurl" core-link auto-login="no"> {{ badge.imageauthorurl }} </a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.imagecaption">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.imagecaption' | translate}}</h2>
|
||||
<p>{{ badge.imagecaption }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="course">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.course' | translate}}</h2>
|
||||
<p>
|
||||
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="courseId">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- Criteria (not yet available) -->
|
||||
</ion-item-group>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.issuancedetails' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.dateissued">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.dateawarded' | translate}}</h2>
|
||||
<p>{{badge.dateissued * 1000 | coreFormatDate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.dateexpire">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.expirydate' | translate}}</h2>
|
||||
<p>
|
||||
{{ badge.dateexpire * 1000 | coreFormatDate }}
|
||||
<span class="text-danger" *ngIf="currentTime >= badge.dateexpire">
|
||||
{{ 'addon.badges.warnexpired' | translate }}
|
||||
</span>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- Evidence (not yet available) -->
|
||||
</ion-item-group>
|
||||
|
||||
<!-- Endorsement -->
|
||||
<ion-item-group *ngIf="badge.endorsement">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.bendorsement' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.endorsement.issuername">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.issuername' | translate}}</h2>
|
||||
<p>{{ badge.endorsement.issuername }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.endorsement.issueremail">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.issueremail' | translate}}</h2>
|
||||
<p>
|
||||
<a href="mailto:{{badge.endorsement.issueremail}}" core-link auto-login="no" [showBrowserWarning]="false">
|
||||
{{ badge.endorsement.issueremail }}
|
||||
</a>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.endorsement.issuerurl">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.issuerurl' | translate}}</h2>
|
||||
<p><a [href]="badge.endorsement.issuerurl" core-link auto-login="no"> {{ badge.endorsement.issuerurl }} </a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.endorsement.dateissued">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.dateawarded' | translate}}</h2>
|
||||
<p>{{ badge.endorsement.dateissued * 1000 | coreFormatDate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.endorsement.claimid">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.claimid' | translate}}</h2>
|
||||
<p><a [href]="badge.endorsement.claimid" core-link auto-login="no"> {{ badge.endorsement.claimid }} </a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.endorsement.claimcomment">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.claimcomment' | translate}}</h2>
|
||||
<p>{{ badge.endorsement.claimcomment }}</p>
|
||||
<p>{{ user.fullname }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- Related badges -->
|
||||
<ion-item-group *ngIf="badge.relatedbadges">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.relatedbages' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let relatedBadge of badge.relatedbadges">
|
||||
<ion-label>
|
||||
<h2>{{ relatedBadge.name }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.relatedbadges.length == 0">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.norelated' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
<ng-container *ngIf="badge">
|
||||
<ion-item-group>
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.issuerdetails' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.issuername">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.issuername' | translate}}</h2>
|
||||
<p>{{ badge.issuername }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.issuercontact">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.contact' | translate}}</h2>
|
||||
<p><a href="mailto:{{badge.issuercontact}}" core-link auto-login="no" [showBrowserWarning]="false">
|
||||
{{ badge.issuercontact }}
|
||||
</a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- Competencies alignment -->
|
||||
<ion-item-group *ngIf="badge.alignment">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.alignment' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let alignment of badge.alignment" [href]="alignment.targeturl" core-link
|
||||
auto-login="no">
|
||||
<ion-label>
|
||||
<h2>{{ alignment.targetname }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.alignment.length == 0">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.noalignment' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</core-loading>
|
||||
<ion-item-group>
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.badgedetails' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.name">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.name' | translate}}</h2>
|
||||
<p>{{ badge.name }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.version">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.version' | translate}}</h2>
|
||||
<p>{{ badge.version }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.language">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.language' | translate}}</h2>
|
||||
<p>{{ badge.language }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.description">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.description' | translate}}</h2>
|
||||
<p>{{ badge.description }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.imageauthorname">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.imageauthorname' | translate}}</h2>
|
||||
<p>{{ badge.imageauthorname }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.imageauthoremail">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.imageauthoremail' | translate}}</h2>
|
||||
<p><a href="mailto:{{badge.imageauthoremail}}" core-link auto-login="no" [showBrowserWarning]="false">
|
||||
{{ badge.imageauthoremail }}
|
||||
</a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.imageauthorurl">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.imageauthorurl' | translate}}</h2>
|
||||
<p><a [href]="badge.imageauthorurl" core-link auto-login="no"> {{ badge.imageauthorurl }} </a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.imagecaption">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.imagecaption' | translate}}</h2>
|
||||
<p>{{ badge.imagecaption }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="course">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.course' | translate}}</h2>
|
||||
<p>
|
||||
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="courseId">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- Criteria (not yet available) -->
|
||||
</ion-item-group>
|
||||
|
||||
<ion-item-group>
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.issuancedetails' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.dateissued">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.dateawarded' | translate}}</h2>
|
||||
<p>{{badge.dateissued * 1000 | coreFormatDate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.dateexpire">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.expirydate' | translate}}</h2>
|
||||
<p>
|
||||
{{ badge.dateexpire * 1000 | coreFormatDate }}
|
||||
<span class="text-danger" *ngIf="currentTime >= badge.dateexpire">
|
||||
{{ 'addon.badges.warnexpired' | translate }}
|
||||
</span>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<!-- Evidence (not yet available) -->
|
||||
</ion-item-group>
|
||||
|
||||
<!-- Endorsement -->
|
||||
<ion-item-group *ngIf="badge.endorsement">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.bendorsement' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.endorsement.issuername">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.issuername' | translate}}</h2>
|
||||
<p>{{ badge.endorsement.issuername }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.endorsement.issueremail">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.issueremail' | translate}}</h2>
|
||||
<p>
|
||||
<a href="mailto:{{badge.endorsement.issueremail}}" core-link auto-login="no" [showBrowserWarning]="false">
|
||||
{{ badge.endorsement.issueremail }}
|
||||
</a>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.endorsement.issuerurl">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.issuerurl' | translate}}</h2>
|
||||
<p><a [href]="badge.endorsement.issuerurl" core-link auto-login="no"> {{ badge.endorsement.issuerurl }} </a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.endorsement.dateissued">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.dateawarded' | translate}}</h2>
|
||||
<p>{{ badge.endorsement.dateissued * 1000 | coreFormatDate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.endorsement.claimid">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.claimid' | translate}}</h2>
|
||||
<p><a [href]="badge.endorsement.claimid" core-link auto-login="no"> {{ badge.endorsement.claimid }} </a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.endorsement.claimcomment">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.claimcomment' | translate}}</h2>
|
||||
<p>{{ badge.endorsement.claimcomment }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- Related badges -->
|
||||
<ion-item-group *ngIf="badge.relatedbadges">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.relatedbages' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let relatedBadge of badge.relatedbadges">
|
||||
<ion-label>
|
||||
<h2>{{ relatedBadge.name }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.relatedbadges.length == 0">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.norelated' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- Competencies alignment -->
|
||||
<ion-item-group *ngIf="badge.alignment">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.alignment' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let alignment of badge.alignment" [href]="alignment.targeturl" core-link
|
||||
auto-login="no">
|
||||
<ion-label>
|
||||
<h2>{{ alignment.targetname }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.alignment.length == 0">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.noalignment' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
</core-loading>
|
||||
</core-swipe-navigation>
|
||||
</ion-content>
|
||||
|
|
|
@ -23,6 +23,9 @@ import { CoreUtils } from '@services/utils/utils';
|
|||
import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
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.
|
||||
|
@ -40,12 +43,11 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
|
|||
user?: CoreUserProfile;
|
||||
course?: CoreEnrolledCourseData;
|
||||
badge?: AddonBadgesUserBadge;
|
||||
badges?: CoreSwipeItemsManager;
|
||||
badgeLoaded = false;
|
||||
currentTime = 0;
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
) { }
|
||||
constructor(protected route: ActivatedRoute) { }
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
|
@ -58,6 +60,11 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
|
|||
this.fetchIssuedBadge().finally(() => {
|
||||
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 { CoreSites } from '@services/sites';
|
||||
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 { 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.
|
||||
|
@ -34,7 +35,7 @@ import { CoreNavigator } from '@services/navigator';
|
|||
export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
|
||||
|
||||
currentTime = 0;
|
||||
badges: AddonBadgesUserBadgesManager;
|
||||
badges: CoreListItemsManager<AddonBadgesUserBadge, AddonBadgesUserBadgesSource>;
|
||||
|
||||
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||
|
||||
|
@ -47,7 +48,10 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
|
|||
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.
|
||||
*/
|
||||
async refreshBadges(refresher?: IonRefresher): Promise<void> {
|
||||
await CoreUtils.ignoreErrors(AddonBadges.invalidateUserBadges(this.badges.courseId, this.badges.userId));
|
||||
await CoreUtils.ignoreErrors(this.fetchBadges());
|
||||
await CoreUtils.ignoreErrors(
|
||||
AddonBadges.invalidateUserBadges(
|
||||
this.badges.getSource().COURSE_ID,
|
||||
this.badges.getSource().USER_ID,
|
||||
),
|
||||
);
|
||||
await CoreUtils.ignoreErrors(this.badges.reload());
|
||||
|
||||
refresher?.complete();
|
||||
}
|
||||
|
@ -85,55 +94,12 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
|
|||
this.currentTime = CoreTimeUtils.timestamp();
|
||||
|
||||
try {
|
||||
await this.fetchBadges();
|
||||
await this.badges.reload();
|
||||
} catch (message) {
|
||||
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>
|
||||
<core-split-view>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded || !submissions.loaded" (ionRefresh)="refreshList($event.target)">
|
||||
<ion-refresher slot="fixed" [disabled]="!submissions.loaded" (ionRefresh)="refreshList($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded && submissions.loaded">
|
||||
<core-loading [hideUntil]="submissions.loaded">
|
||||
<core-empty-box *ngIf="!submissions || submissions.empty" icon="fas-file-signature"
|
||||
[message]="'addon.mod_assign.submissionstatus_' | translate">
|
||||
</core-empty-box>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<ion-label id="addon-assign-groupslabel" *ngIf="groupInfo.visibleGroups">
|
||||
{{ 'core.groupsvisible' | translate }}
|
||||
</ion-label>
|
||||
<ion-select [(ngModel)]="groupId" (ionChange)="setGroup(groupId)" aria-labelledby="addon-assign-groupslabel"
|
||||
<ion-select [(ngModel)]="groupId" (ionChange)="reloadSubmissions()" aria-labelledby="addon-assign-groupslabel"
|
||||
interface="action-sheet" slot="end" [interfaceOptions]="{header: 'core.group' | translate}">
|
||||
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
|
||||
{{groupOpt.name}}
|
||||
|
|
|
@ -13,29 +13,20 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, OnDestroy, AfterViewInit, ViewChild } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
||||
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
|
||||
import { CoreGroupInfo } from '@services/groups';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import {
|
||||
AddonModAssignAssign,
|
||||
AddonModAssignSubmission,
|
||||
AddonModAssignProvider,
|
||||
AddonModAssign,
|
||||
AddonModAssignGrade,
|
||||
} from '../../services/assign';
|
||||
import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../../services/assign-helper';
|
||||
import { AddonModAssignOffline } from '../../services/assign-offline';
|
||||
import { AddonModAssignSubmissionForList, AddonModAssignSubmissionsSource } from '../../classes/submissions-source';
|
||||
import { AddonModAssignAssign, AddonModAssignProvider } from '../../services/assign';
|
||||
import {
|
||||
AddonModAssignSyncProvider,
|
||||
AddonModAssignSync,
|
||||
AddonModAssignManualSyncData,
|
||||
AddonModAssignAutoSyncData,
|
||||
} from '../../services/assign-sync';
|
||||
|
@ -51,47 +42,26 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
|||
|
||||
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||
|
||||
title = ''; // Title to display.
|
||||
assign?: AddonModAssignAssign; // Assignment.
|
||||
submissions: AddonModAssignSubmissionListManager; // List of submissions
|
||||
loaded = false; // Whether data has been loaded.
|
||||
groupId = 0; // Group ID to show.
|
||||
courseId!: number; // Course ID the assignment belongs to.
|
||||
moduleId!: number; // Module ID the submission belongs to.
|
||||
title = '';
|
||||
submissions!: CoreListItemsManager<AddonModAssignSubmissionForList, AddonModAssignSubmissionsSource>; // List of submissions
|
||||
|
||||
groupInfo: CoreGroupInfo = {
|
||||
groups: [],
|
||||
separateGroups: false,
|
||||
visibleGroups: false,
|
||||
defaultGroupId: 0,
|
||||
};
|
||||
|
||||
protected selectedStatus?: string; // The status to see.
|
||||
protected gradedObserver: CoreEventObserver; // Observer to refresh data when a grade changes.
|
||||
protected syncObserver: CoreEventObserver; // Observer to refresh data when the async is synchronized.
|
||||
protected submissionsData: { canviewsubmissions: boolean; submissions?: AddonModAssignSubmission[] } = {
|
||||
canviewsubmissions: false,
|
||||
};
|
||||
protected sourceUnsubscribe?: () => void;
|
||||
|
||||
constructor() {
|
||||
this.submissions = new AddonModAssignSubmissionListManager(AddonModAssignSubmissionListPage);
|
||||
|
||||
// Update data if some grade changes.
|
||||
this.gradedObserver = CoreEvents.on(
|
||||
AddonModAssignProvider.GRADED_EVENT,
|
||||
(data) => {
|
||||
if (
|
||||
this.loaded &&
|
||||
this.assign &&
|
||||
data.assignmentId == this.assign.id &&
|
||||
this.submissions.loaded &&
|
||||
this.submissions.getSource().assign &&
|
||||
data.assignmentId == this.submissions.getSource().assign?.id &&
|
||||
data.userId == CoreSites.getCurrentSiteUserId()
|
||||
) {
|
||||
// Grade changed, refresh the data.
|
||||
this.loaded = false;
|
||||
|
||||
this.refreshAllData(true).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
this.refreshAllData(true);
|
||||
}
|
||||
},
|
||||
CoreSites.getCurrentSiteId(),
|
||||
|
@ -102,29 +72,36 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
|||
this.syncObserver = CoreEvents.onMultiple<AddonModAssignAutoSyncData | AddonModAssignManualSyncData>(
|
||||
events,
|
||||
(data) => {
|
||||
if (!this.loaded || ('context' in data && data.context == 'submission-list')) {
|
||||
if (!this.submissions.loaded || ('context' in data && data.context == 'submission-list')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loaded = false;
|
||||
|
||||
this.refreshAllData(false).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
this.refreshAllData(false);
|
||||
},
|
||||
CoreSites.getCurrentSiteId(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngAfterViewInit(): void {
|
||||
try {
|
||||
this.moduleId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||
this.groupId = CoreNavigator.getRouteNumberParam('groupId') || 0;
|
||||
this.selectedStatus = CoreNavigator.getRouteParam('status');
|
||||
const moduleId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||
const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||
const groupId = CoreNavigator.getRouteNumberParam('groupId') || 0;
|
||||
const selectedStatus = CoreNavigator.getRouteParam('status');
|
||||
const submissionsSource = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||
AddonModAssignSubmissionsSource,
|
||||
[courseId, moduleId, selectedStatus],
|
||||
);
|
||||
|
||||
submissionsSource.groupId = groupId;
|
||||
this.sourceUnsubscribe = submissionsSource.addListener({
|
||||
onItemsUpdated: () => {
|
||||
this.title = this.submissions.getSource().assign?.name || this.title;
|
||||
},
|
||||
});
|
||||
|
||||
this.submissions = new CoreListItemsManager(
|
||||
submissionsSource,
|
||||
AddonModAssignSubmissionListPage,
|
||||
);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
|
||||
|
@ -132,18 +109,48 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
|||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
get assign(): AddonModAssignAssign | undefined {
|
||||
return this.submissions.getSource().assign;
|
||||
}
|
||||
|
||||
get groupInfo(): CoreGroupInfo {
|
||||
return this.submissions.getSource().groupInfo;
|
||||
}
|
||||
|
||||
get moduleId(): number {
|
||||
return this.submissions.getSource().MODULE_ID;
|
||||
}
|
||||
|
||||
get courseId(): number {
|
||||
return this.submissions.getSource().COURSE_ID;
|
||||
}
|
||||
|
||||
get groupId(): number {
|
||||
return this.submissions.getSource().groupId;
|
||||
}
|
||||
|
||||
set groupId(value: number) {
|
||||
this.submissions.getSource().groupId = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngAfterViewInit(): void {
|
||||
const selectedStatus = this.submissions.getSource().SELECTED_STATUS;
|
||||
this.title = Translate.instant(
|
||||
selectedStatus
|
||||
? (
|
||||
selectedStatus === AddonModAssignProvider.NEED_GRADING
|
||||
? 'addon.mod_assign.numberofsubmissionsneedgrading'
|
||||
: `addon.mod_assign.submissionstatus_${selectedStatus}`
|
||||
)
|
||||
: 'addon.mod_assign.numberofparticipants',
|
||||
);
|
||||
|
||||
if (this.selectedStatus) {
|
||||
if (this.selectedStatus == AddonModAssignProvider.NEED_GRADING) {
|
||||
this.title = Translate.instant('addon.mod_assign.numberofsubmissionsneedgrading');
|
||||
} else {
|
||||
this.title = Translate.instant('addon.mod_assign.submissionstatus_' + this.selectedStatus);
|
||||
}
|
||||
} else {
|
||||
this.title = Translate.instant('addon.mod_assign.numberofparticipants');
|
||||
}
|
||||
this.fetchAssignment(true).finally(() => {
|
||||
this.loaded = true;
|
||||
this.submissions.start(this.splitView);
|
||||
});
|
||||
}
|
||||
|
@ -156,148 +163,12 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
|||
*/
|
||||
protected async fetchAssignment(sync = false): Promise<void> {
|
||||
try {
|
||||
// Get assignment data.
|
||||
this.assign = await AddonModAssign.getAssignment(this.courseId, this.moduleId);
|
||||
|
||||
this.title = this.assign.name || this.title;
|
||||
|
||||
if (sync) {
|
||||
try {
|
||||
// Try to synchronize data.
|
||||
const result = await AddonModAssignSync.syncAssign(this.assign.id);
|
||||
|
||||
if (result && result.updated) {
|
||||
CoreEvents.trigger(
|
||||
AddonModAssignSyncProvider.MANUAL_SYNCED,
|
||||
{
|
||||
assignId: this.assign.id,
|
||||
warnings: result.warnings,
|
||||
gradesBlocked: result.gradesBlocked,
|
||||
context: 'submission-list',
|
||||
},
|
||||
CoreSites.getCurrentSiteId(),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors, probably user is offline or sync is blocked.
|
||||
}
|
||||
}
|
||||
|
||||
// Get assignment submissions.
|
||||
this.submissionsData = await AddonModAssign.getSubmissions(this.assign.id, { cmId: this.assign.cmid });
|
||||
|
||||
if (!this.submissionsData.canviewsubmissions) {
|
||||
// User shouldn't be able to reach here.
|
||||
throw new Error('Cannot view submissions.');
|
||||
}
|
||||
|
||||
// Check if groupmode is enabled to avoid showing wrong numbers.
|
||||
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.assign.cmid, false);
|
||||
|
||||
await this.setGroup(CoreGroups.validateGroupId(this.groupId, this.groupInfo));
|
||||
await this.submissions.getSource().loadAssignment(sync);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error getting assigment data.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group to see the summary.
|
||||
*
|
||||
* @param groupId Group ID.
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
async setGroup(groupId: number): Promise<void> {
|
||||
this.groupId = groupId;
|
||||
|
||||
// Fetch submissions and grades.
|
||||
const submissions =
|
||||
await AddonModAssignHelper.getSubmissionsUserData(
|
||||
this.assign!,
|
||||
this.submissionsData.submissions,
|
||||
this.groupId,
|
||||
);
|
||||
// Get assignment grades only if workflow is not enabled to check grading date.
|
||||
const grades = !this.assign!.markingworkflow
|
||||
? await AddonModAssign.getAssignmentGrades(this.assign!.id, { cmId: this.assign!.cmid })
|
||||
: [];
|
||||
|
||||
// Filter the submissions to get only the ones with the right status and add some extra data.
|
||||
const getNeedGrading = this.selectedStatus == AddonModAssignProvider.NEED_GRADING;
|
||||
const searchStatus = getNeedGrading ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED : this.selectedStatus;
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
const showSubmissions: AddonModAssignSubmissionForList[] = [];
|
||||
|
||||
submissions.forEach((submission: AddonModAssignSubmissionForList) => {
|
||||
if (!searchStatus || searchStatus == submission.status) {
|
||||
promises.push(
|
||||
CoreUtils.ignoreErrors(
|
||||
AddonModAssignOffline.getSubmissionGrade(this.assign!.id, submission.userid),
|
||||
).then(async (data) => {
|
||||
if (getNeedGrading) {
|
||||
// Only show the submissions that need to be graded.
|
||||
const add = await AddonModAssign.needsSubmissionToBeGraded(submission, this.assign!.id);
|
||||
|
||||
if (!add) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Load offline grades.
|
||||
const notSynced = !!data && submission.timemodified < data.timemodified;
|
||||
|
||||
if (submission.gradingstatus == 'graded' && !this.assign!.markingworkflow) {
|
||||
// Get the last grade of the submission.
|
||||
const grade = grades
|
||||
.filter((grade) => grade.userid == submission.userid)
|
||||
.reduce(
|
||||
(a, b) => (a && a.timemodified > b.timemodified ? a : b),
|
||||
<AddonModAssignGrade | undefined> undefined,
|
||||
);
|
||||
|
||||
if (grade && grade.timemodified < submission.timemodified) {
|
||||
submission.gradingstatus = AddonModAssignProvider.GRADED_FOLLOWUP_SUBMIT;
|
||||
}
|
||||
}
|
||||
submission.statusColor = AddonModAssign.getSubmissionStatusColor(submission.status);
|
||||
submission.gradingColor = AddonModAssign.getSubmissionGradingStatusColor(
|
||||
submission.gradingstatus,
|
||||
);
|
||||
|
||||
// Show submission status if not submitted for grading.
|
||||
if (submission.statusColor != 'success' || !submission.gradingstatus) {
|
||||
submission.statusTranslated = Translate.instant(
|
||||
'addon.mod_assign.submissionstatus_' + submission.status,
|
||||
);
|
||||
} else {
|
||||
submission.statusTranslated = '';
|
||||
}
|
||||
|
||||
if (notSynced) {
|
||||
submission.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced';
|
||||
submission.gradingColor = '';
|
||||
} else if (submission.statusColor != 'danger' || submission.gradingColor != 'danger') {
|
||||
// Show grading status if one of the statuses is not done.
|
||||
submission.gradingStatusTranslationId = AddonModAssign.getSubmissionGradingStatusTranslationId(
|
||||
submission.gradingstatus,
|
||||
);
|
||||
} else {
|
||||
submission.gradingStatusTranslationId = '';
|
||||
}
|
||||
|
||||
showSubmissions.push(submission);
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
this.submissions.setItems(showSubmissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh all the data.
|
||||
*
|
||||
|
@ -305,18 +176,8 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async refreshAllData(sync?: boolean): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(AddonModAssign.invalidateAssignmentData(this.courseId));
|
||||
if (this.assign) {
|
||||
promises.push(AddonModAssign.invalidateAllSubmissionData(this.assign.id));
|
||||
promises.push(AddonModAssign.invalidateAssignmentUserMappingsData(this.assign.id));
|
||||
promises.push(AddonModAssign.invalidateAssignmentGradesData(this.assign.id));
|
||||
promises.push(AddonModAssign.invalidateListParticipantsData(this.assign.id));
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
await this.submissions.getSource().invalidateCache();
|
||||
} finally {
|
||||
this.fetchAssignment(sync);
|
||||
}
|
||||
|
@ -333,6 +194,13 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload submissions list.
|
||||
*/
|
||||
async reloadSubmissions(): Promise<void> {
|
||||
await this.submissions.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
|
@ -340,43 +208,7 @@ export class AddonModAssignSubmissionListPage implements AfterViewInit, OnDestro
|
|||
this.gradedObserver?.off();
|
||||
this.syncObserver?.off();
|
||||
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>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<addon-mod-assign-submission [courseId]="courseId" [moduleId]="moduleId" [submitId]="submitId" [blindId]="blindId">
|
||||
</addon-mod-assign-submission>
|
||||
</core-loading>
|
||||
<core-swipe-navigation [manager]="submissions">
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<addon-mod-assign-submission *ngIf="loaded"
|
||||
[courseId]="courseId" [moduleId]="moduleId" [submitId]="submitId" [blindId]="blindId">
|
||||
</addon-mod-assign-submission>
|
||||
</core-loading>
|
||||
</core-swipe-navigation>
|
||||
</ion-content>
|
||||
|
|
|
@ -12,14 +12,17 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||
import { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CanLeave } from '@guards/can-leave';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreScreen } from '@services/screen';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { AddonModAssignSubmissionsSource } from '../../classes/submissions-source';
|
||||
import { AddonModAssignSubmissionComponent } from '../../components/submission/submission';
|
||||
import { AddonModAssign, AddonModAssignAssign } from '../../services/assign';
|
||||
|
||||
|
@ -30,11 +33,12 @@ import { AddonModAssign, AddonModAssignAssign } from '../../services/assign';
|
|||
selector: 'page-addon-mod-assign-submission-review',
|
||||
templateUrl: 'submission-review.html',
|
||||
})
|
||||
export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave {
|
||||
export class AddonModAssignSubmissionReviewPage implements OnInit, OnDestroy, CanLeave {
|
||||
|
||||
@ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent;
|
||||
|
||||
title = ''; // Title to display.
|
||||
submissions?: AddonModAssignSubmissionSwipeItemsManager;
|
||||
moduleId!: number; // Module ID the submission belongs to.
|
||||
courseId!: number; // Course ID the assignment belongs to.
|
||||
submitId!: number; // User that did the submission.
|
||||
|
@ -46,9 +50,7 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave {
|
|||
protected blindMarking = false; // Whether it uses blind marking.
|
||||
protected forceLeave = false; // To allow leaving the page without checking for changes.
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
) { }
|
||||
constructor(protected route: ActivatedRoute) { }
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
|
@ -60,6 +62,19 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave {
|
|||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||
this.submitId = CoreNavigator.getRouteNumberParam('submitId') || 0;
|
||||
this.blindId = CoreNavigator.getRouteNumberParam('blindId', { params });
|
||||
const groupId = CoreNavigator.getRequiredRouteNumberParam('groupId');
|
||||
const selectedStatus = CoreNavigator.getRouteParam('selectedStatus');
|
||||
const submissionsSource = CoreItemsManagerSourcesTracker.getOrCreateSource(
|
||||
AddonModAssignSubmissionsSource,
|
||||
[this.courseId, this.moduleId, selectedStatus],
|
||||
);
|
||||
|
||||
this.submissions?.destroy();
|
||||
|
||||
submissionsSource.groupId = groupId;
|
||||
this.submissions = new AddonModAssignSubmissionSwipeItemsManager(submissionsSource);
|
||||
|
||||
this.submissions.start();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
|
||||
|
@ -74,6 +89,13 @@ export class AddonModAssignSubmissionReviewPage implements OnInit, CanLeave {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.submissions?.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can leave the page or not.
|
||||
*
|
||||
|
@ -190,3 +212,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"
|
||||
(action)="gotoBlog()">
|
||||
</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"
|
||||
(action)="doRefresh(null, $event)">
|
||||
</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"
|
||||
(action)="doRefresh(null, $event, true)">
|
||||
</core-context-menu-item>
|
||||
|
@ -32,11 +32,11 @@
|
|||
|
||||
<!-- Content. -->
|
||||
<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>
|
||||
|
||||
<core-loading [hideUntil]="discussions.loaded">
|
||||
<core-loading [hideUntil]="discussions && discussions.loaded">
|
||||
<!-- Activity info. -->
|
||||
<core-course-module-info [module]="module" (completionChanged)="onCompletionChange()"
|
||||
[description]="forum && forum.type != 'single' && description" [component]="component" [componentId]="componentId"
|
||||
|
@ -57,17 +57,18 @@
|
|||
</ion-card>
|
||||
|
||||
<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>
|
||||
|
||||
<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"
|
||||
[label]="('core.sort' | translate)" (onChange)="setSortOrder($event)" [selection]="selectedSortOrder.label | translate"
|
||||
interface="modal">
|
||||
</core-combobox>
|
||||
</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)"
|
||||
(click)="discussions.select(discussion)" button>
|
||||
<ion-label>
|
||||
|
@ -96,17 +97,16 @@
|
|||
<ion-icon name="fas-users" [attr.aria-label]="'addon.mod_forum.group' | translate">
|
||||
</ion-icon> {{ discussion.groupname }}
|
||||
</p>
|
||||
<p *ngIf="discussions.isOnlineDiscussion(discussion)">
|
||||
<p *ngIf="isOnlineDiscussion(discussion)">
|
||||
{{discussion.created * 1000 | coreFormatDate: "strftimerecentfull"}}
|
||||
</p>
|
||||
<p *ngIf="discussions.isOfflineDiscussion(discussion)">
|
||||
<p *ngIf="isOfflineDiscussion(discussion)">
|
||||
<ion-icon name="fas-clock" aria-hidden="true"></ion-icon>
|
||||
{{ 'core.notsent' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ion-row *ngIf="discussions.isOnlineDiscussion(discussion)"
|
||||
class="ion-text-center addon-mod-forum-discussion-more-info">
|
||||
<ion-row *ngIf="isOnlineDiscussion(discussion)" class="ion-text-center addon-mod-forum-discussion-more-info">
|
||||
<ion-col class="ion-text-start">
|
||||
<ion-note>
|
||||
<ion-icon name="fas-clock" aria-hidden="true"></ion-icon> {{ 'addon.mod_forum.lastpost' | translate }}
|
||||
|
@ -134,7 +134,7 @@
|
|||
</ion-label>
|
||||
</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)">
|
||||
</core-infinite-loading>
|
||||
</ng-container>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
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 { ModalOptions } from '@ionic/core';
|
||||
|
||||
|
@ -27,7 +27,7 @@ import {
|
|||
AddonModForumNewDiscussionData,
|
||||
AddonModForumReplyDiscussionData,
|
||||
} 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 { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||
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 { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
import { AddonModForumDiscussionOptionsMenuComponent } from '../discussion-options-menu/discussion-options-menu';
|
||||
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 { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
||||
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.
|
||||
|
@ -72,24 +74,21 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
component = AddonModForumProvider.COMPONENT;
|
||||
moduleName = 'forum';
|
||||
descriptionNote?: string;
|
||||
forum?: AddonModForumData;
|
||||
discussions: AddonModForumDiscussionsManager;
|
||||
discussions!: AddonModForumDiscussionsManager;
|
||||
discussionsItems: AddonModForumDiscussionItem[] = [];
|
||||
fetchFailed = false;
|
||||
canAddDiscussion = false;
|
||||
addDiscussionText!: string;
|
||||
availabilityMessage: string | null = null;
|
||||
sortingAvailable!: boolean;
|
||||
sortOrders: AddonModForumSortOrder[] = [];
|
||||
selectedSortOrder: AddonModForumSortOrder | null = null;
|
||||
canPin = false;
|
||||
trackPosts = false;
|
||||
hasOfflineRatings = false;
|
||||
sortOrderSelectorModalOptions: ModalOptions = {
|
||||
component: AddonModForumSortOrderSelectorComponent,
|
||||
};
|
||||
|
||||
protected syncEventName = AddonModForumSyncProvider.AUTO_SYNCED;
|
||||
protected page = 0;
|
||||
protected usesGroups = false;
|
||||
protected syncManualObserver?: CoreEventObserver; // It will observe the sync manual event.
|
||||
protected replyObserver?: CoreEventObserver;
|
||||
protected newDiscObserver?: CoreEventObserver;
|
||||
|
@ -97,19 +96,42 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
protected changeDiscObserver?: CoreEventObserver;
|
||||
protected ratingOfflineObserver?: CoreEventObserver;
|
||||
protected ratingSyncObserver?: CoreEventObserver;
|
||||
protected sourceUnsubscribe?: () => void;
|
||||
|
||||
constructor(
|
||||
route: ActivatedRoute,
|
||||
public route: ActivatedRoute,
|
||||
@Optional() protected content?: IonContent,
|
||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||
) {
|
||||
super('AddonModForumIndexComponent', content, courseContentsPage);
|
||||
}
|
||||
|
||||
this.discussions = new AddonModForumDiscussionsManager(
|
||||
route.component,
|
||||
this,
|
||||
courseContentsPage ? `${AddonModForumModuleHandlerService.PAGE_NAME}/` : '',
|
||||
);
|
||||
get forum(): AddonModForumData | undefined {
|
||||
return this.discussions?.getSource().forum;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 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.
|
||||
this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, (data) => {
|
||||
this.autoSyncEventReceived(data);
|
||||
|
@ -141,12 +205,16 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
this.eventReceived.bind(this, false),
|
||||
);
|
||||
this.changeDiscObserver = CoreEvents.on(AddonModForumProvider.CHANGE_DISCUSSION_EVENT, data => {
|
||||
if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module.id) {
|
||||
AddonModForum.invalidateDiscussionsList(this.forum!.id).finally(() => {
|
||||
if (!this.forum) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.forum.id === data.forumId || data.cmId === this.module.id) {
|
||||
AddonModForum.invalidateDiscussionsList(this.forum.id).finally(() => {
|
||||
if (data.discussionId) {
|
||||
// Discussion changed, search it in the list of discussions.
|
||||
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;
|
||||
|
||||
if (discussion) {
|
||||
|
@ -196,20 +264,6 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
async ngAfterViewInit(): Promise<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -226,6 +280,8 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
this.changeDiscObserver && this.changeDiscObserver.off();
|
||||
this.ratingOfflineObserver && this.ratingOfflineObserver.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.
|
||||
*/
|
||||
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||
this.discussions.fetchFailed = false;
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(this.fetchForum(sync, showErrors));
|
||||
promises.push(this.fetchSortOrderPreference());
|
||||
this.fetchFailed = false;
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
await Promise.all([
|
||||
this.fetchOfflineDiscussions(),
|
||||
this.fetchDiscussions(refresh),
|
||||
CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.forum!.cmid).then((hasRatings) => {
|
||||
this.fetchForum(sync, showErrors),
|
||||
this.fetchSortOrderPreference(),
|
||||
]);
|
||||
|
||||
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;
|
||||
|
||||
return;
|
||||
|
@ -258,7 +316,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
if (refresh) {
|
||||
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 {
|
||||
// Get forum failed, retry without using cache since it might be a new activity.
|
||||
await this.refreshContent(sync);
|
||||
|
@ -273,19 +331,19 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
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.availabilityMessage = AddonModForumHelper.getAvailabilityMessage(forum);
|
||||
this.descriptionNote = Translate.instant('addon.mod_forum.numdiscussions', {
|
||||
numdiscussions: forum.numdiscussions,
|
||||
});
|
||||
|
||||
if (typeof forum.istracked != 'undefined') {
|
||||
this.trackPosts = forum.istracked;
|
||||
}
|
||||
|
||||
this.dataRetrieved.emit(forum);
|
||||
|
||||
switch (forum.type) {
|
||||
|
@ -319,10 +377,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
// Check if the activity uses groups.
|
||||
promises.push(
|
||||
CoreGroups.instance
|
||||
.getActivityGroupMode(this.forum.cmid)
|
||||
.getActivityGroupMode(forum.cmid)
|
||||
.then(async mode => {
|
||||
this.usesGroups = mode === CoreGroupsProvider.SEPARATEGROUPS
|
||||
|| mode === CoreGroupsProvider.VISIBLEGROUPS;
|
||||
this.discussions.getSource().usesGroups =
|
||||
mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS;
|
||||
|
||||
return;
|
||||
}),
|
||||
|
@ -330,14 +388,14 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
|
||||
promises.push(
|
||||
AddonModForum.instance
|
||||
.getAccessInformation(this.forum.id, { cmId: this.module.id })
|
||||
.getAccessInformation(forum.id, { cmId: this.module.id })
|
||||
.then(async accessInfo => {
|
||||
// Disallow adding discussions if cut-off date is reached and the user has not the
|
||||
// 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.
|
||||
const cutoffDateReached = AddonModForumHelper.isCutoffDateReached(this.forum!)
|
||||
const cutoffDateReached = AddonModForumHelper.isCutoffDateReached(forum)
|
||||
&& !accessInfo.cancanoverridecutoff;
|
||||
this.canAddDiscussion = !!this.forum?.cancreatediscussions && !cutoffDateReached;
|
||||
this.canAddDiscussion = !!forum.cancreatediscussions && !cutoffDateReached;
|
||||
|
||||
return;
|
||||
}),
|
||||
|
@ -347,7 +405,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
// Use the canAddDiscussion WS to check if the user can pin discussions.
|
||||
promises.push(
|
||||
AddonModForum.instance
|
||||
.canAddDiscussionToAll(this.forum.id, { cmId: this.module.id })
|
||||
.canAddDiscussionToAll(forum.id, { cmId: this.module.id })
|
||||
.then(async response => {
|
||||
this.canPin = !!response.canpindiscussions;
|
||||
|
||||
|
@ -366,124 +424,6 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
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.
|
||||
*
|
||||
|
@ -492,11 +432,13 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
*/
|
||||
async fetchMoreDiscussions(complete: () => void): Promise<void> {
|
||||
try {
|
||||
await this.fetchDiscussions(false);
|
||||
this.fetchFailed = false;
|
||||
|
||||
await this.discussions.load();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true);
|
||||
|
||||
this.discussions.fetchFailed = true;
|
||||
this.fetchFailed = true;
|
||||
} finally {
|
||||
complete();
|
||||
}
|
||||
|
@ -521,9 +463,13 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
};
|
||||
|
||||
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.sortOrderSelectorModalOptions.componentProps!.selected = this.selectedSortOrder.value;
|
||||
this.discussions.getSource().selectedSortOrder = selectedOrder;
|
||||
|
||||
if (this.sortOrderSelectorModalOptions.componentProps) {
|
||||
this.sortOrderSelectorModalOptions.componentProps.selected = selectedOrder.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -597,11 +543,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
if (isNewDiscussion && CoreScreen.isTablet) {
|
||||
const newDiscussionData = data as AddonModForumNewDiscussionData;
|
||||
const discussion = this.discussions.items.find(disc => {
|
||||
if (this.discussions.isOfflineDiscussion(disc)) {
|
||||
if (this.discussions.getSource().isOfflineDiscussion(disc)) {
|
||||
return disc.timecreated === newDiscussionData.discTimecreated;
|
||||
}
|
||||
|
||||
if (this.discussions.isOnlineDiscussion(disc)) {
|
||||
if (this.discussions.getSource().isOnlineDiscussion(disc)) {
|
||||
return CoreArray.contains(newDiscussionData.discussionIds ?? [], disc.discussion);
|
||||
}
|
||||
|
||||
|
@ -625,7 +571,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
* @param timeCreated Creation time of the offline discussion.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
async setSortOrder(sortOrder: AddonModForumSortOrder): Promise<void> {
|
||||
if (sortOrder.value != this.selectedSortOrder?.value) {
|
||||
this.selectedSortOrder = sortOrder;
|
||||
this.sortOrderSelectorModalOptions.componentProps!.selected = this.selectedSortOrder.value;
|
||||
this.page = 0;
|
||||
if (sortOrder.value != this.discussions.getSource().selectedSortOrder?.value) {
|
||||
this.discussions.getSource().selectedSortOrder = sortOrder;
|
||||
this.discussions.getSource().setDirty(true);
|
||||
|
||||
if (this.sortOrderSelectorModalOptions.componentProps) {
|
||||
this.sortOrderSelectorModalOptions.componentProps.selected = sortOrder.value;
|
||||
}
|
||||
|
||||
try {
|
||||
await CoreUser.setUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, sortOrder.value.toFixed(0));
|
||||
|
@ -666,6 +615,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
* @param discussion Discussion.
|
||||
*/
|
||||
async showOptionsMenu(event: Event, discussion: AddonModForumDiscussion): Promise<void> {
|
||||
if (!this.forum) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
@ -673,7 +626,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
|||
component: AddonModForumDiscussionOptionsMenuComponent,
|
||||
componentProps: {
|
||||
discussion,
|
||||
forumId: this.forum!.id,
|
||||
forumId: this.forum.id,
|
||||
cmId: this.module.id,
|
||||
},
|
||||
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.
|
||||
*/
|
||||
class AddonModForumDiscussionsManager extends CorePageItemsListManager<DiscussionItem> {
|
||||
class AddonModForumDiscussionsManager extends CoreListItemsManager<AddonModForumDiscussionItem, AddonModForumDiscussionsSource> {
|
||||
|
||||
onlineLoaded = false;
|
||||
fetchFailed = false;
|
||||
page: AddonModForumIndexComponent;
|
||||
|
||||
private discussionsPathPrefix: string;
|
||||
private component: AddonModForumIndexComponent;
|
||||
constructor(source: AddonModForumDiscussionsSource, page: AddonModForumIndexComponent) {
|
||||
super(source, page.route.component);
|
||||
|
||||
constructor(pageComponent: unknown, component: AddonModForumIndexComponent, discussionsPathPrefix: string) {
|
||||
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[];
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getItemQueryParams(discussion: DiscussionItem): Params {
|
||||
return {
|
||||
courseId: this.component.courseId,
|
||||
cmId: this.component.module.id,
|
||||
forumId: this.component.forum!.id,
|
||||
...(this.isOnlineDiscussion(discussion) ? { discussion, trackPosts: this.component.trackPosts } : {}),
|
||||
};
|
||||
}
|
||||
protected getDefaultItem(): AddonModForumDiscussionItem | null {
|
||||
const source = this.getSource();
|
||||
|
||||
/**
|
||||
* 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);
|
||||
return this.items.find(discussion => !source.isNewDiscussionForm(discussion)) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected getItemPath(discussion: DiscussionItem): string {
|
||||
const getRelativePath = () => {
|
||||
if (this.isOnlineDiscussion(discussion)) {
|
||||
return discussion.discussion;
|
||||
}
|
||||
protected async logActivity(): Promise<void> {
|
||||
const forum = this.getSource().forum;
|
||||
|
||||
if (this.isOfflineDiscussion(discussion)) {
|
||||
return `new/${discussion.timecreated}`;
|
||||
}
|
||||
if (!forum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return 'new/0';
|
||||
};
|
||||
CoreUtils.ignoreErrors(
|
||||
AddonModForum.instance
|
||||
.logView(forum.id, forum.name)
|
||||
.then(async () => {
|
||||
CoreCourse.checkModuleCompletion(this.page.courseId, this.page.module.completiondata);
|
||||
|
||||
return this.discussionsPathPrefix + getRelativePath();
|
||||
return;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ const mainMenuRoutes: Routes = [
|
|||
{
|
||||
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/discussion/:discussionId`,
|
||||
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
||||
data: { swipeEnabled: false },
|
||||
},
|
||||
{
|
||||
path: AddonModForumModuleHandlerService.PAGE_NAME,
|
||||
|
@ -66,10 +67,12 @@ const mainMenuRoutes: Routes = [
|
|||
path: `${COURSE_CONTENTS_PATH}/${AddonModForumModuleHandlerService.PAGE_NAME}/new/:timeCreated`,
|
||||
loadChildren: () => import('./pages/new-discussion/new-discussion.module')
|
||||
.then(m => m.AddonForumNewDiscussionPageModule),
|
||||
data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` },
|
||||
},
|
||||
{
|
||||
path: `${COURSE_CONTENTS_PATH}/${AddonModForumModuleHandlerService.PAGE_NAME}/:discussionId`,
|
||||
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
||||
data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` },
|
||||
},
|
||||
],
|
||||
() => CoreScreen.isMobile,
|
||||
|
@ -82,10 +85,12 @@ const courseContentsRoutes: Routes = conditionalRoutes(
|
|||
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/new/:timeCreated`,
|
||||
loadChildren: () => import('./pages/new-discussion/new-discussion.module')
|
||||
.then(m => m.AddonForumNewDiscussionPageModule),
|
||||
data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` },
|
||||
},
|
||||
{
|
||||
path: `${AddonModForumModuleHandlerService.PAGE_NAME}/:discussionId`,
|
||||
loadChildren: () => import('./pages/discussion/discussion.module').then(m => m.AddonForumDiscussionPageModule),
|
||||
data: { discussionsPathPrefix: `${AddonModForumModuleHandlerService.PAGE_NAME}/` },
|
||||
},
|
||||
],
|
||||
() => CoreScreen.isTablet,
|
||||
|
|
|
@ -56,72 +56,74 @@
|
|||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!discussionLoaded" (ionRefresh)="doRefresh($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-swipe-navigation [manager]="discussions">
|
||||
<ion-refresher slot="fixed" [disabled]="!discussionLoaded" (ionRefresh)="doRefresh($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="discussionLoaded">
|
||||
<!-- Discussion replies found to be synchronized -->
|
||||
<ion-card class="core-warning-card" *ngIf="postHasOffline || hasOfflineRatings">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: discussionStr} }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<!-- Cut-off date or due date message -->
|
||||
<ion-card class="core-info-card" *ngIf="availabilityMessage">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ availabilityMessage }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<ion-card class="core-info-card" *ngIf="discussion && discussion.locked">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-lock" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'addon.mod_forum.discussionlocked' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<div *ngIf="startingPost" class="ion-margin-bottom">
|
||||
<addon-mod-forum-post [post]="startingPost" [discussion]="discussion" [courseId]="courseId" [highlight]="true"
|
||||
[discussionId]="discussionId" [component]="component" [componentId]="cmId" [formData]="formData"
|
||||
[originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo"
|
||||
[leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
||||
</addon-mod-forum-post>
|
||||
</div>
|
||||
|
||||
<ion-card *ngIf="sort != 'nested'">
|
||||
<ng-container *ngFor="let post of posts; first as first">
|
||||
<core-spacer *ngIf="!first"></core-spacer>
|
||||
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component"
|
||||
[componentId]="cmId" [formData]="formData" [originalData]="originalData" [parentSubject]="postSubjects[post.parentid]"
|
||||
[forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo"
|
||||
[leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
||||
</addon-mod-forum-post>
|
||||
</ng-container>
|
||||
</ion-card>
|
||||
|
||||
<ng-container *ngIf="sort == 'nested'">
|
||||
<ng-container *ngFor="let post of posts">
|
||||
<ng-container *ngTemplateOutlet="nestedPosts; context: {post: post}"></ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #nestedPosts let-post="post">
|
||||
<ion-card>
|
||||
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component"
|
||||
[componentId]="cmId" [formData]="formData" [originalData]="originalData" [parentSubject]="postSubjects[post.parentid]"
|
||||
[forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts" [ratingInfo]="ratingInfo"
|
||||
[leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
||||
</addon-mod-forum-post>
|
||||
<core-loading [hideUntil]="discussionLoaded">
|
||||
<!-- Discussion replies found to be synchronized -->
|
||||
<ion-card class="core-warning-card" *ngIf="postHasOffline || hasOfflineRatings">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: discussionStr} }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
<div class="ion-padding-start" *ngIf="post.children && post.children.length && post.children[0].subject">
|
||||
<ng-container *ngFor="let child of post.children">
|
||||
<ng-container *ngTemplateOutlet="nestedPosts; context: {post: child}"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- Cut-off date or due date message -->
|
||||
<ion-card class="core-info-card" *ngIf="availabilityMessage">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ availabilityMessage }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<ion-card class="core-info-card" *ngIf="discussion && discussion.locked">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-lock" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'addon.mod_forum.discussionlocked' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<div *ngIf="startingPost" class="ion-margin-bottom">
|
||||
<addon-mod-forum-post [post]="startingPost" [discussion]="discussion" [courseId]="courseId" [highlight]="true"
|
||||
[discussionId]="discussionId" [component]="component" [componentId]="cmId" [formData]="formData"
|
||||
[originalData]="originalData" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts"
|
||||
[ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
||||
</addon-mod-forum-post>
|
||||
</div>
|
||||
</ng-template>
|
||||
</core-loading>
|
||||
|
||||
<ion-card *ngIf="sort != 'nested'">
|
||||
<ng-container *ngFor="let post of posts; first as first">
|
||||
<core-spacer *ngIf="!first"></core-spacer>
|
||||
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component"
|
||||
[componentId]="cmId" [formData]="formData" [originalData]="originalData"
|
||||
[parentSubject]="postSubjects[post.parentid]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts"
|
||||
[ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
||||
</addon-mod-forum-post>
|
||||
</ng-container>
|
||||
</ion-card>
|
||||
|
||||
<ng-container *ngIf="sort == 'nested'">
|
||||
<ng-container *ngFor="let post of posts">
|
||||
<ng-container *ngTemplateOutlet="nestedPosts; context: {post: post}"></ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #nestedPosts let-post="post">
|
||||
<ion-card>
|
||||
<addon-mod-forum-post [post]="post" [courseId]="courseId" [discussionId]="discussionId" [component]="component"
|
||||
[componentId]="cmId" [formData]="formData" [originalData]="originalData"
|
||||
[parentSubject]="postSubjects[post.parentid]" [forum]="forum" [accessInfo]="accessInfo" [trackPosts]="trackPosts"
|
||||
[ratingInfo]="ratingInfo" [leavingPage]="leavingPage" (onPostChange)="postListChanged()">
|
||||
</addon-mod-forum-post>
|
||||
</ion-card>
|
||||
<div class="ion-padding-start" *ngIf="post.children && post.children.length && post.children[0].subject">
|
||||
<ng-container *ngFor="let child of post.children">
|
||||
<ng-container *ngTemplateOutlet="nestedPosts; context: {post: child}"></ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
</core-loading>
|
||||
</core-swipe-navigation>
|
||||
</ion-content>
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
import { ContextLevel, CoreConstants } from '@/core/constants';
|
||||
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 { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
|
||||
|
@ -32,6 +34,8 @@ import { Network, NgZone, Translate } from '@singletons';
|
|||
import { CoreArray } from '@singletons/array';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
||||
import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discussions-swipe-manager';
|
||||
import {
|
||||
AddonModForum,
|
||||
AddonModForumAccessInformation,
|
||||
|
@ -68,6 +72,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
|||
forum: Partial<AddonModForumData> = {};
|
||||
accessInfo: AddonModForumAccessInformation = {};
|
||||
discussion?: AddonModForumDiscussion;
|
||||
discussions?: AddonModForumDiscussionDiscussionsSwipeManager;
|
||||
startingPost?: Post;
|
||||
posts!: Post[];
|
||||
discussionLoaded = false;
|
||||
|
@ -117,14 +122,16 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
|||
constructor(
|
||||
@Optional() protected splitView: CoreSplitViewComponent,
|
||||
protected elementRef: ElementRef,
|
||||
protected route: ActivatedRoute,
|
||||
) {}
|
||||
|
||||
get isMobile(): boolean {
|
||||
return CoreScreen.isMobile;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
async ngOnInit(): Promise<void> {
|
||||
try {
|
||||
const routeData = this.route.snapshot.data;
|
||||
this.courseId = CoreNavigator.getRouteNumberParam('courseId');
|
||||
this.cmId = CoreNavigator.getRouteNumberParam('cmId');
|
||||
this.forumId = CoreNavigator.getRouteNumberParam('forumId');
|
||||
|
@ -136,6 +143,16 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
|||
this.postId = CoreNavigator.getRouteNumberParam('postId');
|
||||
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) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
|
||||
|
@ -311,6 +328,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
|
|||
*/
|
||||
ngOnDestroy(): void {
|
||||
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.
|
||||
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,76 +12,77 @@
|
|||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!groupsLoaded" (ionRefresh)="refreshGroups($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="groupsLoaded">
|
||||
<form *ngIf="showForm" #newDiscFormEl>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">{{ 'addon.mod_forum.subject' | translate }}</ion-label>
|
||||
<ion-input [(ngModel)]="newDiscussion.subject" type="text" [placeholder]="'addon.mod_forum.subject' | translate"
|
||||
name="subject">
|
||||
</ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">{{ 'addon.mod_forum.message' | translate }}</ion-label>
|
||||
<core-rich-text-editor name="addon_mod_forum_new_discussion" contextLevel="module" elementId="message"
|
||||
[control]="messageControl" [placeholder]="'addon.mod_forum.message' | translate" [component]="component"
|
||||
[componentId]="forum.cmid" [autoSave]="true" [contextInstanceId]="forum.cmid"
|
||||
(contentChanged)="onMessageChange($event)">
|
||||
</core-rich-text-editor>
|
||||
</ion-item>
|
||||
<ion-item button class="divider ion-text-wrap" (click)="toggleAdvanced()" detail="false" [attr.aria-expanded]="advanced"
|
||||
[attr.aria-label]="(advanced ? 'core.hideadvanced' : 'core.showadvanced') | translate" role="heading"
|
||||
aria-controls="addon-mod-forum-new-discussion-advanced">
|
||||
<ion-icon *ngIf="!advanced" name="fas-caret-right" flip-rtl slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-icon *ngIf="advanced" name="fas-caret-down" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_forum.advanced' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<div *ngIf="advanced" id="addon-mod-forum-new-discussion-advanced">
|
||||
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups">
|
||||
<ion-label>{{ 'addon.mod_forum.posttomygroups' | translate }}</ion-label>
|
||||
<ion-toggle [(ngModel)]="newDiscussion.postToAllGroups" name="postallgroups"></ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="showGroups">
|
||||
<ion-label id="addon-mod-forum-groupslabel">{{ 'addon.mod_forum.group' | translate }}</ion-label>
|
||||
<ion-select [(ngModel)]="newDiscussion.groupId" [disabled]="newDiscussion.postToAllGroups"
|
||||
aria-labelledby="addon-mod-forum-groupslabel" interface="action-sheet" name="groupid"
|
||||
[interfaceOptions]="{header: 'addon.mod_forum.group' | translate}">
|
||||
<ion-select-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-select-option>
|
||||
</ion-select>
|
||||
<core-swipe-navigation [manager]="discussions">
|
||||
<ion-refresher slot="fixed" [disabled]="!groupsLoaded" (ionRefresh)="refreshGroups($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="groupsLoaded">
|
||||
<form *ngIf="showForm" #newDiscFormEl>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">{{ 'addon.mod_forum.subject' | translate }}</ion-label>
|
||||
<ion-input [(ngModel)]="newDiscussion.subject" type="text" [placeholder]="'addon.mod_forum.subject' | translate"
|
||||
name="subject">
|
||||
</ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>{{ 'addon.mod_forum.discussionsubscription' | translate }}</ion-label>
|
||||
<ion-toggle [(ngModel)]="newDiscussion.subscribe" name="subscribe"></ion-toggle>
|
||||
<ion-label position="stacked">{{ 'addon.mod_forum.message' | translate }}</ion-label>
|
||||
<core-rich-text-editor name="addon_mod_forum_new_discussion" contextLevel="module" elementId="message"
|
||||
[control]="messageControl" [placeholder]="'addon.mod_forum.message' | translate" [component]="component"
|
||||
[componentId]="forum.cmid" [autoSave]="true" [contextInstanceId]="forum.cmid"
|
||||
(contentChanged)="onMessageChange($event)">
|
||||
</core-rich-text-editor>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="canPin">
|
||||
<ion-label>{{ 'addon.mod_forum.discussionpinned' | translate }}</ion-label>
|
||||
<ion-toggle [(ngModel)]="newDiscussion.pin" name="pin"></ion-toggle>
|
||||
<ion-item button class="divider ion-text-wrap" (click)="toggleAdvanced()" detail="false" [attr.aria-expanded]="advanced"
|
||||
[attr.aria-label]="(advanced ? 'core.hideadvanced' : 'core.showadvanced') | translate" role="heading"
|
||||
aria-controls="addon-mod-forum-new-discussion-advanced">
|
||||
<ion-icon *ngIf="!advanced" name="fas-caret-right" flip-rtl slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-icon *ngIf="advanced" name="fas-caret-down" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_forum.advanced' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<core-attachments *ngIf="canCreateAttachments && forum && forum.maxattachments > 0" [files]="newDiscussion.files"
|
||||
[maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component" [componentId]="forum.cmid"
|
||||
[allowOffline]="true" [courseId]="courseId">
|
||||
</core-attachments>
|
||||
</div>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<ion-button expand="block" [disabled]="newDiscussion.subject == '' || newDiscussion.message == null"
|
||||
(click)="add()">
|
||||
{{ 'addon.mod_forum.posttoforum' | translate }}
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col *ngIf="hasOffline">
|
||||
<ion-button expand="block" color="light" (click)="discard()">{{ 'core.discard' | translate }}</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</form>
|
||||
</core-loading>
|
||||
<div *ngIf="advanced" id="addon-mod-forum-new-discussion-advanced">
|
||||
<ion-item *ngIf="showGroups && groupIds.length > 1 && accessInfo.cancanposttomygroups">
|
||||
<ion-label>{{ 'addon.mod_forum.posttomygroups' | translate }}</ion-label>
|
||||
<ion-toggle [(ngModel)]="newDiscussion.postToAllGroups" name="postallgroups"></ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="showGroups">
|
||||
<ion-label id="addon-mod-forum-groupslabel">{{ 'addon.mod_forum.group' | translate }}</ion-label>
|
||||
<ion-select [(ngModel)]="newDiscussion.groupId" [disabled]="newDiscussion.postToAllGroups"
|
||||
aria-labelledby="addon-mod-forum-groupslabel" interface="action-sheet" name="groupid"
|
||||
[interfaceOptions]="{header: 'addon.mod_forum.group' | translate}">
|
||||
<ion-select-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>{{ 'addon.mod_forum.discussionsubscription' | translate }}</ion-label>
|
||||
<ion-toggle [(ngModel)]="newDiscussion.subscribe" name="subscribe"></ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="canPin">
|
||||
<ion-label>{{ 'addon.mod_forum.discussionpinned' | translate }}</ion-label>
|
||||
<ion-toggle [(ngModel)]="newDiscussion.pin" name="pin"></ion-toggle>
|
||||
</ion-item>
|
||||
<core-attachments *ngIf="canCreateAttachments && forum && forum.maxattachments > 0" [files]="newDiscussion.files"
|
||||
[maxSize]="forum.maxbytes" [maxSubmissions]="forum.maxattachments" [component]="component"
|
||||
[componentId]="forum.cmid" [allowOffline]="true" [courseId]="courseId">
|
||||
</core-attachments>
|
||||
</div>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<ion-button expand="block" [disabled]="newDiscussion.subject == '' || newDiscussion.message == null"
|
||||
(click)="add()">
|
||||
{{ 'addon.mod_forum.posttoforum' | translate }}
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col *ngIf="hasOffline">
|
||||
<ion-button expand="block" color="light" (click)="discard()">{{ 'core.discard' | translate }}</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</form>
|
||||
</core-loading>
|
||||
</core-swipe-navigation>
|
||||
</ion-content>
|
||||
|
|
|
@ -40,6 +40,10 @@ import { CoreTextUtils } from '@services/utils/text';
|
|||
import { CanLeave } from '@guards/can-leave';
|
||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
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 = {
|
||||
subject: string;
|
||||
|
@ -88,6 +92,8 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
|
|||
accessInfo: AddonModForumAccessInformation = {};
|
||||
courseId!: number;
|
||||
|
||||
discussions?: AddonModForumNewDiscussionDiscussionsSwipeManager;
|
||||
|
||||
protected cmId!: number;
|
||||
protected forumId!: number;
|
||||
protected timeCreated!: number;
|
||||
|
@ -97,17 +103,29 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
|
|||
protected originalData?: Partial<NewDiscussionData>;
|
||||
protected forceLeave = false;
|
||||
|
||||
constructor(@Optional() protected splitView: CoreSplitViewComponent) {}
|
||||
constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
async ngOnInit(): Promise<void> {
|
||||
try {
|
||||
const routeData = this.route.snapshot.data;
|
||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||
this.forumId = CoreNavigator.getRequiredRouteNumberParam('forumId');
|
||||
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) {
|
||||
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">
|
||||
</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-label>
|
||||
<h2>{{ 'addon.mod_glossary.entriestobesynced' | translate }}</h2>
|
||||
|
@ -70,7 +70,7 @@
|
|||
</ion-item>
|
||||
</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">
|
||||
<ion-item-divider *ngIf="getDivider && showDivider(entry, entries.onlineEntries[index - 1])">
|
||||
<ion-label>
|
||||
|
@ -88,11 +88,11 @@
|
|||
</ng-container>
|
||||
</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">
|
||||
</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-loading>
|
||||
|
||||
|
|
|
@ -14,8 +14,9 @@
|
|||
|
||||
import { ContextLevel } from '@/core/constants';
|
||||
import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
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 { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||
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 { Translate } from '@singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import {
|
||||
AddonModGlossaryEntriesSource,
|
||||
AddonModGlossaryEntryItem,
|
||||
AddonModGlossaryFetchMode,
|
||||
} from '../../classes/glossary-entries-source';
|
||||
import {
|
||||
AddonModGlossary,
|
||||
AddonModGlossaryEntry,
|
||||
AddonModGlossaryEntryWithCategory,
|
||||
AddonModGlossaryGetEntriesOptions,
|
||||
AddonModGlossaryGetEntriesWSResponse,
|
||||
AddonModGlossaryGlossary,
|
||||
AddonModGlossaryProvider,
|
||||
} from '../../services/glossary';
|
||||
import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
|
||||
import { AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
|
||||
import {
|
||||
AddonModGlossaryAutoSyncData,
|
||||
AddonModGlossarySyncProvider,
|
||||
|
@ -63,23 +67,17 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
|||
component = AddonModGlossaryProvider.COMPONENT;
|
||||
moduleName = 'glossary';
|
||||
|
||||
isSearch = false;
|
||||
hasSearched = false;
|
||||
canAdd = false;
|
||||
loadMoreError = false;
|
||||
loadingMessage?: string;
|
||||
entries: AddonModGlossaryEntriesManager;
|
||||
loadingMessage: string;
|
||||
entries!: AddonModGlossaryEntriesManager;
|
||||
hasOfflineRatings = false;
|
||||
glossary?: AddonModGlossaryGlossary;
|
||||
|
||||
protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
|
||||
protected fetchFunction?: (options?: AddonModGlossaryGetEntriesOptions) => AddonModGlossaryGetEntriesWSResponse;
|
||||
protected fetchInvalidate?: () => Promise<void>;
|
||||
protected addEntryObserver?: CoreEventObserver;
|
||||
protected fetchMode?: AddonModGlossaryFetchMode;
|
||||
protected viewMode?: string;
|
||||
protected fetchedEntriesCanLoadMore = false;
|
||||
protected fetchedEntries: AddonModGlossaryEntry[] = [];
|
||||
protected sourceUnsubscribe?: () => void;
|
||||
protected ratingOfflineObserver?: CoreEventObserver;
|
||||
protected ratingSyncObserver?: CoreEventObserver;
|
||||
|
||||
|
@ -87,26 +85,47 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
|||
showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false;
|
||||
|
||||
constructor(
|
||||
route: ActivatedRoute,
|
||||
protected route: ActivatedRoute,
|
||||
protected content?: IonContent,
|
||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||
@Optional() protected courseContentsPage?: CoreCourseContentsPage,
|
||||
) {
|
||||
super('AddonModGlossaryIndexComponent', content, courseContentsPage);
|
||||
|
||||
this.entries = new AddonModGlossaryEntriesManager(
|
||||
route.component,
|
||||
this,
|
||||
courseContentsPage ? `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` : '',
|
||||
);
|
||||
this.loadingMessage = Translate.instant('core.loading');
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
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.
|
||||
this.addEntryObserver = CoreEvents.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, (data) => {
|
||||
|
@ -143,11 +162,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
|||
return;
|
||||
}
|
||||
|
||||
this.entries.start(this.splitView);
|
||||
await this.entries.start(this.splitView);
|
||||
|
||||
try {
|
||||
await AddonModGlossary.logView(this.glossary.id, this.viewMode!, this.glossary.name);
|
||||
|
||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||
} catch (error) {
|
||||
// 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> {
|
||||
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.canAdd = !!this.glossary.canaddentry || false;
|
||||
|
||||
this.dataRetrieved.emit(this.glossary);
|
||||
|
||||
if (!this.fetchMode) {
|
||||
if (!this.entries.getSource().fetchMode) {
|
||||
this.switchMode('letter_all');
|
||||
}
|
||||
|
||||
|
@ -177,7 +198,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
|||
|
||||
const [hasOfflineRatings] = await Promise.all([
|
||||
CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule),
|
||||
this.fetchEntries(),
|
||||
refresh ? this.entries.reload() : this.entries.load(),
|
||||
]);
|
||||
|
||||
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
|
||||
*/
|
||||
protected async invalidateContent(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
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);
|
||||
await this.entries.getSource().invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -277,109 +250,50 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
|||
* @param mode New mode.
|
||||
*/
|
||||
protected switchMode(mode: AddonModGlossaryFetchMode): void {
|
||||
this.fetchMode = mode;
|
||||
this.isSearch = false;
|
||||
this.entries.getSource().switchMode(mode);
|
||||
|
||||
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',
|
||||
);
|
||||
this.getDivider = (entry) => entry.userfullname;
|
||||
this.showDivider = (entry, previous) => !previous || entry.userid != previous.userid;
|
||||
break;
|
||||
|
||||
case 'cat_all':
|
||||
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,
|
||||
);
|
||||
this.getDivider = (entry: AddonModGlossaryEntryWithCategory) => entry.categoryname || '';
|
||||
this.showDivider = (entry, previous) => !previous || this.getDivider!(entry) != this.getDivider!(previous);
|
||||
const getDivider = (entry: AddonModGlossaryEntryWithCategory) => entry.categoryname || '';
|
||||
|
||||
this.getDivider = getDivider;
|
||||
this.showDivider = (entry, previous) => !previous || getDivider(entry) != getDivider(previous);
|
||||
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',
|
||||
);
|
||||
this.getDivider = undefined;
|
||||
this.showDivider = () => false;
|
||||
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',
|
||||
);
|
||||
this.getDivider = undefined;
|
||||
this.showDivider = () => false;
|
||||
break;
|
||||
|
||||
case 'letter_all':
|
||||
default:
|
||||
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',
|
||||
);
|
||||
this.getDivider = (entry) => {
|
||||
const getDivider = (entry) => {
|
||||
// Try to get the first letter without HTML tags.
|
||||
const noTags = CoreTextUtils.cleanTags(entry.concept);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -391,7 +305,9 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
|||
*/
|
||||
async loadMoreEntries(infiniteComplete?: () => void): Promise<void> {
|
||||
try {
|
||||
await this.fetchEntries(true);
|
||||
this.loadMoreError = false;
|
||||
|
||||
await this.entries.load();
|
||||
} catch (error) {
|
||||
this.loadMoreError = true;
|
||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
|
||||
|
@ -406,21 +322,34 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
|||
* @param event Event.
|
||||
*/
|
||||
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,
|
||||
componentProps: {
|
||||
browseModes: this.glossary!.browsemodes,
|
||||
selectedMode: this.isSearch ? '' : this.fetchMode,
|
||||
browseModes: this.glossary.browsemodes,
|
||||
selectedMode: this.isSearch ? '' : previousMode,
|
||||
},
|
||||
event,
|
||||
});
|
||||
|
||||
if (mode) {
|
||||
if (mode !== this.fetchMode) {
|
||||
this.changeFetchMode(mode);
|
||||
} else if (this.isSearch) {
|
||||
this.toggleSearch();
|
||||
}
|
||||
if (!newMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMode !== previousMode) {
|
||||
this.changeFetchMode(newMode);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isSearch) {
|
||||
this.toggleSearch();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -429,20 +358,22 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
|||
*/
|
||||
toggleSearch(): void {
|
||||
if (this.isSearch) {
|
||||
this.isSearch = false;
|
||||
this.hasSearched = false;
|
||||
this.entries.setOnlineEntries(this.fetchedEntries, this.fetchedEntriesCanLoadMore);
|
||||
this.switchMode(this.fetchMode!);
|
||||
} else {
|
||||
// Search for entries. The fetch function will be set when searching.
|
||||
this.getDivider = undefined;
|
||||
this.showDivider = () => false;
|
||||
this.isSearch = true;
|
||||
const fetchMode = this.entries.getSource().fetchMode;
|
||||
|
||||
this.fetchedEntries = this.entries.onlineEntries;
|
||||
this.fetchedEntriesCanLoadMore = !this.entries.completed;
|
||||
this.entries.setItems([], false);
|
||||
fetchMode && this.switchMode(fetchMode);
|
||||
this.entries.getSource().stopSearch(this.fetchedEntries, this.fetchedEntriesCanLoadMore);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.showDivider = () => false;
|
||||
|
||||
this.entries.reset();
|
||||
this.entries.getSource().startSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -451,7 +382,6 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
|||
* @param mode Mode.
|
||||
*/
|
||||
changeFetchMode(mode: AddonModGlossaryFetchMode): void {
|
||||
this.isSearch = false;
|
||||
this.loadingMessage = Translate.instant('core.loading');
|
||||
this.content?.scrollToTop();
|
||||
this.switchMode(mode);
|
||||
|
@ -463,7 +393,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
|||
* Opens new entry editor.
|
||||
*/
|
||||
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 {
|
||||
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.hasSearched = true;
|
||||
|
||||
this.entries.getSource().search(query);
|
||||
this.loadContent();
|
||||
}
|
||||
|
||||
|
@ -503,154 +418,44 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
|||
this.addEntryObserver?.off();
|
||||
this.ratingOfflineObserver?.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.
|
||||
*/
|
||||
class AddonModGlossaryEntriesManager extends CorePageItemsListManager<EntryItem> {
|
||||
class AddonModGlossaryEntriesManager extends CoreListItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> {
|
||||
|
||||
onlineEntries: AddonModGlossaryEntry[] = [];
|
||||
offlineEntries: AddonModGlossaryOfflineEntry[] = [];
|
||||
|
||||
protected glossaryPathPrefix: string;
|
||||
protected component: AddonModGlossaryIndexComponent;
|
||||
|
||||
constructor(
|
||||
pageComponent: unknown,
|
||||
component: AddonModGlossaryIndexComponent,
|
||||
glossaryPathPrefix: string,
|
||||
) {
|
||||
super(pageComponent);
|
||||
|
||||
this.component = component;
|
||||
this.glossaryPathPrefix = glossaryPathPrefix;
|
||||
get offlineEntries(): AddonModGlossaryOfflineEntry[] {
|
||||
return this.getSource().offlineEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to infer NewEntryForm objects.
|
||||
*
|
||||
* @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);
|
||||
get onlineEntries(): AddonModGlossaryEntry[] {
|
||||
return this.getSource().onlineEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
setItems(entries: EntryItem[], hasMoreItems: boolean = false): void {
|
||||
super.setItems(entries, hasMoreItems);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
protected getDefaultItem(): AddonModGlossaryEntryItem | null {
|
||||
return this.getSource().onlineEntries[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
resetItems(): void {
|
||||
super.resetItems();
|
||||
this.onlineEntries = [];
|
||||
this.offlineEntries = [];
|
||||
}
|
||||
protected async logActivity(): Promise<void> {
|
||||
const glossary = this.getSource().glossary;
|
||||
const viewMode = this.getSource().viewMode;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected getItemPath(entry: EntryItem): string {
|
||||
if (this.isOnlineEntry(entry)) {
|
||||
return `${this.glossaryPathPrefix}entry/${entry.id}`;
|
||||
if (!glossary || !viewMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
await AddonModGlossary.logView(glossary.id, viewMode, glossary.name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 { PopoverController } from '@singletons';
|
||||
import { AddonModGlossaryFetchMode } from '../index';
|
||||
import { AddonModGlossaryFetchMode } from '../../classes/glossary-entries-source';
|
||||
|
||||
/**
|
||||
* Component to display the mode picker.
|
||||
|
|
|
@ -51,10 +51,12 @@ const mainMenuRoutes: Routes = [
|
|||
{
|
||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
||||
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
||||
data: { swipeEnabled: false },
|
||||
},
|
||||
{
|
||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
||||
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
||||
data: { swipeEnabled: false },
|
||||
},
|
||||
{
|
||||
path: AddonModGlossaryModuleHandlerService.PAGE_NAME,
|
||||
|
@ -65,10 +67,12 @@ const mainMenuRoutes: Routes = [
|
|||
{
|
||||
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
||||
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
||||
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||
},
|
||||
{
|
||||
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
||||
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
||||
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||
},
|
||||
],
|
||||
() => CoreScreen.isMobile,
|
||||
|
@ -80,10 +84,12 @@ const courseContentsRoutes: Routes = conditionalRoutes(
|
|||
{
|
||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
||||
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
|
||||
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||
},
|
||||
{
|
||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
||||
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
|
||||
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||
},
|
||||
],
|
||||
() => CoreScreen.isTablet,
|
||||
|
|
|
@ -12,72 +12,75 @@
|
|||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<form #editFormEl *ngIf="glossary">
|
||||
<ion-item>
|
||||
<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>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
|
||||
<core-rich-text-editor [control]="definitionControl" (contentChanged)="onDefinitionChange($event)"
|
||||
[placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit" [component]="component"
|
||||
[componentId]="cmId" [autoSave]="true" contextLevel="module" [contextInstanceId]="cmId" elementId="definition_editor"
|
||||
[draftExtraParams]="editorExtraParams">
|
||||
</core-rich-text-editor>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="categories.length > 0">
|
||||
<ion-label position="stacked" id="addon-mod-glossary-categories-label">
|
||||
{{ 'addon.mod_glossary.categories' | translate }}
|
||||
</ion-label>
|
||||
<ion-select [(ngModel)]="options.categories" multiple="true" aria-labelledby="addon-mod-glossary-categories-label"
|
||||
interface="action-sheet" [placeholder]="'addon.mod_glossary.categories' | translate" name="categories"
|
||||
[interfaceOptions]="{header: 'addon.mod_glossary.categories' | translate}">
|
||||
<ion-select-option *ngFor="let category of categories" [value]="category.id">
|
||||
{{ category.name }}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked" id="addon-mod-glossary-aliases-label">
|
||||
{{ 'addon.mod_glossary.aliases' | translate }}
|
||||
</ion-label>
|
||||
<ion-textarea [(ngModel)]="options.aliases" rows="1" [core-auto-rows]="options.aliases"
|
||||
aria-labelledby="addon-mod-glossary-aliases-label" name="aliases">
|
||||
</ion-textarea>
|
||||
</ion-item>
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_glossary.attachment' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<core-attachments [files]="attachments" [component]="component" [componentId]="glossary.coursemodule" [allowOffline]="true"
|
||||
[courseId]="courseId">
|
||||
</core-attachments>
|
||||
<ng-container *ngIf="glossary.usedynalink">
|
||||
<core-swipe-navigation [manager]="entries">
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<form #editFormEl *ngIf="glossary">
|
||||
<ion-item>
|
||||
<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>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
|
||||
<core-rich-text-editor [control]="definitionControl" (contentChanged)="onDefinitionChange($event)"
|
||||
[placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit" [component]="component"
|
||||
[componentId]="cmId" [autoSave]="true" contextLevel="module" [contextInstanceId]="cmId"
|
||||
elementId="definition_editor" [draftExtraParams]="editorExtraParams">
|
||||
</core-rich-text-editor>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="categories.length > 0">
|
||||
<ion-label position="stacked" id="addon-mod-glossary-categories-label">
|
||||
{{ 'addon.mod_glossary.categories' | translate }}
|
||||
</ion-label>
|
||||
<ion-select [(ngModel)]="options.categories" multiple="true" aria-labelledby="addon-mod-glossary-categories-label"
|
||||
interface="action-sheet" [placeholder]="'addon.mod_glossary.categories' | translate" name="categories"
|
||||
[interfaceOptions]="{header: 'addon.mod_glossary.categories' | translate}">
|
||||
<ion-select-option *ngFor="let category of categories" [value]="category.id">
|
||||
{{ category.name }}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked" id="addon-mod-glossary-aliases-label">
|
||||
{{ 'addon.mod_glossary.aliases' | translate }}
|
||||
</ion-label>
|
||||
<ion-textarea [(ngModel)]="options.aliases" rows="1" [core-auto-rows]="options.aliases"
|
||||
aria-labelledby="addon-mod-glossary-aliases-label" name="aliases">
|
||||
</ion-textarea>
|
||||
</ion-item>
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_glossary.linking' | translate }}</h2>
|
||||
<h2>{{ 'addon.mod_glossary.attachment' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>
|
||||
<ion-toggle [(ngModel)]="options.usedynalink" name="usedynalink"></ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label>
|
||||
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.casesensitive" name="casesensitive">
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>{{ 'addon.mod_glossary.fullmatch' | translate }}</ion-label>
|
||||
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.fullmatch" name="fullmatch"></ion-toggle>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ion-button class="ion-margin" expand="block" [disabled]="!entry.concept || !entry.definition" (click)="save()">
|
||||
{{ 'core.save' | translate }}
|
||||
</ion-button>
|
||||
</form>
|
||||
</core-loading>
|
||||
<core-attachments [files]="attachments" [component]="component" [componentId]="glossary.coursemodule" [allowOffline]="true"
|
||||
[courseId]="courseId">
|
||||
</core-attachments>
|
||||
<ng-container *ngIf="glossary.usedynalink">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_glossary.linking' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>
|
||||
<ion-toggle [(ngModel)]="options.usedynalink" name="usedynalink"></ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label>
|
||||
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.casesensitive" name="casesensitive">
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>{{ 'addon.mod_glossary.fullmatch' | translate }}</ion-label>
|
||||
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.fullmatch" name="fullmatch"></ion-toggle>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ion-button class="ion-margin" expand="block" [disabled]="!entry.concept || !entry.definition" (click)="save()">
|
||||
{{ 'core.save' | translate }}
|
||||
</ion-button>
|
||||
</form>
|
||||
</core-loading>
|
||||
</core-swipe-navigation>
|
||||
</ion-content>
|
||||
|
|
|
@ -12,9 +12,11 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// 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 { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||
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 { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||
import { CanLeave } from '@guards/can-leave';
|
||||
|
@ -26,6 +28,8 @@ import { CoreTextUtils } from '@services/utils/text';
|
|||
import { Translate } from '@singletons';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreForms } from '@singletons/form';
|
||||
import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source';
|
||||
import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager';
|
||||
import {
|
||||
AddonModGlossary,
|
||||
AddonModGlossaryCategory,
|
||||
|
@ -45,7 +49,7 @@ import { AddonModGlossaryOffline } from '../../services/glossary-offline';
|
|||
selector: 'page-addon-mod-glossary-edit',
|
||||
templateUrl: 'edit.html',
|
||||
})
|
||||
export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||
export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
||||
|
||||
@ViewChild('editFormEl') formElement?: ElementRef;
|
||||
|
||||
|
@ -64,6 +68,8 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
timecreated: 0,
|
||||
};
|
||||
|
||||
entries?: AddonModGlossaryEditEntriesSwipeManager;
|
||||
|
||||
options = {
|
||||
categories: <string[]> [],
|
||||
aliases: '',
|
||||
|
@ -80,18 +86,30 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
protected originalData?: AddonModGlossaryNewEntryWithFiles;
|
||||
protected saved = false;
|
||||
|
||||
constructor(@Optional() protected splitView: CoreSplitViewComponent) {}
|
||||
constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
async ngOnInit(): Promise<void> {
|
||||
try {
|
||||
const routeData = this.route.snapshot.data;
|
||||
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||
this.timecreated = CoreNavigator.getRequiredRouteNumberParam('timecreated');
|
||||
this.concept = CoreNavigator.getRouteParam<string>('concept') || '';
|
||||
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) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
|
||||
|
@ -103,6 +121,13 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
this.fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.entries?.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch required data.
|
||||
*
|
||||
|
@ -134,7 +159,11 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
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.definition = entry.definition || '';
|
||||
|
@ -159,7 +188,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
|
||||
// Treat offline attachments if any.
|
||||
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();
|
||||
}
|
||||
|
@ -236,6 +265,10 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
definition = CoreTextUtils.formatHtmlLines(definition);
|
||||
|
||||
try {
|
||||
if (!this.glossary) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload attachments first if any.
|
||||
const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated);
|
||||
|
||||
|
@ -244,7 +277,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
categories: this.options.categories.join(','),
|
||||
};
|
||||
|
||||
if (this.glossary!.usedynalink) {
|
||||
if (this.glossary.usedynalink) {
|
||||
options.usedynalink = this.options.usedynalink ? 1 : 0;
|
||||
if (this.options.usedynalink) {
|
||||
options.casesensitive = this.options.casesensitive ? 1 : 0;
|
||||
|
@ -253,9 +286,9 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
}
|
||||
|
||||
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.
|
||||
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,
|
||||
cmId: this.cmId,
|
||||
});
|
||||
|
@ -268,7 +301,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
|
||||
// Save entry in offline.
|
||||
await AddonModGlossaryOffline.addNewEntry(
|
||||
this.glossary!.id,
|
||||
this.glossary.id,
|
||||
this.entry.concept,
|
||||
definition,
|
||||
this.courseId,
|
||||
|
@ -283,7 +316,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
// Try to send it to server.
|
||||
// Don't allow offline if there are attachments since they were uploaded fine.
|
||||
await AddonModGlossary.addEntry(
|
||||
this.glossary!.id,
|
||||
this.glossary.id,
|
||||
this.entry.concept,
|
||||
definition,
|
||||
this.courseId,
|
||||
|
@ -293,7 +326,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
timeCreated: timecreated,
|
||||
discardEntry: this.entry,
|
||||
allowOffline: !this.attachments.length,
|
||||
checkDuplicates: !this.glossary!.allowduplicatedentries,
|
||||
checkDuplicates: !this.glossary.allowduplicatedentries,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -303,12 +336,12 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
|
||||
if (entryId) {
|
||||
// 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(AddonModGlossaryProvider.ADD_ENTRY_EVENT, {
|
||||
glossaryId: this.glossary!.id,
|
||||
glossaryId: this.glossary.id,
|
||||
entryId: entryId,
|
||||
}, CoreSites.getCurrentSiteId());
|
||||
|
||||
|
@ -342,7 +375,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
protected async uploadAttachments(
|
||||
timecreated: number,
|
||||
): Promise<{saveOffline: boolean; attachmentsResult?: number | CoreFileUploaderStoreFilesResult}> {
|
||||
if (!this.attachments.length) {
|
||||
if (!this.attachments.length || !this.glossary) {
|
||||
return {
|
||||
saveOffline: false,
|
||||
};
|
||||
|
@ -352,7 +385,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles(
|
||||
this.attachments,
|
||||
AddonModGlossaryProvider.COMPONENT,
|
||||
this.glossary!.id,
|
||||
this.glossary.id,
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -362,7 +395,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
|||
} catch {
|
||||
// Cannot upload them in online, save them in offline.
|
||||
const attachmentsResult = await AddonModGlossaryHelper.storeFiles(
|
||||
this.glossary!.id,
|
||||
this.glossary.id,
|
||||
this.entry.concept,
|
||||
timecreated,
|
||||
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,73 +12,75 @@
|
|||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-swipe-navigation [manager]="entries">
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ng-container *ngIf="entry && loaded">
|
||||
<ion-item class="ion-text-wrap" *ngIf="showAuthor">
|
||||
<core-user-avatar [user]="entry" slot="start"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId"
|
||||
[courseId]="courseId">
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ng-container *ngIf="entry && loaded">
|
||||
<ion-item class="ion-text-wrap" *ngIf="showAuthor">
|
||||
<core-user-avatar [user]="entry" slot="start"></core-user-avatar>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId"
|
||||
[courseId]="courseId">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
<p>{{ entry.userfullname }}</p>
|
||||
</ion-label>
|
||||
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="!showAuthor">
|
||||
<ion-label>
|
||||
<p class="item-heading">
|
||||
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition"
|
||||
contextLevel="module" [contextInstanceId]="componentId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
<p>{{ entry.userfullname }}</p>
|
||||
</ion-label>
|
||||
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="!showAuthor">
|
||||
<ion-label>
|
||||
<p class="item-heading">
|
||||
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition" contextLevel="module"
|
||||
[contextInstanceId]="componentId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<div *ngIf="entry.attachment" lines="none">
|
||||
<core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId">
|
||||
</core-file>
|
||||
</div>
|
||||
<ion-item class="ion-text-wrap" *ngIf="tagsEnabled && entry && entry.tags && entry.tags.length > 0">
|
||||
<ion-label>
|
||||
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
|
||||
<core-tag-list [tags]="entry.tags"></core-tag-list>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="!entry.approved">
|
||||
<ion-label>
|
||||
<p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<core-comments *ngIf="glossary && glossary.allowcomments && entry && entry.id > 0 && commentsEnabled" contextLevel="module"
|
||||
[instanceId]="glossary.coursemodule" component="mod_glossary" [itemId]="entry.id" area="glossary_entry"
|
||||
[courseId]="glossary.course" [showItem]="true">
|
||||
</core-comments>
|
||||
<core-rating-rate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
|
||||
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [itemSetId]="0" [courseId]="glossary.course"
|
||||
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" [userId]="entry.userid" (onUpdate)="ratingUpdated()">
|
||||
</core-rating-rate>
|
||||
<core-rating-aggregate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
|
||||
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [courseId]="glossary.course" [aggregateMethod]="glossary.assessed"
|
||||
[scaleId]="glossary.scale">
|
||||
</core-rating-aggregate>
|
||||
</ng-container>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<div *ngIf="entry.attachment" lines="none">
|
||||
<core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId">
|
||||
</core-file>
|
||||
</div>
|
||||
<ion-item class="ion-text-wrap" *ngIf="tagsEnabled && entry && entry.tags && entry.tags.length > 0">
|
||||
<ion-label>
|
||||
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
|
||||
<core-tag-list [tags]="entry.tags"></core-tag-list>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="!entry.approved">
|
||||
<ion-label>
|
||||
<p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<core-comments *ngIf="glossary && glossary.allowcomments && entry && entry.id > 0 && commentsEnabled" contextLevel="module"
|
||||
[instanceId]="glossary.coursemodule" component="mod_glossary" [itemId]="entry.id" area="glossary_entry"
|
||||
[courseId]="glossary.course" [showItem]="true">
|
||||
</core-comments>
|
||||
<core-rating-rate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
|
||||
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [itemSetId]="0" [courseId]="glossary.course"
|
||||
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" [userId]="entry.userid" (onUpdate)="ratingUpdated()">
|
||||
</core-rating-rate>
|
||||
<core-rating-aggregate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
|
||||
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [courseId]="glossary.course"
|
||||
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale">
|
||||
</core-rating-aggregate>
|
||||
</ng-container>
|
||||
|
||||
<ion-card *ngIf="!entry" class="core-warning-card">
|
||||
<ion-item>
|
||||
<ion-label>{{ 'addon.mod_glossary.errorloadingentry' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
</core-loading>
|
||||
<ion-card *ngIf="!entry" class="core-warning-card">
|
||||
<ion-item>
|
||||
<ion-label>{{ 'addon.mod_glossary.errorloadingentry' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
</core-loading>
|
||||
</core-swipe-navigation>
|
||||
</ion-content>
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// 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 { CoreComments } from '@features/comments/services/comments';
|
||||
import { CoreRatingInfo } from '@features/rating/services/rating';
|
||||
|
@ -21,6 +23,8 @@ import { IonRefresher } from '@ionic/angular';
|
|||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source';
|
||||
import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager';
|
||||
import {
|
||||
AddonModGlossary,
|
||||
AddonModGlossaryEntry,
|
||||
|
@ -35,13 +39,14 @@ import {
|
|||
selector: 'page-addon-mod-glossary-entry',
|
||||
templateUrl: 'entry.html',
|
||||
})
|
||||
export class AddonModGlossaryEntryPage implements OnInit {
|
||||
export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
||||
|
||||
@ViewChild(CoreCommentsCommentsComponent) comments?: CoreCommentsCommentsComponent;
|
||||
|
||||
component = AddonModGlossaryProvider.COMPONENT;
|
||||
componentId?: number;
|
||||
entry?: AddonModGlossaryEntry;
|
||||
entries?: AddonModGlossaryEntryEntriesSwipeManager;
|
||||
glossary?: AddonModGlossaryGlossary;
|
||||
loaded = false;
|
||||
showAuthor = false;
|
||||
|
@ -53,15 +58,30 @@ export class AddonModGlossaryEntryPage implements OnInit {
|
|||
|
||||
protected entryId!: number;
|
||||
|
||||
constructor(protected route: ActivatedRoute) {}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
try {
|
||||
const routeData = this.route.snapshot.data;
|
||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||
this.entryId = CoreNavigator.getRequiredRouteNumberParam('entryId');
|
||||
this.tagsEnabled = CoreTag.areTagsAvailableInSite();
|
||||
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) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
|
||||
|
@ -73,16 +93,23 @@ export class AddonModGlossaryEntryPage implements OnInit {
|
|||
try {
|
||||
await this.fetchEntry();
|
||||
|
||||
if (!this.glossary) {
|
||||
if (!this.glossary || !this.componentId) {
|
||||
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 {
|
||||
this.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.entries?.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
// limitations under the License.
|
||||
|
||||
import { Params } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Updates listener.
|
||||
*/
|
||||
export interface CoreItemsListSourceListener<Item> {
|
||||
onItemsUpdated(items: Item[], hasMoreItems: boolean): void;
|
||||
onReset(): void;
|
||||
onItemsUpdated?(items: Item[], hasMoreItems: boolean): void;
|
||||
onReset?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,9 +37,10 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
|||
return args.map(argument => String(argument)).join('-');
|
||||
}
|
||||
|
||||
private items: Item[] | null = null;
|
||||
private hasMoreItems = true;
|
||||
private listeners: CoreItemsListSourceListener<Item>[] = [];
|
||||
protected items: Item[] | null = null;
|
||||
protected hasMoreItems = true;
|
||||
protected listeners: CoreItemsListSourceListener<Item>[] = [];
|
||||
protected dirty = false;
|
||||
|
||||
/**
|
||||
* Check whether any page has been loaded.
|
||||
|
@ -57,6 +60,17 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
|||
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.
|
||||
*
|
||||
|
@ -76,7 +90,12 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
|||
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 {
|
||||
this.items = null;
|
||||
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> {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @param page Page number (starting at 0).
|
||||
* @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.
|
||||
*
|
||||
* @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.
|
||||
|
@ -163,7 +214,7 @@ export abstract class CoreItemsManagerSource<Item = unknown> {
|
|||
this.items = items;
|
||||
this.hasMoreItems = hasMoreItems;
|
||||
|
||||
this.listeners.forEach(listener => listener.onItemsUpdated(items, hasMoreItems));
|
||||
this.listeners.forEach(listener => listener.onItemsUpdated?.call(listener, items, hasMoreItems));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ type SourceConstructor<T extends CoreItemsManagerSource = CoreItemsManagerSource
|
|||
getSourceId(...args: unknown[]): string;
|
||||
new (...args: unknown[]): T;
|
||||
};
|
||||
type SourceConstuctorInstance<T> = T extends { new(...args: unknown[]): infer P } ? P : never;
|
||||
type InstanceTracking = { instance: CoreItemsManagerSource; references: unknown[] };
|
||||
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.
|
||||
* @returns Source.
|
||||
*/
|
||||
static getOrCreateSource<T extends CoreItemsManagerSource>(
|
||||
constructor: SourceConstructor<T>,
|
||||
constructorArguments: ConstructorParameters<SourceConstructor<T>>,
|
||||
): T {
|
||||
static getOrCreateSource<T extends CoreItemsManagerSource, C extends SourceConstructor<T>>(
|
||||
constructor: C,
|
||||
constructorArguments: ConstructorParameters<C>,
|
||||
): SourceConstuctorInstance<C> {
|
||||
const id = constructor.getSourceId(...constructorArguments);
|
||||
const constructorInstances = this.getConstructorInstances(constructor);
|
||||
|
||||
return constructorInstances[id]?.instance as T
|
||||
return constructorInstances[id]?.instance as SourceConstuctorInstance<C>
|
||||
?? this.createInstance(id, constructor, constructorArguments);
|
||||
}
|
||||
|
||||
|
@ -57,7 +58,7 @@ export class CoreItemsManagerSourcesTracker {
|
|||
const constructorInstances = this.getConstructorInstances(source.constructor as SourceConstructor);
|
||||
const instanceId = this.instanceIds.get(source);
|
||||
|
||||
if (!instanceId) {
|
||||
if (instanceId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -82,7 +83,7 @@ export class CoreItemsManagerSourcesTracker {
|
|||
const instanceId = this.instanceIds.get(source);
|
||||
const index = constructorInstances?.[instanceId ?? '']?.references.indexOf(reference) ?? -1;
|
||||
|
||||
if (!constructorInstances || !instanceId || index === -1) {
|
||||
if (!constructorInstances || instanceId === undefined || index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot, Params } from '@angular/router';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||
|
||||
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.
|
||||
*/
|
||||
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 selectedItem: Item | null = null;
|
||||
|
||||
constructor(source: CoreItemsManagerSource<Item>) {
|
||||
constructor(source: Source) {
|
||||
this.setSource(source);
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ export abstract class CoreItemsManager<Item = unknown> {
|
|||
*
|
||||
* @returns Source.
|
||||
*/
|
||||
getSource(): CoreItemsManagerSource<Item> {
|
||||
getSource(): Source {
|
||||
if (!this.source) {
|
||||
throw new Error('Source is missing from items manager');
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ export abstract class CoreItemsManager<Item = unknown> {
|
|||
*
|
||||
* @param newSource New source.
|
||||
*/
|
||||
setSource(newSource: CoreItemsManagerSource<Item> | null): void {
|
||||
setSource(newSource: Source | null): void {
|
||||
if (this.source) {
|
||||
CoreItemsManagerSourcesTracker.removeReference(this.source.instance, this);
|
||||
|
||||
|
@ -92,31 +92,26 @@ export abstract class CoreItemsManager<Item = unknown> {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
* @param route Page 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.
|
||||
* @return Query parameters to use when navigating to the item page.
|
||||
* @param route Page route, if any.
|
||||
* @return Path of the selected item.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected getItemQueryParams(item: Item): Params {
|
||||
return {};
|
||||
protected getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null {
|
||||
if (!route) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getSelectedItemPathFromRoute(route);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -152,7 +147,7 @@ export abstract class CoreItemsManager<Item = unknown> {
|
|||
}
|
||||
|
||||
// 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);
|
||||
|
||||
if (selectedItemPath === itemPath) {
|
||||
|
@ -160,7 +155,7 @@ export abstract class CoreItemsManager<Item = unknown> {
|
|||
}
|
||||
|
||||
// Navigate to item.
|
||||
const params = this.getItemQueryParams(item);
|
||||
const params = this.getSource().getItemQueryParams(item);
|
||||
const pathPrefix = selectedItemPath ? selectedItemPath.split('/').fill('../').join('') : '';
|
||||
|
||||
await CoreNavigator.navigate(pathPrefix + itemPath, { params, ...options });
|
||||
|
@ -173,7 +168,7 @@ export abstract class CoreItemsManager<Item = unknown> {
|
|||
*/
|
||||
protected onSourceItemsUpdated(items: Item[]): void {
|
||||
this.itemsMap = items.reduce((map, item) => {
|
||||
map[this.getItemPath(item)] = item;
|
||||
map[this.getSource().getItemPath(item)] = item;
|
||||
|
||||
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.
|
||||
*/
|
||||
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 splitView?: CoreSplitViewComponent;
|
||||
protected splitViewOutletSubscription?: Subscription;
|
||||
|
||||
constructor(source: CoreItemsManagerSource<Item>, pageRouteLocator: unknown | ActivatedRoute) {
|
||||
constructor(source: Source, pageRouteLocator: unknown | ActivatedRoute) {
|
||||
super(source);
|
||||
|
||||
this.pageRouteLocator = pageRouteLocator;
|
||||
|
@ -67,15 +70,6 @@ export abstract class CoreListItemsManager<Item = unknown> extends CoreItemsMana
|
|||
// Calculate current selected item.
|
||||
this.updateSelectedItem();
|
||||
|
||||
// Select default item if none is selected on a non-mobile layout.
|
||||
if (!CoreScreen.isMobile && this.selectedItem === null && !splitView.isNested) {
|
||||
const defaultItem = this.getDefaultItem();
|
||||
|
||||
if (defaultItem) {
|
||||
this.select(defaultItem);
|
||||
}
|
||||
}
|
||||
|
||||
// Log activity.
|
||||
await CoreUtils.ignoreErrors(this.logActivity());
|
||||
}
|
||||
|
@ -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> {
|
||||
await this.getSource().loadNextPage();
|
||||
async load(): Promise<void> {
|
||||
await this.getSource().load();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -172,6 +166,25 @@ export abstract class CoreListItemsManager<Item = unknown> extends CoreItemsMana
|
|||
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.
|
||||
*/
|
||||
|
@ -193,10 +206,12 @@ export abstract class CoreListItemsManager<Item = unknown> extends CoreItemsMana
|
|||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null {
|
||||
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||
const segments: UrlSegment[] = [];
|
||||
|
||||
while ((route = route?.firstChild)) {
|
||||
while (route.firstChild) {
|
||||
route = route.firstChild;
|
||||
|
||||
segments.push(...route.url);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,16 +12,21 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot, UrlSegment } from '@angular/router';
|
||||
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
|
||||
import { CoreItemsManager } from './items-manager';
|
||||
import { CoreItemsManagerSource } from './items-manager-source';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -51,6 +56,25 @@ export abstract class CoreSwipeItemsManager<Item = unknown> extends CoreItemsMan
|
|||
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.
|
||||
*
|
||||
|
@ -86,7 +110,7 @@ export abstract class CoreSwipeItemsManager<Item = unknown> extends CoreItemsMan
|
|||
const item = items?.[index + delta] ?? null;
|
||||
|
||||
if (!item && !this.getSource().isCompleted()) {
|
||||
await this.getSource().loadNextPage();
|
||||
await this.getSource().load();
|
||||
|
||||
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>
|
||||
<ng-content></ng-content>
|
||||
</ion-slide>
|
||||
|
|
|
@ -5,3 +5,15 @@ ion-slides {
|
|||
ion-slide {
|
||||
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 { CoreSwipeItemsManager } from '@classes/items-management/swipe-items-manager';
|
||||
import { CoreScreen } from '@services/screen';
|
||||
|
||||
@Component({
|
||||
selector: 'core-swipe-navigation',
|
||||
|
@ -24,10 +25,18 @@ export class CoreSwipeNavigationComponent {
|
|||
|
||||
@Input() manager?: CoreSwipeItemsManager;
|
||||
|
||||
get enabled(): boolean {
|
||||
return CoreScreen.isMobile && !!this.manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swipe to previous item.
|
||||
*/
|
||||
swipeLeft(): void {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.manager?.navigateToPreviousItem();
|
||||
}
|
||||
|
||||
|
@ -35,6 +44,10 @@ export class CoreSwipeNavigationComponent {
|
|||
* Swipe to next item.
|
||||
*/
|
||||
swipeRight(): void {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.manager?.navigateToNextItem();
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
// 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, CoreUserData, CoreUserParticipant, CoreUserProvider } from '../services/user';
|
||||
|
@ -40,6 +41,20 @@ export class CoreUserParticipantsSource extends CoreItemsManagerSource<CoreUserP
|
|||
this.SEARCH_QUERY = searchQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getItemPath(user: CoreUserParticipant | CoreUserData): string {
|
||||
return user.id.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getItemQueryParams(): Params {
|
||||
return { search: this.SEARCH_QUERY };
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
|
||||
import { CoreApp } from '@services/app';
|
||||
|
@ -50,7 +49,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
|||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||
this.participants = new CoreUserParticipantsManager(
|
||||
CoreItemsManagerSourcesTracker.getOrCreateSource(CoreUserParticipantsSource, [this.courseId]),
|
||||
this,
|
||||
CoreUserParticipantsPage,
|
||||
);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
|
@ -186,7 +185,7 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
|||
private async fetchParticipants(reload: boolean): Promise<void> {
|
||||
reload
|
||||
? await this.participants.reload()
|
||||
: await this.participants.loadNextPage();
|
||||
: await this.participants.load();
|
||||
|
||||
this.fetchMoreParticipantsFailed = false;
|
||||
}
|
||||
|
@ -196,35 +195,13 @@ export class CoreUserParticipantsPage implements OnInit, AfterViewInit, OnDestro
|
|||
/**
|
||||
* Helper to manage the list of participants.
|
||||
*/
|
||||
class CoreUserParticipantsManager extends CoreListItemsManager<CoreUserParticipant | CoreUserData> {
|
||||
|
||||
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 };
|
||||
}
|
||||
class CoreUserParticipantsManager extends CoreListItemsManager<CoreUserParticipant | CoreUserData, CoreUserParticipantsSource> {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
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
|
||||
// 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 { IonRefresher } from '@ionic/angular';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
@ -21,7 +21,7 @@ import { CoreSite } from '@classes/site';
|
|||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
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 { CoreUserDelegate, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
|
||||
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 { CoreUserParticipantsSource } from '@features/user/classes/participants-source';
|
||||
import { CoreItemsManagerSourcesTracker } from '@classes/items-management/items-manager-sources-tracker';
|
||||
import { CoreItemsManagerSource } from '@classes/items-management/items-manager-source';
|
||||
|
||||
@Component({
|
||||
selector: 'page-core-user-profile',
|
||||
|
@ -57,7 +56,6 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
|
|||
communicationHandlers: CoreUserProfileHandlerData[] = [];
|
||||
|
||||
users?: CoreUserSwipeItemsManager;
|
||||
usersQueryParams: Params = {};
|
||||
|
||||
constructor(private route: ActivatedRoute) {
|
||||
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') {
|
||||
const search = CoreNavigator.getRouteParam('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();
|
||||
}
|
||||
|
||||
|
@ -227,38 +224,12 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
|
|||
/**
|
||||
* Helper to manage swiping within a collection of users.
|
||||
*/
|
||||
class CoreUserSwipeItemsManager extends CoreSwipeItemsManager<CoreUserBasicData> {
|
||||
|
||||
page: CoreUserProfilePage;
|
||||
|
||||
constructor(source: CoreItemsManagerSource<CoreUserBasicData>, page: CoreUserProfilePage) {
|
||||
super(source);
|
||||
|
||||
this.page = page;
|
||||
}
|
||||
class CoreUserSwipeItemsManager extends CoreSwipeItemsManager {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected getItemPath(item: CoreUserBasicData): string {
|
||||
return String(item.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected getItemQueryParams(): Params {
|
||||
return this.page.usersQueryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected getSelectedItemPath(route?: ActivatedRouteSnapshot | null): string | null {
|
||||
if (!route) {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||
return route.params.userId;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,9 +19,6 @@
|
|||
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 {
|
||||
if (courseId && userId) {
|
||||
const cacheKey = this.getCacheKey(courseId, userId);
|
||||
|
||||
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 {
|
||||
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.
|
||||
* @return Value of the parameter, undefined if not found.
|
||||
*/
|
||||
getRouteParam<T = unknown>(name: string, routeOptions: CoreNavigatorCurrentRouteOptions = {}): T | undefined {
|
||||
getRouteParam<T = string>(name: string, routeOptions: CoreNavigatorCurrentRouteOptions = {}): T | undefined {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let value: any;
|
||||
|
||||
|
|
Loading…
Reference in New Issue