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 |