forked from CIT/Vmeda.Online
		
	MOBILE-3939 feedback: Attempts swipe navigation
This commit is contained in:
		
							parent
							
								
									00a12df79b
								
							
						
					
					
						commit
						12e30f1c86
					
				
							
								
								
									
										163
									
								
								src/addons/mod/feedback/classes/feedback-attempts-source.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								src/addons/mod/feedback/classes/feedback-attempts-source.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
| @ -12,45 +12,47 @@ | ||||
|     </ion-toolbar> | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <core-loading [hideUntil]="loaded"> | ||||
|         <ion-list class="ion-no-margin" *ngIf="attempt || anonAttempt"> | ||||
|             <ion-item *ngIf="attempt" class="ion-text-wrap" core-user-link [userId]="attempt.userid" | ||||
|                 [attr.aria-label]=" 'core.user.viewprofile' | translate" [courseId]="attempt.courseid"> | ||||
|                 <core-user-avatar [user]="attempt" slot="start"></core-user-avatar> | ||||
|                 <ion-label> | ||||
|                     <h2>{{attempt.fullname}}</h2> | ||||
|                     <p *ngIf="attempt.timemodified">{{attempt.timemodified * 1000 | coreFormatDate }}</p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|     <core-swipe-navigation [manager]="attempts"> | ||||
|         <core-loading [hideUntil]="loaded"> | ||||
|             <ion-list class="ion-no-margin" *ngIf="attempt || anonAttempt"> | ||||
|                 <ion-item *ngIf="attempt" class="ion-text-wrap" core-user-link [userId]="attempt.userid" | ||||
|                     [attr.aria-label]=" 'core.user.viewprofile' | translate" [courseId]="attempt.courseid"> | ||||
|                     <core-user-avatar [user]="attempt" slot="start"></core-user-avatar> | ||||
|                     <ion-label> | ||||
|                         <h2>{{attempt.fullname}}</h2> | ||||
|                         <p *ngIf="attempt.timemodified">{{attempt.timemodified * 1000 | coreFormatDate }}</p> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|             <ion-item class="ion-text-wrap" *ngIf="anonAttempt"> | ||||
|                 <ion-label> | ||||
|                     <h2> | ||||
|                         {{ 'addon.mod_feedback.response_nr' |translate }}: {{anonAttempt.number}} | ||||
|                         ({{ 'addon.mod_feedback.anonymous' |translate }}) | ||||
|                     </h2> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ng-container *ngIf="items && items.length"> | ||||
|                 <ng-container *ngFor="let item of items"> | ||||
|                     <core-spacer *ngIf="item.typ == 'pagebreak'"></core-spacer> | ||||
|                     <ion-item class="ion-text-wrap" *ngIf="item.typ != 'pagebreak'" [color]="item.dependitem > 0 ? 'light' : ''"> | ||||
|                         <ion-label> | ||||
|                             <h2 *ngIf="item.name" [core-mark-required]="item.required"> | ||||
|                                 <span *ngIf="feedback!.autonumbering && item.itemnumber">{{item.itemnumber}}. </span> | ||||
|                                 <core-format-text [component]="component" [componentId]="cmId" [text]="item.name" contextLevel="module" | ||||
|                                     [contextInstanceId]="cmId" [courseId]="courseId"> | ||||
|                                 </core-format-text> | ||||
|                             </h2> | ||||
|                             <p *ngIf="item.submittedValue"> | ||||
|                                 <core-format-text [component]="component" [componentId]="cmId" [text]="item.submittedValue" | ||||
|                                     contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId"> | ||||
|                                 </core-format-text> | ||||
|                             </p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                 <ion-item class="ion-text-wrap" *ngIf="anonAttempt"> | ||||
|                     <ion-label> | ||||
|                         <h2> | ||||
|                             {{ 'addon.mod_feedback.response_nr' |translate }}: {{anonAttempt.number}} | ||||
|                             ({{ 'addon.mod_feedback.anonymous' |translate }}) | ||||
|                         </h2> | ||||
|                     </ion-label> | ||||
|                 </ion-item> | ||||
|                 <ng-container *ngIf="items && items.length"> | ||||
|                     <ng-container *ngFor="let item of items"> | ||||
|                         <core-spacer *ngIf="item.typ == 'pagebreak'"></core-spacer> | ||||
|                         <ion-item class="ion-text-wrap" *ngIf="item.typ != 'pagebreak'" [color]="item.dependitem > 0 ? 'light' : ''"> | ||||
|                             <ion-label> | ||||
|                                 <h2 *ngIf="item.name" [core-mark-required]="item.required"> | ||||
|                                     <span *ngIf="feedback!.autonumbering && item.itemnumber">{{item.itemnumber}}. </span> | ||||
|                                     <core-format-text [component]="component" [componentId]="cmId" [text]="item.name" contextLevel="module" | ||||
|                                         [contextInstanceId]="cmId" [courseId]="courseId"> | ||||
|                                     </core-format-text> | ||||
|                                 </h2> | ||||
|                                 <p *ngIf="item.submittedValue"> | ||||
|                                     <core-format-text [component]="component" [componentId]="cmId" [text]="item.submittedValue" | ||||
|                                         contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId"> | ||||
|                                     </core-format-text> | ||||
|                                 </p> | ||||
|                             </ion-label> | ||||
|                         </ion-item> | ||||
|                     </ng-container> | ||||
|                 </ng-container> | ||||
|             </ng-container> | ||||
|         </ion-list> | ||||
|     </core-loading> | ||||
|             </ion-list> | ||||
|         </core-loading> | ||||
|     </core-swipe-navigation> | ||||
| </ion-content> | ||||
|  | ||||
| @ -12,10 +12,14 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // 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 { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| import { AddonModFeedbackAttemptsSource } from '../../classes/feedback-attempts-source'; | ||||
| import { | ||||
|     AddonModFeedback, | ||||
|     AddonModFeedbackProvider, | ||||
| @ -32,7 +36,7 @@ import { AddonModFeedbackFormItem, AddonModFeedbackHelper } from '../../services | ||||
|     selector: 'page-addon-mod-feedback-attempt', | ||||
|     templateUrl: 'attempt.html', | ||||
| }) | ||||
| export class AddonModFeedbackAttemptPage implements OnInit { | ||||
| export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     protected attemptId!: number; | ||||
| 
 | ||||
| @ -40,6 +44,7 @@ export class AddonModFeedbackAttemptPage implements OnInit { | ||||
|     courseId!: number; | ||||
|     feedback?: AddonModFeedbackWSFeedback; | ||||
|     attempt?: AddonModFeedbackWSAttempt; | ||||
|     attempts?: AddonModFeedbackAttemptsSwipeManager; | ||||
|     anonAttempt?: AddonModFeedbackWSAnonAttempt; | ||||
|     items: AddonModFeedbackAttemptItem[] = []; | ||||
|     component = AddonModFeedbackProvider.COMPONENT; | ||||
| @ -53,6 +58,15 @@ export class AddonModFeedbackAttemptPage implements OnInit { | ||||
|             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); | ||||
| 
 | ||||
|             this.attempts.start(); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModal(error); | ||||
| 
 | ||||
| @ -64,6 +78,13 @@ export class AddonModFeedbackAttemptPage implements OnInit { | ||||
|         this.fetchData(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.attempts?.destroy(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch all the data required for the view. | ||||
|      * | ||||
| @ -131,3 +152,17 @@ export class AddonModFeedbackAttemptPage implements OnInit { | ||||
| type AddonModFeedbackAttemptItem = AddonModFeedbackFormItem & { | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -10,77 +10,57 @@ | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <core-split-view> | ||||
|         <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshFeedback($event.target)"> | ||||
|         <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]="loaded"> | ||||
|         <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)="loadAttempts(selectedGroup)" | ||||
|                         aria-labelledby="addon-feedback-groupslabel" interface="action-sheet" | ||||
|                         [interfaceOptions]="{header: 'core.group' | translate}"> | ||||
|                     <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="attempts.identifiable.total > 0"> | ||||
|                 <ng-container *ngIf="identifiableAttemptsTotal > 0"> | ||||
|                     <ion-item-divider> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_feedback.non_anonymous_entries' | translate : {$a: attempts.identifiable.total } }} | ||||
|                             </h2> | ||||
|                             <h2>{{ 'addon.mod_feedback.non_anonymous_entries' | translate : {$a: identifiableAttemptsTotal } }}</h2> | ||||
|                         </ion-label> | ||||
|                     </ion-item-divider> | ||||
|                     <ion-item *ngFor="let attempt of attempts.identifiable.items" class="ion-text-wrap" button detail="true" | ||||
|                         (click)="attempts.select(attempt)" [attr.aria-current]="attempts.getItemAriaCurrent(attempt)"> | ||||
|                         <core-user-avatar [user]="attempt" slot="start"></core-user-avatar> | ||||
|                     <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> | ||||
| 
 | ||||
|                     <!-- Button and spinner to show more attempts. --> | ||||
|                     <ion-button *ngIf="attempts.identifiable.canLoadMore && !loadingMore" class="ion-margin" expand="block" | ||||
|                         (click)="loadAttempts()"> | ||||
|                         {{ 'core.loadmore' | translate }} | ||||
|                     </ion-button> | ||||
|                     <ion-item *ngIf="attempts.identifiable.canLoadMore && loadingMore" class="ion-text-center"> | ||||
|                         <ion-label> | ||||
|                             <ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner> | ||||
|                             <p *ngIf="attempt.timemodified">{{ attempt.timemodified * 1000 | coreFormatDate }}</p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                 </ng-container> | ||||
| 
 | ||||
|                 <ng-container *ngIf="attempts.anonymous.total > 0"> | ||||
|                 <ng-container *ngIf="identifiableAttemptsTotal === identifiableAttempts.length && anonymousAttemptsTotal > 0"> | ||||
|                     <ion-item-divider> | ||||
|                         <ion-label> | ||||
|                             <h2>{{ 'addon.mod_feedback.anonymous_entries' |translate : {$a: attempts.anonymous.total } }}</h2> | ||||
|                             <h2>{{ 'addon.mod_feedback.anonymous_entries' | translate : {$a: anonymousAttemptsTotal } }}</h2> | ||||
|                         </ion-label> | ||||
|                     </ion-item-divider> | ||||
|                     <ion-item *ngFor="let attempt of attempts.anonymous.items" class="ion-text-wrap" button detail="true" | ||||
|                         (click)="attempts.select(attempt)" [attr.aria-current]="attempts.getItemAriaCurrent(attempt)"> | ||||
|                     <ion-item *ngFor="let attempt of anonymousAttempts" class="ion-text-wrap" button detail="true" | ||||
|                         (click)="attempts?.select(attempt)" [attr.aria-current]="attempts?.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="attempts.anonymous.canLoadMore && !loadingMore" class="ion-margin" expand="block" | ||||
|                         (click)="loadAttempts()"> | ||||
|                         {{ 'core.loadmore' | translate }} | ||||
|                     </ion-button> | ||||
|                     <ion-item *ngIf="attempts.anonymous.canLoadMore && loadingMore" class="ion-text-center"> | ||||
|                         <ion-label> | ||||
|                             <ion-spinner [attr.aria-label]="'core.loading' | translate"></ion-spinner> | ||||
|                             <h2>{{ 'addon.mod_feedback.response_nr' | translate }}: {{attempt.number}}</h2> | ||||
|                         </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> | ||||
|  | ||||
| @ -12,22 +12,19 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { AfterViewInit, Component, ViewChild } from '@angular/core'; | ||||
| import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { CorePageItemsListManager } from '@classes/page-items-list-manager'; | ||||
| 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, CoreGroups } from '@services/groups'; | ||||
| import { CoreGroupInfo } 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'; | ||||
| import { AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource } from '../../classes/feedback-attempts-source'; | ||||
| import { AddonModFeedbackWSAnonAttempt, AddonModFeedbackWSAttempt } from '../../services/feedback'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays feedback attempts. | ||||
| @ -36,27 +33,52 @@ import { AddonModFeedbackHelper, AddonModFeedbackResponsesAnalysis } from '../.. | ||||
|     selector: 'page-addon-mod-feedback-attempts', | ||||
|     templateUrl: 'attempts.html', | ||||
| }) | ||||
| export class AddonModFeedbackAttemptsPage implements AfterViewInit { | ||||
| export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy { | ||||
| 
 | ||||
|     @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; | ||||
| 
 | ||||
|     protected cmId!: number; | ||||
|     protected courseId!: number; | ||||
|     protected page = 0; | ||||
|     protected feedback?: AddonModFeedbackWSFeedback; | ||||
|     promisedAttempts: CorePromisedValue<CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource>>; | ||||
|     fetchFailed = false; | ||||
| 
 | ||||
|     attempts: AddonModFeedbackAttemptsManager; | ||||
|     selectedGroup!: number; | ||||
|     groupInfo?: CoreGroupInfo; | ||||
|     loaded = false; | ||||
|     loadingMore = false; | ||||
|     constructor(protected route: ActivatedRoute) { | ||||
|         this.promisedAttempts = new CorePromisedValue(); | ||||
|     } | ||||
| 
 | ||||
|     constructor( | ||||
|         route: ActivatedRoute, | ||||
|     ) { | ||||
|         this.attempts = new AddonModFeedbackAttemptsManager( | ||||
|             route.component, | ||||
|         ); | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -64,9 +86,16 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit { | ||||
|      */ | ||||
|     async ngAfterViewInit(): Promise<void> { | ||||
|         try { | ||||
|             this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); | ||||
|             this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); | ||||
|             this.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0; | ||||
|             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); | ||||
| 
 | ||||
| @ -75,79 +104,47 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         await this.fetchData(); | ||||
| 
 | ||||
|         this.attempts.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.attempts.resetItems(); | ||||
|         const attempts = await this.promisedAttempts; | ||||
| 
 | ||||
|         try { | ||||
|             this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.cmId); | ||||
|             this.fetchFailed = false; | ||||
| 
 | ||||
|             this.groupInfo = await CoreGroups.getActivityGroupInfo(this.cmId); | ||||
| 
 | ||||
|             this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo); | ||||
| 
 | ||||
|             await this.loadGroupAttempts(this.selectedGroup); | ||||
|             await attempts.getSource().loadFeedback(); | ||||
|             await attempts.load(); | ||||
|         } catch (error) { | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); | ||||
|             this.fetchFailed = true; | ||||
| 
 | ||||
|             if (!refresh) { | ||||
|                 // Some call failed on first fetch, go back.
 | ||||
|                 CoreNavigator.back(); | ||||
|             } | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); | ||||
|         } | ||||
| 
 | ||||
|         await attempts.start(this.splitView); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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. | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async loadGroupAttempts(groupId?: number): Promise<void> { | ||||
|         if (groupId === undefined) { | ||||
|             this.page++; | ||||
|             this.loadingMore = true; | ||||
|         } else { | ||||
|             this.selectedGroup = groupId; | ||||
|             this.page = 0; | ||||
|             this.attempts.resetItems(); | ||||
|         } | ||||
|     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 { | ||||
|             const attempts = await AddonModFeedbackHelper.getResponsesAnalysis(this.feedback!.id, { | ||||
|                 groupId: this.selectedGroup, | ||||
|                 page: this.page, | ||||
|                 cmId: this.cmId, | ||||
|             }); | ||||
|             this.fetchFailed = false; | ||||
| 
 | ||||
|             this.attempts.setAttempts(attempts); | ||||
|             await attempts.load(); | ||||
|         } catch (error) { | ||||
|             this.fetchFailed = true; | ||||
| 
 | ||||
|             CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); | ||||
|         } 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); | ||||
|             infiniteComplete && infiniteComplete(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -157,100 +154,30 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit { | ||||
|      * @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)); | ||||
|         } | ||||
|         const attempts = await this.promisedAttempts; | ||||
| 
 | ||||
|         try { | ||||
|             await CoreUtils.ignoreErrors(Promise.all(promises)); | ||||
|             this.fetchFailed = false; | ||||
| 
 | ||||
|             await this.fetchData(true); | ||||
|             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(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Type of items that can be held by the entries manager. | ||||
|  */ | ||||
| type EntryItem = AddonModFeedbackWSAttempt | AddonModFeedbackWSAnonAttempt; | ||||
| 
 | ||||
| /** | ||||
|  * Entries manager. | ||||
|  */ | ||||
| class AddonModFeedbackAttemptsManager extends CorePageItemsListManager<EntryItem> { | ||||
| 
 | ||||
|     identifiable: AddonModFeedbackIdentifiableAttempts = { | ||||
|         items: [], | ||||
|         total: 0, | ||||
|         canLoadMore: false, | ||||
|     }; | ||||
| 
 | ||||
|     anonymous: AddonModFeedbackAnonymousAttempts = { | ||||
|         items: [], | ||||
|         total: 0, | ||||
|         canLoadMore: false, | ||||
|     }; | ||||
| 
 | ||||
|     constructor(pageComponent: unknown) { | ||||
|         super(pageComponent); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update attempts. | ||||
|      * | ||||
|      * @param attempts Attempts. | ||||
|      * Reload attempts list. | ||||
|      */ | ||||
|     setAttempts(attempts: AddonModFeedbackResponsesAnalysis): void { | ||||
|         this.identifiable.total = attempts.totalattempts; | ||||
|         this.anonymous.total = attempts.totalanonattempts; | ||||
|     async reloadAttempts(): Promise<void> { | ||||
|         const attempts = await this.promisedAttempts; | ||||
| 
 | ||||
|         if (this.anonymous.items.length < attempts.totalanonattempts) { | ||||
|             this.anonymous.items = this.anonymous.items.concat(attempts.anonattempts); | ||||
|         } | ||||
|         if (this.identifiable.items.length < attempts.totalattempts) { | ||||
|             this.identifiable.items = this.identifiable.items.concat(attempts.attempts); | ||||
|         } | ||||
| 
 | ||||
|         this.anonymous.canLoadMore = this.anonymous.items.length < attempts.totalanonattempts; | ||||
|         this.identifiable.canLoadMore = this.identifiable.items.length < attempts.totalattempts; | ||||
| 
 | ||||
|         this.setItems((<EntryItem[]> this.identifiable.items).concat(this.anonymous.items)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     resetItems(): void { | ||||
|         super.resetItems(); | ||||
|         this.identifiable.total = 0; | ||||
|         this.identifiable.items = []; | ||||
|         this.anonymous.total = 0; | ||||
|         this.anonymous.items = []; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected getItemPath(entry: EntryItem): string { | ||||
|         return entry.id.toString(); | ||||
|         await attempts.reload(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type AddonModFeedbackIdentifiableAttempts = { | ||||
|     items: AddonModFeedbackWSAttempt[]; | ||||
|     total: number; | ||||
|     canLoadMore: boolean; | ||||
| }; | ||||
| 
 | ||||
| type AddonModFeedbackAnonymousAttempts = { | ||||
|     items: AddonModFeedbackWSAnonAttempt[]; | ||||
|     total: number; | ||||
|     canLoadMore: boolean; | ||||
| }; | ||||
|  | ||||
							
								
								
									
										147
									
								
								src/core/classes/promised-value.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								src/core/classes/promised-value.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										44
									
								
								src/core/classes/tests/promised-value.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/core/classes/tests/promised-value.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user