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"}}" />
<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"}}
</head>

View file

@ -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() {

View file

@ -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}`);
});
}
});

View file

@ -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));
});
}

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() {
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();
});
});
});