Convert general settings page to ember data

Issue #2846
-Implement custom RESTAdapter and serializer for settings data.
-Convert theme selector to Ember.Select.
-Finish upload modal and wire into settings page.
This commit is contained in:
Jason Williams 2014-06-20 02:29:49 +00:00
parent 0603be73ef
commit 7490d350ea
10 changed files with 116 additions and 134 deletions

21
adapters/setting.js Normal file
View File

@ -0,0 +1,21 @@
import ApplicationAdapter from 'ghost/adapters/application';
var SettingAdapter = ApplicationAdapter.extend({
updateRecord: function (store, type, record) {
var data = {},
serializer = store.serializerFor(type.typeKey);
// remove the fake id that we added onto the model.
delete record.id;
// use the SettingSerializer to transform the model back into
// an array of settings objects like the API expects
serializer.serializeIntoHash(data, type, record);
// use the ApplicationAdapter's buildURL method but do not
// pass in an id.
return this.ajax(this.buildURL(type.typeKey), 'PUT', { data: data });
}
});
export default SettingAdapter;

View File

@ -43,6 +43,7 @@ var UploadModal = ModalDialog.extend({
func.apply(this);
}
this.sendAction();
this.sendAction('confirm' + type);
}
}
});

View File

@ -2,15 +2,17 @@
var UploadController = Ember.Controller.extend({
acceptEncoding: 'image/*',
actions: {
confirmReject: function () {
return true;
}
},
confirmAccept: function () {
var self = this;
confirm: {
reject: {
buttonClass: true,
text: 'Cancel' // The reject button text
this.get('model').save().then(function (model) {
self.notifications.showSuccess('Saved');
return model;
}).catch(this.notifications.showErrors);
},
confirmReject: function () {
return false;
}
}
});

View File

@ -1,10 +1,3 @@
var elementLookup = {
title: '#blog-title',
description: '#blog-description',
email: '#email-address',
postsPerPage: '#postsPerPage'
};
var SettingsGeneralController = Ember.ObjectController.extend({
isDatedPermalinks: function (key, value) {
// setter
@ -18,41 +11,31 @@ var SettingsGeneralController = Ember.ObjectController.extend({
return slugForm !== '/:slug/';
}.property('permalinks'),
themes: function () {
return this.get('availableThemes').reduce(function (themes, t) {
var theme = {};
theme.name = t.name;
theme.label = t.package ? t.package.name + ' - ' + t.package.version : t.name;
theme.package = t.package;
theme.active = !!t.active;
themes.push(theme);
return themes;
}, []);
}.property('availableThemes').readOnly(),
actions: {
save: function () {
// Validate and save settings
var model = this.get('model'),
// @TODO: Don't know how to scope this to this controllers view because this.view is null
errs = model.validate();
var self = this;
if (errs.length > 0) {
// Set the actual element from this view based on the error
errs.forEach(function (err) {
// @TODO: Probably should still be scoped to this controllers root element.
err.el = $(elementLookup[err.el]);
});
// Let the applicationRoute handle validation errors
this.send('handleErrors', errs);
} else {
model.save().then(function () {
// @TODO: Notification of success
window.alert('Saved data!');
}, function () {
// @TODO: Notification of error
window.alert('Error saving data');
});
}
return this.get('model').save().then(function (model) {
self.notifications.showSuccess('Settings successfully saved.');
return model;
}).catch(this.notifications.showErrors);
},
uploadLogo: function () {
// @TODO: Integrate with Modal component
},
uploadCover: function () {
// @TODO: Integrate with Modal component
}
}
});
export default SettingsGeneralController;
export default SettingsGeneralController;

15
models/setting.js Normal file
View File

@ -0,0 +1,15 @@
var Setting = DS.Model.extend({
title: DS.attr('string'),
description: DS.attr('string'),
email: DS.attr('string'),
logo: DS.attr('string'),
cover: DS.attr('string'),
defaultLang: DS.attr('string'),
postsPerPage: DS.attr('number'),
forceI18n: DS.attr('boolean'),
permalinks: DS.attr('string'),
activeTheme: DS.attr('string'),
availableThemes: DS.attr()
});
export default Setting;

View File

@ -1,76 +0,0 @@
var validator = window.validator;
import BaseModel from 'ghost/models/base';
var SettingsModel = BaseModel.extend({
url: BaseModel.apiRoot + '/settings/?type=blog,theme,app',
title: null,
description: null,
email: null,
logo: null,
cover: null,
defaultLang: null,
postsPerPage: null,
forceI18n: null,
permalinks: null,
activeTheme: null,
activeApps: null,
installedApps: null,
availableThemes: null,
availableApps: null,
validate: function () {
var validationErrors = [],
postsPerPage;
if (!validator.isLength(this.get('title'), 0, 150)) {
validationErrors.push({message: 'Title is too long', el: 'title'});
}
if (!validator.isLength(this.get('description'), 0, 200)) {
validationErrors.push({message: 'Description is too long', el: 'description'});
}
if (!validator.isEmail(this.get('email')) || !validator.isLength(this.get('email'), 0, 254)) {
validationErrors.push({message: 'Please supply a valid email address', el: 'email'});
}
postsPerPage = this.get('postsPerPage');
if (!validator.isInt(postsPerPage) || postsPerPage > 1000) {
validationErrors.push({message: 'Please use a number less than 1000', el: 'postsPerPage'});
}
if (!validator.isInt(postsPerPage) || postsPerPage < 0) {
validationErrors.push({message: 'Please use a number greater than 0', el: 'postsPerPage'});
}
return validationErrors;
},
exportPath: BaseModel.adminRoot + '/export/',
importFrom: function (file) {
var formData = new FormData();
formData.append('importfile', file);
return ic.ajax.request(BaseModel.apiRoot + '/db/', {
headers: {
'X-CSRF-Token': $('meta[name="csrf-param"]').attr('content')
},
type: 'POST',
data: formData,
dataType: 'json',
cache: false,
contentType: false,
processData: false
});
},
sendTestEmail: function () {
return ic.ajax.request(BaseModel.apiRoot + '/mail/test/', {
type: 'POST',
headers: {
'X-CSRF-Token': $('meta[name="csrf-param"]').attr('content')
}
});
}
});
export default SettingsModel;

View File

@ -1,13 +1,11 @@
import ajax from 'ghost/utils/ajax';
import AuthenticatedRoute from 'ghost/routes/authenticated';
import SettingsModel from 'ghost/models/settings';
var SettingsGeneralRoute = AuthenticatedRoute.extend({
model: function () {
return ajax('/ghost/api/v0.1/settings/?type=blog,theme,app').then(function (resp) {
return SettingsModel.create(resp);
return this.store.find('setting', { type: 'blog,theme' }).then(function (records) {
return records.get('firstObject');
});
}
});
export default SettingsGeneralRoute;
export default SettingsGeneralRoute;

View File

@ -1,10 +1,9 @@
import AuthenticatedRoute from 'ghost/routes/authenticated';
var SettingsIndexRoute = AuthenticatedRoute.extend({
// redirect to general tab
redirect: function () {
this.transitionTo('settings.general');
}
});
export default SettingsIndexRoute;
export default SettingsIndexRoute;

37
serializers/setting.js Normal file
View File

@ -0,0 +1,37 @@
import ApplicationSerializer from 'ghost/serializers/application';
var SettingSerializer = ApplicationSerializer.extend({
serializeIntoHash: function (hash, type, record, options) {
// Settings API does not want ids
options = options || {};
options.includeId = false;
var root = Ember.String.pluralize(type.typeKey),
data = this.serialize(record, options),
payload = [];
delete data.id;
Object.keys(data).forEach(function (k) {
payload.push({ key: k, value: data[k] });
});
hash[root] = payload;
},
extractArray: function (store, type, _payload) {
var payload = { id: '0' };
_payload.settings.forEach(function (setting) {
payload[setting.key] = setting.value;
});
return [payload];
},
extractSingle: function (store, type, payload) {
return this.extractArray(store, type, payload).pop();
}
});
export default SettingSerializer;

View File

@ -67,11 +67,13 @@
<div class="form-group">
<label for="activeTheme">Theme</label>
<select id="activeTheme" name="general[activeTheme]">
{{#each availableThemes}}
<option value="{{name}}" {{#if active}}selected{{/if}}>{{#if package}}{{package.name}} - {{package.version}}{{else}}{{name}}{{/if}}</option>
{{/each}}
</select>
{{view Ember.Select
id="activeTheme"
name="general[activeTheme]"
content=themes
optionValuePath="content.name"
optionLabelPath="content.label"
value=activeTheme}}
<p>Select a theme for your blog</p>
</div>