// (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 { AddonModGlossaryHelper } from '@addons/mod/glossary/services/glossary-helper'; import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '@addons/mod/glossary/services/glossary-offline'; import { Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core'; import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments'; import { CoreComments } from '@features/comments/services/comments'; import { CoreRatingInfo } from '@features/rating/services/rating'; import { CoreTag } from '@features/tag/services/tag'; import { FileEntry } from '@awesome-cordova-plugins/file/ngx'; import { CoreNavigator } from '@services/navigator'; import { CoreNetwork } from '@services/network'; import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { AddonModGlossaryEntriesSource, AddonModGlossaryEntryItem } from '../../classes/glossary-entries-source'; import { AddonModGlossary, AddonModGlossaryEntry, AddonModGlossaryGlossary, AddonModGlossaryProvider, GLOSSARY_ENTRY_UPDATED, } from '../../services/glossary'; import { CoreTime } from '@singletons/time'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; /** * Page that displays a glossary entry. */ @Component({ selector: 'page-addon-mod-glossary-entry', templateUrl: 'entry.html', }) export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { @ViewChild(CoreCommentsCommentsComponent) comments?: CoreCommentsCommentsComponent; component = AddonModGlossaryProvider.COMPONENT; componentId?: number; onlineEntry?: AddonModGlossaryEntry; offlineEntry?: AddonModGlossaryOfflineEntry; offlineEntryFiles?: FileEntry[]; entries!: AddonModGlossaryEntryEntriesSwipeManager; glossary?: AddonModGlossaryGlossary; entryUpdatedObserver?: CoreEventObserver; loaded = false; showAuthor = false; showDate = false; ratingInfo?: CoreRatingInfo; tagsEnabled = false; canEdit = false; canDelete = false; commentsEnabled = false; courseId!: number; cmId!: number; protected logView: () => void; constructor(@Optional() protected splitView: CoreSplitViewComponent, protected route: ActivatedRoute) { this.logView = CoreTime.once(async () => { if (!this.onlineEntry || !this.glossary || !this.componentId) { return; } await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.onlineEntry.id, this.componentId)); this.analyticsLogEvent('mod_glossary_get_entry_by_id', `/mod/glossary/showentry.php?eid=${this.onlineEntry.id}`); }); } get entry(): AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | undefined { return this.onlineEntry ?? this.offlineEntry; } /** * @inheritdoc */ async ngOnInit(): Promise { let onlineEntryId: number | null = null; let offlineEntryTimeCreated: number | null = null; try { this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); this.tagsEnabled = CoreTag.areTagsAvailableInSite(); this.commentsEnabled = CoreComments.areCommentsEnabledInSite(); this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); const entrySlug = CoreNavigator.getRequiredRouteParam('entrySlug'); const routeData = this.route.snapshot.data; const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( AddonModGlossaryEntriesSource, [this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''], ); this.entries = new AddonModGlossaryEntryEntriesSwipeManager(source); await this.entries.start(); if (entrySlug.startsWith('new-')) { offlineEntryTimeCreated = Number(entrySlug.slice(4)); } else { onlineEntryId = Number(entrySlug); } } catch (error) { CoreDomUtils.showErrorModal(error); CoreNavigator.back(); return; } this.entryUpdatedObserver = CoreEvents.on(GLOSSARY_ENTRY_UPDATED, data => { if (data.glossaryId !== this.glossary?.id) { return; } if ( (this.onlineEntry && this.onlineEntry.id === data.entryId) || (this.offlineEntry && this.offlineEntry.timecreated === data.timecreated) ) { this.doRefresh(); } }); try { if (onlineEntryId) { await this.loadOnlineEntry(onlineEntryId); } else if (offlineEntryTimeCreated) { await this.loadOfflineEntry(offlineEntryTimeCreated); } } finally { this.loaded = true; } } /** * @inheritdoc */ ngOnDestroy(): void { this.entries.destroy(); this.entryUpdatedObserver?.off(); } /** * Edit entry. */ async editEntry(): Promise { await CoreNavigator.navigate('./edit'); } /** * Delete entry. */ async deleteEntry(): Promise { // Log analytics even if the user cancels for consistency with LMS. this.analyticsLogEvent( 'mod_glossary_delete_entry', `/mod/glossary/deleteentry.php?id=${this.glossary?.id}&mode=delete&entry=${this.onlineEntry?.id}`, ); const glossaryId = this.glossary?.id; const cancelled = await CoreUtils.promiseFails( CoreDomUtils.showConfirm(Translate.instant('addon.mod_glossary.areyousuredelete')), ); if (!glossaryId || cancelled) { return; } const modal = await CoreDomUtils.showModalLoading(); try { if (this.onlineEntry) { const entryId = this.onlineEntry.id; await AddonModGlossary.deleteEntry(glossaryId, entryId); await Promise.all([ CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(entryId)), CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByLetter(glossaryId)), CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByAuthor(glossaryId)), CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByCategory(glossaryId)), CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'CREATION')), CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'UPDATE')), CoreUtils.ignoreErrors(this.entries.getSource().invalidateCache(false)), ]); } else if (this.offlineEntry) { const concept = this.offlineEntry.concept; const timecreated = this.offlineEntry.timecreated; await AddonModGlossaryOffline.deleteOfflineEntry(glossaryId, timecreated); await AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timecreated); } CoreDomUtils.showToast('addon.mod_glossary.entrydeleted', true, ToastDuration.LONG); if (this.splitView?.outletActivated) { await CoreNavigator.navigate('../../'); } else { await CoreNavigator.back(); } } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errordeleting', true); } finally { modal.dismiss(); } } /** * Refresh the data. * * @param refresher Refresher. * @returns Promise resolved when done. */ async doRefresh(refresher?: HTMLIonRefresherElement): Promise { if (this.onlineEntry && this.glossary?.allowcomments && this.onlineEntry.id > 0 && this.commentsEnabled && this.comments) { // Refresh comments asynchronously (without blocking the current promise). CoreUtils.ignoreErrors(this.comments.doRefresh()); } try { if (this.onlineEntry) { await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(this.onlineEntry.id)); await this.loadOnlineEntry(this.onlineEntry.id); } else if (this.offlineEntry) { const entrySlug = CoreNavigator.getRequiredRouteParam('entrySlug'); const timecreated = Number(entrySlug.slice(4)); await this.loadOfflineEntry(timecreated); } } finally { refresher?.complete(); } } /** * Load online entry data. */ protected async loadOnlineEntry(entryId: number): Promise { try { const result = await AddonModGlossary.getEntry(entryId); const canDeleteEntries = CoreNetwork.isOnline() && await AddonModGlossary.canDeleteEntries(); const canUpdateEntries = CoreNetwork.isOnline() && await AddonModGlossary.canUpdateEntries(); this.onlineEntry = result.entry; this.ratingInfo = result.ratinginfo; this.canDelete = canDeleteEntries && !!result.permissions?.candelete; this.canEdit = canUpdateEntries && !!result.permissions?.canupdate; await this.loadGlossary(); this.logView(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); } } /** * Load offline entry data. * * @param timecreated Entry Timecreated. */ protected async loadOfflineEntry(timecreated: number): Promise { try { const glossary = await this.loadGlossary(); this.offlineEntry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, timecreated); this.offlineEntryFiles = this.offlineEntry.attachments && this.offlineEntry.attachments.offline > 0 ? await AddonModGlossaryHelper.getStoredFiles( glossary.id, this.offlineEntry.concept, timecreated, ) : undefined; this.canEdit = true; this.canDelete = true; } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); } } /** * Load glossary data. * * @returns Glossary. */ protected async loadGlossary(): Promise { if (this.glossary) { return this.glossary; } this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.cmId); this.componentId = this.glossary.coursemodule; switch (this.glossary.displayformat) { case 'fullwithauthor': case 'encyclopedia': this.showAuthor = true; this.showDate = true; break; case 'fullwithoutauthor': this.showAuthor = false; this.showDate = true; break; default: // Default, and faq, simple, entrylist, continuous. this.showAuthor = false; this.showDate = false; } return this.glossary; } /** * Function called when rating is updated online. */ ratingUpdated(): void { if (!this.onlineEntry) { return; } AddonModGlossary.invalidateEntry(this.onlineEntry.id); } /** * Log analytics event. * * @param wsName WS name. * @param url URL. */ protected analyticsLogEvent(wsName: string, url: string): void { if (!this.onlineEntry || !this.glossary) { return; } CoreAnalytics.logEvent({ type: CoreAnalyticsEventType.VIEW_ITEM, ws: wsName, name: this.onlineEntry.concept, data: { id: this.onlineEntry.id, glossaryid: this.glossary.id, category: 'glossary' }, url, }); } } /** * Helper to manage swiping within a collection of glossary entries. */ class AddonModGlossaryEntryEntriesSwipeManager extends CoreSwipeNavigationItemsManager { /** * @inheritdoc */ protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null { const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; return `${this.getSource().GLOSSARY_PATH_PREFIX}entry/${snapshot.params.entrySlug}`; } }