0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-24 23:48:13 -05:00

🎨 implement night mode

no issue
- add functionality for night mode feature flag using alternate
stylesheets
- modify lazy loader service to work with alternate stylesheets
- update feature service to use user accessibility property & add tests
This commit is contained in:
Austin Burdine 2017-03-03 10:14:33 -06:00 committed by Kevin Ansfield
parent 3536d91c9c
commit 6619f09eca
5 changed files with 233 additions and 49 deletions

View file

@ -32,7 +32,7 @@
<meta name="msapplication-square310x310logo" content="{{asset "img/large.png" ghost="true"}}" /> <meta name="msapplication-square310x310logo" content="{{asset "img/large.png" ghost="true"}}" />
<link rel="stylesheet" href="{{asset "vendor.css" ghost="true" minifyInProduction="true"}}" /> <link rel="stylesheet" href="{{asset "vendor.css" ghost="true" minifyInProduction="true"}}" />
<link rel="stylesheet" href="{{asset "ghost.css" ghost="true" minifyInProduction="true"}}" /> <link rel="stylesheet" href="{{asset "ghost.css" ghost="true" minifyInProduction="true"}}" title="light" />
{{content-for "head-footer"}} {{content-for "head-footer"}}
</head> </head>

View file

@ -3,6 +3,8 @@ import {htmlSafe} from 'ember-string';
import injectService from 'ember-service/inject'; import injectService from 'ember-service/inject';
import run from 'ember-runloop'; import run from 'ember-runloop';
import {isEmberArray} from 'ember-array/utils'; import {isEmberArray} from 'ember-array/utils';
import observer from 'ember-metal/observer';
import $ from 'jquery';
import AuthConfiguration from 'ember-simple-auth/configuration'; import AuthConfiguration from 'ember-simple-auth/configuration';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin'; import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
@ -27,6 +29,7 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
config: injectService(), config: injectService(),
feature: injectService(), feature: injectService(),
dropdown: injectService(), dropdown: injectService(),
lazyLoader: injectService(),
notifications: injectService(), notifications: injectService(),
upgradeNotification: injectService(), upgradeNotification: injectService(),
@ -54,7 +57,11 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
// return the feature loading promise so that we block until settings // return the feature loading promise so that we block until settings
// are loaded in order for synchronous access everywhere // 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: { actions: {
openMobileMenu() { openMobileMenu() {
this.controller.set('showMobileMenu', true); this.controller.set('showMobileMenu', true);
@ -113,6 +133,10 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
signedIn() { signedIn() {
this.get('notifications').clearAll(); this.get('notifications').clearAll();
this.send('loadServerNotifications', true); this.send('loadServerNotifications', true);
if (this.get('feature.nightShift')) {
this._setAdminTheme();
}
}, },
invalidateSession() { invalidateSession() {

View file

@ -3,13 +3,20 @@ import Service from 'ember-service';
import computed from 'ember-computed'; import computed from 'ember-computed';
import injectService from 'ember-service/inject'; import injectService from 'ember-service/inject';
import set from 'ember-metal/set'; import set from 'ember-metal/set';
import RSVP from 'rsvp';
// ember-cli-shims doesn't export Error // ember-cli-shims doesn't export Error
const {Error: EmberError} = Ember; const {Error: EmberError} = Ember;
export function feature(name) { export function feature(name, user = false) {
return computed(`config.${name}`, `labs.${name}`, { let watchedProps = user ? [`accessibility.${name}`] : [`config.${name}`, `labs.${name}`];
return computed.apply(Ember, watchedProps.concat({
get() { get() {
if (user) {
return this.get(`accessibility.${name}`);
}
if (this.get(`config.${name}`)) { if (this.get(`config.${name}`)) {
return this.get(`config.${name}`); return this.get(`config.${name}`);
} }
@ -17,21 +24,24 @@ export function feature(name) {
return this.get(`labs.${name}`) || false; return this.get(`labs.${name}`) || false;
}, },
set(key, value) { set(key, value) {
this.update(key, value); this.update(key, value, user);
return value; return value;
} }
}); }));
} }
export default Service.extend({ export default Service.extend({
store: injectService(), store: injectService(),
config: injectService(), config: injectService(),
session: injectService(),
notifications: injectService(), notifications: injectService(),
publicAPI: feature('publicAPI'), publicAPI: feature('publicAPI'),
subscribers: feature('subscribers'), subscribers: feature('subscribers'),
nightShift: feature('nightShift', true),
_settings: null, _settings: null,
_user: null,
labs: computed('_settings.labs', function () { labs: computed('_settings.labs', function () {
let labs = this.get('_settings.labs'); 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() { 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('_settings', settings);
this.set('_user', user);
return true; return true;
}); });
}, },
update(key, value) { update(key, value, user = false) {
let settings = this.get('_settings'); let serviceProperty = user ? 'accessibility' : 'labs';
let labs = this.get('labs'); let model = this.get(user ? '_user' : '_settings');
let featureObject = this.get(serviceProperty);
// set the new labs key value // set the new key value for either the labs property or the accessibility property
set(labs, key, value); set(featureObject, key, value);
// update the 'labs' key of the settings model
settings.set('labs', JSON.stringify(labs));
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 // return the labs key value that we get from the server
this.notifyPropertyChange('labs'); this.notifyPropertyChange(serviceProperty);
return this.get(`labs.${key}`); return this.get(`${serviceProperty}.${key}`);
}).catch((error) => { }).catch((error) => {
settings.rollbackAttributes(); model.rollbackAttributes();
this.notifyPropertyChange('labs'); this.notifyPropertyChange(serviceProperty);
// we'll always have an errors object unless we hit a // we'll always have an errors object unless we hit a
// validation error // validation error
if (!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); this.get('notifications').showAPIError(error);
return this.get(`labs.${key}`); return this.get(`${serviceProperty}.${key}`);
}); });
} }
}); });

View file

@ -39,7 +39,7 @@ export default Service.extend({
return scriptPromise; return scriptPromise;
}, },
loadStyle(key, url) { loadStyle(key, url, alternate = false) {
if (this.get('testing') || $(`#${key}-styles`).length) { if (this.get('testing') || $(`#${key}-styles`).length) {
return RSVP.resolve(); return RSVP.resolve();
} }
@ -47,10 +47,22 @@ export default Service.extend({
return new RSVP.Promise((resolve, reject) => { return new RSVP.Promise((resolve, reject) => {
let link = document.createElement('link'); let link = document.createElement('link');
link.id = `${key}-styles`; link.id = `${key}-styles`;
link.rel = 'stylesheet'; link.rel = alternate ? 'alternate stylesheet' : 'stylesheet';
link.href = `${this.get('ghostPaths.adminRoot')}${url}`; 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; link.onerror = reject;
if (alternate) {
link.title = key;
}
$('head').append($(link)); $('head').append($(link));
}); });
} }

View file

@ -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() { function addTestFlag() {
FeatureService.reopen({ FeatureService.reopen({
testFlag: feature('testFlag') testFlag: feature('testFlag'),
testUserFlag: feature('testUserFlag', true)
}); });
} }
@ -64,82 +95,118 @@ describe('Integration: Service: feature', function () {
server.shutdown(); server.shutdown();
}); });
it('loads labs settings correctly', function (done) { it('loads labs and user settings correctly', function () {
stubSettings(server, {testFlag: true}); stubSettings(server, {testFlag: true});
stubUser(server, {testUserFlag: true});
addTestFlag(); addTestFlag();
let service = this.subject(); let service = this.subject();
service.fetch().then(() => { return service.fetch().then(() => {
expect(service.get('testFlag')).to.be.true; 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}); stubSettings(server, {testFlag: false});
stubUser(server, {});
addTestFlag(); addTestFlag();
let service = this.subject(); let service = this.subject();
service.get('config').set('testFlag', false); service.get('config').set('testFlag', false);
service.fetch().then(() => { return service.fetch().then(() => {
expect(service.get('labs.testFlag')).to.be.false; expect(service.get('labs.testFlag')).to.be.false;
expect(service.get('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}); stubSettings(server, {testFlag: false});
stubUser(server, {});
addTestFlag(); addTestFlag();
let service = this.subject(); let service = this.subject();
service.get('config').set('testFlag', true); service.get('config').set('testFlag', true);
service.fetch().then(() => { return service.fetch().then(() => {
expect(service.get('labs.testFlag')).to.be.false; expect(service.get('labs.testFlag')).to.be.false;
expect(service.get('testFlag')).to.be.true; 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}); stubSettings(server, {testFlag: true});
stubUser(server, {});
addTestFlag(); addTestFlag();
let service = this.subject(); let service = this.subject();
service.get('config').set('testFlag', false); service.get('config').set('testFlag', false);
service.fetch().then(() => { return service.fetch().then(() => {
expect(service.get('labs.testFlag')).to.be.true; expect(service.get('labs.testFlag')).to.be.true;
expect(service.get('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}); stubSettings(server, {testFlag: true});
stubUser(server, {});
addTestFlag(); addTestFlag();
let service = this.subject(); let service = this.subject();
service.get('config').set('testFlag', true); service.get('config').set('testFlag', true);
service.fetch().then(() => { return service.fetch().then(() => {
expect(service.get('labs.testFlag')).to.be.true; expect(service.get('labs.testFlag')).to.be.true;
expect(service.get('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}); stubSettings(server, {testFlag: false});
stubUser(server, {testUserFlag: false});
addTestFlag(); addTestFlag();
let service = this.subject(); let service = this.subject();
service.get('config').set('testFlag', false); service.get('config').set('testFlag', false);
service.fetch().then(() => { return service.fetch().then(() => {
expect(service.get('testFlag')).to.be.false; expect(service.get('testFlag')).to.be.false;
run(() => { run(() => {
@ -149,19 +216,42 @@ describe('Integration: Service: feature', function () {
return wait().then(() => { return wait().then(() => {
expect(server.handlers[1].numberOfCalls).to.equal(1); expect(server.handlers[1].numberOfCalls).to.equal(1);
expect(service.get('testFlag')).to.be.true; 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); stubSettings(server, {testFlag: false}, false);
stubUser(server, {});
addTestFlag(); addTestFlag();
let service = this.subject(); let service = this.subject();
service.get('config').set('testFlag', false); service.get('config').set('testFlag', false);
service.fetch().then(() => { return service.fetch().then(() => {
expect(service.get('testFlag')).to.be.false; expect(service.get('testFlag')).to.be.false;
run(() => { run(() => {
@ -180,19 +270,51 @@ describe('Integration: Service: feature', function () {
).to.equal(1); ).to.equal(1);
expect(service.get('testFlag')).to.be.false; 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); stubSettings(server, {testFlag: false}, true, false);
stubUser(server, {});
addTestFlag(); addTestFlag();
let service = this.subject(); let service = this.subject();
service.get('config').set('testFlag', false); service.get('config').set('testFlag', false);
service.fetch().then(() => { return service.fetch().then(() => {
expect(service.get('testFlag')).to.be.false; expect(service.get('testFlag')).to.be.false;
run(() => { run(() => {
@ -205,7 +327,6 @@ describe('Integration: Service: feature', function () {
// ensure validation is happening before the API is hit // ensure validation is happening before the API is hit
expect(server.handlers[1].numberOfCalls).to.equal(0); expect(server.handlers[1].numberOfCalls).to.equal(0);
expect(service.get('testFlag')).to.be.false; expect(service.get('testFlag')).to.be.false;
done();
}); });
}); });
}); });