MOBILE-2652 glossary: Implement deleting entries
parent
7e8e694c63
commit
5de4cfbbd2
|
@ -681,6 +681,7 @@
|
||||||
"addon.mod_forum.yourreply": "forum",
|
"addon.mod_forum.yourreply": "forum",
|
||||||
"addon.mod_glossary.addentry": "glossary",
|
"addon.mod_glossary.addentry": "glossary",
|
||||||
"addon.mod_glossary.aliases": "glossary",
|
"addon.mod_glossary.aliases": "glossary",
|
||||||
|
"addon.mod_glossary.areyousuredelete": "glossary",
|
||||||
"addon.mod_glossary.attachment": "glossary",
|
"addon.mod_glossary.attachment": "glossary",
|
||||||
"addon.mod_glossary.browsemode": "local_moodlemobileapp",
|
"addon.mod_glossary.browsemode": "local_moodlemobileapp",
|
||||||
"addon.mod_glossary.byalphabet": "local_moodlemobileapp",
|
"addon.mod_glossary.byalphabet": "local_moodlemobileapp",
|
||||||
|
@ -694,9 +695,12 @@
|
||||||
"addon.mod_glossary.categories": "glossary",
|
"addon.mod_glossary.categories": "glossary",
|
||||||
"addon.mod_glossary.concept": "glossary",
|
"addon.mod_glossary.concept": "glossary",
|
||||||
"addon.mod_glossary.definition": "glossary",
|
"addon.mod_glossary.definition": "glossary",
|
||||||
|
"addon.mod_glossary.deleteentry": "glossary",
|
||||||
"addon.mod_glossary.entriestobesynced": "local_moodlemobileapp",
|
"addon.mod_glossary.entriestobesynced": "local_moodlemobileapp",
|
||||||
|
"addon.mod_glossary.entrydeleted": "glossary",
|
||||||
"addon.mod_glossary.entrypendingapproval": "local_moodlemobileapp",
|
"addon.mod_glossary.entrypendingapproval": "local_moodlemobileapp",
|
||||||
"addon.mod_glossary.entryusedynalink": "glossary",
|
"addon.mod_glossary.entryusedynalink": "glossary",
|
||||||
|
"addon.mod_glossary.errordeleting": "local_moodlemobileapp",
|
||||||
"addon.mod_glossary.errconceptalreadyexists": "glossary",
|
"addon.mod_glossary.errconceptalreadyexists": "glossary",
|
||||||
"addon.mod_glossary.errorloadingentries": "local_moodlemobileapp",
|
"addon.mod_glossary.errorloadingentries": "local_moodlemobileapp",
|
||||||
"addon.mod_glossary.errorloadingentry": "local_moodlemobileapp",
|
"addon.mod_glossary.errorloadingentry": "local_moodlemobileapp",
|
||||||
|
|
|
@ -179,12 +179,14 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidate glossary cache.
|
* Invalidate glossary cache.
|
||||||
|
*
|
||||||
|
* @param invalidateGlossary Whether to invalidate the entire glossary or not
|
||||||
*/
|
*/
|
||||||
async invalidateCache(): Promise<void> {
|
async invalidateCache(invalidateGlossary: boolean = true): Promise<void> {
|
||||||
await Promise.all([
|
await Promise.all<unknown>([
|
||||||
AddonModGlossary.invalidateCourseGlossaries(this.COURSE_ID),
|
|
||||||
this.fetchInvalidate && this.fetchInvalidate(),
|
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),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,12 +42,14 @@ import {
|
||||||
AddonModGlossaryEntryWithCategory,
|
AddonModGlossaryEntryWithCategory,
|
||||||
AddonModGlossaryGlossary,
|
AddonModGlossaryGlossary,
|
||||||
AddonModGlossaryProvider,
|
AddonModGlossaryProvider,
|
||||||
|
GLOSSARY_ENTRY_ADDED,
|
||||||
|
GLOSSARY_ENTRY_DELETED,
|
||||||
} from '../../services/glossary';
|
} from '../../services/glossary';
|
||||||
import { AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
|
import { AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
|
||||||
import {
|
import {
|
||||||
AddonModGlossaryAutoSyncData,
|
AddonModGlossaryAutoSyncedData,
|
||||||
AddonModGlossarySyncProvider,
|
|
||||||
AddonModGlossarySyncResult,
|
AddonModGlossarySyncResult,
|
||||||
|
GLOSSARY_AUTO_SYNCED,
|
||||||
} from '../../services/glossary-sync';
|
} from '../../services/glossary-sync';
|
||||||
import { AddonModGlossaryModuleHandlerService } from '../../services/handlers/module';
|
import { AddonModGlossaryModuleHandlerService } from '../../services/handlers/module';
|
||||||
import { AddonModGlossaryPrefetchHandler } from '../../services/handlers/prefetch';
|
import { AddonModGlossaryPrefetchHandler } from '../../services/handlers/prefetch';
|
||||||
|
@ -75,13 +77,11 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
|
|
||||||
protected hasOfflineEntries = false;
|
protected hasOfflineEntries = false;
|
||||||
protected hasOfflineRatings = false;
|
protected hasOfflineRatings = false;
|
||||||
protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
|
protected syncEventName = GLOSSARY_AUTO_SYNCED;
|
||||||
protected addEntryObserver?: CoreEventObserver;
|
|
||||||
protected fetchedEntriesCanLoadMore = false;
|
protected fetchedEntriesCanLoadMore = false;
|
||||||
protected fetchedEntries: AddonModGlossaryEntry[] = [];
|
protected fetchedEntries: AddonModGlossaryEntry[] = [];
|
||||||
protected sourceUnsubscribe?: () => void;
|
protected sourceUnsubscribe?: () => void;
|
||||||
protected ratingOfflineObserver?: CoreEventObserver;
|
protected observers?: CoreEventObserver[];
|
||||||
protected ratingSyncObserver?: CoreEventObserver;
|
|
||||||
protected checkCompletionAfterLog = false; // Use CoreListItemsManager log system instead.
|
protected checkCompletionAfterLog = false; // Use CoreListItemsManager log system instead.
|
||||||
|
|
||||||
getDivider?: (entry: AddonModGlossaryEntry) => string;
|
getDivider?: (entry: AddonModGlossaryEntry) => string;
|
||||||
|
@ -136,30 +136,41 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
});
|
});
|
||||||
|
|
||||||
// When an entry is added, we reload the data.
|
// When an entry is added, we reload the data.
|
||||||
this.addEntryObserver = CoreEvents.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, (data) => {
|
this.observers = [
|
||||||
if (this.glossary && this.glossary.id === data.glossaryId) {
|
CoreEvents.on(GLOSSARY_ENTRY_ADDED, ({ glossaryId }) => {
|
||||||
this.showLoadingAndRefresh(false);
|
if (this.glossary?.id !== glossaryId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check completion since it could be configured to complete once the user adds a new entry.
|
// Check completion since it could be configured to complete once the user adds a new entry.
|
||||||
this.checkCompletion();
|
this.checkCompletion();
|
||||||
}
|
|
||||||
});
|
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.
|
// 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'
|
if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
|
||||||
&& data.instanceId == this.glossary.coursemodule) {
|
&& data.instanceId == this.glossary.coursemodule) {
|
||||||
this.hasOfflineRatings = true;
|
this.hasOfflineRatings = true;
|
||||||
this.hasOffline = 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'
|
if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
|
||||||
&& data.instanceId == this.glossary.coursemodule) {
|
&& data.instanceId == this.glossary.coursemodule) {
|
||||||
this.hasOfflineRatings = false;
|
this.hasOfflineRatings = false;
|
||||||
this.hasOffline = this.hasOfflineEntries;
|
this.hasOffline = this.hasOfflineEntries;
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -227,7 +238,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* @param syncEventData Data receiven on sync observer.
|
* @param syncEventData Data receiven on sync observer.
|
||||||
* @returns True if refresh is needed, false otherwise.
|
* @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 &&
|
return !!this.glossary && syncEventData.glossaryId == this.glossary.id &&
|
||||||
syncEventData.userId == CoreSites.getCurrentSiteUserId();
|
syncEventData.userId == CoreSites.getCurrentSiteUserId();
|
||||||
}
|
}
|
||||||
|
@ -410,9 +421,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
super.ngOnDestroy();
|
super.ngOnDestroy();
|
||||||
|
|
||||||
this.addEntryObserver?.off();
|
this.observers?.forEach(observer => observer.off());
|
||||||
this.ratingOfflineObserver?.off();
|
|
||||||
this.ratingSyncObserver?.off();
|
|
||||||
this.sourceUnsubscribe?.call(null);
|
this.sourceUnsubscribe?.call(null);
|
||||||
this.entries?.destroy();
|
this.entries?.destroy();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"addentry": "Add a new entry",
|
"addentry": "Add a new entry",
|
||||||
"aliases": "Keyword(s)",
|
"aliases": "Keyword(s)",
|
||||||
|
"areyousuredelete": "Are you sure you want to delete this entry?",
|
||||||
"attachment": "Attachment",
|
"attachment": "Attachment",
|
||||||
"browsemode": "Browse entries",
|
"browsemode": "Browse entries",
|
||||||
"byalphabet": "Alphabetically",
|
"byalphabet": "Alphabetically",
|
||||||
|
@ -14,10 +15,13 @@
|
||||||
"categories": "Categories",
|
"categories": "Categories",
|
||||||
"concept": "Concept",
|
"concept": "Concept",
|
||||||
"definition": "Definition",
|
"definition": "Definition",
|
||||||
|
"deleteentry": "Delete entry",
|
||||||
"entriestobesynced": "Entries to be synced",
|
"entriestobesynced": "Entries to be synced",
|
||||||
|
"entrydeleted": "Entry deleted",
|
||||||
"entrypendingapproval": "This entry is pending approval.",
|
"entrypendingapproval": "This entry is pending approval.",
|
||||||
"entryusedynalink": "This entry should be automatically linked",
|
"entryusedynalink": "This entry should be automatically linked",
|
||||||
"errconceptalreadyexists": "This concept already exists. No duplicates allowed in this glossary.",
|
"errconceptalreadyexists": "This concept already exists. No duplicates allowed in this glossary.",
|
||||||
|
"errordeleting": "Error deleting entry.",
|
||||||
"errorloadingentries": "An error occurred while loading entries.",
|
"errorloadingentries": "An error occurred while loading entries.",
|
||||||
"errorloadingentry": "An error occurred while loading the entry.",
|
"errorloadingentry": "An error occurred while loading the entry.",
|
||||||
"errorloadingglossary": "An error occurred while loading the glossary.",
|
"errorloadingglossary": "An error occurred while loading the glossary.",
|
||||||
|
|
|
@ -46,6 +46,13 @@
|
||||||
</core-format-text>
|
</core-format-text>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
<ion-item *ngIf="canDelete">
|
||||||
|
<div slot="end">
|
||||||
|
<ion-button fill="clear" (click)="deleteEntry()" [attr.aria-label]="'addon.mod_glossary.deleteentry' | translate">
|
||||||
|
<ion-icon slot="icon-only" name="fas-trash" aria-hidden="true"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</ion-item>
|
||||||
<div *ngIf="entry.attachment">
|
<div *ngIf="entry.attachment">
|
||||||
<core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId">
|
<core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId">
|
||||||
</core-file>
|
</core-file>
|
||||||
|
|
|
@ -12,17 +12,20 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
import { Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
|
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
|
||||||
import { CoreComments } from '@features/comments/services/comments';
|
import { CoreComments } from '@features/comments/services/comments';
|
||||||
import { CoreRatingInfo } from '@features/rating/services/rating';
|
import { CoreRatingInfo } from '@features/rating/services/rating';
|
||||||
import { CoreTag } from '@features/tag/services/tag';
|
import { CoreTag } from '@features/tag/services/tag';
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
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 { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source';
|
import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source';
|
||||||
import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager';
|
import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager';
|
||||||
import {
|
import {
|
||||||
|
@ -53,13 +56,14 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
||||||
showDate = false;
|
showDate = false;
|
||||||
ratingInfo?: CoreRatingInfo;
|
ratingInfo?: CoreRatingInfo;
|
||||||
tagsEnabled = false;
|
tagsEnabled = false;
|
||||||
|
canDelete = false;
|
||||||
commentsEnabled = false;
|
commentsEnabled = false;
|
||||||
courseId!: number;
|
courseId!: number;
|
||||||
cmId?: number;
|
cmId?: number;
|
||||||
|
|
||||||
protected entryId!: number;
|
protected entryId!: number;
|
||||||
|
|
||||||
constructor(protected route: ActivatedRoute) {}
|
constructor(@Optional() protected splitView: CoreSplitViewComponent, protected route: ActivatedRoute) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
|
@ -113,6 +117,46 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
||||||
this.entries?.destroy();
|
this.entries?.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete entry.
|
||||||
|
*/
|
||||||
|
async deleteEntry(): Promise<void> {
|
||||||
|
const entryId = this.entry?.id;
|
||||||
|
const glossaryId = this.glossary?.id;
|
||||||
|
const cancelled = await CoreUtils.promiseFails(
|
||||||
|
CoreDomUtils.showConfirm(Translate.instant('addon.mod_glossary.areyousuredelete')),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!entryId || !glossaryId || cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = await CoreDomUtils.showModalLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AddonModGlossary.deleteEntry(glossaryId, entryId);
|
||||||
|
await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(entryId));
|
||||||
|
await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByLetter(glossaryId));
|
||||||
|
await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByAuthor(glossaryId));
|
||||||
|
await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByCategory(glossaryId));
|
||||||
|
await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'CREATION'));
|
||||||
|
await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'UPDATE'));
|
||||||
|
await CoreUtils.ignoreErrors(this.entries?.getSource().invalidateCache(false));
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh the data.
|
* Refresh the data.
|
||||||
*
|
*
|
||||||
|
@ -142,9 +186,11 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
||||||
protected async fetchEntry(): Promise<void> {
|
protected async fetchEntry(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = await AddonModGlossary.getEntry(this.entryId);
|
const result = await AddonModGlossary.getEntry(this.entryId);
|
||||||
|
const canDeleteEntries = CoreNetwork.isOnline() && await AddonModGlossary.canDeleteEntries();
|
||||||
|
|
||||||
this.entry = result.entry;
|
this.entry = result.entry;
|
||||||
this.ratingInfo = result.ratinginfo;
|
this.ratingInfo = result.ratinginfo;
|
||||||
|
this.canDelete = canDeleteEntries && !!result.permissions?.candelete;
|
||||||
|
|
||||||
if (this.glossary) {
|
if (this.glossary) {
|
||||||
// Glossary already loaded, nothing else to load.
|
// Glossary already loaded, nothing else to load.
|
||||||
|
|
|
@ -19,9 +19,10 @@ import { CoreSites } from '@services/sites';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreEvents } from '@singletons/events';
|
||||||
import { CorePath } from '@singletons/path';
|
import { CorePath } from '@singletons/path';
|
||||||
import { AddonModGlossaryOfflineEntryDBRecord, OFFLINE_ENTRIES_TABLE_NAME } from './database/glossary';
|
import { AddonModGlossaryOfflineEntryDBRecord, OFFLINE_ENTRIES_TABLE_NAME } from './database/glossary';
|
||||||
import { AddonModGlossaryDiscardedEntry, AddonModGlossaryEntryOption } from './glossary';
|
import { AddonModGlossaryEntryOption, GLOSSARY_ENTRY_ADDED } from './glossary';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to handle offline glossary.
|
* Service to handle offline glossary.
|
||||||
|
@ -159,7 +160,7 @@ export class AddonModGlossaryOfflineProvider {
|
||||||
* @param courseId Course ID of the glossary.
|
* @param courseId Course ID of the glossary.
|
||||||
* @param options Options for the entry.
|
* @param options Options for the entry.
|
||||||
* @param attachments Result of CoreFileUploaderProvider#storeFilesToUpload for attachments.
|
* @param attachments Result of CoreFileUploaderProvider#storeFilesToUpload for attachments.
|
||||||
* @param timeCreated The time the entry was created. If not defined, current time.
|
* @param timecreated The time the entry was created. If not defined, current time.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @param userId User the entry belong to. If not defined, current user in 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.
|
* @param discardEntry The entry provided will be discarded if found.
|
||||||
|
@ -172,12 +173,13 @@ export class AddonModGlossaryOfflineProvider {
|
||||||
courseId: number,
|
courseId: number,
|
||||||
options?: Record<string, AddonModGlossaryEntryOption>,
|
options?: Record<string, AddonModGlossaryEntryOption>,
|
||||||
attachments?: CoreFileUploaderStoreFilesResult,
|
attachments?: CoreFileUploaderStoreFilesResult,
|
||||||
timeCreated?: number,
|
timecreated?: number,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
userId?: number,
|
userId?: number,
|
||||||
discardEntry?: AddonModGlossaryDiscardedEntry,
|
discardEntry?: AddonModGlossaryDiscardedEntry,
|
||||||
): Promise<false> {
|
): Promise<false> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
timecreated = timecreated || Date.now();
|
||||||
|
|
||||||
const entry: AddonModGlossaryOfflineEntryDBRecord = {
|
const entry: AddonModGlossaryOfflineEntryDBRecord = {
|
||||||
glossaryid: glossaryId,
|
glossaryid: glossaryId,
|
||||||
|
@ -188,7 +190,7 @@ export class AddonModGlossaryOfflineProvider {
|
||||||
options: JSON.stringify(options || {}),
|
options: JSON.stringify(options || {}),
|
||||||
attachments: JSON.stringify(attachments),
|
attachments: JSON.stringify(attachments),
|
||||||
userid: userId || site.getUserId(),
|
userid: userId || site.getUserId(),
|
||||||
timecreated: timeCreated || Date.now(),
|
timecreated,
|
||||||
};
|
};
|
||||||
|
|
||||||
// If editing an offline entry, delete previous first.
|
// If editing an offline entry, delete previous first.
|
||||||
|
@ -198,6 +200,8 @@ export class AddonModGlossaryOfflineProvider {
|
||||||
|
|
||||||
await site.getDb().insertRecord(OFFLINE_ENTRIES_TABLE_NAME, entry);
|
await site.getDb().insertRecord(OFFLINE_ENTRIES_TABLE_NAME, entry);
|
||||||
|
|
||||||
|
CoreEvents.trigger(GLOSSARY_ENTRY_ADDED, { glossaryId, timecreated }, siteId);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,14 +31,14 @@ import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from './glossar
|
||||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||||
import { CoreFileEntry } from '@services/file-helper';
|
import { CoreFileEntry } from '@services/file-helper';
|
||||||
|
|
||||||
|
export const GLOSSARY_AUTO_SYNCED = 'addon_mod_glossary_auto_synced';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to sync glossaries.
|
* Service to sync glossaries.
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModGlossarySyncResult> {
|
export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModGlossarySyncResult> {
|
||||||
|
|
||||||
static readonly AUTO_SYNCED = 'addon_mod_glossary_autom_synced';
|
|
||||||
|
|
||||||
protected componentTranslatableString = 'glossary';
|
protected componentTranslatableString = 'glossary';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -98,7 +98,7 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
|
||||||
|
|
||||||
if (result?.updated) {
|
if (result?.updated) {
|
||||||
// Sync successful, send event.
|
// Sync successful, send event.
|
||||||
CoreEvents.trigger(AddonModGlossarySyncProvider.AUTO_SYNCED, {
|
CoreEvents.trigger(GLOSSARY_AUTO_SYNCED, {
|
||||||
glossaryId: entry.glossaryid,
|
glossaryId: entry.glossaryid,
|
||||||
userId: entry.userid,
|
userId: entry.userid,
|
||||||
warnings: result.warnings,
|
warnings: result.warnings,
|
||||||
|
@ -341,15 +341,28 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
|
||||||
|
|
||||||
export const AddonModGlossarySync = makeSingleton(AddonModGlossarySyncProvider);
|
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.
|
* Data returned by a glossary sync.
|
||||||
*/
|
*/
|
||||||
export type AddonModGlossarySyncResult = CoreSyncResult;
|
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;
|
glossaryId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
|
|
|
@ -25,11 +25,14 @@ import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
|
import { CoreEvents } from '@singletons/events';
|
||||||
import { AddonModGlossaryEntryDBRecord, ENTRIES_TABLE_NAME } from './database/glossary';
|
import { AddonModGlossaryEntryDBRecord, ENTRIES_TABLE_NAME } from './database/glossary';
|
||||||
import { AddonModGlossaryOffline } from './glossary-offline';
|
import { AddonModGlossaryOffline } from './glossary-offline';
|
||||||
import { AddonModGlossaryAutoSyncData, AddonModGlossarySyncProvider } from './glossary-sync';
|
|
||||||
import { CoreFileEntry } from '@services/file-helper';
|
import { CoreFileEntry } from '@services/file-helper';
|
||||||
|
|
||||||
|
export const GLOSSARY_ENTRY_ADDED = 'addon_mod_glossary_entry_added';
|
||||||
|
export const GLOSSARY_ENTRY_DELETED = 'addon_mod_glossary_entry_deleted';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that provides some features for glossaries.
|
* Service that provides some features for glossaries.
|
||||||
*/
|
*/
|
||||||
|
@ -40,8 +43,6 @@ export class AddonModGlossaryProvider {
|
||||||
static readonly LIMIT_ENTRIES = 25;
|
static readonly LIMIT_ENTRIES = 25;
|
||||||
static readonly LIMIT_CATEGORIES = 10;
|
static readonly LIMIT_CATEGORIES = 10;
|
||||||
|
|
||||||
static readonly ADD_ENTRY_EVENT = 'addon_mod_glossary_add_entry';
|
|
||||||
|
|
||||||
private static readonly SHOW_ALL_CATEGORIES = 0;
|
private static readonly SHOW_ALL_CATEGORIES = 0;
|
||||||
private static readonly ROOT_CACHE_KEY = 'mmaModGlossary:';
|
private static readonly ROOT_CACHE_KEY = 'mmaModGlossary:';
|
||||||
|
|
||||||
|
@ -606,6 +607,18 @@ export class AddonModGlossaryProvider {
|
||||||
throw new CoreError('Entry not found.');
|
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<boolean> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
return site.wsAvailable('mod_glossary_delete_entry');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs the whole fetch of the entries using the proper function and arguments.
|
* Performs the whole fetch of the entries using the proper function and arguments.
|
||||||
*
|
*
|
||||||
|
@ -847,7 +860,7 @@ export class AddonModGlossaryProvider {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to add it in online.
|
// Try to add it in online.
|
||||||
return await this.addEntryOnline(
|
const entryId = await this.addEntryOnline(
|
||||||
glossaryId,
|
glossaryId,
|
||||||
concept,
|
concept,
|
||||||
definition,
|
definition,
|
||||||
|
@ -855,6 +868,8 @@ export class AddonModGlossaryProvider {
|
||||||
<number> attachments,
|
<number> attachments,
|
||||||
otherOptions.siteId,
|
otherOptions.siteId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return entryId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (otherOptions.allowOffline && !CoreUtils.isWebServiceError(error)) {
|
if (otherOptions.allowOffline && !CoreUtils.isWebServiceError(error)) {
|
||||||
// Couldn't connect to server, store in offline.
|
// Couldn't connect to server, store in offline.
|
||||||
|
@ -904,9 +919,25 @@ export class AddonModGlossaryProvider {
|
||||||
|
|
||||||
const response = await site.write<AddonModGlossaryAddEntryWSResponse>('mod_glossary_add_entry', params);
|
const response = await site.write<AddonModGlossaryAddEntryWSResponse>('mod_glossary_add_entry', params);
|
||||||
|
|
||||||
|
CoreEvents.trigger(GLOSSARY_ENTRY_ADDED, { glossaryId, entryId: response.entryid }, siteId);
|
||||||
|
|
||||||
return response.entryid;
|
return response.entryid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete entry.
|
||||||
|
*
|
||||||
|
* @param glossaryId Glossary id.
|
||||||
|
* @param entryId Entry id.
|
||||||
|
*/
|
||||||
|
async deleteEntry(glossaryId: number, entryId: number): Promise<void> {
|
||||||
|
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.
|
* Check if a entry concept is already used.
|
||||||
*
|
*
|
||||||
|
@ -1040,18 +1071,27 @@ declare module '@singletons/events' {
|
||||||
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
||||||
*/
|
*/
|
||||||
export interface CoreEventsData {
|
export interface CoreEventsData {
|
||||||
[AddonModGlossaryProvider.ADD_ENTRY_EVENT]: AddonModGlossaryAddEntryEventData;
|
[GLOSSARY_ENTRY_ADDED]: AddonModGlossaryEntryAddedEventData;
|
||||||
[AddonModGlossarySyncProvider.AUTO_SYNCED]: AddonModGlossaryAutoSyncData;
|
[GLOSSARY_ENTRY_DELETED]: AddonModGlossaryEntryDeletedEventData;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data passed to ADD_ENTRY_EVENT.
|
* GLOSSARY_ENTRY_ADDED event payload.
|
||||||
*/
|
*/
|
||||||
export type AddonModGlossaryAddEntryEventData = {
|
export type AddonModGlossaryEntryAddedEventData = {
|
||||||
glossaryId: number;
|
glossaryId: number;
|
||||||
entryId?: number;
|
entryId?: number;
|
||||||
|
timecreated?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GLOSSARY_ENTRY_DELETED event payload.
|
||||||
|
*/
|
||||||
|
export type AddonModGlossaryEntryDeletedEventData = {
|
||||||
|
glossaryId: number;
|
||||||
|
entryId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -154,6 +154,54 @@ Feature: Test basic usage of glossary in app
|
||||||
Then I should find "Garlic" in the app
|
Then I should find "Garlic" in the app
|
||||||
And I should find "Allium sativum" in the app
|
And I should find "Allium sativum" in the app
|
||||||
|
|
||||||
|
@noeldebug
|
||||||
|
Scenario: Edit entries (basic info)
|
||||||
|
Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app
|
||||||
|
|
||||||
|
# TODO online
|
||||||
|
|
||||||
|
# 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 "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
|
||||||
|
|
||||||
|
When I set the following fields to these values in the app:
|
||||||
|
| Concept | Pickle |
|
||||||
|
| Definition | Pickle Rick |
|
||||||
|
And I press "Save" in the app
|
||||||
|
Then I should find "Pickle Rick" in the app
|
||||||
|
But 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
|
||||||
|
|
||||||
|
# TODO test attachments? (yes, in all scenarios!!)
|
||||||
|
# TODO And I upload "stub.txt" to "File" ".action-sheet-button" in the app
|
||||||
|
|
||||||
|
Scenario: Delete entries
|
||||||
|
Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
Scenario: Sync
|
Scenario: Sync
|
||||||
Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app
|
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
|
And I press "Add a new entry" in the app
|
||||||
|
|
|
@ -1772,9 +1772,9 @@ export class CoreUtilsProvider {
|
||||||
* @param fallback Value to return if the promise is rejected.
|
* @param fallback Value to return if the promise is rejected.
|
||||||
* @returns Promise with ignored errors, resolving to the fallback result if provided.
|
* @returns Promise with ignored errors, resolving to the fallback result if provided.
|
||||||
*/
|
*/
|
||||||
async ignoreErrors<Result>(promise: Promise<Result>): Promise<Result | undefined>;
|
async ignoreErrors<Result>(promise?: Promise<Result>): Promise<Result | undefined>;
|
||||||
async ignoreErrors<Result, Fallback>(promise: Promise<Result>, fallback: Fallback): Promise<Result | Fallback>;
|
async ignoreErrors<Result, Fallback>(promise: Promise<Result>, fallback: Fallback): Promise<Result | Fallback>;
|
||||||
async ignoreErrors<Result, Fallback>(promise: Promise<Result>, fallback?: Fallback): Promise<Result | Fallback | undefined> {
|
async ignoreErrors<Result, Fallback>(promise?: Promise<Result>, fallback?: Fallback): Promise<Result | Fallback | undefined> {
|
||||||
try {
|
try {
|
||||||
const result = await promise;
|
const result = await promise;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue