diff --git a/local_moodleappbehat/tests/behat/behat_app.php b/local_moodleappbehat/tests/behat/behat_app.php index befe86f47..0f3b92d5e 100644 --- a/local_moodleappbehat/tests/behat/behat_app.php +++ b/local_moodleappbehat/tests/behat/behat_app.php @@ -37,7 +37,14 @@ class behat_app extends behat_app_helper { protected $ionicurl = ''; /** @var array Config overrides */ - protected $appconfig = ['disableUserTours' => true]; + protected $appconfig = [ + 'disableUserTours' => true, + 'toastDurations' => [ // Extend toast durations in Behat so they don't disappear too soon. + 'short' => 7500, + 'long' => 10000, + 'sticky' => 0, + ], + ]; protected $windowsize = '360x720'; diff --git a/moodle.config.json b/moodle.config.json index ab1c55d95..13f142090 100644 --- a/moodle.config.json +++ b/moodle.config.json @@ -104,5 +104,10 @@ }, "wsrequestqueuelimit": 10, "wsrequestqueuedelay": 100, - "calendarreminderdefaultvalue": 3600 + "calendarreminderdefaultvalue": 3600, + "toastDurations": { + "short": 2000, + "long": 3500, + "sticky": 0 + } } diff --git a/scripts/langindex.json b/scripts/langindex.json index fd706d2f8..c61abf87b 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -654,6 +654,8 @@ "addon.mod_forum.numreplies": "local_moodlemobileapp", "addon.mod_forum.pindiscussion": "forum", "addon.mod_forum.pinupdated": "forum", + "addon.mod_forum.postaddedsuccess": "forum", + "addon.mod_forum.postingroup": "local_moodlemobileapp", "addon.mod_forum.postisprivatereply": "forum", "addon.mod_forum.posttoforum": "forum", "addon.mod_forum.posttomygroups": "forum", diff --git a/src/addons/calendar/pages/event/event.page.ts b/src/addons/calendar/pages/event/event.page.ts index ac9a3ac11..362c4332e 100644 --- a/src/addons/calendar/pages/event/event.page.ts +++ b/src/addons/calendar/pages/event/event.page.ts @@ -25,7 +25,7 @@ import { AddonCalendarOffline } from '../../services/calendar-offline'; import { AddonCalendarSync, AddonCalendarSyncEvents, AddonCalendarSyncProvider } from '../../services/calendar-sync'; import { CoreNetwork } from '@services/network'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreSites } from '@services/sites'; import { CoreLocalNotifications } from '@services/local-notifications'; @@ -556,7 +556,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { } if (onlineEventDeleted || this.event.id < 0) { - CoreDomUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000); + CoreDomUtils.showToast('addon.calendar.eventcalendareventdeleted', true, ToastDuration.LONG); // Event deleted, close the view. CoreNavigator.back(); @@ -611,7 +611,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { } if (data.deleted && data.deleted.indexOf(this.eventId) != -1) { - CoreDomUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000); + CoreDomUtils.showToast('addon.calendar.eventcalendareventdeleted', true, ToastDuration.LONG); // Event was deleted, close the view. CoreNavigator.back(); diff --git a/src/addons/mod/assign/pages/edit/edit.ts b/src/addons/mod/assign/pages/edit/edit.ts index a1a29932e..8cc7d519a 100644 --- a/src/addons/mod/assign/pages/edit/edit.ts +++ b/src/addons/mod/assign/pages/edit/edit.ts @@ -20,7 +20,7 @@ import { CanLeave } from '@guards/can-leave'; import { CoreNavigator } from '@services/navigator'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSync } from '@services/sync'; -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreFormFields, CoreForms } from '@singletons/form'; import { Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; @@ -467,7 +467,7 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave { async timeUp(): Promise { this.timeUpToast = await CoreDomUtils.showToastWithOptions({ message: Translate.instant('addon.mod_assign.caneditsubmission'), - duration: 0, + duration: ToastDuration.STICKY, buttons: [Translate.instant('core.dismiss')], cssClass: 'core-danger-toast', }); diff --git a/src/addons/mod/data/services/data-helper.ts b/src/addons/mod/data/services/data-helper.ts index e11bc25d9..6993f0210 100644 --- a/src/addons/mod/data/services/data-helper.ts +++ b/src/addons/mod/data/services/data-helper.ts @@ -19,7 +19,7 @@ import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fi import { CoreRatingOffline } from '@features/rating/services/rating-offline'; import { FileEntry } from '@ionic-native/file/ngx'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreFormFields } from '@singletons/form'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; @@ -173,7 +173,11 @@ export class AddonModDataHelperProvider { CoreEvents.trigger(AddonModDataProvider.ENTRY_CHANGED, { dataId: dataId, entryId: entryId }, siteId); - CoreDomUtils.showToast(approve ? 'addon.mod_data.recordapproved' : 'addon.mod_data.recorddisapproved', true, 3000); + CoreDomUtils.showToast( + approve ? 'addon.mod_data.recordapproved' : 'addon.mod_data.recorddisapproved', + true, + ToastDuration.LONG, + ); } catch { // Ignore error, it was already displayed. } finally { @@ -725,7 +729,7 @@ export class AddonModDataHelperProvider { CoreEvents.trigger(AddonModDataProvider.ENTRY_CHANGED, { dataId, entryId, deleted: true }, siteId); - CoreDomUtils.showToast('addon.mod_data.recorddeleted', true, 3000); + CoreDomUtils.showToast('addon.mod_data.recorddeleted', true, ToastDuration.LONG); modal.dismiss(); } catch { diff --git a/src/addons/mod/forum/classes/forum-discussions-source.ts b/src/addons/mod/forum/classes/forum-discussions-source.ts index 7ef18281f..f525f4d02 100644 --- a/src/addons/mod/forum/classes/forum-discussions-source.ts +++ b/src/addons/mod/forum/classes/forum-discussions-source.ts @@ -15,8 +15,11 @@ import { Params } from '@angular/router'; import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source'; import { CoreUser } from '@features/user/services/user'; +import { CoreGroupInfo, CoreGroups } from '@services/groups'; +import { CoreUtils } from '@services/utils/utils'; import { AddonModForum, + AddonModForumCanAddDiscussion, AddonModForumData, AddonModForumDiscussion, AddonModForumProvider, @@ -35,7 +38,12 @@ export class AddonModForumDiscussionsSource extends CoreRoutedItemsManagerSource forum?: AddonModForumData; trackPosts = false; usesGroups = false; + supportsChangeGroup = false; selectedSortOrder: AddonModForumSortOrder | null = null; + groupId = 0; + groupInfo?: CoreGroupInfo; + allPartsPermissions?: AddonModForumCanAddDiscussion; + canAddDiscussionToGroup = true; constructor(courseId: number, cmId: number, discussionsPathPrefix: string) { super(); @@ -94,12 +102,20 @@ export class AddonModForumDiscussionsSource extends CoreRoutedItemsManagerSource * @inheritdoc */ getItemQueryParams(discussion: AddonModForumDiscussionItem): Params { - return { + const params: Params = { courseId: this.COURSE_ID, cmId: this.CM_ID, forumId: this.forum?.id, - ...(this.isOnlineDiscussion(discussion) ? { discussion, trackPosts: this.trackPosts } : {}), }; + + if (this.isOnlineDiscussion(discussion)) { + params.discussion = discussion; + params.trackPosts = this.trackPosts; + } else if (this.isNewDiscussionForm(discussion)) { + params.groupId = this.usesGroups ? this.groupId : undefined; + } + + return params; } /** @@ -133,6 +149,42 @@ export class AddonModForumDiscussionsSource extends CoreRoutedItemsManagerSource } } + /** + * Load group info. + */ + async loadGroupInfo(forumId: number): Promise { + [this.groupInfo, this.allPartsPermissions] = await Promise.all([ + CoreGroups.getActivityGroupInfo(this.CM_ID, false), + CoreUtils.ignoreErrors(AddonModForum.canAddDiscussionToAll(forumId, { cmId: this.CM_ID })), + ]); + + this.supportsChangeGroup = AddonModForum.isGetDiscussionPostsAvailable(); + this.usesGroups = !!(this.groupInfo.separateGroups || this.groupInfo.visibleGroups); + this.groupId = CoreGroups.validateGroupId(this.groupId, this.groupInfo); + + await this.loadSelectedGroupData(); + } + + /** + * Load some specific data for current group. + * + * @return Promise resolved when done. + */ + async loadSelectedGroupData(): Promise { + if (!this.usesGroups) { + this.canAddDiscussionToGroup = true; + } else if (this.groupId === 0) { + this.canAddDiscussionToGroup = !this.allPartsPermissions || this.allPartsPermissions.status; + } else if (this.forum) { + const addDiscussionData = await AddonModForum.canAddDiscussion(this.forum.id, this.groupId, { cmId: this.CM_ID }); + + this.canAddDiscussionToGroup = addDiscussionData.status; + } else { + // Shouldn't happen, assume the user can. + this.canAddDiscussionToGroup = true; + } + } + /** * @inheritdoc */ @@ -174,6 +226,7 @@ export class AddonModForumDiscussionsSource extends CoreRoutedItemsManagerSource cmId: this.forum.cmid, sortOrder: this.selectedSortOrder.value, page, + groupId: this.groupId, }); let discussions = response.discussions; @@ -252,6 +305,36 @@ export class AddonModForumDiscussionsSource extends CoreRoutedItemsManagerSource return offlineDiscussions; } + /** + * Invalidate cache data. + * + * @return Promise resolved when done. + */ + async invalidateCache(): Promise { + const promises: Promise[] = []; + + promises.push(AddonModForum.invalidateForumData(this.COURSE_ID)); + + if (this.forum) { + promises.push(AddonModForum.invalidateDiscussionsList(this.forum.id)); + promises.push(AddonModForum.invalidateCanAddDiscussion(this.forum.id)); + promises.push(CoreGroups.invalidateActivityGroupInfo(this.forum.cmid)); + } + + await Promise.all(promises); + } + + /** + * Invalidate list cache data. + * + * @return Promise resolved when done. + */ + async invalidateList(): Promise { + if (this.forum) { + await AddonModForum.invalidateDiscussionsList(this.forum.id); + } + } + } /** diff --git a/src/addons/mod/forum/components/index/index.html b/src/addons/mod/forum/components/index/index.html index 5ddd4c954..cf24dd7d5 100644 --- a/src/addons/mod/forum/components/index/index.html +++ b/src/addons/mod/forum/components/index/index.html @@ -23,6 +23,20 @@ + + + {{'core.groupsseparate' | translate }} + {{'core.groupsvisible' | translate }} + + + + {{groupOpt.name}} + + + + @@ -39,6 +53,17 @@ + + + + + + {{ 'addon.mod_forum.cannotadddiscussionall' | translate }} + {{ 'addon.mod_forum.cannotadddiscussion' | translate }} + + + + @@ -128,7 +153,7 @@ - + {{ addDiscussionText }} diff --git a/src/addons/mod/forum/components/index/index.scss b/src/addons/mod/forum/components/index/index.scss index 4fb44d094..cb8ebcfdb 100644 --- a/src/addons/mod/forum/components/index/index.scss +++ b/src/addons/mod/forum/components/index/index.scss @@ -52,4 +52,8 @@ } + .core-group-selector { + border-top: 1px solid var(--spacer-color); + } + } diff --git a/src/addons/mod/forum/components/index/index.ts b/src/addons/mod/forum/components/index/index.ts index 33c844ddf..59aa22c5e 100644 --- a/src/addons/mod/forum/components/index/index.ts +++ b/src/addons/mod/forum/components/index/index.ts @@ -31,7 +31,7 @@ import { AddonModForumOffline } from '@addons/mod/forum/services/forum-offline'; import { Translate } from '@singletons'; import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper'; -import { CoreGroups, CoreGroupsProvider } from '@services/groups'; +import { CoreGroupInfo } from '@services/groups'; import { CoreEvents, CoreEventObserver } from '@singletons/events'; import { AddonModForumAutoSyncData, @@ -42,7 +42,6 @@ import { import { CoreSites } from '@services/sites'; import { CoreUser } from '@features/user/services/user'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUtils } from '@services/utils/utils'; import { CoreCourse } from '@features/course/services/course'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { AddonModForumDiscussionOptionsMenuComponent } from '../discussion-options-menu/discussion-options-menu'; @@ -82,9 +81,9 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom availabilityMessage: string | null = null; sortingAvailable!: boolean; sortOrders: AddonModForumSortOrder[] = []; - canPin = false; hasOfflineRatings = false; showQAMessage = false; + isSetPinAvailable = false; sortOrderSelectorModalOptions: ModalOptions = { component: AddonModForumSortOrderSelectorComponent, }; @@ -123,6 +122,36 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom return this.discussions?.getSource().selectedSortOrder ?? undefined; } + get supportsChangeGroup(): boolean { + return this.discussions?.getSource().supportsChangeGroup ?? false; + } + + get groupId(): number { + return this.discussions?.getSource().groupId ?? 0; + } + + set groupId(value: number) { + if (this.discussions) { + this.discussions.getSource().groupId = value; + } + } + + get groupInfo(): CoreGroupInfo | undefined { + return this.discussions?.getSource().groupInfo; + } + + get usesGroups(): boolean { + return !!(this.discussions?.getSource().usesGroups); + } + + get canPin(): boolean { + return !!(this.isSetPinAvailable && this.discussions?.getSource().allPartsPermissions?.canpindiscussions); + } + + get canAddDiscussionToGroup(): boolean { + return !!(this.forum && this.canAddDiscussion && this.discussions?.getSource().canAddDiscussionToGroup); + } + /** * Check whether a discussion is online. * @@ -150,6 +179,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom this.addDiscussionText = Translate.instant('addon.mod_forum.addanewdiscussion'); this.sortingAvailable = AddonModForum.isDiscussionListSortingAvailable(); this.sortOrders = AddonModForum.getAvailableSortOrders(); + this.isSetPinAvailable = AddonModForum.isSetPinStateAvailableForSite(); this.sortOrderSelectorModalOptions.componentProps = { sortOrders: this.sortOrders, @@ -383,19 +413,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom const promises: Promise[] = []; // Check if the activity uses groups. - promises.push( - CoreGroups.instance - .getActivityGroupMode(forum.cmid) - .then(async mode => { - discussions.getSource().usesGroups = - mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS; - - return; - }), - ); + promises.push(discussions.getSource().loadGroupInfo(forum.id)); promises.push( - AddonModForum.instance + AddonModForum .getAccessInformation(forum.id, { cmId: this.module.id }) .then(async accessInfo => { // Disallow adding discussions if cut-off date is reached and the user has not the @@ -410,26 +431,6 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom }), ); - if (AddonModForum.isSetPinStateAvailableForSite()) { - // Use the canAddDiscussion WS to check if the user can pin discussions. - promises.push( - AddonModForum.instance - .canAddDiscussionToAll(forum.id, { cmId: this.module.id }) - .then(async response => { - this.canPin = !!response.canpindiscussions; - - return; - }) - .catch(async () => { - this.canPin = false; - - return; - }), - ); - } else { - this.canPin = false; - } - await Promise.all(promises); } @@ -461,21 +462,8 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * @return Promise resolved when done. */ protected async fetchSortOrderPreference(): Promise { - const getSortOrder = async () => { - if (!this.sortingAvailable) { - return null; - } - - const value = await CoreUtils.ignoreErrors( - CoreUser.getUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER), - ); - - 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]; + const selectedOrder = await AddonModForum.getSelectedSortOrder(); discussions.getSource().selectedSortOrder = selectedOrder; @@ -492,11 +480,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom protected async invalidateContent(): Promise { const promises: Promise[] = []; - promises.push(AddonModForum.invalidateForumData(this.courseId)); + if (this.discussions) { + promises.push(this.discussions.getSource().invalidateCache()); + } if (this.forum) { - promises.push(AddonModForum.invalidateDiscussionsList(this.forum.id)); - promises.push(CoreGroups.invalidateActivityGroupMode(this.forum.cmid)); promises.push(AddonModForum.invalidateAccessInformation(this.forum.id)); } @@ -545,36 +533,52 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * @param isNewDiscussion Whether it's a new discussion event. * @param data Event data. */ - protected eventReceived( + protected async eventReceived( isNewDiscussion: boolean, data: AddonModForumNewDiscussionData | AddonModForumReplyDiscussionData, - ): void { - if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module.id) { - this.showLoadingAndRefresh(false).finally(() => { - // 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)) { - return disc.timecreated === newDiscussionData.discTimecreated; - } - - if (this.discussions?.getSource().isOnlineDiscussion(disc)) { - return (newDiscussionData.discussionIds ?? []).includes(disc.discussion); - } - - return false; - }); - - if (this.discussions && (discussion || !this.discussions.empty)) { - this.discussions.select(discussion ?? this.discussions.items[0]); - } - } - }); - - // Check completion since it could be configured to complete once the user adds a new discussion or replies. - this.checkCompletion(); + ): Promise { + if ((!this.forum || this.forum.id !== data.forumId) && data.cmId !== this.module.id) { + return; // Not current forum. } + + // Check completion since it could be configured to complete once the user adds a new discussion or replies. + this.checkCompletion(); + + try { + if (isNewDiscussion) { + CoreDomUtils.showToast('addon.mod_forum.postaddedsuccess', true); + + const newDiscGroupId = (data as AddonModForumNewDiscussionData).groupId; + + if (!newDiscGroupId || newDiscGroupId < 0 || !this.groupId || newDiscGroupId === this.groupId) { + await this.showLoadingAndRefresh(false); + } else { + // Discussion is in a different group than the one currently viewed, only invalidate data. + await this.discussions?.getSource().invalidateList(); + } + } else { + await this.showLoadingAndRefresh(false); + } + } finally { + // If it's a new discussion in tablet mode, try to open it. + if (isNewDiscussion && CoreScreen.isTablet && this.discussions) { + const newDiscussionData = data as AddonModForumNewDiscussionData; + const discussion = this.discussions.items.find(disc => { + if (this.discussions?.getSource().isOfflineDiscussion(disc)) { + return disc.timecreated === newDiscussionData.discTimecreated; + } + + if (this.discussions?.getSource().isOnlineDiscussion(disc)) { + return (newDiscussionData.discussionIds ?? []).includes(disc.discussion); + } + + return false; + }); + + this.discussions.select(discussion ?? null); + } + } + } /** @@ -661,6 +665,24 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom } } + /** + * Group has changed. + */ + async groupChanged(): Promise { + const modal = await CoreDomUtils.showModalLoading(); + + try { + await Promise.all([ + this.discussions?.getSource().loadSelectedGroupData(), + this.discussions?.reload(), + ]); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'core.errorloadingcontent', true); + } finally { + modal.dismiss(); + } + } + } /** @@ -696,7 +718,7 @@ class AddonModForumDiscussionsManager extends CoreListItemsManager{{ 'addon.mod_forum.group' | translate }} + [interfaceOptions]="{header: 'addon.mod_forum.group' | translate}" (ionChange)="calculateGroupName()"> {{ group.name }} @@ -66,7 +66,11 @@ [allowOffline]="true" [courseId]="courseId"> - + + + {{ 'addon.mod_forum.postingroup' | translate:{groupname: groupName} }} + + diff --git a/src/addons/mod/forum/pages/new-discussion/new-discussion.page.ts b/src/addons/mod/forum/pages/new-discussion/new-discussion.page.ts index bd1592ce4..a4bb114e9 100644 --- a/src/addons/mod/forum/pages/new-discussion/new-discussion.page.ts +++ b/src/addons/mod/forum/pages/new-discussion/new-discussion.page.ts @@ -61,6 +61,7 @@ type NewDiscussionData = { @Component({ selector: 'page-addon-mod-forum-new-discussion', templateUrl: 'new-discussion.html', + styleUrls: ['new-discussion.scss'], }) export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLeave { @@ -91,6 +92,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea advanced = false; // Display all form fields. accessInfo: AddonModForumAccessInformation = {}; courseId!: number; + groupName?: string; discussions?: AddonModForumNewDiscussionDiscussionsSwipeManager; @@ -102,6 +104,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea protected isDestroyed = false; protected originalData?: Partial; protected forceLeave = false; + protected initialGroupId?: number; constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {} @@ -115,6 +118,10 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); this.forumId = CoreNavigator.getRequiredRouteNumberParam('forumId'); this.timeCreated = CoreNavigator.getRequiredRouteNumberParam('timeCreated'); + this.initialGroupId = CoreNavigator.getRouteNumberParam('groupId'); + + // Discussion list uses 0 for all participants, but this page WebServices use a different value. Convert it. + this.initialGroupId = this.initialGroupId === 0 ? AddonModForumProvider.ALL_PARTICIPANTS : this.initialGroupId; if (this.timeCreated !== 0 && (routeData.swipeEnabled ?? true)) { const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( @@ -188,8 +195,9 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea this.groups = forumGroups; this.groupIds = forumGroups.map((group) => group.id).filter((id) => id > 0); // Do not override group id. - this.newDiscussion.groupId = this.newDiscussion.groupId || forumGroups[0].id; + this.newDiscussion.groupId = this.newDiscussion.groupId || this.getInitialGroupId(); this.showGroups = true; + this.calculateGroupName(); if (this.groupIds.length <= 1) { this.newDiscussion.postToAllGroups = false; } @@ -263,6 +271,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea this.newDiscussion.subscribe = !!discussion.options.discussionsubscribe; this.newDiscussion.pin = !!discussion.options.discussionpinned; this.messageControl.setValue(discussion.message); + this.calculateGroupName(); // Treat offline attachments if any. if (typeof discussion.options.attachmentsid === 'object' && discussion.options.attachmentsid.offline) { @@ -377,6 +386,16 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea return forumGroups.filter(forumGroup => userGroupsIds.indexOf(forumGroup.id) > -1); } + /** + * Get the initial group ID. + * + * @return Initial group ID. + */ + protected getInitialGroupId(): number { + return (this.initialGroupId && this.groups.find(group => group.id === this.initialGroupId)) ? + this.initialGroupId : this.groups[0].id; + } + /** * Add the "All participants" option to a list of groups if the user can add a discussion to all participants. * @@ -453,6 +472,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea cmId: this.cmId, discussionIds: discussionIds, discTimecreated: discTimecreated, + groupId: this.showGroups && !this.newDiscussion.postToAllGroups ? this.newDiscussion.groupId : undefined, }, CoreSites.getCurrentSiteId(), ); @@ -588,6 +608,17 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea this.advanced = !this.advanced; } + /** + * Calculate current group's name. + */ + calculateGroupName(): void { + if (this.newDiscussion.groupId <= 0) { + this.groupName = undefined; + } else { + this.groupName = this.groups.find(group => group.id === this.newDiscussion.groupId)?.name; + } + } + /** * Check if we can leave the page or not. * diff --git a/src/addons/mod/forum/pages/new-discussion/new-discussion.scss b/src/addons/mod/forum/pages/new-discussion/new-discussion.scss new file mode 100644 index 000000000..9ddfaed1c --- /dev/null +++ b/src/addons/mod/forum/pages/new-discussion/new-discussion.scss @@ -0,0 +1,22 @@ +@import "~theme/globals"; + +:host { + .addon-forum-group-info { + > ion-icon[slot] { + color: var(--ion-color-info); + @include margin-horizontal(null, 16px); + } + } + + .addon-forum-new-discussion-buttons { + ion-label { + margin-top: 0; + } + + ion-col { + padding-top: 0; + padding-bottom: 0; + } + } + +} diff --git a/src/addons/mod/forum/services/forum.ts b/src/addons/mod/forum/services/forum.ts index dc8d60079..9c2e1c989 100644 --- a/src/addons/mod/forum/services/forum.ts +++ b/src/addons/mod/forum/services/forum.ts @@ -159,19 +159,33 @@ export class AddonModForumProvider { return ROOT_CACHE_KEY + 'discussion:' + discussionId; } + /** + * Get common cache key for forum discussions list WS calls. + * + * @param forumId Forum ID. + * @return Cache key. + */ + protected getDiscussionsListCommonCacheKey(forumId: number): string { + return ROOT_CACHE_KEY + 'discussions:' + forumId; + } + /** * Get cache key for forum discussions list WS calls. * * @param forumId Forum ID. * @param sortOrder Sort order. + * @param groupId Group ID. * @return Cache key. */ - protected getDiscussionsListCacheKey(forumId: number, sortOrder: number): string { - let key = ROOT_CACHE_KEY + 'discussions:' + forumId; + protected getDiscussionsListCacheKey(forumId: number, sortOrder: number, groupId?: number): string { + let key = this.getDiscussionsListCommonCacheKey(forumId); if (sortOrder != AddonModForumProvider.SORTORDER_LASTPOST_DESC) { key += ':' + sortOrder; } + if (groupId) { + key += `:group${groupId}`; + } return key; } @@ -700,6 +714,26 @@ export class AddonModForumProvider { return sortOrders; } + /** + * Get sort order selected by the user. + * + * @return Promise resolved with sort order. + */ + async getSelectedSortOrder(): Promise { + const sortOrders = this.getAvailableSortOrders(); + let sortOrderValue: number | null = null; + + if (this.isDiscussionListSortingAvailable()) { + const preferenceValue = await CoreUtils.ignoreErrors( + CoreUser.getUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER), + ); + + sortOrderValue = preferenceValue ? parseInt(preferenceValue, 10) : null; + } + + return sortOrders.find(sortOrder => sortOrder.value === sortOrderValue) || sortOrders[0]; + } + /** * Get forum discussions. * @@ -729,6 +763,7 @@ export class AddonModForumProvider { // Since Moodle 3.7. method = 'mod_forum_get_forum_discussions'; (params as AddonModForumGetForumDiscussionsWSParams).sortorder = options.sortOrder; + (params as AddonModForumGetForumDiscussionsWSParams).groupid = options.groupId; } else { if (options.sortOrder !== AddonModForumProvider.SORTORDER_LASTPOST_DESC) { throw new Error('Sorting not supported with the old WS method.'); @@ -945,10 +980,7 @@ export class AddonModForumProvider { async invalidateDiscussionsList(forumId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); - await CoreUtils.allPromises( - this.getAvailableSortOrders() - .map(sortOrder => site.invalidateWsCacheForKey(this.getDiscussionsListCacheKey(forumId, sortOrder.value))), - ); + await site.invalidateWsCacheForKeyStartingWith(this.getDiscussionsListCommonCacheKey(forumId)); } /** @@ -1499,6 +1531,7 @@ export type AddonModForumLegacyPost = { export type AddonModForumGetDiscussionsOptions = CoreCourseCommonModWSOptions & { sortOrder?: number; // Sort order. page?: number; // Page. Defaults to 0. + groupId?: number; // Group ID. }; /** @@ -2079,6 +2112,7 @@ export type AddonModForumNewDiscussionData = { cmId: number; discussionIds?: number[] | null; discTimecreated?: number; + groupId?: number; // The discussion group if it's created in a certain group, ALL_PARTICIPANTS for all participants. }; /** diff --git a/src/addons/mod/forum/services/handlers/prefetch.ts b/src/addons/mod/forum/services/handlers/prefetch.ts index a460bc6e7..ed48fb29b 100644 --- a/src/addons/mod/forum/services/handlers/prefetch.ts +++ b/src/addons/mod/forum/services/handlers/prefetch.ts @@ -95,51 +95,80 @@ export class AddonModForumPrefetchHandlerService extends CoreCourseActivityPrefe * @param options Other options. * @return Promise resolved with array of posts. */ - protected getPostsForPrefetch( + protected async getPostsForPrefetch( forum: AddonModForumData, options: CoreCourseCommonModWSOptions = {}, ): Promise { - const promises = AddonModForum.getAvailableSortOrders().map((sortOrder) => { + // Only prefetch selected sort order. + const sortOrder = await AddonModForum.getSelectedSortOrder(); + + const groupsIds = await this.getGroupsIdsToPrefetch(forum); + + const results = await Promise.all(groupsIds.map(async (groupId) => { // Get discussions in first 2 pages. const discussionsOptions = { sortOrder: sortOrder.value, + groupId: groupId, numPages: 2, ...options, // Include all options. }; - return AddonModForum.getDiscussionsInPages(forum.id, discussionsOptions).then((response) => { - if (response.error) { - throw new Error('Failed getting discussions'); - } + const response = await AddonModForum.getDiscussionsInPages(forum.id, discussionsOptions); - const promises: Promise<{ posts: AddonModForumPost[] }>[] = []; + if (response.error) { + throw new Error('Failed getting discussions'); + } - response.discussions.forEach((discussion) => { - promises.push(AddonModForum.getDiscussionPosts(discussion.discussion, options)); + return await Promise.all( + response.discussions.map((discussion) => AddonModForum.getDiscussionPosts(discussion.discussion, options)), + ); + })); + + const posts: AddonModForumPost[] = []; + const postIds: Record = {}; // To make the array unique. + + results.forEach((groupResults) => { + groupResults.forEach((groupDiscussion) => { + groupDiscussion.posts.forEach((post) => { + if (!postIds[post.id]) { + postIds[post.id] = true; + posts.push(post); + } }); - - return Promise.all(promises); }); }); - return Promise.all(promises).then((results) => { - // Each order has returned its own list of posts. Merge all the lists, preventing duplicates. - const posts: AddonModForumPost[] = []; - const postIds = {}; // To make the array unique. + return posts; + } - results.forEach((orderResults) => { - orderResults.forEach((orderResult) => { - orderResult.posts.forEach((post) => { - if (!postIds[post.id]) { - postIds[post.id] = true; - posts.push(post); - } - }); - }); - }); + /** + * Get the group IDs to prefetch in a forum. + * Prefetch all participants if the user can view them. Otherwise, prefetch the groups the user can view. + * + * @param forum Forum instance. + * @return Promise resolved with array of group IDs. + */ + protected async getGroupsIdsToPrefetch(forum: AddonModForumData): Promise { + const groupInfo = await CoreGroups.getActivityGroupInfo(forum.cmid); - return posts; - }); + const supportsChangeGroup = AddonModForum.isGetDiscussionPostsAvailable(); + const usesGroups = !!(groupInfo.separateGroups || groupInfo.visibleGroups); + + if (!usesGroups) { + return [0]; + } + + const allPartsGroup = groupInfo.groups?.find(group => group.id === 0); + if (allPartsGroup) { + return [0]; // Prefetch all participants. + } + + if (!supportsChangeGroup) { + // Cannot change group, prefetch only the default group. + return [groupInfo.defaultGroupId]; + } + + return groupInfo.groups?.map(group => group.id) ?? [0]; } /** @@ -225,11 +254,6 @@ export class AddonModForumPrefetchHandlerService extends CoreCourseActivityPrefe // Prefetch access information. promises.push(AddonModForum.getAccessInformation(forum.id, modOptions)); - // Prefetch sort order preference. - if (AddonModForum.isDiscussionListSortingAvailable()) { - promises.push(CoreUser.getUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER, siteId)); - } - // Get course data, needed to determine upload max size if it's configured to be course limit. promises.push(CoreUtils.ignoreErrors(CoreCourses.getCourseByField('id', courseId, siteId))); @@ -269,34 +293,15 @@ export class AddonModForumPrefetchHandlerService extends CoreCourseActivityPrefe // Activity uses groups, prefetch allowed groups. const result = await CoreGroups.getActivityAllowedGroups(forum.cmid, undefined, siteId); - if (mode === CoreGroupsProvider.SEPARATEGROUPS) { - // Groups are already filtered by WS. Prefetch canAddDiscussionToAll to determine if user can pin/attach. - await CoreUtils.ignoreErrors(AddonModForum.canAddDiscussionToAll(forum.id, options)); - - return; - } - - if (canCreateDiscussions) { - // Prefetch data to check the visible groups when creating discussions. - const response = await CoreUtils.ignoreErrors( - AddonModForum.canAddDiscussionToAll(forum.id, options), - { status: false }, - ); - - if (response.status) { - // User can post to all groups, nothing else to prefetch. - return; - } - - // The user can't post to all groups, let's check which groups he can post to. - await Promise.all( - result.groups.map( - async (group) => CoreUtils.ignoreErrors( - AddonModForum.canAddDiscussion(forum.id, group.id, options), - ), + await Promise.all( + result.groups.map( + async (group) => CoreUtils.ignoreErrors( + AddonModForum.canAddDiscussion(forum.id, group.id, options), ), - ); - } + ).concat( + CoreUtils.ignoreErrors(AddonModForum.canAddDiscussionToAll(forum.id, options)), + ), + ); } catch (error) { // Ignore errors if cannot create discussions. if (canCreateDiscussions) { diff --git a/src/addons/mod/forum/tests/behat/basic_usage.feature b/src/addons/mod/forum/tests/behat/basic_usage.feature index c2f3553b4..afdb62ebc 100755 --- a/src/addons/mod/forum/tests/behat/basic_usage.feature +++ b/src/addons/mod/forum/tests/behat/basic_usage.feature @@ -23,6 +23,9 @@ Feature: Test basic usage of forum activity in app And the following "activities" exist: | activity | name | intro | course | idnumber | groupmode | assessed | scale | | forum | Test forum name | Test forum | C1 | forum | 0 | 1 | 1 | + And the following "mod_forum > discussions" exist: + | forum | name | subject | message | + | forum | Initial discussion | Initial discussion | Initial discussion message | Scenario: Create new discussion Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app @@ -36,105 +39,66 @@ Feature: Test basic usage of forum activity in app When I press "My happy subject" in the app Then I should find "An awesome message" in the app + Scenario: New discussion automatically opened in tablet + Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app + And I change viewport size to "1200x640" + + When I press "Add discussion topic" in the app + And I set the field "Subject" to "My happy subject" in the app + And I set the field "Message" to "An awesome message" in the app + And I press "Post to forum" in the app + Then I should find "My happy subject" in the app + And I should find "An awesome message" inside the split-view content in the app + Scenario: Reply a post Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app - When I press "Add discussion topic" in the app - And I set the following fields to these values in the app: - | Subject | DiscussionSubject | - | Message | DiscussionMessage | - And I press "Post to forum" in the app - And I press "DiscussionSubject" in the app - Then I should find "Reply" in the app - - When I press "Reply" in the app + When I press "Initial discussion" in the app + And I press "Reply" in the app And I set the field "Message" to "ReplyMessage" in the app And I press "Post to forum" in the app - Then I should find "DiscussionMessage" in the app + Then I should find "Initial discussion message" in the app And I should find "ReplyMessage" in the app Scenario: Star and pin discussions (student) Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app - When I press "Add discussion topic" in the app - And I set the following fields to these values in the app: - | Subject | starred subject | - | Message | starred message | - And I press "Post to forum" in the app - And I press "Add discussion topic" in the app - And I set the following fields to these values in the app: - | Subject | normal subject | - | Message | normal message | - And I press "Post to forum" in the app - And I press "starred subject" in the app - Then I should find "starred message" in the app - - When I press the back button in the app - And I press "Display options" near "starred subject" in the app + When I press "Display options" near "Initial discussion" in the app And I press "Star this discussion" in the app - And I press "starred subject" in the app - Then I should find "starred message" in the app + Then I should find "Your star option has been updated." in the app - When I press the back button in the app - And I press "normal subject" in the app - Then I should find "normal message" in the app - - When I press the back button in the app - And I press "Display options" near "starred subject" in the app + When I press "Display options" near "Initial discussion" in the app And I press "Unstar this discussion" in the app - And I press "starred subject" in the app - Then I should find "starred message" in the app + Then I should find "Your star option has been updated." in the app - When I press the back button in the app - And I press "normal subject" in the app - Then I should find "normal message" in the app + When I press "Display options" near "Initial discussion" in the app + Then I should not find "Pin this discussion" in the app Scenario: Star and pin discussions (teacher) Given I entered the forum activity "Test forum name" on course "Course 1" as "teacher1" in the app - When I press "Add discussion topic" in the app - And I set the following fields to these values in the app: - | Subject | Auto-test star | - | Message | Auto-test star message | - And I press "Post to forum" in the app - And I press "Add discussion topic" in the app - And I set the following fields to these values in the app: - | Subject | Auto-test pin | - | Message | Auto-test pin message | - And I press "Post to forum" in the app - And I press "Add discussion topic" in the app - And I set the following fields to these values in the app: - | Subject | Auto-test plain | - | Message | Auto-test plain message | - And I press "Post to forum" in the app - And I press "Display options" near "Auto-test star" in the app + When I press "Display options" near "Initial discussion" in the app And I press "Star this discussion" in the app - And I press "Display options" near "Auto-test pin" in the app - And I press "Pin this discussion" in the app - Then I should find "Auto-test pin" in the app - And I should find "Auto-test star" in the app - And I should find "Auto-test plain" in the app + Then I should find "Your star option has been updated." in the app - When I press "Display options" near "Auto-test pin" in the app - And I press "Unpin this discussion" in the app - And I press "Display options" near "Auto-test star" in the app + When I press "Display options" near "Initial discussion" in the app + And I press "Pin this discussion" in the app + Then I should find "The pin option has been updated." in the app + + When I press "Display options" near "Initial discussion" in the app And I press "Unstar this discussion" in the app - Then I should find "Auto-test star" in the app - And I should find "Auto-test pin" in the app + Then I should find "Your star option has been updated." in the app + + When I press "Display options" near "Initial discussion" in the app + And I press "Unpin this discussion" in the app + Then I should find "The pin option has been updated." in the app Scenario: Edit a not sent reply offline Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app - When I press "Add discussion topic" in the app - And I set the following fields to these values in the app: - | Subject | Auto-test | - | Message | Auto-test message | - And I press "Post to forum" in the app - And I press "Auto-test" near "Sort by last post creation date in descending order" in the app - And I should find "Reply" in the app + When I press "Initial discussion" in the app + Then I should find "Reply" in the app When I press the back button in the app And I switch offline mode to "true" - And I press "Auto-test" near "Sort by last post creation date in descending order" in the app - Then I should find "Reply" in the app - - When I press "Reply" in the app + And I press "Initial discussion" in the app + And I press "Reply" in the app And I set the field "Message" to "not sent reply" in the app And I press "Post to forum" in the app And I press "Display options" within "not sent reply" "ion-card" in the app @@ -148,7 +112,7 @@ Feature: Test basic usage of forum activity in app When I switch offline mode to "false" And I press the back button in the app - And I press "Auto-test" near "Sort by last post creation date in descending order" in the app + And I press "Initial discussion" in the app Then I should not find "Not sent" in the app And I should not find "This Discussion has offline data to be synchronised" in the app @@ -172,7 +136,7 @@ Feature: Test basic usage of forum activity in app When I press "Post to forum" in the app Then I should not find "This Forum has offline data to be synchronised." in the app - When I press "Auto-test" near "Sort by last post creation date in descending order" in the app + When I press "Auto-test" in the app And I should find "Auto-test message edited" in the app Scenario: Edit a forum post (only online) @@ -182,17 +146,8 @@ Feature: Test basic usage of forum activity in app | Subject | Auto-test | | Message | Auto-test message | And I press "Post to forum" in the app - Then I should find "Auto-test" in the app - - When I press the back button in the app - And I press "Course downloads" in the app - And I press "Download" within "Test forum name" "ion-item" in the app - And I press the back button in the app - And I press "Test forum name" in the app - And I press "Auto-test" near "Sort by last post creation date in descending order" in the app - Then I should find "Reply" in the app - - When I press "Display options" near "Reply" in the app + And I press "Auto-test" in the app + And I press "Display options" near "Reply" in the app Then I should find "Edit" in the app When I press "Edit" in the app @@ -201,6 +156,20 @@ Feature: Test basic usage of forum activity in app And I press "Save changes" in the app Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app + When I press "OK" in the app + And I press "Cancel" in the app + And I press "OK" in the app + And I press "Display options" near "Reply" in the app + And I press "Edit" in the app + Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app + + When I switch offline mode to "false" + And I press "OK" in the app + And I press "Edit" in the app + And I set the field "Message" to "Auto-test message edited" in the app + And I press "Save changes" in the app + Then I should find "Auto-test message edited" in the app + Scenario: Delete a forum post (only online) Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app When I press "Add discussion topic" in the app @@ -208,17 +177,8 @@ Feature: Test basic usage of forum activity in app | Subject | Auto-test | | Message | Auto-test message | And I press "Post to forum" in the app - Then I should find "Auto-test" in the app - - When I press the back button in the app - And I press "Course downloads" in the app - And I press "Download" within "Test forum name" "ion-item" in the app - And I press the back button in the app - And I press "Test forum name" in the app - And I press "Auto-test" near "Sort by last post creation date in descending order" in the app - Then I should find "Reply" in the app - - When I press "Display options" near "Reply" in the app + And I press "Auto-test" in the app + And I press "Display options" near "Reply" in the app Then I should find "Delete" in the app When I press "Delete" in the app @@ -246,18 +206,14 @@ Feature: Test basic usage of forum activity in app | Message | Auto-test message | And I press "Post to forum" in the app And I press "Auto-test" in the app - Then I should find "Reply" in the app - - When I press "Reply" in the app + And I press "Reply" in the app And I set the field "Message" to "test2" in the app And I press "Post to forum" in the app Then I should find "test2" "ion-card" in the app Given I entered the forum activity "Test forum name" on course "Course 1" as "teacher1" in the app When I press "Auto-test" in the app - Then I should find "Reply" in the app - - When I press "None" near "Auto-test message" in the app + And I press "None" near "Auto-test message" in the app And I press "1" near "Cancel" in the app And I switch offline mode to "true" And I press "None" near "test2" in the app @@ -287,31 +243,21 @@ Feature: Test basic usage of forum activity in app Scenario: Reply a post offline Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app - When I press "Add discussion topic" in the app - And I set the following fields to these values in the app: - | Subject | DiscussionSubject | - | Message | DiscussionMessage | - And I press "Post to forum" in the app - And I press the back button in the app - And I press "Course downloads" in the app - And I press "Download" within "Test forum name" "ion-item" in the app - And I press the back button in the app - And I press "Test forum name" in the app - And I press "DiscussionSubject" in the app + When I press "Initial discussion" in the app And I switch offline mode to "true" Then I should find "Reply" in the app When I press "Reply" in the app And I set the field "Message" to "ReplyMessage" in the app And I press "Post to forum" in the app - Then I should find "DiscussionMessage" in the app + Then I should find "Initial discussion message" in the app And I should find "ReplyMessage" in the app And I should find "Not sent" in the app When I press the back button in the app And I switch offline mode to "false" - And I press "DiscussionSubject" in the app - Then I should find "DiscussionMessage" in the app + And I press "Initial discussion" in the app + Then I should find "Initial discussion message" in the app And I should find "ReplyMessage" in the app But I should not find "Not sent" in the app @@ -332,7 +278,7 @@ Feature: Test basic usage of forum activity in app And I press "Test forum name" in the app And I press "Information" in the app And I press "Refresh" in the app - And I press "DiscussionSubject" near "Sort by last post creation date in descending order" in the app + And I press "DiscussionSubject" in the app Then I should find "DiscussionSubject" in the app And I should find "DiscussionMessage" in the app But I should not find "Not sent" in the app @@ -355,45 +301,32 @@ Feature: Test basic usage of forum activity in app And I wait loading to finish in the app Then I should not find "Not sent" in the app - When I press "DiscussionSubject" near "Sort by last post creation date in descending order" in the app + When I press "DiscussionSubject" in the app Then I should find "DiscussionSubject" in the app And I should find "DiscussionMessage" in the app But I should not find "Not sent" in the app And I should not find "This Forum has offline data to be synchronised." in the app Scenario: Prefetch - Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app - When I press "Add discussion topic" in the app - And I set the following fields to these values in the app: - | Subject | DiscussionSubject 1 | - | Message | DiscussionMessage 1 | - And I press "Post to forum" in the app - Then I should find "DiscussionSubject 1" in the app - - When I press the back button in the app - And I press "Course downloads" in the app + Given I entered the course "Course 1" as "student1" in the app + When I press "Course downloads" in the app And I press "Download" within "Test forum name" "ion-item" in the app Then I should find "Downloaded" within "Test forum name" "ion-item" in the app - And I press the back button in the app - - When I press "Test forum name" in the app - And I press "Add discussion topic" in the app - And I set the following fields to these values in the app: - | Subject | DiscussionSubject 2 | - | Message | DiscussionMessage 2 | - And I press "Post to forum" in the app - Then I should find "DiscussionSubject 1" in the app - And I should find "DiscussionSubject 2" in the app When I press the back button in the app And I switch offline mode to "true" And I press "Test forum name" in the app - And I press "DiscussionSubject 2" in the app - Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app + Then I should find "Initial discussion" in the app - When I press "OK" in the app - And I press the back button in the app - And I press "DiscussionSubject 1" in the app - Then I should find "DiscussionSubject 1" in the app - And I should find "DiscussionMessage 1" in the app - But I should not find "There was a problem connecting to the site. Please check your connection and try again." in the app + When I press "Initial discussion" in the app + Then I should find "Initial discussion" in the app + And I should find "Initial discussion message" in the app + + When I press the back button in the app + And I press "Add discussion topic" in the app + Then I should not find "There was a problem connecting to the site. Please check your connection and try again." in the app + + When I press the back button in the app + And I press "Sort by last post creation date in descending order" in the app + And I press "Sort by last post creation date in ascending order" in the app + Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app diff --git a/src/addons/mod/forum/tests/behat/groups.feature b/src/addons/mod/forum/tests/behat/groups.feature new file mode 100755 index 000000000..1d10056eb --- /dev/null +++ b/src/addons/mod/forum/tests/behat/groups.feature @@ -0,0 +1,369 @@ +@mod @mod_forum @app @javascript +Feature: Test usage of forum activity with groups in app + + Background: + Given the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "users" exist: + | username | + | student1 | + | teacher1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | teacher1 | C1 | editingteacher | + And the following "groups" exist: + | name | course | idnumber | + | Group 1 | C1 | G1 | + | Group 2 | C1 | G2 | + And the following "group members" exist: + | user | group | + | student1 | G1 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | groupmode | assessed | scale | + | forum | Separate groups forum | Test forum | C1 | forum | 1 | 1 | 1 | + | forum | Visible groups forum | Test forum | C1 | forum2 | 2 | 1 | 1 | + And the following "mod_forum > discussions" exist: + | forum | name | subject | message | group | + | forum | Disc sep G1 | Disc sep G1 | Disc sep G1 content | G1 | + | forum | Disc sep G2 | Disc sep G2 | Disc sep G2 content | G2 | + | forum | Disc sep ALL | Disc sep ALL | Disc sep ALL content | All participants | + | forum2 | Disc vis G1 | Disc vis G1 | Disc vis G1 content | G1 | + | forum2 | Disc vis G2 | Disc vis G2 | Disc vis G2 content | G2 | + | forum2 | Disc vis ALL | Disc vis ALL | Disc vis ALL content | All participants | + + Scenario: Student can only see the right groups + Given I entered the forum activity "Separate groups forum" on course "Course 1" as "student1" in the app + Then I should find "Disc sep G1" in the app + And I should find "Disc sep ALL" in the app + But I should not find "Disc sep G2" in the app + + When I press "Separate groups" in the app + Then I should find "Group 1" in the app + But I should not find "All participants" in the app + And I should not find "Group 2" in the app + + When I press "Group 1" in the app + And I press the back button in the app + And I press "Visible groups forum" in the app + And I press "Visible groups" in the app + Then I should find "All participants" in the app + And I should find "Group 1" in the app + And I should find "Group 2" in the app + + When I press "All participants" in the app + Then I should find "Disc vis G1" in the app + And I should find "Disc vis ALL" in the app + And I should find "Disc vis G2" in the app + + When I press "Visible groups" in the app + And I press "Group 1" in the app + Then I should find "Disc vis G1" in the app + And I should find "Disc vis ALL" in the app + But I should not find "Disc vis G2" in the app + + Scenario: Teacher can see all groups + Given I entered the forum activity "Separate groups forum" on course "Course 1" as "teacher1" in the app + When I press "Separate groups" in the app + Then I should find "All participants" in the app + And I should find "Group 1" in the app + And I should find "Group 2" in the app + + When I press "All participants" in the app + Then I should find "Disc sep G1" in the app + And I should find "Disc sep ALL" in the app + And I should find "Disc sep G2" in the app + + When I press "Separate groups" in the app + And I press "Group 1" in the app + Then I should find "Disc sep G1" in the app + And I should find "Disc sep ALL" in the app + But I should not find "Disc sep G2" in the app + + When I press "Separate groups" in the app + And I press "Group 2" in the app + Then I should find "Disc sep G2" in the app + And I should find "Disc sep ALL" in the app + But I should not find "Disc sep G1" in the app + + When I press the back button in the app + And I press "Visible groups forum" in the app + And I press "Visible groups" in the app + Then I should find "All participants" in the app + And I should find "Group 1" in the app + And I should find "Group 2" in the app + + When I press "All participants" in the app + Then I should find "Disc vis G1" in the app + And I should find "Disc vis ALL" in the app + And I should find "Disc vis G2" in the app + + When I press "Visible groups" in the app + And I press "Group 1" in the app + Then I should find "Disc vis G1" in the app + And I should find "Disc vis ALL" in the app + But I should not find "Disc vis G2" in the app + + Scenario: Student can only add discussions in his groups + Given I entered the forum activity "Separate groups forum" on course "Course 1" as "student1" in the app + When I press "Add discussion topic" in the app + And I press "Advanced" in the app + Then I should not find "Post a copy to all groups" in the app + And I should find "Posting in group \"Group 1\"" in the app + + When I press "Group" in the app + Then I should find "Group 1" in the app + But I should not find "All participants" in the app + And I should not find "Group 2" in the app + + When I press "Group 1" in the app + And I set the field "Subject" to "My happy subject" in the app + And I set the field "Message" to "An awesome message" in the app + And I press "Post to forum" in the app + Then I should find "Your post was successfully added" in the app + And I should find "My happy subject" in the app + + When I press the back button in the app + And I press "Visible groups forum" in the app + And I press "Visible groups" in the app + And I press "All participants" in the app + Then I should not find "Add discussion topic" in the app + But I should find "You do not have permission to add a new discussion topic for all participants" in the app + + When I press "Visible groups" in the app + And I press "Group 2" in the app + Then I should not find "Add discussion topic" in the app + But I should find "Adding discussions to this forum requires group membership" in the app + + When I press "Visible groups" in the app + And I press "Group 1" in the app + And I press "Add discussion topic" in the app + And I press "Advanced" in the app + Then I should not find "Post a copy to all groups" in the app + And I should find "Posting in group \"Group 1\"" in the app + + When I press "Group" in the app + Then I should find "Group 1" in the app + But I should not find "All participants" in the app + And I should not find "Group 2" in the app + + When I press "Group 1" in the app + And I set the field "Subject" to "My happy subject" in the app + And I set the field "Message" to "An awesome message" in the app + And I press "Post to forum" in the app + Then I should find "Your post was successfully added" in the app + And I should find "My happy subject" in the app + + When I press "Visible groups" in the app + And I press "Group 2" in the app + Then I should not find "My happy subject" in the app + + When I press "Visible groups" in the app + And I press "All participants" in the app + Then I should find "My happy subject" in the app + + Scenario: Teacher can add discussion to any group + Given I entered the forum activity "Separate groups forum" on course "Course 1" as "teacher1" in the app + And I press "Separate groups" in the app + And I press "All participants" in the app + And I press "Add discussion topic" in the app + And I press "Advanced" in the app + Then I should find "Post a copy to all groups" in the app + And I should find "All participants" in the app + But I should not find "Posting in group" in the app + + When I set the field "Subject" to "My first subject" in the app + And I set the field "Message" to "An awesome message" in the app + And I press "Post to forum" in the app + Then I should find "Your post was successfully added" in the app + And I should find "My first subject" in the app + + When I press "Separate groups" in the app + And I press "Group 1" in the app + Then I should find "My first subject" in the app + + When I press "Separate groups" in the app + And I press "Group 2" in the app + Then I should find "My first subject" in the app + + When I press "Add discussion topic" in the app + And I press "Advanced" in the app + Then I should find "Post a copy to all groups" in the app + And I should find "Posting in group \"Group 2\"" in the app + + When I set the field "Subject" to "My second subject" in the app + And I set the field "Message" to "An awesome message" in the app + And I press "Post to forum" in the app + Then I should find "Your post was successfully added" in the app + And I should find "My second subject" in the app + + When I press "Separate groups" in the app + And I press "Group 1" in the app + Then I should not find "My second subject" in the app + + When I press "Add discussion topic" in the app + Then I should find "Posting in group \"Group 1\"" in the app + + When I press "Advanced" in the app + And I press "Group" in the app + And I press "Group 2" in the app + Then I should find "Posting in group \"Group 2\"" in the app + + When I set the field "Subject" to "My third subject" in the app + And I set the field "Message" to "An awesome message" in the app + And I press "Post to forum" in the app + Then I should find "Your post was successfully added" in the app + But I should not find "My third subject" in the app + + When I press "Separate groups" in the app + And I press "Group 2" in the app + Then I should find "My third subject" in the app + + When I press the back button in the app + And I press "Visible groups forum" in the app + And I press "Visible groups" in the app + And I press "All participants" in the app + And I press "Add discussion topic" in the app + And I press "Advanced" in the app + Then I should find "Post a copy to all groups" in the app + And I should find "All participants" in the app + But I should not find "Posting in group" in the app + + When I set the field "Subject" to "My first subject" in the app + And I set the field "Message" to "An awesome message" in the app + And I press "Post to forum" in the app + Then I should find "Your post was successfully added" in the app + And I should find "My first subject" in the app + + When I press "Visible groups" in the app + And I press "Group 1" in the app + Then I should find "My first subject" in the app + + When I press "Visible groups" in the app + And I press "Group 2" in the app + Then I should find "My first subject" in the app + + When I press "Add discussion topic" in the app + And I press "Advanced" in the app + Then I should find "Post a copy to all groups" in the app + And I should find "Posting in group \"Group 2\"" in the app + + When I set the field "Subject" to "My second subject" in the app + And I set the field "Message" to "An awesome message" in the app + And I press "Post to forum" in the app + Then I should find "Your post was successfully added" in the app + And I should find "My second subject" in the app + + When I press "Visible groups" in the app + And I press "Group 1" in the app + Then I should not find "My second subject" in the app + + When I press "Add discussion topic" in the app + Then I should find "Posting in group \"Group 1\"" in the app + + When I press "Advanced" in the app + And I press "Group" in the app + And I press "Group 2" in the app + Then I should find "Posting in group \"Group 2\"" in the app + + When I set the field "Subject" to "My third subject" in the app + And I set the field "Message" to "An awesome message" in the app + And I press "Post to forum" in the app + Then I should find "Your post was successfully added" in the app + But I should not find "My third subject" in the app + + When I press "Visible groups" in the app + And I press "Group 2" in the app + Then I should find "My third subject" in the app + + Scenario: Teacher can post a copy in all groups + Given I entered the forum activity "Separate groups forum" on course "Course 1" as "teacher1" in the app + And I press "Separate groups" in the app + And I press "Group 1" in the app + And I press "Add discussion topic" in the app + And I press "Advanced" in the app + Then I should find "Post a copy to all groups" in the app + And I should find "Posting in group \"Group 1\"" in the app + + When I press "Post a copy to all groups" in the app + Then I should not find "Posting in group \"Group 1\"" in the app + + When I set the field "Subject" to "My happy subject" in the app + And I set the field "Message" to "An awesome message" in the app + And I press "Post to forum" in the app + Then I should find "Your post was successfully added" in the app + And I should find "My happy subject" in the app + + When I press "Separate groups" in the app + And I press "Group 2" in the app + Then I should find "My happy subject" in the app + + When I press the back button in the app + And I press "Visible groups forum" in the app + And I press "Visible groups" in the app + And I press "Group 1" in the app + And I press "Add discussion topic" in the app + And I press "Advanced" in the app + Then I should find "Post a copy to all groups" in the app + And I should find "Posting in group \"Group 1\"" in the app + + When I press "Post a copy to all groups" in the app + Then I should not find "Posting in group \"Group 1\"" in the app + + When I set the field "Subject" to "My happy subject" in the app + And I set the field "Message" to "An awesome message" in the app + And I press "Post to forum" in the app + Then I should find "Your post was successfully added" in the app + And I should find "My happy subject" in the app + + When I press "Visible groups" in the app + And I press "Group 2" in the app + Then I should find "My happy subject" in the app + + Scenario: New discussion not opened in tablet if not visible + Given I entered the forum activity "Separate groups forum" on course "Course 1" as "teacher1" in the app + And I change viewport size to "1200x640" + + When I press "Separate groups" in the app + And I press "Group 1" in the app + And I press "Add discussion topic" in the app + And I set the field "Subject" to "My happy subject" in the app + And I set the field "Message" to "An awesome message" in the app + And I press "Advanced" in the app + And I press "Group" near "Advanced" in the app + And I press "Group 2" in the app + And I press "Post to forum" in the app + Then I should not find "My happy subject" in the app + And I should not find "An awesome message" inside the split-view content in the app + + Scenario: Prefetch + Given I entered the course "Course 1" as "student1" in the app + When I press "Course downloads" in the app + And I press "Download" within "Separate groups" "ion-item" in the app + And I press "Download" within "Visible groups" "ion-item" in the app + Then I should find "Downloaded" within "Separate groups" "ion-item" in the app + And I should find "Downloaded" within "Visible groups" "ion-item" in the app + + When I press the back button in the app + And I switch offline mode to "true" + And I press "Separate groups forum" in the app + Then I should find "Disc sep G1" in the app + And I should be able to press "Add discussion topic" in the app + + When I press "Disc sep G1" in the app + Then I should find "Disc sep G1" in the app + And I should find "Disc sep G1 content" in the app + + When I press the back button in the app + And I press the back button in the app + And I press "Visible groups forum" in the app + Then I should find "Disc vis ALL" in the app + And I should find "Disc vis G1" in the app + And I should find "Disc vis G2" in the app + And I should not be able to press "Add discussion topic" in the app + And I should find "You do not have permission to add a new discussion topic for all participants." in the app + + When I press "Visible groups" in the app + And I press "Group 1" in the app + Then I should find "There was a problem connecting to the site. Please check your connection and try again." in the app diff --git a/src/addons/notes/components/add/add-modal.ts b/src/addons/notes/components/add/add-modal.ts index 46b38c524..ae840fd41 100644 --- a/src/addons/notes/components/add/add-modal.ts +++ b/src/addons/notes/components/add/add-modal.ts @@ -16,7 +16,7 @@ import { AddonNotes, AddonNotesPublishState } from '@addons/notes/services/notes import { Component, ViewChild, ElementRef, Input } from '@angular/core'; import { CoreApp } from '@services/app'; import { CoreSites } from '@services/sites'; -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreForms } from '@singletons/form'; import { ModalController } from '@singletons'; @@ -57,7 +57,7 @@ export class AddonNotesAddComponent { CoreForms.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.getCurrentSiteId()); ModalController.dismiss({ type: this.type, sent: true }).finally(() => { - CoreDomUtils.showToast(sent ? 'addon.notes.eventnotecreated' : 'core.datastoredoffline', true, 3000); + CoreDomUtils.showToast(sent ? 'addon.notes.eventnotecreated' : 'core.datastoredoffline', true, ToastDuration.LONG); }); } catch (error){ CoreDomUtils.showErrorModal(error); diff --git a/src/addons/notes/pages/list/list.page.ts b/src/addons/notes/pages/list/list.page.ts index 64c0a373d..8f5547af5 100644 --- a/src/addons/notes/pages/list/list.page.ts +++ b/src/addons/notes/pages/list/list.page.ts @@ -23,7 +23,7 @@ import { CoreUser, CoreUserProfile } from '@features/user/services/user'; import { IonContent, IonRefresher } from '@ionic/angular'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -232,7 +232,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy { this.refreshNotes(false); - CoreDomUtils.showToast('addon.notes.eventnotedeleted', true, 3000); + CoreDomUtils.showToast('addon.notes.eventnotedeleted', true, ToastDuration.LONG); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Delete note failed.'); diff --git a/src/core/classes/site.ts b/src/core/classes/site.ts index 58c994959..5fcd79060 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -29,7 +29,7 @@ import { CoreWSUploadFileResult, CoreWSPreSetsSplitRequest, } from '@services/ws'; -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUrlUtils, CoreUrlParams } from '@services/utils/url'; @@ -572,7 +572,7 @@ export class CoreSite { if (wsPreSets.cleanUnicode && CoreTextUtils.hasUnicodeData(data)) { // Data will be cleaned, notify the user. - CoreDomUtils.showToast('core.unicodenotsupported', true, 3000); + CoreDomUtils.showToast('core.unicodenotsupported', true, ToastDuration.LONG); } else { // No need to clean data in this call. wsPreSets.cleanUnicode = false; diff --git a/src/core/features/comments/pages/viewer/viewer.page.ts b/src/core/features/comments/pages/viewer/viewer.page.ts index 4b79eedfe..32084a2ac 100644 --- a/src/core/features/comments/pages/viewer/viewer.page.ts +++ b/src/core/features/comments/pages/viewer/viewer.page.ts @@ -31,7 +31,7 @@ import { ContextLevel, CoreConstants } from '@/core/constants'; import { CoreNavigator } from '@services/navigator'; import { NgZone, Translate } from '@singletons'; import { CoreUtils } from '@services/utils/utils'; -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreUser } from '@features/user/services/user'; import { CoreTextUtils } from '@services/utils/text'; import { CoreError } from '@classes/errors/error'; @@ -319,7 +319,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { CoreDomUtils.showToast( commentsResponse ? 'core.comments.eventcommentcreated' : 'core.datastoredoffline', true, - 3000, + ToastDuration.LONG, ); if (commentsResponse) { @@ -417,7 +417,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { this.invalidateComments(); - CoreDomUtils.showToast('core.comments.eventcommentdeleted', true, 3000); + CoreDomUtils.showToast('core.comments.eventcommentdeleted', true, ToastDuration.LONG); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'Delete comment failed.'); } diff --git a/src/core/features/rating/components/rate/rate.ts b/src/core/features/rating/components/rate/rate.ts index cf2c43639..fd87f9dd0 100644 --- a/src/core/features/rating/components/rate/rate.ts +++ b/src/core/features/rating/components/rate/rate.ts @@ -23,7 +23,7 @@ import { } from '@features/rating/services/rating'; import { CoreRatingOffline } from '@features/rating/services/rating-offline'; import { CoreSites } from '@services/sites'; -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -143,7 +143,7 @@ export class CoreRatingRateComponent implements OnChanges, OnDestroy { ); if (response === undefined) { - CoreDomUtils.showToast('core.datastoredoffline', true, 3000); + CoreDomUtils.showToast('core.datastoredoffline', true, ToastDuration.LONG); } else { this.onUpdate.emit(); } diff --git a/src/core/features/user/services/database/user.ts b/src/core/features/user/services/database/user.ts index 7fc2e7a1c..147351ef3 100644 --- a/src/core/features/user/services/database/user.ts +++ b/src/core/features/user/services/database/user.ts @@ -86,5 +86,5 @@ export type CoreUserDBRecord = CoreUserBasicData; export type CoreUserPreferenceDBRecord = { name: string; value: string; - onlinevalue: string; + onlinevalue: string | null; }; diff --git a/src/core/features/user/services/user-offline.ts b/src/core/features/user/services/user-offline.ts index 1eafad1c1..424d08a55 100644 --- a/src/core/features/user/services/user-offline.ts +++ b/src/core/features/user/services/user-offline.ts @@ -58,19 +58,12 @@ export class CoreUserOfflineProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - async setPreference(name: string, value: string, onlineValue?: string, siteId?: string): Promise { + async setPreference(name: string, value: string, onlineValue?: string | null , siteId?: string): Promise { const site = await CoreSites.getSite(siteId); - if (onlineValue === undefined) { - const preference = await this.getPreference(name, site.id); - - onlineValue = preference.onlinevalue; - } - - const record: CoreUserPreferenceDBRecord = { + const record: Partial = { name, value, - onlinevalue: onlineValue, }; await site.getDb().insertRecord(PREFERENCES_TABLE_NAME, record); diff --git a/src/core/features/user/services/user.ts b/src/core/features/user/services/user.ts index 44913b195..0be9e9a8a 100644 --- a/src/core/features/user/services/user.ts +++ b/src/core/features/user/services/user.ts @@ -1106,7 +1106,7 @@ type CoreUserGetUserPreferencesWSParams = { type CoreUserGetUserPreferencesWSResponse = { preferences: { // User custom fields (also known as user profile fields). name: string; // The name of the preference. - value: string; // The value of the preference. + value: string | null; // The value of the preference. }[]; warnings?: CoreWSExternalWarning[]; }; diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 7861e147a..70cd4c47a 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -1614,23 +1614,19 @@ export class CoreDomUtilsProvider { async showToast( text: string, needsTranslate?: boolean, - duration: number = 2000, + duration: ToastDuration | number = ToastDuration.SHORT, cssClass: string = '', ): Promise { if (needsTranslate) { text = Translate.instant(text); } - const loader = await ToastController.create({ + return this.showToastWithOptions({ message: text, duration: duration, position: 'bottom', cssClass: cssClass, }); - - await loader.present(); - - return loader; } /** @@ -1639,12 +1635,15 @@ export class CoreDomUtilsProvider { * @param options Options. * @return Promise resolved with Toast instance. */ - async showToastWithOptions(options: ToastOptions): Promise { - // Set some default values. - options.duration = options.duration ?? 2000; - options.position = options.position ?? 'bottom'; + async showToastWithOptions(options: ShowToastOptions): Promise { + // Convert some values and set default values. + const toastOptions: ToastOptions = { + ...options, + duration: CoreConstants.CONFIG.toastDurations[options.duration] ?? options.duration ?? 2000, + position: options.position ?? 'bottom', + }; - const loader = await ToastController.create(options); + const loader = await ToastController.create(toastOptions); await loader.present(); @@ -2130,3 +2129,19 @@ export enum VerticalPoint { MID = 'mid', BOTTOM = 'bottom', } + +/** + * Toast duration. + */ +export enum ToastDuration { + LONG = 'long', + SHORT = 'short', + STICKY = 'sticky', +} + +/** + * Options for showToastWithOptions. + */ +export type ShowToastOptions = Omit & { + duration: ToastDuration | number; +}; diff --git a/src/testing/services/behat-dom.ts b/src/testing/services/behat-dom.ts index 101cb9657..f872f3bac 100644 --- a/src/testing/services/behat-dom.ts +++ b/src/testing/services/behat-dom.ts @@ -89,7 +89,10 @@ export class TestingBehatDomUtilsService { text: string, options: TestingBehatFindOptions, ): ElementsWithExact[] { - const attributesSelector = `[aria-label*="${text}"], a[title*="${text}"], img[alt*="${text}"], [placeholder*="${text}"]`; + // Escape double quotes to prevent breaking the query selector. + const escapedText = text.replace(/"/g, '\\"'); + const attributesSelector = `[aria-label*="${escapedText}"], a[title*="${escapedText}"], ` + + `img[alt*="${escapedText}"], [placeholder*="${escapedText}"]`; const elements = Array.from(container.querySelectorAll(attributesSelector)) .filter((element => this.isElementVisible(element, container))) @@ -397,12 +400,13 @@ export class TestingBehatDomUtilsService { const withinElements = this.findElementsBasedOnTextInContainer(locator.within, topContainer, options); if (withinElements.length === 0) { - throw new Error('There was no match for within text'); + return []; } else if (withinElements.length > 1) { const withinElementsAncestors = this.getTopAncestors(withinElements); if (withinElementsAncestors.length > 1) { - throw new Error('Too many matches for within text ('+withinElementsAncestors.length+')'); + // Too many matches for within text. + return []; } topContainer = container = withinElementsAncestors[0]; @@ -418,12 +422,13 @@ export class TestingBehatDomUtilsService { }); if (nearElements.length === 0) { - throw new Error('There was no match for near text'); + return []; } else if (nearElements.length > 1) { const nearElementsAncestors = this.getTopAncestors(nearElements); if (nearElementsAncestors.length > 1) { - throw new Error('Too many matches for near text ('+nearElementsAncestors.length+')'); + // Too many matches for near text. + return []; } container = this.getParentElement(nearElementsAncestors[0]); diff --git a/src/types/config.d.ts b/src/types/config.d.ts index 75c1e84f3..571e3ecd3 100644 --- a/src/types/config.d.ts +++ b/src/types/config.d.ts @@ -18,6 +18,7 @@ import { CoreSitesDemoSiteData } from '@services/sites'; import { OpenFileAction } from '@services/utils/utils'; import { CoreLoginSiteSelectorListMethod } from '@features/login/services/login-helper'; import { CoreDatabaseConfiguration } from '@classes/database/database-table'; +import { ToastDuration } from '@services/utils/dom'; /* eslint-disable @typescript-eslint/naming-convention */ @@ -69,4 +70,5 @@ export interface EnvironmentConfig { calendarreminderdefaultvalue: number; // Initial value for default reminders (in seconds). User can change it later. removeaccountonlogout?: boolean; // True to remove the account when the user clicks logout. Doesn't affect switch account. uselegacycompletion?: boolean; // Whether to use legacy completion by default in all course formats. + toastDurations: Record; }