|
import Mixin from '@ember/object/mixin'; |
|
import { A } from '@ember/array'; |
|
import { |
|
defineProperty, |
|
computed |
|
} from '@ember/object'; |
|
|
|
import { |
|
addObserver, |
|
removeObserver |
|
} from '@ember/object/observers'; |
|
|
|
function resetItem(item) { |
|
if ('resetChanges' in item) { |
|
item.resetChanges(); |
|
} else { |
|
if (item.get('hasDirtyAttributes')) { |
|
item.rollbackAttributes(); |
|
} |
|
} |
|
} |
|
|
|
function resetItems(keysToSet) { |
|
Object.keys(keysToSet).forEach((propertyName)=>{ |
|
if (!keysToSet[propertyName]) { |
|
return; |
|
} |
|
if (Array.isArray(keysToSet[propertyName])) { |
|
keysToSet[propertyName].forEach(item=>{ |
|
resetItem(item); |
|
}) |
|
} else { |
|
resetItem(keysToSet[propertyName]); |
|
} |
|
}); |
|
} |
|
|
|
function peekItemIfExists(property, ctx, store, id) { |
|
if (id) { |
|
let [firstPath, secondPath = false] = property.split('.'); |
|
|
|
let type = secondPath ? ctx.get(firstPath).relationshipFor(secondPath).type : ctx.relationshipFor(firstPath).type |
|
|
|
let item = store.peekRecord(type, id); |
|
|
|
if (!item) { |
|
return false; |
|
} |
|
|
|
if (item.get('isDestroyed') || item.get('isDeleted')) { |
|
console.error('Unable to restore deleted or destroyed item', item, property); |
|
return false; |
|
} |
|
|
|
return item; |
|
|
|
} |
|
return false; |
|
} |
|
|
|
function getOrigin(ctx, modelChanges, keysToSet) { |
|
let store = ctx.get('store'); |
|
Object.keys(modelChanges).forEach((property)=>{ |
|
let initialState = modelChanges[property].get('firstObject'); |
|
if (Array.isArray(initialState)) { |
|
let items = A(); |
|
initialState.forEach((id)=>{ |
|
let item = peekItemIfExists(property, ctx, store, id); |
|
if (item) { |
|
items.push(item); |
|
} |
|
}); |
|
keysToSet[property] = items; |
|
} else { |
|
if (!initialState) { |
|
keysToSet[property] = null; |
|
} else { |
|
let item = peekItemIfExists(property, ctx, store, initialState); |
|
if (item) { |
|
keysToSet[property] = item; |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
|
|
const debug = true; |
|
|
|
const log = function() { |
|
if (!debug) { |
|
return; |
|
} |
|
console.log(...arguments); |
|
} |
|
|
|
const dirtyComputed = function(ctx, hasMany) { |
|
let dirtyKey = 'hasDirtyAttributes'; |
|
let rels = hasMany.map((item) => `${item}.@each.${dirtyKey}`); |
|
return computed.apply(ctx, [dirtyKey].concat(rels).concat(function () { |
|
let items = this.getProperties([dirtyKey].concat(hasMany)); |
|
let dirtyResults = hasMany.reduce((results, relationshipName)=>{ |
|
return results.concat(items[relationshipName].mapBy('hasDirtyAttributes')); |
|
}, A()).map((el)=>{ |
|
return { |
|
result: el |
|
}; |
|
}); |
|
log('dirtyResults', dirtyResults, this.modelChanges); |
|
|
|
if (items.hasDirtyAttributes) { |
|
return true; |
|
} |
|
return dirtyResults.isAny('result', true); |
|
})) |
|
} |
|
|
|
export default Mixin.create({ |
|
init() { |
|
this._super(...arguments); |
|
this.modelChanges = {}; |
|
this._initTracker(); |
|
}, |
|
_initTracker() { |
|
let { |
|
hasMany |
|
} = this.get('rollabackable'); |
|
let rels = this._relationshipsForObserver(); |
|
|
|
rels.forEach((rel) => { |
|
addObserver(this, rel, this, 'onAnyRelationshipChanged'); |
|
}); |
|
|
|
defineProperty(this, 'hasChangedAttributes', dirtyComputed(this, hasMany)); |
|
}, |
|
destroy() { |
|
let rels = this._relationshipsForObserver(); |
|
rels.forEach((rel)=>{ |
|
removeObserver(this, rel, this, 'onAnyRelationshipChanged'); |
|
}); |
|
this._super(...arguments); |
|
}, |
|
_relationshipsForObserver() { |
|
let {hasMany, belongsTo} = this.get('rollabackable'); |
|
let dirtyKey = 'id'; |
|
let rels = hasMany.map((item) => `${item}.@each.${dirtyKey}`).concat( |
|
belongsTo.map((item) => `${item}.${dirtyKey}`) |
|
); |
|
return rels; |
|
}, |
|
_addLastChanges() { |
|
let rels = this._relationshipsForObserver(); |
|
|
|
rels.forEach((rel) => { |
|
this.onAnyRelationshipChanged(this, rel); |
|
}); |
|
}, |
|
ready() { |
|
this._super(...arguments); |
|
this._addLastChanges(); |
|
this.commitLastChanges(); |
|
}, |
|
//configurablePart, hasMany - list of hasMany keys to track, belongsTo - ...same; |
|
rollabackable: computed(function(){ |
|
return { |
|
hasMany: [], |
|
belongsTo: [], |
|
unloadNulls: false |
|
}; |
|
}), |
|
onAnyRelationshipChanged(model, key){ |
|
// console.log('onAnyRelationshipChanged', model, key, model.get(key)); |
|
let hasManyId = '[email protected]'; |
|
let belongsToId = '.id'; |
|
let value = null; |
|
if (key.endsWith(hasManyId)) { |
|
value = model.get(key.split('.')[0]).mapBy('id'); |
|
} else if (key.endsWith(belongsToId)) { |
|
value = model.get(key); |
|
} |
|
this.trackChanges(key.replace(hasManyId,'').replace(belongsToId,''), value); |
|
}, |
|
hasChangedRelationships() { |
|
let isAnyItemHasHistory = false; |
|
Object.keys(this.modelChanges).forEach((key) => { |
|
if (Array.isArray(this.modelChanges[key]) && this.modelChanges[key].length >= 2) { |
|
let firstItem = this.modelChanges[key].get('firstObject'); |
|
let lastItem = this.modelChanges[key].get('lastObject'); |
|
if (Array.isArray(lastItem)) { |
|
lastItem = JSON.stringify(lastItem); |
|
} |
|
if (Array.isArray(firstItem)) { |
|
firstItem = JSON.stringify(firstItem); |
|
} |
|
|
|
let firstState = String(firstItem).valueOf(); |
|
let lastState = String(lastItem).valueOf(); |
|
|
|
if (firstState !== lastState) { |
|
isAnyItemHasHistory = true; |
|
} |
|
} |
|
}); |
|
return isAnyItemHasHistory; |
|
}, |
|
hasChanges({validAttibutes=null}={}) { |
|
let hasChangedAttributes = this.get('hasChangedAttributes'); |
|
if (typeof validAttibutes === 'object' && validAttibutes !== null) { |
|
hasChangedAttributes = JSON.stringify(this.relationshipsAttributesChanges()) !== JSON.stringify(validAttibutes); |
|
} |
|
return hasChangedAttributes || this.hasChangedRelationships() || false; |
|
}, |
|
commitChanges() { |
|
Object.keys(this.modelChanges).forEach((key)=>{ |
|
if (Array.isArray(this.modelChanges[key])) { |
|
if (this.modelChanges[key].length) { |
|
this.modelChanges[key] = A([this.modelChanges[key].shift()]); |
|
} |
|
} |
|
}); |
|
}, |
|
relationshipsAttributesChanges() { |
|
let changedAttributes = {}; |
|
|
|
this.get('rollabackable.hasMany').forEach((relationshipName) => { |
|
changedAttributes[relationshipName] = this.get(relationshipName).map(el => el.changedAttributes()); |
|
}); |
|
|
|
return changedAttributes; |
|
}, |
|
commitLastChanges() { |
|
Object.keys(this.modelChanges).forEach((key) => { |
|
if (Array.isArray(this.modelChanges[key])) { |
|
if (this.modelChanges[key].length) { |
|
this.modelChanges[key] = A([this.modelChanges[key].pop()]); |
|
} |
|
} |
|
}); |
|
}, |
|
resetChanges() { |
|
let keysToSet = {}; |
|
getOrigin(this, this.modelChanges, keysToSet); |
|
resetItems(keysToSet); |
|
this.rollbackAttributes(); |
|
log('resetChanges', arguments, { ...this.modelChanges}); |
|
this.commitChanges(); |
|
this.setProperties(keysToSet); |
|
}, |
|
didLoad() { |
|
this._super(...arguments); |
|
log('didLoad', arguments, {...this.modelChanges}); |
|
this._addLastChanges(); |
|
this.commitLastChanges(); |
|
}, |
|
didUpdate() { |
|
this._super(...arguments); |
|
log('didUpdate', arguments, { ...this.modelChanges}); |
|
this._addLastChanges(); |
|
this.commitLastChanges(); |
|
}, |
|
didCreate() { |
|
this._super(...arguments); |
|
log('didUpdate', arguments, { ...this.modelChanges}); |
|
this._addLastChanges(); |
|
this.commitLastChanges(); |
|
}, |
|
_normalizeValue(raw) { |
|
if (Array.isArray(raw)) { |
|
return JSON.stringify(raw); |
|
} |
|
if (isNaN(raw)) { |
|
return null; |
|
} |
|
if (typeof raw === 'string') { |
|
return raw; |
|
} |
|
if (typeof raw === 'number') { |
|
return raw; |
|
} |
|
if (typeof raw === 'object') { |
|
return raw; |
|
} |
|
if (typeof raw === 'boolean') { |
|
return raw; |
|
} |
|
if (typeof raw === 'undefined') { |
|
return undefined; |
|
} |
|
return raw; |
|
}, |
|
trackChanges(key, rawValue) { |
|
|
|
if (this.get('isNew')) { |
|
return; |
|
} |
|
|
|
let value = String(this._normalizeValue(rawValue)).valueOf(); |
|
|
|
if (!this.modelChanges[key]) { |
|
this.modelChanges[key] = A(); |
|
} |
|
|
|
let lastObject = this.modelChanges[key].get('lastObject'); |
|
|
|
if (Array.isArray(lastObject)) { |
|
lastObject = JSON.stringify(lastObject); |
|
} |
|
|
|
|
|
if (!this.modelChanges[key].length) { |
|
this.modelChanges[key].pushObject(rawValue); |
|
} else { |
|
if (lastObject === rawValue) { |
|
return; |
|
} |
|
if (String(lastObject).valueOf() !== value) { |
|
this.modelChanges[key].pushObject(rawValue); |
|
} |
|
} |
|
|
|
log(key, this.modelChanges[key], lastObject, rawValue); |
|
} |
|
}); |