From 9391fe412275acfd443ec0a62fb77431f184afe2 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 29 Mar 2023 15:43:13 +0200 Subject: [PATCH] MOBILE-2652 glossary: Edit online entries --- src/addons/mod/glossary/pages/edit/edit.ts | 79 +++++++++++++++++++ src/addons/mod/glossary/pages/entry/entry.ts | 2 + src/addons/mod/glossary/services/glossary.ts | 78 ++++++++++++++++++ .../glossary/tests/behat/basic_usage.feature | 34 +++++++- 4 files changed, 192 insertions(+), 1 deletion(-) diff --git a/src/addons/mod/glossary/pages/edit/edit.ts b/src/addons/mod/glossary/pages/edit/edit.ts index d2c32c45d..d550fd417 100644 --- a/src/addons/mod/glossary/pages/edit/edit.ts +++ b/src/addons/mod/glossary/pages/edit/edit.ts @@ -16,11 +16,13 @@ import { Component, OnInit, ViewChild, ElementRef, Optional } from '@angular/cor import { FormControl } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { CoreError } from '@classes/errors/error'; +import { CoreNetworkError } from '@classes/errors/network-error'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CanLeave } from '@guards/can-leave'; import { CoreFileEntry } from '@services/file-helper'; import { CoreNavigator } from '@services/navigator'; +import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; @@ -31,6 +33,7 @@ import { CoreForms } from '@singletons/form'; import { AddonModGlossary, AddonModGlossaryCategory, + AddonModGlossaryEntry, AddonModGlossaryEntryOption, AddonModGlossaryGlossary, AddonModGlossaryProvider, @@ -92,6 +95,11 @@ export class AddonModGlossaryEditPage implements OnInit, CanLeave { const timecreated = Number(entrySlug.slice(4)); this.editorExtraParams.timecreated = timecreated; this.handler = new AddonModGlossaryOfflineFormHandler(this, timecreated); + } else if (entrySlug) { + const { entry } = await AddonModGlossary.getEntry(Number(entrySlug)); + + this.editorExtraParams.timecreated = entry.timecreated; + this.handler = new AddonModGlossaryOnlineFormHandler(this, entry); } else { this.handler = new AddonModGlossaryNewFormHandler(this); } @@ -584,6 +592,77 @@ class AddonModGlossaryNewFormHandler extends AddonModGlossaryFormHandler { } +/** + * Helper to manage the form data for an online entry. + */ +class AddonModGlossaryOnlineFormHandler extends AddonModGlossaryFormHandler { + + private entry: AddonModGlossaryEntry; + + constructor(page: AddonModGlossaryEditPage, entry: AddonModGlossaryEntry) { + super(page); + + this.entry = entry; + } + + /** + * @inheritdoc + */ + async loadData(): Promise { + const data = this.page.data; + + data.concept = this.entry.concept; + data.definition = this.entry.definition || ''; + data.timecreated = this.entry.timecreated; + data.usedynalink = this.entry.usedynalink; + + if (data.usedynalink) { + data.casesensitive = this.entry.casesensitive; + data.fullmatch = this.entry.fullmatch; + } + + // Treat offline attachments if any. + if (this.entry.attachments) { + data.attachments = this.entry.attachments; + } + + this.page.originalData = { + concept: data.concept, + definition: data.definition, + attachments: data.attachments.slice(), + timecreated: data.timecreated, + categories: data.categories.slice(), + aliases: data.aliases, + usedynalink: data.usedynalink, + casesensitive: data.casesensitive, + fullmatch: data.fullmatch, + }; + + this.page.definitionControl.setValue(data.definition); + } + + /** + * @inheritdoc + */ + async save(glossary: AddonModGlossaryGlossary): Promise { + if (!CoreNetwork.isOnline()) { + throw new CoreNetworkError(); + } + + const data = this.page.data; + const options = this.getSaveOptions(glossary); + const definition = CoreTextUtils.formatHtmlLines(data.definition); + + // Save entry data. + await AddonModGlossary.updateEntry(glossary.id, this.entry.id, data.concept, definition, options); + + CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' }); + + return true; + } + +} + /** * Form data. */ diff --git a/src/addons/mod/glossary/pages/entry/entry.ts b/src/addons/mod/glossary/pages/entry/entry.ts index 5315d4375..c1649d67d 100644 --- a/src/addons/mod/glossary/pages/entry/entry.ts +++ b/src/addons/mod/glossary/pages/entry/entry.ts @@ -240,10 +240,12 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy { try { const result = await AddonModGlossary.getEntry(entryId); const canDeleteEntries = CoreNetwork.isOnline() && await AddonModGlossary.canDeleteEntries(); + const canUpdateEntries = CoreNetwork.isOnline() && await AddonModGlossary.canUpdateEntries(); this.onlineEntry = result.entry; this.ratingInfo = result.ratinginfo; this.canDelete = canDeleteEntries && !!result.permissions?.candelete; + this.canEdit = canUpdateEntries && !!result.permissions?.canupdate; await this.loadGlossary(); } catch (error) { diff --git a/src/addons/mod/glossary/services/glossary.ts b/src/addons/mod/glossary/services/glossary.ts index fde1157b2..012a4c1d0 100644 --- a/src/addons/mod/glossary/services/glossary.ts +++ b/src/addons/mod/glossary/services/glossary.ts @@ -619,6 +619,18 @@ export class AddonModGlossaryProvider { return site.wsAvailable('mod_glossary_delete_entry'); } + /** + * Check whether the site can update glossary entries. + * + * @param siteId Site id. + * @returns Whether the site can update entries. + */ + async canUpdateEntries(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.wsAvailable('mod_glossary_update_entry'); + } + /** * Performs the whole fetch of the entries using the proper function and arguments. * @@ -910,6 +922,43 @@ export class AddonModGlossaryProvider { return response.entryid; } + /** + * Update an existing entry on a glossary. + * + * @param glossaryId Glossary ID. + * @param entryId Entry ID. + * @param concept Glossary entry concept. + * @param definition Glossary entry concept definition. + * @param options Options for the entry. + * @param siteId Site ID. If not defined, current site. + */ + async updateEntry( + glossaryId: number, + entryId: number, + concept: string, + definition: string, + options?: Record, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModGlossaryUpdateEntryWSParams = { + entryid: entryId, + concept: concept, + definition: definition, + definitionformat: 1, + options: CoreUtils.objectToArrayOfObjects(options || {}, 'name', 'value'), + }; + + const response = await site.write('mod_glossary_update_entry', params); + + if (!response.result) { + throw new CoreError(response.warnings?.[0].message ?? 'Error updating entry'); + } + + CoreEvents.trigger(GLOSSARY_ENTRY_UPDATED, { glossaryId, entryId }, siteId); + } + /** * Delete entry. * @@ -1339,6 +1388,35 @@ export type AddonModGlossaryAddEntryWSResponse = { warnings?: CoreWSExternalWarning[]; }; +/** + * Params of mod_glossary_update_entry WS. + */ +export type AddonModGlossaryUpdateEntryWSParams = { + entryid: number; // Glossary entry id to update. + concept: string; // Glossary concept. + definition: string; // Glossary concept definition. + definitionformat: number; // Definition format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + options?: { // Optional settings. + name: string; // The allowed keys (value format) are: + // inlineattachmentsid (int); the draft file area id for inline attachments + // attachmentsid (int); the draft file area id for attachments + // categories (comma separated int); comma separated category ids + // aliases (comma separated str); comma separated aliases + // usedynalink (bool); whether the entry should be automatically linked. + // casesensitive (bool); whether the entry is case sensitive. + // fullmatch (bool); whether to match whole words only. + value: string | number; // The value of the option (validated inside the function). + }[]; +}; + +/** + * Data returned by mod_glossary_update_entry WS. + */ +export type AddonModGlossaryUpdateEntryWSResponse = { + result: boolean; // The update result. + warnings?: CoreWSExternalWarning[]; +}; + /** * Params of mod_glossary_view_glossary WS. */ diff --git a/src/addons/mod/glossary/tests/behat/basic_usage.feature b/src/addons/mod/glossary/tests/behat/basic_usage.feature index a2f249ffe..1a271896e 100644 --- a/src/addons/mod/glossary/tests/behat/basic_usage.feature +++ b/src/addons/mod/glossary/tests/behat/basic_usage.feature @@ -157,7 +157,39 @@ Feature: Test basic usage of glossary in app Scenario: Edit entries (basic info) Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app - # TODO online + # Online + When I press "Add a new entry" in the app + And I set the following fields to these values in the app: + | Concept | Cashew | + | Definition | Cashew is a fruit | + And I press "Save" in the app + Then I should find "Cashew" in the app + + When I press "Cashew" in the app + And I press "Edit entry" in the app + Then the field "Concept" matches value "Cashew" in the app + And the field "Definition" matches value "Cashew is a fruit" in the app + + When I set the following fields to these values in the app: + | Concept | Coconut | + | Definition | Coconut is a fruit | + And I press "This entry should be automatically linked" "ion-toggle" in the app + And I press "This entry is case sensitive" "ion-toggle" in the app + And I press "Match whole words only" "ion-toggle" in the app + And I press "Save" in the app + Then I should find "Coconut is a fruit" in the app + But I should not find "Cashew is a fruit" in the app + + When I press "Edit entry" in the app + Then "This entry should be automatically linked" "ion-toggle" should be selected in the app + And "This entry is case sensitive" "ion-toggle" should be selected in the app + And "Match whole words only" "ion-toggle" should be selected in the app + + When I press "Save" in the app + And I press the back button in the app + Then I should find "Coconut" in the app + And I should find "Potato" in the app + But I should not find "Cashew" in the app # Offline When I press "Add a new entry" in the app