Ember-cli, Ember, & Ember Data 1.13.x upgrades

closes #5630
- upgrade ember-cli to latest version
- upgrade ember to latest 1.13.x release
- upgrade ember data to latest 1.13.x release
    - update custom adapters and serialisers for new internal JSON-API compatible formats [(docs)][1]
    - update all store queries to use new standardised query methods [(docs)][2]
    - add ember-data-filter addon ready for store.filter removal in ember-data 2.0 [(docs)][3]
- remove use of prototype extensions for computed properties and observers
- consolidate pagination into a single route mixin and simplify configuration

[1]: http://emberjs.com/blog/2015/06/18/ember-data-1-13-released.html#toc_transition-to-the-new-jsonserializer-and-restserializer-apis
[2]: http://emberjs.com/blog/2015/06/18/ember-data-1-13-released.html#toc_simplified-find-methods
[3]: http://emberjs.com/blog/2015/06/18/ember-data-1-13-released.html#toc_ds-store-filter-moved-to-an-addon
This commit is contained in:
Kevin Ansfield 2015-09-03 12:06:50 +01:00
parent 5c9a824d53
commit 5c0b63f300
62 changed files with 339 additions and 321 deletions

View File

@ -5,7 +5,7 @@ export default DS.RESTAdapter.extend({
host: window.location.origin,
namespace: ghostPaths().apiRoot.slice(1),
findQuery: function (store, type, query) {
query: function (store, type, query) {
var id;
if (query.id) {

View File

@ -2,7 +2,7 @@ import Ember from 'ember';
import BaseAdapter from 'ghost/adapters/base';
// EmbeddedRelationAdapter will augment the query object in calls made to
// DS.Store#find, findQuery, and findAll with the correct "includes"
// DS.Store#findRecord, findAll, query, and queryRecord with the correct "includes"
// (?include=relatedType) by introspecting on the provided subclass of the DS.Model.
// In cases where there is no query object (DS.Model#save, or simple finds) the URL
// that is built will be augmented with ?include=... where appropriate.
@ -12,58 +12,68 @@ import BaseAdapter from 'ghost/adapters/base';
// roles: DS.hasMany('role', { embedded: 'always' }) => ?include=roles
export default BaseAdapter.extend({
find: function (store, type, id) {
return this.ajax(this.buildIncludeURL(store, type, id), 'GET');
find: function (store, type, id, snapshot) {
return this.ajax(this.buildIncludeURL(store, type.modelName, id, snapshot, 'find'), 'GET');
},
findQuery: function (store, type, query) {
return this._super(store, type, this.buildQuery(store, type, query));
findRecord: function (store, type, id, snapshot) {
return this.ajax(this.buildIncludeURL(store, type.modelName, id, snapshot, 'findRecord'), 'GET');
},
findAll: function (store, type, sinceToken) {
var query = {};
var query, url;
if (sinceToken) {
query.since = sinceToken;
query = {since: sinceToken};
}
return this.findQuery(store, type, query);
url = this.buildIncludeURL(store, type.modelName, null, null, 'findAll');
return this.ajax(url, 'GET', {data: query});
},
createRecord: function (store, type, record) {
return this.saveRecord(store, type, record, {method: 'POST'});
query: function (store, type, query) {
return this._super(store, type, this.buildQuery(store, type.modelName, query));
},
updateRecord: function (store, type, record) {
queryRecord: function (store, type, query) {
return this._super(store, type, this.buildQuery(store, type.modelName, query));
},
createRecord: function (store, type, snapshot) {
return this.saveRecord(store, type, snapshot, {method: 'POST'});
},
updateRecord: function (store, type, snapshot) {
var options = {
method: 'PUT',
id: Ember.get(record, 'id')
id: Ember.get(snapshot, 'id')
};
return this.saveRecord(store, type, record, options);
return this.saveRecord(store, type, snapshot, options);
},
saveRecord: function (store, type, record, options) {
saveRecord: function (store, type, snapshot, options) {
options = options || {};
var url = this.buildIncludeURL(store, type, options.id),
payload = this.preparePayload(store, type, record);
var url = this.buildIncludeURL(store, type.modelName, options.id, snapshot, 'createRecord'),
payload = this.preparePayload(store, type, snapshot);
return this.ajax(url, options.method, payload);
},
preparePayload: function (store, type, record) {
preparePayload: function (store, type, snapshot) {
var serializer = store.serializerFor(type.modelName),
payload = {};
serializer.serializeIntoHash(payload, type, record);
serializer.serializeIntoHash(payload, type, snapshot);
return {data: payload};
},
buildIncludeURL: function (store, type, id) {
var url = this.buildURL(type.modelName, id),
includes = this.getEmbeddedRelations(store, type);
buildIncludeURL: function (store, modelName, id, snapshot, requestType, query) {
var url = this.buildURL(modelName, id, snapshot, requestType, query),
includes = this.getEmbeddedRelations(store, modelName);
if (includes.length) {
url += '?include=' + includes.join(',');
@ -72,8 +82,8 @@ export default BaseAdapter.extend({
return url;
},
buildQuery: function (store, type, options) {
var toInclude = this.getEmbeddedRelations(store, type),
buildQuery: function (store, modelName, options) {
var toInclude = this.getEmbeddedRelations(store, modelName),
query = options || {},
deDupe = {};
@ -102,8 +112,8 @@ export default BaseAdapter.extend({
return query;
},
getEmbeddedRelations: function (store, type) {
var model = store.modelFor(type),
getEmbeddedRelations: function (store, modelName) {
var model = store.modelFor(modelName),
ret = [];
// Iterate through the model's relationships and build a list

View File

@ -3,5 +3,9 @@ import ApplicationAdapter from 'ghost/adapters/application';
export default ApplicationAdapter.extend({
find: function (store, type, id) {
return this.findQuery(store, type, {id: id, status: 'all'});
},
findAll: function (store, type, id) {
return this.query(store, type, {id: id, status: 'all'});
}
});

View File

@ -6,9 +6,9 @@ export default Ember.Component.extend({
active: false,
linkClasses: null,
unfocusLink: function () {
unfocusLink: Ember.on('click', function () {
this.$('a').blur();
}.on('click'),
}),
actions: {
setActive: function (value) {

View File

@ -41,7 +41,7 @@ export default Ember.Component.extend({
_loadPosts: function () {
var store = this.get('_store'),
postsUrl = store.adapterFor('post').urlForFindQuery({}, 'post') + '/',
postsUrl = store.adapterFor('post').urlForQuery({}, 'post') + '/',
postsQuery = {fields: 'id,title,page', limit: 'all', status: 'all', staticPages: 'all'},
content = this.get('content'),
self = this;
@ -61,7 +61,7 @@ export default Ember.Component.extend({
_loadUsers: function () {
var store = this.get('_store'),
usersUrl = store.adapterFor('user').urlForFindQuery({}, 'user') + '/',
usersUrl = store.adapterFor('user').urlForQuery({}, 'user') + '/',
usersQuery = {fields: 'name,slug', limit: 'all'},
content = this.get('content'),
self = this;

View File

@ -13,7 +13,7 @@ export default Ember.Component.extend({
// anchor behaviors or ignored
href: Ember.String.htmlSafe('javascript:;'),
scrollTo: function () {
scrollTo: Ember.on('click', function () {
var anchor = this.get('anchor'),
$el = Ember.$(anchor);
@ -30,5 +30,5 @@ export default Ember.Component.extend({
$(this).removeAttr('tabindex');
}).focus();
}
}.on('click')
})
});

View File

@ -10,7 +10,7 @@ export default Ember.Component.extend({
return this.get('tabsManager.activeTab') === this;
}),
index: Ember.computed('tabsManager.tabs.@each', function () {
index: Ember.computed('tabsManager.tabs.[]', function () {
return this.get('tabsManager.tabs').indexOf(this);
}),

View File

@ -4,7 +4,7 @@ export default Ember.Controller.extend(Ember.PromiseProxyMixin, {
init: function () {
var promise;
promise = this.store.find('setting', {type: 'blog,theme'}).then(function (settings) {
promise = this.store.query('setting', {type: 'blog,theme'}).then(function (settings) {
return settings.get('firstObject');
});

View File

@ -10,7 +10,7 @@ export default Ember.Controller.extend({
status: 'all'
};
promise = this.store.find('post', query).then(function (results) {
promise = this.store.query('post', query).then(function (results) {
return results.meta.pagination.total;
});

View File

@ -10,7 +10,7 @@ export default Ember.Controller.extend(ValidationEngine, {
authorRole: null,
roles: Ember.computed(function () {
return this.store.find('role', {permissions: 'assign'});
return this.store.query('role', {permissions: 'assign'});
}),
// Used to set the initial value for the dropdown
@ -53,7 +53,7 @@ export default Ember.Controller.extend(ValidationEngine, {
this.set('email', '');
this.set('role', self.get('authorRole'));
this.store.find('user').then(function (result) {
this.store.findAll('user', {reload: true}).then(function (result) {
var invitedUser = result.findBy('email', email);
if (invitedUser) {

View File

@ -32,11 +32,11 @@ export default Ember.Controller.extend({
model.deleteRecord();
} else {
// roll back changes on model props
model.rollback();
model.rollbackAttributes();
}
// setting isDirty to false here allows willTransition on the editor route to succeed
editorController.set('isDirty', false);
// setting hasDirtyAttributes to false here allows willTransition on the editor route to succeed
editorController.set('hasDirtyAttributes', false);
// since the transition is now certain to complete, we can unset window.onbeforeunload here
window.onbeforeunload = null;

View File

@ -26,8 +26,8 @@ export default Ember.Controller.extend({
// because store.pushPayload is not working with embedded relations
if (response && Ember.isArray(response.users)) {
response.users.forEach(function (userJSON) {
var user = self.store.getById('user', userJSON.id),
role = self.store.getById('role', userJSON.roles[0].id);
var user = self.store.peekRecord('user', userJSON.id),
role = self.store.peekRecord('role', userJSON.roles[0].id);
user.set('role', role);
});

View File

@ -29,7 +29,7 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
// Loaded asynchronously, so must use promise proxies.
var deferred = {};
deferred.promise = this.store.find('user', {limit: 'all'}).then(function (users) {
deferred.promise = this.store.query('user', {limit: 'all'}).then(function (users) {
return users.rejectBy('id', 'me').sortBy('name');
}).then(function (users) {
return users.filter(function (user) {
@ -217,7 +217,7 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
this.get('model').save().catch(function (errors) {
self.showErrors(errors);
self.get('model').rollback();
self.get('model').rollbackAttributes();
});
},
@ -234,7 +234,7 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
this.get('model').save(this.get('saveOptions')).catch(function (errors) {
self.showErrors(errors);
self.get('model').rollback();
self.get('model').rollbackAttributes();
});
},
@ -299,7 +299,7 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
return self.get('model').save();
}).catch(function (errors) {
self.showErrors(errors);
self.get('model').rollback();
self.get('model').rollbackAttributes();
});
},
@ -355,7 +355,7 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
this.get('model').save().catch(function (errors) {
self.showErrors(errors);
self.get('model').rollback();
self.get('model').rollbackAttributes();
});
},
@ -412,7 +412,7 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
this.get('model').save().catch(function (errors) {
self.showErrors(errors);
self.get('model').rollback();
self.get('model').rollbackAttributes();
});
},
@ -427,7 +427,7 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
this.get('model').save().catch(function (errors) {
self.showErrors(errors);
self.get('model').rollback();
self.get('model').rollbackAttributes();
});
},
@ -467,7 +467,7 @@ export default Ember.Controller.extend(SettingsMenuMixin, {
model.save().catch(function (errors) {
self.showErrors(errors);
self.set('selectedAuthor', author);
model.rollback();
model.rollbackAttributes();
});
},

View File

@ -1,5 +1,4 @@
import Ember from 'ember';
import PaginationControllerMixin from 'ghost/mixins/pagination-controller';
// a custom sort function is needed in order to sort the posts list the same way the server would:
// status: ASC
@ -64,7 +63,8 @@ function publishedAtCompare(item1, item2) {
return Ember.compare(published1.valueOf(), published2.valueOf());
}
export default Ember.Controller.extend(PaginationControllerMixin, {
export default Ember.Controller.extend({
// See PostsRoute's shortcuts
postListFocused: Ember.computed.equal('keyboardFocus', 'postList'),
postContentFocused: Ember.computed.equal('keyboardFocus', 'postContent'),
@ -75,12 +75,6 @@ export default Ember.Controller.extend(PaginationControllerMixin, {
return postsArray.sort(comparator);
}),
init: function () {
// let the PaginationControllerMixin know what type of model we will be paginating
// this is necessary because we do not have access to the model inside the Controller::init method
this._super({modelType: 'post'});
},
actions: {
showPostContent: function (post) {
if (!post) {

View File

@ -59,7 +59,7 @@ export default Ember.Controller.extend(SettingsSaveMixin, {
generatePassword: Ember.observer('model.isPrivate', function () {
this.get('model.errors').remove('password');
if (this.get('model.isPrivate') && this.get('model.isDirty')) {
if (this.get('model.isPrivate') && this.get('model.hasDirtyAttributes')) {
this.get('model').set('password', randomPassword());
}
}),

View File

@ -24,7 +24,7 @@ export default Ember.Controller.extend({
this.get('model').save().catch(function (errors) {
self.showErrors(errors);
self.get('model').rollback();
self.get('model').rollbackAttributes();
});
},
@ -51,7 +51,7 @@ export default Ember.Controller.extend({
// Clear the store, so that all the new data gets fetched correctly.
self.store.unloadAll();
// Reload currentUser and set session
self.set('session.user', self.store.find('user', currentUserId));
self.set('session.user', self.store.findRecord('user', currentUserId));
// TODO: keep as notification, add link to view content
notifications.showNotification('Import successful.');
}).catch(function (response) {

View File

@ -1,9 +1,8 @@
import Ember from 'ember';
import PaginationMixin from 'ghost/mixins/pagination-controller';
import SettingsMenuMixin from 'ghost/mixins/settings-menu-controller';
import boundOneWay from 'ghost/utils/bound-one-way';
export default Ember.Controller.extend(PaginationMixin, SettingsMenuMixin, {
export default Ember.Controller.extend(SettingsMenuMixin, {
tags: Ember.computed.alias('model'),
activeTag: null,
@ -13,12 +12,6 @@ export default Ember.Controller.extend(PaginationMixin, SettingsMenuMixin, {
activeTagMetaTitleScratch: boundOneWay('activeTag.meta_title'),
activeTagMetaDescriptionScratch: boundOneWay('activeTag.meta_description'),
init: function (options) {
options = options || {};
options.modelType = 'tag';
this._super(options);
},
application: Ember.inject.controller(),
config: Ember.inject.service(),
notifications: Ember.inject.service(),

View File

@ -119,7 +119,7 @@ export default Ember.Controller.extend({
}),
authorRole: Ember.computed(function () {
return this.store.find('role').then(function (roles) {
return this.store.findAll('role', {reload: true}).then(function (roles) {
return roles.findBy('name', 'Author');
});
}),

View File

@ -1,12 +1,6 @@
import Ember from 'ember';
import PaginationControllerMixin from 'ghost/mixins/pagination-controller';
export default Ember.Controller.extend(PaginationControllerMixin, {
init: function () {
// let the PaginationControllerMixin know what type of model we will be paginating
// this is necessary because we do not have access to the model inside the Controller::init method
this._super({modelType: 'user'});
},
export default Ember.Controller.extend({
users: Ember.computed.alias('model'),

View File

@ -83,7 +83,7 @@ export default Ember.Controller.extend(ValidationEngine, {
}),
roles: Ember.computed(function () {
return this.store.find('role', {permissions: 'assign'});
return this.store.query('role', {permissions: 'assign'});
}),
actions: {

View File

@ -1,15 +1,15 @@
import Ember from 'ember';
export default Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) {
export default Ember.Helper.helper(function (params) {
var el = document.createElement('span'),
length,
content;
if (!arr || !arr.length) {
if (!params || !params.length) {
return;
}
content = arr[0] || '';
content = params[0] || '';
length = content.length;
el.className = 'word-count';

View File

@ -1,17 +1,17 @@
import Ember from 'ember';
export default Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) {
export default Ember.Helper.helper(function (params) {
var el = document.createElement('span'),
content,
maxCharacters,
length;
if (!arr || arr.length < 2) {
if (!params || params.length < 2) {
return;
}
content = arr[0] || '';
maxCharacters = arr[1];
content = params[0] || '';
maxCharacters = params[1];
length = content.length;
el.className = 'word-count';

View File

@ -1,15 +1,15 @@
import Ember from 'ember';
import counter from 'ghost/utils/word-count';
export default Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) {
if (!arr || !arr.length) {
export default Ember.Helper.helper(function (params) {
if (!params || !params.length) {
return;
}
var markdown,
count;
markdown = arr[0] || '';
markdown = params[0] || '';
if (/^\s*$/.test(markdown)) {
return '0 words';

View File

@ -2,12 +2,12 @@ import Ember from 'ember';
/* global html_sanitize*/
import cajaSanitizers from 'ghost/utils/caja-sanitizers';
export default Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) {
if (!arr || !arr.length) {
export default Ember.Helper.helper(function (params) {
if (!params || !params.length) {
return;
}
var escapedhtml = arr[0] || '';
var escapedhtml = params[0] || '';
// replace script and iFrame
escapedhtml = escapedhtml.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,

View File

@ -4,13 +4,13 @@ import cajaSanitizers from 'ghost/utils/caja-sanitizers';
var showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes', 'highlight']});
export default Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) {
if (!arr || !arr.length) {
export default Ember.Helper.helper(function (params) {
if (!params || !params.length) {
return;
}
var escapedhtml = '',
markdown = arr[0] || '';
markdown = params[0] || '';
// convert markdown to HTML
escapedhtml = showdown.makeHtml(markdown);

View File

@ -1,11 +1,11 @@
import Ember from 'ember';
export default Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) {
if (!arr || !arr.length) {
export default Ember.Helper.helper(function (params) {
if (!params || !params.length) {
return;
}
var timeago = arr[0];
var timeago = params[0];
return moment(timeago).fromNow();
// stefanpenner says cool for small number of timeagos.

View File

@ -8,7 +8,7 @@ import ghostPaths from 'ghost/utils/ghost-paths';
// {{gh-path 'api'}} for Ghost's api root (/myblog/ghost/api/v0.1/)
// {{gh-path 'admin' '/assets/hi.png'}} for resolved url (/myblog/ghost/assets/hi.png)
function ghostPathsHelper(params/*, hash */) {
export default Ember.Helper.helper(function (params) {
var base,
paths = ghostPaths(),
[path, url] = params;
@ -50,6 +50,4 @@ function ghostPathsHelper(params/*, hash */) {
}
return Ember.String.htmlSafe(base);
}
export default Ember.HTMLBars.makeBoundHelper(ghostPathsHelper);
});

View File

@ -8,4 +8,6 @@ export function ghUserCanAdmin(params) {
return !!(params[0].get('isOwner') || params[0].get('isAdmin'));
}
export default Ember.HTMLBars.makeBoundHelper(ghUserCanAdmin);
export default Ember.Helper.helper(function (params) {
return ghUserCanAdmin(params);
});

View File

@ -1,9 +1,11 @@
import Ember from 'ember';
export function isEqual(params/*, hash*/) {
export function isEqual(params) {
var [lhs, rhs] = params;
return lhs === rhs;
}
export default Ember.HTMLBars.makeBoundHelper(isEqual);
export default Ember.Helper.helper(function (params) {
return isEqual(params);
});

View File

@ -1,7 +1,9 @@
import Ember from 'ember';
export function isNot(params/*, hash*/) {
export function isNot(params) {
return !params;
}
export default Ember.HTMLBars.makeBoundHelper(isNot);
export default Ember.Helper.helper(function (params) {
return isNot(params);
});

View File

@ -1,9 +1,11 @@
import Ember from 'ember';
export function readPath(params/*, hash*/) {
export function readPath(params) {
var [obj, path] = params;
return Ember.get(obj, path);
}
export default Ember.HTMLBars.makeBoundHelper(readPath);
export default Ember.Helper.helper(function (params) {
return readPath(params);
});

View File

@ -3,8 +3,8 @@ import GhostOauth2Authenticator from 'ghost/authenticators/oauth2';
export default {
name: 'ghost-authentictor',
initialize: function (container) {
container.register(
initialize: function (registry, application) {
application.register(
'ghost-authenticator:oauth2-password-grant',
GhostOauth2Authenticator
);

View File

@ -12,7 +12,7 @@ var trailingHistory = Ember.HistoryLocation.extend({
export default {
name: 'registerTrailingLocationHistory',
initialize: function (container, application) {
initialize: function (registry, application) {
application.register('location:trailing-history', trailingHistory);
}
};

View File

@ -1,16 +1,18 @@
import Ember from 'ember';
export function initialize(instance) {
var store = instance.container.lookup('service:store'),
Session = instance.container.lookup('simple-auth-session:main');
Session.reopen({
user: Ember.computed(function () {
return store.findRecord('user', 'me');
})
});
}
export default {
name: 'authentication',
initialize: function (instance) {
var store = instance.container.lookup('store:main'),
Session = instance.container.lookup('simple-auth-session:main');
Session.reopen({
user: Ember.computed(function () {
return store.find('user', 'me');
})
});
}
after: 'ember-data',
initialize: initialize
};

View File

@ -4,8 +4,8 @@ import boundOneWay from 'ghost/utils/bound-one-way';
import imageManager from 'ghost/utils/ed-image-manager';
// this array will hold properties we need to watch
// to know if the model has been changed (`controller.isDirty`)
var watchedProps = ['model.scratch', 'model.titleScratch', 'model.isDirty', 'model.tags.[]'];
// to know if the model has been changed (`controller.hasDirtyAttributes`)
var watchedProps = ['model.scratch', 'model.titleScratch', 'model.hasDirtyAttributes', 'model.tags.[]'];
PostModel.eachAttribute(function (name) {
watchedProps.push('model.' + name);
@ -27,7 +27,7 @@ export default Ember.Mixin.create({
this._super();
window.onbeforeunload = function () {
return self.get('isDirty') ? self.unloadDirtyMessage() : null;
return self.get('hasDirtyAttributes') ? self.unloadDirtyMessage() : null;
};
},
@ -61,8 +61,8 @@ export default Ember.Mixin.create({
*/
willPublish: boundOneWay('model.isPublished'),
// set by the editor route and `isDirty`. useful when checking
// whether the number of tags has changed for `isDirty`.
// set by the editor route and `hasDirtyAttributes`. useful when checking
// whether the number of tags has changed for `hasDirtyAttributes`.
previousTagNames: null,
tagNames: Ember.computed('model.tags.@each.name', function () {
@ -102,23 +102,23 @@ export default Ember.Mixin.create({
// rather than in all other places save is called
model.updateTags();
// set previousTagNames to current tagNames for isDirty check
// set previousTagNames to current tagNames for hasDirtyAttributes check
this.set('previousTagNames', this.get('tagNames'));
// `updateTags` triggers `isDirty => true`.
// `updateTags` triggers `hasDirtyAttributes => true`.
// for a saved model it would otherwise be false.
// if the two "scratch" properties (title and content) match the model, then
// it's ok to set isDirty to false
// it's ok to set hasDirtyAttributes to false
if (model.get('titleScratch') === model.get('title') &&
model.get('scratch') === model.get('markdown')) {
this.set('isDirty', false);
this.set('hasDirtyAttributes', false);
}
},
// an ugly hack, but necessary to watch all the model's properties
// and more, without having to be explicit and do it manually
isDirty: Ember.computed.apply(Ember, watchedProps.concat({
hasDirtyAttributes: Ember.computed.apply(Ember, watchedProps.concat({
get: function () {
var model = this.get('model'),
markdown = model.get('markdown'),
@ -147,10 +147,10 @@ export default Ember.Mixin.create({
return true;
}
// models created on the client always return `isDirty: true`,
// models created on the client always return `hasDirtyAttributes: true`,
// so we need to see which properties have actually changed.
if (model.get('isNew')) {
changedAttributes = Ember.keys(model.changedAttributes());
changedAttributes = Object.keys(model.changedAttributes());
if (changedAttributes.length) {
return true;
@ -160,10 +160,10 @@ export default Ember.Mixin.create({
}
// even though we use the `scratch` prop to show edits,
// which does *not* change the model's `isDirty` property,
// `isDirty` will tell us if the other props have changed,
// which does *not* change the model's `hasDirtyAttributes` property,
// `hasDirtyAttributes` will tell us if the other props have changed,
// as long as the model is not new (model.isNew === false).
return model.get('isDirty');
return model.get('hasDirtyAttributes');
},
set: function (key, value) {
return value;

View File

@ -33,9 +33,9 @@ export default Ember.Mixin.create(styleBody, ShortcutsRoute, {
willTransition: function (transition) {
var controller = this.get('controller'),
scratch = controller.get('model.scratch'),
controllerIsDirty = controller.get('isDirty'),
controllerIsDirty = controller.get('hasDirtyAttributes'),
model = controller.get('model'),
state = model.getProperties('isDeleted', 'isSaving', 'isDirty', 'isNew'),
state = model.getProperties('isDeleted', 'isSaving', 'hasDirtyAttributes', 'isNew'),
fromNewToEdit,
deletedWithoutChanges;
@ -57,7 +57,7 @@ export default Ember.Mixin.create(styleBody, ShortcutsRoute, {
transition.intent.contexts[0].id === model.get('id');
deletedWithoutChanges = state.isDeleted &&
(state.isSaving || !state.isDirty);
(state.isSaving || !state.hasDirtyAttributes);
if (!fromNewToEdit && !deletedWithoutChanges && controllerIsDirty) {
transition.abort();
@ -99,7 +99,7 @@ export default Ember.Mixin.create(styleBody, ShortcutsRoute, {
attachModelHooks: function (controller, model) {
// this will allow us to track when the model is saved and update the controller
// so that we can be sure controller.isDirty is correct, without having to update the
// so that we can be sure controller.hasDirtyAttributes is correct, without having to update the
// controller on each instance of `model.save()`.
//
// another reason we can't do this on `model.save().then()` is because the post-settings-menu

View File

@ -1,61 +0,0 @@
import Ember from 'ember';
import getRequestErrorMessage from 'ghost/utils/ajax';
export default Ember.Mixin.create({
notifications: Ember.inject.service(),
// set from PaginationRouteMixin
paginationSettings: null,
// indicates whether we're currently loading the next page
isLoading: null,
/**
* Takes an ajax response, concatenates any error messages, then generates an error notification.
* @param {jqXHR} response The jQuery ajax reponse object.
* @return
*/
reportLoadError: function (response) {
var message = 'A problem was encountered while loading more records';
if (response) {
// Get message from response
message += ': ' + getRequestErrorMessage(response, true);
} else {
message += '.';
}
this.get('notifications').showAlert(message, {type: 'error'});
},
actions: {
/**
* Loads the next paginated page of posts into the ember-data store. Will cause the posts list UI to update.
* @return
*/
loadNextPage: function () {
var self = this,
store = this.get('store'),
recordType = this.get('model').get('type'),
metadata = this.store.metadataFor(recordType),
nextPage = metadata.pagination && metadata.pagination.next,
paginationSettings = this.get('paginationSettings');
if (nextPage) {
this.set('isLoading', true);
this.set('paginationSettings.page', nextPage);
store.find(recordType, paginationSettings).then(function () {
self.set('isLoading', false);
}, function (response) {
self.reportLoadError(response);
});
}
},
resetPagination: function () {
this.set('paginationSettings.page', 1);
this.store.setMetadataFor('tag', {pagination: undefined});
}
}
});

View File

@ -1,4 +1,5 @@
import Ember from 'ember';
import getRequestErrorMessage from 'ghost/utils/ajax';
var defaultPaginationSettings = {
page: 1,
@ -6,21 +7,87 @@ var defaultPaginationSettings = {
};
export default Ember.Mixin.create({
notifications: Ember.inject.service(),
paginationModel: null,
paginationSettings: null,
paginationMeta: null,
init: function () {
var paginationSettings = this.get('paginationSettings'),
settings = Ember.$.extend({}, defaultPaginationSettings, paginationSettings);
this._super(...arguments);
this.set('paginationSettings', settings);
this.set('paginationMeta', {});
},
/**
* Sets up pagination details
* @param {object} settings specifies additional pagination details
* Takes an ajax response, concatenates any error messages, then generates an error notification.
* @param {jqXHR} response The jQuery ajax reponse object.
* @return
*/
setupPagination: function (settings) {
settings = settings || {};
for (var key in defaultPaginationSettings) {
if (defaultPaginationSettings.hasOwnProperty(key)) {
if (!settings.hasOwnProperty(key)) {
settings[key] = defaultPaginationSettings[key];
}
}
reportLoadError: function (response) {
var message = 'A problem was encountered while loading more records';
if (response) {
// Get message from response
message += ': ' + getRequestErrorMessage(response, true);
} else {
message += '.';
}
this.set('paginationSettings', settings);
this.controller.set('paginationSettings', settings);
this.get('notifications').showAlert(message, {type: 'error'});
},
loadFirstPage: function () {
var paginationSettings = this.get('paginationSettings'),
modelName = this.get('paginationModel'),
self = this;
paginationSettings.page = 1;
return this.get('store').query(modelName, paginationSettings).then(function (results) {
self.set('paginationMeta', results.meta);
return results;
}, function (response) {
self.reportLoadError(response);
});
},
actions: {
loadFirstPage: function () {
return this.loadFirstPage();
},
/**
* Loads the next paginated page of posts into the ember-data store. Will cause the posts list UI to update.
* @return
*/
loadNextPage: function () {
var self = this,
store = this.get('store'),
modelName = this.get('paginationModel'),
metadata = this.get('paginationMeta'),
nextPage = metadata.pagination && metadata.pagination.next,
paginationSettings = this.get('paginationSettings');
if (nextPage) {
this.set('isLoading', true);
this.set('paginationSettings.page', nextPage);
store.query(modelName, paginationSettings).then(function (results) {
self.set('isLoading', false);
self.set('paginationMeta', results.meta);
return results;
}, function (response) {
self.reportLoadError(response);
});
}
},
resetPagination: function () {
this.set('paginationSettings.page', 1);
}
}
});

View File

@ -47,7 +47,7 @@ export default Ember.Mixin.create({
var self = this,
shortcuts = this.get('shortcuts');
Ember.keys(shortcuts).forEach(function (shortcut) {
Object.keys(shortcuts).forEach(function (shortcut) {
var scope = shortcuts[shortcut].scope || 'default',
action = shortcuts[shortcut],
options;
@ -68,7 +68,7 @@ export default Ember.Mixin.create({
removeShortcuts: function () {
var shortcuts = this.get('shortcuts');
Ember.keys(shortcuts).forEach(function (shortcut) {
Object.keys(shortcuts).forEach(function (shortcut) {
var scope = shortcuts[shortcut].scope || 'default';
key.unbind(shortcut, scope);
});

View File

@ -17,7 +17,7 @@ export default DS.Model.extend(ValidationEngine, {
language: DS.attr('string', {defaultValue: 'en_US'}),
meta_title: DS.attr('string'),
meta_description: DS.attr('string'),
author: DS.belongsTo('user', {async: true}),
author: DS.belongsTo('user', {async: true}),
author_id: DS.attr('number'),
updated_at: DS.attr('moment-date'),
updated_by: DS.attr(),
@ -25,7 +25,10 @@ export default DS.Model.extend(ValidationEngine, {
published_by: DS.belongsTo('user', {async: true}),
created_at: DS.attr('moment-date'),
created_by: DS.attr(),
tags: DS.hasMany('tag', {embedded: 'always'}),
tags: DS.hasMany('tag', {
embedded: 'always',
async: false
}),
url: DS.attr('string'),
config: Ember.inject.service(),

View File

@ -25,7 +25,10 @@ export default DS.Model.extend(ValidationEngine, {
created_by: DS.attr('number'),
updated_at: DS.attr('moment-date'),
updated_by: DS.attr('number'),
roles: DS.hasMany('role', {embedded: 'always'}),
roles: DS.hasMany('role', {
embedded: 'always',
async: false
}),
ghostPaths: Ember.inject.service('ghost-paths'),

View File

@ -143,7 +143,7 @@ export default Ember.Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
if (this.session.isAuthenticated) {
this.get('session.user').then(function (user) {
if (!user.get('isAuthor') && !user.get('isEditor')) {
self.store.findAll('notification').then(function (serverNotifications) {
self.store.findAll('notification', {reload: true}).then(function (serverNotifications) {
serverNotifications.forEach(function (notification) {
self.get('notifications').handleNotification(notification, isDelayed);
});

View File

@ -29,7 +29,7 @@ export default AuthenticatedRoute.extend(base, {
staticPages: 'all'
};
return self.store.find('post', query).then(function (records) {
return self.store.query('post', query).then(function (records) {
var post = records.get('firstObject');
if (post) {

View File

@ -3,40 +3,38 @@ import AuthenticatedRoute from 'ghost/routes/authenticated';
import ShortcutsRoute from 'ghost/mixins/shortcuts-route';
import PaginationRouteMixin from 'ghost/mixins/pagination-route';
var paginationSettings = {
status: 'all',
staticPages: 'all',
page: 1
};
export default AuthenticatedRoute.extend(ShortcutsRoute, PaginationRouteMixin, {
titleToken: 'Content',
paginationModel: 'post',
paginationSettings: {
status: 'all',
staticPages: 'all'
},
model: function () {
var self = this;
var paginationSettings = this.get('paginationSettings'),
self = this;
return this.get('session.user').then(function (user) {
if (user.get('isAuthor')) {
paginationSettings.author = user.get('slug');
}
// using `.filter` allows the template to auto-update when new models are pulled in from the server.
// we just need to 'return true' to allow all models by default.
return self.store.filter('post', paginationSettings, function (post) {
if (user.get('isAuthor')) {
return post.isAuthoredByUser(user);
}
return self.loadFirstPage().then(function () {
// using `.filter` allows the template to auto-update when new models are pulled in from the server.
// we just need to 'return true' to allow all models by default.
return self.store.filter('post', function (post) {
if (user.get('isAuthor')) {
return post.isAuthoredByUser(user);
}
return true;
return true;
});
});
});
},
setupController: function (controller, model) {
this._super(controller, model);
this.setupPagination(paginationSettings);
},
stepThroughPosts: function (step) {
var currentPost = this.get('controller.currentPost'),
posts = this.get('controller.sortedPosts'),

View File

@ -20,7 +20,7 @@ export default MobileIndexRoute.extend(AuthenticatedRouteMixin, {
goToPost: function () {
var self = this,
// the store has been populated by PostsRoute
posts = this.store.all('post'),
posts = this.store.peekAll('post'),
post;
return this.get('session.user').then(function (user) {

View File

@ -16,7 +16,7 @@ export default AuthenticatedRoute.extend(ShortcutsRoute, {
return this.transitionTo('error404', params.post_id);
}
post = this.store.getById('post', postId);
post = this.store.peekRecord('post', postId);
if (post) {
return post;
}
@ -27,9 +27,7 @@ export default AuthenticatedRoute.extend(ShortcutsRoute, {
staticPages: 'all'
};
return self.store.find('post', query).then(function (records) {
var post = records.get('firstObject');
return self.store.queryRecord('post', query).then(function (post) {
if (post) {
return post;
}

View File

@ -14,7 +14,7 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
},
model: function () {
return this.store.find('setting', {type: 'blog,theme'}).then(function (records) {
return this.store.query('setting', {type: 'blog,theme'}).then(function (records) {
return records.get('firstObject');
});
},

View File

@ -15,7 +15,7 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
},
model: function () {
return this.store.find('setting', {type: 'blog,theme,private'}).then(function (records) {
return this.store.query('setting', {type: 'blog,theme,private'}).then(function (records) {
return records.get('firstObject');
});
},

View File

@ -15,7 +15,7 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
},
model: function () {
return this.store.find('setting', {type: 'blog,theme'}).then(function (records) {
return this.store.query('setting', {type: 'blog,theme'}).then(function (records) {
return records.get('firstObject');
});
}

View File

@ -14,7 +14,7 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
},
model: function () {
return this.store.find('setting', {type: 'blog,theme'}).then(function (records) {
return this.store.query('setting', {type: 'blog,theme'}).then(function (records) {
return records.get('firstObject');
});
},

View File

@ -2,36 +2,31 @@ import AuthenticatedRoute from 'ghost/routes/authenticated';
import CurrentUserSettings from 'ghost/mixins/current-user-settings';
import PaginationRouteMixin from 'ghost/mixins/pagination-route';
var paginationSettings;
paginationSettings = {
page: 1,
include: 'post_count',
limit: 15
};
export default AuthenticatedRoute.extend(CurrentUserSettings, PaginationRouteMixin, {
titleToken: 'Settings - Tags',
beforeModel: function (transition) {
this._super(transition);
paginationModel: 'tag',
paginationSettings: {
include: 'post_count',
limit: 15
},
beforeModel: function () {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor());
},
model: function () {
this.store.unloadAll('tag');
this.loadFirstPage();
return this.store.filter('tag', paginationSettings, function (tag) {
return this.store.filter('tag', function (tag) {
return !tag.get('isNew');
});
},
setupController: function (controller, model) {
this._super(controller, model);
this.setupPagination(paginationSettings);
},
renderTemplate: function (controller, model) {
this._super(controller, model);
this.render('settings/tags/settings-menu', {
@ -41,6 +36,6 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, PaginationRouteMix
},
deactivate: function () {
this.controller.send('resetPagination');
this.send('resetPagination');
}
});

View File

@ -3,33 +3,24 @@ import CurrentUserSettings from 'ghost/mixins/current-user-settings';
import PaginationRouteMixin from 'ghost/mixins/pagination-route';
import styleBody from 'ghost/mixins/style-body';
var paginationSettings;
paginationSettings = {
page: 1,
limit: 20,
status: 'active'
};
export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, PaginationRouteMixin, {
titleToken: 'Team',
classNames: ['view-team'],
setupController: function (controller, model) {
this._super(controller, model);
this.setupPagination(paginationSettings);
},
beforeModel: function (transition) {
this._super(transition);
paginationModel: 'user',
paginationSettings: {
status: 'active',
limit: 20
},
model: function () {
var self = this;
return self.store.find('user', {limit: 'all', status: 'invited'}).then(function () {
return self.store.filter('user', paginationSettings, function () {
this.loadFirstPage();
return self.store.query('user', {limit: 'all', status: 'invited'}).then(function () {
return self.store.filter('user', function () {
return true;
});
});

View File

@ -13,7 +13,7 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
// return this.store.find('user', { slug: params.slug });
// Instead, get all the users and then find by slug
return this.store.find('user').then(function (result) {
return this.store.findAll('user', {reload: true}).then(function (result) {
var user = result.findBy('slug', params.slug);
if (!user) {
@ -42,8 +42,8 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
var model = this.modelFor('team.user');
// we want to revert any unsaved changes on exit
if (model && model.get('isDirty')) {
model.rollback();
if (model && model.get('hasDirtyAttributes')) {
model.rollbackAttributes();
}
model.get('errors').clear();

View File

@ -2,6 +2,9 @@ import Ember from 'ember';
import DS from 'ember-data';
export default DS.RESTSerializer.extend({
isNewSerializerAPI: true,
serializeIntoHash: function (hash, type, record, options) {
// Our API expects an id on the posted object
options = options || {};

View File

@ -8,30 +8,29 @@ export default ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, {
tags: {embedded: 'always'}
},
normalize: function (type, hash) {
normalize: function (typeClass, hash, prop) {
// this is to enable us to still access the raw author_id
// without requiring an extra get request (since it is an
// async relationship).
hash.author_id = hash.author;
return this._super(type, hash);
return this._super(typeClass, hash, prop);
},
extractSingle: function (store, primaryType, payload) {
var root = this.keyForAttribute(primaryType.modelName),
pluralizedRoot = Ember.String.pluralize(primaryType.modelName);
normalizeSingleResponse: function (store, primaryModelClass, payload) {
var root = this.keyForAttribute(primaryModelClass.modelName),
pluralizedRoot = Ember.String.pluralize(primaryModelClass.modelName);
// make payload { post: { title: '', tags: [obj, obj], etc. } }.
// this allows ember-data to pull the embedded tags out again,
// in the function `updatePayloadWithEmbeddedHasMany` of the
// EmbeddedRecordsMixin (line: `if (!partial[attribute])`):
// https://github.com/emberjs/data/blob/master/packages/activemodel-adapter/lib/system/embedded_records_mixin.js#L499
payload[root] = payload[pluralizedRoot][0];
delete payload[pluralizedRoot];
return this._super.apply(this, arguments);
},
normalizeArrayResponse: function () {
return this._super.apply(this, arguments);
},
serializeIntoHash: function (hash, type, record, options) {
options = options || {};
options.includeId = true;

View File

@ -20,19 +20,23 @@ export default ApplicationSerializer.extend({
hash[root] = payload;
},
extractArray: function (store, type, _payload) {
normalizeArrayResponse: function (store, primaryModelClass, _payload, id, requestType) {
var payload = {settings: [this._extractObjectFromArrayPayload(_payload)]};
return this._super(store, primaryModelClass, payload, id, requestType);
},
normalizeSingleResponse: function (store, primaryModelClass, _payload, id, requestType) {
var payload = {setting: this._extractObjectFromArrayPayload(_payload)};
return this._super(store, primaryModelClass, payload, id, requestType);
},
_extractObjectFromArrayPayload: function (_payload) {
var payload = {id: '0'};
_payload.settings.forEach(function (setting) {
payload[setting.key] = setting.value;
});
payload = this.normalize(type, payload);
return [payload];
},
extractSingle: function (store, type, payload) {
return this.extractArray(store, type, payload).pop();
return payload;
}
});

View File

@ -14,6 +14,16 @@ export default ApplicationSerializer.extend(DS.EmbeddedRecordsMixin, {
payload[root] = payload[pluralizedRoot][0];
delete payload[pluralizedRoot];
return this._super.apply(this, arguments);
},
normalizeSingleResponse: function (store, primaryModelClass, payload) {
var root = this.keyForAttribute(primaryModelClass.modelName),
pluralizedRoot = Ember.String.pluralize(primaryModelClass.modelName);
payload[root] = payload[pluralizedRoot][0];
delete payload[pluralizedRoot];
return this._super.apply(this, arguments);
}
});

View File

@ -45,9 +45,9 @@ export default function () {
});
Ember.Router.reopen({
updateTitle: function () {
updateTitle: Ember.on('didTransition', function () {
this.send('collectTitleTokens', []);
}.on('didTransition'),
}),
setTitle: function (title) {
if (Ember.testing) {

View File

@ -1,5 +1,5 @@
import Ember from 'ember';
Ember.TextField.reopen({
attributeBindings: ['autofocus']
});
Ember.TextField.reopen({
attributeBindings: ['autofocus']
});

View File

@ -4,10 +4,10 @@
"blueimp-md5": "1.1.0",
"codemirror": "5.2.0",
"devicejs": "0.2.7",
"ember": "1.13.2",
"ember": "1.13.10",
"ember-cli-shims": "ember-cli/ember-cli-shims#0.0.3",
"ember-cli-test-loader": "0.1.3",
"ember-data": "1.0.0-beta.18",
"ember-data": "1.13.13",
"ember-mocha": "0.8.2",
"ember-load-initializers": "ember-cli/ember-load-initializers#0.1.5",
"ember-resolver": "0.1.18",

View File

@ -19,28 +19,32 @@
"author": "",
"license": "MIT",
"devDependencies": {
"broccoli-asset-rev": "^2.0.2",
"ember-cli": "1.13.0",
"ember-cli-app-version": "0.4.0",
"ember-cli-babel": "^5.0.0",
"broccoli-asset-rev": "^2.1.2",
"ember-cli": "1.13.8",
"ember-cli-app-version": "0.5.0",
"ember-cli-babel": "^5.1.3",
"ember-cli-content-security-policy": "0.4.0",
"ember-cli-dependency-checker": "^1.0.0",
"ember-cli-dependency-checker": "^1.0.1",
"ember-cli-fastclick": "1.0.3",
"ember-cli-htmlbars": "0.7.9",
"ember-cli-htmlbars-inline-precompile": "^0.1.1",
"ember-cli-htmlbars-inline-precompile": "^0.2.0",
"ember-cli-ic-ajax": "0.2.1",
"ember-cli-inject-live-reload": "^1.3.0",
"ember-cli-inject-live-reload": "^1.3.1",
"ember-cli-mocha": "0.9.3",
"ember-cli-release": "0.2.3",
"ember-cli-selectize": "0.4.0",
"ember-cli-simple-auth": "0.8.0",
"ember-cli-simple-auth-oauth2": "0.8.0",
"ember-cli-uglify": "^1.0.1",
"ember-data": "1.0.0-beta.18",
"ember-cli-sri": "^1.0.3",
"ember-cli-uglify": "^1.2.0",
"ember-data": "1.13.13",
"ember-data-filter": "1.13.0",
"ember-disable-proxy-controllers": "^1.0.0",
"ember-export-application-global": "^1.0.2",
"ember-export-application-global": "^1.0.3",
"ember-myth": "0.1.1",
"ember-resize": "0.0.10",
"ember-sinon": "0.2.1",
"ember-watson": "^0.6.4",
"fs-extra": "0.16.3",
"glob": "^4.0.5",
"walk-sync": "^0.1.3"

View File

@ -13,6 +13,7 @@ describeComponent(
function () {
it('identifies a URL as the base URL', function () {
var component = this.subject({
url: '',
baseUrl: 'http://example.com/'
});