3
0
Noel De Martin 3621bc46a5 MOBILE-3947 glossary: Fix tests
There were some bugs in Angular v10 that countered this bug in our code, that's why it wasn't manifested until now. It seems to be related with the changes in createUrlTreeFromSegmentGroup.

See https://github.com/angular/angular/commits/16.0.x/packages/router/src/create_url_tree.ts?since=2020-06-25&until=2024-01-11
2024-01-15 16:35:57 +01:00

709 lines
26 KiB
TypeScript

// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnDestroy, ViewChild, ElementRef, OnInit, Optional } from '@angular/core';
import { FileEntry } from '@awesome-cordova-plugins/file/ngx';
import { FormControl } from '@angular/forms';
import { CoreEvents, CoreEventObserver } from '@singletons/events';
import { CoreGroup, CoreGroups, CoreGroupsProvider } from '@services/groups';
import { CoreNavigator } from '@services/navigator';
import {
AddonModForum,
AddonModForumAccessInformation,
AddonModForumCanAddDiscussion,
AddonModForumData,
AddonModForumProvider,
} from '@addons/mod/forum/services/forum';
import { CoreEditorRichTextEditorComponent } from '@features/editor/components/rich-text-editor/rich-text-editor';
import { AddonModForumSync, AddonModForumSyncProvider } from '@addons/mod/forum/services/forum-sync';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { Translate } from '@singletons';
import { CoreSync } from '@services/sync';
import { AddonModForumDiscussionOptions, AddonModForumOffline } from '@addons/mod/forum/services/forum-offline';
import { CoreUtils } from '@services/utils/utils';
import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper';
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
import { CoreTextUtils } from '@services/utils/text';
import { CanLeave } from '@guards/can-leave';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreForms } from '@singletons/form';
import { AddonModForumDiscussionsSwipeManager } from '../../classes/forum-discussions-swipe-manager';
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
import { AddonModForumDiscussionsSource } from '../../classes/forum-discussions-source';
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
import { CoreTime } from '@singletons/time';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
type NewDiscussionData = {
subject: string;
message: string | null; // Null means empty or just white space.
postToAllGroups: boolean;
groupId: number;
subscribe: boolean;
pin: boolean;
files: FileEntry[];
};
/**
* Page that displays the new discussion form.
*/
@Component({
selector: 'page-addon-mod-forum-new-discussion',
templateUrl: 'new-discussion.html',
styleUrls: ['new-discussion.scss'],
})
export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLeave {
@ViewChild('newDiscFormEl') formElement!: ElementRef;
@ViewChild(CoreEditorRichTextEditorComponent) messageEditor!: CoreEditorRichTextEditorComponent;
component = AddonModForumProvider.COMPONENT;
messageControl = new FormControl<string | null>(null);
groupsLoaded = false;
showGroups = false;
hasOffline = false;
canCreateAttachments = true; // Assume we can by default.
canPin = false;
forum!: AddonModForumData;
showForm = false;
groups: CoreGroup[] = [];
groupIds: number[] = [];
newDiscussion: NewDiscussionData = {
subject: '',
message: null,
postToAllGroups: false,
groupId: 0,
subscribe: true,
pin: false,
files: [],
};
advanced = false; // Display all form fields.
accessInfo: AddonModForumAccessInformation = {};
courseId!: number;
postInGroupMessage?: string;
discussions?: AddonModForumNewDiscussionDiscussionsSwipeManager;
protected cmId!: number;
protected forumId!: number;
protected timeCreated!: number;
protected syncId!: string;
protected syncObserver?: CoreEventObserver;
protected isDestroyed = false;
protected originalData?: Partial<NewDiscussionData>;
protected forceLeave = false;
protected initialGroupId?: number;
protected logView: () => void;
constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {
this.logView = CoreTime.once(() => {
CoreAnalytics.logEvent({
type: CoreAnalyticsEventType.VIEW_ITEM,
ws: 'mod_forum_add_discussion',
name: Translate.instant('addon.mod_forum.addanewdiscussion'),
data: { id: this.forumId, category: 'forum' },
url: '/mod/forum/post.php',
});
});
}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
try {
const routeData = this.route.snapshot.data;
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
this.forumId = CoreNavigator.getRequiredRouteNumberParam('forumId');
this.timeCreated = CoreNavigator.getRequiredRouteNumberParam('timeCreated');
this.initialGroupId = CoreNavigator.getRouteNumberParam('groupId');
// Discussion list uses 0 for all participants, but this page WebServices use a different value. Convert it.
this.initialGroupId = this.initialGroupId === 0 ? AddonModForumProvider.ALL_PARTICIPANTS : this.initialGroupId;
if (this.timeCreated !== 0 && (routeData.swipeEnabled ?? true)) {
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
AddonModForumDiscussionsSource,
[this.courseId, this.cmId, routeData.discussionsPathPrefix ?? ''],
);
this.discussions = new AddonModForumNewDiscussionDiscussionsSwipeManager(source);
await this.discussions.start();
}
} catch (error) {
CoreDomUtils.showErrorModal(error);
this.goBack();
return;
}
this.fetchDiscussionData().finally(() => {
this.groupsLoaded = true;
});
}
/**
* User entered the page that contains the component.
*/
ionViewDidEnter(): void {
if (this.syncObserver) {
// Already setup.
return;
}
// Refresh data if this discussion is synchronized automatically.
this.syncObserver = CoreEvents.on(AddonModForumSyncProvider.AUTO_SYNCED, data => {
if (data.forumId == this.forumId && data.userId == CoreSites.getCurrentSiteUserId()) {
CoreDomUtils.showAlertTranslated('core.notice', 'core.contenteditingsynced');
this.returnToDiscussions();
}
}, CoreSites.getCurrentSiteId());
}
/**
* Fetch if forum uses groups and the groups it uses.
*
* @param refresh Whether we're refreshing data.
* @returns Promise resolved when done.
*/
protected async fetchDiscussionData(refresh?: boolean): Promise<void> {
try {
const mode = await CoreGroups.getActivityGroupMode(this.cmId);
const promises: Promise<unknown>[] = [];
if (mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS) {
promises.push(
CoreGroups.instance
.getActivityAllowedGroups(this.cmId)
.then((result) => {
let promise;
if (mode === CoreGroupsProvider.VISIBLEGROUPS) {
// We need to check which of the returned groups the user can post to.
promise = this.validateVisibleGroups(result.groups);
} else {
// WS already filters groups, no need to do it ourselves. Add "All participants" if needed.
promise = this.addAllParticipantsOption(result.groups, true);
}
// eslint-disable-next-line promise/no-nesting
return promise.then(async (forumGroups) => {
if (forumGroups.length > 0) {
this.groups = forumGroups;
this.groupIds = forumGroups.map((group) => group.id).filter((id) => id > 0);
// Do not override group id.
this.newDiscussion.groupId = this.newDiscussion.groupId || this.getInitialGroupId();
this.showGroups = true;
await this.calculateGroupName();
if (this.groupIds.length <= 1) {
this.newDiscussion.postToAllGroups = false;
}
return;
} else {
const message = mode === CoreGroupsProvider.SEPARATEGROUPS ?
'addon.mod_forum.cannotadddiscussionall' : 'addon.mod_forum.cannotadddiscussion';
throw new Error(Translate.instant(message));
}
});
}),
);
} else {
this.showGroups = false;
this.newDiscussion.postToAllGroups = false;
// Use the canAddDiscussion WS to check if the user can add attachments and pin discussions.
promises.push(
CoreUtils.ignoreErrors(
AddonModForum.instance
.canAddDiscussionToAll(this.forumId, { cmId: this.cmId })
.then((response) => {
this.canPin = !!response.canpindiscussions;
this.canCreateAttachments = !!response.cancreateattachment;
return;
}),
),
);
}
// Get forum.
promises.push(AddonModForum.getForum(this.courseId, this.cmId).then((forum) => this.forum = forum));
// Get access information.
promises.push(
AddonModForum.instance
.getAccessInformation(this.forumId, { cmId: this.cmId })
.then((accessInfo) => this.accessInfo = accessInfo),
);
await Promise.all(promises);
// If editing a discussion, get offline data.
if (this.timeCreated && !refresh) {
this.syncId = AddonModForumSync.getForumSyncId(this.forumId);
await AddonModForumSync.waitForSync(this.syncId).then(() => {
// Do not block if the scope is already destroyed.
if (!this.isDestroyed) {
CoreSync.blockOperation(AddonModForumProvider.COMPONENT, this.syncId);
}
return AddonModForumOffline.instance
.getNewDiscussion(this.forumId, this.timeCreated)
.then(async (discussion) => {
this.hasOffline = true;
discussion.options = discussion.options || {};
if (discussion.groupid == AddonModForumProvider.ALL_GROUPS) {
this.newDiscussion.groupId = this.groups[0].id;
this.newDiscussion.postToAllGroups = true;
} else {
this.newDiscussion.groupId = discussion.groupid;
this.newDiscussion.postToAllGroups = false;
}
this.newDiscussion.subject = discussion.subject;
this.newDiscussion.message = discussion.message;
this.newDiscussion.subscribe = !!discussion.options.discussionsubscribe;
this.newDiscussion.pin = !!discussion.options.discussionpinned;
this.messageControl.setValue(discussion.message);
await this.calculateGroupName();
// Treat offline attachments if any.
if (typeof discussion.options.attachmentsid === 'object' && discussion.options.attachmentsid.offline) {
const files = await AddonModForumHelper.getNewDiscussionStoredFiles(
this.forumId,
this.timeCreated,
);
this.newDiscussion.files = files;
}
// Show advanced fields by default if any of them has not the default value.
if (
!this.newDiscussion.subscribe ||
this.newDiscussion.pin ||
this.newDiscussion.files.length ||
this.groups.length > 0 && this.newDiscussion.groupId != this.groups[0].id ||
this.newDiscussion.postToAllGroups
) {
this.advanced = true;
}
return;
});
});
}
if (!this.originalData) {
// Initialize original data.
this.originalData = {
subject: this.newDiscussion.subject,
message: this.newDiscussion.message,
files: this.newDiscussion.files.slice(),
};
}
this.showForm = true;
this.logView();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetgroups', true);
this.showForm = false;
}
}
/**
* Validate which of the groups returned by getActivityAllowedGroups in visible groups should be shown to post to.
*
* @param forumGroups Forum groups.
* @returns Promise resolved with the list of groups.
*/
protected async validateVisibleGroups(forumGroups: CoreGroup[]): Promise<CoreGroup[]> {
let response: AddonModForumCanAddDiscussion;
// We first check if the user can post to all the groups.
try {
response = await AddonModForum.canAddDiscussionToAll(this.forumId, { cmId: this.cmId });
} catch (error) {
// The call failed, let's assume he can't.
response = {
status: false,
canpindiscussions: false,
cancreateattachment: true,
};
}
this.canPin = !!response.canpindiscussions;
this.canCreateAttachments = !!response.cancreateattachment;
// The user can post to all groups, add the "All participants" option and return them all.
if (response.status) {
return this.addAllParticipantsOption(forumGroups, false);
}
// The user can't post to all groups, let's check which groups he can post to.
const promises: Promise<unknown>[] = [];
const filtered: CoreGroup[] = [];
forumGroups.forEach((group) => {
promises.push(
AddonModForum.instance
.canAddDiscussion(this.forumId, group.id, { cmId: this.cmId })
// The call failed, let's return true so the group is shown.
// If the user can't post to it an error will be shown when he tries to add the discussion.
.catch(() =>({ status: true }))
.then((response) => {
if (response.status) {
filtered.push(group);
}
return;
}),
);
});
await Promise.all(promises);
return filtered;
}
/**
* Filter forum groups, returning only those that are inside user groups.
*
* @param forumGroups Forum groups.
* @param userGroups User groups.
* @returns Filtered groups.
*/
protected filterGroups(forumGroups: CoreGroup[], userGroups: CoreGroup[]): CoreGroup[] {
const userGroupsIds = userGroups.map(group => group.id);
return forumGroups.filter(forumGroup => userGroupsIds.indexOf(forumGroup.id) > -1);
}
/**
* Get the initial group ID.
*
* @returns 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.
*
* @param groups Groups.
* @param check True to check if the user can add a discussion to all participants.
* @returns Promise resolved with the list of groups.
*/
protected addAllParticipantsOption(groups: CoreGroup[], check: boolean): Promise<CoreGroup[]> {
let promise: Promise<boolean>;
if (check) {
// We need to check if the user can add a discussion to all participants.
promise = AddonModForum.canAddDiscussionToAll(this.forumId, { cmId: this.cmId }).then((response) => {
this.canPin = !!response.canpindiscussions;
this.canCreateAttachments = !!response.cancreateattachment;
return response.status;
}).catch(() =>
// The call failed, let's assume he can't.
false);
} else {
// No need to check, assume the user can.
promise = Promise.resolve(true);
}
return promise.then((canAdd) => {
if (canAdd) {
groups.unshift({
courseid: this.courseId,
id: AddonModForumProvider.ALL_PARTICIPANTS,
name: Translate.instant('core.allparticipants'),
});
}
return groups;
});
}
/**
* Pull to refresh.
*
* @param refresher Refresher.
*/
refreshGroups(refresher?: HTMLIonRefresherElement): void {
const promises = [
CoreGroups.invalidateActivityGroupMode(this.cmId),
CoreGroups.invalidateActivityAllowedGroups(this.cmId),
AddonModForum.invalidateCanAddDiscussion(this.forumId),
];
Promise.all(promises).finally(() => {
this.fetchDiscussionData(true).finally(() => {
refresher?.complete();
});
});
}
/**
* Convenience function to update or return to discussions depending on device.
*
* @param discussionIds Ids of the new discussions.
* @param discTimecreated The time created of the discussion (if offline).
*/
protected returnToDiscussions(discussionIds?: number[] | null, discTimecreated?: number): void {
this.forceLeave = true;
// Delete the local files from the tmp folder.
CoreFileUploader.clearTmpFiles(this.newDiscussion.files);
CoreEvents.trigger(
AddonModForumProvider.NEW_DISCUSSION_EVENT,
{
forumId: this.forumId,
cmId: this.cmId,
discussionIds: discussionIds,
discTimecreated: discTimecreated,
groupId: this.showGroups && !this.newDiscussion.postToAllGroups ? this.newDiscussion.groupId : undefined,
},
CoreSites.getCurrentSiteId(),
);
if (this.splitView?.outletActivated) {
// Empty form.
this.hasOffline = false;
this.newDiscussion.subject = '';
this.newDiscussion.message = null;
this.newDiscussion.files = [];
this.newDiscussion.postToAllGroups = false;
this.messageEditor.clearText();
this.originalData = CoreUtils.clone(this.newDiscussion);
} else {
CoreNavigator.back();
}
}
/**
* Message changed.
*
* @param text The new text.
*/
onMessageChange(text?: string | null): void {
this.newDiscussion.message = text ?? null;
}
/**
* Add a new discussion.
*/
async add(): Promise<void> {
const forumName = this.forum.name;
const subject = this.newDiscussion.subject;
let message = this.newDiscussion.message || '';
const pin = this.newDiscussion.pin;
const attachments = this.newDiscussion.files;
const discTimecreated = this.timeCreated || Date.now();
const options: AddonModForumDiscussionOptions = {
discussionsubscribe: !!this.newDiscussion.subscribe,
};
if (!subject) {
CoreDomUtils.showErrorModal('addon.mod_forum.erroremptysubject', true);
return;
}
if (!message) {
CoreDomUtils.showErrorModal('addon.mod_forum.erroremptymessage', true);
return;
}
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
// Add some HTML to the message if needed.
message = CoreTextUtils.formatHtmlLines(message);
if (pin) {
options.discussionpinned = true;
}
const groupIds = this.newDiscussion.postToAllGroups ? this.groupIds : [this.newDiscussion.groupId];
try {
const discussionIds = await AddonModForumHelper.addNewDiscussion(
this.forumId,
forumName,
this.courseId,
subject,
message,
attachments,
options,
groupIds,
discTimecreated,
);
if (discussionIds) {
// Data sent to server, delete stored files (if any).
AddonModForumHelper.deleteNewDiscussionStoredFiles(this.forumId, discTimecreated);
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'forum' });
}
if (discussionIds && discussionIds.length < groupIds.length) {
// Some discussions could not be created.
CoreDomUtils.showErrorModalDefault(null, 'addon.mod_forum.errorposttoallgroups', true);
}
CoreForms.triggerFormSubmittedEvent(
this.formElement,
!!discussionIds,
CoreSites.getCurrentSiteId(),
);
this.returnToDiscussions(discussionIds, discTimecreated);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.cannotcreatediscussion', true);
} finally {
modal.dismiss();
}
}
/**
* Discard an offline saved discussion.
*/
async discard(): Promise<void> {
try {
await CoreDomUtils.showConfirm(Translate.instant('core.areyousure'));
const promises: Promise<unknown>[] = [];
promises.push(AddonModForumOffline.deleteNewDiscussion(this.forumId, this.timeCreated));
promises.push(
CoreUtils.ignoreErrors(
AddonModForumHelper.deleteNewDiscussionStoredFiles(this.forumId, this.timeCreated),
),
);
await Promise.all(promises);
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
this.returnToDiscussions();
} catch (error) {
// Cancelled.
}
}
/**
* Show or hide advanced form fields.
*/
toggleAdvanced(): void {
this.advanced = !this.advanced;
}
/**
* Calculate current group's name.
*/
async calculateGroupName(): Promise<void> {
if (this.newDiscussion.groupId <= 0) {
this.postInGroupMessage = undefined;
} else {
const groupName = this.groups.find(group => group.id === this.newDiscussion.groupId)?.name;
this.postInGroupMessage = groupName && Translate.instant('addon.mod_forum.postingroup', { groupname: groupName });
}
}
/**
* Check if we can leave the page or not.
*
* @returns Resolved if we can leave it, rejected if not.
*/
async canLeave(): Promise<boolean> {
if (this.forceLeave) {
return true;
}
if (AddonModForumHelper.hasPostDataChanged(this.newDiscussion, this.originalData)) {
// Show confirmation if some data has been modified.
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
}
// Delete the local files from the tmp folder.
CoreFileUploader.clearTmpFiles(this.newDiscussion.files);
if (this.formElement) {
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
}
return true;
}
/**
* Runs when the page is about to leave and no longer be the active page.
*/
ionViewWillLeave(): void {
this.syncObserver && this.syncObserver.off();
delete this.syncObserver;
}
/**
* Helper function to go back.
*/
protected goBack(): void {
if (this.splitView?.outletActivated) {
CoreNavigator.navigate('../../');
} else {
CoreNavigator.back();
}
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
if (this.syncId) {
CoreSync.unblockOperation(AddonModForumProvider.COMPONENT, this.syncId);
}
this.isDestroyed = true;
this.discussions?.destroy();
}
}
/**
* Helper to manage swiping within a collection of discussions.
*/
class AddonModForumNewDiscussionDiscussionsSwipeManager extends AddonModForumDiscussionsSwipeManager {
/**
* @inheritdoc
*/
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return `${this.getSource().DISCUSSIONS_PATH_PREFIX}new/${snapshot.params.timeCreated}`;
}
}