From 00a12df79b4fb94b50d0f7a316a4dfbd5384b871 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 2 Dec 2021 13:23:49 +0100 Subject: [PATCH 1/6] MOBILE-3939 feedback: rename respondents --- .../index/addon-mod-feedback-index.html | 2 +- .../mod/feedback/components/index/index.ts | 6 +- .../mod/feedback/feedback-lazy.module.ts | 16 ++-- .../attempts.html} | 25 ++++--- .../respondents.ts => attempts/attempts.ts} | 74 +++++++++---------- .../mod/feedback/services/feedback-helper.ts | 2 +- 6 files changed, 63 insertions(+), 62 deletions(-) rename src/addons/mod/feedback/pages/{respondents/respondents.html => attempts/attempts.html} (74%) rename src/addons/mod/feedback/pages/{respondents/respondents.ts => attempts/attempts.ts} (73%) diff --git a/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html b/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html index c9cb04c87..685a98db0 100644 --- a/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html +++ b/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html @@ -72,7 +72,7 @@ -

{{ 'addon.mod_feedback.completed_feedbacks' | translate }}

diff --git a/src/addons/mod/feedback/components/index/index.ts b/src/addons/mod/feedback/components/index/index.ts index bfa04b763..3f3f94027 100644 --- a/src/addons/mod/feedback/components/index/index.ts +++ b/src/addons/mod/feedback/components/index/index.ts @@ -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) { return; } CoreNavigator.navigateToSitePath( - AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/respondents`, + AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/attempts`, { params: { group: this.group, diff --git a/src/addons/mod/feedback/feedback-lazy.module.ts b/src/addons/mod/feedback/feedback-lazy.module.ts index 15bf99ecd..b055ceedd 100644 --- a/src/addons/mod/feedback/feedback-lazy.module.ts +++ b/src/addons/mod/feedback/feedback-lazy.module.ts @@ -17,7 +17,7 @@ import { RouterModule, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; import { AddonModFeedbackComponentsModule } from './components/components.module'; 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 { CoreScreen } from '@services/screen'; @@ -40,11 +40,11 @@ const commonRoutes: Routes = [ const mobileRoutes: Routes = [ ...commonRoutes, { - path: ':courseId/:cmId/respondents', - component: AddonModFeedbackRespondentsPage, + path: ':courseId/:cmId/attempts', + component: AddonModFeedbackAttemptsPage, }, { - path: ':courseId/:cmId/respondents/attempt/:attemptId', + path: ':courseId/:cmId/attempts/:attemptId', loadChildren: () => import('./pages/attempt/attempt.module').then(m => m.AddonModFeedbackAttemptPageModule), }, ]; @@ -52,11 +52,11 @@ const mobileRoutes: Routes = [ const tabletRoutes: Routes = [ ...commonRoutes, { - path: ':courseId/:cmId/respondents', - component: AddonModFeedbackRespondentsPage, + path: ':courseId/:cmId/attempts', + component: AddonModFeedbackAttemptsPage, children: [ { - path: 'attempt/:attemptId', + path: ':attemptId', loadChildren: () => import('./pages/attempt/attempt.module').then(m => m.AddonModFeedbackAttemptPageModule), }, ], @@ -76,7 +76,7 @@ const routes: Routes = [ ], declarations: [ AddonModFeedbackIndexPage, - AddonModFeedbackRespondentsPage, + AddonModFeedbackAttemptsPage, ], }) export class AddonModFeedbackLazyModule {} diff --git a/src/addons/mod/feedback/pages/respondents/respondents.html b/src/addons/mod/feedback/pages/attempts/attempts.html similarity index 74% rename from src/addons/mod/feedback/pages/respondents/respondents.html rename to src/addons/mod/feedback/pages/attempts/attempts.html index 51c2184d0..795bde959 100644 --- a/src/addons/mod/feedback/pages/respondents/respondents.html +++ b/src/addons/mod/feedback/pages/attempts/attempts.html @@ -29,14 +29,15 @@
- + -

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

+

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

- +

{{ attempt.fullname }}

@@ -45,36 +46,36 @@
- {{ 'core.loadmore' | translate }} - +
- + -

{{ 'addon.mod_feedback.anonymous_entries' |translate : {$a: responses.anonResponses.total } }}

+

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

- +

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

- {{ 'core.loadmore' | translate }} - + diff --git a/src/addons/mod/feedback/pages/respondents/respondents.ts b/src/addons/mod/feedback/pages/attempts/attempts.ts similarity index 73% rename from src/addons/mod/feedback/pages/respondents/respondents.ts rename to src/addons/mod/feedback/pages/attempts/attempts.ts index 73a95a75f..dc5915976 100644 --- a/src/addons/mod/feedback/pages/respondents/respondents.ts +++ b/src/addons/mod/feedback/pages/attempts/attempts.ts @@ -30,13 +30,13 @@ import { import { AddonModFeedbackHelper, AddonModFeedbackResponsesAnalysis } from '../../services/feedback-helper'; /** - * Page that displays feedback respondents. + * Page that displays feedback attempts. */ @Component({ - selector: 'page-addon-mod-feedback-respondents', - templateUrl: 'respondents.html', + selector: 'page-addon-mod-feedback-attempts', + templateUrl: 'attempts.html', }) -export class AddonModFeedbackRespondentsPage implements AfterViewInit { +export class AddonModFeedbackAttemptsPage implements AfterViewInit { @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; @@ -45,7 +45,7 @@ export class AddonModFeedbackRespondentsPage implements AfterViewInit { protected page = 0; protected feedback?: AddonModFeedbackWSFeedback; - responses: AddonModFeedbackResponsesManager; + attempts: AddonModFeedbackAttemptsManager; selectedGroup!: number; groupInfo?: CoreGroupInfo; loaded = false; @@ -54,7 +54,7 @@ export class AddonModFeedbackRespondentsPage implements AfterViewInit { constructor( route: ActivatedRoute, ) { - this.responses = new AddonModFeedbackResponsesManager( + this.attempts = new AddonModFeedbackAttemptsManager( route.component, ); } @@ -77,7 +77,7 @@ export class AddonModFeedbackRespondentsPage implements AfterViewInit { await this.fetchData(); - this.responses.start(this.splitView); + this.attempts.start(this.splitView); } /** @@ -88,7 +88,7 @@ export class AddonModFeedbackRespondentsPage implements AfterViewInit { */ async fetchData(refresh: boolean = false): Promise { this.page = 0; - this.responses.resetItems(); + this.attempts.resetItems(); try { this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.cmId); @@ -121,17 +121,17 @@ export class AddonModFeedbackRespondentsPage implements AfterViewInit { } else { this.selectedGroup = groupId; this.page = 0; - this.responses.resetItems(); + this.attempts.resetItems(); } try { - const responses = await AddonModFeedbackHelper.getResponsesAnalysis(this.feedback!.id, { + const attempts = await AddonModFeedbackHelper.getResponsesAnalysis(this.feedback!.id, { groupId: this.selectedGroup, page: this.page, cmId: this.cmId, }); - this.responses.setResponses(responses); + this.attempts.setAttempts(attempts); } finally { this.loadingMore = false; this.loaded = true; @@ -183,16 +183,16 @@ type EntryItem = AddonModFeedbackWSAttempt | AddonModFeedbackWSAnonAttempt; /** * Entries manager. */ -class AddonModFeedbackResponsesManager extends CorePageItemsListManager { +class AddonModFeedbackAttemptsManager extends CorePageItemsListManager { - responses: AddonModFeedbackResponses = { - attempts: [], + identifiable: AddonModFeedbackIdentifiableAttempts = { + items: [], total: 0, canLoadMore: false, }; - anonResponses: AddonModFeedbackAnonResponses = { - attempts: [], + anonymous: AddonModFeedbackAnonymousAttempts = { + items: [], total: 0, canLoadMore: false, }; @@ -202,25 +202,25 @@ class AddonModFeedbackResponsesManager extends CorePageItemsListManager this.responses.attempts).concat(this.anonResponses.attempts)); + this.setItems(( this.identifiable.items).concat(this.anonymous.items)); } /** @@ -228,29 +228,29 @@ class AddonModFeedbackResponsesManager extends CorePageItemsListManager Date: Thu, 9 Dec 2021 13:47:36 +0100 Subject: [PATCH 2/6] MOBILE-3939 feedback: Attempts swipe navigation --- .../classes/feedback-attempts-source.ts | 163 +++++++++++ .../mod/feedback/pages/attempt/attempt.html | 80 +++--- .../mod/feedback/pages/attempt/attempt.ts | 39 ++- .../mod/feedback/pages/attempts/attempts.html | 58 ++-- .../mod/feedback/pages/attempts/attempts.ts | 269 +++++++----------- src/core/classes/promised-value.ts | 147 ++++++++++ src/core/classes/tests/promised-value.test.ts | 44 +++ 7 files changed, 549 insertions(+), 251 deletions(-) create mode 100644 src/addons/mod/feedback/classes/feedback-attempts-source.ts create mode 100644 src/core/classes/promised-value.ts create mode 100644 src/core/classes/tests/promised-value.test.ts 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'); + }); + +}); From e628e039fc6765152fd8adbc7e6d7cbe17973813 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 9 Dec 2021 14:10:50 +0100 Subject: [PATCH 3/6] MOBILE-3939 feedback: Consolidate user profiles --- scripts/langindex.json | 1 + src/addons/mod/feedback/lang.json | 1 + src/addons/mod/feedback/pages/attempt/attempt.html | 13 +++++-------- .../mod/feedback/pages/attempts/attempts.html | 4 +++- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index 26f2815f3..3ab3ca3a4 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -559,6 +559,7 @@ "addon.mod_feedback.analysis": "feedback", "addon.mod_feedback.anonymous": "feedback", "addon.mod_feedback.anonymous_entries": "feedback", + "addon.mod_feedback.anonymous_user": "feedback", "addon.mod_feedback.average": "feedback", "addon.mod_feedback.captchaofflinewarning": "local_moodlemobileapp", "addon.mod_feedback.complete_the_form": "feedback", diff --git a/src/addons/mod/feedback/lang.json b/src/addons/mod/feedback/lang.json index 8d575a97b..903a127e3 100644 --- a/src/addons/mod/feedback/lang.json +++ b/src/addons/mod/feedback/lang.json @@ -2,6 +2,7 @@ "analysis": "Analysis", "anonymous": "Anonymous", "anonymous_entries": "Anonymous entries ({{$a}})", + "anonymous_user": "Anonymous user", "average": "Average", "captchaofflinewarning": "Feedback with CAPTCHA cannot be completed offline, or if not configured, or if the server is down.", "complete_the_form": "Answer the questions", diff --git a/src/addons/mod/feedback/pages/attempt/attempt.html b/src/addons/mod/feedback/pages/attempt/attempt.html index 14d03f7fc..3fee6027f 100644 --- a/src/addons/mod/feedback/pages/attempt/attempt.html +++ b/src/addons/mod/feedback/pages/attempt/attempt.html @@ -5,9 +5,7 @@

{{ attempt.fullname }}

-

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

+

{{ 'addon.mod_feedback.anonymous_user' | translate }}

@@ -16,7 +14,7 @@ + [attr.aria-label]="'core.user.viewprofile' | translate" [courseId]="attempt.courseid">

{{attempt.fullname}}

@@ -25,11 +23,10 @@
+ -

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

+

{{ 'addon.mod_feedback.anonymous_user' |translate }}

+

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

diff --git a/src/addons/mod/feedback/pages/attempts/attempts.html b/src/addons/mod/feedback/pages/attempts/attempts.html index 69e914b21..123818d86 100644 --- a/src/addons/mod/feedback/pages/attempts/attempts.html +++ b/src/addons/mod/feedback/pages/attempts/attempts.html @@ -52,8 +52,10 @@ + -

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

+

{{ 'addon.mod_feedback.anonymous_user' | translate }}

+

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

From 3cf8c1df7416b28b2cb64b21ced2449e2d59eb94 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 9 Dec 2021 14:25:36 +0100 Subject: [PATCH 4/6] MOBILE-3833 glossary: Fix index page --- .../mod/glossary/components/index/index.ts | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/addons/mod/glossary/components/index/index.ts b/src/addons/mod/glossary/components/index/index.ts index 7599c7afc..beb7aad9c 100644 --- a/src/addons/mod/glossary/components/index/index.ts +++ b/src/addons/mod/glossary/components/index/index.ts @@ -17,6 +17,7 @@ import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from import { ActivatedRoute } from '@angular/router'; import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; 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 { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component'; import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; @@ -70,7 +71,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity canAdd = false; loadMoreError = false; loadingMessage: string; - entries!: AddonModGlossaryEntriesManager; + promisedEntries: CorePromisedValue; hasOfflineRatings = false; protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED; @@ -92,18 +93,23 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity super('AddonModGlossaryIndexComponent', content, courseContentsPage); this.loadingMessage = Translate.instant('core.loading'); + this.promisedEntries = new CorePromisedValue(); + } + + get entries(): AddonModGlossaryEntriesManager | null { + return this.promisedEntries.value; } get glossary(): AddonModGlossaryGlossary | undefined { - return this.entries.getSource().glossary; + return this.entries?.getSource().glossary; } get isSearch(): boolean { - return this.entries.getSource().isSearch; + return this.entries?.getSource().isSearch ?? false; } 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.entries = new AddonModGlossaryEntriesManager( + this.promisedEntries.resolve(new AddonModGlossaryEntriesManager( source, this.route.component, - ); + )); this.sourceUnsubscribe = source.addListener({ onItemsUpdated: items => this.hasOffline = !!items.find(item => source.isOfflineEntry(item)), @@ -156,13 +162,10 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity * @inheritdoc */ async ngAfterViewInit(): Promise { + const entries = await this.promisedEntries; + await this.loadContent(false, true); - - if (!this.glossary) { - return; - } - - await this.entries.start(this.splitView); + await entries.start(this.splitView); try { CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); @@ -175,8 +178,10 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity * @inheritdoc */ protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + const entries = await this.promisedEntries; + try { - await this.entries.getSource().loadGlossary(); + await entries.getSource().loadGlossary(); if (!this.glossary) { return; @@ -187,7 +192,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity this.dataRetrieved.emit(this.glossary); - if (!this.entries.getSource().fetchMode) { + if (!entries.getSource().fetchMode) { this.switchMode('letter_all'); } @@ -198,7 +203,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity const [hasOfflineRatings] = await Promise.all([ 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; @@ -211,7 +216,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity * @inheritdoc */ protected async invalidateContent(): Promise { - await this.entries.getSource().invalidateCache(); + await this.entries?.getSource().invalidateCache(); } /** @@ -250,7 +255,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity * @param mode New mode. */ protected switchMode(mode: AddonModGlossaryFetchMode): void { - this.entries.getSource().switchMode(mode); + this.entries?.getSource().switchMode(mode); switch (mode) { case 'author_all': @@ -304,10 +309,12 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity * @return Promise resolved when done. */ async loadMoreEntries(infiniteComplete?: () => void): Promise { + const entries = await this.promisedEntries; + try { this.loadMoreError = false; - await this.entries.load(); + await entries.load(); } catch (error) { this.loadMoreError = true; CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true); @@ -326,7 +333,8 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity return; } - const previousMode = this.entries.getSource().fetchMode; + const entries = await this.promisedEntries; + const previousMode = entries.getSource().fetchMode; const newMode = await CoreDomUtils.openPopover({ component: AddonModGlossaryModePickerPopoverComponent, componentProps: { @@ -357,6 +365,10 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity * Toggles between search and fetch mode. */ toggleSearch(): void { + if (!this.entries) { + return; + } + if (this.isSearch) { const fetchMode = this.entries.getSource().fetchMode; @@ -393,7 +405,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity * Opens new entry editor. */ 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.loaded = false; - this.entries.getSource().search(query); + this.entries?.getSource().search(query); this.loadContent(); } @@ -419,7 +431,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity this.ratingOfflineObserver?.off(); this.ratingSyncObserver?.off(); this.sourceUnsubscribe?.call(null); - this.entries.destroy(); + this.entries?.destroy(); } } From 848b5e79b5538631c30dd29be9f9845ecc4c298d Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 9 Dec 2021 15:18:10 +0100 Subject: [PATCH 5/6] MOBILE-3833 forum: Avoid assert declarations --- .../mod/forum/components/index/index.html | 4 +- .../mod/forum/components/index/index.ts | 58 ++++++++++++------- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/addons/mod/forum/components/index/index.html b/src/addons/mod/forum/components/index/index.html index 63cd1b7b6..5000be62a 100644 --- a/src/addons/mod/forum/components/index/index.html +++ b/src/addons/mod/forum/components/index/index.html @@ -69,8 +69,8 @@ + [lines]="discussion.groupname && 'none'" [attr.aria-current]="discussions?.getItemAriaCurrent(discussion)" + (click)="discussions?.select(discussion)" button>

diff --git a/src/addons/mod/forum/components/index/index.ts b/src/addons/mod/forum/components/index/index.ts index 9afeb028e..b2ac0282c 100644 --- a/src/addons/mod/forum/components/index/index.ts +++ b/src/addons/mod/forum/components/index/index.ts @@ -58,6 +58,7 @@ import { ContextLevel } from '@/core/constants'; import { AddonModForumDiscussionItem, AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source'; import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CorePromisedValue } from '@classes/promised-value'; /** * Component that displays a forum entry page. @@ -74,7 +75,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom component = AddonModForumProvider.COMPONENT; moduleName = 'forum'; descriptionNote?: string; - discussions!: AddonModForumDiscussionsManager; + promisedDiscussions: CorePromisedValue; discussionsItems: AddonModForumDiscussionItem[] = []; fetchFailed = false; canAddDiscussion = false; @@ -104,6 +105,12 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom @Optional() courseContentsPage?: CoreCourseContentsPage, ) { super('AddonModForumIndexComponent', content, courseContentsPage); + + this.promisedDiscussions = new CorePromisedValue(); + } + + get discussions(): AddonModForumDiscussionsManager | null { + return this.promisedDiscussions.value; } get forum(): AddonModForumData | undefined { @@ -121,7 +128,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * @return Whether the discussion is online. */ 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. */ isOfflineDiscussion(discussion: AddonModForumDiscussionItem): boolean { - return this.discussions && this.discussions.getSource().isOfflineDiscussion(discussion); + return !!this.discussions?.getSource().isOfflineDiscussion(discussion); } /** @@ -170,7 +177,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom if (hasOffline) { // Only update new fetched discussions. const promises = discussions.map(async (discussion) => { - if (!this.discussions.getSource().isOnlineDiscussion(discussion)) { + if (!this.discussions?.getSource().isOnlineDiscussion(discussion)) { return; } @@ -188,7 +195,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. this.syncManualObserver = CoreEvents.on(AddonModForumSyncProvider.MANUAL_SYNCED, (data) => { @@ -213,8 +220,8 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom AddonModForum.invalidateDiscussionsList(this.forum.id).finally(() => { if (data.discussionId) { // Discussion changed, search it in the list of discussions. - const discussion = this.discussions.items.find( - (disc) => this.discussions.getSource().isOnlineDiscussion(disc) && data.discussionId == disc.discussion, + const discussion = this.discussions?.items.find( + disc => this.discussions?.getSource().isOnlineDiscussion(disc) && data.discussionId == disc.discussion, ) as AddonModForumDiscussion; if (discussion) { @@ -233,7 +240,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom } 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. this.discussions.select(this.discussions[0]); } @@ -264,7 +271,9 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom async ngAfterViewInit(): Promise { await this.loadContent(false, true); - this.discussions.start(this.splitView); + const discussions = await this.promisedDiscussions; + + discussions.start(this.splitView); } /** @@ -281,7 +290,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom this.ratingOfflineObserver && this.ratingOfflineObserver.off(); this.ratingSyncObserver && this.ratingSyncObserver.off(); this.sourceUnsubscribe && this.sourceUnsubscribe(); - this.discussions.destroy(); + this.discussions?.destroy(); } /** @@ -304,8 +313,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom return; } + const discussions = await this.promisedDiscussions; + 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) => { this.hasOfflineRatings = hasRatings; @@ -331,7 +342,9 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom return; } - await this.discussions.getSource().loadForum(); + const discussions = await this.promisedDiscussions; + + await discussions.getSource().loadForum(); if (!this.forum) { return; @@ -379,7 +392,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom CoreGroups.instance .getActivityGroupMode(forum.cmid) .then(async mode => { - this.discussions.getSource().usesGroups = + discussions.getSource().usesGroups = mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS; return; @@ -431,10 +444,12 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * @return Promise resolved when done. */ async fetchMoreDiscussions(complete: () => void): Promise { + const discussions = await this.promisedDiscussions; + try { this.fetchFailed = false; - await this.discussions.load(); + await discussions.load(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true); @@ -462,10 +477,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom return value ? parseInt(value, 10) : null; }; + const discussions = await this.promisedDiscussions; const value = await getSortOrder(); 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) { this.sortOrderSelectorModalOptions.componentProps.selected = selectedOrder.value; @@ -542,19 +558,19 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom // If it's a new discussion in tablet mode, try to open it. if (isNewDiscussion && CoreScreen.isTablet) { const newDiscussionData = data as AddonModForumNewDiscussionData; - const discussion = this.discussions.items.find(disc => { - if (this.discussions.getSource().isOfflineDiscussion(disc)) { + const discussion = this.discussions?.items.find(disc => { + if (this.discussions?.getSource().isOfflineDiscussion(disc)) { 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 false; }); - if (discussion || !this.discussions.empty) { + if (this.discussions && (discussion || !this.discussions.empty)) { this.discussions.select(discussion ?? this.discussions.items[0]); } } @@ -571,7 +587,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * @param timeCreated Creation time of the offline discussion. */ openNewDiscussion(): void { - this.discussions.select(AddonModForumDiscussionsSource.NEW_DISCUSSION); + this.discussions?.select(AddonModForumDiscussionsSource.NEW_DISCUSSION); } /** @@ -580,7 +596,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * @param sortOrder Sort order new data. */ async setSortOrder(sortOrder: AddonModForumSortOrder): Promise { - 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().setDirty(true); From ec9ca06654228e566e094ced69b81981cbc7c5bd Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 9 Dec 2021 15:19:37 +0100 Subject: [PATCH 6/6] MOBILE-3833 addons: Improve manager initialization --- .../pages/issued-badge/issued-badge.page.ts | 23 ++++++------ .../mod/feedback/pages/attempt/attempt.ts | 36 ++++++++++--------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/addons/badges/pages/issued-badge/issued-badge.page.ts b/src/addons/badges/pages/issued-badge/issued-badge.page.ts index 4a9285995..1fc7a21be 100644 --- a/src/addons/badges/pages/issued-badge/issued-badge.page.ts +++ b/src/addons/badges/pages/issued-badge/issued-badge.page.ts @@ -43,29 +43,30 @@ export class AddonBadgesIssuedBadgePage implements OnInit { user?: CoreUserProfile; course?: CoreEnrolledCourseData; badge?: AddonBadgesUserBadge; - badges?: CoreSwipeNavigationItemsManager; + badges: CoreSwipeNavigationItemsManager; badgeLoaded = false; currentTime = 0; - constructor(protected route: ActivatedRoute) { } - - /** - * View loaded. - */ - ngOnInit(): void { + constructor(protected route: ActivatedRoute) { this.courseId = CoreNavigator.getRouteNumberParam('courseId') || this.courseId; // Use 0 for site badges. this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getRequiredCurrentSite().getUserId(); this.badgeHash = CoreNavigator.getRouteParam('badgeHash') || ''; - this.fetchIssuedBadge().finally(() => { - this.badgeLoaded = true; - }); - const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( AddonBadgesUserBadgesSource, [this.courseId, this.userId], ); + this.badges = new CoreSwipeNavigationItemsManager(source); + } + + /** + * View loaded. + */ + ngOnInit(): void { + this.fetchIssuedBadge().finally(() => { + this.badgeLoaded = true; + }); this.badges.start(); } diff --git a/src/addons/mod/feedback/pages/attempt/attempt.ts b/src/addons/mod/feedback/pages/attempt/attempt.ts index f406be269..35733c4ed 100644 --- a/src/addons/mod/feedback/pages/attempt/attempt.ts +++ b/src/addons/mod/feedback/pages/attempt/attempt.ts @@ -38,34 +38,36 @@ import { AddonModFeedbackFormItem, AddonModFeedbackHelper } from '../../services }) export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy { - protected attemptId!: number; - - cmId!: number; - courseId!: number; + cmId: number; + courseId: number; feedback?: AddonModFeedbackWSFeedback; attempt?: AddonModFeedbackWSAttempt; - attempts?: AddonModFeedbackAttemptsSwipeManager; + attempts: AddonModFeedbackAttemptsSwipeManager; anonAttempt?: AddonModFeedbackWSAnonAttempt; items: AddonModFeedbackAttemptItem[] = []; component = AddonModFeedbackProvider.COMPONENT; 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 */ ngOnInit(): void { try { - 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); @@ -82,7 +84,7 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy { * @inheritdoc */ ngOnDestroy(): void { - this.attempts?.destroy(); + this.attempts.destroy(); } /**