MOBILE-2342 glossary: Implement index, entry and edit pages

main
Albert Gasset 2018-05-25 15:56:15 +02:00
parent c2da659be3
commit 8c8028e277
9 changed files with 619 additions and 0 deletions

View File

@ -0,0 +1,51 @@
<ion-header>
<ion-navbar>
<ion-title><core-format-text [text]="module.name"></core-format-text></ion-title>
<ion-buttons end>
<button ion-button (click)="save()"> {{ 'core.save' | translate }}</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-list>
<ion-item>
<ion-label stacked>{{ 'addon.mod_glossary.concept' | translate }}</ion-label>
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="entry.concept"></ion-input>
</ion-item>
<ion-item>
<ion-label stacked>{{ 'addon.mod_glossary.definition' | translate }}</ion-label>
<core-rich-text-editor item-content [control]="definitionControl" (contentChanged)="onDefinitionChange($event)" [placeholder]="'addon.mod_glossary.definition' | translate" name="addon_mod_glossary_edit"></core-rich-text-editor>
<!-- @todo: Attributes that were passed to RTE in Ionic 1 but now they aren't supported yet:
[component]="component" [componentId]="glossary.cmid" -->
</ion-item>
<ion-item *ngIf="categories.length > 0">
<ion-label 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="popover">
<ion-option *ngFor="let category of categories" [value]="category.id">{{ category.name }}</ion-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label 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"></ion-textarea>
</ion-item>
<ion-item-divider color="light">{{ 'addon.mod_glossary.attachment' | translate }}</ion-item-divider>
<core-attachments [files]="attachments" [component]="component" [componentId]="glossary.cmid" [allowOffline]="true"></core-attachments>
<ng-container *ngIf="glossary.usedynalink">
<ion-item-divider color="light">{{ 'addon.mod_glossary.linking' | translate }}</ion-item-divider>
<ion-item text-wrap>
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>
<ion-toggle [(ngModel)]="options.usedynalink"></ion-toggle>
</ion-item>
<ion-item text-wrap>
<ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label>
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.casesensitive"></ion-toggle>
</ion-item>
<ion-item text-wrap>
<ion-label>{{ 'addon.mod_glossary.fullmatch' | translate }}</ion-label>
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.fullmatch"></ion-toggle>
</ion-item>
</ng-container>
</ion-list>
</core-loading>
</ion-content>

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModGlossaryEditPage } from './edit';
@NgModule({
declarations: [
AddonModGlossaryEditPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
IonicPageModule.forChild(AddonModGlossaryEditPage),
TranslateModule.forChild()
],
})
export class AddonModGlossaryNewDiscussionPageModule {}

View File

@ -0,0 +1,253 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 } from '@angular/core';
import { FormControl } from '@angular/forms';
import { IonicPage, NavController, NavParams } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
import { AddonModGlossaryProvider } from '../../providers/glossary';
import { AddonModGlossaryOfflineProvider } from '../../providers/offline';
import { AddonModGlossaryHelperProvider } from '../../providers/helper';
/**
* Page that displays the edit form.
*/
@IonicPage({ segment: 'addon-mod-glossary-edit' })
@Component({
selector: 'page-addon-mod-glossary-edit',
templateUrl: 'edit.html',
})
export class AddonModGlossaryEditPage implements OnInit {
component = AddonModGlossaryProvider.COMPONENT;
loaded = false;
entry = {
concept: '',
definition: '',
timecreated: 0,
};
options = {
categories: [],
aliases: '',
usedynalink: false,
casesensitive: false,
fullmatch: false
};
attachments = [];
definitionControl = new FormControl();
categories = [];
protected courseId: number;
protected module: any;
protected glossary: any;
protected syncId: string;
protected syncObserver: any;
protected isDestroyed = false;
protected originalData: any;
protected saved = false;
constructor(private navParams: NavParams,
private navCtrl: NavController,
private translate: TranslateService,
private domUtils: CoreDomUtilsProvider,
private eventsProvider: CoreEventsProvider,
private sitesProvider: CoreSitesProvider,
private uploaderProvider: CoreFileUploaderProvider,
private textUtils: CoreTextUtilsProvider,
private glossaryProvider: AddonModGlossaryProvider,
private glossaryOffline: AddonModGlossaryOfflineProvider,
private glossaryHelper: AddonModGlossaryHelperProvider) {
this.courseId = navParams.get('courseId');
this.module = navParams.get('module');
this.glossary = navParams.get('glossary');
}
/**
* Component being initialized.
*/
ngOnInit(): void {
const entry = this.navParams.get('entry');
let promise;
if (entry) {
this.entry.concept = entry.concept || '';
this.entry.definition = entry.definition || '';
this.originalData = {
concept: this.entry.concept,
definition: this.entry.definition,
files: [],
};
if (entry.options) {
this.options.categories = entry.options.categories || [];
this.options.aliases = entry.options.aliases || '';
this.options.usedynalink = !!entry.options.usedynalink;
if (this.options.usedynalink) {
this.options.casesensitive = !!entry.options.casesensitive;
this.options.fullmatch = !!entry.options.fullmatch;
}
}
// Treat offline attachments if any.
if (entry.attachments && entry.attachments.offline) {
promise = this.glossaryHelper.getStoredFiles(this.glossary.id, entry.concept, entry.timecreated).then((files) => {
this.attachments = files;
this.originalData.files = files.slice();
});
}
}
this.definitionControl.setValue(this.entry.definition);
Promise.resolve(promise).then(() => {
this.glossaryProvider.getAllCategories(this.glossary.id).then((categories) => {
this.categories = categories;
}).finally(() => {
this.loaded = true;
});
});
}
/**
* Definition changed.
*
* @param {string} text The new text.
*/
onDefinitionChange(text: string): void {
this.entry.definition = text;
}
/**
* Check if we can leave the page or not.
*
* @return {boolean|Promise<void>} Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
let promise: any;
if (!this.saved && this.glossaryHelper.hasEntryDataChanged(this.entry, this.attachments, this.originalData)) {
// Show confirmation if some data has been modified.
promise = this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
} else {
promise = Promise.resolve();
}
return promise.then(() => {
// Delete the local files from the tmp folder.
this.uploaderProvider.clearTmpFiles(this.attachments);
});
}
/**
* Save the entry.
*/
save(): void {
let definition = this.entry.definition;
const timecreated = this.entry.timecreated || Date.now();
let saveOffline = false;
if (!this.entry.concept || !definition) {
this.domUtils.showErrorModal('addon.mod_glossary.fillfields', true);
return;
}
const modal = this.domUtils.showModalLoading('core.sending', true);
// Check if rich text editor is enabled or not.
this.domUtils.isRichTextEditorEnabled().then((enabled) => {
if (!enabled) {
// Rich text editor not enabled, add some HTML to the definition if needed.
definition = this.textUtils.formatHtmlLines(definition);
}
// Upload attachments first if any.
if (this.attachments.length > 0) {
return this.glossaryHelper.uploadOrStoreFiles(this.glossary.id, this.entry.concept, timecreated, this.attachments,
false).catch(() => {
// Cannot upload them in online, save them in offline.
saveOffline = true;
return this.glossaryHelper.uploadOrStoreFiles(this.glossary.id, this.entry.concept, timecreated,
this.attachments, true);
});
}
}).then((attach) => {
const options: any = {
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) {
let promise;
if (this.entry && !this.glossary.allowduplicatedentries) {
// Check if the entry is duplicated in online or offline mode.
promise = this.glossaryProvider.isConceptUsed(this.glossary.id, this.entry.concept, this.entry.timecreated)
.then((used) => {
if (used) {
// There's a entry with same name, reject with error message.
return Promise.reject(this.translate.instant('addon.mod_glossary.errconceptalreadyexists'));
}
});
} else {
promise = Promise.resolve();
}
return promise.then(() => {
// Save entry in offline.
return this.glossaryOffline.addNewEntry(this.glossary.id, this.entry.concept, definition, this.courseId,
options, attach, timecreated, undefined, undefined, this.entry).then(() => {
// Don't return anything.
});
});
} else {
// Try to send it to server.
// Don't allow offline if there are attachments since they were uploaded fine.
return this.glossaryProvider.addEntry(this.glossary.id, this.entry.concept, definition, this.courseId, options,
attach, timecreated, undefined, this.entry, !this.attachments.length, !this.glossary.allowduplicatedentries);
}
}).then((entryId) => {
if (entryId) {
// Data sent to server, delete stored files (if any).
this.glossaryHelper.deleteStoredFiles(this.glossary.id, this.entry.concept, timecreated);
}
const data = {
glossaryId: this.glossary.id,
};
this.eventsProvider.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, data, this.sitesProvider.getCurrentSiteId());
this.saved = true;
this.navCtrl.pop();
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.mod_glossary.cannoteditentry', true);
}).finally(() => {
modal.dismiss();
});
}
}

View File

@ -0,0 +1,45 @@
<ion-header>
<ion-navbar>
<ion-title *ngIf="entry"><core-format-text [text]="entry.concept"></core-format-text></ion-title>
<ion-buttons end>
<!-- The context menu will be added in here. -->
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ng-container *ngIf="entry">
<ion-item text-wrap *ngIf="showAuthor">
<ion-avatar item-start (click)="openUserProfile(post.userid)">
<img [src]="entry.userpictureurl" onError="this.src='assets/img/user-avatar.png'" core-external-content [alt]="'core.pictureof' | translate:{$a: entry.userfullname}" role="presentation">
</ion-avatar>
<h2><core-format-text [text]="entry.concept"></core-format-text></h2>
<ion-note item-end *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
<p><core-format-text [text]="entry.userfullname"></core-format-text></p>
</ion-item>
<ion-item text-wrap *ngIf="!showAuthor">
<h2><core-format-text [text]="entry.concept"></core-format-text></h2>
<ion-note item-end *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
</ion-item>
<ion-item text-wrap>
<core-format-text [component]="component" [componentId]="componentId" [text]="entry.definition"></core-format-text>
</ion-item>
<ng-container *ngIf="entry.attachment">
<core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId"></core-file>
</ng-container>
<ion-item text-wrap *ngIf="entry.approved != 1">
<p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p>
</ion-item>
</ng-container>
<ion-card *ngIf="!entry">
<ion-item class="core-error-card">
{{ 'addon.mod_glossary.errorloadingentry' | translate }}
</ion-item>
</ion-card>
</core-loading>
</ion-content>

View File

@ -0,0 +1,35 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { AddonModGlossaryEntryPage } from './entry';
@NgModule({
declarations: [
AddonModGlossaryEntryPage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
IonicPageModule.forChild(AddonModGlossaryEntryPage),
TranslateModule.forChild()
],
})
export class AddonModForumDiscussionPageModule {}

View File

@ -0,0 +1,110 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { AddonModGlossaryProvider } from '../../providers/glossary';
/**
* Page that displays a glossary entry.
*/
@IonicPage({ segment: 'addon-mod-glossary-entry' })
@Component({
selector: 'page-addon-mod-glossary-entry',
templateUrl: 'entry.html',
})
export class AddonModGlossaryEntryPage {
component = AddonModGlossaryProvider.COMPONENT;
componentId: number;
entry: any;
loaded = false;
showAuthor = false;
showDate = false;
protected courseId: number;
protected entryId: number;
constructor(navParams: NavParams,
private domUtils: CoreDomUtilsProvider,
private glossaryProvider: AddonModGlossaryProvider) {
this.courseId = navParams.get('courseId');
this.entryId = navParams.get('entryId');
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.fetchEntry().then(() => {
this.glossaryProvider.logEntryView(this.entry.id);
}).finally(() => {
this.loaded = true;
});
}
/**
* Refresh the data.
*
* @param {any} [refresher] Refresher.
* @return {Promise<any>} Promise resolved when done.
*/
doRefresh(refresher?: any): Promise<any> {
return this.glossaryProvider.invalidateEntry(this.entry.id).catch(() => {
// Ignore errors.
}).then(() => {
return this.fetchEntry(true);
}).finally(() => {
refresher && refresher.complete();
});
}
/**
* Convenience function to get the glossary entry.
*
* @param {boolean} [refresh] Whether we're refreshing data.
* @return {Promise<any>} Promise resolved when done.
*/
protected fetchEntry(refresh?: boolean): Promise<any> {
return this.glossaryProvider.getEntry(this.entryId).then((result) => {
this.entry = result;
if (!refresh) {
// Load the glossary.
return this.glossaryProvider.getGlossaryById(this.courseId, this.entry.glossaryid).then((glossary) => {
this.componentId = glossary.coursemodule;
switch (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) => {
this.domUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true);
return Promise.reject(null);
});
}
}

View File

@ -0,0 +1,11 @@
<ion-header>
<ion-navbar>
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
<ion-buttons end>
<!-- The buttons defined by the component will be added in here. -->
</ion-buttons>
</ion-navbar>
</ion-header>
<addon-mod-glossary-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-glossary-index>

View File

@ -0,0 +1,33 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreDirectivesModule } from '@directives/directives.module';
import { AddonModGlossaryComponentsModule } from '../../components/components.module';
import { AddonModGlossaryIndexPage } from './index';
@NgModule({
declarations: [
AddonModGlossaryIndexPage,
],
imports: [
CoreDirectivesModule,
AddonModGlossaryComponentsModule,
IonicPageModule.forChild(AddonModGlossaryIndexPage),
TranslateModule.forChild()
],
})
export class AddonModGlossaryIndexPageModule {}

View File

@ -0,0 +1,48 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { IonicPage, NavParams } from 'ionic-angular';
import { AddonModGlossaryIndexComponent } from '../../components/index/index';
/**
* Page that displays a glossary.
*/
@IonicPage({ segment: 'addon-mod-glossary-index' })
@Component({
selector: 'page-addon-mod-glossary-index',
templateUrl: 'index.html',
})
export class AddonModGlossaryIndexPage {
@ViewChild(AddonModGlossaryIndexComponent) glossaryComponent: AddonModGlossaryIndexComponent;
title: string;
module: any;
courseId: number;
constructor(navParams: NavParams) {
this.module = navParams.get('module') || {};
this.courseId = navParams.get('courseId');
this.title = this.module.name;
}
/**
* Update some data based on the glossary instance.
*
* @param {any} glossary Glossary instance.
*/
updateData(glossary: any): void {
this.title = glossary.name || this.title;
}
}