Support saving posts via Store

This commit is contained in:
Robin Ward 2015-04-09 14:54:17 -04:00
parent d4a05825da
commit 76f7786d0d
16 changed files with 205 additions and 160 deletions

View File

@ -1,11 +1,20 @@
import RestAdapter from 'discourse/adapters/rest'; import RestAdapter from 'discourse/adapters/rest';
import { Result } from 'discourse/adapters/rest';
export default RestAdapter.extend({ export default RestAdapter.extend({
// GET /posts doesn't include a type
find(store, type, findArgs) { find(store, type, findArgs) {
return this._super(store, type, findArgs).then(function(result) { return this._super(store, type, findArgs).then(function(result) {
return {post: result}; return {post: result};
}); });
},
createRecord(store, type, args) {
const typeField = Ember.String.underscore(type);
args.nested_post = true;
return Discourse.ajax(this.pathFor(store, type), { method: 'POST', data: args }).then(function (json) {
return new Result(json[typeField], json);
});
} }
}); });

View File

@ -1,5 +1,11 @@
const ADMIN_MODELS = ['plugin']; const ADMIN_MODELS = ['plugin'];
export function Result(payload, responseJson) {
this.payload = payload;
this.responseJson = responseJson;
this.target = null;
}
export default Ember.Object.extend({ export default Ember.Object.extend({
pathFor(store, type, findArgs) { pathFor(store, type, findArgs) {
let path = "/" + Ember.String.underscore(store.pluralize(type)); let path = "/" + Ember.String.underscore(store.pluralize(type));
@ -35,7 +41,18 @@ export default Ember.Object.extend({
update(store, type, id, attrs) { update(store, type, id, attrs) {
const data = {}; const data = {};
data[Ember.String.underscore(type)] = attrs; data[Ember.String.underscore(type)] = attrs;
return Discourse.ajax(this.pathFor(store, type, id), { method: 'PUT', data }); return Discourse.ajax(this.pathFor(store, type, id), { method: 'PUT', data }).then(function(json) {
return new Result(json[type], json);
});
},
createRecord(store, type, attrs) {
const data = {};
const typeField = Ember.String.underscore(type);
data[typeField] = attrs;
return Discourse.ajax(this.pathFor(store, type), { method: 'POST', data }).then(function (json) {
return new Result(json[typeField], json);
});
}, },
destroyRecord(store, type, record) { destroyRecord(store, type, record) {

View File

@ -219,6 +219,7 @@ export default DiscourseController.extend({
imageSizes: this.get('view').imageSizes(), imageSizes: this.get('view').imageSizes(),
editReason: this.get("editReason") editReason: this.get("editReason")
}).then(function(opts) { }).then(function(opts) {
// If we replied as a new topic successfully, remove the draft. // If we replied as a new topic successfully, remove the draft.
if (self.get('replyAsNewTopicDraft')) { if (self.get('replyAsNewTopicDraft')) {
self.destroyDraft(); self.destroyDraft();
@ -246,7 +247,6 @@ export default DiscourseController.extend({
bootbox.alert(error); bootbox.alert(error);
}); });
if (this.get('controllers.application.currentRouteName').split('.')[0] === 'topic' && if (this.get('controllers.application.currentRouteName').split('.')[0] === 'topic' &&
composer.get('topic.id') === this.get('controllers.topic.model.id')) { composer.get('topic.id') === this.get('controllers.topic.model.id')) {
staged = composer.get('stagedPost'); staged = composer.get('stagedPost');

View File

@ -0,0 +1,30 @@
export function throwAjaxError(undoCallback) {
return function(error) {
if (error instanceof Error) {
Ember.Logger.error(error.stack);
}
if (typeof error === "string") {
Ember.Logger.error(error);
}
// If we provided an `undo` callback
if (undoCallback) { undoCallback(error); }
let parsedError;
if (error.responseText) {
try {
const parsedJSON = $.parseJSON(error.responseText);
if (parsedJSON.errors) {
parsedError = parsedJSON.errors[0];
} else if (parsedJSON.failed) {
parsedError = parsedJSON.message;
}
} catch(ex) {
// in case the JSON doesn't parse
Ember.Logger.error(ex.stack);
}
}
throw parsedError || I18n.t('generic_error');
};
}

View File

@ -1,6 +1,6 @@
import RestModel from 'discourse/models/rest'; import RestModel from 'discourse/models/rest';
import Post from 'discourse/models/post';
import Topic from 'discourse/models/topic'; import Topic from 'discourse/models/topic';
import { throwAjaxError } from 'discourse/lib/ajax-error';
const CLOSED = 'closed', const CLOSED = 'closed',
SAVING = 'saving', SAVING = 'saving',
@ -430,25 +430,22 @@ const Composer = RestModel.extend({
promise = Ember.RSVP.resolve(); promise = Ember.RSVP.resolve();
} }
post.setProperties({ const props = {
raw: this.get('reply'), raw: this.get('reply'),
editReason: opts.editReason, edit_reason: opts.editReason,
imageSizes: opts.imageSizes, image_sizes: opts.imageSizes,
cooked: this.getCookedHtml() cooked: this.getCookedHtml()
}); };
this.set('composeState', CLOSED); this.set('composeState', CLOSED);
return promise.then(function() { return promise.then(function() {
return post.save(function(result) { return post.save(props).then(function() {
post.updateFromPost(result);
self.clearState(); self.clearState();
}, function (error) { }).catch(throwAjaxError(function() {
post.set('cooked', oldCooked); post.set('cooked', oldCooked);
self.set('composeState', OPEN); self.set('composeState', OPEN);
const response = $.parseJSON(error.responseText); }));
throw response && response.errors ? response.errors[0] : I18n.t('generic_error');
});
}); });
}, },
@ -473,7 +470,7 @@ const Composer = RestModel.extend({
let addedToStream = false; let addedToStream = false;
// Build the post object // Build the post object
const createdPost = Post.create({ const createdPost = this.store.createRecord('post', {
imageSizes: opts.imageSizes, imageSizes: opts.imageSizes,
cooked: this.getCookedHtml(), cooked: this.getCookedHtml(),
reply_count: 0, reply_count: 0,
@ -489,7 +486,6 @@ const Composer = RestModel.extend({
moderator: user.get('moderator'), moderator: user.get('moderator'),
admin: user.get('admin'), admin: user.get('admin'),
yours: true, yours: true,
newPost: true,
read: true read: true
}); });
@ -521,11 +517,7 @@ const Composer = RestModel.extend({
// engine, staging will just cause a blank post to render // engine, staging will just cause a blank post to render
if (!_.isEmpty(createdPost.get('cooked'))) { if (!_.isEmpty(createdPost.get('cooked'))) {
state = postStream.stagePost(createdPost, user); state = postStream.stagePost(createdPost, user);
if (state === "alreadyStaging") { return; }
if(state === "alreadyStaging"){
return;
}
} }
} }
@ -534,13 +526,12 @@ const Composer = RestModel.extend({
composer.set("stagedPost", state === "staged" && createdPost); composer.set("stagedPost", state === "staged" && createdPost);
return createdPost.save().then(function(result) { return createdPost.save().then(function(result) {
let saving = true; let saving = true;
createdPost.updateFromJson(result);
if (topic) { if (topic) {
// It's no longer a new post // It's no longer a new post
createdPost.set('newPost', false); topic.set('draft_sequence', result.target.draft_sequence);
topic.set('draft_sequence', result.draft_sequence);
postStream.commitPost(createdPost); postStream.commitPost(createdPost);
addedToStream = true; addedToStream = true;
} else { } else {
@ -563,30 +554,13 @@ const Composer = RestModel.extend({
composer.set('composeState', SAVING); composer.set('composeState', SAVING);
} }
return { post: result }; return { post: createdPost };
}).catch(function(error) { }).catch(throwAjaxError(function() {
// If an error occurs
if (postStream) { if (postStream) {
postStream.undoPost(createdPost); postStream.undoPost(createdPost);
} }
composer.set('composeState', OPEN); composer.set('composeState', OPEN);
}));
// TODO extract error handling code
let parsedError;
try {
const parsedJSON = $.parseJSON(error.responseText);
if (parsedJSON.errors) {
parsedError = parsedJSON.errors[0];
} else if (parsedJSON.failed) {
parsedError = parsedJSON.message;
}
}
catch(ex) {
parsedError = "Unknown error saving post, try again. Error: " + error.status + " " + error.statusText;
}
throw parsedError;
});
}, },
getCookedHtml() { getCookedHtml() {

View File

@ -113,45 +113,33 @@ const Post = RestModel.extend({
}); });
}.property('actions_summary.@each.users', 'actions_summary.@each.count'), }.property('actions_summary.@each.users', 'actions_summary.@each.count'),
save() { afterUpdate(res) {
const self = this; if (res.category) {
if (!this.get('newPost')) { Discourse.Site.current().updateCategory(res.category);
// We're updating a post }
return Discourse.ajax("/posts/" + (this.get('id')), { },
type: 'PUT',
dataType: 'json', updateProperties() {
data: { return {
post: { raw: this.get('raw'), edit_reason: this.get('editReason') }, post: { raw: this.get('raw'), edit_reason: this.get('editReason') },
image_sizes: this.get('imageSizes') image_sizes: this.get('imageSizes')
} };
}).then(function(result) { },
// If we received a category update, update it
self.set('version', result.post.version);
if (result.category) Discourse.Site.current().updateCategory(result.category);
return Discourse.Post.create(result.post);
});
} else { createProperties() {
// We're saving a post
const data = this.getProperties(Discourse.Composer.serializedFieldsForCreate()); const data = this.getProperties(Discourse.Composer.serializedFieldsForCreate());
data.reply_to_post_number = this.get('reply_to_post_number'); data.reply_to_post_number = this.get('reply_to_post_number');
data.image_sizes = this.get('imageSizes'); data.image_sizes = this.get('imageSizes');
data.nested_post = true;
const metaData = this.get('metaData'); const metaData = this.get('metaData');
// Put the metaData into the request // Put the metaData into the request
if (metaData) { if (metaData) {
data.meta_data = {}; data.meta_data = {};
Ember.keys(metaData).forEach(function(key) { data.meta_data[key] = metaData.get(key); }); Ember.keys(metaData).forEach(function(key) { data.meta_data[key] = metaData.get(key); });
} }
return Discourse.ajax("/posts", { return data;
type: 'POST',
data: data
}).then(function(result) {
return Discourse.Post.create(result.post);
});
}
}, },
// Expands the first post's content, if embedded and shortened. // Expands the first post's content, if embedded and shortened.
@ -266,50 +254,6 @@ const Post = RestModel.extend({
}); });
}, },
/**
Updates a post from a JSON packet. This is normally done after the post is saved to refresh any
attributes.
**/
updateFromJson(obj) {
if (!obj) return;
let skip, oldVal;
// Update all the properties
const post = this;
_.each(obj, function(val,key) {
if (key !== 'actions_summary'){
oldVal = post[key];
skip = false;
if (val && val !== oldVal) {
if (key === "reply_to_user" && val && oldVal) {
skip = val.username === oldVal.username || Em.get(val, "username") === Em.get(oldVal, "username");
}
if(!skip) {
post.set(key, val);
}
}
}
});
// Rebuild actions summary
this.set('actions_summary', Em.A());
if (obj.actions_summary) {
const lookup = Em.Object.create();
_.each(obj.actions_summary,function(a) {
a.post = post;
a.actionType = Discourse.Site.current().postActionTypeById(a.id);
const actionSummary = Discourse.ActionSummary.create(a);
post.get('actions_summary').pushObject(actionSummary);
lookup.set(a.actionType.get('name_key'), actionSummary);
});
this.set('actionByName', lookup);
}
},
// Load replies to this post // Load replies to this post
loadReplies() { loadReplies() {
if(this.get('loadingReplies')){ if(this.get('loadingReplies')){

View File

@ -1,22 +1,52 @@
import Presence from 'discourse/mixins/presence'; import Presence from 'discourse/mixins/presence';
const RestModel = Ember.Object.extend(Presence, { const RestModel = Ember.Object.extend(Presence, {
update(attrs) { isNew: Ember.computed.equal('__state', 'new'),
const self = this, isCreated: Ember.computed.equal('__state', 'created'),
type = this.get('__type');
const munge = this.__munge; afterUpdate: Ember.K,
return this.store.update(type, this.get('id'), attrs).then(function(result) {
if (result && result[type]) { update(props) {
Object.keys(result).forEach(function(k) { props = props || this.updateProperties();
attrs[k] = result[k];
const type = this.get('__type'),
store = this.get('store');
const self = this;
return store.update(type, this.get('id'), props).then(function(res) {
self.setProperties(self.__munge(res.payload || res.responseJson));
self.afterUpdate(res);
return res;
}); });
} },
self.setProperties(munge(attrs));
return result; _saveNew(props) {
props = props || this.createProperties();
const type = this.get('__type'),
store = this.get('store'),
adapter = store.adapterFor(type);
const self = this;
return adapter.createRecord(store, type, props).then(function(res) {
if (!res) { throw "Received no data back from createRecord"; }
self.setProperties(self.__munge(res.payload));
self.set('__state', 'created');
res.target = self;
return res;
}); });
}, },
createProperties() {
throw "You must overwrite `createProperties()` before saving a record";
},
save(props) {
return this.get('isNew') ? this._saveNew(props) : this.update(props);
},
destroyRecord() { destroyRecord() {
const type = this.get('__type'); const type = this.get('__type');
return this.store.destroyRecord(type, this); return this.store.destroyRecord(type, this);
@ -34,7 +64,7 @@ RestModel.reopenClass({
args = args || {}; args = args || {};
if (!args.store) { if (!args.store) {
const container = Discourse.__container__; const container = Discourse.__container__;
Ember.warn('Use `store.createRecord` to create records instead of `.create()`'); // Ember.warn('Use `store.createRecord` to create records instead of `.create()`');
args.store = container.lookup('store:main'); args.store = container.lookup('store:main');
} }

View File

@ -16,26 +16,23 @@ export default Ember.Object.extend({
}, },
findAll(type) { findAll(type) {
const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
const self = this; const self = this;
return adapter.findAll(this, type).then(function(result) { return this.adapterFor(type).findAll(this, type).then(function(result) {
return self._resultSet(type, result); return self._resultSet(type, result);
}); });
}, },
// Mostly for legacy, things like TopicList without ResultSets // Mostly for legacy, things like TopicList without ResultSets
findFiltered(type, findArgs) { findFiltered(type, findArgs) {
const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
const self = this; const self = this;
return adapter.find(this, type, findArgs).then(function(result) { return this.adapterFor(type).find(this, type, findArgs).then(function(result) {
return self._build(type, result); return self._build(type, result);
}); });
}, },
find(type, findArgs) { find(type, findArgs) {
const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
const self = this; const self = this;
return adapter.find(this, type, findArgs).then(function(result) { return this.adapterFor(type).find(this, type, findArgs).then(function(result) {
if (typeof findArgs === "object") { if (typeof findArgs === "object") {
return self._resultSet(type, result); return self._resultSet(type, result);
} else { } else {
@ -64,8 +61,7 @@ export default Ember.Object.extend({
}, },
update(type, id, attrs) { update(type, id, attrs) {
const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest'); return this.adapterFor(type).update(this, type, id, attrs, function(result) {
return adapter.update(this, type, id, attrs, function(result) {
if (result && result[type] && result[type].id) { if (result && result[type] && result[type].id) {
const oldRecord = _identityMap[type][id]; const oldRecord = _identityMap[type][id];
delete _identityMap[type][id]; delete _identityMap[type][id];
@ -81,8 +77,7 @@ export default Ember.Object.extend({
}, },
destroyRecord(type, record) { destroyRecord(type, record) {
const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest'); return this.adapterFor(type).destroyRecord(this, type, record).then(function(result) {
return adapter.destroyRecord(this, type, record).then(function(result) {
const forType = _identityMap[type]; const forType = _identityMap[type];
if (forType) { delete forType[record.get('id')]; } if (forType) { delete forType[record.get('id')]; }
return result; return result;
@ -101,6 +96,7 @@ export default Ember.Object.extend({
_build(type, obj) { _build(type, obj) {
obj.store = this; obj.store = this;
obj.__type = type; obj.__type = type;
obj.__state = obj.id ? "created" : "new";
const klass = this.container.lookupFactory('model:' + type) || RestModel; const klass = this.container.lookupFactory('model:' + type) || RestModel;
const model = klass.create(obj); const model = klass.create(obj);
@ -111,6 +107,10 @@ export default Ember.Object.extend({
return model; return model;
}, },
adapterFor(type) {
return this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
},
_hydrate(type, obj) { _hydrate(type, obj) {
if (!obj) { throw "Can't hydrate " + type + " of `null`"; } if (!obj) { throw "Can't hydrate " + type + " of `null`"; }
if (!obj.id) { throw "Can't hydrate " + type + " without an `id`"; } if (!obj.id) { throw "Can't hydrate " + type + " without an `id`"; }

View File

@ -15,6 +15,7 @@
//= require ./discourse/helpers/register-unbound //= require ./discourse/helpers/register-unbound
//= require ./discourse/mixins/scrolling //= require ./discourse/mixins/scrolling
//= require_tree ./discourse/mixins //= require_tree ./discourse/mixins
//= require ./discourse/lib/ajax-error
//= require ./discourse/lib/markdown //= require ./discourse/lib/markdown
//= require ./discourse/lib/search-for-term //= require ./discourse/lib/search-for-term
//= require ./discourse/lib/user-search //= require ./discourse/lib/user-search

View File

@ -100,6 +100,8 @@ test("Create a Reply", () => {
test("Edit the first post", () => { test("Edit the first post", () => {
visit("/t/internationalization-localization/280"); visit("/t/internationalization-localization/280");
ok(!exists('.topic-post:eq(0) .post-info.edits'), 'it has no edits icon at first');
click('.topic-post:eq(0) button[data-action=showMoreActions]'); click('.topic-post:eq(0) button[data-action=showMoreActions]');
click('.topic-post:eq(0) button[data-action=edit]'); click('.topic-post:eq(0) button[data-action=edit]');
andThen(() => { andThen(() => {
@ -111,6 +113,7 @@ test("Edit the first post", () => {
click('#reply-control button.create'); click('#reply-control button.create');
andThen(() => { andThen(() => {
ok(!exists('#wmd-input'), 'it closes the composer'); ok(!exists('#wmd-input'), 'it closes the composer');
ok(exists('.topic-post:eq(0) .post-info.edits'), 'it has the edits icon');
ok(find('#topic-title h1').text().indexOf('This is the new text for the title') !== -1, 'it shows the new title'); ok(find('#topic-title h1').text().indexOf('This is the new text for the title') !== -1, 'it shows the new title');
ok(find('.topic-post:eq(0) .cooked').text().indexOf('This is the new text for the post') !== -1, 'it updates the post'); ok(find('.topic-post:eq(0) .cooked').text().indexOf('This is the new text for the post') !== -1, 'it updates the post');
}); });

View File

@ -1,4 +1,4 @@
export default { export default {
"/posts/398": {"id":398,"name":"Uwe Keim","username":"uwe_keim","avatar_template":"/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png","uploaded_avatar_id":5697,"created_at":"2013-02-05T21:29:00.280Z","cooked":"<p>Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?</p>","post_number":1,"post_type":1,"updated_at":"2013-02-05T21:29:00.280Z","like_count":0,"reply_count":1,"reply_to_post_number":null,"quote_count":0,"avg_time":25,"incoming_link_count":314,"reads":475,"score":1702.25,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Uwe Keim","primary_group_name":null,"version":2,"can_edit":true,"can_delete":false,"can_recover":true,"user_title":null,"raw":"Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?","actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":255,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false} "/posts/398": {"id":398,"name":"Uwe Keim","username":"uwe_keim","avatar_template":"/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png","uploaded_avatar_id":5697,"created_at":"2013-02-05T21:29:00.280Z","cooked":"<p>Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?</p>","post_number":1,"post_type":1,"updated_at":"2013-02-05T21:29:00.280Z","like_count":0,"reply_count":1,"reply_to_post_number":null,"quote_count":0,"avg_time":25,"incoming_link_count":314,"reads":475,"score":1702.25,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Uwe Keim","primary_group_name":null,"version":1,"can_edit":true,"can_delete":false,"can_recover":true,"user_title":null,"raw":"Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?","actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":255,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false}
}; };

File diff suppressed because one or more lines are too long

View File

@ -2,15 +2,21 @@ function parsePostData(query) {
const result = {}; const result = {};
query.split("&").forEach(function(part) { query.split("&").forEach(function(part) {
const item = part.split("="); const item = part.split("=");
result[item[0]] = decodeURIComponent(item[1]).replace(/\+/g, ' '); const firstSeg = decodeURIComponent(item[0]);
const m = /^([^\[]+)\[([^\]]+)\]/.exec(firstSeg);
const val = decodeURIComponent(item[1]).replace(/\+/g, ' ');
if (m) {
result[m[1]] = result[m[1]] || {};
result[m[1]][m[2]] = val;
} else {
result[firstSeg] = val;
}
}); });
return result; return result;
} }
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
function response(code, obj) { function response(code, obj) {
if (typeof code === "object") { if (typeof code === "object") {
obj = code; obj = code;
@ -122,7 +128,10 @@ export default function() {
this.put('/posts/:post_id/recover', success); this.put('/posts/:post_id/recover', success);
this.put('/posts/:post_id', (request) => { this.put('/posts/:post_id', (request) => {
return response({ post: {id: request.params.post_id, version: 2 } }); const data = parsePostData(request.requestBody);
data.post.id = request.params.post_id;
data.post.version = 2;
return response(200, data.post);
}); });
this.put('/t/:slug/:id', (request) => { this.put('/t/:slug/:id', (request) => {
@ -157,9 +166,15 @@ export default function() {
} }
}); });
this.post('/widgets', function(request) {
const widget = parsePostData(request.requestBody).widget;
widget.id = 100;
return response(200, {widget});
});
this.put('/widgets/:widget_id', function(request) { this.put('/widgets/:widget_id', function(request) {
const w = _widgets.findBy('id', parseInt(request.params.widget_id)); const widget = parsePostData(request.requestBody).widget;
return response({ widget: clone(w) }); return response({ widget });
}); });
this.get('/widgets', function(request) { this.get('/widgets', function(request) {

View File

@ -28,6 +28,21 @@ test('update', function() {
}); });
}); });
test('save new', function() {
const store = createStore();
const widget = store.createRecord('widget');
ok(widget.get('isNew'), 'it is a new record');
ok(!widget.get('isCreated'), 'it is not created');
widget.save({ name: 'Evil Widget' }).then(function() {
ok(widget.get('id'), 'it has an id');
ok(widget.get('name'), 'Evil Widget');
ok(widget.get('isCreated'), 'it is created');
ok(!widget.get('isNew'), 'it is no longer new');
});
});
test('destroyRecord', function() { test('destroyRecord', function() {
const store = createStore(); const store = createStore();
store.find('widget', 123).then(function(widget) { store.find('widget', 123).then(function(widget) {

View File

@ -5,6 +5,8 @@ import createStore from 'helpers/create-store';
test('createRecord', function() { test('createRecord', function() {
const store = createStore(); const store = createStore();
const widget = store.createRecord('widget', {id: 111, name: 'hello'}); const widget = store.createRecord('widget', {id: 111, name: 'hello'});
ok(!widget.get('isNew'), 'it is not a new record');
equal(widget.get('name'), 'hello'); equal(widget.get('name'), 'hello');
equal(widget.get('id'), 111); equal(widget.get('id'), 111);
}); });
@ -13,6 +15,7 @@ test('createRecord without an `id`', function() {
const store = createStore(); const store = createStore();
const widget = store.createRecord('widget', {name: 'hello'}); const widget = store.createRecord('widget', {name: 'hello'});
ok(widget.get('isNew'), 'it is a new record');
ok(!widget.get('id'), 'there is no id'); ok(!widget.get('id'), 'there is no id');
}); });
@ -21,6 +24,7 @@ test('createRecord without attributes', function() {
const widget = store.createRecord('widget'); const widget = store.createRecord('widget');
ok(!widget.get('id'), 'there is no id'); ok(!widget.get('id'), 'there is no id');
ok(widget.get('isNew'), 'it is a new record');
}); });
test('createRecord with a record as attributes returns that record from the map', function() { test('createRecord with a record as attributes returns that record from the map', function() {
@ -36,6 +40,7 @@ test('find', function() {
store.find('widget', 123).then(function(w) { store.find('widget', 123).then(function(w) {
equal(w.get('name'), 'Trout Lure'); equal(w.get('name'), 'Trout Lure');
equal(w.get('id'), 123); equal(w.get('id'), 123);
ok(!w.get('isNew'), 'found records are not new');
// A second find by id returns the same object // A second find by id returns the same object
store.find('widget', 123).then(function(w2) { store.find('widget', 123).then(function(w2) {
@ -70,6 +75,7 @@ test('findAll', function() {
store.findAll('widget').then(function(result) { store.findAll('widget').then(function(result) {
equal(result.get('length'), 2); equal(result.get('length'), 2);
const w = result.findBy('id', 124); const w = result.findBy('id', 124);
ok(!w.get('isNew'), 'found records are not new');
equal(w.get('name'), 'Evil Repellant'); equal(w.get('name'), 'Evil Repellant');
}); });
}); });

View File

@ -437,9 +437,10 @@ var bootbox = window.bootbox || (function(document, $) {
// wire up button handlers // wire up button handlers
div.on('click', '.modal-footer a', function(e) { div.on('click', '.modal-footer a', function(e) {
var self = this;
Ember.run(function() { Ember.run(function() {
var handler = $(this).data("handler"), var handler = $(self).data("handler"),
cb = callbacks[handler], cb = callbacks[handler],
hideModal = null; hideModal = null;