Skip to content

Commit

Permalink
Merge pull request #3555 from tchak/rollback-from-invalid
Browse files Browse the repository at this point in the history
[BUGFIX release] Transition to loaded.saved state after rollback
  • Loading branch information
bmac committed Jul 20, 2015
2 parents 265a141 + ec5d73e commit 2d40783
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 48 deletions.
39 changes: 20 additions & 19 deletions packages/ember-data/lib/system/model/errors.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
var get = Ember.get;
var set = Ember.set;
var isEmpty = Ember.isEmpty;
var makeArray = Ember.makeArray;

import {
MapWithDefault
Expand Down Expand Up @@ -91,7 +93,7 @@ import {
@uses Ember.Enumerable
@uses Ember.Evented
*/
export default Ember.Object.extend(Ember.Enumerable, Ember.Evented, {
export default Ember.ArrayProxy.extend(Ember.Evented, {
/**
Register with target handler
Expand Down Expand Up @@ -176,22 +178,13 @@ export default Ember.Object.extend(Ember.Enumerable, Ember.Evented, {
return errors;
},

/**
@method nextObject
@private
*/
nextObject: function(index, previousObject, context) {
return get(this, 'content').objectAt(index);
},

/**
Total number of errors.
@property length
@type {Number}
@readOnly
*/
length: Ember.computed.oneWay('content.length').readOnly(),

/**
@property isEmpty
Expand Down Expand Up @@ -220,11 +213,10 @@ export default Ember.Object.extend(Ember.Enumerable, Ember.Evented, {
var wasEmpty = get(this, 'isEmpty');

messages = this._findOrCreateMessages(attribute, messages);
get(this, 'content').addObjects(messages);
this.addObjects(messages);
get(this, 'errorsByAttributeName').get(attribute).addObjects(messages);

this.notifyPropertyChange(attribute);
this.enumerableContentDidChange();

if (wasEmpty && !get(this, 'isEmpty')) {
this.trigger('becameInvalid');
Expand All @@ -238,7 +230,7 @@ export default Ember.Object.extend(Ember.Enumerable, Ember.Evented, {
_findOrCreateMessages: function(attribute, messages) {
var errors = this.errorsFor(attribute);

return Ember.makeArray(messages).map((message) => {
return makeArray(messages).map((message) => {
return errors.findBy('message', message) || {
attribute: attribute,
message: message
Expand Down Expand Up @@ -283,12 +275,11 @@ export default Ember.Object.extend(Ember.Enumerable, Ember.Evented, {
remove: function(attribute) {
if (get(this, 'isEmpty')) { return; }

var content = get(this, 'content').rejectBy('attribute', attribute);
get(this, 'content').setObjects(content);
let content = this.rejectBy('attribute', attribute);
set(this, 'content', content);
get(this, 'errorsByAttributeName').delete(attribute);

this.notifyPropertyChange(attribute);
this.enumerableContentDidChange();

if (get(this, 'isEmpty')) {
this.trigger('becameValid');
Expand Down Expand Up @@ -319,9 +310,19 @@ export default Ember.Object.extend(Ember.Enumerable, Ember.Evented, {
clear: function() {
if (get(this, 'isEmpty')) { return; }

get(this, 'content').clear();
get(this, 'errorsByAttributeName').clear();
this.enumerableContentDidChange();
let errorsByAttributeName = get(this, 'errorsByAttributeName');
let attributes = Ember.A();

errorsByAttributeName.forEach(function(_, attribute) {
attributes.push(attribute);
});

errorsByAttributeName.clear();
attributes.forEach(function(attribute) {
this.notifyPropertyChange(attribute);
}, this);

this._super();

this.trigger('becameValid');
},
Expand Down
1 change: 1 addition & 0 deletions packages/ember-data/lib/system/model/states.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ var DirtyState = {

rolledBack: function(internalModel) {
internalModel.clearErrorMessages();
internalModel.transitionTo('loaded.saved');
internalModel.triggerLater('ready');
},

Expand Down
87 changes: 58 additions & 29 deletions packages/ember-data/tests/unit/model/rollback-attributes-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ test("a record's changes can be made if it fails to save", function() {
});

test("a deleted record's attributes can be rollbacked if it fails to save, record arrays are updated accordingly", function() {
expect(7);
expect(8);
env.adapter.deleteRecord = function(store, type, snapshot) {
return Ember.RSVP.reject();
};
Expand All @@ -136,6 +136,7 @@ test("a deleted record's attributes can be rollbacked if it fails to save, recor
});
equal(person.get('isDeleted'), false);
equal(person.get('isError'), false);
equal(person.get('hasDirtyAttributes'), false, "must be not dirty");
}).then(function() {
equal(people.get('length'), 1, "the underlying record array is updated accordingly in an asynchronous way");
});
Expand All @@ -161,10 +162,14 @@ test("new record's attributes can be rollbacked", function() {

test("invalid new record's attributes can be rollbacked", function() {
var person;
var error = new DS.InvalidError([
{
detail: 'is invalid',
source: { pointer: 'data/attributes/name' }
}
]);
var adapter = DS.RESTAdapter.extend({
ajax: function(url, type, hash) {
var adapter = this;

return new Ember.RSVP.Promise(function(resolve, reject) {
/* If InvalidError is passed back in the reject it will throw the
exception which will bubble up the call stack (crashing the test)
Expand All @@ -173,13 +178,9 @@ test("invalid new record's attributes can be rollbacked", function() {
completes without failure and the failure hits the failure route
of the promise instead of crashing the save. */
Ember.run.next(function() {
reject(adapter.ajaxError({ name: 'is invalid' }));
reject(error);
});
});
},

ajaxError: function(jqXHR) {
return new DS.InvalidError(jqXHR);
}
});

Expand Down Expand Up @@ -227,14 +228,20 @@ test("deleted record's attributes can be rollbacked", function() {
});

test("invalid record's attributes can be rollbacked", function() {
expect(10);
Dog = DS.Model.extend({
name: DS.attr()
});

var error = new DS.InvalidError([
{
detail: 'is invalid',
source: { pointer: 'data/attributes/name' }
}
]);

var adapter = DS.RESTAdapter.extend({
ajax: function(url, type, hash) {
var adapter = this;

return new Ember.RSVP.Promise(function(resolve, reject) {
/* If InvalidError is passed back in the reject it will throw the
exception which will bubble up the call stack (crashing the test)
Expand All @@ -243,13 +250,9 @@ test("invalid record's attributes can be rollbacked", function() {
completes without failure and the failure hits the failure route
of the promise instead of crashing the save. */
Ember.run.next(function() {
reject(adapter.ajaxError({ name: 'is invalid' }));
reject(error);
});
});
},

ajaxError: function(jqXHR) {
return new DS.InvalidError(jqXHR);
}
});

Expand All @@ -261,25 +264,46 @@ test("invalid record's attributes can be rollbacked", function() {
});

run(function() {
Ember.addObserver(dog, 'errors.name', function() {
ok(true, 'errors.name did change');
});

dog.get('errors').addArrayObserver({}, {
willChange: function() {
ok(true, 'errors will change');
},
didChange: function() {
ok(true, 'errors did change');
}
});

dog.save().then(null, async(function() {
dog.rollbackAttributes();

equal(dog.get('hasDirtyAttributes'), false, "must not be dirty");
equal(dog.get('name'), "Pluto");
ok(Ember.isEmpty(dog.get('errors.name')));
ok(dog.get('isValid'));
}));
});
});

test("invalid record's attributes rolled back to correct state after set", function() {
expect(13);
Dog = DS.Model.extend({
name: DS.attr(),
breed: DS.attr()
});

var error = new DS.InvalidError([
{
detail: 'is invalid',
source: { pointer: 'data/attributes/name' }
}
]);

var adapter = DS.RESTAdapter.extend({
ajax: function(url, type, hash) {
var adapter = this;

return new Ember.RSVP.Promise(function(resolve, reject) {
/* If InvalidError is passed back in the reject it will throw the
exception which will bubble up the call stack (crashing the test)
Expand All @@ -288,13 +312,9 @@ test("invalid record's attributes rolled back to correct state after set", funct
completes without failure and the failure hits the failure route
of the promise instead of crashing the save. */
Ember.run.next(function() {
reject(adapter.ajaxError({ name: 'is invalid' }));
reject(error);
});
});
},

ajaxError: function(jqXHR) {
return new Error(jqXHR);
}
});

Expand All @@ -307,9 +327,15 @@ test("invalid record's attributes rolled back to correct state after set", funct
});

run(function() {
Ember.addObserver(dog, 'errors.name', function() {
ok(true, 'errors.name did change');
});

dog.save().then(null, async(function() {
equal(dog.get('name'), "is a dwarf planet");
equal(dog.get('breed'), "planet");
ok(Ember.isPresent(dog.get('errors.name')));
equal(dog.get('errors.name.length'), 1);

run(function() {
dog.set('name', 'Seymour Asses');
Expand All @@ -324,6 +350,8 @@ test("invalid record's attributes rolled back to correct state after set", funct

equal(dog.get('name'), "Pluto");
equal(dog.get('breed'), "Disney");
equal(dog.get('hasDirtyAttributes'), false, "must not be dirty");
ok(Ember.isEmpty(dog.get('errors.name')));
ok(dog.get('isValid'));
}));
});
Expand All @@ -334,19 +362,20 @@ test("when destroying a record setup the record state to invalid, the record's a
name: DS.attr()
});

var error = new DS.InvalidError([
{
detail: 'is invalid',
source: { pointer: 'data/attributes/name' }
}
]);

var adapter = DS.RESTAdapter.extend({
ajax: function(url, type, hash) {
var adapter = this;

return new Ember.RSVP.Promise(function(resolve, reject) {
Ember.run.next(function() {
reject(adapter.ajaxError({ name: 'is invalid' }));
reject(error);
});
});
},

ajaxError: function(jqXHR) {
return new DS.InvalidError(jqXHR);
}
});

Expand Down

0 comments on commit 2d40783

Please sign in to comment.