diff --git a/ghost/admin/app/index.html b/ghost/admin/app/index.html
index fa6e6f1bec..b055a733a8 100644
--- a/ghost/admin/app/index.html
+++ b/ghost/admin/app/index.html
@@ -32,7 +32,7 @@
-
+
{{content-for "head-footer"}}
diff --git a/ghost/admin/app/routes/application.js b/ghost/admin/app/routes/application.js
index ae3e4c151b..60a35eb6bb 100644
--- a/ghost/admin/app/routes/application.js
+++ b/ghost/admin/app/routes/application.js
@@ -3,6 +3,8 @@ import {htmlSafe} from 'ember-string';
import injectService from 'ember-service/inject';
import run from 'ember-runloop';
import {isEmberArray} from 'ember-array/utils';
+import observer from 'ember-metal/observer';
+import $ from 'jquery';
import AuthConfiguration from 'ember-simple-auth/configuration';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
@@ -27,6 +29,7 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
config: injectService(),
feature: injectService(),
dropdown: injectService(),
+ lazyLoader: injectService(),
notifications: injectService(),
upgradeNotification: injectService(),
@@ -54,7 +57,11 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
// return the feature loading promise so that we block until settings
// are loaded in order for synchronous access everywhere
- return this.get('feature').fetch();
+ return this.get('feature').fetch().then(() => {
+ if (this.get('feature.nightShift')) {
+ return this._setAdminTheme();
+ }
+ });
}
},
@@ -88,6 +95,19 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
}
},
+ _nightShift: observer('feature.nightShift', function () {
+ this._setAdminTheme();
+ }),
+
+ _setAdminTheme() {
+ let nightShift = this.get('feature.nightShift');
+
+ return this.get('lazyLoader').loadStyle('dark', 'assets/ghost-dark.css', true).then(() => {
+ $('link[title=dark]').prop('disabled', !nightShift);
+ $('link[title=light]').prop('disabled', nightShift);
+ });
+ },
+
actions: {
openMobileMenu() {
this.controller.set('showMobileMenu', true);
@@ -113,6 +133,10 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
signedIn() {
this.get('notifications').clearAll();
this.send('loadServerNotifications', true);
+
+ if (this.get('feature.nightShift')) {
+ this._setAdminTheme();
+ }
},
invalidateSession() {
diff --git a/ghost/admin/app/services/feature.js b/ghost/admin/app/services/feature.js
index b6c0b9c6f5..275d6df4ed 100644
--- a/ghost/admin/app/services/feature.js
+++ b/ghost/admin/app/services/feature.js
@@ -3,13 +3,20 @@ import Service from 'ember-service';
import computed from 'ember-computed';
import injectService from 'ember-service/inject';
import set from 'ember-metal/set';
+import RSVP from 'rsvp';
// ember-cli-shims doesn't export Error
const {Error: EmberError} = Ember;
-export function feature(name) {
- return computed(`config.${name}`, `labs.${name}`, {
+export function feature(name, user = false) {
+ let watchedProps = user ? [`accessibility.${name}`] : [`config.${name}`, `labs.${name}`];
+
+ return computed.apply(Ember, watchedProps.concat({
get() {
+ if (user) {
+ return this.get(`accessibility.${name}`);
+ }
+
if (this.get(`config.${name}`)) {
return this.get(`config.${name}`);
}
@@ -17,21 +24,24 @@ export function feature(name) {
return this.get(`labs.${name}`) || false;
},
set(key, value) {
- this.update(key, value);
+ this.update(key, value, user);
return value;
}
- });
+ }));
}
export default Service.extend({
store: injectService(),
config: injectService(),
+ session: injectService(),
notifications: injectService(),
publicAPI: feature('publicAPI'),
subscribers: feature('subscribers'),
+ nightShift: feature('nightShift', true),
_settings: null,
+ _user: null,
labs: computed('_settings.labs', function () {
let labs = this.get('_settings.labs');
@@ -43,40 +53,57 @@ export default Service.extend({
}
}),
+ accessibility: computed('_user.accessibility', function () {
+ let accessibility = this.get('_user.accessibility');
+
+ try {
+ return JSON.parse(accessibility) || {};
+ } catch (e) {
+ return {};
+ }
+ }),
+
fetch() {
- return this.get('store').queryRecord('setting', {type: 'blog,theme,private'}).then((settings) => {
+ return RSVP.hash({
+ settings: this.get('store').queryRecord('setting', {type: 'blog,theme,private'}),
+ user: this.get('session.user')
+ }).then(({settings, user}) => {
this.set('_settings', settings);
+ this.set('_user', user);
+
return true;
});
},
- update(key, value) {
- let settings = this.get('_settings');
- let labs = this.get('labs');
+ update(key, value, user = false) {
+ let serviceProperty = user ? 'accessibility' : 'labs';
+ let model = this.get(user ? '_user' : '_settings');
+ let featureObject = this.get(serviceProperty);
- // set the new labs key value
- set(labs, key, value);
- // update the 'labs' key of the settings model
- settings.set('labs', JSON.stringify(labs));
+ // set the new key value for either the labs property or the accessibility property
+ set(featureObject, key, value);
- return settings.save().then(() => {
+ // update the 'labs' or 'accessibility' key of the model
+ model.set(serviceProperty, JSON.stringify(featureObject));
+
+ return model.save().then(() => {
// return the labs key value that we get from the server
- this.notifyPropertyChange('labs');
- return this.get(`labs.${key}`);
+ this.notifyPropertyChange(serviceProperty);
+ return this.get(`${serviceProperty}.${key}`);
}).catch((error) => {
- settings.rollbackAttributes();
- this.notifyPropertyChange('labs');
+ model.rollbackAttributes();
+ this.notifyPropertyChange(serviceProperty);
// we'll always have an errors object unless we hit a
// validation error
if (!error) {
- throw new EmberError('Validation of the feature service settings model failed when updating labs.');
+ throw new EmberError(`Validation of the feature service ${user ? 'user' : 'settings'} model failed when updating ${serviceProperty}.`);
}
this.get('notifications').showAPIError(error);
- return this.get(`labs.${key}`);
+ return this.get(`${serviceProperty}.${key}`);
});
}
});
diff --git a/ghost/admin/app/services/lazy-loader.js b/ghost/admin/app/services/lazy-loader.js
index b853296273..6d8c6896b1 100644
--- a/ghost/admin/app/services/lazy-loader.js
+++ b/ghost/admin/app/services/lazy-loader.js
@@ -39,7 +39,7 @@ export default Service.extend({
return scriptPromise;
},
- loadStyle(key, url) {
+ loadStyle(key, url, alternate = false) {
if (this.get('testing') || $(`#${key}-styles`).length) {
return RSVP.resolve();
}
@@ -47,10 +47,22 @@ export default Service.extend({
return new RSVP.Promise((resolve, reject) => {
let link = document.createElement('link');
link.id = `${key}-styles`;
- link.rel = 'stylesheet';
+ link.rel = alternate ? 'alternate stylesheet' : 'stylesheet';
link.href = `${this.get('ghostPaths.adminRoot')}${url}`;
- link.onload = resolve;
+ link.onload = () => {
+ if (alternate) {
+ // If stylesheet is alternate and we disable the stylesheet before injecting into the DOM,
+ // the onload handler never gets called. Thus, we should disable the link after it has finished loading
+ link.disabled = true;
+ }
+ resolve();
+ };
link.onerror = reject;
+
+ if (alternate) {
+ link.title = key;
+ }
+
$('head').append($(link));
});
}
diff --git a/ghost/admin/tests/integration/services/feature-test.js b/ghost/admin/tests/integration/services/feature-test.js
index 6738e3db7b..4de3171daa 100644
--- a/ghost/admin/tests/integration/services/feature-test.js
+++ b/ghost/admin/tests/integration/services/feature-test.js
@@ -43,9 +43,40 @@ function stubSettings(server, labs, validSave = true, validSettings = true) {
});
}
+function stubUser(server, accessibility, validSave = true) {
+ let users = [{
+ id: '1',
+ // Add extra properties for the validations
+ name: 'Test User',
+ email: 'test@example.com',
+ accessibility: JSON.stringify(accessibility),
+ roles: [{
+ id: 1,
+ name: 'Owner',
+ description: 'Owner'
+ }]
+ }];
+
+ server.get('/ghost/api/v0.1/users/me/', function () {
+ return [200, {'Content-Type': 'application/json'}, JSON.stringify({users})];
+ });
+
+ server.put('/ghost/api/v0.1/users/1/', function (request) {
+ let statusCode = (validSave) ? 200 : 400;
+ let response = (validSave) ? request.requestBody : JSON.stringify({
+ errors: [{
+ message: 'Test Error'
+ }]
+ });
+
+ return [statusCode, {'Content-Type': 'application/json'}, response];
+ });
+}
+
function addTestFlag() {
FeatureService.reopen({
- testFlag: feature('testFlag')
+ testFlag: feature('testFlag'),
+ testUserFlag: feature('testUserFlag', true)
});
}
@@ -64,82 +95,118 @@ describe('Integration: Service: feature', function () {
server.shutdown();
});
- it('loads labs settings correctly', function (done) {
+ it('loads labs and user settings correctly', function () {
stubSettings(server, {testFlag: true});
+ stubUser(server, {testUserFlag: true});
+
addTestFlag();
let service = this.subject();
- service.fetch().then(() => {
+ return service.fetch().then(() => {
expect(service.get('testFlag')).to.be.true;
- done();
+ expect(service.get('testUserFlag')).to.be.true;
});
});
- it('returns false for set flag with config false and labs false', function (done) {
+ it('returns false for set flag with config false and labs false', function () {
stubSettings(server, {testFlag: false});
+ stubUser(server, {});
+
addTestFlag();
let service = this.subject();
service.get('config').set('testFlag', false);
- service.fetch().then(() => {
+ return service.fetch().then(() => {
expect(service.get('labs.testFlag')).to.be.false;
expect(service.get('testFlag')).to.be.false;
- done();
});
});
- it('returns true for set flag with config true and labs false', function (done) {
+ it('returns true for set flag with config true and labs false', function () {
stubSettings(server, {testFlag: false});
+ stubUser(server, {});
+
addTestFlag();
let service = this.subject();
service.get('config').set('testFlag', true);
- service.fetch().then(() => {
+ return service.fetch().then(() => {
expect(service.get('labs.testFlag')).to.be.false;
expect(service.get('testFlag')).to.be.true;
- done();
});
});
- it('returns true for set flag with config false and labs true', function (done) {
+ it('returns true for set flag with config false and labs true', function () {
stubSettings(server, {testFlag: true});
+ stubUser(server, {});
+
addTestFlag();
let service = this.subject();
service.get('config').set('testFlag', false);
- service.fetch().then(() => {
+ return service.fetch().then(() => {
expect(service.get('labs.testFlag')).to.be.true;
expect(service.get('testFlag')).to.be.true;
- done();
});
});
- it('returns true for set flag with config true and labs true', function (done) {
+ it('returns true for set flag with config true and labs true', function () {
stubSettings(server, {testFlag: true});
+ stubUser(server, {});
+
addTestFlag();
let service = this.subject();
service.get('config').set('testFlag', true);
- service.fetch().then(() => {
+ return service.fetch().then(() => {
expect(service.get('labs.testFlag')).to.be.true;
expect(service.get('testFlag')).to.be.true;
- done();
});
});
- it('saves correctly', function (done) {
+ it('returns false for set flag with accessibility false', function () {
+ stubSettings(server, {});
+ stubUser(server, {testUserFlag: false});
+
+ addTestFlag();
+
+ let service = this.subject();
+
+ return service.fetch().then(() => {
+ expect(service.get('accessibility.testUserFlag')).to.be.false;
+ expect(service.get('testUserFlag')).to.be.false;
+ });
+ });
+
+ it('returns true for set flag with accessibility true', function () {
+ stubSettings(server, {});
+ stubUser(server, {testUserFlag: true});
+
+ addTestFlag();
+
+ let service = this.subject();
+
+ return service.fetch().then(() => {
+ expect(service.get('accessibility.testUserFlag')).to.be.true;
+ expect(service.get('testUserFlag')).to.be.true;
+ });
+ });
+
+ it('saves labs setting correctly', function () {
stubSettings(server, {testFlag: false});
+ stubUser(server, {testUserFlag: false});
+
addTestFlag();
let service = this.subject();
service.get('config').set('testFlag', false);
- service.fetch().then(() => {
+ return service.fetch().then(() => {
expect(service.get('testFlag')).to.be.false;
run(() => {
@@ -149,19 +216,42 @@ describe('Integration: Service: feature', function () {
return wait().then(() => {
expect(server.handlers[1].numberOfCalls).to.equal(1);
expect(service.get('testFlag')).to.be.true;
- done();
});
});
});
- it('notifies for server errors', function (done) {
+ it('saves accessibility setting correctly', function () {
+ stubSettings(server, {});
+ stubUser(server, {testUserFlag: false});
+
+ addTestFlag();
+
+ let service = this.subject();
+
+ return service.fetch().then(() => {
+ expect(service.get('testUserFlag')).to.be.false;
+
+ run(() => {
+ service.set('testUserFlag', true);
+ });
+
+ return wait().then(() => {
+ expect(server.handlers[3].numberOfCalls).to.equal(1);
+ expect(service.get('testUserFlag')).to.be.true;
+ });
+ });
+ });
+
+ it('notifies for server errors on labs save', function () {
stubSettings(server, {testFlag: false}, false);
+ stubUser(server, {});
+
addTestFlag();
let service = this.subject();
service.get('config').set('testFlag', false);
- service.fetch().then(() => {
+ return service.fetch().then(() => {
expect(service.get('testFlag')).to.be.false;
run(() => {
@@ -180,19 +270,51 @@ describe('Integration: Service: feature', function () {
).to.equal(1);
expect(service.get('testFlag')).to.be.false;
- done();
});
});
});
- it('notifies for validation errors', function (done) {
+ it('notifies for server errors on accessibility save', function () {
+ stubSettings(server, {});
+ stubUser(server, {testUserFlag: false}, false);
+
+ addTestFlag();
+
+ let service = this.subject();
+
+ return service.fetch().then(() => {
+ expect(service.get('testUserFlag')).to.be.false;
+
+ run(() => {
+ service.set('testUserFlag', true);
+ });
+
+ return wait().then(() => {
+ expect(
+ server.handlers[3].numberOfCalls,
+ 'PUT call is made'
+ ).to.equal(1);
+
+ expect(
+ service.get('notifications.alerts').length,
+ 'number of alerts shown'
+ ).to.equal(1);
+
+ expect(service.get('testUserFlag')).to.be.false;
+ });
+ });
+ });
+
+ it('notifies for validation errors', function () {
stubSettings(server, {testFlag: false}, true, false);
+ stubUser(server, {});
+
addTestFlag();
let service = this.subject();
service.get('config').set('testFlag', false);
- service.fetch().then(() => {
+ return service.fetch().then(() => {
expect(service.get('testFlag')).to.be.false;
run(() => {
@@ -205,7 +327,6 @@ describe('Integration: Service: feature', function () {
// ensure validation is happening before the API is hit
expect(server.handlers[1].numberOfCalls).to.equal(0);
expect(service.get('testFlag')).to.be.false;
- done();
});
});
});