MOBILE-2652 glossary: Edit offline entries
This commit is contained in:
		
							parent
							
								
									957dece787
								
							
						
					
					
						commit
						3c443a26c4
					
				| @ -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", | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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), | ||||
| ]; | ||||
|  | ||||
| @ -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`, | ||||
|  | ||||
| @ -16,6 +16,7 @@ | ||||
|     "concept": "Concept", | ||||
|     "definition": "Definition", | ||||
|     "deleteentry": "Delete entry", | ||||
|     "editentry": "Edit entry", | ||||
|     "entriestobesynced": "Entries to be synced", | ||||
|     "entry": "Entry", | ||||
|     "entrydeleted": "Entry deleted", | ||||
|  | ||||
| @ -84,10 +84,17 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         try { | ||||
|             const entrySlug = CoreNavigator.getRouteParam<string>('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<void> { | ||||
|         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<void> { | ||||
|         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<number | false> { | ||||
|         const data = this.page.data; | ||||
|         const options = this.getSaveOptions(glossary); | ||||
|         const definition = CoreTextUtils.formatHtmlLines(data.definition); | ||||
|         const entryId = await AddonModGlossary.addEntry( | ||||
|             glossary.id, | ||||
|             data.concept, | ||||
|             definition, | ||||
|             this.page.courseId, | ||||
|             options, | ||||
|             uploadedAttachmentsId, | ||||
|             { | ||||
|                 timeCreated: timecreated, | ||||
|                 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<void> { | ||||
|         const data = this.page.data; | ||||
|         const entry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, this.timecreated); | ||||
| 
 | ||||
|         data.concept = entry.concept || ''; | ||||
|         data.definition = entry.definition || ''; | ||||
|         data.timecreated = entry.timecreated; | ||||
| 
 | ||||
|         if (entry.options) { | ||||
|             data.categories = (entry.options.categories && (<string> entry.options.categories).split(',')) || []; | ||||
|             data.aliases = <string> entry.options.aliases || ''; | ||||
|             data.usedynalink = !!entry.options.usedynalink; | ||||
| 
 | ||||
|             if (data.usedynalink) { | ||||
|                 data.casesensitive = !!entry.options.casesensitive; | ||||
|                 data.fullmatch = !!entry.options.fullmatch; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Treat offline attachments if any.
 | ||||
|         if (entry.attachments?.offline) { | ||||
|             data.attachments = await AddonModGlossaryHelper.getStoredFiles(glossary.id, entry.concept, entry.timecreated); | ||||
|         } | ||||
| 
 | ||||
|         this.page.originalData = { | ||||
|             concept: data.concept, | ||||
|             definition: data.definition, | ||||
|             attachments: data.attachments.slice(), | ||||
|             timecreated: data.timecreated, | ||||
|             categories: data.categories.slice(), | ||||
|             aliases: data.aliases, | ||||
|             usedynalink: data.usedynalink, | ||||
|             casesensitive: data.casesensitive, | ||||
|             fullmatch: data.fullmatch, | ||||
|         }; | ||||
| 
 | ||||
|         this.page.definitionControl.setValue(data.definition); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async save(glossary: AddonModGlossaryGlossary): Promise<boolean> { | ||||
|         const data = this.page.data; | ||||
| 
 | ||||
|         // Upload attachments first if any.
 | ||||
|         let offlineAttachments: CoreFileUploaderStoreFilesResult | undefined = undefined; | ||||
| 
 | ||||
|         if (data.attachments.length) { | ||||
|             offlineAttachments = await this.storeAttachments(glossary, data.timecreated); | ||||
|         } | ||||
| 
 | ||||
|         // Save entry data.
 | ||||
|         await this.updateOfflineEntry(glossary, offlineAttachments); | ||||
| 
 | ||||
|         // Delete the local files from the tmp folder.
 | ||||
|         CoreFileUploader.clearTmpFiles(data.attachments); | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update an offline entry. | ||||
|      * | ||||
|      * @param glossary Glossary. | ||||
|      * @param uploadedAttachments Uploaded attachments. | ||||
|      */ | ||||
|     protected async updateOfflineEntry( | ||||
|         glossary: AddonModGlossaryGlossary, | ||||
|         uploadedAttachments?: CoreFileUploaderStoreFilesResult, | ||||
|     ): Promise<void> { | ||||
|         const originalData = this.page.originalData; | ||||
|         const data = this.page.data; | ||||
|         const options = this.getSaveOptions(glossary); | ||||
|         const definition = CoreTextUtils.formatHtmlLines(data.definition); | ||||
| 
 | ||||
|         if (!originalData) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         await this.checkDuplicates(glossary); | ||||
|         await AddonModGlossaryOffline.updateOfflineEntry( | ||||
|             { | ||||
|                 glossaryid: glossary.id, | ||||
|                 courseid: this.page.courseId, | ||||
|                 concept: originalData.concept, | ||||
|                 timecreated: originalData.timecreated, | ||||
|             }, | ||||
|             data.concept, | ||||
|             definition, | ||||
|             options, | ||||
|             uploadedAttachments, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Helper to manage the form data for creating a new entry. | ||||
|  */ | ||||
| @ -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<void> { | ||||
|         const data = this.page.data; | ||||
|         const options = this.getSaveOptions(glossary); | ||||
|         const definition = CoreTextUtils.formatHtmlLines(data.definition); | ||||
| 
 | ||||
|         await this.checkDuplicates(glossary); | ||||
|         await AddonModGlossaryOffline.addOfflineEntry( | ||||
|             glossary.id, | ||||
|             data.concept, | ||||
|             definition, | ||||
|             this.page.courseId, | ||||
|             timecreated, | ||||
|             options, | ||||
|             uploadedAttachments, | ||||
|             undefined, | ||||
|             undefined, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create an online entry. | ||||
|      * | ||||
|      * @param glossary Glossary. | ||||
|      * @param timecreated Time created. | ||||
|      * @param uploadedAttachmentsId Id of the uploaded attachments. | ||||
|      * @param allowOffline Allow falling back to creating the entry offline. | ||||
|      * @returns Entry id. | ||||
|      */ | ||||
|     protected async createOnlineEntry( | ||||
|         glossary: AddonModGlossaryGlossary, | ||||
|         timecreated: number, | ||||
|         uploadedAttachmentsId?: number, | ||||
|         allowOffline?: boolean, | ||||
|     ): Promise<number | false> { | ||||
|         const data = this.page.data; | ||||
|         const options = this.getSaveOptions(glossary); | ||||
|         const definition = CoreTextUtils.formatHtmlLines(data.definition); | ||||
|         const entryId = await AddonModGlossary.addEntry( | ||||
|             glossary.id, | ||||
|             data.concept, | ||||
|             definition, | ||||
|             this.page.courseId, | ||||
|             options, | ||||
|             uploadedAttachmentsId, | ||||
|             { | ||||
|                 timeCreated: timecreated, | ||||
|                 allowOffline: allowOffline, | ||||
|                 checkDuplicates: !glossary.allowduplicatedentries, | ||||
|             }, | ||||
|         ); | ||||
| 
 | ||||
|         return entryId; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -52,11 +52,16 @@ | ||||
|                     </core-format-text> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item *ngIf="canDelete"> | ||||
|             <ion-item *ngIf="canDelete || canEdit"> | ||||
|                 <div slot="end"> | ||||
|                     <ion-button fill="clear" (click)="deleteEntry()" [attr.aria-label]="'addon.mod_glossary.deleteentry' | translate"> | ||||
|                     <ion-button *ngIf="canDelete" fill="clear" (click)="deleteEntry()" | ||||
|                         [attr.aria-label]="'addon.mod_glossary.deleteentry' | translate"> | ||||
|                         <ion-icon slot="icon-only" name="fas-trash" aria-hidden="true"></ion-icon> | ||||
|                     </ion-button> | ||||
|                     <ion-button *ngIf="canEdit" fill="clear" (click)="editEntry()" | ||||
|                         [attr.aria-label]="'addon.mod_glossary.editentry' | translate"> | ||||
|                         <ion-icon slot="icon-only" name="fas-pen" aria-hidden="true"></ion-icon> | ||||
|                     </ion-button> | ||||
|                 </div> | ||||
|             </ion-item> | ||||
|             <div *ngIf="onlineEntry && onlineEntry.attachment"> | ||||
|  | ||||
| @ -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<void> { | ||||
|         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<string>('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<void> { | ||||
|         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<void> { | ||||
|     protected async loadOfflineEntry(timecreated: number): Promise<void> { | ||||
|         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); | ||||
|  | ||||
| @ -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<void> { | ||||
|     async deleteOfflineEntry(glossaryId: number, timecreated: number, siteId?: string): Promise<void> { | ||||
|         const site = await CoreSites.getSite(siteId); | ||||
| 
 | ||||
|         const conditions: Partial<AddonModGlossaryOfflineEntryDBRecord> = { | ||||
|             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<AddonModGlossaryOfflineEntry> { | ||||
| @ -85,7 +81,6 @@ export class AddonModGlossaryOfflineProvider { | ||||
| 
 | ||||
|         const conditions: Partial<AddonModGlossaryOfflineEntryDBRecord> = { | ||||
|             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<string, AddonModGlossaryEntryOption>, | ||||
|         attachments?: CoreFileUploaderStoreFilesResult, | ||||
|         timecreated?: number, | ||||
|         siteId?: string, | ||||
|         userId?: number, | ||||
|         discardEntry?: AddonModGlossaryDiscardedEntry, | ||||
|     ): Promise<false> { | ||||
|         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<string, AddonModGlossaryEntryOption>, | ||||
|         attachments?: CoreFileUploaderStoreFilesResult, | ||||
|     ): Promise<void> { | ||||
|         const site = await CoreSites.getSite(); | ||||
|         const entry: Omit<AddonModGlossaryOfflineEntryDBRecord, 'courseid'|'glossaryid'|'userid'|'timecreated'> = { | ||||
|             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. | ||||
|      * | ||||
|  | ||||
| @ -285,7 +285,7 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv | ||||
|      */ | ||||
|     protected async deleteAddEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise<void> { | ||||
|         await Promise.all([ | ||||
|             AddonModGlossaryOffline.deleteOfflineEntry(glossaryId, concept, timeCreated, siteId), | ||||
|             AddonModGlossaryOffline.deleteOfflineEntry(glossaryId, timeCreated, siteId), | ||||
|             AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timeCreated, siteId), | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
| @ -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<false> => { | ||||
|             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. | ||||
|  */ | ||||
|  | ||||
| @ -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 | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user