commit
						51410fd697
					
				| @ -37,7 +37,14 @@ class behat_app extends behat_app_helper { | |||||||
|     protected $ionicurl = ''; |     protected $ionicurl = ''; | ||||||
| 
 | 
 | ||||||
|     /** @var array Config overrides */ |     /** @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'; |     protected $windowsize = '360x720'; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -104,5 +104,10 @@ | |||||||
|     }, |     }, | ||||||
|     "wsrequestqueuelimit": 10, |     "wsrequestqueuelimit": 10, | ||||||
|     "wsrequestqueuedelay": 100, |     "wsrequestqueuedelay": 100, | ||||||
|     "calendarreminderdefaultvalue": 3600 |     "calendarreminderdefaultvalue": 3600, | ||||||
|  |     "toastDurations": { | ||||||
|  |         "short": 2000, | ||||||
|  |         "long": 3500, | ||||||
|  |         "sticky": 0 | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -654,6 +654,8 @@ | |||||||
|   "addon.mod_forum.numreplies": "local_moodlemobileapp", |   "addon.mod_forum.numreplies": "local_moodlemobileapp", | ||||||
|   "addon.mod_forum.pindiscussion": "forum", |   "addon.mod_forum.pindiscussion": "forum", | ||||||
|   "addon.mod_forum.pinupdated": "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.postisprivatereply": "forum", | ||||||
|   "addon.mod_forum.posttoforum": "forum", |   "addon.mod_forum.posttoforum": "forum", | ||||||
|   "addon.mod_forum.posttomygroups": "forum", |   "addon.mod_forum.posttomygroups": "forum", | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ import { AddonCalendarOffline } from '../../services/calendar-offline'; | |||||||
| import { AddonCalendarSync, AddonCalendarSyncEvents, AddonCalendarSyncProvider } from '../../services/calendar-sync'; | import { AddonCalendarSync, AddonCalendarSyncEvents, AddonCalendarSyncProvider } from '../../services/calendar-sync'; | ||||||
| import { CoreNetwork } from '@services/network'; | import { CoreNetwork } from '@services/network'; | ||||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | 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 { CoreTextUtils } from '@services/utils/text'; | ||||||
| import { CoreSites } from '@services/sites'; | import { CoreSites } from '@services/sites'; | ||||||
| import { CoreLocalNotifications } from '@services/local-notifications'; | import { CoreLocalNotifications } from '@services/local-notifications'; | ||||||
| @ -556,7 +556,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { | |||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (onlineEventDeleted || this.event.id < 0) { |             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.
 |                 // Event deleted, close the view.
 | ||||||
|                 CoreNavigator.back(); |                 CoreNavigator.back(); | ||||||
| @ -611,7 +611,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (data.deleted && data.deleted.indexOf(this.eventId) != -1) { |         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.
 |             // Event was deleted, close the view.
 | ||||||
|             CoreNavigator.back(); |             CoreNavigator.back(); | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ import { CanLeave } from '@guards/can-leave'; | |||||||
| import { CoreNavigator } from '@services/navigator'; | import { CoreNavigator } from '@services/navigator'; | ||||||
| import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; | import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; | ||||||
| import { CoreSync } from '@services/sync'; | 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 { CoreFormFields, CoreForms } from '@singletons/form'; | ||||||
| import { Translate } from '@singletons'; | import { Translate } from '@singletons'; | ||||||
| import { CoreEvents } from '@singletons/events'; | import { CoreEvents } from '@singletons/events'; | ||||||
| @ -467,7 +467,7 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave { | |||||||
|     async timeUp(): Promise<void> { |     async timeUp(): Promise<void> { | ||||||
|         this.timeUpToast = await CoreDomUtils.showToastWithOptions({ |         this.timeUpToast = await CoreDomUtils.showToastWithOptions({ | ||||||
|             message: Translate.instant('addon.mod_assign.caneditsubmission'), |             message: Translate.instant('addon.mod_assign.caneditsubmission'), | ||||||
|             duration: 0, |             duration: ToastDuration.STICKY, | ||||||
|             buttons: [Translate.instant('core.dismiss')], |             buttons: [Translate.instant('core.dismiss')], | ||||||
|             cssClass: 'core-danger-toast', |             cssClass: 'core-danger-toast', | ||||||
|         }); |         }); | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fi | |||||||
| import { CoreRatingOffline } from '@features/rating/services/rating-offline'; | import { CoreRatingOffline } from '@features/rating/services/rating-offline'; | ||||||
| import { FileEntry } from '@ionic-native/file/ngx'; | import { FileEntry } from '@ionic-native/file/ngx'; | ||||||
| import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; | 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 { CoreFormFields } from '@singletons/form'; | ||||||
| import { CoreTextUtils } from '@services/utils/text'; | import { CoreTextUtils } from '@services/utils/text'; | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| @ -173,7 +173,11 @@ export class AddonModDataHelperProvider { | |||||||
| 
 | 
 | ||||||
|             CoreEvents.trigger(AddonModDataProvider.ENTRY_CHANGED, { dataId: dataId, entryId: entryId }, siteId); |             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 { |         } catch { | ||||||
|             // Ignore error, it was already displayed.
 |             // Ignore error, it was already displayed.
 | ||||||
|         } finally { |         } finally { | ||||||
| @ -725,7 +729,7 @@ export class AddonModDataHelperProvider { | |||||||
| 
 | 
 | ||||||
|             CoreEvents.trigger(AddonModDataProvider.ENTRY_CHANGED, { dataId, entryId, deleted: true }, siteId); |             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(); |             modal.dismiss(); | ||||||
|         } catch { |         } catch { | ||||||
|  | |||||||
| @ -15,8 +15,11 @@ | |||||||
| import { Params } from '@angular/router'; | import { Params } from '@angular/router'; | ||||||
| import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source'; | import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source'; | ||||||
| import { CoreUser } from '@features/user/services/user'; | import { CoreUser } from '@features/user/services/user'; | ||||||
|  | import { CoreGroupInfo, CoreGroups } from '@services/groups'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
| import { | import { | ||||||
|     AddonModForum, |     AddonModForum, | ||||||
|  |     AddonModForumCanAddDiscussion, | ||||||
|     AddonModForumData, |     AddonModForumData, | ||||||
|     AddonModForumDiscussion, |     AddonModForumDiscussion, | ||||||
|     AddonModForumProvider, |     AddonModForumProvider, | ||||||
| @ -35,7 +38,12 @@ export class AddonModForumDiscussionsSource extends CoreRoutedItemsManagerSource | |||||||
|     forum?: AddonModForumData; |     forum?: AddonModForumData; | ||||||
|     trackPosts = false; |     trackPosts = false; | ||||||
|     usesGroups = false; |     usesGroups = false; | ||||||
|  |     supportsChangeGroup = false; | ||||||
|     selectedSortOrder: AddonModForumSortOrder | null = null; |     selectedSortOrder: AddonModForumSortOrder | null = null; | ||||||
|  |     groupId = 0; | ||||||
|  |     groupInfo?: CoreGroupInfo; | ||||||
|  |     allPartsPermissions?: AddonModForumCanAddDiscussion; | ||||||
|  |     canAddDiscussionToGroup = true; | ||||||
| 
 | 
 | ||||||
|     constructor(courseId: number, cmId: number, discussionsPathPrefix: string) { |     constructor(courseId: number, cmId: number, discussionsPathPrefix: string) { | ||||||
|         super(); |         super(); | ||||||
| @ -94,12 +102,20 @@ export class AddonModForumDiscussionsSource extends CoreRoutedItemsManagerSource | |||||||
|      * @inheritdoc |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
|     getItemQueryParams(discussion: AddonModForumDiscussionItem): Params { |     getItemQueryParams(discussion: AddonModForumDiscussionItem): Params { | ||||||
|         return { |         const params: Params = { | ||||||
|             courseId: this.COURSE_ID, |             courseId: this.COURSE_ID, | ||||||
|             cmId: this.CM_ID, |             cmId: this.CM_ID, | ||||||
|             forumId: this.forum?.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<void> { | ||||||
|  |         [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<void> { | ||||||
|  |         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 |      * @inheritdoc | ||||||
|      */ |      */ | ||||||
| @ -174,6 +226,7 @@ export class AddonModForumDiscussionsSource extends CoreRoutedItemsManagerSource | |||||||
|             cmId: this.forum.cmid, |             cmId: this.forum.cmid, | ||||||
|             sortOrder: this.selectedSortOrder.value, |             sortOrder: this.selectedSortOrder.value, | ||||||
|             page, |             page, | ||||||
|  |             groupId: this.groupId, | ||||||
|         }); |         }); | ||||||
|         let discussions = response.discussions; |         let discussions = response.discussions; | ||||||
| 
 | 
 | ||||||
| @ -252,6 +305,36 @@ export class AddonModForumDiscussionsSource extends CoreRoutedItemsManagerSource | |||||||
|         return offlineDiscussions; |         return offlineDiscussions; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Invalidate cache data. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async invalidateCache(): Promise<void> { | ||||||
|  |         const promises: Promise<void>[] = []; | ||||||
|  | 
 | ||||||
|  |         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<void> { | ||||||
|  |         if (this.forum) { | ||||||
|  |             await AddonModForum.invalidateDiscussionsList(this.forum.id); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | |||||||
| @ -23,6 +23,20 @@ | |||||||
|             </ion-item> |             </ion-item> | ||||||
|         </core-course-module-info> |         </core-course-module-info> | ||||||
| 
 | 
 | ||||||
|  |         <ion-item class="ion-text-wrap core-group-selector" lines="none" | ||||||
|  |             *ngIf="supportsChangeGroup && groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)"> | ||||||
|  |             <ion-label id="addon-forum-groupslabel"> | ||||||
|  |                 <ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container> | ||||||
|  |                 <ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container> | ||||||
|  |             </ion-label> | ||||||
|  |             <ion-select [(ngModel)]="groupId" (ionChange)="groupChanged()" aria-labelledby="addon-forum-groupslabel" | ||||||
|  |                 interface="action-sheet" [interfaceOptions]="{header: 'core.group' | translate}"> | ||||||
|  |                 <ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id"> | ||||||
|  |                     {{groupOpt.name}} | ||||||
|  |                 </ion-select-option> | ||||||
|  |             </ion-select> | ||||||
|  |         </ion-item> | ||||||
|  | 
 | ||||||
|         <!-- Cut-off date or due date message --> |         <!-- Cut-off date or due date message --> | ||||||
|         <ion-card class="core-info-card" *ngIf="availabilityMessage"> |         <ion-card class="core-info-card" *ngIf="availabilityMessage"> | ||||||
|             <ion-item> |             <ion-item> | ||||||
| @ -39,6 +53,17 @@ | |||||||
|             </ion-item> |             </ion-item> | ||||||
|         </ion-card> |         </ion-card> | ||||||
| 
 | 
 | ||||||
|  |         <!-- Cannot add discussion to group messages. --> | ||||||
|  |         <ion-card class="core-info-card" *ngIf="usesGroups && canAddDiscussion && !canAddDiscussionToGroup"> | ||||||
|  |             <ion-item> | ||||||
|  |                 <ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon> | ||||||
|  |                 <ion-label> | ||||||
|  |                     <span *ngIf="groupId <= 0">{{ 'addon.mod_forum.cannotadddiscussionall' | translate }}</span> | ||||||
|  |                     <span *ngIf="groupId > 0">{{ 'addon.mod_forum.cannotadddiscussion' | translate }}</span> | ||||||
|  |                 </ion-label> | ||||||
|  |             </ion-item> | ||||||
|  |         </ion-card> | ||||||
|  | 
 | ||||||
|         <ng-container *ngIf="forum"> |         <ng-container *ngIf="forum"> | ||||||
|             <core-empty-box *ngIf="!discussions || !discussions.hasDiscussions" icon="far-comments" |             <core-empty-box *ngIf="!discussions || !discussions.hasDiscussions" icon="far-comments" | ||||||
|                 [message]="'addon.mod_forum.forumnodiscussionsyet' | translate"> |                 [message]="'addon.mod_forum.forumnodiscussionsyet' | translate"> | ||||||
| @ -128,7 +153,7 @@ | |||||||
|     <core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModuleId]="module.id"> |     <core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModuleId]="module.id"> | ||||||
|     </core-course-module-navigation> |     </core-course-module-navigation> | ||||||
| 
 | 
 | ||||||
|     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="forum && canAddDiscussion"> |     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAddDiscussionToGroup"> | ||||||
|         <ion-fab-button (click)="openNewDiscussion()" [attr.aria-label]="addDiscussionText"> |         <ion-fab-button (click)="openNewDiscussion()" [attr.aria-label]="addDiscussionText"> | ||||||
|             <ion-icon name="fas-plus" aria-hidden="true"></ion-icon> |             <ion-icon name="fas-plus" aria-hidden="true"></ion-icon> | ||||||
|             <span class="sr-only">{{ addDiscussionText }}</span> |             <span class="sr-only">{{ addDiscussionText }}</span> | ||||||
|  | |||||||
| @ -52,4 +52,8 @@ | |||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     .core-group-selector { | ||||||
|  |         border-top: 1px solid var(--spacer-color); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -31,7 +31,7 @@ import { AddonModForumOffline } from '@addons/mod/forum/services/forum-offline'; | |||||||
| import { Translate } from '@singletons'; | import { Translate } from '@singletons'; | ||||||
| import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; | import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; | ||||||
| import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper'; | 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 { CoreEvents, CoreEventObserver } from '@singletons/events'; | ||||||
| import { | import { | ||||||
|     AddonModForumAutoSyncData, |     AddonModForumAutoSyncData, | ||||||
| @ -42,7 +42,6 @@ import { | |||||||
| import { CoreSites } from '@services/sites'; | import { CoreSites } from '@services/sites'; | ||||||
| import { CoreUser } from '@features/user/services/user'; | import { CoreUser } from '@features/user/services/user'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
| import { CoreUtils } from '@services/utils/utils'; |  | ||||||
| import { CoreCourse } from '@features/course/services/course'; | import { CoreCourse } from '@features/course/services/course'; | ||||||
| import { CoreSplitViewComponent } from '@components/split-view/split-view'; | import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||||
| import { AddonModForumDiscussionOptionsMenuComponent } from '../discussion-options-menu/discussion-options-menu'; | import { AddonModForumDiscussionOptionsMenuComponent } from '../discussion-options-menu/discussion-options-menu'; | ||||||
| @ -82,9 +81,9 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
|     availabilityMessage: string | null = null; |     availabilityMessage: string | null = null; | ||||||
|     sortingAvailable!: boolean; |     sortingAvailable!: boolean; | ||||||
|     sortOrders: AddonModForumSortOrder[] = []; |     sortOrders: AddonModForumSortOrder[] = []; | ||||||
|     canPin = false; |  | ||||||
|     hasOfflineRatings = false; |     hasOfflineRatings = false; | ||||||
|     showQAMessage = false; |     showQAMessage = false; | ||||||
|  |     isSetPinAvailable = false; | ||||||
|     sortOrderSelectorModalOptions: ModalOptions = { |     sortOrderSelectorModalOptions: ModalOptions = { | ||||||
|         component: AddonModForumSortOrderSelectorComponent, |         component: AddonModForumSortOrderSelectorComponent, | ||||||
|     }; |     }; | ||||||
| @ -123,6 +122,36 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
|         return this.discussions?.getSource().selectedSortOrder ?? undefined; |         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. |      * Check whether a discussion is online. | ||||||
|      * |      * | ||||||
| @ -150,6 +179,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
|         this.addDiscussionText = Translate.instant('addon.mod_forum.addanewdiscussion'); |         this.addDiscussionText = Translate.instant('addon.mod_forum.addanewdiscussion'); | ||||||
|         this.sortingAvailable = AddonModForum.isDiscussionListSortingAvailable(); |         this.sortingAvailable = AddonModForum.isDiscussionListSortingAvailable(); | ||||||
|         this.sortOrders = AddonModForum.getAvailableSortOrders(); |         this.sortOrders = AddonModForum.getAvailableSortOrders(); | ||||||
|  |         this.isSetPinAvailable = AddonModForum.isSetPinStateAvailableForSite(); | ||||||
| 
 | 
 | ||||||
|         this.sortOrderSelectorModalOptions.componentProps = { |         this.sortOrderSelectorModalOptions.componentProps = { | ||||||
|             sortOrders: this.sortOrders, |             sortOrders: this.sortOrders, | ||||||
| @ -383,19 +413,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
|         const promises: Promise<void>[] = []; |         const promises: Promise<void>[] = []; | ||||||
| 
 | 
 | ||||||
|         // Check if the activity uses groups.
 |         // Check if the activity uses groups.
 | ||||||
|         promises.push( |         promises.push(discussions.getSource().loadGroupInfo(forum.id)); | ||||||
|             CoreGroups.instance |  | ||||||
|                 .getActivityGroupMode(forum.cmid) |  | ||||||
|                 .then(async mode => { |  | ||||||
|                     discussions.getSource().usesGroups = |  | ||||||
|                         mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS; |  | ||||||
| 
 |  | ||||||
|                     return; |  | ||||||
|                 }), |  | ||||||
|         ); |  | ||||||
| 
 | 
 | ||||||
|         promises.push( |         promises.push( | ||||||
|             AddonModForum.instance |             AddonModForum | ||||||
|                 .getAccessInformation(forum.id, { cmId: this.module.id }) |                 .getAccessInformation(forum.id, { cmId: this.module.id }) | ||||||
|                 .then(async accessInfo => { |                 .then(async accessInfo => { | ||||||
|                     // Disallow adding discussions if cut-off date is reached and the user has not the
 |                     // 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); |         await Promise.all(promises); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -461,21 +462,8 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     protected async fetchSortOrderPreference(): Promise<void> { |     protected async fetchSortOrderPreference(): Promise<void> { | ||||||
|         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 discussions = await this.promisedDiscussions; | ||||||
|         const value = await getSortOrder(); |         const selectedOrder = await AddonModForum.getSelectedSortOrder(); | ||||||
|         const selectedOrder = this.sortOrders.find(sortOrder => sortOrder.value === value) || this.sortOrders[0]; |  | ||||||
| 
 | 
 | ||||||
|         discussions.getSource().selectedSortOrder = selectedOrder; |         discussions.getSource().selectedSortOrder = selectedOrder; | ||||||
| 
 | 
 | ||||||
| @ -492,11 +480,11 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
|     protected async invalidateContent(): Promise<void> { |     protected async invalidateContent(): Promise<void> { | ||||||
|         const promises: Promise<void>[] = []; |         const promises: Promise<void>[] = []; | ||||||
| 
 | 
 | ||||||
|         promises.push(AddonModForum.invalidateForumData(this.courseId)); |         if (this.discussions) { | ||||||
|  |             promises.push(this.discussions.getSource().invalidateCache()); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         if (this.forum) { |         if (this.forum) { | ||||||
|             promises.push(AddonModForum.invalidateDiscussionsList(this.forum.id)); |  | ||||||
|             promises.push(CoreGroups.invalidateActivityGroupMode(this.forum.cmid)); |  | ||||||
|             promises.push(AddonModForum.invalidateAccessInformation(this.forum.id)); |             promises.push(AddonModForum.invalidateAccessInformation(this.forum.id)); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -545,16 +533,37 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
|      * @param isNewDiscussion Whether it's a new discussion event. |      * @param isNewDiscussion Whether it's a new discussion event. | ||||||
|      * @param data Event data. |      * @param data Event data. | ||||||
|      */ |      */ | ||||||
|     protected eventReceived( |     protected async eventReceived( | ||||||
|         isNewDiscussion: boolean, |         isNewDiscussion: boolean, | ||||||
|         data: AddonModForumNewDiscussionData | AddonModForumReplyDiscussionData, |         data: AddonModForumNewDiscussionData | AddonModForumReplyDiscussionData, | ||||||
|     ): void { |     ): Promise<void> { | ||||||
|         if ((this.forum && this.forum.id === data.forumId) || data.cmId === this.module.id) { |         if ((!this.forum || this.forum.id !== data.forumId) && data.cmId !== this.module.id) { | ||||||
|             this.showLoadingAndRefresh(false).finally(() => { |             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 it's a new discussion in tablet mode, try to open it.
 | ||||||
|                 if (isNewDiscussion && CoreScreen.isTablet) { |             if (isNewDiscussion && CoreScreen.isTablet && this.discussions) { | ||||||
|                 const newDiscussionData = data as AddonModForumNewDiscussionData; |                 const newDiscussionData = data as AddonModForumNewDiscussionData; | ||||||
|                     const discussion = this.discussions?.items.find(disc => { |                 const discussion = this.discussions.items.find(disc => { | ||||||
|                     if (this.discussions?.getSource().isOfflineDiscussion(disc)) { |                     if (this.discussions?.getSource().isOfflineDiscussion(disc)) { | ||||||
|                         return disc.timecreated === newDiscussionData.discTimecreated; |                         return disc.timecreated === newDiscussionData.discTimecreated; | ||||||
|                     } |                     } | ||||||
| @ -566,15 +575,10 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
|                     return false; |                     return false; | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|                     if (this.discussions && (discussion || !this.discussions.empty)) { |                 this.discussions.select(discussion ?? null); | ||||||
|                         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(); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -661,6 +665,24 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Group has changed. | ||||||
|  |      */ | ||||||
|  |     async groupChanged(): Promise<void> { | ||||||
|  |         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<AddonModForum | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|             await AddonModForum.instance.logView(forum.id, forum.name); |             await AddonModForum.logView(forum.id, forum.name); | ||||||
| 
 | 
 | ||||||
|             CoreCourse.checkModuleCompletion(this.page.courseId, this.page.module.completiondata); |             CoreCourse.checkModuleCompletion(this.page.courseId, this.page.module.completiondata); | ||||||
|         } catch { |         } catch { | ||||||
|  | |||||||
| @ -45,6 +45,8 @@ | |||||||
|     "numreplies": "{{numreplies}} replies", |     "numreplies": "{{numreplies}} replies", | ||||||
|     "pindiscussion": "Pin this discussion", |     "pindiscussion": "Pin this discussion", | ||||||
|     "pinupdated": "The pin option has been updated.", |     "pinupdated": "The pin option has been updated.", | ||||||
|  |     "postaddedsuccess": "Your post was successfully added.", | ||||||
|  |     "postingroup": "Posting in group \"{{groupname}}\".", | ||||||
|     "postisprivatereply": "This is a private reply. It is only visible to you and anyone with the capability to view private replies, such as teachers or managers.", |     "postisprivatereply": "This is a private reply. It is only visible to you and anyone with the capability to view private replies, such as teachers or managers.", | ||||||
|     "posttoforum": "Post to forum", |     "posttoforum": "Post to forum", | ||||||
|     "posttomygroups": "Post a copy to all groups", |     "posttomygroups": "Post a copy to all groups", | ||||||
|  | |||||||
| @ -49,7 +49,7 @@ | |||||||
|                     <ion-label id="addon-mod-forum-groupslabel">{{ 'addon.mod_forum.group' | translate }}</ion-label> |                     <ion-label id="addon-mod-forum-groupslabel">{{ 'addon.mod_forum.group' | translate }}</ion-label> | ||||||
|                     <ion-select [(ngModel)]="newDiscussion.groupId" [disabled]="newDiscussion.postToAllGroups" |                     <ion-select [(ngModel)]="newDiscussion.groupId" [disabled]="newDiscussion.postToAllGroups" | ||||||
|                         aria-labelledby="addon-mod-forum-groupslabel" interface="action-sheet" name="groupid" |                         aria-labelledby="addon-mod-forum-groupslabel" interface="action-sheet" name="groupid" | ||||||
|                         [interfaceOptions]="{header: 'addon.mod_forum.group' | translate}"> |                         [interfaceOptions]="{header: 'addon.mod_forum.group' | translate}" (ionChange)="calculateGroupName()"> | ||||||
|                         <ion-select-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-select-option> |                         <ion-select-option *ngFor="let group of groups" [value]="group.id">{{ group.name }}</ion-select-option> | ||||||
|                     </ion-select> |                     </ion-select> | ||||||
|                 </ion-item> |                 </ion-item> | ||||||
| @ -66,7 +66,11 @@ | |||||||
|                     [allowOffline]="true" [courseId]="courseId"> |                     [allowOffline]="true" [courseId]="courseId"> | ||||||
|                 </core-attachments> |                 </core-attachments> | ||||||
|             </div> |             </div> | ||||||
|             <ion-item> |             <ion-item *ngIf="showGroups && groupName && !newDiscussion.postToAllGroups" class="addon-forum-group-info"> | ||||||
|  |                 <ion-icon name="fas-info-circle" slot="start" aria-hidden="true"></ion-icon> | ||||||
|  |                 <ion-label>{{ 'addon.mod_forum.postingroup' | translate:{groupname: groupName} }}</ion-label> | ||||||
|  |             </ion-item> | ||||||
|  |             <ion-item class="addon-forum-new-discussion-buttons"> | ||||||
|                 <ion-label> |                 <ion-label> | ||||||
|                     <ion-row> |                     <ion-row> | ||||||
|                         <ion-col *ngIf="hasOffline"> |                         <ion-col *ngIf="hasOffline"> | ||||||
|  | |||||||
| @ -61,6 +61,7 @@ type NewDiscussionData = { | |||||||
| @Component({ | @Component({ | ||||||
|     selector: 'page-addon-mod-forum-new-discussion', |     selector: 'page-addon-mod-forum-new-discussion', | ||||||
|     templateUrl: 'new-discussion.html', |     templateUrl: 'new-discussion.html', | ||||||
|  |     styleUrls: ['new-discussion.scss'], | ||||||
| }) | }) | ||||||
| export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLeave { | export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLeave { | ||||||
| 
 | 
 | ||||||
| @ -91,6 +92,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea | |||||||
|     advanced = false; // Display all form fields.
 |     advanced = false; // Display all form fields.
 | ||||||
|     accessInfo: AddonModForumAccessInformation = {}; |     accessInfo: AddonModForumAccessInformation = {}; | ||||||
|     courseId!: number; |     courseId!: number; | ||||||
|  |     groupName?: string; | ||||||
| 
 | 
 | ||||||
|     discussions?: AddonModForumNewDiscussionDiscussionsSwipeManager; |     discussions?: AddonModForumNewDiscussionDiscussionsSwipeManager; | ||||||
| 
 | 
 | ||||||
| @ -102,6 +104,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea | |||||||
|     protected isDestroyed = false; |     protected isDestroyed = false; | ||||||
|     protected originalData?: Partial<NewDiscussionData>; |     protected originalData?: Partial<NewDiscussionData>; | ||||||
|     protected forceLeave = false; |     protected forceLeave = false; | ||||||
|  |     protected initialGroupId?: number; | ||||||
| 
 | 
 | ||||||
|     constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {} |     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.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); | ||||||
|             this.forumId = CoreNavigator.getRequiredRouteNumberParam('forumId'); |             this.forumId = CoreNavigator.getRequiredRouteNumberParam('forumId'); | ||||||
|             this.timeCreated = CoreNavigator.getRequiredRouteNumberParam('timeCreated'); |             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)) { |             if (this.timeCreated !== 0 && (routeData.swipeEnabled ?? true)) { | ||||||
|                 const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( |                 const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( | ||||||
| @ -188,8 +195,9 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea | |||||||
|                                     this.groups = forumGroups; |                                     this.groups = forumGroups; | ||||||
|                                     this.groupIds = forumGroups.map((group) => group.id).filter((id) => id > 0); |                                     this.groupIds = forumGroups.map((group) => group.id).filter((id) => id > 0); | ||||||
|                                     // Do not override group id.
 |                                     // 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.showGroups = true; | ||||||
|  |                                     this.calculateGroupName(); | ||||||
|                                     if (this.groupIds.length <= 1) { |                                     if (this.groupIds.length <= 1) { | ||||||
|                                         this.newDiscussion.postToAllGroups = false; |                                         this.newDiscussion.postToAllGroups = false; | ||||||
|                                     } |                                     } | ||||||
| @ -263,6 +271,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea | |||||||
|                             this.newDiscussion.subscribe = !!discussion.options.discussionsubscribe; |                             this.newDiscussion.subscribe = !!discussion.options.discussionsubscribe; | ||||||
|                             this.newDiscussion.pin = !!discussion.options.discussionpinned; |                             this.newDiscussion.pin = !!discussion.options.discussionpinned; | ||||||
|                             this.messageControl.setValue(discussion.message); |                             this.messageControl.setValue(discussion.message); | ||||||
|  |                             this.calculateGroupName(); | ||||||
| 
 | 
 | ||||||
|                             // Treat offline attachments if any.
 |                             // Treat offline attachments if any.
 | ||||||
|                             if (typeof discussion.options.attachmentsid === 'object' && discussion.options.attachmentsid.offline) { |                             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); |         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. |      * 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, |                 cmId: this.cmId, | ||||||
|                 discussionIds: discussionIds, |                 discussionIds: discussionIds, | ||||||
|                 discTimecreated: discTimecreated, |                 discTimecreated: discTimecreated, | ||||||
|  |                 groupId: this.showGroups && !this.newDiscussion.postToAllGroups ? this.newDiscussion.groupId : undefined, | ||||||
|             }, |             }, | ||||||
|             CoreSites.getCurrentSiteId(), |             CoreSites.getCurrentSiteId(), | ||||||
|         ); |         ); | ||||||
| @ -588,6 +608,17 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea | |||||||
|         this.advanced = !this.advanced; |         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. |      * Check if we can leave the page or not. | ||||||
|      * |      * | ||||||
|  | |||||||
| @ -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; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -159,19 +159,33 @@ export class AddonModForumProvider { | |||||||
|         return ROOT_CACHE_KEY + 'discussion:' + discussionId; |         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. |      * Get cache key for forum discussions list WS calls. | ||||||
|      * |      * | ||||||
|      * @param forumId Forum ID. |      * @param forumId Forum ID. | ||||||
|      * @param sortOrder Sort order. |      * @param sortOrder Sort order. | ||||||
|  |      * @param groupId Group ID. | ||||||
|      * @return Cache key. |      * @return Cache key. | ||||||
|      */ |      */ | ||||||
|     protected getDiscussionsListCacheKey(forumId: number, sortOrder: number): string { |     protected getDiscussionsListCacheKey(forumId: number, sortOrder: number, groupId?: number): string { | ||||||
|         let key = ROOT_CACHE_KEY + 'discussions:' + forumId; |         let key = this.getDiscussionsListCommonCacheKey(forumId); | ||||||
| 
 | 
 | ||||||
|         if (sortOrder != AddonModForumProvider.SORTORDER_LASTPOST_DESC) { |         if (sortOrder != AddonModForumProvider.SORTORDER_LASTPOST_DESC) { | ||||||
|             key += ':' + sortOrder; |             key += ':' + sortOrder; | ||||||
|         } |         } | ||||||
|  |         if (groupId) { | ||||||
|  |             key += `:group${groupId}`; | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         return key; |         return key; | ||||||
|     } |     } | ||||||
| @ -700,6 +714,26 @@ export class AddonModForumProvider { | |||||||
|         return sortOrders; |         return sortOrders; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Get sort order selected by the user. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved with sort order. | ||||||
|  |      */ | ||||||
|  |     async getSelectedSortOrder(): Promise<AddonModForumSortOrder> { | ||||||
|  |         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. |      * Get forum discussions. | ||||||
|      * |      * | ||||||
| @ -729,6 +763,7 @@ export class AddonModForumProvider { | |||||||
|             // Since Moodle 3.7.
 |             // Since Moodle 3.7.
 | ||||||
|             method = 'mod_forum_get_forum_discussions'; |             method = 'mod_forum_get_forum_discussions'; | ||||||
|             (params as AddonModForumGetForumDiscussionsWSParams).sortorder = options.sortOrder; |             (params as AddonModForumGetForumDiscussionsWSParams).sortorder = options.sortOrder; | ||||||
|  |             (params as AddonModForumGetForumDiscussionsWSParams).groupid = options.groupId; | ||||||
|         } else { |         } else { | ||||||
|             if (options.sortOrder !== AddonModForumProvider.SORTORDER_LASTPOST_DESC) { |             if (options.sortOrder !== AddonModForumProvider.SORTORDER_LASTPOST_DESC) { | ||||||
|                 throw new Error('Sorting not supported with the old WS method.'); |                 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<void> { |     async invalidateDiscussionsList(forumId: number, siteId?: string): Promise<void> { | ||||||
|         const site = await CoreSites.getSite(siteId); |         const site = await CoreSites.getSite(siteId); | ||||||
| 
 | 
 | ||||||
|         await CoreUtils.allPromises( |         await site.invalidateWsCacheForKeyStartingWith(this.getDiscussionsListCommonCacheKey(forumId)); | ||||||
|             this.getAvailableSortOrders() |  | ||||||
|                 .map(sortOrder => site.invalidateWsCacheForKey(this.getDiscussionsListCacheKey(forumId, sortOrder.value))), |  | ||||||
|         ); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -1499,6 +1531,7 @@ export type AddonModForumLegacyPost = { | |||||||
| export type AddonModForumGetDiscussionsOptions = CoreCourseCommonModWSOptions & { | export type AddonModForumGetDiscussionsOptions = CoreCourseCommonModWSOptions & { | ||||||
|     sortOrder?: number; // Sort order.
 |     sortOrder?: number; // Sort order.
 | ||||||
|     page?: number; // Page. Defaults to 0.
 |     page?: number; // Page. Defaults to 0.
 | ||||||
|  |     groupId?: number; // Group ID.
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -2079,6 +2112,7 @@ export type AddonModForumNewDiscussionData = { | |||||||
|     cmId: number; |     cmId: number; | ||||||
|     discussionIds?: number[] | null; |     discussionIds?: number[] | null; | ||||||
|     discTimecreated?: number; |     discTimecreated?: number; | ||||||
|  |     groupId?: number; // The discussion group if it's created in a certain group, ALL_PARTICIPANTS for all participants.
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | |||||||
| @ -95,41 +95,41 @@ export class AddonModForumPrefetchHandlerService extends CoreCourseActivityPrefe | |||||||
|      * @param options Other options. |      * @param options Other options. | ||||||
|      * @return Promise resolved with array of posts. |      * @return Promise resolved with array of posts. | ||||||
|      */ |      */ | ||||||
|     protected getPostsForPrefetch( |     protected async getPostsForPrefetch( | ||||||
|         forum: AddonModForumData, |         forum: AddonModForumData, | ||||||
|         options: CoreCourseCommonModWSOptions = {}, |         options: CoreCourseCommonModWSOptions = {}, | ||||||
|     ): Promise<AddonModForumPost[]> { |     ): Promise<AddonModForumPost[]> { | ||||||
|         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.
 |             // Get discussions in first 2 pages.
 | ||||||
|             const discussionsOptions = { |             const discussionsOptions = { | ||||||
|                 sortOrder: sortOrder.value, |                 sortOrder: sortOrder.value, | ||||||
|  |                 groupId: groupId, | ||||||
|                 numPages: 2, |                 numPages: 2, | ||||||
|                 ...options, // Include all options.
 |                 ...options, // Include all options.
 | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|             return AddonModForum.getDiscussionsInPages(forum.id, discussionsOptions).then((response) => { |             const response = await AddonModForum.getDiscussionsInPages(forum.id, discussionsOptions); | ||||||
|  | 
 | ||||||
|             if (response.error) { |             if (response.error) { | ||||||
|                 throw new Error('Failed getting discussions'); |                 throw new Error('Failed getting discussions'); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|                 const promises: Promise<{ posts: AddonModForumPost[] }>[] = []; |             return await Promise.all( | ||||||
|  |                 response.discussions.map((discussion) => AddonModForum.getDiscussionPosts(discussion.discussion, options)), | ||||||
|  |             ); | ||||||
|  |         })); | ||||||
| 
 | 
 | ||||||
|                 response.discussions.forEach((discussion) => { |  | ||||||
|                     promises.push(AddonModForum.getDiscussionPosts(discussion.discussion, options)); |  | ||||||
|                 }); |  | ||||||
| 
 |  | ||||||
|                 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 posts: AddonModForumPost[] = []; | ||||||
|             const postIds = {}; // To make the array unique.
 |         const postIds: Record<number, boolean> = {}; // To make the array unique.
 | ||||||
| 
 | 
 | ||||||
|             results.forEach((orderResults) => { |         results.forEach((groupResults) => { | ||||||
|                 orderResults.forEach((orderResult) => { |             groupResults.forEach((groupDiscussion) => { | ||||||
|                     orderResult.posts.forEach((post) => { |                 groupDiscussion.posts.forEach((post) => { | ||||||
|                     if (!postIds[post.id]) { |                     if (!postIds[post.id]) { | ||||||
|                         postIds[post.id] = true; |                         postIds[post.id] = true; | ||||||
|                         posts.push(post); |                         posts.push(post); | ||||||
| @ -139,7 +139,36 @@ export class AddonModForumPrefetchHandlerService extends CoreCourseActivityPrefe | |||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         return posts; |         return posts; | ||||||
|         }); |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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<number[]> { | ||||||
|  |         const groupInfo = await CoreGroups.getActivityGroupInfo(forum.cmid); | ||||||
|  | 
 | ||||||
|  |         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.
 |         // Prefetch access information.
 | ||||||
|         promises.push(AddonModForum.getAccessInformation(forum.id, modOptions)); |         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.
 |         // 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))); |         promises.push(CoreUtils.ignoreErrors(CoreCourses.getCourseByField('id', courseId, siteId))); | ||||||
| 
 | 
 | ||||||
| @ -269,34 +293,15 @@ export class AddonModForumPrefetchHandlerService extends CoreCourseActivityPrefe | |||||||
| 
 | 
 | ||||||
|             // Activity uses groups, prefetch allowed groups.
 |             // Activity uses groups, prefetch allowed groups.
 | ||||||
|             const result = await CoreGroups.getActivityAllowedGroups(forum.cmid, undefined, siteId); |             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( |             await Promise.all( | ||||||
|                 result.groups.map( |                 result.groups.map( | ||||||
|                     async (group) => CoreUtils.ignoreErrors( |                     async (group) => CoreUtils.ignoreErrors( | ||||||
|                         AddonModForum.canAddDiscussion(forum.id, group.id, options), |                         AddonModForum.canAddDiscussion(forum.id, group.id, options), | ||||||
|                     ), |                     ), | ||||||
|  |                 ).concat( | ||||||
|  |                     CoreUtils.ignoreErrors(AddonModForum.canAddDiscussionToAll(forum.id, options)), | ||||||
|                 ), |                 ), | ||||||
|             ); |             ); | ||||||
|             } |  | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             // Ignore errors if cannot create discussions.
 |             // Ignore errors if cannot create discussions.
 | ||||||
|             if (canCreateDiscussions) { |             if (canCreateDiscussions) { | ||||||
|  | |||||||
| @ -23,6 +23,9 @@ Feature: Test basic usage of forum activity in app | |||||||
|     And the following "activities" exist: |     And the following "activities" exist: | ||||||
|       | activity   | name            | intro       | course | idnumber | groupmode | assessed | scale | |       | activity   | name            | intro       | course | idnumber | groupmode | assessed | scale | | ||||||
|       | forum      | Test forum name | Test forum  | C1     | forum    | 0         | 1        | 1     | |       | 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 |   Scenario: Create new discussion | ||||||
|     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app |     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 |     When I press "My happy subject" in the app | ||||||
|     Then I should find "An awesome message" 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 |   Scenario: Reply a post | ||||||
|     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app |     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 |     When I press "Initial discussion" in the app | ||||||
|     And I set the following fields to these values in the app: |     And I press "Reply" 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 |  | ||||||
|     And I set the field "Message" to "ReplyMessage" in the app |     And I set the field "Message" to "ReplyMessage" in the app | ||||||
|     And I press "Post to forum" 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 "ReplyMessage" in the app | ||||||
| 
 | 
 | ||||||
|   Scenario: Star and pin discussions (student) |   Scenario: Star and pin discussions (student) | ||||||
|     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app |     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 |     When I press "Display options" near "Initial discussion" 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 |  | ||||||
|     And I press "Star this 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 "Your star option has been updated." in the app | ||||||
|     Then I should find "starred message" in the app |  | ||||||
| 
 | 
 | ||||||
|     When I press the back button in the app |     When I press "Display options" near "Initial discussion" 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 |  | ||||||
|     And I press "Unstar this 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 "Your star option has been updated." in the app | ||||||
|     Then I should find "starred message" in the app |  | ||||||
| 
 | 
 | ||||||
|     When I press the back button in the app |     When I press "Display options" near "Initial discussion" in the app | ||||||
|     And I press "normal subject" in the app |     Then I should not find "Pin this discussion" in the app | ||||||
|     Then I should find "normal message" in the app |  | ||||||
| 
 | 
 | ||||||
|   Scenario: Star and pin discussions (teacher) |   Scenario: Star and pin discussions (teacher) | ||||||
|     Given I entered the forum activity "Test forum name" on course "Course 1" as "teacher1" in the app |     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 |     When I press "Display options" near "Initial discussion" 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 |  | ||||||
|     And I press "Star this 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 |     Then I should find "Your star option has been updated." 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 |  | ||||||
| 
 | 
 | ||||||
|     When I press "Display options" near "Auto-test pin" in the app |     When I press "Display options" near "Initial discussion" in the app | ||||||
|     And I press "Unpin this discussion" in the app |     And I press "Pin this discussion" in the app | ||||||
|     And I press "Display options" near "Auto-test star" 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 |     And I press "Unstar this discussion" in the app | ||||||
|     Then I should find "Auto-test star" in the app |     Then I should find "Your star option has been updated." in the app | ||||||
|     And I should find "Auto-test pin" 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 |   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 |     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 |     When I press "Initial discussion" in the app | ||||||
|     And I set the following fields to these values in the app: |     Then I should find "Reply" 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 the back button in the app |     When I press the back button in the app | ||||||
|     And I switch offline mode to "true" |     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 |     And I press "Initial discussion" in the app | ||||||
|     Then I should find "Reply" in the app |     And I press "Reply" in the app | ||||||
| 
 |  | ||||||
|     When I press "Reply" in the app |  | ||||||
|     And I set the field "Message" to "not sent 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 "Post to forum" in the app | ||||||
|     And I press "Display options" within "not sent reply" "ion-card" 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" |     When I switch offline mode to "false" | ||||||
|     And I press the back button in the app |     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 |     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 |     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 |     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 |     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 |     And I should find "Auto-test message edited" in the app | ||||||
| 
 | 
 | ||||||
|   Scenario: Edit a forum post (only online) |   Scenario: Edit a forum post (only online) | ||||||
| @ -182,17 +146,8 @@ Feature: Test basic usage of forum activity in app | |||||||
|       | Subject | Auto-test | |       | Subject | Auto-test | | ||||||
|       | Message | Auto-test message | |       | Message | Auto-test message | | ||||||
|     And I press "Post to forum" in the app |     And I press "Post to forum" in the app | ||||||
|     Then I should find "Auto-test" in the app |     And I press "Auto-test" in the app | ||||||
| 
 |     And I press "Display options" near "Reply" 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 |  | ||||||
|     Then I should find "Edit" in the app |     Then I should find "Edit" in the app | ||||||
| 
 | 
 | ||||||
|     When I press "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 |     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 |     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) |   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 |     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 |     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 | |       | Subject | Auto-test | | ||||||
|       | Message | Auto-test message | |       | Message | Auto-test message | | ||||||
|     And I press "Post to forum" in the app |     And I press "Post to forum" in the app | ||||||
|     Then I should find "Auto-test" in the app |     And I press "Auto-test" in the app | ||||||
| 
 |     And I press "Display options" near "Reply" 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 |  | ||||||
|     Then I should find "Delete" in the app |     Then I should find "Delete" in the app | ||||||
| 
 | 
 | ||||||
|     When I press "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 | |       | Message | Auto-test message | | ||||||
|     And I press "Post to forum" in the app |     And I press "Post to forum" in the app | ||||||
|     And I press "Auto-test" in the app |     And I press "Auto-test" in the app | ||||||
|     Then I should find "Reply" in the app |     And I press "Reply" in the app | ||||||
| 
 |  | ||||||
|     When I press "Reply" in the app |  | ||||||
|     And I set the field "Message" to "test2" in the app |     And I set the field "Message" to "test2" in the app | ||||||
|     And I press "Post to forum" in the app |     And I press "Post to forum" in the app | ||||||
|     Then I should find "test2" "ion-card" 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 |     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 |     When I press "Auto-test" in the app | ||||||
|     Then I should find "Reply" in the app |     And I press "None" near "Auto-test message" in the app | ||||||
| 
 |  | ||||||
|     When I press "None" near "Auto-test message" in the app |  | ||||||
|     And I press "1" near "Cancel" in the app |     And I press "1" near "Cancel" in the app | ||||||
|     And I switch offline mode to "true" |     And I switch offline mode to "true" | ||||||
|     And I press "None" near "test2" in the app |     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 |   Scenario: Reply a post offline | ||||||
|     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app |     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 |     When I press "Initial discussion" 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 |  | ||||||
|     And I switch offline mode to "true" |     And I switch offline mode to "true" | ||||||
|     Then I should find "Reply" in the app |     Then I should find "Reply" in the app | ||||||
| 
 | 
 | ||||||
|     When I press "Reply" in the app |     When I press "Reply" in the app | ||||||
|     And I set the field "Message" to "ReplyMessage" in the app |     And I set the field "Message" to "ReplyMessage" in the app | ||||||
|     And I press "Post to forum" 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 "ReplyMessage" in the app | ||||||
|     And I should find "Not sent" in the app |     And I should find "Not sent" in the app | ||||||
| 
 | 
 | ||||||
|     When I press the back button in the app |     When I press the back button in the app | ||||||
|     And I switch offline mode to "false" |     And I switch offline mode to "false" | ||||||
|     And I press "DiscussionSubject" in the app |     And I press "Initial discussion" 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 "ReplyMessage" in the app | ||||||
|     But I should not find "Not sent" 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 "Test forum name" in the app | ||||||
|     And I press "Information" in the app |     And I press "Information" in the app | ||||||
|     And I press "Refresh" 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 |     Then I should find "DiscussionSubject" in the app | ||||||
|     And I should find "DiscussionMessage" in the app |     And I should find "DiscussionMessage" in the app | ||||||
|     But I should not find "Not sent" 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 |     And I wait loading to finish in the app | ||||||
|     Then I should not find "Not sent" 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 |     Then I should find "DiscussionSubject" in the app | ||||||
|     And I should find "DiscussionMessage" in the app |     And I should find "DiscussionMessage" in the app | ||||||
|     But I should not find "Not sent" 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 |     And I should not find "This Forum has offline data to be synchronised." in the app | ||||||
| 
 | 
 | ||||||
|   Scenario: Prefetch |   Scenario: Prefetch | ||||||
|     Given I entered the forum activity "Test forum name" on course "Course 1" as "student1" in the app |     Given I entered the course "Course 1" as "student1" in the app | ||||||
|     When I press "Add discussion topic" in the app |     When I press "Course downloads" 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 |  | ||||||
|     And I press "Download" within "Test forum name" "ion-item" 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 |     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 |     When I press the back button in the app | ||||||
|     And I switch offline mode to "true" |     And I switch offline mode to "true" | ||||||
|     And I press "Test forum name" in the app |     And I press "Test forum name" in the app | ||||||
|     And I press "DiscussionSubject 2" in the app |     Then I should find "Initial discussion" 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 |     When I press "Initial discussion" in the app | ||||||
|     And I press the back button in the app |     Then I should find "Initial discussion" in the app | ||||||
|     And I press "DiscussionSubject 1" in the app |     And I should find "Initial discussion message" in the app | ||||||
|     Then I should find "DiscussionSubject 1" in the app | 
 | ||||||
|     And I should find "DiscussionMessage 1" in the app |     When I press the back button 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 |     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 | ||||||
|  | |||||||
							
								
								
									
										369
									
								
								src/addons/mod/forum/tests/behat/groups.feature
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										369
									
								
								src/addons/mod/forum/tests/behat/groups.feature
									
									
									
									
									
										Executable file
									
								
							| @ -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 | ||||||
| @ -16,7 +16,7 @@ import { AddonNotes, AddonNotesPublishState } from '@addons/notes/services/notes | |||||||
| import { Component, ViewChild, ElementRef, Input } from '@angular/core'; | import { Component, ViewChild, ElementRef, Input } from '@angular/core'; | ||||||
| import { CoreApp } from '@services/app'; | import { CoreApp } from '@services/app'; | ||||||
| import { CoreSites } from '@services/sites'; | import { CoreSites } from '@services/sites'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; | ||||||
| import { CoreForms } from '@singletons/form'; | import { CoreForms } from '@singletons/form'; | ||||||
| import { ModalController } from '@singletons'; | import { ModalController } from '@singletons'; | ||||||
| 
 | 
 | ||||||
| @ -57,7 +57,7 @@ export class AddonNotesAddComponent { | |||||||
|             CoreForms.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.getCurrentSiteId()); |             CoreForms.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.getCurrentSiteId()); | ||||||
| 
 | 
 | ||||||
|             ModalController.dismiss(<AddonNotesAddModalReturn>{ type: this.type, sent: true }).finally(() => { |             ModalController.dismiss(<AddonNotesAddModalReturn>{ 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){ |         } catch (error){ | ||||||
|             CoreDomUtils.showErrorModal(error); |             CoreDomUtils.showErrorModal(error); | ||||||
|  | |||||||
| @ -23,7 +23,7 @@ import { CoreUser, CoreUserProfile } from '@features/user/services/user'; | |||||||
| import { IonContent, IonRefresher } from '@ionic/angular'; | import { IonContent, IonRefresher } from '@ionic/angular'; | ||||||
| import { CoreNavigator } from '@services/navigator'; | import { CoreNavigator } from '@services/navigator'; | ||||||
| import { CoreSites } from '@services/sites'; | 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 { CoreTextUtils } from '@services/utils/text'; | ||||||
| import { CoreUtils } from '@services/utils/utils'; | import { CoreUtils } from '@services/utils/utils'; | ||||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||||
| @ -232,7 +232,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy { | |||||||
| 
 | 
 | ||||||
|                 this.refreshNotes(false); |                 this.refreshNotes(false); | ||||||
| 
 | 
 | ||||||
|                 CoreDomUtils.showToast('addon.notes.eventnotedeleted', true, 3000); |                 CoreDomUtils.showToast('addon.notes.eventnotedeleted', true, ToastDuration.LONG); | ||||||
| 
 | 
 | ||||||
|             } catch (error) { |             } catch (error) { | ||||||
|                 CoreDomUtils.showErrorModalDefault(error, 'Delete note failed.'); |                 CoreDomUtils.showErrorModalDefault(error, 'Delete note failed.'); | ||||||
|  | |||||||
| @ -29,7 +29,7 @@ import { | |||||||
|     CoreWSUploadFileResult, |     CoreWSUploadFileResult, | ||||||
|     CoreWSPreSetsSplitRequest, |     CoreWSPreSetsSplitRequest, | ||||||
| } from '@services/ws'; | } from '@services/ws'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; | ||||||
| import { CoreTextUtils } from '@services/utils/text'; | import { CoreTextUtils } from '@services/utils/text'; | ||||||
| import { CoreTimeUtils } from '@services/utils/time'; | import { CoreTimeUtils } from '@services/utils/time'; | ||||||
| import { CoreUrlUtils, CoreUrlParams } from '@services/utils/url'; | import { CoreUrlUtils, CoreUrlParams } from '@services/utils/url'; | ||||||
| @ -572,7 +572,7 @@ export class CoreSite { | |||||||
| 
 | 
 | ||||||
|         if (wsPreSets.cleanUnicode && CoreTextUtils.hasUnicodeData(data)) { |         if (wsPreSets.cleanUnicode && CoreTextUtils.hasUnicodeData(data)) { | ||||||
|             // Data will be cleaned, notify the user.
 |             // Data will be cleaned, notify the user.
 | ||||||
|             CoreDomUtils.showToast('core.unicodenotsupported', true, 3000); |             CoreDomUtils.showToast('core.unicodenotsupported', true, ToastDuration.LONG); | ||||||
|         } else { |         } else { | ||||||
|             // No need to clean data in this call.
 |             // No need to clean data in this call.
 | ||||||
|             wsPreSets.cleanUnicode = false; |             wsPreSets.cleanUnicode = false; | ||||||
|  | |||||||
| @ -31,7 +31,7 @@ import { ContextLevel, CoreConstants } from '@/core/constants'; | |||||||
| import { CoreNavigator } from '@services/navigator'; | import { CoreNavigator } from '@services/navigator'; | ||||||
| import { NgZone, Translate } from '@singletons'; | import { NgZone, Translate } from '@singletons'; | ||||||
| import { CoreUtils } from '@services/utils/utils'; | 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 { CoreUser } from '@features/user/services/user'; | ||||||
| import { CoreTextUtils } from '@services/utils/text'; | import { CoreTextUtils } from '@services/utils/text'; | ||||||
| import { CoreError } from '@classes/errors/error'; | import { CoreError } from '@classes/errors/error'; | ||||||
| @ -319,7 +319,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { | |||||||
|             CoreDomUtils.showToast( |             CoreDomUtils.showToast( | ||||||
|                 commentsResponse ? 'core.comments.eventcommentcreated' : 'core.datastoredoffline', |                 commentsResponse ? 'core.comments.eventcommentcreated' : 'core.datastoredoffline', | ||||||
|                 true, |                 true, | ||||||
|                 3000, |                 ToastDuration.LONG, | ||||||
|             ); |             ); | ||||||
| 
 | 
 | ||||||
|             if (commentsResponse) { |             if (commentsResponse) { | ||||||
| @ -417,7 +417,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { | |||||||
| 
 | 
 | ||||||
|             this.invalidateComments(); |             this.invalidateComments(); | ||||||
| 
 | 
 | ||||||
|             CoreDomUtils.showToast('core.comments.eventcommentdeleted', true, 3000); |             CoreDomUtils.showToast('core.comments.eventcommentdeleted', true, ToastDuration.LONG); | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             CoreDomUtils.showErrorModalDefault(error, 'Delete comment failed.'); |             CoreDomUtils.showErrorModalDefault(error, 'Delete comment failed.'); | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -23,7 +23,7 @@ import { | |||||||
| } from '@features/rating/services/rating'; | } from '@features/rating/services/rating'; | ||||||
| import { CoreRatingOffline } from '@features/rating/services/rating-offline'; | import { CoreRatingOffline } from '@features/rating/services/rating-offline'; | ||||||
| import { CoreSites } from '@services/sites'; | import { CoreSites } from '@services/sites'; | ||||||
| import { CoreDomUtils } from '@services/utils/dom'; | import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; | ||||||
| import { Translate } from '@singletons'; | import { Translate } from '@singletons'; | ||||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||||
| 
 | 
 | ||||||
| @ -143,7 +143,7 @@ export class CoreRatingRateComponent implements OnChanges, OnDestroy { | |||||||
|             ); |             ); | ||||||
| 
 | 
 | ||||||
|             if (response === undefined) { |             if (response === undefined) { | ||||||
|                 CoreDomUtils.showToast('core.datastoredoffline', true, 3000); |                 CoreDomUtils.showToast('core.datastoredoffline', true, ToastDuration.LONG); | ||||||
|             } else { |             } else { | ||||||
|                 this.onUpdate.emit(); |                 this.onUpdate.emit(); | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -86,5 +86,5 @@ export type CoreUserDBRecord = CoreUserBasicData; | |||||||
| export type CoreUserPreferenceDBRecord = { | export type CoreUserPreferenceDBRecord = { | ||||||
|     name: string; |     name: string; | ||||||
|     value: string; |     value: string; | ||||||
|     onlinevalue: string; |     onlinevalue: string | null; | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -58,19 +58,12 @@ export class CoreUserOfflineProvider { | |||||||
|      * @param siteId Site ID. If not defined, current site. |      * @param siteId Site ID. If not defined, current site. | ||||||
|      * @return Promise resolved when done. |      * @return Promise resolved when done. | ||||||
|      */ |      */ | ||||||
|     async setPreference(name: string, value: string, onlineValue?: string, siteId?: string): Promise<void> { |     async setPreference(name: string, value: string, onlineValue?: string | null , siteId?: string): Promise<void> { | ||||||
|         const site = await CoreSites.getSite(siteId); |         const site = await CoreSites.getSite(siteId); | ||||||
| 
 | 
 | ||||||
|         if (onlineValue === undefined) { |         const record: Partial<CoreUserPreferenceDBRecord> = { | ||||||
|             const preference = await this.getPreference(name, site.id); |  | ||||||
| 
 |  | ||||||
|             onlineValue = preference.onlinevalue; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const record: CoreUserPreferenceDBRecord = { |  | ||||||
|             name, |             name, | ||||||
|             value, |             value, | ||||||
|             onlinevalue: onlineValue, |  | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         await site.getDb().insertRecord(PREFERENCES_TABLE_NAME, record); |         await site.getDb().insertRecord(PREFERENCES_TABLE_NAME, record); | ||||||
|  | |||||||
| @ -1106,7 +1106,7 @@ type CoreUserGetUserPreferencesWSParams = { | |||||||
| type CoreUserGetUserPreferencesWSResponse = { | type CoreUserGetUserPreferencesWSResponse = { | ||||||
|     preferences: { // User custom fields (also known as user profile fields).
 |     preferences: { // User custom fields (also known as user profile fields).
 | ||||||
|         name: string; // The name of the preference.
 |         name: string; // The name of the preference.
 | ||||||
|         value: string; // The value of the preference.
 |         value: string | null; // The value of the preference.
 | ||||||
|     }[]; |     }[]; | ||||||
|     warnings?: CoreWSExternalWarning[]; |     warnings?: CoreWSExternalWarning[]; | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -1614,23 +1614,19 @@ export class CoreDomUtilsProvider { | |||||||
|     async showToast( |     async showToast( | ||||||
|         text: string, |         text: string, | ||||||
|         needsTranslate?: boolean, |         needsTranslate?: boolean, | ||||||
|         duration: number = 2000, |         duration: ToastDuration | number = ToastDuration.SHORT, | ||||||
|         cssClass: string = '', |         cssClass: string = '', | ||||||
|     ): Promise<HTMLIonToastElement> { |     ): Promise<HTMLIonToastElement> { | ||||||
|         if (needsTranslate) { |         if (needsTranslate) { | ||||||
|             text = Translate.instant(text); |             text = Translate.instant(text); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const loader = await ToastController.create({ |         return this.showToastWithOptions({ | ||||||
|             message: text, |             message: text, | ||||||
|             duration: duration, |             duration: duration, | ||||||
|             position: 'bottom', |             position: 'bottom', | ||||||
|             cssClass: cssClass, |             cssClass: cssClass, | ||||||
|         }); |         }); | ||||||
| 
 |  | ||||||
|         await loader.present(); |  | ||||||
| 
 |  | ||||||
|         return loader; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -1639,12 +1635,15 @@ export class CoreDomUtilsProvider { | |||||||
|      * @param options Options. |      * @param options Options. | ||||||
|      * @return Promise resolved with Toast instance. |      * @return Promise resolved with Toast instance. | ||||||
|      */ |      */ | ||||||
|     async showToastWithOptions(options: ToastOptions): Promise<HTMLIonToastElement> { |     async showToastWithOptions(options: ShowToastOptions): Promise<HTMLIonToastElement> { | ||||||
|         // Set some default values.
 |         // Convert some values and set default values.
 | ||||||
|         options.duration = options.duration ?? 2000; |         const toastOptions: ToastOptions = { | ||||||
|         options.position = options.position ?? 'bottom'; |             ...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(); |         await loader.present(); | ||||||
| 
 | 
 | ||||||
| @ -2130,3 +2129,19 @@ export enum VerticalPoint { | |||||||
|     MID = 'mid', |     MID = 'mid', | ||||||
|     BOTTOM = 'bottom', |     BOTTOM = 'bottom', | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Toast duration. | ||||||
|  |  */ | ||||||
|  | export enum ToastDuration { | ||||||
|  |     LONG = 'long', | ||||||
|  |     SHORT = 'short', | ||||||
|  |     STICKY = 'sticky', | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Options for showToastWithOptions. | ||||||
|  |  */ | ||||||
|  | export type ShowToastOptions = Omit<ToastOptions, 'duration'> & { | ||||||
|  |     duration: ToastDuration | number; | ||||||
|  | }; | ||||||
|  | |||||||
| @ -89,7 +89,10 @@ export class TestingBehatDomUtilsService { | |||||||
|         text: string, |         text: string, | ||||||
|         options: TestingBehatFindOptions, |         options: TestingBehatFindOptions, | ||||||
|     ): ElementsWithExact[] { |     ): 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<HTMLElement>(attributesSelector)) |         const elements = Array.from(container.querySelectorAll<HTMLElement>(attributesSelector)) | ||||||
|             .filter((element => this.isElementVisible(element, container))) |             .filter((element => this.isElementVisible(element, container))) | ||||||
| @ -397,12 +400,13 @@ export class TestingBehatDomUtilsService { | |||||||
|             const withinElements = this.findElementsBasedOnTextInContainer(locator.within, topContainer, options); |             const withinElements = this.findElementsBasedOnTextInContainer(locator.within, topContainer, options); | ||||||
| 
 | 
 | ||||||
|             if (withinElements.length === 0) { |             if (withinElements.length === 0) { | ||||||
|                 throw new Error('There was no match for within text'); |                 return []; | ||||||
|             } else if (withinElements.length > 1) { |             } else if (withinElements.length > 1) { | ||||||
|                 const withinElementsAncestors = this.getTopAncestors(withinElements); |                 const withinElementsAncestors = this.getTopAncestors(withinElements); | ||||||
| 
 | 
 | ||||||
|                 if (withinElementsAncestors.length > 1) { |                 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]; |                 topContainer = container = withinElementsAncestors[0]; | ||||||
| @ -418,12 +422,13 @@ export class TestingBehatDomUtilsService { | |||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|             if (nearElements.length === 0) { |             if (nearElements.length === 0) { | ||||||
|                 throw new Error('There was no match for near text'); |                 return []; | ||||||
|             } else if (nearElements.length > 1) { |             } else if (nearElements.length > 1) { | ||||||
|                 const nearElementsAncestors = this.getTopAncestors(nearElements); |                 const nearElementsAncestors = this.getTopAncestors(nearElements); | ||||||
| 
 | 
 | ||||||
|                 if (nearElementsAncestors.length > 1) { |                 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]); |                 container = this.getParentElement(nearElementsAncestors[0]); | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								src/types/config.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/types/config.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -18,6 +18,7 @@ import { CoreSitesDemoSiteData } from '@services/sites'; | |||||||
| import { OpenFileAction } from '@services/utils/utils'; | import { OpenFileAction } from '@services/utils/utils'; | ||||||
| import { CoreLoginSiteSelectorListMethod } from '@features/login/services/login-helper'; | import { CoreLoginSiteSelectorListMethod } from '@features/login/services/login-helper'; | ||||||
| import { CoreDatabaseConfiguration } from '@classes/database/database-table'; | import { CoreDatabaseConfiguration } from '@classes/database/database-table'; | ||||||
|  | import { ToastDuration } from '@services/utils/dom'; | ||||||
| 
 | 
 | ||||||
| /* eslint-disable @typescript-eslint/naming-convention */ | /* 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.
 |     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.
 |     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.
 |     uselegacycompletion?: boolean; // Whether to use legacy completion by default in all course formats.
 | ||||||
|  |     toastDurations: Record<ToastDuration, number>; | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user