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 ===