diff --git a/gulpfile.js b/gulpfile.js
index d7098d9b7..295e4a6df 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -71,5 +71,14 @@ gulp.task('watch', () => {
});
gulp.task('watch-behat', () => {
- gulp.watch(['./src/**/*.feature', './src/**/*.png', './local_moodleappbehat'], { interval: 500 }, gulp.parallel('behat'));
+ gulp.watch(
+ [
+ './src/**/*.feature',
+ './src/**/tests/behat/fixtures/**',
+ './src/**/tests/behat/snapshots/**',
+ './local_moodleappbehat',
+ ],
+ { interval: 500 },
+ gulp.parallel('behat')
+ );
});
diff --git a/local_moodleappbehat/tests/behat/behat_app.php b/local_moodleappbehat/tests/behat/behat_app.php
index 04395fbaf..67776a49f 100644
--- a/local_moodleappbehat/tests/behat/behat_app.php
+++ b/local_moodleappbehat/tests/behat/behat_app.php
@@ -44,27 +44,21 @@ class behat_app extends behat_app_helper {
],
];
+ protected $featurepath = '';
protected $windowsize = '360x720';
/**
* @BeforeScenario
*/
public function before_scenario(ScenarioScope $scope) {
- if (!$scope->getFeature()->hasTag('app')) {
+ $feature = $scope->getFeature();
+
+ if (!$feature->hasTag('app')) {
return;
}
- global $CFG;
-
- $performanceLogs = $CFG->behat_profiles['default']['capabilities']['extra_capabilities']['goog:loggingPrefs']['performance'] ?? null;
-
- if ($performanceLogs !== 'ALL') {
- return;
- }
-
- // Enable DB Logging only for app tests with performance logs activated.
- $this->getSession()->visit($this->get_app_url() . '/assets/env.json');
- $this->execute_script("document.cookie = 'MoodleAppDBLoggingEnabled=true;path=/';");
+ $this->featurepath = dirname($feature->getFile());
+ $this->configure_performance_logs();
}
/**
@@ -89,6 +83,23 @@ class behat_app extends behat_app_helper {
$this->enter_site();
}
+ /**
+ * Configure performance logs.
+ */
+ protected function configure_performance_logs() {
+ global $CFG;
+
+ $performanceLogs = $CFG->behat_profiles['default']['capabilities']['extra_capabilities']['goog:loggingPrefs']['performance'] ?? null;
+
+ if ($performanceLogs !== 'ALL') {
+ return;
+ }
+
+ // Enable DB Logging only for app tests with performance logs activated.
+ $this->getSession()->visit($this->get_app_url() . '/assets/env.json');
+ $this->execute_script("document.cookie = 'MoodleAppDBLoggingEnabled=true;path=/';");
+ }
+
/**
* Check whether the current page is the login form.
*/
@@ -778,6 +789,35 @@ class behat_app extends behat_app_helper {
}
}
+ /**
+ * Uploads a file to a file input, the file path should be relative to a fixtures folder next to the feature file.
+ * The ìnput locator can match a container with a file input inside, it doesn't have to be the input itself.
+ *
+ * @Given /^I upload "((?:[^"]|\\")+)" to (".+") in the app$/
+ * @param string $filename
+ * @param string $inputlocator
+ */
+ public function i_upload_a_file_in_the_app(string $filename, string $inputlocator) {
+ $filepath = str_replace('/', DIRECTORY_SEPARATOR, "{$this->featurepath}/fixtures/$filename");
+ $inputlocator = $this->parse_element_locator($inputlocator);
+
+ $id = $this->spin(function() use ($inputlocator) {
+ $result = $this->runtime_js("getFileInputId($inputlocator)");
+
+ if (str_starts_with($result, 'ERROR')) {
+ throw new DriverException('Error finding input - ' . $result);
+ }
+
+ return $result;
+ });
+
+ $this->wait_for_pending_js();
+
+ $fileinput = $this ->getSession()->getPage()->findById($id);
+
+ $fileinput->attachFile($filepath);
+ }
+
/**
* Checks a field matches a certain value in the app.
*
diff --git a/scripts/build-behat-plugin.js b/scripts/build-behat-plugin.js
index 621ad007f..04daa3e94 100755
--- a/scripts/build-behat-plugin.js
+++ b/scripts/build-behat-plugin.js
@@ -33,7 +33,7 @@ async function main() {
: [];
if (!existsSync(pluginPath)) {
- mkdirSync(pluginPath);
+ mkdirSync(pluginPath, { recursive: true });
} else {
// Empty directory, except the excluding list.
const excludeFromErase = [
@@ -76,21 +76,29 @@ async function main() {
};
writeFileSync(pluginFilePath, replaceArguments(fileContents, replacements));
- // Copy feature and snapshot files.
+ // Copy features, snapshots, and fixtures.
if (!excludeFeatures) {
const behatTempFeaturesPath = `${pluginPath}/behat-tmp`;
copySync(projectPath('src'), behatTempFeaturesPath, { filter: shouldCopyFileOrDirectory });
const behatFeaturesPath = `${pluginPath}/tests/behat`;
if (!existsSync(behatFeaturesPath)) {
- mkdirSync(behatFeaturesPath, {recursive: true});
+ mkdirSync(behatFeaturesPath, { recursive: true });
}
for await (const file of getDirectoryFiles(behatTempFeaturesPath)) {
const filePath = dirname(file);
+ const snapshotsIndex = file.indexOf('/tests/behat/snapshots/');
+ const fixturesIndex = file.indexOf('/tests/behat/fixtures/');
- if (filePath.endsWith('/tests/behat/snapshots')) {
- renameSync(file, behatFeaturesPath + '/snapshots/' + basename(file));
+ if (snapshotsIndex !== -1) {
+ moveFile(file, behatFeaturesPath + '/snapshots/' + file.slice(snapshotsIndex + 23));
+
+ continue;
+ }
+
+ if (fixturesIndex !== -1) {
+ moveFile(file, behatFeaturesPath + '/fixtures/' + file.slice(fixturesIndex + 22));
continue;
}
@@ -103,7 +111,7 @@ async function main() {
const searchRegExp = /\//g;
const prefix = relative(behatTempFeaturesPath, newPath).replace(searchRegExp,'-') || 'core';
const featureFilename = prefix + '-' + basename(file);
- renameSync(file, behatFeaturesPath + '/' + featureFilename);
+ moveFile(file, behatFeaturesPath + '/' + featureFilename);
}
rmSync(behatTempFeaturesPath, {recursive: true});
@@ -115,7 +123,8 @@ function shouldCopyFileOrDirectory(path) {
return stats.isDirectory()
|| extname(path) === '.feature'
- || extname(path) === '.png';
+ || path.includes('/tests/behat/snapshots')
+ || path.includes('/tests/behat/fixtures');
}
function isExcluded(file, exclusions) {
@@ -127,6 +136,16 @@ function fail(message) {
process.exit(1);
}
+function moveFile(from, to) {
+ const targetDir = dirname(to);
+
+ if (!existsSync(targetDir)) {
+ mkdirSync(targetDir, { recursive: true });
+ }
+
+ renameSync(from, to);
+}
+
function guessPluginPath() {
if (process.env.MOODLE_APP_BEHAT_PLUGIN_PATH) {
return process.env.MOODLE_APP_BEHAT_PLUGIN_PATH;
diff --git a/scripts/langindex.json b/scripts/langindex.json
index 0aa311eec..64a23a98d 100644
--- a/scripts/langindex.json
+++ b/scripts/langindex.json
@@ -681,6 +681,7 @@
"addon.mod_forum.yourreply": "forum",
"addon.mod_glossary.addentry": "glossary",
"addon.mod_glossary.aliases": "glossary",
+ "addon.mod_glossary.areyousuredelete": "glossary",
"addon.mod_glossary.attachment": "glossary",
"addon.mod_glossary.browsemode": "local_moodlemobileapp",
"addon.mod_glossary.byalphabet": "local_moodlemobileapp",
@@ -694,9 +695,14 @@
"addon.mod_glossary.categories": "glossary",
"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",
"addon.mod_glossary.entrypendingapproval": "local_moodlemobileapp",
"addon.mod_glossary.entryusedynalink": "glossary",
+ "addon.mod_glossary.errordeleting": "local_moodlemobileapp",
"addon.mod_glossary.errconceptalreadyexists": "glossary",
"addon.mod_glossary.errorloadingentries": "local_moodlemobileapp",
"addon.mod_glossary.errorloadingentry": "local_moodlemobileapp",
diff --git a/src/addons/mod/glossary/classes/glossary-entries-source.ts b/src/addons/mod/glossary/classes/glossary-entries-source.ts
index 7fd2e2814..08d525dc4 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,38 +69,28 @@ 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}`;
}
/**
* @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;
}
/**
@@ -164,21 +142,8 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<
const glossaryId = this.glossary.id;
- this.fetchFunction = (options) => AddonModGlossary.getEntriesBySearch(
- glossaryId,
- query,
- true,
- 'CONCEPT',
- 'ASC',
- options,
- );
- this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesBySearch(
- glossaryId,
- query,
- true,
- 'CONCEPT',
- 'ASC',
- );
+ this.fetchFunction = (options) => AddonModGlossary.getEntriesBySearch(glossaryId, query, true, options);
+ this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesBySearch(glossaryId, query, true);
this.hasSearched = true;
this.setDirty(true);
}
@@ -192,12 +157,14 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<
/**
* Invalidate glossary cache.
+ *
+ * @param invalidateGlossary Whether to invalidate the entire glossary or not
*/
- async invalidateCache(): Promise {
- await Promise.all([
- AddonModGlossary.invalidateCourseGlossaries(this.COURSE_ID),
+ async invalidateCache(invalidateGlossary: boolean = true): Promise {
+ await Promise.all([
this.fetchInvalidate && this.fetchInvalidate(),
- this.glossary && AddonModGlossary.invalidateCategories(this.glossary.id),
+ invalidateGlossary && AddonModGlossary.invalidateCourseGlossaries(this.COURSE_ID),
+ invalidateGlossary && this.glossary && AddonModGlossary.invalidateCategories(this.glossary.id),
]);
}
@@ -220,65 +187,29 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<
case 'author_all':
// Browse by author.
this.viewMode = 'author';
- this.fetchFunction = (options) => AddonModGlossary.getEntriesByAuthor(
- glossaryId,
- 'ALL',
- 'LASTNAME',
- 'ASC',
- options,
- );
- this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByAuthor(
- glossaryId,
- 'ALL',
- 'LASTNAME',
- 'ASC',
- );
+ this.fetchFunction = (options) => AddonModGlossary.getEntriesByAuthor(glossaryId, options);
+ this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByAuthor(glossaryId);
break;
case 'cat_all':
// Browse by category.
this.viewMode = 'cat';
- this.fetchFunction = (options) => AddonModGlossary.getEntriesByCategory(
- glossaryId,
- AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
- options,
- );
- this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByCategory(
- glossaryId,
- AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
- );
+ this.fetchFunction = (options) => AddonModGlossary.getEntriesByCategory(glossaryId, options);
+ this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByCategory(glossaryId);
break;
case 'newest_first':
// Newest first.
this.viewMode = 'date';
- this.fetchFunction = (options) => AddonModGlossary.getEntriesByDate(
- glossaryId,
- 'CREATION',
- 'DESC',
- options,
- );
- this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByDate(
- glossaryId,
- 'CREATION',
- 'DESC',
- );
+ this.fetchFunction = (options) => AddonModGlossary.getEntriesByDate(glossaryId, 'CREATION', options);
+ this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByDate(glossaryId, 'CREATION');
break;
case 'recently_updated':
// Recently updated.
this.viewMode = 'date';
- this.fetchFunction = (options) => AddonModGlossary.getEntriesByDate(
- glossaryId,
- 'UPDATE',
- 'DESC',
- options,
- );
- this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByDate(
- glossaryId,
- 'UPDATE',
- 'DESC',
- );
+ this.fetchFunction = (options) => AddonModGlossary.getEntriesByDate(glossaryId, 'UPDATE', options);
+ this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByDate(glossaryId, 'UPDATE');
break;
case 'letter_all':
@@ -286,15 +217,8 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<
// Consider it is 'letter_all'.
this.viewMode = 'letter';
this.fetchMode = 'letter_all';
- this.fetchFunction = (options) => AddonModGlossary.getEntriesByLetter(
- glossaryId,
- 'ALL',
- options,
- );
- this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByLetter(
- glossaryId,
- 'ALL',
- );
+ this.fetchFunction = (options) => AddonModGlossary.getEntriesByLetter(glossaryId, options);
+ this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByLetter(glossaryId);
break;
}
}
@@ -313,11 +237,10 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<
const entries: AddonModGlossaryEntryItem[] = [];
if (page === 0) {
- const offlineEntries = await AddonModGlossaryOffline.getGlossaryNewEntries(glossary.id);
+ const offlineEntries = await AddonModGlossaryOffline.getGlossaryOfflineEntries(glossary.id);
offlineEntries.sort((a, b) => a.concept.localeCompare(b.concept));
- entries.push(AddonModGlossaryEntriesSource.NEW_ENTRY);
entries.push(...offlineEntries);
}
@@ -369,12 +292,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()">
- 0">
+ 0" class="addon-mod-glossary-index--offline-entries">
{{ '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 6b0acca1c..3ca1498ca 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';
@@ -42,12 +43,15 @@ import {
AddonModGlossaryEntryWithCategory,
AddonModGlossaryGlossary,
AddonModGlossaryProvider,
+ GLOSSARY_ENTRY_ADDED,
+ GLOSSARY_ENTRY_DELETED,
+ GLOSSARY_ENTRY_UPDATED,
} from '../../services/glossary';
import { AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
import {
- AddonModGlossaryAutoSyncData,
- AddonModGlossarySyncProvider,
+ AddonModGlossaryAutoSyncedData,
AddonModGlossarySyncResult,
+ GLOSSARY_AUTO_SYNCED,
} from '../../services/glossary-sync';
import { AddonModGlossaryModuleHandlerService } from '../../services/handlers/module';
import { AddonModGlossaryPrefetchHandler } from '../../services/handlers/prefetch';
@@ -59,6 +63,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 {
@@ -75,13 +80,11 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
protected hasOfflineEntries = false;
protected hasOfflineRatings = false;
- protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
- protected addEntryObserver?: CoreEventObserver;
+ protected syncEventName = GLOSSARY_AUTO_SYNCED;
protected fetchedEntriesCanLoadMore = false;
protected fetchedEntries: AddonModGlossaryEntry[] = [];
protected sourceUnsubscribe?: () => void;
- protected ratingOfflineObserver?: CoreEventObserver;
- protected ratingSyncObserver?: CoreEventObserver;
+ protected observers?: CoreEventObserver[];
protected checkCompletionAfterLog = false; // Use CoreListItemsManager log system instead.
getDivider?: (entry: AddonModGlossaryEntry) => string;
@@ -136,30 +139,48 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
});
// When an entry is added, we reload the data.
- this.addEntryObserver = CoreEvents.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, (data) => {
- if (this.glossary && this.glossary.id === data.glossaryId) {
- this.showLoadingAndRefresh(false);
+ this.observers = [
+ CoreEvents.on(GLOSSARY_ENTRY_ADDED, ({ glossaryId }) => {
+ if (this.glossary?.id !== glossaryId) {
+ return;
+ }
// Check completion since it could be configured to complete once the user adds a new entry.
this.checkCompletion();
- }
- });
+
+ 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;
+ }
+
+ this.showLoadingAndRefresh(false);
+ }),
+ ];
// Listen for offline ratings saved and synced.
- this.ratingOfflineObserver = CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => {
+ this.observers.push(CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => {
if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
&& data.instanceId == this.glossary.coursemodule) {
this.hasOfflineRatings = true;
this.hasOffline = true;
}
- });
- this.ratingSyncObserver = CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => {
+ }));
+ this.observers.push(CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => {
if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
&& data.instanceId == this.glossary.coursemodule) {
this.hasOfflineRatings = false;
this.hasOffline = this.hasOfflineEntries;
}
- });
+ }));
}
/**
@@ -227,7 +248,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
* @param syncEventData Data receiven on sync observer.
* @returns True if refresh is needed, false otherwise.
*/
- protected isRefreshSyncNeeded(syncEventData: AddonModGlossaryAutoSyncData): boolean {
+ protected isRefreshSyncNeeded(syncEventData: AddonModGlossaryAutoSyncedData): boolean {
return !!this.glossary && syncEventData.glossaryId == this.glossary.id &&
syncEventData.userId == CoreSites.getCurrentSiteUserId();
}
@@ -388,7 +409,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',
+ );
}
/**
@@ -410,9 +435,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
ngOnDestroy(): void {
super.ngOnDestroy();
- this.addEntryObserver?.off();
- this.ratingOfflineObserver?.off();
- this.ratingSyncObserver?.off();
+ this.observers?.forEach(observer => observer.off());
this.sourceUnsubscribe?.call(null);
this.entries?.destroy();
}
diff --git a/src/addons/mod/glossary/glossary-lazy.module.ts b/src/addons/mod/glossary/glossary-lazy.module.ts
index c96ce6e7d..7d74f30f7 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,22 @@ 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),
+ },
+ {
+ path: ':courseId/:cmId/entry/:entrySlug/edit',
+ loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule),
+ },
...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile),
...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet),
];
diff --git a/src/addons/mod/glossary/glossary.module.ts b/src/addons/mod/glossary/glossary.module.ts
index 82def7f99..da86adf1c 100644
--- a/src/addons/mod/glossary/glossary.module.ts
+++ b/src/addons/mod/glossary/glossary.module.ts
@@ -49,50 +49,40 @@ 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}/` },
+ },
+ {
+ 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/: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 ba4329f33..c380778c6 100644
--- a/src/addons/mod/glossary/lang.json
+++ b/src/addons/mod/glossary/lang.json
@@ -1,6 +1,7 @@
{
"addentry": "Add a new entry",
"aliases": "Keyword(s)",
+ "areyousuredelete": "Are you sure you want to delete this entry?",
"attachment": "Attachment",
"browsemode": "Browse entries",
"byalphabet": "Alphabetically",
@@ -14,10 +15,15 @@
"categories": "Categories",
"concept": "Concept",
"definition": "Definition",
+ "deleteentry": "Delete entry",
+ "editentry": "Edit entry",
"entriestobesynced": "Entries to be synced",
+ "entry": "Entry",
+ "entrydeleted": "Entry deleted",
"entrypendingapproval": "This entry is pending approval.",
"entryusedynalink": "This entry should be automatically linked",
"errconceptalreadyexists": "This concept already exists. No duplicates allowed in this glossary.",
+ "errordeleting": "Error deleting entry.",
"errorloadingentries": "An error occurred while loading entries.",
"errorloadingentry": "An error occurred while loading the entry.",
"errorloadingglossary": "An error occurred while loading the glossary.",
diff --git a/src/addons/mod/glossary/pages/edit/edit.html b/src/addons/mod/glossary/pages/edit/edit.html
index 8850c93d3..0af9d5629 100644
--- a/src/addons/mod/glossary/pages/edit/edit.html
+++ b/src/addons/mod/glossary/pages/edit/edit.html
@@ -11,12 +11,12 @@
-
+
diff --git a/src/addons/mod/glossary/pages/edit/edit.ts b/src/addons/mod/glossary/pages/edit/edit.ts
index 334672856..e27fb967d 100644
--- a/src/addons/mod/glossary/pages/edit/edit.ts
+++ b/src/addons/mod/glossary/pages/edit/edit.ts
@@ -12,16 +12,17 @@
// 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 { 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 { FileEntry } from '@ionic-native/file/ngx';
+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';
@@ -29,15 +30,12 @@ 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,
+ AddonModGlossaryEntry,
AddonModGlossaryEntryOption,
AddonModGlossaryGlossary,
- AddonModGlossaryNewEntry,
- AddonModGlossaryNewEntryWithFiles,
AddonModGlossaryProvider,
} from '../../services/glossary';
import { AddonModGlossaryHelper } from '../../services/glossary-helper';
@@ -50,7 +48,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;
@@ -59,32 +57,28 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
courseId!: number;
loaded = false;
glossary?: AddonModGlossaryGlossary;
- attachments: FileEntry[] = [];
definitionControl = new FormControl();
categories: AddonModGlossaryCategory[] = [];
+ showAliases = true;
editorExtraParams: Record = {};
- entry: AddonModGlossaryNewEntry = {
+ handler!: AddonModGlossaryFormHandler;
+ data: AddonModGlossaryFormData = {
concept: '',
definition: '',
timecreated: 0,
- };
-
- entries?: AddonModGlossaryEditEntriesSwipeManager;
-
- options = {
- categories: [],
+ attachments: [],
+ categories: [],
aliases: '',
usedynalink: false,
casesensitive: false,
fullmatch: false,
};
- protected timecreated!: number;
- protected concept = '';
+ originalData?: AddonModGlossaryFormData;
+
protected syncId?: string;
protected syncObserver?: CoreEventObserver;
protected isDestroyed = false;
- protected originalData?: AddonModGlossaryNewEntryWithFiles;
protected saved = false;
constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {}
@@ -94,22 +88,21 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
*/
async ngOnInit(): Promise {
try {
- const routeData = this.route.snapshot.data;
+ const entrySlug = CoreNavigator.getRouteParam('entrySlug');
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
- this.timecreated = CoreNavigator.getRequiredRouteNumberParam('timecreated');
- this.concept = CoreNavigator.getRouteParam('concept') || '';
- this.editorExtraParams.timecreated = this.timecreated;
- if (this.timecreated !== 0 && (routeData.swipeEnabled ?? true)) {
- const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
- AddonModGlossaryEntriesSource,
- [this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
- );
+ if (entrySlug?.startsWith('new-')) {
+ 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.entries = new AddonModGlossaryEditEntriesSwipeManager(source);
-
- await this.entries.start();
+ this.editorExtraParams.timecreated = entry.timecreated;
+ this.handler = new AddonModGlossaryOnlineFormHandler(this, entry);
+ } else {
+ this.handler = new AddonModGlossaryNewFormHandler(this);
}
} catch (error) {
CoreDomUtils.showErrorModal(error);
@@ -122,13 +115,6 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
this.fetchData();
}
- /**
- * @inheritdoc
- */
- ngOnDestroy(): void {
- this.entries?.destroy();
- }
-
/**
* Fetch required data.
*
@@ -138,13 +124,7 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
try {
this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.cmId);
- if (this.timecreated > 0) {
- await this.loadOfflineData();
- }
-
- this.categories = await AddonModGlossary.getAllCategories(this.glossary.id, {
- cmId: this.cmId,
- });
+ await this.handler.loadData(this.glossary);
this.loaded = true;
} catch (error) {
@@ -154,64 +134,21 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
}
}
- /**
- * Load offline data when editing an offline entry.
- *
- * @returns Promise resolved when done.
- */
- protected async loadOfflineData(): Promise {
- if (!this.glossary) {
- return;
- }
-
- const entry = await AddonModGlossaryOffline.getNewEntry(this.glossary.id, this.concept, this.timecreated);
-
- this.entry.concept = entry.concept || '';
- this.entry.definition = entry.definition || '';
- this.entry.timecreated = entry.timecreated;
-
- this.originalData = {
- concept: this.entry.concept,
- definition: this.entry.definition,
- files: [],
- timecreated: entry.timecreated,
- };
-
- if (entry.options) {
- this.options.categories = (entry.options.categories && ( entry.options.categories).split(',')) || [];
- this.options.aliases = entry.options.aliases || '';
- this.options.usedynalink = !!entry.options.usedynalink;
- if (this.options.usedynalink) {
- this.options.casesensitive = !!entry.options.casesensitive;
- this.options.fullmatch = !!entry.options.fullmatch;
- }
- }
-
- // Treat offline attachments if any.
- if (entry.attachments?.offline) {
- this.attachments = await AddonModGlossaryHelper.getStoredFiles(this.glossary.id, entry.concept, entry.timecreated);
-
- this.originalData.files = this.attachments.slice();
- }
-
- this.definitionControl.setValue(this.entry.definition);
- }
-
/**
* Reset the form data.
*/
protected resetForm(): void {
- this.entry.concept = '';
- this.entry.definition = '';
- this.entry.timecreated = 0;
this.originalData = undefined;
- this.options.categories = [];
- this.options.aliases = '';
- this.options.usedynalink = false;
- this.options.casesensitive = false;
- this.options.fullmatch = false;
- this.attachments.length = 0; // Empty the array.
+ this.data.concept = '';
+ this.data.definition = '';
+ this.data.timecreated = 0;
+ this.data.categories = [];
+ this.data.aliases = '';
+ this.data.usedynalink = false;
+ this.data.casesensitive = false;
+ this.data.fullmatch = false;
+ this.data.attachments.length = 0; // Empty the array.
this.definitionControl.setValue('');
}
@@ -222,7 +159,7 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
* @param text The new text.
*/
onDefinitionChange(text: string): void {
- this.entry.definition = text;
+ this.data.definition = text;
}
/**
@@ -235,13 +172,13 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
return true;
}
- if (AddonModGlossaryHelper.hasEntryDataChanged(this.entry, this.attachments, this.originalData)) {
+ if (this.hasDataChanged()) {
// Show confirmation if some data has been modified.
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
}
// Delete the local files from the tmp folder.
- CoreFileUploader.clearTmpFiles(this.attachments);
+ CoreFileUploader.clearTmpFiles(this.data.attachments);
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
@@ -252,114 +189,26 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
* Save the entry.
*/
async save(): Promise {
- let definition = this.entry.definition;
- let entryId: number | undefined;
- const timecreated = this.entry.timecreated || Date.now();
-
- if (!this.entry.concept || !definition) {
+ if (!this.data.concept || !this.data.definition) {
CoreDomUtils.showErrorModal('addon.mod_glossary.fillfields', true);
return;
}
+ if (!this.glossary) {
+ return;
+ }
+
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
- definition = CoreTextUtils.formatHtmlLines(definition);
try {
- if (!this.glossary) {
- return;
- }
+ const savedOnline = await this.handler.save(this.glossary);
- // Upload attachments first if any.
- const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated);
+ this.saved = true;
- const options: Record = {
- aliases: this.options.aliases,
- categories: this.options.categories.join(','),
- };
+ CoreForms.triggerFormSubmittedEvent(this.formElement, savedOnline, CoreSites.getCurrentSiteId());
- if (this.glossary.usedynalink) {
- options.usedynalink = this.options.usedynalink ? 1 : 0;
- if (this.options.usedynalink) {
- options.casesensitive = this.options.casesensitive ? 1 : 0;
- options.fullmatch = this.options.fullmatch ? 1 : 0;
- }
- }
-
- if (saveOffline) {
- if (this.entry && !this.glossary.allowduplicatedentries) {
- // Check if the entry is duplicated in online or offline mode.
- const isUsed = await AddonModGlossary.isConceptUsed(this.glossary.id, this.entry.concept, {
- timeCreated: this.entry.timecreated,
- cmId: this.cmId,
- });
-
- if (isUsed) {
- // There's a entry with same name, reject with error message.
- throw new CoreError(Translate.instant('addon.mod_glossary.errconceptalreadyexists'));
- }
- }
-
- // Save entry in offline.
- await AddonModGlossaryOffline.addNewEntry(
- this.glossary.id,
- this.entry.concept,
- definition,
- this.courseId,
- options,
- attachmentsResult,
- timecreated,
- undefined,
- undefined,
- this.entry,
- );
- } else {
- // Try to send it to server.
- // Don't allow offline if there are attachments since they were uploaded fine.
- await AddonModGlossary.addEntry(
- this.glossary.id,
- this.entry.concept,
- definition,
- this.courseId,
- options,
- attachmentsResult,
- {
- timeCreated: timecreated,
- discardEntry: this.entry,
- allowOffline: !this.attachments.length,
- checkDuplicates: !this.glossary.allowduplicatedentries,
- },
- );
- }
-
- // Delete the local files from the tmp folder.
- CoreFileUploader.clearTmpFiles(this.attachments);
-
- if (entryId) {
- // Data sent to server, delete stored files (if any).
- AddonModGlossaryHelper.deleteStoredFiles(this.glossary.id, this.entry.concept, timecreated);
- CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' });
- }
-
- CoreEvents.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, {
- glossaryId: this.glossary.id,
- entryId: entryId,
- }, CoreSites.getCurrentSiteId());
-
- CoreForms.triggerFormSubmittedEvent(this.formElement, !!entryId, CoreSites.getCurrentSiteId());
-
- if (this.splitView?.outletActivated) {
- if (this.timecreated > 0) {
- // Reload the data.
- await this.loadOfflineData();
- } else {
- // Empty form.
- this.resetForm();
- }
- } else {
- this.saved = true;
- CoreNavigator.back();
- }
+ this.goBack();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.cannoteditentry', true);
} finally {
@@ -368,49 +217,21 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
}
/**
- * Upload entry attachments if any.
+ * Check if the form data has changed.
*
- * @param timecreated Entry's timecreated.
- * @returns Promise resolved when done.
+ * @returns True if data has changed, false otherwise.
*/
- protected async uploadAttachments(
- timecreated: number,
- ): Promise<{saveOffline: boolean; attachmentsResult?: number | CoreFileUploaderStoreFilesResult}> {
- if (!this.attachments.length || !this.glossary) {
- return {
- saveOffline: false,
- };
+ protected hasDataChanged(): boolean {
+ if (!this.originalData || this.originalData.concept === undefined) {
+ // There is no original data.
+ return !!(this.data.definition || this.data.concept || this.data.attachments.length > 0);
}
- try {
- const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles(
- this.attachments,
- AddonModGlossaryProvider.COMPONENT,
- this.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(
- this.glossary.id,
- this.entry.concept,
- timecreated,
- this.attachments,
- );
-
- return {
- saveOffline: true,
- attachmentsResult,
- };
+ if (this.originalData.definition != this.data.definition || this.originalData.concept != this.data.concept) {
+ return true;
}
+
+ return CoreFileUploader.areFileListDifferent(this.data.attachments, this.originalData.attachments);
}
/**
@@ -427,15 +248,463 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
}
/**
- * Helper to manage swiping within a collection of glossary entries.
+ * Helper to manage form data.
*/
-class AddonModGlossaryEditEntriesSwipeManager extends AddonModGlossaryEntriesSwipeManager {
+abstract class AddonModGlossaryFormHandler {
+
+ constructor(protected page: AddonModGlossaryEditPage) {}
+
+ /**
+ * Load form data.
+ *
+ * @param glossary Glossary.
+ */
+ abstract loadData(glossary: AddonModGlossaryGlossary): Promise;
+
+ /**
+ * Save form data.
+ *
+ * @param glossary Glossary.
+ * @returns Whether the form was saved online.
+ */
+ abstract save(glossary: AddonModGlossaryGlossary): Promise;
+
+ /**
+ * Load form categories.
+ *
+ * @param glossary Glossary.
+ */
+ protected async loadCategories(glossary: AddonModGlossaryGlossary): Promise {
+ this.page.categories = await AddonModGlossary.getAllCategories(glossary.id, {
+ cmId: this.page.cmId,
+ });
+ }
+
+ /**
+ * Upload attachments online.
+ *
+ * @param glossary Glossary.
+ * @returns Uploaded attachments item id.
+ */
+ protected async uploadAttachments(glossary: AddonModGlossaryGlossary): Promise {
+ const data = this.page.data;
+ const itemId = await CoreFileUploader.uploadOrReuploadFiles(
+ data.attachments,
+ AddonModGlossaryProvider.COMPONENT,
+ glossary.id,
+ );
+
+ return itemId;
+ }
+
+ /**
+ * Store attachments offline.
+ *
+ * @param glossary Glossary.
+ * @param timecreated Entry time created.
+ * @returns Storage result.
+ */
+ protected async storeAttachments(
+ glossary: AddonModGlossaryGlossary,
+ timecreated: number,
+ ): Promise {
+ const data = this.page.data;
+ const result = await AddonModGlossaryHelper.storeFiles(
+ glossary.id,
+ data.concept,
+ timecreated,
+ data.attachments,
+ );
+
+ return result;
+ }
+
+ /**
+ * Make sure that the new entry won't create any duplicates.
+ *
+ * @param glossary Glossary.
+ */
+ protected async checkDuplicates(glossary: AddonModGlossaryGlossary): Promise {
+ if (glossary.allowduplicatedentries) {
+ return;
+ }
+
+ const data = this.page.data;
+ 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'));
+ }
+ }
+
+ /**
+ * 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 = {};
+
+ if (this.page.showAliases) {
+ options.aliases = data.aliases;
+ }
+
+ if (this.page.categories.length > 0) {
+ options.categories = data.categories.join(',');
+ }
+
+ if (glossary.usedynalink) {
+ options.usedynalink = data.usedynalink ? 1 : 0;
+
+ if (data.usedynalink) {
+ options.casesensitive = data.casesensitive ? 1 : 0;
+ options.fullmatch = data.fullmatch ? 1 : 0;
+ }
+ }
+
+ return options;
+ }
+
+}
+
+/**
+ * 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
*/
- protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
- return `${this.getSource().GLOSSARY_PATH_PREFIX}edit/${route.params.timecreated}`;
+ async loadData(glossary: AddonModGlossaryGlossary): Promise {
+ const data = this.page.data;
+ const entry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, this.timecreated);
+
+ data.concept = entry.concept || '';
+ data.definition = entry.definition || '';
+ data.timecreated = entry.timecreated;
+
+ if (entry.options) {
+ data.categories = ((entry.options.categories as string)?.split(',') ?? []).map(id => Number(id));
+ data.aliases = entry.options.aliases as string ?? '';
+ 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);
+
+ await this.loadCategories(glossary);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ async save(glossary: AddonModGlossaryGlossary): Promise {
+ const originalData = this.page.data;
+ 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);
+ }
+
+ if (originalData.concept !== data.concept) {
+ await AddonModGlossaryHelper.deleteStoredFiles(glossary.id, originalData.concept, data.timecreated);
+ }
+
+ // Save entry data.
+ await this.updateOfflineEntry(glossary, offlineAttachments);
+
+ // Delete the local files from the tmp folder.
+ CoreFileUploader.clearTmpFiles(data.attachments);
+
+ return false;
+ }
+
+ /**
+ * Update an offline entry.
+ *
+ * @param glossary Glossary.
+ * @param uploadedAttachments Uploaded attachments.
+ */
+ protected async updateOfflineEntry(
+ glossary: AddonModGlossaryGlossary,
+ uploadedAttachments?: CoreFileUploaderStoreFilesResult,
+ ): Promise {
+ const originalData = this.page.originalData;
+ const data = this.page.data;
+ const options = this.getSaveOptions(glossary);
+ const definition = CoreTextUtils.formatHtmlLines(data.definition);
+
+ if (!originalData) {
+ return;
+ }
+
+ await this.checkDuplicates(glossary);
+ await AddonModGlossaryOffline.updateOfflineEntry(
+ {
+ glossaryid: glossary.id,
+ courseid: this.page.courseId,
+ concept: originalData.concept,
+ timecreated: originalData.timecreated,
+ },
+ data.concept,
+ definition,
+ options,
+ uploadedAttachments,
+ );
}
}
+
+/**
+ * Helper to manage the form data for creating a new entry.
+ */
+class AddonModGlossaryNewFormHandler extends AddonModGlossaryFormHandler {
+
+ /**
+ * @inheritdoc
+ */
+ async loadData(glossary: AddonModGlossaryGlossary): Promise {
+ await this.loadCategories(glossary);
+ }
+
+ /**
+ * @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;
+ }
+
+ 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);
+
+ if (entryId) {
+ // Data sent to server, delete stored files (if any).
+ AddonModGlossaryHelper.deleteStoredFiles(glossary.id, data.concept, timecreated);
+ CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' });
+ }
+
+ return !!entryId;
+ }
+
+ /**
+ * Create an offline entry.
+ *
+ * @param glossary Glossary.
+ * @param timecreated Time created.
+ * @param uploadedAttachments Uploaded attachments.
+ */
+ protected async createOfflineEntry(
+ glossary: AddonModGlossaryGlossary,
+ timecreated: number,
+ uploadedAttachments?: CoreFileUploaderStoreFilesResult,
+ ): Promise {
+ const data = this.page.data;
+ const options = this.getSaveOptions(glossary);
+ const definition = CoreTextUtils.formatHtmlLines(data.definition);
+
+ await this.checkDuplicates(glossary);
+ await AddonModGlossaryOffline.addOfflineEntry(
+ glossary.id,
+ data.concept,
+ definition,
+ this.page.courseId,
+ timecreated,
+ options,
+ uploadedAttachments,
+ undefined,
+ undefined,
+ );
+ }
+
+ /**
+ * Create an online entry.
+ *
+ * @param glossary Glossary.
+ * @param timecreated Time created.
+ * @param uploadedAttachmentsId Id of the uploaded attachments.
+ * @param allowOffline Allow falling back to creating the entry offline.
+ * @returns Entry id.
+ */
+ protected async createOnlineEntry(
+ glossary: AddonModGlossaryGlossary,
+ timecreated: number,
+ uploadedAttachmentsId?: number,
+ allowOffline?: boolean,
+ ): Promise {
+ const data = this.page.data;
+ const options = this.getSaveOptions(glossary);
+ const definition = CoreTextUtils.formatHtmlLines(data.definition);
+ const entryId = await AddonModGlossary.addEntry(
+ glossary.id,
+ data.concept,
+ definition,
+ this.page.courseId,
+ options,
+ uploadedAttachmentsId,
+ {
+ timeCreated: timecreated,
+ allowOffline: allowOffline,
+ checkDuplicates: !glossary.allowduplicatedentries,
+ },
+ );
+
+ return entryId;
+ }
+
+}
+
+/**
+ * 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);
+ this.page.showAliases = false;
+ }
+
+ /**
+ * @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);
+
+ // Upload attachments, if any.
+ let attachmentsId: number | undefined = undefined;
+
+ if (data.attachments.length) {
+ attachmentsId = await this.uploadAttachments(glossary);
+ }
+
+ // Save entry data.
+ await AddonModGlossary.updateEntry(glossary.id, this.entry.id, data.concept, definition, options, attachmentsId);
+
+ // Delete the local files from the tmp folder.
+ CoreFileUploader.clearTmpFiles(data.attachments);
+
+ CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' });
+
+ return true;
+ }
+
+}
+
+/**
+ * Form data.
+ */
+type AddonModGlossaryFormData = {
+ concept: string;
+ definition: string;
+ timecreated: number;
+ attachments: CoreFileEntry[];
+ categories: number[];
+ aliases: string;
+ usedynalink: boolean;
+ casesensitive: boolean;
+ fullmatch: boolean;
+};
diff --git a/src/addons/mod/glossary/pages/entry/entry.html b/src/addons/mod/glossary/pages/entry/entry.html
index ee112af6e..aa50fb45b 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 }}
@@ -46,32 +52,53 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
- 0">
+
+
+
+
+
+
+
+
+ 0">
{{ 'core.tag.tags' | translate }}:
-
+
-
+
{{ 'addon.mod_glossary.entrypendingapproval' | translate }}
- 0 && commentsEnabled" contextLevel="module"
- [instanceId]="glossary.coursemodule" component="mod_glossary" [itemId]="entry.id" area="glossary_entry"
- [courseId]="glossary.course" [showItem]="true">
+ 0 && commentsEnabled"
+ contextLevel="module" [instanceId]="glossary.coursemodule" component="mod_glossary" [itemId]="onlineEntry.id"
+ area="glossary_entry" [courseId]="glossary.course" [showItem]="true">
-
-
+
diff --git a/src/addons/mod/glossary/pages/entry/entry.ts b/src/addons/mod/glossary/pages/entry/entry.ts
index f4ee71044..ccbcedea7 100644
--- a/src/addons/mod/glossary/pages/entry/entry.ts
+++ b/src/addons/mod/glossary/pages/entry/entry.ts
@@ -12,24 +12,32 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
+import { AddonModGlossaryHelper } from '@addons/mod/glossary/services/glossary-helper';
+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';
import { CoreRatingInfo } from '@features/rating/services/rating';
import { CoreTag } from '@features/tag/services/tag';
+import { FileEntry } from '@ionic-native/file/ngx';
import { IonRefresher } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
-import { CoreDomUtils } from '@services/utils/dom';
+import { CoreNetwork } from '@services/network';
+import { CoreDomUtils, ToastDuration } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
-import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source';
-import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager';
+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';
/**
@@ -45,62 +53,90 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
component = AddonModGlossaryProvider.COMPONENT;
componentId?: number;
- entry?: AddonModGlossaryEntry;
- entries?: AddonModGlossaryEntryEntriesSwipeManager;
+ onlineEntry?: AddonModGlossaryEntry;
+ offlineEntry?: AddonModGlossaryOfflineEntry;
+ offlineEntryFiles?: FileEntry[];
+ entries!: AddonModGlossaryEntryEntriesSwipeManager;
glossary?: AddonModGlossaryGlossary;
+ entryUpdatedObserver?: CoreEventObserver;
loaded = false;
showAuthor = false;
showDate = false;
ratingInfo?: CoreRatingInfo;
tagsEnabled = false;
+ canEdit = false;
+ canDelete = false;
commentsEnabled = false;
courseId!: number;
- cmId?: number;
+ cmId!: number;
- protected entryId!: number;
+ constructor(@Optional() protected splitView: CoreSplitViewComponent, protected route: ActivatedRoute) {}
- constructor(protected route: ActivatedRoute) {}
+ get entry(): AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | undefined {
+ return this.onlineEntry ?? this.offlineEntry;
+ }
/**
* @inheritdoc
*/
async ngOnInit(): Promise {
+ let onlineEntryId: number | null = null;
+ let offlineEntryTimeCreated: 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-')) {
+ offlineEntryTimeCreated = 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 (!this.glossary || !this.componentId) {
+ this.entryUpdatedObserver = CoreEvents.on(GLOSSARY_ENTRY_UPDATED, data => {
+ if (data.glossaryId !== this.glossary?.id) {
return;
}
- await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.entryId, this.componentId, this.glossary.name));
+ if (
+ (this.onlineEntry && this.onlineEntry.id === data.entryId) ||
+ (this.offlineEntry && this.offlineEntry.timecreated === data.timecreated)
+ ) {
+ this.doRefresh();
+ }
+ });
+
+ try {
+ if (onlineEntryId) {
+ await this.loadOnlineEntry(onlineEntryId);
+
+ if (!this.glossary || !this.componentId) {
+ return;
+ }
+
+ await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(onlineEntryId, this.componentId, this.glossary?.name));
+ } else if (offlineEntryTimeCreated) {
+ await this.loadOfflineEntry(offlineEntryTimeCreated);
+ }
} finally {
this.loaded = true;
}
@@ -110,7 +146,66 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
* @inheritdoc
*/
ngOnDestroy(): void {
- this.entries?.destroy();
+ this.entries.destroy();
+ this.entryUpdatedObserver?.off();
+ }
+
+ /**
+ * Edit entry.
+ */
+ async editEntry(): Promise {
+ await CoreNavigator.navigate('./edit');
+ }
+
+ /**
+ * Delete entry.
+ */
+ async deleteEntry(): Promise {
+ const glossaryId = this.glossary?.id;
+ const cancelled = await CoreUtils.promiseFails(
+ CoreDomUtils.showConfirm(Translate.instant('addon.mod_glossary.areyousuredelete')),
+ );
+
+ if (!glossaryId || cancelled) {
+ return;
+ }
+
+ const modal = await CoreDomUtils.showModalLoading();
+
+ try {
+ if (this.onlineEntry) {
+ const entryId = this.onlineEntry.id;
+
+ await AddonModGlossary.deleteEntry(glossaryId, entryId);
+ await Promise.all([
+ CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(entryId)),
+ CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByLetter(glossaryId)),
+ CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByAuthor(glossaryId)),
+ CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByCategory(glossaryId)),
+ CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'CREATION')),
+ CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'UPDATE')),
+ CoreUtils.ignoreErrors(this.entries.getSource().invalidateCache(false)),
+ ]);
+ } else if (this.offlineEntry) {
+ const concept = this.offlineEntry.concept;
+ const timecreated = this.offlineEntry.timecreated;
+
+ await AddonModGlossaryOffline.deleteOfflineEntry(glossaryId, timecreated);
+ await AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timecreated);
+ }
+
+ CoreDomUtils.showToast('addon.mod_glossary.entrydeleted', true, ToastDuration.LONG);
+
+ if (this.splitView?.outletActivated) {
+ await CoreNavigator.navigate('../');
+ } else {
+ await CoreNavigator.back();
+ }
+ } catch (error) {
+ CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errordeleting', true);
+ } finally {
+ modal.dismiss();
+ }
}
/**
@@ -120,65 +215,110 @@ 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();
+ const canUpdateEntries = CoreNetwork.isOnline() && await AddonModGlossary.canUpdateEntries();
- this.entry = result.entry;
+ this.onlineEntry = result.entry;
this.ratingInfo = result.ratinginfo;
+ this.canDelete = canDeleteEntries && !!result.permissions?.candelete;
+ this.canEdit = canUpdateEntries && !!result.permissions?.canupdate;
- 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 timecreated Entry Timecreated.
+ */
+ protected async loadOfflineEntry(timecreated: number): Promise {
+ try {
+ const glossary = await this.loadGlossary();
+
+ this.offlineEntry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, timecreated);
+ this.offlineEntryFiles = this.offlineEntry.attachments && this.offlineEntry.attachments.offline > 0
+ ? await AddonModGlossaryHelper.getStoredFiles(
+ glossary.id,
+ this.offlineEntry.concept,
+ timecreated,
+ )
+ : undefined;
+ this.canEdit = true;
+ this.canDelete = true;
+ } 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);
}
}
@@ -186,13 +326,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/glossary-helper.ts b/src/addons/mod/glossary/services/glossary-helper.ts
index d51c457d2..46e4e25ca 100644
--- a/src/addons/mod/glossary/services/glossary-helper.ts
+++ b/src/addons/mod/glossary/services/glossary-helper.ts
@@ -18,7 +18,6 @@ import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fi
import { CoreFile } from '@services/file';
import { CoreUtils } from '@services/utils/utils';
import { AddonModGlossaryOffline } from './glossary-offline';
-import { AddonModGlossaryNewEntry, AddonModGlossaryNewEntryWithFiles } from './glossary';
import { makeSingleton } from '@singletons';
import { CoreFileEntry } from '@services/file-helper';
@@ -58,31 +57,6 @@ export class AddonModGlossaryHelperProvider {
return CoreFileUploader.getStoredFiles(folderPath);
}
- /**
- * Check if the data of an entry has changed.
- *
- * @param entry Current data.
- * @param files Files attached.
- * @param original Original content.
- * @returns True if data has changed, false otherwise.
- */
- hasEntryDataChanged(
- entry: AddonModGlossaryNewEntry,
- files: CoreFileEntry[],
- original?: AddonModGlossaryNewEntryWithFiles,
- ): boolean {
- if (!original || original.concept === undefined) {
- // There is no original data.
- return !!(entry.definition || entry.concept || files.length > 0);
- }
-
- if (original.definition != entry.definition || original.concept != entry.concept) {
- return true;
- }
-
- return CoreFileUploader.areFileListDifferent(files, original.files);
- }
-
/**
* Given a list of files (either online files or local files), store the local files in a local folder
* to be submitted later.
diff --git a/src/addons/mod/glossary/services/glossary-offline.ts b/src/addons/mod/glossary/services/glossary-offline.ts
index a0dadeee1..cc4b89c86 100644
--- a/src/addons/mod/glossary/services/glossary-offline.ts
+++ b/src/addons/mod/glossary/services/glossary-offline.ts
@@ -17,11 +17,11 @@ import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/service
import { CoreFile } from '@services/file';
import { CoreSites } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
-import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
+import { CoreEvents } from '@singletons/events';
import { CorePath } from '@singletons/path';
import { AddonModGlossaryOfflineEntryDBRecord, OFFLINE_ENTRIES_TABLE_NAME } from './database/glossary';
-import { AddonModGlossaryDiscardedEntry, AddonModGlossaryEntryOption } from './glossary';
+import { AddonModGlossaryEntryOption, GLOSSARY_ENTRY_ADDED, GLOSSARY_ENTRY_DELETED, GLOSSARY_ENTRY_UPDATED } from './glossary';
/**
* Service to handle offline glossary.
@@ -30,33 +30,33 @@ import { AddonModGlossaryDiscardedEntry, AddonModGlossaryEntryOption } from './g
export class AddonModGlossaryOfflineProvider {
/**
- * Delete a new entry.
+ * Delete an offline entry.
*
* @param glossaryId Glossary ID.
- * @param concept Glossary entry concept.
- * @param timeCreated The time the entry was created.
+ * @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 deleteNewEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise {
+ async deleteOfflineEntry(glossaryId: number, timecreated: number, siteId?: string): Promise {
const site = await CoreSites.getSite(siteId);
const conditions: Partial = {
glossaryid: glossaryId,
- concept: concept,
- timecreated: timeCreated,
+ timecreated: timecreated,
};
await site.getDb().deleteRecords(OFFLINE_ENTRIES_TABLE_NAME, conditions);
+
+ CoreEvents.trigger(GLOSSARY_ENTRY_DELETED, { glossaryId, timecreated });
}
/**
- * Get all the stored new entries from all the glossaries.
+ * Get all the stored offline entries from all the glossaries.
*
* @param siteId Site ID. If not defined, current site.
* @returns Promise resolved with entries.
*/
- async getAllNewEntries(siteId?: string): Promise {
+ async getAllOfflineEntries(siteId?: string): Promise {
const site = await CoreSites.getSite(siteId);
const records = await site.getDb().getRecords(OFFLINE_ENTRIES_TABLE_NAME);
@@ -65,17 +65,15 @@ export class AddonModGlossaryOfflineProvider {
}
/**
- * Get a stored new entry.
+ * 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 getNewEntry(
+ async getOfflineEntry(
glossaryId: number,
- concept: string,
timeCreated: number,
siteId?: string,
): Promise {
@@ -83,7 +81,6 @@ export class AddonModGlossaryOfflineProvider {
const conditions: Partial = {
glossaryid: glossaryId,
- concept: concept,
timecreated: timeCreated,
};
@@ -100,7 +97,7 @@ export class AddonModGlossaryOfflineProvider {
* @param userId User the entries belong to. If not defined, current user in site.
* @returns Promise resolved with entries.
*/
- async getGlossaryNewEntries(glossaryId: number, siteId?: string, userId?: number): Promise {
+ async getGlossaryOfflineEntries(glossaryId: number, siteId?: string, userId?: number): Promise {
const site = await CoreSites.getSite(siteId);
const conditions: Partial = {
@@ -143,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.getNewEntry(glossaryId, concept, timeCreated, siteId));
+ return entries[0].timecreated !== timeCreated;
} catch {
// No offline data found, return false.
return false;
@@ -151,31 +148,29 @@ export class AddonModGlossaryOfflineProvider {
}
/**
- * Save a new entry to be sent later.
+ * Save an offline entry to be sent later.
*
* @param glossaryId Glossary ID.
* @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 addNewEntry(
+ async addOfflineEntry(
glossaryId: number,
concept: string,
definition: string,
courseId: number,
+ timecreated: number,
options?: Record,
attachments?: CoreFileUploaderStoreFilesResult,
- timeCreated?: number,
siteId?: string,
userId?: number,
- discardEntry?: AddonModGlossaryDiscardedEntry,
): Promise {
const site = await CoreSites.getSite(siteId);
@@ -188,19 +183,52 @@ export class AddonModGlossaryOfflineProvider {
options: JSON.stringify(options || {}),
attachments: JSON.stringify(attachments),
userid: userId || site.getUserId(),
- timecreated: timeCreated || Date.now(),
+ timecreated,
};
- // If editing an offline entry, delete previous first.
- if (discardEntry) {
- await this.deleteNewEntry(glossaryId, discardEntry.concept, discardEntry.timecreated, site.getId());
- }
-
await site.getDb().insertRecord(OFFLINE_ENTRIES_TABLE_NAME, entry);
+ CoreEvents.trigger(GLOSSARY_ENTRY_ADDED, { glossaryId, timecreated }, siteId);
+
return false;
}
+ /**
+ * Update an offline entry to be sent later.
+ *
+ * @param originalEntry Original entry data.
+ * @param concept Glossary entry concept.
+ * @param definition Glossary entry concept definition.
+ * @param options Options for the entry.
+ * @param attachments Result of CoreFileUploaderProvider#storeFilesToUpload for attachments.
+ */
+ async updateOfflineEntry(
+ originalEntry: Pick< AddonModGlossaryOfflineEntryDBRecord, 'glossaryid'|'courseid'|'concept'|'timecreated'>,
+ concept: string,
+ definition: string,
+ options?: Record,
+ attachments?: CoreFileUploaderStoreFilesResult,
+ ): Promise {
+ const site = await CoreSites.getSite();
+ const entry: Omit = {
+ concept: concept,
+ definition: definition,
+ definitionformat: 'html',
+ options: JSON.stringify(options || {}),
+ attachments: JSON.stringify(attachments),
+ };
+
+ await site.getDb().updateRecords(OFFLINE_ENTRIES_TABLE_NAME, entry, {
+ ...originalEntry,
+ userid: site.getUserId(),
+ });
+
+ CoreEvents.trigger(GLOSSARY_ENTRY_UPDATED, {
+ glossaryId: originalEntry.glossaryid,
+ timecreated: originalEntry.timecreated,
+ });
+ }
+
/**
* Get the path to the folder where to store files for offline attachments in a glossary.
*
@@ -218,7 +246,7 @@ export class AddonModGlossaryOfflineProvider {
}
/**
- * Get the path to the folder where to store files for a new offline entry.
+ * Get the path to the folder where to store files for an offline entry.
*
* @param glossaryId Glossary ID.
* @param concept The name of the entry.
diff --git a/src/addons/mod/glossary/services/glossary-sync.ts b/src/addons/mod/glossary/services/glossary-sync.ts
index 0fc65ad13..b922d7262 100644
--- a/src/addons/mod/glossary/services/glossary-sync.ts
+++ b/src/addons/mod/glossary/services/glossary-sync.ts
@@ -31,14 +31,14 @@ import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from './glossar
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
import { CoreFileEntry } from '@services/file-helper';
+export const GLOSSARY_AUTO_SYNCED = 'addon_mod_glossary_auto_synced';
+
/**
* Service to sync glossaries.
*/
@Injectable({ providedIn: 'root' })
export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProvider {
- static readonly AUTO_SYNCED = 'addon_mod_glossary_autom_synced';
-
protected componentTranslatableString = 'glossary';
constructor() {
@@ -50,10 +50,9 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
*
* @param siteId Site ID to sync. If not defined, sync all sites.
* @param force Wether to force sync not depending on last execution.
- * @returns Promise resolved if sync is successful, rejected if sync fails.
*/
- syncAllGlossaries(siteId?: string, force?: boolean): Promise {
- return this.syncOnSites('all glossaries', (siteId) => this.syncAllGlossariesFunc(!!force, siteId), siteId);
+ async syncAllGlossaries(siteId?: string, force?: boolean): Promise {
+ await this.syncOnSites('all glossaries', (siteId) => this.syncAllGlossariesFunc(!!force, siteId), siteId);
}
/**
@@ -61,7 +60,6 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
*
* @param force Wether to force sync not depending on last execution.
* @param siteId Site ID to sync.
- * @returns Promise resolved if sync is successful, rejected if sync fails.
*/
protected async syncAllGlossariesFunc(force: boolean, siteId: string): Promise {
siteId = siteId || CoreSites.getCurrentSiteId();
@@ -73,14 +71,13 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
}
/**
- * Sync entried of all glossaries on a site.
+ * Sync entries of all glossaries on a site.
*
* @param force Wether to force sync not depending on last execution.
* @param siteId Site ID to sync.
- * @returns Promise resolved if sync is successful, rejected if sync fails.
*/
protected async syncAllGlossariesEntries(force: boolean, siteId: string): Promise {
- const entries = await AddonModGlossaryOffline.getAllNewEntries(siteId);
+ const entries = await AddonModGlossaryOffline.getAllOfflineEntries(siteId);
// Do not sync same glossary twice.
const treated: Record = {};
@@ -98,7 +95,7 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
if (result?.updated) {
// Sync successful, send event.
- CoreEvents.trigger(AddonModGlossarySyncProvider.AUTO_SYNCED, {
+ CoreEvents.trigger(GLOSSARY_AUTO_SYNCED, {
glossaryId: entry.glossaryid,
userId: entry.userid,
warnings: result.warnings,
@@ -180,7 +177,7 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
// Get offline responses to be sent.
const entries = await CoreUtils.ignoreErrors(
- AddonModGlossaryOffline.getGlossaryNewEntries(glossaryId, siteId, userId),
+ AddonModGlossaryOffline.getGlossaryOfflineEntries(glossaryId, siteId, userId),
[],
);
@@ -285,11 +282,10 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
* @param concept Glossary entry concept.
* @param timeCreated Time to allow duplicated entries.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when deleted.
*/
protected async deleteAddEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise {
await Promise.all([
- AddonModGlossaryOffline.deleteNewEntry(glossaryId, concept, timeCreated, siteId),
+ AddonModGlossaryOffline.deleteOfflineEntry(glossaryId, timeCreated, siteId),
AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timeCreated, siteId),
]);
}
@@ -341,15 +337,28 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
export const AddonModGlossarySync = makeSingleton(AddonModGlossarySyncProvider);
+declare module '@singletons/events' {
+
+ /**
+ * Augment CoreEventsData interface with events specific to this service.
+ *
+ * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
+ */
+ export interface CoreEventsData {
+ [GLOSSARY_AUTO_SYNCED]: AddonModGlossaryAutoSyncedData;
+ }
+
+}
+
/**
* Data returned by a glossary sync.
*/
export type AddonModGlossarySyncResult = CoreSyncResult;
/**
- * Data passed to AUTO_SYNCED event.
+ * Data passed to GLOSSARY_AUTO_SYNCED event.
*/
-export type AddonModGlossaryAutoSyncData = {
+export type AddonModGlossaryAutoSyncedData = {
glossaryId: number;
userId: number;
warnings: string[];
diff --git a/src/addons/mod/glossary/services/glossary.ts b/src/addons/mod/glossary/services/glossary.ts
index 69645bbee..487544639 100644
--- a/src/addons/mod/glossary/services/glossary.ts
+++ b/src/addons/mod/glossary/services/glossary.ts
@@ -25,12 +25,13 @@ import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
import { makeSingleton, Translate } from '@singletons';
+import { CoreEvents } from '@singletons/events';
import { AddonModGlossaryEntryDBRecord, ENTRIES_TABLE_NAME } from './database/glossary';
import { AddonModGlossaryOffline } from './glossary-offline';
-import { AddonModGlossaryAutoSyncData, AddonModGlossarySyncProvider } from './glossary-sync';
-import { CoreFileEntry } from '@services/file-helper';
-const ROOT_CACHE_KEY = 'mmaModGlossary:';
+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';
/**
* Service that provides some features for glossaries.
@@ -41,10 +42,9 @@ export class AddonModGlossaryProvider {
static readonly COMPONENT = 'mmaModGlossary';
static readonly LIMIT_ENTRIES = 25;
static readonly LIMIT_CATEGORIES = 10;
- static readonly SHOW_ALL_CATEGORIES = 0;
- static readonly SHOW_NOT_CATEGORISED = -1;
- static readonly ADD_ENTRY_EVENT = 'addon_mod_glossary_add_entry';
+ private static readonly SHOW_ALL_CATEGORIES = 0;
+ private static readonly ROOT_CACHE_KEY = 'mmaModGlossary:';
/**
* Get the course glossary cache key.
@@ -53,7 +53,7 @@ export class AddonModGlossaryProvider {
* @returns Cache key.
*/
protected getCourseGlossariesCacheKey(courseId: number): string {
- return ROOT_CACHE_KEY + 'courseGlossaries:' + courseId;
+ return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}courseGlossaries:${courseId}`;
}
/**
@@ -90,7 +90,6 @@ export class AddonModGlossaryProvider {
*
* @param courseId Course Id.
* @param siteId Site ID. If not defined, current site.
- * @returns Resolved when data is invalidated.
*/
async invalidateCourseGlossaries(courseId: number, siteId?: string): Promise {
const site = await CoreSites.getSite(siteId);
@@ -104,44 +103,35 @@ export class AddonModGlossaryProvider {
* Get the entries by author cache key.
*
* @param glossaryId Glossary Id.
- * @param letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL.
- * @param field Search and order using: FIRSTNAME or LASTNAME
- * @param sort The direction of the order: ASC or DESC
* @returns Cache key.
*/
- protected getEntriesByAuthorCacheKey(glossaryId: number, letter: string, field: string, sort: string): string {
- return ROOT_CACHE_KEY + 'entriesByAuthor:' + glossaryId + ':' + letter + ':' + field + ':' + sort;
+ protected getEntriesByAuthorCacheKey(glossaryId: number): string {
+ return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesByAuthor:${glossaryId}:ALL:LASTNAME:ASC`;
}
/**
* Get entries by author.
*
* @param glossaryId Glossary Id.
- * @param letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL.
- * @param field Search and order using: FIRSTNAME or LASTNAME
- * @param sort The direction of the order: ASC or DESC
* @param options Other options.
* @returns Resolved with the entries.
*/
async getEntriesByAuthor(
glossaryId: number,
- letter: string,
- field: string,
- sort: string,
options: AddonModGlossaryGetEntriesOptions = {},
): Promise {
const site = await CoreSites.getSite(options.siteId);
const params: AddonModGlossaryGetEntriesByAuthorWSParams = {
id: glossaryId,
- letter: letter,
- field: field,
- sort: sort,
+ letter: 'ALL',
+ field: 'LASTNAME',
+ sort: 'ASC',
from: options.from || 0,
limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES,
};
const preSets: CoreSiteWSPreSets = {
- cacheKey: this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort),
+ cacheKey: this.getEntriesByAuthorCacheKey(glossaryId),
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
component: AddonModGlossaryProvider.COMPONENT,
componentId: options.cmId,
@@ -155,22 +145,12 @@ export class AddonModGlossaryProvider {
* Invalidate cache of entries by author.
*
* @param glossaryId Glossary Id.
- * @param letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL.
- * @param field Search and order using: FIRSTNAME or LASTNAME
- * @param sort The direction of the order: ASC or DESC
* @param siteId Site ID. If not defined, current site.
- * @returns Resolved when data is invalidated.
*/
- async invalidateEntriesByAuthor(
- glossaryId: number,
- letter: string,
- field: string,
- sort: string,
- siteId?: string,
- ): Promise {
+ async invalidateEntriesByAuthor(glossaryId: number, siteId?: string): Promise {
const site = await CoreSites.getSite(siteId);
- const key = this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort);
+ const key = this.getEntriesByAuthorCacheKey(glossaryId);
await site.invalidateWsCacheForKey(key);
}
@@ -179,26 +159,23 @@ export class AddonModGlossaryProvider {
* Get entries by category.
*
* @param glossaryId Glossary Id.
- * @param categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or
- * constant SHOW_NOT_CATEGORISED for uncategorised entries.
* @param options Other options.
* @returns Resolved with the entries.
*/
async getEntriesByCategory(
glossaryId: number,
- categoryId: number,
options: AddonModGlossaryGetEntriesOptions = {},
): Promise {
const site = await CoreSites.getSite(options.siteId);
const params: AddonModGlossaryGetEntriesByCategoryWSParams = {
id: glossaryId,
- categoryid: categoryId,
+ categoryid: AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
from: options.from || 0,
limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES,
};
const preSets: CoreSiteWSPreSets = {
- cacheKey: this.getEntriesByCategoryCacheKey(glossaryId, categoryId),
+ cacheKey: this.getEntriesByCategoryCacheKey(glossaryId),
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
component: AddonModGlossaryProvider.COMPONENT,
componentId: options.cmId,
@@ -212,15 +189,12 @@ export class AddonModGlossaryProvider {
* Invalidate cache of entries by category.
*
* @param glossaryId Glossary Id.
- * @param categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or
- * constant SHOW_NOT_CATEGORISED for uncategorised entries.
* @param siteId Site ID. If not defined, current site.
- * @returns Resolved when data is invalidated.
*/
- async invalidateEntriesByCategory(glossaryId: number, categoryId: number, siteId?: string): Promise {
+ async invalidateEntriesByCategory(glossaryId: number, siteId?: string): Promise {
const site = await CoreSites.getSite(siteId);
- const key = this.getEntriesByCategoryCacheKey(glossaryId, categoryId);
+ const key = this.getEntriesByCategoryCacheKey(glossaryId);
await site.invalidateWsCacheForKey(key);
}
@@ -229,12 +203,12 @@ export class AddonModGlossaryProvider {
* Get the entries by category cache key.
*
* @param glossaryId Glossary Id.
- * @param categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or
- * constant SHOW_NOT_CATEGORISED for uncategorised entries.
* @returns Cache key.
*/
- getEntriesByCategoryCacheKey(glossaryId: number, categoryId: number): string {
- return ROOT_CACHE_KEY + 'entriesByCategory:' + glossaryId + ':' + categoryId;
+ getEntriesByCategoryCacheKey(glossaryId: number): string {
+ const prefix = `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesByCategory`;
+
+ return `${prefix}:${glossaryId}:${AddonModGlossaryProvider.SHOW_ALL_CATEGORIES}`;
}
/**
@@ -242,11 +216,10 @@ export class AddonModGlossaryProvider {
*
* @param glossaryId Glossary Id.
* @param order The way to order the records.
- * @param sort The direction of the order.
* @returns Cache key.
*/
- getEntriesByDateCacheKey(glossaryId: number, order: string, sort: string): string {
- return ROOT_CACHE_KEY + 'entriesByDate:' + glossaryId + ':' + order + ':' + sort;
+ getEntriesByDateCacheKey(glossaryId: number, order: string): string {
+ return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesByDate:${glossaryId}:${order}:DESC`;
}
/**
@@ -254,14 +227,12 @@ export class AddonModGlossaryProvider {
*
* @param glossaryId Glossary Id.
* @param order The way to order the records.
- * @param sort The direction of the order.
* @param options Other options.
* @returns Resolved with the entries.
*/
async getEntriesByDate(
glossaryId: number,
order: string,
- sort: string,
options: AddonModGlossaryGetEntriesOptions = {},
): Promise {
const site = await CoreSites.getSite(options.siteId);
@@ -269,12 +240,12 @@ export class AddonModGlossaryProvider {
const params: AddonModGlossaryGetEntriesByDateWSParams = {
id: glossaryId,
order: order,
- sort: sort,
+ sort: 'DESC',
from: options.from || 0,
limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES,
};
const preSets: CoreSiteWSPreSets = {
- cacheKey: this.getEntriesByDateCacheKey(glossaryId, order, sort),
+ cacheKey: this.getEntriesByDateCacheKey(glossaryId, order),
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
component: AddonModGlossaryProvider.COMPONENT,
componentId: options.cmId,
@@ -289,14 +260,12 @@ export class AddonModGlossaryProvider {
*
* @param glossaryId Glossary Id.
* @param order The way to order the records.
- * @param sort The direction of the order.
* @param siteId Site ID. If not defined, current site.
- * @returns Resolved when data is invalidated.
*/
- async invalidateEntriesByDate(glossaryId: number, order: string, sort: string, siteId?: string): Promise {
+ async invalidateEntriesByDate(glossaryId: number, order: string, siteId?: string): Promise {
const site = await CoreSites.getSite(siteId);
- const key = this.getEntriesByDateCacheKey(glossaryId, order, sort);
+ const key = this.getEntriesByDateCacheKey(glossaryId, order);
await site.invalidateWsCacheForKey(key);
}
@@ -305,24 +274,21 @@ export class AddonModGlossaryProvider {
* Get the entries by letter cache key.
*
* @param glossaryId Glossary Id.
- * @param letter A letter, or a special keyword.
* @returns Cache key.
*/
- protected getEntriesByLetterCacheKey(glossaryId: number, letter: string): string {
- return ROOT_CACHE_KEY + 'entriesByLetter:' + glossaryId + ':' + letter;
+ protected getEntriesByLetterCacheKey(glossaryId: number): string {
+ return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesByLetter:${glossaryId}:ALL`;
}
/**
* Get entries by letter.
*
* @param glossaryId Glossary Id.
- * @param letter A letter, or a special keyword.
* @param options Other options.
* @returns Resolved with the entries.
*/
async getEntriesByLetter(
glossaryId: number,
- letter: string,
options: AddonModGlossaryGetEntriesOptions = {},
): Promise {
options.from = options.from || 0;
@@ -332,12 +298,12 @@ export class AddonModGlossaryProvider {
const params: AddonModGlossaryGetEntriesByLetterWSParams = {
id: glossaryId,
- letter: letter,
+ letter: 'ALL',
from: options.from,
limit: options.limit,
};
const preSets: CoreSiteWSPreSets = {
- cacheKey: this.getEntriesByLetterCacheKey(glossaryId, letter),
+ cacheKey: this.getEntriesByLetterCacheKey(glossaryId),
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
component: AddonModGlossaryProvider.COMPONENT,
componentId: options.cmId,
@@ -362,16 +328,14 @@ export class AddonModGlossaryProvider {
* Invalidate cache of entries by letter.
*
* @param glossaryId Glossary Id.
- * @param letter A letter, or a special keyword.
* @param siteId Site ID. If not defined, current site.
- * @returns Resolved when data is invalidated.
*/
- async invalidateEntriesByLetter(glossaryId: number, letter: string, siteId?: string): Promise {
+ async invalidateEntriesByLetter(glossaryId: number, siteId?: string): Promise {
const site = await CoreSites.getSite(siteId);
- const key = this.getEntriesByLetterCacheKey(glossaryId, letter);
+ const key = this.getEntriesByLetterCacheKey(glossaryId);
- return site.invalidateWsCacheForKey(key);
+ await site.invalidateWsCacheForKey(key);
}
/**
@@ -380,18 +344,10 @@ export class AddonModGlossaryProvider {
* @param glossaryId Glossary Id.
* @param query The search query.
* @param fullSearch Whether or not full search is required.
- * @param order The way to order the results.
- * @param sort The direction of the order.
* @returns Cache key.
*/
- protected getEntriesBySearchCacheKey(
- glossaryId: number,
- query: string,
- fullSearch: boolean,
- order: string,
- sort: string,
- ): string {
- return ROOT_CACHE_KEY + 'entriesBySearch:' + glossaryId + ':' + fullSearch + ':' + order + ':' + sort + ':' + query;
+ protected getEntriesBySearchCacheKey(glossaryId: number, query: string, fullSearch: boolean): string {
+ return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesBySearch:${glossaryId}:${fullSearch}:CONCEPT:ASC:${query}`;
}
/**
@@ -400,8 +356,6 @@ export class AddonModGlossaryProvider {
* @param glossaryId Glossary Id.
* @param query The search query.
* @param fullSearch Whether or not full search is required.
- * @param order The way to order the results.
- * @param sort The direction of the order.
* @param options Get entries options.
* @returns Resolved with the entries.
*/
@@ -409,8 +363,6 @@ export class AddonModGlossaryProvider {
glossaryId: number,
query: string,
fullSearch: boolean,
- order: string,
- sort: string,
options: AddonModGlossaryGetEntriesOptions = {},
): Promise {
const site = await CoreSites.getSite(options.siteId);
@@ -419,13 +371,13 @@ export class AddonModGlossaryProvider {
id: glossaryId,
query: query,
fullsearch: fullSearch,
- order: order,
- sort: sort,
+ order: 'CONCEPT',
+ sort: 'ASC',
from: options.from || 0,
limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES,
};
const preSets: CoreSiteWSPreSets = {
- cacheKey: this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort),
+ cacheKey: this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch),
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
component: AddonModGlossaryProvider.COMPONENT,
componentId: options.cmId,
@@ -441,22 +393,17 @@ export class AddonModGlossaryProvider {
* @param glossaryId Glossary Id.
* @param query The search query.
* @param fullSearch Whether or not full search is required.
- * @param order The way to order the results.
- * @param sort The direction of the order.
* @param siteId Site ID. If not defined, current site.
- * @returns Resolved when data is invalidated.
*/
async invalidateEntriesBySearch(
glossaryId: number,
query: string,
fullSearch: boolean,
- order: string,
- sort: string,
siteId?: string,
): Promise {
const site = await CoreSites.getSite(siteId);
- const key = this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort);
+ const key = this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch);
await site.invalidateWsCacheForKey(key);
}
@@ -468,7 +415,7 @@ export class AddonModGlossaryProvider {
* @returns The cache key.
*/
protected getCategoriesCacheKey(glossaryId: number): string {
- return ROOT_CACHE_KEY + 'categories:' + glossaryId;
+ return AddonModGlossaryProvider.ROOT_CACHE_KEY + 'categories:' + glossaryId;
}
/**
@@ -533,7 +480,6 @@ export class AddonModGlossaryProvider {
*
* @param glossaryId Glossary Id.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when categories data has been invalidated,
*/
async invalidateCategories(glossaryId: number, siteId?: string): Promise {
const site = await CoreSites.getSite(siteId);
@@ -548,7 +494,7 @@ export class AddonModGlossaryProvider {
* @returns Cache key.
*/
protected getEntryCacheKey(entryId: number): string {
- return ROOT_CACHE_KEY + 'getEntry:' + entryId;
+ return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}getEntry:${entryId}`;
}
/**
@@ -637,7 +583,7 @@ export class AddonModGlossaryProvider {
options: CoreCourseCommonModWSOptions = {},
): Promise {
// Get the entries from this "page" and check if the entry we're looking for is in it.
- const result = await this.getEntriesByLetter(glossaryId, 'ALL', {
+ const result = await this.getEntriesByLetter(glossaryId, {
from: from,
readingStrategy: CoreSitesReadingStrategy.ONLY_CACHE,
cmId: options.cmId,
@@ -661,6 +607,30 @@ export class AddonModGlossaryProvider {
throw new CoreError('Entry not found.');
}
+ /**
+ * Check whether the site can delete glossary entries.
+ *
+ * @param siteId Site id.
+ * @returns Whether the site can delete entries.
+ */
+ async canDeleteEntries(siteId?: string): Promise {
+ const site = await CoreSites.getSite(siteId);
+
+ 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.
*
@@ -695,7 +665,6 @@ export class AddonModGlossaryProvider {
*
* @param entryId Entry Id.
* @param siteId Site ID. If not defined, current site.
- * @returns Resolved when data is invalidated.
*/
async invalidateEntry(entryId: number, siteId?: string): Promise {
const site = await CoreSites.getSite(siteId);
@@ -708,7 +677,6 @@ export class AddonModGlossaryProvider {
*
* @param entries Entry objects to invalidate.
* @param siteId Site ID. If not defined, current site.
- * @returns Resolved when data is invalidated.
*/
protected async invalidateEntries(entries: AddonModGlossaryEntry[], siteId?: string): Promise {
const keys: string[] = [];
@@ -727,7 +695,6 @@ export class AddonModGlossaryProvider {
*
* @param moduleId The module ID.
* @param courseId Course ID.
- * @returns Promise resolved when data is invalidated.
*/
async invalidateContent(moduleId: number, courseId: number): Promise {
const glossary = await this.getGlossary(courseId, moduleId);
@@ -747,7 +714,6 @@ export class AddonModGlossaryProvider {
* @param glossary The glossary object.
* @param onlyEntriesList If true, entries won't be invalidated.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when data is invalidated.
*/
async invalidateGlossaryEntries(glossary: AddonModGlossaryGlossary, onlyEntriesList?: boolean, siteId?: string): Promise {
siteId = siteId || CoreSites.getCurrentSiteId();
@@ -755,7 +721,7 @@ export class AddonModGlossaryProvider {
const promises: Promise[] = [];
if (!onlyEntriesList) {
- promises.push(this.fetchAllEntries((options) => this.getEntriesByLetter(glossary.id, 'ALL', options), {
+ promises.push(this.fetchAllEntries((options) => this.getEntriesByLetter(glossary.id, options), {
cmId: glossary.coursemodule,
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
siteId,
@@ -765,21 +731,17 @@ export class AddonModGlossaryProvider {
glossary.browsemodes.forEach((mode) => {
switch (mode) {
case 'letter':
- promises.push(this.invalidateEntriesByLetter(glossary.id, 'ALL', siteId));
+ promises.push(this.invalidateEntriesByLetter(glossary.id, siteId));
break;
case 'cat':
- promises.push(this.invalidateEntriesByCategory(
- glossary.id,
- AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
- siteId,
- ));
+ promises.push(this.invalidateEntriesByCategory(glossary.id, siteId));
break;
case 'date':
- promises.push(this.invalidateEntriesByDate(glossary.id, 'CREATION', 'DESC', siteId));
- promises.push(this.invalidateEntriesByDate(glossary.id, 'UPDATE', 'DESC', siteId));
+ promises.push(this.invalidateEntriesByDate(glossary.id, 'CREATION', siteId));
+ promises.push(this.invalidateEntriesByDate(glossary.id, 'UPDATE', siteId));
break;
case 'author':
- promises.push(this.invalidateEntriesByAuthor(glossary.id, 'ALL', 'LASTNAME', 'ASC', siteId));
+ promises.push(this.invalidateEntriesByAuthor(glossary.id, siteId));
break;
default:
}
@@ -857,13 +819,10 @@ export class AddonModGlossaryProvider {
// Convenience function to store a new entry to be synchronized later.
const storeOffline = async (): Promise => {
- const discardTime = otherOptions.discardEntry?.timecreated;
-
if (otherOptions.checkDuplicates) {
// Check if the entry is duplicated in online or offline mode.
const conceptUsed = await this.isConceptUsed(glossaryId, concept, {
cmId: otherOptions.cmId,
- timeCreated: discardTime,
siteId: otherOptions.siteId,
});
@@ -877,17 +836,16 @@ export class AddonModGlossaryProvider {
throw new CoreError('Error adding entry.');
}
- await AddonModGlossaryOffline.addNewEntry(
+ await AddonModGlossaryOffline.addOfflineEntry(
glossaryId,
concept,
definition,
courseId,
+ otherOptions.timeCreated ?? Date.now(),
entryOptions,
attachments,
- otherOptions.timeCreated,
otherOptions.siteId,
undefined,
- otherOptions.discardEntry,
);
return false;
@@ -898,19 +856,9 @@ export class AddonModGlossaryProvider {
return storeOffline();
}
- // If we are editing an offline entry, discard previous first.
- if (otherOptions.discardEntry) {
- await AddonModGlossaryOffline.deleteNewEntry(
- glossaryId,
- otherOptions.discardEntry.concept,
- otherOptions.discardEntry.timecreated,
- otherOptions.siteId,
- );
- }
-
try {
// Try to add it in online.
- return await this.addEntryOnline(
+ const entryId = await this.addEntryOnline(
glossaryId,
concept,
definition,
@@ -918,6 +866,8 @@ export class AddonModGlossaryProvider {
attachments,
otherOptions.siteId,
);
+
+ return entryId;
} catch (error) {
if (otherOptions.allowOffline && !CoreUtils.isWebServiceError(error)) {
// Couldn't connect to server, store in offline.
@@ -959,7 +909,7 @@ export class AddonModGlossaryProvider {
};
if (attachId) {
- params.options!.push({
+ params.options?.push({
name: 'attachmentsid',
value: String(attachId),
});
@@ -967,9 +917,71 @@ export class AddonModGlossaryProvider {
const response = await site.write('mod_glossary_add_entry', params);
+ CoreEvents.trigger(GLOSSARY_ENTRY_ADDED, { glossaryId, entryId: response.entryid }, siteId);
+
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 attachId Attachments ID (if any attachment).
+ * @param siteId Site ID. If not defined, current site.
+ */
+ async updateEntry(
+ glossaryId: number,
+ entryId: number,
+ concept: string,
+ definition: string,
+ options?: Record,
+ attachId?: number,
+ 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'),
+ };
+
+ if (attachId) {
+ params.options?.push({
+ name: 'attachmentsid',
+ value: String(attachId),
+ });
+ }
+
+ 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.
+ *
+ * @param glossaryId Glossary id.
+ * @param entryId Entry id.
+ */
+ async deleteEntry(glossaryId: number, entryId: number): Promise {
+ const site = CoreSites.getRequiredCurrentSite();
+
+ await site.write('mod_glossary_delete_entry', { entryid: entryId });
+
+ CoreEvents.trigger(GLOSSARY_ENTRY_DELETED, { glossaryId, entryId });
+ }
+
/**
* Check if a entry concept is already used.
*
@@ -989,7 +1001,7 @@ export class AddonModGlossaryProvider {
// If we get here, there's no offline entry with this name, check online.
// Get entries from the cache.
- const entries = await this.fetchAllEntries((options) => this.getEntriesByLetter(glossaryId, 'ALL', options), {
+ const entries = await this.fetchAllEntries((options) => this.getEntriesByLetter(glossaryId, options), {
cmId: options.cmId,
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
siteId: options.siteId,
@@ -1010,15 +1022,14 @@ export class AddonModGlossaryProvider {
* @param mode The mode in which the glossary was viewed.
* @param name Name of the glossary.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when the WS call is successful.
*/
- logView(glossaryId: number, mode: string, name?: string, siteId?: string): Promise {
+ async logView(glossaryId: number, mode: string, name?: string, siteId?: string): Promise {
const params: AddonModGlossaryViewGlossaryWSParams = {
id: glossaryId,
mode: mode,
};
- return CoreCourseLogHelper.logSingle(
+ await CoreCourseLogHelper.logSingle(
'mod_glossary_view_glossary',
params,
AddonModGlossaryProvider.COMPONENT,
@@ -1037,14 +1048,13 @@ export class AddonModGlossaryProvider {
* @param glossaryId Glossary ID.
* @param name Name of the glossary.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when the WS call is successful.
*/
- logEntryView(entryId: number, glossaryId: number, name?: string, siteId?: string): Promise {
+ async logEntryView(entryId: number, glossaryId: number, name?: string, siteId?: string): Promise {
const params: AddonModGlossaryViewEntryWSParams = {
id: entryId,
};
- return CoreCourseLogHelper.logSingle(
+ await CoreCourseLogHelper.logSingle(
'mod_glossary_view_entry',
params,
AddonModGlossaryProvider.COMPONENT,
@@ -1063,7 +1073,6 @@ export class AddonModGlossaryProvider {
* @param entries Entries.
* @param from The "page" the entries belong to.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when done.
*/
protected async storeEntries(
glossaryId: number,
@@ -1081,7 +1090,6 @@ export class AddonModGlossaryProvider {
* @param entryId Entry ID.
* @param from The "page" the entry belongs to.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when done.
*/
protected async storeEntryId(glossaryId: number, entryId: number, from: number, siteId?: string): Promise {
const site = await CoreSites.getSite(siteId);
@@ -1107,18 +1115,38 @@ declare module '@singletons/events' {
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
*/
export interface CoreEventsData {
- [AddonModGlossaryProvider.ADD_ENTRY_EVENT]: AddonModGlossaryAddEntryEventData;
- [AddonModGlossarySyncProvider.AUTO_SYNCED]: AddonModGlossaryAutoSyncData;
+ [GLOSSARY_ENTRY_ADDED]: AddonModGlossaryEntryAddedEventData;
+ [GLOSSARY_ENTRY_UPDATED]: AddonModGlossaryEntryUpdatedEventData;
+ [GLOSSARY_ENTRY_DELETED]: AddonModGlossaryEntryDeletedEventData;
}
}
/**
- * Data passed to ADD_ENTRY_EVENT.
+ * GLOSSARY_ENTRY_ADDED event payload.
*/
-export type AddonModGlossaryAddEntryEventData = {
+export type AddonModGlossaryEntryAddedEventData = {
glossaryId: number;
entryId?: number;
+ 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;
+ timecreated?: number;
};
/**
@@ -1369,6 +1397,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.
*/
@@ -1389,37 +1446,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;
-};
-
-/**
- * Entry to be added.
- */
-export type AddonModGlossaryNewEntry = {
- concept: string;
- definition: string;
- timecreated: number;
-};
-
-/**
- * Entry to be added, including attachments.
- */
-export type AddonModGlossaryNewEntryWithFiles = AddonModGlossaryNewEntry & {
- files: CoreFileEntry[];
-};
-
/**
* Options to pass to the different get entries functions.
*/
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/services/handlers/prefetch.ts b/src/addons/mod/glossary/services/handlers/prefetch.ts
index d6b6cf5f4..f9566bd0e 100644
--- a/src/addons/mod/glossary/services/handlers/prefetch.ts
+++ b/src/addons/mod/glossary/services/handlers/prefetch.ts
@@ -45,7 +45,7 @@ export class AddonModGlossaryPrefetchHandlerService extends CoreCourseActivityPr
const glossary = await AddonModGlossary.getGlossary(courseId, module.id);
const entries = await AddonModGlossary.fetchAllEntries(
- (options) => AddonModGlossary.getEntriesByLetter(glossary.id, 'ALL', options),
+ (options) => AddonModGlossary.getEntriesByLetter(glossary.id, options),
{
cmId: module.id,
},
@@ -125,43 +125,23 @@ export class AddonModGlossaryPrefetchHandlerService extends CoreCourseActivityPr
break;
case 'cat':
promises.push(AddonModGlossary.fetchAllEntries(
- (newOptions) => AddonModGlossary.getEntriesByCategory(
- glossary.id,
- AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
- newOptions,
- ),
+ (newOptions) => AddonModGlossary.getEntriesByCategory(glossary.id, newOptions),
options,
));
break;
case 'date':
promises.push(AddonModGlossary.fetchAllEntries(
- (newOptions) => AddonModGlossary.getEntriesByDate(
- glossary.id,
- 'CREATION',
- 'DESC',
- newOptions,
- ),
+ (newOptions) => AddonModGlossary.getEntriesByDate(glossary.id, 'CREATION', newOptions),
options,
));
promises.push(AddonModGlossary.fetchAllEntries(
- (newOptions) => AddonModGlossary.getEntriesByDate(
- glossary.id,
- 'UPDATE',
- 'DESC',
- newOptions,
- ),
+ (newOptions) => AddonModGlossary.getEntriesByDate(glossary.id, 'UPDATE', newOptions),
options,
));
break;
case 'author':
promises.push(AddonModGlossary.fetchAllEntries(
- (newOptions) => AddonModGlossary.getEntriesByAuthor(
- glossary.id,
- 'ALL',
- 'LASTNAME',
- 'ASC',
- newOptions,
- ),
+ (newOptions) => AddonModGlossary.getEntriesByAuthor(glossary.id, newOptions),
options,
));
break;
@@ -171,7 +151,7 @@ export class AddonModGlossaryPrefetchHandlerService extends CoreCourseActivityPr
// Fetch all entries to get information from.
promises.push(AddonModGlossary.fetchAllEntries(
- (newOptions) => AddonModGlossary.getEntriesByLetter(glossary.id, 'ALL', newOptions),
+ (newOptions) => AddonModGlossary.getEntriesByLetter(glossary.id, newOptions),
options,
).then((entries) => {
const promises: Promise[] = [];
diff --git a/src/addons/mod/glossary/tests/behat/basic_usage.feature b/src/addons/mod/glossary/tests/behat/basic_usage.feature
index 6a32f476c..4d2e0d270 100644
--- a/src/addons/mod/glossary/tests/behat/basic_usage.feature
+++ b/src/addons/mod/glossary/tests/behat/basic_usage.feature
@@ -154,6 +154,152 @@ 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
+ Scenario: Edit entries
+ Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app
+
+ # Online
+ When I press "Cucumber" in the app
+ And I press "Edit entry" in the app
+ Then the field "Concept" matches value "Cucumber" in the app
+ And the field "Definition" matches value "Sweet cucumber" in the app
+ But I should not find "Keyword(s)" in the app
+ And I should not find "Categories" 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 "Add file" in the app
+ And I upload "stub1.txt" to "File" ".action-sheet-button" in the app
+ And I press "Add file" in the app
+ And I upload "stub2.txt" to "File" ".action-sheet-button" in the app
+ 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
+ And I should find "stub1.txt" in the app
+ And I should find "stub2.txt" in the app
+ But I should not find "Cucumber is a fruit" in the app
+
+ When I press "Edit entry" in the app
+ Then I should find "stub1.txt" in the app
+ And I should find "stub2.txt" 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 press "Delete" within "stub2.txt" "ion-item" in the app
+ And I press "Delete" near "Are you sure you want to delete this file?" in the app
+ And I press "Add file" in the app
+ And I upload "stub3.txt" to "File" ".action-sheet-button" in the app
+ And I press "Save" in the app
+ Then I should find "stub1.txt" in the app
+ And I should find "stub3.txt" in the app
+ But I should not find "stub2.txt" in the app
+
+ When 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 "Cucumber" in the app
+
+ # Offline
+ When I press "Add a new entry" in the app
+ And I switch network connection to offline
+ And I set the following fields to these values in the app:
+ | Concept | Broccoli |
+ | Definition | Brassica oleracea var. italica |
+ | Keyword(s) | vegetable, healthy |
+ And I press "Categories" in the app
+ And I press "The ones I like" in the app
+ And I press "OK" in the app
+ And I press "Add file" in the app
+ And I upload "stub1.txt" to "File" ".action-sheet-button" in the app
+ And I press "Add file" in the app
+ And I upload "stub2.txt" to "File" ".action-sheet-button" in the app
+ 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
+
+ When I press "Broccoli" in the app
+ Then I should find "Brassica oleracea var. italica" in the app
+ And I should find "stub1.txt" in the app
+ And I should find "stub2.txt" in the 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 the field "Keyword(s)" matches value "vegetable, healthy" in the app
+ And I should find "The ones I like" in the app
+ And I should find "stub1.txt" in the app
+ And I should find "stub2.txt" 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 |
+ | Definition | Pickle Rick |
+ And I press "Delete" within "stub2.txt" "ion-item" in the app
+ And I press "Delete" near "Are you sure you want to delete this file?" in the app
+ And I press "Add file" in the app
+ And I upload "stub3.txt" to "File" ".action-sheet-button" in the app
+ And I press "Save" in the app
+ Then I should find "Pickle Rick" in the app
+ And I should find "stub1.txt" in the app
+ And I should find "stub3.txt" in the app
+ But I should not find "stub2.txt" in the app
+ And I should not find "Brassica oleracea var. italica" in the app
+
+ When I press the back button in the app
+ Then I should find "Pickle" in the app
+ And I should find "Potato" in the app
+ But I should not find "Broccoli" in the app
+
+ When I switch network connection to wifi
+ And I press "Information" in the app
+ And I press "Synchronise now" in the app
+ Then I should not find "This Glossary has offline data to be synchronised" in the app
+
+ When I press "Pickle" in the app
+ Then I should find "Pickle Rick" in the app
+ And I should find "stub1.txt" in the app
+ And I should find "stub3.txt" in the app
+ But I should not find "stub2.txt" in the app
+ And I should not find "Brassica oleracea var. italica" in the app
+
+ Scenario: Delete entries
+ Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app
+
+ # Online
+ When I press "Cucumber" in the app
+ And I press "Delete entry" in the app
+ And I press "OK" near "Are you sure you want to delete this entry?" in the app
+ Then I should find "Entry deleted" in the app
+ And I should find "Potato" in the app
+ But I should not find "Cucumber" in the app
+
+ # Offline
+ When I press "Add a new entry" in the app
+ And I switch network connection to offline
+ And I set the following fields to these values in the app:
+ | Concept | Broccoli |
+ | Definition | Brassica oleracea var. italica |
+ And I press "Save" in the app
+ Then I should find "Potato" in the app
+ And I should find "Broccoli" in the app
+
+ When I press "Broccoli" in the app
+ Then I should find "Brassica oleracea var. italica" in the app
+
+ When I press "Delete entry" in the app
+ And I press "OK" near "Are you sure you want to delete this entry?" in the app
+ Then I should find "Entry deleted" in the app
+ And I should find "Potato" in the app
+ But I should not find "Broccoli" in the app
+
Scenario: Sync
Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app
And I press "Add a new entry" in the app
@@ -192,8 +338,8 @@ Feature: Test basic usage of glossary in app
And I should find "Broccoli" in the app
And I should find "Cabbage" in the app
And I should find "Garlic" in the app
- But I should not see "Entries to be synced"
- And I should not see "This Glossary has offline data to be synchronised."
+ But I should not find "Entries to be synced" in the app
+ And I should not find "This Glossary has offline data to be synchronised." in the app
When I press "Garlic" in the app
Then I should find "Garlic" in the app
diff --git a/src/addons/mod/glossary/tests/behat/fixtures/stub1.txt b/src/addons/mod/glossary/tests/behat/fixtures/stub1.txt
new file mode 100644
index 000000000..38257d448
--- /dev/null
+++ b/src/addons/mod/glossary/tests/behat/fixtures/stub1.txt
@@ -0,0 +1 @@
+This is a stub.
diff --git a/src/addons/mod/glossary/tests/behat/fixtures/stub2.txt b/src/addons/mod/glossary/tests/behat/fixtures/stub2.txt
new file mode 100644
index 000000000..38257d448
--- /dev/null
+++ b/src/addons/mod/glossary/tests/behat/fixtures/stub2.txt
@@ -0,0 +1 @@
+This is a stub.
diff --git a/src/addons/mod/glossary/tests/behat/fixtures/stub3.txt b/src/addons/mod/glossary/tests/behat/fixtures/stub3.txt
new file mode 100644
index 000000000..38257d448
--- /dev/null
+++ b/src/addons/mod/glossary/tests/behat/fixtures/stub3.txt
@@ -0,0 +1 @@
+This is a stub.
diff --git a/src/addons/mod/glossary/tests/behat/navigation.feature b/src/addons/mod/glossary/tests/behat/navigation.feature
index 659d286ff..4700884e6 100644
--- a/src/addons/mod/glossary/tests/behat/navigation.feature
+++ b/src/addons/mod/glossary/tests/behat/navigation.feature
@@ -200,6 +200,17 @@ Feature: Test glossary navigation
When I swipe to the left in the app
Then I should find "Acerola is a fruit" in the app
+ # Edit
+ When I swipe to the right in the app
+ And I press "Edit entry" in the app
+ And I press "Save" in the app
+ Then I should find "Tomato is a fruit" in the app
+
+ When I press the back button in the app
+ Then I should find "Tomato" in the app
+ And I should find "Cashew" in the app
+ And I should find "Acerola" in the app
+
@ci_jenkins_skip
Scenario: Tablet navigation on glossary
Given I entered the course "Course 1" as "student1" in the app
@@ -280,6 +291,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 |
@@ -300,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
diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts
index 395afc37e..39ef612e6 100644
--- a/src/core/services/utils/utils.ts
+++ b/src/core/services/utils/utils.ts
@@ -1772,9 +1772,9 @@ export class CoreUtilsProvider {
* @param fallback Value to return if the promise is rejected.
* @returns Promise with ignored errors, resolving to the fallback result if provided.
*/
- async ignoreErrors(promise: Promise): Promise;
+ async ignoreErrors(promise?: Promise): Promise;
async ignoreErrors(promise: Promise, fallback: Fallback): Promise;
- async ignoreErrors(promise: Promise, fallback?: Fallback): Promise {
+ async ignoreErrors(promise?: Promise, fallback?: Fallback): Promise {
try {
const result = await promise;
diff --git a/src/testing/services/behat-dom.ts b/src/testing/services/behat-dom.ts
index e2cadf34e..2705a33ac 100644
--- a/src/testing/services/behat-dom.ts
+++ b/src/testing/services/behat-dom.ts
@@ -421,7 +421,7 @@ export class TestingBehatDomUtilsService {
*/
findElementBasedOnText(
locator: TestingBehatElementLocator,
- options: TestingBehatFindOptions,
+ options: TestingBehatFindOptions = {},
): HTMLElement | undefined {
return this.findElementsBasedOnText(locator, options)[0];
}
@@ -437,7 +437,7 @@ export class TestingBehatDomUtilsService {
locator: TestingBehatElementLocator,
options: TestingBehatFindOptions,
): HTMLElement[] {
- const topContainers = this.getCurrentTopContainerElements(options.containerName);
+ const topContainers = this.getCurrentTopContainerElements(options.containerName ?? '');
let elements: HTMLElement[] = [];
for (let i = 0; i < topContainers.length; i++) {
diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts
index e9cb4f2e4..b5d73ce60 100644
--- a/src/testing/services/behat-runtime.ts
+++ b/src/testing/services/behat-runtime.ts
@@ -361,6 +361,40 @@ export class TestingBehatRuntimeService {
}
}
+ /**
+ * Get a file input id, adding it if necessary.
+ *
+ * @param locator Input locator.
+ * @returns Input id if successful, or ERROR: followed by message
+ */
+ async getFileInputId(locator: TestingBehatElementLocator): Promise {
+ this.log('Action - Upload File', { locator });
+
+ try {
+ const inputOrContainer = TestingBehatDomUtils.findElementBasedOnText(locator);
+
+ if (!inputOrContainer) {
+ return 'ERROR: No element matches input locator.';
+ }
+
+ const input = inputOrContainer.matches('input[type="file"]')
+ ? inputOrContainer
+ : inputOrContainer.querySelector('input[type="file"]');
+
+ if (!input) {
+ return 'ERROR: Input element does not contain a file input.';
+ }
+
+ if (!input.hasAttribute('id')) {
+ input.setAttribute('id', `file-${Date.now()}`);
+ }
+
+ return input.getAttribute('id') ?? '';
+ } catch (error) {
+ return 'ERROR: ' + error.message;
+ }
+ }
+
/**
* Trigger a pull to refresh gesture in the current page.
*
@@ -635,8 +669,8 @@ export type BehatTestsWindow = Window & {
};
export type TestingBehatFindOptions = {
- containerName: string;
- onlyClickable: boolean;
+ containerName?: string;
+ onlyClickable?: boolean;
};
export type TestingBehatElementLocator = {
diff --git a/upgrade.txt b/upgrade.txt
index eebe0c6c4..bf8043196 100644
--- a/upgrade.txt
+++ b/upgrade.txt
@@ -6,6 +6,7 @@ information provided here is intended especially for developers.
- CoreIconComponent has been removed after deprecation period: Use CoreFaIconDirective instead.
- The courseSummaryComponent property has been removed from the CoreCourseFormatComponent component, and the getCourseSummaryComponent method from the CoreCourseFormatHandler interface.
- Font Awesome icon library has been updated to 6.3.0.
+- Some methods in glossary addon services have changed.
=== 4.1.0 ===