From bbc6fcdff5cfa32f59bc7cdf48679eafce1b787c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 12 Apr 2018 14:56:51 +0200 Subject: [PATCH 01/10] MOBILE-2338 data: Implement index page --- .../data/classes/field-plugin-component.ts | 28 + .../mod/data/components/components.module.ts | 50 ++ .../components/field-plugin/field-plugin.html | 5 + .../components/field-plugin/field-plugin.ts | 82 +++ .../mod/data/components/index/index.html | 99 +++ .../mod/data/components/index/index.scss | 3 + src/addon/mod/data/components/index/index.ts | 454 ++++++++++++++ src/addon/mod/data/data.module.ts | 63 ++ .../data/fields/checkbox/checkbox.module.ts | 49 ++ .../fields/checkbox/component/checkbox.html | 16 + .../fields/checkbox/component/checkbox.ts | 66 ++ .../data/fields/checkbox/providers/handler.ts | 165 +++++ src/addon/mod/data/fields/field.module.ts | 27 + src/addon/mod/data/lang/en.json | 3 + src/addon/mod/data/pages/index/index.html | 16 + .../mod/data/pages/index/index.module.ts | 33 + src/addon/mod/data/pages/index/index.ts | 50 ++ src/addon/mod/data/providers/data.ts | 573 ++++++++++++++++++ .../data/providers/default-field-handler.ts | 100 +++ .../mod/data/providers/fields-delegate.ts | 226 +++++++ src/addon/mod/data/providers/helper.ts | 348 +++++++++++ src/addon/mod/data/providers/link-handler.ts | 29 + .../mod/data/providers/module-handler.ts | 72 +++ src/addon/mod/data/providers/offline.ts | 195 ++++++ .../mod/data/providers/prefetch-handler.ts | 102 ++++ .../mod/data/providers/sync-cron-handler.ts | 47 ++ src/addon/mod/data/providers/sync.ts | 337 ++++++++++ src/addon/mod/feedback/providers/sync.ts | 2 +- src/app/app.module.ts | 2 + src/classes/delegate.ts | 14 + .../course/classes/main-activity-component.ts | 4 +- 31 files changed, 3258 insertions(+), 2 deletions(-) create mode 100644 src/addon/mod/data/classes/field-plugin-component.ts create mode 100644 src/addon/mod/data/components/components.module.ts create mode 100644 src/addon/mod/data/components/field-plugin/field-plugin.html create mode 100644 src/addon/mod/data/components/field-plugin/field-plugin.ts create mode 100644 src/addon/mod/data/components/index/index.html create mode 100644 src/addon/mod/data/components/index/index.scss create mode 100644 src/addon/mod/data/components/index/index.ts create mode 100644 src/addon/mod/data/data.module.ts create mode 100644 src/addon/mod/data/fields/checkbox/checkbox.module.ts create mode 100644 src/addon/mod/data/fields/checkbox/component/checkbox.html create mode 100644 src/addon/mod/data/fields/checkbox/component/checkbox.ts create mode 100644 src/addon/mod/data/fields/checkbox/providers/handler.ts create mode 100644 src/addon/mod/data/fields/field.module.ts create mode 100644 src/addon/mod/data/lang/en.json create mode 100644 src/addon/mod/data/pages/index/index.html create mode 100644 src/addon/mod/data/pages/index/index.module.ts create mode 100644 src/addon/mod/data/pages/index/index.ts create mode 100644 src/addon/mod/data/providers/data.ts create mode 100644 src/addon/mod/data/providers/default-field-handler.ts create mode 100644 src/addon/mod/data/providers/fields-delegate.ts create mode 100644 src/addon/mod/data/providers/helper.ts create mode 100644 src/addon/mod/data/providers/link-handler.ts create mode 100644 src/addon/mod/data/providers/module-handler.ts create mode 100644 src/addon/mod/data/providers/offline.ts create mode 100644 src/addon/mod/data/providers/prefetch-handler.ts create mode 100644 src/addon/mod/data/providers/sync-cron-handler.ts create mode 100644 src/addon/mod/data/providers/sync.ts diff --git a/src/addon/mod/data/classes/field-plugin-component.ts b/src/addon/mod/data/classes/field-plugin-component.ts new file mode 100644 index 000000000..dcd853b82 --- /dev/null +++ b/src/addon/mod/data/classes/field-plugin-component.ts @@ -0,0 +1,28 @@ +// (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 { Input } from '@angular/core'; + +/** + * Base class for component to render a field. + */ +export class AddonModDataFieldPluginComponent { + @Input() mode: string; // The render mode. + @Input() field: any; // The field to render. + @Input() value?: any; // The value of the field. + @Input() database?: any; // Database object. + @Input() error?: string; // Error when editing. + @Input() viewAction: string; // Action to perform. + + constructor() { } +} diff --git a/src/addon/mod/data/components/components.module.ts b/src/addon/mod/data/components/components.module.ts new file mode 100644 index 000000000..7b979b396 --- /dev/null +++ b/src/addon/mod/data/components/components.module.ts @@ -0,0 +1,50 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreCourseComponentsModule } from '@core/course/components/components.module'; +import { AddonModDataIndexComponent } from './index/index'; +import { AddonModDataFieldPluginComponent } from './field-plugin/field-plugin'; +import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module'; + +@NgModule({ + declarations: [ + AddonModDataIndexComponent, + AddonModDataFieldPluginComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreCourseComponentsModule, + CoreCompileHtmlComponentModule + ], + providers: [ + ], + exports: [ + AddonModDataIndexComponent, + AddonModDataFieldPluginComponent + ], + entryComponents: [ + AddonModDataIndexComponent + ] +}) +export class AddonModDataComponentsModule {} diff --git a/src/addon/mod/data/components/field-plugin/field-plugin.html b/src/addon/mod/data/components/field-plugin/field-plugin.html new file mode 100644 index 000000000..489bbda21 --- /dev/null +++ b/src/addon/mod/data/components/field-plugin/field-plugin.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/components/field-plugin/field-plugin.ts b/src/addon/mod/data/components/field-plugin/field-plugin.ts new file mode 100644 index 000000000..819d43e2d --- /dev/null +++ b/src/addon/mod/data/components/field-plugin/field-plugin.ts @@ -0,0 +1,82 @@ +// (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, Input, OnInit, Injector, ViewChild } from '@angular/core'; +import { AddonModDataProvider } from '../../providers/data'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; + +/** + * Component that displays an assignment feedback plugin. + */ +@Component({ + selector: 'addon-mod-data-field-plugin', + templateUrl: 'field-plugin.html', +}) +export class AddonModDataFieldPluginComponent implements OnInit { + @ViewChild(CoreDynamicComponent) dynamicComponent: CoreDynamicComponent; + + @Input() mode: string; // The render mode. + @Input() field: any; // The field to render. + @Input() value?: any; // The value of the field. + @Input() database?: any; // Database object. + @Input() error?: string; // Error when editing. + @Input() viewAction: string; // Action to perform. + + fieldComponent: any; // Component to render the plugin. + data: any; // Data to pass to the component. + fieldLoaded: boolean; + + constructor(protected injector: Injector, protected dataDelegate: AddonModDataFieldsDelegate, + protected dataProvider: AddonModDataProvider) { } + + /** + * Component being initialized. + */ + ngOnInit(): void { + console.error('HERE'); + if (!this.field) { + this.fieldLoaded = true; + + return; + } + + // Check if the plugin has defined its own component to render itself. + this.dataDelegate.getComponentForField(this.injector, this.field).then((component) => { + this.fieldComponent = component; + + if (component) { + // Prepare the data to pass to the component. + this.data = { + mode: this.mode, + field: this.field, + value: this.value, + database: this.database, + error: this.error, + viewAction: this.viewAction + }; + } else { + this.fieldLoaded = true; + } + }); + } + + /** + * Invalidate the plugin data. + * + * @return {Promise} Promise resolved when done. + */ + invalidate(): Promise { + return Promise.resolve(this.dynamicComponent && this.dynamicComponent.callComponentFunction('invalidate', [])); + } +} diff --git a/src/addon/mod/data/components/index/index.html b/src/addon/mod/data/components/index/index.html new file mode 100644 index 000000000..e53c61d7a --- /dev/null +++ b/src/addon/mod/data/components/index/index.html @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + {{ 'core.hasdatatosync' | translate: {$a: moduleName} }} +
+ + + {{ 'core.groupsseparate' | translate }} + {{ 'core.groupsvisible' | translate }} + + {{groupOpt.name}} + + + +
+ + {{ 'addon.mod_data.notopenyet' | translate:{$a: timeAvailableFromReadable} }} +
+ +
+ + {{ 'addon.mod_data.expired' | translate:{$a: timeAvailableToReadable} }} +
+ +
+ + {{ 'addon.mod_data.entrieslefttoaddtoview' | translate:{$a: {entrieslefttoview: access.entrieslefttoview} } }} +
+ +
+ + {{ 'addon.mod_data.entrieslefttoadd' | translate:{$a: {entriesleft: access.entrieslefttoadd} } }} +
+ + + {{ 'addon.mod_data.resetsettings' | translate}} + + +
+ + + +
+ + + + + + + + + + + + + +
+ +
+
+ + + {{ 'addon.mod_data.resetsettings' | translate}} + + + +
diff --git a/src/addon/mod/data/components/index/index.scss b/src/addon/mod/data/components/index/index.scss new file mode 100644 index 000000000..1a748d124 --- /dev/null +++ b/src/addon/mod/data/components/index/index.scss @@ -0,0 +1,3 @@ +addon-mod-data-index { + +} \ No newline at end of file diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts new file mode 100644 index 000000000..713731112 --- /dev/null +++ b/src/addon/mod/data/components/index/index.ts @@ -0,0 +1,454 @@ +// (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, Optional, Injector } from '@angular/core'; +import { Content, ModalController } from 'ionic-angular'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; +import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; +import { CoreCommentsProvider } from '@core/comments/providers/comments'; +import { AddonModDataProvider } from '../../providers/data'; +import { AddonModDataHelperProvider } from '../../providers/helper'; +import { AddonModDataOfflineProvider } from '../../providers/offline'; +import { AddonModDataSyncProvider } from '../../providers/sync'; +import * as moment from 'moment'; + +/** + * Component that displays a data index page. + */ +@Component({ + selector: 'addon-mod-data-index', + templateUrl: 'index.html', +}) +export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComponent { + component = AddonModDataProvider.COMPONENT; + moduleName = 'data'; + + access: any = {}; + data: any = {}; + fields: any; + selectedGroup: number; + advancedSearch: any; + timeAvailableFrom: number | boolean; + timeAvailableFromReadable: string | boolean; + timeAvailableTo: number | boolean; + timeAvailableToReadable: string | boolean; + isEmpty = false; + groupInfo: CoreGroupInfo; + entries = {}; + firstEntry = false; + canAdd = false; + canSearch = false; + search = { + sortBy: '0', + sortDirection: 'DESC', + page: 0, + text: '', + searching: false, + searchingAdvanced: false, + advanced: [] + }; + hasNextPage = false; + offlineActions: any; + offlineEntries: any; + entriesRendered = ''; + cssTemplate = ''; + + protected syncEventName = AddonModDataSyncProvider.AUTO_SYNCED; + protected entryChangedObserver: any; + protected hasComments = false; + + constructor(injector: Injector, private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider, + private dataOffline: AddonModDataOfflineProvider, @Optional() private content: Content, + private dataSync: AddonModDataSyncProvider, private timeUtils: CoreTimeUtilsProvider, + private groupsProvider: CoreGroupsProvider, private commentsProvider: CoreCommentsProvider, + private modalCtrl: ModalController, private utils: CoreUtilsProvider) { + super(injector); + + // Refresh entries on change. + this.entryChangedObserver = this.eventsProvider.on(AddonModDataProvider.ENTRY_CHANGED, (eventData) => { + if (this.data.id == eventData.dataId) { + this.loaded = false; + + return this.loadContent(true); + } + }, this.siteId); + } + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + + this.selectedGroup = this.group || 0; + + this.loadContent(false, true).then(() => { + this.dataProvider.logView(this.data.id).then(() => { + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + }); + }); + + // Setup search modal. + } + + /** + * Perform the invalidate content function. + * + * @return {Promise} Resolved when done. + */ + protected invalidateContent(): Promise { + const promises = []; + + promises.push(this.dataProvider.invalidateDatabaseData(this.courseId)); + if (this.data) { + promises.push(this.dataProvider.invalidateDatabaseAccessInformationData(this.data.id)); + promises.push(this.groupsProvider.invalidateActivityGroupInfo(this.data.coursemodule)); + promises.push(this.dataProvider.invalidateEntriesData(this.data.id)); + if (this.hasComments) { + promises.push(this.commentsProvider.invalidateCommentsByInstance('module', this.data.coursemodule)); + } + } + + return Promise.all(promises); + } + + /** + * Compares sync event data with current data to check if refresh content is needed. + * + * @param {any} syncEventData Data receiven on sync observer. + * @return {boolean} True if refresh is needed, false otherwise. + */ + protected isRefreshSyncNeeded(syncEventData: any): boolean { + if (this.data && syncEventData.dataId == this.data.id && typeof syncEventData.entryId == 'undefined') { + this.loaded = false; + // Refresh the data. + this.content.scrollToTop(); + + return true; + } + + return false; + } + + /** + * Download data contents. + * + * @param {boolean} [refresh=false] If it's refreshing content. + * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [showErrors=false] If show errors to the user of hide them. + * @return {Promise} Promise resolved when done. + */ + protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + let canAdd = false, + canSearch = false; + + return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { + this.data = data; + + this.description = data.intro || data.description; + this.dataRetrieved.emit(data); + + if (sync) { + // Try to synchronize the data. + return this.syncActivity(showErrors).catch(() => { + // Ignore errors. + }); + } + }).then(() => { + return this.dataProvider.getDatabaseAccessInformation(this.data.id); + }).then((accessData) => { + this.access = accessData; + + if (!accessData.timeavailable) { + const time = this.timeUtils.timestamp(); + + this.timeAvailableFrom = this.data.timeavailablefrom && time < this.data.timeavailablefrom ? + parseInt(this.data.timeavailablefrom, 10) * 1000 : false; + this.timeAvailableFromReadable = this.timeAvailableFrom ? + moment(this.timeAvailableFrom).format('LLL') : false; + this.timeAvailableTo = this.data.timeavailableto && time > this.data.timeavailableto ? + parseInt(this.data.timeavailableto, 10) * 1000 : false; + this.timeAvailableToReadable = this.timeAvailableTo ? moment(this.timeAvailableTo).format('LLL') : false; + + this.isEmpty = true; + this.groupInfo = null; + + return; + } + + canSearch = true; + canAdd = accessData.canaddentry; + + return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule, accessData.canmanageentries) + .then((groupInfo) => { + this.groupInfo = groupInfo; + + // Check selected group is accessible. + if (groupInfo && groupInfo.groups && groupInfo.groups.length > 0) { + if (!groupInfo.groups.some((group) => this.selectedGroup == group.id)) { + this.selectedGroup = groupInfo.groups[0].id; + } + } + + return this.fetchOfflineEntries(); + }); + }).then(() => { + return this.dataProvider.getFields(this.data.id).then((fields) => { + if (fields.length == 0) { + canSearch = false; + canAdd = false; + } + this.search.advanced = []; + + this.fields = {}; + fields.forEach((field) => { + this.fields[field.id] = field; + }); + this.fields = this.utils.objectToArray(this.fields); + this.advancedSearch = this.dataHelper.displayAdvancedSearchFields(this.data.asearchtemplate, this.fields); + + return this.fetchEntriesData(); + }); + }).then(() => { + // All data obtained, now fill the context menu. + this.fillContextMenu(refresh); + }).finally(() => { + this.canAdd = canAdd; + this.canSearch = canSearch; + }); + } + + /** + * Fetch current database entries. + * + * @return {Promise} Resolved then done. + */ + protected fetchEntriesData(): Promise { + this.hasComments = false; + + return this.dataProvider.getDatabaseAccessInformation(this.data.id, this.selectedGroup).then((accessData) => { + // Update values for current group. + this.access.canaddentry = accessData.canaddentry; + + if (this.search.searching) { + const text = this.search.searchingAdvanced ? undefined : this.search.text, + advanced = this.search.searchingAdvanced ? this.search.advanced : undefined; + + return this.dataProvider.searchEntries(this.data.id, this.selectedGroup, text, advanced, this.search.sortBy, + this.search.sortDirection, this.search.page); + } else { + return this.dataProvider.getEntries(this.data.id, this.selectedGroup, this.search.sortBy, this.search.sortDirection, + this.search.page); + } + }).then((entries) => { + const numEntries = (entries && entries.entries && entries.entries.length) || 0; + this.isEmpty = !numEntries && !Object.keys(this.offlineActions).length && !Object.keys(this.offlineEntries).length; + this.hasNextPage = numEntries >= AddonModDataProvider.PER_PAGE && ((this.search.page + 1) * + AddonModDataProvider.PER_PAGE) < entries.totalcount; + this.entriesRendered = ''; + + if (!this.isEmpty) { + this.cssTemplate = this.dataHelper.prefixCSS(this.data.csstemplate, '.addon-data-entries-' + this.data.id); + + const siteInfo = this.sitesProvider.getCurrentSite().getInfo(), + promises = []; + + this.utils.objectToArray(this.offlineEntries).forEach((offlineActions) => { + const offlineEntry = offlineActions.find((offlineEntry) => offlineEntry.action == 'add'); + + if (offlineEntry) { + const entry = { + id: offlineEntry.entryid, + canmanageentry: true, + approved: !this.data.approval || this.data.manageapproved, + dataid: offlineEntry.dataid, + groupid: offlineEntry.groupid, + timecreated: -offlineEntry.entryid, + timemodified: -offlineEntry.entryid, + userid: siteInfo.userid, + fullname: siteInfo.fullname, + contents: {} + }; + + if (offlineActions.length > 0) { + promises.push(this.dataHelper.applyOfflineActions(entry, offlineActions, this.fields)); + } else { + promises.push(Promise.resolve(entry)); + } + } + }); + + entries.entries.forEach((entry) => { + // Index contents by fieldid. + const contents = {}; + entry.contents.forEach((field) => { + contents[field.fieldid] = field; + }); + entry.contents = contents; + + if (typeof this.offlineActions[entry.id] != 'undefined') { + promises.push(this.dataHelper.applyOfflineActions(entry, this.offlineActions[entry.id], this.fields)); + } else { + promises.push(Promise.resolve(entry)); + } + }); + + return Promise.all(promises).then((entries) => { + let entriesHTML = this.data.listtemplateheader || ''; + + // Get first entry from the whole list. + if (entries && entries[0] && (!this.search.searching || !this.firstEntry)) { + this.firstEntry = entries[0].id; + } + + entries.forEach((entry) => { + this.entries[entry.id] = entry; + + const actions = this.dataHelper.getActions(this.data, this.access, entry); + + entriesHTML += this.dataHelper.displayShowFields(this.data.listtemplate, this.fields, entry, 'list', + actions); + }); + entriesHTML += this.data.listtemplatefooter || ''; + + this.entriesRendered = entriesHTML; + }); + } else if (!this.search.searching) { + // Empty and no searching. + this.canSearch = false; + } + this.firstEntry = false; + }); + } + + /** + * Display the chat users modal. + */ + showSearch(): void { + const modal = this.modalCtrl.create('AddonModDataSearchPage'); + modal.onDidDismiss((data) => { + // @TODO. + }); + modal.present(); + } + + /** + * Performs the search and closes the modal. + * + * @param {number} page Page number. + * @return {Promise} Resolved when done. + */ + searchEntries(page: number): Promise { + this.loaded = false; + this.search.page = page; + + if (this.search.searchingAdvanced) { + this.search.advanced = this.dataHelper.getSearchDataFromForm(document.forms['addon-mod_data-advanced-search-form'], + this.fields); + this.search.searching = this.search.advanced.length > 0; + } else { + this.search.searching = this.search.text.length > 0; + } + + return this.fetchEntriesData().catch((message) => { + this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Reset all search filters and closes the modal. + */ + searchReset(): void { + this.search.sortBy = '0'; + this.search.sortDirection = 'DESC'; + this.search.text = ''; + this.search.advanced = []; + this.search.searchingAdvanced = false; + this.search.searching = false; + this.searchEntries(0); + } + + // Set group to see the database. + setGroup(groupId: number): Promise { + this.selectedGroup = groupId; + + return this.fetchEntriesData().catch((message) => { + this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); + + return Promise.reject(null); + }); + } + + /** + * Fetch offline entries. + * + * @return {Promise} Resolved then done. + */ + protected fetchOfflineEntries(): Promise { + // Check if there are entries stored in offline. + return this.dataOffline.getDatabaseEntries(this.data.id).then((offlineEntries) => { + this.hasOffline = !!offlineEntries.length; + + this.offlineActions = {}; + this.offlineEntries = {}; + + // Only show offline entries on first page. + if (this.search.page == 0 && this.hasOffline) { + offlineEntries.forEach((entry) => { + if (entry.entryid > 0) { + if (typeof this.offlineActions[entry.entryid] == 'undefined') { + this.offlineActions[entry.entryid] = []; + } + this.offlineActions[entry.entryid].push(entry); + } else { + if (typeof this.offlineActions[entry.entryid] == 'undefined') { + this.offlineEntries[entry.entryid] = []; + } + this.offlineEntries[entry.entryid].push(entry); + } + }); + } + }); + } + + /** + * Performs the sync of the activity. + * + * @return {Promise} Promise resolved when done. + */ + protected sync(): Promise { + return this.dataSync.syncDatabase(this.data.id); + } + + /** + * Checks if sync has succeed from result sync data. + * + * @param {any} result Data returned on the sync function. + * @return {boolean} If suceed or not. + */ + protected hasSyncSucceed(result: any): boolean { + return result.updated; + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + this.entryChangedObserver && this.entryChangedObserver.off(); + } +} diff --git a/src/addon/mod/data/data.module.ts b/src/addon/mod/data/data.module.ts new file mode 100644 index 000000000..12f6eb73b --- /dev/null +++ b/src/addon/mod/data/data.module.ts @@ -0,0 +1,63 @@ +// (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 { CoreCronDelegate } from '@providers/cron'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; +import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { AddonModDataComponentsModule } from './components/components.module'; +import { AddonModDataModuleHandler } from './providers/module-handler'; +import { AddonModDataProvider } from './providers/data'; +import { AddonModDataLinkHandler } from './providers/link-handler'; +import { AddonModDataHelperProvider } from './providers/helper'; +import { AddonModDataPrefetchHandler } from './providers/prefetch-handler'; +import { AddonModDataSyncProvider } from './providers/sync'; +import { AddonModDataSyncCronHandler } from './providers/sync-cron-handler'; +import { AddonModDataOfflineProvider } from './providers/offline'; +import { AddonModDataFieldsDelegate } from './providers/fields-delegate'; +import { AddonModDataDefaultFieldHandler } from './providers/default-field-handler'; +import { AddonModDataFieldModule } from './fields/field.module'; + +@NgModule({ + declarations: [ + ], + imports: [ + AddonModDataComponentsModule, + AddonModDataFieldModule + ], + providers: [ + AddonModDataProvider, + AddonModDataModuleHandler, + AddonModDataPrefetchHandler, + AddonModDataHelperProvider, + AddonModDataLinkHandler, + AddonModDataSyncCronHandler, + AddonModDataSyncProvider, + AddonModDataOfflineProvider, + AddonModDataFieldsDelegate, + AddonModDataDefaultFieldHandler + ] +}) +export class AddonModDataModule { + constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModDataModuleHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModDataPrefetchHandler, + contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModDataLinkHandler, + cronDelegate: CoreCronDelegate, syncHandler: AddonModDataSyncCronHandler) { + moduleDelegate.registerHandler(moduleHandler); + prefetchDelegate.registerHandler(prefetchHandler); + contentLinksDelegate.registerHandler(linkHandler); + cronDelegate.register(syncHandler); + } +} diff --git a/src/addon/mod/data/fields/checkbox/checkbox.module.ts b/src/addon/mod/data/fields/checkbox/checkbox.module.ts new file mode 100644 index 000000000..5ac3377f2 --- /dev/null +++ b/src/addon/mod/data/fields/checkbox/checkbox.module.ts @@ -0,0 +1,49 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonModDataFieldCheckboxHandler } from './providers/handler'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { AddonModDataFieldCheckboxComponent } from './component/checkbox'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonModDataFieldCheckboxComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModDataFieldCheckboxHandler + ], + exports: [ + AddonModDataFieldCheckboxComponent + ], + entryComponents: [ + AddonModDataFieldCheckboxComponent + ] +}) +export class AddonModDataFieldCheckboxModule { + constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldCheckboxHandler) { + fieldDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/data/fields/checkbox/component/checkbox.html b/src/addon/mod/data/fields/checkbox/component/checkbox.html new file mode 100644 index 000000000..cc37e6f1e --- /dev/null +++ b/src/addon/mod/data/fields/checkbox/component/checkbox.html @@ -0,0 +1,16 @@ + + + + {{ option }} + + + + + + + {{ 'addon.mod_data.selectedrequired' | translate }} + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/checkbox/component/checkbox.ts b/src/addon/mod/data/fields/checkbox/component/checkbox.ts new file mode 100644 index 000000000..64f4db6e2 --- /dev/null +++ b/src/addon/mod/data/fields/checkbox/component/checkbox.ts @@ -0,0 +1,66 @@ +// (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, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; + +/** + * Component to render data checkbox field. + */ +@Component({ + selector: 'addon-mod-data-field-checkbox', + templateUrl: 'checkbox.html' +}) +export class AddonModDataFieldCheckboxComponent extends AddonModDataFieldPluginComponent implements OnInit { + + control: FormControl; + options: number; + values = {}; + + constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, + element: ElementRef) { + + super(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.mode = this.mode == 'list' ? 'show' : this.mode; + this.render(); + } + + protected render(): void { + if (this.mode == 'show') { + this.value.content.split('##').join('
'); + + return; + } + + this.options = this.field.param1.split('\n'); + + if (this.mode == 'edit' && this.value) { + this.values = {}; + + this.value.content.split('##').forEach((value) => { + this.values[value] = true; + }); + + //this.control = this.fb.control(text); + } + } +} diff --git a/src/addon/mod/data/fields/checkbox/providers/handler.ts b/src/addon/mod/data/fields/checkbox/providers/handler.ts new file mode 100644 index 000000000..d49a0a3c0 --- /dev/null +++ b/src/addon/mod/data/fields/checkbox/providers/handler.ts @@ -0,0 +1,165 @@ +// (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 { Injector, Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonModDataFieldHandler } from '../../../providers/fields-delegate'; +import { AddonModDataFieldCheckboxComponent } from '../component/checkbox'; + +/** + * Handler for checkbox data field plugin. + */ +@Injectable() +export class AddonModDataFieldCheckboxHandler implements AddonModDataFieldHandler { + name = 'AddonModDataFieldCheckboxHandler'; + type = 'checkbox'; + + constructor(private translate: TranslateService) { } + + /** + * Return the Component to use to display the plugin data. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} field The field object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + console.error(AddonModDataFieldCheckboxComponent); + return AddonModDataFieldCheckboxComponent; + } + + /** + * Get field search data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @return {any} With name and value of the data to be sent. + */ + getFieldSearchData(field: any, inputData: any): any { + const fieldName = 'f_' + field.id, + reqName = 'f_' + field.id + '_allreq'; + + const checkboxes = [], + values = []; + inputData[fieldName].forEach((value, option) => { + if (value) { + checkboxes.push(option); + } + }); + if (checkboxes.length > 0) { + values.push({ + name: fieldName, + value: checkboxes + }); + + if (inputData[reqName]['1']) { + values.push({ + name: reqName, + value: true + }); + } + + return values; + } + + return false; + } + + /** + * Get field edit data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditData(field: any, inputData: any, originalFieldData: any): any { + const fieldName = 'f_' + field.id; + + const checkboxes = []; + inputData[fieldName].forEach((value, option) => { + if (value) { + checkboxes.push(option); + } + }); + if (checkboxes.length > 0) { + return [{ + fieldid: field.id, + value: checkboxes + }]; + } + + return false; + } + + /** + * Get field data in changed. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @param {any} originalFieldData Original field entered data. + * @return {Promise | boolean} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | boolean { + const fieldName = 'f_' + field.id, + checkboxes = []; + + inputData[fieldName].forEach((value, option) => { + if (value) { + checkboxes.push(option); + } + }); + + originalFieldData = (originalFieldData && originalFieldData.content) || ''; + + return checkboxes.join('##') != originalFieldData; + } + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string | false} String with the notification or false. + */ + getFieldsNotifications(field: any, inputData: any): string | false { + if (field.required && (!inputData || !inputData.length || !inputData[0].value)) { + return this.translate.instant('addon.mod_data.errormustsupplyvalue'); + } + + return false; + } + + /** + * Override field content data with offline submission. + * + * @param {any} originalContent Original data to be overriden. + * @param {any} offlineContent Array with all the offline data to override. + * @param {any} [offlineFiles] Array with all the offline files in the field. + * @return {any} Data overriden + */ + overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any { + originalContent.content = (offlineContent[''] && offlineContent[''].join('##')) || ''; + + return originalContent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/field.module.ts b/src/addon/mod/data/fields/field.module.ts new file mode 100644 index 000000000..c38ed2f56 --- /dev/null +++ b/src/addon/mod/data/fields/field.module.ts @@ -0,0 +1,27 @@ +// (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 { AddonModDataFieldCheckboxModule } from './checkbox/checkbox.module'; + +@NgModule({ + declarations: [], + imports: [ + AddonModDataFieldCheckboxModule + ], + providers: [ + ], + exports: [] +}) +export class AddonModDataFieldModule { } diff --git a/src/addon/mod/data/lang/en.json b/src/addon/mod/data/lang/en.json new file mode 100644 index 000000000..0e0dcd235 --- /dev/null +++ b/src/addon/mod/data/lang/en.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/src/addon/mod/data/pages/index/index.html b/src/addon/mod/data/pages/index/index.html new file mode 100644 index 000000000..ff39e903d --- /dev/null +++ b/src/addon/mod/data/pages/index/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/addon/mod/data/pages/index/index.module.ts b/src/addon/mod/data/pages/index/index.module.ts new file mode 100644 index 000000000..0215fd3f4 --- /dev/null +++ b/src/addon/mod/data/pages/index/index.module.ts @@ -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 { AddonModDataComponentsModule } from '../../components/components.module'; +import { AddonModDataIndexPage } from './index'; + +@NgModule({ + declarations: [ + AddonModDataIndexPage, + ], + imports: [ + CoreDirectivesModule, + AddonModDataComponentsModule, + IonicPageModule.forChild(AddonModDataIndexPage), + TranslateModule.forChild() + ], +}) +export class AddonModDataIndexPageModule {} diff --git a/src/addon/mod/data/pages/index/index.ts b/src/addon/mod/data/pages/index/index.ts new file mode 100644 index 000000000..f415352a4 --- /dev/null +++ b/src/addon/mod/data/pages/index/index.ts @@ -0,0 +1,50 @@ +// (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 { AddonModDataIndexComponent } from '../../components/index/index'; + +/** + * Page that displays a data. + */ +@IonicPage({ segment: 'addon-mod-data-index' }) +@Component({ + selector: 'page-addon-mod-data-index', + templateUrl: 'index.html', +}) +export class AddonModDataIndexPage { + @ViewChild(AddonModDataIndexComponent) dataComponent: AddonModDataIndexComponent; + + title: string; + module: any; + courseId: number; + group: number; + + constructor(navParams: NavParams) { + this.module = navParams.get('module') || {}; + this.courseId = navParams.get('courseId'); + this.group = navParams.get('group') || 0; + this.title = this.module.name; + } + + /** + * Update some data based on the data instance. + * + * @param {any} data Data instance. + */ + updateData(data: any): void { + this.title = data.name || this.title; + } +} diff --git a/src/addon/mod/data/providers/data.ts b/src/addon/mod/data/providers/data.ts new file mode 100644 index 000000000..5ffa8fdaa --- /dev/null +++ b/src/addon/mod/data/providers/data.ts @@ -0,0 +1,573 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { AddonModDataOfflineProvider } from './offline'; + +/** + * Service that provides some features for databases. + */ +@Injectable() +export class AddonModDataProvider { + static COMPONENT = 'mmaModData'; + static PER_PAGE = 25; + static ENTRY_CHANGED = 'addon_mod_data_entry_changed'; + + protected ROOT_CACHE_KEY = AddonModDataProvider.COMPONENT + ':'; + protected logger; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, + private filepoolProvider: CoreFilepoolProvider, private dataOffline: AddonModDataOfflineProvider) { + this.logger = logger.getInstance('AddonModDataProvider'); + } + + /** + * Adds a new entry to a database. It does not cache calls. It will fail if offline or cannot connect. + * + * @param {number} dataId Database ID. + * @param {any} data The fields data to be created. + * @param {number} [groupId] Group id, 0 means that the function will determine the user group. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the action is done. + */ + addEntryOnline(dataId: number, data: any, groupId?: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + databaseid: dataId, + data: data + }; + + if (typeof groupId !== 'undefined') { + params['groupid'] = groupId; + } + + return site.write('mod_data_add_entry', params); + }); + } + + /** + * Approves or unapproves an entry. It does not cache calls. It will fail if offline or cannot connect. + * + * @param {number} entryId Entry ID. + * @param {boolean} approve Whether to approve (true) or unapprove the entry. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the action is done. + */ + approveEntryOnline(entryId: number, approve: boolean, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + entryid: entryId, + approve: approve ? 1 : 0 + }; + + return site.write('mod_data_approve_entry', params); + }); + } + + /** + * Deletes an entry. It does not cache calls. It will fail if offline or cannot connect. + * + * @param {number} entryId Entry ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the action is done. + */ + deleteEntryOnline(entryId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + entryid: entryId + }; + + return site.write('mod_data_delete_entry', params); + }); + } + + /** + * Updates an existing entry. It does not cache calls. It will fail if offline or cannot connect. + * + * @param {number} entryId Entry ID. + * @param {any} data The fields data to be updated. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the action is done. + */ + editEntryOnline(entryId: number, data: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + entryid: entryId, + data: data + }; + + return site.write('mod_data_update_entry', params); + }); + } + + /** + * Get cache key for data data WS calls. + * + * @param {number} courseId Course ID. + * @return {string} Cache key. + */ + protected getDatabaseDataCacheKey(courseId: number): string { + return this.ROOT_CACHE_KEY + 'data:' + courseId; + } + + /** + * Get prefix cache key for all database activity data WS calls. + * + * @param {number} dataId Data ID. + * @return {string} Cache key. + */ + protected getDatabaseDataPrefixCacheKey(dataId: number): string { + return this.ROOT_CACHE_KEY + dataId; + } + + /** + * Get a database data. If more than one is found, only the first will be returned. + * + * @param {number} courseId Course ID. + * @param {string} key Name of the property to check. + * @param {any} value Value to search. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @return {Promise} Promise resolved when the data is retrieved. + */ + protected getDatabaseByKey(courseId: number, key: string, value: any, siteId?: string, forceCache: boolean = false): + Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + courseids: [courseId] + }, + preSets = { + cacheKey: this.getDatabaseDataCacheKey(courseId) + }; + if (forceCache) { + preSets['omitExpires'] = true; + } + + return site.read('mod_data_get_databases_by_courses', params, preSets).then((response) => { + if (response && response.databases) { + const currentData = response.databases.find((data) => data[key] == value); + if (currentData) { + return currentData; + } + } + + return Promise.reject(null); + }); + }); + } + + /** + * Get a data by course module ID. + * + * @param {number} courseId Course ID. + * @param {number} cmId Course module ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @return {Promise} Promise resolved when the data is retrieved. + */ + getDatabase(courseId: number, cmId: number, siteId?: string, forceCache: boolean = false): Promise { + return this.getDatabaseByKey(courseId, 'coursemodule', cmId, siteId, forceCache); + } + + /** + * Get a data by ID. + * + * @param {number} courseId Course ID. + * @param {number} id Data ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @return {Promise} Promise resolved when the data is retrieved. + */ + getDatabaseById(courseId: number, id: number, siteId?: string, forceCache: boolean = false): Promise { + return this.getDatabaseByKey(courseId, 'id', id, siteId, forceCache); + } + + /** + * Get prefix cache key for all database access information data WS calls. + * + * @param {number} dataId Data ID. + * @return {string} Cache key. + */ + protected getDatabaseAccessInformationDataPrefixCacheKey(dataId: number): string { + return this.getDatabaseDataPrefixCacheKey(dataId) + ':access:'; + } + + /** + * Get cache key for database access information data WS calls. + * + * @param {number} dataId Data ID. + * @param {number} [groupId=0] Group ID. + * @return {string} Cache key. + */ + protected getDatabaseAccessInformationDataCacheKey(dataId: number, groupId: number = 0): string { + return this.getDatabaseAccessInformationDataPrefixCacheKey(dataId) + groupId; + } + + /** + * Get access information for a given database. + * + * @param {number} dataId Data ID. + * @param {number} [groupId] Group ID. + * @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it'll always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the database is retrieved. + */ + getDatabaseAccessInformation(dataId: number, groupId?: number, offline: boolean = false, ignoreCache: boolean = false, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + databaseid: dataId + }, + preSets = { + cacheKey: this.getDatabaseAccessInformationDataCacheKey(dataId, groupId) + }; + + if (typeof groupId !== 'undefined') { + params['groupid'] = groupId; + } + + if (offline) { + preSets['omitExpires'] = true; + } else if (ignoreCache) { + preSets['getFromCache'] = false; + preSets['emergencyCache'] = false; + } + + return site.read('mod_data_get_data_access_information', params, preSets); + }); + } + + /** + * Get entries for a specific database and group. + * + * @param {number} dataId Data ID. + * @param {number} [groupId=0] Group ID. + * @param {string} [sort=0] Sort the records by this field id, reserved ids are: + * 0: timeadded + * -1: firstname + * -2: lastname + * -3: approved + * -4: timemodified. + * Empty for using the default database setting. + * @param {string} [order=DESC] The direction of the sorting: 'ASC' or 'DESC'. + * Empty for using the default database setting. + * @param {number} [page=0] Page of records to return. + * @param {number} [perPage=PER_PAGE] Records per page to return. Default on PER_PAGE. + * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it'll always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the database is retrieved. + */ + getEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC', page: number = 0, + perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false, + siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + // Always use sort and order params to improve cache usage (entries are identified by params). + const params = { + databaseid: dataId, + returncontents: 1, + page: page, + perpage: perPage, + groupid: groupId, + sort: sort, + order: order + }, + preSets = { + cacheKey: this.getEntriesCacheKey(dataId, groupId) + }; + + if (forceCache) { + preSets['omitExpires'] = true; + } else if (ignoreCache) { + preSets['getFromCache'] = false; + preSets['emergencyCache'] = false; + } + + return site.read('mod_data_get_entries', params, preSets); + }); + } + + /** + * Get cache key for database entries data WS calls. + * + * @param {number} dataId Data ID. + * @param {number} [groupId=0] Group ID. + * @return {string} Cache key. + */ + protected getEntriesCacheKey(dataId: number, groupId: number = 0): string { + return this.getEntriesPrefixCacheKey(dataId) + groupId; + } + + /** + * Get prefix cache key for database all entries data WS calls. + * + * @param {number} dataId Data ID. + * @return {string} Cache key. + */ + protected getEntriesPrefixCacheKey(dataId: number): string { + return this.getDatabaseDataPrefixCacheKey(dataId) + ':entries:'; + } + + /** + * Get an entry of the database activity. + * + * @param {number} dataId Data ID for caching purposes. + * @param {number} entryId Entry ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the database entry is retrieved. + */ + getEntry(dataId: number, entryId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + entryid: entryId, + returncontents: 1 + }, + preSets = { + cacheKey: this.getEntryCacheKey(dataId, entryId) + }; + + return site.read('mod_data_get_entry', params, preSets); + }); + } + + /** + * Get cache key for database entry data WS calls. + * + * @param {number} dataId Data ID for caching purposes. + * @param {number} entryId Entry ID. + * @return {string} Cache key. + */ + protected getEntryCacheKey(dataId: number, entryId: number): string { + return this.getDatabaseDataPrefixCacheKey(dataId) + ':entry:' + entryId; + } + + /** + * Get the list of configured fields for the given database. + * + * @param {number} dataId Data ID. + * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the fields are retrieved. + */ + getFields(dataId: number, forceCache: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + databaseid: dataId + }, + preSets = { + cacheKey: this.getFieldsCacheKey(dataId) + }; + + if (forceCache) { + preSets['omitExpires'] = true; + } else if (ignoreCache) { + preSets['getFromCache'] = false; + preSets['emergencyCache'] = false; + } + + return site.read('mod_data_get_fields', params, preSets).then((response) => { + if (response && response.fields) { + return response.fields; + } + + return Promise.reject(null); + }); + }); + } + + /** + * Get cache key for database fields data WS calls. + * + * @param {number} dataId Data ID. + * @return {string} Cache key. + */ + protected getFieldsCacheKey(dataId: number): string { + return this.getDatabaseDataPrefixCacheKey(dataId) + ':fields'; + } + + /** + * Invalidate the prefetched content. + * To invalidate files, use AddonDataProvider#invalidateFiles. + * + * @param {number} moduleId The module ID. + * @param {number} courseId Course ID of the module. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const promises = []; + + promises.push(this.getDatabase(courseId, moduleId).then((data) => { + const ps = []; + + // Do not invalidate module data before getting module info, we need it! + ps.push(this.invalidateDatabaseData(courseId, siteId)); + ps.push(this.invalidateDatabaseWSData(data.id, siteId)); + + return Promise.all(ps); + })); + + promises.push(this.invalidateFiles(moduleId, siteId)); + + return this.utils.allPromises(promises); + } + + /** + * Invalidates database access information data. + * + * @param {number} dataId Data ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateDatabaseAccessInformationData(dataId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getDatabaseAccessInformationDataPrefixCacheKey(dataId)); + }); + } + + /** + * Invalidates database entries data. + * + * @param {number} dataId Data ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateEntriesData(dataId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getEntriesPrefixCacheKey(dataId)); + }); + } + + /** + * Invalidate the prefetched files. + * + * @param {number} moduleId The module ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the files are invalidated. + */ + invalidateFiles(moduleId: number, siteId?: string): Promise { + return this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModDataProvider.COMPONENT, moduleId); + } + + /** + * Invalidates database data. + * + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateDatabaseData(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getDatabaseDataCacheKey(courseId)); + }); + } + + /** + * Invalidates database data except files and module info. + * + * @param {number} databaseId Data ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateDatabaseWSData(databaseId: number, siteId: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKeyStartingWith(this.getDatabaseDataPrefixCacheKey(databaseId)); + }); + } + + /** + * Return whether or not the plugin is enabled in a certain site. Plugin is enabled if the database WS are available. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. + * @since 3.3 + */ + isPluginEnabled(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.wsAvailable('mod_data_get_data_access_information'); + }); + } + + /** + * Report the database as being viewed. + * + * @param {number} id Module ID. + * @return {Promise} Promise resolved when the WS call is successful. + */ + logView(id: number): Promise { + const params = { + databaseid: id + }; + + return this.sitesProvider.getCurrentSite().write('mod_data_view_database', params); + } + + /** + * Performs search over a database. + * + * @param {number} dataId The data instance id. + * @param {number} [groupId=0] Group id, 0 means that the function will determine the user group. + * @param {string} [search] Search text. It will be used if advSearch is not defined. + * @param {any} [advSearch] Advanced search data. + * @param {string} [sort] Sort by this field. + * @param {string} [order] The direction of the sorting. + * @param {number} [page=0] Page of records to return. + * @param {number} [perPage=PER_PAGE] Records per page to return. Default on AddonModDataProvider.PER_PAGE. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the action is done. + */ + searchEntries(dataId: number, groupId: number = 0, search?: string, advSearch?: any, sort?: string, order?: string, + page: number = 0, perPage: number = AddonModDataProvider.PER_PAGE, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + databaseid: dataId, + groupid: groupId, + returncontents: 1, + page: page, + perpage: perPage + }, + preSets = { + getFromCache: false, + saveToCache: true, + emergencyCache: true + }; + + if (typeof sort != 'undefined') { + params['sort'] = sort; + } + + if (typeof order !== 'undefined') { + params['order'] = order; + } + + if (typeof search !== 'undefined') { + params['search'] = search; + } + + if (typeof advSearch !== 'undefined') { + params['advsearch'] = advSearch; + } + + return site.read('mod_data_search_entries', params, preSets); + }); + } +} diff --git a/src/addon/mod/data/providers/default-field-handler.ts b/src/addon/mod/data/providers/default-field-handler.ts new file mode 100644 index 000000000..2160d8a9e --- /dev/null +++ b/src/addon/mod/data/providers/default-field-handler.ts @@ -0,0 +1,100 @@ +// (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 { Injectable } from '@angular/core'; +import { AddonModDataFieldHandler } from './fields-delegate'; + +/** + * Default handler used when a field plugin doesn't have a specific implementation. + */ +@Injectable() +export class AddonModDataDefaultFieldHandler implements AddonModDataFieldHandler { + name = 'AddonModDataDefaultFieldHandler'; + type = 'default'; + + /** + * Get field search data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @return {any} With name and value of the data to be sent. + */ + getFieldSearchData(field: any, inputData: any): any { + return false; + } + + /** + * Get field edit data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditData(field: any, inputData: any, originalFieldData: any): any { + return false; + } + + /** + * Get field data in changed. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @param {any} originalFieldData Original field entered data. + * @return {Promise | boolean} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | boolean { + return false; + } + + /** + * Get field edit files in the input data. + * + * @param {any} field Defines the field.. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditFiles(field: any, inputData: any, originalFieldData: any): any { + return []; + } + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string | false} String with the notification or false. + */ + getFieldsNotifications(field: any, inputData: any): string | false { + return false; + } + + /** + * Override field content data with offline submission. + * + * @param {any} originalContent Original data to be overriden. + * @param {any} offlineContent Array with all the offline data to override. + * @param {any} [offlineFiles] Array with all the offline files in the field. + * @return {any} Data overriden + */ + overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any { + return originalContent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/providers/fields-delegate.ts b/src/addon/mod/data/providers/fields-delegate.ts new file mode 100644 index 000000000..a72f0ad46 --- /dev/null +++ b/src/addon/mod/data/providers/fields-delegate.ts @@ -0,0 +1,226 @@ +// (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 { Injector, Injectable } from '@angular/core'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { AddonModDataDefaultFieldHandler } from './default-field-handler'; + +/** + * Interface that all fields handlers must implement. + */ +export interface AddonModDataFieldHandler extends CoreDelegateHandler { + + /** + * Name of the type of data field the handler supports. E.g. 'checkbox'. + * @type {string} + */ + type: string; + + /** + * Return the Component to use to display the plugin data. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} field The field object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent?(injector: Injector, plugin: any): any | Promise; + + /** + * Get field search data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @return {any} With name and value of the data to be sent. + */ + getFieldSearchData?(field: any, inputData: any): any; + + /** + * Get field edit data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditData?(field: any, inputData: any, originalFieldData: any): any; + + /** + * Get field data in changed. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @param {any} originalFieldData Original field entered data. + * @return {Promise | boolean} If the field has changes. + */ + hasFieldDataChanged?(field: any, inputData: any, originalFieldData: any): Promise | boolean; + + /** + * Get field edit files in the input data. + * + * @param {any} field Defines the field.. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditFiles?(field: any, inputData: any, originalFieldData: any): any; + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string | false} String with the notification or false. + */ + getFieldsNotifications?(field: any, inputData: any): string | false; + + /** + * Override field content data with offline submission. + * + * @param {any} originalContent Original data to be overriden. + * @param {any} offlineContent Array with all the offline data to override. + * @param {any} [offlineFiles] Array with all the offline files in the field. + * @return {any} Data overriden + */ + overrideData?(originalContent: any, offlineContent: any, offlineFiles?: any): any; +} + +/** + * Delegate to register database fields handlers. + */ +@Injectable() +export class AddonModDataFieldsDelegate extends CoreDelegate { + + protected handlerNameProperty = 'type'; + + constructor(logger: CoreLoggerProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, + protected utils: CoreUtilsProvider, protected defaultHandler: AddonModDataDefaultFieldHandler) { + super('AddonModDataFieldsDelegate', logger, sitesProvider, eventsProvider); + } + + /** + * Get the component to use for a certain field field. + * + * @param {Injector} injector Injector. + * @param {any} field The field object. + * @return {Promise} Promise resolved with the component to use, undefined if not found. + */ + getComponentForField(injector: Injector, field: any): Promise { + return Promise.resolve(this.executeFunctionOnEnabled(field.type, 'getComponent', [injector, field])); + } + + /** + * Get database data in the input data to search. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @return {any} Name and data field. + */ + getFieldSearchData(field: any, inputData: any): any { + return this.executeFunctionOnEnabled(field.type, 'getFieldSearchData', [field, inputData]); + } + + /** + * Get database data in the input data to add or update entry. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @param {any} originalFieldData Original field entered data. + * @return {any} Name and data field. + */ + getFieldEditData(field: any, inputData: any, originalFieldData: any): any { + return this.executeFunctionOnEnabled(field.type, 'getFieldEditData', [field, inputData, originalFieldData]); + } + + /** + * Get database data in the input files to add or update entry. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @param {any} originalFieldData Original field entered data. + * @return {any} Name and data field. + */ + getFieldEditFiles(field: any, inputData: any, originalFieldData: any): any { + return this.executeFunctionOnEnabled(field.type, 'getFieldEditFiles', [field, inputData, originalFieldData]); + } + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string} String with the notification or false. + */ + getFieldsNotifications(field: any, inputData: any): string { + return this.executeFunctionOnEnabled(field.type, 'getFieldsNotifications', [field, inputData]); + } + + /** + * Check if field type manage files or not. + * + * @param {any} field Defines the field to be checked. + * @return {boolean} If the field type manages files. + */ + hasFiles(field: any): boolean { + return this.hasFunction(field.type, 'getFieldEditFiles'); + } + + /** + * Check if the data has changed for a certain field. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @param {any} originalFieldData Original field entered data. + * @return {Promise} Promise rejected if has changed, resolved if no changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise { + if (!this.hasFunction(field.type, 'hasFieldDataChanged')) { + return Promise.resolve(); + } + + return Promise.resolve(this.executeFunctionOnEnabled(field.type, 'hasFieldDataChanged', + [field, inputData, originalFieldData])).then((result) => { + return result ? Promise.reject(null) : Promise.resolve(); + }); + } + + /** + * Check if a field plugin is supported. + * + * @param {string} pluginType Type of the plugin. + * @return {boolean} True if supported, false otherwise. + */ + isPluginSupported(pluginType: string): boolean { + return this.hasHandler(pluginType, true); + } + + /** + * Override field content data with offline submission. + * + * @param {any} field Defines the field to be rendered. + * @param {any} originalContent Original data to be overriden. + * @param {any} offlineContent Array with all the offline data to override. + * @param {any} [offlineFiles] Array with all the offline files in the field. + * @return {any} Data overriden + */ + overrideData(field: any, originalContent: any, offlineContent: any, offlineFiles?: any): any { + if (!offlineContent || !this.hasFunction(field.type, 'overrideData')) { + return originalContent; + } + + return this.executeFunctionOnEnabled(field.type, 'overrideData', [originalContent || {}, offlineContent, offlineFiles]); + } + +} diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts new file mode 100644 index 000000000..43ffe33fe --- /dev/null +++ b/src/addon/mod/data/providers/helper.ts @@ -0,0 +1,348 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonModDataFieldsDelegate } from './fields-delegate'; +import { AddonModDataOfflineProvider } from './offline'; +import { AddonModDataProvider } from './data'; + +/** + * Service that provides helper functions for datas. + */ +@Injectable() +export class AddonModDataHelperProvider { + + constructor(private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider, + private translate: TranslateService, private fieldsDelegate: AddonModDataFieldsDelegate, + private dataOffline: AddonModDataOfflineProvider, private fileUploaderProvider: CoreFileUploaderProvider) { } + + /** + * Returns the record with the offline actions applied. + * + * @param {any} record Entry to modify. + * @param {any} offlineActions Offline data with the actions done. + * @param {any} fields Entry defined fields indexed by fieldid. + * @return {any} Modified entry. + */ + applyOfflineActions(record: any, offlineActions: any[], fields: any[]): any { + const promises = []; + + offlineActions.forEach((action) => { + switch (action.action) { + case 'approve': + record.approved = true; + break; + case 'disapprove': + record.approved = false; + break; + case 'delete': + record.deleted = true; + break; + case 'add': + case 'edit': + const offlineContents = {}; + + action.fields.forEach((offlineContent) => { + if (typeof offlineContents[offlineContent.fieldid] == 'undefined') { + offlineContents[offlineContent.fieldid] = {}; + } + + if (offlineContent.subfield) { + offlineContents[offlineContent.fieldid][offlineContent.subfield] = JSON.parse(offlineContent.value); + } else { + offlineContents[offlineContent.fieldid][''] = JSON.parse(offlineContent.value); + } + }); + + // Override field contents. + fields.forEach((field) => { + if (this.fieldsDelegate.hasFiles(field)) { + promises.push(this.getStoredFiles(record.dataid, record.id, field.id).then((offlineFiles) => { + record.contents[field.id] = this.fieldsDelegate.overrideData(field, record.contents[field.id], + offlineContents[field.id], offlineFiles); + })); + } else { + record.contents[field.id] = this.fieldsDelegate.overrideData(field, record.contents[field.id], + offlineContents[field.id]); + } + }); + break; + default: + break; + } + }); + + return Promise.all(promises).then(() => { + return record; + }); + } + + /** + * Displays Advanced Search Fields. + * + * @param {string} template Template HMTL. + * @param {any[]} fields Fields that defines every content in the entry. + * @return {string} Generated HTML. + */ + displayAdvancedSearchFields(template: string, fields: any[]): string { + if (!template) { + return ''; + } + + let replace; + + // Replace the fields found on template. + fields.forEach((field) => { + replace = '[[' + field.name + ']]'; + replace = replace.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); + replace = new RegExp(replace, 'gi'); + + // Replace field by a generic directive. + const render = ''; + template = template.replace(replace, render); + }); + + // Not pluginable other search elements. + // Replace firstname field by the text input. + replace = new RegExp('##fn##', 'gi'); + let render = ''; + template = template.replace(replace, render); + + // Replace lastname field by the text input. + replace = new RegExp('##ln##', 'gi'); + render = ''; + template = template.replace(replace, render); + + return template; + } + + /** + * Displays fields for being shown. + * + * @param {string} template Template HMTL. + * @param {any[]} fields Fields that defines every content in the entry. + * @param {any} entry Entry. + * @param {string} mode Mode list or show. + * @param {any} actions Actions that can be performed to the record. + * @return {string} Generated HTML. + */ + displayShowFields(template: string, fields: any[], entry: any, mode: string, actions: any): string { + if (!template) { + return ''; + } + + let replace, render; + + // Replace the fields found on template. + fields.forEach((field) => { + replace = '[[' + field.name + ']]'; + replace = replace.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); + replace = new RegExp(replace, 'gi'); + + // Replace field by a generic directive. + render = ''; + template = template.replace(replace, render); + }); + + for (const action in actions) { + replace = new RegExp('##' + action + '##', 'gi'); + // Is enabled? + if (actions[action]) { + if (action == 'moreurl') { + // Render more url directly because it can be part of an HTML attribute. + render = this.sitesProvider.getCurrentSite().getURL() + '/mod/data/view.php?d={{data.id}}&rid=' + entry.id; + } else if (action == 'approvalstatus') { + render = this.translate.instant('addon.mod_data.' + (entry.approved ? 'approved' : 'notapproved')); + } else { + render = ''; + } + template = template.replace(replace, render); + } else { + template = template.replace(replace, ''); + } + } + + return template; + } + + /** + * Returns an object with all the actions that the user can do over the record. + * + * @param {any} database Database activity. + * @param {any} accessInfo Access info to the activity. + * @param {any} record Entry or record where the actions will be performed. + * @return {any} Keyed with the action names and boolean to evalute if it can or cannot be done. + */ + getActions(database: any, accessInfo: any, record: any): any { + return { + more: true, + moreurl: true, + user: true, + userpicture: true, + timeadded: true, + timemodified: true, + + edit: record.canmanageentry && !record.deleted, // This already checks capabilities and readonly period. + delete: record.canmanageentry, + approve: database.approval && accessInfo.canapprove && !record.approved && !record.deleted, + disapprove: database.approval && accessInfo.canapprove && record.approved && !record.deleted, + + approvalstatus: database.approval, + comments: database.comments, + + // Unsupported actions. + delcheck: false, + export: false + }; + } + + /** + * Retrieve the entered data in search in a form. + * We don't use ng-model because it doesn't detect changes done by JavaScript. + * + * @param {any} form Form (DOM element). + * @param {any[]} fields Fields that defines every content in the entry. + * @return {any[]} Array with the answers. + */ + getSearchDataFromForm(form: any, fields: any[]): any[] { + if (!form || !form.elements) { + return []; + } + + const searchedData = this.domUtils.getDataFromForm(form); + + // Filter and translate fields to each field plugin. + const advancedSearch = []; + fields.forEach((field) => { + const fieldData = this.fieldsDelegate.getFieldSearchData(field, searchedData); + + if (fieldData) { + fieldData.forEach((data) => { + data.value = JSON.stringify(data.value); + // WS wants values in Json format. + advancedSearch.push(data); + }); + } + }); + + // Not pluginable other search elements. + if (searchedData['firstname']) { + // WS wants values in Json format. + advancedSearch.push({ + name: 'firstname', + value: JSON.stringify(searchedData['firstname']) + }); + } + + if (searchedData['lastname']) { + // WS wants values in Json format. + advancedSearch.push({ + name: 'lastname', + value: JSON.stringify(searchedData['lastname']) + }); + } + + return advancedSearch; + } + + /** + * Get a list of stored attachment files for a new entry. See $mmaModDataHelper#storeFiles. + * + * @param {number} dataId Database ID. + * @param {number} entryId Entry ID or, if creating, timemodified. + * @param {number} fieldId Field ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the files. + */ + getStoredFiles(dataId: number, entryId: number, fieldId: number, siteId?: string): Promise { + return this.dataOffline.getEntryFieldFolder(dataId, entryId, fieldId, siteId).then((folderPath) => { + return this.fileUploaderProvider.getStoredFiles(folderPath).catch(() => { + // Ignore not found files. + return []; + }); + }); + } + + /** + * Add a prefix to all rules in a CSS string. + * + * @param {string} css CSS code to be prefixed. + * @param {string} prefix Prefix css selector. + * @return {string} Prefixed CSS. + */ + prefixCSS(css: string, prefix: string): string { + if (!css) { + return ''; + } + + // Remove comments first. + let regExp = /\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm; + css = css.replace(regExp, ''); + // Add prefix. + regExp = /([^]*?)({[^]*?}|,)/g; + + return css.replace(regExp, prefix + ' $1 $2'); + } + + /** + * Given a list of files (either online files or local files), store the local files in a local folder + * to be submitted later. + * + * @param {number} dataId Database ID. + * @param {number} entryId Entry ID or, if creating, timemodified. + * @param {number} fieldId Field ID. + * @param {any[]} files List of files. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success, rejected otherwise. + */ + storeFiles(dataId: number, entryId: number, fieldId: number, files: any[], siteId?: string): Promise { + // Get the folder where to store the files. + return this.dataOffline.getEntryFieldFolder(dataId, entryId, fieldId, siteId).then((folderPath) => { + return this.fileUploaderProvider.storeFilesToUpload(folderPath, files); + }); + } + + /** + * Upload or store some files, depending if the user is offline or not. + * + * @param {number} dataId Database ID. + * @param {number} [itemId=0] Draft ID to use. Undefined or 0 to create a new draft ID. + * @param {number} entryId Entry ID or, if creating, timemodified. + * @param {number} fieldId Field ID. + * @param {any[]} files List of files. + * @param {boolean} offline True if files sould be stored for offline, false to upload them. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success. + */ + uploadOrStoreFiles(dataId: number, itemId: number = 0, entryId: number, fieldId: number, files: any[], offline: boolean, + siteId?: string): Promise { + if (files.length) { + if (offline) { + return this.storeFiles(dataId, entryId, fieldId, files, siteId); + } + + return this.fileUploaderProvider.uploadOrReuploadFiles(files, AddonModDataProvider.COMPONENT, itemId, siteId); + } + + return Promise.resolve(0); + } +} diff --git a/src/addon/mod/data/providers/link-handler.ts b/src/addon/mod/data/providers/link-handler.ts new file mode 100644 index 000000000..4ba21ea6c --- /dev/null +++ b/src/addon/mod/data/providers/link-handler.ts @@ -0,0 +1,29 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; + +/** + * Handler to treat links to data. + */ +@Injectable() +export class AddonModDataLinkHandler extends CoreContentLinksModuleIndexHandler { + name = 'AddonModDataLinkHandler'; + + constructor(courseHelper: CoreCourseHelperProvider) { + super(courseHelper, AddonModDataLinkHandler.name, 'data'); + } +} diff --git a/src/addon/mod/data/providers/module-handler.ts b/src/addon/mod/data/providers/module-handler.ts new file mode 100644 index 000000000..b741ec975 --- /dev/null +++ b/src/addon/mod/data/providers/module-handler.ts @@ -0,0 +1,72 @@ +// (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 { Injectable } from '@angular/core'; +import { NavController, NavOptions } from 'ionic-angular'; +import { AddonModDataIndexComponent } from '../components/index/index'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { AddonModDataProvider } from './data'; + +/** + * Handler to support data modules. + */ +@Injectable() +export class AddonModDataModuleHandler implements CoreCourseModuleHandler { + name = 'AddonModData'; + modName = 'data'; + + constructor(private courseProvider: CoreCourseProvider, private dataProvider: AddonModDataProvider) { } + + /** + * Check if the handler is enabled on a site level. + * + * @return {Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): Promise { + return this.dataProvider.isPluginEnabled(); + } + + /** + * Get the data required to display the module in the course contents view. + * + * @param {any} module The module object. + * @param {number} courseId The course ID. + * @param {number} sectionId The section ID. + * @return {CoreCourseModuleHandlerData} Data to render the module. + */ + getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData { + return { + icon: this.courseProvider.getModuleIconSrc('data'), + title: module.name, + class: 'addon-mod_data-handler', + showDownloadButton: true, + action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { + navCtrl.push('AddonModDataIndexPage', {module: module, courseId: courseId}, options); + } + }; + } + + /** + * Get the component to render the module. This is needed to support singleactivity course format. + * The component returned must implement CoreCourseModuleMainComponent. + * + * @param {any} course The course object. + * @param {any} module The module object. + * @return {any} The component to use, undefined if not found. + */ + getMainComponent(course: any, module: any): any { + return AddonModDataIndexComponent; + } +} diff --git a/src/addon/mod/data/providers/offline.ts b/src/addon/mod/data/providers/offline.ts new file mode 100644 index 000000000..a4f01382b --- /dev/null +++ b/src/addon/mod/data/providers/offline.ts @@ -0,0 +1,195 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreFileProvider } from '@providers/file'; + +/** + * Service to handle Offline data. + */ +@Injectable() +export class AddonModDataOfflineProvider { + + protected logger; + + // Variables for database. + protected SURVEY_TABLE = 'addon_mod_data_entry'; + protected tablesSchema = [ + { + name: this.SURVEY_TABLE, + columns: [ + { + name: 'dataid', + type: 'INTEGER' + }, + { + name: 'courseid', + type: 'INTEGER' + }, + { + name: 'groupid', + type: 'INTEGER' + }, + { + name: 'action', + type: 'TEXT' + }, + { + name: 'entryid', + type: 'INTEGER' + }, + { + name: 'fields', + type: 'TEXT' + }, + { + name: 'timemodified', + type: 'INTEGER' + } + ], + primaryKeys: ['dataid', 'entryid'] + } + ]; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, + private fileProvider: CoreFileProvider) { + this.logger = logger.getInstance('AddonModDataOfflineProvider'); + this.sitesProvider.createTablesFromSchema(this.tablesSchema); + } + + /** + * Delete all the actions of an entry. + * + * @param {number} dataId Database ID. + * @param {number} entryId Database entry ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + deleteAllEntryActions(dataId: number, entryId: number, siteId?: string): Promise { + return this.getEntryActions(dataId, entryId, siteId).then((actions) => { + const promises = []; + + actions.forEach((action) => { + promises.push(this.deleteEntry(dataId, entryId, action.action, siteId)); + }); + + return Promise.all(promises); + }); + } + + /** + * Delete an stored entry. + * + * @param {number} dataId Database ID. + * @param {number} entryId Database entry Id. + * @param {string} action Action to be done + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if deleted, rejected if failure. + */ + deleteEntry(dataId: number, entryId: number, action: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(this.SURVEY_TABLE, {dataid: dataId, entryid: entryId, action: action}); + }); + } + + /** + * Get all the stored entry data from all the databases. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with entries. + */ + getAllEntries(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getAllRecords(this.SURVEY_TABLE); + }); + } + + /** + * Get all the stored entry data from a certain database. + * + * @param {number} dataId Database ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with entries. + */ + getDatabaseEntries(dataId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.SURVEY_TABLE, {dataid: dataId}); + }); + } + + /** + * Get an all stored entry actions data. + * + * @param {number} dataId Database ID. + * @param {number} entryId Database entry Id. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with entry actions. + */ + getEntryActions(dataId: number, entryId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(this.SURVEY_TABLE, {dataid: dataId, entryid: entryId}); + }); + } + + /** + * Check if there are offline entries to send. + * + * @param {number} dataId Database ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has offline answers, false otherwise. + */ + hasOfflineData(dataId: number, siteId?: string): Promise { + return this.getDatabaseEntries(dataId, siteId).then((entries) => { + return !!entries.length; + }).catch(() => { + // No offline data found, return false. + return false; + }); + } + + /** + * Get the path to the folder where to store files for offline files in a database. + * + * @param {number} dataId Database ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the path. + */ + protected getDatabaseFolder(dataId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + + const siteFolderPath = this.fileProvider.getSiteFolder(site.getId()), + folderPath = 'offlinedatabase/' + dataId; + + return this.textUtils.concatenatePaths(siteFolderPath, folderPath); + }); + } + + /** + * Get the path to the folder where to store files for a new offline entry. + * + * @param {number} dataId Database ID. + * @param {number} entryId The ID of the entry. + * @param {number} fieldId Field ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the path. + */ + getEntryFieldFolder(dataId: number, entryId: number, fieldId: number, siteId?: string): Promise { + return this.getDatabaseFolder(dataId, siteId).then((folderPath) => { + return this.textUtils.concatenatePaths(folderPath, entryId + '_' + fieldId); + }); + } +} diff --git a/src/addon/mod/data/providers/prefetch-handler.ts b/src/addon/mod/data/providers/prefetch-handler.ts new file mode 100644 index 000000000..6d575e031 --- /dev/null +++ b/src/addon/mod/data/providers/prefetch-handler.ts @@ -0,0 +1,102 @@ +// (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 { Injectable, Injector } from '@angular/core'; +import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler'; +import { AddonModDataProvider } from './data'; +import { AddonModDataHelperProvider } from './helper'; +import { CoreFilepoolProvider } from '@providers/filepool'; + +/** + * Handler to prefetch databases. + */ +@Injectable() +export class AddonModDataPrefetchHandler extends CoreCourseModulePrefetchHandlerBase { + name = 'data'; + component = AddonModDataProvider.COMPONENT; + updatesNames = /^configuration$|^.*files$|^entries$|^gradeitems$|^outcomes$|^comments$|^ratings/; + + constructor(injector: Injector, protected dataProvider: AddonModDataProvider, + protected filepoolProvider: CoreFilepoolProvider, protected dataHelper: AddonModDataHelperProvider) { + super(injector); + } + + /** + * Download or prefetch the content. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @param {boolean} [prefetch] True to prefetch, false to download right away. + * @param {string} [dirPath] Path of the directory where to store all the content files. This is to keep the files + * relative paths and make the package work in an iframe. Undefined to download the files + * in the filepool root data. + * @return {Promise} Promise resolved when all content is downloaded. Data returned is not reliable. + */ + downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string): Promise { + const promises = []; + + promises.push(super.downloadOrPrefetch(module, courseId, prefetch)); + promises.push(this.dataProvider.getDatabase(courseId, module.id).then((data) => { + // @TODO + })); + + return Promise.all(promises); + } + + /** + * Returns data intro files. + * + * @param {any} module The module object returned by WS. + * @param {number} courseId Course ID. + * @return {Promise} Promise resolved with list of intro files. + */ + getIntroFiles(module: any, courseId: number): Promise { + return this.dataProvider.getDatabase(courseId, module.id).catch(() => { + // Not found, return undefined so module description is used. + }).then((data) => { + return this.getIntroFilesFromInstance(module, data); + }); + } + + /** + * Invalidate the prefetched content. + * + * @param {number} moduleId The module ID. + * @param {number} courseId Course ID the module belongs to. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number): Promise { + return this.dataProvider.invalidateContent(moduleId, courseId); + } + + /** + * Invalidate WS calls needed to determine module status. + * + * @param {any} module Module. + * @param {number} courseId Course ID the module belongs to. + * @return {Promise} Promise resolved when invalidated. + */ + invalidateModule(module: any, courseId: number): Promise { + return this.dataProvider.invalidateDatabaseData(courseId); + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. + */ + isEnabled(): boolean | Promise { + return this.dataProvider.isPluginEnabled(); + } +} diff --git a/src/addon/mod/data/providers/sync-cron-handler.ts b/src/addon/mod/data/providers/sync-cron-handler.ts new file mode 100644 index 000000000..2560e571b --- /dev/null +++ b/src/addon/mod/data/providers/sync-cron-handler.ts @@ -0,0 +1,47 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreCronHandler } from '@providers/cron'; +import { AddonModDataSyncProvider } from './sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class AddonModDataSyncCronHandler implements CoreCronHandler { + name = 'AddonModDataSyncCronHandler'; + + constructor(private dataSync: AddonModDataSyncProvider) {} + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param {string} [siteId] ID of the site affected, undefined for all sites. + * @return {Promise} Promise resolved when done, rejected if failure. + */ + execute(siteId?: string): Promise { + return this.dataSync.syncAllDatabases(siteId); + } + + /** + * Get the time between consecutive executions. + * + * @return {number} Time between consecutive executions (in ms). + */ + getInterval(): number { + return 600000; // 10 minutes. + } +} diff --git a/src/addon/mod/data/providers/sync.ts b/src/addon/mod/data/providers/sync.ts new file mode 100644 index 000000000..3a7ccfb0f --- /dev/null +++ b/src/addon/mod/data/providers/sync.ts @@ -0,0 +1,337 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncBaseProvider } from '@classes/base-sync'; +import { CoreAppProvider } from '@providers/app'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModDataOfflineProvider } from './offline'; +import { AddonModDataProvider } from './data'; +import { AddonModDataHelperProvider } from './helper'; +import { CoreEventsProvider } from '@providers/events'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreSyncProvider } from '@providers/sync'; + +/** + * Service to sync databases. + */ +@Injectable() +export class AddonModDataSyncProvider extends CoreSyncBaseProvider { + + static AUTO_SYNCED = 'addon_mod_data_autom_synced'; + protected componentTranslate: string; + + constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider, + protected appProvider: CoreAppProvider, private dataOffline: AddonModDataOfflineProvider, + private eventsProvider: CoreEventsProvider, private dataProvider: AddonModDataProvider, + protected translate: TranslateService, private utils: CoreUtilsProvider, courseProvider: CoreCourseProvider, + syncProvider: CoreSyncProvider, protected textUtils: CoreTextUtilsProvider, + private dataHelper: AddonModDataHelperProvider) { + super('AddonModDataSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate); + this.componentTranslate = courseProvider.translateModuleName('data'); + } + + /** + * Check if a database has data to synchronize. + * + * @param {number} dataId Database ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has data to sync, false otherwise. + */ + hasDataToSync(dataId: number, siteId?: string): Promise { + return this.dataOffline.hasOfflineData(dataId, siteId); + } + + /** + * Try to synchronize all the databases in a certain site or in all sites. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @return {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllDatabases(siteId?: string): Promise { + return this.syncOnSites('all databases', this.syncAllDatabasesFunc.bind(this), undefined, siteId); + } + + /** + * Sync all pending databases on a site. + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @param {Promise} Promise resolved if sync is successful, rejected if sync fails. + */ + protected syncAllDatabasesFunc(siteId?: string): Promise { + // Get all data answers pending to be sent in the site. + return this.dataOffline.getAllEntries(siteId).then((offlineActions) => { + const promises = {}; + + // Do not sync same database twice. + offlineActions.forEach((action) => { + if (typeof promises[action.dataid] != 'undefined') { + return; + } + + promises[action.dataid] = this.syncDatabaseIfNeeded(action.dataid, siteId) + .then((result) => { + if (result && result.updated) { + // Sync done. Send event. + this.eventsProvider.trigger(AddonModDataSyncProvider.AUTO_SYNCED, { + dataId: action.dataid, + warnings: result.warnings + }, siteId); + } + }); + }); + + // Promises will be an object so, convert to an array first; + return Promise.all(this.utils.objectToArray(promises)); + }); + } + + /** + * Sync a database only if a certain time has passed since the last time. + * + * @param {number} dataId Database ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is synced or if it doesn't need to be synced. + */ + syncDatabaseIfNeeded(dataId: number, siteId?: string): Promise { + return this.isSyncNeeded(dataId, siteId).then((needed) => { + if (needed) { + return this.syncDatabase(dataId, siteId); + } + }); + } + + /** + * Synchronize a data. + * + * @param {number} dataId Data ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + */ + syncDatabase(dataId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (this.isSyncing(dataId, siteId)) { + // There's already a sync ongoing for this data and user, return the promise. + return this.getOngoingSync(dataId, siteId); + } + + // Verify that data isn't blocked. + if (this.syncProvider.isBlocked(AddonModDataProvider.COMPONENT, dataId, siteId)) { + this.logger.debug(`Cannot sync database '${dataId}' because it is blocked.`); + + return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate})); + } + + this.logger.debug(`Try to sync data '${dataId}' in site ${siteId}'`); + + let courseId, + data; + const result = { + warnings: [], + updated: false + }; + + // Get answers to be sent. + const syncPromise = this.dataOffline.getDatabaseEntries(dataId, siteId).catch(() => { + // No offline data found, return empty object. + return []; + }).then((offlineActions) => { + if (!offlineActions.length) { + // Nothing to sync. + return; + } + + if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + return Promise.reject(null); + } + + courseId = offlineActions[0].courseid; + + // Send the answers. + return this.dataProvider.getDatabaseById(courseId, dataId, siteId).then((database) => { + data = database; + + const offlineEntries = {}; + + offlineActions.forEach((entry) => { + if (typeof offlineEntries[entry.entryid] == 'undefined') { + offlineEntries[entry.entryid] = []; + } + offlineEntries[entry.entryid].push(entry); + }); + + const promises = this.utils.objectToArray(offlineEntries).map((entryActions) => { + return this.syncEntry(data, entryActions, result, siteId); + }); + + return Promise.all(promises); + }).then(() => { + if (result.updated) { + // Data has been sent to server. Now invalidate the WS calls. + return this.dataProvider.invalidateContent(data.cmid, courseId, siteId).catch(() => { + // Ignore errors. + }); + } + }); + }).then(() => { + // Sync finished, set sync time. + return this.setSyncTime(dataId, siteId); + }).then(() => { + return result; + }); + + return this.addOngoingSync(dataId, syncPromise, siteId); + } + + /** + * Synchronize an entry. + * + * @param {any} data Database. + * @param {any} entryActions Entry actions. + * @param {any} result Object with the result of the sync. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success, rejected otherwise. + */ + protected syncEntry(data: any, entryActions: any[], result: any, siteId?: string): Promise { + let discardError, + timePromise, + entryId = 0, + offlineId, + deleted = false; + + const promises = []; + + // Sort entries by timemodified. + entryActions = entryActions.sort((a: any, b: any) => a.timemodified - b.timemodified); + + entryId = entryActions[0].entryid; + + if (entryId > 0) { + timePromise = this.dataProvider.getEntry(data.id, entryId, siteId).then((entry) => { + return entry.entry.timemodified; + }).catch(() => { + return -1; + }); + } else { + offlineId = entryId; + timePromise = Promise.resolve(0); + } + + return timePromise.then((timemodified) => { + if (timemodified < 0 || timemodified >= entryActions[0].timemodified) { + // The entry was not found in Moodle or the entry has been modified, discard the action. + result.updated = true; + discardError = this.translate.instant('addon.mod_data.warningsubmissionmodified'); + + return this.dataOffline.deleteAllEntryActions(data.id, entryId, siteId); + } + + entryActions.forEach((action) => { + let actionPromise; + const proms = []; + + entryId = action.entryid > 0 ? action.entryid : entryId; + + if (action.fields) { + action.fields.forEach((field) => { + // Upload Files if asked. + const value = JSON.parse(field.value); + if (value.online || value.offline) { + let files = value.online || []; + const fileProm = value.offline ? this.dataHelper.getStoredFiles(action.dataid, entryId, field.fieldid) : + Promise.resolve([]); + + proms.push(fileProm.then((offlineFiles) => { + files = files.concat(offlineFiles); + + return this.dataHelper.uploadOrStoreFiles(action.dataid, 0, entryId, field.fieldid, files, false, + siteId).then((filesResult) => { + field.value = JSON.stringify(filesResult); + }); + })); + } + }); + } + + actionPromise = Promise.all(proms).then(() => { + // Perform the action. + switch (action.action) { + case 'add': + return this.dataProvider.addEntryOnline(action.dataid, action.fields, data.groupid, siteId) + .then((result) => { + entryId = result.newentryid; + }); + case 'edit': + return this.dataProvider.editEntryOnline(entryId, action.fields, siteId); + case 'approve': + return this.dataProvider.approveEntryOnline(entryId, true, siteId); + case 'disapprove': + return this.dataProvider.approveEntryOnline(entryId, false, siteId); + case 'delete': + return this.dataProvider.deleteEntryOnline(entryId, siteId).then(() => { + deleted = true; + }); + default: + break; + } + }); + + promises.push(actionPromise.catch((error) => { + if (error && error.wserror) { + // The WebService has thrown an error, this means it cannot be performed. Discard. + discardError = error.error; + } else { + // Couldn't connect to server, reject. + return Promise.reject(error && error.error); + } + }).then(() => { + // Delete the offline data. + result.updated = true; + + return this.dataOffline.deleteEntry(action.dataid, action.entryid, action.action, siteId); + })); + }); + + return Promise.all(promises); + }).then(() => { + if (discardError) { + // Submission was discarded, add a warning. + const message = this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: data.name, + error: discardError + }); + + if (result.warnings.indexOf(message) == -1) { + result.warnings.push(message); + } + } + + // Sync done. Send event. + this.eventsProvider.trigger(AddonModDataSyncProvider.AUTO_SYNCED, { + dataId: data.id, + entryId: entryId, + offlineEntryId: offlineId, + warnings: result.warnings, + deleted: deleted + }, siteId); + }); + } + +} diff --git a/src/addon/mod/feedback/providers/sync.ts b/src/addon/mod/feedback/providers/sync.ts index 7653014b8..469090738 100644 --- a/src/addon/mod/feedback/providers/sync.ts +++ b/src/addon/mod/feedback/providers/sync.ts @@ -139,7 +139,7 @@ export class AddonModFeedbackSyncProvider extends CoreSyncBaseProvider { let courseId, feedback; - this.logger.debug(`Try to sync feedback '${feedbackId}'`); + this.logger.debug(`Try to sync feedback '${feedbackId}' in site ${siteId}'`); // Get offline responses to be sent. const syncPromise = this.feedbackOffline.getFeedbackResponses(feedbackId, siteId).catch(() => { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0d21aa600..e690cd1ba 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -81,6 +81,7 @@ import { AddonModAssignModule } from '@addon/mod/assign/assign.module'; import { AddonModBookModule } from '@addon/mod/book/book.module'; import { AddonModChatModule } from '@addon/mod/chat/chat.module'; import { AddonModChoiceModule } from '@addon/mod/choice/choice.module'; +import { AddonModDataModule } from '@addon/mod/data/data.module'; import { AddonModLabelModule } from '@addon/mod/label/label.module'; import { AddonModLtiModule } from '@addon/mod/lti/lti.module'; import { AddonModResourceModule } from '@addon/mod/resource/resource.module'; @@ -185,6 +186,7 @@ export const CORE_PROVIDERS: any[] = [ AddonModBookModule, AddonModChatModule, AddonModChoiceModule, + AddonModDataModule, AddonModLabelModule, AddonModResourceModule, AddonModFeedbackModule, diff --git a/src/classes/delegate.ts b/src/classes/delegate.ts index 412dad239..b95626153 100644 --- a/src/classes/delegate.ts +++ b/src/classes/delegate.ts @@ -164,6 +164,20 @@ export class CoreDelegate { return enabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName]; } + /** + * Check if function exists on a handler. + * + * @param {string} handlerName The handler name. + * @param {string} fnName Name of the function to execute. + * @param {booealn} [onlyEnabled=true] If check only enabled handlers or all. + * @return {any} Function returned value or default value. + */ + protected hasFunction(handlerName: string, fnName: string, onlyEnabled: boolean = true): any { + const handler = onlyEnabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName]; + + return handler && handler[fnName]; + } + /** * Check if a handler name has a registered handler (not necessarily enabled). * diff --git a/src/core/course/classes/main-activity-component.ts b/src/core/course/classes/main-activity-component.ts index 5a76726cf..909a9bbb6 100644 --- a/src/core/course/classes/main-activity-component.ts +++ b/src/core/course/classes/main-activity-component.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injector } from '@angular/core'; +import { Injector, Input } from '@angular/core'; import { Content } from 'ionic-angular'; import { CoreSitesProvider } from '@providers/sites'; import { CoreCourseProvider } from '@core/course/providers/course'; @@ -26,6 +26,8 @@ import { CoreCourseModuleMainResourceComponent } from './main-resource-component * Template class to easily create CoreCourseModuleMainComponent of activities. */ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainResourceComponent { + @Input() group?: number; // Group ID the component belongs to. + moduleName: string; // Raw module name to be translated. It will be translated on init. // Data for context menu. From 661dbe22605411512d88902f22fc4f2fbff5e37d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 2 May 2018 10:44:28 +0200 Subject: [PATCH 02/10] MOBILE-2338 data: Add field plugins --- config.xml | 4 + .../components/field-plugin/field-plugin.ts | 5 +- .../mod/data/components/index/index.html | 2 +- .../mod/data/components/index/index.scss | 25 +++ src/addon/mod/data/components/index/index.ts | 26 ++- .../fields/checkbox/component/checkbox.ts | 3 +- .../data/fields/checkbox/providers/handler.ts | 1 - .../mod/data/fields/date/component/date.html | 12 ++ .../mod/data/fields/date/component/date.ts | 63 ++++++ src/addon/mod/data/fields/date/date.module.ts | 51 +++++ .../mod/data/fields/date/providers/handler.ts | 180 ++++++++++++++++ src/addon/mod/data/fields/field.module.ts | 24 ++- .../mod/data/fields/file/component/file.html | 14 ++ .../mod/data/fields/file/component/file.ts | 81 ++++++++ src/addon/mod/data/fields/file/file.module.ts | 49 +++++ .../mod/data/fields/file/providers/handler.ts | 158 ++++++++++++++ .../fields/latlong/component/latlong.html | 19 ++ .../data/fields/latlong/component/latlong.ts | 90 ++++++++ .../mod/data/fields/latlong/latlong.module.ts | 49 +++++ .../data/fields/latlong/providers/handler.ts | 159 ++++++++++++++ .../mod/data/fields/menu/component/menu.html | 9 + .../mod/data/fields/menu/component/menu.ts | 57 +++++ src/addon/mod/data/fields/menu/menu.module.ts | 49 +++++ .../mod/data/fields/menu/providers/handler.ts | 134 ++++++++++++ .../fields/multimenu/component/multimenu.html | 8 + .../fields/multimenu/component/multimenu.ts | 63 ++++++ .../data/fields/multimenu/multimenu.module.ts | 49 +++++ .../fields/multimenu/providers/handler.ts | 152 ++++++++++++++ .../data/fields/number/component/number.html | 7 + .../data/fields/number/component/number.ts | 54 +++++ .../mod/data/fields/number/number.module.ts | 49 +++++ .../data/fields/number/providers/handler.ts | 57 +++++ .../fields/picture/component/picture.html | 17 ++ .../data/fields/picture/component/picture.ts | 128 ++++++++++++ .../mod/data/fields/picture/picture.module.ts | 49 +++++ .../data/fields/picture/providers/handler.ts | 194 ++++++++++++++++++ .../radiobutton/component/radiobutton.html | 9 + .../radiobutton/component/radiobutton.ts | 57 +++++ .../fields/radiobutton/providers/handler.ts | 133 ++++++++++++ .../fields/radiobutton/radiobutton.module.ts | 49 +++++ .../mod/data/fields/text/component/text.html | 7 + .../mod/data/fields/text/component/text.ts | 54 +++++ .../mod/data/fields/text/providers/handler.ts | 134 ++++++++++++ src/addon/mod/data/fields/text/text.module.ts | 49 +++++ .../fields/textarea/component/textarea.html | 10 + .../fields/textarea/component/textarea.ts | 69 +++++++ .../data/fields/textarea/providers/handler.ts | 145 +++++++++++++ .../data/fields/textarea/textarea.module.ts | 49 +++++ .../mod/data/fields/url/component/url.html | 7 + .../mod/data/fields/url/component/url.ts | 54 +++++ .../mod/data/fields/url/providers/handler.ts | 57 +++++ src/addon/mod/data/fields/url/url.module.ts | 49 +++++ src/addon/mod/data/providers/helper.ts | 10 +- src/addon/mod/feedback/providers/helper.ts | 5 +- .../components/compile-html/compile-html.ts | 7 +- src/core/compile/providers/compile.ts | 12 +- 56 files changed, 3028 insertions(+), 29 deletions(-) create mode 100644 src/addon/mod/data/fields/date/component/date.html create mode 100644 src/addon/mod/data/fields/date/component/date.ts create mode 100644 src/addon/mod/data/fields/date/date.module.ts create mode 100644 src/addon/mod/data/fields/date/providers/handler.ts create mode 100644 src/addon/mod/data/fields/file/component/file.html create mode 100644 src/addon/mod/data/fields/file/component/file.ts create mode 100644 src/addon/mod/data/fields/file/file.module.ts create mode 100644 src/addon/mod/data/fields/file/providers/handler.ts create mode 100644 src/addon/mod/data/fields/latlong/component/latlong.html create mode 100644 src/addon/mod/data/fields/latlong/component/latlong.ts create mode 100644 src/addon/mod/data/fields/latlong/latlong.module.ts create mode 100644 src/addon/mod/data/fields/latlong/providers/handler.ts create mode 100644 src/addon/mod/data/fields/menu/component/menu.html create mode 100644 src/addon/mod/data/fields/menu/component/menu.ts create mode 100644 src/addon/mod/data/fields/menu/menu.module.ts create mode 100644 src/addon/mod/data/fields/menu/providers/handler.ts create mode 100644 src/addon/mod/data/fields/multimenu/component/multimenu.html create mode 100644 src/addon/mod/data/fields/multimenu/component/multimenu.ts create mode 100644 src/addon/mod/data/fields/multimenu/multimenu.module.ts create mode 100644 src/addon/mod/data/fields/multimenu/providers/handler.ts create mode 100644 src/addon/mod/data/fields/number/component/number.html create mode 100644 src/addon/mod/data/fields/number/component/number.ts create mode 100644 src/addon/mod/data/fields/number/number.module.ts create mode 100644 src/addon/mod/data/fields/number/providers/handler.ts create mode 100644 src/addon/mod/data/fields/picture/component/picture.html create mode 100644 src/addon/mod/data/fields/picture/component/picture.ts create mode 100644 src/addon/mod/data/fields/picture/picture.module.ts create mode 100644 src/addon/mod/data/fields/picture/providers/handler.ts create mode 100644 src/addon/mod/data/fields/radiobutton/component/radiobutton.html create mode 100644 src/addon/mod/data/fields/radiobutton/component/radiobutton.ts create mode 100644 src/addon/mod/data/fields/radiobutton/providers/handler.ts create mode 100644 src/addon/mod/data/fields/radiobutton/radiobutton.module.ts create mode 100644 src/addon/mod/data/fields/text/component/text.html create mode 100644 src/addon/mod/data/fields/text/component/text.ts create mode 100644 src/addon/mod/data/fields/text/providers/handler.ts create mode 100644 src/addon/mod/data/fields/text/text.module.ts create mode 100644 src/addon/mod/data/fields/textarea/component/textarea.html create mode 100644 src/addon/mod/data/fields/textarea/component/textarea.ts create mode 100644 src/addon/mod/data/fields/textarea/providers/handler.ts create mode 100644 src/addon/mod/data/fields/textarea/textarea.module.ts create mode 100644 src/addon/mod/data/fields/url/component/url.html create mode 100644 src/addon/mod/data/fields/url/component/url.ts create mode 100644 src/addon/mod/data/fields/url/providers/handler.ts create mode 100644 src/addon/mod/data/fields/url/url.module.ts diff --git a/config.xml b/config.xml index 5b9064b2c..8d93693b2 100644 --- a/config.xml +++ b/config.xml @@ -13,6 +13,10 @@ + + + + diff --git a/src/addon/mod/data/components/field-plugin/field-plugin.ts b/src/addon/mod/data/components/field-plugin/field-plugin.ts index 819d43e2d..cbac94fa2 100644 --- a/src/addon/mod/data/components/field-plugin/field-plugin.ts +++ b/src/addon/mod/data/components/field-plugin/field-plugin.ts @@ -38,13 +38,13 @@ export class AddonModDataFieldPluginComponent implements OnInit { fieldLoaded: boolean; constructor(protected injector: Injector, protected dataDelegate: AddonModDataFieldsDelegate, - protected dataProvider: AddonModDataProvider) { } + protected dataProvider: AddonModDataProvider) { + } /** * Component being initialized. */ ngOnInit(): void { - console.error('HERE'); if (!this.field) { this.fieldLoaded = true; @@ -65,6 +65,7 @@ export class AddonModDataFieldPluginComponent implements OnInit { error: this.error, viewAction: this.viewAction }; + } else { this.fieldLoaded = true; } diff --git a/src/addon/mod/data/components/index/index.html b/src/addon/mod/data/components/index/index.html index e53c61d7a..2fe8fe250 100644 --- a/src/addon/mod/data/components/index/index.html +++ b/src/addon/mod/data/components/index/index.html @@ -63,7 +63,7 @@ {{ cssTemplate }} - + diff --git a/src/addon/mod/data/components/index/index.scss b/src/addon/mod/data/components/index/index.scss index 1a748d124..2c2a8e5a3 100644 --- a/src/addon/mod/data/components/index/index.scss +++ b/src/addon/mod/data/components/index/index.scss @@ -1,3 +1,28 @@ addon-mod-data-index { + .core-data-contents { + overflow: visible; + white-space: normal; + word-break: break-word; + padding: $content-padding; + background-color: white; + border-top-width: 1px; + border-bottom-width: 1px; + border-right-width: 0; + border-left-width: 0; + border-style: solid; + border-color: $list-border-color; + table, tbody { + display: block; + } + + tr { + @extend .row; + padding: 0; + } + + td, th { + @extend .col; + } + } } \ No newline at end of file diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index 713731112..e2517fc62 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -23,6 +23,7 @@ import { AddonModDataProvider } from '../../providers/data'; import { AddonModDataHelperProvider } from '../../providers/helper'; import { AddonModDataOfflineProvider } from '../../providers/offline'; import { AddonModDataSyncProvider } from '../../providers/sync'; +import { AddonModDataComponentsModule } from '../components.module'; import * as moment from 'moment'; /** @@ -33,6 +34,7 @@ import * as moment from 'moment'; templateUrl: 'index.html', }) export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComponent { + component = AddonModDataProvider.COMPONENT; moduleName = 'data'; @@ -65,13 +67,16 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp offlineEntries: any; entriesRendered = ''; cssTemplate = ''; + extraImports = [AddonModDataComponentsModule]; + jsData; protected syncEventName = AddonModDataSyncProvider.AUTO_SYNCED; protected entryChangedObserver: any; protected hasComments = false; + protected fieldsArray: any; constructor(injector: Injector, private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider, - private dataOffline: AddonModDataOfflineProvider, @Optional() private content: Content, + private dataOffline: AddonModDataOfflineProvider, @Optional() @Optional() content: Content, private dataSync: AddonModDataSyncProvider, private timeUtils: CoreTimeUtilsProvider, private groupsProvider: CoreGroupsProvider, private commentsProvider: CoreCommentsProvider, private modalCtrl: ModalController, private utils: CoreUtilsProvider) { @@ -216,8 +221,8 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp fields.forEach((field) => { this.fields[field.id] = field; }); - this.fields = this.utils.objectToArray(this.fields); - this.advancedSearch = this.dataHelper.displayAdvancedSearchFields(this.data.asearchtemplate, this.fields); + this.fieldsArray = this.utils.objectToArray(this.fields); + this.advancedSearch = this.dataHelper.displayAdvancedSearchFields(this.data.asearchtemplate, this.fieldsArray); return this.fetchEntriesData(); }); @@ -283,7 +288,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp }; if (offlineActions.length > 0) { - promises.push(this.dataHelper.applyOfflineActions(entry, offlineActions, this.fields)); + promises.push(this.dataHelper.applyOfflineActions(entry, offlineActions, this.fieldsArray)); } else { promises.push(Promise.resolve(entry)); } @@ -299,7 +304,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp entry.contents = contents; if (typeof this.offlineActions[entry.id] != 'undefined') { - promises.push(this.dataHelper.applyOfflineActions(entry, this.offlineActions[entry.id], this.fields)); + promises.push(this.dataHelper.applyOfflineActions(entry, this.offlineActions[entry.id], this.fieldsArray)); } else { promises.push(Promise.resolve(entry)); } @@ -318,12 +323,19 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp const actions = this.dataHelper.getActions(this.data, this.access, entry); - entriesHTML += this.dataHelper.displayShowFields(this.data.listtemplate, this.fields, entry, 'list', + entriesHTML += this.dataHelper.displayShowFields(this.data.listtemplate, this.fieldsArray, entry, 'list', actions); }); entriesHTML += this.data.listtemplatefooter || ''; this.entriesRendered = entriesHTML; + + // Pass the input data to the component. + this.jsData = { + fields: this.fields, + entries: this.entries, + data: this.data + }; }); } else if (!this.search.searching) { // Empty and no searching. @@ -356,7 +368,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp if (this.search.searchingAdvanced) { this.search.advanced = this.dataHelper.getSearchDataFromForm(document.forms['addon-mod_data-advanced-search-form'], - this.fields); + this.fieldsArray); this.search.searching = this.search.advanced.length > 0; } else { this.search.searching = this.search.text.length > 0; diff --git a/src/addon/mod/data/fields/checkbox/component/checkbox.ts b/src/addon/mod/data/fields/checkbox/component/checkbox.ts index 64f4db6e2..48404b1a8 100644 --- a/src/addon/mod/data/fields/checkbox/component/checkbox.ts +++ b/src/addon/mod/data/fields/checkbox/component/checkbox.ts @@ -32,7 +32,6 @@ export class AddonModDataFieldCheckboxComponent extends AddonModDataFieldPluginC constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, element: ElementRef) { - super(); } @@ -46,7 +45,7 @@ export class AddonModDataFieldCheckboxComponent extends AddonModDataFieldPluginC protected render(): void { if (this.mode == 'show') { - this.value.content.split('##').join('
'); + this.value.content = this.value.content.split('##').join('
'); return; } diff --git a/src/addon/mod/data/fields/checkbox/providers/handler.ts b/src/addon/mod/data/fields/checkbox/providers/handler.ts index d49a0a3c0..14632ab59 100644 --- a/src/addon/mod/data/fields/checkbox/providers/handler.ts +++ b/src/addon/mod/data/fields/checkbox/providers/handler.ts @@ -35,7 +35,6 @@ export class AddonModDataFieldCheckboxHandler implements AddonModDataFieldHandle * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. */ getComponent(injector: Injector, plugin: any): any | Promise { - console.error(AddonModDataFieldCheckboxComponent); return AddonModDataFieldCheckboxComponent; } diff --git a/src/addon/mod/data/fields/date/component/date.html b/src/addon/mod/data/fields/date/component/date.html new file mode 100644 index 000000000..66a9c8de0 --- /dev/null +++ b/src/addon/mod/data/fields/date/component/date.html @@ -0,0 +1,12 @@ + + + + + + {{ 'addon.mod_data.usedate' | translate }} + + + + + + diff --git a/src/addon/mod/data/fields/date/component/date.ts b/src/addon/mod/data/fields/date/component/date.ts new file mode 100644 index 000000000..034669864 --- /dev/null +++ b/src/addon/mod/data/fields/date/component/date.ts @@ -0,0 +1,63 @@ +// (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, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; + +/** + * Component to render data date field. + */ +@Component({ + selector: 'addon-mod-data-field-date', + templateUrl: 'date.html' +}) +export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginComponent implements OnInit { + + control: FormControl; + values = {}; + enable: boolean; + val: any; + + constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, + element: ElementRef) { + super(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.mode = this.mode == 'list' ? 'show' : this.mode; + this.render(); + } + + protected render(): void { + if (this.mode == 'show') { + return; + } + + if (this.mode == 'edit' && this.value) { + this.enable = true; + } else { + this.value = { + content: Math.floor(Date.now() / 1000) + }; + this.enable = false; + } + + this.val = new Date(this.value.content * 1000); + } +} diff --git a/src/addon/mod/data/fields/date/date.module.ts b/src/addon/mod/data/fields/date/date.module.ts new file mode 100644 index 000000000..faf696cf5 --- /dev/null +++ b/src/addon/mod/data/fields/date/date.module.ts @@ -0,0 +1,51 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonModDataFieldDateHandler } from './providers/handler'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { AddonModDataFieldDateComponent } from './component/date'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; + +@NgModule({ + declarations: [ + AddonModDataFieldDateComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule + ], + providers: [ + AddonModDataFieldDateHandler + ], + exports: [ + AddonModDataFieldDateComponent + ], + entryComponents: [ + AddonModDataFieldDateComponent + ] +}) +export class AddonModDataFieldDateModule { + constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldDateHandler) { + fieldDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/data/fields/date/providers/handler.ts b/src/addon/mod/data/fields/date/providers/handler.ts new file mode 100644 index 000000000..9bb37150b --- /dev/null +++ b/src/addon/mod/data/fields/date/providers/handler.ts @@ -0,0 +1,180 @@ +// (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 { Injector, Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonModDataFieldHandler } from '../../../providers/fields-delegate'; +import { AddonModDataFieldDateComponent } from '../component/date'; + +/** + * Handler for date data field plugin. + */ +@Injectable() +export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler { + name = 'AddonModDataFieldDateHandler'; + type = 'date'; + + constructor(private translate: TranslateService) { } + + /** + * Return the Component to use to display the plugin data. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} field The field object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + return AddonModDataFieldDateComponent; + } + + /** + * Get field search data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @return {any} With name and value of the data to be sent. + */ + getFieldSearchData(field: any, inputData: any): any { + const fieldName = 'f_' + field.id, + enabledName = 'f_' + field.id + '_z'; + + if (inputData[enabledName]['1']) { + const values = [], + date = inputData[fieldName].split('-'), + year = date[0], + month = date[1], + day = date[2]; + values.push({ + name: fieldName + '_y', + value: year + }); + values.push({ + name: fieldName + '_m', + value: month + }); + values.push({ + name: fieldName + '_d', + value: day + }); + values.push({ + name: enabledName, + value: 1 + }); + + return values; + } + + return false; + } + + /** + * Get field edit data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditData(field: any, inputData: any, originalFieldData: any): any { + const fieldName = 'f_' + field.id; + + if (inputData[fieldName]) { + const values = [], + date = inputData[fieldName].split('-'), + year = date[0], + month = date[1], + day = date[2]; + values.push({ + fieldid: field.id, + subfield: 'year', + value: year + }); + values.push({ + fieldid: field.id, + subfield: 'month', + value: month + }); + values.push({ + fieldid: field.id, + subfield: 'day', + value: day + }); + + return values; + } + + return false; + } + + /** + * Get field data in changed. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @param {any} originalFieldData Original field entered data. + * @return {Promise | boolean} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | boolean { + const fieldName = 'f_' + field.id, + input = inputData[fieldName] || ''; + + originalFieldData = (originalFieldData && originalFieldData.content && + new Date(originalFieldData.content * 1000).toISOString().substr(0, 10)) || ''; + + return input != originalFieldData; + } + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string | false} String with the notification or false. + */ + getFieldsNotifications(field: any, inputData: any): string | false { + if (field.required && + (!inputData || inputData.length < 2 || !inputData[0].value || !inputData[1].value || !inputData[2].value)) { + + return this.translate.instant('addon.mod_data.errormustsupplyvalue'); + } + + return false; + } + + /** + * Override field content data with offline submission. + * + * @param {any} originalContent Original data to be overriden. + * @param {any} offlineContent Array with all the offline data to override. + * @param {any} [offlineFiles] Array with all the offline files in the field. + * @return {any} Data overriden + */ + overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any { + let date = Date.UTC(offlineContent['year'] || '', offlineContent['month'] ? offlineContent['month'] - 1 : null, + offlineContent['day'] || null); + date = Math.floor(date / 1000); + + originalContent.content = date || ''; + + return originalContent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/field.module.ts b/src/addon/mod/data/fields/field.module.ts index c38ed2f56..3bd3d9e3d 100644 --- a/src/addon/mod/data/fields/field.module.ts +++ b/src/addon/mod/data/fields/field.module.ts @@ -14,11 +14,33 @@ import { NgModule } from '@angular/core'; import { AddonModDataFieldCheckboxModule } from './checkbox/checkbox.module'; +import { AddonModDataFieldDateModule } from './date/date.module'; +import { AddonModDataFieldFileModule } from './file/file.module'; +import { AddonModDataFieldLatlongModule } from './latlong/latlong.module'; +import { AddonModDataFieldMenuModule } from './menu/menu.module'; +import { AddonModDataFieldMultimenuModule } from './multimenu/multimenu.module'; +import { AddonModDataFieldNumberModule } from './number/number.module'; +import { AddonModDataFieldPictureModule } from './picture/picture.module'; +import { AddonModDataFieldRadiobuttonModule } from './radiobutton/radiobutton.module'; +import { AddonModDataFieldTextModule } from './text/text.module'; +import { AddonModDataFieldTextareaModule } from './textarea/textarea.module'; +import { AddonModDataFieldUrlModule } from './url/url.module'; @NgModule({ declarations: [], imports: [ - AddonModDataFieldCheckboxModule + AddonModDataFieldCheckboxModule, + AddonModDataFieldDateModule, + AddonModDataFieldFileModule, + AddonModDataFieldLatlongModule, + AddonModDataFieldMenuModule, + AddonModDataFieldMultimenuModule, + AddonModDataFieldNumberModule, + AddonModDataFieldPictureModule, + AddonModDataFieldRadiobuttonModule, + AddonModDataFieldTextModule, + AddonModDataFieldTextareaModule, + AddonModDataFieldUrlModule ], providers: [ ], diff --git a/src/addon/mod/data/fields/file/component/file.html b/src/addon/mod/data/fields/file/component/file.html new file mode 100644 index 000000000..1ed18a0f3 --- /dev/null +++ b/src/addon/mod/data/fields/file/component/file.html @@ -0,0 +1,14 @@ + + + + + + +
+ + + + + +
+
diff --git a/src/addon/mod/data/fields/file/component/file.ts b/src/addon/mod/data/fields/file/component/file.ts new file mode 100644 index 000000000..773a3b7ed --- /dev/null +++ b/src/addon/mod/data/fields/file/component/file.ts @@ -0,0 +1,81 @@ +// (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, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; +import { CoreFileSessionProvider } from '@providers/file-session'; +import { AddonModDataProvider } from '../../../providers/data'; + +/** + * Component to render data file field. + */ +@Component({ + selector: 'addon-mod-data-field-file', + templateUrl: 'file.html' +}) +export class AddonModDataFieldFileComponent extends AddonModDataFieldPluginComponent implements OnInit { + + control: FormControl; + files = []; + component: string; + componentId: number; + maxSizeBytes: number; + + constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, + element: ElementRef, private fileSessionprovider: CoreFileSessionProvider) { + super(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.mode = this.mode == 'list' ? 'show' : this.mode; + this.render(); + } + + /** + * Get the files from the input value. + * + * @param {any} value Input value. + * @return {any} List of files. + */ + protected getFiles(value: any): any { + let files = (value && value.files) || []; + + // Reduce to first element. + if (files.length > 0) { + files = [files[0]]; + } + + return files; + } + + protected render(): void { + if (this.mode == 'show' || this.mode == 'edit') { + this.component = AddonModDataProvider.COMPONENT; + this.componentId = this.database.coursemodule; + + this.files = this.getFiles(this.value); + + if (this.mode != 'show') { + // Edit mode, the list shouldn't change so there is no need to watch it. + this.maxSizeBytes = parseInt(this.field.param3, 10); + this.fileSessionprovider.setFiles(this.component, this.database.id + '_' + this.field.id, this.files); + } + } + } +} diff --git a/src/addon/mod/data/fields/file/file.module.ts b/src/addon/mod/data/fields/file/file.module.ts new file mode 100644 index 000000000..b2d240329 --- /dev/null +++ b/src/addon/mod/data/fields/file/file.module.ts @@ -0,0 +1,49 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonModDataFieldFileHandler } from './providers/handler'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { AddonModDataFieldFileComponent } from './component/file'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonModDataFieldFileComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + providers: [ + AddonModDataFieldFileHandler + ], + exports: [ + AddonModDataFieldFileComponent + ], + entryComponents: [ + AddonModDataFieldFileComponent + ] +}) +export class AddonModDataFieldFileModule { + constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldFileHandler) { + fieldDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/data/fields/file/providers/handler.ts b/src/addon/mod/data/fields/file/providers/handler.ts new file mode 100644 index 000000000..38a58e7de --- /dev/null +++ b/src/addon/mod/data/fields/file/providers/handler.ts @@ -0,0 +1,158 @@ +// (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 { Injector, Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreFileSessionProvider } from '@providers/file-session'; +import { AddonModDataFieldHandler } from '../../../providers/fields-delegate'; +import { AddonModDataProvider } from '../../../providers/data'; +import { AddonModDataFieldFileComponent } from '../component/file'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; + +/** + * Handler for file data field plugin. + */ +@Injectable() +export class AddonModDataFieldFileHandler implements AddonModDataFieldHandler { + name = 'AddonModDataFieldFileHandler'; + type = 'file'; + + constructor(private translate: TranslateService, private fileSessionprovider: CoreFileSessionProvider, + private fileUploaderProvider: CoreFileUploaderProvider) { } + + /** + * Return the Component to use to display the plugin data. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} field The field object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + return AddonModDataFieldFileComponent; + } + + /** + * Get field search data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @return {any} With name and value of the data to be sent. + */ + getFieldSearchData(field: any, inputData: any): any { + const fieldName = 'f_' + field.id; + + if (inputData[fieldName]) { + return [{ + name: fieldName, + value: inputData[fieldName] + }]; + } + + return false; + } + + /** + * Get field edit data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditData(field: any, inputData: any, originalFieldData: any): any { + const files = this.getFieldEditFiles(field); + + if (files.length) { + return [{ + fieldid: field.id, + subfield: 'file', + files: files + }]; + } + + return false; + } + + /** + * Get field edit files in the input data. + * + * @param {any} field Defines the field.. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditFiles(field: any): any { + return this.fileSessionprovider.getFiles(AddonModDataProvider.COMPONENT, field.dataid + '_' + field.id); + } + + /** + * Get field data in changed. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @param {any} originalFieldData Original field entered data. + * @return {Promise | boolean} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | boolean { + const files = this.fileSessionprovider.getFiles(AddonModDataProvider.COMPONENT, field.dataid + '_' + field.id) || []; + let originalFiles = (originalFieldData && originalFieldData.files) || []; + + if (originalFiles.length) { + originalFiles = [originalFiles[0]]; + } + + return this.fileUploaderProvider.areFileListDifferent(files, originalFiles); + } + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string | false} String with the notification or false. + */ + getFieldsNotifications(field: any, inputData: any): string | false { + if (field.required && (!inputData || !inputData.length || !inputData[0].value)) { + return this.translate.instant('addon.mod_data.errormustsupplyvalue'); + } + + return false; + } + + /** + * Override field content data with offline submission. + * + * @param {any} originalContent Original data to be overriden. + * @param {any} offlineContent Array with all the offline data to override. + * @param {any} [offlineFiles] Array with all the offline files in the field. + * @return {any} Data overriden + */ + overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any { + if (offlineContent && offlineContent.file && offlineContent.file.offline > 0 && offlineFiles && offlineFiles.length > 0) { + originalContent.content = offlineFiles[0].filename; + originalContent.files = [offlineFiles[0]]; + } else if (offlineContent && offlineContent.file && offlineContent.file.online && offlineContent.file.online.length > 0) { + originalContent.content = offlineContent.file.online[0].filename; + originalContent.files = [offlineContent.file.online[0]]; + } + + return originalContent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/latlong/component/latlong.html b/src/addon/mod/data/fields/latlong/component/latlong.html new file mode 100644 index 000000000..0b2ef6c9e --- /dev/null +++ b/src/addon/mod/data/fields/latlong/component/latlong.html @@ -0,0 +1,19 @@ + + + + + + + + + °N + + + + °E + + + + + {{ formatLatLong(north, east) }} + \ No newline at end of file diff --git a/src/addon/mod/data/fields/latlong/component/latlong.ts b/src/addon/mod/data/fields/latlong/component/latlong.ts new file mode 100644 index 000000000..2d4e555a8 --- /dev/null +++ b/src/addon/mod/data/fields/latlong/component/latlong.ts @@ -0,0 +1,90 @@ +// (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, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { Platform } from 'ionic-angular'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; + +/** + * Component to render data latlong field. + */ +@Component({ + selector: 'addon-mod-data-field-latlong', + templateUrl: 'latlong.html' +}) +export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginComponent implements OnInit { + + control: FormControl; + values = {}; + north: number; + east: number; + + constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, + element: ElementRef, private platform: Platform) { + super(); + } + + /** + * Format latitude and longitude in a simple text. + * + * @param {number} north Degrees north. + * @param {number} east Degrees East. + * @return {string} Readable Latitude and logitude. + */ + formatLatLong(north: number, east: number): string { + if (north !== null || east !== null) { + const northFixed = north ? Math.abs(north).toFixed(4) : '0.0000', + eastFixed = east ? Math.abs(east).toFixed(4) : '0.0000'; + + return northFixed + (north < 0 ? '°S' : '°N') + ' ' + eastFixed + (east < 0 ? '°W' : '°E'); + } + } + + /** + * Get link to maps from latitude and longitude. + * + * @param {number} north Degrees north. + * @param {number} east Degrees East. + * @return {string} Link to maps depending on platform. + */ + getLatLongLink(north: number, east: number): string { + if (north !== null || east !== null) { + const northFixed = north ? north.toFixed(4) : '0.0000', + eastFixed = east ? east.toFixed(4) : '0.0000'; + + if (this.platform.is('ios')) { + return 'http://maps.apple.com/?ll=' + northFixed + ',' + eastFixed + '&near=' + northFixed + ',' + eastFixed; + } + + return 'geo:' + northFixed + ',' + eastFixed; + } + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.mode = this.mode == 'list' ? 'show' : this.mode; + this.render(); + } + + protected render(): void { + if (this.value) { + this.north = (this.value && parseFloat(this.value.content)) || null; + this.east = (this.value && parseFloat(this.value.content1)) || null; + } + } +} diff --git a/src/addon/mod/data/fields/latlong/latlong.module.ts b/src/addon/mod/data/fields/latlong/latlong.module.ts new file mode 100644 index 000000000..5b9b4f11a --- /dev/null +++ b/src/addon/mod/data/fields/latlong/latlong.module.ts @@ -0,0 +1,49 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonModDataFieldLatlongHandler } from './providers/handler'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { AddonModDataFieldLatlongComponent } from './component/latlong'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonModDataFieldLatlongComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModDataFieldLatlongHandler + ], + exports: [ + AddonModDataFieldLatlongComponent + ], + entryComponents: [ + AddonModDataFieldLatlongComponent + ] +}) +export class AddonModDataFieldLatlongModule { + constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldLatlongHandler) { + fieldDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/data/fields/latlong/providers/handler.ts b/src/addon/mod/data/fields/latlong/providers/handler.ts new file mode 100644 index 000000000..fb549c7c4 --- /dev/null +++ b/src/addon/mod/data/fields/latlong/providers/handler.ts @@ -0,0 +1,159 @@ +// (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 { Injector, Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonModDataFieldHandler } from '../../../providers/fields-delegate'; +import { AddonModDataFieldLatlongComponent } from '../component/latlong'; + +/** + * Handler for latlong data field plugin. + */ +@Injectable() +export class AddonModDataFieldLatlongHandler implements AddonModDataFieldHandler { + name = 'AddonModDataFieldLatlongHandler'; + type = 'latlong'; + + constructor(private translate: TranslateService) { } + + /** + * Return the Component to use to display the plugin data. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} field The field object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + return AddonModDataFieldLatlongComponent; + } + + /** + * Get field search data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @return {any} With name and value of the data to be sent. + */ + getFieldSearchData(field: any, inputData: any): any { + const fieldName = 'f_' + field.id; + + if (inputData[fieldName]) { + return [{ + name: fieldName, + value: inputData[fieldName] + }]; + } + + return false; + } + + /** + * Get field edit data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditData(field: any, inputData: any, originalFieldData: any): any { + const fieldName = 'f_' + field.id, + values = []; + + if (inputData[fieldName + '_0']) { + values.push({ + fieldid: field.id, + subfield: '0', + value: inputData[fieldName + '_0'] + }); + } + + if (inputData[fieldName + '_1']) { + values.push({ + fieldid: field.id, + subfield: '1', + value: inputData[fieldName + '_1'] + }); + } + + return values; + } + + /** + * Get field data in changed. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @param {any} originalFieldData Original field entered data. + * @return {Promise | boolean} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | boolean { + const fieldName = 'f_' + field.id, + lat = inputData[fieldName + '_0'] || '', + long = inputData[fieldName + '_1'] || '', + originalLat = (originalFieldData && originalFieldData.content) || '', + originalLong = (originalFieldData && originalFieldData.content1) || ''; + + return lat != originalLat || long != originalLong; + } + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string | false} String with the notification or false. + */ + getFieldsNotifications(field: any, inputData: any): string | false { + let valueCount = 0; + + // The lat long class has two values that need to be checked. + inputData.forEach((value) => { + if (typeof value.value != 'undefined' && value.value != '') { + valueCount++; + } + }); + + // If we get here then only one field has been filled in. + if (valueCount == 1) { + return this.translate.instant('addon.mod_data.latlongboth'); + } else if (field.required && valueCount == 0) { + return this.translate.instant('addon.mod_data.errormustsupplyvalue'); + } + + return false; + } + + /** + * Override field content data with offline submission. + * + * @param {any} originalContent Original data to be overriden. + * @param {any} offlineContent Array with all the offline data to override. + * @param {any} [offlineFiles] Array with all the offline files in the field. + * @return {any} Data overriden + */ + overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any { + originalContent.content = offlineContent[0] || ''; + originalContent.content1 = offlineContent[1] || ''; + + return originalContent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/menu/component/menu.html b/src/addon/mod/data/fields/menu/component/menu.html new file mode 100644 index 000000000..12f41a13f --- /dev/null +++ b/src/addon/mod/data/fields/menu/component/menu.html @@ -0,0 +1,9 @@ + + + + + {{ 'addon.mod_data.menuchoose' | translate }} + {{option}} + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/menu/component/menu.ts b/src/addon/mod/data/fields/menu/component/menu.ts new file mode 100644 index 000000000..08628a556 --- /dev/null +++ b/src/addon/mod/data/fields/menu/component/menu.ts @@ -0,0 +1,57 @@ +// (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, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; + +/** + * Component to render data menu field. + */ +@Component({ + selector: 'addon-mod-data-field-menu', + templateUrl: 'menu.html' +}) +export class AddonModDataFieldMenuComponent extends AddonModDataFieldPluginComponent implements OnInit { + + control: FormControl; + val: string; + options = []; + + constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, + element: ElementRef) { + super(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.mode = this.mode == 'list' ? 'show' : this.mode; + this.render(); + } + + protected render(): void { + if (this.mode == 'show') { + return; + } + + this.options = this.field.param1.split('\n'); + + if (this.mode == 'edit' && this.value) { + this.val = this.value.content; + } + } +} diff --git a/src/addon/mod/data/fields/menu/menu.module.ts b/src/addon/mod/data/fields/menu/menu.module.ts new file mode 100644 index 000000000..2c4a16427 --- /dev/null +++ b/src/addon/mod/data/fields/menu/menu.module.ts @@ -0,0 +1,49 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonModDataFieldMenuHandler } from './providers/handler'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { AddonModDataFieldMenuComponent } from './component/menu'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonModDataFieldMenuComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModDataFieldMenuHandler + ], + exports: [ + AddonModDataFieldMenuComponent + ], + entryComponents: [ + AddonModDataFieldMenuComponent + ] +}) +export class AddonModDataFieldMenuModule { + constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldMenuHandler) { + fieldDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/data/fields/menu/providers/handler.ts b/src/addon/mod/data/fields/menu/providers/handler.ts new file mode 100644 index 000000000..4e6938c28 --- /dev/null +++ b/src/addon/mod/data/fields/menu/providers/handler.ts @@ -0,0 +1,134 @@ +// (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 { Injector, Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonModDataFieldHandler } from '../../../providers/fields-delegate'; +import { AddonModDataFieldMenuComponent } from '../component/menu'; + +/** + * Handler for menu data field plugin. + */ +@Injectable() +export class AddonModDataFieldMenuHandler implements AddonModDataFieldHandler { + name = 'AddonModDataFieldMenuHandler'; + type = 'menu'; + + constructor(private translate: TranslateService) { } + + /** + * Return the Component to use to display the plugin data. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} field The field object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + return AddonModDataFieldMenuComponent; + } + + /** + * Get field search data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @return {any} With name and value of the data to be sent. + */ + getFieldSearchData(field: any, inputData: any): any { + const fieldName = 'f_' + field.id; + + if (inputData[fieldName]) { + return [{ + name: fieldName, + value: inputData[fieldName] + }]; + } + + return false; + } + + /** + * Get field edit data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditData(field: any, inputData: any, originalFieldData: any): any { + const fieldName = 'f_' + field.id; + + if (inputData[fieldName]) { + return [{ + fieldid: field.id, + value: inputData[fieldName] + }]; + } + + return false; + } + + /** + * Get field data in changed. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @param {any} originalFieldData Original field entered data. + * @return {Promise | boolean} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | boolean { + const fieldName = 'f_' + field.id, + input = inputData[fieldName] || ''; + originalFieldData = (originalFieldData && originalFieldData.content) || ''; + + return input != originalFieldData; + } + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string | false} String with the notification or false. + */ + getFieldsNotifications(field: any, inputData: any): string | false { + if (field.required && (!inputData || !inputData.length || !inputData[0].value)) { + return this.translate.instant('addon.mod_data.errormustsupplyvalue'); + } + + return false; + } + + /** + * Override field content data with offline submission. + * + * @param {any} originalContent Original data to be overriden. + * @param {any} offlineContent Array with all the offline data to override. + * @param {any} [offlineFiles] Array with all the offline files in the field. + * @return {any} Data overriden + */ + overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any { + originalContent.content = offlineContent[''] || ''; + + return originalContent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/multimenu/component/multimenu.html b/src/addon/mod/data/fields/multimenu/component/multimenu.html new file mode 100644 index 000000000..fbf41a7d7 --- /dev/null +++ b/src/addon/mod/data/fields/multimenu/component/multimenu.html @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/multimenu/component/multimenu.ts b/src/addon/mod/data/fields/multimenu/component/multimenu.ts new file mode 100644 index 000000000..c233c08c2 --- /dev/null +++ b/src/addon/mod/data/fields/multimenu/component/multimenu.ts @@ -0,0 +1,63 @@ +// (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, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; + +/** + * Component to render data multimenu field. + */ +@Component({ + selector: 'addon-mod-data-field-multimenu', + templateUrl: 'multimenu.html' +}) +export class AddonModDataFieldMultimenuComponent extends AddonModDataFieldPluginComponent implements OnInit { + + control: FormControl; + options = []; + + constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, + element: ElementRef) { + super(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.mode = this.mode == 'list' ? 'show' : this.mode; + this.render(); + } + + protected render(): void { + if (this.mode == 'show') { + return; + } + + this.options = this.field.param1.split('\n').map((option) => { + return { key: option, value: option }; + }); + + if (this.mode == 'edit' && this.value && this.value.content) { + this.value.content.split('##').forEach((value) => { + const x = this.options.findIndex((option) => value == option.key); + if (x >= 0) { + this.options[x].selected = true; + } + }); + } + } +} diff --git a/src/addon/mod/data/fields/multimenu/multimenu.module.ts b/src/addon/mod/data/fields/multimenu/multimenu.module.ts new file mode 100644 index 000000000..7b6b72d12 --- /dev/null +++ b/src/addon/mod/data/fields/multimenu/multimenu.module.ts @@ -0,0 +1,49 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonModDataFieldMultimenuHandler } from './providers/handler'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { AddonModDataFieldMultimenuComponent } from './component/multimenu'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonModDataFieldMultimenuComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModDataFieldMultimenuHandler + ], + exports: [ + AddonModDataFieldMultimenuComponent + ], + entryComponents: [ + AddonModDataFieldMultimenuComponent + ] +}) +export class AddonModDataFieldMultimenuModule { + constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldMultimenuHandler) { + fieldDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/data/fields/multimenu/providers/handler.ts b/src/addon/mod/data/fields/multimenu/providers/handler.ts new file mode 100644 index 000000000..9f65ee8bf --- /dev/null +++ b/src/addon/mod/data/fields/multimenu/providers/handler.ts @@ -0,0 +1,152 @@ +// (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 { Injector, Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonModDataFieldHandler } from '../../../providers/fields-delegate'; +import { AddonModDataFieldMultimenuComponent } from '../component/multimenu'; + +/** + * Handler for multimenu data field plugin. + */ +@Injectable() +export class AddonModDataFieldMultimenuHandler implements AddonModDataFieldHandler { + name = 'AddonModDataFieldMultimenuHandler'; + type = 'multimenu'; + + constructor(private translate: TranslateService) { } + + /** + * Return the Component to use to display the plugin data. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} field The field object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + return AddonModDataFieldMultimenuComponent; + } + + /** + * Get field search data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @return {any} With name and value of the data to be sent. + */ + getFieldSearchData(field: any, inputData: any): any { + const fieldName = 'f_' + field.id, + reqName = 'f_' + field.id + '_allreq'; + + if (inputData[fieldName].length > 0) { + const options = inputData[fieldName].split('###'), + values = []; + + if (options.length > 0) { + values.push({ + name: fieldName, + value: options + }); + + if (inputData[reqName]['1']) { + values.push({ + name: reqName, + value: true + }); + } + + return values; + } + } + + return false; + } + + /** + * Get field edit data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditData(field: any, inputData: any, originalFieldData: any): any { + const fieldName = 'f_' + field.id; + + if (inputData[fieldName] && inputData[fieldName].length > 0) { + const options = inputData[fieldName].split('###'); + if (options.length > 0) { + return [{ + fieldid: field.id, + value: options + }]; + } + } + + return false; + } + + /** + * Get field data in changed. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @param {any} originalFieldData Original field entered data. + * @return {Promise | boolean} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | boolean { + const fieldName = 'f_' + field.id, + input = inputData[fieldName] || ''; + originalFieldData = (originalFieldData && originalFieldData.content) || ''; + + return input != originalFieldData; + } + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string | false} String with the notification or false. + */ + getFieldsNotifications(field: any, inputData: any): string | false { + if (field.required && (!inputData || !inputData.length || !inputData[0].value)) { + return this.translate.instant('addon.mod_data.errormustsupplyvalue'); + } + + return false; + } + + /** + * Override field content data with offline submission. + * + * @param {any} originalContent Original data to be overriden. + * @param {any} offlineContent Array with all the offline data to override. + * @param {any} [offlineFiles] Array with all the offline files in the field. + * @return {any} Data overriden + */ + overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any { + originalContent.content = (offlineContent[''] && offlineContent[''].join('###')) || ''; + + return originalContent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/number/component/number.html b/src/addon/mod/data/fields/number/component/number.html new file mode 100644 index 000000000..41bbe1fb0 --- /dev/null +++ b/src/addon/mod/data/fields/number/component/number.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/number/component/number.ts b/src/addon/mod/data/fields/number/component/number.ts new file mode 100644 index 000000000..c463ea5e4 --- /dev/null +++ b/src/addon/mod/data/fields/number/component/number.ts @@ -0,0 +1,54 @@ +// (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, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; + +/** + * Component to render data number field. + */ +@Component({ + selector: 'addon-mod-data-field-number', + templateUrl: 'number.html' +}) +export class AddonModDataFieldNumberComponent extends AddonModDataFieldPluginComponent implements OnInit { + + control: FormControl; + val: number; + + constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, + element: ElementRef) { + super(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.mode = this.mode == 'list' ? 'show' : this.mode; + this.render(); + } + + protected render(): void { + if (this.mode == 'show') { + return; + } + + if (this.mode == 'edit' && this.value) { + this.val = this.value && parseFloat(this.value.content); + } + } +} diff --git a/src/addon/mod/data/fields/number/number.module.ts b/src/addon/mod/data/fields/number/number.module.ts new file mode 100644 index 000000000..ce2acc789 --- /dev/null +++ b/src/addon/mod/data/fields/number/number.module.ts @@ -0,0 +1,49 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonModDataFieldNumberHandler } from './providers/handler'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { AddonModDataFieldNumberComponent } from './component/number'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonModDataFieldNumberComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModDataFieldNumberHandler + ], + exports: [ + AddonModDataFieldNumberComponent + ], + entryComponents: [ + AddonModDataFieldNumberComponent + ] +}) +export class AddonModDataFieldNumberModule { + constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldNumberHandler) { + fieldDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/data/fields/number/providers/handler.ts b/src/addon/mod/data/fields/number/providers/handler.ts new file mode 100644 index 000000000..654681d72 --- /dev/null +++ b/src/addon/mod/data/fields/number/providers/handler.ts @@ -0,0 +1,57 @@ +// (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 { Injector, Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonModDataFieldTextHandler } from '../../text/providers/handler'; +import { AddonModDataFieldNumberComponent } from '../component/number'; + +/** + * Handler for number data field plugin. + */ +@Injectable() +export class AddonModDataFieldNumberHandler extends AddonModDataFieldTextHandler { + name = 'AddonModDataFieldNumberHandler'; + type = 'number'; + + constructor(protected translate: TranslateService) { + super(translate); + } + + /** + * Return the Component to use to display the plugin data. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} field The field object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + return AddonModDataFieldNumberComponent; + } + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string | false} String with the notification or false. + */ + getFieldsNotifications(field: any, inputData: any): string | false { + if (field.required && (!inputData || !inputData.length || inputData[0].value == '')) { + return this.translate.instant('addon.mod_data.errormustsupplyvalue'); + } + + return false; + } +} diff --git a/src/addon/mod/data/fields/picture/component/picture.html b/src/addon/mod/data/fields/picture/component/picture.html new file mode 100644 index 000000000..e435409f8 --- /dev/null +++ b/src/addon/mod/data/fields/picture/component/picture.html @@ -0,0 +1,17 @@ + + + + + + + + + + {{ 'addon.mod_data.alttext' | translate }} + + °N + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/picture/component/picture.ts b/src/addon/mod/data/fields/picture/component/picture.ts new file mode 100644 index 000000000..b4516a4a3 --- /dev/null +++ b/src/addon/mod/data/fields/picture/component/picture.ts @@ -0,0 +1,128 @@ +// (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, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; +import { CoreFileSessionProvider } from '@providers/file-session'; +import { AddonModDataProvider } from '../../../providers/data'; + +/** + * Component to render data picture field. + */ +@Component({ + selector: 'addon-mod-data-field-picture', + templateUrl: 'picture.html' +}) +export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginComponent implements OnInit { + + control: FormControl; + files = []; + component: string; + componentId: number; + maxSizeBytes: number; + + image: any; + entryId: number; + imageUrl: string; + title: string; + alttext: string; + width: string; + height: string; + + constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, + element: ElementRef, private fileSessionprovider: CoreFileSessionProvider) { + super(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.render(); + } + + /** + * Get the files from the input value. + * + * @param {any} value Input value. + * @return {any} List of files. + */ + protected getFiles(value: any): any { + let files = (value && value.files) || []; + + // Reduce to first element. + if (files.length > 0) { + files = [files[0]]; + } + + return files; + } + + /** + * Find file in a list. + * + * @param {any[]} files File list where to search. + * @param {string} filenameSeek Filename to search. + * @return {any} File found or false. + */ + protected findFile(files: any[], filenameSeek: string): any { + return files.find((file) => file.filename == filenameSeek) || false; + } + + protected render(): void { + if (this.mode != 'search') { + this.component = AddonModDataProvider.COMPONENT; + this.componentId = this.database.coursemodule; + + // Edit mode, the list shouldn't change so there is no need to watch it. + const files = this.value && this.value.files || []; + + // Get image or thumb. + if (files.length > 0) { + const filenameSeek = this.mode == 'list' ? 'thumb_' + this.value.content : this.value.content; + this.image = this.findFile(files, filenameSeek); + + if (!this.image && this.mode == 'list') { + this.image = this.findFile(files, this.value.content); + } + + this.files = [this.image]; + } else { + this.image = false; + this.files = []; + } + + if (this.mode == 'edit') { + this.maxSizeBytes = parseInt(this.field.param3, 10); + this.fileSessionprovider.setFiles(this.component, this.database.id + '_' + this.field.id, this.files); + this.alttext = (this.value && this.value.content1) || ''; + } else { + this.entryId = (this.value && this.value.recordid) || null; + this.title = (this.value && this.value.content1) || ''; + this.imageUrl = null; + if (this.image) { + if (this.image.offline) { + this.imageUrl = (this.image && this.image.toURL()) || null; + } else { + this.imageUrl = (this.image && this.image.fileurl) || null; + } + } + this.width = this.field.param1 || ''; + this.height = this.field.param2 || ''; + } + } + } +} diff --git a/src/addon/mod/data/fields/picture/picture.module.ts b/src/addon/mod/data/fields/picture/picture.module.ts new file mode 100644 index 000000000..c666e1bad --- /dev/null +++ b/src/addon/mod/data/fields/picture/picture.module.ts @@ -0,0 +1,49 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonModDataFieldPictureHandler } from './providers/handler'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { AddonModDataFieldPictureComponent } from './component/picture'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonModDataFieldPictureComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + ], + providers: [ + AddonModDataFieldPictureHandler + ], + exports: [ + AddonModDataFieldPictureComponent + ], + entryComponents: [ + AddonModDataFieldPictureComponent + ] +}) +export class AddonModDataFieldPictureModule { + constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldPictureHandler) { + fieldDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/data/fields/picture/providers/handler.ts b/src/addon/mod/data/fields/picture/providers/handler.ts new file mode 100644 index 000000000..a5eab43b0 --- /dev/null +++ b/src/addon/mod/data/fields/picture/providers/handler.ts @@ -0,0 +1,194 @@ +// (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 { Injector, Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreFileSessionProvider } from '@providers/file-session'; +import { AddonModDataFieldHandler } from '../../../providers/fields-delegate'; +import { AddonModDataProvider } from '../../../providers/data'; +import { AddonModDataFieldPictureComponent } from '../component/picture'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; + +/** + * Handler for picture data field plugin. + */ +@Injectable() +export class AddonModDataFieldPictureHandler implements AddonModDataFieldHandler { + name = 'AddonModDataFieldPictureHandler'; + type = 'picture'; + + constructor(private translate: TranslateService, private fileSessionprovider: CoreFileSessionProvider, + private fileUploaderProvider: CoreFileUploaderProvider) { } + + /** + * Return the Component to use to display the plugin data. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} field The field object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + return AddonModDataFieldPictureComponent; + } + + /** + * Get field search data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @return {any} With name and value of the data to be sent. + */ + getFieldSearchData(field: any, inputData: any): any { + const fieldName = 'f_' + field.id; + + if (inputData[fieldName]) { + return [{ + name: fieldName, + value: inputData[fieldName] + }]; + } + + return false; + } + + /** + * Get field edit data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditData(field: any, inputData: any, originalFieldData: any): any { + const files = this.getFieldEditFiles(field), + values = [], + fieldName = 'f_' + field.id + '_alttext'; + + if (files.length) { + values.push({ + fieldid: field.id, + subfield: 'file', + files: files + }); + } + + if (inputData[fieldName]) { + values.push({ + fieldid: field.id, + subfield: 'alttext', + value: inputData[fieldName] + }); + } + + return values; + } + + /** + * Get field edit files in the input data. + * + * @param {any} field Defines the field.. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditFiles(field: any): any { + return this.fileSessionprovider.getFiles(AddonModDataProvider.COMPONENT, field.dataid + '_' + field.id); + } + + /** + * Get field data in changed. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @param {any} originalFieldData Original field entered data. + * @return {Promise | boolean} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | boolean { + const fieldName = 'f_' + field.id + '_alttext', + altText = inputData[fieldName] || '', + originalAltText = (originalFieldData && originalFieldData.content1) || '', + files = this.getFieldEditFiles(field) || []; + let originalFiles = (originalFieldData && originalFieldData.files) || []; + + // Get image. + if (originalFiles.length > 0) { + const filenameSeek = (originalFieldData && originalFieldData.content) || '', + file = originalFiles.find((file) => file.filename == filenameSeek); + if (file) { + originalFiles = [file]; + } + } else { + originalFiles = []; + } + + return altText != originalAltText || this.fileUploaderProvider.areFileListDifferent(files, originalFiles); + } + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string | false} String with the notification or false. + */ + getFieldsNotifications(field: any, inputData: any): string | false { + if (field.required) { + if (!inputData || !inputData.length) { + return this.translate.instant('addon.mod_data.errormustsupplyvalue'); + } + + const found = inputData.some((input) => { + if (typeof input.subfield != 'undefined' && input.subfield == 'file') { + return !!input.value; + } + + return false; + }); + + if (!found) { + return this.translate.instant('addon.mod_data.errormustsupplyvalue'); + } + } + + return false; + } + + /** + * Override field content data with offline submission. + * + * @param {any} originalContent Original data to be overriden. + * @param {any} offlineContent Array with all the offline data to override. + * @param {any} [offlineFiles] Array with all the offline files in the field. + * @return {any} Data overriden + */ + overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any { + if (offlineContent && offlineContent.file && offlineContent.file.offline > 0 && offlineFiles && offlineFiles.length > 0) { + originalContent.content = offlineFiles[0].filename; + originalContent.files = [offlineFiles[0]]; + } else if (offlineContent && offlineContent.file && offlineContent.file.online && offlineContent.file.online.length > 0) { + originalContent.content = offlineContent.file.online[0].filename; + originalContent.files = [offlineContent.file.online[0]]; + } + + originalContent.content1 = offlineContent.alttext || ''; + + return originalContent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/radiobutton/component/radiobutton.html b/src/addon/mod/data/fields/radiobutton/component/radiobutton.html new file mode 100644 index 000000000..12f41a13f --- /dev/null +++ b/src/addon/mod/data/fields/radiobutton/component/radiobutton.html @@ -0,0 +1,9 @@ + + + + + {{ 'addon.mod_data.menuchoose' | translate }} + {{option}} + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/radiobutton/component/radiobutton.ts b/src/addon/mod/data/fields/radiobutton/component/radiobutton.ts new file mode 100644 index 000000000..208d5d2cb --- /dev/null +++ b/src/addon/mod/data/fields/radiobutton/component/radiobutton.ts @@ -0,0 +1,57 @@ +// (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, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; + +/** + * Component to render data radiobutton field. + */ +@Component({ + selector: 'addon-mod-data-field-radiobutton', + templateUrl: 'radiobutton.html' +}) +export class AddonModDataFieldRadiobuttonComponent extends AddonModDataFieldPluginComponent implements OnInit { + + control: FormControl; + options: number; + val: number; + + constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, + element: ElementRef) { + super(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.mode = this.mode == 'list' ? 'show' : this.mode; + this.render(); + } + + protected render(): void { + if (this.mode == 'show') { + return; + } + + this.options = this.field.param1.split('\n'); + + if (this.mode == 'edit' && this.value) { + this.val = this.value.content; + } + } +} diff --git a/src/addon/mod/data/fields/radiobutton/providers/handler.ts b/src/addon/mod/data/fields/radiobutton/providers/handler.ts new file mode 100644 index 000000000..6efbd744b --- /dev/null +++ b/src/addon/mod/data/fields/radiobutton/providers/handler.ts @@ -0,0 +1,133 @@ +// (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 { Injector, Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonModDataFieldHandler } from '../../../providers/fields-delegate'; +import { AddonModDataFieldRadiobuttonComponent } from '../component/radiobutton'; + +/** + * Handler for checkbox data field plugin. + */ +@Injectable() +export class AddonModDataFieldRadiobuttonHandler implements AddonModDataFieldHandler { + name = 'AddonModDataFieldRadiobuttonHandler'; + type = 'radiobutton'; + + constructor(private translate: TranslateService) { } + + /** + * Return the Component to use to display the plugin data. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} field The field object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + return AddonModDataFieldRadiobuttonComponent; + } + + /** + * Get field search data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @return {any} With name and value of the data to be sent. + */ + getFieldSearchData(field: any, inputData: any): any { + const fieldName = 'f_' + field.id; + if (inputData[fieldName]) { + return [{ + name: fieldName, + value: inputData[fieldName] + }]; + } + + return false; + } + + /** + * Get field edit data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditData(field: any, inputData: any, originalFieldData: any): any { + const fieldName = 'f_' + field.id; + + if (inputData[fieldName]) { + return [{ + fieldid: field.id, + value: inputData[fieldName] + }]; + } + + return false; + } + + /** + * Get field data in changed. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @param {any} originalFieldData Original field entered data. + * @return {Promise | boolean} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | boolean { + const fieldName = 'f_' + field.id, + input = inputData[fieldName] || ''; + originalFieldData = (originalFieldData && originalFieldData.content) || ''; + + return input != originalFieldData; + } + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string | false} String with the notification or false. + */ + getFieldsNotifications(field: any, inputData: any): string | false { + if (field.required && (!inputData || !inputData.length || !inputData[0].value)) { + return this.translate.instant('addon.mod_data.errormustsupplyvalue'); + } + + return false; + } + + /** + * Override field content data with offline submission. + * + * @param {any} originalContent Original data to be overriden. + * @param {any} offlineContent Array with all the offline data to override. + * @param {any} [offlineFiles] Array with all the offline files in the field. + * @return {any} Data overriden + */ + overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any { + originalContent.content = offlineContent[''] || ''; + + return originalContent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/radiobutton/radiobutton.module.ts b/src/addon/mod/data/fields/radiobutton/radiobutton.module.ts new file mode 100644 index 000000000..d361bc934 --- /dev/null +++ b/src/addon/mod/data/fields/radiobutton/radiobutton.module.ts @@ -0,0 +1,49 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonModDataFieldRadiobuttonHandler } from './providers/handler'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { AddonModDataFieldRadiobuttonComponent } from './component/radiobutton'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonModDataFieldRadiobuttonComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModDataFieldRadiobuttonHandler + ], + exports: [ + AddonModDataFieldRadiobuttonComponent + ], + entryComponents: [ + AddonModDataFieldRadiobuttonComponent + ] +}) +export class AddonModDataFieldRadiobuttonModule { + constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldRadiobuttonHandler) { + fieldDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/data/fields/text/component/text.html b/src/addon/mod/data/fields/text/component/text.html new file mode 100644 index 000000000..d907c4a9c --- /dev/null +++ b/src/addon/mod/data/fields/text/component/text.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/text/component/text.ts b/src/addon/mod/data/fields/text/component/text.ts new file mode 100644 index 000000000..174bc1cd2 --- /dev/null +++ b/src/addon/mod/data/fields/text/component/text.ts @@ -0,0 +1,54 @@ +// (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, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; + +/** + * Component to render data text field. + */ +@Component({ + selector: 'addon-mod-data-field-text', + templateUrl: 'text.html' +}) +export class AddonModDataFieldTextComponent extends AddonModDataFieldPluginComponent implements OnInit { + + control: FormControl; + val: number; + + constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, + element: ElementRef) { + super(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.mode = this.mode == 'list' ? 'show' : this.mode; + this.render(); + } + + protected render(): void { + if (this.mode == 'show') { + return; + } + + if (this.mode == 'edit' && this.value) { + this.val = this.value.content; + } + } +} diff --git a/src/addon/mod/data/fields/text/providers/handler.ts b/src/addon/mod/data/fields/text/providers/handler.ts new file mode 100644 index 000000000..19d49b713 --- /dev/null +++ b/src/addon/mod/data/fields/text/providers/handler.ts @@ -0,0 +1,134 @@ +// (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 { Injector, Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonModDataFieldHandler } from '../../../providers/fields-delegate'; +import { AddonModDataFieldTextComponent } from '../component/text'; + +/** + * Handler for number data field plugin. + */ +@Injectable() +export class AddonModDataFieldTextHandler implements AddonModDataFieldHandler { + name = 'AddonModDataFieldTextHandler'; + type = 'text'; + + constructor(protected translate: TranslateService) { } + + /** + * Return the Component to use to display the plugin data. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} field The field object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + return AddonModDataFieldTextComponent; + } + + /** + * Get field search data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the search form. + * @return {any} With name and value of the data to be sent. + */ + getFieldSearchData(field: any, inputData: any): any { + const fieldName = 'f_' + field.id; + + if (inputData[fieldName]) { + return [{ + name: fieldName, + value: inputData[fieldName] + }]; + } + + return false; + } + + /** + * Get field edit data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditData(field: any, inputData: any, originalFieldData: any): any { + const fieldName = 'f_' + field.id; + + if (inputData[fieldName]) { + return [{ + fieldid: field.id, + value: inputData[fieldName] + }]; + } + + return false; + } + + /** + * Get field data in changed. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @param {any} originalFieldData Original field entered data. + * @return {Promise | boolean} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | boolean { + const fieldName = 'f_' + field.id, + input = inputData[fieldName] || ''; + originalFieldData = (originalFieldData && originalFieldData.content) || ''; + + return input != originalFieldData; + } + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string | false} String with the notification or false. + */ + getFieldsNotifications(field: any, inputData: any): string | false { + if (field.required && (!inputData || !inputData.length || !inputData[0].value)) { + return this.translate.instant('addon.mod_data.errormustsupplyvalue'); + } + + return false; + } + + /** + * Override field content data with offline submission. + * + * @param {any} originalContent Original data to be overriden. + * @param {any} offlineContent Array with all the offline data to override. + * @param {any} [offlineFiles] Array with all the offline files in the field. + * @return {any} Data overriden + */ + overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any { + originalContent.content = offlineContent[''] || ''; + + return originalContent; + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return {boolean|Promise} True or promise resolved with true if enabled. + */ + isEnabled(): boolean | Promise { + return true; + } +} diff --git a/src/addon/mod/data/fields/text/text.module.ts b/src/addon/mod/data/fields/text/text.module.ts new file mode 100644 index 000000000..a96ddf9de --- /dev/null +++ b/src/addon/mod/data/fields/text/text.module.ts @@ -0,0 +1,49 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonModDataFieldTextHandler } from './providers/handler'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { AddonModDataFieldTextComponent } from './component/text'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonModDataFieldTextComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModDataFieldTextHandler + ], + exports: [ + AddonModDataFieldTextComponent + ], + entryComponents: [ + AddonModDataFieldTextComponent + ] +}) +export class AddonModDataFieldTextModule { + constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldTextHandler) { + fieldDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/data/fields/textarea/component/textarea.html b/src/addon/mod/data/fields/textarea/component/textarea.html new file mode 100644 index 000000000..5a625183b --- /dev/null +++ b/src/addon/mod/data/fields/textarea/component/textarea.html @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/textarea/component/textarea.ts b/src/addon/mod/data/fields/textarea/component/textarea.ts new file mode 100644 index 000000000..83336cff7 --- /dev/null +++ b/src/addon/mod/data/fields/textarea/component/textarea.ts @@ -0,0 +1,69 @@ +// (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, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModDataProvider } from '../../../providers/data'; +import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; + +/** + * Component to render data number field. + */ +@Component({ + selector: 'addon-mod-data-field-textarea', + templateUrl: 'textarea.html' +}) +export class AddonModDataFieldTextareaComponent extends AddonModDataFieldPluginComponent implements OnInit { + + control: FormControl; + component: string; + componentId: number; + + constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, + element: ElementRef) { + super(); + } + + format(value: any): string { + const files = (value && value.files) || []; + + return value ? this.textUtils.replacePluginfileUrls(value.content, files) : ''; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.mode = this.mode == 'list' ? 'show' : this.mode; + this.render(); + } + + protected render(): void { + if (this.mode == 'show') { + this.component = AddonModDataProvider.COMPONENT; + this.componentId = this.database.coursemodule; + + return; + } + + // Check if rich text editor is enabled. + if (this.mode == 'edit') { + const files = (this.value && this.value.files) || [], + text = this.value ? this.textUtils.replacePluginfileUrls(this.value.content, files) : ''; + + this.control = this.fb.control(text); + } + } +} diff --git a/src/addon/mod/data/fields/textarea/providers/handler.ts b/src/addon/mod/data/fields/textarea/providers/handler.ts new file mode 100644 index 000000000..0a5829698 --- /dev/null +++ b/src/addon/mod/data/fields/textarea/providers/handler.ts @@ -0,0 +1,145 @@ +// (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 { Injector, Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonModDataFieldTextHandler } from '../../text/providers/handler'; +import { AddonModDataFieldTextareaComponent } from '../component/textarea'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; + +/** + * Handler for textarea data field plugin. + */ +@Injectable() +export class AddonModDataFieldTextareaHandler extends AddonModDataFieldTextHandler { + name = 'AddonModDataFieldTextareaHandler'; + type = 'textarea'; + + constructor(protected translate: TranslateService, private textUtils: CoreTextUtilsProvider, + private domUtils: CoreDomUtilsProvider) { + super(translate); + } + + /** + * Return the Component to use to display the plugin data. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} field The field object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + return AddonModDataFieldTextareaComponent; + } + + /** + * Get field edit data in the input data. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditData(field: any, inputData: any, originalFieldData: any): any { + const fieldName = 'f_' + field.id; + + if (inputData[fieldName]) { + return this.domUtils.isRichTextEditorEnabled().then((enabled) => { + const files = this.getFieldEditFiles(field, inputData, originalFieldData); + let text = this.textUtils.restorePluginfileUrls(inputData[fieldName], files); + + if (!enabled) { + // Rich text editor not enabled, add some HTML to the text if needed. + text = this.textUtils.formatHtmlLines(text); + } + + return [{ + fieldid: field.id, + value: text + }, + { + fieldid: field.id, + subfield: 'content1', + value: 1 + }, + { + fieldid: field.id, + subfield: 'itemid', + files: files + } + ]; + }); + } + + return false; + } + + /** + * Get field edit files in the input data. + * + * @param {any} field Defines the field.. + * @param {any} inputData Data entered in the edit form. + * @param {any} originalFieldData Original field entered data. + * @return {any} With name and value of the data to be sent. + */ + getFieldEditFiles(field: any, inputData: any, originalFieldData: any): any { + return (originalFieldData && originalFieldData.files) || []; + } + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string | false} String with the notification or false. + */ + getFieldsNotifications(field: any, inputData: any): string | false { + if (field.required) { + if (!inputData || !inputData.length) { + return this.translate.instant('addon.mod_data.errormustsupplyvalue'); + } + + const found = inputData.some((input) => { + if (!input.subfield) { + return !!input.value; + } + + return false; + }); + + if (!found) { + return this.translate.instant('addon.mod_data.errormustsupplyvalue'); + } + } + + return false; + } + + /** + * Override field content data with offline submission. + * + * @param {any} originalContent Original data to be overriden. + * @param {any} offlineContent Array with all the offline data to override. + * @param {any} [offlineFiles] Array with all the offline files in the field. + * @return {any} Data overriden + */ + overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any { + originalContent.content = offlineContent[''] || ''; + if (originalContent.content.length > 0 && originalContent.files && originalContent.files.length > 0) { + // Take the original files since we cannot edit them on the app. + originalContent.content = this.textUtils.replacePluginfileUrls(originalContent.content, originalContent.files); + } + + return originalContent; + } +} diff --git a/src/addon/mod/data/fields/textarea/textarea.module.ts b/src/addon/mod/data/fields/textarea/textarea.module.ts new file mode 100644 index 000000000..8c63d53f7 --- /dev/null +++ b/src/addon/mod/data/fields/textarea/textarea.module.ts @@ -0,0 +1,49 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonModDataFieldTextareaHandler } from './providers/handler'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { AddonModDataFieldTextareaComponent } from './component/textarea'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonModDataFieldTextareaComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModDataFieldTextareaHandler + ], + exports: [ + AddonModDataFieldTextareaComponent + ], + entryComponents: [ + AddonModDataFieldTextareaComponent + ] +}) +export class AddonModDataFieldTextareaModule { + constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldTextareaHandler) { + fieldDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/data/fields/url/component/url.html b/src/addon/mod/data/fields/url/component/url.html new file mode 100644 index 000000000..ddd31d55f --- /dev/null +++ b/src/addon/mod/data/fields/url/component/url.html @@ -0,0 +1,7 @@ + + + + + + +{{field.name}} \ No newline at end of file diff --git a/src/addon/mod/data/fields/url/component/url.ts b/src/addon/mod/data/fields/url/component/url.ts new file mode 100644 index 000000000..15ac1f53f --- /dev/null +++ b/src/addon/mod/data/fields/url/component/url.ts @@ -0,0 +1,54 @@ +// (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, ElementRef } from '@angular/core'; +import { FormBuilder, FormControl } from '@angular/forms'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; + +/** + * Component to render data url field. + */ +@Component({ + selector: 'addon-mod-data-field-url', + templateUrl: 'url.html' +}) +export class AddonModDataFieldUrlComponent extends AddonModDataFieldPluginComponent implements OnInit { + + control: FormControl; + val: number; + + constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, + element: ElementRef) { + super(); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.mode = this.mode == 'list' ? 'show' : this.mode; + this.render(); + } + + protected render(): void { + if (this.mode == 'show') { + return; + } + + if (this.mode == 'edit' && this.value) { + this.val = this.value.content; + } + } +} diff --git a/src/addon/mod/data/fields/url/providers/handler.ts b/src/addon/mod/data/fields/url/providers/handler.ts new file mode 100644 index 000000000..7e6ea803e --- /dev/null +++ b/src/addon/mod/data/fields/url/providers/handler.ts @@ -0,0 +1,57 @@ +// (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 { Injector, Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonModDataFieldTextHandler } from '../../text/providers/handler'; +import { AddonModDataFieldUrlComponent } from '../component/url'; + +/** + * Handler for url data field plugin. + */ +@Injectable() +export class AddonModDataFieldUrlHandler extends AddonModDataFieldTextHandler { + name = 'AddonModDataFieldUrlHandler'; + type = 'url'; + + constructor(protected translate: TranslateService) { + super(translate); + } + + /** + * Return the Component to use to display the plugin data. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} field The field object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + return AddonModDataFieldUrlComponent; + } + + /** + * Check and get field requeriments. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @return {string | false} String with the notification or false. + */ + getFieldsNotifications(field: any, inputData: any): string | false { + if (field.required && (!inputData || !inputData.length || !inputData[0].value)) { + return this.translate.instant('addon.mod_data.errormustsupplyvalue'); + } + + return false; + } +} diff --git a/src/addon/mod/data/fields/url/url.module.ts b/src/addon/mod/data/fields/url/url.module.ts new file mode 100644 index 000000000..09cd8911e --- /dev/null +++ b/src/addon/mod/data/fields/url/url.module.ts @@ -0,0 +1,49 @@ +// (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 { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonModDataFieldUrlHandler } from './providers/handler'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { AddonModDataFieldUrlComponent } from './component/url'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonModDataFieldUrlComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule + ], + providers: [ + AddonModDataFieldUrlHandler + ], + exports: [ + AddonModDataFieldUrlComponent + ], + entryComponents: [ + AddonModDataFieldUrlComponent + ] +}) +export class AddonModDataFieldUrlModule { + constructor(fieldDelegate: AddonModDataFieldsDelegate, handler: AddonModDataFieldUrlHandler) { + fieldDelegate.registerHandler(handler); + } +} diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index 43ffe33fe..8f929e8b0 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -113,7 +113,7 @@ export class AddonModDataHelperProvider { replace = new RegExp(replace, 'gi'); // Replace field by a generic directive. - const render = ''; template = template.replace(replace, render); }); @@ -156,13 +156,13 @@ export class AddonModDataHelperProvider { replace = new RegExp(replace, 'gi'); // Replace field by a generic directive. - render = ''; template = template.replace(replace, render); }); - for (const action in actions) { + /*for (const action in actions) { replace = new RegExp('##' + action + '##', 'gi'); // Is enabled? if (actions[action]) { @@ -179,7 +179,7 @@ export class AddonModDataHelperProvider { } else { template = template.replace(replace, ''); } - } + }*/ return template; } diff --git a/src/addon/mod/feedback/providers/helper.ts b/src/addon/mod/feedback/providers/helper.ts index 7f5527623..cbd1ea4d5 100644 --- a/src/addon/mod/feedback/providers/helper.ts +++ b/src/addon/mod/feedback/providers/helper.ts @@ -47,7 +47,6 @@ export class AddonModFeedbackHelperProvider { protected getActivityHistoryBackCounter(pageName: string, instance: number, paramName: string, prefix: string, navCtrl: NavController): number { let historyInstance, params, - backTimes = 0, view = navCtrl.getActive(); while (!view.isFirst()) { @@ -60,9 +59,7 @@ export class AddonModFeedbackHelperProvider { historyInstance = params.get(paramName) ? params.get(paramName) : params.get('module').instance; // Check we are not changing to another activity. - if (historyInstance && historyInstance == instance) { - backTimes++; - } else { + if (!historyInstance || historyInstance != instance) { break; } diff --git a/src/core/compile/components/compile-html/compile-html.ts b/src/core/compile/components/compile-html/compile-html.ts index c530b08f2..8b7a7a218 100644 --- a/src/core/compile/components/compile-html/compile-html.ts +++ b/src/core/compile/components/compile-html/compile-html.ts @@ -42,6 +42,8 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { @Input() text: string; // The HTML text to display. @Input() javascript: string; // The Javascript to execute in the component. @Input() jsData: any; // Data to pass to the fake component. + @Input() extraImports: any[] = []; // Extra import modules. + @Input() extraProviders: any[] = []; // Extra providers. @Output() created: EventEmitter = new EventEmitter(); // Will emit an event when the component is instantiated. // Get the container where to put the content. @@ -61,7 +63,8 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { ngOnChanges(changes: { [name: string]: SimpleChange }): void { if ((changes.text || changes.javascript) && this.text) { // Create a new component and a new module. - this.compileProvider.createAndCompileComponent(this.text, this.getComponentClass()).then((factory) => { + this.compileProvider.createAndCompileComponent(this.text, this.getComponentClass(), this.extraImports) + .then((factory) => { // Destroy previous components. this.componentRef && this.componentRef.destroy(); @@ -95,7 +98,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { constructor() { // If there is some javascript to run, prepare the instance. if (compileInstance.javascript) { - compileInstance.compileProvider.injectLibraries(this); + compileInstance.compileProvider.injectLibraries(this, compileInstance.extraProviders); } // Always add these elements, they could be needed on component init (componentObservable). diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index 828238d0f..411b62fee 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -112,17 +112,20 @@ export class CoreCompileProvider { * * @param {string} template The template of the component. * @param {any} componentClass The JS class of the component. + * @param {any[]} [extraImports] Extra imported modules if needed and not imported by this class. * @return {Promise>} Promise resolved with the factory to instantiate the component. */ - createAndCompileComponent(template: string, componentClass: any): Promise> { + createAndCompileComponent(template: string, componentClass: any, extraImports: any[] = []): Promise> { // Create the component using the template and the class. const component = Component({ template: template }) (componentClass); + const imports = this.IMPORTS.concat(extraImports); + // Now create the module containing the component. - const module = NgModule({imports: this.IMPORTS, declarations: [component]})(class {}); + const module = NgModule({imports: imports, declarations: [component]})(class {}); // Compile the module and the component. return this.compiler.compileModuleAndAllComponentsAsync(module).then((factories) => { @@ -166,13 +169,14 @@ export class CoreCompileProvider { * Inject all the core libraries in a certain object. * * @param {any} instance The instance where to inject the libraries. + * @param {any[]} [extraProviders] Extra imported providers if needed and not imported by this class. */ - injectLibraries(instance: any): void { + injectLibraries(instance: any, extraProviders: any[] = []): void { const providers = ( CORE_PROVIDERS).concat(CORE_CONTENTLINKS_PROVIDERS).concat(CORE_COURSE_PROVIDERS) .concat(CORE_COURSES_PROVIDERS).concat(CORE_FILEUPLOADER_PROVIDERS).concat(CORE_GRADES_PROVIDERS) .concat(CORE_LOGIN_PROVIDERS).concat(CORE_MAINMENU_PROVIDERS).concat(CORE_SHAREDFILES_PROVIDERS) .concat(CORE_SITEHOME_PROVIDERS).concat([CoreSitePluginsProvider]).concat(CORE_USER_PROVIDERS) - .concat(CORE_QUESTION_PROVIDERS).concat(IONIC_NATIVE_PROVIDERS).concat(this.OTHER_PROVIDERS); + .concat(CORE_QUESTION_PROVIDERS).concat(IONIC_NATIVE_PROVIDERS).concat(this.OTHER_PROVIDERS).concat(extraProviders); // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance. for (const i in providers) { From 44e8acbb5e9fb21a3232900c0f0b5c6b0e3d0271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 8 May 2018 15:10:32 +0200 Subject: [PATCH 03/10] MOBILE-2338 data: Add actions --- .../mod/data/components/action/action.html | 34 +++++++ .../mod/data/components/action/action.ts | 92 +++++++++++++++++++ .../mod/data/components/components.module.ts | 13 ++- .../components/field-plugin/field-plugin.ts | 2 +- src/addon/mod/data/providers/helper.ts | 8 +- src/addon/mod/data/providers/offline.ts | 15 +++ 6 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 src/addon/mod/data/components/action/action.html create mode 100644 src/addon/mod/data/components/action/action.ts diff --git a/src/addon/mod/data/components/action/action.html b/src/addon/mod/data/components/action/action.html new file mode 100644 index 000000000..9588699a2 --- /dev/null +++ b/src/addon/mod/data/components/action/action.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +{{ entry.timecreated * 1000 | coreFormatDate:"dffulldate" }} +{{ entry.timemodified * 1000 | coreFormatDate:"dffulldate" }} + + + + + +{{entry.fullname}} diff --git a/src/addon/mod/data/components/action/action.ts b/src/addon/mod/data/components/action/action.ts new file mode 100644 index 000000000..0cdb76f45 --- /dev/null +++ b/src/addon/mod/data/components/action/action.ts @@ -0,0 +1,92 @@ +// (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, Input, OnInit, Injector } from '@angular/core'; +import { CoreEventsProvider } from '@providers/events'; +import { AddonModDataProvider } from '../../providers/data'; +import { AddonModDataOfflineProvider } from '../../providers/offline'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUserProvider } from '@core/user/providers/user'; + +/** + * Component that displays a database action. + */ +@Component({ + selector: 'addon-mod-data-action', + templateUrl: 'action.html', +}) +export class AddonModDataActionComponent implements OnInit { + @Input() mode: string; // The render mode. + @Input() action: string; // The field to render. + @Input() entry?: any; // The value of the field. + @Input() database: any; // Database object. + + siteId: string; + rootUrl: string; + url: string; + userPicture: string; + + constructor(protected injector: Injector, protected dataProvider: AddonModDataProvider, + protected dataOffline: AddonModDataOfflineProvider, protected eventsProvider: CoreEventsProvider, + sitesProvider: CoreSitesProvider, protected userProvider: CoreUserProvider) { + this.rootUrl = sitesProvider.getCurrentSite().getURL(); + this.siteId = sitesProvider.getCurrentSiteId(); + } + + /** + * Undo delete action. + * + * @return {Promise} Solved when done. + */ + undoDelete(): Promise { + const dataId = this.database.id, + entryId = this.entry.id; + + return this.dataOffline.getEntry(dataId, entryId, 'delete', this.siteId).then(() => { + // Found. Just delete the action. + return this.dataOffline.deleteEntry(dataId, entryId, 'delete', this.siteId); + }).then(() => { + this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId}, this.siteId); + }); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + switch (this.action) { + case 'more': + this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&rid=' + this.entry.id; + break; + case 'edit': + this.url = this.rootUrl + '/mod/data/edit.php?d= ' + this.entry.dataid + '&rid=' + this.entry.id; + break; + case 'delete': + this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&delete=' + this.entry.id; + break; + case 'approve': + this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&approve=' + this.entry.id; + break; + case 'disapprove': + this.url = this.rootUrl + '/mod/data/view.php?d= ' + this.entry.dataid + '&disapprove=' + this.entry.id; + break; + case 'userpicture': + this.userProvider.getProfile(this.entry.userid, this.database.courseid).then((profile) => { + this.userPicture = profile.profileimageurl; + }); + break; + default: + break; + } + } +} diff --git a/src/addon/mod/data/components/components.module.ts b/src/addon/mod/data/components/components.module.ts index 7b979b396..3470ae872 100644 --- a/src/addon/mod/data/components/components.module.ts +++ b/src/addon/mod/data/components/components.module.ts @@ -18,15 +18,19 @@ import { IonicModule } 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 { CoreCourseComponentsModule } from '@core/course/components/components.module'; import { AddonModDataIndexComponent } from './index/index'; import { AddonModDataFieldPluginComponent } from './field-plugin/field-plugin'; +import { AddonModDataActionComponent } from './action/action'; import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module'; +import { CoreCommentsComponentsModule } from '@core/comments/components/components.module'; @NgModule({ declarations: [ AddonModDataIndexComponent, - AddonModDataFieldPluginComponent + AddonModDataFieldPluginComponent, + AddonModDataActionComponent ], imports: [ CommonModule, @@ -34,14 +38,17 @@ import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, + CorePipesModule, CoreCourseComponentsModule, - CoreCompileHtmlComponentModule + CoreCompileHtmlComponentModule, + CoreCommentsComponentsModule ], providers: [ ], exports: [ AddonModDataIndexComponent, - AddonModDataFieldPluginComponent + AddonModDataFieldPluginComponent, + AddonModDataActionComponent ], entryComponents: [ AddonModDataIndexComponent diff --git a/src/addon/mod/data/components/field-plugin/field-plugin.ts b/src/addon/mod/data/components/field-plugin/field-plugin.ts index cbac94fa2..8fcf975f0 100644 --- a/src/addon/mod/data/components/field-plugin/field-plugin.ts +++ b/src/addon/mod/data/components/field-plugin/field-plugin.ts @@ -17,7 +17,7 @@ import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; /** - * Component that displays an assignment feedback plugin. + * Component that displays a database field plugin. */ @Component({ selector: 'addon-mod-data-field-plugin', diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index 8f929e8b0..52e90e251 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -162,7 +162,7 @@ export class AddonModDataHelperProvider { template = template.replace(replace, render); }); - /*for (const action in actions) { + for (const action in actions) { replace = new RegExp('##' + action + '##', 'gi'); // Is enabled? if (actions[action]) { @@ -172,14 +172,14 @@ export class AddonModDataHelperProvider { } else if (action == 'approvalstatus') { render = this.translate.instant('addon.mod_data.' + (entry.approved ? 'approved' : 'notapproved')); } else { - render = ''; + render = ''; } template = template.replace(replace, render); } else { template = template.replace(replace, ''); } - }*/ + } return template; } diff --git a/src/addon/mod/data/providers/offline.ts b/src/addon/mod/data/providers/offline.ts index a4f01382b..03cff2081 100644 --- a/src/addon/mod/data/providers/offline.ts +++ b/src/addon/mod/data/providers/offline.ts @@ -131,6 +131,21 @@ export class AddonModDataOfflineProvider { }); } + /** + * Get an stored entry data. + * + * @param {number} dataId Database ID. + * @param {number} entryId Database entry Id. + * @param {string} action Action to be done + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with entry. + */ + getEntry(dataId: number, entryId: number, action: string, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecord(this.SURVEY_TABLE, {dataid: dataId, entryid: entryId, action: action}); + }); + } + /** * Get an all stored entry actions data. * From 88269125b6567e8e258bc6b77c9f3ff7c1dc6dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 9 May 2018 11:47:40 +0200 Subject: [PATCH 04/10] MOBILE-2338 data: Implement link handlers --- .../data/classes/field-plugin-component.ts | 2 - .../mod/data/components/action/action.html | 10 +- .../components/field-plugin/field-plugin.ts | 9 -- src/addon/mod/data/data.module.ts | 16 ++- .../mod/data/fields/url/component/url.html | 2 +- src/addon/mod/data/lang/en.json | 40 +++++- .../data/providers/approve-link-handler.ts | 122 ++++++++++++++++++ src/addon/mod/data/providers/data.ts | 119 ++++++++++++++++- .../mod/data/providers/delete-link-handler.ts | 121 +++++++++++++++++ .../mod/data/providers/edit-link-handler.ts | 93 +++++++++++++ src/addon/mod/data/providers/offline.ts | 48 ++++++- .../mod/data/providers/show-link-handler.ts | 105 +++++++++++++++ 12 files changed, 660 insertions(+), 27 deletions(-) create mode 100644 src/addon/mod/data/providers/approve-link-handler.ts create mode 100644 src/addon/mod/data/providers/delete-link-handler.ts create mode 100644 src/addon/mod/data/providers/edit-link-handler.ts create mode 100644 src/addon/mod/data/providers/show-link-handler.ts diff --git a/src/addon/mod/data/classes/field-plugin-component.ts b/src/addon/mod/data/classes/field-plugin-component.ts index dcd853b82..bcff4da64 100644 --- a/src/addon/mod/data/classes/field-plugin-component.ts +++ b/src/addon/mod/data/classes/field-plugin-component.ts @@ -23,6 +23,4 @@ export class AddonModDataFieldPluginComponent { @Input() database?: any; // Database object. @Input() error?: string; // Error when editing. @Input() viewAction: string; // Action to perform. - - constructor() { } } diff --git a/src/addon/mod/data/components/action/action.html b/src/addon/mod/data/components/action/action.html index 9588699a2..53f5096d7 100644 --- a/src/addon/mod/data/components/action/action.html +++ b/src/addon/mod/data/components/action/action.html @@ -1,12 +1,12 @@ - + - + - + @@ -14,11 +14,11 @@ - + - + diff --git a/src/addon/mod/data/components/field-plugin/field-plugin.ts b/src/addon/mod/data/components/field-plugin/field-plugin.ts index 8fcf975f0..0dbf7c501 100644 --- a/src/addon/mod/data/components/field-plugin/field-plugin.ts +++ b/src/addon/mod/data/components/field-plugin/field-plugin.ts @@ -71,13 +71,4 @@ export class AddonModDataFieldPluginComponent implements OnInit { } }); } - - /** - * Invalidate the plugin data. - * - * @return {Promise} Promise resolved when done. - */ - invalidate(): Promise { - return Promise.resolve(this.dynamicComponent && this.dynamicComponent.callComponentFunction('invalidate', [])); - } } diff --git a/src/addon/mod/data/data.module.ts b/src/addon/mod/data/data.module.ts index 12f6eb73b..6231d8e79 100644 --- a/src/addon/mod/data/data.module.ts +++ b/src/addon/mod/data/data.module.ts @@ -21,6 +21,10 @@ import { AddonModDataComponentsModule } from './components/components.module'; import { AddonModDataModuleHandler } from './providers/module-handler'; import { AddonModDataProvider } from './providers/data'; import { AddonModDataLinkHandler } from './providers/link-handler'; +import { AddonModDataApproveLinkHandler } from './providers/approve-link-handler'; +import { AddonModDataDeleteLinkHandler } from './providers/delete-link-handler'; +import { AddonModDataShowLinkHandler } from './providers/show-link-handler'; +import { AddonModDataEditLinkHandler } from './providers/edit-link-handler'; import { AddonModDataHelperProvider } from './providers/helper'; import { AddonModDataPrefetchHandler } from './providers/prefetch-handler'; import { AddonModDataSyncProvider } from './providers/sync'; @@ -43,6 +47,10 @@ import { AddonModDataFieldModule } from './fields/field.module'; AddonModDataPrefetchHandler, AddonModDataHelperProvider, AddonModDataLinkHandler, + AddonModDataApproveLinkHandler, + AddonModDataDeleteLinkHandler, + AddonModDataShowLinkHandler, + AddonModDataEditLinkHandler, AddonModDataSyncCronHandler, AddonModDataSyncProvider, AddonModDataOfflineProvider, @@ -54,10 +62,16 @@ export class AddonModDataModule { constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModDataModuleHandler, prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModDataPrefetchHandler, contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModDataLinkHandler, - cronDelegate: CoreCronDelegate, syncHandler: AddonModDataSyncCronHandler) { + cronDelegate: CoreCronDelegate, syncHandler: AddonModDataSyncCronHandler, + approveLinkHandler: AddonModDataApproveLinkHandler, deleteLinkHandler: AddonModDataDeleteLinkHandler, + showLinkHandler: AddonModDataShowLinkHandler, editLinkHandler: AddonModDataEditLinkHandler) { moduleDelegate.registerHandler(moduleHandler); prefetchDelegate.registerHandler(prefetchHandler); contentLinksDelegate.registerHandler(linkHandler); + contentLinksDelegate.registerHandler(approveLinkHandler); + contentLinksDelegate.registerHandler(deleteLinkHandler); + contentLinksDelegate.registerHandler(showLinkHandler); + contentLinksDelegate.registerHandler(editLinkHandler); cronDelegate.register(syncHandler); } } diff --git a/src/addon/mod/data/fields/url/component/url.html b/src/addon/mod/data/fields/url/component/url.html index ddd31d55f..9212b3aae 100644 --- a/src/addon/mod/data/fields/url/component/url.html +++ b/src/addon/mod/data/fields/url/component/url.html @@ -4,4 +4,4 @@ -{{field.name}} \ No newline at end of file +{{field.name}} \ No newline at end of file diff --git a/src/addon/mod/data/lang/en.json b/src/addon/mod/data/lang/en.json index 0e0dcd235..fdd3f402a 100644 --- a/src/addon/mod/data/lang/en.json +++ b/src/addon/mod/data/lang/en.json @@ -1,3 +1,41 @@ { - + "addentries": "Add entries", + "advancedsearch": "Advanced search", + "alttext": "Alternative text", + "approve": "Approve", + "approved": "Approved", + "ascending": "Ascending", + "authorfirstname": "Author first name", + "authorlastname": "Author surname", + "confirmdeleterecord": "Are you sure you want to delete this entry?", + "descending": "Descending", + "disapprove": "Undo approval", + "emptyaddform": "You did not fill out any fields!", + "entrieslefttoadd": "You must add {{$a.entriesleft}} more entry/entries in order to complete this activity", + "entrieslefttoaddtoview": "You must add {{$a.entrieslefttoview}} more entry/entries before you can view other participants' entries.", + "errorapproving": "Error approving or unapproving entry.", + "errordeleting": "Error deleting entry.", + "errormustsupplyvalue": "You must supply a value here.", + "expired": "Sorry, this activity closed on {{$a}} and is no longer available", + "fields": "Fields", + "latlongboth": "Both latitude and longitude are required.", + "menuchoose": "Choose...", + "more": "More", + "nomatch": "No matching entries found!", + "norecords": "No entries in database", + "notapproved": "Entry is not approved yet.", + "notopenyet": "Sorry, this activity is not available until {{$a}}", + "numrecords": "{{$a}} entries", + "other": "Other", + "recordapproved": "Entry approved", + "recorddeleted": "Entry deleted", + "recorddisapproved": "Entry unapproved", + "resetsettings": "Reset filters", + "search": "Search", + "single": "View single", + "selectedrequired": "All selected required", + "single": "View single", + "timeadded": "Time added", + "timemodified": "Time modified", + "usedate": "Include in search." } \ No newline at end of file diff --git a/src/addon/mod/data/providers/approve-link-handler.ts b/src/addon/mod/data/providers/approve-link-handler.ts new file mode 100644 index 000000000..bd3c9c71a --- /dev/null +++ b/src/addon/mod/data/providers/approve-link-handler.ts @@ -0,0 +1,122 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { AddonModDataProvider } from './data'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreEventsProvider } from '@providers/events'; + +/** + * Content links handler for database approve/disapprove entry. + * Match mod/data/view.php?d=6&approve=5 with a valid data id and entryid. + */ +@Injectable() +export class AddonModDataApproveLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonModDataApproveLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModData'; + pattern = /\/mod\/data\/view\.php.*([\?\&](d|approve|disapprove)=\d+)/; + + constructor(private dataProvider: AddonModDataProvider, private courseProvider: CoreCourseProvider, + private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider) { + super(); + } + + /** + * Convenience function to help get courseId. + * + * @param {number} dataId Database Id. + * @param {string} siteId Site Id, if not set, current site will be used. + * @param {number} courseId Course Id if already set. + * @return {Promise} Resolved with course Id when done. + */ + protected getActivityCourseIdIfNotSet(dataId: number, siteId: string, courseId: number): Promise { + if (courseId) { + return Promise.resolve(courseId); + } + + return this.courseProvider.getModuleBasicInfoByInstance(dataId, 'data', siteId).then((module) => { + return module.course; + }); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + return [{ + action: (siteId, navCtrl?): void => { + const modal = this.domUtils.showModalLoading(), + dataId = parseInt(params.d, 10), + entryId = parseInt(params.approve, 10) || parseInt(params.disapprove, 10), + approve = parseInt(params.approve, 10) ? true : false; + + this.getActivityCourseIdIfNotSet(dataId, siteId, courseId).then((cId) => { + courseId = cId; + + // Approve/disapprove entry. + return this.dataProvider.approveEntry(dataId, entryId, approve, courseId, siteId).catch((message) => { + modal.dismiss(); + this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errorapproving', true); + + return Promise.reject(null); + }); + }).then(() => { + const promises = []; + promises.push(this.dataProvider.invalidateEntryData(dataId, entryId, siteId)); + promises.push(this.dataProvider.invalidateEntriesData(dataId, siteId)); + + return Promise.all(promises); + }).then(() => { + this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId}, siteId); + + modal.dismiss(); + this.domUtils.showToast(approve ? 'addon.mod_data.recordapproved' : 'addon.mod_data.recorddisapproved', true, + 3000); + }).finally(() => { + // Just in case. In fact we need to dismiss the modal before showing a toast or error message. + modal.dismiss(); + }); + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + if (typeof params.d == 'undefined' || (typeof params.approve == 'undefined' && typeof params.disapprove == 'undefined')) { + // Required fields not defined. Cannot treat the URL. + return false; + } + + return this.dataProvider.isPluginEnabled(siteId); + } +} diff --git a/src/addon/mod/data/providers/data.ts b/src/addon/mod/data/providers/data.ts index 5ffa8fdaa..b4beec32b 100644 --- a/src/addon/mod/data/providers/data.ts +++ b/src/addon/mod/data/providers/data.ts @@ -18,6 +18,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFilepoolProvider } from '@providers/filepool'; import { AddonModDataOfflineProvider } from './offline'; +import { CoreAppProvider } from '@providers/app'; /** * Service that provides some features for databases. @@ -32,7 +33,8 @@ export class AddonModDataProvider { protected logger; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, - private filepoolProvider: CoreFilepoolProvider, private dataOffline: AddonModDataOfflineProvider) { + private filepoolProvider: CoreFilepoolProvider, private dataOffline: AddonModDataOfflineProvider, + private appProvider: CoreAppProvider) { this.logger = logger.getInstance('AddonModDataProvider'); } @@ -60,6 +62,51 @@ export class AddonModDataProvider { }); } + /** + * Approves or unapproves an entry. + * + * @param {number} dataId Database ID. + * @param {number} entryId Entry ID. + * @param {boolean} approve Whether to approve (true) or unapprove the entry. + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the action is done. + */ + approveEntry(dataId: number, entryId: number, approve: boolean, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a data to be synchronized later. + const storeOffline = (): Promise => { + const action = approve ? 'approve' : 'disapprove'; + + return this.dataOffline.saveEntry(dataId, entryId, action, courseId, null, null, null, siteId); + }; + + // Get if the opposite action is not synced. + const oppositeAction = approve ? 'disapprove' : 'approve'; + + return this.dataOffline.getEntry(dataId, entryId, oppositeAction, siteId).then(() => { + // Found. Just delete the action. + return this.dataOffline.deleteEntry(dataId, entryId, oppositeAction, siteId); + }).catch(() => { + + if (!this.appProvider.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + return this.approveEntryOnline(entryId, approve, siteId).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + return Promise.reject(error); + } + + // Couldn't connect to server, store in offline. + return storeOffline(); + }); + }); + } + /** * Approves or unapproves an entry. It does not cache calls. It will fail if offline or cannot connect. * @@ -79,6 +126,62 @@ export class AddonModDataProvider { }); } + /** + * Deletes an entry. + * + * @param {number} dataId Database ID. + * @param {number} entryId Entry ID. + * @param {number} courseId Course ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the action is done. + */ + deleteEntry(dataId: number, entryId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a data to be synchronized later. + const storeOffline = (): Promise => { + return this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, null, null, null, siteId); + }; + + let justAdded = false; + + // Check if the opposite action is not synced and just delete it. + return this.dataOffline.getEntryActions(dataId, entryId, siteId).then((entries) => { + if (entries && entries.length) { + // Found. Delete other actions first. + const proms = entries.map((entry) => { + if (entry.action == 'add') { + justAdded = true; + } + + return this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId); + }); + + return Promise.all(proms); + } + }).then(() => { + if (justAdded) { + // The field was added offline, delete and stop. + return; + } + + if (!this.appProvider.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + return this.deleteEntryOnline(entryId, siteId).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + return Promise.reject(error); + } + + // Couldn't connect to server, store in offline. + return storeOffline(); + }); + }); + } + /** * Deletes an entry. It does not cache calls. It will fail if offline or cannot connect. * @@ -494,6 +597,20 @@ export class AddonModDataProvider { }); } + /** + * Invalidates database entry data. + * + * @param {number} dataId Data ID for caching purposes. + * @param {number} entryId Entry ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateEntryData(dataId: number, entryId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getEntryCacheKey(dataId, entryId)); + }); + } + /** * Return whether or not the plugin is enabled in a certain site. Plugin is enabled if the database WS are available. * diff --git a/src/addon/mod/data/providers/delete-link-handler.ts b/src/addon/mod/data/providers/delete-link-handler.ts new file mode 100644 index 000000000..43af7fb8a --- /dev/null +++ b/src/addon/mod/data/providers/delete-link-handler.ts @@ -0,0 +1,121 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { AddonModDataProvider } from './data'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreEventsProvider } from '@providers/events'; + +/** + * Content links handler for database delete entry. + * Match mod/data/view.php?d=6&delete=5 with a valid data id and entryid. + */ +@Injectable() +export class AddonModDataDeleteLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonModDataDeleteLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModData'; + pattern = /\/mod\/data\/view\.php.*([\?\&](d|delete)=\d+)/; + + constructor(private dataProvider: AddonModDataProvider, private courseProvider: CoreCourseProvider, + private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider) { + super(); + } + + /** + * Convenience function to help get courseId. + * + * @param {number} dataId Database Id. + * @param {string} siteId Site Id, if not set, current site will be used. + * @param {number} courseId Course Id if already set. + * @return {Promise} Resolved with course Id when done. + */ + protected getActivityCourseIdIfNotSet(dataId: number, siteId: string, courseId: number): Promise { + if (courseId) { + return Promise.resolve(courseId); + } + + return this.courseProvider.getModuleBasicInfoByInstance(dataId, 'data', siteId).then((module) => { + return module.course; + }); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + return [{ + action: (siteId, navCtrl?): void => { + const modal = this.domUtils.showModalLoading(), + dataId = parseInt(params.d, 10), + entryId = parseInt(params.delete, 10); + + this.getActivityCourseIdIfNotSet(dataId, siteId, courseId).then((cId) => { + courseId = cId; + + // Delete entry. + return this.dataProvider.deleteEntry(dataId, entryId, courseId, siteId).catch((message) => { + modal.dismiss(); + this.domUtils.showErrorModalDefault(message, 'addon.mod_data.errordeleting', true); + + return Promise.reject(null); + }); + }).then(() => { + const promises = []; + promises.push(this.dataProvider.invalidateEntryData(dataId, entryId, siteId)); + promises.push(this.dataProvider.invalidateEntriesData(dataId, siteId)); + + return Promise.all(promises); + }).then(() => { + this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, {dataId: dataId, entryId: entryId, + deleted: true}, siteId); + + modal.dismiss(); + this.domUtils.showToast('addon.mod_data.recorddeleted', true, 3000); + }).finally(() => { + // Just in case. In fact we need to dismiss the modal before showing a toast or error message. + modal.dismiss(); + }); + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + if (typeof params.d == 'undefined' || typeof params.delete == 'undefined') { + // Required fields not defined. Cannot treat the URL. + return false; + } + + return this.dataProvider.isPluginEnabled(siteId); + } +} diff --git a/src/addon/mod/data/providers/edit-link-handler.ts b/src/addon/mod/data/providers/edit-link-handler.ts new file mode 100644 index 000000000..448035aeb --- /dev/null +++ b/src/addon/mod/data/providers/edit-link-handler.ts @@ -0,0 +1,93 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { AddonModDataProvider } from './data'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; + +/** + * Content links handler for database add or edit entry. + * Match mod/data/edit.php?d=6&rid=6 with a valid data and optional record id. + */ +@Injectable() +export class AddonModDataEditLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonModDataEditLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModData'; + pattern = /\/mod\/data\/edit\.php.*([\?\&](d|rid)=\d+)/; + + constructor(private linkHelper: CoreContentLinksHelperProvider, private dataProvider: AddonModDataProvider, + private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + return [{ + action: (siteId, navCtrl?): void => { + const modal = this.domUtils.showModalLoading(), + dataId = parseInt(params.d, 10), + rId = parseInt(params.rid, 10) || false; + + this.courseProvider.getModuleBasicInfoByInstance(dataId, 'data', siteId).then((module) => { + const stateParams = { + moduleId: module.id, + module: module, + courseId: module.course + }; + + if (rId) { + stateParams['entryId'] = rId; + } + + return this.linkHelper.goInSite(navCtrl, 'AddonModDataEditPage', stateParams, siteId); + }).finally(() => { + // Just in case. In fact we need to dismiss the modal before showing a toast or error message. + modal.dismiss(); + }); + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + if (typeof params.d == 'undefined') { + // Id not defined. Cannot treat the URL. + return false; + } + + return this.dataProvider.isPluginEnabled(siteId); + } +} diff --git a/src/addon/mod/data/providers/offline.ts b/src/addon/mod/data/providers/offline.ts index 03cff2081..c30316b1a 100644 --- a/src/addon/mod/data/providers/offline.ts +++ b/src/addon/mod/data/providers/offline.ts @@ -27,10 +27,10 @@ export class AddonModDataOfflineProvider { protected logger; // Variables for database. - protected SURVEY_TABLE = 'addon_mod_data_entry'; + protected DATA_ENTRY_TABLE = 'addon_mod_data_entry'; protected tablesSchema = [ { - name: this.SURVEY_TABLE, + name: this.DATA_ENTRY_TABLE, columns: [ { name: 'dataid', @@ -102,7 +102,7 @@ export class AddonModDataOfflineProvider { */ deleteEntry(dataId: number, entryId: number, action: string, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().deleteRecords(this.SURVEY_TABLE, {dataid: dataId, entryid: entryId, action: action}); + return site.getDb().deleteRecords(this.DATA_ENTRY_TABLE, {dataid: dataId, entryid: entryId, action: action}); }); } @@ -114,7 +114,7 @@ export class AddonModDataOfflineProvider { */ getAllEntries(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getAllRecords(this.SURVEY_TABLE); + return site.getDb().getAllRecords(this.DATA_ENTRY_TABLE); }); } @@ -127,7 +127,7 @@ export class AddonModDataOfflineProvider { */ getDatabaseEntries(dataId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecords(this.SURVEY_TABLE, {dataid: dataId}); + return site.getDb().getRecords(this.DATA_ENTRY_TABLE, {dataid: dataId}); }); } @@ -142,7 +142,7 @@ export class AddonModDataOfflineProvider { */ getEntry(dataId: number, entryId: number, action: string, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecord(this.SURVEY_TABLE, {dataid: dataId, entryid: entryId, action: action}); + return site.getDb().getRecord(this.DATA_ENTRY_TABLE, {dataid: dataId, entryid: entryId, action: action}); }); } @@ -156,7 +156,7 @@ export class AddonModDataOfflineProvider { */ getEntryActions(dataId: number, entryId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecords(this.SURVEY_TABLE, {dataid: dataId, entryid: entryId}); + return site.getDb().getRecords(this.DATA_ENTRY_TABLE, {dataid: dataId, entryid: entryId}); }); } @@ -207,4 +207,38 @@ export class AddonModDataOfflineProvider { return this.textUtils.concatenatePaths(folderPath, entryId + '_' + fieldId); }); } + + /** + * Save an entry data to be sent later. + * + * @param {number} dataId Database ID. + * @param {number} entryId Database entry Id. If action is add entryId should be 0 and -timemodified will be used. + * @param {string} action Action to be done to the entry: [add, edit, delete, approve, disapprove] + * @param {number} courseId Course ID of the database. + * @param {number} [groupId] Group ID. Only provided when adding. + * @param {any[]} [fields] Array of field data of the entry if needed. + * @param {number} [timemodified] The time the entry was modified. If not defined, current time. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + saveEntry(dataId: number, entryId: number, action: string, courseId: number, groupId?: number, fields?: any[], + timemodified?: number, siteId?: string): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + timemodified = timemodified || new Date().getTime(); + entryId = typeof entryId == 'undefined' || entryId === null ? -timemodified : entryId; + const entry = { + dataid: dataId, + courseid: courseId, + groupid: groupId, + action: action, + entryid: entryId, + fields: fields, + timemodified: timemodified + }; + + return site.getDb().insertRecord(this.DATA_ENTRY_TABLE, entry); + }); + } + } diff --git a/src/addon/mod/data/providers/show-link-handler.ts b/src/addon/mod/data/providers/show-link-handler.ts new file mode 100644 index 000000000..702a03345 --- /dev/null +++ b/src/addon/mod/data/providers/show-link-handler.ts @@ -0,0 +1,105 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; +import { AddonModDataProvider } from './data'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; + +/** + * Content links handler for database show entry. + * Match mod/data/view.php?d=6&rid=5 with a valid data id and entryid. + */ +@Injectable() +export class AddonModDataShowLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonModDataShowLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModData'; + pattern = /\/mod\/data\/view\.php.*([\?\&](d|rid|page|group|mode)=\d+)/; + + constructor(private linkHelper: CoreContentLinksHelperProvider, private dataProvider: AddonModDataProvider, + private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + return [{ + action: (siteId, navCtrl?): void => { + const modal = this.domUtils.showModalLoading(), + dataId = parseInt(params.d, 10), + rId = parseInt(params.rid, 10) || false, + group = parseInt(params.group, 10) || false, + page = parseInt(params.page, 10) || false; + + this.courseProvider.getModuleBasicInfoByInstance(dataId, 'data', siteId).then((module) => { + const stateParams = { + moduleId: module.id, + module: module, + courseId: module.course + }; + + if (group) { + stateParams['group'] = group; + } + + if (params.mode && params.mode == 'single') { + stateParams['page'] = page || 1; + } else if (rId) { + stateParams['entryId'] = rId; + } + + return this.linkHelper.goInSite(navCtrl, 'AddonModDataEntryPage', stateParams, siteId); + }).finally(() => { + // Just in case. In fact we need to dismiss the modal before showing a toast or error message. + modal.dismiss(); + }); + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + if (typeof params.d == 'undefined') { + // Id not defined. Cannot treat the URL. + return false; + } + + if ((!params.mode || params.mode != 'single') && typeof params.rid == 'undefined') { + return false; + } + + return this.dataProvider.isPluginEnabled(siteId); + } +} From b7769ec2a43e6994c4833221059be8605c4f0ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 9 May 2018 14:14:31 +0200 Subject: [PATCH 05/10] MOBILE-2338 data: Search modal --- .../data/classes/field-plugin-component.ts | 27 ++- .../components/field-plugin/field-plugin.ts | 11 +- .../mod/data/components/index/index.html | 2 +- .../mod/data/components/index/index.scss | 2 +- src/addon/mod/data/components/index/index.ts | 56 ++++-- .../fields/checkbox/component/checkbox.html | 21 +- .../fields/checkbox/component/checkbox.ts | 38 ++-- .../data/fields/checkbox/providers/handler.ts | 17 +- .../mod/data/fields/date/component/date.html | 18 +- .../mod/data/fields/date/component/date.ts | 30 +-- .../mod/data/fields/date/providers/handler.ts | 2 +- .../mod/data/fields/file/component/file.html | 13 +- .../mod/data/fields/file/component/file.ts | 14 +- .../fields/latlong/component/latlong.html | 24 ++- .../data/fields/latlong/component/latlong.ts | 20 +- .../mod/data/fields/menu/component/menu.html | 14 +- .../mod/data/fields/menu/component/menu.ts | 18 +- .../fields/multimenu/component/multimenu.html | 19 +- .../fields/multimenu/component/multimenu.ts | 20 +- .../fields/multimenu/providers/handler.ts | 27 ++- .../data/fields/number/component/number.html | 10 +- .../data/fields/number/component/number.ts | 14 +- .../fields/picture/component/picture.html | 22 +- .../data/fields/picture/component/picture.ts | 14 +- .../radiobutton/component/radiobutton.html | 14 +- .../radiobutton/component/radiobutton.ts | 22 +- .../mod/data/fields/text/component/text.html | 8 +- .../mod/data/fields/text/component/text.ts | 14 +- .../fields/textarea/component/textarea.html | 13 +- .../fields/textarea/component/textarea.ts | 15 +- .../mod/data/fields/url/component/url.html | 8 +- .../mod/data/fields/url/component/url.ts | 14 +- src/addon/mod/data/pages/search/search.html | 55 +++++ .../mod/data/pages/search/search.module.ts | 35 ++++ src/addon/mod/data/pages/search/search.scss | 57 ++++++ src/addon/mod/data/pages/search/search.ts | 189 ++++++++++++++++++ src/addon/mod/data/providers/helper.ts | 89 --------- src/app/app.scss | 4 + src/components/tabs/tabs.scss | 95 +++++---- 39 files changed, 727 insertions(+), 358 deletions(-) create mode 100644 src/addon/mod/data/pages/search/search.html create mode 100644 src/addon/mod/data/pages/search/search.module.ts create mode 100644 src/addon/mod/data/pages/search/search.scss create mode 100644 src/addon/mod/data/pages/search/search.ts diff --git a/src/addon/mod/data/classes/field-plugin-component.ts b/src/addon/mod/data/classes/field-plugin-component.ts index bcff4da64..512037367 100644 --- a/src/addon/mod/data/classes/field-plugin-component.ts +++ b/src/addon/mod/data/classes/field-plugin-component.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. import { Input } from '@angular/core'; +import { FormGroup, FormBuilder } from '@angular/forms'; /** * Base class for component to render a field. @@ -22,5 +23,29 @@ export class AddonModDataFieldPluginComponent { @Input() value?: any; // The value of the field. @Input() database?: any; // Database object. @Input() error?: string; // Error when editing. - @Input() viewAction: string; // Action to perform. + @Input() viewAction?: string; // Action to perform. + @Input() form?: FormGroup; // Form where to add the form control. Just required for edit and search modes. + @Input() search?: any; // The search value of all fields. + + constructor(protected fb: FormBuilder) { } + + /** + * Add the form control for the search mode. + * + * @param {string} fieldName Control field name. + * @param {any} value Initial set value. + */ + protected addControl(fieldName: string, value?: any): void { + if (!this.form) { + return; + } + + if (this.mode == 'search') { + this.form.addControl(fieldName, this.fb.control(this.search[fieldName] || null)); + } + + if (this.mode == 'edit') { + this.form.addControl(fieldName, this.fb.control(value || null)); + } + } } diff --git a/src/addon/mod/data/components/field-plugin/field-plugin.ts b/src/addon/mod/data/components/field-plugin/field-plugin.ts index 0dbf7c501..4e73fafa2 100644 --- a/src/addon/mod/data/components/field-plugin/field-plugin.ts +++ b/src/addon/mod/data/components/field-plugin/field-plugin.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. import { Component, Input, OnInit, Injector, ViewChild } from '@angular/core'; +import { FormGroup } from '@angular/forms'; import { AddonModDataProvider } from '../../providers/data'; import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; @@ -32,6 +33,8 @@ export class AddonModDataFieldPluginComponent implements OnInit { @Input() database?: any; // Database object. @Input() error?: string; // Error when editing. @Input() viewAction: string; // Action to perform. + @Input() form?: FormGroup; // Form where to add the form control. Just required for edit and search modes. + @Input() search?: any; // The search value of all fields. fieldComponent: any; // Component to render the plugin. data: any; // Data to pass to the component. @@ -63,12 +66,14 @@ export class AddonModDataFieldPluginComponent implements OnInit { value: this.value, database: this.database, error: this.error, - viewAction: this.viewAction + viewAction: this.viewAction, + form: this.form, + search: this.search }; - } else { - this.fieldLoaded = true; } + }).finally(() => { + this.fieldLoaded = true; }); } } diff --git a/src/addon/mod/data/components/index/index.html b/src/addon/mod/data/components/index/index.html index 2fe8fe250..ce67bfa94 100644 --- a/src/addon/mod/data/components/index/index.html +++ b/src/addon/mod/data/components/index/index.html @@ -58,7 +58,7 @@ {{ 'addon.mod_data.resetsettings' | translate}} -
+
diff --git a/src/addon/mod/data/components/index/index.scss b/src/addon/mod/data/components/index/index.scss index 2c2a8e5a3..106a72942 100644 --- a/src/addon/mod/data/components/index/index.scss +++ b/src/addon/mod/data/components/index/index.scss @@ -1,5 +1,5 @@ addon-mod-data-index { - .core-data-contents { + .addon-data-contents { overflow: visible; white-space: normal; word-break: break-word; diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index e2517fc62..fcfae95e3 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, Optional, Injector } from '@angular/core'; -import { Content, ModalController } from 'ionic-angular'; +import { Content, ModalController, NavController } from 'ionic-angular'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreGroupsProvider, CoreGroupInfo } from '@providers/groups'; @@ -42,7 +42,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp data: any = {}; fields: any; selectedGroup: number; - advancedSearch: any; timeAvailableFrom: number | boolean; timeAvailableFromReadable: string | boolean; timeAvailableTo: number | boolean; @@ -79,7 +78,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp private dataOffline: AddonModDataOfflineProvider, @Optional() @Optional() content: Content, private dataSync: AddonModDataSyncProvider, private timeUtils: CoreTimeUtilsProvider, private groupsProvider: CoreGroupsProvider, private commentsProvider: CoreCommentsProvider, - private modalCtrl: ModalController, private utils: CoreUtilsProvider) { + private modalCtrl: ModalController, private utils: CoreUtilsProvider, protected navCtrl: NavController) { super(injector); // Refresh entries on change. @@ -222,7 +221,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp this.fields[field.id] = field; }); this.fieldsArray = this.utils.objectToArray(this.fields); - this.advancedSearch = this.dataHelper.displayAdvancedSearchFields(this.data.asearchtemplate, this.fieldsArray); return this.fetchEntriesData(); }); @@ -349,9 +347,16 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp * Display the chat users modal. */ showSearch(): void { - const modal = this.modalCtrl.create('AddonModDataSearchPage'); + const modal = this.modalCtrl.create('AddonModDataSearchPage', { + search: this.search, + fields: this.fields, + data: this.data}); modal.onDidDismiss((data) => { - // @TODO. + // Add data to search object. + if (data) { + this.search = data; + this.searchEntries(0); + } }); modal.present(); } @@ -366,14 +371,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp this.loaded = false; this.search.page = page; - if (this.search.searchingAdvanced) { - this.search.advanced = this.dataHelper.getSearchDataFromForm(document.forms['addon-mod_data-advanced-search-form'], - this.fieldsArray); - this.search.searching = this.search.advanced.length > 0; - } else { - this.search.searching = this.search.text.length > 0; - } - return this.fetchEntriesData().catch((message) => { this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); }).finally(() => { @@ -405,6 +402,37 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp }); } + /** + * Opens add entries form. + */ + gotoAddEntries(): void { + const stateParams = { + moduleId: this.module.id, + module: this.module, + courseId: this.courseId, + group: this.selectedGroup + }; + + this.navCtrl.push('AddonModDataEditPage', stateParams); + } + + /** + * Goto the selected entry. + * + * @param {number} entryId Entry ID. + */ + gotoEntry(entryId: number): void { + const stateParams = { + module: this.module, + moduleid: this.module.id, + courseid: this.courseId, + entryid: entryId, + group: this.selectedGroup + }; + + this.navCtrl.push('AddonModDataEntryPage', stateParams); + } + /** * Fetch offline entries. * diff --git a/src/addon/mod/data/fields/checkbox/component/checkbox.html b/src/addon/mod/data/fields/checkbox/component/checkbox.html index cc37e6f1e..6bd9c1bd6 100644 --- a/src/addon/mod/data/fields/checkbox/component/checkbox.html +++ b/src/addon/mod/data/fields/checkbox/component/checkbox.html @@ -1,16 +1,17 @@ - + - - {{ option }} - - - - + - + + {{option.key}} + + + + {{ 'addon.mod_data.selectedrequired' | translate }} - + - + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/checkbox/component/checkbox.ts b/src/addon/mod/data/fields/checkbox/component/checkbox.ts index 48404b1a8..37fe87a89 100644 --- a/src/addon/mod/data/fields/checkbox/component/checkbox.ts +++ b/src/addon/mod/data/fields/checkbox/component/checkbox.ts @@ -11,10 +11,8 @@ // 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, ElementRef } from '@angular/core'; -import { FormBuilder, FormControl } from '@angular/forms'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; /** @@ -26,13 +24,10 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- }) export class AddonModDataFieldCheckboxComponent extends AddonModDataFieldPluginComponent implements OnInit { - control: FormControl; - options: number; - values = {}; + options = []; - constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, - element: ElementRef) { - super(); + constructor(protected fb: FormBuilder) { + super(fb); } /** @@ -45,21 +40,28 @@ export class AddonModDataFieldCheckboxComponent extends AddonModDataFieldPluginC protected render(): void { if (this.mode == 'show') { - this.value.content = this.value.content.split('##').join('
'); + this.value.content = this.value && this.value.content && this.value.content.split('##').join('
'); return; } - this.options = this.field.param1.split('\n'); - - if (this.mode == 'edit' && this.value) { - this.values = {}; + this.options = this.field.param1.split('\n').map((option) => { + return { key: option, value: option }; + }); + if (this.mode == 'edit' && this.value && this.value.content) { this.value.content.split('##').forEach((value) => { - this.values[value] = true; + const x = this.options.findIndex((option) => value == option.key); + if (x >= 0) { + this.options[x].selected = true; + } }); - - //this.control = this.fb.control(text); } + + if (this.mode == 'search') { + this.addControl('f_' + this.field.id + '_allreq'); + } + + this.addControl('f_' + this.field.id); } } diff --git a/src/addon/mod/data/fields/checkbox/providers/handler.ts b/src/addon/mod/data/fields/checkbox/providers/handler.ts index 14632ab59..e307cc6e2 100644 --- a/src/addon/mod/data/fields/checkbox/providers/handler.ts +++ b/src/addon/mod/data/fields/checkbox/providers/handler.ts @@ -49,20 +49,22 @@ export class AddonModDataFieldCheckboxHandler implements AddonModDataFieldHandle const fieldName = 'f_' + field.id, reqName = 'f_' + field.id + '_allreq'; - const checkboxes = [], + const options = field.param1.split('\n'), + checkboxes = [], values = []; - inputData[fieldName].forEach((value, option) => { - if (value) { + options.forEach((option) => { + if (inputData[fieldName + '_' + option]) { checkboxes.push(option); } }); + if (checkboxes.length > 0) { values.push({ name: fieldName, value: checkboxes }); - if (inputData[reqName]['1']) { + if (inputData[reqName]) { values.push({ name: reqName, value: true @@ -85,9 +87,10 @@ export class AddonModDataFieldCheckboxHandler implements AddonModDataFieldHandle getFieldEditData(field: any, inputData: any, originalFieldData: any): any { const fieldName = 'f_' + field.id; - const checkboxes = []; - inputData[fieldName].forEach((value, option) => { - if (value) { + const options = field.param1.split('\n'), + checkboxes = []; + options.forEach((option) => { + if (inputData[fieldName + '_' + option]) { checkboxes.push(option); } }); diff --git a/src/addon/mod/data/fields/date/component/date.html b/src/addon/mod/data/fields/date/component/date.html index 66a9c8de0..41aa4d756 100644 --- a/src/addon/mod/data/fields/date/component/date.html +++ b/src/addon/mod/data/fields/date/component/date.html @@ -1,12 +1,14 @@ - - - + + + + - - {{ 'addon.mod_data.usedate' | translate }} - - - + + {{ 'addon.mod_data.usedate' | translate }} + + + + diff --git a/src/addon/mod/data/fields/date/component/date.ts b/src/addon/mod/data/fields/date/component/date.ts index 034669864..1ea4b8656 100644 --- a/src/addon/mod/data/fields/date/component/date.ts +++ b/src/addon/mod/data/fields/date/component/date.ts @@ -11,10 +11,9 @@ // 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, ElementRef } from '@angular/core'; -import { FormBuilder, FormControl } from '@angular/forms'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; /** @@ -26,14 +25,13 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- }) export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginComponent implements OnInit { - control: FormControl; values = {}; enable: boolean; val: any; + format: string; - constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, - element: ElementRef) { - super(); + constructor(protected fb: FormBuilder, protected timeUtils: CoreTimeUtilsProvider) { + super(fb); } /** @@ -49,15 +47,23 @@ export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginCompo return; } - if (this.mode == 'edit' && this.value) { - this.enable = true; - } else { + if (!this.value) { this.value = { content: Math.floor(Date.now() / 1000) }; - this.enable = false; } this.val = new Date(this.value.content * 1000); + + this.format = this.timeUtils.getLocalizedDateFormat('LL'); + + if (this.mode == 'search') { + this.addControl('f_' + this.field.id + '_z'); + this.search['f_' + this.field.id] = this.search['f_' + this.field.id + '_y'] ? new Date( + this.search['f_' + this.field.id + '_y'] + '-' + this.search['f_' + this.field.id + '_m'] + '-' + + this.search['f_' + this.field.id + '_d']) : this.val; + } + + this.addControl('f_' + this.field.id, this.val); } } diff --git a/src/addon/mod/data/fields/date/providers/handler.ts b/src/addon/mod/data/fields/date/providers/handler.ts index 9bb37150b..c96d1c65f 100644 --- a/src/addon/mod/data/fields/date/providers/handler.ts +++ b/src/addon/mod/data/fields/date/providers/handler.ts @@ -49,7 +49,7 @@ export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler { const fieldName = 'f_' + field.id, enabledName = 'f_' + field.id + '_z'; - if (inputData[enabledName]['1']) { + if (inputData[enabledName] && typeof inputData[fieldName] == 'string') { const values = [], date = inputData[fieldName].split('-'), year = date[0], diff --git a/src/addon/mod/data/fields/file/component/file.html b/src/addon/mod/data/fields/file/component/file.html index 1ed18a0f3..2416b3c6c 100644 --- a/src/addon/mod/data/fields/file/component/file.html +++ b/src/addon/mod/data/fields/file/component/file.html @@ -1,7 +1,12 @@ - - - - + + + + + + + + +
diff --git a/src/addon/mod/data/fields/file/component/file.ts b/src/addon/mod/data/fields/file/component/file.ts index 773a3b7ed..b802d6f17 100644 --- a/src/addon/mod/data/fields/file/component/file.ts +++ b/src/addon/mod/data/fields/file/component/file.ts @@ -11,10 +11,8 @@ // 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, ElementRef } from '@angular/core'; -import { FormBuilder, FormControl } from '@angular/forms'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; import { CoreFileSessionProvider } from '@providers/file-session'; import { AddonModDataProvider } from '../../../providers/data'; @@ -28,15 +26,13 @@ import { AddonModDataProvider } from '../../../providers/data'; }) export class AddonModDataFieldFileComponent extends AddonModDataFieldPluginComponent implements OnInit { - control: FormControl; files = []; component: string; componentId: number; maxSizeBytes: number; - constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, - element: ElementRef, private fileSessionprovider: CoreFileSessionProvider) { - super(); + constructor(protected fb: FormBuilder, private fileSessionprovider: CoreFileSessionProvider) { + super(fb); } /** @@ -77,5 +73,7 @@ export class AddonModDataFieldFileComponent extends AddonModDataFieldPluginCompo this.fileSessionprovider.setFiles(this.component, this.database.id + '_' + this.field.id, this.files); } } + + this.addControl('f_' + this.field.id); } } diff --git a/src/addon/mod/data/fields/latlong/component/latlong.html b/src/addon/mod/data/fields/latlong/component/latlong.html index 0b2ef6c9e..9303ec6d7 100644 --- a/src/addon/mod/data/fields/latlong/component/latlong.html +++ b/src/addon/mod/data/fields/latlong/component/latlong.html @@ -1,17 +1,19 @@ - + + - + - + - - - °N - - - - °E - + + + °N + + + + °E + + diff --git a/src/addon/mod/data/fields/latlong/component/latlong.ts b/src/addon/mod/data/fields/latlong/component/latlong.ts index 2d4e555a8..f9933bf6e 100644 --- a/src/addon/mod/data/fields/latlong/component/latlong.ts +++ b/src/addon/mod/data/fields/latlong/component/latlong.ts @@ -11,11 +11,9 @@ // 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, ElementRef } from '@angular/core'; -import { FormBuilder, FormControl } from '@angular/forms'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; import { Platform } from 'ionic-angular'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; /** @@ -27,14 +25,11 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- }) export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginComponent implements OnInit { - control: FormControl; - values = {}; north: number; east: number; - constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, - element: ElementRef, private platform: Platform) { - super(); + constructor(protected fb: FormBuilder, private platform: Platform) { + super(fb); } /** @@ -86,5 +81,12 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo this.north = (this.value && parseFloat(this.value.content)) || null; this.east = (this.value && parseFloat(this.value.content1)) || null; } + + if (this.mode == 'edit') { + this.addControl('f_' + this.field.id + '_0', this.north); + this.addControl('f_' + this.field.id + '_1', this.east); + } else if (this.mode == 'search') { + this.addControl('f_' + this.field.id); + } } } diff --git a/src/addon/mod/data/fields/menu/component/menu.html b/src/addon/mod/data/fields/menu/component/menu.html index 12f41a13f..c7e9e03c8 100644 --- a/src/addon/mod/data/fields/menu/component/menu.html +++ b/src/addon/mod/data/fields/menu/component/menu.html @@ -1,9 +1,11 @@ - - + + + - - {{ 'addon.mod_data.menuchoose' | translate }} - {{option}} - + + {{ 'addon.mod_data.menuchoose' | translate }} + {{option}} + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/menu/component/menu.ts b/src/addon/mod/data/fields/menu/component/menu.ts index 08628a556..7a7f867bf 100644 --- a/src/addon/mod/data/fields/menu/component/menu.ts +++ b/src/addon/mod/data/fields/menu/component/menu.ts @@ -11,10 +11,8 @@ // 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, ElementRef } from '@angular/core'; -import { FormBuilder, FormControl } from '@angular/forms'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; /** @@ -26,13 +24,10 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- }) export class AddonModDataFieldMenuComponent extends AddonModDataFieldPluginComponent implements OnInit { - control: FormControl; - val: string; options = []; - constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, - element: ElementRef) { - super(); + constructor(protected fb: FormBuilder) { + super(fb); } /** @@ -50,8 +45,11 @@ export class AddonModDataFieldMenuComponent extends AddonModDataFieldPluginCompo this.options = this.field.param1.split('\n'); + let val; if (this.mode == 'edit' && this.value) { - this.val = this.value.content; + val = this.value.content; } + + this.addControl('f_' + this.field.id, val); } } diff --git a/src/addon/mod/data/fields/multimenu/component/multimenu.html b/src/addon/mod/data/fields/multimenu/component/multimenu.html index fbf41a7d7..6bd9c1bd6 100644 --- a/src/addon/mod/data/fields/multimenu/component/multimenu.html +++ b/src/addon/mod/data/fields/multimenu/component/multimenu.html @@ -1,8 +1,17 @@ - - + + + - - \ No newline at end of file + + {{ 'addon.mod_data.selectedrequired' | translate }} + + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/multimenu/component/multimenu.ts b/src/addon/mod/data/fields/multimenu/component/multimenu.ts index c233c08c2..963b279f9 100644 --- a/src/addon/mod/data/fields/multimenu/component/multimenu.ts +++ b/src/addon/mod/data/fields/multimenu/component/multimenu.ts @@ -11,10 +11,8 @@ // 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, ElementRef } from '@angular/core'; -import { FormBuilder, FormControl } from '@angular/forms'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; /** @@ -26,12 +24,10 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- }) export class AddonModDataFieldMultimenuComponent extends AddonModDataFieldPluginComponent implements OnInit { - control: FormControl; options = []; - constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, - element: ElementRef) { - super(); + constructor(protected fb: FormBuilder) { + super(fb); } /** @@ -44,6 +40,8 @@ export class AddonModDataFieldMultimenuComponent extends AddonModDataFieldPlugin protected render(): void { if (this.mode == 'show') { + this.value.content = this.value && this.value.content && this.value.content.split('##').join('
'); + return; } @@ -59,5 +57,11 @@ export class AddonModDataFieldMultimenuComponent extends AddonModDataFieldPlugin } }); } + + if (this.mode == 'search') { + this.addControl('f_' + this.field.id + '_allreq'); + } + + this.addControl('f_' + this.field.id); } } diff --git a/src/addon/mod/data/fields/multimenu/providers/handler.ts b/src/addon/mod/data/fields/multimenu/providers/handler.ts index 9f65ee8bf..c5d5c5fcc 100644 --- a/src/addon/mod/data/fields/multimenu/providers/handler.ts +++ b/src/addon/mod/data/fields/multimenu/providers/handler.ts @@ -49,25 +49,22 @@ export class AddonModDataFieldMultimenuHandler implements AddonModDataFieldHandl const fieldName = 'f_' + field.id, reqName = 'f_' + field.id + '_allreq'; - if (inputData[fieldName].length > 0) { - const options = inputData[fieldName].split('###'), - values = []; + if (inputData[fieldName] && inputData[fieldName].length > 0) { + const values = []; - if (options.length > 0) { + values.push({ + name: fieldName, + value: inputData[fieldName] + }); + + if (inputData[reqName]) { values.push({ - name: fieldName, - value: options + name: reqName, + value: true }); - - if (inputData[reqName]['1']) { - values.push({ - name: reqName, - value: true - }); - } - - return values; } + + return values; } return false; diff --git a/src/addon/mod/data/fields/number/component/number.html b/src/addon/mod/data/fields/number/component/number.html index 41bbe1fb0..17a4a6423 100644 --- a/src/addon/mod/data/fields/number/component/number.html +++ b/src/addon/mod/data/fields/number/component/number.html @@ -1,7 +1,9 @@ - + + - + - + + - \ No newline at end of file + diff --git a/src/addon/mod/data/fields/number/component/number.ts b/src/addon/mod/data/fields/number/component/number.ts index c463ea5e4..9fe4d6472 100644 --- a/src/addon/mod/data/fields/number/component/number.ts +++ b/src/addon/mod/data/fields/number/component/number.ts @@ -11,10 +11,8 @@ // 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, ElementRef } from '@angular/core'; -import { FormBuilder, FormControl } from '@angular/forms'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; /** @@ -26,12 +24,10 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- }) export class AddonModDataFieldNumberComponent extends AddonModDataFieldPluginComponent implements OnInit { - control: FormControl; val: number; - constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, - element: ElementRef) { - super(); + constructor(protected fb: FormBuilder) { + super(fb); } /** @@ -50,5 +46,7 @@ export class AddonModDataFieldNumberComponent extends AddonModDataFieldPluginCom if (this.mode == 'edit' && this.value) { this.val = this.value && parseFloat(this.value.content); } + + this.addControl('f_' + this.field.id, this.val); } } diff --git a/src/addon/mod/data/fields/picture/component/picture.html b/src/addon/mod/data/fields/picture/component/picture.html index e435409f8..7b24e5a3f 100644 --- a/src/addon/mod/data/fields/picture/component/picture.html +++ b/src/addon/mod/data/fields/picture/component/picture.html @@ -1,16 +1,20 @@ - + + - + - + - + + {{ 'addon.mod_data.alttext' | translate }} + + °N + + - - {{ 'addon.mod_data.alttext' | translate }} - - °N - + + + diff --git a/src/addon/mod/data/fields/picture/component/picture.ts b/src/addon/mod/data/fields/picture/component/picture.ts index b4516a4a3..810b63cc7 100644 --- a/src/addon/mod/data/fields/picture/component/picture.ts +++ b/src/addon/mod/data/fields/picture/component/picture.ts @@ -11,10 +11,8 @@ // 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, ElementRef } from '@angular/core'; -import { FormBuilder, FormControl } from '@angular/forms'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; import { CoreFileSessionProvider } from '@providers/file-session'; import { AddonModDataProvider } from '../../../providers/data'; @@ -28,7 +26,6 @@ import { AddonModDataProvider } from '../../../providers/data'; }) export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginComponent implements OnInit { - control: FormControl; files = []; component: string; componentId: number; @@ -42,9 +39,8 @@ export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginCo width: string; height: string; - constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, - element: ElementRef, private fileSessionprovider: CoreFileSessionProvider) { - super(); + constructor(protected fb: FormBuilder, private fileSessionprovider: CoreFileSessionProvider) { + super(fb); } /** @@ -124,5 +120,7 @@ export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginCo this.height = this.field.param2 || ''; } } + + this.addControl('f_' + this.field.id); } } diff --git a/src/addon/mod/data/fields/radiobutton/component/radiobutton.html b/src/addon/mod/data/fields/radiobutton/component/radiobutton.html index 12f41a13f..c7e9e03c8 100644 --- a/src/addon/mod/data/fields/radiobutton/component/radiobutton.html +++ b/src/addon/mod/data/fields/radiobutton/component/radiobutton.html @@ -1,9 +1,11 @@ - - + + + - - {{ 'addon.mod_data.menuchoose' | translate }} - {{option}} - + + {{ 'addon.mod_data.menuchoose' | translate }} + {{option}} + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/radiobutton/component/radiobutton.ts b/src/addon/mod/data/fields/radiobutton/component/radiobutton.ts index 208d5d2cb..400d3aa78 100644 --- a/src/addon/mod/data/fields/radiobutton/component/radiobutton.ts +++ b/src/addon/mod/data/fields/radiobutton/component/radiobutton.ts @@ -11,10 +11,8 @@ // 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, ElementRef } from '@angular/core'; -import { FormBuilder, FormControl } from '@angular/forms'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; /** @@ -26,13 +24,10 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- }) export class AddonModDataFieldRadiobuttonComponent extends AddonModDataFieldPluginComponent implements OnInit { - control: FormControl; - options: number; - val: number; + options = []; - constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, - element: ElementRef) { - super(); + constructor(protected fb: FormBuilder) { + super(fb); } /** @@ -48,10 +43,13 @@ export class AddonModDataFieldRadiobuttonComponent extends AddonModDataFieldPlug return; } - this.options = this.field.param1.split('\n'); + this.options = this.field.param1.split('\n'); + let val; if (this.mode == 'edit' && this.value) { - this.val = this.value.content; + val = this.value.content; } + + this.addControl('f_' + this.field.id, val); } } diff --git a/src/addon/mod/data/fields/text/component/text.html b/src/addon/mod/data/fields/text/component/text.html index d907c4a9c..ad562369f 100644 --- a/src/addon/mod/data/fields/text/component/text.html +++ b/src/addon/mod/data/fields/text/component/text.html @@ -1,7 +1,9 @@ - + + - + - + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/text/component/text.ts b/src/addon/mod/data/fields/text/component/text.ts index 174bc1cd2..ce5f91649 100644 --- a/src/addon/mod/data/fields/text/component/text.ts +++ b/src/addon/mod/data/fields/text/component/text.ts @@ -11,10 +11,8 @@ // 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, ElementRef } from '@angular/core'; -import { FormBuilder, FormControl } from '@angular/forms'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; /** @@ -26,12 +24,10 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- }) export class AddonModDataFieldTextComponent extends AddonModDataFieldPluginComponent implements OnInit { - control: FormControl; val: number; - constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, - element: ElementRef) { - super(); + constructor(protected fb: FormBuilder) { + super(fb); } /** @@ -50,5 +46,7 @@ export class AddonModDataFieldTextComponent extends AddonModDataFieldPluginCompo if (this.mode == 'edit' && this.value) { this.val = this.value.content; } + + this.addControl('f_' + this.field.id, this.val); } } diff --git a/src/addon/mod/data/fields/textarea/component/textarea.html b/src/addon/mod/data/fields/textarea/component/textarea.html index 5a625183b..d7948298b 100644 --- a/src/addon/mod/data/fields/textarea/component/textarea.html +++ b/src/addon/mod/data/fields/textarea/component/textarea.html @@ -1,10 +1,13 @@ - + - + - + - - + + + + + \ No newline at end of file diff --git a/src/addon/mod/data/fields/textarea/component/textarea.ts b/src/addon/mod/data/fields/textarea/component/textarea.ts index 83336cff7..24bf5ecf9 100644 --- a/src/addon/mod/data/fields/textarea/component/textarea.ts +++ b/src/addon/mod/data/fields/textarea/component/textarea.ts @@ -11,9 +11,8 @@ // 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, ElementRef } from '@angular/core'; -import { FormBuilder, FormControl } from '@angular/forms'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { AddonModDataProvider } from '../../../providers/data'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; @@ -27,13 +26,11 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- }) export class AddonModDataFieldTextareaComponent extends AddonModDataFieldPluginComponent implements OnInit { - control: FormControl; component: string; componentId: number; - constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, - element: ElementRef) { - super(); + constructor(protected fb: FormBuilder, protected textUtils: CoreTextUtilsProvider) { + super(fb); } format(value: any): string { @@ -62,8 +59,8 @@ export class AddonModDataFieldTextareaComponent extends AddonModDataFieldPluginC if (this.mode == 'edit') { const files = (this.value && this.value.files) || [], text = this.value ? this.textUtils.replacePluginfileUrls(this.value.content, files) : ''; - - this.control = this.fb.control(text); } + + this.addControl('f_' + this.field.id, ''); } } diff --git a/src/addon/mod/data/fields/url/component/url.html b/src/addon/mod/data/fields/url/component/url.html index 9212b3aae..e7ff179db 100644 --- a/src/addon/mod/data/fields/url/component/url.html +++ b/src/addon/mod/data/fields/url/component/url.html @@ -1,7 +1,9 @@ - + + - + - + + {{field.name}} \ No newline at end of file diff --git a/src/addon/mod/data/fields/url/component/url.ts b/src/addon/mod/data/fields/url/component/url.ts index 15ac1f53f..e4395fb75 100644 --- a/src/addon/mod/data/fields/url/component/url.ts +++ b/src/addon/mod/data/fields/url/component/url.ts @@ -11,10 +11,8 @@ // 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, ElementRef } from '@angular/core'; -import { FormBuilder, FormControl } from '@angular/forms'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; /** @@ -26,12 +24,10 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- }) export class AddonModDataFieldUrlComponent extends AddonModDataFieldPluginComponent implements OnInit { - control: FormControl; val: number; - constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider, - element: ElementRef) { - super(); + constructor(protected fb: FormBuilder) { + super(fb); } /** @@ -50,5 +46,7 @@ export class AddonModDataFieldUrlComponent extends AddonModDataFieldPluginCompon if (this.mode == 'edit' && this.value) { this.val = this.value.content; } + + this.addControl('f_' + this.field.id, this.val); } } diff --git a/src/addon/mod/data/pages/search/search.html b/src/addon/mod/data/pages/search/search.html new file mode 100644 index 000000000..ad1be4d75 --- /dev/null +++ b/src/addon/mod/data/pages/search/search.html @@ -0,0 +1,55 @@ + + + {{ 'addon.mod_data.search' | translate }} + + + + + + + +
+ + + + + + {{ 'core.sortby' | translate }} + + + {{field.name}} + + + {{ 'addon.mod_data.timeadded' | translate }} + {{ 'addon.mod_data.timemodified' | translate }} + {{ 'addon.mod_data.authorfirstname' | translate }} + {{ 'addon.mod_data.authorlastname' | translate }} + {{ 'addon.mod_data.approved' | translate }} + + + + + + {{ 'addon.mod_data.ascending' | translate }} + + + + {{ 'addon.mod_data.descending' | translate }} + + + + + + +
+
diff --git a/src/addon/mod/data/pages/search/search.module.ts b/src/addon/mod/data/pages/search/search.module.ts new file mode 100644 index 000000000..09b8cfa41 --- /dev/null +++ b/src/addon/mod/data/pages/search/search.module.ts @@ -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 { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModDataComponentsModule } from '../../components/components.module'; +import { AddonModDataSearchPage } from './search'; +import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module'; + +@NgModule({ + declarations: [ + AddonModDataSearchPage, + ], + imports: [ + CoreDirectivesModule, + AddonModDataComponentsModule, + CoreCompileHtmlComponentModule, + IonicPageModule.forChild(AddonModDataSearchPage), + TranslateModule.forChild() + ], +}) +export class AddonModDataSearchPageModule {} diff --git a/src/addon/mod/data/pages/search/search.scss b/src/addon/mod/data/pages/search/search.scss new file mode 100644 index 000000000..4ca495a75 --- /dev/null +++ b/src/addon/mod/data/pages/search/search.scss @@ -0,0 +1,57 @@ +page-addon-mod-data-search { + form { + background-color: $list-background-color; + } + + table { + width: 100%; + } + td { + vertical-align: top; + } + + .addon-data-advanced-search { + background-color: $list-background-color; + + @if ($text-input-md-show-focus-highlight) { + .input-md input:focus { + @include md-input-highlight($text-input-md-highlight-color); + } + } + + .input-md input { + @include padding-horizontal(null, ($item-md-padding-end / 2)); + border-bottom: 1px solid $list-md-border-color; + &:focus { + @include md-input-highlight($text-input-md-highlight-color); + } + } + + .input-ios input { + @include padding-horizontal(null, $item-ios-padding-end / 2); + @include safe-area-padding-horizontal(null, $item-ios-padding-end / 2); + border-bottom: $hairlines-width solid $list-ios-border-color; + &:focus { + @include ios-input-highlight($text-input-ios-highlight-color); + } + } + + .input-wp input { + @include padding-horizontal(null, ($item-wp-padding-end / 2)); + border-bottom: 1px solid $list-wp-border-color; + &:focus { + border-color: $text-input-wp-highlight-color; + } + } + + ion-select { + width: 100%; + left: 0; + max-width: none; + } + + .core-item-has-rich-text-editor { + margin-right: 1px; + } + } +} \ No newline at end of file diff --git a/src/addon/mod/data/pages/search/search.ts b/src/addon/mod/data/pages/search/search.ts new file mode 100644 index 000000000..d50a121ec --- /dev/null +++ b/src/addon/mod/data/pages/search/search.ts @@ -0,0 +1,189 @@ +// (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, ViewController } from 'ionic-angular'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { AddonModDataComponentsModule } from '../../components/components.module'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; + +/** + * Page that displays the search modal. + */ +@IonicPage({ segment: 'addon-mod-data-search' }) +@Component({ + selector: 'page-addon-mod-data-search', + templateUrl: 'search.html', +}) +export class AddonModDataSearchPage { + search: any; + fields: any; + data: any; + advancedSearch: any; + extraImports = [AddonModDataComponentsModule]; + searchForm: FormGroup; + jsData: any; + fieldsArray: any; + + constructor(params: NavParams, private viewCtrl: ViewController, fb: FormBuilder, protected utils: CoreUtilsProvider, + protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate) { + this.search = params.get('search'); + this.fields = params.get('fields'); + this.data = params.get('data'); + + const advanced = {}; + this.search.advanced.forEach((field) => { + advanced[field.name] = field.value ? JSON.parse(field.value) : ''; + }); + this.search.advanced = advanced; + + this.searchForm = fb.group({ + text: [this.search.text], + sortBy: [this.search.sortBy], + sortDirection: [this.search.sortDirection], + firstname: [this.search.advanced['firstname'] || ''], + lastname: [this.search.advanced['lastname'] || ''] + }); + + this.fieldsArray = this.utils.objectToArray(this.fields); + this.advancedSearch = this.renderAdvancedSearchFields(); + } + + /** + * Displays Advanced Search Fields. + * + * @return {string} Generated HTML. + */ + protected renderAdvancedSearchFields(): string { + if (!this.data.asearchtemplate) { + return ''; + } + + this.jsData = { + fields: this.fields, + form: this.searchForm, + search: this.search.advanced + }; + + let template = this.data.asearchtemplate, + replace, render; + + // Replace the fields found on template. + this.fieldsArray.forEach((field) => { + replace = '[[' + field.name + ']]'; + replace = replace.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); + replace = new RegExp(replace, 'gi'); + + // Replace field by a generic directive. + render = ''; + template = template.replace(replace, render); + }); + + // Not pluginable other search elements. + // Replace firstname field by the text input. + replace = new RegExp('##firstname##', 'gi'); + render = ''; + template = template.replace(replace, render); + + // Replace lastname field by the text input. + replace = new RegExp('##lastname##', 'gi'); + render = ''; + template = template.replace(replace, render); + + return template; + } + + /** + * Retrieve the entered data in search in a form. + * + * @param {any} searchedData Array with the entered form values. + * @return {any[]} Array with the answers. + */ + getSearchDataFromForm(searchedData: any): any[] { + const advancedSearch = []; + + // Filter and translate fields to each field plugin. + this.fieldsArray.forEach((field) => { + const fieldData = this.fieldsDelegate.getFieldSearchData(field, searchedData); + + if (fieldData) { + fieldData.forEach((data) => { + data.value = JSON.stringify(data.value); + // WS wants values in Json format. + advancedSearch.push(data); + }); + } + }); + + // Not pluginable other search elements. + if (searchedData['firstname']) { + // WS wants values in Json format. + advancedSearch.push({ + name: 'firstname', + value: JSON.stringify(searchedData['firstname']) + }); + } + + if (searchedData['lastname']) { + // WS wants values in Json format. + advancedSearch.push({ + name: 'lastname', + value: JSON.stringify(searchedData['lastname']) + }); + } + + return advancedSearch; + } + + /** + * Close modal. + * + * @param {any} [data] Data to return to the page. + */ + closeModal(data?: any): void { + this.viewCtrl.dismiss(data); + } + + /** + * Toggles between advanced to normal search. + */ + toggleAdvanced(): void { + this.search.searchingAdvanced = !this.search.searchingAdvanced; + } + + /** + * Done editing. + */ + searchEntries(): void { + const searchedData = this.searchForm.value; + + if (this.search.searchingAdvanced) { + this.search.advanced = this.getSearchDataFromForm(searchedData); + this.search.searching = this.search.advanced.length > 0; + } else { + this.search.text = searchedData.text; + this.search.searching = this.search.text.length > 0; + } + + this.search.sortBy = searchedData.sortBy; + this.search.sortDirection = searchedData.sortDirection; + + this.closeModal(this.search); + } +} diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index 52e90e251..6bba1cea2 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -92,46 +92,6 @@ export class AddonModDataHelperProvider { }); } - /** - * Displays Advanced Search Fields. - * - * @param {string} template Template HMTL. - * @param {any[]} fields Fields that defines every content in the entry. - * @return {string} Generated HTML. - */ - displayAdvancedSearchFields(template: string, fields: any[]): string { - if (!template) { - return ''; - } - - let replace; - - // Replace the fields found on template. - fields.forEach((field) => { - replace = '[[' + field.name + ']]'; - replace = replace.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); - replace = new RegExp(replace, 'gi'); - - // Replace field by a generic directive. - const render = ''; - template = template.replace(replace, render); - }); - - // Not pluginable other search elements. - // Replace firstname field by the text input. - replace = new RegExp('##fn##', 'gi'); - let render = ''; - template = template.replace(replace, render); - - // Replace lastname field by the text input. - replace = new RegExp('##ln##', 'gi'); - render = ''; - template = template.replace(replace, render); - - return template; - } - /** * Displays fields for being shown. * @@ -215,55 +175,6 @@ export class AddonModDataHelperProvider { }; } - /** - * Retrieve the entered data in search in a form. - * We don't use ng-model because it doesn't detect changes done by JavaScript. - * - * @param {any} form Form (DOM element). - * @param {any[]} fields Fields that defines every content in the entry. - * @return {any[]} Array with the answers. - */ - getSearchDataFromForm(form: any, fields: any[]): any[] { - if (!form || !form.elements) { - return []; - } - - const searchedData = this.domUtils.getDataFromForm(form); - - // Filter and translate fields to each field plugin. - const advancedSearch = []; - fields.forEach((field) => { - const fieldData = this.fieldsDelegate.getFieldSearchData(field, searchedData); - - if (fieldData) { - fieldData.forEach((data) => { - data.value = JSON.stringify(data.value); - // WS wants values in Json format. - advancedSearch.push(data); - }); - } - }); - - // Not pluginable other search elements. - if (searchedData['firstname']) { - // WS wants values in Json format. - advancedSearch.push({ - name: 'firstname', - value: JSON.stringify(searchedData['firstname']) - }); - } - - if (searchedData['lastname']) { - // WS wants values in Json format. - advancedSearch.push({ - name: 'lastname', - value: JSON.stringify(searchedData['lastname']) - }); - } - - return advancedSearch; - } - /** * Get a list of stored attachment files for a new entry. See $mmaModDataHelper#storeFiles. * diff --git a/src/app/app.scss b/src/app/app.scss index 510ee0f62..b8ea96c8f 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -171,6 +171,10 @@ ion-avatar ion-img, ion-avatar img { font-style: italic; } +ion-datetime { + position: relative; +} + /** Format Text */ core-format-text[maxHeight], *[core-format-text][maxHeight], core-format-text[ng-reflect-max-height], *[core-format-text][ng-reflect-max-height] { diff --git a/src/components/tabs/tabs.scss b/src/components/tabs/tabs.scss index 09e3ec64a..56b3113aa 100644 --- a/src/components/tabs/tabs.scss +++ b/src/components/tabs/tabs.scss @@ -1,28 +1,59 @@ -core-tabs { - .core-tabs-bar { - left: 0; - position: relative; - z-index: $z-index-toolbar; - display: flex; - width: 100%; +.core-tabs-bar { + left: 0; + position: relative; + z-index: $z-index-toolbar; + display: flex; + width: 100%; + background: $core-top-tabs-background; + + > a { + @extend .tab-button; + background: $core-top-tabs-background; + color: $core-top-tabs-color !important; + font-size: 1.6rem; + border: 0; - > a { - @extend .tab-button; - - background: $core-top-tabs-background; - color: $core-top-tabs-color !important; - font-size: 1.6rem; - border: 0; - - &[aria-selected=true] { - color: $core-top-tabs-color-active !important; - border: 0 !important; - border-bottom: 2px solid $core-top-tabs-color-active !important; - } + &[aria-selected=true] { + color: $core-top-tabs-color-active !important; + border: 0 !important; + border-bottom: 2px solid $core-top-tabs-color-active !important; } } +} +.md .core-tabs-bar > a { + // @extend .tabs-md .tab-button; + min-height: $tabs-md-tab-min-height; + + font-weight: $tabs-md-tab-font-weight; + color: $tabs-md-tab-text-color; +} + +.ios .core-tabs-bar > a { + // @extend .tabs-ios .tab-button; + max-width: $tabs-ios-tab-max-width; + min-height: $tabs-ios-tab-min-height; + + font-size: $tabs-ios-tab-font-size; + font-weight: $tabs-ios-tab-font-weight; + color: $tabs-ios-tab-text-color; +} + +.wp .core-tabs-bar > a { + //@extend .tabs-wp .tab-button; + @include border-radius(0); + + min-height: $tabs-wp-tab-min-height; + + border-bottom: $tabs-wp-tab-border; + font-size: $tabs-wp-tab-font-size; + font-weight: $tabs-wp-tab-font-weight; + color: $tabs-wp-tab-color; + box-shadow: none; +} + +core-tabs { .core-tabs-content-container { height: 100%; @@ -47,7 +78,7 @@ core-tabs { } } - core-tab { + core-tab, .core-tab { display: none; height: 100%; position: relative; @@ -71,22 +102,18 @@ core-tabs { overflow: hidden !important; } -.ios core-tabs { - .core-tabs-bar { - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - > a { - font-size: 1.6rem; - } +.ios .core-tabs-bar { + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + > a { + font-size: 1.6rem; } } -.md core-tabs { - .core-tabs-bar::after { - @extend .header-md::after; - } +.md .core-tabs-bar::after { + @extend .header-md::after; } .ios, .md, .wp { From c626bee40772e056f140dde504ee7f0e837a6b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 16 May 2018 16:12:54 +0200 Subject: [PATCH 06/10] MOBILE-2338 data: Entry page --- .../mod/data/components/index/index.scss | 28 -- src/addon/mod/data/components/index/index.ts | 10 +- src/addon/mod/data/data.scss | 26 ++ src/addon/mod/data/pages/entry/entry.html | 54 +++ .../mod/data/pages/entry/entry.module.ts | 39 +++ src/addon/mod/data/pages/entry/entry.ts | 307 ++++++++++++++++++ src/addon/mod/data/providers/data.ts | 53 +++ src/addon/mod/data/providers/helper.ts | 127 +++++++- 8 files changed, 609 insertions(+), 35 deletions(-) delete mode 100644 src/addon/mod/data/components/index/index.scss create mode 100644 src/addon/mod/data/data.scss create mode 100644 src/addon/mod/data/pages/entry/entry.html create mode 100644 src/addon/mod/data/pages/entry/entry.module.ts create mode 100644 src/addon/mod/data/pages/entry/entry.ts diff --git a/src/addon/mod/data/components/index/index.scss b/src/addon/mod/data/components/index/index.scss deleted file mode 100644 index 106a72942..000000000 --- a/src/addon/mod/data/components/index/index.scss +++ /dev/null @@ -1,28 +0,0 @@ -addon-mod-data-index { - .addon-data-contents { - overflow: visible; - white-space: normal; - word-break: break-word; - padding: $content-padding; - background-color: white; - border-top-width: 1px; - border-bottom-width: 1px; - border-right-width: 0; - border-left-width: 0; - border-style: solid; - border-color: $list-border-color; - - table, tbody { - display: block; - } - - tr { - @extend .row; - padding: 0; - } - - td, th { - @extend .col; - } - } -} \ No newline at end of file diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts index fcfae95e3..e1d85cda6 100644 --- a/src/addon/mod/data/components/index/index.ts +++ b/src/addon/mod/data/components/index/index.ts @@ -75,11 +75,11 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp protected fieldsArray: any; constructor(injector: Injector, private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider, - private dataOffline: AddonModDataOfflineProvider, @Optional() @Optional() content: Content, + private dataOffline: AddonModDataOfflineProvider, @Optional() content: Content, private dataSync: AddonModDataSyncProvider, private timeUtils: CoreTimeUtilsProvider, private groupsProvider: CoreGroupsProvider, private commentsProvider: CoreCommentsProvider, private modalCtrl: ModalController, private utils: CoreUtilsProvider, protected navCtrl: NavController) { - super(injector); + super(injector, content); // Refresh entries on change. this.entryChangedObserver = this.eventsProvider.on(AddonModDataProvider.ENTRY_CHANGED, (eventData) => { @@ -424,9 +424,9 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp gotoEntry(entryId: number): void { const stateParams = { module: this.module, - moduleid: this.module.id, - courseid: this.courseId, - entryid: entryId, + moduleId: this.module.id, + courseId: this.courseId, + entryId: entryId, group: this.selectedGroup }; diff --git a/src/addon/mod/data/data.scss b/src/addon/mod/data/data.scss new file mode 100644 index 000000000..fcc24ecde --- /dev/null +++ b/src/addon/mod/data/data.scss @@ -0,0 +1,26 @@ +.addon-data-contents { + overflow: visible; + white-space: normal; + word-break: break-word; + padding: $content-padding; + background-color: white; + border-top-width: 1px; + border-bottom-width: 1px; + border-right-width: 0; + border-left-width: 0; + border-style: solid; + border-color: $list-border-color; + + table, tbody { + display: block; + } + + tr { + @extend .row; + padding: 0; + } + + td, th { + @extend .col; + } +} diff --git a/src/addon/mod/data/pages/entry/entry.html b/src/addon/mod/data/pages/entry/entry.html new file mode 100644 index 000000000..55db45b17 --- /dev/null +++ b/src/addon/mod/data/pages/entry/entry.html @@ -0,0 +1,54 @@ + + + + + + + + + + + +
+ + {{ 'core.hasdatatosync' | translate: {$a: moduleName} }} +
+ + + {{ 'core.groupsseparate' | translate }} + {{ 'core.groupsvisible' | translate }} + + {{groupOpt.name}} + + + +
+ + + +
+ + + + + + + + + + + + + + + +
+
diff --git a/src/addon/mod/data/pages/entry/entry.module.ts b/src/addon/mod/data/pages/entry/entry.module.ts new file mode 100644 index 000000000..cebf202e2 --- /dev/null +++ b/src/addon/mod/data/pages/entry/entry.module.ts @@ -0,0 +1,39 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreCommentsComponentsModule } from '@core/comments/components/components.module'; +import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module'; +import { AddonModDataComponentsModule } from '../../components/components.module'; +import { AddonModDataEntryPage } from './entry'; + +@NgModule({ + declarations: [ + AddonModDataEntryPage, + ], + imports: [ + CoreDirectivesModule, + CoreComponentsModule, + AddonModDataComponentsModule, + CoreCompileHtmlComponentModule, + CoreCommentsComponentsModule, + IonicPageModule.forChild(AddonModDataEntryPage), + TranslateModule.forChild() + ], +}) +export class AddonModDataEntryPageModule {} diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts new file mode 100644 index 000000000..2dce9294e --- /dev/null +++ b/src/addon/mod/data/pages/entry/entry.ts @@ -0,0 +1,307 @@ +// (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 { Content, IonicPage, NavParams, NavController } from 'ionic-angular'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreGroupsProvider } from '@providers/groups'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { AddonModDataProvider } from '../../providers/data'; +import { AddonModDataHelperProvider } from '../../providers/helper'; +import { AddonModDataOfflineProvider } from '../../providers/offline'; +import { AddonModDataSyncProvider } from '../../providers/sync'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { AddonModDataComponentsModule } from '../../components/components.module'; + +/** + * Page that displays the view entry page. + */ +@IonicPage({ segment: 'addon-mod-data-entry' }) +@Component({ + selector: 'page-addon-mod-data-entry', + templateUrl: 'entry.html', +}) +export class AddonModDataEntryPage { + @ViewChild(Content) content: Content; + + protected module: any; + protected entryId: number; + protected courseId: number; + protected page: number; + protected syncObserver: any; // It will observe the sync auto event. + protected entryChangedObserver: any; // It will observe the changed entry event. + protected fields = {}; + + title = ''; + moduleName = 'data'; + component = AddonModDataProvider.COMPONENT; + entryLoaded = false; + selectedGroup = 0; + entry: any; + offlineActions = []; + hasOffline = false; + cssTemplate = ''; + previousId: number; + nextId: number; + access: any; + data: any; + groupInfo: any; + showComments: any; + entryRendered = ''; + siteId: string; + cssClass = ''; + extraImports = [AddonModDataComponentsModule]; + jsData; + + constructor(params: NavParams, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider, + protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate, + protected courseProvider: CoreCourseProvider, protected dataProvider: AddonModDataProvider, + protected dataOffline: AddonModDataOfflineProvider, protected dataHelper: AddonModDataHelperProvider, + sitesProvider: CoreSitesProvider, protected navCtrl: NavController, + protected eventsProvider: CoreEventsProvider) { + this.module = params.get('module') || {}; + this.entryId = params.get('entryId') || null; + this.courseId = params.get('courseId'); + this.selectedGroup = params.get('group') || 0; + this.page = params.get('page') || null; + + this.siteId = sitesProvider.getCurrentSiteId(); + + this.title = this.module.name; + this.moduleName = this.courseProvider.translateModuleName('data'); + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + this.fetchEntryData(); + + // Refresh data if this discussion is synchronized automatically. + this.syncObserver = this.eventsProvider.on(AddonModDataSyncProvider.AUTO_SYNCED, (data) => { + if ((data.entryId == this.entryId || data.offlineEntryId == this.entryId) && this.data.id == data.dataId) { + if (data.deleted) { + // If deleted, go back. + this.navCtrl.pop(); + } else { + this.entryId = data.entryid; + this.entryLoaded = false; + this.fetchEntryData(true); + } + } + }, this.siteId); + + // Refresh entry on change. + this.entryChangedObserver = this.eventsProvider.on(AddonModDataProvider.ENTRY_CHANGED, (data) => { + if (data.entryId == this.entryId && data.id == data.dataId) { + if (data.deleted) { + // If deleted, go back. + this.navCtrl.pop(); + } else { + this.entryLoaded = false; + this.fetchEntryData(true); + } + } + }, this.siteId); + } + + /** + * Fetch the entry data. + * + * @param {boolean} refresh If refresh the current data or not. + * @return {Promise} Resolved when done. + */ + protected fetchEntryData(refresh?: boolean): Promise { + return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { + this.title = data.name || this.title; + this.data = data; + this.cssClass = 'addon-data-entries-' + data.id; + + return this.setEntryIdFromPage(data.id, this.page, this.selectedGroup).then(() => { + return this.dataProvider.getDatabaseAccessInformation(data.id); + }); + }).then((accessData) => { + this.access = accessData; + + return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule, accessData.canmanageentries) + .then((groupInfo) => { + this.groupInfo = groupInfo; + + // Check selected group is accessible. + if (groupInfo && groupInfo.groups && groupInfo.groups.length > 0) { + if (!groupInfo.groups.some((group) => this.selectedGroup == group.id)) { + this.selectedGroup = groupInfo.groups[0].id; + } + } + + return this.dataOffline.getEntryActions(this.data.id, this.entryId); + }); + }).then((actions) => { + this.offlineActions = actions; + this.hasOffline = !!actions.length; + + return this.dataProvider.getFields(this.data.id).then((fieldsData) => { + this.fields = {}; + fieldsData.forEach((field) => { + this.fields[field.id] = field; + }); + + return this.dataHelper.getEntry(this.data, this.entryId, this.offlineActions); + }); + }).then((entry) => { + entry = entry.entry; + this.cssTemplate = this.dataHelper.prefixCSS(this.data.csstemplate, '.' + this.cssClass); + + // Index contents by fieldid. + const contents = {}; + entry.contents.forEach((field) => { + contents[field.fieldid] = field; + }); + entry.contents = contents; + + const fieldsArray = this.utils.objectToArray(this.fields); + + return this.dataHelper.applyOfflineActions(entry, this.offlineActions, fieldsArray); + }).then((entryData) => { + this.entry = entryData; + + const actions = this.dataHelper.getActions(this.data, this.access, this.entry), + fieldsArray = this.utils.objectToArray(this.fields); + + this.entryRendered = this.dataHelper.displayShowFields(this.data.singletemplate, fieldsArray, + this.entry, 'show', actions); + this.showComments = actions.comments; + + const entries = {}; + entries[this.entryId] = this.entry; + + // Pass the input data to the component. + this.jsData = { + fields: this.fields, + entries: entries, + data: this.data + }; + + return this.dataHelper.getPageInfoByEntry(this.data.id, this.entryId, this.selectedGroup).then((result) => { + this.previousId = result.previousId; + this.nextId = result.nextId; + }); + }).catch((message) => { + if (!refresh) { + // Some call failed, retry without using cache since it might be a new activity. + return this.refreshAllData(); + } + + this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); + + return Promise.reject(null); + }).finally(() => { + this.content && this.content.scrollToTop(); + this.entryLoaded = true; + }); + } + + /** + * Go to selected entry without changing state. + * + * @param {number} entry Entry Id where to go. + * @return {Promise} Resolved when done. + */ + gotoEntry(entry: number): Promise { + this.entryId = entry; + this.page = null; + this.entryLoaded = false; + + return this.fetchEntryData(); + } + + /** + * Refresh all the data. + * + * @return {Promise} Promise resolved when done. + */ + protected refreshAllData(): Promise { + const promises = []; + + promises.push(this.dataProvider.invalidateDatabaseData(this.courseId)); + if (this.data) { + promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId)); + promises.push(this.groupsProvider.invalidateActivityGroupInfo(this.data.coursemodule)); + promises.push(this.dataProvider.invalidateEntriesData(this.data.id)); + } + + return Promise.all(promises).finally(() => { + return this.fetchEntryData(true); + }); + } + + /** + * Refresh the data. + * + * @param {any} [refresher] Refresher. + * @return {Promise} Promise resolved when done. + */ + refreshDatabase(refresher?: any): Promise { + if (this.entryLoaded) { + return this.refreshAllData().finally(() => { + refresher && refresher.complete(); + }); + } + } + + /** + * Set group to see the database. + * + * @param {number} groupId Group identifier to set. + * @return {Promise} Resolved when done. + */ + setGroup(groupId: number): Promise { + this.selectedGroup = groupId; + this.entryLoaded = false; + + return this.setEntryIdFromPage(this.data.id, 0, this.selectedGroup).then(() => { + return this.fetchEntryData(); + }); + } + + /** + * Convenience function to translate page number to entry identifier. + * + * @param {number} dataId Data Id. + * @param {number} [pageNumber] Page number where to go + * @param {number} group Group Id to get the entry. + * @return {Promise} Resolved when done. + */ + protected setEntryIdFromPage(dataId: number, pageNumber?: number, group?: number): Promise { + if (typeof pageNumber == 'number') { + return this.dataHelper.getPageInfoByPage(dataId, pageNumber, group).then((result) => { + this.entryId = result.entryId; + this.page = null; + }); + } + + return Promise.resolve(); + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + this.syncObserver && this.syncObserver.off(); + this.entryChangedObserver && this.entryChangedObserver.off(); + } +} diff --git a/src/addon/mod/data/providers/data.ts b/src/addon/mod/data/providers/data.ts index b4beec32b..f38951efa 100644 --- a/src/addon/mod/data/providers/data.ts +++ b/src/addon/mod/data/providers/data.ts @@ -218,6 +218,59 @@ export class AddonModDataProvider { }); } + /** + * Performs the whole fetch of the entries in the database. + * + * @param {number} dataId Data ID. + * @param {number} [groupId] Group ID. + * @param {string} [sort] Sort the records by this field id. See AddonModDataProvider#getEntries for more info. + * @param {string} [order] The direction of the sorting. See AddonModDataProvider#getEntries for more info. + * @param {number} [perPage] Records per page to fetch. It has to match with the prefetch. + * Default on AddonModDataProvider.PER_PAGE. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + fetchAllEntries(dataId: number, groupId: number = 0, sort: string = '0', order: string = 'DESC', + perPage: number = AddonModDataProvider.PER_PAGE, forceCache: boolean = false, ignoreCache: boolean = false, + siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, [], 0, siteId); + } + + /** + * Recursive call on fetch all entries. + * + * @param {number} dataId Data ID. + * @param {number} groupId Group ID. + * @param {string} sort Sort the records by this field id. See AddonModDataProvider#getEntries for more info. + * @param {string} order The direction of the sorting. See AddonModDataProvider#getEntries for more info. + * @param {number} perPage Records per page to fetch. It has to match with the prefetch. + * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false. + * @param {boolean} ignoreCache True if it should ignore cached data (it will always fail in offline or server down). + * @param {any} entries Entries already fetch (just to concatenate them). + * @param {number} page Page of records to return. + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved when done. + */ + protected fetchEntriesRecursive(dataId: number, groupId: number, sort: string, order: string, perPage: number, + forceCache: boolean, ignoreCache: boolean, entries: any, page: number, siteId: string): Promise { + return this.getEntries(dataId, groupId, sort, order, page, perPage, forceCache, ignoreCache, siteId) + .then((result) => { + entries = entries.concat(result.entries); + + const canLoadMore = perPage > 0 && ((page + 1) * perPage) < result.totalcount; + if (canLoadMore) { + return this.fetchEntriesRecursive(dataId, groupId, sort, order, perPage, forceCache, ignoreCache, entries, page + 1, + siteId); + } + + return entries; + }); + } + /** * Get cache key for data data WS calls. * diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index 6bba1cea2..91834da47 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -15,7 +15,6 @@ import { Injectable } from '@angular/core'; import { CoreSitesProvider } from '@providers/sites'; import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { TranslateService } from '@ngx-translate/core'; import { AddonModDataFieldsDelegate } from './fields-delegate'; import { AddonModDataOfflineProvider } from './offline'; @@ -27,7 +26,7 @@ import { AddonModDataProvider } from './data'; @Injectable() export class AddonModDataHelperProvider { - constructor(private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider, + constructor(private sitesProvider: CoreSitesProvider, protected dataProvider: AddonModDataProvider, private translate: TranslateService, private fieldsDelegate: AddonModDataFieldsDelegate, private dataOffline: AddonModDataOfflineProvider, private fileUploaderProvider: CoreFileUploaderProvider) { } @@ -175,6 +174,130 @@ export class AddonModDataHelperProvider { }; } + /** + * Fetch all entries and return it's Id + * + * @param {number} dataId Data ID. + * @param {number} groupId Group ID. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. Current if not defined. + * @return {Promise} Resolved with an array of entry ID. + */ + getAllEntriesIds(dataId: number, groupId: number, forceCache: boolean = false, ignoreCache: boolean = false, siteId?: string): + Promise { + return this.dataProvider.fetchAllEntries(dataId, groupId, undefined, undefined, undefined, forceCache, ignoreCache, siteId) + .then((entries) => { + return entries.map((entry) => { + return entry.id; + }); + }); + } + + /** + * Get an online or offline entry. + * + * @param {any} data Database. + * @param {number} entryId Entry ID. + * @param {any} [offlineActions] Offline data with the actions done. Required for offline entries. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with the entry. + */ + getEntry(data: any, entryId: number, offlineActions?: any, siteId?: string): Promise { + if (entryId > 0) { + // It's an online entry, get it from WS. + return this.dataProvider.getEntry(data.id, entryId, siteId); + } + + // It's an offline entry, search it in the offline actions. + return this.sitesProvider.getSite(siteId).then((site) => { + const offlineEntry = offlineActions.find((offlineAction) => { + return offlineAction.action == 'add'; + }); + + if (offlineEntry) { + const siteInfo = site.getInfo(); + + return {entry: { + id: offlineEntry.entryid, + canmanageentry: true, + approved: !data.approval || data.manageapproved, + dataid: offlineEntry.dataid, + groupid: offlineEntry.groupid, + timecreated: -offlineEntry.entryid, + timemodified: -offlineEntry.entryid, + userid: siteInfo.userid, + fullname: siteInfo.fullname, + contents: {} + } + }; + } + }); + } + + /** + * Get page info related to an entry. + * + * @param {number} dataId Data ID. + * @param {number} entryId Entry ID. + * @param {number} groupId Group ID. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. Current if not defined. + * @return {Promise} Containing page number, if has next and have following page. + */ + getPageInfoByEntry(dataId: number, entryId: number, groupId: number, forceCache: boolean = false, + ignoreCache: boolean = false, siteId?: string): Promise { + return this.getAllEntriesIds(dataId, groupId, forceCache, ignoreCache, siteId).then((entries) => { + const index = entries.findIndex((entry) => { + return entry == entryId; + }); + + if (index >= 0) { + return { + previousId: entries[index - 1] || false, + nextId: entries[index + 1] || false, + entryId: entryId, + page: index + 1, // Parsed to natural language. + numEntries: entries.length + }; + } + + return false; + }); + } + + /** + * Get page info related to an entry by page number. + * + * @param {number} dataId Data ID. + * @param {number} page Page number. + * @param {number} groupId Group ID. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. Current if not defined. + * @return {Promise} Containing page number, if has next and have following page. + */ + getPageInfoByPage(dataId: number, page: number, groupId: number, forceCache: boolean = false, + ignoreCache: boolean = false, siteId?: string): Promise { + return this.getAllEntriesIds(dataId, groupId, forceCache, ignoreCache, siteId).then((entries) => { + const index = page - 1, + entryId = entries[index]; + + if (entryId) { + return { + previousId: entries[index - 1] || null, + nextId: entries[index + 1] || null, + entryId: entryId, + page: page, // Parsed to natural language. + numEntries: entries.length + }; + } + + return false; + }); + } + /** * Get a list of stored attachment files for a new entry. See $mmaModDataHelper#storeFiles. * From d53a7acda52fb052eebd32a5e3edb2856c32f44b Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 22 May 2018 13:31:52 +0200 Subject: [PATCH 07/10] MOBILE-2338 compile: Monitor jsData changes --- .../dynamic-component/dynamic-component.ts | 32 +++----------- .../components/compile-html/compile-html.ts | 44 ++++++++++++++++--- src/providers/utils/dom.ts | 28 +++++++++++- 3 files changed, 70 insertions(+), 34 deletions(-) diff --git a/src/components/dynamic-component/dynamic-component.ts b/src/components/dynamic-component/dynamic-component.ts index 1692b7663..5e232ce04 100644 --- a/src/components/dynamic-component/dynamic-component.ts +++ b/src/components/dynamic-component/dynamic-component.ts @@ -18,6 +18,7 @@ import { } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreLoggerProvider } from '@providers/logger'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; /** * Component to create another component dynamically. @@ -68,7 +69,9 @@ export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck { protected differ: any; // To detect changes in the data input. constructor(logger: CoreLoggerProvider, protected factoryResolver: ComponentFactoryResolver, differs: KeyValueDiffers, - @Optional() protected navCtrl: NavController, protected cdr: ChangeDetectorRef, protected element: ElementRef) { + @Optional() protected navCtrl: NavController, protected cdr: ChangeDetectorRef, protected element: ElementRef, + protected domUtils: CoreDomUtilsProvider) { + this.logger = logger.getInstance('CoreDynamicComponent'); this.differ = differs.find([]).create(); } @@ -99,7 +102,7 @@ export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck { if (changes) { this.setInputData(); if (this.instance.ngOnChanges) { - this.instance.ngOnChanges(this.createChangesForComponent(changes)); + this.instance.ngOnChanges(this.domUtils.createChangesFromKeyValueDiff(changes)); } } } @@ -170,29 +173,4 @@ export class CoreDynamicComponent implements OnInit, OnChanges, DoCheck { this.instance[name] = this.data[name]; } } - - /** - * Given the changes on the data input, create the changes object for the component. - * - * @param {any} changes Changes in the data input (detected by KeyValueDiffer). - * @return {{[name: string]: SimpleChange}} List of changes for the component. - */ - protected createChangesForComponent(changes: any): { [name: string]: SimpleChange } { - const newChanges: { [name: string]: SimpleChange } = {}; - - // Added items are considered first change. - changes.forEachAddedItem((item) => { - newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, true); - }); - - // Changed or removed items aren't first change. - changes.forEachChangedItem((item) => { - newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, false); - }); - changes.forEachRemovedItem((item) => { - newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, true); - }); - - return newChanges; - } } diff --git a/src/core/compile/components/compile-html/compile-html.ts b/src/core/compile/components/compile-html/compile-html.ts index 8b7a7a218..3151f4d29 100644 --- a/src/core/compile/components/compile-html/compile-html.ts +++ b/src/core/compile/components/compile-html/compile-html.ts @@ -14,10 +14,11 @@ import { Component, Input, OnInit, OnChanges, OnDestroy, ViewContainerRef, ViewChild, ComponentRef, SimpleChange, ChangeDetectorRef, - ElementRef, Optional, Output, EventEmitter + ElementRef, Optional, Output, EventEmitter, DoCheck, KeyValueDiffers } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreCompileProvider } from '../../providers/compile'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; /** * This component has a behaviour similar to $compile for AngularJS. Given an HTML code, it will compile it so all its @@ -38,7 +39,7 @@ import { CoreCompileProvider } from '../../providers/compile'; selector: 'core-compile-html', template: '' }) -export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { +export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { @Input() text: string; // The HTML text to display. @Input() javascript: string; // The Javascript to execute in the component. @Input() jsData: any; // Data to pass to the fake component. @@ -50,11 +51,30 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { @ViewChild('dynamicComponent', { read: ViewContainerRef }) container: ViewContainerRef; protected componentRef: ComponentRef; + protected componentInstance: any; protected element; + protected differ: any; // To detect changes in the jsData input. constructor(protected compileProvider: CoreCompileProvider, protected cdr: ChangeDetectorRef, element: ElementRef, - @Optional() protected navCtrl: NavController) { + @Optional() protected navCtrl: NavController, differs: KeyValueDiffers, protected domUtils: CoreDomUtilsProvider) { this.element = element.nativeElement; + this.differ = differs.find([]).create(); + } + + /** + * Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays). + */ + ngDoCheck(): void { + if (this.componentInstance) { + // Check if there's any change in the jsData object. + const changes = this.differ.diff(this.jsData); + if (changes) { + this.setInputData(); + if (this.componentInstance.ngOnChanges) { + this.componentInstance.ngOnChanges(this.domUtils.createChangesFromKeyValueDiff(changes)); + } + } + } } /** @@ -96,6 +116,9 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { // Create the component, using the text as the template. return class CoreCompileHtmlFakeComponent implements OnInit { constructor() { + // Store this instance so it can be accessed by the outer component. + compileInstance.componentInstance = this; + // If there is some javascript to run, prepare the instance. if (compileInstance.javascript) { compileInstance.compileProvider.injectLibraries(this, compileInstance.extraProviders); @@ -107,9 +130,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { this['componentContainer'] = compileInstance.element; // Add the data passed to the component. - for (const name in compileInstance.jsData) { - this[name] = compileInstance.jsData[name]; - } + compileInstance.setInputData(); } ngOnInit(): void { @@ -120,4 +141,15 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { } }; } + + /** + * Set the JS data as input data of the component instance. + */ + protected setInputData(): void { + if (this.componentInstance) { + for (const name in this.jsData) { + this.componentInstance[name] = this.jsData[name]; + } + } + } } diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index c66f47855..510c7876d 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, SimpleChange } from '@angular/core'; import { LoadingController, Loading, ToastController, Toast, AlertController, Alert, Platform, Content, ModalController @@ -148,6 +148,32 @@ export class CoreDomUtilsProvider { return {coreCanceled: true}; } + /** + * Given a list of changes for a component input detected by a KeyValueDiffers, create an object similar to the one + * passed to the ngOnChanges functions. + * + * @param {any} changes Changes detected by KeyValueDiffer. + * @return {{[name: string]: SimpleChange}} Changes in a format like ngOnChanges. + */ + createChangesFromKeyValueDiff(changes: any): { [name: string]: SimpleChange } { + const newChanges: { [name: string]: SimpleChange } = {}; + + // Added items are considered first change. + changes.forEachAddedItem((item) => { + newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, true); + }); + + // Changed or removed items aren't first change. + changes.forEachChangedItem((item) => { + newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, false); + }); + changes.forEachRemovedItem((item) => { + newChanges[item.key] = new SimpleChange(item.previousValue, item.currentValue, true); + }); + + return newChanges; + } + /** * Extract the downloadable URLs from an HTML code. * From 1bcc3c35b778a750d9529b914ad55cd0c70b8673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 17 May 2018 16:31:19 +0200 Subject: [PATCH 08/10] MOBILE-2338 data: Edit page --- .../components/submission/submission.html | 2 +- .../data/classes/field-plugin-component.ts | 47 ++- .../components/field-plugin/field-plugin.ts | 19 +- src/addon/mod/data/data.scss | 75 ++++ .../fields/checkbox/component/checkbox.html | 12 +- .../fields/checkbox/component/checkbox.ts | 32 +- .../data/fields/checkbox/providers/handler.ts | 35 +- .../mod/data/fields/date/component/date.html | 8 +- .../mod/data/fields/date/component/date.ts | 39 +- .../mod/data/fields/date/providers/handler.ts | 8 +- .../mod/data/fields/file/component/file.html | 8 +- .../mod/data/fields/file/component/file.ts | 36 +- .../fields/latlong/component/latlong.html | 22 +- .../data/fields/latlong/component/latlong.ts | 27 +- .../mod/data/fields/menu/component/menu.html | 11 +- .../mod/data/fields/menu/component/menu.ts | 15 +- .../mod/data/fields/menu/providers/handler.ts | 1 - .../fields/multimenu/component/multimenu.html | 11 +- .../fields/multimenu/component/multimenu.ts | 32 +- .../fields/multimenu/providers/handler.ts | 17 +- .../data/fields/number/component/number.html | 10 +- .../data/fields/number/component/number.ts | 23 +- .../data/fields/number/providers/handler.ts | 18 + .../fields/picture/component/picture.html | 17 +- .../data/fields/picture/component/picture.ts | 96 +++-- .../radiobutton/component/radiobutton.html | 11 +- .../radiobutton/component/radiobutton.ts | 15 +- .../mod/data/fields/text/component/text.html | 12 +- .../mod/data/fields/text/component/text.ts | 22 +- .../fields/textarea/component/textarea.html | 11 +- .../fields/textarea/component/textarea.ts | 28 +- .../mod/data/fields/url/component/url.html | 12 +- .../mod/data/fields/url/component/url.ts | 22 +- src/addon/mod/data/pages/edit/edit.html | 31 ++ src/addon/mod/data/pages/edit/edit.module.ts | 39 ++ src/addon/mod/data/pages/edit/edit.ts | 373 ++++++++++++++++++ src/addon/mod/data/pages/entry/entry.ts | 6 +- src/addon/mod/data/pages/search/search.html | 2 +- src/addon/mod/data/pages/search/search.scss | 57 --- src/addon/mod/data/pages/search/search.ts | 4 +- src/addon/mod/data/providers/data.ts | 172 +++++++- src/addon/mod/data/providers/helper.ts | 127 +++++- src/addon/mod/data/providers/link-handler.ts | 2 +- .../mod/data/providers/prefetch-handler.ts | 193 ++++++++- .../mod/feedback/providers/link-handler.ts | 3 +- .../feedback/providers/prefetch-handler.ts | 4 +- .../datetime/component/datetime.html | 2 +- .../userprofilefield/menu/component/menu.html | 2 +- .../userprofilefield/text/component/text.html | 2 +- src/components/input-errors/input-errors.html | 9 +- src/components/input-errors/input-errors.ts | 22 +- 51 files changed, 1375 insertions(+), 429 deletions(-) create mode 100644 src/addon/mod/data/pages/edit/edit.html create mode 100644 src/addon/mod/data/pages/edit/edit.module.ts create mode 100644 src/addon/mod/data/pages/edit/edit.ts delete mode 100644 src/addon/mod/data/pages/search/search.scss diff --git a/src/addon/mod/assign/components/submission/submission.html b/src/addon/mod/assign/components/submission/submission.html index 8d4cb9ffc..11becfd51 100644 --- a/src/addon/mod/assign/components/submission/submission.html +++ b/src/addon/mod/assign/components/submission/submission.html @@ -153,7 +153,7 @@ {{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo.grade} }} - + diff --git a/src/addon/mod/data/classes/field-plugin-component.ts b/src/addon/mod/data/classes/field-plugin-component.ts index 512037367..349ac1be7 100644 --- a/src/addon/mod/data/classes/field-plugin-component.ts +++ b/src/addon/mod/data/classes/field-plugin-component.ts @@ -11,13 +11,13 @@ // 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 { Input } from '@angular/core'; -import { FormGroup, FormBuilder } from '@angular/forms'; +import { Input, OnInit, OnChanges, SimpleChange } from '@angular/core'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; /** * Base class for component to render a field. */ -export class AddonModDataFieldPluginComponent { +export class AddonModDataFieldPluginComponent implements OnInit, OnChanges { @Input() mode: string; // The render mode. @Input() field: any; // The field to render. @Input() value?: any; // The value of the field. @@ -45,7 +45,46 @@ export class AddonModDataFieldPluginComponent { } if (this.mode == 'edit') { - this.form.addControl(fieldName, this.fb.control(value || null)); + this.form.addControl(fieldName, this.fb.control(value, this.field.required ? Validators.required : null)); } } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.init(); + } + + /** + * Initialize field. + */ + protected init(): void { + return; + } + + /** + * Return if is shown or list mode. + * + * @return {boolean} True if mode is show or list. + */ + isShowOrListMode(): boolean { + return this.mode == 'list' || this.mode == 'show'; + } + + /** + * Component being changed. + */ + ngOnChanges(changes: { [name: string]: SimpleChange }): void { + if (this.isShowOrListMode() && changes.value) { + this.updateValue(changes.value.currentValue); + } + } + + /** + * Update value being shown. + */ + protected updateValue(value: any): void { + this.value = value; + } } diff --git a/src/addon/mod/data/components/field-plugin/field-plugin.ts b/src/addon/mod/data/components/field-plugin/field-plugin.ts index 4e73fafa2..c2ae6751f 100644 --- a/src/addon/mod/data/components/field-plugin/field-plugin.ts +++ b/src/addon/mod/data/components/field-plugin/field-plugin.ts @@ -11,7 +11,7 @@ // 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, Injector, ViewChild } from '@angular/core'; +import { Component, Input, OnInit, Injector, ViewChild, OnChanges, SimpleChange } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { AddonModDataProvider } from '../../providers/data'; import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; @@ -24,7 +24,7 @@ import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-comp selector: 'addon-mod-data-field-plugin', templateUrl: 'field-plugin.html', }) -export class AddonModDataFieldPluginComponent implements OnInit { +export class AddonModDataFieldPluginComponent implements OnInit, OnChanges { @ViewChild(CoreDynamicComponent) dynamicComponent: CoreDynamicComponent; @Input() mode: string; // The render mode. @@ -70,10 +70,23 @@ export class AddonModDataFieldPluginComponent implements OnInit { form: this.form, search: this.search }; - } }).finally(() => { this.fieldLoaded = true; }); } + + /** + * Component being changed. + */ + ngOnChanges(changes: { [name: string]: SimpleChange }): void { + if (this.fieldLoaded && this.data) { + if (this.mode == 'edit' && changes.error) { + this.data.error = changes.error.currentValue; + } + if ((this.mode == 'show' || this.mode == 'list') && changes.value) { + this.data.value = changes.value.currentValue; + } + } + } } diff --git a/src/addon/mod/data/data.scss b/src/addon/mod/data/data.scss index fcc24ecde..75b6d30ae 100644 --- a/src/addon/mod/data/data.scss +++ b/src/addon/mod/data/data.scss @@ -24,3 +24,78 @@ @extend .col; } } + +page-addon-mod-data-search, +page-addon-mod-data-edit { + table { + width: 100%; + } + td { + vertical-align: top; + } + + .item.item-input.item-block .item-inner ion-input, + .item.item-input.item-input-has-focus .item-inner ion-input, + .item.item-input.input-has-focus .item-inner ion-input { + border: 0 !important; + box-shadow: none; + } + + .addon-data-lantlong { + display: flex; + } + + form, .addon-data-advanced-search { + background-color: $list-background-color; + + .core-mark-required { + float: right; + + + ion-input, + + ion-select { + padding-right: 20px; + } + } + + @if ($text-input-md-show-focus-highlight) { + .input-md input:focus { + @include md-input-highlight($text-input-md-highlight-color); + } + } + + .input-md input { + @include padding-horizontal(null, ($item-md-padding-end / 2)); + border-bottom: 1px solid $list-md-border-color; + &:focus { + @include md-input-highlight($text-input-md-highlight-color); + } + } + + .input-ios input { + @include padding-horizontal(null, $item-ios-padding-end / 2); + @include safe-area-padding-horizontal(null, $item-ios-padding-end / 2); + border-bottom: $hairlines-width solid $list-ios-border-color; + &:focus { + @include ios-input-highlight($text-input-ios-highlight-color); + } + } + + .input-wp input { + @include padding-horizontal(null, ($item-wp-padding-end / 2)); + border-bottom: 1px solid $list-wp-border-color; + &:focus { + border-color: $text-input-wp-highlight-color; + } + } + + ion-select { + width: 100%; + left: 0; + max-width: none; + } + + .core-item-has-rich-text-editor { + margin-right: 1px; + } + } +} \ No newline at end of file diff --git a/src/addon/mod/data/fields/checkbox/component/checkbox.html b/src/addon/mod/data/fields/checkbox/component/checkbox.html index 6bd9c1bd6..8006a3fdb 100644 --- a/src/addon/mod/data/fields/checkbox/component/checkbox.html +++ b/src/addon/mod/data/fields/checkbox/component/checkbox.html @@ -1,11 +1,9 @@ - - - - - + + + {{option.key}} - + {{ 'addon.mod_data.selectedrequired' | translate }} @@ -14,4 +12,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/addon/mod/data/fields/checkbox/component/checkbox.ts b/src/addon/mod/data/fields/checkbox/component/checkbox.ts index 37fe87a89..5d0ef6f35 100644 --- a/src/addon/mod/data/fields/checkbox/component/checkbox.ts +++ b/src/addon/mod/data/fields/checkbox/component/checkbox.ts @@ -11,7 +11,7 @@ // 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 { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; @@ -22,7 +22,7 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- selector: 'addon-mod-data-field-checkbox', templateUrl: 'checkbox.html' }) -export class AddonModDataFieldCheckboxComponent extends AddonModDataFieldPluginComponent implements OnInit { +export class AddonModDataFieldCheckboxComponent extends AddonModDataFieldPluginComponent { options = []; @@ -31,16 +31,11 @@ export class AddonModDataFieldCheckboxComponent extends AddonModDataFieldPluginC } /** - * Component being initialized. + * Initialize field. */ - ngOnInit(): void { - this.mode = this.mode == 'list' ? 'show' : this.mode; - this.render(); - } - - protected render(): void { - if (this.mode == 'show') { - this.value.content = this.value && this.value.content && this.value.content.split('##').join('
'); + protected init(): void { + if (this.isShowOrListMode()) { + this.updateValue(this.value); return; } @@ -49,11 +44,12 @@ export class AddonModDataFieldCheckboxComponent extends AddonModDataFieldPluginC return { key: option, value: option }; }); + const values = []; if (this.mode == 'edit' && this.value && this.value.content) { this.value.content.split('##').forEach((value) => { const x = this.options.findIndex((option) => value == option.key); if (x >= 0) { - this.options[x].selected = true; + values.push(value); } }); } @@ -62,6 +58,16 @@ export class AddonModDataFieldCheckboxComponent extends AddonModDataFieldPluginC this.addControl('f_' + this.field.id + '_allreq'); } - this.addControl('f_' + this.field.id); + this.addControl('f_' + this.field.id, values); + } + + /** + * Update value being shown. + * + * @param {any} value New value to be set. + */ + protected updateValue(value: any): void { + this.value = value; + this.value.content = value && value.content && value.content.split('##').join('
'); } } diff --git a/src/addon/mod/data/fields/checkbox/providers/handler.ts b/src/addon/mod/data/fields/checkbox/providers/handler.ts index e307cc6e2..48c50d8d7 100644 --- a/src/addon/mod/data/fields/checkbox/providers/handler.ts +++ b/src/addon/mod/data/fields/checkbox/providers/handler.ts @@ -49,19 +49,12 @@ export class AddonModDataFieldCheckboxHandler implements AddonModDataFieldHandle const fieldName = 'f_' + field.id, reqName = 'f_' + field.id + '_allreq'; - const options = field.param1.split('\n'), - checkboxes = [], - values = []; - options.forEach((option) => { - if (inputData[fieldName + '_' + option]) { - checkboxes.push(option); - } - }); + const values = []; - if (checkboxes.length > 0) { + if (inputData[fieldName] && inputData[fieldName].length > 0) { values.push({ name: fieldName, - value: checkboxes + value: inputData[fieldName] }); if (inputData[reqName]) { @@ -87,17 +80,10 @@ export class AddonModDataFieldCheckboxHandler implements AddonModDataFieldHandle getFieldEditData(field: any, inputData: any, originalFieldData: any): any { const fieldName = 'f_' + field.id; - const options = field.param1.split('\n'), - checkboxes = []; - options.forEach((option) => { - if (inputData[fieldName + '_' + option]) { - checkboxes.push(option); - } - }); - if (checkboxes.length > 0) { + if (inputData[fieldName] && inputData[fieldName].length > 0) { return [{ fieldid: field.id, - value: checkboxes + value: inputData[fieldName] }]; } @@ -113,18 +99,11 @@ export class AddonModDataFieldCheckboxHandler implements AddonModDataFieldHandle * @return {Promise | boolean} If the field has changes. */ hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | boolean { - const fieldName = 'f_' + field.id, - checkboxes = []; - - inputData[fieldName].forEach((value, option) => { - if (value) { - checkboxes.push(option); - } - }); + const fieldName = 'f_' + field.id; originalFieldData = (originalFieldData && originalFieldData.content) || ''; - return checkboxes.join('##') != originalFieldData; + return inputData[fieldName].join('##') != originalFieldData; } /** diff --git a/src/addon/mod/data/fields/date/component/date.html b/src/addon/mod/data/fields/date/component/date.html index 41aa4d756..9d69823cc 100644 --- a/src/addon/mod/data/fields/date/component/date.html +++ b/src/addon/mod/data/fields/date/component/date.html @@ -1,7 +1,7 @@ - - + + - + {{ 'addon.mod_data.usedate' | translate }} @@ -10,5 +10,5 @@ - + diff --git a/src/addon/mod/data/fields/date/component/date.ts b/src/addon/mod/data/fields/date/component/date.ts index 1ea4b8656..c2c368be8 100644 --- a/src/addon/mod/data/fields/date/component/date.ts +++ b/src/addon/mod/data/fields/date/component/date.ts @@ -11,7 +11,7 @@ // 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 { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; @@ -23,11 +23,8 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- selector: 'addon-mod-data-field-date', templateUrl: 'date.html' }) -export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginComponent implements OnInit { +export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginComponent { - values = {}; - enable: boolean; - val: any; format: string; constructor(protected fb: FormBuilder, protected timeUtils: CoreTimeUtilsProvider) { @@ -35,35 +32,27 @@ export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginCompo } /** - * Component being initialized. + * Initialize field. */ - ngOnInit(): void { - this.mode = this.mode == 'list' ? 'show' : this.mode; - this.render(); - } - - protected render(): void { - if (this.mode == 'show') { + protected init(): void { + if (this.isShowOrListMode()) { return; } - if (!this.value) { - this.value = { - content: Math.floor(Date.now() / 1000) - }; - } - - this.val = new Date(this.value.content * 1000); - + let val; this.format = this.timeUtils.getLocalizedDateFormat('LL'); if (this.mode == 'search') { this.addControl('f_' + this.field.id + '_z'); - this.search['f_' + this.field.id] = this.search['f_' + this.field.id + '_y'] ? new Date( - this.search['f_' + this.field.id + '_y'] + '-' + this.search['f_' + this.field.id + '_m'] + '-' + - this.search['f_' + this.field.id + '_d']) : this.val; + val = this.search['f_' + this.field.id + '_y'] ? new Date(this.search['f_' + this.field.id + '_y'] + '-' + + this.search['f_' + this.field.id + '_m'] + '-' + this.search['f_' + this.field.id + '_d']) : new Date(); + + this.search['f_' + this.field.id] = val.toISOString(); + } else { + val = this.value && this.value.content ? new Date(parseInt(this.value.content, 10) * 1000) : new Date(); + val = val.toISOString(); } - this.addControl('f_' + this.field.id, this.val); + this.addControl('f_' + this.field.id, val); } } diff --git a/src/addon/mod/data/fields/date/providers/handler.ts b/src/addon/mod/data/fields/date/providers/handler.ts index c96d1c65f..f36d166b7 100644 --- a/src/addon/mod/data/fields/date/providers/handler.ts +++ b/src/addon/mod/data/fields/date/providers/handler.ts @@ -51,7 +51,7 @@ export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler { if (inputData[enabledName] && typeof inputData[fieldName] == 'string') { const values = [], - date = inputData[fieldName].split('-'), + date = inputData[fieldName].substr(0, 10).split('-'), year = date[0], month = date[1], day = date[2]; @@ -88,9 +88,9 @@ export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler { getFieldEditData(field: any, inputData: any, originalFieldData: any): any { const fieldName = 'f_' + field.id; - if (inputData[fieldName]) { + if (typeof inputData[fieldName] == 'string') { const values = [], - date = inputData[fieldName].split('-'), + date = inputData[fieldName].substr(0, 10).split('-'), year = date[0], month = date[1], day = date[2]; @@ -126,7 +126,7 @@ export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler { */ hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | boolean { const fieldName = 'f_' + field.id, - input = inputData[fieldName] || ''; + input = inputData[fieldName] && inputData[fieldName].substr(0, 10) || ''; originalFieldData = (originalFieldData && originalFieldData.content && new Date(originalFieldData.content * 1000).toISOString().substr(0, 10)) || ''; diff --git a/src/addon/mod/data/fields/file/component/file.html b/src/addon/mod/data/fields/file/component/file.html index 2416b3c6c..9b6dd2791 100644 --- a/src/addon/mod/data/fields/file/component/file.html +++ b/src/addon/mod/data/fields/file/component/file.html @@ -1,14 +1,14 @@ - - - + + + - +
diff --git a/src/addon/mod/data/fields/file/component/file.ts b/src/addon/mod/data/fields/file/component/file.ts index b802d6f17..e39faafe5 100644 --- a/src/addon/mod/data/fields/file/component/file.ts +++ b/src/addon/mod/data/fields/file/component/file.ts @@ -11,7 +11,7 @@ // 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 { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; import { CoreFileSessionProvider } from '@providers/file-session'; @@ -24,7 +24,7 @@ import { AddonModDataProvider } from '../../../providers/data'; selector: 'addon-mod-data-field-file', templateUrl: 'file.html' }) -export class AddonModDataFieldFileComponent extends AddonModDataFieldPluginComponent implements OnInit { +export class AddonModDataFieldFileComponent extends AddonModDataFieldPluginComponent { files = []; component: string; @@ -35,14 +35,6 @@ export class AddonModDataFieldFileComponent extends AddonModDataFieldPluginCompo super(fb); } - /** - * Component being initialized. - */ - ngOnInit(): void { - this.mode = this.mode == 'list' ? 'show' : this.mode; - this.render(); - } - /** * Get the files from the input value. * @@ -60,20 +52,32 @@ export class AddonModDataFieldFileComponent extends AddonModDataFieldPluginCompo return files; } - protected render(): void { - if (this.mode == 'show' || this.mode == 'edit') { + /** + * Initialize field. + */ + protected init(): void { + if (this.mode != 'search') { this.component = AddonModDataProvider.COMPONENT; this.componentId = this.database.coursemodule; - this.files = this.getFiles(this.value); + this.updateValue(this.value); - if (this.mode != 'show') { - // Edit mode, the list shouldn't change so there is no need to watch it. + if (this.mode == 'edit') { this.maxSizeBytes = parseInt(this.field.param3, 10); this.fileSessionprovider.setFiles(this.component, this.database.id + '_' + this.field.id, this.files); } + } else { + this.addControl('f_' + this.field.id); } + } - this.addControl('f_' + this.field.id); + /** + * Update value being shown. + * + * @param {any} value New value to be set. + */ + protected updateValue(value: any): void { + this.value = value; + this.files = this.getFiles(value); } } diff --git a/src/addon/mod/data/fields/latlong/component/latlong.html b/src/addon/mod/data/fields/latlong/component/latlong.html index 9303ec6d7..a219e554f 100644 --- a/src/addon/mod/data/fields/latlong/component/latlong.html +++ b/src/addon/mod/data/fields/latlong/component/latlong.html @@ -1,21 +1,19 @@ - + - - - - - - + +
+ °N - - - +
+
+ °E - +
+
- + {{ formatLatLong(north, east) }} \ No newline at end of file diff --git a/src/addon/mod/data/fields/latlong/component/latlong.ts b/src/addon/mod/data/fields/latlong/component/latlong.ts index f9933bf6e..2b7fda2be 100644 --- a/src/addon/mod/data/fields/latlong/component/latlong.ts +++ b/src/addon/mod/data/fields/latlong/component/latlong.ts @@ -11,7 +11,7 @@ // 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 { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { Platform } from 'ionic-angular'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; @@ -23,7 +23,7 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- selector: 'addon-mod-data-field-latlong', templateUrl: 'latlong.html' }) -export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginComponent implements OnInit { +export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginComponent { north: number; east: number; @@ -69,17 +69,11 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo } /** - * Component being initialized. + * Initialize field. */ - ngOnInit(): void { - this.mode = this.mode == 'list' ? 'show' : this.mode; - this.render(); - } - - protected render(): void { + protected init(): void { if (this.value) { - this.north = (this.value && parseFloat(this.value.content)) || null; - this.east = (this.value && parseFloat(this.value.content1)) || null; + this.updateValue(this.value); } if (this.mode == 'edit') { @@ -89,4 +83,15 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo this.addControl('f_' + this.field.id); } } + + /** + * Update value being shown. + * + * @param {any} value New value to be set. + */ + protected updateValue(value: any): void { + this.value = value; + this.north = (value && parseFloat(value.content)) || null; + this.east = (value && parseFloat(value.content1)) || null; + } } diff --git a/src/addon/mod/data/fields/menu/component/menu.html b/src/addon/mod/data/fields/menu/component/menu.html index c7e9e03c8..9a9357500 100644 --- a/src/addon/mod/data/fields/menu/component/menu.html +++ b/src/addon/mod/data/fields/menu/component/menu.html @@ -1,11 +1,10 @@ - - - - - + + + {{ 'addon.mod_data.menuchoose' | translate }} {{option}} + - \ No newline at end of file + \ No newline at end of file diff --git a/src/addon/mod/data/fields/menu/component/menu.ts b/src/addon/mod/data/fields/menu/component/menu.ts index 7a7f867bf..e722375ef 100644 --- a/src/addon/mod/data/fields/menu/component/menu.ts +++ b/src/addon/mod/data/fields/menu/component/menu.ts @@ -11,7 +11,7 @@ // 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 { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; @@ -22,7 +22,7 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- selector: 'addon-mod-data-field-menu', templateUrl: 'menu.html' }) -export class AddonModDataFieldMenuComponent extends AddonModDataFieldPluginComponent implements OnInit { +export class AddonModDataFieldMenuComponent extends AddonModDataFieldPluginComponent { options = []; @@ -31,15 +31,10 @@ export class AddonModDataFieldMenuComponent extends AddonModDataFieldPluginCompo } /** - * Component being initialized. + * Initialize field. */ - ngOnInit(): void { - this.mode = this.mode == 'list' ? 'show' : this.mode; - this.render(); - } - - protected render(): void { - if (this.mode == 'show') { + protected init(): void { + if (this.isShowOrListMode()) { return; } diff --git a/src/addon/mod/data/fields/menu/providers/handler.ts b/src/addon/mod/data/fields/menu/providers/handler.ts index 4e6938c28..e8427cc66 100644 --- a/src/addon/mod/data/fields/menu/providers/handler.ts +++ b/src/addon/mod/data/fields/menu/providers/handler.ts @@ -47,7 +47,6 @@ export class AddonModDataFieldMenuHandler implements AddonModDataFieldHandler { */ getFieldSearchData(field: any, inputData: any): any { const fieldName = 'f_' + field.id; - if (inputData[fieldName]) { return [{ name: fieldName, diff --git a/src/addon/mod/data/fields/multimenu/component/multimenu.html b/src/addon/mod/data/fields/multimenu/component/multimenu.html index 6bd9c1bd6..e1d9081ba 100644 --- a/src/addon/mod/data/fields/multimenu/component/multimenu.html +++ b/src/addon/mod/data/fields/multimenu/component/multimenu.html @@ -1,10 +1,9 @@ - - - - - + + + {{option.key}} + @@ -14,4 +13,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/addon/mod/data/fields/multimenu/component/multimenu.ts b/src/addon/mod/data/fields/multimenu/component/multimenu.ts index 963b279f9..a93ee67d5 100644 --- a/src/addon/mod/data/fields/multimenu/component/multimenu.ts +++ b/src/addon/mod/data/fields/multimenu/component/multimenu.ts @@ -11,7 +11,7 @@ // 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 { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; @@ -22,7 +22,7 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- selector: 'addon-mod-data-field-multimenu', templateUrl: 'multimenu.html' }) -export class AddonModDataFieldMultimenuComponent extends AddonModDataFieldPluginComponent implements OnInit { +export class AddonModDataFieldMultimenuComponent extends AddonModDataFieldPluginComponent { options = []; @@ -31,16 +31,11 @@ export class AddonModDataFieldMultimenuComponent extends AddonModDataFieldPlugin } /** - * Component being initialized. + * Initialize field. */ - ngOnInit(): void { - this.mode = this.mode == 'list' ? 'show' : this.mode; - this.render(); - } - - protected render(): void { - if (this.mode == 'show') { - this.value.content = this.value && this.value.content && this.value.content.split('##').join('
'); + protected init(): void { + if (this.isShowOrListMode()) { + this.updateValue(this.value); return; } @@ -49,11 +44,12 @@ export class AddonModDataFieldMultimenuComponent extends AddonModDataFieldPlugin return { key: option, value: option }; }); + const values = []; if (this.mode == 'edit' && this.value && this.value.content) { this.value.content.split('##').forEach((value) => { const x = this.options.findIndex((option) => value == option.key); if (x >= 0) { - this.options[x].selected = true; + values.push(value); } }); } @@ -62,6 +58,16 @@ export class AddonModDataFieldMultimenuComponent extends AddonModDataFieldPlugin this.addControl('f_' + this.field.id + '_allreq'); } - this.addControl('f_' + this.field.id); + this.addControl('f_' + this.field.id, values); + } + + /** + * Update value being shown. + * + * @param {any} value New value to be set. + */ + protected updateValue(value: any): void { + this.value = value; + this.value.content = value && value.content && value.content.split('##').join('
'); } } diff --git a/src/addon/mod/data/fields/multimenu/providers/handler.ts b/src/addon/mod/data/fields/multimenu/providers/handler.ts index c5d5c5fcc..716da06bf 100644 --- a/src/addon/mod/data/fields/multimenu/providers/handler.ts +++ b/src/addon/mod/data/fields/multimenu/providers/handler.ts @@ -81,13 +81,10 @@ export class AddonModDataFieldMultimenuHandler implements AddonModDataFieldHandl const fieldName = 'f_' + field.id; if (inputData[fieldName] && inputData[fieldName].length > 0) { - const options = inputData[fieldName].split('###'); - if (options.length > 0) { - return [{ - fieldid: field.id, - value: options - }]; - } + return [{ + fieldid: field.id, + value: inputData[fieldName] + }]; } return false; @@ -102,11 +99,11 @@ export class AddonModDataFieldMultimenuHandler implements AddonModDataFieldHandl * @return {Promise | boolean} If the field has changes. */ hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | boolean { - const fieldName = 'f_' + field.id, - input = inputData[fieldName] || ''; + const fieldName = 'f_' + field.id; + originalFieldData = (originalFieldData && originalFieldData.content) || ''; - return input != originalFieldData; + return inputData[fieldName].join('##') != originalFieldData; } /** diff --git a/src/addon/mod/data/fields/number/component/number.html b/src/addon/mod/data/fields/number/component/number.html index 17a4a6423..aa71edc43 100644 --- a/src/addon/mod/data/fields/number/component/number.html +++ b/src/addon/mod/data/fields/number/component/number.html @@ -1,9 +1,7 @@ - - - - - + + + - + diff --git a/src/addon/mod/data/fields/number/component/number.ts b/src/addon/mod/data/fields/number/component/number.ts index 9fe4d6472..c7dcb1127 100644 --- a/src/addon/mod/data/fields/number/component/number.ts +++ b/src/addon/mod/data/fields/number/component/number.ts @@ -11,7 +11,7 @@ // 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 { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; @@ -22,31 +22,26 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- selector: 'addon-mod-data-field-number', templateUrl: 'number.html' }) -export class AddonModDataFieldNumberComponent extends AddonModDataFieldPluginComponent implements OnInit { - - val: number; +export class AddonModDataFieldNumberComponent extends AddonModDataFieldPluginComponent{ constructor(protected fb: FormBuilder) { super(fb); } /** - * Component being initialized. + * Initialize field. */ - ngOnInit(): void { - this.mode = this.mode == 'list' ? 'show' : this.mode; - this.render(); - } - - protected render(): void { - if (this.mode == 'show') { + protected init(): void { + if (this.isShowOrListMode()) { return; } + let value; if (this.mode == 'edit' && this.value) { - this.val = this.value && parseFloat(this.value.content); + const v = parseFloat(this.value.content); + value = isNaN(v) ? '' : v; } - this.addControl('f_' + this.field.id, this.val); + this.addControl('f_' + this.field.id, value); } } diff --git a/src/addon/mod/data/fields/number/providers/handler.ts b/src/addon/mod/data/fields/number/providers/handler.ts index 654681d72..40663e8f0 100644 --- a/src/addon/mod/data/fields/number/providers/handler.ts +++ b/src/addon/mod/data/fields/number/providers/handler.ts @@ -40,6 +40,24 @@ export class AddonModDataFieldNumberHandler extends AddonModDataFieldTextHandler return AddonModDataFieldNumberComponent; } + /** + * Get field data in changed. + * + * @param {any} field Defines the field to be rendered. + * @param {any} inputData Data entered in the edit form. + * @param {any} originalFieldData Original field entered data. + * @return {Promise | boolean} If the field has changes. + */ + hasFieldDataChanged(field: any, inputData: any, originalFieldData: any): Promise | boolean { + const fieldName = 'f_' + field.id, + input = typeof inputData[fieldName] != 'undefined' ? parseFloat(inputData[fieldName]) : ''; + + originalFieldData = (originalFieldData && typeof originalFieldData.content != 'undefined') ? + parseFloat(originalFieldData.content) : ''; + + return input != originalFieldData; + } + /** * Check and get field requeriments. * diff --git a/src/addon/mod/data/fields/picture/component/picture.html b/src/addon/mod/data/fields/picture/component/picture.html index 7b24e5a3f..ee6cdcad0 100644 --- a/src/addon/mod/data/fields/picture/component/picture.html +++ b/src/addon/mod/data/fields/picture/component/picture.html @@ -1,15 +1,10 @@ - + + + - - - - - - {{ 'addon.mod_data.alttext' | translate }} - - °N - + {{ 'addon.mod_data.alttext' | translate }} + @@ -18,4 +13,4 @@ - \ No newline at end of file + diff --git a/src/addon/mod/data/fields/picture/component/picture.ts b/src/addon/mod/data/fields/picture/component/picture.ts index 810b63cc7..5c6796d90 100644 --- a/src/addon/mod/data/fields/picture/component/picture.ts +++ b/src/addon/mod/data/fields/picture/component/picture.ts @@ -11,7 +11,7 @@ // 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 { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; import { CoreFileSessionProvider } from '@providers/file-session'; @@ -24,7 +24,7 @@ import { AddonModDataProvider } from '../../../providers/data'; selector: 'addon-mod-data-field-picture', templateUrl: 'picture.html' }) -export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginComponent implements OnInit { +export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginComponent { files = []; component: string; @@ -35,7 +35,6 @@ export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginCo entryId: number; imageUrl: string; title: string; - alttext: string; width: string; height: string; @@ -43,13 +42,6 @@ export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginCo super(fb); } - /** - * Component being initialized. - */ - ngOnInit(): void { - this.render(); - } - /** * Get the files from the input value. * @@ -78,49 +70,67 @@ export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginCo return files.find((file) => file.filename == filenameSeek) || false; } - protected render(): void { + /** + * Initialize field. + */ + protected init(): void { if (this.mode != 'search') { this.component = AddonModDataProvider.COMPONENT; this.componentId = this.database.coursemodule; - // Edit mode, the list shouldn't change so there is no need to watch it. - const files = this.value && this.value.files || []; - - // Get image or thumb. - if (files.length > 0) { - const filenameSeek = this.mode == 'list' ? 'thumb_' + this.value.content : this.value.content; - this.image = this.findFile(files, filenameSeek); - - if (!this.image && this.mode == 'list') { - this.image = this.findFile(files, this.value.content); - } - - this.files = [this.image]; - } else { - this.image = false; - this.files = []; - } + this.updateValue(this.value); if (this.mode == 'edit') { this.maxSizeBytes = parseInt(this.field.param3, 10); this.fileSessionprovider.setFiles(this.component, this.database.id + '_' + this.field.id, this.files); - this.alttext = (this.value && this.value.content1) || ''; - } else { - this.entryId = (this.value && this.value.recordid) || null; - this.title = (this.value && this.value.content1) || ''; - this.imageUrl = null; - if (this.image) { - if (this.image.offline) { - this.imageUrl = (this.image && this.image.toURL()) || null; - } else { - this.imageUrl = (this.image && this.image.fileurl) || null; - } - } - this.width = this.field.param1 || ''; - this.height = this.field.param2 || ''; + + const alttext = (this.value && this.value.content1) || ''; + this.addControl('f_' + this.field.id + '_alttext', alttext); } + } else { + this.addControl('f_' + this.field.id); + } + } + + /** + * Update value being shown. + * + * @param {any} value New value to be set. + */ + protected updateValue(value: any): void { + this.value = value; + + // Edit mode, the list shouldn't change so there is no need to watch it. + const files = value && value.files || []; + + // Get image or thumb. + if (files.length > 0) { + const filenameSeek = this.mode == 'list' ? 'thumb_' + value.content : value.content; + this.image = this.findFile(files, filenameSeek); + + if (!this.image && this.mode == 'list') { + this.image = this.findFile(files, value.content); + } + + this.files = [this.image]; + } else { + this.image = false; + this.files = []; } - this.addControl('f_' + this.field.id); + if (this.mode != 'edit') { + this.entryId = (value && value.recordid) || null; + this.title = (value && value.content1) || ''; + this.imageUrl = null; + if (this.image) { + if (this.image.offline) { + this.imageUrl = (this.image && this.image.toURL()) || null; + } else { + this.imageUrl = (this.image && this.image.fileurl) || null; + } + } + this.width = this.field.param1 || ''; + this.height = this.field.param2 || ''; + } } } diff --git a/src/addon/mod/data/fields/radiobutton/component/radiobutton.html b/src/addon/mod/data/fields/radiobutton/component/radiobutton.html index c7e9e03c8..9a9357500 100644 --- a/src/addon/mod/data/fields/radiobutton/component/radiobutton.html +++ b/src/addon/mod/data/fields/radiobutton/component/radiobutton.html @@ -1,11 +1,10 @@ - - - - - + + + {{ 'addon.mod_data.menuchoose' | translate }} {{option}} + - \ No newline at end of file + \ No newline at end of file diff --git a/src/addon/mod/data/fields/radiobutton/component/radiobutton.ts b/src/addon/mod/data/fields/radiobutton/component/radiobutton.ts index 400d3aa78..e2b64425b 100644 --- a/src/addon/mod/data/fields/radiobutton/component/radiobutton.ts +++ b/src/addon/mod/data/fields/radiobutton/component/radiobutton.ts @@ -11,7 +11,7 @@ // 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 { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; @@ -22,7 +22,7 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- selector: 'addon-mod-data-field-radiobutton', templateUrl: 'radiobutton.html' }) -export class AddonModDataFieldRadiobuttonComponent extends AddonModDataFieldPluginComponent implements OnInit { +export class AddonModDataFieldRadiobuttonComponent extends AddonModDataFieldPluginComponent { options = []; @@ -31,15 +31,10 @@ export class AddonModDataFieldRadiobuttonComponent extends AddonModDataFieldPlug } /** - * Component being initialized. + * Initialize field. */ - ngOnInit(): void { - this.mode = this.mode == 'list' ? 'show' : this.mode; - this.render(); - } - - protected render(): void { - if (this.mode == 'show') { + protected init(): void { + if (this.isShowOrListMode()) { return; } diff --git a/src/addon/mod/data/fields/text/component/text.html b/src/addon/mod/data/fields/text/component/text.html index ad562369f..fa8846daf 100644 --- a/src/addon/mod/data/fields/text/component/text.html +++ b/src/addon/mod/data/fields/text/component/text.html @@ -1,9 +1,7 @@ - - - - - - + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/src/addon/mod/data/fields/text/component/text.ts b/src/addon/mod/data/fields/text/component/text.ts index ce5f91649..1d9523d51 100644 --- a/src/addon/mod/data/fields/text/component/text.ts +++ b/src/addon/mod/data/fields/text/component/text.ts @@ -11,7 +11,7 @@ // 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 { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; @@ -22,31 +22,25 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- selector: 'addon-mod-data-field-text', templateUrl: 'text.html' }) -export class AddonModDataFieldTextComponent extends AddonModDataFieldPluginComponent implements OnInit { - - val: number; +export class AddonModDataFieldTextComponent extends AddonModDataFieldPluginComponent { constructor(protected fb: FormBuilder) { super(fb); } /** - * Component being initialized. + * Initialize field. */ - ngOnInit(): void { - this.mode = this.mode == 'list' ? 'show' : this.mode; - this.render(); - } - - protected render(): void { - if (this.mode == 'show') { + protected init(): void { + if (this.isShowOrListMode()) { return; } + let value; if (this.mode == 'edit' && this.value) { - this.val = this.value.content; + value = this.value.content; } - this.addControl('f_' + this.field.id, this.val); + this.addControl('f_' + this.field.id, value); } } diff --git a/src/addon/mod/data/fields/textarea/component/textarea.html b/src/addon/mod/data/fields/textarea/component/textarea.html index d7948298b..d07c54193 100644 --- a/src/addon/mod/data/fields/textarea/component/textarea.html +++ b/src/addon/mod/data/fields/textarea/component/textarea.html @@ -1,13 +1,10 @@ - - - - - - + + + - \ No newline at end of file + \ No newline at end of file diff --git a/src/addon/mod/data/fields/textarea/component/textarea.ts b/src/addon/mod/data/fields/textarea/component/textarea.ts index 24bf5ecf9..532d762fa 100644 --- a/src/addon/mod/data/fields/textarea/component/textarea.ts +++ b/src/addon/mod/data/fields/textarea/component/textarea.ts @@ -11,7 +11,7 @@ // 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 { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { AddonModDataProvider } from '../../../providers/data'; @@ -24,7 +24,7 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- selector: 'addon-mod-data-field-textarea', templateUrl: 'textarea.html' }) -export class AddonModDataFieldTextareaComponent extends AddonModDataFieldPluginComponent implements OnInit { +export class AddonModDataFieldTextareaComponent extends AddonModDataFieldPluginComponent { component: string; componentId: number; @@ -33,6 +33,12 @@ export class AddonModDataFieldTextareaComponent extends AddonModDataFieldPluginC super(fb); } + /** + * Format value to be shown. Replacing plugin file Urls. + * + * @param {any} value Value to replace. + * @return {string} Replaced string to be rendered. + */ format(value: any): string { const files = (value && value.files) || []; @@ -40,27 +46,23 @@ export class AddonModDataFieldTextareaComponent extends AddonModDataFieldPluginC } /** - * Component being initialized. + * Initialize field. */ - ngOnInit(): void { - this.mode = this.mode == 'list' ? 'show' : this.mode; - this.render(); - } - - protected render(): void { - if (this.mode == 'show') { + protected init(): void { + if (this.isShowOrListMode()) { this.component = AddonModDataProvider.COMPONENT; this.componentId = this.database.coursemodule; return; } + let text; // Check if rich text editor is enabled. if (this.mode == 'edit') { - const files = (this.value && this.value.files) || [], - text = this.value ? this.textUtils.replacePluginfileUrls(this.value.content, files) : ''; + const files = (this.value && this.value.files) || []; + text = this.value ? this.textUtils.replacePluginfileUrls(this.value.content, files) : ''; } - this.addControl('f_' + this.field.id, ''); + this.addControl('f_' + this.field.id, text); } } diff --git a/src/addon/mod/data/fields/url/component/url.html b/src/addon/mod/data/fields/url/component/url.html index e7ff179db..f1c018713 100644 --- a/src/addon/mod/data/fields/url/component/url.html +++ b/src/addon/mod/data/fields/url/component/url.html @@ -1,9 +1,7 @@ - - - - - - + + + + -{{field.name}} \ No newline at end of file +{{field.name}} \ No newline at end of file diff --git a/src/addon/mod/data/fields/url/component/url.ts b/src/addon/mod/data/fields/url/component/url.ts index e4395fb75..f6d2a8450 100644 --- a/src/addon/mod/data/fields/url/component/url.ts +++ b/src/addon/mod/data/fields/url/component/url.ts @@ -11,7 +11,7 @@ // 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 { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; @@ -22,31 +22,25 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- selector: 'addon-mod-data-field-url', templateUrl: 'url.html' }) -export class AddonModDataFieldUrlComponent extends AddonModDataFieldPluginComponent implements OnInit { - - val: number; +export class AddonModDataFieldUrlComponent extends AddonModDataFieldPluginComponent { constructor(protected fb: FormBuilder) { super(fb); } /** - * Component being initialized. + * Initialize field. */ - ngOnInit(): void { - this.mode = this.mode == 'list' ? 'show' : this.mode; - this.render(); - } - - protected render(): void { - if (this.mode == 'show') { + protected init(): void { + if (this.isShowOrListMode()) { return; } + let value; if (this.mode == 'edit' && this.value) { - this.val = this.value.content; + value = this.value.content; } - this.addControl('f_' + this.field.id, this.val); + this.addControl('f_' + this.field.id, value); } } diff --git a/src/addon/mod/data/pages/edit/edit.html b/src/addon/mod/data/pages/edit/edit.html new file mode 100644 index 000000000..85b2fbcba --- /dev/null +++ b/src/addon/mod/data/pages/edit/edit.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + {{ 'core.groupsseparate' | translate }} + {{ 'core.groupsvisible' | translate }} + + {{groupOpt.name}} + + + +
+ + +
+ +
+
+
+
diff --git a/src/addon/mod/data/pages/edit/edit.module.ts b/src/addon/mod/data/pages/edit/edit.module.ts new file mode 100644 index 000000000..fac3251a6 --- /dev/null +++ b/src/addon/mod/data/pages/edit/edit.module.ts @@ -0,0 +1,39 @@ +// (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 { CoreComponentsModule } from '@components/components.module'; +import { CoreCommentsComponentsModule } from '@core/comments/components/components.module'; +import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module'; +import { AddonModDataComponentsModule } from '../../components/components.module'; +import { AddonModDataEditPage } from './edit'; + +@NgModule({ + declarations: [ + AddonModDataEditPage, + ], + imports: [ + CoreDirectivesModule, + CoreComponentsModule, + AddonModDataComponentsModule, + CoreCompileHtmlComponentModule, + CoreCommentsComponentsModule, + IonicPageModule.forChild(AddonModDataEditPage), + TranslateModule.forChild() + ], +}) +export class AddonModDataEditPageModule {} diff --git a/src/addon/mod/data/pages/edit/edit.ts b/src/addon/mod/data/pages/edit/edit.ts new file mode 100644 index 000000000..2f6e44566 --- /dev/null +++ b/src/addon/mod/data/pages/edit/edit.ts @@ -0,0 +1,373 @@ +// (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 { Content, IonicPage, NavParams, NavController } from 'ionic-angular'; +import { TranslateService } from '@ngx-translate/core'; +import { FormGroup } from '@angular/forms'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreGroupsProvider } from '@providers/groups'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { AddonModDataProvider } from '../../providers/data'; +import { AddonModDataHelperProvider } from '../../providers/helper'; +import { AddonModDataOfflineProvider } from '../../providers/offline'; +import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate'; +import { AddonModDataComponentsModule } from '../../components/components.module'; + +/** + * Page that displays the view edit page. + */ +@IonicPage({ segment: 'addon-mod-data-edit' }) +@Component({ + selector: 'page-addon-mod-data-edit', + templateUrl: 'edit.html', +}) +export class AddonModDataEditPage { + @ViewChild(Content) content: Content; + + protected module: any; + protected courseId: number; + protected data: any; + protected entryId: number; + protected entry: any; + protected offlineActions = []; + protected fields = {}; + protected fieldsArray = []; + protected siteId: string; + protected offline: boolean; + protected forceLeave = false; // To allow leaving the page without checking for changes. + + title = ''; + component = AddonModDataProvider.COMPONENT; + loaded = false; + selectedGroup = 0; + cssClass = ''; + cssTemplate = ''; + groupInfo: any; + editFormRender = ''; + editForm: FormGroup; + extraImports = [AddonModDataComponentsModule]; + jsData: any; + errors = {}; + + constructor(params: NavParams, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider, + protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate, + protected courseProvider: CoreCourseProvider, protected dataProvider: AddonModDataProvider, + protected dataOffline: AddonModDataOfflineProvider, protected dataHelper: AddonModDataHelperProvider, + sitesProvider: CoreSitesProvider, protected navCtrl: NavController, protected translate: TranslateService, + protected eventsProvider: CoreEventsProvider, protected fileUploaderProvider: CoreFileUploaderProvider) { + this.module = params.get('module') || {}; + this.entryId = params.get('entryId') || null; + this.courseId = params.get('courseId'); + this.selectedGroup = params.get('group') || 0; + + this.siteId = sitesProvider.getCurrentSiteId(); + + this.title = this.module.name; + + this.editForm = new FormGroup({}); + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + this.fetchEntryData(); + } + + /** + * Check if we can leave the page or not and ask to confirm the lost of data. + * + * @return {boolean | Promise} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + if (this.forceLeave) { + return true; + } + + const inputData = this.editForm.value; + + return this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, + this.entry.contents).then((changed) => { + if (!changed) { + return Promise.resolve(); + } + + // Show confirmation if some data has been modified. + return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')); + }).then(() => { + // Delete the local files from the tmp folder. + return this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id, + this.entry.contents).then((files) => { + this.fileUploaderProvider.clearTmpFiles(files); + }); + }); + } + + /** + * Fetch the entry data. + * + * @return {Promise} Resolved when done. + */ + protected fetchEntryData(): Promise { + return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => { + this.title = data.name || this.title; + this.data = data; + this.cssClass = 'addon-data-entries-' + data.id; + + return this.dataProvider.getDatabaseAccessInformation(data.id); + }).then((accessData) => { + this.cssTemplate = this.dataHelper.prefixCSS(this.data.csstemplate, '.' + this.cssClass); + + if (this.entryId) { + return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule, accessData.canmanageentries) + .then((groupInfo) => { + this.groupInfo = groupInfo; + + // Check selected group is accessible. + if (groupInfo && groupInfo.groups && groupInfo.groups.length > 0) { + if (!groupInfo.groups.some((group) => this.selectedGroup == group.id)) { + this.selectedGroup = groupInfo.groups[0].id; + } + } + }); + } + }).then(() => { + return this.dataOffline.getEntryActions(this.data.id, this.entryId); + }).then((actions) => { + this.offlineActions = actions; + + return this.dataProvider.getFields(this.data.id); + }).then((fieldsData) => { + this.fields = {}; + fieldsData.forEach((field) => { + this.fields[field.id] = field; + }); + + this.fieldsArray = fieldsData; + + return this.dataHelper.getEntry(this.data, this.entryId, this.offlineActions); + }).then((entry) => { + if (entry) { + entry = entry.entry; + + // Index contents by fieldid. + const contents = {}; + entry.contents.forEach((field) => { + contents[field.fieldid] = field; + }); + entry.contents = contents; + } else { + entry = { + contents: {} + }; + } + + return this.dataHelper.applyOfflineActions(entry, this.offlineActions, this.fieldsArray); + }).then((entryData) => { + this.entry = entryData; + + this.editFormRender = this.displayEditFields(); + }).catch((message) => { + this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); + + return Promise.reject(null); + }).finally(() => { + this.loaded = true; + }); + } + + /** + * Saves data. + * + * @return {Promise} Resolved when done. + */ + save(): Promise { + const inputData = this.editForm.value; + + return this.dataHelper.hasEditDataChanged(inputData, this.fieldsArray, this.data.id, + this.entry.contents).then((changed) => { + + if (!changed) { + if (this.entryId) { + return this.returnToEntryList(); + } + + // New entry, no changes means no field filled, warn the user. + return Promise.reject('addon.mod_data.emptyaddform'); + } + + const modal = this.domUtils.showModalLoading('core.sending', true); + + // Create an ID to assign files. + const entryTemp = this.entryId ? this.entryId : - (new Date().getTime()); + + return this.dataHelper.getEditDataFromForm(inputData, this.fieldsArray, this.data.id, entryTemp, this.entry.contents, + this.offline).catch((e) => { + if (!this.offline) { + // Cannot submit in online, prepare for offline usage. + this.offline = true; + + return this.dataHelper.getEditDataFromForm(inputData, this.fieldsArray, this.data.id, entryTemp, + this.entry.contents, this.offline); + } + + return Promise.reject(e); + }).then((editData) => { + if (editData.length > 0) { + if (this.entryId) { + return this.dataProvider.editEntry(this.data.id, this.entryId, this.courseId, editData, this.fields, + undefined, this.offline); + } + + return this.dataProvider.addEntry(this.data.id, entryTemp, this.courseId, editData, this.selectedGroup, + this.fields, undefined, this.offline); + } + + return false; + }).then((result: any) => { + if (!result) { + // No field filled, warn the user. + return Promise.reject('addon.mod_data.emptyaddform'); + } + + // This is done if entry is updated when editing or creating if not. + if ((this.entryId && result.updated) || (!this.entryId && result.newentryid)) { + const promises = []; + + this.entryId = this.entryId || result.newentryid; + + promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId, this.siteId)); + promises.push(this.dataProvider.invalidateEntriesData(this.data.id, this.siteId)); + + return Promise.all(promises).then(() => { + this.eventsProvider.trigger(AddonModDataProvider.ENTRY_CHANGED, + { dataId: this.data.id, entryId: this.entryId } , this.siteId); + }).finally(() => { + return this.returnToEntryList(); + }); + } else { + this.errors = {}; + result.fieldnotifications.forEach((fieldNotif) => { + const field = this.fieldsArray.find((field) => field.name == fieldNotif.fieldname); + if (field) { + this.errors[field.id] = fieldNotif.notification; + } + }); + this.jsData['errors'] = this.errors; + + setTimeout(() => { + this.scrollToFirstError(); + }); + } + }).finally(() => { + modal.dismiss(); + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Cannot edit entry', true); + + return Promise.reject(null); + }); + + } + + /** + * Set group to see the database. + * + * @param {number} groupId Group identifier to set. + * @return {Promise} Resolved when done. + */ + setGroup(groupId: number): Promise { + this.selectedGroup = groupId; + this.loaded = false; + + return this.fetchEntryData(); + } + + /** + * Displays Edit Search Fields. + * + * @return {string} Generated HTML. + */ + protected displayEditFields(): string { + if (!this.data.addtemplate) { + return ''; + } + + this.jsData = { + fields: this.fields, + contents: this.entry.contents, + form: this.editForm, + data: this.data, + errors: this.errors + }; + + let replace, + render, + template = this.data.addtemplate; + + // Replace the fields found on template. + this.fieldsArray.forEach((field) => { + replace = '[[' + field.name + ']]'; + replace = replace.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); + replace = new RegExp(replace, 'gi'); + + // Replace field by a generic directive. + render = '\ + '; + template = template.replace(replace, render); + + // Replace the field id tag. + replace = '[[' + field.name + '#id]]'; + replace = replace.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); + replace = new RegExp(replace, 'gi'); + + template = template.replace(replace, 'field_' + field.id); + }); + + return template; + } + + /** + * Return to the entry list (previous page) discarding temp data. + * + * @return {Promise} Resolved when done. + */ + protected returnToEntryList(): Promise { + const inputData = this.editForm.value; + + return this.dataHelper.getEditTmpFiles(inputData, this.fieldsArray, this.data.id, + this.entry.contents).then((files) => { + this.fileUploaderProvider.clearTmpFiles(files); + }).finally(() => { + // Go back to entry list. + this.forceLeave = true; + this.navCtrl.pop(); + }); + } + + /** + * Scroll to first error or to the top if not found. + */ + protected scrollToFirstError(): void { + if (!this.domUtils.scrollToElementBySelector(this.content, '.addon-data-error')) { + this.content.scrollToTop(); + } + } +} diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts index 2dce9294e..c6e4c8cb5 100644 --- a/src/addon/mod/data/pages/entry/entry.ts +++ b/src/addon/mod/data/pages/entry/entry.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChild } from '@angular/core'; +import { Component, ViewChild, OnDestroy } from '@angular/core'; import { Content, IonicPage, NavParams, NavController } from 'ionic-angular'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -35,7 +35,7 @@ import { AddonModDataComponentsModule } from '../../components/components.module selector: 'page-addon-mod-data-entry', templateUrl: 'entry.html', }) -export class AddonModDataEntryPage { +export class AddonModDataEntryPage implements OnDestroy { @ViewChild(Content) content: Content; protected module: any; @@ -107,7 +107,7 @@ export class AddonModDataEntryPage { // Refresh entry on change. this.entryChangedObserver = this.eventsProvider.on(AddonModDataProvider.ENTRY_CHANGED, (data) => { - if (data.entryId == this.entryId && data.id == data.dataId) { + if (data.entryId == this.entryId && this.data.id == data.dataId) { if (data.deleted) { // If deleted, go back. this.navCtrl.pop(); diff --git a/src/addon/mod/data/pages/search/search.html b/src/addon/mod/data/pages/search/search.html index ad1be4d75..687e42605 100644 --- a/src/addon/mod/data/pages/search/search.html +++ b/src/addon/mod/data/pages/search/search.html @@ -19,7 +19,7 @@ - {{ 'core.sortby' | translate }} + {{ 'core.sortby' | translate }} {{field.name}} diff --git a/src/addon/mod/data/pages/search/search.scss b/src/addon/mod/data/pages/search/search.scss deleted file mode 100644 index 4ca495a75..000000000 --- a/src/addon/mod/data/pages/search/search.scss +++ /dev/null @@ -1,57 +0,0 @@ -page-addon-mod-data-search { - form { - background-color: $list-background-color; - } - - table { - width: 100%; - } - td { - vertical-align: top; - } - - .addon-data-advanced-search { - background-color: $list-background-color; - - @if ($text-input-md-show-focus-highlight) { - .input-md input:focus { - @include md-input-highlight($text-input-md-highlight-color); - } - } - - .input-md input { - @include padding-horizontal(null, ($item-md-padding-end / 2)); - border-bottom: 1px solid $list-md-border-color; - &:focus { - @include md-input-highlight($text-input-md-highlight-color); - } - } - - .input-ios input { - @include padding-horizontal(null, $item-ios-padding-end / 2); - @include safe-area-padding-horizontal(null, $item-ios-padding-end / 2); - border-bottom: $hairlines-width solid $list-ios-border-color; - &:focus { - @include ios-input-highlight($text-input-ios-highlight-color); - } - } - - .input-wp input { - @include padding-horizontal(null, ($item-wp-padding-end / 2)); - border-bottom: 1px solid $list-wp-border-color; - &:focus { - border-color: $text-input-wp-highlight-color; - } - } - - ion-select { - width: 100%; - left: 0; - max-width: none; - } - - .core-item-has-rich-text-editor { - margin-right: 1px; - } - } -} \ No newline at end of file diff --git a/src/addon/mod/data/pages/search/search.ts b/src/addon/mod/data/pages/search/search.ts index d50a121ec..df40db75a 100644 --- a/src/addon/mod/data/pages/search/search.ts +++ b/src/addon/mod/data/pages/search/search.ts @@ -52,8 +52,8 @@ export class AddonModDataSearchPage { this.searchForm = fb.group({ text: [this.search.text], - sortBy: [this.search.sortBy], - sortDirection: [this.search.sortDirection], + sortBy: [this.search.sortBy || 0], + sortDirection: [this.search.sortDirection || 'DESC'], firstname: [this.search.advanced['firstname'] || ''], lastname: [this.search.advanced['lastname'] || ''] }); diff --git a/src/addon/mod/data/providers/data.ts b/src/addon/mod/data/providers/data.ts index f38951efa..5c665e605 100644 --- a/src/addon/mod/data/providers/data.ts +++ b/src/addon/mod/data/providers/data.ts @@ -13,12 +13,13 @@ // limitations under the License. import { Injectable } from '@angular/core'; +import { CoreAppProvider } from '@providers/app'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreFilepoolProvider } from '@providers/filepool'; import { AddonModDataOfflineProvider } from './offline'; -import { CoreAppProvider } from '@providers/app'; +import { AddonModDataFieldsDelegate } from './fields-delegate'; /** * Service that provides some features for databases. @@ -34,10 +35,58 @@ export class AddonModDataProvider { constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider, private filepoolProvider: CoreFilepoolProvider, private dataOffline: AddonModDataOfflineProvider, - private appProvider: CoreAppProvider) { + private appProvider: CoreAppProvider, private fieldsDelegate: AddonModDataFieldsDelegate) { this.logger = logger.getInstance('AddonModDataProvider'); } + /** + * Adds a new entry to a database. + * + * @param {number} dataId Data instance ID. + * @param {number} entryId EntryId or provisional entry ID when offline. + * @param {number} courseId Course ID. + * @param {any} contents The fields data to be created. + * @param {number} [groupId] Group id, 0 means that the function will determine the user group. + * @param {any} fields The fields that define the contents. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [forceOffline] Force editing entry in offline. + * @return {Promise} Promise resolved when the action is done. + */ + addEntry(dataId: number, entryId: number, courseId: number, contents: any, groupId: number = 0, fields: any, siteId?: string, + forceOffline: boolean = false): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a data to be synchronized later. + const storeOffline = (): Promise => { + return this.dataOffline.saveEntry(dataId, entryId, 'add', courseId, groupId, contents, undefined, siteId) + .then((entry) => { + return { + // Return provissional entry Id. + newentryid: entry[1] + }; + }); + }; + + if (!this.appProvider.isOnline() || forceOffline) { + const notifications = this.checkFields(fields, contents); + if (notifications) { + return Promise.resolve({ + fieldnotifications: notifications + }); + } + } + + return this.addEntryOnline(dataId, contents, groupId, siteId).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + return Promise.reject(error); + } + + // Couldn't connect to server, store in offline. + return storeOffline(); + }); + } + /** * Adds a new entry to a database. It does not cache calls. It will fail if offline or cannot connect. * @@ -79,7 +128,7 @@ export class AddonModDataProvider { const storeOffline = (): Promise => { const action = approve ? 'approve' : 'disapprove'; - return this.dataOffline.saveEntry(dataId, entryId, action, courseId, null, null, null, siteId); + return this.dataOffline.saveEntry(dataId, entryId, action, courseId, undefined, undefined, undefined, siteId); }; // Get if the opposite action is not synced. @@ -126,6 +175,38 @@ export class AddonModDataProvider { }); } + /** + * Convenience function to check fields requeriments here named "notifications". + * + * @param {any} fields The fields that define the contents. + * @param {any} contents The contents data of the fields. + * @return {any} Array of notifications if any or false. + */ + protected checkFields(fields: any, contents: any): any { + const notifications = [], + contentsIndexed = {}; + + contents.forEach((content) => { + if (typeof contentsIndexed[content.fieldid] == 'undefined') { + contentsIndexed[content.fieldid] = []; + } + contentsIndexed[content.fieldid].push(content); + }); + + // App is offline, check required fields. + fields.forEach((field) => { + const notification = this.fieldsDelegate.getFieldsNotifications(field, contentsIndexed[field.id]); + if (notification) { + notifications.push({ + fieldname: field.name, + notification: notification + }); + } + }); + + return notifications.length ? notifications : false; + } + /** * Deletes an entry. * @@ -140,7 +221,7 @@ export class AddonModDataProvider { // Convenience function to store a data to be synchronized later. const storeOffline = (): Promise => { - return this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, null, null, null, siteId); + return this.dataOffline.saveEntry(dataId, entryId, 'delete', courseId, undefined, undefined, undefined, siteId); }; let justAdded = false; @@ -199,6 +280,89 @@ export class AddonModDataProvider { }); } + /** + * Updates an existing entry. + * + * @param {number} dataId Database ID. + * @param {number} entryId Entry ID. + * @param {number} courseId Course ID. + * @param {any} contents The contents data to be updated. + * @param {any} fields The fields that define the contents. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} forceOffline Force editing entry in offline. + * @return {Promise} Promise resolved when the action is done. + */ + editEntry(dataId: number, entryId: number, courseId: number, contents: any, fields: any, siteId?: string, + forceOffline: boolean = false): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Convenience function to store a data to be synchronized later. + const storeOffline = (): Promise => { + return this.dataOffline.saveEntry(dataId, entryId, 'edit', courseId, undefined, contents, undefined, siteId) + .then(() => { + return { + updated: true + }; + }); + }; + + let justAdded = false, + groupId; + + if (!this.appProvider.isOnline() || forceOffline) { + const notifications = this.checkFields(fields, contents); + if (notifications) { + return Promise.resolve({ + fieldnotifications: notifications + }); + } + } + + // Get other not not synced actions. + return this.dataOffline.getEntryActions(dataId, entryId, siteId).then((entries) => { + if (entries && entries.length) { + // Found. Delete add and edit actions first. + const proms = []; + entries.forEach((entry) => { + if (entry.action == 'add') { + justAdded = true; + groupId = entry.groupid; + proms.push(this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId)); + } else if (entry.action == 'edit') { + proms.push(this.dataOffline.deleteEntry(dataId, entryId, entry.action, siteId)); + } + }); + + return Promise.all(proms); + } + }).then(() => { + if (justAdded) { + // The field was added offline, add again and stop. + return this.addEntry(dataId, entryId, courseId, contents, groupId, fields, siteId, forceOffline) + .then((result) => { + result.updated = true; + + return result; + }); + } + + if (!this.appProvider.isOnline() || forceOffline) { + // App is offline, store the action. + return storeOffline(); + } + + return this.editEntryOnline(entryId, contents, siteId).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + return Promise.reject(error); + } + + // Couldn't connect to server, store in offline. + return storeOffline(); + }); + }); + } + /** * Updates an existing entry. It does not cache calls. It will fail if offline or cannot connect. * diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts index 91834da47..96f049947 100644 --- a/src/addon/mod/data/providers/helper.ts +++ b/src/addon/mod/data/providers/helper.ts @@ -188,9 +188,98 @@ export class AddonModDataHelperProvider { Promise { return this.dataProvider.fetchAllEntries(dataId, groupId, undefined, undefined, undefined, forceCache, ignoreCache, siteId) .then((entries) => { - return entries.map((entry) => { - return entry.id; - }); + return entries.map((entry) => entry.id); + }); + } + + /** + * Retrieve the entered data in the edit form. + * We don't use ng-model because it doesn't detect changes done by JavaScript. + * + * @param {any} inputData Array with the entered form values. + * @param {Array} fields Fields that defines every content in the entry. + * @param {number} [dataId] Database Id. If set, files will be uploaded and itemId set. + * @param {number} entryId Entry Id. + * @param {any} entryContents Original entry contents indexed by field id. + * @param {boolean} offline True to prepare the data for an offline uploading, false otherwise. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} That contains object with the answers. + */ + getEditDataFromForm(inputData: any, fields: any, dataId: number, entryId: number, entryContents: any, offline: boolean = false, + siteId?: string): Promise { + if (!inputData) { + return Promise.resolve({}); + } + + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + // Filter and translate fields to each field plugin. + const edit = [], + promises = []; + fields.forEach((field) => { + promises.push(Promise.resolve(this.fieldsDelegate.getFieldEditData(field, inputData, entryContents[field.id])) + .then((fieldData) => { + if (fieldData) { + const proms = []; + + fieldData.forEach((data) => { + let dataProm; + + // Upload Files if asked. + if (dataId && data.files) { + dataProm = this.uploadOrStoreFiles(dataId, 0, entryId, data.fieldid, data.files, offline, siteId) + .then((filesResult) => { + delete data.files; + data.value = filesResult; + }); + } else { + dataProm = Promise.resolve(); + } + + proms.push(dataProm.then(() => { + if (data.value) { + data.value = JSON.stringify(data.value); + } + if (typeof data.subfield == 'undefined') { + data.subfield = ''; + } + + // WS wants values in Json format. + edit.push(data); + })); + }); + + return Promise.all(proms); + } + })); + }); + + return Promise.all(promises).then(() => { + return edit; + }); + } + + /** + * Retrieve the temp files to be updated. + * + * @param {any} inputData Array with the entered form values. + * @param {Array} fields Fields that defines every content in the entry. + * @param {number} [dataId] Database Id. If set, fils will be uploaded and itemId set. + * @param {any} entryContents Original entry contents indexed by field id. + * @return {Promise} That contains object with the files. + */ + getEditTmpFiles(inputData: any, fields: any, dataId: number, entryContents: any): Promise { + if (!inputData) { + return Promise.resolve([]); + } + + // Filter and translate fields to each field plugin. + const promises = fields.map((field) => { + return Promise.resolve(this.fieldsDelegate.getFieldEditFiles(field, inputData, entryContents[field.id])); + }); + + return Promise.all(promises).then((fieldsFiles) => { + return fieldsFiles.reduce((files: any[], fieldFiles: any) => files.concat(fieldFiles), []); }); } @@ -211,9 +300,7 @@ export class AddonModDataHelperProvider { // It's an offline entry, search it in the offline actions. return this.sitesProvider.getSite(siteId).then((site) => { - const offlineEntry = offlineActions.find((offlineAction) => { - return offlineAction.action == 'add'; - }); + const offlineEntry = offlineActions.find((offlineAction) => offlineAction.action == 'add'); if (offlineEntry) { const siteInfo = site.getInfo(); @@ -249,9 +336,7 @@ export class AddonModDataHelperProvider { getPageInfoByEntry(dataId: number, entryId: number, groupId: number, forceCache: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise { return this.getAllEntriesIds(dataId, groupId, forceCache, ignoreCache, siteId).then((entries) => { - const index = entries.findIndex((entry) => { - return entry == entryId; - }); + const index = entries.findIndex((entry) => entry == entryId); if (index >= 0) { return { @@ -316,6 +401,30 @@ export class AddonModDataHelperProvider { }); } + /** + * Check if data has been changed by the user. + * + * @param {any} inputData Array with the entered form values. + * @param {any} fields Fields that defines every content in the entry. + * @param {number} [dataId] Database Id. If set, fils will be uploaded and itemId set. + * @param {any} entryContents Original entry contents indexed by field id. + * @return {Promise} True if changed, false if not. + */ + hasEditDataChanged(inputData: any, fields: any, dataId: number, entryContents: any): Promise { + const promises = fields.map((field) => { + return this.fieldsDelegate.hasFieldDataChanged(field, inputData, entryContents[field.id]); + }); + + // Will reject on first change detected. + return Promise.all(promises).then(() => { + // No changes. + return false; + }).catch(() => { + // Has changes. + return true; + }); + } + /** * Add a prefix to all rules in a CSS string. * diff --git a/src/addon/mod/data/providers/link-handler.ts b/src/addon/mod/data/providers/link-handler.ts index 4ba21ea6c..2f8fd041f 100644 --- a/src/addon/mod/data/providers/link-handler.ts +++ b/src/addon/mod/data/providers/link-handler.ts @@ -24,6 +24,6 @@ export class AddonModDataLinkHandler extends CoreContentLinksModuleIndexHandler name = 'AddonModDataLinkHandler'; constructor(courseHelper: CoreCourseHelperProvider) { - super(courseHelper, AddonModDataLinkHandler.name, 'data'); + super(courseHelper, 'AddonModData', 'data'); } } diff --git a/src/addon/mod/data/providers/prefetch-handler.ts b/src/addon/mod/data/providers/prefetch-handler.ts index 6d575e031..fbd8921ce 100644 --- a/src/addon/mod/data/providers/prefetch-handler.ts +++ b/src/addon/mod/data/providers/prefetch-handler.ts @@ -13,22 +13,29 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreGroupsProvider } from '@providers/groups'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreCommentsProvider } from '@core/comments/providers/comments'; +import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler'; import { AddonModDataProvider } from './data'; import { AddonModDataHelperProvider } from './helper'; -import { CoreFilepoolProvider } from '@providers/filepool'; /** * Handler to prefetch databases. */ @Injectable() export class AddonModDataPrefetchHandler extends CoreCourseModulePrefetchHandlerBase { - name = 'data'; + name = 'AddonModData'; + modName = 'data'; component = AddonModDataProvider.COMPONENT; updatesNames = /^configuration$|^.*files$|^entries$|^gradeitems$|^outcomes$|^comments$|^ratings/; - constructor(injector: Injector, protected dataProvider: AddonModDataProvider, - protected filepoolProvider: CoreFilepoolProvider, protected dataHelper: AddonModDataHelperProvider) { + constructor(injector: Injector, protected dataProvider: AddonModDataProvider, protected timeUtils: CoreTimeUtilsProvider, + protected filepoolProvider: CoreFilepoolProvider, protected dataHelper: AddonModDataHelperProvider, + protected groupsProvider: CoreGroupsProvider, protected commentsProvider: CoreCommentsProvider, + protected courseProvider: CoreCourseProvider) { super(injector); } @@ -44,16 +51,159 @@ export class AddonModDataPrefetchHandler extends CoreCourseModulePrefetchHandler * @return {Promise} Promise resolved when all content is downloaded. Data returned is not reliable. */ downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string): Promise { - const promises = []; + const promises = [], + siteId = this.sitesProvider.getCurrentSiteId(); promises.push(super.downloadOrPrefetch(module, courseId, prefetch)); - promises.push(this.dataProvider.getDatabase(courseId, module.id).then((data) => { - // @TODO + promises.push(this.getDatabaseInfoHelper(module, courseId, false, false, true, siteId).then((info) => { + // Prefetch the database data. + const database = info.database, + promises = []; + + promises.push(this.dataProvider.getFields(database.id, false, true, siteId)); + + promises.push(this.filepoolProvider.addFilesToQueue(siteId, info.files, this.component, module.id)); + + info.groups.forEach((group) => { + promises.push(this.dataProvider.getDatabaseAccessInformation(database.id, group.id, false, true, siteId)); + }); + + info.entries.forEach((entry) => { + promises.push(this.dataProvider.getEntry(database.id, entry.id, siteId)); + if (database.comments) { + promises.push(this.commentsProvider.getComments('module', database.coursemodule, 'mod_data', entry.id, + 'database_entry', 0, siteId)); + } + }); + + // Add Basic Info to manage links. + promises.push(this.courseProvider.getModuleBasicInfoByInstance(database.id, 'data', siteId)); + + return Promise.all(promises); })); return Promise.all(promises); } + /** + * Retrieves all the entries for all the groups and then returns only unique entries. + * + * @param {number} dataId Database Id. + * @param {any[]} groups Array of groups in the activity. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} [siteId] Site ID. + * @return {Promise} All unique entries. + */ + protected getAllUniqueEntries(dataId: number, groups: any[], forceCache: boolean = false, ignoreCache: boolean = false, + siteId?: string): Promise { + const promises = groups.map((group) => { + return this.dataProvider.fetchAllEntries(dataId, group.id, undefined, undefined, undefined, forceCache, ignoreCache, + siteId); + }); + + return Promise.all(promises).then((responses) => { + const uniqueEntries = {}; + + responses.forEach((groupEntries) => { + groupEntries.forEach((entry) => { + uniqueEntries[entry.id] = entry; + }); + }); + + return this.utils.objectToArray(uniqueEntries); + }); + } + + /** + * Helper function to get all database info just once. + * + * @param {any} module Module to get the files. + * @param {number} courseId Course ID the module belongs to. + * @param {boolean} [omitFail] True to always return even if fails. Default false. + * @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false. + * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down). + * @param {string} siteId Site ID. + * @return {Promise} Promise resolved with the info fetched. + */ + protected getDatabaseInfoHelper(module: any, courseId: number, omitFail: boolean = false, forceCache: boolean = false, + ignoreCache: boolean = false, siteId?: string): Promise { + let database, + groups = [], + entries = [], + files = []; + + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.dataProvider.getDatabase(courseId, module.id, siteId, forceCache).then((data) => { + files = this.getIntroFilesFromInstance(module, data); + database = data; + + return this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId).then((groupInfo) => { + if (!groupInfo.groups || groupInfo.groups.length == 0) { + groupInfo.groups = [{id: 0}]; + } + groups = groupInfo.groups; + + return this.getAllUniqueEntries(database.id, groups, forceCache, ignoreCache, siteId); + }); + }).then((uniqueEntries) => { + entries = uniqueEntries; + files = files.concat(this.getEntriesFiles(entries)); + + return { + database: database, + groups: groups, + entries: entries, + files: files + }; + }).catch((message): any => { + if (omitFail) { + // Any error, return the info we have. + return { + database: database, + groups: groups, + entries: entries, + files: files + }; + } + + return Promise.reject(message); + }); + } + + /** + * Returns the file contained in the entries. + * + * @param {any[]} entries List of entries to get files from. + * @return {any[]} List of files. + */ + protected getEntriesFiles(entries: any[]): any[] { + let files = []; + + entries.forEach((entry) => { + entry.contents.forEach((content) => { + files = files.concat(content.files); + }); + }); + + return files; + } + + /** + * Get the list of downloadable files. + * + * @param {any} module Module to get the files. + * @param {number} courseId Course ID the module belongs to. + * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. + * @return {Promise} Promise resolved with the list of files. + */ + getFiles(module: any, courseId: number, single?: boolean): Promise { + return this.getDatabaseInfoHelper(module, courseId, true).then((info) => { + return info.files; + }); + } + /** * Returns data intro files. * @@ -91,6 +241,35 @@ export class AddonModDataPrefetchHandler extends CoreCourseModulePrefetchHandler return this.dataProvider.invalidateDatabaseData(courseId); } + /** + * Check if a database is downloadable. + * A database isn't downloadable if it's not open yet. + * + * @param {any} module Module to check. + * @param {number} courseId Course ID the module belongs to. + * @return {Promise} Promise resolved with true if downloadable, resolved with false otherwise. + */ + isDownloadable(module: any, courseId: number): boolean | Promise { + return this.dataProvider.getDatabase(courseId, module.id, undefined, true).then((database) => { + return this.dataProvider.getDatabaseAccessInformation(database.id).then((accessData) => { + // Check if database is restricted by time. + if (!accessData.timeavailable) { + const time = this.timeUtils.timestamp(); + + // It is restricted, checking times. + if (database.timeavailablefrom && time < database.timeavailablefrom) { + return false; + } + if (database.timeavailableto && time > database.timeavailableto) { + return false; + } + } + + return true; + }); + }); + } + /** * Whether or not the handler is enabled on a site level. * diff --git a/src/addon/mod/feedback/providers/link-handler.ts b/src/addon/mod/feedback/providers/link-handler.ts index e4df7edf0..fe080bf63 100644 --- a/src/addon/mod/feedback/providers/link-handler.ts +++ b/src/addon/mod/feedback/providers/link-handler.ts @@ -15,7 +15,6 @@ import { Injectable } from '@angular/core'; import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; -import { AddonModFeedbackProvider } from './feedback'; /** * Handler to treat links to feedback. @@ -25,6 +24,6 @@ export class AddonModFeedbackLinkHandler extends CoreContentLinksModuleIndexHand name = 'AddonModFeedbackLinkHandler'; constructor(courseHelper: CoreCourseHelperProvider) { - super(courseHelper, AddonModFeedbackProvider.COMPONENT, 'feedback'); + super(courseHelper, 'AddonModFeedback', 'feedback'); } } diff --git a/src/addon/mod/feedback/providers/prefetch-handler.ts b/src/addon/mod/feedback/providers/prefetch-handler.ts index b5802f326..a3200300f 100644 --- a/src/addon/mod/feedback/providers/prefetch-handler.ts +++ b/src/addon/mod/feedback/providers/prefetch-handler.ts @@ -124,9 +124,9 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseModulePrefetchHan /** * Get the list of downloadable files. * - * @param {any} module Module to get the files. + * @param {any} module Module to get the files. * @param {number} courseId Course ID the module belongs to. - * @param {string} [siteId] Site ID. If not defined, current site. + * @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section. * @return {Promise} Promise resolved with the list of files. */ getFiles(module: any, courseId: number, single?: boolean): Promise { diff --git a/src/addon/userprofilefield/datetime/component/datetime.html b/src/addon/userprofilefield/datetime/component/datetime.html index 57b8e7ca2..5f7a554a0 100644 --- a/src/addon/userprofilefield/datetime/component/datetime.html +++ b/src/addon/userprofilefield/datetime/component/datetime.html @@ -6,5 +6,5 @@ {{ field.name }} - + \ No newline at end of file diff --git a/src/addon/userprofilefield/menu/component/menu.html b/src/addon/userprofilefield/menu/component/menu.html index 39f54dbc8..ce81fe385 100644 --- a/src/addon/userprofilefield/menu/component/menu.html +++ b/src/addon/userprofilefield/menu/component/menu.html @@ -6,7 +6,7 @@ {{ field.name }} - + {{ 'core.choosedots' | translate }} {{option}} diff --git a/src/addon/userprofilefield/text/component/text.html b/src/addon/userprofilefield/text/component/text.html index aaa43b1ac..8701248f2 100644 --- a/src/addon/userprofilefield/text/component/text.html +++ b/src/addon/userprofilefield/text/component/text.html @@ -6,5 +6,5 @@ {{ field.name }} - + diff --git a/src/components/input-errors/input-errors.html b/src/components/input-errors/input-errors.html index 51be581b5..390ad0ce0 100644 --- a/src/components/input-errors/input-errors.html +++ b/src/components/input-errors/input-errors.html @@ -1,5 +1,8 @@ -