commit
f48433215b
|
@ -28,8 +28,5 @@ import { AddonBlockActivityModulesComponent } from './activitymodules/activitymo
|
|||
exports: [
|
||||
AddonBlockActivityModulesComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockActivityModulesComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockActivityModulesComponentsModule {}
|
||||
|
|
|
@ -27,8 +27,5 @@ import { AddonBlockActivityResultsComponent } from './activityresults/activityre
|
|||
exports: [
|
||||
AddonBlockActivityResultsComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockActivityResultsComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockActivityResultsComponentsModule {}
|
||||
|
|
|
@ -27,8 +27,5 @@ import { AddonBlockBadgesComponent } from './badges/badges';
|
|||
exports: [
|
||||
AddonBlockBadgesComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockBadgesComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockBadgesComponentsModule {}
|
||||
|
|
|
@ -27,8 +27,5 @@ import { AddonBlockBlogMenuComponent } from './blogmenu/blogmenu';
|
|||
exports: [
|
||||
AddonBlockBlogMenuComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockBlogMenuComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockBlogMenuComponentsModule {}
|
||||
|
|
|
@ -27,8 +27,5 @@ import { AddonBlockBlogRecentComponent } from './blogrecent/blogrecent';
|
|||
exports: [
|
||||
AddonBlockBlogRecentComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockBlogRecentComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockBlogRecentComponentsModule {}
|
||||
|
|
|
@ -27,8 +27,5 @@ import { AddonBlockBlogTagsComponent } from './blogtags/blogtags';
|
|||
exports: [
|
||||
AddonBlockBlogTagsComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockBlogTagsComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockBlogTagsComponentsModule {}
|
||||
|
|
|
@ -29,8 +29,5 @@ import { AddonBlockMyOverviewComponent } from './myoverview/myoverview';
|
|||
exports: [
|
||||
AddonBlockMyOverviewComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockMyOverviewComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockMyOverviewComponentsModule {}
|
||||
|
|
|
@ -27,8 +27,5 @@ import { AddonBlockNewsItemsComponent } from './newsitems/newsitems';
|
|||
exports: [
|
||||
AddonBlockNewsItemsComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockNewsItemsComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockNewsItemsComponentsModule {}
|
||||
|
|
|
@ -27,8 +27,5 @@ import { AddonBlockOnlineUsersComponent } from './onlineusers/onlineusers';
|
|||
exports: [
|
||||
AddonBlockOnlineUsersComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockOnlineUsersComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockOnlineUsersComponentsModule {}
|
||||
|
|
|
@ -27,8 +27,5 @@ import { AddonBlockRecentActivityComponent } from './recentactivity/recentactivi
|
|||
exports: [
|
||||
AddonBlockRecentActivityComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockRecentActivityComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockRecentActivityComponentsModule {}
|
||||
|
|
|
@ -30,8 +30,5 @@ import { AddonBlockRecentlyAccessedCoursesComponent } from './recentlyaccessedco
|
|||
exports: [
|
||||
AddonBlockRecentlyAccessedCoursesComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockRecentlyAccessedCoursesComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockRecentlyAccessedCoursesComponentsModule {}
|
||||
|
|
|
@ -30,8 +30,5 @@ import { AddonBlockRecentlyAccessedItemsComponent } from './recentlyaccesseditem
|
|||
exports: [
|
||||
AddonBlockRecentlyAccessedItemsComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockRecentlyAccessedItemsComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockRecentlyAccessedItemsComponentsModule {}
|
||||
|
|
|
@ -27,8 +27,5 @@ import { AddonBlockRssClientComponent } from './rssclient/rssclient';
|
|||
exports: [
|
||||
AddonBlockRssClientComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockRssClientComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockRssClientComponentsModule {}
|
||||
|
|
|
@ -30,8 +30,5 @@ import { AddonBlockSiteMainMenuComponent } from './sitemainmenu/sitemainmenu';
|
|||
exports: [
|
||||
AddonBlockSiteMainMenuComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockSiteMainMenuComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockSiteMainMenuComponentsModule {}
|
||||
|
|
|
@ -30,8 +30,5 @@ import { AddonBlockStarredCoursesComponent } from './starredcourses/starredcours
|
|||
exports: [
|
||||
AddonBlockStarredCoursesComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockStarredCoursesComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockStarredCoursesComponentsModule {}
|
||||
|
|
|
@ -27,8 +27,5 @@ import { AddonBlockTagsComponent } from './tags/tags';
|
|||
exports: [
|
||||
AddonBlockTagsComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockTagsComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockTagsComponentsModule {}
|
||||
|
|
|
@ -35,9 +35,5 @@ import { AddonBlockTimelineEventsComponent } from './events/events';
|
|||
AddonBlockTimelineComponent,
|
||||
AddonBlockTimelineEventsComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonBlockTimelineComponent,
|
||||
AddonBlockTimelineEventsComponent,
|
||||
],
|
||||
})
|
||||
export class AddonBlockTimelineComponentsModule {}
|
||||
|
|
|
@ -36,8 +36,5 @@ import { AddonCalendarFilterPopoverComponent } from './filter/filter';
|
|||
AddonCalendarUpcomingEventsComponent,
|
||||
AddonCalendarFilterPopoverComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonCalendarFilterPopoverComponent,
|
||||
],
|
||||
})
|
||||
export class AddonCalendarComponentsModule {}
|
||||
|
|
|
@ -25,8 +25,5 @@ import { AddonMessagesConversationInfoComponent } from './conversation-info/conv
|
|||
imports: [
|
||||
CoreSharedModule,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonMessagesConversationInfoComponent,
|
||||
],
|
||||
})
|
||||
export class AddonMessagesComponentsModule {}
|
||||
|
|
|
@ -40,8 +40,5 @@ import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate
|
|||
exports: [
|
||||
AddonModAssignFeedbackCommentsComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModAssignFeedbackCommentsComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModAssignFeedbackCommentsModule {}
|
||||
|
|
|
@ -38,8 +38,5 @@ import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate
|
|||
exports: [
|
||||
AddonModAssignFeedbackEditPdfComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModAssignFeedbackEditPdfComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModAssignFeedbackEditPdfModule {}
|
||||
|
|
|
@ -38,8 +38,5 @@ import { AddonModAssignFeedbackDelegate } from '../../services/feedback-delegate
|
|||
exports: [
|
||||
AddonModAssignFeedbackFileComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModAssignFeedbackFileComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModAssignFeedbackFileModule {}
|
||||
|
|
|
@ -40,8 +40,5 @@ import { CoreCommentsComponentsModule } from '@features/comments/components/comp
|
|||
exports: [
|
||||
AddonModAssignSubmissionCommentsComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModAssignSubmissionCommentsComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModAssignSubmissionCommentsModule {}
|
||||
|
|
|
@ -38,8 +38,5 @@ import { AddonModAssignSubmissionDelegate } from '../../services/submission-dele
|
|||
exports: [
|
||||
AddonModAssignSubmissionFileComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModAssignSubmissionFileComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModAssignSubmissionFileModule {}
|
||||
|
|
|
@ -40,8 +40,5 @@ import { AddonModAssignSubmissionDelegate } from '../../services/submission-dele
|
|||
exports: [
|
||||
AddonModAssignSubmissionOnlineTextComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModAssignSubmissionOnlineTextComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModAssignSubmissionOnlineTextModule {}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -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';
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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"
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
|
@ -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;
|
||||
};
|
|
@ -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
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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 { }
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -38,8 +38,5 @@ import { CoreSharedModule } from '@/core/shared.module';
|
|||
exports: [
|
||||
AddonUserProfileFieldCheckboxComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonUserProfileFieldCheckboxComponent,
|
||||
],
|
||||
})
|
||||
export class AddonUserProfileFieldCheckboxModule {}
|
||||
|
|
|
@ -38,8 +38,5 @@ import { CoreSharedModule } from '@/core/shared.module';
|
|||
exports: [
|
||||
AddonUserProfileFieldDatetimeComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonUserProfileFieldDatetimeComponent,
|
||||
],
|
||||
})
|
||||
export class AddonUserProfileFieldDatetimeModule {}
|
||||
|
|
|
@ -38,8 +38,5 @@ import { CoreSharedModule } from '@/core/shared.module';
|
|||
exports: [
|
||||
AddonUserProfileFieldMenuComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonUserProfileFieldMenuComponent,
|
||||
],
|
||||
})
|
||||
export class AddonUserProfileFieldMenuModule {}
|
||||
|
|
|
@ -38,8 +38,5 @@ import { CoreSharedModule } from '@/core/shared.module';
|
|||
exports: [
|
||||
AddonUserProfileFieldTextComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonUserProfileFieldTextComponent,
|
||||
],
|
||||
})
|
||||
export class AddonUserProfileFieldTextModule {}
|
||||
|
|
|
@ -40,8 +40,5 @@ import { CoreEditorComponentsModule } from '@features/editor/components/componen
|
|||
exports: [
|
||||
AddonUserProfileFieldTextareaComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AddonUserProfileFieldTextareaComponent,
|
||||
],
|
||||
})
|
||||
export class AddonUserProfileFieldTextareaModule {}
|
||||
|
|
|
@ -37,7 +37,6 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
|||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
entryComponents: [],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -35,10 +35,5 @@ import { CoreSharedModule } from '@/core/shared.module';
|
|||
CoreBlockPreRenderedComponent,
|
||||
CoreBlockCourseBlocksComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
CoreBlockOnlyTitleComponent,
|
||||
CoreBlockPreRenderedComponent,
|
||||
CoreBlockCourseBlocksComponent,
|
||||
],
|
||||
})
|
||||
export class CoreBlockComponentsModule {}
|
||||
|
|
|
@ -29,8 +29,5 @@ import { CoreCommentsCommentsComponent } from './comments/comments';
|
|||
CoreCommentsCommentsComponent,
|
||||
CoreCommentsAddComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
CoreCommentsCommentsComponent,
|
||||
],
|
||||
})
|
||||
export class CoreCommentsComponentsModule {}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -36,8 +36,5 @@ import { CoreCoursesSelfEnrolPasswordComponent } from './self-enrol-password/sel
|
|||
CoreCoursesCourseOptionsMenuComponent,
|
||||
CoreCoursesSelfEnrolPasswordComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
CoreCoursesCourseOptionsMenuComponent,
|
||||
],
|
||||
})
|
||||
export class CoreCoursesComponentsModule {}
|
||||
|
|
|
@ -29,8 +29,5 @@ import { CoreSharedModule } from '@/core/shared.module';
|
|||
exports: [
|
||||
CoreEditorRichTextEditorComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
CoreEditorRichTextEditorComponent,
|
||||
],
|
||||
})
|
||||
export class CoreEditorComponentsModule {}
|
||||
|
|
|
@ -32,8 +32,5 @@ import { CoreRatingRatingsComponent } from './ratings/ratings';
|
|||
CoreRatingRateComponent,
|
||||
CoreRatingRatingsComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
CoreRatingRatingsComponent,
|
||||
],
|
||||
})
|
||||
export class CoreRatingComponentsModule {}
|
||||
|
|
Loading…
Reference in New Issue