From ce09ee8a6c0a6800f86046e6ec444135f5417d91 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 28 Mar 2023 13:44:20 +0200 Subject: [PATCH] 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 |