forked from EVOgeek/Vmeda.Online
Merge pull request #3019 from NoelDeMartin/MOBILE-3939
MOBILE-3939: Add swipe navigation to feedback attemptsmain
commit
11d219ddce
|
@ -559,6 +559,7 @@
|
||||||
"addon.mod_feedback.analysis": "feedback",
|
"addon.mod_feedback.analysis": "feedback",
|
||||||
"addon.mod_feedback.anonymous": "feedback",
|
"addon.mod_feedback.anonymous": "feedback",
|
||||||
"addon.mod_feedback.anonymous_entries": "feedback",
|
"addon.mod_feedback.anonymous_entries": "feedback",
|
||||||
|
"addon.mod_feedback.anonymous_user": "feedback",
|
||||||
"addon.mod_feedback.average": "feedback",
|
"addon.mod_feedback.average": "feedback",
|
||||||
"addon.mod_feedback.captchaofflinewarning": "local_moodlemobileapp",
|
"addon.mod_feedback.captchaofflinewarning": "local_moodlemobileapp",
|
||||||
"addon.mod_feedback.complete_the_form": "feedback",
|
"addon.mod_feedback.complete_the_form": "feedback",
|
||||||
|
|
|
@ -43,29 +43,30 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
|
||||||
user?: CoreUserProfile;
|
user?: CoreUserProfile;
|
||||||
course?: CoreEnrolledCourseData;
|
course?: CoreEnrolledCourseData;
|
||||||
badge?: AddonBadgesUserBadge;
|
badge?: AddonBadgesUserBadge;
|
||||||
badges?: CoreSwipeNavigationItemsManager;
|
badges: CoreSwipeNavigationItemsManager;
|
||||||
badgeLoaded = false;
|
badgeLoaded = false;
|
||||||
currentTime = 0;
|
currentTime = 0;
|
||||||
|
|
||||||
constructor(protected route: ActivatedRoute) { }
|
constructor(protected route: ActivatedRoute) {
|
||||||
|
|
||||||
/**
|
|
||||||
* View loaded.
|
|
||||||
*/
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.courseId = CoreNavigator.getRouteNumberParam('courseId') || this.courseId; // Use 0 for site badges.
|
this.courseId = CoreNavigator.getRouteNumberParam('courseId') || this.courseId; // Use 0 for site badges.
|
||||||
this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getRequiredCurrentSite().getUserId();
|
this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getRequiredCurrentSite().getUserId();
|
||||||
this.badgeHash = CoreNavigator.getRouteParam('badgeHash') || '';
|
this.badgeHash = CoreNavigator.getRouteParam('badgeHash') || '';
|
||||||
|
|
||||||
this.fetchIssuedBadge().finally(() => {
|
|
||||||
this.badgeLoaded = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
AddonBadgesUserBadgesSource,
|
AddonBadgesUserBadgesSource,
|
||||||
[this.courseId, this.userId],
|
[this.courseId, this.userId],
|
||||||
);
|
);
|
||||||
|
|
||||||
this.badges = new CoreSwipeNavigationItemsManager(source);
|
this.badges = new CoreSwipeNavigationItemsManager(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View loaded.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.fetchIssuedBadge().finally(() => {
|
||||||
|
this.badgeLoaded = true;
|
||||||
|
});
|
||||||
|
|
||||||
this.badges.start();
|
this.badges.start();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source';
|
||||||
|
import { CoreGroupInfo, CoreGroups } from '@services/groups';
|
||||||
|
import {
|
||||||
|
AddonModFeedback,
|
||||||
|
AddonModFeedbackProvider,
|
||||||
|
AddonModFeedbackWSAnonAttempt,
|
||||||
|
AddonModFeedbackWSAttempt,
|
||||||
|
AddonModFeedbackWSFeedback,
|
||||||
|
} from '../services/feedback';
|
||||||
|
import { AddonModFeedbackHelper } from '../services/feedback-helper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feedback attempts.
|
||||||
|
*/
|
||||||
|
export class AddonModFeedbackAttemptsSource extends CoreRoutedItemsManagerSource<AddonModFeedbackAttemptItem> {
|
||||||
|
|
||||||
|
readonly COURSE_ID: number;
|
||||||
|
readonly CM_ID: number;
|
||||||
|
|
||||||
|
selectedGroup?: number;
|
||||||
|
identifiable?: AddonModFeedbackWSAttempt[];
|
||||||
|
identifiableTotal?: number;
|
||||||
|
anonymous?: AddonModFeedbackWSAnonAttempt[];
|
||||||
|
anonymousTotal?: number;
|
||||||
|
groupInfo?: CoreGroupInfo;
|
||||||
|
|
||||||
|
protected feedback?: AddonModFeedbackWSFeedback;
|
||||||
|
|
||||||
|
constructor(courseId: number, cmId: number) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.COURSE_ID = courseId;
|
||||||
|
this.CM_ID = cmId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getItemPath(attempt: AddonModFeedbackAttemptItem): string {
|
||||||
|
return attempt.id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getPagesLoaded(): number {
|
||||||
|
if (!this.identifiable || !this.anonymous) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageLength = this.getPageLength();
|
||||||
|
|
||||||
|
return Math.ceil(Math.max(this.anonymous.length, this.identifiable.length) / pageLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to infer AddonModFeedbackWSAttempt objects.
|
||||||
|
*
|
||||||
|
* @param discussion Item to check.
|
||||||
|
* @return Whether the item is an identifieable attempt.
|
||||||
|
*/
|
||||||
|
isIdentifiableAttempt(attempt: AddonModFeedbackAttemptItem): attempt is AddonModFeedbackWSAttempt {
|
||||||
|
return 'fullname' in attempt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to infer AddonModFeedbackWSAnonAttempt objects.
|
||||||
|
*
|
||||||
|
* @param discussion Item to check.
|
||||||
|
* @return Whether the item is an anonymous attempt.
|
||||||
|
*/
|
||||||
|
isAnonymousAttempt(attempt: AddonModFeedbackAttemptItem): attempt is AddonModFeedbackWSAnonAttempt {
|
||||||
|
return 'number' in attempt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate feedback cache.
|
||||||
|
*/
|
||||||
|
async invalidateCache(): Promise<void> {
|
||||||
|
await Promise.all([
|
||||||
|
CoreGroups.invalidateActivityGroupInfo(this.CM_ID),
|
||||||
|
this.feedback && AddonModFeedback.invalidateResponsesAnalysisData(this.feedback.id),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load feedback.
|
||||||
|
*/
|
||||||
|
async loadFeedback(): Promise<void> {
|
||||||
|
this.feedback = await AddonModFeedback.getFeedback(this.COURSE_ID, this.CM_ID);
|
||||||
|
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.CM_ID);
|
||||||
|
|
||||||
|
this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getPageLength(): number {
|
||||||
|
return AddonModFeedbackProvider.PER_PAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async loadPageItems(page: number): Promise<{ items: AddonModFeedbackAttemptItem[]; hasMoreItems: boolean }> {
|
||||||
|
if (!this.feedback) {
|
||||||
|
throw new Error('Can\'t load attempts without feeback');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await AddonModFeedbackHelper.getResponsesAnalysis(this.feedback.id, {
|
||||||
|
page,
|
||||||
|
groupId: this.selectedGroup,
|
||||||
|
cmId: this.CM_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (page === 0) {
|
||||||
|
this.identifiableTotal = result.totalattempts;
|
||||||
|
this.anonymousTotal = result.totalanonattempts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalItemsLoaded = this.getPageLength() * (page + 1);
|
||||||
|
const pageAttempts: AddonModFeedbackAttemptItem[] = [
|
||||||
|
...result.attempts,
|
||||||
|
...result.anonattempts,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: pageAttempts,
|
||||||
|
hasMoreItems: result.totalattempts > totalItemsLoaded || result.totalanonattempts > totalItemsLoaded,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected setItems(attempts: AddonModFeedbackAttemptItem[], hasMoreItems: boolean): void {
|
||||||
|
this.identifiable = attempts.filter(this.isIdentifiableAttempt);
|
||||||
|
this.anonymous = attempts.filter(this.isAnonymousAttempt);
|
||||||
|
|
||||||
|
super.setItems((this.identifiable as AddonModFeedbackAttemptItem[]).concat(this.anonymous), hasMoreItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of items that can be held in the source.
|
||||||
|
*/
|
||||||
|
export type AddonModFeedbackAttemptItem = AddonModFeedbackWSAttempt | AddonModFeedbackWSAnonAttempt;
|
|
@ -72,7 +72,7 @@
|
||||||
</ion-select-option>
|
</ion-select-option>
|
||||||
</ion-select>
|
</ion-select>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item class="ion-text-wrap" (click)="openRespondents()" [class.hide-detail]="!(access.canviewreports && completedCount > 0)"
|
<ion-item class="ion-text-wrap" (click)="openAttempts()" [class.hide-detail]="!(access.canviewreports && completedCount > 0)"
|
||||||
detail="true" [button]="access.canviewreports && completedCount > 0">
|
detail="true" [button]="access.canviewreports && completedCount > 0">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ 'addon.mod_feedback.completed_feedbacks' | translate }}</h2>
|
<h2>{{ 'addon.mod_feedback.completed_feedbacks' | translate }}</h2>
|
||||||
|
|
|
@ -400,15 +400,15 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open respondents page.
|
* Open attempts page.
|
||||||
*/
|
*/
|
||||||
openRespondents(): void {
|
openAttempts(): void {
|
||||||
if (!this.access!.canviewreports || this.completedCount <= 0) {
|
if (!this.access!.canviewreports || this.completedCount <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CoreNavigator.navigateToSitePath(
|
CoreNavigator.navigateToSitePath(
|
||||||
AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/respondents`,
|
AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/attempts`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
group: this.group,
|
group: this.group,
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { RouterModule, Routes } from '@angular/router';
|
||||||
import { CoreSharedModule } from '@/core/shared.module';
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
import { AddonModFeedbackComponentsModule } from './components/components.module';
|
import { AddonModFeedbackComponentsModule } from './components/components.module';
|
||||||
import { AddonModFeedbackIndexPage } from './pages/index/index';
|
import { AddonModFeedbackIndexPage } from './pages/index/index';
|
||||||
import { AddonModFeedbackRespondentsPage } from './pages/respondents/respondents';
|
import { AddonModFeedbackAttemptsPage } from './pages/attempts/attempts';
|
||||||
import { conditionalRoutes } from '@/app/app-routing.module';
|
import { conditionalRoutes } from '@/app/app-routing.module';
|
||||||
import { CoreScreen } from '@services/screen';
|
import { CoreScreen } from '@services/screen';
|
||||||
|
|
||||||
|
@ -40,11 +40,11 @@ const commonRoutes: Routes = [
|
||||||
const mobileRoutes: Routes = [
|
const mobileRoutes: Routes = [
|
||||||
...commonRoutes,
|
...commonRoutes,
|
||||||
{
|
{
|
||||||
path: ':courseId/:cmId/respondents',
|
path: ':courseId/:cmId/attempts',
|
||||||
component: AddonModFeedbackRespondentsPage,
|
component: AddonModFeedbackAttemptsPage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':courseId/:cmId/respondents/attempt/:attemptId',
|
path: ':courseId/:cmId/attempts/:attemptId',
|
||||||
loadChildren: () => import('./pages/attempt/attempt.module').then(m => m.AddonModFeedbackAttemptPageModule),
|
loadChildren: () => import('./pages/attempt/attempt.module').then(m => m.AddonModFeedbackAttemptPageModule),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -52,11 +52,11 @@ const mobileRoutes: Routes = [
|
||||||
const tabletRoutes: Routes = [
|
const tabletRoutes: Routes = [
|
||||||
...commonRoutes,
|
...commonRoutes,
|
||||||
{
|
{
|
||||||
path: ':courseId/:cmId/respondents',
|
path: ':courseId/:cmId/attempts',
|
||||||
component: AddonModFeedbackRespondentsPage,
|
component: AddonModFeedbackAttemptsPage,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'attempt/:attemptId',
|
path: ':attemptId',
|
||||||
loadChildren: () => import('./pages/attempt/attempt.module').then(m => m.AddonModFeedbackAttemptPageModule),
|
loadChildren: () => import('./pages/attempt/attempt.module').then(m => m.AddonModFeedbackAttemptPageModule),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -76,7 +76,7 @@ const routes: Routes = [
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AddonModFeedbackIndexPage,
|
AddonModFeedbackIndexPage,
|
||||||
AddonModFeedbackRespondentsPage,
|
AddonModFeedbackAttemptsPage,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AddonModFeedbackLazyModule {}
|
export class AddonModFeedbackLazyModule {}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"analysis": "Analysis",
|
"analysis": "Analysis",
|
||||||
"anonymous": "Anonymous",
|
"anonymous": "Anonymous",
|
||||||
"anonymous_entries": "Anonymous entries ({{$a}})",
|
"anonymous_entries": "Anonymous entries ({{$a}})",
|
||||||
|
"anonymous_user": "Anonymous user",
|
||||||
"average": "Average",
|
"average": "Average",
|
||||||
"captchaofflinewarning": "Feedback with CAPTCHA cannot be completed offline, or if not configured, or if the server is down.",
|
"captchaofflinewarning": "Feedback with CAPTCHA cannot be completed offline, or if not configured, or if the server is down.",
|
||||||
"complete_the_form": "Answer the questions",
|
"complete_the_form": "Answer the questions",
|
||||||
|
|
|
@ -5,17 +5,16 @@
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
<ion-title>
|
<ion-title>
|
||||||
<h1 *ngIf="attempt">{{ attempt.fullname }}</h1>
|
<h1 *ngIf="attempt">{{ attempt.fullname }}</h1>
|
||||||
<h1 *ngIf="anonAttempt">
|
<h1 *ngIf="anonAttempt">{{ 'addon.mod_feedback.anonymous_user' | translate }}</h1>
|
||||||
{{ 'addon.mod_feedback.response_nr' |translate }}: {{anonAttempt.number}}
|
|
||||||
</h1>
|
|
||||||
</ion-title>
|
</ion-title>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
|
<core-swipe-navigation [manager]="attempts">
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-loading [hideUntil]="loaded">
|
||||||
<ion-list class="ion-no-margin" *ngIf="attempt || anonAttempt">
|
<ion-list class="ion-no-margin" *ngIf="attempt || anonAttempt">
|
||||||
<ion-item *ngIf="attempt" class="ion-text-wrap" core-user-link [userId]="attempt.userid"
|
<ion-item *ngIf="attempt" class="ion-text-wrap" core-user-link [userId]="attempt.userid"
|
||||||
[attr.aria-label]=" 'core.user.viewprofile' | translate" [courseId]="attempt.courseid">
|
[attr.aria-label]="'core.user.viewprofile' | translate" [courseId]="attempt.courseid">
|
||||||
<core-user-avatar [user]="attempt" slot="start"></core-user-avatar>
|
<core-user-avatar [user]="attempt" slot="start"></core-user-avatar>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{attempt.fullname}}</h2>
|
<h2>{{attempt.fullname}}</h2>
|
||||||
|
@ -24,11 +23,10 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="anonAttempt">
|
<ion-item class="ion-text-wrap" *ngIf="anonAttempt">
|
||||||
|
<core-user-avatar [linkProfile]="false" slot="start"></core-user-avatar>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>
|
<h2>{{ 'addon.mod_feedback.anonymous_user' |translate }}</h2>
|
||||||
{{ 'addon.mod_feedback.response_nr' |translate }}: {{anonAttempt.number}}
|
<p>{{ 'addon.mod_feedback.response_nr' | translate }}: {{anonAttempt.number}}</p>
|
||||||
({{ 'addon.mod_feedback.anonymous' |translate }})
|
|
||||||
</h2>
|
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ng-container *ngIf="items && items.length">
|
<ng-container *ngIf="items && items.length">
|
||||||
|
@ -53,4 +51,5 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
</core-swipe-navigation>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -12,10 +12,14 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot } from '@angular/router';
|
||||||
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
|
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { AddonModFeedbackAttemptsSource } from '../../classes/feedback-attempts-source';
|
||||||
import {
|
import {
|
||||||
AddonModFeedback,
|
AddonModFeedback,
|
||||||
AddonModFeedbackProvider,
|
AddonModFeedbackProvider,
|
||||||
|
@ -32,27 +36,39 @@ import { AddonModFeedbackFormItem, AddonModFeedbackHelper } from '../../services
|
||||||
selector: 'page-addon-mod-feedback-attempt',
|
selector: 'page-addon-mod-feedback-attempt',
|
||||||
templateUrl: 'attempt.html',
|
templateUrl: 'attempt.html',
|
||||||
})
|
})
|
||||||
export class AddonModFeedbackAttemptPage implements OnInit {
|
export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
protected attemptId!: number;
|
cmId: number;
|
||||||
|
courseId: number;
|
||||||
cmId!: number;
|
|
||||||
courseId!: number;
|
|
||||||
feedback?: AddonModFeedbackWSFeedback;
|
feedback?: AddonModFeedbackWSFeedback;
|
||||||
attempt?: AddonModFeedbackWSAttempt;
|
attempt?: AddonModFeedbackWSAttempt;
|
||||||
|
attempts: AddonModFeedbackAttemptsSwipeManager;
|
||||||
anonAttempt?: AddonModFeedbackWSAnonAttempt;
|
anonAttempt?: AddonModFeedbackWSAnonAttempt;
|
||||||
items: AddonModFeedbackAttemptItem[] = [];
|
items: AddonModFeedbackAttemptItem[] = [];
|
||||||
component = AddonModFeedbackProvider.COMPONENT;
|
component = AddonModFeedbackProvider.COMPONENT;
|
||||||
loaded = false;
|
loaded = false;
|
||||||
|
|
||||||
|
protected attemptId: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
|
this.attemptId = CoreNavigator.getRequiredRouteNumberParam('attemptId');
|
||||||
|
|
||||||
|
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
AddonModFeedbackAttemptsSource,
|
||||||
|
[this.courseId, this.cmId],
|
||||||
|
);
|
||||||
|
|
||||||
|
this.attempts = new AddonModFeedbackAttemptsSwipeManager(source);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
try {
|
try {
|
||||||
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
this.attempts.start();
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
|
||||||
this.attemptId = CoreNavigator.getRequiredRouteNumberParam('attemptId');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
@ -64,6 +80,13 @@ export class AddonModFeedbackAttemptPage implements OnInit {
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.attempts.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all the data required for the view.
|
* Fetch all the data required for the view.
|
||||||
*
|
*
|
||||||
|
@ -131,3 +154,17 @@ export class AddonModFeedbackAttemptPage implements OnInit {
|
||||||
type AddonModFeedbackAttemptItem = AddonModFeedbackFormItem & {
|
type AddonModFeedbackAttemptItem = AddonModFeedbackFormItem & {
|
||||||
submittedValue?: string;
|
submittedValue?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage swiping within a collection of discussions.
|
||||||
|
*/
|
||||||
|
class AddonModFeedbackAttemptsSwipeManager extends CoreSwipeNavigationItemsManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||||
|
return route.params.attemptId;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>
|
||||||
|
<h1>{{ 'addon.mod_feedback.responses' |translate }}</h1>
|
||||||
|
</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<core-split-view>
|
||||||
|
<ion-refresher slot="fixed" [disabled]="!attempts || !attempts.loaded" (ionRefresh)="refreshFeedback($event.target)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<core-loading [hideUntil]="attempts && attempts.loaded">
|
||||||
|
<ion-list class="ion-no-margin">
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
|
||||||
|
<ion-label id="addon-feedback-groupslabel">
|
||||||
|
<ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container>
|
||||||
|
<ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container>
|
||||||
|
</ion-label>
|
||||||
|
<ion-select [(ngModel)]="selectedGroup" (ionChange)="reloadAttempts()" aria-labelledby="addon-feedback-groupslabel"
|
||||||
|
interface="action-sheet" [interfaceOptions]="{header: 'core.group' | translate}">
|
||||||
|
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
|
||||||
|
{{groupOpt.name}}
|
||||||
|
</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ng-container *ngIf="identifiableAttemptsTotal > 0">
|
||||||
|
<ion-item-divider>
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ 'addon.mod_feedback.non_anonymous_entries' | translate : {$a: identifiableAttemptsTotal } }}</h2>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item-divider>
|
||||||
|
<ion-item *ngFor="let attempt of identifiableAttempts" class="ion-text-wrap" button detail="true"
|
||||||
|
(click)="attempts?.select(attempt)" [attr.aria-current]="attempts?.getItemAriaCurrent(attempt)">
|
||||||
|
<core-user-avatar [user]="attempt" [linkProfile]="false" slot="start"></core-user-avatar>
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ attempt.fullname }}</p>
|
||||||
|
<p *ngIf="attempt.timemodified">{{ attempt.timemodified * 1000 | coreFormatDate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="identifiableAttemptsTotal === identifiableAttempts.length && anonymousAttemptsTotal > 0">
|
||||||
|
<ion-item-divider>
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ 'addon.mod_feedback.anonymous_entries' | translate : {$a: anonymousAttemptsTotal } }}</h2>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item-divider>
|
||||||
|
<ion-item *ngFor="let attempt of anonymousAttempts" class="ion-text-wrap" button detail="true"
|
||||||
|
(click)="attempts?.select(attempt)" [attr.aria-current]="attempts?.getItemAriaCurrent(attempt)">
|
||||||
|
<core-user-avatar [linkProfile]="false" slot="start"></core-user-avatar>
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ 'addon.mod_feedback.anonymous_user' | translate }}</p>
|
||||||
|
<p>{{ 'addon.mod_feedback.response_nr' | translate }}: {{attempt.number}}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<core-infinite-loading [enabled]="attempts && attempts.loaded && !attempts.completed" [error]="fetchFailed"
|
||||||
|
(action)="fetchMoreAttempts($event)">
|
||||||
|
</core-infinite-loading>
|
||||||
|
</ion-list>
|
||||||
|
</core-loading>
|
||||||
|
</core-split-view>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,183 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
|
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||||
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
import { CoreGroupInfo } from '@services/groups';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource } from '../../classes/feedback-attempts-source';
|
||||||
|
import { AddonModFeedbackWSAnonAttempt, AddonModFeedbackWSAttempt } from '../../services/feedback';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays feedback attempts.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-feedback-attempts',
|
||||||
|
templateUrl: 'attempts.html',
|
||||||
|
})
|
||||||
|
export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
|
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||||
|
|
||||||
|
promisedAttempts: CorePromisedValue<CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource>>;
|
||||||
|
fetchFailed = false;
|
||||||
|
|
||||||
|
constructor(protected route: ActivatedRoute) {
|
||||||
|
this.promisedAttempts = new CorePromisedValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
get attempts(): CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource> | null {
|
||||||
|
return this.promisedAttempts.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get groupInfo(): CoreGroupInfo | undefined {
|
||||||
|
return this.attempts?.getSource().groupInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedGroup(): number | undefined {
|
||||||
|
return this.attempts?.getSource().selectedGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
set selectedGroup(group: number | undefined) {
|
||||||
|
if (!this.attempts) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.attempts.getSource().selectedGroup = group;
|
||||||
|
this.attempts.getSource().setDirty(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
get identifiableAttempts(): AddonModFeedbackWSAttempt[] {
|
||||||
|
return this.attempts?.getSource().identifiable ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get identifiableAttemptsTotal(): number {
|
||||||
|
return this.attempts?.getSource().identifiableTotal ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get anonymousAttempts(): AddonModFeedbackWSAnonAttempt[] {
|
||||||
|
return this.attempts?.getSource().anonymous ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get anonymousAttemptsTotal(): number {
|
||||||
|
return this.attempts?.getSource().anonymousTotal ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngAfterViewInit(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
|
const courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
|
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
|
AddonModFeedbackAttemptsSource,
|
||||||
|
[courseId, cmId],
|
||||||
|
);
|
||||||
|
|
||||||
|
source.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0;
|
||||||
|
|
||||||
|
this.promisedAttempts.resolve(new CoreListItemsManager(source, this.route.component));
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
|
CoreNavigator.back();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attempts = await this.promisedAttempts;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.fetchFailed = false;
|
||||||
|
|
||||||
|
await attempts.getSource().loadFeedback();
|
||||||
|
await attempts.load();
|
||||||
|
} catch (error) {
|
||||||
|
this.fetchFailed = true;
|
||||||
|
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
await attempts.start(this.splitView);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.attempts?.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch more attempts, if any.
|
||||||
|
*
|
||||||
|
* @param infiniteComplete Complete callback for infinite loader.
|
||||||
|
*/
|
||||||
|
async fetchMoreAttempts(infiniteComplete?: () => void): Promise<void> {
|
||||||
|
const attempts = await this.promisedAttempts;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.fetchFailed = false;
|
||||||
|
|
||||||
|
await attempts.load();
|
||||||
|
} catch (error) {
|
||||||
|
this.fetchFailed = true;
|
||||||
|
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
||||||
|
} finally {
|
||||||
|
infiniteComplete && infiniteComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the attempts.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
*/
|
||||||
|
async refreshFeedback(refresher: IonRefresher): Promise<void> {
|
||||||
|
const attempts = await this.promisedAttempts;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.fetchFailed = false;
|
||||||
|
|
||||||
|
await CoreUtils.ignoreErrors(attempts.getSource().invalidateCache());
|
||||||
|
await attempts.getSource().loadFeedback();
|
||||||
|
await attempts.reload();
|
||||||
|
} catch (error) {
|
||||||
|
this.fetchFailed = true;
|
||||||
|
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
||||||
|
} finally {
|
||||||
|
refresher.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload attempts list.
|
||||||
|
*/
|
||||||
|
async reloadAttempts(): Promise<void> {
|
||||||
|
const attempts = await this.promisedAttempts;
|
||||||
|
|
||||||
|
await attempts.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,86 +0,0 @@
|
||||||
<ion-header>
|
|
||||||
<ion-toolbar>
|
|
||||||
<ion-buttons slot="start">
|
|
||||||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
|
||||||
</ion-buttons>
|
|
||||||
<ion-title>
|
|
||||||
<h1>{{ 'addon.mod_feedback.responses' |translate }}</h1>
|
|
||||||
</ion-title>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
<ion-content>
|
|
||||||
<core-split-view>
|
|
||||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshFeedback($event.target)">
|
|
||||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
|
||||||
</ion-refresher>
|
|
||||||
<core-loading [hideUntil]="loaded">
|
|
||||||
<ion-list class="ion-no-margin">
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
|
|
||||||
<ion-label id="addon-feedback-groupslabel">
|
|
||||||
<ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container>
|
|
||||||
<ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container>
|
|
||||||
</ion-label>
|
|
||||||
<ion-select [(ngModel)]="selectedGroup" (ionChange)="loadAttempts(selectedGroup)"
|
|
||||||
aria-labelledby="addon-feedback-groupslabel" interface="action-sheet"
|
|
||||||
[interfaceOptions]="{header: 'core.group' | translate}">
|
|
||||||
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
|
|
||||||
{{groupOpt.name}}
|
|
||||||
</ion-select-option>
|
|
||||||
</ion-select>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ng-container *ngIf="responses.responses.total > 0">
|
|
||||||
<ion-item-divider>
|
|
||||||
<ion-label>
|
|
||||||
<h2>{{ 'addon.mod_feedback.non_anonymous_entries' | translate : {$a: responses.responses.total } }}</h2>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item-divider>
|
|
||||||
<ion-item *ngFor="let attempt of responses.responses.attempts" class="ion-text-wrap" button detail="true"
|
|
||||||
(click)="responses.select(attempt)" [attr.aria-current]="responses.getItemAriaCurrent(attempt)">
|
|
||||||
<core-user-avatar [user]="attempt" slot="start"></core-user-avatar>
|
|
||||||
<ion-label>
|
|
||||||
<p class="item-heading">{{ attempt.fullname }}</p>
|
|
||||||
<p *ngIf="attempt.timemodified">{{attempt.timemodified * 1000 | coreFormatDate }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<!-- Button and spinner to show more attempts. -->
|
|
||||||
<ion-button *ngIf="responses.responses.canLoadMore && !loadingMore" class="ion-margin" expand="block"
|
|
||||||
(click)="loadAttempts()">
|
|
||||||
{{ 'core.loadmore' | translate }}
|
|
||||||
</ion-button>
|
|
||||||
<ion-item *ngIf="responses.responses.canLoadMore && loadingMore" class="ion-text-center">
|
|
||||||
<ion-label>
|
|
||||||
<ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="responses.anonResponses.total > 0">
|
|
||||||
<ion-item-divider>
|
|
||||||
<ion-label>
|
|
||||||
<h2>{{ 'addon.mod_feedback.anonymous_entries' |translate : {$a: responses.anonResponses.total } }}</h2>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item-divider>
|
|
||||||
<ion-item *ngFor="let attempt of responses.anonResponses.attempts" class="ion-text-wrap" button detail="true"
|
|
||||||
(click)="responses.select(attempt)" [attr.aria-current]="responses.getItemAriaCurrent(attempt)">
|
|
||||||
<ion-label>
|
|
||||||
<h2>{{ 'addon.mod_feedback.response_nr' |translate }}: {{attempt.number}}</h2>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<!-- Button and spinner to show more attempts. -->
|
|
||||||
<ion-button *ngIf="responses.anonResponses.canLoadMore && !loadingMore" class="ion-margin" expand="block"
|
|
||||||
(click)="loadAttempts()">
|
|
||||||
{{ 'core.loadmore' | translate }}
|
|
||||||
</ion-button>
|
|
||||||
<ion-item *ngIf="responses.anonResponses.canLoadMore && loadingMore" class="ion-text-center">
|
|
||||||
<ion-label>
|
|
||||||
<ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ng-container>
|
|
||||||
</ion-list>
|
|
||||||
</core-loading>
|
|
||||||
</core-split-view>
|
|
||||||
</ion-content>
|
|
|
@ -1,256 +0,0 @@
|
||||||
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import { AfterViewInit, Component, ViewChild } from '@angular/core';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
|
||||||
import { IonRefresher } from '@ionic/angular';
|
|
||||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
|
|
||||||
import { CoreNavigator } from '@services/navigator';
|
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
|
||||||
import {
|
|
||||||
AddonModFeedback,
|
|
||||||
AddonModFeedbackWSAnonAttempt,
|
|
||||||
AddonModFeedbackWSAttempt,
|
|
||||||
AddonModFeedbackWSFeedback,
|
|
||||||
} from '../../services/feedback';
|
|
||||||
import { AddonModFeedbackHelper, AddonModFeedbackResponsesAnalysis } from '../../services/feedback-helper';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page that displays feedback respondents.
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'page-addon-mod-feedback-respondents',
|
|
||||||
templateUrl: 'respondents.html',
|
|
||||||
})
|
|
||||||
export class AddonModFeedbackRespondentsPage implements AfterViewInit {
|
|
||||||
|
|
||||||
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
|
||||||
|
|
||||||
protected cmId!: number;
|
|
||||||
protected courseId!: number;
|
|
||||||
protected page = 0;
|
|
||||||
protected feedback?: AddonModFeedbackWSFeedback;
|
|
||||||
|
|
||||||
responses: AddonModFeedbackResponsesManager;
|
|
||||||
selectedGroup!: number;
|
|
||||||
groupInfo?: CoreGroupInfo;
|
|
||||||
loaded = false;
|
|
||||||
loadingMore = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
route: ActivatedRoute,
|
|
||||||
) {
|
|
||||||
this.responses = new AddonModFeedbackResponsesManager(
|
|
||||||
route.component,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
async ngAfterViewInit(): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
|
||||||
this.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0;
|
|
||||||
} catch (error) {
|
|
||||||
CoreDomUtils.showErrorModal(error);
|
|
||||||
|
|
||||||
CoreNavigator.back();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.fetchData();
|
|
||||||
|
|
||||||
this.responses.start(this.splitView);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch all the data required for the view.
|
|
||||||
*
|
|
||||||
* @param refresh Empty events array first.
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
async fetchData(refresh: boolean = false): Promise<void> {
|
|
||||||
this.page = 0;
|
|
||||||
this.responses.resetItems();
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.cmId);
|
|
||||||
|
|
||||||
this.groupInfo = await CoreGroups.getActivityGroupInfo(this.cmId);
|
|
||||||
|
|
||||||
this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
|
|
||||||
|
|
||||||
await this.loadGroupAttempts(this.selectedGroup);
|
|
||||||
} catch (error) {
|
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
|
||||||
|
|
||||||
if (!refresh) {
|
|
||||||
// Some call failed on first fetch, go back.
|
|
||||||
CoreNavigator.back();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load Group attempts.
|
|
||||||
*
|
|
||||||
* @param groupId If defined it will change group if not, it will load more attempts for the same group.
|
|
||||||
* @return Resolved with the attempts loaded.
|
|
||||||
*/
|
|
||||||
protected async loadGroupAttempts(groupId?: number): Promise<void> {
|
|
||||||
if (groupId === undefined) {
|
|
||||||
this.page++;
|
|
||||||
this.loadingMore = true;
|
|
||||||
} else {
|
|
||||||
this.selectedGroup = groupId;
|
|
||||||
this.page = 0;
|
|
||||||
this.responses.resetItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const responses = await AddonModFeedbackHelper.getResponsesAnalysis(this.feedback!.id, {
|
|
||||||
groupId: this.selectedGroup,
|
|
||||||
page: this.page,
|
|
||||||
cmId: this.cmId,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.responses.setResponses(responses);
|
|
||||||
} finally {
|
|
||||||
this.loadingMore = false;
|
|
||||||
this.loaded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change selected group or load more attempts.
|
|
||||||
*
|
|
||||||
* @param groupId Group ID selected. If not defined, it will load more attempts.
|
|
||||||
*/
|
|
||||||
async loadAttempts(groupId?: number): Promise<void> {
|
|
||||||
try {
|
|
||||||
await this.loadGroupAttempts(groupId);
|
|
||||||
} catch (error) {
|
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh the attempts.
|
|
||||||
*
|
|
||||||
* @param refresher Refresher.
|
|
||||||
*/
|
|
||||||
async refreshFeedback(refresher: IonRefresher): Promise<void> {
|
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
|
|
||||||
promises.push(CoreGroups.invalidateActivityGroupInfo(this.cmId));
|
|
||||||
if (this.feedback) {
|
|
||||||
promises.push(AddonModFeedback.invalidateResponsesAnalysisData(this.feedback.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await CoreUtils.ignoreErrors(Promise.all(promises));
|
|
||||||
|
|
||||||
await this.fetchData(true);
|
|
||||||
} finally {
|
|
||||||
refresher.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of items that can be held by the entries manager.
|
|
||||||
*/
|
|
||||||
type EntryItem = AddonModFeedbackWSAttempt | AddonModFeedbackWSAnonAttempt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entries manager.
|
|
||||||
*/
|
|
||||||
class AddonModFeedbackResponsesManager extends CorePageItemsListManager<EntryItem> {
|
|
||||||
|
|
||||||
responses: AddonModFeedbackResponses = {
|
|
||||||
attempts: [],
|
|
||||||
total: 0,
|
|
||||||
canLoadMore: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
anonResponses: AddonModFeedbackAnonResponses = {
|
|
||||||
attempts: [],
|
|
||||||
total: 0,
|
|
||||||
canLoadMore: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(pageComponent: unknown) {
|
|
||||||
super(pageComponent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update responses.
|
|
||||||
*
|
|
||||||
* @param responses Responses.
|
|
||||||
*/
|
|
||||||
setResponses(responses: AddonModFeedbackResponsesAnalysis): void {
|
|
||||||
this.responses.total = responses.totalattempts;
|
|
||||||
this.anonResponses.total = responses.totalanonattempts;
|
|
||||||
|
|
||||||
if (this.anonResponses.attempts.length < responses.totalanonattempts) {
|
|
||||||
this.anonResponses.attempts = this.anonResponses.attempts.concat(responses.anonattempts);
|
|
||||||
}
|
|
||||||
if (this.responses.attempts.length < responses.totalattempts) {
|
|
||||||
this.responses.attempts = this.responses.attempts.concat(responses.attempts);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.anonResponses.canLoadMore = this.anonResponses.attempts.length < responses.totalanonattempts;
|
|
||||||
this.responses.canLoadMore = this.responses.attempts.length < responses.totalattempts;
|
|
||||||
|
|
||||||
this.setItems((<EntryItem[]> this.responses.attempts).concat(this.anonResponses.attempts));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
resetItems(): void {
|
|
||||||
super.resetItems();
|
|
||||||
this.responses.total = 0;
|
|
||||||
this.responses.attempts = [];
|
|
||||||
this.anonResponses.total = 0;
|
|
||||||
this.anonResponses.attempts = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
protected getItemPath(entry: EntryItem): string {
|
|
||||||
return `attempt/${entry.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddonModFeedbackResponses = {
|
|
||||||
attempts: AddonModFeedbackWSAttempt[];
|
|
||||||
total: number;
|
|
||||||
canLoadMore: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AddonModFeedbackAnonResponses = {
|
|
||||||
attempts: AddonModFeedbackWSAnonAttempt[];
|
|
||||||
total: number;
|
|
||||||
canLoadMore: boolean;
|
|
||||||
};
|
|
|
@ -184,7 +184,7 @@ export class AddonModFeedbackHelperProvider {
|
||||||
if (params.showcompleted === undefined) {
|
if (params.showcompleted === undefined) {
|
||||||
// Param showcompleted not defined. Show entry list.
|
// Param showcompleted not defined. Show entry list.
|
||||||
await CoreNavigator.navigateToSitePath(
|
await CoreNavigator.navigateToSitePath(
|
||||||
AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${module.course}/${module.id}/respondents`,
|
AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${module.course}/${module.id}/attempts`,
|
||||||
{ siteId },
|
{ siteId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -69,8 +69,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ion-item *ngFor="let discussion of discussionsItems" class="addon-mod-forum-discussion" detail="true"
|
<ion-item *ngFor="let discussion of discussionsItems" class="addon-mod-forum-discussion" detail="true"
|
||||||
[lines]="discussion.groupname && 'none'" [attr.aria-current]="discussions.getItemAriaCurrent(discussion)"
|
[lines]="discussion.groupname && 'none'" [attr.aria-current]="discussions?.getItemAriaCurrent(discussion)"
|
||||||
(click)="discussions.select(discussion)" button>
|
(click)="discussions?.select(discussion)" button>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<div class="addon-mod-forum-discussion-title">
|
<div class="addon-mod-forum-discussion-title">
|
||||||
<p class="ion-text-wrap item-heading">
|
<p class="ion-text-wrap item-heading">
|
||||||
|
|
|
@ -58,6 +58,7 @@ import { ContextLevel } from '@/core/constants';
|
||||||
import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
|
||||||
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays a forum entry page.
|
* Component that displays a forum entry page.
|
||||||
|
@ -74,7 +75,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
component = AddonModForumProvider.COMPONENT;
|
component = AddonModForumProvider.COMPONENT;
|
||||||
moduleName = 'forum';
|
moduleName = 'forum';
|
||||||
descriptionNote?: string;
|
descriptionNote?: string;
|
||||||
discussions!: AddonModForumDiscussionsManager;
|
promisedDiscussions: CorePromisedValue<AddonModForumDiscussionsManager>;
|
||||||
discussionsItems: AddonModForumDiscussionItem[] = [];
|
discussionsItems: AddonModForumDiscussionItem[] = [];
|
||||||
fetchFailed = false;
|
fetchFailed = false;
|
||||||
canAddDiscussion = false;
|
canAddDiscussion = false;
|
||||||
|
@ -104,6 +105,12 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||||
) {
|
) {
|
||||||
super('AddonModForumIndexComponent', content, courseContentsPage);
|
super('AddonModForumIndexComponent', content, courseContentsPage);
|
||||||
|
|
||||||
|
this.promisedDiscussions = new CorePromisedValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
get discussions(): AddonModForumDiscussionsManager | null {
|
||||||
|
return this.promisedDiscussions.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get forum(): AddonModForumData | undefined {
|
get forum(): AddonModForumData | undefined {
|
||||||
|
@ -121,7 +128,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @return Whether the discussion is online.
|
* @return Whether the discussion is online.
|
||||||
*/
|
*/
|
||||||
isOnlineDiscussion(discussion: AddonModForumDiscussionItem): boolean {
|
isOnlineDiscussion(discussion: AddonModForumDiscussionItem): boolean {
|
||||||
return this.discussions && this.discussions.getSource().isOnlineDiscussion(discussion);
|
return !!this.discussions?.getSource().isOnlineDiscussion(discussion);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -131,7 +138,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @return Whether the discussion is offline.
|
* @return Whether the discussion is offline.
|
||||||
*/
|
*/
|
||||||
isOfflineDiscussion(discussion: AddonModForumDiscussionItem): boolean {
|
isOfflineDiscussion(discussion: AddonModForumDiscussionItem): boolean {
|
||||||
return this.discussions && this.discussions.getSource().isOfflineDiscussion(discussion);
|
return !!this.discussions?.getSource().isOfflineDiscussion(discussion);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -171,7 +178,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
if (hasOffline) {
|
if (hasOffline) {
|
||||||
// Only update new fetched discussions.
|
// Only update new fetched discussions.
|
||||||
const promises = discussions.map(async (discussion) => {
|
const promises = discussions.map(async (discussion) => {
|
||||||
if (!this.discussions.getSource().isOnlineDiscussion(discussion)) {
|
if (!this.discussions?.getSource().isOnlineDiscussion(discussion)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +196,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.discussions = new AddonModForumDiscussionsManager(source, this);
|
this.promisedDiscussions.resolve(new AddonModForumDiscussionsManager(source, this));
|
||||||
|
|
||||||
// Refresh data if this forum discussion is synchronized from discussions list.
|
// Refresh data if this forum discussion is synchronized from discussions list.
|
||||||
this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, (data) => {
|
this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, (data) => {
|
||||||
|
@ -214,8 +221,8 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
AddonModForum.invalidateDiscussionsList(this.forum.id).finally(() => {
|
AddonModForum.invalidateDiscussionsList(this.forum.id).finally(() => {
|
||||||
if (data.discussionId) {
|
if (data.discussionId) {
|
||||||
// Discussion changed, search it in the list of discussions.
|
// Discussion changed, search it in the list of discussions.
|
||||||
const discussion = this.discussions.items.find(
|
const discussion = this.discussions?.items.find(
|
||||||
(disc) => this.discussions.getSource().isOnlineDiscussion(disc) && data.discussionId == disc.discussion,
|
disc => this.discussions?.getSource().isOnlineDiscussion(disc) && data.discussionId == disc.discussion,
|
||||||
) as AddonModForumDiscussion;
|
) as AddonModForumDiscussion;
|
||||||
|
|
||||||
if (discussion) {
|
if (discussion) {
|
||||||
|
@ -234,7 +241,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.deleted !== undefined && data.deleted) {
|
if (data.deleted !== undefined && data.deleted) {
|
||||||
if (data.post?.parentid == 0 && CoreScreen.isTablet && !this.discussions.empty) {
|
if (data.post?.parentid == 0 && CoreScreen.isTablet && this.discussions && !this.discussions.empty) {
|
||||||
// Discussion deleted, clear details page.
|
// Discussion deleted, clear details page.
|
||||||
this.discussions.select(this.discussions[0]);
|
this.discussions.select(this.discussions[0]);
|
||||||
}
|
}
|
||||||
|
@ -265,7 +272,9 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
async ngAfterViewInit(): Promise<void> {
|
async ngAfterViewInit(): Promise<void> {
|
||||||
await this.loadContent(false, true);
|
await this.loadContent(false, true);
|
||||||
|
|
||||||
this.discussions.start(this.splitView);
|
const discussions = await this.promisedDiscussions;
|
||||||
|
|
||||||
|
discussions.start(this.splitView);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -282,7 +291,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
this.ratingOfflineObserver && this.ratingOfflineObserver.off();
|
this.ratingOfflineObserver && this.ratingOfflineObserver.off();
|
||||||
this.ratingSyncObserver && this.ratingSyncObserver.off();
|
this.ratingSyncObserver && this.ratingSyncObserver.off();
|
||||||
this.sourceUnsubscribe && this.sourceUnsubscribe();
|
this.sourceUnsubscribe && this.sourceUnsubscribe();
|
||||||
this.discussions.destroy();
|
this.discussions?.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -305,8 +314,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const discussions = await this.promisedDiscussions;
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
refresh ? this.discussions.reload() : this.discussions.load(),
|
refresh ? discussions.reload() : discussions.load(),
|
||||||
CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.forum.cmid).then((hasRatings) => {
|
CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.forum.cmid).then((hasRatings) => {
|
||||||
this.hasOfflineRatings = hasRatings;
|
this.hasOfflineRatings = hasRatings;
|
||||||
|
|
||||||
|
@ -332,7 +343,9 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.discussions.getSource().loadForum();
|
const discussions = await this.promisedDiscussions;
|
||||||
|
|
||||||
|
await discussions.getSource().loadForum();
|
||||||
|
|
||||||
if (!this.forum) {
|
if (!this.forum) {
|
||||||
return;
|
return;
|
||||||
|
@ -380,7 +393,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
CoreGroups.instance
|
CoreGroups.instance
|
||||||
.getActivityGroupMode(forum.cmid)
|
.getActivityGroupMode(forum.cmid)
|
||||||
.then(async mode => {
|
.then(async mode => {
|
||||||
this.discussions.getSource().usesGroups =
|
discussions.getSource().usesGroups =
|
||||||
mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS;
|
mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -432,10 +445,12 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async fetchMoreDiscussions(complete: () => void): Promise<void> {
|
async fetchMoreDiscussions(complete: () => void): Promise<void> {
|
||||||
|
const discussions = await this.promisedDiscussions;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.fetchFailed = false;
|
this.fetchFailed = false;
|
||||||
|
|
||||||
await this.discussions.load();
|
await discussions.load();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true);
|
||||||
|
|
||||||
|
@ -463,10 +478,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
return value ? parseInt(value, 10) : null;
|
return value ? parseInt(value, 10) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const discussions = await this.promisedDiscussions;
|
||||||
const value = await getSortOrder();
|
const value = await getSortOrder();
|
||||||
const selectedOrder = this.sortOrders.find(sortOrder => sortOrder.value === value) || this.sortOrders[0];
|
const selectedOrder = this.sortOrders.find(sortOrder => sortOrder.value === value) || this.sortOrders[0];
|
||||||
|
|
||||||
this.discussions.getSource().selectedSortOrder = selectedOrder;
|
discussions.getSource().selectedSortOrder = selectedOrder;
|
||||||
|
|
||||||
if (this.sortOrderSelectorModalOptions.componentProps) {
|
if (this.sortOrderSelectorModalOptions.componentProps) {
|
||||||
this.sortOrderSelectorModalOptions.componentProps.selected = selectedOrder.value;
|
this.sortOrderSelectorModalOptions.componentProps.selected = selectedOrder.value;
|
||||||
|
@ -543,19 +559,19 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
// If it's a new discussion in tablet mode, try to open it.
|
// If it's a new discussion in tablet mode, try to open it.
|
||||||
if (isNewDiscussion && CoreScreen.isTablet) {
|
if (isNewDiscussion && CoreScreen.isTablet) {
|
||||||
const newDiscussionData = data as AddonModForumNewDiscussionData;
|
const newDiscussionData = data as AddonModForumNewDiscussionData;
|
||||||
const discussion = this.discussions.items.find(disc => {
|
const discussion = this.discussions?.items.find(disc => {
|
||||||
if (this.discussions.getSource().isOfflineDiscussion(disc)) {
|
if (this.discussions?.getSource().isOfflineDiscussion(disc)) {
|
||||||
return disc.timecreated === newDiscussionData.discTimecreated;
|
return disc.timecreated === newDiscussionData.discTimecreated;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.discussions.getSource().isOnlineDiscussion(disc)) {
|
if (this.discussions?.getSource().isOnlineDiscussion(disc)) {
|
||||||
return CoreArray.contains(newDiscussionData.discussionIds ?? [], disc.discussion);
|
return CoreArray.contains(newDiscussionData.discussionIds ?? [], disc.discussion);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (discussion || !this.discussions.empty) {
|
if (this.discussions && (discussion || !this.discussions.empty)) {
|
||||||
this.discussions.select(discussion ?? this.discussions.items[0]);
|
this.discussions.select(discussion ?? this.discussions.items[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -572,7 +588,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @param timeCreated Creation time of the offline discussion.
|
* @param timeCreated Creation time of the offline discussion.
|
||||||
*/
|
*/
|
||||||
openNewDiscussion(): void {
|
openNewDiscussion(): void {
|
||||||
this.discussions.select(AddonModForumDiscussionsSource.NEW_DISCUSSION);
|
this.discussions?.select(AddonModForumDiscussionsSource.NEW_DISCUSSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -581,7 +597,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
|
||||||
* @param sortOrder Sort order new data.
|
* @param sortOrder Sort order new data.
|
||||||
*/
|
*/
|
||||||
async setSortOrder(sortOrder: AddonModForumSortOrder): Promise<void> {
|
async setSortOrder(sortOrder: AddonModForumSortOrder): Promise<void> {
|
||||||
if (sortOrder.value != this.discussions.getSource().selectedSortOrder?.value) {
|
if (this.discussions && sortOrder.value != this.discussions.getSource().selectedSortOrder?.value) {
|
||||||
this.discussions.getSource().selectedSortOrder = sortOrder;
|
this.discussions.getSource().selectedSortOrder = sortOrder;
|
||||||
this.discussions.getSource().setDirty(true);
|
this.discussions.getSource().setDirty(true);
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
|
import { CorePromisedValue } from '@classes/promised-value';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||||
|
@ -70,7 +71,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
canAdd = false;
|
canAdd = false;
|
||||||
loadMoreError = false;
|
loadMoreError = false;
|
||||||
loadingMessage: string;
|
loadingMessage: string;
|
||||||
entries!: AddonModGlossaryEntriesManager;
|
promisedEntries: CorePromisedValue<AddonModGlossaryEntriesManager>;
|
||||||
hasOfflineRatings = false;
|
hasOfflineRatings = false;
|
||||||
|
|
||||||
protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
|
protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
|
||||||
|
@ -92,18 +93,23 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
super('AddonModGlossaryIndexComponent', content, courseContentsPage);
|
super('AddonModGlossaryIndexComponent', content, courseContentsPage);
|
||||||
|
|
||||||
this.loadingMessage = Translate.instant('core.loading');
|
this.loadingMessage = Translate.instant('core.loading');
|
||||||
|
this.promisedEntries = new CorePromisedValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
get entries(): AddonModGlossaryEntriesManager | null {
|
||||||
|
return this.promisedEntries.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get glossary(): AddonModGlossaryGlossary | undefined {
|
get glossary(): AddonModGlossaryGlossary | undefined {
|
||||||
return this.entries.getSource().glossary;
|
return this.entries?.getSource().glossary;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isSearch(): boolean {
|
get isSearch(): boolean {
|
||||||
return this.entries.getSource().isSearch;
|
return this.entries?.getSource().isSearch ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasSearched(): boolean {
|
get hasSearched(): boolean {
|
||||||
return this.entries.getSource().hasSearched;
|
return this.entries?.getSource().hasSearched ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -118,10 +124,10 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
[this.courseId, this.module.id, this.courseContentsPage ? `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` : ''],
|
[this.courseId, this.module.id, this.courseContentsPage ? `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` : ''],
|
||||||
);
|
);
|
||||||
|
|
||||||
this.entries = new AddonModGlossaryEntriesManager(
|
this.promisedEntries.resolve(new AddonModGlossaryEntriesManager(
|
||||||
source,
|
source,
|
||||||
this.route.component,
|
this.route.component,
|
||||||
);
|
));
|
||||||
|
|
||||||
this.sourceUnsubscribe = source.addListener({
|
this.sourceUnsubscribe = source.addListener({
|
||||||
onItemsUpdated: items => this.hasOffline = !!items.find(item => source.isOfflineEntry(item)),
|
onItemsUpdated: items => this.hasOffline = !!items.find(item => source.isOfflineEntry(item)),
|
||||||
|
@ -156,13 +162,10 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngAfterViewInit(): Promise<void> {
|
async ngAfterViewInit(): Promise<void> {
|
||||||
|
const entries = await this.promisedEntries;
|
||||||
|
|
||||||
await this.loadContent(false, true);
|
await this.loadContent(false, true);
|
||||||
|
await entries.start(this.splitView);
|
||||||
if (!this.glossary) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.entries.start(this.splitView);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||||
|
@ -175,8 +178,10 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
const entries = await this.promisedEntries;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.entries.getSource().loadGlossary();
|
await entries.getSource().loadGlossary();
|
||||||
|
|
||||||
if (!this.glossary) {
|
if (!this.glossary) {
|
||||||
return;
|
return;
|
||||||
|
@ -187,7 +192,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
|
|
||||||
this.dataRetrieved.emit(this.glossary);
|
this.dataRetrieved.emit(this.glossary);
|
||||||
|
|
||||||
if (!this.entries.getSource().fetchMode) {
|
if (!entries.getSource().fetchMode) {
|
||||||
this.switchMode('letter_all');
|
this.switchMode('letter_all');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,7 +203,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
|
|
||||||
const [hasOfflineRatings] = await Promise.all([
|
const [hasOfflineRatings] = await Promise.all([
|
||||||
CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule),
|
CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule),
|
||||||
refresh ? this.entries.reload() : this.entries.load(),
|
refresh ? entries.reload() : entries.load(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.hasOfflineRatings = hasOfflineRatings;
|
this.hasOfflineRatings = hasOfflineRatings;
|
||||||
|
@ -211,7 +216,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected async invalidateContent(): Promise<void> {
|
protected async invalidateContent(): Promise<void> {
|
||||||
await this.entries.getSource().invalidateCache();
|
await this.entries?.getSource().invalidateCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -250,7 +255,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* @param mode New mode.
|
* @param mode New mode.
|
||||||
*/
|
*/
|
||||||
protected switchMode(mode: AddonModGlossaryFetchMode): void {
|
protected switchMode(mode: AddonModGlossaryFetchMode): void {
|
||||||
this.entries.getSource().switchMode(mode);
|
this.entries?.getSource().switchMode(mode);
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'author_all':
|
case 'author_all':
|
||||||
|
@ -304,10 +309,12 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async loadMoreEntries(infiniteComplete?: () => void): Promise<void> {
|
async loadMoreEntries(infiniteComplete?: () => void): Promise<void> {
|
||||||
|
const entries = await this.promisedEntries;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.loadMoreError = false;
|
this.loadMoreError = false;
|
||||||
|
|
||||||
await this.entries.load();
|
await entries.load();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.loadMoreError = true;
|
this.loadMoreError = true;
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
|
||||||
|
@ -326,7 +333,8 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousMode = this.entries.getSource().fetchMode;
|
const entries = await this.promisedEntries;
|
||||||
|
const previousMode = entries.getSource().fetchMode;
|
||||||
const newMode = await CoreDomUtils.openPopover<AddonModGlossaryFetchMode>({
|
const newMode = await CoreDomUtils.openPopover<AddonModGlossaryFetchMode>({
|
||||||
component: AddonModGlossaryModePickerPopoverComponent,
|
component: AddonModGlossaryModePickerPopoverComponent,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
|
@ -357,6 +365,10 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* Toggles between search and fetch mode.
|
* Toggles between search and fetch mode.
|
||||||
*/
|
*/
|
||||||
toggleSearch(): void {
|
toggleSearch(): void {
|
||||||
|
if (!this.entries) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isSearch) {
|
if (this.isSearch) {
|
||||||
const fetchMode = this.entries.getSource().fetchMode;
|
const fetchMode = this.entries.getSource().fetchMode;
|
||||||
|
|
||||||
|
@ -393,7 +405,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* Opens new entry editor.
|
* Opens new entry editor.
|
||||||
*/
|
*/
|
||||||
openNewEntry(): void {
|
openNewEntry(): void {
|
||||||
this.entries.select(AddonModGlossaryEntriesSource.NEW_ENTRY);
|
this.entries?.select(AddonModGlossaryEntriesSource.NEW_ENTRY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -405,7 +417,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
this.loadingMessage = Translate.instant('core.searching');
|
this.loadingMessage = Translate.instant('core.searching');
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
|
|
||||||
this.entries.getSource().search(query);
|
this.entries?.getSource().search(query);
|
||||||
this.loadContent();
|
this.loadContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -419,7 +431,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
this.ratingOfflineObserver?.off();
|
this.ratingOfflineObserver?.off();
|
||||||
this.ratingSyncObserver?.off();
|
this.ratingSyncObserver?.off();
|
||||||
this.sourceUnsubscribe?.call(null);
|
this.sourceUnsubscribe?.call(null);
|
||||||
this.entries.destroy();
|
this.entries?.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
// (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.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promise wrapper to expose result synchronously.
|
||||||
|
*/
|
||||||
|
export class CorePromisedValue<T = unknown> implements Promise<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap an existing promise.
|
||||||
|
*
|
||||||
|
* @param promise Promise.
|
||||||
|
* @returns Promised value.
|
||||||
|
*/
|
||||||
|
static from<T>(promise: Promise<T>): CorePromisedValue<T> {
|
||||||
|
const promisedValue = new CorePromisedValue<T>();
|
||||||
|
|
||||||
|
promise
|
||||||
|
.then(promisedValue.resolve.bind(promisedValue))
|
||||||
|
.catch(promisedValue.reject.bind(promisedValue));
|
||||||
|
|
||||||
|
return promisedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _resolvedValue?: T;
|
||||||
|
private _rejectedReason?: Error;
|
||||||
|
declare private promise: Promise<T>;
|
||||||
|
declare private _resolve: (result: T) => void;
|
||||||
|
declare private _reject: (error?: Error) => void;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.initPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.toStringTag]: string;
|
||||||
|
|
||||||
|
get value(): T | null {
|
||||||
|
return this._resolvedValue ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the promise resolved successfully.
|
||||||
|
*
|
||||||
|
* @return Whether the promise resolved successfuly.
|
||||||
|
*/
|
||||||
|
isResolved(): this is { value: T } {
|
||||||
|
return '_resolvedValue' in this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the promise was rejected.
|
||||||
|
*
|
||||||
|
* @return Whether the promise was rejected.
|
||||||
|
*/
|
||||||
|
isRejected(): boolean {
|
||||||
|
return '_rejectedReason' in this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the promise is settled.
|
||||||
|
*
|
||||||
|
* @returns Whether the promise is settled.
|
||||||
|
*/
|
||||||
|
isSettled(): boolean {
|
||||||
|
return this.isResolved() || this.isRejected();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
then<TResult1 = T, TResult2 = never>(
|
||||||
|
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
|
||||||
|
onRejected?: ((reason: Error) => TResult2 | PromiseLike<TResult2>) | undefined | null,
|
||||||
|
): Promise<TResult1 | TResult2> {
|
||||||
|
return this.promise.then(onFulfilled, onRejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
catch<TResult = never>(
|
||||||
|
onRejected?: ((reason: Error) => TResult | PromiseLike<TResult>) | undefined | null,
|
||||||
|
): Promise<T | TResult> {
|
||||||
|
return this.promise.catch(onRejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
finally(onFinally?: (() => void) | null): Promise<T> {
|
||||||
|
return this.promise.finally(onFinally);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the promise.
|
||||||
|
*
|
||||||
|
* @param value Promise result.
|
||||||
|
*/
|
||||||
|
resolve(value: T): void {
|
||||||
|
if (this.isSettled()) {
|
||||||
|
delete this._rejectedReason;
|
||||||
|
|
||||||
|
this.initPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._resolvedValue = value;
|
||||||
|
this._resolve(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject the promise.
|
||||||
|
*
|
||||||
|
* @param value Rejection reason.
|
||||||
|
*/
|
||||||
|
reject(reason?: Error): void {
|
||||||
|
if (this.isSettled()) {
|
||||||
|
delete this._resolvedValue;
|
||||||
|
|
||||||
|
this.initPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._rejectedReason = reason;
|
||||||
|
this._reject(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the promise and the callbacks.
|
||||||
|
*/
|
||||||
|
private initPromise(): void {
|
||||||
|
this.promise = new Promise((resolve, reject) => {
|
||||||
|
this._resolve = resolve;
|
||||||
|
this._reject = reject;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
// (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 { CorePromisedValue } from '../promised-value';
|
||||||
|
|
||||||
|
describe('PromisedValue', () => {
|
||||||
|
|
||||||
|
it('works like a promise', async () => {
|
||||||
|
const promisedString = new CorePromisedValue<string>();
|
||||||
|
expect(promisedString.value).toBe(null);
|
||||||
|
expect(promisedString.isResolved()).toBe(false);
|
||||||
|
|
||||||
|
promisedString.resolve('foo');
|
||||||
|
expect(promisedString.isResolved()).toBe(true);
|
||||||
|
expect(promisedString.value).toBe('foo');
|
||||||
|
|
||||||
|
const resolvedValue = await promisedString;
|
||||||
|
expect(resolvedValue).toBe('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can update values', async () => {
|
||||||
|
const promisedString = new CorePromisedValue<string>();
|
||||||
|
promisedString.resolve('foo');
|
||||||
|
promisedString.resolve('bar');
|
||||||
|
|
||||||
|
expect(promisedString.isResolved()).toBe(true);
|
||||||
|
expect(promisedString.value).toBe('bar');
|
||||||
|
|
||||||
|
const resolvedValue = await promisedString;
|
||||||
|
expect(resolvedValue).toBe('bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
Loading…
Reference in New Issue