diff --git a/src/addons/mod/feedback/classes/feedback-attempts-source.ts b/src/addons/mod/feedback/classes/feedback-attempts-source.ts new file mode 100644 index 000000000..4ef92a857 --- /dev/null +++ b/src/addons/mod/feedback/classes/feedback-attempts-source.ts @@ -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 { + + 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 { + await Promise.all([ + CoreGroups.invalidateActivityGroupInfo(this.CM_ID), + this.feedback && AddonModFeedback.invalidateResponsesAnalysisData(this.feedback.id), + ]); + } + + /** + * Load feedback. + */ + async loadFeedback(): Promise { + 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; diff --git a/src/addons/mod/feedback/pages/attempt/attempt.html b/src/addons/mod/feedback/pages/attempt/attempt.html index 909f8e6a0..14d03f7fc 100644 --- a/src/addons/mod/feedback/pages/attempt/attempt.html +++ b/src/addons/mod/feedback/pages/attempt/attempt.html @@ -12,45 +12,47 @@ - - - - - -

{{attempt.fullname}}

-

{{attempt.timemodified * 1000 | coreFormatDate }}

-
-
+ + + + + + +

{{attempt.fullname}}

+

{{attempt.timemodified * 1000 | coreFormatDate }}

+
+
- - -

- {{ 'addon.mod_feedback.response_nr' |translate }}: {{anonAttempt.number}} - ({{ 'addon.mod_feedback.anonymous' |translate }}) -

-
-
- - - - - -

- {{item.itemnumber}}. - - -

-

- - -

-
-
+ + +

+ {{ 'addon.mod_feedback.response_nr' |translate }}: {{anonAttempt.number}} + ({{ 'addon.mod_feedback.anonymous' |translate }}) +

+
+
+ + + + + +

+ {{item.itemnumber}}. + + +

+

+ + +

+
+
+
-
-
-
+
+
+
diff --git a/src/addons/mod/feedback/pages/attempt/attempt.ts b/src/addons/mod/feedback/pages/attempt/attempt.ts index 194f22f06..f406be269 100644 --- a/src/addons/mod/feedback/pages/attempt/attempt.ts +++ b/src/addons/mod/feedback/pages/attempt/attempt.ts @@ -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; + } + +} diff --git a/src/addons/mod/feedback/pages/attempts/attempts.html b/src/addons/mod/feedback/pages/attempts/attempts.html index 795bde959..69e914b21 100644 --- a/src/addons/mod/feedback/pages/attempts/attempts.html +++ b/src/addons/mod/feedback/pages/attempts/attempts.html @@ -10,77 +10,57 @@ - + - + {{'core.groupsseparate' | translate }} {{'core.groupsvisible' | translate }} - + {{groupOpt.name}} - + -

{{ 'addon.mod_feedback.non_anonymous_entries' | translate : {$a: attempts.identifiable.total } }} -

+

{{ 'addon.mod_feedback.non_anonymous_entries' | translate : {$a: identifiableAttemptsTotal } }}

- - + +

{{ attempt.fullname }}

-

{{attempt.timemodified * 1000 | coreFormatDate }}

-
-
- - - - {{ 'core.loadmore' | translate }} - - - - +

{{ attempt.timemodified * 1000 | coreFormatDate }}

- + -

{{ 'addon.mod_feedback.anonymous_entries' |translate : {$a: attempts.anonymous.total } }}

+

{{ 'addon.mod_feedback.anonymous_entries' | translate : {$a: anonymousAttemptsTotal } }}

- + -

{{ 'addon.mod_feedback.response_nr' |translate }}: {{attempt.number}}

-
-
- - - - {{ 'core.loadmore' | translate }} - - - - +

{{ 'addon.mod_feedback.response_nr' | translate }}: {{attempt.number}}

+ + +
diff --git a/src/addons/mod/feedback/pages/attempts/attempts.ts b/src/addons/mod/feedback/pages/attempts/attempts.ts index dc5915976..40fe0a21f 100644 --- a/src/addons/mod/feedback/pages/attempts/attempts.ts +++ b/src/addons/mod/feedback/pages/attempts/attempts.ts @@ -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>; + 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 | 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 { 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 { - 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 { - 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 { + 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 { - 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 { - const promises: Promise[] = []; - - 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 { - - 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 { + 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(( 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; -}; diff --git a/src/core/classes/promised-value.ts b/src/core/classes/promised-value.ts new file mode 100644 index 000000000..ae7bbbdec --- /dev/null +++ b/src/core/classes/promised-value.ts @@ -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 implements Promise { + + /** + * Wrap an existing promise. + * + * @param promise Promise. + * @returns Promised value. + */ + static from(promise: Promise): CorePromisedValue { + const promisedValue = new CorePromisedValue(); + + promise + .then(promisedValue.resolve.bind(promisedValue)) + .catch(promisedValue.reject.bind(promisedValue)); + + return promisedValue; + } + + private _resolvedValue?: T; + private _rejectedReason?: Error; + declare private promise: Promise; + 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( + onFulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, + onRejected?: ((reason: Error) => TResult2 | PromiseLike) | undefined | null, + ): Promise { + return this.promise.then(onFulfilled, onRejected); + } + + /** + * @inheritdoc + */ + catch( + onRejected?: ((reason: Error) => TResult | PromiseLike) | undefined | null, + ): Promise { + return this.promise.catch(onRejected); + } + + /** + * @inheritdoc + */ + finally(onFinally?: (() => void) | null): Promise { + 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; + }); + } + +} diff --git a/src/core/classes/tests/promised-value.test.ts b/src/core/classes/tests/promised-value.test.ts new file mode 100644 index 000000000..71b5a2821 --- /dev/null +++ b/src/core/classes/tests/promised-value.test.ts @@ -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(); + 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(); + promisedString.resolve('foo'); + promisedString.resolve('bar'); + + expect(promisedString.isResolved()).toBe(true); + expect(promisedString.value).toBe('bar'); + + const resolvedValue = await promisedString; + expect(resolvedValue).toBe('bar'); + }); + +});