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 @@ -