salix/front/core/components/watcher/watcher.js

430 lines
9.8 KiB
JavaScript

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: '<?',
data: '=',
form: '<',
save: '<?',
get: '<?',
insertMode: '<?',
autoFill: '<?',
filter: '<?',
fields: '<?',
where: '<?',
include: '<?'
},
controller: Watcher
});