680 lines
20 KiB
TypeScript
680 lines
20 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, 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<string, unknown> = {};
|
|
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<void> {
|
|
try {
|
|
const entrySlug = CoreNavigator.getRouteParam<string>('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<void> {
|
|
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<boolean> {
|
|
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<void> {
|
|
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<void>;
|
|
|
|
/**
|
|
* Save form data.
|
|
*
|
|
* @param glossary Glossary.
|
|
* @returns Whether the form was saved online.
|
|
*/
|
|
abstract save(glossary: AddonModGlossaryGlossary): Promise<boolean>;
|
|
|
|
/**
|
|
* Upload attachments online.
|
|
*
|
|
* @param glossary Glossary.
|
|
* @returns Uploaded attachments item id.
|
|
*/
|
|
protected async uploadAttachments(glossary: AddonModGlossaryGlossary): Promise<number> {
|
|
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<CoreFileUploaderStoreFilesResult> {
|
|
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<void> {
|
|
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<string, AddonModGlossaryEntryOption> {
|
|
const data = this.page.data;
|
|
const options: Record<string, AddonModGlossaryEntryOption> = {
|
|
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<void> {
|
|
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 && (<string> entry.options.categories).split(',')) || [];
|
|
data.aliases = <string> 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<boolean> {
|
|
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<void> {
|
|
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<void> {
|
|
// There is no data to load, given that this is a new entry.
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
async save(glossary: AddonModGlossaryGlossary): Promise<boolean> {
|
|
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<void> {
|
|
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<number | false> {
|
|
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<void> {
|
|
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<boolean> {
|
|
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;
|
|
};
|