MOBILE-3644 glossary: Migrate edit and entry page
parent
184a7b561b
commit
8f991cecd0
|
@ -464,15 +464,6 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
*/
|
*/
|
||||||
openNewEntry(): void {
|
openNewEntry(): void {
|
||||||
this.entries.select({ newEntry: true });
|
this.entries.select({ newEntry: true });
|
||||||
// @todo
|
|
||||||
// const params = {
|
|
||||||
// courseId: this.courseId,
|
|
||||||
// module: this.module,
|
|
||||||
// glossary: this.glossary,
|
|
||||||
// entry: entry,
|
|
||||||
// };
|
|
||||||
// this.splitviewCtrl.getMasterNav().push('AddonModGlossaryEditPage', params);
|
|
||||||
// this.selectedEntry = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -537,19 +528,6 @@ class AddonModGlossaryEntriesManager extends CorePageItemsListManager<EntryItem>
|
||||||
super(pageComponent);
|
super(pageComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
getItemQueryParams(entry: EntryItem): Params {
|
|
||||||
// @todo
|
|
||||||
return {
|
|
||||||
// courseId: this.component.courseId,
|
|
||||||
// cmId: this.component.module.id,
|
|
||||||
// forumId: this.component.forum!.id,
|
|
||||||
// ...(this.isOnlineDiscussion(discussion) ? { discussion, trackPosts: this.component.trackPosts } : {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type guard to infer NewEntryForm objects.
|
* Type guard to infer NewEntryForm objects.
|
||||||
*
|
*
|
||||||
|
@ -641,6 +619,26 @@ class AddonModGlossaryEntriesManager extends CorePageItemsListManager<EntryItem>
|
||||||
return 'edit/0';
|
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';
|
export type AddonModGlossaryFetchMode = 'author_all' | 'cat_all' | 'newest_first' | 'recently_updated' | 'letter_all';
|
||||||
|
|
|
@ -18,12 +18,44 @@ import { RouterModule, Routes } from '@angular/router';
|
||||||
import { CoreSharedModule } from '@/core/shared.module';
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
import { AddonModGlossaryComponentsModule } from './components/components.module';
|
import { AddonModGlossaryComponentsModule } from './components/components.module';
|
||||||
import { AddonModGlossaryIndexPage } from './pages/index/index';
|
import { AddonModGlossaryIndexPage } from './pages/index/index';
|
||||||
|
import { conditionalRoutes } from '@/app/app-routing.module';
|
||||||
|
import { CoreScreen } from '@services/screen';
|
||||||
|
|
||||||
const routes: Routes = [
|
const mobileRoutes: Routes = [
|
||||||
{
|
{
|
||||||
path: ':courseId/:cmId',
|
path: ':courseId/:cmId',
|
||||||
component: AddonModGlossaryIndexPage,
|
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({
|
@NgModule({
|
||||||
|
|
|
@ -43,7 +43,15 @@ export const ADDON_MOD_GLOSSARY_SERVICES: Type<unknown>[] = [
|
||||||
AddonModGlossaryHelperProvider,
|
AddonModGlossaryHelperProvider,
|
||||||
];
|
];
|
||||||
|
|
||||||
const routes: Routes = [
|
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,
|
path: AddonModGlossaryModuleHandlerService.PAGE_NAME,
|
||||||
loadChildren: () => import('./glossary-lazy.module').then(m => m.AddonModGlossaryLazyModule),
|
loadChildren: () => import('./glossary-lazy.module').then(m => m.AddonModGlossaryLazyModule),
|
||||||
|
@ -52,7 +60,7 @@ const routes: Routes = [
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
CoreMainMenuTabRoutingModule.forChild(mainMenuRoutes),
|
||||||
AddonModGlossaryComponentsModule,
|
AddonModGlossaryComponentsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -59,10 +59,15 @@ export class CoreAutoRowsDirective implements AfterViewInit {
|
||||||
* Resize the textarea.
|
* Resize the textarea.
|
||||||
*/
|
*/
|
||||||
protected resize(): void {
|
protected resize(): void {
|
||||||
let nativeElement = this.element.nativeElement;
|
let nativeElement: HTMLElement = this.element.nativeElement;
|
||||||
if (nativeElement.tagName == 'ION-TEXTAREA') {
|
if (nativeElement.tagName == 'ION-TEXTAREA') {
|
||||||
// The first child of ion-textarea is the actual textarea element.
|
// Search the actual textarea.
|
||||||
nativeElement = nativeElement.firstElementChild;
|
const textarea = nativeElement.querySelector('textarea');
|
||||||
|
if (!textarea) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeElement = textarea;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set height to 1px to force scroll height to calculate correctly.
|
// Set height to 1px to force scroll height to calculate correctly.
|
||||||
|
|
Loading…
Reference in New Issue