From 7e8e694c63bb1f59c2c05d4b3999260d43eba8fe Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 21 Mar 2023 13:54:41 +0100 Subject: [PATCH 01/10] MOBILE-2652 glossary: Clean up service class --- .../classes/glossary-entries-source.ts | 80 ++------ src/addons/mod/glossary/services/glossary.ts | 175 ++++++------------ .../glossary/services/handlers/prefetch.ts | 32 +--- upgrade.txt | 1 + 4 files changed, 73 insertions(+), 215 deletions(-) diff --git a/src/addons/mod/glossary/classes/glossary-entries-source.ts b/src/addons/mod/glossary/classes/glossary-entries-source.ts index 7fd2e2814..a58e0cfe7 100644 --- a/src/addons/mod/glossary/classes/glossary-entries-source.ts +++ b/src/addons/mod/glossary/classes/glossary-entries-source.ts @@ -164,21 +164,8 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource< const glossaryId = this.glossary.id; - this.fetchFunction = (options) => AddonModGlossary.getEntriesBySearch( - glossaryId, - query, - true, - 'CONCEPT', - 'ASC', - options, - ); - this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesBySearch( - glossaryId, - query, - true, - 'CONCEPT', - 'ASC', - ); + this.fetchFunction = (options) => AddonModGlossary.getEntriesBySearch(glossaryId, query, true, options); + this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesBySearch(glossaryId, query, true); this.hasSearched = true; this.setDirty(true); } @@ -220,65 +207,29 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource< case 'author_all': // Browse by author. this.viewMode = 'author'; - this.fetchFunction = (options) => AddonModGlossary.getEntriesByAuthor( - glossaryId, - 'ALL', - 'LASTNAME', - 'ASC', - options, - ); - this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByAuthor( - glossaryId, - 'ALL', - 'LASTNAME', - 'ASC', - ); + this.fetchFunction = (options) => AddonModGlossary.getEntriesByAuthor(glossaryId, options); + this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByAuthor(glossaryId); break; case 'cat_all': // Browse by category. this.viewMode = 'cat'; - this.fetchFunction = (options) => AddonModGlossary.getEntriesByCategory( - glossaryId, - AddonModGlossaryProvider.SHOW_ALL_CATEGORIES, - options, - ); - this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByCategory( - glossaryId, - AddonModGlossaryProvider.SHOW_ALL_CATEGORIES, - ); + this.fetchFunction = (options) => AddonModGlossary.getEntriesByCategory(glossaryId, options); + this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByCategory(glossaryId); break; case 'newest_first': // Newest first. this.viewMode = 'date'; - this.fetchFunction = (options) => AddonModGlossary.getEntriesByDate( - glossaryId, - 'CREATION', - 'DESC', - options, - ); - this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByDate( - glossaryId, - 'CREATION', - 'DESC', - ); + this.fetchFunction = (options) => AddonModGlossary.getEntriesByDate(glossaryId, 'CREATION', options); + this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByDate(glossaryId, 'CREATION'); break; case 'recently_updated': // Recently updated. this.viewMode = 'date'; - this.fetchFunction = (options) => AddonModGlossary.getEntriesByDate( - glossaryId, - 'UPDATE', - 'DESC', - options, - ); - this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByDate( - glossaryId, - 'UPDATE', - 'DESC', - ); + this.fetchFunction = (options) => AddonModGlossary.getEntriesByDate(glossaryId, 'UPDATE', options); + this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByDate(glossaryId, 'UPDATE'); break; case 'letter_all': @@ -286,15 +237,8 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource< // Consider it is 'letter_all'. this.viewMode = 'letter'; this.fetchMode = 'letter_all'; - this.fetchFunction = (options) => AddonModGlossary.getEntriesByLetter( - glossaryId, - 'ALL', - options, - ); - this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByLetter( - glossaryId, - 'ALL', - ); + this.fetchFunction = (options) => AddonModGlossary.getEntriesByLetter(glossaryId, options); + this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByLetter(glossaryId); break; } } diff --git a/src/addons/mod/glossary/services/glossary.ts b/src/addons/mod/glossary/services/glossary.ts index 69645bbee..831b9876c 100644 --- a/src/addons/mod/glossary/services/glossary.ts +++ b/src/addons/mod/glossary/services/glossary.ts @@ -30,8 +30,6 @@ import { AddonModGlossaryOffline } from './glossary-offline'; import { AddonModGlossaryAutoSyncData, AddonModGlossarySyncProvider } from './glossary-sync'; import { CoreFileEntry } from '@services/file-helper'; -const ROOT_CACHE_KEY = 'mmaModGlossary:'; - /** * Service that provides some features for glossaries. */ @@ -41,11 +39,12 @@ 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'; + private static readonly SHOW_ALL_CATEGORIES = 0; + private static readonly ROOT_CACHE_KEY = 'mmaModGlossary:'; + /** * Get the course glossary cache key. * @@ -53,7 +52,7 @@ export class AddonModGlossaryProvider { * @returns Cache key. */ protected getCourseGlossariesCacheKey(courseId: number): string { - return ROOT_CACHE_KEY + 'courseGlossaries:' + courseId; + return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}courseGlossaries:${courseId}`; } /** @@ -90,7 +89,6 @@ export class AddonModGlossaryProvider { * * @param courseId Course Id. * @param siteId Site ID. If not defined, current site. - * @returns Resolved when data is invalidated. */ async invalidateCourseGlossaries(courseId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); @@ -104,44 +102,35 @@ export class AddonModGlossaryProvider { * 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 * @returns Cache key. */ - protected getEntriesByAuthorCacheKey(glossaryId: number, letter: string, field: string, sort: string): string { - return ROOT_CACHE_KEY + 'entriesByAuthor:' + glossaryId + ':' + letter + ':' + field + ':' + sort; + protected getEntriesByAuthorCacheKey(glossaryId: number): string { + return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesByAuthor:${glossaryId}:ALL:LASTNAME:ASC`; } /** * 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. * @returns 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, + letter: 'ALL', + field: 'LASTNAME', + sort: 'ASC', from: options.from || 0, limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, }; const preSets: CoreSiteWSPreSets = { - cacheKey: this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort), + cacheKey: this.getEntriesByAuthorCacheKey(glossaryId), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, component: AddonModGlossaryProvider.COMPONENT, componentId: options.cmId, @@ -155,22 +144,12 @@ export class AddonModGlossaryProvider { * 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. - * @returns Resolved when data is invalidated. */ - async invalidateEntriesByAuthor( - glossaryId: number, - letter: string, - field: string, - sort: string, - siteId?: string, - ): Promise { + async invalidateEntriesByAuthor(glossaryId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); - const key = this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort); + const key = this.getEntriesByAuthorCacheKey(glossaryId); await site.invalidateWsCacheForKey(key); } @@ -179,26 +158,23 @@ export class AddonModGlossaryProvider { * 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. * @returns 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, + categoryid: AddonModGlossaryProvider.SHOW_ALL_CATEGORIES, from: options.from || 0, limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, }; const preSets: CoreSiteWSPreSets = { - cacheKey: this.getEntriesByCategoryCacheKey(glossaryId, categoryId), + cacheKey: this.getEntriesByCategoryCacheKey(glossaryId), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, component: AddonModGlossaryProvider.COMPONENT, componentId: options.cmId, @@ -212,15 +188,12 @@ export class AddonModGlossaryProvider { * 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. - * @returns Resolved when data is invalidated. */ - async invalidateEntriesByCategory(glossaryId: number, categoryId: number, siteId?: string): Promise { + async invalidateEntriesByCategory(glossaryId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); - const key = this.getEntriesByCategoryCacheKey(glossaryId, categoryId); + const key = this.getEntriesByCategoryCacheKey(glossaryId); await site.invalidateWsCacheForKey(key); } @@ -229,12 +202,12 @@ export class AddonModGlossaryProvider { * 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. * @returns Cache key. */ - getEntriesByCategoryCacheKey(glossaryId: number, categoryId: number): string { - return ROOT_CACHE_KEY + 'entriesByCategory:' + glossaryId + ':' + categoryId; + getEntriesByCategoryCacheKey(glossaryId: number): string { + const prefix = `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesByCategory`; + + return `${prefix}:${glossaryId}:${AddonModGlossaryProvider.SHOW_ALL_CATEGORIES}`; } /** @@ -242,11 +215,10 @@ export class AddonModGlossaryProvider { * * @param glossaryId Glossary Id. * @param order The way to order the records. - * @param sort The direction of the order. * @returns Cache key. */ - getEntriesByDateCacheKey(glossaryId: number, order: string, sort: string): string { - return ROOT_CACHE_KEY + 'entriesByDate:' + glossaryId + ':' + order + ':' + sort; + getEntriesByDateCacheKey(glossaryId: number, order: string): string { + return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesByDate:${glossaryId}:${order}:DESC`; } /** @@ -254,14 +226,12 @@ export class AddonModGlossaryProvider { * * @param glossaryId Glossary Id. * @param order The way to order the records. - * @param sort The direction of the order. * @param options Other options. * @returns Resolved with the entries. */ async getEntriesByDate( glossaryId: number, order: string, - sort: string, options: AddonModGlossaryGetEntriesOptions = {}, ): Promise { const site = await CoreSites.getSite(options.siteId); @@ -269,12 +239,12 @@ export class AddonModGlossaryProvider { const params: AddonModGlossaryGetEntriesByDateWSParams = { id: glossaryId, order: order, - sort: sort, + sort: 'DESC', from: options.from || 0, limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, }; const preSets: CoreSiteWSPreSets = { - cacheKey: this.getEntriesByDateCacheKey(glossaryId, order, sort), + cacheKey: this.getEntriesByDateCacheKey(glossaryId, order), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, component: AddonModGlossaryProvider.COMPONENT, componentId: options.cmId, @@ -289,14 +259,12 @@ export class AddonModGlossaryProvider { * * @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. - * @returns Resolved when data is invalidated. */ - async invalidateEntriesByDate(glossaryId: number, order: string, sort: string, siteId?: string): Promise { + async invalidateEntriesByDate(glossaryId: number, order: string, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); - const key = this.getEntriesByDateCacheKey(glossaryId, order, sort); + const key = this.getEntriesByDateCacheKey(glossaryId, order); await site.invalidateWsCacheForKey(key); } @@ -305,24 +273,21 @@ export class AddonModGlossaryProvider { * Get the entries by letter cache key. * * @param glossaryId Glossary Id. - * @param letter A letter, or a special keyword. * @returns Cache key. */ - protected getEntriesByLetterCacheKey(glossaryId: number, letter: string): string { - return ROOT_CACHE_KEY + 'entriesByLetter:' + glossaryId + ':' + letter; + protected getEntriesByLetterCacheKey(glossaryId: number): string { + return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesByLetter:${glossaryId}:ALL`; } /** * Get entries by letter. * * @param glossaryId Glossary Id. - * @param letter A letter, or a special keyword. * @param options Other options. * @returns Resolved with the entries. */ async getEntriesByLetter( glossaryId: number, - letter: string, options: AddonModGlossaryGetEntriesOptions = {}, ): Promise { options.from = options.from || 0; @@ -332,12 +297,12 @@ export class AddonModGlossaryProvider { const params: AddonModGlossaryGetEntriesByLetterWSParams = { id: glossaryId, - letter: letter, + letter: 'ALL', from: options.from, limit: options.limit, }; const preSets: CoreSiteWSPreSets = { - cacheKey: this.getEntriesByLetterCacheKey(glossaryId, letter), + cacheKey: this.getEntriesByLetterCacheKey(glossaryId), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, component: AddonModGlossaryProvider.COMPONENT, componentId: options.cmId, @@ -362,16 +327,14 @@ export class AddonModGlossaryProvider { * 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. - * @returns Resolved when data is invalidated. */ - async invalidateEntriesByLetter(glossaryId: number, letter: string, siteId?: string): Promise { + async invalidateEntriesByLetter(glossaryId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); - const key = this.getEntriesByLetterCacheKey(glossaryId, letter); + const key = this.getEntriesByLetterCacheKey(glossaryId); - return site.invalidateWsCacheForKey(key); + await site.invalidateWsCacheForKey(key); } /** @@ -380,18 +343,10 @@ export class AddonModGlossaryProvider { * @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. * @returns 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; + protected getEntriesBySearchCacheKey(glossaryId: number, query: string, fullSearch: boolean): string { + return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesBySearch:${glossaryId}:${fullSearch}:CONCEPT:ASC:${query}`; } /** @@ -400,8 +355,6 @@ export class AddonModGlossaryProvider { * @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 options Get entries options. * @returns Resolved with the entries. */ @@ -409,8 +362,6 @@ export class AddonModGlossaryProvider { glossaryId: number, query: string, fullSearch: boolean, - order: string, - sort: string, options: AddonModGlossaryGetEntriesOptions = {}, ): Promise { const site = await CoreSites.getSite(options.siteId); @@ -419,13 +370,13 @@ export class AddonModGlossaryProvider { id: glossaryId, query: query, fullsearch: fullSearch, - order: order, - sort: sort, + order: 'CONCEPT', + sort: 'ASC', from: options.from || 0, limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES, }; const preSets: CoreSiteWSPreSets = { - cacheKey: this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort), + cacheKey: this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, component: AddonModGlossaryProvider.COMPONENT, componentId: options.cmId, @@ -441,22 +392,17 @@ export class AddonModGlossaryProvider { * @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. - * @returns 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); + const key = this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch); await site.invalidateWsCacheForKey(key); } @@ -468,7 +414,7 @@ export class AddonModGlossaryProvider { * @returns The cache key. */ protected getCategoriesCacheKey(glossaryId: number): string { - return ROOT_CACHE_KEY + 'categories:' + glossaryId; + return AddonModGlossaryProvider.ROOT_CACHE_KEY + 'categories:' + glossaryId; } /** @@ -533,7 +479,6 @@ export class AddonModGlossaryProvider { * * @param glossaryId Glossary Id. * @param siteId Site ID. If not defined, current site. - * @returns Promise resolved when categories data has been invalidated, */ async invalidateCategories(glossaryId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); @@ -548,7 +493,7 @@ export class AddonModGlossaryProvider { * @returns Cache key. */ protected getEntryCacheKey(entryId: number): string { - return ROOT_CACHE_KEY + 'getEntry:' + entryId; + return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}getEntry:${entryId}`; } /** @@ -637,7 +582,7 @@ export class AddonModGlossaryProvider { 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', { + const result = await this.getEntriesByLetter(glossaryId, { from: from, readingStrategy: CoreSitesReadingStrategy.ONLY_CACHE, cmId: options.cmId, @@ -695,7 +640,6 @@ export class AddonModGlossaryProvider { * * @param entryId Entry Id. * @param siteId Site ID. If not defined, current site. - * @returns Resolved when data is invalidated. */ async invalidateEntry(entryId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); @@ -708,7 +652,6 @@ export class AddonModGlossaryProvider { * * @param entries Entry objects to invalidate. * @param siteId Site ID. If not defined, current site. - * @returns Resolved when data is invalidated. */ protected async invalidateEntries(entries: AddonModGlossaryEntry[], siteId?: string): Promise { const keys: string[] = []; @@ -727,7 +670,6 @@ export class AddonModGlossaryProvider { * * @param moduleId The module ID. * @param courseId Course ID. - * @returns Promise resolved when data is invalidated. */ async invalidateContent(moduleId: number, courseId: number): Promise { const glossary = await this.getGlossary(courseId, moduleId); @@ -747,7 +689,6 @@ export class AddonModGlossaryProvider { * @param glossary The glossary object. * @param onlyEntriesList If true, entries won't be invalidated. * @param siteId Site ID. If not defined, current site. - * @returns Promise resolved when data is invalidated. */ async invalidateGlossaryEntries(glossary: AddonModGlossaryGlossary, onlyEntriesList?: boolean, siteId?: string): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); @@ -755,7 +696,7 @@ export class AddonModGlossaryProvider { const promises: Promise[] = []; if (!onlyEntriesList) { - promises.push(this.fetchAllEntries((options) => this.getEntriesByLetter(glossary.id, 'ALL', options), { + promises.push(this.fetchAllEntries((options) => this.getEntriesByLetter(glossary.id, options), { cmId: glossary.coursemodule, readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE, siteId, @@ -765,21 +706,17 @@ export class AddonModGlossaryProvider { glossary.browsemodes.forEach((mode) => { switch (mode) { case 'letter': - promises.push(this.invalidateEntriesByLetter(glossary.id, 'ALL', siteId)); + promises.push(this.invalidateEntriesByLetter(glossary.id, siteId)); break; case 'cat': - promises.push(this.invalidateEntriesByCategory( - glossary.id, - AddonModGlossaryProvider.SHOW_ALL_CATEGORIES, - siteId, - )); + promises.push(this.invalidateEntriesByCategory(glossary.id, siteId)); break; case 'date': - promises.push(this.invalidateEntriesByDate(glossary.id, 'CREATION', 'DESC', siteId)); - promises.push(this.invalidateEntriesByDate(glossary.id, 'UPDATE', 'DESC', siteId)); + promises.push(this.invalidateEntriesByDate(glossary.id, 'CREATION', siteId)); + promises.push(this.invalidateEntriesByDate(glossary.id, 'UPDATE', siteId)); break; case 'author': - promises.push(this.invalidateEntriesByAuthor(glossary.id, 'ALL', 'LASTNAME', 'ASC', siteId)); + promises.push(this.invalidateEntriesByAuthor(glossary.id, siteId)); break; default: } @@ -959,7 +896,7 @@ export class AddonModGlossaryProvider { }; if (attachId) { - params.options!.push({ + params.options?.push({ name: 'attachmentsid', value: String(attachId), }); @@ -989,7 +926,7 @@ export class AddonModGlossaryProvider { // If we get here, there's no offline entry with this name, check online. // Get entries from the cache. - const entries = await this.fetchAllEntries((options) => this.getEntriesByLetter(glossaryId, 'ALL', options), { + const entries = await this.fetchAllEntries((options) => this.getEntriesByLetter(glossaryId, options), { cmId: options.cmId, readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE, siteId: options.siteId, @@ -1010,15 +947,14 @@ export class AddonModGlossaryProvider { * @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. - * @returns Promise resolved when the WS call is successful. */ - logView(glossaryId: number, mode: string, name?: string, siteId?: string): Promise { + async logView(glossaryId: number, mode: string, name?: string, siteId?: string): Promise { const params: AddonModGlossaryViewGlossaryWSParams = { id: glossaryId, mode: mode, }; - return CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.logSingle( 'mod_glossary_view_glossary', params, AddonModGlossaryProvider.COMPONENT, @@ -1037,14 +973,13 @@ export class AddonModGlossaryProvider { * @param glossaryId Glossary ID. * @param name Name of the glossary. * @param siteId Site ID. If not defined, current site. - * @returns Promise resolved when the WS call is successful. */ - logEntryView(entryId: number, glossaryId: number, name?: string, siteId?: string): Promise { + async logEntryView(entryId: number, glossaryId: number, name?: string, siteId?: string): Promise { const params: AddonModGlossaryViewEntryWSParams = { id: entryId, }; - return CoreCourseLogHelper.logSingle( + await CoreCourseLogHelper.logSingle( 'mod_glossary_view_entry', params, AddonModGlossaryProvider.COMPONENT, @@ -1063,7 +998,6 @@ export class AddonModGlossaryProvider { * @param entries Entries. * @param from The "page" the entries belong to. * @param siteId Site ID. If not defined, current site. - * @returns Promise resolved when done. */ protected async storeEntries( glossaryId: number, @@ -1081,7 +1015,6 @@ export class AddonModGlossaryProvider { * @param entryId Entry ID. * @param from The "page" the entry belongs to. * @param siteId Site ID. If not defined, current site. - * @returns Promise resolved when done. */ protected async storeEntryId(glossaryId: number, entryId: number, from: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); diff --git a/src/addons/mod/glossary/services/handlers/prefetch.ts b/src/addons/mod/glossary/services/handlers/prefetch.ts index d6b6cf5f4..f9566bd0e 100644 --- a/src/addons/mod/glossary/services/handlers/prefetch.ts +++ b/src/addons/mod/glossary/services/handlers/prefetch.ts @@ -45,7 +45,7 @@ export class AddonModGlossaryPrefetchHandlerService extends CoreCourseActivityPr const glossary = await AddonModGlossary.getGlossary(courseId, module.id); const entries = await AddonModGlossary.fetchAllEntries( - (options) => AddonModGlossary.getEntriesByLetter(glossary.id, 'ALL', options), + (options) => AddonModGlossary.getEntriesByLetter(glossary.id, options), { cmId: module.id, }, @@ -125,43 +125,23 @@ export class AddonModGlossaryPrefetchHandlerService extends CoreCourseActivityPr break; case 'cat': promises.push(AddonModGlossary.fetchAllEntries( - (newOptions) => AddonModGlossary.getEntriesByCategory( - glossary.id, - AddonModGlossaryProvider.SHOW_ALL_CATEGORIES, - newOptions, - ), + (newOptions) => AddonModGlossary.getEntriesByCategory(glossary.id, newOptions), options, )); break; case 'date': promises.push(AddonModGlossary.fetchAllEntries( - (newOptions) => AddonModGlossary.getEntriesByDate( - glossary.id, - 'CREATION', - 'DESC', - newOptions, - ), + (newOptions) => AddonModGlossary.getEntriesByDate(glossary.id, 'CREATION', newOptions), options, )); promises.push(AddonModGlossary.fetchAllEntries( - (newOptions) => AddonModGlossary.getEntriesByDate( - glossary.id, - 'UPDATE', - 'DESC', - newOptions, - ), + (newOptions) => AddonModGlossary.getEntriesByDate(glossary.id, 'UPDATE', newOptions), options, )); break; case 'author': promises.push(AddonModGlossary.fetchAllEntries( - (newOptions) => AddonModGlossary.getEntriesByAuthor( - glossary.id, - 'ALL', - 'LASTNAME', - 'ASC', - newOptions, - ), + (newOptions) => AddonModGlossary.getEntriesByAuthor(glossary.id, newOptions), options, )); break; @@ -171,7 +151,7 @@ export class AddonModGlossaryPrefetchHandlerService extends CoreCourseActivityPr // Fetch all entries to get information from. promises.push(AddonModGlossary.fetchAllEntries( - (newOptions) => AddonModGlossary.getEntriesByLetter(glossary.id, 'ALL', newOptions), + (newOptions) => AddonModGlossary.getEntriesByLetter(glossary.id, newOptions), options, ).then((entries) => { const promises: Promise[] = []; diff --git a/upgrade.txt b/upgrade.txt index eebe0c6c4..5fe38986f 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -6,6 +6,7 @@ information provided here is intended especially for developers. - CoreIconComponent has been removed after deprecation period: Use CoreFaIconDirective instead. - The courseSummaryComponent property has been removed from the CoreCourseFormatComponent component, and the getCourseSummaryComponent method from the CoreCourseFormatHandler interface. - Font Awesome icon library has been updated to 6.3.0. +- Some methods in AddonModGlossaryProvider have changed their signatures to remove unused parameters. === 4.1.0 === From 5de4cfbbd2348e081a4e21a88a2b996e8f499ace Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 21 Mar 2023 17:46:47 +0100 Subject: [PATCH 02/10] MOBILE-2652 glossary: Implement deleting entries --- scripts/langindex.json | 4 ++ .../classes/glossary-entries-source.ts | 10 ++-- .../mod/glossary/components/index/index.ts | 47 +++++++++------- src/addons/mod/glossary/lang.json | 4 ++ .../mod/glossary/pages/entry/entry.html | 7 +++ src/addons/mod/glossary/pages/entry/entry.ts | 52 ++++++++++++++++- .../mod/glossary/services/glossary-offline.ts | 12 ++-- .../mod/glossary/services/glossary-sync.ts | 23 ++++++-- src/addons/mod/glossary/services/glossary.ts | 56 ++++++++++++++++--- .../glossary/tests/behat/basic_usage.feature | 48 ++++++++++++++++ src/core/services/utils/utils.ts | 4 +- 11 files changed, 222 insertions(+), 45 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index ff29240d8..4d1fdd0b8 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -681,6 +681,7 @@ "addon.mod_forum.yourreply": "forum", "addon.mod_glossary.addentry": "glossary", "addon.mod_glossary.aliases": "glossary", + "addon.mod_glossary.areyousuredelete": "glossary", "addon.mod_glossary.attachment": "glossary", "addon.mod_glossary.browsemode": "local_moodlemobileapp", "addon.mod_glossary.byalphabet": "local_moodlemobileapp", @@ -694,9 +695,12 @@ "addon.mod_glossary.categories": "glossary", "addon.mod_glossary.concept": "glossary", "addon.mod_glossary.definition": "glossary", + "addon.mod_glossary.deleteentry": "glossary", "addon.mod_glossary.entriestobesynced": "local_moodlemobileapp", + "addon.mod_glossary.entrydeleted": "glossary", "addon.mod_glossary.entrypendingapproval": "local_moodlemobileapp", "addon.mod_glossary.entryusedynalink": "glossary", + "addon.mod_glossary.errordeleting": "local_moodlemobileapp", "addon.mod_glossary.errconceptalreadyexists": "glossary", "addon.mod_glossary.errorloadingentries": "local_moodlemobileapp", "addon.mod_glossary.errorloadingentry": "local_moodlemobileapp", diff --git a/src/addons/mod/glossary/classes/glossary-entries-source.ts b/src/addons/mod/glossary/classes/glossary-entries-source.ts index a58e0cfe7..3e0dc06c6 100644 --- a/src/addons/mod/glossary/classes/glossary-entries-source.ts +++ b/src/addons/mod/glossary/classes/glossary-entries-source.ts @@ -179,12 +179,14 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource< /** * Invalidate glossary cache. + * + * @param invalidateGlossary Whether to invalidate the entire glossary or not */ - async invalidateCache(): Promise { - await Promise.all([ - AddonModGlossary.invalidateCourseGlossaries(this.COURSE_ID), + async invalidateCache(invalidateGlossary: boolean = true): Promise { + await Promise.all([ this.fetchInvalidate && this.fetchInvalidate(), - this.glossary && AddonModGlossary.invalidateCategories(this.glossary.id), + invalidateGlossary && AddonModGlossary.invalidateCourseGlossaries(this.COURSE_ID), + invalidateGlossary && this.glossary && AddonModGlossary.invalidateCategories(this.glossary.id), ]); } diff --git a/src/addons/mod/glossary/components/index/index.ts b/src/addons/mod/glossary/components/index/index.ts index 6b0acca1c..b7a797451 100644 --- a/src/addons/mod/glossary/components/index/index.ts +++ b/src/addons/mod/glossary/components/index/index.ts @@ -42,12 +42,14 @@ import { AddonModGlossaryEntryWithCategory, AddonModGlossaryGlossary, AddonModGlossaryProvider, + GLOSSARY_ENTRY_ADDED, + GLOSSARY_ENTRY_DELETED, } from '../../services/glossary'; import { AddonModGlossaryOfflineEntry } from '../../services/glossary-offline'; import { - AddonModGlossaryAutoSyncData, - AddonModGlossarySyncProvider, + AddonModGlossaryAutoSyncedData, AddonModGlossarySyncResult, + GLOSSARY_AUTO_SYNCED, } from '../../services/glossary-sync'; import { AddonModGlossaryModuleHandlerService } from '../../services/handlers/module'; import { AddonModGlossaryPrefetchHandler } from '../../services/handlers/prefetch'; @@ -75,13 +77,11 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity protected hasOfflineEntries = false; protected hasOfflineRatings = false; - protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED; - protected addEntryObserver?: CoreEventObserver; + protected syncEventName = GLOSSARY_AUTO_SYNCED; protected fetchedEntriesCanLoadMore = false; protected fetchedEntries: AddonModGlossaryEntry[] = []; protected sourceUnsubscribe?: () => void; - protected ratingOfflineObserver?: CoreEventObserver; - protected ratingSyncObserver?: CoreEventObserver; + protected observers?: CoreEventObserver[]; protected checkCompletionAfterLog = false; // Use CoreListItemsManager log system instead. getDivider?: (entry: AddonModGlossaryEntry) => string; @@ -136,30 +136,41 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity }); // 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); + this.observers = [ + CoreEvents.on(GLOSSARY_ENTRY_ADDED, ({ glossaryId }) => { + if (this.glossary?.id !== glossaryId) { + return; + } // Check completion since it could be configured to complete once the user adds a new entry. this.checkCompletion(); - } - }); + + this.showLoadingAndRefresh(false); + }), + CoreEvents.on(GLOSSARY_ENTRY_DELETED, ({ glossaryId }) => { + if (this.glossary?.id !== glossaryId) { + return; + } + + this.showLoadingAndRefresh(false); + }), + ]; // Listen for offline ratings saved and synced. - this.ratingOfflineObserver = CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => { + this.observers.push(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.hasOffline = true; } - }); - this.ratingSyncObserver = CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => { + })); + this.observers.push(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; this.hasOffline = this.hasOfflineEntries; } - }); + })); } /** @@ -227,7 +238,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity * @param syncEventData Data receiven on sync observer. * @returns True if refresh is needed, false otherwise. */ - protected isRefreshSyncNeeded(syncEventData: AddonModGlossaryAutoSyncData): boolean { + protected isRefreshSyncNeeded(syncEventData: AddonModGlossaryAutoSyncedData): boolean { return !!this.glossary && syncEventData.glossaryId == this.glossary.id && syncEventData.userId == CoreSites.getCurrentSiteUserId(); } @@ -410,9 +421,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity ngOnDestroy(): void { super.ngOnDestroy(); - this.addEntryObserver?.off(); - this.ratingOfflineObserver?.off(); - this.ratingSyncObserver?.off(); + this.observers?.forEach(observer => observer.off()); this.sourceUnsubscribe?.call(null); this.entries?.destroy(); } diff --git a/src/addons/mod/glossary/lang.json b/src/addons/mod/glossary/lang.json index ba4329f33..6206a6430 100644 --- a/src/addons/mod/glossary/lang.json +++ b/src/addons/mod/glossary/lang.json @@ -1,6 +1,7 @@ { "addentry": "Add a new entry", "aliases": "Keyword(s)", + "areyousuredelete": "Are you sure you want to delete this entry?", "attachment": "Attachment", "browsemode": "Browse entries", "byalphabet": "Alphabetically", @@ -14,10 +15,13 @@ "categories": "Categories", "concept": "Concept", "definition": "Definition", + "deleteentry": "Delete entry", "entriestobesynced": "Entries to be synced", + "entrydeleted": "Entry deleted", "entrypendingapproval": "This entry is pending approval.", "entryusedynalink": "This entry should be automatically linked", "errconceptalreadyexists": "This concept already exists. No duplicates allowed in this glossary.", + "errordeleting": "Error deleting entry.", "errorloadingentries": "An error occurred while loading entries.", "errorloadingentry": "An error occurred while loading the entry.", "errorloadingglossary": "An error occurred while loading the glossary.", diff --git a/src/addons/mod/glossary/pages/entry/entry.html b/src/addons/mod/glossary/pages/entry/entry.html index ee112af6e..28c2b60b1 100644 --- a/src/addons/mod/glossary/pages/entry/entry.html +++ b/src/addons/mod/glossary/pages/entry/entry.html @@ -46,6 +46,13 @@ + +
+ + + +
+
diff --git a/src/addons/mod/glossary/pages/entry/entry.ts b/src/addons/mod/glossary/pages/entry/entry.ts index f4ee71044..1db3fc59e 100644 --- a/src/addons/mod/glossary/pages/entry/entry.ts +++ b/src/addons/mod/glossary/pages/entry/entry.ts @@ -12,17 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core'; import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments'; import { CoreComments } from '@features/comments/services/comments'; import { CoreRatingInfo } from '@features/rating/services/rating'; import { CoreTag } from '@features/tag/services/tag'; import { IonRefresher } from '@ionic/angular'; import { CoreNavigator } from '@services/navigator'; -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreNetwork } from '@services/network'; +import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source'; import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager'; import { @@ -53,13 +56,14 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { showDate = false; ratingInfo?: CoreRatingInfo; tagsEnabled = false; + canDelete = false; commentsEnabled = false; courseId!: number; cmId?: number; protected entryId!: number; - constructor(protected route: ActivatedRoute) {} + constructor(@Optional() protected splitView: CoreSplitViewComponent, protected route: ActivatedRoute) {} /** * @inheritdoc @@ -113,6 +117,46 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { this.entries?.destroy(); } + /** + * Delete entry. + */ + async deleteEntry(): Promise { + const entryId = this.entry?.id; + const glossaryId = this.glossary?.id; + const cancelled = await CoreUtils.promiseFails( + CoreDomUtils.showConfirm(Translate.instant('addon.mod_glossary.areyousuredelete')), + ); + + if (!entryId || !glossaryId || cancelled) { + return; + } + + const modal = await CoreDomUtils.showModalLoading(); + + try { + await AddonModGlossary.deleteEntry(glossaryId, entryId); + await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(entryId)); + await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByLetter(glossaryId)); + await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByAuthor(glossaryId)); + await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByCategory(glossaryId)); + await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'CREATION')); + await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'UPDATE')); + await CoreUtils.ignoreErrors(this.entries?.getSource().invalidateCache(false)); + + CoreDomUtils.showToast('addon.mod_glossary.entrydeleted', true, ToastDuration.LONG); + + if (this.splitView?.outletActivated) { + await CoreNavigator.navigate('../'); + } else { + await CoreNavigator.back(); + } + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errordeleting', true); + } finally { + modal.dismiss(); + } + } + /** * Refresh the data. * @@ -142,9 +186,11 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { protected async fetchEntry(): Promise { try { const result = await AddonModGlossary.getEntry(this.entryId); + const canDeleteEntries = CoreNetwork.isOnline() && await AddonModGlossary.canDeleteEntries(); this.entry = result.entry; this.ratingInfo = result.ratinginfo; + this.canDelete = canDeleteEntries && !!result.permissions?.candelete; if (this.glossary) { // Glossary already loaded, nothing else to load. diff --git a/src/addons/mod/glossary/services/glossary-offline.ts b/src/addons/mod/glossary/services/glossary-offline.ts index a0dadeee1..b8df14b00 100644 --- a/src/addons/mod/glossary/services/glossary-offline.ts +++ b/src/addons/mod/glossary/services/glossary-offline.ts @@ -19,9 +19,10 @@ import { CoreSites } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; +import { CoreEvents } from '@singletons/events'; import { CorePath } from '@singletons/path'; import { AddonModGlossaryOfflineEntryDBRecord, OFFLINE_ENTRIES_TABLE_NAME } from './database/glossary'; -import { AddonModGlossaryDiscardedEntry, AddonModGlossaryEntryOption } from './glossary'; +import { AddonModGlossaryEntryOption, GLOSSARY_ENTRY_ADDED } from './glossary'; /** * Service to handle offline glossary. @@ -159,7 +160,7 @@ export class AddonModGlossaryOfflineProvider { * @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 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. @@ -172,12 +173,13 @@ export class AddonModGlossaryOfflineProvider { courseId: number, options?: Record, attachments?: CoreFileUploaderStoreFilesResult, - timeCreated?: number, + timecreated?: number, siteId?: string, userId?: number, discardEntry?: AddonModGlossaryDiscardedEntry, ): Promise { const site = await CoreSites.getSite(siteId); + timecreated = timecreated || Date.now(); const entry: AddonModGlossaryOfflineEntryDBRecord = { glossaryid: glossaryId, @@ -188,7 +190,7 @@ export class AddonModGlossaryOfflineProvider { options: JSON.stringify(options || {}), attachments: JSON.stringify(attachments), userid: userId || site.getUserId(), - timecreated: timeCreated || Date.now(), + timecreated, }; // If editing an offline entry, delete previous first. @@ -198,6 +200,8 @@ export class AddonModGlossaryOfflineProvider { await site.getDb().insertRecord(OFFLINE_ENTRIES_TABLE_NAME, entry); + CoreEvents.trigger(GLOSSARY_ENTRY_ADDED, { glossaryId, timecreated }, siteId); + return false; } diff --git a/src/addons/mod/glossary/services/glossary-sync.ts b/src/addons/mod/glossary/services/glossary-sync.ts index 0fc65ad13..f404366ae 100644 --- a/src/addons/mod/glossary/services/glossary-sync.ts +++ b/src/addons/mod/glossary/services/glossary-sync.ts @@ -31,14 +31,14 @@ import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from './glossar import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; import { CoreFileEntry } from '@services/file-helper'; +export const GLOSSARY_AUTO_SYNCED = 'addon_mod_glossary_auto_synced'; + /** * 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() { @@ -98,7 +98,7 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv if (result?.updated) { // Sync successful, send event. - CoreEvents.trigger(AddonModGlossarySyncProvider.AUTO_SYNCED, { + CoreEvents.trigger(GLOSSARY_AUTO_SYNCED, { glossaryId: entry.glossaryid, userId: entry.userid, warnings: result.warnings, @@ -341,15 +341,28 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv export const AddonModGlossarySync = makeSingleton(AddonModGlossarySyncProvider); +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 { + [GLOSSARY_AUTO_SYNCED]: AddonModGlossaryAutoSyncedData; + } + +} + /** * Data returned by a glossary sync. */ export type AddonModGlossarySyncResult = CoreSyncResult; /** - * Data passed to AUTO_SYNCED event. + * Data passed to GLOSSARY_AUTO_SYNCED event. */ -export type AddonModGlossaryAutoSyncData = { +export type AddonModGlossaryAutoSyncedData = { glossaryId: number; userId: number; warnings: string[]; diff --git a/src/addons/mod/glossary/services/glossary.ts b/src/addons/mod/glossary/services/glossary.ts index 831b9876c..3f37c1d27 100644 --- a/src/addons/mod/glossary/services/glossary.ts +++ b/src/addons/mod/glossary/services/glossary.ts @@ -25,11 +25,14 @@ import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@ import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; +import { CoreEvents } from '@singletons/events'; import { AddonModGlossaryEntryDBRecord, ENTRIES_TABLE_NAME } from './database/glossary'; import { AddonModGlossaryOffline } from './glossary-offline'; -import { AddonModGlossaryAutoSyncData, AddonModGlossarySyncProvider } from './glossary-sync'; import { CoreFileEntry } from '@services/file-helper'; +export const GLOSSARY_ENTRY_ADDED = 'addon_mod_glossary_entry_added'; +export const GLOSSARY_ENTRY_DELETED = 'addon_mod_glossary_entry_deleted'; + /** * Service that provides some features for glossaries. */ @@ -40,8 +43,6 @@ export class AddonModGlossaryProvider { static readonly LIMIT_ENTRIES = 25; static readonly LIMIT_CATEGORIES = 10; - static readonly ADD_ENTRY_EVENT = 'addon_mod_glossary_add_entry'; - private static readonly SHOW_ALL_CATEGORIES = 0; private static readonly ROOT_CACHE_KEY = 'mmaModGlossary:'; @@ -606,6 +607,18 @@ export class AddonModGlossaryProvider { throw new CoreError('Entry not found.'); } + /** + * Check whether the site can delete glossary entries. + * + * @param siteId Site id. + * @returns Whether the site can delete entries. + */ + async canDeleteEntries(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.wsAvailable('mod_glossary_delete_entry'); + } + /** * Performs the whole fetch of the entries using the proper function and arguments. * @@ -847,7 +860,7 @@ export class AddonModGlossaryProvider { try { // Try to add it in online. - return await this.addEntryOnline( + const entryId = await this.addEntryOnline( glossaryId, concept, definition, @@ -855,6 +868,8 @@ export class AddonModGlossaryProvider { attachments, otherOptions.siteId, ); + + return entryId; } catch (error) { if (otherOptions.allowOffline && !CoreUtils.isWebServiceError(error)) { // Couldn't connect to server, store in offline. @@ -904,9 +919,25 @@ export class AddonModGlossaryProvider { const response = await site.write('mod_glossary_add_entry', params); + CoreEvents.trigger(GLOSSARY_ENTRY_ADDED, { glossaryId, entryId: response.entryid }, siteId); + return response.entryid; } + /** + * Delete entry. + * + * @param glossaryId Glossary id. + * @param entryId Entry id. + */ + async deleteEntry(glossaryId: number, entryId: number): Promise { + const site = CoreSites.getRequiredCurrentSite(); + + await site.write('mod_glossary_delete_entry', { entryid: entryId }); + + CoreEvents.trigger(GLOSSARY_ENTRY_DELETED, { glossaryId, entryId }); + } + /** * Check if a entry concept is already used. * @@ -1040,18 +1071,27 @@ declare module '@singletons/events' { * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation */ export interface CoreEventsData { - [AddonModGlossaryProvider.ADD_ENTRY_EVENT]: AddonModGlossaryAddEntryEventData; - [AddonModGlossarySyncProvider.AUTO_SYNCED]: AddonModGlossaryAutoSyncData; + [GLOSSARY_ENTRY_ADDED]: AddonModGlossaryEntryAddedEventData; + [GLOSSARY_ENTRY_DELETED]: AddonModGlossaryEntryDeletedEventData; } } /** - * Data passed to ADD_ENTRY_EVENT. + * GLOSSARY_ENTRY_ADDED event payload. */ -export type AddonModGlossaryAddEntryEventData = { +export type AddonModGlossaryEntryAddedEventData = { glossaryId: number; entryId?: number; + timecreated?: number; +}; + +/** + * GLOSSARY_ENTRY_DELETED event payload. + */ +export type AddonModGlossaryEntryDeletedEventData = { + glossaryId: number; + entryId: number; }; /** diff --git a/src/addons/mod/glossary/tests/behat/basic_usage.feature b/src/addons/mod/glossary/tests/behat/basic_usage.feature index 6a32f476c..48bea05b8 100644 --- a/src/addons/mod/glossary/tests/behat/basic_usage.feature +++ b/src/addons/mod/glossary/tests/behat/basic_usage.feature @@ -154,6 +154,54 @@ Feature: Test basic usage of glossary in app Then I should find "Garlic" in the app And I should find "Allium sativum" in the app + @noeldebug + Scenario: Edit entries (basic info) + Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app + + # TODO online + + # Offline + When I press "Add a new entry" in the app + And I switch network connection to offline + And I set the following fields to these values in the app: + | Concept | Broccoli | + | Definition | Brassica oleracea var. italica | + And I press "Save" in the app + Then I should find "Potato" in the app + And I should find "Broccoli" in the app + + When I press "Broccoli" in the app + Then I should find "Brassica oleracea var. italica" in the app + + When I press "Edit entry" in the app + Then the field "Concept" matches value "Broccoli" in the app + And the field "Definition" matches value "Brassica oleracea var. italica" in the app + + When I set the following fields to these values in the app: + | Concept | Pickle | + | Definition | Pickle Rick | + And I press "Save" in the app + Then I should find "Pickle Rick" in the app + But I should not find "Brassica oleracea var. italica" in the app + + When I press the back button in the app + Then I should find "Pickle" in the app + And I should find "Potato" in the app + But I should not find "Broccoli" in the app + + # TODO test attachments? (yes, in all scenarios!!) + # TODO And I upload "stub.txt" to "File" ".action-sheet-button" in the app + + Scenario: Delete entries + Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app + + When I press "Cucumber" in the app + And I press "Delete entry" in the app + And I press "OK" near "Are you sure you want to delete this entry?" in the app + Then I should find "Entry deleted" in the app + And I should find "Potato" in the app + But I should not find "Cucumber" in the app + Scenario: Sync Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app And I press "Add a new entry" in the app diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 395afc37e..39ef612e6 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -1772,9 +1772,9 @@ export class CoreUtilsProvider { * @param fallback Value to return if the promise is rejected. * @returns Promise with ignored errors, resolving to the fallback result if provided. */ - async ignoreErrors(promise: Promise): Promise; + async ignoreErrors(promise?: Promise): Promise; async ignoreErrors(promise: Promise, fallback: Fallback): Promise; - async ignoreErrors(promise: Promise, fallback?: Fallback): Promise { + async ignoreErrors(promise?: Promise, fallback?: Fallback): Promise { try { const result = await promise; From 39a6d67c25773ff9f1688ab69f110524d21e1d4b Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 22 Mar 2023 15:41:10 +0100 Subject: [PATCH 03/10] MOBILE-2652 glossary: Clean up edit form --- .../classes/glossary-entries-source.ts | 2 +- src/addons/mod/glossary/pages/edit/edit.html | 16 +- src/addons/mod/glossary/pages/edit/edit.ts | 166 +++++++++++------- .../mod/glossary/services/glossary-helper.ts | 26 --- .../mod/glossary/services/glossary-offline.ts | 24 +-- .../mod/glossary/services/glossary-sync.ts | 16 +- src/addons/mod/glossary/services/glossary.ts | 21 +-- upgrade.txt | 2 +- 8 files changed, 132 insertions(+), 141 deletions(-) diff --git a/src/addons/mod/glossary/classes/glossary-entries-source.ts b/src/addons/mod/glossary/classes/glossary-entries-source.ts index 3e0dc06c6..057856e55 100644 --- a/src/addons/mod/glossary/classes/glossary-entries-source.ts +++ b/src/addons/mod/glossary/classes/glossary-entries-source.ts @@ -259,7 +259,7 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource< const entries: AddonModGlossaryEntryItem[] = []; if (page === 0) { - const offlineEntries = await AddonModGlossaryOffline.getGlossaryNewEntries(glossary.id); + const offlineEntries = await AddonModGlossaryOffline.getGlossaryOfflineEntries(glossary.id); offlineEntries.sort((a, b) => a.concept.localeCompare(b.concept)); diff --git a/src/addons/mod/glossary/pages/edit/edit.html b/src/addons/mod/glossary/pages/edit/edit.html index 8850c93d3..835718d27 100644 --- a/src/addons/mod/glossary/pages/edit/edit.html +++ b/src/addons/mod/glossary/pages/edit/edit.html @@ -16,7 +16,7 @@
{{ 'addon.mod_glossary.concept' | translate }} - + @@ -31,7 +31,7 @@ {{ 'addon.mod_glossary.categories' | translate }} - @@ -43,7 +43,7 @@ {{ 'addon.mod_glossary.aliases' | translate }} - + @@ -51,7 +51,7 @@

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

- @@ -62,19 +62,19 @@ {{ '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.ts b/src/addons/mod/glossary/pages/edit/edit.ts index 334672856..a3bc5bfb7 100644 --- a/src/addons/mod/glossary/pages/edit/edit.ts +++ b/src/addons/mod/glossary/pages/edit/edit.ts @@ -20,7 +20,7 @@ import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/ 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 { CoreFileEntry } from '@services/file-helper'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; @@ -36,8 +36,6 @@ import { AddonModGlossaryCategory, AddonModGlossaryEntryOption, AddonModGlossaryGlossary, - AddonModGlossaryNewEntry, - AddonModGlossaryNewEntryWithFiles, AddonModGlossaryProvider, } from '../../services/glossary'; import { AddonModGlossaryHelper } from '../../services/glossary-helper'; @@ -59,32 +57,29 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { courseId!: number; loaded = false; glossary?: AddonModGlossaryGlossary; - attachments: FileEntry[] = []; definitionControl = new FormControl(); categories: AddonModGlossaryCategory[] = []; editorExtraParams: Record = {}; - entry: AddonModGlossaryNewEntry = { + data: AddonModGlossaryFormData = { concept: '', definition: '', timecreated: 0, - }; - - entries?: AddonModGlossaryEditEntriesSwipeManager; - - options = { - categories: [], + attachments: [], + categories: [], aliases: '', usedynalink: false, casesensitive: false, fullmatch: false, }; + entries?: AddonModGlossaryEditEntriesSwipeManager; + protected timecreated!: number; protected concept = ''; protected syncId?: string; protected syncObserver?: CoreEventObserver; protected isDestroyed = false; - protected originalData?: AddonModGlossaryNewEntryWithFiles; + protected originalData?: AddonModGlossaryFormData; protected saved = false; constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {} @@ -164,54 +159,64 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { return; } - const entry = await AddonModGlossaryOffline.getNewEntry(this.glossary.id, this.concept, this.timecreated); + const entry = await AddonModGlossaryOffline.getOfflineEntry(this.glossary.id, this.concept, this.timecreated); - this.entry.concept = entry.concept || ''; - this.entry.definition = entry.definition || ''; - this.entry.timecreated = entry.timecreated; + this.data.concept = entry.concept || ''; + this.data.definition = entry.definition || ''; + this.data.timecreated = entry.timecreated; this.originalData = { - concept: this.entry.concept, - definition: this.entry.definition, - files: [], + concept: this.data.concept, + definition: this.data.definition, + attachments: this.data.attachments.slice(), timecreated: entry.timecreated, + categories: this.data.categories.slice(), + aliases: this.data.aliases, + usedynalink: this.data.usedynalink, + casesensitive: this.data.casesensitive, + fullmatch: this.data.fullmatch, }; 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; + this.data.categories = (entry.options.categories && ( entry.options.categories).split(',')) || []; + this.data.aliases = entry.options.aliases || ''; + this.data.usedynalink = !!entry.options.usedynalink; + + if (this.data.usedynalink) { + this.data.casesensitive = !!entry.options.casesensitive; + this.data.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.data.attachments = await AddonModGlossaryHelper.getStoredFiles( + this.glossary.id, + entry.concept, + entry.timecreated, + ); - this.originalData.files = this.attachments.slice(); + this.originalData.attachments = this.data.attachments.slice(); } - this.definitionControl.setValue(this.entry.definition); + this.definitionControl.setValue(this.data.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.data.concept = ''; + this.data.definition = ''; + this.data.timecreated = 0; + this.data.categories = []; + this.data.aliases = ''; + this.data.usedynalink = false; + this.data.casesensitive = false; + this.data.fullmatch = false; + this.data.attachments.length = 0; // Empty the array. this.definitionControl.setValue(''); } @@ -222,7 +227,7 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { * @param text The new text. */ onDefinitionChange(text: string): void { - this.entry.definition = text; + this.data.definition = text; } /** @@ -235,13 +240,13 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { return true; } - if (AddonModGlossaryHelper.hasEntryDataChanged(this.entry, this.attachments, this.originalData)) { + if (this.hasDataChanged()) { // Show confirmation if some data has been modified. await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit')); } // Delete the local files from the tmp folder. - CoreFileUploader.clearTmpFiles(this.attachments); + CoreFileUploader.clearTmpFiles(this.data.attachments); CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId()); @@ -252,11 +257,11 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { * Save the entry. */ async save(): Promise { - let definition = this.entry.definition; + let definition = this.data.definition; let entryId: number | undefined; - const timecreated = this.entry.timecreated || Date.now(); + const timecreated = this.data.timecreated || Date.now(); - if (!this.entry.concept || !definition) { + if (!this.data.concept || !definition) { CoreDomUtils.showErrorModal('addon.mod_glossary.fillfields', true); return; @@ -274,23 +279,23 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated); const options: Record = { - aliases: this.options.aliases, - categories: this.options.categories.join(','), + aliases: this.data.aliases, + categories: this.data.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; + options.usedynalink = this.data.usedynalink ? 1 : 0; + if (this.data.usedynalink) { + options.casesensitive = this.data.casesensitive ? 1 : 0; + options.fullmatch = this.data.fullmatch ? 1 : 0; } } if (saveOffline) { - if (this.entry && !this.glossary.allowduplicatedentries) { + if (this.data && !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, + const isUsed = await AddonModGlossary.isConceptUsed(this.glossary.id, this.data.concept, { + timeCreated: this.data.timecreated, cmId: this.cmId, }); @@ -301,9 +306,9 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { } // Save entry in offline. - await AddonModGlossaryOffline.addNewEntry( + await AddonModGlossaryOffline.addOfflineEntry( this.glossary.id, - this.entry.concept, + this.data.concept, definition, this.courseId, options, @@ -311,33 +316,33 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { timecreated, undefined, undefined, - this.entry, + this.data, ); } 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, + this.data.concept, definition, this.courseId, options, attachmentsResult, { timeCreated: timecreated, - discardEntry: this.entry, - allowOffline: !this.attachments.length, + discardEntry: this.data, + allowOffline: !this.data.attachments.length, checkDuplicates: !this.glossary.allowduplicatedentries, }, ); } // Delete the local files from the tmp folder. - CoreFileUploader.clearTmpFiles(this.attachments); + CoreFileUploader.clearTmpFiles(this.data.attachments); if (entryId) { // Data sent to server, delete stored files (if any). - AddonModGlossaryHelper.deleteStoredFiles(this.glossary.id, this.entry.concept, timecreated); + AddonModGlossaryHelper.deleteStoredFiles(this.glossary.id, this.data.concept, timecreated); CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' }); } @@ -367,6 +372,24 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { } } + /** + * Check if the form data has changed. + * + * @returns True if data has changed, false otherwise. + */ + protected hasDataChanged(): boolean { + if (!this.originalData || this.originalData.concept === undefined) { + // There is no original data. + return !!(this.data.definition || this.data.concept || this.data.attachments.length > 0); + } + + if (this.originalData.definition != this.data.definition || this.originalData.concept != this.data.concept) { + return true; + } + + return CoreFileUploader.areFileListDifferent(this.data.attachments, this.originalData.attachments); + } + /** * Upload entry attachments if any. * @@ -376,7 +399,7 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { protected async uploadAttachments( timecreated: number, ): Promise<{saveOffline: boolean; attachmentsResult?: number | CoreFileUploaderStoreFilesResult}> { - if (!this.attachments.length || !this.glossary) { + if (!this.data.attachments.length || !this.glossary) { return { saveOffline: false, }; @@ -384,7 +407,7 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { try { const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles( - this.attachments, + this.data.attachments, AddonModGlossaryProvider.COMPONENT, this.glossary.id, ); @@ -401,9 +424,9 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { // Cannot upload them in online, save them in offline. const attachmentsResult = await AddonModGlossaryHelper.storeFiles( this.glossary.id, - this.entry.concept, + this.data.concept, timecreated, - this.attachments, + this.data.attachments, ); return { @@ -439,3 +462,18 @@ class AddonModGlossaryEditEntriesSwipeManager extends AddonModGlossaryEntriesSwi } } + +/** + * Form data. + */ +type AddonModGlossaryFormData = { + concept: string; + definition: string; + timecreated: number; + attachments: CoreFileEntry[]; + categories: string[]; + aliases: string; + usedynalink: boolean; + casesensitive: boolean; + fullmatch: boolean; +}; diff --git a/src/addons/mod/glossary/services/glossary-helper.ts b/src/addons/mod/glossary/services/glossary-helper.ts index d51c457d2..46e4e25ca 100644 --- a/src/addons/mod/glossary/services/glossary-helper.ts +++ b/src/addons/mod/glossary/services/glossary-helper.ts @@ -18,7 +18,6 @@ import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fi 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 { CoreFileEntry } from '@services/file-helper'; @@ -58,31 +57,6 @@ export class AddonModGlossaryHelperProvider { 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. - * @returns True if data has changed, false otherwise. - */ - hasEntryDataChanged( - entry: AddonModGlossaryNewEntry, - files: CoreFileEntry[], - original?: AddonModGlossaryNewEntryWithFiles, - ): boolean { - if (!original || 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. diff --git a/src/addons/mod/glossary/services/glossary-offline.ts b/src/addons/mod/glossary/services/glossary-offline.ts index b8df14b00..3c748248f 100644 --- a/src/addons/mod/glossary/services/glossary-offline.ts +++ b/src/addons/mod/glossary/services/glossary-offline.ts @@ -31,7 +31,7 @@ import { AddonModGlossaryEntryOption, GLOSSARY_ENTRY_ADDED } from './glossary'; export class AddonModGlossaryOfflineProvider { /** - * Delete a new entry. + * Delete an offline entry. * * @param glossaryId Glossary ID. * @param concept Glossary entry concept. @@ -39,7 +39,7 @@ export class AddonModGlossaryOfflineProvider { * @param siteId Site ID. If not defined, current site. * @returns Promise resolved if deleted, rejected if failure. */ - async deleteNewEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise { + async deleteOfflineEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const conditions: Partial = { @@ -52,12 +52,12 @@ export class AddonModGlossaryOfflineProvider { } /** - * Get all the stored new entries from all the glossaries. + * Get all the stored offline entries from all the glossaries. * * @param siteId Site ID. If not defined, current site. * @returns Promise resolved with entries. */ - async getAllNewEntries(siteId?: string): Promise { + async getAllOfflineEntries(siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const records = await site.getDb().getRecords(OFFLINE_ENTRIES_TABLE_NAME); @@ -66,7 +66,7 @@ export class AddonModGlossaryOfflineProvider { } /** - * Get a stored new entry. + * Get a stored offline entry. * * @param glossaryId Glossary ID. * @param concept Glossary entry concept. @@ -74,7 +74,7 @@ export class AddonModGlossaryOfflineProvider { * @param siteId Site ID. If not defined, current site. * @returns Promise resolved with entry. */ - async getNewEntry( + async getOfflineEntry( glossaryId: number, concept: string, timeCreated: number, @@ -101,7 +101,7 @@ export class AddonModGlossaryOfflineProvider { * @param userId User the entries belong to. If not defined, current user in site. * @returns Promise resolved with entries. */ - async getGlossaryNewEntries(glossaryId: number, siteId?: string, userId?: number): Promise { + async getGlossaryOfflineEntries(glossaryId: number, siteId?: string, userId?: number): Promise { const site = await CoreSites.getSite(siteId); const conditions: Partial = { @@ -144,7 +144,7 @@ export class AddonModGlossaryOfflineProvider { } // If there's only one entry, check that is not the one we are editing. - return CoreUtils.promiseFails(this.getNewEntry(glossaryId, concept, timeCreated, siteId)); + return CoreUtils.promiseFails(this.getOfflineEntry(glossaryId, concept, timeCreated, siteId)); } catch { // No offline data found, return false. return false; @@ -152,7 +152,7 @@ export class AddonModGlossaryOfflineProvider { } /** - * Save a new entry to be sent later. + * Save an offline entry to be sent later. * * @param glossaryId Glossary ID. * @param concept Glossary entry concept. @@ -166,7 +166,7 @@ export class AddonModGlossaryOfflineProvider { * @param discardEntry The entry provided will be discarded if found. * @returns Promise resolved if stored, rejected if failure. */ - async addNewEntry( + async addOfflineEntry( glossaryId: number, concept: string, definition: string, @@ -195,7 +195,7 @@ export class AddonModGlossaryOfflineProvider { // If editing an offline entry, delete previous first. if (discardEntry) { - await this.deleteNewEntry(glossaryId, discardEntry.concept, discardEntry.timecreated, site.getId()); + await this.deleteOfflineEntry(glossaryId, discardEntry.concept, discardEntry.timecreated, site.getId()); } await site.getDb().insertRecord(OFFLINE_ENTRIES_TABLE_NAME, entry); @@ -222,7 +222,7 @@ export class AddonModGlossaryOfflineProvider { } /** - * Get the path to the folder where to store files for a new offline entry. + * Get the path to the folder where to store files for an offline entry. * * @param glossaryId Glossary ID. * @param concept The name of the entry. diff --git a/src/addons/mod/glossary/services/glossary-sync.ts b/src/addons/mod/glossary/services/glossary-sync.ts index f404366ae..62e04f3c9 100644 --- a/src/addons/mod/glossary/services/glossary-sync.ts +++ b/src/addons/mod/glossary/services/glossary-sync.ts @@ -50,10 +50,9 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv * * @param siteId Site ID to sync. If not defined, sync all sites. * @param force Wether to force sync not depending on last execution. - * @returns Promise resolved if sync is successful, rejected if sync fails. */ - syncAllGlossaries(siteId?: string, force?: boolean): Promise { - return this.syncOnSites('all glossaries', (siteId) => this.syncAllGlossariesFunc(!!force, siteId), siteId); + async syncAllGlossaries(siteId?: string, force?: boolean): Promise { + await this.syncOnSites('all glossaries', (siteId) => this.syncAllGlossariesFunc(!!force, siteId), siteId); } /** @@ -61,7 +60,6 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv * * @param force Wether to force sync not depending on last execution. * @param siteId Site ID to sync. - * @returns Promise resolved if sync is successful, rejected if sync fails. */ protected async syncAllGlossariesFunc(force: boolean, siteId: string): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); @@ -73,14 +71,13 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv } /** - * Sync entried of all glossaries on a site. + * Sync entries of all glossaries on a site. * * @param force Wether to force sync not depending on last execution. * @param siteId Site ID to sync. - * @returns Promise resolved if sync is successful, rejected if sync fails. */ protected async syncAllGlossariesEntries(force: boolean, siteId: string): Promise { - const entries = await AddonModGlossaryOffline.getAllNewEntries(siteId); + const entries = await AddonModGlossaryOffline.getAllOfflineEntries(siteId); // Do not sync same glossary twice. const treated: Record = {}; @@ -180,7 +177,7 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv // Get offline responses to be sent. const entries = await CoreUtils.ignoreErrors( - AddonModGlossaryOffline.getGlossaryNewEntries(glossaryId, siteId, userId), + AddonModGlossaryOffline.getGlossaryOfflineEntries(glossaryId, siteId, userId), [], ); @@ -285,11 +282,10 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv * @param concept Glossary entry concept. * @param timeCreated Time to allow duplicated entries. * @param siteId Site ID. If not defined, current site. - * @returns 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), + AddonModGlossaryOffline.deleteOfflineEntry(glossaryId, concept, timeCreated, siteId), AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timeCreated, siteId), ]); } diff --git a/src/addons/mod/glossary/services/glossary.ts b/src/addons/mod/glossary/services/glossary.ts index 3f37c1d27..caa61c3f6 100644 --- a/src/addons/mod/glossary/services/glossary.ts +++ b/src/addons/mod/glossary/services/glossary.ts @@ -28,7 +28,6 @@ import { makeSingleton, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { AddonModGlossaryEntryDBRecord, ENTRIES_TABLE_NAME } from './database/glossary'; import { AddonModGlossaryOffline } from './glossary-offline'; -import { CoreFileEntry } from '@services/file-helper'; export const GLOSSARY_ENTRY_ADDED = 'addon_mod_glossary_entry_added'; export const GLOSSARY_ENTRY_DELETED = 'addon_mod_glossary_entry_deleted'; @@ -827,7 +826,7 @@ export class AddonModGlossaryProvider { throw new CoreError('Error adding entry.'); } - await AddonModGlossaryOffline.addNewEntry( + await AddonModGlossaryOffline.addOfflineEntry( glossaryId, concept, definition, @@ -850,7 +849,7 @@ export class AddonModGlossaryProvider { // If we are editing an offline entry, discard previous first. if (otherOptions.discardEntry) { - await AddonModGlossaryOffline.deleteNewEntry( + await AddonModGlossaryOffline.deleteOfflineEntry( glossaryId, otherOptions.discardEntry.concept, otherOptions.discardEntry.timecreated, @@ -1377,22 +1376,6 @@ export type AddonModGlossaryDiscardedEntry = { 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: CoreFileEntry[]; -}; - /** * Options to pass to the different get entries functions. */ diff --git a/upgrade.txt b/upgrade.txt index 5fe38986f..bf8043196 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -6,7 +6,7 @@ information provided here is intended especially for developers. - CoreIconComponent has been removed after deprecation period: Use CoreFaIconDirective instead. - The courseSummaryComponent property has been removed from the CoreCourseFormatComponent component, and the getCourseSummaryComponent method from the CoreCourseFormatHandler interface. - Font Awesome icon library has been updated to 6.3.0. -- Some methods in AddonModGlossaryProvider have changed their signatures to remove unused parameters. +- Some methods in glossary addon services have changed. === 4.1.0 === From f59cf0a1c54bd0a1f3b6deedd17ce151d6a8b375 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 23 Mar 2023 11:23:39 +0100 Subject: [PATCH 04/10] MOBILE-2652 glossary: Extract form handler --- src/addons/mod/glossary/pages/edit/edit.ts | 396 ++++++++++++--------- 1 file changed, 219 insertions(+), 177 deletions(-) diff --git a/src/addons/mod/glossary/pages/edit/edit.ts b/src/addons/mod/glossary/pages/edit/edit.ts index a3bc5bfb7..7f608c813 100644 --- a/src/addons/mod/glossary/pages/edit/edit.ts +++ b/src/addons/mod/glossary/pages/edit/edit.ts @@ -60,6 +60,7 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { definitionControl = new FormControl(); categories: AddonModGlossaryCategory[] = []; editorExtraParams: Record = {}; + handler!: AddonModGlossaryFormHandler; data: AddonModGlossaryFormData = { concept: '', definition: '', @@ -72,14 +73,12 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { fullmatch: false, }; + originalData?: AddonModGlossaryFormData; entries?: AddonModGlossaryEditEntriesSwipeManager; - protected timecreated!: number; - protected concept = ''; protected syncId?: string; protected syncObserver?: CoreEventObserver; protected isDestroyed = false; - protected originalData?: AddonModGlossaryFormData; protected saved = false; constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {} @@ -90,13 +89,18 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { async ngOnInit(): Promise { try { const routeData = this.route.snapshot.data; + const timecreated = CoreNavigator.getRequiredRouteNumberParam('timecreated'); this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); - this.timecreated = CoreNavigator.getRequiredRouteNumberParam('timecreated'); - this.concept = CoreNavigator.getRouteParam('concept') || ''; - this.editorExtraParams.timecreated = this.timecreated; - if (this.timecreated !== 0 && (routeData.swipeEnabled ?? true)) { + this.editorExtraParams.timecreated = timecreated; + this.handler = new AddonModGlossaryOfflineFormHandler( + this, + timecreated, + CoreNavigator.getRouteParam('concept'), + ); + + if (timecreated !== 0 && (routeData.swipeEnabled ?? true)) { const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( AddonModGlossaryEntriesSource, [this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''], @@ -133,9 +137,7 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { try { this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.cmId); - if (this.timecreated > 0) { - await this.loadOfflineData(); - } + await this.handler.loadData(this.glossary); this.categories = await AddonModGlossary.getAllCategories(this.glossary.id, { cmId: this.cmId, @@ -149,59 +151,6 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { } } - /** - * Load offline data when editing an offline entry. - * - * @returns Promise resolved when done. - */ - protected async loadOfflineData(): Promise { - if (!this.glossary) { - return; - } - - const entry = await AddonModGlossaryOffline.getOfflineEntry(this.glossary.id, this.concept, this.timecreated); - - this.data.concept = entry.concept || ''; - this.data.definition = entry.definition || ''; - this.data.timecreated = entry.timecreated; - - this.originalData = { - concept: this.data.concept, - definition: this.data.definition, - attachments: this.data.attachments.slice(), - timecreated: entry.timecreated, - categories: this.data.categories.slice(), - aliases: this.data.aliases, - usedynalink: this.data.usedynalink, - casesensitive: this.data.casesensitive, - fullmatch: this.data.fullmatch, - }; - - if (entry.options) { - this.data.categories = (entry.options.categories && ( entry.options.categories).split(',')) || []; - this.data.aliases = entry.options.aliases || ''; - this.data.usedynalink = !!entry.options.usedynalink; - - if (this.data.usedynalink) { - this.data.casesensitive = !!entry.options.casesensitive; - this.data.fullmatch = !!entry.options.fullmatch; - } - } - - // Treat offline attachments if any. - if (entry.attachments?.offline) { - this.data.attachments = await AddonModGlossaryHelper.getStoredFiles( - this.glossary.id, - entry.concept, - entry.timecreated, - ); - - this.originalData.attachments = this.data.attachments.slice(); - } - - this.definitionControl.setValue(this.data.definition); - } - /** * Reset the form data. */ @@ -257,114 +206,26 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { * Save the entry. */ async save(): Promise { - let definition = this.data.definition; - let entryId: number | undefined; - const timecreated = this.data.timecreated || Date.now(); - - if (!this.data.concept || !definition) { + if (!this.data.concept || !this.data.definition) { CoreDomUtils.showErrorModal('addon.mod_glossary.fillfields', true); return; } + if (!this.glossary) { + return; + } + const modal = await CoreDomUtils.showModalLoading('core.sending', true); - definition = CoreTextUtils.formatHtmlLines(definition); try { - if (!this.glossary) { - return; - } + const savedOnline = await this.handler.save(this.glossary); - // Upload attachments first if any. - const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated); + this.saved = true; - const options: Record = { - aliases: this.data.aliases, - categories: this.data.categories.join(','), - }; + CoreForms.triggerFormSubmittedEvent(this.formElement, savedOnline, CoreSites.getCurrentSiteId()); - if (this.glossary.usedynalink) { - options.usedynalink = this.data.usedynalink ? 1 : 0; - if (this.data.usedynalink) { - options.casesensitive = this.data.casesensitive ? 1 : 0; - options.fullmatch = this.data.fullmatch ? 1 : 0; - } - } - - if (saveOffline) { - if (this.data && !this.glossary.allowduplicatedentries) { - // Check if the entry is duplicated in online or offline mode. - const isUsed = await AddonModGlossary.isConceptUsed(this.glossary.id, this.data.concept, { - timeCreated: this.data.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.addOfflineEntry( - this.glossary.id, - this.data.concept, - definition, - this.courseId, - options, - attachmentsResult, - timecreated, - undefined, - undefined, - this.data, - ); - } 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.data.concept, - definition, - this.courseId, - options, - attachmentsResult, - { - timeCreated: timecreated, - discardEntry: this.data, - allowOffline: !this.data.attachments.length, - checkDuplicates: !this.glossary.allowduplicatedentries, - }, - ); - } - - // Delete the local files from the tmp folder. - CoreFileUploader.clearTmpFiles(this.data.attachments); - - if (entryId) { - // Data sent to server, delete stored files (if any). - AddonModGlossaryHelper.deleteStoredFiles(this.glossary.id, this.data.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(); - } + this.goBack(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.cannoteditentry', true); } finally { @@ -390,16 +251,55 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { return CoreFileUploader.areFileListDifferent(this.data.attachments, this.originalData.attachments); } + /** + * Helper function to go back. + */ + protected goBack(): void { + if (this.splitView?.outletActivated) { + CoreNavigator.navigate('../../'); + } else { + CoreNavigator.back(); + } + } + +} + +/** + * Helper to manage form data. + */ +abstract class AddonModGlossaryFormHandler { + + constructor(protected page: AddonModGlossaryEditPage) {} + + /** + * Load form data. + * + * @param glossary Glossary. + */ + abstract loadData(glossary: AddonModGlossaryGlossary): Promise; + + /** + * Save form data. + * + * @param glossary Glossary. + * @returns Whether the form was saved online. + */ + abstract save(glossary: AddonModGlossaryGlossary): Promise; + /** * Upload entry attachments if any. * - * @param timecreated Entry's timecreated. - * @returns Promise resolved when done. + * @param timecreated Time when the entry was created. + * @param glossary Glossary. + * @returns Attachements result. */ - protected async uploadAttachments( - timecreated: number, - ): Promise<{saveOffline: boolean; attachmentsResult?: number | CoreFileUploaderStoreFilesResult}> { - if (!this.data.attachments.length || !this.glossary) { + protected async uploadAttachments(timecreated: number, glossary: AddonModGlossaryGlossary): Promise<{ + saveOffline: boolean; + attachmentsResult?: number | CoreFileUploaderStoreFilesResult; + }> { + const data = this.page.data; + + if (!data.attachments.length) { return { saveOffline: false, }; @@ -407,9 +307,9 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { try { const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles( - this.data.attachments, + data.attachments, AddonModGlossaryProvider.COMPONENT, - this.glossary.id, + glossary.id, ); return { @@ -423,10 +323,10 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { // Cannot upload them in online, save them in offline. const attachmentsResult = await AddonModGlossaryHelper.storeFiles( - this.glossary.id, - this.data.concept, + glossary.id, + data.concept, timecreated, - this.data.attachments, + data.attachments, ); return { @@ -436,15 +336,157 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { } } +} + +/** + * Helper to manage offline form data. + */ +class AddonModGlossaryOfflineFormHandler extends AddonModGlossaryFormHandler { + + private timecreated: number; + private concept: string; + + constructor(page: AddonModGlossaryEditPage, timecreated: number, concept: string | undefined) { + super(page); + + this.timecreated = timecreated; + this.concept = concept ?? ''; + } + /** - * Helper function to go back. + * @inheritdoc */ - protected goBack(): void { - if (this.splitView?.outletActivated) { - CoreNavigator.navigate('../../'); - } else { - CoreNavigator.back(); + async loadData(glossary: AddonModGlossaryGlossary): Promise { + if (this.timecreated === 0) { + return; } + + const data = this.page.data; + const entry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, this.concept, this.timecreated); + + data.concept = entry.concept || ''; + data.definition = entry.definition || ''; + data.timecreated = entry.timecreated; + + this.page.originalData = { + concept: data.concept, + definition: data.definition, + attachments: data.attachments.slice(), + timecreated: data.timecreated, + categories: data.categories.slice(), + aliases: data.aliases, + usedynalink: data.usedynalink, + casesensitive: data.casesensitive, + fullmatch: data.fullmatch, + }; + + if (entry.options) { + data.categories = (entry.options.categories && ( entry.options.categories).split(',')) || []; + data.aliases = entry.options.aliases || ''; + data.usedynalink = !!entry.options.usedynalink; + + if (data.usedynalink) { + data.casesensitive = !!entry.options.casesensitive; + data.fullmatch = !!entry.options.fullmatch; + } + } + + // Treat offline attachments if any. + if (entry.attachments?.offline) { + data.attachments = await AddonModGlossaryHelper.getStoredFiles(glossary.id, entry.concept, entry.timecreated); + + this.page.originalData.attachments = data.attachments.slice(); + } + + this.page.definitionControl.setValue(data.definition); + } + + /** + * @inheritdoc + */ + async save(glossary: AddonModGlossaryGlossary): Promise { + let entryId: number | false = false; + const data = this.page.data; + const timecreated = this.timecreated || Date.now(); + const definition = CoreTextUtils.formatHtmlLines(data.definition); + + // Upload attachments first if any. + const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated, glossary); + + const options: Record = { + aliases: data.aliases, + categories: data.categories.join(','), + }; + + if (glossary.usedynalink) { + options.usedynalink = data.usedynalink ? 1 : 0; + if (data.usedynalink) { + options.casesensitive = data.casesensitive ? 1 : 0; + options.fullmatch = data.fullmatch ? 1 : 0; + } + } + + if (saveOffline) { + if (!glossary.allowduplicatedentries) { + // Check if the entry is duplicated in online or offline mode. + const isUsed = await AddonModGlossary.isConceptUsed(glossary.id, data.concept, { + timeCreated: data.timecreated, + cmId: this.page.cmId, + }); + + if (isUsed) { + // There's a entry with same name, reject with error message. + throw new CoreError(Translate.instant('addon.mod_glossary.errconceptalreadyexists')); + } + } + + // Save entry in offline. + await AddonModGlossaryOffline.addOfflineEntry( + glossary.id, + data.concept, + definition, + this.page.courseId, + options, + attachmentsResult, + timecreated, + undefined, + undefined, + data, + ); + } else { + // Try to send it to server. + // Don't allow offline if there are attachments since they were uploaded fine. + entryId = await AddonModGlossary.addEntry( + glossary.id, + data.concept, + definition, + this.page.courseId, + options, + attachmentsResult, + { + timeCreated: timecreated, + discardEntry: data, + allowOffline: !data.attachments.length, + checkDuplicates: !glossary.allowduplicatedentries, + }, + ); + } + + // Delete the local files from the tmp folder. + CoreFileUploader.clearTmpFiles(data.attachments); + + if (entryId) { + // Data sent to server, delete stored files (if any). + AddonModGlossaryHelper.deleteStoredFiles(glossary.id, data.concept, timecreated); + CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' }); + } + + CoreEvents.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, { + glossaryId: glossary.id, + entryId: entryId || undefined, + }, CoreSites.getCurrentSiteId()); + + return !!entryId; } } From ce09ee8a6c0a6800f86046e6ec444135f5417d91 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 28 Mar 2023 13:44:20 +0200 Subject: [PATCH 05/10] MOBILE-2652 glossary: Refactor navigation Instead of showing the form for offline entries, we're showing them as normal entries and the form is only used for creating new entries. Additionally, the form won't be shown as a split view item any longer, it will always open a new page. --- scripts/langindex.json | 1 + .../classes/glossary-entries-source.ts | 30 +- .../classes/glossary-entries-swipe-manager.ts | 31 -- .../index/addon-mod-glossary-index.html | 11 +- .../mod/glossary/components/index/index.scss | 13 + .../mod/glossary/components/index/index.ts | 8 +- .../mod/glossary/glossary-lazy.module.ts | 16 +- src/addons/mod/glossary/glossary.module.ts | 53 +-- src/addons/mod/glossary/lang.json | 1 + src/addons/mod/glossary/pages/edit/edit.html | 2 +- src/addons/mod/glossary/pages/edit/edit.ts | 346 ++++++++---------- .../mod/glossary/pages/entry/entry.html | 43 ++- src/addons/mod/glossary/pages/entry/entry.ts | 181 +++++---- .../glossary/services/handlers/edit-link.ts | 10 +- .../glossary/services/handlers/entry-link.ts | 10 +- .../glossary/tests/behat/navigation.feature | 1 + 16 files changed, 360 insertions(+), 397 deletions(-) delete mode 100644 src/addons/mod/glossary/classes/glossary-entries-swipe-manager.ts create mode 100644 src/addons/mod/glossary/components/index/index.scss diff --git a/scripts/langindex.json b/scripts/langindex.json index 4d1fdd0b8..cfc5ef1f8 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -697,6 +697,7 @@ "addon.mod_glossary.definition": "glossary", "addon.mod_glossary.deleteentry": "glossary", "addon.mod_glossary.entriestobesynced": "local_moodlemobileapp", + "addon.mod_glossary.entry": "glossary", "addon.mod_glossary.entrydeleted": "glossary", "addon.mod_glossary.entrypendingapproval": "local_moodlemobileapp", "addon.mod_glossary.entryusedynalink": "glossary", diff --git a/src/addons/mod/glossary/classes/glossary-entries-source.ts b/src/addons/mod/glossary/classes/glossary-entries-source.ts index 057856e55..cd45af84c 100644 --- a/src/addons/mod/glossary/classes/glossary-entries-source.ts +++ b/src/addons/mod/glossary/classes/glossary-entries-source.ts @@ -29,8 +29,6 @@ import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '../servic */ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource { - static readonly NEW_ENTRY: AddonModGlossaryNewEntryForm = { newEntry: true }; - readonly COURSE_ID: number; readonly CM_ID: number; readonly GLOSSARY_PATH_PREFIX: string; @@ -54,16 +52,6 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource< this.GLOSSARY_PATH_PREFIX = glossaryPathPrefix; } - /** - * Type guard to infer NewEntryForm objects. - * - * @param entry Item to check. - * @returns Whether the item is a new entry form. - */ - isNewEntryForm(entry: AddonModGlossaryEntryItem): entry is AddonModGlossaryNewEntryForm { - return 'newEntry' in entry; - } - /** * Type guard to infer entry objects. * @@ -81,22 +69,18 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource< * @returns Whether the item is an offline entry. */ isOfflineEntry(entry: AddonModGlossaryEntryItem): entry is AddonModGlossaryOfflineEntry { - return !this.isNewEntryForm(entry) && !this.isOnlineEntry(entry); + return !this.isOnlineEntry(entry); } /** * @inheritdoc */ getItemPath(entry: AddonModGlossaryEntryItem): string { - if (this.isOnlineEntry(entry)) { - return `${this.GLOSSARY_PATH_PREFIX}entry/${entry.id}`; - } - if (this.isOfflineEntry(entry)) { - return `${this.GLOSSARY_PATH_PREFIX}edit/${entry.timecreated}`; + return `${this.GLOSSARY_PATH_PREFIX}entry/new-${entry.timecreated}`; } - return `${this.GLOSSARY_PATH_PREFIX}edit/0`; + return `${this.GLOSSARY_PATH_PREFIX}entry/${entry.id}`; } /** @@ -263,7 +247,6 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource< offlineEntries.sort((a, b) => a.concept.localeCompare(b.concept)); - entries.push(AddonModGlossaryEntriesSource.NEW_ENTRY); entries.push(...offlineEntries); } @@ -315,12 +298,7 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource< /** * Type of items that can be held by the entries manager. */ -export type AddonModGlossaryEntryItem = AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | AddonModGlossaryNewEntryForm; - -/** - * Type to select the new entry form. - */ -export type AddonModGlossaryNewEntryForm = { newEntry: true }; +export type AddonModGlossaryEntryItem = AddonModGlossaryEntry | AddonModGlossaryOfflineEntry; /** * Fetch mode to sort entries. diff --git a/src/addons/mod/glossary/classes/glossary-entries-swipe-manager.ts b/src/addons/mod/glossary/classes/glossary-entries-swipe-manager.ts deleted file mode 100644 index b1136068b..000000000 --- a/src/addons/mod/glossary/classes/glossary-entries-swipe-manager.ts +++ /dev/null @@ -1,31 +0,0 @@ -// (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 { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; -import { AddonModGlossaryEntriesSource, AddonModGlossaryEntryItem } from './glossary-entries-source'; - -/** - * Helper to manage swiping within a collection of glossary entries. - */ -export abstract class AddonModGlossaryEntriesSwipeManager - extends CoreSwipeNavigationItemsManager { - - /** - * @inheritdoc - */ - protected skipItemInSwipe(item: AddonModGlossaryEntryItem): boolean { - return this.getSource().isNewEntryForm(item); - } - -} 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 index 2413ed563..1198552e1 100644 --- a/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html +++ b/src/addons/mod/glossary/components/index/addon-mod-glossary-index.html @@ -31,7 +31,7 @@ [componentId]="componentId" [courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()"> - +

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

@@ -40,9 +40,12 @@ - - +
+ + + +
diff --git a/src/addons/mod/glossary/components/index/index.scss b/src/addons/mod/glossary/components/index/index.scss new file mode 100644 index 000000000..96c31cca1 --- /dev/null +++ b/src/addons/mod/glossary/components/index/index.scss @@ -0,0 +1,13 @@ +:host { + + .addon-mod-glossary-index--offline-entries { + border-bottom: 1px solid var(--stroke); + } + + .addon-mod-glossary-index--offline-entry { + display: flex; + justify-content: flex-start; + align-items: center; + } + +} diff --git a/src/addons/mod/glossary/components/index/index.ts b/src/addons/mod/glossary/components/index/index.ts index b7a797451..904f91028 100644 --- a/src/addons/mod/glossary/components/index/index.ts +++ b/src/addons/mod/glossary/components/index/index.ts @@ -26,6 +26,7 @@ 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 { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; @@ -61,6 +62,7 @@ import { AddonModGlossaryModePickerPopoverComponent } from '../mode-picker/mode- @Component({ selector: 'addon-mod-glossary-index', templateUrl: 'addon-mod-glossary-index.html', + styleUrls: ['index.scss'], }) export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, AfterViewInit, OnDestroy { @@ -399,7 +401,11 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity * Opens new entry editor. */ openNewEntry(): void { - this.entries?.select(AddonModGlossaryEntriesSource.NEW_ENTRY); + CoreNavigator.navigate( + this.splitView.outletActivated + ? '../new' + : './entry/new', + ); } /** diff --git a/src/addons/mod/glossary/glossary-lazy.module.ts b/src/addons/mod/glossary/glossary-lazy.module.ts index c96ce6e7d..8f12bc001 100644 --- a/src/addons/mod/glossary/glossary-lazy.module.ts +++ b/src/addons/mod/glossary/glossary-lazy.module.ts @@ -27,13 +27,9 @@ const mobileRoutes: Routes = [ component: AddonModGlossaryIndexPage, }, { - path: ':courseId/:cmId/entry/:entryId', + path: ':courseId/:cmId/entry/:entrySlug', loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule), }, - { - path: ':courseId/:cmId/edit/:timecreated', - loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule), - }, ]; const tabletRoutes: Routes = [ @@ -42,18 +38,18 @@ const tabletRoutes: Routes = [ component: AddonModGlossaryIndexPage, children: [ { - path: 'entry/:entryId', + path: 'entry/:entrySlug', loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule), }, - { - path: 'edit/:timecreated', - loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule), - }, ], }, ]; const routes: Routes = [ + { + path: ':courseId/:cmId/entry/new', + loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule), + }, ...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile), ...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet), ]; diff --git a/src/addons/mod/glossary/glossary.module.ts b/src/addons/mod/glossary/glossary.module.ts index 82def7f99..3b58c3b10 100644 --- a/src/addons/mod/glossary/glossary.module.ts +++ b/src/addons/mod/glossary/glossary.module.ts @@ -49,50 +49,35 @@ export const ADDON_MOD_GLOSSARY_SERVICES: Type[] = [ ]; const mainMenuRoutes: Routes = [ - { - path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`, - loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule), - data: { swipeEnabled: false }, - }, - { - path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`, - loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule), - data: { swipeEnabled: false }, - }, + // Course activity navigation. { path: AddonModGlossaryModuleHandlerService.PAGE_NAME, loadChildren: () => import('./glossary-lazy.module').then(m => m.AddonModGlossaryLazyModule), }, + + // Single Activity format navigation. + { + path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/new`, + loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule), + data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` }, + }, ...conditionalRoutes( - [ - { - path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`, - loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule), - data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` }, - }, - { - path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`, - loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule), - data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` }, - }, - ], + [{ + path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entrySlug`, + loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule), + data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` }, + }], () => CoreScreen.isMobile, ), ]; +// Single Activity format navigation. const courseContentsRoutes: Routes = conditionalRoutes( - [ - { - path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`, - loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule), - data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` }, - }, - { - path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`, - loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule), - data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` }, - }, - ], + [{ + path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entrySlug`, + loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule), + data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` }, + }], () => CoreScreen.isTablet, ); diff --git a/src/addons/mod/glossary/lang.json b/src/addons/mod/glossary/lang.json index 6206a6430..124f2c100 100644 --- a/src/addons/mod/glossary/lang.json +++ b/src/addons/mod/glossary/lang.json @@ -17,6 +17,7 @@ "definition": "Definition", "deleteentry": "Delete entry", "entriestobesynced": "Entries to be synced", + "entry": "Entry", "entrydeleted": "Entry deleted", "entrypendingapproval": "This entry is pending approval.", "entryusedynalink": "This entry should be automatically linked", diff --git a/src/addons/mod/glossary/pages/edit/edit.html b/src/addons/mod/glossary/pages/edit/edit.html index 835718d27..eee439c4a 100644 --- a/src/addons/mod/glossary/pages/edit/edit.html +++ b/src/addons/mod/glossary/pages/edit/edit.html @@ -11,7 +11,7 @@ - +
diff --git a/src/addons/mod/glossary/pages/edit/edit.ts b/src/addons/mod/glossary/pages/edit/edit.ts index 7f608c813..f37856ade 100644 --- a/src/addons/mod/glossary/pages/edit/edit.ts +++ b/src/addons/mod/glossary/pages/edit/edit.ts @@ -12,11 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, ViewChild, ElementRef, Optional, OnDestroy } from '@angular/core'; +import { Component, OnInit, ViewChild, ElementRef, Optional } from '@angular/core'; import { FormControl } from '@angular/forms'; -import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { CoreError } from '@classes/errors/error'; -import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CanLeave } from '@guards/can-leave'; @@ -29,8 +28,6 @@ import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreForms } from '@singletons/form'; -import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source'; -import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager'; import { AddonModGlossary, AddonModGlossaryCategory, @@ -48,7 +45,7 @@ import { AddonModGlossaryOffline } from '../../services/glossary-offline'; selector: 'page-addon-mod-glossary-edit', templateUrl: 'edit.html', }) -export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { +export class AddonModGlossaryEditPage implements OnInit, CanLeave { @ViewChild('editFormEl') formElement?: ElementRef; @@ -74,7 +71,6 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { }; originalData?: AddonModGlossaryFormData; - entries?: AddonModGlossaryEditEntriesSwipeManager; protected syncId?: string; protected syncObserver?: CoreEventObserver; @@ -88,28 +84,10 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { */ async ngOnInit(): Promise { try { - const routeData = this.route.snapshot.data; - const timecreated = CoreNavigator.getRequiredRouteNumberParam('timecreated'); this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); - this.editorExtraParams.timecreated = timecreated; - this.handler = new AddonModGlossaryOfflineFormHandler( - this, - timecreated, - CoreNavigator.getRouteParam('concept'), - ); - - if (timecreated !== 0 && (routeData.swipeEnabled ?? true)) { - const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( - AddonModGlossaryEntriesSource, - [this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''], - ); - - this.entries = new AddonModGlossaryEditEntriesSwipeManager(source); - - await this.entries.start(); - } + this.handler = new AddonModGlossaryNewFormHandler(this); } catch (error) { CoreDomUtils.showErrorModal(error); @@ -121,13 +99,6 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave { this.fetchData(); } - /** - * @inheritdoc - */ - ngOnDestroy(): void { - this.entries?.destroy(); - } - /** * Fetch required data. * @@ -287,132 +258,131 @@ abstract class AddonModGlossaryFormHandler { abstract save(glossary: AddonModGlossaryGlossary): Promise; /** - * Upload entry attachments if any. + * Upload attachments online. * - * @param timecreated Time when the entry was created. * @param glossary Glossary. - * @returns Attachements result. + * @returns Uploaded attachments item id. */ - protected async uploadAttachments(timecreated: number, glossary: AddonModGlossaryGlossary): Promise<{ - saveOffline: boolean; - attachmentsResult?: number | CoreFileUploaderStoreFilesResult; - }> { + protected async uploadAttachments(glossary: AddonModGlossaryGlossary): Promise { const data = this.page.data; + const itemId = await CoreFileUploader.uploadOrReuploadFiles( + data.attachments, + AddonModGlossaryProvider.COMPONENT, + glossary.id, + ); - if (!data.attachments.length) { - return { - saveOffline: false, - }; - } - - try { - const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles( - data.attachments, - AddonModGlossaryProvider.COMPONENT, - glossary.id, - ); - - return { - saveOffline: false, - attachmentsResult, - }; - } catch (error) { - if (CoreUtils.isWebServiceError(error)) { - throw error; - } - - // Cannot upload them in online, save them in offline. - const attachmentsResult = await AddonModGlossaryHelper.storeFiles( - glossary.id, - data.concept, - timecreated, - data.attachments, - ); - - return { - saveOffline: true, - attachmentsResult, - }; - } - } - -} - -/** - * Helper to manage offline form data. - */ -class AddonModGlossaryOfflineFormHandler extends AddonModGlossaryFormHandler { - - private timecreated: number; - private concept: string; - - constructor(page: AddonModGlossaryEditPage, timecreated: number, concept: string | undefined) { - super(page); - - this.timecreated = timecreated; - this.concept = concept ?? ''; + return itemId; } /** - * @inheritdoc + * Store attachments offline. + * + * @param glossary Glossary. + * @param timecreated Entry time created. + * @returns Storage result. */ - async loadData(glossary: AddonModGlossaryGlossary): Promise { - if (this.timecreated === 0) { - return; - } - + protected async storeAttachments( + glossary: AddonModGlossaryGlossary, + timecreated: number, + ): Promise { const data = this.page.data; - const entry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, this.concept, this.timecreated); + const result = await AddonModGlossaryHelper.storeFiles( + glossary.id, + data.concept, + timecreated, + data.attachments, + ); - data.concept = entry.concept || ''; - data.definition = entry.definition || ''; - data.timecreated = entry.timecreated; - - this.page.originalData = { - concept: data.concept, - definition: data.definition, - attachments: data.attachments.slice(), - timecreated: data.timecreated, - categories: data.categories.slice(), - aliases: data.aliases, - usedynalink: data.usedynalink, - casesensitive: data.casesensitive, - fullmatch: data.fullmatch, - }; - - if (entry.options) { - data.categories = (entry.options.categories && ( entry.options.categories).split(',')) || []; - data.aliases = entry.options.aliases || ''; - data.usedynalink = !!entry.options.usedynalink; - - if (data.usedynalink) { - data.casesensitive = !!entry.options.casesensitive; - data.fullmatch = !!entry.options.fullmatch; - } - } - - // Treat offline attachments if any. - if (entry.attachments?.offline) { - data.attachments = await AddonModGlossaryHelper.getStoredFiles(glossary.id, entry.concept, entry.timecreated); - - this.page.originalData.attachments = data.attachments.slice(); - } - - this.page.definitionControl.setValue(data.definition); + return result; } /** - * @inheritdoc + * Create an offline entry. + * + * @param glossary Glossary. + * @param timecreated Time created. + * @param uploadedAttachments Uploaded attachments. */ - async save(glossary: AddonModGlossaryGlossary): Promise { - let entryId: number | false = false; + protected async createOfflineEntry( + glossary: AddonModGlossaryGlossary, + timecreated: number, + uploadedAttachments?: CoreFileUploaderStoreFilesResult, + ): Promise { const data = this.page.data; - const timecreated = this.timecreated || Date.now(); + const options = this.getSaveOptions(glossary); const definition = CoreTextUtils.formatHtmlLines(data.definition); - // Upload attachments first if any. - const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated, glossary); + if (!glossary.allowduplicatedentries) { + // Check if the entry is duplicated in online or offline mode. + const isUsed = await AddonModGlossary.isConceptUsed(glossary.id, data.concept, { + timeCreated: data.timecreated, + cmId: this.page.cmId, + }); + if (isUsed) { + // There's a entry with same name, reject with error message. + throw new CoreError(Translate.instant('addon.mod_glossary.errconceptalreadyexists')); + } + } + + await AddonModGlossaryOffline.addOfflineEntry( + glossary.id, + data.concept, + definition, + this.page.courseId, + options, + uploadedAttachments, + timecreated, + undefined, + undefined, + data, + ); + } + + /** + * Create an online entry. + * + * @param glossary Glossary. + * @param timecreated Time created. + * @param uploadedAttachmentsId Id of the uploaded attachments. + * @param allowOffline Allow falling back to creating the entry offline. + * @returns Entry id. + */ + protected async createOnlineEntry( + glossary: AddonModGlossaryGlossary, + timecreated: number, + uploadedAttachmentsId?: number, + allowOffline?: boolean, + ): Promise { + const data = this.page.data; + const options = this.getSaveOptions(glossary); + const definition = CoreTextUtils.formatHtmlLines(data.definition); + const entryId = await AddonModGlossary.addEntry( + glossary.id, + data.concept, + definition, + this.page.courseId, + options, + uploadedAttachmentsId, + { + timeCreated: timecreated, + discardEntry: data, + allowOffline: allowOffline, + checkDuplicates: !glossary.allowduplicatedentries, + }, + ); + + return entryId; + } + + /** + * Get additional options to save an entry. + * + * @param glossary Glossary. + * @returns Options. + */ + protected getSaveOptions(glossary: AddonModGlossaryGlossary): Record { + const data = this.page.data; const options: Record = { aliases: data.aliases, categories: data.categories.join(','), @@ -420,58 +390,58 @@ class AddonModGlossaryOfflineFormHandler extends AddonModGlossaryFormHandler { if (glossary.usedynalink) { options.usedynalink = data.usedynalink ? 1 : 0; + if (data.usedynalink) { options.casesensitive = data.casesensitive ? 1 : 0; options.fullmatch = data.fullmatch ? 1 : 0; } } - if (saveOffline) { - if (!glossary.allowduplicatedentries) { - // Check if the entry is duplicated in online or offline mode. - const isUsed = await AddonModGlossary.isConceptUsed(glossary.id, data.concept, { - timeCreated: data.timecreated, - cmId: this.page.cmId, - }); + return options; + } - if (isUsed) { - // There's a entry with same name, reject with error message. - throw new CoreError(Translate.instant('addon.mod_glossary.errconceptalreadyexists')); +} + +/** + * Helper to manage the form data for creating a new entry. + */ +class AddonModGlossaryNewFormHandler extends AddonModGlossaryFormHandler { + + /** + * @inheritdoc + */ + async loadData(): Promise { + // There is no data to load, given that this is a new entry. + } + + /** + * @inheritdoc + */ + async save(glossary: AddonModGlossaryGlossary): Promise { + const data = this.page.data; + const timecreated = Date.now(); + + // Upload attachments first if any. + let onlineAttachments: number | undefined = undefined; + let offlineAttachments: CoreFileUploaderStoreFilesResult | undefined = undefined; + + if (data.attachments.length) { + try { + onlineAttachments = await this.uploadAttachments(glossary); + } catch (error) { + if (CoreUtils.isWebServiceError(error)) { + throw error; } - } - // Save entry in offline. - await AddonModGlossaryOffline.addOfflineEntry( - glossary.id, - data.concept, - definition, - this.page.courseId, - options, - attachmentsResult, - timecreated, - undefined, - undefined, - data, - ); - } else { - // Try to send it to server. - // Don't allow offline if there are attachments since they were uploaded fine. - entryId = await AddonModGlossary.addEntry( - glossary.id, - data.concept, - definition, - this.page.courseId, - options, - attachmentsResult, - { - timeCreated: timecreated, - discardEntry: data, - allowOffline: !data.attachments.length, - checkDuplicates: !glossary.allowduplicatedentries, - }, - ); + offlineAttachments = await this.storeAttachments(glossary, timecreated); + } } + // Save entry data. + const entryId = offlineAttachments + ? await this.createOfflineEntry(glossary, timecreated, offlineAttachments) + : await this.createOnlineEntry(glossary, timecreated, onlineAttachments, !data.attachments.length); + // Delete the local files from the tmp folder. CoreFileUploader.clearTmpFiles(data.attachments); @@ -491,20 +461,6 @@ class AddonModGlossaryOfflineFormHandler extends AddonModGlossaryFormHandler { } -/** - * Helper to manage swiping within a collection of glossary entries. - */ -class AddonModGlossaryEditEntriesSwipeManager extends AddonModGlossaryEntriesSwipeManager { - - /** - * @inheritdoc - */ - protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { - return `${this.getSource().GLOSSARY_PATH_PREFIX}edit/${route.params.timecreated}`; - } - -} - /** * Form data. */ diff --git a/src/addons/mod/glossary/pages/entry/entry.html b/src/addons/mod/glossary/pages/entry/entry.html index 28c2b60b1..6804d9ab1 100644 --- a/src/addons/mod/glossary/pages/entry/entry.html +++ b/src/addons/mod/glossary/pages/entry/entry.html @@ -18,6 +18,12 @@ + + + + {{ 'core.hasdatatosync' | translate: { $a: 'addon.mod_glossary.entry' | translate } }} + + @@ -26,9 +32,9 @@ [courseId]="courseId"> -

{{ entry.userfullname }}

+

{{ onlineEntry.userfullname }}

- {{ entry.timemodified | coreDateDayOrTime }} + {{ onlineEntry.timemodified | coreDateDayOrTime }}
@@ -37,7 +43,7 @@

- {{ entry.timemodified | coreDateDayOrTime }} + {{ onlineEntry.timemodified | coreDateDayOrTime }}
@@ -53,32 +59,37 @@
-
- +
+
- +
+ + +
+
{{ 'core.tag.tags' | translate }}:
- +
- +

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

- + - - + diff --git a/src/addons/mod/glossary/pages/entry/entry.ts b/src/addons/mod/glossary/pages/entry/entry.ts index 1db3fc59e..7c6e6076d 100644 --- a/src/addons/mod/glossary/pages/entry/entry.ts +++ b/src/addons/mod/glossary/pages/entry/entry.ts @@ -12,9 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '@addons/mod/glossary/services/glossary-offline'; import { Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core'; import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments'; import { CoreComments } from '@features/comments/services/comments'; @@ -26,8 +28,7 @@ import { CoreNetwork } from '@services/network'; import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; -import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source'; -import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager'; +import { AddonModGlossaryEntriesSource, AddonModGlossaryEntryItem } from '../../classes/glossary-entries-source'; import { AddonModGlossary, AddonModGlossaryEntry, @@ -48,8 +49,9 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { component = AddonModGlossaryProvider.COMPONENT; componentId?: number; - entry?: AddonModGlossaryEntry; - entries?: AddonModGlossaryEntryEntriesSwipeManager; + onlineEntry?: AddonModGlossaryEntry; + offlineEntry?: AddonModGlossaryOfflineEntry; + entries!: AddonModGlossaryEntryEntriesSwipeManager; glossary?: AddonModGlossaryGlossary; loaded = false; showAuthor = false; @@ -59,52 +61,67 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { canDelete = false; commentsEnabled = false; courseId!: number; - cmId?: number; - - protected entryId!: number; + cmId!: number; constructor(@Optional() protected splitView: CoreSplitViewComponent, protected route: ActivatedRoute) {} + get entry(): AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | undefined { + return this.onlineEntry ?? this.offlineEntry; + } + /** * @inheritdoc */ async ngOnInit(): Promise { + let onlineEntryId: number | null = null; + let offlineEntry: { + concept: string; + timecreated: number; + } | null = null; try { - const routeData = this.route.snapshot.data; this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); - this.entryId = CoreNavigator.getRequiredRouteNumberParam('entryId'); this.tagsEnabled = CoreTag.areTagsAvailableInSite(); this.commentsEnabled = !CoreComments.areCommentsDisabledInSite(); + this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); - if (routeData.swipeEnabled ?? true) { - this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); - const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( - AddonModGlossaryEntriesSource, - [this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''], - ); + const entrySlug = CoreNavigator.getRequiredRouteParam('entrySlug'); + const routeData = this.route.snapshot.data; + const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( + AddonModGlossaryEntriesSource, + [this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''], + ); - this.entries = new AddonModGlossaryEntryEntriesSwipeManager(source); + this.entries = new AddonModGlossaryEntryEntriesSwipeManager(source); - await this.entries.start(); + await this.entries.start(); + + if (entrySlug.startsWith('new-')) { + offlineEntry = { + concept : CoreNavigator.getRequiredRouteParam('concept'), + timecreated: Number(entrySlug.slice(4)), + }; } else { - this.cmId = CoreNavigator.getRouteNumberParam('cmId'); + onlineEntryId = Number(entrySlug); } } catch (error) { CoreDomUtils.showErrorModal(error); - CoreNavigator.back(); return; } try { - await this.fetchEntry(); + if (onlineEntryId) { + await this.loadOnlineEntry(onlineEntryId); - if (!this.glossary || !this.componentId) { - return; + if (!this.glossary || !this.componentId) { + return; + } + + await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(onlineEntryId, this.componentId, this.glossary?.name)); + } else if (offlineEntry) { + await this.loadOfflineEntry(offlineEntry.concept, offlineEntry.timecreated); } - - await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.entryId, this.componentId, this.glossary.name)); } finally { this.loaded = true; } @@ -114,14 +131,18 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { * @inheritdoc */ ngOnDestroy(): void { - this.entries?.destroy(); + this.entries.destroy(); } /** * Delete entry. */ async deleteEntry(): Promise { - const entryId = this.entry?.id; + if (!this.onlineEntry) { + return; + } + + const entryId = this.onlineEntry.id; const glossaryId = this.glossary?.id; const cancelled = await CoreUtils.promiseFails( CoreDomUtils.showConfirm(Translate.instant('addon.mod_glossary.areyousuredelete')), @@ -141,7 +162,7 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByCategory(glossaryId)); await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'CREATION')); await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'UPDATE')); - await CoreUtils.ignoreErrors(this.entries?.getSource().invalidateCache(false)); + await CoreUtils.ignoreErrors(this.entries.getSource().invalidateCache(false)); CoreDomUtils.showToast('addon.mod_glossary.entrydeleted', true, ToastDuration.LONG); @@ -164,67 +185,100 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { * @returns 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. + if (this.onlineEntry && this.glossary?.allowcomments && this.onlineEntry.id > 0 && this.commentsEnabled && this.comments) { + // Refresh comments asynchronously (without blocking the current promise). CoreUtils.ignoreErrors(this.comments.doRefresh()); } try { - await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(this.entryId)); + if (this.onlineEntry) { + await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(this.onlineEntry.id)); + await this.loadOnlineEntry(this.onlineEntry.id); + } else if (this.offlineEntry) { + const entrySlug = CoreNavigator.getRequiredRouteParam('entrySlug'); + const timecreated = Number(entrySlug.slice(4)); - await this.fetchEntry(); + await this.loadOfflineEntry(timecreated); + } } finally { refresher?.complete(); } } /** - * Convenience function to get the glossary entry. - * - * @returns Promise resolved when done. + * Load online entry data. */ - protected async fetchEntry(): Promise { + protected async loadOnlineEntry(entryId: number): Promise { try { - const result = await AddonModGlossary.getEntry(this.entryId); + const result = await AddonModGlossary.getEntry(entryId); const canDeleteEntries = CoreNetwork.isOnline() && await AddonModGlossary.canDeleteEntries(); - this.entry = result.entry; + this.onlineEntry = result.entry; this.ratingInfo = result.ratinginfo; this.canDelete = canDeleteEntries && !!result.permissions?.candelete; - 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; - } + await this.loadGlossary(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); } } + /** + * Load offline entry data. + * + * @param concept Entry concept. + * @param timecreated Entry Timecreated. + */ + protected async loadOfflineEntry(concept: string, timecreated: number): Promise { + try { + const glossary = await this.loadGlossary(); + + this.offlineEntry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, concept, timecreated); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); + } + } + + /** + * Load glossary data. + * + * @returns Glossary. + */ + protected async loadGlossary(): Promise { + if (this.glossary) { + return this.glossary; + } + + this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.cmId); + this.componentId = this.glossary.coursemodule; + + switch (this.glossary.displayformat) { + case 'fullwithauthor': + case 'encyclopedia': + this.showAuthor = true; + this.showDate = true; + break; + case 'fullwithoutauthor': + this.showAuthor = false; + this.showDate = true; + break; + default: // Default, and faq, simple, entrylist, continuous. + this.showAuthor = false; + this.showDate = false; + } + + return this.glossary; + } + /** * Function called when rating is updated online. */ ratingUpdated(): void { - AddonModGlossary.invalidateEntry(this.entryId); + if (!this.onlineEntry) { + return; + } + + AddonModGlossary.invalidateEntry(this.onlineEntry.id); } } @@ -232,13 +286,14 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { /** * Helper to manage swiping within a collection of glossary entries. */ -class AddonModGlossaryEntryEntriesSwipeManager extends AddonModGlossaryEntriesSwipeManager { +class AddonModGlossaryEntryEntriesSwipeManager + extends CoreSwipeNavigationItemsManager { /** * @inheritdoc */ protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { - return `${this.getSource().GLOSSARY_PATH_PREFIX}entry/${route.params.entryId}`; + return `${this.getSource().GLOSSARY_PATH_PREFIX}entry/${route.params.entrySlug}`; } } diff --git a/src/addons/mod/glossary/services/handlers/edit-link.ts b/src/addons/mod/glossary/services/handlers/edit-link.ts index 8859a6d72..541a975ed 100644 --- a/src/addons/mod/glossary/services/handlers/edit-link.ts +++ b/src/addons/mod/glossary/services/handlers/edit-link.ts @@ -51,14 +51,8 @@ export class AddonModGlossaryEditLinkHandlerService extends CoreContentLinksHand ); await CoreNavigator.navigateToSitePath( - AddonModGlossaryModuleHandlerService.PAGE_NAME + '/edit/0', - { - params: { - courseId: module.course, - cmId: module.id, - }, - siteId, - }, + AddonModGlossaryModuleHandlerService.PAGE_NAME + `/${module.course}/${module.id}/entry/new`, + { siteId }, ); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingglossary', true); diff --git a/src/addons/mod/glossary/services/handlers/entry-link.ts b/src/addons/mod/glossary/services/handlers/entry-link.ts index d58a0bac3..2402b7f53 100644 --- a/src/addons/mod/glossary/services/handlers/entry-link.ts +++ b/src/addons/mod/glossary/services/handlers/entry-link.ts @@ -56,14 +56,8 @@ export class AddonModGlossaryEntryLinkHandlerService extends CoreContentLinksHan ); await CoreNavigator.navigateToSitePath( - AddonModGlossaryModuleHandlerService.PAGE_NAME + `/entry/${entryId}`, - { - params: { - courseId: module.course, - cmId: module.id, - }, - siteId, - }, + AddonModGlossaryModuleHandlerService.PAGE_NAME + `/${module.course}/${module.id}/entry/${entryId}`, + { siteId }, ); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); diff --git a/src/addons/mod/glossary/tests/behat/navigation.feature b/src/addons/mod/glossary/tests/behat/navigation.feature index 659d286ff..d245ac8da 100644 --- a/src/addons/mod/glossary/tests/behat/navigation.feature +++ b/src/addons/mod/glossary/tests/behat/navigation.feature @@ -280,6 +280,7 @@ Feature: Test glossary navigation | Concept | Tomato | | Definition | Tomato is a fruit | And I press "Save" in the app + And I press "Add a new entry" in the app And I set the following fields to these values in the app: | Concept | Cashew | | Definition | Cashew is a fruit | From 957dece7875deccd3ae7e9c9a74e6d24564c6096 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 29 Mar 2023 11:39:00 +0200 Subject: [PATCH 06/10] MOBILE-2652 glossary: Delete offline entries --- src/addons/mod/glossary/pages/entry/entry.ts | 37 ++++++++++++------- .../mod/glossary/services/glossary-offline.ts | 11 +++--- .../glossary/tests/behat/basic_usage.feature | 24 +++++++++++- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/src/addons/mod/glossary/pages/entry/entry.ts b/src/addons/mod/glossary/pages/entry/entry.ts index 7c6e6076d..bc3d5b022 100644 --- a/src/addons/mod/glossary/pages/entry/entry.ts +++ b/src/addons/mod/glossary/pages/entry/entry.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { AddonModGlossaryHelper } from '@addons/mod/glossary/services/glossary-helper'; import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '@addons/mod/glossary/services/glossary-offline'; import { Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core'; import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; @@ -138,31 +139,38 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { * Delete entry. */ async deleteEntry(): Promise { - if (!this.onlineEntry) { - return; - } - - const entryId = this.onlineEntry.id; const glossaryId = this.glossary?.id; const cancelled = await CoreUtils.promiseFails( CoreDomUtils.showConfirm(Translate.instant('addon.mod_glossary.areyousuredelete')), ); - if (!entryId || !glossaryId || cancelled) { + if (!glossaryId || cancelled) { return; } const modal = await CoreDomUtils.showModalLoading(); try { - await AddonModGlossary.deleteEntry(glossaryId, entryId); - await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(entryId)); - await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByLetter(glossaryId)); - await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByAuthor(glossaryId)); - await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByCategory(glossaryId)); - await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'CREATION')); - await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'UPDATE')); - await CoreUtils.ignoreErrors(this.entries.getSource().invalidateCache(false)); + if (this.onlineEntry) { + const entryId = this.onlineEntry.id; + + await AddonModGlossary.deleteEntry(glossaryId, entryId); + await Promise.all([ + CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(entryId)), + CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByLetter(glossaryId)), + CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByAuthor(glossaryId)), + CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByCategory(glossaryId)), + CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'CREATION')), + CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'UPDATE')), + CoreUtils.ignoreErrors(this.entries.getSource().invalidateCache(false)), + ]); + } else if (this.offlineEntry) { + const concept = this.offlineEntry.concept; + const timecreated = this.offlineEntry.timecreated; + + await AddonModGlossaryOffline.deleteOfflineEntry(glossaryId, concept, timecreated); + await AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timecreated); + } CoreDomUtils.showToast('addon.mod_glossary.entrydeleted', true, ToastDuration.LONG); @@ -234,6 +242,7 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { const glossary = await this.loadGlossary(); this.offlineEntry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, concept, timecreated); + this.canDelete = true; } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); } diff --git a/src/addons/mod/glossary/services/glossary-offline.ts b/src/addons/mod/glossary/services/glossary-offline.ts index 3c748248f..784b1ea8c 100644 --- a/src/addons/mod/glossary/services/glossary-offline.ts +++ b/src/addons/mod/glossary/services/glossary-offline.ts @@ -17,12 +17,11 @@ import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/service 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 { CoreEvents } from '@singletons/events'; import { CorePath } from '@singletons/path'; import { AddonModGlossaryOfflineEntryDBRecord, OFFLINE_ENTRIES_TABLE_NAME } from './database/glossary'; -import { AddonModGlossaryEntryOption, GLOSSARY_ENTRY_ADDED } from './glossary'; +import { AddonModGlossaryEntryOption, GLOSSARY_ENTRY_ADDED, GLOSSARY_ENTRY_DELETED } from './glossary'; /** * Service to handle offline glossary. @@ -35,20 +34,22 @@ export class AddonModGlossaryOfflineProvider { * * @param glossaryId Glossary ID. * @param concept Glossary entry concept. - * @param timeCreated The time the entry was created. + * @param timecreated The time the entry was created. * @param siteId Site ID. If not defined, current site. * @returns Promise resolved if deleted, rejected if failure. */ - async deleteOfflineEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise { + async deleteOfflineEntry(glossaryId: number, concept: string, timecreated: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const conditions: Partial = { glossaryid: glossaryId, concept: concept, - timecreated: timeCreated, + timecreated, }; await site.getDb().deleteRecords(OFFLINE_ENTRIES_TABLE_NAME, conditions); + + CoreEvents.trigger(GLOSSARY_ENTRY_DELETED, { glossaryId, timecreated }); } /** diff --git a/src/addons/mod/glossary/tests/behat/basic_usage.feature b/src/addons/mod/glossary/tests/behat/basic_usage.feature index 48bea05b8..f9b898bd8 100644 --- a/src/addons/mod/glossary/tests/behat/basic_usage.feature +++ b/src/addons/mod/glossary/tests/behat/basic_usage.feature @@ -195,6 +195,7 @@ Feature: Test basic usage of glossary in app Scenario: Delete entries Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app + # Online When I press "Cucumber" in the app And I press "Delete entry" in the app And I press "OK" near "Are you sure you want to delete this entry?" in the app @@ -202,6 +203,25 @@ Feature: Test basic usage of glossary in app And I should find "Potato" in the app But I should not find "Cucumber" in the app + # Offline + When I press "Add a new entry" in the app + And I switch network connection to offline + And I set the following fields to these values in the app: + | Concept | Broccoli | + | Definition | Brassica oleracea var. italica | + And I press "Save" in the app + Then I should find "Potato" in the app + And I should find "Broccoli" in the app + + When I press "Broccoli" in the app + Then I should find "Brassica oleracea var. italica" in the app + + When I press "Delete entry" in the app + And I press "OK" near "Are you sure you want to delete this entry?" in the app + Then I should find "Entry deleted" in the app + And I should find "Potato" in the app + But I should not find "Broccoli" in the app + Scenario: Sync Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app And I press "Add a new entry" in the app @@ -240,8 +260,8 @@ Feature: Test basic usage of glossary in app And I should find "Broccoli" in the app And I should find "Cabbage" in the app And I should find "Garlic" in the app - But I should not see "Entries to be synced" - And I should not see "This Glossary has offline data to be synchronised." + But I should not find "Entries to be synced" in the app + And I should not find "This Glossary has offline data to be synchronised." in the app When I press "Garlic" in the app Then I should find "Garlic" in the app From 3c443a26c4e6d83cfcecbac765708ed1a4118845 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 29 Mar 2023 14:02:26 +0200 Subject: [PATCH 07/10] MOBILE-2652 glossary: Edit offline entries --- scripts/langindex.json | 1 + .../classes/glossary-entries-source.ts | 10 +- .../mod/glossary/components/index/index.ts | 8 + .../mod/glossary/glossary-lazy.module.ts | 4 + src/addons/mod/glossary/glossary.module.ts | 5 + src/addons/mod/glossary/lang.json | 1 + src/addons/mod/glossary/pages/edit/edit.ts | 273 +++++++++++++----- .../mod/glossary/pages/entry/entry.html | 9 +- src/addons/mod/glossary/pages/entry/entry.ts | 48 ++- .../mod/glossary/services/glossary-offline.ts | 61 ++-- .../mod/glossary/services/glossary-sync.ts | 2 +- src/addons/mod/glossary/services/glossary.ts | 39 +-- .../glossary/tests/behat/basic_usage.feature | 10 +- .../glossary/tests/behat/navigation.feature | 20 ++ 14 files changed, 343 insertions(+), 148 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index cfc5ef1f8..5bad1c2bb 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -696,6 +696,7 @@ "addon.mod_glossary.concept": "glossary", "addon.mod_glossary.definition": "glossary", "addon.mod_glossary.deleteentry": "glossary", + "addon.mod_glossary.editentry": "glossary", "addon.mod_glossary.entriestobesynced": "local_moodlemobileapp", "addon.mod_glossary.entry": "glossary", "addon.mod_glossary.entrydeleted": "glossary", diff --git a/src/addons/mod/glossary/classes/glossary-entries-source.ts b/src/addons/mod/glossary/classes/glossary-entries-source.ts index cd45af84c..08d525dc4 100644 --- a/src/addons/mod/glossary/classes/glossary-entries-source.ts +++ b/src/addons/mod/glossary/classes/glossary-entries-source.ts @@ -86,17 +86,11 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource< /** * @inheritdoc */ - getItemQueryParams(entry: AddonModGlossaryEntryItem): Params { - const params: Params = { + getItemQueryParams(): Params { + return { cmId: this.CM_ID, courseId: this.COURSE_ID, }; - - if (this.isOfflineEntry(entry)) { - params.concept = entry.concept; - } - - return params; } /** diff --git a/src/addons/mod/glossary/components/index/index.ts b/src/addons/mod/glossary/components/index/index.ts index 904f91028..3ca1498ca 100644 --- a/src/addons/mod/glossary/components/index/index.ts +++ b/src/addons/mod/glossary/components/index/index.ts @@ -45,6 +45,7 @@ import { AddonModGlossaryProvider, GLOSSARY_ENTRY_ADDED, GLOSSARY_ENTRY_DELETED, + GLOSSARY_ENTRY_UPDATED, } from '../../services/glossary'; import { AddonModGlossaryOfflineEntry } from '../../services/glossary-offline'; import { @@ -149,6 +150,13 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity this.showLoadingAndRefresh(false); }), + CoreEvents.on(GLOSSARY_ENTRY_UPDATED, ({ glossaryId }) => { + if (this.glossary?.id !== glossaryId) { + return; + } + + this.showLoadingAndRefresh(false); + }), CoreEvents.on(GLOSSARY_ENTRY_DELETED, ({ glossaryId }) => { if (this.glossary?.id !== glossaryId) { return; diff --git a/src/addons/mod/glossary/glossary-lazy.module.ts b/src/addons/mod/glossary/glossary-lazy.module.ts index 8f12bc001..7d74f30f7 100644 --- a/src/addons/mod/glossary/glossary-lazy.module.ts +++ b/src/addons/mod/glossary/glossary-lazy.module.ts @@ -50,6 +50,10 @@ const routes: Routes = [ path: ':courseId/:cmId/entry/new', loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule), }, + { + path: ':courseId/:cmId/entry/:entrySlug/edit', + loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule), + }, ...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile), ...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet), ]; diff --git a/src/addons/mod/glossary/glossary.module.ts b/src/addons/mod/glossary/glossary.module.ts index 3b58c3b10..da86adf1c 100644 --- a/src/addons/mod/glossary/glossary.module.ts +++ b/src/addons/mod/glossary/glossary.module.ts @@ -61,6 +61,11 @@ const mainMenuRoutes: Routes = [ loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule), data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` }, }, + { + path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entrySlug/edit`, + loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule), + data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` }, + }, ...conditionalRoutes( [{ path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entrySlug`, diff --git a/src/addons/mod/glossary/lang.json b/src/addons/mod/glossary/lang.json index 124f2c100..c380778c6 100644 --- a/src/addons/mod/glossary/lang.json +++ b/src/addons/mod/glossary/lang.json @@ -16,6 +16,7 @@ "concept": "Concept", "definition": "Definition", "deleteentry": "Delete entry", + "editentry": "Edit entry", "entriestobesynced": "Entries to be synced", "entry": "Entry", "entrydeleted": "Entry deleted", diff --git a/src/addons/mod/glossary/pages/edit/edit.ts b/src/addons/mod/glossary/pages/edit/edit.ts index f37856ade..d2c32c45d 100644 --- a/src/addons/mod/glossary/pages/edit/edit.ts +++ b/src/addons/mod/glossary/pages/edit/edit.ts @@ -84,10 +84,17 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { */ async ngOnInit(): Promise { try { + const entrySlug = CoreNavigator.getRouteParam('entrySlug'); this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); - this.handler = new AddonModGlossaryNewFormHandler(this); + if (entrySlug?.startsWith('new-')) { + const timecreated = Number(entrySlug.slice(4)); + this.editorExtraParams.timecreated = timecreated; + this.handler = new AddonModGlossaryOfflineFormHandler(this, timecreated); + } else { + this.handler = new AddonModGlossaryNewFormHandler(this); + } } catch (error) { CoreDomUtils.showErrorModal(error); @@ -297,82 +304,25 @@ abstract class AddonModGlossaryFormHandler { } /** - * Create an offline entry. + * Make sure that the new entry won't create any duplicates. * * @param glossary Glossary. - * @param timecreated Time created. - * @param uploadedAttachments Uploaded attachments. */ - protected async createOfflineEntry( - glossary: AddonModGlossaryGlossary, - timecreated: number, - uploadedAttachments?: CoreFileUploaderStoreFilesResult, - ): Promise { - const data = this.page.data; - const options = this.getSaveOptions(glossary); - const definition = CoreTextUtils.formatHtmlLines(data.definition); - - if (!glossary.allowduplicatedentries) { - // Check if the entry is duplicated in online or offline mode. - const isUsed = await AddonModGlossary.isConceptUsed(glossary.id, data.concept, { - timeCreated: data.timecreated, - cmId: this.page.cmId, - }); - - if (isUsed) { - // There's a entry with same name, reject with error message. - throw new CoreError(Translate.instant('addon.mod_glossary.errconceptalreadyexists')); - } + protected async checkDuplicates(glossary: AddonModGlossaryGlossary): Promise { + if (glossary.allowduplicatedentries) { + return; } - await AddonModGlossaryOffline.addOfflineEntry( - glossary.id, - data.concept, - definition, - this.page.courseId, - options, - uploadedAttachments, - timecreated, - undefined, - undefined, - data, - ); - } - - /** - * Create an online entry. - * - * @param glossary Glossary. - * @param timecreated Time created. - * @param uploadedAttachmentsId Id of the uploaded attachments. - * @param allowOffline Allow falling back to creating the entry offline. - * @returns Entry id. - */ - protected async createOnlineEntry( - glossary: AddonModGlossaryGlossary, - timecreated: number, - uploadedAttachmentsId?: number, - allowOffline?: boolean, - ): Promise { const data = this.page.data; - const options = this.getSaveOptions(glossary); - const definition = CoreTextUtils.formatHtmlLines(data.definition); - const entryId = await AddonModGlossary.addEntry( - glossary.id, - data.concept, - definition, - this.page.courseId, - options, - uploadedAttachmentsId, - { - timeCreated: timecreated, - discardEntry: data, - allowOffline: allowOffline, - checkDuplicates: !glossary.allowduplicatedentries, - }, - ); + const isUsed = await AddonModGlossary.isConceptUsed(glossary.id, data.concept, { + timeCreated: data.timecreated, + cmId: this.page.cmId, + }); - return entryId; + if (isUsed) { + // There's a entry with same name, reject with error message. + throw new CoreError(Translate.instant('addon.mod_glossary.errconceptalreadyexists')); + } } /** @@ -402,6 +352,119 @@ abstract class AddonModGlossaryFormHandler { } +/** + * Helper to manage the form data for an offline entry. + */ +class AddonModGlossaryOfflineFormHandler extends AddonModGlossaryFormHandler { + + private timecreated: number; + + constructor(page: AddonModGlossaryEditPage, timecreated: number) { + super(page); + + this.timecreated = timecreated; + } + + /** + * @inheritdoc + */ + async loadData(glossary: AddonModGlossaryGlossary): Promise { + const data = this.page.data; + const entry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, this.timecreated); + + data.concept = entry.concept || ''; + data.definition = entry.definition || ''; + data.timecreated = entry.timecreated; + + if (entry.options) { + data.categories = (entry.options.categories && ( entry.options.categories).split(',')) || []; + data.aliases = entry.options.aliases || ''; + data.usedynalink = !!entry.options.usedynalink; + + if (data.usedynalink) { + data.casesensitive = !!entry.options.casesensitive; + data.fullmatch = !!entry.options.fullmatch; + } + } + + // Treat offline attachments if any. + if (entry.attachments?.offline) { + data.attachments = await AddonModGlossaryHelper.getStoredFiles(glossary.id, entry.concept, entry.timecreated); + } + + this.page.originalData = { + concept: data.concept, + definition: data.definition, + attachments: data.attachments.slice(), + timecreated: data.timecreated, + categories: data.categories.slice(), + aliases: data.aliases, + usedynalink: data.usedynalink, + casesensitive: data.casesensitive, + fullmatch: data.fullmatch, + }; + + this.page.definitionControl.setValue(data.definition); + } + + /** + * @inheritdoc + */ + async save(glossary: AddonModGlossaryGlossary): Promise { + const data = this.page.data; + + // Upload attachments first if any. + let offlineAttachments: CoreFileUploaderStoreFilesResult | undefined = undefined; + + if (data.attachments.length) { + offlineAttachments = await this.storeAttachments(glossary, data.timecreated); + } + + // Save entry data. + await this.updateOfflineEntry(glossary, offlineAttachments); + + // Delete the local files from the tmp folder. + CoreFileUploader.clearTmpFiles(data.attachments); + + return false; + } + + /** + * Update an offline entry. + * + * @param glossary Glossary. + * @param uploadedAttachments Uploaded attachments. + */ + protected async updateOfflineEntry( + glossary: AddonModGlossaryGlossary, + uploadedAttachments?: CoreFileUploaderStoreFilesResult, + ): Promise { + const originalData = this.page.originalData; + const data = this.page.data; + const options = this.getSaveOptions(glossary); + const definition = CoreTextUtils.formatHtmlLines(data.definition); + + if (!originalData) { + return; + } + + await this.checkDuplicates(glossary); + await AddonModGlossaryOffline.updateOfflineEntry( + { + glossaryid: glossary.id, + courseid: this.page.courseId, + concept: originalData.concept, + timecreated: originalData.timecreated, + }, + data.concept, + definition, + options, + uploadedAttachments, + ); + } + +} + /** * Helper to manage the form data for creating a new entry. */ @@ -451,14 +514,74 @@ class AddonModGlossaryNewFormHandler extends AddonModGlossaryFormHandler { CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' }); } - CoreEvents.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, { - glossaryId: glossary.id, - entryId: entryId || undefined, - }, CoreSites.getCurrentSiteId()); - return !!entryId; } + /** + * Create an offline entry. + * + * @param glossary Glossary. + * @param timecreated Time created. + * @param uploadedAttachments Uploaded attachments. + */ + protected async createOfflineEntry( + glossary: AddonModGlossaryGlossary, + timecreated: number, + uploadedAttachments?: CoreFileUploaderStoreFilesResult, + ): Promise { + const data = this.page.data; + const options = this.getSaveOptions(glossary); + const definition = CoreTextUtils.formatHtmlLines(data.definition); + + await this.checkDuplicates(glossary); + await AddonModGlossaryOffline.addOfflineEntry( + glossary.id, + data.concept, + definition, + this.page.courseId, + timecreated, + options, + uploadedAttachments, + undefined, + undefined, + ); + } + + /** + * Create an online entry. + * + * @param glossary Glossary. + * @param timecreated Time created. + * @param uploadedAttachmentsId Id of the uploaded attachments. + * @param allowOffline Allow falling back to creating the entry offline. + * @returns Entry id. + */ + protected async createOnlineEntry( + glossary: AddonModGlossaryGlossary, + timecreated: number, + uploadedAttachmentsId?: number, + allowOffline?: boolean, + ): Promise { + const data = this.page.data; + const options = this.getSaveOptions(glossary); + const definition = CoreTextUtils.formatHtmlLines(data.definition); + const entryId = await AddonModGlossary.addEntry( + glossary.id, + data.concept, + definition, + this.page.courseId, + options, + uploadedAttachmentsId, + { + timeCreated: timecreated, + allowOffline: allowOffline, + checkDuplicates: !glossary.allowduplicatedentries, + }, + ); + + return entryId; + } + } /** diff --git a/src/addons/mod/glossary/pages/entry/entry.html b/src/addons/mod/glossary/pages/entry/entry.html index 6804d9ab1..b0de28204 100644 --- a/src/addons/mod/glossary/pages/entry/entry.html +++ b/src/addons/mod/glossary/pages/entry/entry.html @@ -52,11 +52,16 @@
- +
- + + + +
diff --git a/src/addons/mod/glossary/pages/entry/entry.ts b/src/addons/mod/glossary/pages/entry/entry.ts index bc3d5b022..5315d4375 100644 --- a/src/addons/mod/glossary/pages/entry/entry.ts +++ b/src/addons/mod/glossary/pages/entry/entry.ts @@ -29,12 +29,14 @@ import { CoreNetwork } from '@services/network'; import { CoreDomUtils, ToastDuration } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { AddonModGlossaryEntriesSource, AddonModGlossaryEntryItem } from '../../classes/glossary-entries-source'; import { AddonModGlossary, AddonModGlossaryEntry, AddonModGlossaryGlossary, AddonModGlossaryProvider, + GLOSSARY_ENTRY_UPDATED, } from '../../services/glossary'; /** @@ -54,11 +56,13 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { offlineEntry?: AddonModGlossaryOfflineEntry; entries!: AddonModGlossaryEntryEntriesSwipeManager; glossary?: AddonModGlossaryGlossary; + entryUpdatedObserver?: CoreEventObserver; loaded = false; showAuthor = false; showDate = false; ratingInfo?: CoreRatingInfo; tagsEnabled = false; + canEdit = false; canDelete = false; commentsEnabled = false; courseId!: number; @@ -75,10 +79,8 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { */ async ngOnInit(): Promise { let onlineEntryId: number | null = null; - let offlineEntry: { - concept: string; - timecreated: number; - } | null = null; + let offlineEntryTimeCreated: number | null = null; + try { this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); this.tagsEnabled = CoreTag.areTagsAvailableInSite(); @@ -97,10 +99,7 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { await this.entries.start(); if (entrySlug.startsWith('new-')) { - offlineEntry = { - concept : CoreNavigator.getRequiredRouteParam('concept'), - timecreated: Number(entrySlug.slice(4)), - }; + offlineEntryTimeCreated = Number(entrySlug.slice(4)); } else { onlineEntryId = Number(entrySlug); } @@ -111,6 +110,19 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { return; } + this.entryUpdatedObserver = CoreEvents.on(GLOSSARY_ENTRY_UPDATED, data => { + if (data.glossaryId !== this.glossary?.id) { + return; + } + + if ( + (this.onlineEntry && this.onlineEntry.id === data.entryId) || + (this.offlineEntry && this.offlineEntry.timecreated === data.timecreated) + ) { + this.doRefresh(); + } + }); + try { if (onlineEntryId) { await this.loadOnlineEntry(onlineEntryId); @@ -120,8 +132,8 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { } await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(onlineEntryId, this.componentId, this.glossary?.name)); - } else if (offlineEntry) { - await this.loadOfflineEntry(offlineEntry.concept, offlineEntry.timecreated); + } else if (offlineEntryTimeCreated) { + await this.loadOfflineEntry(offlineEntryTimeCreated); } } finally { this.loaded = true; @@ -133,6 +145,14 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.entries.destroy(); + this.entryUpdatedObserver?.off(); + } + + /** + * Edit entry. + */ + async editEntry(): Promise { + await CoreNavigator.navigate('./edit'); } /** @@ -168,7 +188,7 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { const concept = this.offlineEntry.concept; const timecreated = this.offlineEntry.timecreated; - await AddonModGlossaryOffline.deleteOfflineEntry(glossaryId, concept, timecreated); + await AddonModGlossaryOffline.deleteOfflineEntry(glossaryId, timecreated); await AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timecreated); } @@ -234,14 +254,14 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { /** * Load offline entry data. * - * @param concept Entry concept. * @param timecreated Entry Timecreated. */ - protected async loadOfflineEntry(concept: string, timecreated: number): Promise { + protected async loadOfflineEntry(timecreated: number): Promise { try { const glossary = await this.loadGlossary(); - this.offlineEntry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, concept, timecreated); + this.offlineEntry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, timecreated); + this.canEdit = true; this.canDelete = true; } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true); diff --git a/src/addons/mod/glossary/services/glossary-offline.ts b/src/addons/mod/glossary/services/glossary-offline.ts index 784b1ea8c..cc4b89c86 100644 --- a/src/addons/mod/glossary/services/glossary-offline.ts +++ b/src/addons/mod/glossary/services/glossary-offline.ts @@ -21,7 +21,7 @@ import { makeSingleton } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { CorePath } from '@singletons/path'; import { AddonModGlossaryOfflineEntryDBRecord, OFFLINE_ENTRIES_TABLE_NAME } from './database/glossary'; -import { AddonModGlossaryEntryOption, GLOSSARY_ENTRY_ADDED, GLOSSARY_ENTRY_DELETED } from './glossary'; +import { AddonModGlossaryEntryOption, GLOSSARY_ENTRY_ADDED, GLOSSARY_ENTRY_DELETED, GLOSSARY_ENTRY_UPDATED } from './glossary'; /** * Service to handle offline glossary. @@ -33,18 +33,16 @@ export class AddonModGlossaryOfflineProvider { * Delete an offline 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. * @returns Promise resolved if deleted, rejected if failure. */ - async deleteOfflineEntry(glossaryId: number, concept: string, timecreated: number, siteId?: string): Promise { + async deleteOfflineEntry(glossaryId: number, timecreated: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const conditions: Partial = { glossaryid: glossaryId, - concept: concept, - timecreated, + timecreated: timecreated, }; await site.getDb().deleteRecords(OFFLINE_ENTRIES_TABLE_NAME, conditions); @@ -70,14 +68,12 @@ export class AddonModGlossaryOfflineProvider { * Get a stored offline 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. * @returns Promise resolved with entry. */ async getOfflineEntry( glossaryId: number, - concept: string, timeCreated: number, siteId?: string, ): Promise { @@ -85,7 +81,6 @@ export class AddonModGlossaryOfflineProvider { const conditions: Partial = { glossaryid: glossaryId, - concept: concept, timecreated: timeCreated, }; @@ -145,7 +140,7 @@ export class AddonModGlossaryOfflineProvider { } // If there's only one entry, check that is not the one we are editing. - return CoreUtils.promiseFails(this.getOfflineEntry(glossaryId, concept, timeCreated, siteId)); + return entries[0].timecreated !== timeCreated; } catch { // No offline data found, return false. return false; @@ -159,12 +154,11 @@ export class AddonModGlossaryOfflineProvider { * @param concept Glossary entry concept. * @param definition Glossary entry concept definition. * @param courseId Course ID of the glossary. + * @param timecreated The time the entry was created. If not defined, current time. * @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. * @returns Promise resolved if stored, rejected if failure. */ async addOfflineEntry( @@ -172,15 +166,13 @@ export class AddonModGlossaryOfflineProvider { concept: string, definition: string, courseId: number, + timecreated: number, options?: Record, attachments?: CoreFileUploaderStoreFilesResult, - timecreated?: number, siteId?: string, userId?: number, - discardEntry?: AddonModGlossaryDiscardedEntry, ): Promise { const site = await CoreSites.getSite(siteId); - timecreated = timecreated || Date.now(); const entry: AddonModGlossaryOfflineEntryDBRecord = { glossaryid: glossaryId, @@ -194,11 +186,6 @@ export class AddonModGlossaryOfflineProvider { timecreated, }; - // If editing an offline entry, delete previous first. - if (discardEntry) { - await this.deleteOfflineEntry(glossaryId, discardEntry.concept, discardEntry.timecreated, site.getId()); - } - await site.getDb().insertRecord(OFFLINE_ENTRIES_TABLE_NAME, entry); CoreEvents.trigger(GLOSSARY_ENTRY_ADDED, { glossaryId, timecreated }, siteId); @@ -206,6 +193,42 @@ export class AddonModGlossaryOfflineProvider { return false; } + /** + * Update an offline entry to be sent later. + * + * @param originalEntry Original entry data. + * @param concept Glossary entry concept. + * @param definition Glossary entry concept definition. + * @param options Options for the entry. + * @param attachments Result of CoreFileUploaderProvider#storeFilesToUpload for attachments. + */ + async updateOfflineEntry( + originalEntry: Pick< AddonModGlossaryOfflineEntryDBRecord, 'glossaryid'|'courseid'|'concept'|'timecreated'>, + concept: string, + definition: string, + options?: Record, + attachments?: CoreFileUploaderStoreFilesResult, + ): Promise { + const site = await CoreSites.getSite(); + const entry: Omit = { + concept: concept, + definition: definition, + definitionformat: 'html', + options: JSON.stringify(options || {}), + attachments: JSON.stringify(attachments), + }; + + await site.getDb().updateRecords(OFFLINE_ENTRIES_TABLE_NAME, entry, { + ...originalEntry, + userid: site.getUserId(), + }); + + CoreEvents.trigger(GLOSSARY_ENTRY_UPDATED, { + glossaryId: originalEntry.glossaryid, + timecreated: originalEntry.timecreated, + }); + } + /** * Get the path to the folder where to store files for offline attachments in a glossary. * diff --git a/src/addons/mod/glossary/services/glossary-sync.ts b/src/addons/mod/glossary/services/glossary-sync.ts index 62e04f3c9..b922d7262 100644 --- a/src/addons/mod/glossary/services/glossary-sync.ts +++ b/src/addons/mod/glossary/services/glossary-sync.ts @@ -285,7 +285,7 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv */ protected async deleteAddEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise { await Promise.all([ - AddonModGlossaryOffline.deleteOfflineEntry(glossaryId, concept, timeCreated, siteId), + AddonModGlossaryOffline.deleteOfflineEntry(glossaryId, timeCreated, siteId), AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timeCreated, siteId), ]); } diff --git a/src/addons/mod/glossary/services/glossary.ts b/src/addons/mod/glossary/services/glossary.ts index caa61c3f6..fde1157b2 100644 --- a/src/addons/mod/glossary/services/glossary.ts +++ b/src/addons/mod/glossary/services/glossary.ts @@ -30,6 +30,7 @@ import { AddonModGlossaryEntryDBRecord, ENTRIES_TABLE_NAME } from './database/gl import { AddonModGlossaryOffline } from './glossary-offline'; export const GLOSSARY_ENTRY_ADDED = 'addon_mod_glossary_entry_added'; +export const GLOSSARY_ENTRY_UPDATED = 'addon_mod_glossary_entry_updated'; export const GLOSSARY_ENTRY_DELETED = 'addon_mod_glossary_entry_deleted'; /** @@ -806,13 +807,10 @@ export class AddonModGlossaryProvider { // 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, }); @@ -831,12 +829,11 @@ export class AddonModGlossaryProvider { concept, definition, courseId, + otherOptions.timeCreated ?? Date.now(), entryOptions, attachments, - otherOptions.timeCreated, otherOptions.siteId, undefined, - otherOptions.discardEntry, ); return false; @@ -847,16 +844,6 @@ export class AddonModGlossaryProvider { return storeOffline(); } - // If we are editing an offline entry, discard previous first. - if (otherOptions.discardEntry) { - await AddonModGlossaryOffline.deleteOfflineEntry( - glossaryId, - otherOptions.discardEntry.concept, - otherOptions.discardEntry.timecreated, - otherOptions.siteId, - ); - } - try { // Try to add it in online. const entryId = await this.addEntryOnline( @@ -1071,6 +1058,7 @@ declare module '@singletons/events' { */ export interface CoreEventsData { [GLOSSARY_ENTRY_ADDED]: AddonModGlossaryEntryAddedEventData; + [GLOSSARY_ENTRY_UPDATED]: AddonModGlossaryEntryUpdatedEventData; [GLOSSARY_ENTRY_DELETED]: AddonModGlossaryEntryDeletedEventData; } @@ -1085,12 +1073,22 @@ export type AddonModGlossaryEntryAddedEventData = { timecreated?: number; }; +/** + * GLOSSARY_ENTRY_UPDATED event payload. + */ +export type AddonModGlossaryEntryUpdatedEventData = { + glossaryId: number; + entryId?: number; + timecreated?: number; +}; + /** * GLOSSARY_ENTRY_DELETED event payload. */ export type AddonModGlossaryEntryDeletedEventData = { glossaryId: number; - entryId: number; + entryId?: number; + timecreated?: number; }; /** @@ -1361,21 +1359,12 @@ export type AddonModGlossaryViewEntryWSParams = { */ 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; -}; - /** * Options to pass to the different get entries functions. */ diff --git a/src/addons/mod/glossary/tests/behat/basic_usage.feature b/src/addons/mod/glossary/tests/behat/basic_usage.feature index f9b898bd8..a2f249ffe 100644 --- a/src/addons/mod/glossary/tests/behat/basic_usage.feature +++ b/src/addons/mod/glossary/tests/behat/basic_usage.feature @@ -154,7 +154,6 @@ Feature: Test basic usage of glossary in app Then I should find "Garlic" in the app And I should find "Allium sativum" in the app - @noeldebug Scenario: Edit entries (basic info) Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app @@ -166,6 +165,9 @@ Feature: Test basic usage of glossary in app And I set the following fields to these values in the app: | Concept | Broccoli | | Definition | Brassica oleracea var. italica | + And I press "This entry should be automatically linked" "ion-toggle" in the app + And I press "This entry is case sensitive" "ion-toggle" in the app + And I press "Match whole words only" "ion-toggle" in the app And I press "Save" in the app Then I should find "Potato" in the app And I should find "Broccoli" in the app @@ -176,6 +178,9 @@ Feature: Test basic usage of glossary in app When I press "Edit entry" in the app Then the field "Concept" matches value "Broccoli" in the app And the field "Definition" matches value "Brassica oleracea var. italica" in the app + And "This entry should be automatically linked" "ion-toggle" should be selected in the app + And "This entry is case sensitive" "ion-toggle" should be selected in the app + And "Match whole words only" "ion-toggle" should be selected in the app When I set the following fields to these values in the app: | Concept | Pickle | @@ -189,9 +194,6 @@ Feature: Test basic usage of glossary in app And I should find "Potato" in the app But I should not find "Broccoli" in the app - # TODO test attachments? (yes, in all scenarios!!) - # TODO And I upload "stub.txt" to "File" ".action-sheet-button" in the app - Scenario: Delete entries Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app diff --git a/src/addons/mod/glossary/tests/behat/navigation.feature b/src/addons/mod/glossary/tests/behat/navigation.feature index d245ac8da..4700884e6 100644 --- a/src/addons/mod/glossary/tests/behat/navigation.feature +++ b/src/addons/mod/glossary/tests/behat/navigation.feature @@ -200,6 +200,17 @@ Feature: Test glossary navigation When I swipe to the left in the app Then I should find "Acerola is a fruit" in the app + # Edit + When I swipe to the right in the app + And I press "Edit entry" in the app + And I press "Save" in the app + Then I should find "Tomato is a fruit" in the app + + When I press the back button in the app + Then I should find "Tomato" in the app + And I should find "Cashew" in the app + And I should find "Acerola" in the app + @ci_jenkins_skip Scenario: Tablet navigation on glossary Given I entered the course "Course 1" as "student1" in the app @@ -301,3 +312,12 @@ Feature: Test glossary navigation When I press "Acerola" in the app Then "Acerola" near "Tomato" should be selected in the app And I should find "Acerola is a fruit" inside the split-view content in the app + + # Edit + When I press "Tomato" in the app + And I press "Edit entry" in the app + And I press "Save" in the app + Then I should find "Tomato is a fruit" inside the split-view content in the app + And I should find "Tomato" in the app + And I should find "Cashew" in the app + And I should find "Acerola" in the app From 9391fe412275acfd443ec0a62fb77431f184afe2 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 29 Mar 2023 15:43:13 +0200 Subject: [PATCH 08/10] MOBILE-2652 glossary: Edit online entries --- src/addons/mod/glossary/pages/edit/edit.ts | 79 +++++++++++++++++++ src/addons/mod/glossary/pages/entry/entry.ts | 2 + src/addons/mod/glossary/services/glossary.ts | 78 ++++++++++++++++++ .../glossary/tests/behat/basic_usage.feature | 34 +++++++- 4 files changed, 192 insertions(+), 1 deletion(-) diff --git a/src/addons/mod/glossary/pages/edit/edit.ts b/src/addons/mod/glossary/pages/edit/edit.ts index d2c32c45d..d550fd417 100644 --- a/src/addons/mod/glossary/pages/edit/edit.ts +++ b/src/addons/mod/glossary/pages/edit/edit.ts @@ -16,11 +16,13 @@ import { Component, OnInit, ViewChild, ElementRef, Optional } from '@angular/cor import { FormControl } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { CoreError } from '@classes/errors/error'; +import { CoreNetworkError } from '@classes/errors/network-error'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CanLeave } from '@guards/can-leave'; import { CoreFileEntry } from '@services/file-helper'; import { CoreNavigator } from '@services/navigator'; +import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; @@ -31,6 +33,7 @@ import { CoreForms } from '@singletons/form'; import { AddonModGlossary, AddonModGlossaryCategory, + AddonModGlossaryEntry, AddonModGlossaryEntryOption, AddonModGlossaryGlossary, AddonModGlossaryProvider, @@ -92,6 +95,11 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { const timecreated = Number(entrySlug.slice(4)); this.editorExtraParams.timecreated = timecreated; this.handler = new AddonModGlossaryOfflineFormHandler(this, timecreated); + } else if (entrySlug) { + const { entry } = await AddonModGlossary.getEntry(Number(entrySlug)); + + this.editorExtraParams.timecreated = entry.timecreated; + this.handler = new AddonModGlossaryOnlineFormHandler(this, entry); } else { this.handler = new AddonModGlossaryNewFormHandler(this); } @@ -584,6 +592,77 @@ class AddonModGlossaryNewFormHandler extends AddonModGlossaryFormHandler { } +/** + * Helper to manage the form data for an online entry. + */ +class AddonModGlossaryOnlineFormHandler extends AddonModGlossaryFormHandler { + + private entry: AddonModGlossaryEntry; + + constructor(page: AddonModGlossaryEditPage, entry: AddonModGlossaryEntry) { + super(page); + + this.entry = entry; + } + + /** + * @inheritdoc + */ + async loadData(): Promise { + const data = this.page.data; + + data.concept = this.entry.concept; + data.definition = this.entry.definition || ''; + data.timecreated = this.entry.timecreated; + data.usedynalink = this.entry.usedynalink; + + if (data.usedynalink) { + data.casesensitive = this.entry.casesensitive; + data.fullmatch = this.entry.fullmatch; + } + + // Treat offline attachments if any. + if (this.entry.attachments) { + data.attachments = this.entry.attachments; + } + + this.page.originalData = { + concept: data.concept, + definition: data.definition, + attachments: data.attachments.slice(), + timecreated: data.timecreated, + categories: data.categories.slice(), + aliases: data.aliases, + usedynalink: data.usedynalink, + casesensitive: data.casesensitive, + fullmatch: data.fullmatch, + }; + + this.page.definitionControl.setValue(data.definition); + } + + /** + * @inheritdoc + */ + async save(glossary: AddonModGlossaryGlossary): Promise { + if (!CoreNetwork.isOnline()) { + throw new CoreNetworkError(); + } + + const data = this.page.data; + const options = this.getSaveOptions(glossary); + const definition = CoreTextUtils.formatHtmlLines(data.definition); + + // Save entry data. + await AddonModGlossary.updateEntry(glossary.id, this.entry.id, data.concept, definition, options); + + CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' }); + + return true; + } + +} + /** * Form data. */ diff --git a/src/addons/mod/glossary/pages/entry/entry.ts b/src/addons/mod/glossary/pages/entry/entry.ts index 5315d4375..c1649d67d 100644 --- a/src/addons/mod/glossary/pages/entry/entry.ts +++ b/src/addons/mod/glossary/pages/entry/entry.ts @@ -240,10 +240,12 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { try { const result = await AddonModGlossary.getEntry(entryId); const canDeleteEntries = CoreNetwork.isOnline() && await AddonModGlossary.canDeleteEntries(); + const canUpdateEntries = CoreNetwork.isOnline() && await AddonModGlossary.canUpdateEntries(); this.onlineEntry = result.entry; this.ratingInfo = result.ratinginfo; this.canDelete = canDeleteEntries && !!result.permissions?.candelete; + this.canEdit = canUpdateEntries && !!result.permissions?.canupdate; await this.loadGlossary(); } catch (error) { diff --git a/src/addons/mod/glossary/services/glossary.ts b/src/addons/mod/glossary/services/glossary.ts index fde1157b2..012a4c1d0 100644 --- a/src/addons/mod/glossary/services/glossary.ts +++ b/src/addons/mod/glossary/services/glossary.ts @@ -619,6 +619,18 @@ export class AddonModGlossaryProvider { return site.wsAvailable('mod_glossary_delete_entry'); } + /** + * Check whether the site can update glossary entries. + * + * @param siteId Site id. + * @returns Whether the site can update entries. + */ + async canUpdateEntries(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.wsAvailable('mod_glossary_update_entry'); + } + /** * Performs the whole fetch of the entries using the proper function and arguments. * @@ -910,6 +922,43 @@ export class AddonModGlossaryProvider { return response.entryid; } + /** + * Update an existing entry on a glossary. + * + * @param glossaryId Glossary ID. + * @param entryId Entry ID. + * @param concept Glossary entry concept. + * @param definition Glossary entry concept definition. + * @param options Options for the entry. + * @param siteId Site ID. If not defined, current site. + */ + async updateEntry( + glossaryId: number, + entryId: number, + concept: string, + definition: string, + options?: Record, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModGlossaryUpdateEntryWSParams = { + entryid: entryId, + concept: concept, + definition: definition, + definitionformat: 1, + options: CoreUtils.objectToArrayOfObjects(options || {}, 'name', 'value'), + }; + + const response = await site.write('mod_glossary_update_entry', params); + + if (!response.result) { + throw new CoreError(response.warnings?.[0].message ?? 'Error updating entry'); + } + + CoreEvents.trigger(GLOSSARY_ENTRY_UPDATED, { glossaryId, entryId }, siteId); + } + /** * Delete entry. * @@ -1339,6 +1388,35 @@ export type AddonModGlossaryAddEntryWSResponse = { warnings?: CoreWSExternalWarning[]; }; +/** + * Params of mod_glossary_update_entry WS. + */ +export type AddonModGlossaryUpdateEntryWSParams = { + entryid: number; // Glossary entry id to update. + 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_update_entry WS. + */ +export type AddonModGlossaryUpdateEntryWSResponse = { + result: boolean; // The update result. + warnings?: CoreWSExternalWarning[]; +}; + /** * Params of mod_glossary_view_glossary WS. */ diff --git a/src/addons/mod/glossary/tests/behat/basic_usage.feature b/src/addons/mod/glossary/tests/behat/basic_usage.feature index a2f249ffe..1a271896e 100644 --- a/src/addons/mod/glossary/tests/behat/basic_usage.feature +++ b/src/addons/mod/glossary/tests/behat/basic_usage.feature @@ -157,7 +157,39 @@ Feature: Test basic usage of glossary in app Scenario: Edit entries (basic info) Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app - # TODO online + # Online + When I press "Add a new entry" in the app + And I set the following fields to these values in the app: + | Concept | Cashew | + | Definition | Cashew is a fruit | + And I press "Save" in the app + Then I should find "Cashew" in the app + + When I press "Cashew" in the app + And I press "Edit entry" in the app + Then the field "Concept" matches value "Cashew" in the app + And the field "Definition" matches value "Cashew is a fruit" in the app + + When I set the following fields to these values in the app: + | Concept | Coconut | + | Definition | Coconut is a fruit | + And I press "This entry should be automatically linked" "ion-toggle" in the app + And I press "This entry is case sensitive" "ion-toggle" in the app + And I press "Match whole words only" "ion-toggle" in the app + And I press "Save" in the app + Then I should find "Coconut is a fruit" in the app + But I should not find "Cashew is a fruit" in the app + + When I press "Edit entry" in the app + Then "This entry should be automatically linked" "ion-toggle" should be selected in the app + And "This entry is case sensitive" "ion-toggle" should be selected in the app + And "Match whole words only" "ion-toggle" should be selected in the app + + When I press "Save" in the app + And I press the back button in the app + Then I should find "Coconut" in the app + And I should find "Potato" in the app + But I should not find "Cashew" in the app # Offline When I press "Add a new entry" in the app From d2d8a814f62b67ba98a2f84b50fc49403eb3b59e Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 30 Mar 2023 12:39:00 +0200 Subject: [PATCH 09/10] MOBILE-2652 glossary: Edit attachments --- gulpfile.js | 11 ++- .../tests/behat/behat_app.php | 64 +++++++++++++---- scripts/build-behat-plugin.js | 33 +++++++-- src/addons/mod/glossary/pages/edit/edit.ts | 17 ++++- .../mod/glossary/pages/entry/entry.html | 4 ++ src/addons/mod/glossary/pages/entry/entry.ts | 9 +++ src/addons/mod/glossary/services/glossary.ts | 9 +++ .../glossary/tests/behat/basic_usage.feature | 70 ++++++++++++++----- .../glossary/tests/behat/fixtures/stub1.txt | 1 + .../glossary/tests/behat/fixtures/stub2.txt | 1 + .../glossary/tests/behat/fixtures/stub3.txt | 1 + src/testing/services/behat-dom.ts | 4 +- src/testing/services/behat-runtime.ts | 38 +++++++++- 13 files changed, 220 insertions(+), 42 deletions(-) create mode 100644 src/addons/mod/glossary/tests/behat/fixtures/stub1.txt create mode 100644 src/addons/mod/glossary/tests/behat/fixtures/stub2.txt create mode 100644 src/addons/mod/glossary/tests/behat/fixtures/stub3.txt diff --git a/gulpfile.js b/gulpfile.js index d7098d9b7..295e4a6df 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -71,5 +71,14 @@ gulp.task('watch', () => { }); gulp.task('watch-behat', () => { - gulp.watch(['./src/**/*.feature', './src/**/*.png', './local_moodleappbehat'], { interval: 500 }, gulp.parallel('behat')); + gulp.watch( + [ + './src/**/*.feature', + './src/**/tests/behat/fixtures/**', + './src/**/tests/behat/snapshots/**', + './local_moodleappbehat', + ], + { interval: 500 }, + gulp.parallel('behat') + ); }); diff --git a/local_moodleappbehat/tests/behat/behat_app.php b/local_moodleappbehat/tests/behat/behat_app.php index 04395fbaf..67776a49f 100644 --- a/local_moodleappbehat/tests/behat/behat_app.php +++ b/local_moodleappbehat/tests/behat/behat_app.php @@ -44,27 +44,21 @@ class behat_app extends behat_app_helper { ], ]; + protected $featurepath = ''; protected $windowsize = '360x720'; /** * @BeforeScenario */ public function before_scenario(ScenarioScope $scope) { - if (!$scope->getFeature()->hasTag('app')) { + $feature = $scope->getFeature(); + + if (!$feature->hasTag('app')) { return; } - global $CFG; - - $performanceLogs = $CFG->behat_profiles['default']['capabilities']['extra_capabilities']['goog:loggingPrefs']['performance'] ?? null; - - if ($performanceLogs !== 'ALL') { - return; - } - - // Enable DB Logging only for app tests with performance logs activated. - $this->getSession()->visit($this->get_app_url() . '/assets/env.json'); - $this->execute_script("document.cookie = 'MoodleAppDBLoggingEnabled=true;path=/';"); + $this->featurepath = dirname($feature->getFile()); + $this->configure_performance_logs(); } /** @@ -89,6 +83,23 @@ class behat_app extends behat_app_helper { $this->enter_site(); } + /** + * Configure performance logs. + */ + protected function configure_performance_logs() { + global $CFG; + + $performanceLogs = $CFG->behat_profiles['default']['capabilities']['extra_capabilities']['goog:loggingPrefs']['performance'] ?? null; + + if ($performanceLogs !== 'ALL') { + return; + } + + // Enable DB Logging only for app tests with performance logs activated. + $this->getSession()->visit($this->get_app_url() . '/assets/env.json'); + $this->execute_script("document.cookie = 'MoodleAppDBLoggingEnabled=true;path=/';"); + } + /** * Check whether the current page is the login form. */ @@ -778,6 +789,35 @@ class behat_app extends behat_app_helper { } } + /** + * Uploads a file to a file input, the file path should be relative to a fixtures folder next to the feature file. + * The ìnput locator can match a container with a file input inside, it doesn't have to be the input itself. + * + * @Given /^I upload "((?:[^"]|\\")+)" to (".+") in the app$/ + * @param string $filename + * @param string $inputlocator + */ + public function i_upload_a_file_in_the_app(string $filename, string $inputlocator) { + $filepath = str_replace('/', DIRECTORY_SEPARATOR, "{$this->featurepath}/fixtures/$filename"); + $inputlocator = $this->parse_element_locator($inputlocator); + + $id = $this->spin(function() use ($inputlocator) { + $result = $this->runtime_js("getFileInputId($inputlocator)"); + + if (str_starts_with($result, 'ERROR')) { + throw new DriverException('Error finding input - ' . $result); + } + + return $result; + }); + + $this->wait_for_pending_js(); + + $fileinput = $this ->getSession()->getPage()->findById($id); + + $fileinput->attachFile($filepath); + } + /** * Checks a field matches a certain value in the app. * diff --git a/scripts/build-behat-plugin.js b/scripts/build-behat-plugin.js index 621ad007f..04daa3e94 100755 --- a/scripts/build-behat-plugin.js +++ b/scripts/build-behat-plugin.js @@ -33,7 +33,7 @@ async function main() { : []; if (!existsSync(pluginPath)) { - mkdirSync(pluginPath); + mkdirSync(pluginPath, { recursive: true }); } else { // Empty directory, except the excluding list. const excludeFromErase = [ @@ -76,21 +76,29 @@ async function main() { }; writeFileSync(pluginFilePath, replaceArguments(fileContents, replacements)); - // Copy feature and snapshot files. + // Copy features, snapshots, and fixtures. if (!excludeFeatures) { const behatTempFeaturesPath = `${pluginPath}/behat-tmp`; copySync(projectPath('src'), behatTempFeaturesPath, { filter: shouldCopyFileOrDirectory }); const behatFeaturesPath = `${pluginPath}/tests/behat`; if (!existsSync(behatFeaturesPath)) { - mkdirSync(behatFeaturesPath, {recursive: true}); + mkdirSync(behatFeaturesPath, { recursive: true }); } for await (const file of getDirectoryFiles(behatTempFeaturesPath)) { const filePath = dirname(file); + const snapshotsIndex = file.indexOf('/tests/behat/snapshots/'); + const fixturesIndex = file.indexOf('/tests/behat/fixtures/'); - if (filePath.endsWith('/tests/behat/snapshots')) { - renameSync(file, behatFeaturesPath + '/snapshots/' + basename(file)); + if (snapshotsIndex !== -1) { + moveFile(file, behatFeaturesPath + '/snapshots/' + file.slice(snapshotsIndex + 23)); + + continue; + } + + if (fixturesIndex !== -1) { + moveFile(file, behatFeaturesPath + '/fixtures/' + file.slice(fixturesIndex + 22)); continue; } @@ -103,7 +111,7 @@ async function main() { const searchRegExp = /\//g; const prefix = relative(behatTempFeaturesPath, newPath).replace(searchRegExp,'-') || 'core'; const featureFilename = prefix + '-' + basename(file); - renameSync(file, behatFeaturesPath + '/' + featureFilename); + moveFile(file, behatFeaturesPath + '/' + featureFilename); } rmSync(behatTempFeaturesPath, {recursive: true}); @@ -115,7 +123,8 @@ function shouldCopyFileOrDirectory(path) { return stats.isDirectory() || extname(path) === '.feature' - || extname(path) === '.png'; + || path.includes('/tests/behat/snapshots') + || path.includes('/tests/behat/fixtures'); } function isExcluded(file, exclusions) { @@ -127,6 +136,16 @@ function fail(message) { process.exit(1); } +function moveFile(from, to) { + const targetDir = dirname(to); + + if (!existsSync(targetDir)) { + mkdirSync(targetDir, { recursive: true }); + } + + renameSync(from, to); +} + function guessPluginPath() { if (process.env.MOODLE_APP_BEHAT_PLUGIN_PATH) { return process.env.MOODLE_APP_BEHAT_PLUGIN_PATH; diff --git a/src/addons/mod/glossary/pages/edit/edit.ts b/src/addons/mod/glossary/pages/edit/edit.ts index d550fd417..ec8ef84d8 100644 --- a/src/addons/mod/glossary/pages/edit/edit.ts +++ b/src/addons/mod/glossary/pages/edit/edit.ts @@ -419,6 +419,7 @@ class AddonModGlossaryOfflineFormHandler extends AddonModGlossaryFormHandler { * @inheritdoc */ async save(glossary: AddonModGlossaryGlossary): Promise { + const originalData = this.page.data; const data = this.page.data; // Upload attachments first if any. @@ -428,6 +429,10 @@ class AddonModGlossaryOfflineFormHandler extends AddonModGlossaryFormHandler { offlineAttachments = await this.storeAttachments(glossary, data.timecreated); } + if (originalData.concept !== data.concept) { + await AddonModGlossaryHelper.deleteStoredFiles(glossary.id, originalData.concept, data.timecreated); + } + // Save entry data. await this.updateOfflineEntry(glossary, offlineAttachments); @@ -653,8 +658,18 @@ class AddonModGlossaryOnlineFormHandler extends AddonModGlossaryFormHandler { const options = this.getSaveOptions(glossary); const definition = CoreTextUtils.formatHtmlLines(data.definition); + // Upload attachments, if any. + let attachmentsId: number | undefined = undefined; + + if (data.attachments.length) { + attachmentsId = await this.uploadAttachments(glossary); + } + // Save entry data. - await AddonModGlossary.updateEntry(glossary.id, this.entry.id, data.concept, definition, options); + await AddonModGlossary.updateEntry(glossary.id, this.entry.id, data.concept, definition, options, attachmentsId); + + // Delete the local files from the tmp folder. + CoreFileUploader.clearTmpFiles(data.attachments); CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' }); diff --git a/src/addons/mod/glossary/pages/entry/entry.html b/src/addons/mod/glossary/pages/entry/entry.html index b0de28204..aa50fb45b 100644 --- a/src/addons/mod/glossary/pages/entry/entry.html +++ b/src/addons/mod/glossary/pages/entry/entry.html @@ -73,6 +73,10 @@ [componentId]="componentId">
+
+ + +
{{ 'core.tag.tags' | translate }}:
diff --git a/src/addons/mod/glossary/pages/entry/entry.ts b/src/addons/mod/glossary/pages/entry/entry.ts index c1649d67d..ccbcedea7 100644 --- a/src/addons/mod/glossary/pages/entry/entry.ts +++ b/src/addons/mod/glossary/pages/entry/entry.ts @@ -23,6 +23,7 @@ import { CoreCommentsCommentsComponent } from '@features/comments/components/com import { CoreComments } from '@features/comments/services/comments'; import { CoreRatingInfo } from '@features/rating/services/rating'; import { CoreTag } from '@features/tag/services/tag'; +import { FileEntry } from '@ionic-native/file/ngx'; import { IonRefresher } from '@ionic/angular'; import { CoreNavigator } from '@services/navigator'; import { CoreNetwork } from '@services/network'; @@ -54,6 +55,7 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { componentId?: number; onlineEntry?: AddonModGlossaryEntry; offlineEntry?: AddonModGlossaryOfflineEntry; + offlineEntryFiles?: FileEntry[]; entries!: AddonModGlossaryEntryEntriesSwipeManager; glossary?: AddonModGlossaryGlossary; entryUpdatedObserver?: CoreEventObserver; @@ -263,6 +265,13 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { const glossary = await this.loadGlossary(); this.offlineEntry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, timecreated); + this.offlineEntryFiles = this.offlineEntry.attachments && this.offlineEntry.attachments.offline > 0 + ? await AddonModGlossaryHelper.getStoredFiles( + glossary.id, + this.offlineEntry.concept, + timecreated, + ) + : undefined; this.canEdit = true; this.canDelete = true; } catch (error) { diff --git a/src/addons/mod/glossary/services/glossary.ts b/src/addons/mod/glossary/services/glossary.ts index 012a4c1d0..487544639 100644 --- a/src/addons/mod/glossary/services/glossary.ts +++ b/src/addons/mod/glossary/services/glossary.ts @@ -930,6 +930,7 @@ export class AddonModGlossaryProvider { * @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. */ async updateEntry( @@ -938,6 +939,7 @@ export class AddonModGlossaryProvider { concept: string, definition: string, options?: Record, + attachId?: number, siteId?: string, ): Promise { const site = await CoreSites.getSite(siteId); @@ -950,6 +952,13 @@ export class AddonModGlossaryProvider { options: CoreUtils.objectToArrayOfObjects(options || {}, 'name', 'value'), }; + if (attachId) { + params.options?.push({ + name: 'attachmentsid', + value: String(attachId), + }); + } + const response = await site.write('mod_glossary_update_entry', params); if (!response.result) { diff --git a/src/addons/mod/glossary/tests/behat/basic_usage.feature b/src/addons/mod/glossary/tests/behat/basic_usage.feature index 1a271896e..f4a4f9862 100644 --- a/src/addons/mod/glossary/tests/behat/basic_usage.feature +++ b/src/addons/mod/glossary/tests/behat/basic_usage.feature @@ -154,42 +154,51 @@ Feature: Test basic usage of glossary in app Then I should find "Garlic" in the app And I should find "Allium sativum" in the app - Scenario: Edit entries (basic info) + Scenario: Edit entries Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app # Online - When I press "Add a new entry" in the app - And I set the following fields to these values in the app: - | Concept | Cashew | - | Definition | Cashew is a fruit | - And I press "Save" in the app - Then I should find "Cashew" in the app - - When I press "Cashew" in the app + When I press "Cucumber" in the app And I press "Edit entry" in the app - Then the field "Concept" matches value "Cashew" in the app - And the field "Definition" matches value "Cashew is a fruit" in the app + Then the field "Concept" matches value "Cucumber" in the app + And the field "Definition" matches value "Sweet cucumber" in the app When I set the following fields to these values in the app: | Concept | Coconut | | Definition | Coconut is a fruit | + And I press "Add file" in the app + And I upload "stub1.txt" to "File" ".action-sheet-button" in the app + And I press "Add file" in the app + And I upload "stub2.txt" to "File" ".action-sheet-button" in the app And I press "This entry should be automatically linked" "ion-toggle" in the app And I press "This entry is case sensitive" "ion-toggle" in the app And I press "Match whole words only" "ion-toggle" in the app And I press "Save" in the app Then I should find "Coconut is a fruit" in the app - But I should not find "Cashew is a fruit" in the app + And I should find "stub1.txt" in the app + And I should find "stub2.txt" in the app + But I should not find "Cucumber is a fruit" in the app When I press "Edit entry" in the app - Then "This entry should be automatically linked" "ion-toggle" should be selected in the app + Then I should find "stub1.txt" in the app + And I should find "stub2.txt" in the app + And "This entry should be automatically linked" "ion-toggle" should be selected in the app And "This entry is case sensitive" "ion-toggle" should be selected in the app And "Match whole words only" "ion-toggle" should be selected in the app - When I press "Save" in the app - And I press the back button in the app + When I press "Delete" within "stub2.txt" "ion-item" in the app + And I press "Delete" near "Are you sure you want to delete this file?" in the app + And I press "Add file" in the app + And I upload "stub3.txt" to "File" ".action-sheet-button" in the app + And I press "Save" in the app + Then I should find "stub1.txt" in the app + And I should find "stub3.txt" in the app + But I should not find "stub2.txt" in the app + + When I press the back button in the app Then I should find "Coconut" in the app And I should find "Potato" in the app - But I should not find "Cashew" in the app + But I should not find "Cucumber" in the app # Offline When I press "Add a new entry" in the app @@ -197,6 +206,10 @@ Feature: Test basic usage of glossary in app And I set the following fields to these values in the app: | Concept | Broccoli | | Definition | Brassica oleracea var. italica | + And I press "Add file" in the app + And I upload "stub1.txt" to "File" ".action-sheet-button" in the app + And I press "Add file" in the app + And I upload "stub2.txt" to "File" ".action-sheet-button" in the app And I press "This entry should be automatically linked" "ion-toggle" in the app And I press "This entry is case sensitive" "ion-toggle" in the app And I press "Match whole words only" "ion-toggle" in the app @@ -206,10 +219,14 @@ Feature: Test basic usage of glossary in app When I press "Broccoli" in the app Then I should find "Brassica oleracea var. italica" in the app + And I should find "stub1.txt" in the app + And I should find "stub2.txt" in the app When I press "Edit entry" in the app Then the field "Concept" matches value "Broccoli" in the app And the field "Definition" matches value "Brassica oleracea var. italica" in the app + And I should find "stub1.txt" in the app + And I should find "stub2.txt" in the app And "This entry should be automatically linked" "ion-toggle" should be selected in the app And "This entry is case sensitive" "ion-toggle" should be selected in the app And "Match whole words only" "ion-toggle" should be selected in the app @@ -217,15 +234,34 @@ Feature: Test basic usage of glossary in app When I set the following fields to these values in the app: | Concept | Pickle | | Definition | Pickle Rick | + And I press "Delete" within "stub2.txt" "ion-item" in the app + And I press "Delete" near "Are you sure you want to delete this file?" in the app + And I press "Add file" in the app + And I upload "stub3.txt" to "File" ".action-sheet-button" in the app And I press "Save" in the app Then I should find "Pickle Rick" in the app - But I should not find "Brassica oleracea var. italica" in the app + And I should find "stub1.txt" in the app + And I should find "stub3.txt" in the app + But I should not find "stub2.txt" in the app + And I should not find "Brassica oleracea var. italica" in the app When I press the back button in the app Then I should find "Pickle" in the app And I should find "Potato" in the app But I should not find "Broccoli" in the app + When I switch network connection to wifi + And I press "Information" in the app + And I press "Synchronise now" in the app + Then I should not find "This Glossary has offline data to be synchronised" in the app + + When I press "Pickle" in the app + Then I should find "Pickle Rick" in the app + And I should find "stub1.txt" in the app + And I should find "stub3.txt" in the app + But I should not find "stub2.txt" in the app + And I should not find "Brassica oleracea var. italica" in the app + Scenario: Delete entries Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app diff --git a/src/addons/mod/glossary/tests/behat/fixtures/stub1.txt b/src/addons/mod/glossary/tests/behat/fixtures/stub1.txt new file mode 100644 index 000000000..38257d448 --- /dev/null +++ b/src/addons/mod/glossary/tests/behat/fixtures/stub1.txt @@ -0,0 +1 @@ +This is a stub. diff --git a/src/addons/mod/glossary/tests/behat/fixtures/stub2.txt b/src/addons/mod/glossary/tests/behat/fixtures/stub2.txt new file mode 100644 index 000000000..38257d448 --- /dev/null +++ b/src/addons/mod/glossary/tests/behat/fixtures/stub2.txt @@ -0,0 +1 @@ +This is a stub. diff --git a/src/addons/mod/glossary/tests/behat/fixtures/stub3.txt b/src/addons/mod/glossary/tests/behat/fixtures/stub3.txt new file mode 100644 index 000000000..38257d448 --- /dev/null +++ b/src/addons/mod/glossary/tests/behat/fixtures/stub3.txt @@ -0,0 +1 @@ +This is a stub. diff --git a/src/testing/services/behat-dom.ts b/src/testing/services/behat-dom.ts index e2cadf34e..2705a33ac 100644 --- a/src/testing/services/behat-dom.ts +++ b/src/testing/services/behat-dom.ts @@ -421,7 +421,7 @@ export class TestingBehatDomUtilsService { */ findElementBasedOnText( locator: TestingBehatElementLocator, - options: TestingBehatFindOptions, + options: TestingBehatFindOptions = {}, ): HTMLElement | undefined { return this.findElementsBasedOnText(locator, options)[0]; } @@ -437,7 +437,7 @@ export class TestingBehatDomUtilsService { locator: TestingBehatElementLocator, options: TestingBehatFindOptions, ): HTMLElement[] { - const topContainers = this.getCurrentTopContainerElements(options.containerName); + const topContainers = this.getCurrentTopContainerElements(options.containerName ?? ''); let elements: HTMLElement[] = []; for (let i = 0; i < topContainers.length; i++) { diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index e9cb4f2e4..b5d73ce60 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -361,6 +361,40 @@ export class TestingBehatRuntimeService { } } + /** + * Get a file input id, adding it if necessary. + * + * @param locator Input locator. + * @returns Input id if successful, or ERROR: followed by message + */ + async getFileInputId(locator: TestingBehatElementLocator): Promise { + this.log('Action - Upload File', { locator }); + + try { + const inputOrContainer = TestingBehatDomUtils.findElementBasedOnText(locator); + + if (!inputOrContainer) { + return 'ERROR: No element matches input locator.'; + } + + const input = inputOrContainer.matches('input[type="file"]') + ? inputOrContainer + : inputOrContainer.querySelector('input[type="file"]'); + + if (!input) { + return 'ERROR: Input element does not contain a file input.'; + } + + if (!input.hasAttribute('id')) { + input.setAttribute('id', `file-${Date.now()}`); + } + + return input.getAttribute('id') ?? ''; + } catch (error) { + return 'ERROR: ' + error.message; + } + } + /** * Trigger a pull to refresh gesture in the current page. * @@ -635,8 +669,8 @@ export type BehatTestsWindow = Window & { }; export type TestingBehatFindOptions = { - containerName: string; - onlyClickable: boolean; + containerName?: string; + onlyClickable?: boolean; }; export type TestingBehatElementLocator = { From 84d2e9e4da526f844f2a307edfba465a4f240423 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 30 Mar 2023 16:13:56 +0200 Subject: [PATCH 10/10] MOBILE-2652 glossary: Fix categories & aliases These have been removed from online edits because of the limitations described in MDL-77798 --- src/addons/mod/glossary/pages/edit/edit.html | 2 +- src/addons/mod/glossary/pages/edit/edit.ts | 42 +++++++++++++------ .../glossary/tests/behat/basic_usage.feature | 8 ++++ 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/addons/mod/glossary/pages/edit/edit.html b/src/addons/mod/glossary/pages/edit/edit.html index eee439c4a..0af9d5629 100644 --- a/src/addons/mod/glossary/pages/edit/edit.html +++ b/src/addons/mod/glossary/pages/edit/edit.html @@ -39,7 +39,7 @@
- + {{ 'addon.mod_glossary.aliases' | translate }} diff --git a/src/addons/mod/glossary/pages/edit/edit.ts b/src/addons/mod/glossary/pages/edit/edit.ts index ec8ef84d8..e27fb967d 100644 --- a/src/addons/mod/glossary/pages/edit/edit.ts +++ b/src/addons/mod/glossary/pages/edit/edit.ts @@ -59,6 +59,7 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { glossary?: AddonModGlossaryGlossary; definitionControl = new FormControl(); categories: AddonModGlossaryCategory[] = []; + showAliases = true; editorExtraParams: Record = {}; handler!: AddonModGlossaryFormHandler; data: AddonModGlossaryFormData = { @@ -125,10 +126,6 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { await this.handler.loadData(this.glossary); - this.categories = await AddonModGlossary.getAllCategories(this.glossary.id, { - cmId: this.cmId, - }); - this.loaded = true; } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingglossary', true); @@ -272,6 +269,17 @@ abstract class AddonModGlossaryFormHandler { */ abstract save(glossary: AddonModGlossaryGlossary): Promise; + /** + * Load form categories. + * + * @param glossary Glossary. + */ + protected async loadCategories(glossary: AddonModGlossaryGlossary): Promise { + this.page.categories = await AddonModGlossary.getAllCategories(glossary.id, { + cmId: this.page.cmId, + }); + } + /** * Upload attachments online. * @@ -341,10 +349,15 @@ abstract class AddonModGlossaryFormHandler { */ protected getSaveOptions(glossary: AddonModGlossaryGlossary): Record { const data = this.page.data; - const options: Record = { - aliases: data.aliases, - categories: data.categories.join(','), - }; + const options: Record = {}; + + if (this.page.showAliases) { + options.aliases = data.aliases; + } + + if (this.page.categories.length > 0) { + options.categories = data.categories.join(','); + } if (glossary.usedynalink) { options.usedynalink = data.usedynalink ? 1 : 0; @@ -385,8 +398,8 @@ class AddonModGlossaryOfflineFormHandler extends AddonModGlossaryFormHandler { data.timecreated = entry.timecreated; if (entry.options) { - data.categories = (entry.options.categories && ( entry.options.categories).split(',')) || []; - data.aliases = entry.options.aliases || ''; + data.categories = ((entry.options.categories as string)?.split(',') ?? []).map(id => Number(id)); + data.aliases = entry.options.aliases as string ?? ''; data.usedynalink = !!entry.options.usedynalink; if (data.usedynalink) { @@ -413,6 +426,8 @@ class AddonModGlossaryOfflineFormHandler extends AddonModGlossaryFormHandler { }; this.page.definitionControl.setValue(data.definition); + + await this.loadCategories(glossary); } /** @@ -486,8 +501,8 @@ class AddonModGlossaryNewFormHandler extends AddonModGlossaryFormHandler { /** * @inheritdoc */ - async loadData(): Promise { - // There is no data to load, given that this is a new entry. + async loadData(glossary: AddonModGlossaryGlossary): Promise { + await this.loadCategories(glossary); } /** @@ -644,6 +659,7 @@ class AddonModGlossaryOnlineFormHandler extends AddonModGlossaryFormHandler { }; this.page.definitionControl.setValue(data.definition); + this.page.showAliases = false; } /** @@ -686,7 +702,7 @@ type AddonModGlossaryFormData = { definition: string; timecreated: number; attachments: CoreFileEntry[]; - categories: string[]; + categories: number[]; aliases: string; usedynalink: boolean; casesensitive: boolean; diff --git a/src/addons/mod/glossary/tests/behat/basic_usage.feature b/src/addons/mod/glossary/tests/behat/basic_usage.feature index f4a4f9862..4d2e0d270 100644 --- a/src/addons/mod/glossary/tests/behat/basic_usage.feature +++ b/src/addons/mod/glossary/tests/behat/basic_usage.feature @@ -162,6 +162,8 @@ Feature: Test basic usage of glossary in app And I press "Edit entry" in the app Then the field "Concept" matches value "Cucumber" in the app And the field "Definition" matches value "Sweet cucumber" in the app + But I should not find "Keyword(s)" in the app + And I should not find "Categories" in the app When I set the following fields to these values in the app: | Concept | Coconut | @@ -206,6 +208,10 @@ Feature: Test basic usage of glossary in app And I set the following fields to these values in the app: | Concept | Broccoli | | Definition | Brassica oleracea var. italica | + | Keyword(s) | vegetable, healthy | + And I press "Categories" in the app + And I press "The ones I like" in the app + And I press "OK" in the app And I press "Add file" in the app And I upload "stub1.txt" to "File" ".action-sheet-button" in the app And I press "Add file" in the app @@ -225,6 +231,8 @@ Feature: Test basic usage of glossary in app When I press "Edit entry" in the app Then the field "Concept" matches value "Broccoli" in the app And the field "Definition" matches value "Brassica oleracea var. italica" in the app + And the field "Keyword(s)" matches value "vegetable, healthy" in the app + And I should find "The ones I like" in the app And I should find "stub1.txt" in the app And I should find "stub2.txt" in the app And "This entry should be automatically linked" "ion-toggle" should be selected in the app