From 010475b7901aa5e3a770f6de18aa1c1ac9e82c26 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 8 Apr 2021 16:30:00 +0200 Subject: [PATCH 1/6] MOBILE-3644 glossary: Migrate services --- src/addons/mod/glossary/glossary.module.ts | 69 + src/addons/mod/glossary/lang.json | 31 + .../glossary/services/database/glossary.ts | 121 ++ .../mod/glossary/services/glossary-helper.ts | 112 ++ .../mod/glossary/services/glossary-offline.ts | 258 +++ .../mod/glossary/services/glossary-sync.ts | 368 +++++ src/addons/mod/glossary/services/glossary.ts | 1465 +++++++++++++++++ .../glossary/services/handlers/edit-link.ts | 78 + .../glossary/services/handlers/entry-link.ts | 75 + .../glossary/services/handlers/index-link.ts | 33 + .../glossary/services/handlers/list-link.ts | 33 + .../mod/glossary/services/handlers/module.ts | 92 ++ .../glossary/services/handlers/prefetch.ts | 235 +++ .../glossary/services/handlers/sync-cron.ts | 44 + .../glossary/services/handlers/tag-area.ts | 53 + src/addons/mod/mod.module.ts | 2 + src/core/features/compile/services/compile.ts | 4 +- 17 files changed, 3071 insertions(+), 2 deletions(-) create mode 100644 src/addons/mod/glossary/glossary.module.ts create mode 100644 src/addons/mod/glossary/lang.json create mode 100644 src/addons/mod/glossary/services/database/glossary.ts create mode 100644 src/addons/mod/glossary/services/glossary-helper.ts create mode 100644 src/addons/mod/glossary/services/glossary-offline.ts create mode 100644 src/addons/mod/glossary/services/glossary-sync.ts create mode 100644 src/addons/mod/glossary/services/glossary.ts create mode 100644 src/addons/mod/glossary/services/handlers/edit-link.ts create mode 100644 src/addons/mod/glossary/services/handlers/entry-link.ts create mode 100644 src/addons/mod/glossary/services/handlers/index-link.ts create mode 100644 src/addons/mod/glossary/services/handlers/list-link.ts create mode 100644 src/addons/mod/glossary/services/handlers/module.ts create mode 100644 src/addons/mod/glossary/services/handlers/prefetch.ts create mode 100644 src/addons/mod/glossary/services/handlers/sync-cron.ts create mode 100644 src/addons/mod/glossary/services/handlers/tag-area.ts diff --git a/src/addons/mod/glossary/glossary.module.ts b/src/addons/mod/glossary/glossary.module.ts new file mode 100644 index 000000000..21b6a7afb --- /dev/null +++ b/src/addons/mod/glossary/glossary.module.ts @@ -0,0 +1,69 @@ +// (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 { APP_INITIALIZER, NgModule, Type } from '@angular/core'; +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate'; +import { CoreCronDelegate } from '@services/cron'; +import { CORE_SITE_SCHEMAS } from '@services/sites'; +import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/glossary'; +import { AddonModGlossaryProvider } from './services/glossary'; +import { AddonModGlossaryHelperProvider } from './services/glossary-helper'; +import { AddonModGlossaryOfflineProvider } from './services/glossary-offline'; +import { AddonModGlossarySyncProvider } from './services/glossary-sync'; +import { AddonModGlossaryEditLinkHandler } from './services/handlers/edit-link'; +import { AddonModGlossaryEntryLinkHandler } from './services/handlers/entry-link'; +import { AddonModGlossaryIndexLinkHandler } from './services/handlers/index-link'; +import { AddonModGlossaryListLinkHandler } from './services/handlers/list-link'; +import { AddonModGlossaryModuleHandler } from './services/handlers/module'; +import { AddonModGlossaryPrefetchHandler } from './services/handlers/prefetch'; +import { AddonModGlossarySyncCronHandler } from './services/handlers/sync-cron'; +import { AddonModGlossaryTagAreaHandler } from './services/handlers/tag-area'; + +export const ADDON_MOD_GLOSSARY_SERVICES: Type[] = [ + AddonModGlossaryProvider, + AddonModGlossaryOfflineProvider, + AddonModGlossarySyncProvider, + AddonModGlossaryHelperProvider, +]; + +@NgModule({ + imports: [ + ], + providers: [ + { + provide: CORE_SITE_SCHEMAS, + useValue: [SITE_SCHEMA, OFFLINE_SITE_SCHEMA], + multi: true, + }, + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + CoreCourseModuleDelegate.registerHandler(AddonModGlossaryModuleHandler.instance); + CoreCourseModulePrefetchDelegate.registerHandler(AddonModGlossaryPrefetchHandler.instance); + CoreCronDelegate.register(AddonModGlossarySyncCronHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModGlossaryIndexLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModGlossaryListLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModGlossaryEditLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModGlossaryEntryLinkHandler.instance); + CoreTagAreaDelegate.registerHandler(AddonModGlossaryTagAreaHandler.instance); + }, + }, + ], +}) +export class AddonModGlossaryModule {} diff --git a/src/addons/mod/glossary/lang.json b/src/addons/mod/glossary/lang.json new file mode 100644 index 000000000..ba4329f33 --- /dev/null +++ b/src/addons/mod/glossary/lang.json @@ -0,0 +1,31 @@ +{ + "addentry": "Add a new entry", + "aliases": "Keyword(s)", + "attachment": "Attachment", + "browsemode": "Browse entries", + "byalphabet": "Alphabetically", + "byauthor": "Group by author", + "bycategory": "Group by category", + "bynewestfirst": "Newest first", + "byrecentlyupdated": "Recently updated", + "bysearch": "Search", + "cannoteditentry": "Cannot edit entry", + "casesensitive": "This entry is case sensitive", + "categories": "Categories", + "concept": "Concept", + "definition": "Definition", + "entriestobesynced": "Entries to be synced", + "entrypendingapproval": "This entry is pending approval.", + "entryusedynalink": "This entry should be automatically linked", + "errconceptalreadyexists": "This concept already exists. No duplicates allowed in this glossary.", + "errorloadingentries": "An error occurred while loading entries.", + "errorloadingentry": "An error occurred while loading the entry.", + "errorloadingglossary": "An error occurred while loading the glossary.", + "fillfields": "Concept and definition are mandatory fields.", + "fullmatch": "Match whole words only", + "linking": "Auto-linking", + "modulenameplural": "Glossaries", + "noentriesfound": "No entries were found.", + "searchquery": "Search query", + "tagarea_glossary_entries": "Glossary entries" +} diff --git a/src/addons/mod/glossary/services/database/glossary.ts b/src/addons/mod/glossary/services/database/glossary.ts new file mode 100644 index 000000000..4149405c9 --- /dev/null +++ b/src/addons/mod/glossary/services/database/glossary.ts @@ -0,0 +1,121 @@ +// (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 { CoreSiteSchema } from '@services/sites'; + +/** + * Database variables for AddonModGlossaryProvider. + */ +export const ENTRIES_TABLE_NAME = 'addon_mod_glossary_entry_glossaryid'; +export const SITE_SCHEMA: CoreSiteSchema = { + name: 'AddonModGlossaryProvider', + version: 1, + tables: [ + { + name: ENTRIES_TABLE_NAME, + columns: [ + { + name: 'entryid', + type: 'INTEGER', + primaryKey: true, + }, + { + name: 'glossaryid', + type: 'INTEGER', + }, + { + name: 'pagefrom', + type: 'INTEGER', + }, + ], + }, + ], +}; + +/** + * Database variables for AddonModGlossaryOfflineProvider. + */ +export const OFFLINE_ENTRIES_TABLE_NAME = 'addon_mod_glossary_entrues'; +export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = { + name: 'AddonModGlossaryOfflineProvider', + version: 1, + tables: [ + { + name: OFFLINE_ENTRIES_TABLE_NAME, + columns: [ + { + name: 'glossaryid', + type: 'INTEGER', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'concept', + type: 'TEXT', + }, + { + name: 'definition', + type: 'TEXT', + }, + { + name: 'definitionformat', + type: 'TEXT', + }, + { + name: 'userid', + type: 'INTEGER', + }, + { + name: 'timecreated', + type: 'INTEGER', + }, + { + name: 'options', + type: 'TEXT', + }, + { + name: 'attachments', + type: 'TEXT', + }, + ], + primaryKeys: ['glossaryid', 'concept', 'timecreated'], + }, + ], +}; + +/** + * Glossary entry to get glossaryid from entryid. + */ +export type AddonModGlossaryEntryDBRecord = { + entryid: number; + glossaryid: number; + pagefrom: number; +}; + +/** + * Glossary offline entry. + */ +export type AddonModGlossaryOfflineEntryDBRecord = { + glossaryid: number; + courseid: number; + concept: string; + definition: string; + definitionformat: string; + userid: number; + timecreated: number; + options: string; + attachments: string; +}; diff --git a/src/addons/mod/glossary/services/glossary-helper.ts b/src/addons/mod/glossary/services/glossary-helper.ts new file mode 100644 index 000000000..a970e3d77 --- /dev/null +++ b/src/addons/mod/glossary/services/glossary-helper.ts @@ -0,0 +1,112 @@ +// (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 { Injectable } from '@angular/core'; +import { FileEntry } from '@ionic-native/file/ngx'; +import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; +import { CoreFile } from '@services/file'; +import { CoreUtils } from '@services/utils/utils'; +import { AddonModGlossaryOffline } from './glossary-offline'; +import { AddonModGlossaryNewEntry, AddonModGlossaryNewEntryWithFiles } from './glossary'; +import { makeSingleton } from '@singletons'; +import { CoreWSExternalFile } from '@services/ws'; + +/** + * Helper to gather some common functions for glossary. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryHelperProvider { + + /** + * Delete stored attachment files for a new entry. + * + * @param glossaryId Glossary ID. + * @param entryName The name of the entry. + * @param timeCreated The time the entry was created. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when deleted. + */ + async deleteStoredFiles(glossaryId: number, entryName: string, timeCreated: number, siteId?: string): Promise { + const folderPath = await AddonModGlossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId); + + await CoreUtils.ignoreErrors(CoreFile.removeDir(folderPath)); + } + + /** + * Get a list of stored attachment files for a new entry. See AddonModGlossaryHelperProvider#storeFiles. + * + * @param glossaryId lossary ID. + * @param entryName The name of the entry. + * @param timeCreated The time the entry was created. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the files. + */ + async getStoredFiles(glossaryId: number, entryName: string, timeCreated: number, siteId?: string): Promise { + const folderPath = await AddonModGlossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId); + + return CoreFileUploader.getStoredFiles(folderPath); + } + + /** + * Check if the data of an entry has changed. + * + * @param entry Current data. + * @param files Files attached. + * @param original Original content. + * @return True if data has changed, false otherwise. + */ + hasEntryDataChanged( + entry: AddonModGlossaryNewEntry, + files: (CoreWSExternalFile | FileEntry)[], + original?: AddonModGlossaryNewEntryWithFiles, + ): boolean { + if (!original || typeof original.concept == 'undefined') { + // There is no original data. + return !!(entry.definition || entry.concept || files.length > 0); + } + + if (original.definition != entry.definition || original.concept != entry.concept) { + return true; + } + + return CoreFileUploader.areFileListDifferent(files, original.files); + } + + /** + * Given a list of files (either online files or local files), store the local files in a local folder + * to be submitted later. + * + * @param glossaryId Glossary ID. + * @param entryName The name of the entry. + * @param timeCreated The time the entry was created. + * @param files List of files. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if success, rejected otherwise. + */ + async storeFiles( + glossaryId: number, + entryName: string, + timeCreated: number, + files: (CoreWSExternalFile | FileEntry)[], + siteId?: string, + ): Promise { + // Get the folder where to store the files. + const folderPath = await AddonModGlossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId); + + return CoreFileUploader.storeFilesToUpload(folderPath, files); + } + +} + +export const AddonModGlossaryHelper = makeSingleton(AddonModGlossaryHelperProvider); diff --git a/src/addons/mod/glossary/services/glossary-offline.ts b/src/addons/mod/glossary/services/glossary-offline.ts new file mode 100644 index 000000000..4c9d7216b --- /dev/null +++ b/src/addons/mod/glossary/services/glossary-offline.ts @@ -0,0 +1,258 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; +import { CoreFile } from '@services/file'; +import { CoreSites } from '@services/sites'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton } from '@singletons'; +import { AddonModGlossaryOfflineEntryDBRecord, OFFLINE_ENTRIES_TABLE_NAME } from './database/glossary'; +import { AddonModGlossaryDiscardedEntry, AddonModGlossaryEntryOption } from './glossary'; + +/** + * Service to handle offline glossary. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryOfflineProvider { + + /** + * Delete a new entry. + * + * @param glossaryId Glossary ID. + * @param concept Glossary entry concept. + * @param timeCreated The time the entry was created. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if deleted, rejected if failure. + */ + async deleteNewEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const conditions: Partial = { + glossaryid: glossaryId, + concept: concept, + timecreated: timeCreated, + }; + + await site.getDb().deleteRecords(OFFLINE_ENTRIES_TABLE_NAME, conditions); + } + + /** + * Get all the stored new entries from all the glossaries. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with entries. + */ + async getAllNewEntries(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const records = await site.getDb().getRecords(OFFLINE_ENTRIES_TABLE_NAME); + + return records.map(record => this.parseRecord(record)); + } + + /** + * Get a stored new entry. + * + * @param glossaryId Glossary ID. + * @param concept Glossary entry concept. + * @param timeCreated The time the entry was created. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with entry. + */ + async getNewEntry( + glossaryId: number, + concept: string, + timeCreated: number, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const conditions: Partial = { + glossaryid: glossaryId, + concept: concept, + timecreated: timeCreated, + }; + + const record = await site.getDb().getRecord(OFFLINE_ENTRIES_TABLE_NAME, conditions); + + return this.parseRecord(record); + } + + /** + * Get all the stored add entry data from a certain glossary. + * + * @param glossaryId Glossary ID. + * @param siteId Site ID. If not defined, current site. + * @param userId User the entries belong to. If not defined, current user in site. + * @return Promise resolved with entries. + */ + async getGlossaryNewEntries(glossaryId: number, siteId?: string, userId?: number): Promise { + const site = await CoreSites.getSite(siteId); + + const conditions: Partial = { + glossaryid: glossaryId, + userid: userId || site.getUserId(), + }; + + const records = await site.getDb().getRecords(OFFLINE_ENTRIES_TABLE_NAME, conditions); + + return records.map(record => this.parseRecord(record)); + } + + /** + * Check if a concept is used offline. + * + * @param glossaryId Glossary ID. + * @param concept Concept to check. + * @param timeCreated Time of the entry we are editing. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with true if concept is found, false otherwise. + */ + async isConceptUsed(glossaryId: number, concept: string, timeCreated?: number, siteId?: string): Promise { + try { + const site = await CoreSites.getSite(siteId); + + const conditions: Partial = { + glossaryid: glossaryId, + concept: concept, + }; + + const entries = + await site.getDb().getRecords(OFFLINE_ENTRIES_TABLE_NAME, conditions); + + if (!entries.length) { + return false; + } + + if (entries.length > 1 || !timeCreated) { + return true; + } + + // If there's only one entry, check that is not the one we are editing. + return CoreUtils.promiseFails(this.getNewEntry(glossaryId, concept, timeCreated, siteId)); + } catch { + // No offline data found, return false. + return false; + } + } + + /** + * Save a new entry to be sent later. + * + * @param glossaryId Glossary ID. + * @param concept Glossary entry concept. + * @param definition Glossary entry concept definition. + * @param courseId Course ID of the glossary. + * @param options Options for the entry. + * @param attachments Result of CoreFileUploaderProvider#storeFilesToUpload for attachments. + * @param timeCreated The time the entry was created. If not defined, current time. + * @param siteId Site ID. If not defined, current site. + * @param userId User the entry belong to. If not defined, current user in site. + * @param discardEntry The entry provided will be discarded if found. + * @return Promise resolved if stored, rejected if failure. + */ + async addNewEntry( + glossaryId: number, + concept: string, + definition: string, + courseId: number, + options?: Record, + attachments?: CoreFileUploaderStoreFilesResult, + timeCreated?: number, + siteId?: string, + userId?: number, + discardEntry?: AddonModGlossaryDiscardedEntry, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const entry: AddonModGlossaryOfflineEntryDBRecord = { + glossaryid: glossaryId, + courseid: courseId, + concept: concept, + definition: definition, + definitionformat: 'html', + options: JSON.stringify(options || {}), + attachments: JSON.stringify(attachments), + userid: userId || site.getUserId(), + timecreated: timeCreated || Date.now(), + }; + + // If editing an offline entry, delete previous first. + if (discardEntry) { + await this.deleteNewEntry(glossaryId, discardEntry.concept, discardEntry.timecreated, site.getId()); + } + + await site.getDb().insertRecord(OFFLINE_ENTRIES_TABLE_NAME, entry); + + return false; + } + + /** + * Get the path to the folder where to store files for offline attachments in a glossary. + * + * @param glossaryId Glossary ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the path. + */ + async getGlossaryFolder(glossaryId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const siteFolderPath = CoreFile.getSiteFolder(site.getId()); + const folderPath = 'offlineglossary/' + glossaryId; + + return CoreTextUtils.concatenatePaths(siteFolderPath, folderPath); + } + + /** + * Get the path to the folder where to store files for a new offline entry. + * + * @param glossaryId Glossary ID. + * @param concept The name of the entry. + * @param timeCreated Time to allow duplicated entries. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the path. + */ + async getEntryFolder(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise { + const folderPath = await this.getGlossaryFolder(glossaryId, siteId); + + return CoreTextUtils.concatenatePaths(folderPath, 'newentry_' + concept + '_' + timeCreated); + } + + /** + * Parse "options" and "attachments" columns of a fetched record. + * + * @param records Record object + * @return Record object with columns parsed. + */ + protected parseRecord(record: AddonModGlossaryOfflineEntryDBRecord): AddonModGlossaryOfflineEntry { + return Object.assign(record, { + options: > CoreTextUtils.parseJSON(record.options), + attachments: record.attachments ? + CoreTextUtils.parseJSON(record.attachments) : undefined, + }); + } + +} + +export const AddonModGlossaryOffline = makeSingleton(AddonModGlossaryOfflineProvider); + +/** + * Glossary offline entry with parsed data. + */ +export type AddonModGlossaryOfflineEntry = Omit & { + options: Record; + attachments?: CoreFileUploaderStoreFilesResult; +}; diff --git a/src/addons/mod/glossary/services/glossary-sync.ts b/src/addons/mod/glossary/services/glossary-sync.ts new file mode 100644 index 000000000..ca697c46d --- /dev/null +++ b/src/addons/mod/glossary/services/glossary-sync.ts @@ -0,0 +1,368 @@ +// (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 { ContextLevel } from '@/core/constants'; +import { Injectable } from '@angular/core'; +import { FileEntry } from '@ionic-native/file/ngx'; +import { CoreSyncBlockedError } from '@classes/base-sync'; +import { CoreNetworkError } from '@classes/errors/network-error'; +import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync'; +import { CoreCourseLogHelper } from '@features/course/services/log-helper'; +import { CoreRatingSync } from '@features/rating/services/rating-sync'; +import { CoreApp } from '@services/app'; +import { CoreSites } from '@services/sites'; +import { CoreSync } from '@services/sync'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWSExternalFile } from '@services/ws'; +import { makeSingleton, Translate } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { AddonModGlossary, AddonModGlossaryProvider } from './glossary'; +import { AddonModGlossaryHelper } from './glossary-helper'; +import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from './glossary-offline'; +import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; + +/** + * Service to sync glossaries. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProvider { + + static readonly AUTO_SYNCED = 'addon_mod_glossary_autom_synced'; + + protected componentTranslatableString = 'glossary'; + + constructor() { + super('AddonModGlossarySyncProvider'); + } + + /** + * Try to synchronize all the glossaries in a certain site or in all sites. + * + * @param siteId Site ID to sync. If not defined, sync all sites. + * @param force Wether to force sync not depending on last execution. + * @return Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllGlossaries(siteId?: string, force?: boolean): Promise { + return this.syncOnSites('all glossaries', this.syncAllGlossariesFunc.bind(this, !!force), siteId); + } + + /** + * Sync all glossaries on a site. + * + * @param force Wether to force sync not depending on last execution. + * @param siteId Site ID to sync. + * @return Promise resolved if sync is successful, rejected if sync fails. + */ + protected async syncAllGlossariesFunc(force: boolean, siteId: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + await Promise.all([ + this.syncAllGlossariesEntries(force, siteId), + this.syncRatings(undefined, force, siteId), + ]); + } + + /** + * Sync entried of all glossaries on a site. + * + * @param force Wether to force sync not depending on last execution. + * @param siteId Site ID to sync. + * @return Promise resolved if sync is successful, rejected if sync fails. + */ + protected async syncAllGlossariesEntries(force: boolean, siteId: string): Promise { + const entries = await AddonModGlossaryOffline.getAllNewEntries(siteId); + + // Do not sync same glossary twice. + const treated: Record = {}; + + await Promise.all(entries.map(async (entry) => { + if (treated[entry.glossaryid]) { + return; + } + + treated[entry.glossaryid] = true; + + const result = force ? + await this.syncGlossaryEntries(entry.glossaryid, entry.userid, siteId) : + await this.syncGlossaryEntriesIfNeeded(entry.glossaryid, entry.userid, siteId); + + if (result?.updated) { + // Sync successful, send event. + CoreEvents.trigger(AddonModGlossarySyncProvider.AUTO_SYNCED, { + glossaryId: entry.glossaryid, + userId: entry.userid, + warnings: result.warnings, + }, siteId); + } + })); + } + + /** + * Sync a glossary only if a certain time has passed since the last time. + * + * @param glossaryId Glossary ID. + * @param userId User the entry belong to. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the glossary is synced or if it doesn't need to be synced. + */ + async syncGlossaryEntriesIfNeeded( + glossaryId: number, + userId: number, + siteId?: string, + ): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const syncId = this.getGlossarySyncId(glossaryId, userId); + + const needed = await this.isSyncNeeded(syncId, siteId); + + if (needed) { + return this.syncGlossaryEntries(glossaryId, userId, siteId); + } + } + + /** + * Synchronize all offline entries of a glossary. + * + * @param glossaryId Glossary ID to be synced. + * @param userId User the entries belong to. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if sync is successful, rejected otherwise. + */ + syncGlossaryEntries(glossaryId: number, userId?: number, siteId?: string): Promise { + userId = userId || CoreSites.getCurrentSiteUserId(); + siteId = siteId || CoreSites.getCurrentSiteId(); + + const syncId = this.getGlossarySyncId(glossaryId, userId); + if (this.isSyncing(syncId, siteId)) { + // There's already a sync ongoing for this glossary, return the promise. + return this.getOngoingSync(syncId, siteId)!; + } + + // Verify that glossary isn't blocked. + if (CoreSync.isBlocked(AddonModGlossaryProvider.COMPONENT, syncId, siteId)) { + this.logger.debug('Cannot sync glossary ' + glossaryId + ' because it is blocked.'); + + throw new CoreSyncBlockedError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate })); + } + + this.logger.debug('Try to sync glossary ' + glossaryId + ' for user ' + userId); + + const syncPromise = this.performSyncGlossaryEntries(glossaryId, userId, siteId); + + return this.addOngoingSync(syncId, syncPromise, siteId); + } + + protected async performSyncGlossaryEntries( + glossaryId: number, + userId: number, + siteId: string, + ): Promise { + const result: AddonModGlossarySyncResult = { + warnings: [], + updated: false, + }; + const syncId = this.getGlossarySyncId(glossaryId, userId); + + // Sync offline logs. + await CoreUtils.ignoreErrors(CoreCourseLogHelper.syncActivity(AddonModGlossaryProvider.COMPONENT, glossaryId, siteId)); + + // Get offline responses to be sent. + const entries = await CoreUtils.ignoreErrors( + AddonModGlossaryOffline.getGlossaryNewEntries(glossaryId, siteId, userId), + [], + ); + + if (!entries.length) { + // Nothing to sync. + await CoreUtils.ignoreErrors(this.setSyncTime(syncId, siteId)); + + return result; + } else if (!CoreApp.isOnline()) { + // Cannot sync in offline. + throw new CoreNetworkError(); + } + + let courseId: number | undefined; + + await Promise.all(entries.map(async (data) => { + courseId = courseId || data.courseid; + + try { + // First of all upload the attachments (if any). + const itemId = await this.uploadAttachments(glossaryId, data, siteId); + + // Now try to add the entry. + await AddonModGlossary.addEntryOnline(glossaryId, data.concept, data.definition, data.options, itemId, siteId); + + result.updated = true; + + await this.deleteAddEntry(glossaryId, data.concept, data.timecreated, siteId); + } catch (error) { + if (!CoreUtils.isWebServiceError(error)) { + // Couldn't connect to server, reject. + throw error; + } + + // The WebService has thrown an error, this means that responses cannot be submitted. Delete them. + result.updated = true; + + await this.deleteAddEntry(glossaryId, data.concept, data.timecreated, siteId); + + // Responses deleted, add a warning. + result.warnings.push(Translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: data.concept, + error: CoreTextUtils.getErrorMessageFromError(error), + })); + } + })); + + if (result.updated && courseId) { + // Data has been sent to server. Now invalidate the WS calls. + try { + const glossary = await AddonModGlossary.getGlossaryById(courseId, glossaryId); + + await AddonModGlossary.invalidateGlossaryEntries(glossary, true); + } catch { + // Ignore errors. + } + } + + // Sync finished, set sync time. + await CoreUtils.ignoreErrors(this.setSyncTime(syncId, siteId)); + + return result; + } + + /** + * Synchronize offline ratings. + * + * @param cmId Course module to be synced. If not defined, sync all glossaries. + * @param force Wether to force sync not depending on last execution. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if sync is successful, rejected otherwise. + */ + async syncRatings(cmId?: number, force?: boolean, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const results = await CoreRatingSync.syncRatings('mod_glossary', 'entry', ContextLevel.MODULE, cmId, 0, force, siteId); + + let updated = false; + const warnings: string[] = []; + + await CoreUtils.allPromises(results.map(async (result) => { + if (result.updated.length) { + updated = true; + + // Invalidate entry of updated ratings. + await Promise.all(result.updated.map((itemId) => AddonModGlossary.invalidateEntry(itemId, siteId))); + } + + if (result.warnings.length) { + const glossary = await AddonModGlossary.getGlossary(result.itemSet.courseId, result.itemSet.instanceId, { siteId }); + + result.warnings.forEach((warning) => { + warnings.push(Translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: glossary.name, + error: warning, + })); + }); + } + })); + + return { updated, warnings }; + } + + /** + * Delete a new entry. + * + * @param glossaryId Glossary ID. + * @param concept Glossary entry concept. + * @param timeCreated Time to allow duplicated entries. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when deleted. + */ + protected async deleteAddEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise { + await Promise.all([ + AddonModGlossaryOffline.deleteNewEntry(glossaryId, concept, timeCreated, siteId), + AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timeCreated, siteId), + ]); + } + + /** + * Upload attachments of an offline entry. + * + * @param glossaryId Glossary ID. + * @param entry Offline entry. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with draftid if uploaded, resolved with 0 if nothing to upload. + */ + protected async uploadAttachments(glossaryId: number, entry: AddonModGlossaryOfflineEntry, siteId?: string): Promise { + if (!entry.attachments) { + // No attachments. + return 0; + } + + // Has some attachments to sync. + let files: (CoreWSExternalFile | FileEntry)[] = entry.attachments.online || []; + + if (entry.attachments.offline) { + // Has offline files. + const storedFiles = await CoreUtils.ignoreErrors( + AddonModGlossaryHelper.getStoredFiles(glossaryId, entry.concept, entry.timecreated, siteId), + [], // Folder not found, no files to add. + ); + + files = files.concat(storedFiles); + } + + return CoreFileUploader.uploadOrReuploadFiles(files, AddonModGlossaryProvider.COMPONENT, glossaryId, siteId); + } + + /** + * Get the ID of a glossary sync. + * + * @param glossaryId Glossary ID. + * @param userId User the entries belong to.. If not defined, current user. + * @return Sync ID. + */ + protected getGlossarySyncId(glossaryId: number, userId?: number): string { + userId = userId || CoreSites.getCurrentSiteUserId(); + + return 'glossary#' + glossaryId + '#' + userId; + } + +} + +export const AddonModGlossarySync = makeSingleton(AddonModGlossarySyncProvider); + +/** + * Data returned by a glossary sync. + */ +export type AddonModGlossarySyncResult = { + warnings: string[]; // List of warnings. + updated: boolean; // Whether some data was sent to the server or offline data was updated. +}; + +/** + * Data passed to AUTO_SYNCED event. + */ +export type AddonModGlossaryAutoSyncData = { + glossaryId: number; + userId: number; + warnings: string[]; +}; diff --git a/src/addons/mod/glossary/services/glossary.ts b/src/addons/mod/glossary/services/glossary.ts new file mode 100644 index 000000000..b34767f4a --- /dev/null +++ b/src/addons/mod/glossary/services/glossary.ts @@ -0,0 +1,1465 @@ +// (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 { Injectable } from '@angular/core'; +import { FileEntry } from '@ionic-native/file/ngx'; +import { CoreError } from '@classes/errors/error'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; +import { CoreCourseLogHelper } from '@features/course/services/log-helper'; +import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; +import { CoreRatingInfo } from '@features/rating/services/rating'; +import { CoreTagItem } from '@features/tag/services/tag'; +import { CoreApp } from '@services/app'; +import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; +import { makeSingleton, Translate } from '@singletons'; +import { AddonModGlossaryEntryDBRecord, ENTRIES_TABLE_NAME } from './database/glossary'; +import { AddonModGlossaryOffline } from './glossary-offline'; +import { AddonModGlossaryAutoSyncData, AddonModGlossarySyncProvider } from './glossary-sync'; + +const ROOT_CACHE_KEY = 'mmaModGlossary:'; + +/** + * Service that provides some features for glossaries. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryProvider { + + static readonly COMPONENT = 'mmaModGlossary'; + static readonly LIMIT_ENTRIES = 25; + static readonly LIMIT_CATEGORIES = 10; + static readonly SHOW_ALL_CATEGORIES = 0; + static readonly SHOW_NOT_CATEGORISED = -1; + + static readonly ADD_ENTRY_EVENT = 'addon_mod_glossary_add_entry'; + + /** + * Get the course glossary cache key. + * + * @param courseId Course Id. + * @return Cache key. + */ + protected getCourseGlossariesCacheKey(courseId: number): string { + return ROOT_CACHE_KEY + 'courseGlossaries:' + courseId; + } + + /** + * Get all the glossaries in a course. + * + * @param courseId Course Id. + * @param options Other options. + * @return Resolved with the glossaries. + */ + async getCourseGlossaries(courseId: number, options: CoreSitesCommonWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModGlossaryGetGlossariesByCoursesWSParams = { + courseids: [courseId], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getCourseGlossariesCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModGlossaryProvider.COMPONENT, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const result = await site.read( + 'mod_glossary_get_glossaries_by_courses', + params, + preSets, + ); + + return result.glossaries; + } + + /** + * Invalidate all glossaries in a course. + * + * @param courseId Course Id. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + async invalidateCourseGlossaries(courseId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const key = this.getCourseGlossariesCacheKey(courseId); + + await site.invalidateWsCacheForKey(key); + } + + /** + * Get the entries by author cache key. + * + * @param glossaryId Glossary Id. + * @param letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL. + * @param field Search and order using: FIRSTNAME or LASTNAME + * @param sort The direction of the order: ASC or DESC + * @return Cache key. + */ + protected getEntriesByAuthorCacheKey(glossaryId: number, letter: string, field: string, sort: string): string { + return ROOT_CACHE_KEY + 'entriesByAuthor:' + glossaryId + ':' + letter + ':' + field + ':' + sort; + } + + /** + * Get entries by author. + * + * @param glossaryId Glossary Id. + * @param letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL. + * @param field Search and order using: FIRSTNAME or LASTNAME + * @param sort The direction of the order: ASC or DESC + * @param options Other options. + * @return Resolved with the entries. + */ + async getEntriesByAuthor( + glossaryId: number, + letter: string, + field: string, + sort: string, + options: AddonModGlossaryGetEntriesOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModGlossaryGetEntriesByAuthorWSParams = { + id: glossaryId, + letter: letter, + field: field, + sort: sort, + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_glossary_get_entries_by_author', params, preSets); + } + + /** + * Invalidate cache of entries by author. + * + * @param glossaryId Glossary Id. + * @param letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL. + * @param field Search and order using: FIRSTNAME or LASTNAME + * @param sort The direction of the order: ASC or DESC + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + async invalidateEntriesByAuthor( + glossaryId: number, + letter: string, + field: string, + sort: string, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const key = this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort); + + await site.invalidateWsCacheForKey(key); + } + + /** + * Get entries by category. + * + * @param glossaryId Glossary Id. + * @param categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or + * constant SHOW_NOT_CATEGORISED for uncategorised entries. + * @param options Other options. + * @return Resolved with the entries. + */ + async getEntriesByCategory( + glossaryId: number, + categoryId: number, + options: AddonModGlossaryGetEntriesOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModGlossaryGetEntriesByCategoryWSParams = { + id: glossaryId, + categoryid: categoryId, + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getEntriesByCategoryCacheKey(glossaryId, categoryId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_glossary_get_entries_by_category', params, preSets); + } + + /** + * Invalidate cache of entries by category. + * + * @param glossaryId Glossary Id. + * @param categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or + * constant SHOW_NOT_CATEGORISED for uncategorised entries. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + async invalidateEntriesByCategory(glossaryId: number, categoryId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const key = this.getEntriesByCategoryCacheKey(glossaryId, categoryId); + + await site.invalidateWsCacheForKey(key); + } + + /** + * Get the entries by category cache key. + * + * @param glossaryId Glossary Id. + * @param categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or + * constant SHOW_NOT_CATEGORISED for uncategorised entries. + * @return Cache key. + */ + getEntriesByCategoryCacheKey(glossaryId: number, categoryId: number): string { + return ROOT_CACHE_KEY + 'entriesByCategory:' + glossaryId + ':' + categoryId; + } + + /** + * Get the entries by date cache key. + * + * @param glossaryId Glossary Id. + * @param order The way to order the records. + * @param sort The direction of the order. + * @return Cache key. + */ + getEntriesByDateCacheKey(glossaryId: number, order: string, sort: string): string { + return ROOT_CACHE_KEY + 'entriesByDate:' + glossaryId + ':' + order + ':' + sort; + } + + /** + * Get entries by date. + * + * @param glossaryId Glossary Id. + * @param order The way to order the records. + * @param sort The direction of the order. + * @param options Other options. + * @return Resolved with the entries. + */ + async getEntriesByDate( + glossaryId: number, + order: string, + sort: string, + options: AddonModGlossaryGetEntriesOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModGlossaryGetEntriesByDateWSParams = { + id: glossaryId, + order: order, + sort: sort, + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getEntriesByDateCacheKey(glossaryId, order, sort), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_glossary_get_entries_by_date', params, preSets); + } + + /** + * Invalidate cache of entries by date. + * + * @param glossaryId Glossary Id. + * @param order The way to order the records. + * @param sort The direction of the order. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + async invalidateEntriesByDate(glossaryId: number, order: string, sort: string, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const key = this.getEntriesByDateCacheKey(glossaryId, order, sort); + + await site.invalidateWsCacheForKey(key); + } + + /** + * Get the entries by letter cache key. + * + * @param glossaryId Glossary Id. + * @param letter A letter, or a special keyword. + * @return Cache key. + */ + protected getEntriesByLetterCacheKey(glossaryId: number, letter: string): string { + return ROOT_CACHE_KEY + 'entriesByLetter:' + glossaryId + ':' + letter; + } + + /** + * Get entries by letter. + * + * @param glossaryId Glossary Id. + * @param letter A letter, or a special keyword. + * @param options Other options. + * @return Resolved with the entries. + */ + async getEntriesByLetter( + glossaryId: number, + letter: string, + options: AddonModGlossaryGetEntriesOptions = {}, + ): Promise { + options.from = options.from || 0; + options.limit = options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES; + + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModGlossaryGetEntriesByLetterWSParams = { + id: glossaryId, + letter: letter, + from: options.from, + limit: options.limit, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getEntriesByLetterCacheKey(glossaryId, letter), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const result = await site.read( + 'mod_glossary_get_entries_by_letter', + params, + preSets, + ); + + if (options.limit == AddonModGlossaryProvider.LIMIT_ENTRIES) { + // Store entries in background, don't block the user for this. + CoreUtils.ignoreErrors(this.storeEntries(glossaryId, result.entries, options.from, site.getId())); + } + + return result; + } + + /** + * Invalidate cache of entries by letter. + * + * @param glossaryId Glossary Id. + * @param letter A letter, or a special keyword. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + async invalidateEntriesByLetter(glossaryId: number, letter: string, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const key = this.getEntriesByLetterCacheKey(glossaryId, letter); + + return site.invalidateWsCacheForKey(key); + } + + /** + * Get the entries by search cache key. + * + * @param glossaryId Glossary Id. + * @param query The search query. + * @param fullSearch Whether or not full search is required. + * @param order The way to order the results. + * @param sort The direction of the order. + * @return Cache key. + */ + protected getEntriesBySearchCacheKey( + glossaryId: number, + query: string, + fullSearch: boolean, + order: string, + sort: string, + ): string { + return ROOT_CACHE_KEY + 'entriesBySearch:' + glossaryId + ':' + fullSearch + ':' + order + ':' + sort + ':' + query; + } + + /** + * Get entries by search. + * + * @param glossaryId Glossary Id. + * @param query The search query. + * @param fullSearch Whether or not full search is required. + * @param order The way to order the results. + * @param sort The direction of the order. + * @param from Start returning records from here. + * @param limit Number of records to return. + * @param omitExpires True to always get the value from cache. If data isn't cached, it will call the WS. + * @param forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS. + * @param siteId Site ID. If not defined, current site. + * @return Resolved with the entries. + */ + async getEntriesBySearch( + glossaryId: number, + query: string, + fullSearch: boolean, + order: string, + sort: string, + options: AddonModGlossaryGetEntriesOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModGlossaryGetEntriesBySearchWSParams = { + id: glossaryId, + query: query, + fullsearch: fullSearch, + order: order, + sort: sort, + from: options.from || 0, + limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_glossary_get_entries_by_search', params, preSets); + } + + /** + * Invalidate cache of entries by search. + * + * @param glossaryId Glossary Id. + * @param query The search query. + * @param fullSearch Whether or not full search is required. + * @param order The way to order the results. + * @param sort The direction of the order. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + async invalidateEntriesBySearch( + glossaryId: number, + query: string, + fullSearch: boolean, + order: string, + sort: string, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const key = this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort); + + await site.invalidateWsCacheForKey(key); + } + + /** + * Get the glossary categories cache key. + * + * @param glossaryId Glossary Id. + * @return The cache key. + */ + protected getCategoriesCacheKey(glossaryId: number): string { + return ROOT_CACHE_KEY + 'categories:' + glossaryId; + } + + /** + * Get all the categories related to the glossary. + * + * @param glossaryId Glossary Id. + * @param options Other options. + * @return Promise resolved with the categories if supported or empty array if not. + */ + async getAllCategories(glossaryId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + return this.getCategories(glossaryId, [], site, options); + } + + /** + * Get the categories related to the glossary by sections. It's a recursive function see initial call values. + * + * @param glossaryId Glossary Id. + * @param categories Already fetched categories where to append the fetch. + * @param site Site object. + * @param options Other options. + * @return Promise resolved with the categories. + */ + protected async getCategories( + glossaryId: number, + categories: AddonModGlossaryCategory[], + site: CoreSite, + options: AddonModGlossaryGetCategoriesOptions = {}, + ): Promise { + options.from = options.from || 0; + options.limit = options.limit || AddonModGlossaryProvider.LIMIT_CATEGORIES; + + const params: AddonModGlossaryGetCategoriesWSParams = { + id: glossaryId, + from: options.from, + limit: options.limit, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getCategoriesCacheKey(glossaryId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read('mod_glossary_get_categories', params, preSets); + + categories = categories.concat(response.categories); + const canLoadMore = (options.from + options.limit) < response.count; + if (canLoadMore) { + options.from += options.limit; + + return this.getCategories(glossaryId, categories, site, options); + } + + return categories; + } + + /** + * Invalidate cache of categories by glossary id. + * + * @param glossaryId Glossary Id. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when categories data has been invalidated, + */ + async invalidateCategories(glossaryId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getCategoriesCacheKey(glossaryId)); + } + + /** + * Get an entry by ID cache key. + * + * @param entryId Entry Id. + * @return Cache key. + */ + protected getEntryCacheKey(entryId: number): string { + return ROOT_CACHE_KEY + 'getEntry:' + entryId; + } + + /** + * Get one entry by ID. + * + * @param entryId Entry ID. + * @param options Other options. + * @return Promise resolved with the entry. + */ + async getEntry(entryId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModGlossaryGetEntryByIdWSParams = { + id: entryId, + }; + const preSets = { + cacheKey: this.getEntryCacheKey(entryId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModGlossaryProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + try { + return await site.read('mod_glossary_get_entry_by_id', params, preSets); + } catch (error) { + // Entry not found. Search it in the list of entries. + try { + const data = await this.getStoredDataForEntry(entryId, site.getId()); + + if (typeof data.from != 'undefined') { + const response = await CoreUtils.ignoreErrors( + this.getEntryFromList(data.glossaryId, entryId, data.from, false, options), + ); + + if (response) { + return response; + } + } + + // Page not specified or entry not found in the page, search all pages. + return await this.getEntryFromList(data.glossaryId, entryId, 0, true, options); + } catch { + throw error; + } + } + } + + /** + * Get a glossary ID and the "from" of a given entry. + * + * @param entryId Entry ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the glossary ID and the "from". + */ + async getStoredDataForEntry(entryId: number, siteId?: string): Promise<{glossaryId: number; from: number}> { + const site = await CoreSites.getSite(siteId); + + const conditions: Partial = { + entryid: entryId, + }; + + const record = await site.getDb().getRecord(ENTRIES_TABLE_NAME, conditions); + + return { + glossaryId: record.glossaryid, + from: record.pagefrom, + }; + } + + /** + * Get an entry from the list of entries. + * + * @param glossaryId Glossary ID. + * @param entryId Entry ID. + * @param from Page to get. + * @param loadNext Whether to load next pages if not found. + * @param options Options. + * @return Promise resolved with the entry data. + */ + protected async getEntryFromList( + glossaryId: number, + entryId: number, + from: number, + loadNext: boolean, + options: CoreCourseCommonModWSOptions = {}, + ): Promise { + // Get the entries from this "page" and check if the entry we're looking for is in it. + const result = await this.getEntriesByLetter(glossaryId, 'ALL', { + from: from, + readingStrategy: CoreSitesReadingStrategy.OnlyCache, + cmId: options.cmId, + siteId: options.siteId, + }); + + const entry = result.entries.find(entry => entry.id == entryId); + + if (entry) { + // Entry found, return it. + return { entry, from }; + } + + const nextFrom = from + result.entries.length; + if (nextFrom < result.count && loadNext) { + // Get the next "page". + return this.getEntryFromList(glossaryId, entryId, nextFrom, true, options); + } + + // No more pages and the entry wasn't found. Reject. + throw new CoreError('Entry not found.'); + }; + + /** + * Performs the whole fetch of the entries using the proper function and arguments. + * + * @param fetchFunction Function to fetch. + * @param fetchArguments Arguments to call the fetching. + * @param options Other options. + * @return Promise resolved with all entrries. + */ + fetchAllEntries( + fetchFunction: (options?: AddonModGlossaryGetEntriesOptions) => AddonModGlossaryGetEntriesWSResponse, + options: CoreCourseCommonModWSOptions = {}, + ): Promise { + options.siteId = options.siteId || CoreSites.getCurrentSiteId(); + + const entries: AddonModGlossaryEntry[] = []; + + const fetchMoreEntries = async (): Promise => { + const result = await fetchFunction({ + from: entries.length, + ...options, // Include all options. + }); + + Array.prototype.push.apply(entries, result.entries); + + return entries.length < result.count ? fetchMoreEntries() : entries; + }; + + return fetchMoreEntries(); + } + + /** + * Invalidate cache of entry by ID. + * + * @param entryId Entry Id. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + async invalidateEntry(entryId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getEntryCacheKey(entryId)); + } + + /** + * Invalidate cache of all entries in the array. + * + * @param entries Entry objects to invalidate. + * @param siteId Site ID. If not defined, current site. + * @return Resolved when data is invalidated. + */ + protected async invalidateEntries(entries: AddonModGlossaryEntry[], siteId?: string): Promise { + const keys: string[] = []; + entries.forEach((entry) => { + keys.push(this.getEntryCacheKey(entry.id)); + }); + + const site = await CoreSites.getSite(siteId); + + await site.invalidateMultipleWsCacheForKey(keys); + } + + /** + * Invalidate the prefetched content except files. + * To invalidate files, use AddonModGlossary#invalidateFiles. + * + * @param moduleId The module ID. + * @param courseId Course ID. + * @return Promise resolved when data is invalidated. + */ + async invalidateContent(moduleId: number, courseId: number): Promise { + const glossary = await this.getGlossary(courseId, moduleId); + + await CoreUtils.ignoreErrors(this.invalidateGlossaryEntries(glossary)); + + await CoreUtils.allPromises([ + this.invalidateCourseGlossaries(courseId), + this.invalidateCategories(glossary.id), + ]); + } + + /** + * Invalidate the prefetched content for a given glossary, except files. + * To invalidate files, use AddonModGlossaryProvider#invalidateFiles. + * + * @param glossary The glossary object. + * @param onlyEntriesList If true, entries won't be invalidated. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when data is invalidated. + */ + async invalidateGlossaryEntries(glossary: AddonModGlossaryGlossary, onlyEntriesList?: boolean, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const promises: Promise[] = []; + + if (!onlyEntriesList) { + promises.push(this.fetchAllEntries(this.getEntriesByLetter.bind(this, glossary.id, 'ALL'), { + cmId: glossary.coursemodule, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId, + }).then((entries) => this.invalidateEntries(entries, siteId))); + } + + glossary.browsemodes.forEach((mode) => { + switch (mode) { + case 'letter': + promises.push(this.invalidateEntriesByLetter(glossary.id, 'ALL', siteId)); + break; + case 'cat': + promises.push(this.invalidateEntriesByCategory( + glossary.id, + AddonModGlossaryProvider.SHOW_ALL_CATEGORIES, + siteId, + )); + break; + case 'date': + promises.push(this.invalidateEntriesByDate(glossary.id, 'CREATION', 'DESC', siteId)); + promises.push(this.invalidateEntriesByDate(glossary.id, 'UPDATE', 'DESC', siteId)); + break; + case 'author': + promises.push(this.invalidateEntriesByAuthor(glossary.id, 'ALL', 'LASTNAME', 'ASC', siteId)); + break; + default: + } + }); + + await CoreUtils.allPromises(promises); + } + + /** + * Get one glossary by cmid. + * + * @param courseId Course Id. + * @param cmId Course Module Id. + * @param options Other options. + * @return Promise resolved with the glossary. + */ + async getGlossary(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + const glossaries = await this.getCourseGlossaries(courseId, options); + + const glossary = glossaries.find((glossary) => glossary.coursemodule == cmId); + + if (glossary) { + return glossary; + } + + throw new CoreError('Glossary not found.'); + } + + /** + * Get one glossary by glossary ID. + * + * @param courseId Course Id. + * @param glossaryId Glossary Id. + * @param options Other options. + * @return Promise resolved with the glossary. + */ + async getGlossaryById( + courseId: number, + glossaryId: number, + options: CoreSitesCommonWSOptions = {}, + ): Promise { + const glossaries = await this.getCourseGlossaries(courseId, options); + + const glossary = glossaries.find((glossary) => glossary.id == glossaryId); + + if (glossary) { + return glossary; + } + + throw new CoreError('Glossary not found.'); + } + + /** + * Create a new entry on a glossary + * + * @param glossaryId Glossary ID. + * @param concept Glossary entry concept. + * @param definition Glossary entry concept definition. + * @param courseId Course ID of the glossary. + * @param entryOptions Options for the entry. + * @param attachments Attachments ID if sending online, result of CoreFileUploaderProvider#storeFilesToUpload otherwise. + * @param otherOptions Other options. + * @return Promise resolved with entry ID if entry was created in server, false if stored in device. + */ + async addEntry( + glossaryId: number, + concept: string, + definition: string, + courseId: number, + entryOptions: Record, + attachments?: number | CoreFileUploaderStoreFilesResult, + otherOptions: AddonModGlossaryAddEntryOptions = {}, + ): Promise { + otherOptions.siteId = otherOptions.siteId || CoreSites.getCurrentSiteId(); + + // Convenience function to store a new entry to be synchronized later. + const storeOffline = async (): Promise => { + const discardTime = otherOptions.discardEntry?.timecreated; + + if (otherOptions.checkDuplicates) { + // Check if the entry is duplicated in online or offline mode. + const conceptUsed = await this.isConceptUsed(glossaryId, concept, { + cmId: otherOptions.cmId, + timeCreated: discardTime, + siteId: otherOptions.siteId, + }); + + if (conceptUsed) { + throw new CoreError(Translate.instant('addon.mod_glossary.errconceptalreadyexists')); + } + } + + if (typeof attachments == 'number') { + // When storing in offline the attachments can't be a draft ID. + throw new CoreError('Error adding entry.'); + } + + await AddonModGlossaryOffline.addNewEntry( + glossaryId, + concept, + definition, + courseId, + entryOptions, + attachments, + otherOptions.timeCreated, + otherOptions.siteId, + undefined, + otherOptions.discardEntry, + ); + + return false; + }; + + if (!CoreApp.isOnline() && otherOptions.allowOffline) { + // App is offline, store the action. + return storeOffline(); + } + + // If we are editing an offline entry, discard previous first. + if (otherOptions.discardEntry) { + await AddonModGlossaryOffline.deleteNewEntry( + glossaryId, + otherOptions.discardEntry.concept, + otherOptions.discardEntry.timecreated, + otherOptions.siteId, + ); + } + + try { + // Try to add it in online. + return this.addEntryOnline(glossaryId, concept, definition, entryOptions, attachments, otherOptions.siteId); + } catch (error) { + if (otherOptions.allowOffline && !CoreUtils.isWebServiceError(error)) { + // Couldn't connect to server, store in offline. + return storeOffline(); + } + + // The WebService has thrown an error or offline not supported, reject. + throw error; + } + } + + /** + * Create a new entry on a glossary. It does not cache calls. It will fail if offline or cannot connect. + * + * @param glossaryId Glossary ID. + * @param concept Glossary entry concept. + * @param definition Glossary entry concept definition. + * @param options Options for the entry. + * @param attachId Attachments ID (if any attachment). + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the entry ID if created, rejected otherwise. + */ + async addEntryOnline( + glossaryId: number, + concept: string, + definition: string, + options?: Record, + attachId?: number, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModGlossaryAddEntryWSParams = { + glossaryid: glossaryId, + concept: concept, + definition: definition, + definitionformat: 1, + options: CoreUtils.objectToArrayOfObjects(options || {}, 'name', 'value'), + }; + + if (attachId) { + params.options!.push({ + name: 'attachmentsid', + value: String(attachId), + }); + } + + // Workaround for bug MDL-57737. + if (!site.isVersionGreaterEqualThan('3.2.2')) { + params.definition = CoreTextUtils.cleanTags(params.definition); + } + + const response = await site.write('mod_glossary_add_entry', params); + + return response.entryid; + } + + /** + * Check if a entry concept is already used. + * + * @param glossaryId Glossary ID. + * @param concept Concept to check. + * @param options Other options. + * @return Promise resolved with true if used, resolved with false if not used or error. + */ + async isConceptUsed(glossaryId: number, concept: string, options: AddonModGlossaryIsConceptUsedOptions = {}): Promise { + try { + // Check offline first. + const exists = await AddonModGlossaryOffline.isConceptUsed(glossaryId, concept, options.timeCreated, options.siteId); + + if (exists) { + return true; + } + + // If we get here, there's no offline entry with this name, check online. + // Get entries from the cache. + const entries = await this.fetchAllEntries(this.getEntriesByLetter.bind(glossaryId, 'ALL'), { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }); + + // Check if there's any entry with the same concept. + return entries.some((entry) => entry.concept == concept); + } catch { + // Error, assume not used. + return false; + } + } + + /** + * Return whether or not the plugin is enabled for editing in the current site. Plugin is enabled if the glossary WS are + * available. + * + * @return Whether the glossary editing is available or not. + */ + isPluginEnabledForEditing(): boolean { + return !!CoreSites.getCurrentSite()?.wsAvailable('mod_glossary_add_entry'); + } + + /** + * Report a glossary as being viewed. + * + * @param glossaryId Glossary ID. + * @param mode The mode in which the glossary was viewed. + * @param name Name of the glossary. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + logView(glossaryId: number, mode: string, name?: string, siteId?: string): Promise { + const params: AddonModGlossaryViewGlossaryWSParams = { + id: glossaryId, + mode: mode, + }; + + return CoreCourseLogHelper.logSingle( + 'mod_glossary_view_glossary', + params, + AddonModGlossaryProvider.COMPONENT, + glossaryId, + name, + 'glossary', + { mode }, + siteId, + ); + } + + /** + * Report a glossary entry as being viewed. + * + * @param entryId Entry ID. + * @param glossaryId Glossary ID. + * @param name Name of the glossary. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + logEntryView(entryId: number, glossaryId: number, name?: string, siteId?: string): Promise { + const params: AddonModGlossaryViewEntryWSParams = { + id: entryId, + }; + + return CoreCourseLogHelper.logSingle( + 'mod_glossary_view_entry', + params, + AddonModGlossaryProvider.COMPONENT, + glossaryId, + name, + 'glossary', + { entryid: entryId }, + siteId, + ); + } + + /** + * Store several entries so we can determine their glossaryId in offline. + * + * @param glossaryId Glossary ID the entries belongs to. + * @param entries Entries. + * @param from The "page" the entries belong to. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + protected async storeEntries( + glossaryId: number, + entries: AddonModGlossaryEntry[], + from: number, + siteId?: string, + ): Promise { + await Promise.all(entries.map((entry) => this.storeEntryId(glossaryId, entry.id, from, siteId))); + } + + /** + * Store an entry so we can determine its glossaryId in offline. + * + * @param glossaryId Glossary ID the entry belongs to. + * @param entryId Entry ID. + * @param from The "page" the entry belongs to. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + protected async storeEntryId(glossaryId: number, entryId: number, from: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const entry: AddonModGlossaryEntryDBRecord = { + entryid: entryId, + glossaryid: glossaryId, + pagefrom: from, + }; + + await site.getDb().insertRecord(ENTRIES_TABLE_NAME, entry); + } + +} + +export const AddonModGlossary = makeSingleton(AddonModGlossaryProvider); + +declare module '@singletons/events' { + + /** + * Augment CoreEventsData interface with events specific to this service. + * + * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation + */ + export interface CoreEventsData { + [AddonModGlossaryProvider.ADD_ENTRY_EVENT]: AddonModGlossaryAddEntryEventData; + [AddonModGlossarySyncProvider.AUTO_SYNCED]: AddonModGlossaryAutoSyncData; + } + +} + +/** + * Data passed to ADD_ENTRY_EVENT. + */ +export type AddonModGlossaryAddEntryEventData = { + glossaryId: number; + entryId?: number; +}; + +/** + * Params of mod_glossary_get_glossaries_by_courses WS. + */ +export type AddonModGlossaryGetGlossariesByCoursesWSParams = { + courseids?: number[]; // Array of course IDs. +}; + +/** + * Data returned by mod_glossary_get_glossaries_by_courses WS. + */ +export type AddonModGlossaryGetGlossariesByCoursesWSResponse = { + glossaries: AddonModGlossaryGlossary[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Data returned by mod_glossary_get_glossaries_by_courses WS. + */ +export type AddonModGlossaryGlossary = { + id: number; // Glossary id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Glossary name. + intro: string; // The Glossary intro. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles?: CoreWSExternalFile[]; + allowduplicatedentries: number; // If enabled, multiple entries can have the same concept name. + displayformat: string; // Display format type. + mainglossary: number; // If enabled this glossary is a main glossary. + showspecial: number; // If enabled, participants can browse the glossary by special characters, such as @ and #. + showalphabet: number; // If enabled, participants can browse the glossary by letters of the alphabet. + showall: number; // If enabled, participants can browse all entries at once. + allowcomments: number; // If enabled, all participants with permission will be able to add comments to glossary entries. + allowprintview: number; // If enabled, students are provided with a link to a printer-friendly version of the glossary. + usedynalink: number; // If enabled, the entry will be automatically linked. + defaultapproval: number; // If set to no, entries require approving by a teacher before they are viewable by everyone. + approvaldisplayformat: string; // When approving glossary items you may wish to use a different display format. + globalglossary: number; + entbypage: number; // Entries shown per page. + editalways: number; // Always allow editing. + rsstype: number; // RSS type. + rssarticles: number; // This setting specifies the number of glossary entry concepts to include in the RSS feed. + assessed: number; // Aggregate type. + assesstimestart: number; // Restrict rating to items created after this. + assesstimefinish: number; // Restrict rating to items created before this. + scale: number; // Scale ID. + timecreated: number; // Time created. + timemodified: number; // Time modified. + completionentries: number; // Number of entries to complete. + section: number; // Section. + visible: number; // Visible. + groupmode: number; // Group mode. + groupingid: number; // Grouping ID. + browsemodes: string[]; + canaddentry?: number; // Whether the user can add a new entry. +}; + +/** + * Common data passed to the get entries WebServices. + */ +export type AddonModGlossaryCommonGetEntriesWSParams = { + id: number; // Glossary entry ID. + from?: number; // Start returning records from here. + limit?: number; // Number of records to return. + options?: { + // When false, includes the non-approved entries created by the user. + // When true, also includes the ones that the user has the permission to approve. + includenotapproved?: boolean; + }; // An array of options. +}; + +/** + * Data returned by the different get entries WebServices. + */ +export type AddonModGlossaryGetEntriesWSResponse = { + count: number; // The total number of records matching the request. + entries: AddonModGlossaryEntry[]; + ratinginfo?: CoreRatingInfo; // Rating information. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_glossary_get_entries_by_author WS. + */ +export type AddonModGlossaryGetEntriesByAuthorWSParams = AddonModGlossaryCommonGetEntriesWSParams & { + letter: string; // First letter of firstname or lastname, or either keywords: 'ALL' or 'SPECIAL'. + field?: string; // Search and order using: 'FIRSTNAME' or 'LASTNAME'. + sort?: string; // The direction of the order: 'ASC' or 'DESC'. +}; + +/** + * Params of mod_glossary_get_entries_by_category WS. + */ +export type AddonModGlossaryGetEntriesByCategoryWSParams = AddonModGlossaryCommonGetEntriesWSParams & { + categoryid: number; // The category ID. Use '0' for all categories, or '-1' for uncategorised entries. +}; + +/** + * Data returned by mod_glossary_get_entries_by_category WS. + */ +export type AddonModGlossaryGetEntriesByCategoryWSResponse = Omit & { + entries: AddonModGlossaryEntryWithCategory[]; +}; + +/** + * Params of mod_glossary_get_entries_by_date WS. + */ +export type AddonModGlossaryGetEntriesByDateWSParams = AddonModGlossaryCommonGetEntriesWSParams & { + order?: string; // Order the records by: 'CREATION' or 'UPDATE'. + sort?: string; // The direction of the order: 'ASC' or 'DESC'. +}; + +/** + * Params of mod_glossary_get_entries_by_letter WS. + */ +export type AddonModGlossaryGetEntriesByLetterWSParams = AddonModGlossaryCommonGetEntriesWSParams & { + letter: string; // A letter, or either keywords: 'ALL' or 'SPECIAL'. +}; + +/** + * Params of mod_glossary_get_entries_by_search WS. + */ +export type AddonModGlossaryGetEntriesBySearchWSParams = AddonModGlossaryCommonGetEntriesWSParams & { + query: string; // The query string. + fullsearch?: boolean; // The query. + order?: string; // Order by: 'CONCEPT', 'CREATION' or 'UPDATE'. + sort?: string; // The direction of the order: 'ASC' or 'DESC'. +}; + +/** + * Entry data returned by several WS. + */ +export type AddonModGlossaryEntry = { + id: number; // The entry ID. + glossaryid: number; // The glossary ID. + userid: number; // Author ID. + userfullname: string; // Author full name. + userpictureurl: string; // Author picture. + concept: string; // The concept. + definition: string; // The definition. + definitionformat: number; // Definition format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + definitiontrust: boolean; // The definition trust flag. + definitioninlinefiles?: CoreWSExternalFile[]; + attachment: boolean; // Whether or not the entry has attachments. + attachments?: CoreWSExternalFile[]; + timecreated: number; // Time created. + timemodified: number; // Time modified. + teacherentry: boolean; // The entry was created by a teacher, or equivalent. + sourceglossaryid: number; // The source glossary ID. + usedynalink: boolean; // Whether the concept should be automatically linked. + casesensitive: boolean; // When true, the matching is case sensitive. + fullmatch: boolean; // When true, the matching is done on full words only. + approved: boolean; // Whether the entry was approved. + tags?: CoreTagItem[]; +}; + +/** + * Entry data returned by several WS. + */ +export type AddonModGlossaryEntryWithCategory = AddonModGlossaryEntry & { + categoryid?: number; // The category ID. This may be '-1' when the entry is not categorised. + categoryname?: string; // The category name. May be empty when the entry is not categorised. +}; + +/** + * Params of mod_glossary_get_categories WS. + */ +export type AddonModGlossaryGetCategoriesWSParams = { + id: number; // The glossary ID. + from?: number; // Start returning records from here. + limit?: number; // Number of records to return. +}; + +/** + * Data returned by mod_glossary_get_categories WS. + */ +export type AddonModGlossaryGetCategoriesWSResponse = { + count: number; // The total number of records. + categories: AddonModGlossaryCategory[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Data returned by mod_glossary_get_categories WS. + */ +export type AddonModGlossaryCategory = { + id: number; // The category ID. + glossaryid: number; // The glossary ID. + name: string; // The name of the category. + usedynalink: boolean; // Whether the category is automatically linked. +}; + +/** + * Params of mod_glossary_get_entry_by_id WS. + */ +export type AddonModGlossaryGetEntryByIdWSParams = { + id: number; // Glossary entry ID. +}; + +/** + * Data returned by mod_glossary_get_entry_by_id WS. + */ +export type AddonModGlossaryGetEntryByIdWSResponse = { + entry: AddonModGlossaryEntry; + ratinginfo?: CoreRatingInfo; // Rating information. + permissions?: { + candelete: boolean; // Whether the user can delete the entry. + canupdate: boolean; // Whether the user can update the entry. + }; // User permissions for the managing the entry. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Data returned by mod_glossary_get_entry_by_id WS, with some calculated data if needed. + */ +export type AddonModGlossaryGetEntryByIdResponse = AddonModGlossaryGetEntryByIdWSResponse & { + from?: number; +}; + +/** + * Params of mod_glossary_add_entry WS. + */ +export type AddonModGlossaryAddEntryWSParams = { + glossaryid: number; // Glossary id. + concept: string; // Glossary concept. + definition: string; // Glossary concept definition. + definitionformat: number; // Definition format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + options?: { // Optional settings. + name: string; // The allowed keys (value format) are: + // inlineattachmentsid (int); the draft file area id for inline attachments + // attachmentsid (int); the draft file area id for attachments + // categories (comma separated int); comma separated category ids + // aliases (comma separated str); comma separated aliases + // usedynalink (bool); whether the entry should be automatically linked. + // casesensitive (bool); whether the entry is case sensitive. + // fullmatch (bool); whether to match whole words only. + value: string | number; // The value of the option (validated inside the function). + }[]; +}; + +/** + * Data returned by mod_glossary_add_entry WS. + */ +export type AddonModGlossaryAddEntryWSResponse = { + entryid: number; // New glossary entry ID. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_glossary_view_glossary WS. + */ +export type AddonModGlossaryViewGlossaryWSParams = { + id: number; // Glossary instance ID. + mode: string; // The mode in which the glossary is viewed. +}; + +/** + * Params of mod_glossary_view_entry WS. + */ +export type AddonModGlossaryViewEntryWSParams = { + id: number; // Glossary entry ID. +}; + +/** + * Options to pass to add entry. + */ +export type AddonModGlossaryAddEntryOptions = { + timeCreated?: number; // The time the entry was created. If not defined, current time. + discardEntry?: AddonModGlossaryDiscardedEntry; // The entry provided will be discarded if found. + allowOffline?: boolean; // True if it can be stored in offline, false otherwise. + checkDuplicates?: boolean; // Check for duplicates before storing offline. Only used if allowOffline is true. + cmId?: number; // Module ID. + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Entry to discard. + */ +export type AddonModGlossaryDiscardedEntry = { + concept: string; + timecreated: number; +}; + +/** + * Entry to be added. + */ +export type AddonModGlossaryNewEntry = { + concept: string; + definition: string; + timecreated: number; +}; + +/** + * Entry to be added, including attachments. + */ +export type AddonModGlossaryNewEntryWithFiles = AddonModGlossaryNewEntry & { + files: (CoreWSExternalFile | FileEntry)[]; +}; + +/** + * Options to pass to the different get entries functions. + */ +export type AddonModGlossaryGetEntriesOptions = CoreCourseCommonModWSOptions & { + from?: number; // Start returning records from here. Defaults to 0. + limit?: number; // Number of records to return. Defaults to AddonModGlossaryProvider.LIMIT_ENTRIES. +}; + +/** + * Options to pass to get categories. + */ +export type AddonModGlossaryGetCategoriesOptions = CoreCourseCommonModWSOptions & { + from?: number; // Start returning records from here. Defaults to 0. + limit?: number; // Number of records to return. Defaults to AddonModGlossaryProvider.LIMIT_CATEGORIES. +}; + +/** + * Options to pass to is concept used. + */ +export type AddonModGlossaryIsConceptUsedOptions = { + cmId?: number; // Module ID. + timeCreated?: number; // Timecreated to check that is not the timecreated we are editing. + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Possible values for entry options. + */ +export type AddonModGlossaryEntryOption = string | number; diff --git a/src/addons/mod/glossary/services/handlers/edit-link.ts b/src/addons/mod/glossary/services/handlers/edit-link.ts new file mode 100644 index 000000000..089bfaf36 --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/edit-link.ts @@ -0,0 +1,78 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { makeSingleton } from '@singletons'; +import { AddonModGlossaryModuleHandlerService } from './module'; + +/** + * Content links handler for glossary new entry. + * Match mod/glossary/edit.php?cmid=6 with a valid data. + * Currently it only supports new entry. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryEditLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonModGlossaryEditLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModGlossary'; + pattern = /\/mod\/glossary\/edit\.php.*([?&](cmid)=\d+)/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record): CoreContentLinksAction[] { + return [{ + action: async (siteId: string) => { + const modal = await CoreDomUtils.showModalLoading(); + + const cmId = Number(params.cmid); + + try { + const module = await CoreCourse.getModuleBasicInfo(cmId, siteId); + + await CoreNavigator.navigateToSitePath( + AddonModGlossaryModuleHandlerService.PAGE_NAME + '/edit/0', + { + params: { + cmId: module.id, + courseId: module.course, + }, + siteId, + }, + ); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingglossary', true); + } finally { + // Just in case. In fact we need to dismiss the modal before showing a toast or error message. + modal.dismiss(); + } + }, + }]; + } + + /** + * @inheritdoc + */ + async isEnabled(siteId: string, url: string, params: Record): Promise { + return typeof params.cmid != 'undefined'; + } + +} + +export const AddonModGlossaryEditLinkHandler = makeSingleton(AddonModGlossaryEditLinkHandlerService); diff --git a/src/addons/mod/glossary/services/handlers/entry-link.ts b/src/addons/mod/glossary/services/handlers/entry-link.ts new file mode 100644 index 000000000..7ae25bb04 --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/entry-link.ts @@ -0,0 +1,75 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { makeSingleton } from '@singletons'; +import { AddonModGlossary } from '../glossary'; +import { AddonModGlossaryModuleHandlerService } from './module'; + +/** + * Handler to treat links to glossary entries. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryEntryLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonModGlossaryEntryLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModGlossary'; + pattern = /\/mod\/glossary\/(showentry|view)\.php.*([&?](eid|g|mode|hook)=\d+)/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record): CoreContentLinksAction[] { + return [{ + action: async (siteId: string) => { + const modal = await CoreDomUtils.showModalLoading(); + + try { + const entryId = params.mode == 'entry' ? Number(params.hook) : Number(params.eid); + + const response = await AddonModGlossary.getEntry(entryId, { siteId }); + + const module = await CoreCourse.getModuleBasicInfoByInstance( + response.entry.glossaryid, + 'glossary', + siteId, + ); + + await CoreNavigator.navigateToSitePath( + AddonModGlossaryModuleHandlerService.PAGE_NAME + `/entry/${entryId}`, + { + params: { + cmId: module.id, + courseId: module.course, + }, + siteId, + }, + ); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); + } finally { + modal.dismiss(); + } + }, + }]; + } + +} + +export const AddonModGlossaryEntryLinkHandler = makeSingleton(AddonModGlossaryEntryLinkHandlerService); diff --git a/src/addons/mod/glossary/services/handlers/index-link.ts b/src/addons/mod/glossary/services/handlers/index-link.ts new file mode 100644 index 000000000..3658be016 --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/index-link.ts @@ -0,0 +1,33 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to glossary index. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler { + + name = 'AddonModGlossaryIndexLinkHandler'; + + constructor() { + super('AddonModGlossary', 'glossary', 'g'); + } + +} + +export const AddonModGlossaryIndexLinkHandler = makeSingleton(AddonModGlossaryIndexLinkHandlerService); diff --git a/src/addons/mod/glossary/services/handlers/list-link.ts b/src/addons/mod/glossary/services/handlers/list-link.ts new file mode 100644 index 000000000..1386e0589 --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/list-link.ts @@ -0,0 +1,33 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to glossary list page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryListLinkHandlerService extends CoreContentLinksModuleListHandler { + + name = 'AddonModGlossaryListLinkHandler'; + + constructor() { + super('AddonModGlossary', 'glossary'); + } + +} + +export const AddonModGlossaryListLinkHandler = makeSingleton(AddonModGlossaryListLinkHandlerService); diff --git a/src/addons/mod/glossary/services/handlers/module.ts b/src/addons/mod/glossary/services/handlers/module.ts new file mode 100644 index 000000000..f49bfef4e --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/module.ts @@ -0,0 +1,92 @@ +// (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 { CoreConstants } from '@/core/constants'; +import { Injectable, Type } from '@angular/core'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; +import { makeSingleton } from '@singletons'; +import { AddonModGlossaryIndexComponent } from '../../components/index/index'; + +/** + * Handler to support glossary modules. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryModuleHandlerService implements CoreCourseModuleHandler { + + static readonly PAGE_NAME = 'mod_glossary'; + + name = 'AddonModGlossary'; + modName = 'glossary'; + + supportedFeatures = { + [CoreConstants.FEATURE_GROUPS]: false, + [CoreConstants.FEATURE_GROUPINGS]: false, + [CoreConstants.FEATURE_MOD_INTRO]: true, + [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, + [CoreConstants.FEATURE_COMPLETION_HAS_RULES]: true, + [CoreConstants.FEATURE_GRADE_HAS_GRADE]: true, + [CoreConstants.FEATURE_GRADE_OUTCOMES]: true, + [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, + [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, + [CoreConstants.FEATURE_RATE]: true, + [CoreConstants.FEATURE_PLAGIARISM]: true, + }; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData { + return { + icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined), + title: module.name, + class: 'addon-mod_glossary-handler', + showDownloadButton: true, + action: (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => { + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { module }); + const routeParams = '/' + courseId + '/' + module.id; + + CoreNavigator.navigateToSitePath(AddonModGlossaryModuleHandlerService.PAGE_NAME + routeParams, options); + }, + }; + } + + /** + * @inheritdoc + */ + async getMainComponent(): Promise> { + return AddonModGlossaryIndexComponent; + } + + /** + * @inheritdoc + */ + displayRefresherInSingleActivity(): boolean { + return false; + } + +} + +export const AddonModGlossaryModuleHandler = makeSingleton(AddonModGlossaryModuleHandlerService); diff --git a/src/addons/mod/glossary/services/handlers/prefetch.ts b/src/addons/mod/glossary/services/handlers/prefetch.ts new file mode 100644 index 000000000..cc92048a0 --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/prefetch.ts @@ -0,0 +1,235 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreComments } from '@features/comments/services/comments'; +import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreUser } from '@features/user/services/user'; +import { CoreFilepool } from '@services/filepool'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreWSExternalFile } from '@services/ws'; +import { makeSingleton } from '@singletons'; +import { AddonModGlossary, AddonModGlossaryEntry, AddonModGlossaryGlossary, AddonModGlossaryProvider } from '../glossary'; +import { AddonModGlossarySync, AddonModGlossarySyncResult } from '../glossary-sync'; + +/** + * Handler to prefetch forums. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase { + + name = 'AddonModGlossary'; + modName = 'glossary'; + component = AddonModGlossaryProvider.COMPONENT; + updatesNames = /^configuration$|^.*files$|^entries$/; + + /** + * @inheritdoc + */ + async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + try { + const glossary = await AddonModGlossary.getGlossary(courseId, module.id); + + const entries = await AddonModGlossary.fetchAllEntries( + AddonModGlossary.getEntriesByLetter.bind(AddonModGlossary.instance, glossary.id, 'ALL'), + { + cmId: module.id, + }, + ); + + return this.getFilesFromGlossaryAndEntries(module, glossary, entries); + } catch { + // Glossary not found, return empty list. + return []; + } + } + + /** + * Get the list of downloadable files. It includes entry embedded files. + * + * @param module Module to get the files. + * @param glossary Glossary + * @param entries Entries of the Glossary. + * @return List of Files. + */ + protected getFilesFromGlossaryAndEntries( + module: CoreCourseAnyModuleData, + glossary: AddonModGlossaryGlossary, + entries: AddonModGlossaryEntry[], + ): CoreWSExternalFile[] { + let files = this.getIntroFilesFromInstance(module, glossary); + + const getInlineFiles = CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.2'); + + // Get entries files. + entries.forEach((entry) => { + files = files.concat(entry.attachments || []); + + if (getInlineFiles && entry.definitioninlinefiles && entry.definitioninlinefiles.length) { + files = files.concat(entry.definitioninlinefiles); + } else if (entry.definition && !getInlineFiles) { + files = files.concat(CoreFilepool.extractDownloadableFilesFromHtmlAsFakeFileObjects(entry.definition)); + } + }); + + return files; + } + + /** + * @inheritdoc + */ + invalidateContent(moduleId: number, courseId: number): Promise { + return AddonModGlossary.invalidateContent(moduleId, courseId); + } + + /** + * @inheritdoc + */ + prefetch(module: CoreCourseAnyModuleData, courseId?: number): Promise { + return this.prefetchPackage(module, courseId, this.prefetchGlossary.bind(this, module, courseId)); + } + + /** + * Prefetch a glossary. + * + * @param module The module object returned by WS. + * @param courseId Course ID the module belongs to. + * @param siteId Site ID. + * @return Promise resolved when done. + */ + protected async prefetchGlossary(module: CoreCourseAnyModuleData, courseId: number, siteId: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const options = { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + + // Prefetch the glossary data. + const glossary = await AddonModGlossary.getGlossary(courseId, module.id, { siteId }); + + const promises: Promise[] = []; + + glossary.browsemodes.forEach((mode) => { + switch (mode) { + case 'letter': // Always done. Look bellow. + break; + case 'cat': + promises.push(AddonModGlossary.fetchAllEntries( + AddonModGlossary.getEntriesByCategory.bind( + AddonModGlossary.instance, + glossary.id, + AddonModGlossaryProvider.SHOW_ALL_CATEGORIES, + ), + options, + )); + break; + case 'date': + promises.push(AddonModGlossary.fetchAllEntries( + AddonModGlossary.getEntriesByDate.bind( + AddonModGlossary.instance, + glossary.id, + 'CREATION', + 'DESC', + ), + options, + )); + promises.push(AddonModGlossary.fetchAllEntries( + AddonModGlossary.getEntriesByDate.bind( + AddonModGlossary.instance, + glossary.id, + 'UPDATE', + 'DESC', + ), + options, + )); + break; + case 'author': + promises.push(AddonModGlossary.fetchAllEntries( + AddonModGlossary.getEntriesByAuthor.bind( + AddonModGlossary.instance, + glossary.id, + 'ALL', + 'LASTNAME', + 'ASC', + ), + options, + )); + break; + default: + } + }); + + // Fetch all entries to get information from. + promises.push(AddonModGlossary.fetchAllEntries( + AddonModGlossary.getEntriesByLetter.bind(AddonModGlossary.instance, glossary.id, 'ALL'), + options, + ).then((entries) => { + const promises: Promise[] = []; + const commentsEnabled = !CoreComments.areCommentsDisabledInSite(); + + entries.forEach((entry) => { + // Don't fetch individual entries, it's too many WS calls. + if (glossary.allowcomments && commentsEnabled) { + promises.push(CoreComments.getComments( + 'module', + glossary.coursemodule, + 'mod_glossary', + entry.id, + 'glossary_entry', + 0, + siteId, + )); + } + }); + + const files = this.getFilesFromGlossaryAndEntries(module, glossary, entries); + promises.push(CoreFilepool.addFilesToQueue(siteId, files, this.component, module.id)); + + // Prefetch user avatars. + promises.push(CoreUser.prefetchUserAvatars(entries, 'userpictureurl', siteId)); + + return Promise.all(promises); + })); + + // Get all categories. + promises.push(AddonModGlossary.getAllCategories(glossary.id, options)); + + // Prefetch data for link handlers. + promises.push(CoreCourse.getModuleBasicInfo(module.id, siteId)); + promises.push(CoreCourse.getModuleBasicInfoByInstance(glossary.id, 'glossary', siteId)); + + await Promise.all(promises); + } + + /** + * @inheritdoc + */ + async sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise { + const results = await Promise.all([ + AddonModGlossarySync.syncGlossaryEntries(module.instance!, undefined, siteId), + AddonModGlossarySync.syncRatings(module.id, undefined, siteId), + ]); + + return { + updated: results[0].updated || results[1].updated, + warnings: results[0].warnings.concat(results[1].warnings), + }; + } + +} + +export const AddonModGlossaryPrefetchHandler = makeSingleton(AddonModGlossaryPrefetchHandlerService); diff --git a/src/addons/mod/glossary/services/handlers/sync-cron.ts b/src/addons/mod/glossary/services/handlers/sync-cron.ts new file mode 100644 index 000000000..236d67c71 --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/sync-cron.ts @@ -0,0 +1,44 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreCronHandler } from '@services/cron'; +import { makeSingleton } from '@singletons'; +import { AddonModGlossarySync } from '../glossary-sync'; + +/** + * Synchronization cron handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossarySyncCronHandlerService implements CoreCronHandler { + + name = 'AddonModGlossarySyncCronHandler'; + + /** + * @inheritdoc + */ + execute(siteId?: string, force?: boolean): Promise { + return AddonModGlossarySync.syncAllGlossaries(siteId, force); + } + + /** + * @inheritdoc + */ + getInterval(): number { + return AddonModGlossarySync.syncInterval; + } + +} + +export const AddonModGlossarySyncCronHandler = makeSingleton(AddonModGlossarySyncCronHandlerService); diff --git a/src/addons/mod/glossary/services/handlers/tag-area.ts b/src/addons/mod/glossary/services/handlers/tag-area.ts new file mode 100644 index 000000000..7d844e276 --- /dev/null +++ b/src/addons/mod/glossary/services/handlers/tag-area.ts @@ -0,0 +1,53 @@ +// (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 { Injectable, Type } from '@angular/core'; +import { CoreTagFeedComponent } from '@features/tag/components/feed/feed'; +import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate'; +import { CoreTagFeedElement, CoreTagHelper } from '@features/tag/services/tag-helper'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to support tags. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModGlossaryTagAreaHandlerService implements CoreTagAreaHandler { + + name = 'AddonModGlossaryTagAreaHandler'; + type = 'mod_glossary/glossary_entries'; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + parseContent(content: string): CoreTagFeedElement[] { + return CoreTagHelper.parseFeedContent(content); + } + + /** + * @inheritdoc + */ + getComponent(): Type { + return CoreTagFeedComponent; + } + +} + +export const AddonModGlossaryTagAreaHandler = makeSingleton(AddonModGlossaryTagAreaHandlerService); diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts index 5aa0ad1bb..4ce3025a6 100644 --- a/src/addons/mod/mod.module.ts +++ b/src/addons/mod/mod.module.ts @@ -32,6 +32,7 @@ import { AddonModSurveyModule } from './survey/survey.module'; import { AddonModScormModule } from './scorm/scorm.module'; import { AddonModChoiceModule } from './choice/choice.module'; import { AddonModWikiModule } from './wiki/wiki.module'; +import { AddonModGlossaryModule } from './glossary/glossary.module'; @NgModule({ imports: [ @@ -53,6 +54,7 @@ import { AddonModWikiModule } from './wiki/wiki.module'; AddonModScormModule, AddonModChoiceModule, AddonModWikiModule, + AddonModGlossaryModule, ], }) export class AddonModModule { } diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index cad7df44a..80e733aca 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -130,7 +130,7 @@ import { ADDON_MOD_DATA_SERVICES } from '@addons/mod/data/data.module'; // @todo import { ADDON_MOD_FEEDBACK_SERVICES } from '@addons/mod/feedback/feedback.module'; import { ADDON_MOD_FOLDER_SERVICES } from '@addons/mod/folder/folder.module'; import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.module'; -// @todo import { ADDON_MOD_GLOSSARY_SERVICES } from '@addons/mod/glossary/glossary.module'; +import { ADDON_MOD_GLOSSARY_SERVICES } from '@addons/mod/glossary/glossary.module'; import { ADDON_MOD_H5P_ACTIVITY_SERVICES } from '@addons/mod/h5pactivity/h5pactivity.module'; import { ADDON_MOD_IMSCP_SERVICES } from '@addons/mod/imscp/imscp.module'; import { ADDON_MOD_LESSON_SERVICES } from '@addons/mod/lesson/lesson.module'; @@ -296,7 +296,7 @@ export class CoreCompileProvider { // @todo ...ADDON_MOD_FEEDBACK_SERVICES, ...ADDON_MOD_FOLDER_SERVICES, ...ADDON_MOD_FORUM_SERVICES, - // @todo ...ADDON_MOD_GLOSSARY_SERVICES, + ...ADDON_MOD_GLOSSARY_SERVICES, ...ADDON_MOD_H5P_ACTIVITY_SERVICES, ...ADDON_MOD_IMSCP_SERVICES, ...ADDON_MOD_LESSON_SERVICES, From 184a7b561b194837dc93f661712de1aa287ffd35 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 9 Apr 2021 12:34:51 +0200 Subject: [PATCH 2/6] MOBILE-3644 glossary: Migrate index page --- .../glossary/components/components.module.ts | 43 ++ .../index/addon-mod-glossary-index.html | 107 +++ .../mod/glossary/components/index/index.ts | 646 ++++++++++++++++++ .../addon-mod-glossary-mode-picker.html | 6 + .../components/mode-picker/mode-picker.ts | 64 ++ .../mod/glossary/glossary-lazy.module.ts | 39 ++ src/addons/mod/glossary/glossary.module.ts | 14 +- .../mod/glossary/pages/index/index.html | 18 + src/addons/mod/glossary/pages/index/index.ts | 30 + 9 files changed, 966 insertions(+), 1 deletion(-) create mode 100644 src/addons/mod/glossary/components/components.module.ts create mode 100644 src/addons/mod/glossary/components/index/addon-mod-glossary-index.html create mode 100644 src/addons/mod/glossary/components/index/index.ts create mode 100644 src/addons/mod/glossary/components/mode-picker/addon-mod-glossary-mode-picker.html create mode 100644 src/addons/mod/glossary/components/mode-picker/mode-picker.ts create mode 100644 src/addons/mod/glossary/glossary-lazy.module.ts create mode 100644 src/addons/mod/glossary/pages/index/index.html create mode 100644 src/addons/mod/glossary/pages/index/index.ts diff --git a/src/addons/mod/glossary/components/components.module.ts b/src/addons/mod/glossary/components/components.module.ts new file mode 100644 index 000000000..6937ea579 --- /dev/null +++ b/src/addons/mod/glossary/components/components.module.ts @@ -0,0 +1,43 @@ +// (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 { NgModule } from '@angular/core'; +import { AddonModGlossaryIndexComponent } from './index/index'; +import { AddonModGlossaryModePickerPopoverComponent } from './mode-picker/mode-picker'; +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreCourseComponentsModule } from '@features/course/components/components.module'; +import { CoreSearchComponentsModule } from '@features/search/components/components.module'; + +@NgModule({ + declarations: [ + AddonModGlossaryIndexComponent, + AddonModGlossaryModePickerPopoverComponent, + ], + imports: [ + CoreSharedModule, + CoreCourseComponentsModule, + CoreSearchComponentsModule, + ], + providers: [ + ], + exports: [ + AddonModGlossaryIndexComponent, + AddonModGlossaryModePickerPopoverComponent, + ], + entryComponents: [ + AddonModGlossaryIndexComponent, + AddonModGlossaryModePickerPopoverComponent, + ], +}) +export class AddonModGlossaryComponentsModule {} diff --git a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html new file mode 100644 index 000000000..43416ef4a --- /dev/null +++ b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ 'core.hasdatatosync' | translate:{$a: moduleName} }} + + + + + + {{ 'addon.mod_glossary.entriestobesynced' | translate }} + + + + + + + + + + + + + {{ getDivider!(entry) }} + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/addons/mod/glossary/components/index/index.ts b/src/addons/mod/glossary/components/index/index.ts new file mode 100644 index 000000000..3cf621c08 --- /dev/null +++ b/src/addons/mod/glossary/components/index/index.ts @@ -0,0 +1,646 @@ +// (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 { ContextLevel } from '@/core/constants'; +import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; +import { CorePageItemsListManager } from '@classes/page-items-list-manager'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component'; +import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreRatingProvider } from '@features/rating/services/rating'; +import { CoreRatingOffline } from '@features/rating/services/rating-offline'; +import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync'; +import { IonContent } from '@ionic/angular'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreTextUtils } from '@services/utils/text'; +import { PopoverController, Translate } from '@singletons'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { + AddonModGlossary, + AddonModGlossaryEntry, + AddonModGlossaryEntryWithCategory, + AddonModGlossaryGetEntriesOptions, + AddonModGlossaryGetEntriesWSResponse, + AddonModGlossaryGlossary, + AddonModGlossaryProvider, +} from '../../services/glossary'; +import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '../../services/glossary-offline'; +import { + AddonModGlossaryAutoSyncData, + AddonModGlossarySyncProvider, + AddonModGlossarySyncResult, +} from '../../services/glossary-sync'; +import { AddonModGlossaryPrefetchHandler } from '../../services/handlers/prefetch'; +import { AddonModGlossaryModePickerPopoverComponent } from '../mode-picker/mode-picker'; + +/** + * Component that displays a glossary entry page. + */ +@Component({ + selector: 'addon-mod-glossary-index', + templateUrl: 'addon-mod-glossary-index.html', +}) +export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivityComponent + implements OnInit, AfterViewInit, OnDestroy { + + @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; + + component = AddonModGlossaryProvider.COMPONENT; + moduleName = 'glossary'; + + isSearch = false; + canAdd = false; + loadMoreError = false; + loadingMessage?: string; + entries: AddonModGlossaryEntriesManager; + hasOfflineRatings = false; + glossary?: AddonModGlossaryGlossary; + + protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED; + protected fetchFunction?: (options?: AddonModGlossaryGetEntriesOptions) => AddonModGlossaryGetEntriesWSResponse; + protected fetchInvalidate?: () => Promise; + protected addEntryObserver?: CoreEventObserver; + protected fetchMode?: AddonModGlossaryFetchMode; + protected viewMode?: string; + protected fetchedEntriesCanLoadMore = false; + protected fetchedEntries: AddonModGlossaryEntry[] = []; + protected ratingOfflineObserver?: CoreEventObserver; + protected ratingSyncObserver?: CoreEventObserver; + + getDivider?: (entry: AddonModGlossaryEntry) => string; + showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false; + + constructor( + route: ActivatedRoute, + protected content?: IonContent, + @Optional() courseContentsPage?: CoreCourseContentsPage, + ) { + super('AddonModGlossaryIndexComponent', content, courseContentsPage); + + this.entries = new AddonModGlossaryEntriesManager( + route.component, + ); + } + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + super.ngOnInit(); + + this.loadingMessage = Translate.instant('core.loading'); + + // When an entry is added, we reload the data. + this.addEntryObserver = CoreEvents.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, (data) => { + if (this.glossary && this.glossary.id === data.glossaryId) { + this.showLoadingAndRefresh(false); + + // Check completion since it could be configured to complete once the user adds a new entry. + CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); + } + }); + + // Listen for offline ratings saved and synced. + this.ratingOfflineObserver = CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => { + if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module' + && data.instanceId == this.glossary.coursemodule) { + this.hasOfflineRatings = true; + } + }); + this.ratingSyncObserver = CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => { + if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module' + && data.instanceId == this.glossary.coursemodule) { + this.hasOfflineRatings = false; + } + }); + } + + /** + * @inheritdoc + */ + async ngAfterViewInit(): Promise { + await this.loadContent(false, true); + + if (!this.glossary) { + return; + } + + this.entries.start(this.splitView); + + try { + await AddonModGlossary.logView(this.glossary.id, this.viewMode!, this.glossary.name); + + CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); + } catch (error) { + // Ignore errors. + } + } + + /** + * @inheritdoc + */ + protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + try { + this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.module.id); + + this.description = this.glossary.intro || this.description; + this.canAdd = (AddonModGlossary.isPluginEnabledForEditing() && !!this.glossary.canaddentry) || false; + + this.dataRetrieved.emit(this.glossary); + + if (!this.fetchMode) { + this.switchMode('letter_all'); + } + + if (sync) { + // Try to synchronize the glossary. + await this.syncActivity(showErrors); + } + + const [hasOfflineRatings] = await Promise.all([ + CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule), + this.fetchEntries(), + ]); + + this.hasOfflineRatings = hasOfflineRatings; + } finally { + this.fillContextMenu(refresh); + } + } + + /** + * Convenience function to fetch entries. + * + * @param append True if fetched entries are appended to exsiting ones. + * @return Promise resolved when done. + */ + protected async fetchEntries(append: boolean = false): Promise { + if (!this.fetchFunction) { + return; + } + + this.loadMoreError = false; + const from = append ? this.entries.onlineEntries.length : 0; + + const result = await this.fetchFunction({ + from: from, + cmId: this.module.id, + }); + + const hasMoreEntries = from + result.entries.length < result.count; + + if (append) { + this.entries.setItems(this.entries.items.concat(result.entries), hasMoreEntries); + } else { + this.entries.setOnlineEntries(result.entries, hasMoreEntries); + } + + // Now get the ofline entries. + // Check if there are responses stored in offline. + const offlineEntries = await AddonModGlossaryOffline.getGlossaryNewEntries(this.glossary!.id); + + offlineEntries.sort((a, b) => a.concept.localeCompare(b.concept)); + this.hasOffline = !!offlineEntries.length; + this.entries.setOfflineEntries(offlineEntries); + } + + /** + * @inheritdoc + */ + protected async invalidateContent(): Promise { + const promises: Promise[] = []; + + if (this.fetchInvalidate) { + promises.push(this.fetchInvalidate()); + } + + promises.push(AddonModGlossary.invalidateCourseGlossaries(this.courseId)); + + if (this.glossary) { + promises.push(AddonModGlossary.invalidateCategories(this.glossary.id)); + } + + await Promise.all(promises); + } + + /** + * Performs the sync of the activity. + * + * @return Promise resolved when done. + */ + protected sync(): Promise { + return AddonModGlossaryPrefetchHandler.sync(this.module, this.courseId); + } + + /** + * Checks if sync has succeed from result sync data. + * + * @param result Data returned on the sync function. + * @return Whether it succeed or not. + */ + protected hasSyncSucceed(result: AddonModGlossarySyncResult): boolean { + return result.updated; + } + + /** + * Compares sync event data with current data to check if refresh content is needed. + * + * @param syncEventData Data receiven on sync observer. + * @return True if refresh is needed, false otherwise. + */ + protected isRefreshSyncNeeded(syncEventData: AddonModGlossaryAutoSyncData): boolean { + return !!this.glossary && syncEventData.glossaryId == this.glossary.id && + syncEventData.userId == CoreSites.getCurrentSiteUserId(); + } + + /** + * Change fetch mode. + * + * @param mode New mode. + */ + protected switchMode(mode: AddonModGlossaryFetchMode): void { + this.fetchMode = mode; + this.isSearch = false; + + switch (mode) { + case 'author_all': + // Browse by author. + this.viewMode = 'author'; + this.fetchFunction = AddonModGlossary.getEntriesByAuthor.bind( + AddonModGlossary.instance, + this.glossary!.id, + 'ALL', + 'LASTNAME', + 'ASC', + ); + this.fetchInvalidate = AddonModGlossary.invalidateEntriesByAuthor.bind( + AddonModGlossary.instance, + this.glossary!.id, + 'ALL', + 'LASTNAME', + 'ASC', + ); + this.getDivider = (entry) => entry.userfullname; + this.showDivider = (entry, previous) => !previous || entry.userid != previous.userid; + break; + + case 'cat_all': + // Browse by category. + this.viewMode = 'cat'; + this.fetchFunction = AddonModGlossary.getEntriesByCategory.bind( + AddonModGlossary.instance, + this.glossary!.id, + AddonModGlossaryProvider.SHOW_ALL_CATEGORIES, + ); + this.fetchInvalidate = AddonModGlossary.invalidateEntriesByCategory.bind( + AddonModGlossary.instance, + this.glossary!.id, + AddonModGlossaryProvider.SHOW_ALL_CATEGORIES, + ); + this.getDivider = (entry: AddonModGlossaryEntryWithCategory) => entry.categoryname || ''; + this.showDivider = (entry, previous) => !previous || this.getDivider!(entry) != this.getDivider!(previous); + break; + + case 'newest_first': + // Newest first. + this.viewMode = 'date'; + this.fetchFunction = AddonModGlossary.getEntriesByDate.bind( + AddonModGlossary.instance, + this.glossary!.id, + 'CREATION', + 'DESC', + ); + this.fetchInvalidate = AddonModGlossary.invalidateEntriesByDate.bind( + AddonModGlossary.instance, + this.glossary!.id, + 'CREATION', + 'DESC', + ); + this.getDivider = undefined; + this.showDivider = () => false; + break; + + case 'recently_updated': + // Recently updated. + this.viewMode = 'date'; + this.fetchFunction = AddonModGlossary.getEntriesByDate.bind( + AddonModGlossary.instance, + this.glossary!.id, + 'UPDATE', + 'DESC', + ); + this.fetchInvalidate = AddonModGlossary.invalidateEntriesByDate.bind( + AddonModGlossary.instance, + this.glossary!.id, + 'UPDATE', + 'DESC', + ); + this.getDivider = undefined; + this.showDivider = () => false; + break; + + case 'letter_all': + default: + // Consider it is 'letter_all'. + this.viewMode = 'letter'; + this.fetchMode = 'letter_all'; + this.fetchFunction = AddonModGlossary.getEntriesByLetter.bind( + AddonModGlossary.instance, + this.glossary!.id, + 'ALL', + ); + this.fetchInvalidate = AddonModGlossary.invalidateEntriesByLetter.bind( + AddonModGlossary.instance, + this.glossary!.id, + 'ALL', + ); + this.getDivider = (entry) => { + // Try to get the first letter without HTML tags. + const noTags = CoreTextUtils.cleanTags(entry.concept); + + return (noTags || entry.concept).substr(0, 1).toUpperCase(); + }; + this.showDivider = (entry, previous) => !previous || this.getDivider!(entry) != this.getDivider!(previous); + break; + } + } + + /** + * Convenience function to load more entries. + * + * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading. + * @return Promise resolved when done. + */ + async loadMoreEntries(infiniteComplete?: () => void): Promise { + try { + await this.fetchEntries(true); + } catch (error) { + this.loadMoreError = true; + CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true); + } finally { + infiniteComplete && infiniteComplete(); + } + } + + /** + * Show the mode picker menu. + * + * @param event Event. + */ + async openModePicker(event: MouseEvent): Promise { + const popover = await PopoverController.create({ + component: AddonModGlossaryModePickerPopoverComponent, + componentProps: { + browseModes: this.glossary!.browsemodes, + selectedMode: this.isSearch ? '' : this.fetchMode, + }, + event, + }); + + popover.present(); + + const result = await popover.onDidDismiss(); + + const mode = result.data; + if (mode) { + if (mode !== this.fetchMode) { + this.changeFetchMode(mode); + } else if (this.isSearch) { + this.toggleSearch(); + } + } + } + + /** + * Toggles between search and fetch mode. + */ + toggleSearch(): void { + if (this.isSearch) { + this.isSearch = false; + this.entries.setOnlineEntries(this.fetchedEntries, this.fetchedEntriesCanLoadMore); + this.switchMode(this.fetchMode!); + } else { + // Search for entries. The fetch function will be set when searching. + this.getDivider = undefined; + this.showDivider = () => false; + this.isSearch = true; + + this.fetchedEntries = this.entries.onlineEntries; + this.fetchedEntriesCanLoadMore = !this.entries.completed; + this.entries.setItems([], false); + } + } + + /** + * Change fetch mode. + * + * @param mode Mode. + */ + changeFetchMode(mode: AddonModGlossaryFetchMode): void { + this.isSearch = false; + this.loadingMessage = Translate.instant('core.loading'); + this.content?.scrollToTop(); + this.switchMode(mode); + this.loaded = false; + this.loadContent(); + } + + /** + * Opens new entry editor. + */ + openNewEntry(): void { + this.entries.select({ newEntry: true }); + // @todo + // const params = { + // courseId: this.courseId, + // module: this.module, + // glossary: this.glossary, + // entry: entry, + // }; + // this.splitviewCtrl.getMasterNav().push('AddonModGlossaryEditPage', params); + // this.selectedEntry = 0; + } + + /** + * Search entries. + * + * @param query Text entered on the search box. + */ + search(query: string): void { + this.loadingMessage = Translate.instant('core.searching'); + this.fetchFunction = AddonModGlossary.getEntriesBySearch.bind( + AddonModGlossary.instance, + this.glossary!.id, + query, + true, + 'CONCEPT', + 'ASC', + ); + this.fetchInvalidate = AddonModGlossary.invalidateEntriesBySearch.bind( + AddonModGlossary.instance, + this.glossary!.id, + query, + true, + 'CONCEPT', + 'ASC', + ); + this.loaded = false; + this.loadContent(); + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + + this.addEntryObserver?.off(); + this.ratingOfflineObserver?.off(); + this.ratingSyncObserver?.off(); + } + +} + +/** + * Type to select the new entry form. + */ +type NewEntryForm = { newEntry: true }; + +/** + * Type of items that can be held by the entries manager. + */ + type EntryItem = AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | NewEntryForm; + +/** + * Entries manager. + */ +class AddonModGlossaryEntriesManager extends CorePageItemsListManager { + + onlineEntries: AddonModGlossaryEntry[] = []; + offlineEntries: AddonModGlossaryOfflineEntry[] = []; + + constructor(pageComponent: unknown) { + super(pageComponent); + } + + /** + * @inheritdoc + */ + getItemQueryParams(entry: EntryItem): Params { + // @todo + return { + // courseId: this.component.courseId, + // cmId: this.component.module.id, + // forumId: this.component.forum!.id, + // ...(this.isOnlineDiscussion(discussion) ? { discussion, trackPosts: this.component.trackPosts } : {}), + }; + } + + /** + * Type guard to infer NewEntryForm objects. + * + * @param entry Item to check. + * @return Whether the item is a new entry form. + */ + isNewEntryForm(entry: EntryItem): entry is NewEntryForm { + return 'newEntry' in entry; + } + + /** + * Type guard to infer entry objects. + * + * @param entry Item to check. + * @return Whether the item is an offline entry. + */ + isOfflineEntry(entry: EntryItem): entry is AddonModGlossaryOfflineEntry { + return !this.isNewEntryForm(entry) && !this.isOnlineEntry(entry); + } + + /** + * Type guard to infer entry objects. + * + * @param entry Item to check. + * @return Whether the item is an offline entry. + */ + isOnlineEntry(entry: EntryItem): entry is AddonModGlossaryEntry { + return 'id' in entry; + } + + /** + * Update online entries items. + * + * @param onlineEntries Online entries. + */ + setOnlineEntries(onlineEntries: AddonModGlossaryEntry[], hasMoreItems: boolean = false): void { + this.setItems(( this.offlineEntries).concat(onlineEntries), hasMoreItems); + this.onlineEntries.concat(onlineEntries); + } + + /** + * Update offline entries items. + * + * @param offlineEntries Offline entries. + */ + setOfflineEntries(offlineEntries: AddonModGlossaryOfflineEntry[]): void { + this.setItems(( offlineEntries).concat(this.onlineEntries), this.hasMoreItems); + this.offlineEntries = offlineEntries; + } + + /** + * @inheritdoc + */ + setItems(entries: EntryItem[], hasMoreItems: boolean = false): void { + super.setItems(entries, hasMoreItems); + + this.onlineEntries = []; + this.offlineEntries = []; + this.items.forEach(entry => { + if (this.isOfflineEntry(entry)) { + this.offlineEntries.push(entry); + } else if (this.isOnlineEntry(entry)) { + this.onlineEntries.push(entry); + } + }); + } + + /** + * @inheritdoc + */ + resetItems(): void { + super.resetItems(); + this.onlineEntries = []; + this.offlineEntries = []; + } + + /** + * @inheritdoc + */ + protected getItemPath(entry: EntryItem): string { + if (this.isOnlineEntry(entry)) { + return `entry/${entry.id}`; + } + + if (this.isOfflineEntry(entry)) { + return `edit/${entry.timecreated}`; + } + + return 'edit/0'; + } + +} + +export type AddonModGlossaryFetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'letter_all'; diff --git a/src/addons/mod/glossary/components/mode-picker/addon-mod-glossary-mode-picker.html b/src/addons/mod/glossary/components/mode-picker/addon-mod-glossary-mode-picker.html new file mode 100644 index 000000000..747140252 --- /dev/null +++ b/src/addons/mod/glossary/components/mode-picker/addon-mod-glossary-mode-picker.html @@ -0,0 +1,6 @@ + + + {{ mode.langkey | translate }} + + + diff --git a/src/addons/mod/glossary/components/mode-picker/mode-picker.ts b/src/addons/mod/glossary/components/mode-picker/mode-picker.ts new file mode 100644 index 000000000..e3e08071b --- /dev/null +++ b/src/addons/mod/glossary/components/mode-picker/mode-picker.ts @@ -0,0 +1,64 @@ +// (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, Input, OnInit } from '@angular/core'; +import { PopoverController } from '@singletons'; +import { AddonModGlossaryFetchMode } from '../index'; + +/** + * Component to display the mode picker. + */ +@Component({ + selector: 'addon-mod-glossary-mode-picker-popover', + templateUrl: 'addon-mod-glossary-mode-picker.html', +}) +export class AddonModGlossaryModePickerPopoverComponent implements OnInit { + + @Input() browseModes: string[] = []; + @Input() selectedMode = ''; + + modes: { key: AddonModGlossaryFetchMode; langkey: string }[] = []; + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.browseModes.forEach((mode) => { + switch (mode) { + case 'letter' : + this.modes.push({ key: 'letter_all', langkey: 'addon.mod_glossary.byalphabet' }); + break; + case 'cat' : + this.modes.push({ key: 'cat_all', langkey: 'addon.mod_glossary.bycategory' }); + break; + case 'date' : + this.modes.push({ key: 'newest_first', langkey: 'addon.mod_glossary.bynewestfirst' }); + this.modes.push({ key: 'recently_updated', langkey: 'addon.mod_glossary.byrecentlyupdated' }); + break; + case 'author' : + this.modes.push({ key: 'author_all', langkey: 'addon.mod_glossary.byauthor' }); + break; + default: + } + }); + } + + /** + * Function called when a mode is clicked. + */ + modePicked(): void { + PopoverController.dismiss(this.selectedMode); + } + +} diff --git a/src/addons/mod/glossary/glossary-lazy.module.ts b/src/addons/mod/glossary/glossary-lazy.module.ts new file mode 100644 index 000000000..03fe01535 --- /dev/null +++ b/src/addons/mod/glossary/glossary-lazy.module.ts @@ -0,0 +1,39 @@ +// (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 { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonModGlossaryComponentsModule } from './components/components.module'; +import { AddonModGlossaryIndexPage } from './pages/index/index'; + +const routes: Routes = [ + { + path: ':courseId/:cmId', + component: AddonModGlossaryIndexPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + AddonModGlossaryComponentsModule, + ], + declarations: [ + AddonModGlossaryIndexPage, + ], +}) +export class AddonModGlossaryLazyModule {} diff --git a/src/addons/mod/glossary/glossary.module.ts b/src/addons/mod/glossary/glossary.module.ts index 21b6a7afb..5a114381d 100644 --- a/src/addons/mod/glossary/glossary.module.ts +++ b/src/addons/mod/glossary/glossary.module.ts @@ -13,12 +13,15 @@ // limitations under the License. import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; +import { Routes } from '@angular/router'; import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate'; import { CoreCronDelegate } from '@services/cron'; import { CORE_SITE_SCHEMAS } from '@services/sites'; +import { AddonModGlossaryComponentsModule } from './components/components.module'; import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/glossary'; import { AddonModGlossaryProvider } from './services/glossary'; import { AddonModGlossaryHelperProvider } from './services/glossary-helper'; @@ -28,7 +31,7 @@ import { AddonModGlossaryEditLinkHandler } from './services/handlers/edit-link'; import { AddonModGlossaryEntryLinkHandler } from './services/handlers/entry-link'; import { AddonModGlossaryIndexLinkHandler } from './services/handlers/index-link'; import { AddonModGlossaryListLinkHandler } from './services/handlers/list-link'; -import { AddonModGlossaryModuleHandler } from './services/handlers/module'; +import { AddonModGlossaryModuleHandler, AddonModGlossaryModuleHandlerService } from './services/handlers/module'; import { AddonModGlossaryPrefetchHandler } from './services/handlers/prefetch'; import { AddonModGlossarySyncCronHandler } from './services/handlers/sync-cron'; import { AddonModGlossaryTagAreaHandler } from './services/handlers/tag-area'; @@ -40,8 +43,17 @@ export const ADDON_MOD_GLOSSARY_SERVICES: Type[] = [ AddonModGlossaryHelperProvider, ]; +const routes: Routes = [ + { + path: AddonModGlossaryModuleHandlerService.PAGE_NAME, + loadChildren: () => import('./glossary-lazy.module').then(m => m.AddonModGlossaryLazyModule), + }, +]; + @NgModule({ imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + AddonModGlossaryComponentsModule, ], providers: [ { diff --git a/src/addons/mod/glossary/pages/index/index.html b/src/addons/mod/glossary/pages/index/index.html new file mode 100644 index 000000000..190905078 --- /dev/null +++ b/src/addons/mod/glossary/pages/index/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/addons/mod/glossary/pages/index/index.ts b/src/addons/mod/glossary/pages/index/index.ts new file mode 100644 index 000000000..23b135fa3 --- /dev/null +++ b/src/addons/mod/glossary/pages/index/index.ts @@ -0,0 +1,30 @@ +// (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, ViewChild } from '@angular/core'; +import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page'; +import { AddonModGlossaryIndexComponent } from '../../components/index'; + +/** + * Page that displays a glossary. + */ +@Component({ + selector: 'page-addon-mod-glossary-index', + templateUrl: 'index.html', +}) +export class AddonModGlossaryIndexPage extends CoreCourseModuleMainActivityPage { + + @ViewChild(AddonModGlossaryIndexComponent) activityComponent?: AddonModGlossaryIndexComponent; + +} From 8f991cecd08ff440ebc262ee5dab4e6df05c6518 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 12 Apr 2021 09:17:50 +0200 Subject: [PATCH 3/6] MOBILE-3644 glossary: Migrate edit and entry page --- .../mod/glossary/components/index/index.ts | 42 +- .../mod/glossary/glossary-lazy.module.ts | 34 +- src/addons/mod/glossary/glossary.module.ts | 12 +- src/addons/mod/glossary/pages/edit/edit.html | 77 ++++ .../mod/glossary/pages/edit/edit.module.ts | 38 ++ src/addons/mod/glossary/pages/edit/edit.ts | 370 ++++++++++++++++++ .../mod/glossary/pages/entry/entry.html | 85 ++++ .../mod/glossary/pages/entry/entry.module.ts | 40 ++ src/addons/mod/glossary/pages/entry/entry.ts | 146 +++++++ src/core/directives/auto-rows.ts | 11 +- 10 files changed, 827 insertions(+), 28 deletions(-) create mode 100644 src/addons/mod/glossary/pages/edit/edit.html create mode 100644 src/addons/mod/glossary/pages/edit/edit.module.ts create mode 100644 src/addons/mod/glossary/pages/edit/edit.ts create mode 100644 src/addons/mod/glossary/pages/entry/entry.html create mode 100644 src/addons/mod/glossary/pages/entry/entry.module.ts create mode 100644 src/addons/mod/glossary/pages/entry/entry.ts diff --git a/src/addons/mod/glossary/components/index/index.ts b/src/addons/mod/glossary/components/index/index.ts index 3cf621c08..4b15485ff 100644 --- a/src/addons/mod/glossary/components/index/index.ts +++ b/src/addons/mod/glossary/components/index/index.ts @@ -464,15 +464,6 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity */ openNewEntry(): void { this.entries.select({ newEntry: true }); - // @todo - // const params = { - // courseId: this.courseId, - // module: this.module, - // glossary: this.glossary, - // entry: entry, - // }; - // this.splitviewCtrl.getMasterNav().push('AddonModGlossaryEditPage', params); - // this.selectedEntry = 0; } /** @@ -537,19 +528,6 @@ class AddonModGlossaryEntriesManager extends CorePageItemsListManager super(pageComponent); } - /** - * @inheritdoc - */ - getItemQueryParams(entry: EntryItem): Params { - // @todo - return { - // courseId: this.component.courseId, - // cmId: this.component.module.id, - // forumId: this.component.forum!.id, - // ...(this.isOnlineDiscussion(discussion) ? { discussion, trackPosts: this.component.trackPosts } : {}), - }; - } - /** * Type guard to infer NewEntryForm objects. * @@ -641,6 +619,26 @@ class AddonModGlossaryEntriesManager extends CorePageItemsListManager return 'edit/0'; } + /** + * @inheritdoc + */ + getItemQueryParams(entry: EntryItem): Params { + if (this.isOfflineEntry(entry)) { + return { + concept: entry.concept, + }; + } + + return {}; + } + + /** + * @inheritdoc + */ + protected getDefaultItem(): EntryItem | null { + return this.onlineEntries[0] || null; + } + } export type AddonModGlossaryFetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'letter_all'; diff --git a/src/addons/mod/glossary/glossary-lazy.module.ts b/src/addons/mod/glossary/glossary-lazy.module.ts index 03fe01535..c05d7af3f 100644 --- a/src/addons/mod/glossary/glossary-lazy.module.ts +++ b/src/addons/mod/glossary/glossary-lazy.module.ts @@ -18,12 +18,44 @@ import { RouterModule, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; import { AddonModGlossaryComponentsModule } from './components/components.module'; import { AddonModGlossaryIndexPage } from './pages/index/index'; +import { conditionalRoutes } from '@/app/app-routing.module'; +import { CoreScreen } from '@services/screen'; -const routes: Routes = [ +const mobileRoutes: Routes = [ { path: ':courseId/:cmId', component: AddonModGlossaryIndexPage, }, + { + path: ':courseId/:cmId/entry/:entryId', + loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule), + }, + { + path: ':courseId/:cmId/edit/:timecreated', + loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule), + }, +]; + +const tabletRoutes: Routes = [ + { + path: ':courseId/:cmId', + component: AddonModGlossaryIndexPage, + children: [ + { + path: 'entry/:entryId', + loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule), + }, + { + path: 'edit/:timecreated', + loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule), + }, + ], + }, +]; + +const routes: Routes = [ + ...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile), + ...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet), ]; @NgModule({ diff --git a/src/addons/mod/glossary/glossary.module.ts b/src/addons/mod/glossary/glossary.module.ts index 5a114381d..69dfda5de 100644 --- a/src/addons/mod/glossary/glossary.module.ts +++ b/src/addons/mod/glossary/glossary.module.ts @@ -43,7 +43,15 @@ export const ADDON_MOD_GLOSSARY_SERVICES: Type[] = [ AddonModGlossaryHelperProvider, ]; -const routes: Routes = [ +const mainMenuRoutes: Routes = [ + { + path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`, + loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule), + }, + { + path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`, + loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule), + }, { path: AddonModGlossaryModuleHandlerService.PAGE_NAME, loadChildren: () => import('./glossary-lazy.module').then(m => m.AddonModGlossaryLazyModule), @@ -52,7 +60,7 @@ const routes: Routes = [ @NgModule({ imports: [ - CoreMainMenuTabRoutingModule.forChild(routes), + CoreMainMenuTabRoutingModule.forChild(mainMenuRoutes), AddonModGlossaryComponentsModule, ], providers: [ diff --git a/src/addons/mod/glossary/pages/edit/edit.html b/src/addons/mod/glossary/pages/edit/edit.html new file mode 100644 index 000000000..3a8d076c1 --- /dev/null +++ b/src/addons/mod/glossary/pages/edit/edit.html @@ -0,0 +1,77 @@ + + + + + + + + + + + + + +
+ + {{ 'addon.mod_glossary.concept' | translate }} + + + + + {{ 'addon.mod_glossary.definition' | translate }} + + + + + + {{ 'addon.mod_glossary.categories' | translate }} + + + + {{ category.name }} + + + + + + {{ 'addon.mod_glossary.aliases' | translate }} + + + + + + {{ 'addon.mod_glossary.attachment' | translate }} + + + + + + {{ 'addon.mod_glossary.linking' | translate }} + + + {{ 'addon.mod_glossary.entryusedynalink' | translate }} + + + + {{ 'addon.mod_glossary.casesensitive' | translate }} + + + + + {{ 'addon.mod_glossary.fullmatch' | translate }} + + + + + {{ 'core.save' | translate }} + +
+
+
diff --git a/src/addons/mod/glossary/pages/edit/edit.module.ts b/src/addons/mod/glossary/pages/edit/edit.module.ts new file mode 100644 index 000000000..1ff7808da --- /dev/null +++ b/src/addons/mod/glossary/pages/edit/edit.module.ts @@ -0,0 +1,38 @@ +// (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 { NgModule } from '@angular/core'; +import { AddonModGlossaryEditPage } from './edit'; +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreEditorComponentsModule } from '@features/editor/components/components.module'; +import { RouterModule, Routes } from '@angular/router'; +import { CanLeaveGuard } from '@guards/can-leave'; + +const routes: Routes = [{ + path: '', + component: AddonModGlossaryEditPage, + canDeactivate: [CanLeaveGuard], +}]; + +@NgModule({ + declarations: [ + AddonModGlossaryEditPage, + ], + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + CoreEditorComponentsModule, + ], +}) +export class AddonModGlossaryEditPageModule {} diff --git a/src/addons/mod/glossary/pages/edit/edit.ts b/src/addons/mod/glossary/pages/edit/edit.ts new file mode 100644 index 000000000..854c74247 --- /dev/null +++ b/src/addons/mod/glossary/pages/edit/edit.ts @@ -0,0 +1,370 @@ +// (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 { CoreError } from '@classes/errors/error'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; +import { CanLeave } from '@guards/can-leave'; +import { FileEntry } from '@ionic-native/file/ngx'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreTextUtils } from '@services/utils/text'; +import { Translate } from '@singletons'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreForms } from '@singletons/form'; +import { + AddonModGlossary, + AddonModGlossaryCategory, + AddonModGlossaryEntryOption, + AddonModGlossaryGlossary, + AddonModGlossaryNewEntry, + AddonModGlossaryNewEntryWithFiles, + 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; + attachments: FileEntry[] = []; + definitionControl = new FormControl(); + categories: AddonModGlossaryCategory[] = []; + editorExtraParams: Record = {}; + entry: AddonModGlossaryNewEntry = { + concept: '', + definition: '', + timecreated: 0, + }; + + options = { + categories: [], + aliases: '', + usedynalink: false, + casesensitive: false, + fullmatch: false, + }; + + protected timecreated!: number; + protected concept?: string; + protected syncId?: string; + protected syncObserver?: CoreEventObserver; + protected isDestroyed = false; + protected originalData?: AddonModGlossaryNewEntryWithFiles; + protected saved = false; + + constructor(@Optional() protected splitView: CoreSplitViewComponent) {} + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.cmId = CoreNavigator.getRouteNumberParam('cmId')!; + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.timecreated = CoreNavigator.getRouteNumberParam('timecreated')!; + this.concept = CoreNavigator.getRouteParam('concept')!; + this.editorExtraParams.timecreated = this.timecreated; + + this.fetchData(); + } + + /** + * Fetch required data. + * + * @return Promise resolved when done. + */ + protected async fetchData(): Promise { + try { + this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.cmId); + + if (this.timecreated > 0) { + await this.loadOfflineData(); + } + + this.categories = await AddonModGlossary.getAllCategories(this.glossary.id, { + cmId: this.cmId, + }); + + this.loaded = true; + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingglossary', true); + + CoreNavigator.back(); + } + } + + /** + * Load offline data when editing an offline entry. + * + * @return Promise resolved when done. + */ + protected async loadOfflineData(): Promise { + const entry = await AddonModGlossaryOffline.getNewEntry(this.glossary!.id, this.concept || '', this.timecreated); + + this.entry.concept = entry.concept || ''; + this.entry.definition = entry.definition || ''; + this.entry.timecreated = entry.timecreated; + + this.originalData = { + concept: this.entry.concept, + definition: this.entry.definition, + files: [], + timecreated: entry.timecreated, + }; + + if (entry.options) { + this.options.categories = (entry.options.categories && ( entry.options.categories).split(',')) || []; + this.options.aliases = entry.options.aliases || ''; + this.options.usedynalink = !!entry.options.usedynalink; + if (this.options.usedynalink) { + this.options.casesensitive = !!entry.options.casesensitive; + this.options.fullmatch = !!entry.options.fullmatch; + } + } + + // Treat offline attachments if any. + if (entry.attachments?.offline) { + this.attachments = await AddonModGlossaryHelper.getStoredFiles(this.glossary!.id, entry.concept, entry.timecreated); + + this.originalData.files = this.attachments.slice(); + } + + this.definitionControl.setValue(this.entry.definition); + } + + /** + * Reset the form data. + */ + protected resetForm(): void { + this.entry.concept = ''; + this.entry.definition = ''; + this.entry.timecreated = 0; + this.originalData = undefined; + + this.options.categories = []; + this.options.aliases = ''; + this.options.usedynalink = false; + this.options.casesensitive = false; + this.options.fullmatch = false; + this.attachments.length = 0; // Empty the array. + + this.definitionControl.setValue(''); + } + + /** + * Definition changed. + * + * @param text The new text. + */ + onDefinitionChange(text: string): void { + this.entry.definition = text; + } + + /** + * Check if we can leave the page or not. + * + * @return Resolved if we can leave it, rejected if not. + */ + async canLeave(): Promise { + if (this.saved) { + return true; + } + + if (AddonModGlossaryHelper.hasEntryDataChanged(this.entry, this.attachments, 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.attachments); + + CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId()); + + return true; + } + + /** + * Save the entry. + */ + async save(): Promise { + let definition = this.entry.definition; + let entryId: number | undefined; + const timecreated = this.entry.timecreated || Date.now(); + + if (!this.entry.concept || !definition) { + CoreDomUtils.showErrorModal('addon.mod_glossary.fillfields', true); + + return; + } + + const modal = await CoreDomUtils.showModalLoading('core.sending', true); + definition = CoreTextUtils.formatHtmlLines(definition); + + try { + // Upload attachments first if any. + const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated); + + const options: Record = { + aliases: this.options.aliases, + categories: this.options.categories.join(','), + }; + + if (this.glossary!.usedynalink) { + options.usedynalink = this.options.usedynalink ? 1 : 0; + if (this.options.usedynalink) { + options.casesensitive = this.options.casesensitive ? 1 : 0; + options.fullmatch = this.options.fullmatch ? 1 : 0; + } + } + + if (saveOffline) { + if (this.entry && !this.glossary!.allowduplicatedentries) { + // Check if the entry is duplicated in online or offline mode. + const isUsed = await AddonModGlossary.isConceptUsed(this.glossary!.id, this.entry.concept, { + timeCreated: this.entry.timecreated, + cmId: this.cmId, + }); + + if (isUsed) { + // There's a entry with same name, reject with error message. + throw new CoreError(Translate.instant('addon.mod_glossary.errconceptalreadyexists')); + } + } + + // Save entry in offline. + await AddonModGlossaryOffline.addNewEntry( + this.glossary!.id, + this.entry.concept, + definition, + this.courseId, + options, + attachmentsResult, + timecreated, + undefined, + undefined, + this.entry, + ); + } else { + // Try to send it to server. + // Don't allow offline if there are attachments since they were uploaded fine. + await AddonModGlossary.addEntry( + this.glossary!.id, + this.entry.concept, + definition, + this.courseId, + options, + attachmentsResult, + { + timeCreated: timecreated, + discardEntry: this.entry, + allowOffline: !this.attachments.length, + checkDuplicates: !this.glossary!.allowduplicatedentries, + }, + ); + } + + // Delete the local files from the tmp folder. + CoreFileUploader.clearTmpFiles(this.attachments); + + if (entryId) { + // Data sent to server, delete stored files (if any). + AddonModGlossaryHelper.deleteStoredFiles(this.glossary!.id, this.entry.concept, timecreated); + CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' }); + } + + CoreEvents.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, { + glossaryId: this.glossary!.id, + entryId: entryId, + }, CoreSites.getCurrentSiteId()); + + CoreForms.triggerFormSubmittedEvent(this.formElement, !!entryId, CoreSites.getCurrentSiteId()); + + if (this.splitView?.outletActivated) { + if (this.timecreated > 0) { + // Reload the data. + await this.loadOfflineData(); + } else { + // Empty form. + this.resetForm(); + } + } else { + this.saved = true; + CoreNavigator.back(); + } + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.cannoteditentry', true); + } finally { + modal.dismiss(); + } + } + + /** + * Upload entry attachments if any. + * + * @param timecreated Entry's timecreated. + * @return Promise resolved when done. + */ + protected async uploadAttachments( + timecreated: number, + ): Promise<{saveOffline: boolean; attachmentsResult?: number | CoreFileUploaderStoreFilesResult}> { + if (!this.attachments.length) { + return { + saveOffline: false, + }; + } + + try { + const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles( + this.attachments, + AddonModGlossaryProvider.COMPONENT, + this.glossary!.id, + ); + + return { + saveOffline: false, + attachmentsResult, + }; + } catch { + // Cannot upload them in online, save them in offline. + const attachmentsResult = await AddonModGlossaryHelper.storeFiles( + this.glossary!.id, + this.entry.concept, + timecreated, + this.attachments, + ); + + return { + saveOffline: true, + attachmentsResult, + }; + } + } + +} diff --git a/src/addons/mod/glossary/pages/entry/entry.html b/src/addons/mod/glossary/pages/entry/entry.html new file mode 100644 index 000000000..9e3f1b07a --- /dev/null +++ b/src/addons/mod/glossary/pages/entry/entry.html @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + +

+ + +

+

{{ entry.userfullname }}

+
+ {{ entry.timemodified | coreDateDayOrTime }} +
+ + +

+ + +

+
+ {{ entry.timemodified | coreDateDayOrTime }} +
+ + + + + + +
+ + +
+ + +
{{ 'core.tag.tags' | translate }}:
+ +
+
+ +

{{ 'addon.mod_glossary.entrypendingapproval' | translate }}

+
+ + + + + + + + + + +
+ + + + {{ 'addon.mod_glossary.errorloadingentry' | translate }} + + +
+
diff --git a/src/addons/mod/glossary/pages/entry/entry.module.ts b/src/addons/mod/glossary/pages/entry/entry.module.ts new file mode 100644 index 000000000..24da954a9 --- /dev/null +++ b/src/addons/mod/glossary/pages/entry/entry.module.ts @@ -0,0 +1,40 @@ +// (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 { NgModule } from '@angular/core'; +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonModGlossaryEntryPage } from './entry'; +import { CoreCommentsComponentsModule } from '@features/comments/components/components.module'; +import { CoreRatingComponentsModule } from '@features/rating/components/components.module'; +import { CoreTagComponentsModule } from '@features/tag/components/components.module'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [{ + path: '', + component: AddonModGlossaryEntryPage, +}]; + +@NgModule({ + declarations: [ + AddonModGlossaryEntryPage, + ], + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + CoreCommentsComponentsModule, + CoreRatingComponentsModule, + CoreTagComponentsModule, + ], +}) +export class AddonModGlossaryEntryPageModule {} diff --git a/src/addons/mod/glossary/pages/entry/entry.ts b/src/addons/mod/glossary/pages/entry/entry.ts new file mode 100644 index 000000000..5db347e85 --- /dev/null +++ b/src/addons/mod/glossary/pages/entry/entry.ts @@ -0,0 +1,146 @@ +// (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 } from '@angular/core'; +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 { IonRefresher } from '@ionic/angular'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { + AddonModGlossary, + AddonModGlossaryEntry, + AddonModGlossaryGlossary, + AddonModGlossaryProvider, +} from '../../services/glossary'; + +/** + * Page that displays a glossary entry. + */ +@Component({ + selector: 'page-addon-mod-glossary-entry', + templateUrl: 'entry.html', +}) +export class AddonModGlossaryEntryPage implements OnInit { + + @ViewChild(CoreCommentsCommentsComponent) comments?: CoreCommentsCommentsComponent; + + component = AddonModGlossaryProvider.COMPONENT; + componentId?: number; + entry?: AddonModGlossaryEntry; + glossary?: AddonModGlossaryGlossary; + loaded = false; + showAuthor = false; + showDate = false; + ratingInfo?: CoreRatingInfo; + tagsEnabled = false; + commentsEnabled = false; + courseId!: number; + + protected entryId!: number; + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.entryId = CoreNavigator.getRouteNumberParam('entryId')!; + this.tagsEnabled = CoreTag.areTagsAvailableInSite(); + this.commentsEnabled = !CoreComments.areCommentsDisabledInSite(); + + try { + await this.fetchEntry(); + + if (!this.glossary) { + return; + } + + await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.entryId, this.componentId!, this.glossary.name)); + } finally { + this.loaded = true; + } + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + * @return Promise resolved when done. + */ + async doRefresh(refresher?: IonRefresher): Promise { + if (this.glossary?.allowcomments && this.entry && this.entry.id > 0 && this.commentsEnabled && this.comments) { + // Refresh comments. Don't add it to promises because we don't want the comments fetch to block the entry fetch. + CoreUtils.ignoreErrors(this.comments.doRefresh()); + } + + try { + await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(this.entryId)); + + await this.fetchEntry(); + } finally { + refresher?.complete(); + } + } + + /** + * Convenience function to get the glossary entry. + * + * @return Promise resolved when done. + */ + protected async fetchEntry(): Promise { + try { + const result = await AddonModGlossary.getEntry(this.entryId); + + this.entry = result.entry; + this.ratingInfo = result.ratinginfo; + + if (this.glossary) { + // Glossary already loaded, nothing else to load. + return; + } + + // Load the glossary. + this.glossary = await AddonModGlossary.getGlossaryById(this.courseId, this.entry.glossaryid); + 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; + } + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); + } + } + + /** + * Function called when rating is updated online. + */ + ratingUpdated(): void { + AddonModGlossary.invalidateEntry(this.entryId); + } + +} diff --git a/src/core/directives/auto-rows.ts b/src/core/directives/auto-rows.ts index 05d4f12bc..00c570e38 100644 --- a/src/core/directives/auto-rows.ts +++ b/src/core/directives/auto-rows.ts @@ -59,10 +59,15 @@ export class CoreAutoRowsDirective implements AfterViewInit { * Resize the textarea. */ protected resize(): void { - let nativeElement = this.element.nativeElement; + let nativeElement: HTMLElement = this.element.nativeElement; if (nativeElement.tagName == 'ION-TEXTAREA') { - // The first child of ion-textarea is the actual textarea element. - nativeElement = nativeElement.firstElementChild; + // Search the actual textarea. + const textarea = nativeElement.querySelector('textarea'); + if (!textarea) { + return; + } + + nativeElement = textarea; } // Set height to 1px to force scroll height to calculate correctly. From ae97ee3b236de197e46de9eefc0d986ed7a5b4fd Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 12 Apr 2021 14:46:43 +0200 Subject: [PATCH 4/6] MOBILE-3644 core: Fix on changes in dynamic-component --- src/core/components/dynamic-component/dynamic-component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/components/dynamic-component/dynamic-component.ts b/src/core/components/dynamic-component/dynamic-component.ts index 0057964f1..d48e1b4e1 100644 --- a/src/core/components/dynamic-component/dynamic-component.ts +++ b/src/core/components/dynamic-component/dynamic-component.ts @@ -119,8 +119,8 @@ export class CoreDynamicComponent implements OnChanges, DoCheck { const changes = this.differ.diff(this.data || {}); if (changes) { this.setInputData(); - if (this.ngOnChanges) { - this.ngOnChanges(CoreDomUtils.createChangesFromKeyValueDiff(changes)); + if (this.instance.ngOnChanges) { + this.instance.ngOnChanges(CoreDomUtils.createChangesFromKeyValueDiff(changes)); } } } From 8a3fb4a5eb3691b4abc4951a37831553f4a54eb9 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Apr 2021 11:16:36 +0200 Subject: [PATCH 5/6] MOBILE-3644 core: Remove all entryComponents --- .../block/activitymodules/components/components.module.ts | 3 --- .../block/activityresults/components/components.module.ts | 3 --- src/addons/block/badges/components/components.module.ts | 3 --- src/addons/block/blogmenu/components/components.module.ts | 3 --- src/addons/block/blogrecent/components/components.module.ts | 3 --- src/addons/block/blogtags/components/components.module.ts | 3 --- src/addons/block/myoverview/components/components.module.ts | 3 --- src/addons/block/newsitems/components/components.module.ts | 3 --- src/addons/block/onlineusers/components/components.module.ts | 3 --- .../block/recentactivity/components/components.module.ts | 3 --- .../recentlyaccessedcourses/components/components.module.ts | 3 --- .../recentlyaccesseditems/components/components.module.ts | 3 --- src/addons/block/rssclient/components/components.module.ts | 3 --- .../block/sitemainmenu/components/components.module.ts | 3 --- .../block/starredcourses/components/components.module.ts | 3 --- src/addons/block/tags/components/components.module.ts | 3 --- src/addons/block/timeline/components/components.module.ts | 4 ---- src/addons/calendar/components/components.module.ts | 3 --- src/addons/messages/components/components.module.ts | 3 --- src/addons/mod/assign/feedback/comments/comments.module.ts | 3 --- src/addons/mod/assign/feedback/editpdf/editpdf.module.ts | 3 --- src/addons/mod/assign/feedback/file/file.module.ts | 3 --- src/addons/mod/assign/submission/comments/comments.module.ts | 3 --- src/addons/mod/assign/submission/file/file.module.ts | 3 --- .../mod/assign/submission/onlinetext/onlinetext.module.ts | 3 --- src/addons/mod/glossary/components/components.module.ts | 4 ---- src/addons/userprofilefield/checkbox/checkbox.module.ts | 3 --- src/addons/userprofilefield/datetime/datetime.module.ts | 3 --- src/addons/userprofilefield/menu/menu.module.ts | 3 --- src/addons/userprofilefield/text/text.module.ts | 3 --- src/addons/userprofilefield/textarea/textarea.module.ts | 3 --- src/app/app.module.ts | 1 - src/core/components/dynamic-component/dynamic-component.ts | 2 -- src/core/features/block/components/components.module.ts | 5 ----- src/core/features/comments/components/components.module.ts | 3 --- src/core/features/courses/components/components.module.ts | 3 --- src/core/features/editor/components/components.module.ts | 3 --- src/core/features/rating/components/components.module.ts | 3 --- 38 files changed, 115 deletions(-) diff --git a/src/addons/block/activitymodules/components/components.module.ts b/src/addons/block/activitymodules/components/components.module.ts index a577d4b36..e01f255d0 100644 --- a/src/addons/block/activitymodules/components/components.module.ts +++ b/src/addons/block/activitymodules/components/components.module.ts @@ -28,8 +28,5 @@ import { AddonBlockActivityModulesComponent } from './activitymodules/activitymo exports: [ AddonBlockActivityModulesComponent, ], - entryComponents: [ - AddonBlockActivityModulesComponent, - ], }) export class AddonBlockActivityModulesComponentsModule {} diff --git a/src/addons/block/activityresults/components/components.module.ts b/src/addons/block/activityresults/components/components.module.ts index e1af3e1cf..a19585ca9 100644 --- a/src/addons/block/activityresults/components/components.module.ts +++ b/src/addons/block/activityresults/components/components.module.ts @@ -27,8 +27,5 @@ import { AddonBlockActivityResultsComponent } from './activityresults/activityre exports: [ AddonBlockActivityResultsComponent, ], - entryComponents: [ - AddonBlockActivityResultsComponent, - ], }) export class AddonBlockActivityResultsComponentsModule {} diff --git a/src/addons/block/badges/components/components.module.ts b/src/addons/block/badges/components/components.module.ts index 54da4005f..3e1b91d7a 100644 --- a/src/addons/block/badges/components/components.module.ts +++ b/src/addons/block/badges/components/components.module.ts @@ -27,8 +27,5 @@ import { AddonBlockBadgesComponent } from './badges/badges'; exports: [ AddonBlockBadgesComponent, ], - entryComponents: [ - AddonBlockBadgesComponent, - ], }) export class AddonBlockBadgesComponentsModule {} diff --git a/src/addons/block/blogmenu/components/components.module.ts b/src/addons/block/blogmenu/components/components.module.ts index 5c96416a2..7119d9c29 100644 --- a/src/addons/block/blogmenu/components/components.module.ts +++ b/src/addons/block/blogmenu/components/components.module.ts @@ -27,8 +27,5 @@ import { AddonBlockBlogMenuComponent } from './blogmenu/blogmenu'; exports: [ AddonBlockBlogMenuComponent, ], - entryComponents: [ - AddonBlockBlogMenuComponent, - ], }) export class AddonBlockBlogMenuComponentsModule {} diff --git a/src/addons/block/blogrecent/components/components.module.ts b/src/addons/block/blogrecent/components/components.module.ts index 41b7919f0..f38a8dfa5 100644 --- a/src/addons/block/blogrecent/components/components.module.ts +++ b/src/addons/block/blogrecent/components/components.module.ts @@ -27,8 +27,5 @@ import { AddonBlockBlogRecentComponent } from './blogrecent/blogrecent'; exports: [ AddonBlockBlogRecentComponent, ], - entryComponents: [ - AddonBlockBlogRecentComponent, - ], }) export class AddonBlockBlogRecentComponentsModule {} diff --git a/src/addons/block/blogtags/components/components.module.ts b/src/addons/block/blogtags/components/components.module.ts index a835e4508..4b8d24e88 100644 --- a/src/addons/block/blogtags/components/components.module.ts +++ b/src/addons/block/blogtags/components/components.module.ts @@ -27,8 +27,5 @@ import { AddonBlockBlogTagsComponent } from './blogtags/blogtags'; exports: [ AddonBlockBlogTagsComponent, ], - entryComponents: [ - AddonBlockBlogTagsComponent, - ], }) export class AddonBlockBlogTagsComponentsModule {} diff --git a/src/addons/block/myoverview/components/components.module.ts b/src/addons/block/myoverview/components/components.module.ts index 8c19b4938..c1829bd61 100644 --- a/src/addons/block/myoverview/components/components.module.ts +++ b/src/addons/block/myoverview/components/components.module.ts @@ -29,8 +29,5 @@ import { AddonBlockMyOverviewComponent } from './myoverview/myoverview'; exports: [ AddonBlockMyOverviewComponent, ], - entryComponents: [ - AddonBlockMyOverviewComponent, - ], }) export class AddonBlockMyOverviewComponentsModule {} diff --git a/src/addons/block/newsitems/components/components.module.ts b/src/addons/block/newsitems/components/components.module.ts index 144f40d34..d4a440bec 100644 --- a/src/addons/block/newsitems/components/components.module.ts +++ b/src/addons/block/newsitems/components/components.module.ts @@ -27,8 +27,5 @@ import { AddonBlockNewsItemsComponent } from './newsitems/newsitems'; exports: [ AddonBlockNewsItemsComponent, ], - entryComponents: [ - AddonBlockNewsItemsComponent, - ], }) export class AddonBlockNewsItemsComponentsModule {} diff --git a/src/addons/block/onlineusers/components/components.module.ts b/src/addons/block/onlineusers/components/components.module.ts index 834002cfb..b5444458e 100644 --- a/src/addons/block/onlineusers/components/components.module.ts +++ b/src/addons/block/onlineusers/components/components.module.ts @@ -27,8 +27,5 @@ import { AddonBlockOnlineUsersComponent } from './onlineusers/onlineusers'; exports: [ AddonBlockOnlineUsersComponent, ], - entryComponents: [ - AddonBlockOnlineUsersComponent, - ], }) export class AddonBlockOnlineUsersComponentsModule {} diff --git a/src/addons/block/recentactivity/components/components.module.ts b/src/addons/block/recentactivity/components/components.module.ts index 1ebc2eb98..fb854c0ae 100644 --- a/src/addons/block/recentactivity/components/components.module.ts +++ b/src/addons/block/recentactivity/components/components.module.ts @@ -27,8 +27,5 @@ import { AddonBlockRecentActivityComponent } from './recentactivity/recentactivi exports: [ AddonBlockRecentActivityComponent, ], - entryComponents: [ - AddonBlockRecentActivityComponent, - ], }) export class AddonBlockRecentActivityComponentsModule {} diff --git a/src/addons/block/recentlyaccessedcourses/components/components.module.ts b/src/addons/block/recentlyaccessedcourses/components/components.module.ts index cd7380a6a..875fdc377 100644 --- a/src/addons/block/recentlyaccessedcourses/components/components.module.ts +++ b/src/addons/block/recentlyaccessedcourses/components/components.module.ts @@ -30,8 +30,5 @@ import { AddonBlockRecentlyAccessedCoursesComponent } from './recentlyaccessedco exports: [ AddonBlockRecentlyAccessedCoursesComponent, ], - entryComponents: [ - AddonBlockRecentlyAccessedCoursesComponent, - ], }) export class AddonBlockRecentlyAccessedCoursesComponentsModule {} diff --git a/src/addons/block/recentlyaccesseditems/components/components.module.ts b/src/addons/block/recentlyaccesseditems/components/components.module.ts index 3ce06b4f6..486b86055 100644 --- a/src/addons/block/recentlyaccesseditems/components/components.module.ts +++ b/src/addons/block/recentlyaccesseditems/components/components.module.ts @@ -30,8 +30,5 @@ import { AddonBlockRecentlyAccessedItemsComponent } from './recentlyaccesseditem exports: [ AddonBlockRecentlyAccessedItemsComponent, ], - entryComponents: [ - AddonBlockRecentlyAccessedItemsComponent, - ], }) export class AddonBlockRecentlyAccessedItemsComponentsModule {} diff --git a/src/addons/block/rssclient/components/components.module.ts b/src/addons/block/rssclient/components/components.module.ts index f10d2ec45..f0781fccd 100644 --- a/src/addons/block/rssclient/components/components.module.ts +++ b/src/addons/block/rssclient/components/components.module.ts @@ -27,8 +27,5 @@ import { AddonBlockRssClientComponent } from './rssclient/rssclient'; exports: [ AddonBlockRssClientComponent, ], - entryComponents: [ - AddonBlockRssClientComponent, - ], }) export class AddonBlockRssClientComponentsModule {} diff --git a/src/addons/block/sitemainmenu/components/components.module.ts b/src/addons/block/sitemainmenu/components/components.module.ts index b3aa083be..f3b670ae4 100644 --- a/src/addons/block/sitemainmenu/components/components.module.ts +++ b/src/addons/block/sitemainmenu/components/components.module.ts @@ -30,8 +30,5 @@ import { AddonBlockSiteMainMenuComponent } from './sitemainmenu/sitemainmenu'; exports: [ AddonBlockSiteMainMenuComponent, ], - entryComponents: [ - AddonBlockSiteMainMenuComponent, - ], }) export class AddonBlockSiteMainMenuComponentsModule {} diff --git a/src/addons/block/starredcourses/components/components.module.ts b/src/addons/block/starredcourses/components/components.module.ts index 1ecd70b44..ea8ca89b3 100644 --- a/src/addons/block/starredcourses/components/components.module.ts +++ b/src/addons/block/starredcourses/components/components.module.ts @@ -30,8 +30,5 @@ import { AddonBlockStarredCoursesComponent } from './starredcourses/starredcours exports: [ AddonBlockStarredCoursesComponent, ], - entryComponents: [ - AddonBlockStarredCoursesComponent, - ], }) export class AddonBlockStarredCoursesComponentsModule {} diff --git a/src/addons/block/tags/components/components.module.ts b/src/addons/block/tags/components/components.module.ts index 3e605feae..966f87551 100644 --- a/src/addons/block/tags/components/components.module.ts +++ b/src/addons/block/tags/components/components.module.ts @@ -27,8 +27,5 @@ import { AddonBlockTagsComponent } from './tags/tags'; exports: [ AddonBlockTagsComponent, ], - entryComponents: [ - AddonBlockTagsComponent, - ], }) export class AddonBlockTagsComponentsModule {} diff --git a/src/addons/block/timeline/components/components.module.ts b/src/addons/block/timeline/components/components.module.ts index 3f7e58788..dfd4a5fc4 100644 --- a/src/addons/block/timeline/components/components.module.ts +++ b/src/addons/block/timeline/components/components.module.ts @@ -35,9 +35,5 @@ import { AddonBlockTimelineEventsComponent } from './events/events'; AddonBlockTimelineComponent, AddonBlockTimelineEventsComponent, ], - entryComponents: [ - AddonBlockTimelineComponent, - AddonBlockTimelineEventsComponent, - ], }) export class AddonBlockTimelineComponentsModule {} diff --git a/src/addons/calendar/components/components.module.ts b/src/addons/calendar/components/components.module.ts index 16a88884b..22b2b7100 100644 --- a/src/addons/calendar/components/components.module.ts +++ b/src/addons/calendar/components/components.module.ts @@ -36,8 +36,5 @@ import { AddonCalendarFilterPopoverComponent } from './filter/filter'; AddonCalendarUpcomingEventsComponent, AddonCalendarFilterPopoverComponent, ], - entryComponents: [ - AddonCalendarFilterPopoverComponent, - ], }) export class AddonCalendarComponentsModule {} diff --git a/src/addons/messages/components/components.module.ts b/src/addons/messages/components/components.module.ts index d4c3fdd3d..ffd43fd20 100644 --- a/src/addons/messages/components/components.module.ts +++ b/src/addons/messages/components/components.module.ts @@ -25,8 +25,5 @@ import { AddonMessagesConversationInfoComponent } from './conversation-info/conv imports: [ CoreSharedModule, ], - entryComponents: [ - AddonMessagesConversationInfoComponent, - ], }) export class AddonMessagesComponentsModule {} diff --git a/src/addons/mod/assign/feedback/comments/comments.module.ts b/src/addons/mod/assign/feedback/comments/comments.module.ts index ff01066df..388efd827 100644 --- a/src/addons/mod/assign/feedback/comments/comments.module.ts +++ b/src/addons/mod/assign/feedback/comments/comments.module.ts @@ -40,8 +40,5 @@ import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate exports: [ AddonModAssignFeedbackCommentsComponent, ], - entryComponents: [ - AddonModAssignFeedbackCommentsComponent, - ], }) export class AddonModAssignFeedbackCommentsModule {} diff --git a/src/addons/mod/assign/feedback/editpdf/editpdf.module.ts b/src/addons/mod/assign/feedback/editpdf/editpdf.module.ts index b9004c861..d9c5dcb31 100644 --- a/src/addons/mod/assign/feedback/editpdf/editpdf.module.ts +++ b/src/addons/mod/assign/feedback/editpdf/editpdf.module.ts @@ -38,8 +38,5 @@ import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate exports: [ AddonModAssignFeedbackEditPdfComponent, ], - entryComponents: [ - AddonModAssignFeedbackEditPdfComponent, - ], }) export class AddonModAssignFeedbackEditPdfModule {} diff --git a/src/addons/mod/assign/feedback/file/file.module.ts b/src/addons/mod/assign/feedback/file/file.module.ts index 9a005629d..021ce1149 100644 --- a/src/addons/mod/assign/feedback/file/file.module.ts +++ b/src/addons/mod/assign/feedback/file/file.module.ts @@ -38,8 +38,5 @@ import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate exports: [ AddonModAssignFeedbackFileComponent, ], - entryComponents: [ - AddonModAssignFeedbackFileComponent, - ], }) export class AddonModAssignFeedbackFileModule {} diff --git a/src/addons/mod/assign/submission/comments/comments.module.ts b/src/addons/mod/assign/submission/comments/comments.module.ts index 4dfc1d641..361d3a96e 100644 --- a/src/addons/mod/assign/submission/comments/comments.module.ts +++ b/src/addons/mod/assign/submission/comments/comments.module.ts @@ -40,8 +40,5 @@ import { CoreCommentsComponentsModule } from '@features/comments/components/comp exports: [ AddonModAssignSubmissionCommentsComponent, ], - entryComponents: [ - AddonModAssignSubmissionCommentsComponent, - ], }) export class AddonModAssignSubmissionCommentsModule {} diff --git a/src/addons/mod/assign/submission/file/file.module.ts b/src/addons/mod/assign/submission/file/file.module.ts index 1c4f1f238..ce4501efe 100644 --- a/src/addons/mod/assign/submission/file/file.module.ts +++ b/src/addons/mod/assign/submission/file/file.module.ts @@ -38,8 +38,5 @@ import { AddonModAssignSubmissionDelegate } from '../../services/submission-dele exports: [ AddonModAssignSubmissionFileComponent, ], - entryComponents: [ - AddonModAssignSubmissionFileComponent, - ], }) export class AddonModAssignSubmissionFileModule {} diff --git a/src/addons/mod/assign/submission/onlinetext/onlinetext.module.ts b/src/addons/mod/assign/submission/onlinetext/onlinetext.module.ts index cda9519cb..a3d79b5c8 100644 --- a/src/addons/mod/assign/submission/onlinetext/onlinetext.module.ts +++ b/src/addons/mod/assign/submission/onlinetext/onlinetext.module.ts @@ -40,8 +40,5 @@ import { AddonModAssignSubmissionDelegate } from '../../services/submission-dele exports: [ AddonModAssignSubmissionOnlineTextComponent, ], - entryComponents: [ - AddonModAssignSubmissionOnlineTextComponent, - ], }) export class AddonModAssignSubmissionOnlineTextModule {} diff --git a/src/addons/mod/glossary/components/components.module.ts b/src/addons/mod/glossary/components/components.module.ts index 6937ea579..b35fa3942 100644 --- a/src/addons/mod/glossary/components/components.module.ts +++ b/src/addons/mod/glossary/components/components.module.ts @@ -35,9 +35,5 @@ import { CoreSearchComponentsModule } from '@features/search/components/componen AddonModGlossaryIndexComponent, AddonModGlossaryModePickerPopoverComponent, ], - entryComponents: [ - AddonModGlossaryIndexComponent, - AddonModGlossaryModePickerPopoverComponent, - ], }) export class AddonModGlossaryComponentsModule {} diff --git a/src/addons/userprofilefield/checkbox/checkbox.module.ts b/src/addons/userprofilefield/checkbox/checkbox.module.ts index a4aaa572e..9f163b4c2 100644 --- a/src/addons/userprofilefield/checkbox/checkbox.module.ts +++ b/src/addons/userprofilefield/checkbox/checkbox.module.ts @@ -38,8 +38,5 @@ import { CoreSharedModule } from '@/core/shared.module'; exports: [ AddonUserProfileFieldCheckboxComponent, ], - entryComponents: [ - AddonUserProfileFieldCheckboxComponent, - ], }) export class AddonUserProfileFieldCheckboxModule {} diff --git a/src/addons/userprofilefield/datetime/datetime.module.ts b/src/addons/userprofilefield/datetime/datetime.module.ts index a47a1cac1..a70a4da0b 100644 --- a/src/addons/userprofilefield/datetime/datetime.module.ts +++ b/src/addons/userprofilefield/datetime/datetime.module.ts @@ -38,8 +38,5 @@ import { CoreSharedModule } from '@/core/shared.module'; exports: [ AddonUserProfileFieldDatetimeComponent, ], - entryComponents: [ - AddonUserProfileFieldDatetimeComponent, - ], }) export class AddonUserProfileFieldDatetimeModule {} diff --git a/src/addons/userprofilefield/menu/menu.module.ts b/src/addons/userprofilefield/menu/menu.module.ts index 0672fa29d..62aae65da 100644 --- a/src/addons/userprofilefield/menu/menu.module.ts +++ b/src/addons/userprofilefield/menu/menu.module.ts @@ -38,8 +38,5 @@ import { CoreSharedModule } from '@/core/shared.module'; exports: [ AddonUserProfileFieldMenuComponent, ], - entryComponents: [ - AddonUserProfileFieldMenuComponent, - ], }) export class AddonUserProfileFieldMenuModule {} diff --git a/src/addons/userprofilefield/text/text.module.ts b/src/addons/userprofilefield/text/text.module.ts index 019457b98..83131dfe0 100644 --- a/src/addons/userprofilefield/text/text.module.ts +++ b/src/addons/userprofilefield/text/text.module.ts @@ -38,8 +38,5 @@ import { CoreSharedModule } from '@/core/shared.module'; exports: [ AddonUserProfileFieldTextComponent, ], - entryComponents: [ - AddonUserProfileFieldTextComponent, - ], }) export class AddonUserProfileFieldTextModule {} diff --git a/src/addons/userprofilefield/textarea/textarea.module.ts b/src/addons/userprofilefield/textarea/textarea.module.ts index b208dfc98..579b284ff 100644 --- a/src/addons/userprofilefield/textarea/textarea.module.ts +++ b/src/addons/userprofilefield/textarea/textarea.module.ts @@ -40,8 +40,5 @@ import { CoreEditorComponentsModule } from '@features/editor/components/componen exports: [ AddonUserProfileFieldTextareaComponent, ], - entryComponents: [ - AddonUserProfileFieldTextareaComponent, - ], }) export class AddonUserProfileFieldTextareaModule {} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6a1b4a0e8..fed9d6f73 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -37,7 +37,6 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { @NgModule({ declarations: [AppComponent], - entryComponents: [], imports: [ BrowserModule, BrowserAnimationsModule, diff --git a/src/core/components/dynamic-component/dynamic-component.ts b/src/core/components/dynamic-component/dynamic-component.ts index d48e1b4e1..033ab327a 100644 --- a/src/core/components/dynamic-component/dynamic-component.ts +++ b/src/core/components/dynamic-component/dynamic-component.ts @@ -51,8 +51,6 @@ import { CoreLogger } from '@singletons/logger'; *

Cannot render the data.

* * - * Please notice that the component that you pass needs to be declared in entryComponents of the module to be created dynamically. - * * Alternatively, you can also supply a ComponentRef instead of the class of the component. In this case, the component won't * be instantiated because it already is, it will be attached to the view and the right data will be passed to it. * Passing ComponentRef is meant for site plugins. diff --git a/src/core/features/block/components/components.module.ts b/src/core/features/block/components/components.module.ts index 7f8dea3da..7d64e292d 100644 --- a/src/core/features/block/components/components.module.ts +++ b/src/core/features/block/components/components.module.ts @@ -35,10 +35,5 @@ import { CoreSharedModule } from '@/core/shared.module'; CoreBlockPreRenderedComponent, CoreBlockCourseBlocksComponent, ], - entryComponents: [ - CoreBlockOnlyTitleComponent, - CoreBlockPreRenderedComponent, - CoreBlockCourseBlocksComponent, - ], }) export class CoreBlockComponentsModule {} diff --git a/src/core/features/comments/components/components.module.ts b/src/core/features/comments/components/components.module.ts index cf6d3e093..7cbec08f7 100644 --- a/src/core/features/comments/components/components.module.ts +++ b/src/core/features/comments/components/components.module.ts @@ -29,8 +29,5 @@ import { CoreCommentsCommentsComponent } from './comments/comments'; CoreCommentsCommentsComponent, CoreCommentsAddComponent, ], - entryComponents: [ - CoreCommentsCommentsComponent, - ], }) export class CoreCommentsComponentsModule {} diff --git a/src/core/features/courses/components/components.module.ts b/src/core/features/courses/components/components.module.ts index 64a2cc9fa..24068556c 100644 --- a/src/core/features/courses/components/components.module.ts +++ b/src/core/features/courses/components/components.module.ts @@ -36,8 +36,5 @@ import { CoreCoursesSelfEnrolPasswordComponent } from './self-enrol-password/sel CoreCoursesCourseOptionsMenuComponent, CoreCoursesSelfEnrolPasswordComponent, ], - entryComponents: [ - CoreCoursesCourseOptionsMenuComponent, - ], }) export class CoreCoursesComponentsModule {} diff --git a/src/core/features/editor/components/components.module.ts b/src/core/features/editor/components/components.module.ts index f657a39ef..3c46c84a7 100644 --- a/src/core/features/editor/components/components.module.ts +++ b/src/core/features/editor/components/components.module.ts @@ -29,8 +29,5 @@ import { CoreSharedModule } from '@/core/shared.module'; exports: [ CoreEditorRichTextEditorComponent, ], - entryComponents: [ - CoreEditorRichTextEditorComponent, - ], }) export class CoreEditorComponentsModule {} diff --git a/src/core/features/rating/components/components.module.ts b/src/core/features/rating/components/components.module.ts index c6d4578dc..3315306b4 100644 --- a/src/core/features/rating/components/components.module.ts +++ b/src/core/features/rating/components/components.module.ts @@ -32,8 +32,5 @@ import { CoreRatingRatingsComponent } from './ratings/ratings'; CoreRatingRateComponent, CoreRatingRatingsComponent, ], - entryComponents: [ - CoreRatingRatingsComponent, - ], }) export class CoreRatingComponentsModule {} From 8e9b550254f70805e0140a6baf953990d588a6b3 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 14 Apr 2021 11:24:08 +0200 Subject: [PATCH 6/6] MOBILE-3644 sync: Always use new offline warning functions --- src/addons/mod/choice/services/choice-sync.ts | 9 ++------ src/addons/mod/data/services/data-sync.ts | 20 ++++++------------ .../mod/glossary/services/glossary-sync.ts | 13 ++---------- src/addons/mod/wiki/services/wiki-sync.ts | 7 +------ src/core/classes/base-sync.ts | 21 ++++++++++++++----- 5 files changed, 27 insertions(+), 43 deletions(-) diff --git a/src/addons/mod/choice/services/choice-sync.ts b/src/addons/mod/choice/services/choice-sync.ts index d933c29c2..a27a01f05 100644 --- a/src/addons/mod/choice/services/choice-sync.ts +++ b/src/addons/mod/choice/services/choice-sync.ts @@ -19,9 +19,8 @@ import { CoreCourse } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreApp } from '@services/app'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; -import { makeSingleton, Translate } from '@singletons'; +import { makeSingleton } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { AddonModChoice, AddonModChoiceProvider } from './choice'; import { AddonModChoiceOffline } from './choice-offline'; @@ -192,11 +191,7 @@ export class AddonModChoiceSyncProvider extends CoreCourseActivitySyncBaseProvid await AddonModChoiceOffline.deleteResponse(choiceId, siteId, userId); // Responses deleted, add a warning. - result.warnings.push(Translate.instant('core.warningofflinedatadeleted', { - component: this.componentTranslate, - name: data.name, - error: CoreTextUtils.getErrorMessageFromError(error), - })); + this.addOfflineDataDeletedWarning(result.warnings, data.name, error); } // Data has been sent to server, prefetch choice if needed. diff --git a/src/addons/mod/data/services/data-sync.ts b/src/addons/mod/data/services/data-sync.ts index 92d0d94c9..b89e4b2a6 100644 --- a/src/addons/mod/data/services/data-sync.ts +++ b/src/addons/mod/data/services/data-sync.ts @@ -235,28 +235,20 @@ export class AddonModDataSyncProvider extends CoreCourseActivitySyncBaseProvider result: AddonModDataSyncResult, siteId: string, ): Promise { - const synEntryResult = await this.performSyncEntry(database, entryActions, result, siteId); + const syncEntryResult = await this.performSyncEntry(database, entryActions, result, siteId); - if (synEntryResult.discardError) { + if (syncEntryResult.discardError) { // Submission was discarded, add a warning. - const message = Translate.instant('core.warningofflinedatadeleted', { - component: this.componentTranslate, - name: database.name, - error: synEntryResult.discardError, - }); - - if (result.warnings.indexOf(message) == -1) { - result.warnings.push(message); - } + this.addOfflineDataDeletedWarning(result.warnings, database.name, syncEntryResult.discardError); } // Sync done. Send event. CoreEvents.trigger(AddonModDataSyncProvider.AUTO_SYNCED, { dataId: database.id, - entryId: synEntryResult.entryId, - offlineEntryId: synEntryResult.offlineId, + entryId: syncEntryResult.entryId, + offlineEntryId: syncEntryResult.offlineId, warnings: result.warnings, - deleted: synEntryResult.deleted, + deleted: syncEntryResult.deleted, }, siteId); } diff --git a/src/addons/mod/glossary/services/glossary-sync.ts b/src/addons/mod/glossary/services/glossary-sync.ts index ca697c46d..3502a82a5 100644 --- a/src/addons/mod/glossary/services/glossary-sync.ts +++ b/src/addons/mod/glossary/services/glossary-sync.ts @@ -23,7 +23,6 @@ import { CoreRatingSync } from '@features/rating/services/rating-sync'; import { CoreApp } from '@services/app'; import { CoreSites } from '@services/sites'; import { CoreSync } from '@services/sync'; -import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; @@ -222,11 +221,7 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv await this.deleteAddEntry(glossaryId, data.concept, data.timecreated, siteId); // Responses deleted, add a warning. - result.warnings.push(Translate.instant('core.warningofflinedatadeleted', { - component: this.componentTranslate, - name: data.concept, - error: CoreTextUtils.getErrorMessageFromError(error), - })); + this.addOfflineDataDeletedWarning(result.warnings, data.concept, error); } })); @@ -275,11 +270,7 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv const glossary = await AddonModGlossary.getGlossary(result.itemSet.courseId, result.itemSet.instanceId, { siteId }); result.warnings.forEach((warning) => { - warnings.push(Translate.instant('core.warningofflinedatadeleted', { - component: this.componentTranslate, - name: glossary.name, - error: warning, - })); + this.addOfflineDataDeletedWarning(warnings, glossary.name, warning); }); } })); diff --git a/src/addons/mod/wiki/services/wiki-sync.ts b/src/addons/mod/wiki/services/wiki-sync.ts index 0f9375231..691a08e9b 100644 --- a/src/addons/mod/wiki/services/wiki-sync.ts +++ b/src/addons/mod/wiki/services/wiki-sync.ts @@ -20,7 +20,6 @@ import { CoreApp } from '@services/app'; import { CoreGroups } from '@services/groups'; import { CoreSites } from '@services/sites'; import { CoreSync } from '@services/sync'; -import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; @@ -260,11 +259,7 @@ export class AddonModWikiSyncProvider extends CoreSyncBaseProvider { * @param error Specific error message. */ protected addOfflineDataDeletedWarning(warnings: string[], name: string, error: CoreAnyError): void { - const warning = Translate.instant('core.warningofflinedatadeleted', { - component: this.componentTranslate, - name: name, - error: CoreTextUtils.getErrorMessageFromError(error), - }); + const warning = this.getOfflineDataDeletedWarning(name, error); if (warnings.indexOf(warning) == -1) { warnings.push(warning); @@ -113,6 +109,21 @@ export class CoreSyncBaseProvider { } } + /** + * Add an offline data deleted warning to a list of warnings. + * + * @param name Instance name. + * @param error Specific error message. + * @return Warning message. + */ + protected getOfflineDataDeletedWarning(name: string, error: CoreAnyError): string { + return Translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: name, + error: CoreTextUtils.getErrorMessageFromError(error), + }); + } + /** * If there's an ongoing sync for a certain identifier return it. *