// (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, OnInit, ViewChild, ElementRef, Optional } from '@angular/core'; import { FormControl } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { CoreError } from '@classes/errors/error'; import { CoreNetworkError } from '@classes/errors/network-error'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CanLeave } from '@guards/can-leave'; import { CoreFileEntry } from '@services/file-helper'; import { CoreNavigator } from '@services/navigator'; import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreForms } from '@singletons/form'; import { AddonModGlossary, AddonModGlossaryCategory, AddonModGlossaryEntry, AddonModGlossaryEntryOption, AddonModGlossaryGlossary, AddonModGlossaryProvider, } from '../../services/glossary'; import { AddonModGlossaryHelper } from '../../services/glossary-helper'; import { AddonModGlossaryOffline } from '../../services/glossary-offline'; /** * Page that displays the edit form. */ @Component({ selector: 'page-addon-mod-glossary-edit', templateUrl: 'edit.html', }) export class AddonModGlossaryEditPage implements OnInit, CanLeave { @ViewChild('editFormEl') formElement?: ElementRef; component = AddonModGlossaryProvider.COMPONENT; cmId!: number; courseId!: number; loaded = false; glossary?: AddonModGlossaryGlossary; definitionControl = new FormControl(); categories: AddonModGlossaryCategory[] = []; editorExtraParams: Record = {}; handler!: AddonModGlossaryFormHandler; data: AddonModGlossaryFormData = { concept: '', definition: '', timecreated: 0, attachments: [], categories: [], aliases: '', usedynalink: false, casesensitive: false, fullmatch: false, }; originalData?: AddonModGlossaryFormData; protected syncId?: string; protected syncObserver?: CoreEventObserver; protected isDestroyed = false; protected saved = false; constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {} /** * @inheritdoc */ async ngOnInit(): Promise { try { const entrySlug = CoreNavigator.getRouteParam('entrySlug'); this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); if (entrySlug?.startsWith('new-')) { const timecreated = Number(entrySlug.slice(4)); this.editorExtraParams.timecreated = timecreated; this.handler = new AddonModGlossaryOfflineFormHandler(this, timecreated); } else if (entrySlug) { const { entry } = await AddonModGlossary.getEntry(Number(entrySlug)); this.editorExtraParams.timecreated = entry.timecreated; this.handler = new AddonModGlossaryOnlineFormHandler(this, entry); } else { this.handler = new AddonModGlossaryNewFormHandler(this); } } catch (error) { CoreDomUtils.showErrorModal(error); this.goBack(); return; } this.fetchData(); } /** * Fetch required data. * * @returns Promise resolved when done. */ protected async fetchData(): Promise { try { this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.cmId); await this.handler.loadData(this.glossary); this.categories = await AddonModGlossary.getAllCategories(this.glossary.id, { cmId: this.cmId, }); this.loaded = true; } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingglossary', true); this.goBack(); } } /** * Reset the form data. */ protected resetForm(): void { this.originalData = undefined; this.data.concept = ''; this.data.definition = ''; this.data.timecreated = 0; this.data.categories = []; this.data.aliases = ''; this.data.usedynalink = false; this.data.casesensitive = false; this.data.fullmatch = false; this.data.attachments.length = 0; // Empty the array. this.definitionControl.setValue(''); } /** * Definition changed. * * @param text The new text. */ onDefinitionChange(text: string): void { this.data.definition = text; } /** * Check if we can leave the page or not. * * @returns Resolved if we can leave it, rejected if not. */ async canLeave(): Promise { if (this.saved) { return true; } if (this.hasDataChanged()) { // 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.data.attachments); CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId()); return true; } /** * Save the entry. */ async save(): Promise { if (!this.data.concept || !this.data.definition) { CoreDomUtils.showErrorModal('addon.mod_glossary.fillfields', true); return; } if (!this.glossary) { return; } const modal = await CoreDomUtils.showModalLoading('core.sending', true); try { const savedOnline = await this.handler.save(this.glossary); this.saved = true; CoreForms.triggerFormSubmittedEvent(this.formElement, savedOnline, CoreSites.getCurrentSiteId()); this.goBack(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.cannoteditentry', true); } finally { modal.dismiss(); } } /** * Check if the form data has changed. * * @returns True if data has changed, false otherwise. */ protected hasDataChanged(): boolean { if (!this.originalData || this.originalData.concept === undefined) { // There is no original data. return !!(this.data.definition || this.data.concept || this.data.attachments.length > 0); } if (this.originalData.definition != this.data.definition || this.originalData.concept != this.data.concept) { return true; } return CoreFileUploader.areFileListDifferent(this.data.attachments, this.originalData.attachments); } /** * Helper function to go back. */ protected goBack(): void { if (this.splitView?.outletActivated) { CoreNavigator.navigate('../../'); } else { CoreNavigator.back(); } } } /** * Helper to manage form data. */ abstract class AddonModGlossaryFormHandler { constructor(protected page: AddonModGlossaryEditPage) {} /** * Load form data. * * @param glossary Glossary. */ abstract loadData(glossary: AddonModGlossaryGlossary): Promise; /** * Save form data. * * @param glossary Glossary. * @returns Whether the form was saved online. */ abstract save(glossary: AddonModGlossaryGlossary): Promise; /** * Upload attachments online. * * @param glossary Glossary. * @returns Uploaded attachments item id. */ protected async uploadAttachments(glossary: AddonModGlossaryGlossary): Promise { const data = this.page.data; const itemId = await CoreFileUploader.uploadOrReuploadFiles( data.attachments, AddonModGlossaryProvider.COMPONENT, glossary.id, ); return itemId; } /** * Store attachments offline. * * @param glossary Glossary. * @param timecreated Entry time created. * @returns Storage result. */ protected async storeAttachments( glossary: AddonModGlossaryGlossary, timecreated: number, ): Promise { const data = this.page.data; const result = await AddonModGlossaryHelper.storeFiles( glossary.id, data.concept, timecreated, data.attachments, ); return result; } /** * Make sure that the new entry won't create any duplicates. * * @param glossary Glossary. */ protected async checkDuplicates(glossary: AddonModGlossaryGlossary): Promise { if (glossary.allowduplicatedentries) { return; } const data = this.page.data; const isUsed = await AddonModGlossary.isConceptUsed(glossary.id, data.concept, { timeCreated: data.timecreated, cmId: this.page.cmId, }); if (isUsed) { // There's a entry with same name, reject with error message. throw new CoreError(Translate.instant('addon.mod_glossary.errconceptalreadyexists')); } } /** * Get additional options to save an entry. * * @param glossary Glossary. * @returns Options. */ protected getSaveOptions(glossary: AddonModGlossaryGlossary): Record { const data = this.page.data; const options: Record = { aliases: data.aliases, categories: data.categories.join(','), }; if (glossary.usedynalink) { options.usedynalink = data.usedynalink ? 1 : 0; if (data.usedynalink) { options.casesensitive = data.casesensitive ? 1 : 0; options.fullmatch = data.fullmatch ? 1 : 0; } } return options; } } /** * Helper to manage the form data for an offline entry. */ class AddonModGlossaryOfflineFormHandler extends AddonModGlossaryFormHandler { private timecreated: number; constructor(page: AddonModGlossaryEditPage, timecreated: number) { super(page); this.timecreated = timecreated; } /** * @inheritdoc */ async loadData(glossary: AddonModGlossaryGlossary): Promise { const data = this.page.data; const entry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, this.timecreated); data.concept = entry.concept || ''; data.definition = entry.definition || ''; data.timecreated = entry.timecreated; if (entry.options) { data.categories = (entry.options.categories && ( entry.options.categories).split(',')) || []; data.aliases = entry.options.aliases || ''; data.usedynalink = !!entry.options.usedynalink; if (data.usedynalink) { data.casesensitive = !!entry.options.casesensitive; data.fullmatch = !!entry.options.fullmatch; } } // Treat offline attachments if any. if (entry.attachments?.offline) { data.attachments = await AddonModGlossaryHelper.getStoredFiles(glossary.id, entry.concept, entry.timecreated); } this.page.originalData = { concept: data.concept, definition: data.definition, attachments: data.attachments.slice(), timecreated: data.timecreated, categories: data.categories.slice(), aliases: data.aliases, usedynalink: data.usedynalink, casesensitive: data.casesensitive, fullmatch: data.fullmatch, }; this.page.definitionControl.setValue(data.definition); } /** * @inheritdoc */ async save(glossary: AddonModGlossaryGlossary): Promise { const data = this.page.data; // Upload attachments first if any. let offlineAttachments: CoreFileUploaderStoreFilesResult | undefined = undefined; if (data.attachments.length) { offlineAttachments = await this.storeAttachments(glossary, data.timecreated); } // Save entry data. await this.updateOfflineEntry(glossary, offlineAttachments); // Delete the local files from the tmp folder. CoreFileUploader.clearTmpFiles(data.attachments); return false; } /** * Update an offline entry. * * @param glossary Glossary. * @param uploadedAttachments Uploaded attachments. */ protected async updateOfflineEntry( glossary: AddonModGlossaryGlossary, uploadedAttachments?: CoreFileUploaderStoreFilesResult, ): Promise { const originalData = this.page.originalData; const data = this.page.data; const options = this.getSaveOptions(glossary); const definition = CoreTextUtils.formatHtmlLines(data.definition); if (!originalData) { return; } await this.checkDuplicates(glossary); await AddonModGlossaryOffline.updateOfflineEntry( { glossaryid: glossary.id, courseid: this.page.courseId, concept: originalData.concept, timecreated: originalData.timecreated, }, data.concept, definition, options, uploadedAttachments, ); } } /** * Helper to manage the form data for creating a new entry. */ class AddonModGlossaryNewFormHandler extends AddonModGlossaryFormHandler { /** * @inheritdoc */ async loadData(): Promise { // There is no data to load, given that this is a new entry. } /** * @inheritdoc */ async save(glossary: AddonModGlossaryGlossary): Promise { const data = this.page.data; const timecreated = Date.now(); // Upload attachments first if any. let onlineAttachments: number | undefined = undefined; let offlineAttachments: CoreFileUploaderStoreFilesResult | undefined = undefined; if (data.attachments.length) { try { onlineAttachments = await this.uploadAttachments(glossary); } catch (error) { if (CoreUtils.isWebServiceError(error)) { throw error; } offlineAttachments = await this.storeAttachments(glossary, timecreated); } } // Save entry data. const entryId = offlineAttachments ? await this.createOfflineEntry(glossary, timecreated, offlineAttachments) : await this.createOnlineEntry(glossary, timecreated, onlineAttachments, !data.attachments.length); // Delete the local files from the tmp folder. CoreFileUploader.clearTmpFiles(data.attachments); if (entryId) { // Data sent to server, delete stored files (if any). AddonModGlossaryHelper.deleteStoredFiles(glossary.id, data.concept, timecreated); CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' }); } return !!entryId; } /** * Create an offline entry. * * @param glossary Glossary. * @param timecreated Time created. * @param uploadedAttachments Uploaded attachments. */ protected async createOfflineEntry( glossary: AddonModGlossaryGlossary, timecreated: number, uploadedAttachments?: CoreFileUploaderStoreFilesResult, ): Promise { const data = this.page.data; const options = this.getSaveOptions(glossary); const definition = CoreTextUtils.formatHtmlLines(data.definition); await this.checkDuplicates(glossary); await AddonModGlossaryOffline.addOfflineEntry( glossary.id, data.concept, definition, this.page.courseId, timecreated, options, uploadedAttachments, undefined, undefined, ); } /** * Create an online entry. * * @param glossary Glossary. * @param timecreated Time created. * @param uploadedAttachmentsId Id of the uploaded attachments. * @param allowOffline Allow falling back to creating the entry offline. * @returns Entry id. */ protected async createOnlineEntry( glossary: AddonModGlossaryGlossary, timecreated: number, uploadedAttachmentsId?: number, allowOffline?: boolean, ): Promise { const data = this.page.data; const options = this.getSaveOptions(glossary); const definition = CoreTextUtils.formatHtmlLines(data.definition); const entryId = await AddonModGlossary.addEntry( glossary.id, data.concept, definition, this.page.courseId, options, uploadedAttachmentsId, { timeCreated: timecreated, allowOffline: allowOffline, checkDuplicates: !glossary.allowduplicatedentries, }, ); return entryId; } } /** * Helper to manage the form data for an online entry. */ class AddonModGlossaryOnlineFormHandler extends AddonModGlossaryFormHandler { private entry: AddonModGlossaryEntry; constructor(page: AddonModGlossaryEditPage, entry: AddonModGlossaryEntry) { super(page); this.entry = entry; } /** * @inheritdoc */ async loadData(): Promise { const data = this.page.data; data.concept = this.entry.concept; data.definition = this.entry.definition || ''; data.timecreated = this.entry.timecreated; data.usedynalink = this.entry.usedynalink; if (data.usedynalink) { data.casesensitive = this.entry.casesensitive; data.fullmatch = this.entry.fullmatch; } // Treat offline attachments if any. if (this.entry.attachments) { data.attachments = this.entry.attachments; } this.page.originalData = { concept: data.concept, definition: data.definition, attachments: data.attachments.slice(), timecreated: data.timecreated, categories: data.categories.slice(), aliases: data.aliases, usedynalink: data.usedynalink, casesensitive: data.casesensitive, fullmatch: data.fullmatch, }; this.page.definitionControl.setValue(data.definition); } /** * @inheritdoc */ async save(glossary: AddonModGlossaryGlossary): Promise { if (!CoreNetwork.isOnline()) { throw new CoreNetworkError(); } const data = this.page.data; const options = this.getSaveOptions(glossary); const definition = CoreTextUtils.formatHtmlLines(data.definition); // Save entry data. await AddonModGlossary.updateEntry(glossary.id, this.entry.id, data.concept, definition, options); CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' }); return true; } } /** * Form data. */ type AddonModGlossaryFormData = { concept: string; definition: string; timecreated: number; attachments: CoreFileEntry[]; categories: string[]; aliases: string; usedynalink: boolean; casesensitive: boolean; fullmatch: boolean; };