Merge pull request #2721 from dpalou/MOBILE-3644

Mobile 3644
main
Dani Palou 2021-04-14 12:05:30 +02:00 committed by GitHub
commit f48433215b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 4852 additions and 150 deletions

View File

@ -28,8 +28,5 @@ import { AddonBlockActivityModulesComponent } from './activitymodules/activitymo
exports: [
AddonBlockActivityModulesComponent,
],
entryComponents: [
AddonBlockActivityModulesComponent,
],
})
export class AddonBlockActivityModulesComponentsModule {}

View File

@ -27,8 +27,5 @@ import { AddonBlockActivityResultsComponent } from './activityresults/activityre
exports: [
AddonBlockActivityResultsComponent,
],
entryComponents: [
AddonBlockActivityResultsComponent,
],
})
export class AddonBlockActivityResultsComponentsModule {}

View File

@ -27,8 +27,5 @@ import { AddonBlockBadgesComponent } from './badges/badges';
exports: [
AddonBlockBadgesComponent,
],
entryComponents: [
AddonBlockBadgesComponent,
],
})
export class AddonBlockBadgesComponentsModule {}

View File

@ -27,8 +27,5 @@ import { AddonBlockBlogMenuComponent } from './blogmenu/blogmenu';
exports: [
AddonBlockBlogMenuComponent,
],
entryComponents: [
AddonBlockBlogMenuComponent,
],
})
export class AddonBlockBlogMenuComponentsModule {}

View File

@ -27,8 +27,5 @@ import { AddonBlockBlogRecentComponent } from './blogrecent/blogrecent';
exports: [
AddonBlockBlogRecentComponent,
],
entryComponents: [
AddonBlockBlogRecentComponent,
],
})
export class AddonBlockBlogRecentComponentsModule {}

View File

@ -27,8 +27,5 @@ import { AddonBlockBlogTagsComponent } from './blogtags/blogtags';
exports: [
AddonBlockBlogTagsComponent,
],
entryComponents: [
AddonBlockBlogTagsComponent,
],
})
export class AddonBlockBlogTagsComponentsModule {}

View File

@ -29,8 +29,5 @@ import { AddonBlockMyOverviewComponent } from './myoverview/myoverview';
exports: [
AddonBlockMyOverviewComponent,
],
entryComponents: [
AddonBlockMyOverviewComponent,
],
})
export class AddonBlockMyOverviewComponentsModule {}

View File

@ -27,8 +27,5 @@ import { AddonBlockNewsItemsComponent } from './newsitems/newsitems';
exports: [
AddonBlockNewsItemsComponent,
],
entryComponents: [
AddonBlockNewsItemsComponent,
],
})
export class AddonBlockNewsItemsComponentsModule {}

View File

@ -27,8 +27,5 @@ import { AddonBlockOnlineUsersComponent } from './onlineusers/onlineusers';
exports: [
AddonBlockOnlineUsersComponent,
],
entryComponents: [
AddonBlockOnlineUsersComponent,
],
})
export class AddonBlockOnlineUsersComponentsModule {}

View File

@ -27,8 +27,5 @@ import { AddonBlockRecentActivityComponent } from './recentactivity/recentactivi
exports: [
AddonBlockRecentActivityComponent,
],
entryComponents: [
AddonBlockRecentActivityComponent,
],
})
export class AddonBlockRecentActivityComponentsModule {}

View File

@ -30,8 +30,5 @@ import { AddonBlockRecentlyAccessedCoursesComponent } from './recentlyaccessedco
exports: [
AddonBlockRecentlyAccessedCoursesComponent,
],
entryComponents: [
AddonBlockRecentlyAccessedCoursesComponent,
],
})
export class AddonBlockRecentlyAccessedCoursesComponentsModule {}

View File

@ -30,8 +30,5 @@ import { AddonBlockRecentlyAccessedItemsComponent } from './recentlyaccesseditem
exports: [
AddonBlockRecentlyAccessedItemsComponent,
],
entryComponents: [
AddonBlockRecentlyAccessedItemsComponent,
],
})
export class AddonBlockRecentlyAccessedItemsComponentsModule {}

View File

@ -27,8 +27,5 @@ import { AddonBlockRssClientComponent } from './rssclient/rssclient';
exports: [
AddonBlockRssClientComponent,
],
entryComponents: [
AddonBlockRssClientComponent,
],
})
export class AddonBlockRssClientComponentsModule {}

View File

@ -30,8 +30,5 @@ import { AddonBlockSiteMainMenuComponent } from './sitemainmenu/sitemainmenu';
exports: [
AddonBlockSiteMainMenuComponent,
],
entryComponents: [
AddonBlockSiteMainMenuComponent,
],
})
export class AddonBlockSiteMainMenuComponentsModule {}

View File

@ -30,8 +30,5 @@ import { AddonBlockStarredCoursesComponent } from './starredcourses/starredcours
exports: [
AddonBlockStarredCoursesComponent,
],
entryComponents: [
AddonBlockStarredCoursesComponent,
],
})
export class AddonBlockStarredCoursesComponentsModule {}

View File

@ -27,8 +27,5 @@ import { AddonBlockTagsComponent } from './tags/tags';
exports: [
AddonBlockTagsComponent,
],
entryComponents: [
AddonBlockTagsComponent,
],
})
export class AddonBlockTagsComponentsModule {}

View File

@ -35,9 +35,5 @@ import { AddonBlockTimelineEventsComponent } from './events/events';
AddonBlockTimelineComponent,
AddonBlockTimelineEventsComponent,
],
entryComponents: [
AddonBlockTimelineComponent,
AddonBlockTimelineEventsComponent,
],
})
export class AddonBlockTimelineComponentsModule {}

View File

@ -36,8 +36,5 @@ import { AddonCalendarFilterPopoverComponent } from './filter/filter';
AddonCalendarUpcomingEventsComponent,
AddonCalendarFilterPopoverComponent,
],
entryComponents: [
AddonCalendarFilterPopoverComponent,
],
})
export class AddonCalendarComponentsModule {}

View File

@ -25,8 +25,5 @@ import { AddonMessagesConversationInfoComponent } from './conversation-info/conv
imports: [
CoreSharedModule,
],
entryComponents: [
AddonMessagesConversationInfoComponent,
],
})
export class AddonMessagesComponentsModule {}

View File

@ -40,8 +40,5 @@ import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate
exports: [
AddonModAssignFeedbackCommentsComponent,
],
entryComponents: [
AddonModAssignFeedbackCommentsComponent,
],
})
export class AddonModAssignFeedbackCommentsModule {}

View File

@ -38,8 +38,5 @@ import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate
exports: [
AddonModAssignFeedbackEditPdfComponent,
],
entryComponents: [
AddonModAssignFeedbackEditPdfComponent,
],
})
export class AddonModAssignFeedbackEditPdfModule {}

View File

@ -38,8 +38,5 @@ import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate
exports: [
AddonModAssignFeedbackFileComponent,
],
entryComponents: [
AddonModAssignFeedbackFileComponent,
],
})
export class AddonModAssignFeedbackFileModule {}

View File

@ -40,8 +40,5 @@ import { CoreCommentsComponentsModule } from '@features/comments/components/comp
exports: [
AddonModAssignSubmissionCommentsComponent,
],
entryComponents: [
AddonModAssignSubmissionCommentsComponent,
],
})
export class AddonModAssignSubmissionCommentsModule {}

View File

@ -38,8 +38,5 @@ import { AddonModAssignSubmissionDelegate } from '../../services/submission-dele
exports: [
AddonModAssignSubmissionFileComponent,
],
entryComponents: [
AddonModAssignSubmissionFileComponent,
],
})
export class AddonModAssignSubmissionFileModule {}

View File

@ -40,8 +40,5 @@ import { AddonModAssignSubmissionDelegate } from '../../services/submission-dele
exports: [
AddonModAssignSubmissionOnlineTextComponent,
],
entryComponents: [
AddonModAssignSubmissionOnlineTextComponent,
],
})
export class AddonModAssignSubmissionOnlineTextModule {}

View File

@ -19,9 +19,8 @@ import { CoreCourse } from '@features/course/services/course';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton, Translate } from '@singletons';
import { makeSingleton } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { AddonModChoice, AddonModChoiceProvider } from './choice';
import { AddonModChoiceOffline } from './choice-offline';
@ -192,11 +191,7 @@ export class AddonModChoiceSyncProvider extends CoreCourseActivitySyncBaseProvid
await AddonModChoiceOffline.deleteResponse(choiceId, siteId, userId);
// Responses deleted, add a warning.
result.warnings.push(Translate.instant('core.warningofflinedatadeleted', {
component: this.componentTranslate,
name: data.name,
error: CoreTextUtils.getErrorMessageFromError(error),
}));
this.addOfflineDataDeletedWarning(result.warnings, data.name, error);
}
// Data has been sent to server, prefetch choice if needed.

View File

@ -235,28 +235,20 @@ export class AddonModDataSyncProvider extends CoreCourseActivitySyncBaseProvider
result: AddonModDataSyncResult,
siteId: string,
): Promise<void> {
const synEntryResult = await this.performSyncEntry(database, entryActions, result, siteId);
const syncEntryResult = await this.performSyncEntry(database, entryActions, result, siteId);
if (synEntryResult.discardError) {
if (syncEntryResult.discardError) {
// Submission was discarded, add a warning.
const message = Translate.instant('core.warningofflinedatadeleted', {
component: this.componentTranslate,
name: database.name,
error: synEntryResult.discardError,
});
if (result.warnings.indexOf(message) == -1) {
result.warnings.push(message);
}
this.addOfflineDataDeletedWarning(result.warnings, database.name, syncEntryResult.discardError);
}
// Sync done. Send event.
CoreEvents.trigger(AddonModDataSyncProvider.AUTO_SYNCED, {
dataId: database.id,
entryId: synEntryResult.entryId,
offlineEntryId: synEntryResult.offlineId,
entryId: syncEntryResult.entryId,
offlineEntryId: syncEntryResult.offlineId,
warnings: result.warnings,
deleted: synEntryResult.deleted,
deleted: syncEntryResult.deleted,
}, siteId);
}

View File

@ -0,0 +1,39 @@
// (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 { NgModule } from '@angular/core';
import { AddonModGlossaryIndexComponent } from './index/index';
import { AddonModGlossaryModePickerPopoverComponent } from './mode-picker/mode-picker';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
@NgModule({
declarations: [
AddonModGlossaryIndexComponent,
AddonModGlossaryModePickerPopoverComponent,
],
imports: [
CoreSharedModule,
CoreCourseComponentsModule,
CoreSearchComponentsModule,
],
providers: [
],
exports: [
AddonModGlossaryIndexComponent,
AddonModGlossaryModePickerPopoverComponent,
],
})
export class AddonModGlossaryComponentsModule {}

View File

@ -0,0 +1,107 @@
<!-- Buttons to add to the header. -->
<core-navbar-buttons slot="end">
<ion-button *ngIf="glossary && glossary.browsemodes && glossary.browsemodes.length > 1" (click)="openModePicker($event)"
[attr.aria-label]="'addon.mod_glossary.browsemode' | translate">
<ion-icon name="fas-sort"></ion-icon>
</ion-button>
<ion-button *ngIf="glossary" (click)="toggleSearch()" [attr.aria-label]="'addon.mod_glossary.bysearch' | translate">
<ion-icon name="fas-search"></ion-icon>
</ion-button>
<core-context-menu>
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
[href]="externalUrl" iconAction="fas-external-link-alt">
</core-context-menu-item>
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate"
(action)="expandDescription()" iconAction="fas-arrow-right">
</core-context-menu-item>
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
iconAction="far-newspaper" (action)="gotoBlog()">
</core-context-menu-item>
<core-context-menu-item *ngIf="loaded && !(hasOffline || hasOfflineRatings) && isOnline" [priority]="700"
[content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon"
[closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="loaded && (hasOffline || hasOfflineRatings) && isOnline" [priority]="600"
(action)="doRefresh(null, $event, true)" [content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon"
[closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="canAdd" [priority]="550" [content]="'addon.mod_glossary.addentry' | translate"
(action)="openNewEntry()" iconAction="fas-plus">
</core-context-menu-item>
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)"
[iconAction]="prefetchStatusIcon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}"
iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
</core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<!-- Content. -->
<core-split-view>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-search-box *ngIf="isSearch" (onSubmit)="search($event)" [placeholder]="'addon.mod_glossary.searchquery' | translate"
[autoFocus]="true" [lengthCheck]="2" (onClear)="toggleSearch()" searchArea="AddonModGlossary-{{module.id}}">
</core-search-box>
<core-loading [hideUntil]="entries.loaded" class="core-loading-center">
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description>
<!-- Has offline data to be synchronized -->
<ion-card class="core-warning-card" *ngIf="hasOffline || hasOfflineRatings">
<ion-item>
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: moduleName} }}</ion-label>
</ion-item>
</ion-card>
<ion-list *ngIf="!isSearch && entries.offlineEntries.length > 0">
<ion-item-divider>
<ion-label>{{ 'addon.mod_glossary.entriestobesynced' | translate }}</ion-label>
</ion-item-divider>
<ion-item *ngFor="let entry of entries.offlineEntries" (click)="entries.select(entry)" detail="false"
[class.core-selected-item]="entries.isSelected(entry)">
<ion-label>
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="glossary!.coursemodule"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
</ion-list>
<ion-list *ngIf="entries.onlineEntries.length > 0">
<ng-container *ngFor="let entry of entries.onlineEntries; let index = index">
<ion-item-divider *ngIf="getDivider && showDivider(entry, entries.onlineEntries[index - 1])">
{{ getDivider!(entry) }}
</ion-item-divider>
<ion-item (click)="entries.select(entry)" [class.core-selected-item]="entries.isSelected(entry)" detail="false">
<ion-label>
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="glossary!.coursemodule"
[courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
</ng-container>
</ion-list>
<core-empty-box *ngIf="entries.empty" icon="fas-list" [message]="'addon.mod_glossary.noentriesfound' | translate">
</core-empty-box>
<core-infinite-loading [enabled]="!entries.completed" [error]="loadMoreError" (action)="loadMoreEntries($event)">
</core-infinite-loading>
</core-loading>
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="canAdd">
<ion-fab-button (click)="openNewEntry()" [attr.aria-label]="'addon.mod_glossary.addentry' | translate">
<ion-icon name="fas-plus"></ion-icon>
</ion-fab-button>
</ion-fab>
</core-split-view>

View File

@ -0,0 +1,644 @@
// (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 { ContextLevel } from '@/core/constants';
import { AfterViewInit, Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
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 { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { PopoverController, Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import {
AddonModGlossary,
AddonModGlossaryEntry,
AddonModGlossaryEntryWithCategory,
AddonModGlossaryGetEntriesOptions,
AddonModGlossaryGetEntriesWSResponse,
AddonModGlossaryGlossary,
AddonModGlossaryProvider,
} from '../../services/glossary';
import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
import {
AddonModGlossaryAutoSyncData,
AddonModGlossarySyncProvider,
AddonModGlossarySyncResult,
} from '../../services/glossary-sync';
import { AddonModGlossaryPrefetchHandler } from '../../services/handlers/prefetch';
import { AddonModGlossaryModePickerPopoverComponent } from '../mode-picker/mode-picker';
/**
* Component that displays a glossary entry page.
*/
@Component({
selector: 'addon-mod-glossary-index',
templateUrl: 'addon-mod-glossary-index.html',
})
export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivityComponent
implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
component = AddonModGlossaryProvider.COMPONENT;
moduleName = 'glossary';
isSearch = false;
canAdd = false;
loadMoreError = false;
loadingMessage?: string;
entries: AddonModGlossaryEntriesManager;
hasOfflineRatings = false;
glossary?: AddonModGlossaryGlossary;
protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
protected fetchFunction?: (options?: AddonModGlossaryGetEntriesOptions) => AddonModGlossaryGetEntriesWSResponse;
protected fetchInvalidate?: () => Promise<void>;
protected addEntryObserver?: CoreEventObserver;
protected fetchMode?: AddonModGlossaryFetchMode;
protected viewMode?: string;
protected fetchedEntriesCanLoadMore = false;
protected fetchedEntries: AddonModGlossaryEntry[] = [];
protected ratingOfflineObserver?: CoreEventObserver;
protected ratingSyncObserver?: CoreEventObserver;
getDivider?: (entry: AddonModGlossaryEntry) => string;
showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false;
constructor(
route: ActivatedRoute,
protected content?: IonContent,
@Optional() courseContentsPage?: CoreCourseContentsPage,
) {
super('AddonModGlossaryIndexComponent', content, courseContentsPage);
this.entries = new AddonModGlossaryEntriesManager(
route.component,
);
}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
super.ngOnInit();
this.loadingMessage = Translate.instant('core.loading');
// 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);
// Check completion since it could be configured to complete once the user adds a new entry.
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
});
// Listen for offline ratings saved and synced.
this.ratingOfflineObserver = 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.ratingSyncObserver = 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;
}
});
}
/**
* @inheritdoc
*/
async ngAfterViewInit(): Promise<void> {
await this.loadContent(false, true);
if (!this.glossary) {
return;
}
this.entries.start(this.splitView);
try {
await AddonModGlossary.logView(this.glossary.id, this.viewMode!, this.glossary.name);
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch (error) {
// Ignore errors.
}
}
/**
* @inheritdoc
*/
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
try {
this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.module.id);
this.description = this.glossary.intro || this.description;
this.canAdd = (AddonModGlossary.isPluginEnabledForEditing() && !!this.glossary.canaddentry) || false;
this.dataRetrieved.emit(this.glossary);
if (!this.fetchMode) {
this.switchMode('letter_all');
}
if (sync) {
// Try to synchronize the glossary.
await this.syncActivity(showErrors);
}
const [hasOfflineRatings] = await Promise.all([
CoreRatingOffline.hasRatings('mod_glossary', 'entry', ContextLevel.MODULE, this.glossary.coursemodule),
this.fetchEntries(),
]);
this.hasOfflineRatings = hasOfflineRatings;
} finally {
this.fillContextMenu(refresh);
}
}
/**
* Convenience function to fetch entries.
*
* @param append True if fetched entries are appended to exsiting ones.
* @return Promise resolved when done.
*/
protected async fetchEntries(append: boolean = false): Promise<void> {
if (!this.fetchFunction) {
return;
}
this.loadMoreError = false;
const from = append ? this.entries.onlineEntries.length : 0;
const result = await this.fetchFunction({
from: from,
cmId: this.module.id,
});
const hasMoreEntries = from + result.entries.length < result.count;
if (append) {
this.entries.setItems(this.entries.items.concat(result.entries), hasMoreEntries);
} else {
this.entries.setOnlineEntries(result.entries, hasMoreEntries);
}
// Now get the ofline entries.
// Check if there are responses stored in offline.
const offlineEntries = await AddonModGlossaryOffline.getGlossaryNewEntries(this.glossary!.id);
offlineEntries.sort((a, b) => a.concept.localeCompare(b.concept));
this.hasOffline = !!offlineEntries.length;
this.entries.setOfflineEntries(offlineEntries);
}
/**
* @inheritdoc
*/
protected async invalidateContent(): Promise<void> {
const promises: Promise<void>[] = [];
if (this.fetchInvalidate) {
promises.push(this.fetchInvalidate());
}
promises.push(AddonModGlossary.invalidateCourseGlossaries(this.courseId));
if (this.glossary) {
promises.push(AddonModGlossary.invalidateCategories(this.glossary.id));
}
await Promise.all(promises);
}
/**
* Performs the sync of the activity.
*
* @return Promise resolved when done.
*/
protected sync(): Promise<AddonModGlossarySyncResult> {
return AddonModGlossaryPrefetchHandler.sync(this.module, this.courseId);
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned on the sync function.
* @return Whether it succeed or not.
*/
protected hasSyncSucceed(result: AddonModGlossarySyncResult): boolean {
return result.updated;
}
/**
* Compares sync event data with current data to check if refresh content is needed.
*
* @param syncEventData Data receiven on sync observer.
* @return True if refresh is needed, false otherwise.
*/
protected isRefreshSyncNeeded(syncEventData: AddonModGlossaryAutoSyncData): boolean {
return !!this.glossary && syncEventData.glossaryId == this.glossary.id &&
syncEventData.userId == CoreSites.getCurrentSiteUserId();
}
/**
* Change fetch mode.
*
* @param mode New mode.
*/
protected switchMode(mode: AddonModGlossaryFetchMode): void {
this.fetchMode = mode;
this.isSearch = false;
switch (mode) {
case 'author_all':
// Browse by author.
this.viewMode = 'author';
this.fetchFunction = AddonModGlossary.getEntriesByAuthor.bind(
AddonModGlossary.instance,
this.glossary!.id,
'ALL',
'LASTNAME',
'ASC',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByAuthor.bind(
AddonModGlossary.instance,
this.glossary!.id,
'ALL',
'LASTNAME',
'ASC',
);
this.getDivider = (entry) => entry.userfullname;
this.showDivider = (entry, previous) => !previous || entry.userid != previous.userid;
break;
case 'cat_all':
// Browse by category.
this.viewMode = 'cat';
this.fetchFunction = AddonModGlossary.getEntriesByCategory.bind(
AddonModGlossary.instance,
this.glossary!.id,
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByCategory.bind(
AddonModGlossary.instance,
this.glossary!.id,
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
);
this.getDivider = (entry: AddonModGlossaryEntryWithCategory) => entry.categoryname || '';
this.showDivider = (entry, previous) => !previous || this.getDivider!(entry) != this.getDivider!(previous);
break;
case 'newest_first':
// Newest first.
this.viewMode = 'date';
this.fetchFunction = AddonModGlossary.getEntriesByDate.bind(
AddonModGlossary.instance,
this.glossary!.id,
'CREATION',
'DESC',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByDate.bind(
AddonModGlossary.instance,
this.glossary!.id,
'CREATION',
'DESC',
);
this.getDivider = undefined;
this.showDivider = () => false;
break;
case 'recently_updated':
// Recently updated.
this.viewMode = 'date';
this.fetchFunction = AddonModGlossary.getEntriesByDate.bind(
AddonModGlossary.instance,
this.glossary!.id,
'UPDATE',
'DESC',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByDate.bind(
AddonModGlossary.instance,
this.glossary!.id,
'UPDATE',
'DESC',
);
this.getDivider = undefined;
this.showDivider = () => false;
break;
case 'letter_all':
default:
// Consider it is 'letter_all'.
this.viewMode = 'letter';
this.fetchMode = 'letter_all';
this.fetchFunction = AddonModGlossary.getEntriesByLetter.bind(
AddonModGlossary.instance,
this.glossary!.id,
'ALL',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesByLetter.bind(
AddonModGlossary.instance,
this.glossary!.id,
'ALL',
);
this.getDivider = (entry) => {
// Try to get the first letter without HTML tags.
const noTags = CoreTextUtils.cleanTags(entry.concept);
return (noTags || entry.concept).substr(0, 1).toUpperCase();
};
this.showDivider = (entry, previous) => !previous || this.getDivider!(entry) != this.getDivider!(previous);
break;
}
}
/**
* Convenience function to load more entries.
*
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
* @return Promise resolved when done.
*/
async loadMoreEntries(infiniteComplete?: () => void): Promise<void> {
try {
await this.fetchEntries(true);
} catch (error) {
this.loadMoreError = true;
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentries', true);
} finally {
infiniteComplete && infiniteComplete();
}
}
/**
* Show the mode picker menu.
*
* @param event Event.
*/
async openModePicker(event: MouseEvent): Promise<void> {
const popover = await PopoverController.create({
component: AddonModGlossaryModePickerPopoverComponent,
componentProps: {
browseModes: this.glossary!.browsemodes,
selectedMode: this.isSearch ? '' : this.fetchMode,
},
event,
});
popover.present();
const result = await popover.onDidDismiss<AddonModGlossaryFetchMode>();
const mode = result.data;
if (mode) {
if (mode !== this.fetchMode) {
this.changeFetchMode(mode);
} else if (this.isSearch) {
this.toggleSearch();
}
}
}
/**
* Toggles between search and fetch mode.
*/
toggleSearch(): void {
if (this.isSearch) {
this.isSearch = false;
this.entries.setOnlineEntries(this.fetchedEntries, this.fetchedEntriesCanLoadMore);
this.switchMode(this.fetchMode!);
} else {
// Search for entries. The fetch function will be set when searching.
this.getDivider = undefined;
this.showDivider = () => false;
this.isSearch = true;
this.fetchedEntries = this.entries.onlineEntries;
this.fetchedEntriesCanLoadMore = !this.entries.completed;
this.entries.setItems([], false);
}
}
/**
* Change fetch mode.
*
* @param mode Mode.
*/
changeFetchMode(mode: AddonModGlossaryFetchMode): void {
this.isSearch = false;
this.loadingMessage = Translate.instant('core.loading');
this.content?.scrollToTop();
this.switchMode(mode);
this.loaded = false;
this.loadContent();
}
/**
* Opens new entry editor.
*/
openNewEntry(): void {
this.entries.select({ newEntry: true });
}
/**
* Search entries.
*
* @param query Text entered on the search box.
*/
search(query: string): void {
this.loadingMessage = Translate.instant('core.searching');
this.fetchFunction = AddonModGlossary.getEntriesBySearch.bind(
AddonModGlossary.instance,
this.glossary!.id,
query,
true,
'CONCEPT',
'ASC',
);
this.fetchInvalidate = AddonModGlossary.invalidateEntriesBySearch.bind(
AddonModGlossary.instance,
this.glossary!.id,
query,
true,
'CONCEPT',
'ASC',
);
this.loaded = false;
this.loadContent();
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
super.ngOnDestroy();
this.addEntryObserver?.off();
this.ratingOfflineObserver?.off();
this.ratingSyncObserver?.off();
}
}
/**
* Type to select the new entry form.
*/
type NewEntryForm = { newEntry: true };
/**
* Type of items that can be held by the entries manager.
*/
type EntryItem = AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | NewEntryForm;
/**
* Entries manager.
*/
class AddonModGlossaryEntriesManager extends CorePageItemsListManager<EntryItem> {
onlineEntries: AddonModGlossaryEntry[] = [];
offlineEntries: AddonModGlossaryOfflineEntry[] = [];
constructor(pageComponent: unknown) {
super(pageComponent);
}
/**
* Type guard to infer NewEntryForm objects.
*
* @param entry Item to check.
* @return Whether the item is a new entry form.
*/
isNewEntryForm(entry: EntryItem): entry is NewEntryForm {
return 'newEntry' in entry;
}
/**
* Type guard to infer entry objects.
*
* @param entry Item to check.
* @return Whether the item is an offline entry.
*/
isOfflineEntry(entry: EntryItem): entry is AddonModGlossaryOfflineEntry {
return !this.isNewEntryForm(entry) && !this.isOnlineEntry(entry);
}
/**
* Type guard to infer entry objects.
*
* @param entry Item to check.
* @return Whether the item is an offline entry.
*/
isOnlineEntry(entry: EntryItem): entry is AddonModGlossaryEntry {
return 'id' in entry;
}
/**
* Update online entries items.
*
* @param onlineEntries Online entries.
*/
setOnlineEntries(onlineEntries: AddonModGlossaryEntry[], hasMoreItems: boolean = false): void {
this.setItems((<EntryItem[]> this.offlineEntries).concat(onlineEntries), hasMoreItems);
this.onlineEntries.concat(onlineEntries);
}
/**
* Update offline entries items.
*
* @param offlineEntries Offline entries.
*/
setOfflineEntries(offlineEntries: AddonModGlossaryOfflineEntry[]): void {
this.setItems((<EntryItem[]> offlineEntries).concat(this.onlineEntries), this.hasMoreItems);
this.offlineEntries = offlineEntries;
}
/**
* @inheritdoc
*/
setItems(entries: EntryItem[], hasMoreItems: boolean = false): void {
super.setItems(entries, hasMoreItems);
this.onlineEntries = [];
this.offlineEntries = [];
this.items.forEach(entry => {
if (this.isOfflineEntry(entry)) {
this.offlineEntries.push(entry);
} else if (this.isOnlineEntry(entry)) {
this.onlineEntries.push(entry);
}
});
}
/**
* @inheritdoc
*/
resetItems(): void {
super.resetItems();
this.onlineEntries = [];
this.offlineEntries = [];
}
/**
* @inheritdoc
*/
protected getItemPath(entry: EntryItem): string {
if (this.isOnlineEntry(entry)) {
return `entry/${entry.id}`;
}
if (this.isOfflineEntry(entry)) {
return `edit/${entry.timecreated}`;
}
return 'edit/0';
}
/**
* @inheritdoc
*/
getItemQueryParams(entry: EntryItem): Params {
if (this.isOfflineEntry(entry)) {
return {
concept: entry.concept,
};
}
return {};
}
/**
* @inheritdoc
*/
protected getDefaultItem(): EntryItem | null {
return this.onlineEntries[0] || null;
}
}
export type AddonModGlossaryFetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'letter_all';

View File

@ -0,0 +1,6 @@
<ion-radio-group [(ngModel)]="selectedMode" (ionChange)="modePicked()">
<ion-item class="ion-text-wrap" *ngFor="let mode of modes">
<ion-label>{{ mode.langkey | translate }}</ion-label>
<ion-radio slot="end" [value]="mode.key"></ion-radio>
</ion-item>
</ion-radio-group>

View File

@ -0,0 +1,64 @@
// (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 { Component, Input, OnInit } from '@angular/core';
import { PopoverController } from '@singletons';
import { AddonModGlossaryFetchMode } from '../index';
/**
* Component to display the mode picker.
*/
@Component({
selector: 'addon-mod-glossary-mode-picker-popover',
templateUrl: 'addon-mod-glossary-mode-picker.html',
})
export class AddonModGlossaryModePickerPopoverComponent implements OnInit {
@Input() browseModes: string[] = [];
@Input() selectedMode = '';
modes: { key: AddonModGlossaryFetchMode; langkey: string }[] = [];
/**
* @inheritdoc
*/
ngOnInit(): void {
this.browseModes.forEach((mode) => {
switch (mode) {
case 'letter' :
this.modes.push({ key: 'letter_all', langkey: 'addon.mod_glossary.byalphabet' });
break;
case 'cat' :
this.modes.push({ key: 'cat_all', langkey: 'addon.mod_glossary.bycategory' });
break;
case 'date' :
this.modes.push({ key: 'newest_first', langkey: 'addon.mod_glossary.bynewestfirst' });
this.modes.push({ key: 'recently_updated', langkey: 'addon.mod_glossary.byrecentlyupdated' });
break;
case 'author' :
this.modes.push({ key: 'author_all', langkey: 'addon.mod_glossary.byauthor' });
break;
default:
}
});
}
/**
* Function called when a mode is clicked.
*/
modePicked(): void {
PopoverController.dismiss(this.selectedMode);
}
}

View File

@ -0,0 +1,71 @@
// (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 { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonModGlossaryComponentsModule } from './components/components.module';
import { AddonModGlossaryIndexPage } from './pages/index/index';
import { conditionalRoutes } from '@/app/app-routing.module';
import { CoreScreen } from '@services/screen';
const mobileRoutes: Routes = [
{
path: ':courseId/:cmId',
component: AddonModGlossaryIndexPage,
},
{
path: ':courseId/:cmId/entry/:entryId',
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
},
{
path: ':courseId/:cmId/edit/:timecreated',
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
},
];
const tabletRoutes: Routes = [
{
path: ':courseId/:cmId',
component: AddonModGlossaryIndexPage,
children: [
{
path: 'entry/:entryId',
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
},
{
path: 'edit/:timecreated',
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
},
],
},
];
const routes: Routes = [
...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile),
...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet),
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CoreSharedModule,
AddonModGlossaryComponentsModule,
],
declarations: [
AddonModGlossaryIndexPage,
],
})
export class AddonModGlossaryLazyModule {}

View File

@ -0,0 +1,89 @@
// (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 { APP_INITIALIZER, NgModule, Type } from '@angular/core';
import { Routes } from '@angular/router';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate';
import { CoreCronDelegate } from '@services/cron';
import { CORE_SITE_SCHEMAS } from '@services/sites';
import { AddonModGlossaryComponentsModule } from './components/components.module';
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/glossary';
import { AddonModGlossaryProvider } from './services/glossary';
import { AddonModGlossaryHelperProvider } from './services/glossary-helper';
import { AddonModGlossaryOfflineProvider } from './services/glossary-offline';
import { AddonModGlossarySyncProvider } from './services/glossary-sync';
import { AddonModGlossaryEditLinkHandler } from './services/handlers/edit-link';
import { AddonModGlossaryEntryLinkHandler } from './services/handlers/entry-link';
import { AddonModGlossaryIndexLinkHandler } from './services/handlers/index-link';
import { AddonModGlossaryListLinkHandler } from './services/handlers/list-link';
import { AddonModGlossaryModuleHandler, AddonModGlossaryModuleHandlerService } from './services/handlers/module';
import { AddonModGlossaryPrefetchHandler } from './services/handlers/prefetch';
import { AddonModGlossarySyncCronHandler } from './services/handlers/sync-cron';
import { AddonModGlossaryTagAreaHandler } from './services/handlers/tag-area';
export const ADDON_MOD_GLOSSARY_SERVICES: Type<unknown>[] = [
AddonModGlossaryProvider,
AddonModGlossaryOfflineProvider,
AddonModGlossarySyncProvider,
AddonModGlossaryHelperProvider,
];
const mainMenuRoutes: Routes = [
{
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
loadChildren: () => import('./pages/entry/entry.module').then(m => m.AddonModGlossaryEntryPageModule),
},
{
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
loadChildren: () => import('./pages/edit/edit.module').then(m => m.AddonModGlossaryEditPageModule),
},
{
path: AddonModGlossaryModuleHandlerService.PAGE_NAME,
loadChildren: () => import('./glossary-lazy.module').then(m => m.AddonModGlossaryLazyModule),
},
];
@NgModule({
imports: [
CoreMainMenuTabRoutingModule.forChild(mainMenuRoutes),
AddonModGlossaryComponentsModule,
],
providers: [
{
provide: CORE_SITE_SCHEMAS,
useValue: [SITE_SCHEMA, OFFLINE_SITE_SCHEMA],
multi: true,
},
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
CoreCourseModuleDelegate.registerHandler(AddonModGlossaryModuleHandler.instance);
CoreCourseModulePrefetchDelegate.registerHandler(AddonModGlossaryPrefetchHandler.instance);
CoreCronDelegate.register(AddonModGlossarySyncCronHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModGlossaryIndexLinkHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModGlossaryListLinkHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModGlossaryEditLinkHandler.instance);
CoreContentLinksDelegate.registerHandler(AddonModGlossaryEntryLinkHandler.instance);
CoreTagAreaDelegate.registerHandler(AddonModGlossaryTagAreaHandler.instance);
},
},
],
})
export class AddonModGlossaryModule {}

View File

@ -0,0 +1,31 @@
{
"addentry": "Add a new entry",
"aliases": "Keyword(s)",
"attachment": "Attachment",
"browsemode": "Browse entries",
"byalphabet": "Alphabetically",
"byauthor": "Group by author",
"bycategory": "Group by category",
"bynewestfirst": "Newest first",
"byrecentlyupdated": "Recently updated",
"bysearch": "Search",
"cannoteditentry": "Cannot edit entry",
"casesensitive": "This entry is case sensitive",
"categories": "Categories",
"concept": "Concept",
"definition": "Definition",
"entriestobesynced": "Entries to be synced",
"entrypendingapproval": "This entry is pending approval.",
"entryusedynalink": "This entry should be automatically linked",
"errconceptalreadyexists": "This concept already exists. No duplicates allowed in this glossary.",
"errorloadingentries": "An error occurred while loading entries.",
"errorloadingentry": "An error occurred while loading the entry.",
"errorloadingglossary": "An error occurred while loading the glossary.",
"fillfields": "Concept and definition are mandatory fields.",
"fullmatch": "Match whole words only",
"linking": "Auto-linking",
"modulenameplural": "Glossaries",
"noentriesfound": "No entries were found.",
"searchquery": "Search query",
"tagarea_glossary_entries": "Glossary entries"
}

View File

@ -0,0 +1,77 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title *ngIf="glossary">
<core-format-text [text]="glossary.name" contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId">
</core-format-text>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<form #editFormEl *ngIf="glossary">
<ion-item>
<ion-label position="stacked">{{ 'addon.mod_glossary.concept' | translate }}</ion-label>
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="entry.concept"
name="concept">
</ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
<core-rich-text-editor [control]="definitionControl" (contentChanged)="onDefinitionChange($event)"
[placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit"
[component]="component" [componentId]="cmId" [autoSave]="true" contextLevel="module"
[contextInstanceId]="cmId" elementId="definition_editor" [draftExtraParams]="editorExtraParams">
</core-rich-text-editor>
</ion-item>
<ion-item *ngIf="categories.length > 0">
<ion-label position="stacked" id="addon-mod-glossary-categories-label">
{{ 'addon.mod_glossary.categories' | translate }}
</ion-label>
<ion-select [(ngModel)]="options.categories" multiple="true" aria-labelledby="addon-mod-glossary-categories-label"
interface="action-sheet" [placeholder]="'addon.mod_glossary.categories' | translate" name="categories">
<ion-select-option *ngFor="let category of categories" [value]="category.id">
{{ category.name }}
</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label position="stacked" id="addon-mod-glossary-aliases-label">
{{ 'addon.mod_glossary.aliases' | translate }}
</ion-label>
<ion-textarea [(ngModel)]="options.aliases" rows="1" core-auto-rows
aria-labelledby="addon-mod-glossary-aliases-label" name="aliases">
</ion-textarea>
</ion-item>
<ion-item-divider>
<ion-label>{{ 'addon.mod_glossary.attachment' | translate }}</ion-label>
</ion-item-divider>
<core-attachments [files]="attachments" [component]="component" [componentId]="glossary.coursemodule"
[allowOffline]="true">
</core-attachments>
<ng-container *ngIf="glossary.usedynalink">
<ion-item-divider>
<ion-label>{{ 'addon.mod_glossary.linking' | translate }}</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap">
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>
<ion-toggle [(ngModel)]="options.usedynalink" name="usedynalink"></ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label>
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.casesensitive" name="casesensitive">
</ion-toggle>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>{{ 'addon.mod_glossary.fullmatch' | translate }}</ion-label>
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.fullmatch" name="fullmatch"></ion-toggle>
</ion-item>
</ng-container>
<ion-button class="ion-margin" expand="block" [disabled]="!entry.concept || !entry.definition" (click)="save()">
{{ 'core.save' | translate }}
</ion-button>
</form>
</core-loading>
</ion-content>

View File

@ -0,0 +1,38 @@
// (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 { NgModule } from '@angular/core';
import { AddonModGlossaryEditPage } from './edit';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
import { RouterModule, Routes } from '@angular/router';
import { CanLeaveGuard } from '@guards/can-leave';
const routes: Routes = [{
path: '',
component: AddonModGlossaryEditPage,
canDeactivate: [CanLeaveGuard],
}];
@NgModule({
declarations: [
AddonModGlossaryEditPage,
],
imports: [
RouterModule.forChild(routes),
CoreSharedModule,
CoreEditorComponentsModule,
],
})
export class AddonModGlossaryEditPageModule {}

View File

@ -0,0 +1,370 @@
// (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 { Component, OnInit, ViewChild, ElementRef, Optional } from '@angular/core';
import { FormControl } from '@angular/forms';
import { CoreError } from '@classes/errors/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 { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreForms } from '@singletons/form';
import {
AddonModGlossary,
AddonModGlossaryCategory,
AddonModGlossaryEntryOption,
AddonModGlossaryGlossary,
AddonModGlossaryNewEntry,
AddonModGlossaryNewEntryWithFiles,
AddonModGlossaryProvider,
} from '../../services/glossary';
import { AddonModGlossaryHelper } from '../../services/glossary-helper';
import { AddonModGlossaryOffline } from '../../services/glossary-offline';
/**
* Page that displays the edit form.
*/
@Component({
selector: 'page-addon-mod-glossary-edit',
templateUrl: 'edit.html',
})
export class AddonModGlossaryEditPage implements OnInit, CanLeave {
@ViewChild('editFormEl') formElement?: ElementRef;
component = AddonModGlossaryProvider.COMPONENT;
cmId!: number;
courseId!: number;
loaded = false;
glossary?: AddonModGlossaryGlossary;
attachments: FileEntry[] = [];
definitionControl = new FormControl();
categories: AddonModGlossaryCategory[] = [];
editorExtraParams: Record<string, unknown> = {};
entry: AddonModGlossaryNewEntry = {
concept: '',
definition: '',
timecreated: 0,
};
options = {
categories: <string[]> [],
aliases: '',
usedynalink: false,
casesensitive: false,
fullmatch: false,
};
protected timecreated!: number;
protected concept?: string;
protected syncId?: string;
protected syncObserver?: CoreEventObserver;
protected isDestroyed = false;
protected originalData?: AddonModGlossaryNewEntryWithFiles;
protected saved = false;
constructor(@Optional() protected splitView: CoreSplitViewComponent) {}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.cmId = CoreNavigator.getRouteNumberParam('cmId')!;
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
this.timecreated = CoreNavigator.getRouteNumberParam('timecreated')!;
this.concept = CoreNavigator.getRouteParam<string>('concept')!;
this.editorExtraParams.timecreated = this.timecreated;
this.fetchData();
}
/**
* Fetch required data.
*
* @return Promise resolved when done.
*/
protected async fetchData(): Promise<void> {
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,
});
this.loaded = true;
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingglossary', true);
CoreNavigator.back();
}
}
/**
* Load offline data when editing an offline entry.
*
* @return Promise resolved when done.
*/
protected async loadOfflineData(): Promise<void> {
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 && (<string> entry.options.categories).split(',')) || [];
this.options.aliases = <string> 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.definitionControl.setValue('');
}
/**
* Definition changed.
*
* @param text The new text.
*/
onDefinitionChange(text: string): void {
this.entry.definition = text;
}
/**
* Check if we can leave the page or not.
*
* @return Resolved if we can leave it, rejected if not.
*/
async canLeave(): Promise<boolean> {
if (this.saved) {
return true;
}
if (AddonModGlossaryHelper.hasEntryDataChanged(this.entry, this.attachments, this.originalData)) {
// 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);
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
return true;
}
/**
* Save the entry.
*/
async save(): Promise<void> {
let definition = this.entry.definition;
let entryId: number | undefined;
const timecreated = this.entry.timecreated || Date.now();
if (!this.entry.concept || !definition) {
CoreDomUtils.showErrorModal('addon.mod_glossary.fillfields', true);
return;
}
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
definition = CoreTextUtils.formatHtmlLines(definition);
try {
// Upload attachments first if any.
const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated);
const options: Record<string, AddonModGlossaryEntryOption> = {
aliases: this.options.aliases,
categories: this.options.categories.join(','),
};
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,
<CoreFileUploaderStoreFilesResult> 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();
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.cannoteditentry', true);
} finally {
modal.dismiss();
}
}
/**
* Upload entry attachments if any.
*
* @param timecreated Entry's timecreated.
* @return Promise resolved when done.
*/
protected async uploadAttachments(
timecreated: number,
): Promise<{saveOffline: boolean; attachmentsResult?: number | CoreFileUploaderStoreFilesResult}> {
if (!this.attachments.length) {
return {
saveOffline: false,
};
}
try {
const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles(
this.attachments,
AddonModGlossaryProvider.COMPONENT,
this.glossary!.id,
);
return {
saveOffline: false,
attachmentsResult,
};
} catch {
// 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,
};
}
}
}

View File

@ -0,0 +1,85 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title *ngIf="entry">
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId" [courseId]="courseId">
</core-format-text>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ng-container *ngIf="entry && loaded">
<ion-item class="ion-text-wrap" *ngIf="showAuthor">
<core-user-avatar [user]="entry" slot="start"></core-user-avatar>
<ion-label>
<h2>
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId"
[courseId]="courseId">
</core-format-text>
</h2>
<p>{{ entry.userfullname }}</p>
</ion-label>
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="!showAuthor">
<ion-label>
<h2>
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="componentId">
</core-format-text>
</h2>
</ion-label>
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition"
contextLevel="module" [contextInstanceId]="componentId" [courseId]="courseId">
</core-format-text>
</ion-label>
</ion-item>
<div *ngIf="entry.attachment" lines="none">
<core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component"
[componentId]="componentId">
</core-file>
</div>
<ion-item class="ion-text-wrap" *ngIf="tagsEnabled && entry && entry.tags && entry.tags.length > 0">
<ion-label>
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
<core-tag-list [tags]="entry.tags"></core-tag-list>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="!entry.approved">
<ion-label><p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p></ion-label>
</ion-item>
<ion-item *ngIf="glossary && glossary.allowcomments && entry && entry.id > 0 && commentsEnabled">
<ion-label>
<core-comments contextLevel="module" [instanceId]="glossary.coursemodule" component="mod_glossary"
[itemId]="entry.id" area="glossary_entry" [courseId]="glossary.course">
</core-comments>
</ion-label>
</ion-item>
<core-rating-rate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [itemSetId]="0" [courseId]="glossary.course"
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" [userId]="entry.userid"
(onUpdate)="ratingUpdated()">
</core-rating-rate>
<core-rating-aggregate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [courseId]="glossary.course"
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale">
</core-rating-aggregate>
</ng-container>
<ion-card *ngIf="!entry" class="core-warning-card">
<ion-item>
<ion-label>{{ 'addon.mod_glossary.errorloadingentry' | translate }}</ion-label>
</ion-item>
</ion-card>
</core-loading>
</ion-content>

View File

@ -0,0 +1,40 @@
// (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 { NgModule } from '@angular/core';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonModGlossaryEntryPage } from './entry';
import { CoreCommentsComponentsModule } from '@features/comments/components/components.module';
import { CoreRatingComponentsModule } from '@features/rating/components/components.module';
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [{
path: '',
component: AddonModGlossaryEntryPage,
}];
@NgModule({
declarations: [
AddonModGlossaryEntryPage,
],
imports: [
RouterModule.forChild(routes),
CoreSharedModule,
CoreCommentsComponentsModule,
CoreRatingComponentsModule,
CoreTagComponentsModule,
],
})
export class AddonModGlossaryEntryPageModule {}

View File

@ -0,0 +1,146 @@
// (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 { Component, OnInit, ViewChild } from '@angular/core';
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 { IonRefresher } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import {
AddonModGlossary,
AddonModGlossaryEntry,
AddonModGlossaryGlossary,
AddonModGlossaryProvider,
} from '../../services/glossary';
/**
* Page that displays a glossary entry.
*/
@Component({
selector: 'page-addon-mod-glossary-entry',
templateUrl: 'entry.html',
})
export class AddonModGlossaryEntryPage implements OnInit {
@ViewChild(CoreCommentsCommentsComponent) comments?: CoreCommentsCommentsComponent;
component = AddonModGlossaryProvider.COMPONENT;
componentId?: number;
entry?: AddonModGlossaryEntry;
glossary?: AddonModGlossaryGlossary;
loaded = false;
showAuthor = false;
showDate = false;
ratingInfo?: CoreRatingInfo;
tagsEnabled = false;
commentsEnabled = false;
courseId!: number;
protected entryId!: number;
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
this.courseId = CoreNavigator.getRouteNumberParam('courseId')!;
this.entryId = CoreNavigator.getRouteNumberParam('entryId')!;
this.tagsEnabled = CoreTag.areTagsAvailableInSite();
this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
try {
await this.fetchEntry();
if (!this.glossary) {
return;
}
await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.entryId, this.componentId!, this.glossary.name));
} finally {
this.loaded = true;
}
}
/**
* Refresh the data.
*
* @param refresher Refresher.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: IonRefresher): Promise<void> {
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.
CoreUtils.ignoreErrors(this.comments.doRefresh());
}
try {
await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(this.entryId));
await this.fetchEntry();
} finally {
refresher?.complete();
}
}
/**
* Convenience function to get the glossary entry.
*
* @return Promise resolved when done.
*/
protected async fetchEntry(): Promise<void> {
try {
const result = await AddonModGlossary.getEntry(this.entryId);
this.entry = result.entry;
this.ratingInfo = result.ratinginfo;
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;
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true);
}
}
/**
* Function called when rating is updated online.
*/
ratingUpdated(): void {
AddonModGlossary.invalidateEntry(this.entryId);
}
}

View File

@ -0,0 +1,18 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<!-- The buttons defined by the component will be added in here. -->
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<addon-mod-glossary-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)">
</addon-mod-glossary-index>
</ion-content>

View File

@ -0,0 +1,30 @@
// (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 { Component, ViewChild } from '@angular/core';
import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
import { AddonModGlossaryIndexComponent } from '../../components/index';
/**
* Page that displays a glossary.
*/
@Component({
selector: 'page-addon-mod-glossary-index',
templateUrl: 'index.html',
})
export class AddonModGlossaryIndexPage extends CoreCourseModuleMainActivityPage<AddonModGlossaryIndexComponent> {
@ViewChild(AddonModGlossaryIndexComponent) activityComponent?: AddonModGlossaryIndexComponent;
}

View File

@ -0,0 +1,121 @@
// (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 { CoreSiteSchema } from '@services/sites';
/**
* Database variables for AddonModGlossaryProvider.
*/
export const ENTRIES_TABLE_NAME = 'addon_mod_glossary_entry_glossaryid';
export const SITE_SCHEMA: CoreSiteSchema = {
name: 'AddonModGlossaryProvider',
version: 1,
tables: [
{
name: ENTRIES_TABLE_NAME,
columns: [
{
name: 'entryid',
type: 'INTEGER',
primaryKey: true,
},
{
name: 'glossaryid',
type: 'INTEGER',
},
{
name: 'pagefrom',
type: 'INTEGER',
},
],
},
],
};
/**
* Database variables for AddonModGlossaryOfflineProvider.
*/
export const OFFLINE_ENTRIES_TABLE_NAME = 'addon_mod_glossary_entrues';
export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
name: 'AddonModGlossaryOfflineProvider',
version: 1,
tables: [
{
name: OFFLINE_ENTRIES_TABLE_NAME,
columns: [
{
name: 'glossaryid',
type: 'INTEGER',
},
{
name: 'courseid',
type: 'INTEGER',
},
{
name: 'concept',
type: 'TEXT',
},
{
name: 'definition',
type: 'TEXT',
},
{
name: 'definitionformat',
type: 'TEXT',
},
{
name: 'userid',
type: 'INTEGER',
},
{
name: 'timecreated',
type: 'INTEGER',
},
{
name: 'options',
type: 'TEXT',
},
{
name: 'attachments',
type: 'TEXT',
},
],
primaryKeys: ['glossaryid', 'concept', 'timecreated'],
},
],
};
/**
* Glossary entry to get glossaryid from entryid.
*/
export type AddonModGlossaryEntryDBRecord = {
entryid: number;
glossaryid: number;
pagefrom: number;
};
/**
* Glossary offline entry.
*/
export type AddonModGlossaryOfflineEntryDBRecord = {
glossaryid: number;
courseid: number;
concept: string;
definition: string;
definitionformat: string;
userid: number;
timecreated: number;
options: string;
attachments: string;
};

View File

@ -0,0 +1,112 @@
// (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 { Injectable } from '@angular/core';
import { FileEntry } from '@ionic-native/file/ngx';
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
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 { CoreWSExternalFile } from '@services/ws';
/**
* Helper to gather some common functions for glossary.
*/
@Injectable({ providedIn: 'root' })
export class AddonModGlossaryHelperProvider {
/**
* Delete stored attachment files for a new entry.
*
* @param glossaryId Glossary ID.
* @param entryName The name of the entry.
* @param timeCreated The time the entry was created.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when deleted.
*/
async deleteStoredFiles(glossaryId: number, entryName: string, timeCreated: number, siteId?: string): Promise<void> {
const folderPath = await AddonModGlossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId);
await CoreUtils.ignoreErrors(CoreFile.removeDir(folderPath));
}
/**
* Get a list of stored attachment files for a new entry. See AddonModGlossaryHelperProvider#storeFiles.
*
* @param glossaryId lossary ID.
* @param entryName The name of the entry.
* @param timeCreated The time the entry was created.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the files.
*/
async getStoredFiles(glossaryId: number, entryName: string, timeCreated: number, siteId?: string): Promise<FileEntry[]> {
const folderPath = await AddonModGlossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId);
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.
* @return True if data has changed, false otherwise.
*/
hasEntryDataChanged(
entry: AddonModGlossaryNewEntry,
files: (CoreWSExternalFile | FileEntry)[],
original?: AddonModGlossaryNewEntryWithFiles,
): boolean {
if (!original || typeof 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.
*
* @param glossaryId Glossary ID.
* @param entryName The name of the entry.
* @param timeCreated The time the entry was created.
* @param files List of files.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if success, rejected otherwise.
*/
async storeFiles(
glossaryId: number,
entryName: string,
timeCreated: number,
files: (CoreWSExternalFile | FileEntry)[],
siteId?: string,
): Promise<CoreFileUploaderStoreFilesResult> {
// Get the folder where to store the files.
const folderPath = await AddonModGlossaryOffline.getEntryFolder(glossaryId, entryName, timeCreated, siteId);
return CoreFileUploader.storeFilesToUpload(folderPath, files);
}
}
export const AddonModGlossaryHelper = makeSingleton(AddonModGlossaryHelperProvider);

View File

@ -0,0 +1,258 @@
// (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 { Injectable } from '@angular/core';
import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
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 { AddonModGlossaryOfflineEntryDBRecord, OFFLINE_ENTRIES_TABLE_NAME } from './database/glossary';
import { AddonModGlossaryDiscardedEntry, AddonModGlossaryEntryOption } from './glossary';
/**
* Service to handle offline glossary.
*/
@Injectable({ providedIn: 'root' })
export class AddonModGlossaryOfflineProvider {
/**
* Delete a new 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.
* @return Promise resolved if deleted, rejected if failure.
*/
async deleteNewEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise<void> {
const site = await CoreSites.getSite(siteId);
const conditions: Partial<AddonModGlossaryOfflineEntryDBRecord> = {
glossaryid: glossaryId,
concept: concept,
timecreated: timeCreated,
};
await site.getDb().deleteRecords(OFFLINE_ENTRIES_TABLE_NAME, conditions);
}
/**
* Get all the stored new entries from all the glossaries.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with entries.
*/
async getAllNewEntries(siteId?: string): Promise<AddonModGlossaryOfflineEntry[]> {
const site = await CoreSites.getSite(siteId);
const records = await site.getDb().getRecords<AddonModGlossaryOfflineEntryDBRecord>(OFFLINE_ENTRIES_TABLE_NAME);
return records.map(record => this.parseRecord(record));
}
/**
* Get a stored new 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.
* @return Promise resolved with entry.
*/
async getNewEntry(
glossaryId: number,
concept: string,
timeCreated: number,
siteId?: string,
): Promise<AddonModGlossaryOfflineEntry> {
const site = await CoreSites.getSite(siteId);
const conditions: Partial<AddonModGlossaryOfflineEntryDBRecord> = {
glossaryid: glossaryId,
concept: concept,
timecreated: timeCreated,
};
const record = await site.getDb().getRecord<AddonModGlossaryOfflineEntryDBRecord>(OFFLINE_ENTRIES_TABLE_NAME, conditions);
return this.parseRecord(record);
}
/**
* Get all the stored add entry data from a certain glossary.
*
* @param glossaryId Glossary ID.
* @param siteId Site ID. If not defined, current site.
* @param userId User the entries belong to. If not defined, current user in site.
* @return Promise resolved with entries.
*/
async getGlossaryNewEntries(glossaryId: number, siteId?: string, userId?: number): Promise<AddonModGlossaryOfflineEntry[]> {
const site = await CoreSites.getSite(siteId);
const conditions: Partial<AddonModGlossaryOfflineEntryDBRecord> = {
glossaryid: glossaryId,
userid: userId || site.getUserId(),
};
const records = await site.getDb().getRecords<AddonModGlossaryOfflineEntryDBRecord>(OFFLINE_ENTRIES_TABLE_NAME, conditions);
return records.map(record => this.parseRecord(record));
}
/**
* Check if a concept is used offline.
*
* @param glossaryId Glossary ID.
* @param concept Concept to check.
* @param timeCreated Time of the entry we are editing.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with true if concept is found, false otherwise.
*/
async isConceptUsed(glossaryId: number, concept: string, timeCreated?: number, siteId?: string): Promise<boolean> {
try {
const site = await CoreSites.getSite(siteId);
const conditions: Partial<AddonModGlossaryOfflineEntryDBRecord> = {
glossaryid: glossaryId,
concept: concept,
};
const entries =
await site.getDb().getRecords<AddonModGlossaryOfflineEntryDBRecord>(OFFLINE_ENTRIES_TABLE_NAME, conditions);
if (!entries.length) {
return false;
}
if (entries.length > 1 || !timeCreated) {
return true;
}
// If there's only one entry, check that is not the one we are editing.
return CoreUtils.promiseFails(this.getNewEntry(glossaryId, concept, timeCreated, siteId));
} catch {
// No offline data found, return false.
return false;
}
}
/**
* Save a new 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 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.
* @return Promise resolved if stored, rejected if failure.
*/
async addNewEntry(
glossaryId: number,
concept: string,
definition: string,
courseId: number,
options?: Record<string, AddonModGlossaryEntryOption>,
attachments?: CoreFileUploaderStoreFilesResult,
timeCreated?: number,
siteId?: string,
userId?: number,
discardEntry?: AddonModGlossaryDiscardedEntry,
): Promise<false> {
const site = await CoreSites.getSite(siteId);
const entry: AddonModGlossaryOfflineEntryDBRecord = {
glossaryid: glossaryId,
courseid: courseId,
concept: concept,
definition: definition,
definitionformat: 'html',
options: JSON.stringify(options || {}),
attachments: JSON.stringify(attachments),
userid: userId || site.getUserId(),
timecreated: timeCreated || Date.now(),
};
// 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);
return false;
}
/**
* Get the path to the folder where to store files for offline attachments in a glossary.
*
* @param glossaryId Glossary ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the path.
*/
async getGlossaryFolder(glossaryId: number, siteId?: string): Promise<string> {
const site = await CoreSites.getSite(siteId);
const siteFolderPath = CoreFile.getSiteFolder(site.getId());
const folderPath = 'offlineglossary/' + glossaryId;
return CoreTextUtils.concatenatePaths(siteFolderPath, folderPath);
}
/**
* Get the path to the folder where to store files for a new offline entry.
*
* @param glossaryId Glossary ID.
* @param concept The name of the entry.
* @param timeCreated Time to allow duplicated entries.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the path.
*/
async getEntryFolder(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise<string> {
const folderPath = await this.getGlossaryFolder(glossaryId, siteId);
return CoreTextUtils.concatenatePaths(folderPath, 'newentry_' + concept + '_' + timeCreated);
}
/**
* Parse "options" and "attachments" columns of a fetched record.
*
* @param records Record object
* @return Record object with columns parsed.
*/
protected parseRecord(record: AddonModGlossaryOfflineEntryDBRecord): AddonModGlossaryOfflineEntry {
return Object.assign(record, {
options: <Record<string, AddonModGlossaryEntryOption>> CoreTextUtils.parseJSON(record.options),
attachments: record.attachments ?
<CoreFileUploaderStoreFilesResult> CoreTextUtils.parseJSON(record.attachments) : undefined,
});
}
}
export const AddonModGlossaryOffline = makeSingleton(AddonModGlossaryOfflineProvider);
/**
* Glossary offline entry with parsed data.
*/
export type AddonModGlossaryOfflineEntry = Omit<AddonModGlossaryOfflineEntryDBRecord, 'options'|'attachments'> & {
options: Record<string, AddonModGlossaryEntryOption>;
attachments?: CoreFileUploaderStoreFilesResult;
};

View File

@ -0,0 +1,359 @@
// (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 { ContextLevel } from '@/core/constants';
import { Injectable } from '@angular/core';
import { FileEntry } from '@ionic-native/file/ngx';
import { CoreSyncBlockedError } from '@classes/base-sync';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreRatingSync } from '@features/rating/services/rating-sync';
import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile } from '@services/ws';
import { makeSingleton, Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { AddonModGlossary, AddonModGlossaryProvider } from './glossary';
import { AddonModGlossaryHelper } from './glossary-helper';
import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from './glossary-offline';
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
/**
* Service to sync glossaries.
*/
@Injectable({ providedIn: 'root' })
export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModGlossarySyncResult> {
static readonly AUTO_SYNCED = 'addon_mod_glossary_autom_synced';
protected componentTranslatableString = 'glossary';
constructor() {
super('AddonModGlossarySyncProvider');
}
/**
* Try to synchronize all the glossaries in a certain site or in all sites.
*
* @param siteId Site ID to sync. If not defined, sync all sites.
* @param force Wether to force sync not depending on last execution.
* @return Promise resolved if sync is successful, rejected if sync fails.
*/
syncAllGlossaries(siteId?: string, force?: boolean): Promise<void> {
return this.syncOnSites('all glossaries', this.syncAllGlossariesFunc.bind(this, !!force), siteId);
}
/**
* Sync all glossaries on a site.
*
* @param force Wether to force sync not depending on last execution.
* @param siteId Site ID to sync.
* @return Promise resolved if sync is successful, rejected if sync fails.
*/
protected async syncAllGlossariesFunc(force: boolean, siteId: string): Promise<void> {
siteId = siteId || CoreSites.getCurrentSiteId();
await Promise.all([
this.syncAllGlossariesEntries(force, siteId),
this.syncRatings(undefined, force, siteId),
]);
}
/**
* Sync entried of all glossaries on a site.
*
* @param force Wether to force sync not depending on last execution.
* @param siteId Site ID to sync.
* @return Promise resolved if sync is successful, rejected if sync fails.
*/
protected async syncAllGlossariesEntries(force: boolean, siteId: string): Promise<void> {
const entries = await AddonModGlossaryOffline.getAllNewEntries(siteId);
// Do not sync same glossary twice.
const treated: Record<number, boolean> = {};
await Promise.all(entries.map(async (entry) => {
if (treated[entry.glossaryid]) {
return;
}
treated[entry.glossaryid] = true;
const result = force ?
await this.syncGlossaryEntries(entry.glossaryid, entry.userid, siteId) :
await this.syncGlossaryEntriesIfNeeded(entry.glossaryid, entry.userid, siteId);
if (result?.updated) {
// Sync successful, send event.
CoreEvents.trigger(AddonModGlossarySyncProvider.AUTO_SYNCED, {
glossaryId: entry.glossaryid,
userId: entry.userid,
warnings: result.warnings,
}, siteId);
}
}));
}
/**
* Sync a glossary only if a certain time has passed since the last time.
*
* @param glossaryId Glossary ID.
* @param userId User the entry belong to.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the glossary is synced or if it doesn't need to be synced.
*/
async syncGlossaryEntriesIfNeeded(
glossaryId: number,
userId: number,
siteId?: string,
): Promise<AddonModGlossarySyncResult | undefined> {
siteId = siteId || CoreSites.getCurrentSiteId();
const syncId = this.getGlossarySyncId(glossaryId, userId);
const needed = await this.isSyncNeeded(syncId, siteId);
if (needed) {
return this.syncGlossaryEntries(glossaryId, userId, siteId);
}
}
/**
* Synchronize all offline entries of a glossary.
*
* @param glossaryId Glossary ID to be synced.
* @param userId User the entries belong to.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if sync is successful, rejected otherwise.
*/
syncGlossaryEntries(glossaryId: number, userId?: number, siteId?: string): Promise<AddonModGlossarySyncResult> {
userId = userId || CoreSites.getCurrentSiteUserId();
siteId = siteId || CoreSites.getCurrentSiteId();
const syncId = this.getGlossarySyncId(glossaryId, userId);
if (this.isSyncing(syncId, siteId)) {
// There's already a sync ongoing for this glossary, return the promise.
return this.getOngoingSync(syncId, siteId)!;
}
// Verify that glossary isn't blocked.
if (CoreSync.isBlocked(AddonModGlossaryProvider.COMPONENT, syncId, siteId)) {
this.logger.debug('Cannot sync glossary ' + glossaryId + ' because it is blocked.');
throw new CoreSyncBlockedError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
}
this.logger.debug('Try to sync glossary ' + glossaryId + ' for user ' + userId);
const syncPromise = this.performSyncGlossaryEntries(glossaryId, userId, siteId);
return this.addOngoingSync(syncId, syncPromise, siteId);
}
protected async performSyncGlossaryEntries(
glossaryId: number,
userId: number,
siteId: string,
): Promise<AddonModGlossarySyncResult> {
const result: AddonModGlossarySyncResult = {
warnings: [],
updated: false,
};
const syncId = this.getGlossarySyncId(glossaryId, userId);
// Sync offline logs.
await CoreUtils.ignoreErrors(CoreCourseLogHelper.syncActivity(AddonModGlossaryProvider.COMPONENT, glossaryId, siteId));
// Get offline responses to be sent.
const entries = await CoreUtils.ignoreErrors(
AddonModGlossaryOffline.getGlossaryNewEntries(glossaryId, siteId, userId),
<AddonModGlossaryOfflineEntry[]> [],
);
if (!entries.length) {
// Nothing to sync.
await CoreUtils.ignoreErrors(this.setSyncTime(syncId, siteId));
return result;
} else if (!CoreApp.isOnline()) {
// Cannot sync in offline.
throw new CoreNetworkError();
}
let courseId: number | undefined;
await Promise.all(entries.map(async (data) => {
courseId = courseId || data.courseid;
try {
// First of all upload the attachments (if any).
const itemId = await this.uploadAttachments(glossaryId, data, siteId);
// Now try to add the entry.
await AddonModGlossary.addEntryOnline(glossaryId, data.concept, data.definition, data.options, itemId, siteId);
result.updated = true;
await this.deleteAddEntry(glossaryId, data.concept, data.timecreated, siteId);
} catch (error) {
if (!CoreUtils.isWebServiceError(error)) {
// Couldn't connect to server, reject.
throw error;
}
// The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
result.updated = true;
await this.deleteAddEntry(glossaryId, data.concept, data.timecreated, siteId);
// Responses deleted, add a warning.
this.addOfflineDataDeletedWarning(result.warnings, data.concept, error);
}
}));
if (result.updated && courseId) {
// Data has been sent to server. Now invalidate the WS calls.
try {
const glossary = await AddonModGlossary.getGlossaryById(courseId, glossaryId);
await AddonModGlossary.invalidateGlossaryEntries(glossary, true);
} catch {
// Ignore errors.
}
}
// Sync finished, set sync time.
await CoreUtils.ignoreErrors(this.setSyncTime(syncId, siteId));
return result;
}
/**
* Synchronize offline ratings.
*
* @param cmId Course module to be synced. If not defined, sync all glossaries.
* @param force Wether to force sync not depending on last execution.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if sync is successful, rejected otherwise.
*/
async syncRatings(cmId?: number, force?: boolean, siteId?: string): Promise<AddonModGlossarySyncResult> {
siteId = siteId || CoreSites.getCurrentSiteId();
const results = await CoreRatingSync.syncRatings('mod_glossary', 'entry', ContextLevel.MODULE, cmId, 0, force, siteId);
let updated = false;
const warnings: string[] = [];
await CoreUtils.allPromises(results.map(async (result) => {
if (result.updated.length) {
updated = true;
// Invalidate entry of updated ratings.
await Promise.all(result.updated.map((itemId) => AddonModGlossary.invalidateEntry(itemId, siteId)));
}
if (result.warnings.length) {
const glossary = await AddonModGlossary.getGlossary(result.itemSet.courseId, result.itemSet.instanceId, { siteId });
result.warnings.forEach((warning) => {
this.addOfflineDataDeletedWarning(warnings, glossary.name, warning);
});
}
}));
return { updated, warnings };
}
/**
* Delete a new entry.
*
* @param glossaryId Glossary ID.
* @param concept Glossary entry concept.
* @param timeCreated Time to allow duplicated entries.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when deleted.
*/
protected async deleteAddEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise<void> {
await Promise.all([
AddonModGlossaryOffline.deleteNewEntry(glossaryId, concept, timeCreated, siteId),
AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timeCreated, siteId),
]);
}
/**
* Upload attachments of an offline entry.
*
* @param glossaryId Glossary ID.
* @param entry Offline entry.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with draftid if uploaded, resolved with 0 if nothing to upload.
*/
protected async uploadAttachments(glossaryId: number, entry: AddonModGlossaryOfflineEntry, siteId?: string): Promise<number> {
if (!entry.attachments) {
// No attachments.
return 0;
}
// Has some attachments to sync.
let files: (CoreWSExternalFile | FileEntry)[] = entry.attachments.online || [];
if (entry.attachments.offline) {
// Has offline files.
const storedFiles = await CoreUtils.ignoreErrors(
AddonModGlossaryHelper.getStoredFiles(glossaryId, entry.concept, entry.timecreated, siteId),
[], // Folder not found, no files to add.
);
files = files.concat(storedFiles);
}
return CoreFileUploader.uploadOrReuploadFiles(files, AddonModGlossaryProvider.COMPONENT, glossaryId, siteId);
}
/**
* Get the ID of a glossary sync.
*
* @param glossaryId Glossary ID.
* @param userId User the entries belong to.. If not defined, current user.
* @return Sync ID.
*/
protected getGlossarySyncId(glossaryId: number, userId?: number): string {
userId = userId || CoreSites.getCurrentSiteUserId();
return 'glossary#' + glossaryId + '#' + userId;
}
}
export const AddonModGlossarySync = makeSingleton(AddonModGlossarySyncProvider);
/**
* Data returned by a glossary sync.
*/
export type AddonModGlossarySyncResult = {
warnings: string[]; // List of warnings.
updated: boolean; // Whether some data was sent to the server or offline data was updated.
};
/**
* Data passed to AUTO_SYNCED event.
*/
export type AddonModGlossaryAutoSyncData = {
glossaryId: number;
userId: number;
warnings: string[];
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,78 @@
// (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 { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourse } from '@features/course/services/course';
import { CoreNavigator } from '@services/navigator';
import { CoreDomUtils } from '@services/utils/dom';
import { makeSingleton } from '@singletons';
import { AddonModGlossaryModuleHandlerService } from './module';
/**
* Content links handler for glossary new entry.
* Match mod/glossary/edit.php?cmid=6 with a valid data.
* Currently it only supports new entry.
*/
@Injectable({ providedIn: 'root' })
export class AddonModGlossaryEditLinkHandlerService extends CoreContentLinksHandlerBase {
name = 'AddonModGlossaryEditLinkHandler';
featureName = 'CoreCourseModuleDelegate_AddonModGlossary';
pattern = /\/mod\/glossary\/edit\.php.*([?&](cmid)=\d+)/;
/**
* @inheritdoc
*/
getActions(siteIds: string[], url: string, params: Record<string, string>): CoreContentLinksAction[] {
return [{
action: async (siteId: string) => {
const modal = await CoreDomUtils.showModalLoading();
const cmId = Number(params.cmid);
try {
const module = await CoreCourse.getModuleBasicInfo(cmId, siteId);
await CoreNavigator.navigateToSitePath(
AddonModGlossaryModuleHandlerService.PAGE_NAME + '/edit/0',
{
params: {
cmId: module.id,
courseId: module.course,
},
siteId,
},
);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingglossary', true);
} finally {
// Just in case. In fact we need to dismiss the modal before showing a toast or error message.
modal.dismiss();
}
},
}];
}
/**
* @inheritdoc
*/
async isEnabled(siteId: string, url: string, params: Record<string, string>): Promise<boolean> {
return typeof params.cmid != 'undefined';
}
}
export const AddonModGlossaryEditLinkHandler = makeSingleton(AddonModGlossaryEditLinkHandlerService);

View File

@ -0,0 +1,75 @@
// (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 { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourse } from '@features/course/services/course';
import { CoreNavigator } from '@services/navigator';
import { CoreDomUtils } from '@services/utils/dom';
import { makeSingleton } from '@singletons';
import { AddonModGlossary } from '../glossary';
import { AddonModGlossaryModuleHandlerService } from './module';
/**
* Handler to treat links to glossary entries.
*/
@Injectable({ providedIn: 'root' })
export class AddonModGlossaryEntryLinkHandlerService extends CoreContentLinksHandlerBase {
name = 'AddonModGlossaryEntryLinkHandler';
featureName = 'CoreCourseModuleDelegate_AddonModGlossary';
pattern = /\/mod\/glossary\/(showentry|view)\.php.*([&?](eid|g|mode|hook)=\d+)/;
/**
* @inheritdoc
*/
getActions(siteIds: string[], url: string, params: Record<string, string>): CoreContentLinksAction[] {
return [{
action: async (siteId: string) => {
const modal = await CoreDomUtils.showModalLoading();
try {
const entryId = params.mode == 'entry' ? Number(params.hook) : Number(params.eid);
const response = await AddonModGlossary.getEntry(entryId, { siteId });
const module = await CoreCourse.getModuleBasicInfoByInstance(
response.entry.glossaryid,
'glossary',
siteId,
);
await CoreNavigator.navigateToSitePath(
AddonModGlossaryModuleHandlerService.PAGE_NAME + `/entry/${entryId}`,
{
params: {
cmId: module.id,
courseId: module.course,
},
siteId,
},
);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true);
} finally {
modal.dismiss();
}
},
}];
}
}
export const AddonModGlossaryEntryLinkHandler = makeSingleton(AddonModGlossaryEntryLinkHandlerService);

View File

@ -0,0 +1,33 @@
// (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 { Injectable } from '@angular/core';
import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
import { makeSingleton } from '@singletons';
/**
* Handler to treat links to glossary index.
*/
@Injectable({ providedIn: 'root' })
export class AddonModGlossaryIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
name = 'AddonModGlossaryIndexLinkHandler';
constructor() {
super('AddonModGlossary', 'glossary', 'g');
}
}
export const AddonModGlossaryIndexLinkHandler = makeSingleton(AddonModGlossaryIndexLinkHandlerService);

View File

@ -0,0 +1,33 @@
// (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 { Injectable } from '@angular/core';
import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler';
import { makeSingleton } from '@singletons';
/**
* Handler to treat links to glossary list page.
*/
@Injectable({ providedIn: 'root' })
export class AddonModGlossaryListLinkHandlerService extends CoreContentLinksModuleListHandler {
name = 'AddonModGlossaryListLinkHandler';
constructor() {
super('AddonModGlossary', 'glossary');
}
}
export const AddonModGlossaryListLinkHandler = makeSingleton(AddonModGlossaryListLinkHandlerService);

View File

@ -0,0 +1,92 @@
// (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 { CoreConstants } from '@/core/constants';
import { Injectable, Type } from '@angular/core';
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { makeSingleton } from '@singletons';
import { AddonModGlossaryIndexComponent } from '../../components/index/index';
/**
* Handler to support glossary modules.
*/
@Injectable({ providedIn: 'root' })
export class AddonModGlossaryModuleHandlerService implements CoreCourseModuleHandler {
static readonly PAGE_NAME = 'mod_glossary';
name = 'AddonModGlossary';
modName = 'glossary';
supportedFeatures = {
[CoreConstants.FEATURE_GROUPS]: false,
[CoreConstants.FEATURE_GROUPINGS]: false,
[CoreConstants.FEATURE_MOD_INTRO]: true,
[CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
[CoreConstants.FEATURE_COMPLETION_HAS_RULES]: true,
[CoreConstants.FEATURE_GRADE_HAS_GRADE]: true,
[CoreConstants.FEATURE_GRADE_OUTCOMES]: true,
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
[CoreConstants.FEATURE_RATE]: true,
[CoreConstants.FEATURE_PLAGIARISM]: true,
};
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* @inheritdoc
*/
getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData {
return {
icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
title: module.name,
class: 'addon-mod_glossary-handler',
showDownloadButton: true,
action: (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => {
options = options || {};
options.params = options.params || {};
Object.assign(options.params, { module });
const routeParams = '/' + courseId + '/' + module.id;
CoreNavigator.navigateToSitePath(AddonModGlossaryModuleHandlerService.PAGE_NAME + routeParams, options);
},
};
}
/**
* @inheritdoc
*/
async getMainComponent(): Promise<Type<unknown>> {
return AddonModGlossaryIndexComponent;
}
/**
* @inheritdoc
*/
displayRefresherInSingleActivity(): boolean {
return false;
}
}
export const AddonModGlossaryModuleHandler = makeSingleton(AddonModGlossaryModuleHandlerService);

View File

@ -0,0 +1,235 @@
// (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 { Injectable } from '@angular/core';
import { CoreComments } from '@features/comments/services/comments';
import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
import { CoreUser } from '@features/user/services/user';
import { CoreFilepool } from '@services/filepool';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreWSExternalFile } from '@services/ws';
import { makeSingleton } from '@singletons';
import { AddonModGlossary, AddonModGlossaryEntry, AddonModGlossaryGlossary, AddonModGlossaryProvider } from '../glossary';
import { AddonModGlossarySync, AddonModGlossarySyncResult } from '../glossary-sync';
/**
* Handler to prefetch forums.
*/
@Injectable({ providedIn: 'root' })
export class AddonModGlossaryPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
name = 'AddonModGlossary';
modName = 'glossary';
component = AddonModGlossaryProvider.COMPONENT;
updatesNames = /^configuration$|^.*files$|^entries$/;
/**
* @inheritdoc
*/
async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSExternalFile[]> {
try {
const glossary = await AddonModGlossary.getGlossary(courseId, module.id);
const entries = await AddonModGlossary.fetchAllEntries(
AddonModGlossary.getEntriesByLetter.bind(AddonModGlossary.instance, glossary.id, 'ALL'),
{
cmId: module.id,
},
);
return this.getFilesFromGlossaryAndEntries(module, glossary, entries);
} catch {
// Glossary not found, return empty list.
return [];
}
}
/**
* Get the list of downloadable files. It includes entry embedded files.
*
* @param module Module to get the files.
* @param glossary Glossary
* @param entries Entries of the Glossary.
* @return List of Files.
*/
protected getFilesFromGlossaryAndEntries(
module: CoreCourseAnyModuleData,
glossary: AddonModGlossaryGlossary,
entries: AddonModGlossaryEntry[],
): CoreWSExternalFile[] {
let files = this.getIntroFilesFromInstance(module, glossary);
const getInlineFiles = CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.2');
// Get entries files.
entries.forEach((entry) => {
files = files.concat(entry.attachments || []);
if (getInlineFiles && entry.definitioninlinefiles && entry.definitioninlinefiles.length) {
files = files.concat(entry.definitioninlinefiles);
} else if (entry.definition && !getInlineFiles) {
files = files.concat(CoreFilepool.extractDownloadableFilesFromHtmlAsFakeFileObjects(entry.definition));
}
});
return files;
}
/**
* @inheritdoc
*/
invalidateContent(moduleId: number, courseId: number): Promise<void> {
return AddonModGlossary.invalidateContent(moduleId, courseId);
}
/**
* @inheritdoc
*/
prefetch(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> {
return this.prefetchPackage(module, courseId, this.prefetchGlossary.bind(this, module, courseId));
}
/**
* Prefetch a glossary.
*
* @param module The module object returned by WS.
* @param courseId Course ID the module belongs to.
* @param siteId Site ID.
* @return Promise resolved when done.
*/
protected async prefetchGlossary(module: CoreCourseAnyModuleData, courseId: number, siteId: string): Promise<void> {
siteId = siteId || CoreSites.getCurrentSiteId();
const options = {
cmId: module.id,
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
siteId,
};
// Prefetch the glossary data.
const glossary = await AddonModGlossary.getGlossary(courseId, module.id, { siteId });
const promises: Promise<unknown>[] = [];
glossary.browsemodes.forEach((mode) => {
switch (mode) {
case 'letter': // Always done. Look bellow.
break;
case 'cat':
promises.push(AddonModGlossary.fetchAllEntries(
AddonModGlossary.getEntriesByCategory.bind(
AddonModGlossary.instance,
glossary.id,
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
),
options,
));
break;
case 'date':
promises.push(AddonModGlossary.fetchAllEntries(
AddonModGlossary.getEntriesByDate.bind(
AddonModGlossary.instance,
glossary.id,
'CREATION',
'DESC',
),
options,
));
promises.push(AddonModGlossary.fetchAllEntries(
AddonModGlossary.getEntriesByDate.bind(
AddonModGlossary.instance,
glossary.id,
'UPDATE',
'DESC',
),
options,
));
break;
case 'author':
promises.push(AddonModGlossary.fetchAllEntries(
AddonModGlossary.getEntriesByAuthor.bind(
AddonModGlossary.instance,
glossary.id,
'ALL',
'LASTNAME',
'ASC',
),
options,
));
break;
default:
}
});
// Fetch all entries to get information from.
promises.push(AddonModGlossary.fetchAllEntries(
AddonModGlossary.getEntriesByLetter.bind(AddonModGlossary.instance, glossary.id, 'ALL'),
options,
).then((entries) => {
const promises: Promise<unknown>[] = [];
const commentsEnabled = !CoreComments.areCommentsDisabledInSite();
entries.forEach((entry) => {
// Don't fetch individual entries, it's too many WS calls.
if (glossary.allowcomments && commentsEnabled) {
promises.push(CoreComments.getComments(
'module',
glossary.coursemodule,
'mod_glossary',
entry.id,
'glossary_entry',
0,
siteId,
));
}
});
const files = this.getFilesFromGlossaryAndEntries(module, glossary, entries);
promises.push(CoreFilepool.addFilesToQueue(siteId, files, this.component, module.id));
// Prefetch user avatars.
promises.push(CoreUser.prefetchUserAvatars(entries, 'userpictureurl', siteId));
return Promise.all(promises);
}));
// Get all categories.
promises.push(AddonModGlossary.getAllCategories(glossary.id, options));
// Prefetch data for link handlers.
promises.push(CoreCourse.getModuleBasicInfo(module.id, siteId));
promises.push(CoreCourse.getModuleBasicInfoByInstance(glossary.id, 'glossary', siteId));
await Promise.all(promises);
}
/**
* @inheritdoc
*/
async sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise<AddonModGlossarySyncResult> {
const results = await Promise.all([
AddonModGlossarySync.syncGlossaryEntries(module.instance!, undefined, siteId),
AddonModGlossarySync.syncRatings(module.id, undefined, siteId),
]);
return {
updated: results[0].updated || results[1].updated,
warnings: results[0].warnings.concat(results[1].warnings),
};
}
}
export const AddonModGlossaryPrefetchHandler = makeSingleton(AddonModGlossaryPrefetchHandlerService);

View File

@ -0,0 +1,44 @@
// (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 { Injectable } from '@angular/core';
import { CoreCronHandler } from '@services/cron';
import { makeSingleton } from '@singletons';
import { AddonModGlossarySync } from '../glossary-sync';
/**
* Synchronization cron handler.
*/
@Injectable({ providedIn: 'root' })
export class AddonModGlossarySyncCronHandlerService implements CoreCronHandler {
name = 'AddonModGlossarySyncCronHandler';
/**
* @inheritdoc
*/
execute(siteId?: string, force?: boolean): Promise<void> {
return AddonModGlossarySync.syncAllGlossaries(siteId, force);
}
/**
* @inheritdoc
*/
getInterval(): number {
return AddonModGlossarySync.syncInterval;
}
}
export const AddonModGlossarySyncCronHandler = makeSingleton(AddonModGlossarySyncCronHandlerService);

View File

@ -0,0 +1,53 @@
// (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 { Injectable, Type } from '@angular/core';
import { CoreTagFeedComponent } from '@features/tag/components/feed/feed';
import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate';
import { CoreTagFeedElement, CoreTagHelper } from '@features/tag/services/tag-helper';
import { makeSingleton } from '@singletons';
/**
* Handler to support tags.
*/
@Injectable({ providedIn: 'root' })
export class AddonModGlossaryTagAreaHandlerService implements CoreTagAreaHandler {
name = 'AddonModGlossaryTagAreaHandler';
type = 'mod_glossary/glossary_entries';
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* @inheritdoc
*/
parseContent(content: string): CoreTagFeedElement[] {
return CoreTagHelper.parseFeedContent(content);
}
/**
* @inheritdoc
*/
getComponent(): Type<unknown> {
return CoreTagFeedComponent;
}
}
export const AddonModGlossaryTagAreaHandler = makeSingleton(AddonModGlossaryTagAreaHandlerService);

View File

@ -32,6 +32,7 @@ import { AddonModSurveyModule } from './survey/survey.module';
import { AddonModScormModule } from './scorm/scorm.module';
import { AddonModChoiceModule } from './choice/choice.module';
import { AddonModWikiModule } from './wiki/wiki.module';
import { AddonModGlossaryModule } from './glossary/glossary.module';
@NgModule({
imports: [
@ -53,6 +54,7 @@ import { AddonModWikiModule } from './wiki/wiki.module';
AddonModScormModule,
AddonModChoiceModule,
AddonModWikiModule,
AddonModGlossaryModule,
],
})
export class AddonModModule { }

View File

@ -20,7 +20,6 @@ import { CoreApp } from '@services/app';
import { CoreGroups } from '@services/groups';
import { CoreSites } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton, Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
@ -260,11 +259,7 @@ export class AddonModWikiSyncProvider extends CoreSyncBaseProvider<AddonModWikiS
result.updated = true;
// Page deleted, add the page to discarded pages and add a warning.
const warning = Translate.instant('core.warningofflinedatadeleted', {
component: Translate.instant('addon.mod_wiki.wikipage'),
name: page.title,
error: CoreTextUtils.getErrorMessageFromError(error),
});
const warning = this.getOfflineDataDeletedWarning(page.title, error);
result.discarded.push({
title: page.title,

View File

@ -38,8 +38,5 @@ import { CoreSharedModule } from '@/core/shared.module';
exports: [
AddonUserProfileFieldCheckboxComponent,
],
entryComponents: [
AddonUserProfileFieldCheckboxComponent,
],
})
export class AddonUserProfileFieldCheckboxModule {}

View File

@ -38,8 +38,5 @@ import { CoreSharedModule } from '@/core/shared.module';
exports: [
AddonUserProfileFieldDatetimeComponent,
],
entryComponents: [
AddonUserProfileFieldDatetimeComponent,
],
})
export class AddonUserProfileFieldDatetimeModule {}

View File

@ -38,8 +38,5 @@ import { CoreSharedModule } from '@/core/shared.module';
exports: [
AddonUserProfileFieldMenuComponent,
],
entryComponents: [
AddonUserProfileFieldMenuComponent,
],
})
export class AddonUserProfileFieldMenuModule {}

View File

@ -38,8 +38,5 @@ import { CoreSharedModule } from '@/core/shared.module';
exports: [
AddonUserProfileFieldTextComponent,
],
entryComponents: [
AddonUserProfileFieldTextComponent,
],
})
export class AddonUserProfileFieldTextModule {}

View File

@ -40,8 +40,5 @@ import { CoreEditorComponentsModule } from '@features/editor/components/componen
exports: [
AddonUserProfileFieldTextareaComponent,
],
entryComponents: [
AddonUserProfileFieldTextareaComponent,
],
})
export class AddonUserProfileFieldTextareaModule {}

View File

@ -37,7 +37,6 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
@NgModule({
declarations: [AppComponent],
entryComponents: [],
imports: [
BrowserModule,
BrowserAnimationsModule,

View File

@ -72,11 +72,7 @@ export class CoreSyncBaseProvider<T = void> {
* @param error Specific error message.
*/
protected addOfflineDataDeletedWarning(warnings: string[], name: string, error: CoreAnyError): void {
const warning = Translate.instant('core.warningofflinedatadeleted', {
component: this.componentTranslate,
name: name,
error: CoreTextUtils.getErrorMessageFromError(error),
});
const warning = this.getOfflineDataDeletedWarning(name, error);
if (warnings.indexOf(warning) == -1) {
warnings.push(warning);
@ -113,6 +109,21 @@ export class CoreSyncBaseProvider<T = void> {
}
}
/**
* Add an offline data deleted warning to a list of warnings.
*
* @param name Instance name.
* @param error Specific error message.
* @return Warning message.
*/
protected getOfflineDataDeletedWarning(name: string, error: CoreAnyError): string {
return Translate.instant('core.warningofflinedatadeleted', {
component: this.componentTranslate,
name: name,
error: CoreTextUtils.getErrorMessageFromError(error),
});
}
/**
* If there's an ongoing sync for a certain identifier return it.
*

View File

@ -51,8 +51,6 @@ import { CoreLogger } from '@singletons/logger';
* <p>Cannot render the data.</p>
* </core-dynamic-component>
*
* Please notice that the component that you pass needs to be declared in entryComponents of the module to be created dynamically.
*
* Alternatively, you can also supply a ComponentRef instead of the class of the component. In this case, the component won't
* be instantiated because it already is, it will be attached to the view and the right data will be passed to it.
* Passing ComponentRef is meant for site plugins.
@ -119,8 +117,8 @@ export class CoreDynamicComponent implements OnChanges, DoCheck {
const changes = this.differ.diff(this.data || {});
if (changes) {
this.setInputData();
if (this.ngOnChanges) {
this.ngOnChanges(CoreDomUtils.createChangesFromKeyValueDiff(changes));
if (this.instance.ngOnChanges) {
this.instance.ngOnChanges(CoreDomUtils.createChangesFromKeyValueDiff(changes));
}
}
}

View File

@ -59,10 +59,15 @@ export class CoreAutoRowsDirective implements AfterViewInit {
* Resize the textarea.
*/
protected resize(): void {
let nativeElement = this.element.nativeElement;
let nativeElement: HTMLElement = this.element.nativeElement;
if (nativeElement.tagName == 'ION-TEXTAREA') {
// The first child of ion-textarea is the actual textarea element.
nativeElement = nativeElement.firstElementChild;
// Search the actual textarea.
const textarea = nativeElement.querySelector('textarea');
if (!textarea) {
return;
}
nativeElement = textarea;
}
// Set height to 1px to force scroll height to calculate correctly.

View File

@ -35,10 +35,5 @@ import { CoreSharedModule } from '@/core/shared.module';
CoreBlockPreRenderedComponent,
CoreBlockCourseBlocksComponent,
],
entryComponents: [
CoreBlockOnlyTitleComponent,
CoreBlockPreRenderedComponent,
CoreBlockCourseBlocksComponent,
],
})
export class CoreBlockComponentsModule {}

View File

@ -29,8 +29,5 @@ import { CoreCommentsCommentsComponent } from './comments/comments';
CoreCommentsCommentsComponent,
CoreCommentsAddComponent,
],
entryComponents: [
CoreCommentsCommentsComponent,
],
})
export class CoreCommentsComponentsModule {}

View File

@ -130,7 +130,7 @@ import { ADDON_MOD_DATA_SERVICES } from '@addons/mod/data/data.module';
// @todo import { ADDON_MOD_FEEDBACK_SERVICES } from '@addons/mod/feedback/feedback.module';
import { ADDON_MOD_FOLDER_SERVICES } from '@addons/mod/folder/folder.module';
import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.module';
// @todo import { ADDON_MOD_GLOSSARY_SERVICES } from '@addons/mod/glossary/glossary.module';
import { ADDON_MOD_GLOSSARY_SERVICES } from '@addons/mod/glossary/glossary.module';
import { ADDON_MOD_H5P_ACTIVITY_SERVICES } from '@addons/mod/h5pactivity/h5pactivity.module';
import { ADDON_MOD_IMSCP_SERVICES } from '@addons/mod/imscp/imscp.module';
import { ADDON_MOD_LESSON_SERVICES } from '@addons/mod/lesson/lesson.module';
@ -296,7 +296,7 @@ export class CoreCompileProvider {
// @todo ...ADDON_MOD_FEEDBACK_SERVICES,
...ADDON_MOD_FOLDER_SERVICES,
...ADDON_MOD_FORUM_SERVICES,
// @todo ...ADDON_MOD_GLOSSARY_SERVICES,
...ADDON_MOD_GLOSSARY_SERVICES,
...ADDON_MOD_H5P_ACTIVITY_SERVICES,
...ADDON_MOD_IMSCP_SERVICES,
...ADDON_MOD_LESSON_SERVICES,

View File

@ -36,8 +36,5 @@ import { CoreCoursesSelfEnrolPasswordComponent } from './self-enrol-password/sel
CoreCoursesCourseOptionsMenuComponent,
CoreCoursesSelfEnrolPasswordComponent,
],
entryComponents: [
CoreCoursesCourseOptionsMenuComponent,
],
})
export class CoreCoursesComponentsModule {}

View File

@ -29,8 +29,5 @@ import { CoreSharedModule } from '@/core/shared.module';
exports: [
CoreEditorRichTextEditorComponent,
],
entryComponents: [
CoreEditorRichTextEditorComponent,
],
})
export class CoreEditorComponentsModule {}

View File

@ -32,8 +32,5 @@ import { CoreRatingRatingsComponent } from './ratings/ratings';
CoreRatingRateComponent,
CoreRatingRatingsComponent,
],
entryComponents: [
CoreRatingRatingsComponent,
],
})
export class CoreRatingComponentsModule {}