import ngModule from '../../module'; import Component from '../../lib/component'; import getModifiedData from '../../lib/modified'; import isEqual from '../../lib/equals'; import isFullEmpty from '../../lib/full-empty'; import UserError from '../../lib/user-error'; import {mergeFilters} from 'vn-loopback/util/filter'; /** * 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. * * @property {String} idField The id field name, 'id' if not specified * @property {*} idValue The id field value * @property {Boolean} isNew Whether is a new instance * @property {Boolean} insertMode Whether to enable insert mode * @property {String} url The base HTTP request path * @property {Boolean} get Whether to fetch initial data */ export default class Watcher extends Component { constructor(...args) { super(...args); this.idField = 'id'; this.get = true; this.insertMode = false; this.state = null; this.deregisterCallback = this.$transitions.onStart({}, transition => this.callback(transition)); this.snapshot(); } $onInit() { let fetch = !this.insertMode && this.get && this.url && this.idValue; if (fetch) this.fetch(); else { this.isNew = !!this.insertMode; this.snapshot(); } } $onDestroy() { this.deregisterCallback(); } /** * @type {Booelan} The computed instance HTTP path */ get instanceUrl() { return `${this.url}/${this.idValue}`; } /** * @type {Object} The instance data */ get data() { return this._data; } set data(value) { this._data = value; this.isNew = !!this.insertMode; this.snapshot(); } /** * @type {Booelan} Whether it's popullated with data */ get hasData() { return !!this.data; } set hasData(value) { if (value) this.fill(); else this.delete(); } /** * @type {Booelan} Whether instance data have been modified */ get dirty() { return this.form && this.form.$dirty || this.dataChanged(); } fetch() { let filter = mergeFilters({ fields: this.fields, where: this.where, include: this.include }, this.filter); let params = filter ? {filter} : null; return this.$http.get(this.instanceUrl, params) .then(json => { this.overwrite(json.data); this.isNew = false; this.snapshot(); }) .catch(err => { if (!(err.name == 'HttpError' && err.status == 404)) throw err; if (this.autoFill) { this.insert(); this.snapshot(); } }); } insert(data) { this.assign({[this.idField]: this.idValue}, data); this.isNew = true; this.deleted = null; } delete() { if (!this.hasData) return; this.deleted = this.makeSnapshot(); this.clear(); this.isNew = false; } recover() { if (!this.deleted) return; this.restoreSnapshot(this.deleted); } fill() { if (this.hasData) return; if (this.deleted) this.recover(); else if (this.original && this.original.data) this.reset(); else this.insert(); } reset() { this.restoreSnapshot(this.original); this.setPristine(); } snapshot() { const snapshot = this.makeSnapshot(); if (snapshot.data) { const idValue = snapshot.data[this.idField]; if (idValue) this.idValue = idValue; } this.original = snapshot; this.orgData = snapshot.data; this.deleted = null; this.setPristine(); } makeSnapshot() { return { data: this.copyData(), isNew: this.isNew, ref: this.data }; } restoreSnapshot(snapshot) { if (!snapshot) return; this._data = snapshot.ref; this.overwrite(snapshot.data); this.isNew = snapshot.isNew; this.deleted = null; } writeData(res) { if (this.hasData) this.assign(res.data); this.isNew = false; this.snapshot(); return res; } clear() { this._data = null; } overwrite(data) { if (data) { if (!this.data) this._data = {}; overwrite(this.data, data); } else this._data = null; } assign(...args) { this._data = Object.assign(this.data || {}, ...args); } copyData() { return copyObject(this.data); } refresh() { return this.fetch(); } dataChanged() { return !isEqual(this.orgData, this.copyData()); } /** * 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.isNew) this.isInvalid(); else this.check(); } 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(); if (!this.dataChanged() && !this.isNew) { this.snapshot(); return this.$q.resolve(); } let changedData = this.isNew ? this.data : getModifiedData(this.data, this.orgData); // If watcher is associated to mgCrud if (this.save && this.save.accept) { this.save.model = changedData; return this.save.accept() .then(json => this.writeData({data: json})); } // When mgCrud is not used let req; if (this.deleted) req = this.$http.delete(this.instanceUrl); else if (this.isNew) req = this.$http.post(this.url, changedData); else req = this.$http.patch(this.instanceUrl, changedData); return req.then(res => this.writeData(res)); } /** * Checks if data is ready to send. */ check() { this.isInvalid(); 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') { this.reset(); this.$state.go(this.state); } else this.state = null; } /** * @deprecated Use reset() */ loadOriginalData() { this.reset(); } /** * @deprecated Use snapshot() */ updateOriginalData() { this.snapshot(); } } Watcher.$inject = ['$element', '$scope']; function copyObject(data) { let newCopy; if (data && typeof data === 'object') { newCopy = {}; 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] = copyObject(value); else newCopy[key] = value; } } ); } else newCopy = data; return newCopy; } function clearObject(obj) { if (!obj) return; for (let key in obj) { if (obj.hasOwnProperty(key)) delete obj[key]; } } function overwrite(obj, data) { if (!obj) return; clearObject(obj); Object.assign(obj, data); } ngModule.vnComponent('vnWatcher', { template: require('./watcher.html'), bindings: { url: '@?', idField: '@?', idValue: '