From 3c443a26c4e6d83cfcecbac765708ed1a4118845 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 29 Mar 2023 14:02:26 +0200 Subject: [PATCH] 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