import ngModule from '../../module'; import Component from '../../lib/component'; import getModifiedData from '../../lib/modified'; import copyObject from '../../lib/copy'; import isEqual from '../../lib/equals'; import isFullEmpty from '../../lib/full-empty'; import UserError from '../../lib/user-error'; /** * Component that checks for changes on a specific model property and * asks the user to save or discard it when the state changes. * Also it can save the data to the server when the @url and @idField * properties are provided. */ export default class Watcher extends Component { constructor($element, $, $state, $stateParams, $transitions, $http, vnApp, $translate, $attrs, $q) { super($element); Object.assign(this, { $, $state, $stateParams, $http, _: $translate, $attrs, vnApp, $q }); this.state = null; this.deregisterCallback = $transitions.onStart({}, transition => this.callback(transition)); this.updateOriginalData(); } $onInit() { if (this.get && this.url) this.fetchData(); else if (this.get && !this.url) throw new Error('URL parameter ommitted'); } $onChanges() { if (this.data) this.updateOriginalData(); } $onDestroy() { this.deregisterCallback(); } get dirty() { return this.form && this.form.$dirty || this.dataChanged(); } dataChanged() { let data = this.copyInNewObject(this.data); return !isEqual(data, this.orgData); } fetchData() { let id = this.data[this.idField]; return this.$http.get(`${this.url}/${id}`).then( json => { this.data = copyObject(json.data); this.updateOriginalData(); } ); } /** * Submits the data and goes back in the history. * * @return {Promise} The request promise */ submitBack() { return this.submit().then(res => { this.window.history.back(); return res; }); } /** * Submits the data and goes another state. * * @param {String} state The state name * @param {Object} params The request params * @return {Promise} The request promise */ submitGo(state, params) { return this.submit().then(res => { this.$state.go(state, params || {}); return res; }); } /** * Submits the data to the server. * * @return {Promise} The http request promise */ submit() { try { if (this.requestMethod() !== 'post') this.check(); else this.isInvalid(); } catch (err) { return this.$q.reject(err); } return this.realSubmit().then(res => { this.notifySaved(); return res; }); } /** * Submits the data without checking data validity or changes. * * @return {Promise} The http request promise */ realSubmit() { if (this.form) this.form.$setSubmitted(); const isPost = (this.requestMethod() === 'post'); if (!this.dataChanged() && !isPost) { this.updateOriginalData(); return this.$q.resolve(); } let changedData = isPost ? this.data : getModifiedData(this.data, this.orgData); let id = this.idField ? this.orgData[this.idField] : null; // If watcher is associated to mgCrud if (this.save && this.save.accept) { if (id) changedData[this.idField] = id; this.save.model = changedData; return this.$q((resolve, reject) => { this.save.accept().then( json => this.writeData({data: json}, resolve), reject ); }); } // When mgCrud is not used if (id) { return this.$q((resolve, reject) => { this.$http.patch(`${this.url}/${id}`, changedData).then( json => this.writeData(json, resolve), reject ); }); } return this.$q((resolve, reject) => { this.$http.post(this.url, changedData).then( json => this.writeData(json, resolve), reject ); }); } /** * return the request method. */ requestMethod() { return this.$attrs.save && this.$attrs.save.toLowerCase(); } /** * Checks if data is ready to send. */ check() { if (this.form && this.form.$invalid) throw new UserError('Some fields are invalid'); if (!this.dirty) throw new UserError('No changes to save'); } /** * Checks if the form is valid. */ isInvalid() { if (this.form && this.form.$invalid) throw new UserError('Some fields are invalid'); } /** * Notifies the user that the data has been saved. */ notifySaved() { this.vnApp.showSuccess(this.$t('Data saved!')); } setPristine() { if (this.form) this.form.$setPristine(); } setDirty() { if (this.form) this.form.$setDirty(); } callback(transition) { if (!this.state && this.dirty) { this.state = transition.to().name; this.$.confirm.show(); return false; } return true; } onConfirmResponse(response) { if (response === 'accept') { if (this.data) Object.assign(this.data, this.orgData); this.$state.go(this.state); } else this.state = null; } writeData(json, resolve) { Object.assign(this.data, json.data); this.updateOriginalData(); resolve(json); } updateOriginalData() { this.orgData = this.copyInNewObject(this.data); this.setPristine(); } loadOriginalData() { const orgData = JSON.parse(JSON.stringify(this.orgData)); this.data = Object.assign(this.data, orgData); this.setPristine(); } copyInNewObject(data) { let newCopy = {}; if (data && typeof data === 'object') { Object.keys(data).forEach( key => { let value = data[key]; if (value instanceof Date) newCopy[key] = new Date(value.getTime()); else if (!isFullEmpty(value)) { if (typeof value === 'object') newCopy[key] = this.copyInNewObject(value); else newCopy[key] = value; } } ); } return newCopy; } } Watcher.$inject = ['$element', '$scope', '$state', '$stateParams', '$transitions', '$http', 'vnApp', '$translate', '$attrs', '$q']; ngModule.component('vnWatcher', { template: require('./watcher.html'), bindings: { url: '@?', idField: '@?', data: '<', form: '<', save: '<', get: '