mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 14:43:08 +03:00
"400 Version Mismatch" error handling
refs https://github.com/TryGhost/Ghost/issues/6949 Handle version mismatch errors by: - displaying an alert asking the user to copy any data and refresh - disabling navigation so that unsaved data is not accidentally lost Detailed changes: - add `error` action to application route for global route-based error handling - remove 404-handler mixin, move logic into app route error handler - update `.catch` in validation-engine so that promises are rejected with the original error objects - add `VersionMismatchError` and `isVersionMismatchError` to ajax service - add `upgrade-status` service - has a method to trigger the alert and toggle the "upgrade required" mode - is injected into all routes by default so that it can be checked before transitioning - add `Route` override - updates the `willTransition` hook to check the `upgrade-status` service and abort the transition if we're in "upgrade required" mode - update notifications `showAPIError` method to handle version mismatch errors - update any areas where we were catching ajax errors manually so that the version mismatch error handling is obeyed - fix redirect tests in editor acceptance test - fix mirage's handling of 404s for unknown posts in get post requests - adjust alert z-index to to appear above modal backgrounds
This commit is contained in:
parent
bfe542b27d
commit
b4cdc85a59
@ -2,6 +2,7 @@ import Ember from 'ember';
|
||||
import Application from 'ember-application';
|
||||
import Resolver from './resolver';
|
||||
import loadInitializers from 'ember-load-initializers';
|
||||
import 'ghost-admin/utils/route';
|
||||
import 'ghost-admin/utils/link-component';
|
||||
import 'ghost-admin/utils/text-field';
|
||||
import config from './config/environment';
|
||||
|
@ -7,6 +7,7 @@ import run from 'ember-runloop';
|
||||
|
||||
import { invoke, invokeAction } from 'ember-invoke-action';
|
||||
import {
|
||||
isVersionMismatchError,
|
||||
isRequestEntityTooLargeError,
|
||||
isUnsupportedMediaTypeError
|
||||
} from 'ghost-admin/services/ajax';
|
||||
@ -28,6 +29,7 @@ export default Component.extend({
|
||||
uploadPercentage: 0,
|
||||
|
||||
ajax: injectService(),
|
||||
notifications: injectService(),
|
||||
|
||||
formData: computed('file', function () {
|
||||
let paramName = this.get('paramName');
|
||||
@ -128,6 +130,10 @@ export default Component.extend({
|
||||
_uploadFailed(error) {
|
||||
let message;
|
||||
|
||||
if (isVersionMismatchError(error)) {
|
||||
this.get('notifications').showAPIError(error);
|
||||
}
|
||||
|
||||
if (isUnsupportedMediaTypeError(error)) {
|
||||
message = 'The file type you uploaded is not supported.';
|
||||
} else if (isRequestEntityTooLargeError(error)) {
|
||||
|
@ -5,10 +5,12 @@ import {htmlSafe} from 'ember-string';
|
||||
import {isBlank} from 'ember-utils';
|
||||
import run from 'ember-runloop';
|
||||
|
||||
import {invokeAction} from 'ember-invoke-action';
|
||||
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
||||
import {
|
||||
isRequestEntityTooLargeError,
|
||||
isUnsupportedMediaTypeError
|
||||
isUnsupportedMediaTypeError,
|
||||
isVersionMismatchError
|
||||
} from 'ghost-admin/services/ajax';
|
||||
|
||||
export default Component.extend({
|
||||
@ -29,6 +31,7 @@ export default Component.extend({
|
||||
|
||||
ajax: injectService(),
|
||||
config: injectService(),
|
||||
notifications: injectService(),
|
||||
|
||||
// TODO: this wouldn't be necessary if the server could accept direct
|
||||
// file uploads
|
||||
@ -114,13 +117,11 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
uploadStarted() {
|
||||
if (typeof this.attrs.uploadStarted === 'function') {
|
||||
this.attrs.uploadStarted();
|
||||
}
|
||||
_uploadStarted() {
|
||||
invokeAction(this, 'uploadStarted');
|
||||
},
|
||||
|
||||
uploadProgress(event) {
|
||||
_uploadProgress(event) {
|
||||
if (event.lengthComputable) {
|
||||
run(() => {
|
||||
let percentage = Math.round((event.loaded / event.total) * 100);
|
||||
@ -129,21 +130,24 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
uploadFinished() {
|
||||
if (typeof this.attrs.uploadFinished === 'function') {
|
||||
this.attrs.uploadFinished();
|
||||
}
|
||||
_uploadFinished() {
|
||||
invokeAction(this, 'uploadFinished');
|
||||
},
|
||||
|
||||
uploadSuccess(response) {
|
||||
_uploadSuccess(response) {
|
||||
this.set('url', response);
|
||||
this.send('saveUrl');
|
||||
this.send('reset');
|
||||
invokeAction(this, 'uploadSuccess', response);
|
||||
},
|
||||
|
||||
uploadFailed(error) {
|
||||
_uploadFailed(error) {
|
||||
let message;
|
||||
|
||||
if (isVersionMismatchError(error)) {
|
||||
this.get('notifications').showAPIError(error);
|
||||
}
|
||||
|
||||
if (isUnsupportedMediaTypeError(error)) {
|
||||
message = 'The image type you uploaded is not supported. Please use .PNG, .JPG, .GIF, .SVG.';
|
||||
} else if (isRequestEntityTooLargeError(error)) {
|
||||
@ -155,6 +159,7 @@ export default Component.extend({
|
||||
}
|
||||
|
||||
this.set('failureMessage', message);
|
||||
invokeAction(this, 'uploadFailed', error);
|
||||
},
|
||||
|
||||
generateRequest() {
|
||||
@ -162,7 +167,7 @@ export default Component.extend({
|
||||
let formData = this.get('formData');
|
||||
let url = `${ghostPaths().apiRoot}/uploads/`;
|
||||
|
||||
this.uploadStarted();
|
||||
this._uploadStarted();
|
||||
|
||||
ajax.post(url, {
|
||||
data: formData,
|
||||
@ -173,18 +178,18 @@ export default Component.extend({
|
||||
let xhr = new window.XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
this.uploadProgress(event);
|
||||
this._uploadProgress(event);
|
||||
}, false);
|
||||
|
||||
return xhr;
|
||||
}
|
||||
}).then((response) => {
|
||||
let url = JSON.parse(response);
|
||||
this.uploadSuccess(url);
|
||||
this._uploadSuccess(url);
|
||||
}).catch((error) => {
|
||||
this.uploadFailed(error);
|
||||
this._uploadFailed(error);
|
||||
}).finally(() => {
|
||||
this.uploadFinished();
|
||||
this._uploadFinished();
|
||||
});
|
||||
},
|
||||
|
||||
@ -198,10 +203,7 @@ export default Component.extend({
|
||||
|
||||
onInput(url) {
|
||||
this.set('url', url);
|
||||
|
||||
if (typeof this.attrs.onInput === 'function') {
|
||||
this.attrs.onInput(url);
|
||||
}
|
||||
invokeAction(this, 'onInput', url);
|
||||
},
|
||||
|
||||
reset() {
|
||||
@ -212,16 +214,14 @@ export default Component.extend({
|
||||
switchForm(formType) {
|
||||
this.set('formType', formType);
|
||||
|
||||
if (typeof this.attrs.formChanged === 'function') {
|
||||
run.scheduleOnce('afterRender', this, function () {
|
||||
this.attrs.formChanged(formType);
|
||||
});
|
||||
}
|
||||
run.scheduleOnce('afterRender', this, function () {
|
||||
invokeAction(this, 'formChanged', formType);
|
||||
});
|
||||
},
|
||||
|
||||
saveUrl() {
|
||||
let url = this.get('url');
|
||||
this.attrs.update(url);
|
||||
invokeAction(this, 'update', url);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -39,6 +39,7 @@ export default Component.extend({
|
||||
_store: injectService('store'),
|
||||
_routing: injectService('-routing'),
|
||||
ajax: injectService(),
|
||||
notifications: injectService(),
|
||||
|
||||
refreshContent() {
|
||||
let promises = [];
|
||||
@ -91,7 +92,6 @@ export default Component.extend({
|
||||
let content = this.get('content');
|
||||
|
||||
return this.get('ajax').request(postsUrl, {data: postsQuery}).then((posts) => {
|
||||
|
||||
content.pushObjects(posts.posts.map((post) => {
|
||||
return {
|
||||
id: `post.${post.id}`,
|
||||
@ -99,6 +99,8 @@ export default Component.extend({
|
||||
category: post.page ? 'Pages' : 'Posts'
|
||||
};
|
||||
}));
|
||||
}).catch((error) => {
|
||||
this.get('notifications').showAPIError(error, {key: 'search.loadPosts.error'});
|
||||
});
|
||||
},
|
||||
|
||||
@ -116,6 +118,8 @@ export default Component.extend({
|
||||
category: 'Users'
|
||||
};
|
||||
}));
|
||||
}).catch((error) => {
|
||||
this.get('notifications').showAPIError(error, {key: 'search.loadUsers.error'});
|
||||
});
|
||||
},
|
||||
|
||||
@ -133,6 +137,8 @@ export default Component.extend({
|
||||
category: 'Tags'
|
||||
};
|
||||
}));
|
||||
}).catch((error) => {
|
||||
this.get('notifications').showAPIError(error, {key: 'search.loadTags.error'});
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import RSVP from 'rsvp';
|
||||
import injectService from 'ember-service/inject';
|
||||
import {A as emberA} from 'ember-array/utils';
|
||||
import {A as emberA, isEmberArray} from 'ember-array/utils';
|
||||
import run from 'ember-runloop';
|
||||
import ModalComponent from 'ghost-admin/components/modals/base';
|
||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||
@ -113,7 +113,11 @@ export default ModalComponent.extend(ValidationEngine, {
|
||||
}
|
||||
}).catch((errors) => {
|
||||
newUser.deleteRecord();
|
||||
notifications.showErrors(errors, {key: 'invite.send'});
|
||||
if (isEmberArray(errors)) {
|
||||
notifications.showErrors(errors, {key: 'invite.send'});
|
||||
} else {
|
||||
notifications.showAPIError(errors, {key: 'invite.send'});
|
||||
}
|
||||
}).finally(() => {
|
||||
this.send('closeModal');
|
||||
});
|
||||
|
@ -16,12 +16,26 @@ export default ModalComponent.extend({
|
||||
|
||||
confirmAction().then(() => {
|
||||
this.send('closeModal');
|
||||
}).catch((errors) => {
|
||||
let [error] = errors;
|
||||
if (error && error.match(/email/i)) {
|
||||
this.get('model.errors').add('email', error);
|
||||
this.get('model.hasValidated').pushObject('email');
|
||||
}).catch((error) => {
|
||||
// TODO: server-side validation errors should be serialized
|
||||
// properly so that errors are added to the model's errors
|
||||
// property
|
||||
if (error && error.isAdapterError) {
|
||||
let [firstError] = error.errors;
|
||||
let {message, errorType} = firstError;
|
||||
|
||||
if (errorType === 'ValidationError') {
|
||||
if (message && message.match(/email/i)) {
|
||||
this.get('model.errors').add('email', message);
|
||||
this.get('model.hasValidated').pushObject('email');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is a route action so it should bubble up to the global
|
||||
// error handler
|
||||
throw error;
|
||||
}).finally(() => {
|
||||
if (!this.get('isDestroying') && !this.get('isDestroyed')) {
|
||||
this.set('submitting', false);
|
||||
|
@ -4,6 +4,7 @@ import injectService from 'ember-service/inject';
|
||||
import {htmlSafe} from 'ember-string';
|
||||
import ModalComponent from 'ghost-admin/components/modals/base';
|
||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
|
||||
|
||||
export default ModalComponent.extend(ValidationEngine, {
|
||||
validationType: 'signin',
|
||||
@ -49,6 +50,9 @@ export default ModalComponent.extend(ValidationEngine, {
|
||||
}).catch((error) => {
|
||||
if (error && error.errors) {
|
||||
error.errors.forEach((err) => {
|
||||
if (isVersionMismatchError(err)) {
|
||||
return this.get('notifications').showAPIError(error);
|
||||
}
|
||||
err.message = htmlSafe(err.message);
|
||||
});
|
||||
|
||||
|
@ -16,6 +16,7 @@ import {parseDateString} from 'ghost-admin/utils/date-formatting';
|
||||
import SettingsMenuMixin from 'ghost-admin/mixins/settings-menu-controller';
|
||||
import boundOneWay from 'ghost-admin/utils/bound-one-way';
|
||||
import isNumber from 'ghost-admin/utils/isNumber';
|
||||
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
|
||||
|
||||
const {ArrayProxy, Handlebars, PromiseProxyMixin} = Ember;
|
||||
|
||||
@ -74,10 +75,13 @@ export default Controller.extend(SettingsMenuMixin, {
|
||||
if (!isBlank(slug)) {
|
||||
this.set(destination, slug);
|
||||
}
|
||||
}).catch(() => {
|
||||
}).catch((error) => {
|
||||
// Nothing to do (would be nice to log this somewhere though),
|
||||
// but a rejected promise needs to be handled here so that a resolved
|
||||
// promise is returned.
|
||||
if (isVersionMismatchError(error)) {
|
||||
this.get('notifications').showAPIError(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -181,6 +185,9 @@ export default Controller.extend(SettingsMenuMixin, {
|
||||
}),
|
||||
|
||||
showErrors(errors) {
|
||||
if (isVersionMismatchError(errors)) {
|
||||
return this.get('notifications').showAPIError(errors);
|
||||
}
|
||||
errors = isEmberArray(errors) ? errors : [errors];
|
||||
this.get('notifications').showErrors(errors);
|
||||
},
|
||||
|
@ -33,6 +33,7 @@ export default Controller.extend({
|
||||
notifications.showAlert('Check your slack channel test message.', {type: 'info', key: 'slack-test.send.success'});
|
||||
}).catch((error) => {
|
||||
notifications.showAPIError(error, {key: 'slack-test:send'});
|
||||
throw error;
|
||||
});
|
||||
}).catch(() => {
|
||||
// noop - error already handled in .save
|
||||
@ -60,7 +61,9 @@ export default Controller.extend({
|
||||
this.set('isSaving', true);
|
||||
|
||||
return settings.save().catch((err) => {
|
||||
this.get('notifications').showErrors(err);
|
||||
if (err && err.isAdapterError) {
|
||||
this.get('notifications').showAPIError(err);
|
||||
}
|
||||
throw err;
|
||||
}).finally(() => {
|
||||
this.set('isSaving', false);
|
||||
|
@ -38,7 +38,7 @@ export default Controller.extend(SettingsSaveMixin, {
|
||||
|
||||
return RSVP.all(validationPromises).then(() => {
|
||||
return this.get('model').save().catch((err) => {
|
||||
notifications.showErrors(err);
|
||||
notifications.showAPIError(err);
|
||||
});
|
||||
}).catch(() => {
|
||||
// TODO: noop - needed to satisfy spinner button
|
||||
|
@ -4,6 +4,10 @@ import injectService from 'ember-service/inject';
|
||||
import injectController from 'ember-controller/inject';
|
||||
import {isEmberArray} from 'ember-array/utils';
|
||||
|
||||
import {
|
||||
VersionMismatchError,
|
||||
isVersionMismatchError
|
||||
} from 'ghost-admin/services/ajax';
|
||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||
|
||||
export default Controller.extend(ValidationEngine, {
|
||||
@ -31,6 +35,14 @@ export default Controller.extend(ValidationEngine, {
|
||||
this.toggleProperty('loggingIn');
|
||||
|
||||
if (error && error.errors) {
|
||||
// we don't get back an ember-data/ember-ajax error object
|
||||
// back so we need to pass in a null status in order to
|
||||
// test against the payload
|
||||
if (isVersionMismatchError(null, error)) {
|
||||
let versionMismatchError = new VersionMismatchError(error);
|
||||
return this.get('notifications').showAPIError(versionMismatchError);
|
||||
}
|
||||
|
||||
error.errors.forEach((err) => {
|
||||
err.message = err.message.htmlSafe();
|
||||
});
|
||||
@ -62,12 +74,8 @@ export default Controller.extend(ValidationEngine, {
|
||||
this.validate({property: 'signin'}).then(() => {
|
||||
this.toggleProperty('loggingIn');
|
||||
this.send('authenticate');
|
||||
}).catch((error) => {
|
||||
if (error) {
|
||||
this.get('notifications').showAPIError(error, {key: 'signin.authenticate'});
|
||||
} else {
|
||||
this.set('flowErrors', 'Please fill out the form to sign in.');
|
||||
}
|
||||
}).catch(() => {
|
||||
this.set('flowErrors', 'Please fill out the form to sign in.');
|
||||
});
|
||||
},
|
||||
|
||||
@ -89,11 +97,15 @@ export default Controller.extend(ValidationEngine, {
|
||||
}).then(() => {
|
||||
this.toggleProperty('submitting');
|
||||
notifications.showAlert('Please check your email for instructions.', {type: 'info', key: 'forgot-password.send.success'});
|
||||
}).catch((resp) => {
|
||||
}).catch((error) => {
|
||||
this.toggleProperty('submitting');
|
||||
|
||||
if (resp && resp.errors && isEmberArray(resp.errors)) {
|
||||
let [{message}] = resp.errors;
|
||||
if (isVersionMismatchError(error)) {
|
||||
return notifications.showAPIError(error);
|
||||
}
|
||||
|
||||
if (error && error.errors && isEmberArray(error.errors)) {
|
||||
let [{message}] = error.errors;
|
||||
|
||||
this.set('flowErrors', message);
|
||||
|
||||
@ -101,7 +113,7 @@ export default Controller.extend(ValidationEngine, {
|
||||
this.get('model.errors').add('identification', '');
|
||||
}
|
||||
} else {
|
||||
notifications.showAPIError(resp, {defaultErrorText: 'There was a problem with the reset, please try again.', key: 'forgot-password.send'});
|
||||
notifications.showAPIError(error, {defaultErrorText: 'There was a problem with the reset, please try again.', key: 'forgot-password.send'});
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
|
@ -4,6 +4,7 @@ import injectService from 'ember-service/inject';
|
||||
import {isEmberArray} from 'ember-array/utils';
|
||||
|
||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
|
||||
|
||||
const {Promise} = RSVP;
|
||||
|
||||
@ -74,13 +75,16 @@ export default Controller.extend(ValidationEngine, {
|
||||
}).catch((resp) => {
|
||||
notifications.showAPIError(resp, {key: 'signup.complete'});
|
||||
});
|
||||
}).catch((resp) => {
|
||||
}).catch((error) => {
|
||||
this.toggleProperty('submitting');
|
||||
|
||||
if (resp && resp.errors && isEmberArray(resp.errors)) {
|
||||
this.set('flowErrors', resp.errors[0].message);
|
||||
if (error && error.errors && isEmberArray(error.errors)) {
|
||||
if (isVersionMismatchError(error)) {
|
||||
notifications.showAPIError(error);
|
||||
}
|
||||
this.set('flowErrors', error.errors[0].message);
|
||||
} else {
|
||||
notifications.showAPIError(resp, {key: 'signup.complete'});
|
||||
notifications.showAPIError(error, {key: 'signup.complete'});
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
|
@ -141,8 +141,10 @@ export default Controller.extend({
|
||||
|
||||
return model;
|
||||
}).catch((errors) => {
|
||||
if (errors) {
|
||||
if (isEmberArray(errors)) {
|
||||
this.get('notifications').showErrors(errors, {key: 'user.update'});
|
||||
} else {
|
||||
this.get('notifications').showAPIError(errors);
|
||||
}
|
||||
|
||||
this.toggleProperty('submitting');
|
||||
|
8
ghost/admin/app/initializers/upgrade-status.js
Normal file
8
ghost/admin/app/initializers/upgrade-status.js
Normal file
@ -0,0 +1,8 @@
|
||||
export function initialize(application) {
|
||||
application.inject('route', 'upgradeStatus', 'service:upgrade-status');
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'upgrade-status',
|
||||
initialize
|
||||
};
|
@ -8,6 +8,16 @@ const {
|
||||
String: {dasherize}
|
||||
} = Ember;
|
||||
|
||||
/* jshint unused:false */
|
||||
function versionMismatchResponse() {
|
||||
return new Mirage.Response(400, {}, {
|
||||
errors: [{
|
||||
errorType: 'VersionMismatchError'
|
||||
}]
|
||||
});
|
||||
}
|
||||
/* jshint unused:true */
|
||||
|
||||
function paginatedResponse(modelName, allModels, request) {
|
||||
let page = +request.queryParams.page || 1;
|
||||
let limit = request.queryParams.limit || 15;
|
||||
@ -122,7 +132,7 @@ export default function () {
|
||||
this.timing = 400; // delay for each request, automatically set to 0 during testing
|
||||
|
||||
// Mock endpoints here to override real API requests during development
|
||||
mockSubscribers(this);
|
||||
// this.put('/posts/:id/', versionMismatchResponse);
|
||||
|
||||
// keep this line, it allows all other API requests to hit the real server
|
||||
this.passthrough();
|
||||
@ -211,13 +221,20 @@ export function testConfig() {
|
||||
return response;
|
||||
});
|
||||
|
||||
this.get('/posts/:id', function (db, request) {
|
||||
this.get('/posts/:id/', function (db, request) {
|
||||
let {id} = request.params;
|
||||
let post = db.posts.find(id);
|
||||
|
||||
return {
|
||||
posts: [post]
|
||||
};
|
||||
if (!post) {
|
||||
return new Mirage.Response(404, {}, {
|
||||
errors: [{
|
||||
errorType: 'NotFoundError',
|
||||
message: 'Post not found.'
|
||||
}]
|
||||
});
|
||||
} else {
|
||||
return {posts: [post]};
|
||||
}
|
||||
});
|
||||
|
||||
this.put('/posts/:id/', function (db, request) {
|
||||
|
@ -1,23 +0,0 @@
|
||||
import Mixin from 'ember-metal/mixin';
|
||||
|
||||
export default Mixin.create({
|
||||
actions: {
|
||||
error(error, transition) {
|
||||
if (error.errors && error.errors[0].errorType === 'NotFoundError') {
|
||||
transition.abort();
|
||||
|
||||
let routeInfo = transition.handlerInfos[transition.handlerInfos.length - 1];
|
||||
let router = this.get('router');
|
||||
let params = [];
|
||||
|
||||
for (let key of Object.keys(routeInfo.params)) {
|
||||
params.push(routeInfo.params[key]);
|
||||
}
|
||||
|
||||
return this.transitionTo('error404', router.generate(routeInfo.name, ...params).replace('/ghost/', '').replace(/^\//g, ''));
|
||||
}
|
||||
|
||||
return this._super(...arguments);
|
||||
}
|
||||
}
|
||||
});
|
@ -403,10 +403,19 @@ export default Mixin.create({
|
||||
}
|
||||
return model;
|
||||
});
|
||||
}).catch((errors) => {
|
||||
}).catch((error) => {
|
||||
// re-throw if we have a general server error
|
||||
// TODO: use isValidationError(error) once we have
|
||||
// ember-ajax/ember-data integration
|
||||
if (error && error.errors && error.errors[0].errorType !== 'ValidationError') {
|
||||
this.toggleProperty('submitting');
|
||||
this.send('error', error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.silent) {
|
||||
errors = errors || this.get('model.errors.messages');
|
||||
this.showErrorAlert(prevStatus, this.get('model.status'), errors);
|
||||
error = error || this.get('model.errors.messages');
|
||||
this.showErrorAlert(prevStatus, this.get('model.status'), error);
|
||||
}
|
||||
|
||||
this.set('model.status', prevStatus);
|
||||
|
@ -49,6 +49,10 @@ export default Mixin.create(styleBody, ShortcutsRoute, {
|
||||
let deletedWithoutChanges,
|
||||
fromNewToEdit;
|
||||
|
||||
if (this.get('upgradeStatus.isRequired')) {
|
||||
return this._super(...arguments);
|
||||
}
|
||||
|
||||
// if a save is in-flight we don't know whether or not it's safe to leave
|
||||
// so we abort the transition and retry after the save has completed.
|
||||
if (state.isSaving) {
|
||||
|
@ -4,8 +4,6 @@ import computed from 'ember-computed';
|
||||
import RSVP from 'rsvp';
|
||||
import injectService from 'ember-service/inject';
|
||||
|
||||
import getRequestErrorMessage from 'ghost-admin/utils/ajax';
|
||||
|
||||
let defaultPaginationSettings = {
|
||||
page: 1,
|
||||
limit: 15
|
||||
@ -40,25 +38,11 @@ export default Mixin.create({
|
||||
this.set('paginationMeta', {});
|
||||
},
|
||||
|
||||
/**
|
||||
* Takes an ajax response, concatenates any error messages, then generates an error notification.
|
||||
* @param {jqXHR} response The jQuery ajax reponse object.
|
||||
* @return
|
||||
*/
|
||||
reportLoadError(response) {
|
||||
let 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', key: 'pagination.load.failed'});
|
||||
reportLoadError(error) {
|
||||
this.get('notifications').showAPIError(error, {key: 'pagination.load.failed'});
|
||||
},
|
||||
|
||||
loadFirstPage() {
|
||||
loadFirstPage(transition) {
|
||||
let paginationSettings = this.get('paginationSettings');
|
||||
let modelName = this.get('paginationModel');
|
||||
|
||||
@ -69,8 +53,14 @@ export default Mixin.create({
|
||||
return this.get('store').query(modelName, paginationSettings).then((results) => {
|
||||
this.set('paginationMeta', results.meta);
|
||||
return results;
|
||||
}).catch((response) => {
|
||||
this.reportLoadError(response);
|
||||
}).catch((error) => {
|
||||
// if we have a transition we're executing in a route hook so we
|
||||
// want to throw in order to trigger the global error handler
|
||||
if (transition) {
|
||||
throw error;
|
||||
} else {
|
||||
this.reportLoadError(error);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.set('isLoading', false);
|
||||
});
|
||||
@ -99,8 +89,8 @@ export default Mixin.create({
|
||||
return store.query(modelName, paginationSettings).then((results) => {
|
||||
this.set('paginationMeta', results.meta);
|
||||
return results;
|
||||
}).catch((response) => {
|
||||
this.reportLoadError(response);
|
||||
}).catch((error) => {
|
||||
this.reportLoadError(error);
|
||||
}).finally(() => {
|
||||
this.set('isLoading', false);
|
||||
});
|
||||
|
@ -3,7 +3,6 @@ import RSVP from 'rsvp';
|
||||
import {A as emberA, isEmberArray} from 'ember-array/utils';
|
||||
import DS from 'ember-data';
|
||||
import Model from 'ember-data/model';
|
||||
import getRequestErrorMessage from 'ghost-admin/utils/ajax';
|
||||
|
||||
import InviteUserValidator from 'ghost-admin/validators/invite-user';
|
||||
import NavItemValidator from 'ghost-admin/validators/nav-item';
|
||||
@ -146,8 +145,7 @@ export default Mixin.create({
|
||||
}).catch((result) => {
|
||||
// server save failed or validator type doesn't exist
|
||||
if (result && !isEmberArray(result)) {
|
||||
// return the array of errors from the server
|
||||
result = getRequestErrorMessage(result);
|
||||
throw result;
|
||||
}
|
||||
|
||||
return RSVP.reject(result);
|
||||
|
@ -2,6 +2,7 @@ import Route from 'ember-route';
|
||||
import {htmlSafe} from 'ember-string';
|
||||
import injectService from 'ember-service/inject';
|
||||
import run from 'ember-runloop';
|
||||
import {isEmberArray} from 'ember-array/utils';
|
||||
|
||||
import AuthConfiguration from 'ember-simple-auth/configuration';
|
||||
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
|
||||
@ -146,6 +147,46 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
|
||||
},
|
||||
|
||||
// noop default for unhandled save (used from shortcuts)
|
||||
save: K
|
||||
save: K,
|
||||
|
||||
error(error, transition) {
|
||||
if (error && isEmberArray(error.errors)) {
|
||||
switch (error.errors[0].errorType) {
|
||||
|
||||
case 'NotFoundError':
|
||||
if (transition) {
|
||||
transition.abort();
|
||||
}
|
||||
|
||||
let routeInfo = transition.handlerInfos[transition.handlerInfos.length - 1];
|
||||
let router = this.get('router');
|
||||
let params = [];
|
||||
|
||||
for (let key of Object.keys(routeInfo.params)) {
|
||||
params.push(routeInfo.params[key]);
|
||||
}
|
||||
|
||||
return this.transitionTo('error404', router.generate(routeInfo.name, ...params).replace('/ghost/', '').replace(/^\//g, ''));
|
||||
|
||||
case 'VersionMismatchError':
|
||||
if (transition) {
|
||||
transition.abort();
|
||||
}
|
||||
|
||||
this.get('upgradeStatus').requireUpgrade();
|
||||
return false;
|
||||
|
||||
default:
|
||||
this.get('notifications').showAPIError(error);
|
||||
// don't show the 500 page if we weren't navigating
|
||||
if (!transition) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to 500 error page
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,11 +1,10 @@
|
||||
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import base from 'ghost-admin/mixins/editor-base-route';
|
||||
import NotFoundHandler from 'ghost-admin/mixins/404-handler';
|
||||
import isNumber from 'ghost-admin/utils/isNumber';
|
||||
import isFinite from 'ghost-admin/utils/isFinite';
|
||||
|
||||
export default AuthenticatedRoute.extend(base, NotFoundHandler, {
|
||||
export default AuthenticatedRoute.extend(base, {
|
||||
titleToken: 'Editor',
|
||||
|
||||
beforeModel(transition) {
|
||||
|
@ -1,10 +1,9 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import ShortcutsRoute from 'ghost-admin/mixins/shortcuts-route';
|
||||
import NotFoundHandler from 'ghost-admin/mixins/404-handler';
|
||||
import isNumber from 'ghost-admin/utils/isNumber';
|
||||
import isFinite from 'ghost-admin/utils/isFinite';
|
||||
|
||||
export default AuthenticatedRoute.extend(ShortcutsRoute, NotFoundHandler, {
|
||||
export default AuthenticatedRoute.extend(ShortcutsRoute, {
|
||||
model(params) {
|
||||
let post,
|
||||
postId,
|
||||
|
@ -29,8 +29,8 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, PaginationMixin, S
|
||||
.then(this.transitionAuthor());
|
||||
},
|
||||
|
||||
model() {
|
||||
return this.loadFirstPage().then(() => {
|
||||
model(params, transition) {
|
||||
return this.loadFirstPage(transition).then(() => {
|
||||
return this.store.filter('tag', (tag) => {
|
||||
return !tag.get('isNew');
|
||||
});
|
||||
|
@ -1,8 +1,7 @@
|
||||
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import NotFoundHandler from 'ghost-admin/mixins/404-handler';
|
||||
|
||||
export default AuthenticatedRoute.extend(NotFoundHandler, {
|
||||
export default AuthenticatedRoute.extend({
|
||||
|
||||
model(params) {
|
||||
return this.store.queryRecord('tag', {slug: params.tag_slug});
|
||||
|
@ -15,6 +15,7 @@ let DownloadCountPoller = EmberObject.extend({
|
||||
runId: null,
|
||||
|
||||
ajax: AjaxService.create(),
|
||||
notifications: injectService(),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
@ -44,8 +45,9 @@ let DownloadCountPoller = EmberObject.extend({
|
||||
}
|
||||
|
||||
this.set('count', count);
|
||||
}).catch(() => {
|
||||
}).catch((error) => {
|
||||
this.set('count', '');
|
||||
this.get('notifications').showAPIError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -2,9 +2,8 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
|
||||
import styleBody from 'ghost-admin/mixins/style-body';
|
||||
import NotFoundHandler from 'ghost-admin/mixins/404-handler';
|
||||
|
||||
export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, NotFoundHandler, {
|
||||
export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
|
||||
titleToken: 'Team - User',
|
||||
|
||||
classNames: ['team-view-user'],
|
||||
|
@ -1,3 +1,4 @@
|
||||
import get from 'ember-metal/get';
|
||||
import computed from 'ember-computed';
|
||||
import injectService from 'ember-service/inject';
|
||||
import {isEmberArray} from 'ember-array/utils';
|
||||
@ -5,34 +6,58 @@ import AjaxService from 'ember-ajax/services/ajax';
|
||||
import {AjaxError, isAjaxError} from 'ember-ajax/errors';
|
||||
import config from 'ghost-admin/config/environment';
|
||||
|
||||
/* Version mismatch error */
|
||||
|
||||
export function VersionMismatchError(errors) {
|
||||
AjaxError.call(this, errors, 'API server is running a newer version of Ghost, please upgrade.');
|
||||
}
|
||||
|
||||
VersionMismatchError.prototype = Object.create(AjaxError.prototype);
|
||||
|
||||
export function isVersionMismatchError(errorOrStatus, payload) {
|
||||
if (isAjaxError(errorOrStatus)) {
|
||||
return errorOrStatus instanceof VersionMismatchError;
|
||||
} else if (errorOrStatus && get(errorOrStatus, 'isAdapterError')) {
|
||||
return get(errorOrStatus, 'errors.firstObject.errorType') === 'VersionMismatchError';
|
||||
} else {
|
||||
return get(payload || {}, 'errors.firstObject.errorType') === 'VersionMismatchError';
|
||||
}
|
||||
}
|
||||
|
||||
/* Request entity too large error */
|
||||
|
||||
export function RequestEntityTooLargeError(errors) {
|
||||
AjaxError.call(this, errors, 'Request was rejected because it\'s larger than the maximum file size the server allows');
|
||||
}
|
||||
|
||||
RequestEntityTooLargeError.prototype = Object.create(AjaxError.prototype);
|
||||
|
||||
export function isRequestEntityTooLargeError(error) {
|
||||
if (isAjaxError(error)) {
|
||||
return error instanceof RequestEntityTooLargeError;
|
||||
export function isRequestEntityTooLargeError(errorOrStatus) {
|
||||
if (isAjaxError(errorOrStatus)) {
|
||||
return errorOrStatus instanceof RequestEntityTooLargeError;
|
||||
} else {
|
||||
return error === 413;
|
||||
return errorOrStatus === 413;
|
||||
}
|
||||
}
|
||||
|
||||
/* Unsupported media type error */
|
||||
|
||||
export function UnsupportedMediaTypeError(errors) {
|
||||
AjaxError.call(this, errors, 'Request was rejected because it contains an unknown or unsupported file type.');
|
||||
}
|
||||
|
||||
UnsupportedMediaTypeError.prototype = Object.create(AjaxError.prototype);
|
||||
|
||||
export function isUnsupportedMediaTypeError(error) {
|
||||
if (isAjaxError(error)) {
|
||||
return error instanceof UnsupportedMediaTypeError;
|
||||
export function isUnsupportedMediaTypeError(errorOrStatus) {
|
||||
if (isAjaxError(errorOrStatus)) {
|
||||
return errorOrStatus instanceof UnsupportedMediaTypeError;
|
||||
} else {
|
||||
return error === 415;
|
||||
return errorOrStatus === 415;
|
||||
}
|
||||
}
|
||||
|
||||
/* end: custom error types */
|
||||
|
||||
export default AjaxService.extend({
|
||||
session: injectService(),
|
||||
|
||||
@ -52,7 +77,9 @@ export default AjaxService.extend({
|
||||
}).volatile(),
|
||||
|
||||
handleResponse(status, headers, payload) {
|
||||
if (this.isRequestEntityTooLargeError(status, headers, payload)) {
|
||||
if (this.isVersionMismatchError(status, headers, payload)) {
|
||||
return new VersionMismatchError(payload.errors);
|
||||
} else if (this.isRequestEntityTooLargeError(status, headers, payload)) {
|
||||
return new RequestEntityTooLargeError(payload.errors);
|
||||
} else if (this.isUnsupportedMediaTypeError(status, headers, payload)) {
|
||||
return new UnsupportedMediaTypeError(payload.errors);
|
||||
@ -79,6 +106,10 @@ export default AjaxService.extend({
|
||||
return this._super(status, headers, payload);
|
||||
},
|
||||
|
||||
isVersionMismatchError(status, headers, payload) {
|
||||
return isVersionMismatchError(status, payload);
|
||||
},
|
||||
|
||||
isRequestEntityTooLargeError(status/*, headers, payload */) {
|
||||
return isRequestEntityTooLargeError(status);
|
||||
},
|
||||
|
@ -65,17 +65,17 @@ export default Service.extend({
|
||||
this.notifyPropertyChange('labs');
|
||||
return this.get(`labs.${key}`);
|
||||
|
||||
}).catch((errors) => {
|
||||
}).catch((error) => {
|
||||
settings.rollbackAttributes();
|
||||
this.notifyPropertyChange('labs');
|
||||
|
||||
// we'll always have an errors object unless we hit a
|
||||
// validation error
|
||||
if (!errors) {
|
||||
if (!error) {
|
||||
throw new EmberError(`Validation of the feature service settings model failed when updating labs.`);
|
||||
}
|
||||
|
||||
this.get('notifications').showErrors(errors);
|
||||
this.get('notifications').showAPIError(error);
|
||||
|
||||
return this.get(`labs.${key}`);
|
||||
});
|
||||
|
@ -3,7 +3,8 @@ import {filter} from 'ember-computed';
|
||||
import {A as emberA, isEmberArray} from 'ember-array/utils';
|
||||
import get from 'ember-metal/get';
|
||||
import set from 'ember-metal/set';
|
||||
import {isAjaxError} from 'ember-ajax/errors';
|
||||
import injectService from 'ember-service/inject';
|
||||
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
|
||||
|
||||
// Notification keys take the form of "noun.verb.message", eg:
|
||||
//
|
||||
@ -18,6 +19,8 @@ export default Service.extend({
|
||||
delayedNotifications: emberA(),
|
||||
content: emberA(),
|
||||
|
||||
upgradeStatus: injectService(),
|
||||
|
||||
alerts: filter('content', function (notification) {
|
||||
let status = get(notification, 'status');
|
||||
return status === 'alert';
|
||||
@ -99,6 +102,22 @@ export default Service.extend({
|
||||
},
|
||||
|
||||
showAPIError(resp, options) {
|
||||
// handle "global" errors
|
||||
if (isVersionMismatchError(resp)) {
|
||||
return this.get('upgradeStatus').requireUpgrade();
|
||||
}
|
||||
|
||||
// loop over Ember Data / ember-ajax errors object
|
||||
if (resp && isEmberArray(resp.errors)) {
|
||||
return resp.errors.forEach((error) => {
|
||||
this._showAPIError(error, options);
|
||||
});
|
||||
}
|
||||
|
||||
this._showAPIError(resp, options);
|
||||
},
|
||||
|
||||
_showAPIError(resp, options) {
|
||||
options = options || {};
|
||||
options.type = options.type || 'error';
|
||||
// TODO: getting keys from the server would be useful here (necessary for i18n)
|
||||
@ -110,12 +129,10 @@ export default Service.extend({
|
||||
|
||||
options.defaultErrorText = options.defaultErrorText || 'There was a problem on the server, please try again.';
|
||||
|
||||
if (isAjaxError(resp)) {
|
||||
resp = resp.errors;
|
||||
}
|
||||
|
||||
if (resp && isEmberArray(resp) && resp.length) { // Array of errors
|
||||
this.showErrors(resp, options);
|
||||
} else if (resp && resp.message) {
|
||||
this.showAlert(resp.message, options);
|
||||
} else if (resp && resp.detail) { // ember-ajax provided error message
|
||||
this.showAlert(resp.detail, options);
|
||||
} else { // text error or no error
|
||||
|
16
ghost/admin/app/services/upgrade-status.js
Normal file
16
ghost/admin/app/services/upgrade-status.js
Normal file
@ -0,0 +1,16 @@
|
||||
import Service from 'ember-service';
|
||||
import injectService from 'ember-service/inject';
|
||||
|
||||
export default Service.extend({
|
||||
isRequired: false,
|
||||
|
||||
notifications: injectService(),
|
||||
|
||||
requireUpgrade() {
|
||||
this.set('isRequired', true);
|
||||
this.get('notifications').showAlert(
|
||||
'Ghost has been upgraded, please copy any unsaved data and refresh the page to continue.',
|
||||
{type: 'error', key: 'api-error.upgrade-required'}
|
||||
);
|
||||
}
|
||||
});
|
@ -130,7 +130,7 @@
|
||||
|
||||
/* Base alert style */
|
||||
.gh-alert {
|
||||
z-index: 1000;
|
||||
z-index: 9999;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -34,9 +34,9 @@
|
||||
url=uploadUrl
|
||||
paramName="subscribersfile"
|
||||
labelText="Select or drag-and-drop a CSV file."
|
||||
uploadStarted=(action 'uploadStarted')
|
||||
uploadFinished=(action 'uploadFinished')
|
||||
uploadSuccess=(action 'uploadSuccess')}}
|
||||
uploadStarted=(action "uploadStarted")
|
||||
uploadFinished=(action "uploadFinished")
|
||||
uploadSuccess=(action "uploadSuccess")}}
|
||||
{{/liquid-if}}
|
||||
</div>
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
{{gh-fullscreen-modal "import-subscribers"
|
||||
confirm=(route-action "reset")
|
||||
close=(route-action "cancel")}}
|
||||
close=(route-action "cancel")
|
||||
modifier="action wide"}}
|
||||
|
15
ghost/admin/app/utils/route.js
Normal file
15
ghost/admin/app/utils/route.js
Normal file
@ -0,0 +1,15 @@
|
||||
import Route from 'ember-route';
|
||||
|
||||
Route.reopen({
|
||||
actions: {
|
||||
willTransition(transition) {
|
||||
if (this.get('upgradeStatus.isRequired')) {
|
||||
transition.abort();
|
||||
this.get('upgradeStatus').requireUpgrade();
|
||||
return false;
|
||||
} else {
|
||||
this._super(...arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
@ -24,6 +24,8 @@ describe('Acceptance: Editor', function() {
|
||||
});
|
||||
|
||||
it('redirects to signin when not authenticated', function () {
|
||||
server.create('post');
|
||||
|
||||
invalidateSession(application);
|
||||
visit('/editor/1');
|
||||
|
||||
@ -35,6 +37,7 @@ describe('Acceptance: Editor', function() {
|
||||
it('does not redirect to team page when authenticated as author', function () {
|
||||
let role = server.create('role', {name: 'Author'});
|
||||
let user = server.create('user', {roles: [role], slug: 'test-user'});
|
||||
server.create('post');
|
||||
|
||||
authenticateSession(application);
|
||||
visit('/editor/1');
|
||||
@ -47,6 +50,7 @@ describe('Acceptance: Editor', function() {
|
||||
it('does not redirect to team page when authenticated as editor', function () {
|
||||
let role = server.create('role', {name: 'Editor'});
|
||||
let user = server.create('user', {roles: [role], slug: 'test-user'});
|
||||
server.create('post');
|
||||
|
||||
authenticateSession(application);
|
||||
visit('/editor/1');
|
||||
@ -56,6 +60,19 @@ describe('Acceptance: Editor', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('displays 404 when post does not exist', function () {
|
||||
let role = server.create('role', {name: 'Editor'});
|
||||
let user = server.create('user', {roles: [role], slug: 'test-user'});
|
||||
|
||||
authenticateSession(application);
|
||||
visit('/editor/1');
|
||||
|
||||
andThen(() => {
|
||||
expect(currentPath()).to.equal('error404');
|
||||
expect(currentURL()).to.equal('/editor/1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when logged in', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
|
@ -108,6 +108,7 @@ describe('Acceptance: Settings - Apps - Slack', function () {
|
||||
click('.gh-alert-blue .gh-alert-close');
|
||||
click('#sendTestNotification');
|
||||
|
||||
// we shouldn't try to send the test request if the save fails
|
||||
andThen(() => {
|
||||
let [lastRequest] = server.pretender.handledRequests.slice(-1);
|
||||
expect(lastRequest.url).to.not.match(/\/slack\/test/);
|
||||
|
133
ghost/admin/tests/acceptance/version-mismatch-test.js
Normal file
133
ghost/admin/tests/acceptance/version-mismatch-test.js
Normal file
@ -0,0 +1,133 @@
|
||||
/* jshint expr:true */
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
beforeEach,
|
||||
afterEach
|
||||
} from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import startApp from '../helpers/start-app';
|
||||
import destroyApp from '../helpers/destroy-app';
|
||||
import { invalidateSession, authenticateSession } from 'ghost-admin/tests/helpers/ember-simple-auth';
|
||||
import Mirage from 'ember-cli-mirage';
|
||||
|
||||
let versionMismatchResponse = function () {
|
||||
return new Mirage.Response(400, {}, {
|
||||
errors: [{
|
||||
errorType: 'VersionMismatchError',
|
||||
statusCode: 400
|
||||
}]
|
||||
});
|
||||
};
|
||||
|
||||
describe('Acceptance: Version Mismatch', function() {
|
||||
let application;
|
||||
|
||||
beforeEach(function() {
|
||||
application = startApp();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
destroyApp(application);
|
||||
});
|
||||
|
||||
describe('logged in', function () {
|
||||
beforeEach(function () {
|
||||
let role = server.create('role', {name: 'Administrator'});
|
||||
let user = server.create('user', {roles: [role]});
|
||||
|
||||
server.loadFixtures();
|
||||
|
||||
return authenticateSession(application);
|
||||
});
|
||||
|
||||
it('displays an alert and disables navigation when saving', function () {
|
||||
server.createList('post', 3);
|
||||
|
||||
// mock the post save endpoint to return version mismatch
|
||||
server.put('/posts/:id', versionMismatchResponse);
|
||||
|
||||
visit('/');
|
||||
click('.posts-list li:nth-of-type(2) a'); // select second post
|
||||
click('.post-edit'); // preview edit button
|
||||
click('.js-publish-button'); // "Save post"
|
||||
|
||||
andThen(() => {
|
||||
// has the refresh to update alert
|
||||
expect(find('.gh-alert').length).to.equal(1);
|
||||
expect(find('.gh-alert').text()).to.match(/refresh/);
|
||||
});
|
||||
|
||||
// try navigating back to the content list
|
||||
click('.gh-nav-main-content');
|
||||
|
||||
andThen(() => {
|
||||
expect(currentPath()).to.equal('editor.edit');
|
||||
});
|
||||
});
|
||||
|
||||
it('displays alert and aborts the transition when navigating', function () {
|
||||
// mock the tags endpoint to return version mismatch
|
||||
server.get('/tags/', versionMismatchResponse);
|
||||
|
||||
visit('/');
|
||||
click('.gh-nav-settings-tags');
|
||||
|
||||
andThen(() => {
|
||||
// navigation is blocked
|
||||
expect(currentPath()).to.equal('posts.index');
|
||||
|
||||
// has the refresh to update alert
|
||||
expect(find('.gh-alert').length).to.equal(1);
|
||||
expect(find('.gh-alert').text()).to.match(/refresh/);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays alert and aborts the transition when an ember-ajax error is thrown whilst navigating', function () {
|
||||
server.get('/configuration/timezones/', versionMismatchResponse);
|
||||
|
||||
visit('/settings/tags');
|
||||
click('.gh-nav-settings-general');
|
||||
|
||||
andThen(() => {
|
||||
// navigation is blocked
|
||||
expect(currentPath()).to.equal('settings.tags.index');
|
||||
|
||||
// has the refresh to update alert
|
||||
expect(find('.gh-alert').length).to.equal(1);
|
||||
expect(find('.gh-alert').text()).to.match(/refresh/);
|
||||
});
|
||||
});
|
||||
|
||||
it('can be triggered when passed in to a component', function () {
|
||||
server.post('/subscribers/csv/', versionMismatchResponse);
|
||||
|
||||
visit('/subscribers');
|
||||
click('.btn:contains("Import CSV")');
|
||||
fileUpload('.fullscreen-modal input[type="file"]');
|
||||
|
||||
andThen(() => {
|
||||
// alert is shown
|
||||
expect(find('.gh-alert').length).to.equal(1);
|
||||
expect(find('.gh-alert').text()).to.match(/refresh/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('logged out', function () {
|
||||
it('displays alert', function () {
|
||||
server.post('/authentication/token', versionMismatchResponse);
|
||||
|
||||
visit('/signin');
|
||||
fillIn('[name="identification"]', 'test@example.com');
|
||||
fillIn('[name="password"]', 'password');
|
||||
click('.btn-blue');
|
||||
|
||||
andThen(() => {
|
||||
// has the refresh to update alert
|
||||
expect(find('.gh-alert').length).to.equal(1);
|
||||
expect(find('.gh-alert').text()).to.match(/refresh/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -11,6 +11,13 @@ import Pretender from 'pretender';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
import sinon from 'sinon';
|
||||
import {createFile, fileUpload} from '../../helpers/file-upload';
|
||||
import Service from 'ember-service';
|
||||
|
||||
const notificationsStub = Service.extend({
|
||||
showAPIError(error, options) {
|
||||
// noop - to be stubbed
|
||||
}
|
||||
});
|
||||
|
||||
const stubSuccessfulUpload = function (server, delay = 0) {
|
||||
server.post('/ghost/api/v0.1/uploads/', function () {
|
||||
@ -41,6 +48,9 @@ describeComponent(
|
||||
beforeEach(function () {
|
||||
server = new Pretender();
|
||||
this.set('uploadUrl', '/ghost/api/v0.1/uploads/');
|
||||
|
||||
this.register('service:notifications', notificationsStub);
|
||||
this.inject.service('notifications', {as: 'notifications'});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -217,6 +227,35 @@ describeComponent(
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers notifications.showAPIError for VersionMismatchError', function (done) {
|
||||
let showAPIError = sinon.spy();
|
||||
this.set('notifications.showAPIError', showAPIError);
|
||||
|
||||
stubFailedUpload(server, 400, 'VersionMismatchError');
|
||||
|
||||
this.render(hbs`{{gh-file-uploader url=uploadUrl}}`);
|
||||
fileUpload(this.$('input[type="file"]'));
|
||||
|
||||
wait().then(() => {
|
||||
expect(showAPIError.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('doesn\'t trigger notifications.showAPIError for other errors', function (done) {
|
||||
let showAPIError = sinon.spy();
|
||||
this.set('notifications.showAPIError', showAPIError);
|
||||
|
||||
stubFailedUpload(server, 400, 'UnknownError');
|
||||
this.render(hbs`{{gh-file-uploader url=uploadUrl}}`);
|
||||
fileUpload(this.$('input[type="file"]'));
|
||||
|
||||
wait().then(() => {
|
||||
expect(showAPIError.called).to.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can be reset after a failed upload', function (done) {
|
||||
stubFailedUpload(server, 400, 'UnknownError');
|
||||
this.render(hbs`{{gh-file-uploader url=uploadUrl}}`);
|
||||
|
@ -21,6 +21,12 @@ const configStub = Service.extend({
|
||||
fileStorage: true
|
||||
});
|
||||
|
||||
const notificationsStub = Service.extend({
|
||||
showAPIError(error, options) {
|
||||
// noop - to be stubbed
|
||||
}
|
||||
});
|
||||
|
||||
const sessionStub = Service.extend({
|
||||
isAuthenticated: false,
|
||||
authorize(authorizer, block) {
|
||||
@ -59,8 +65,10 @@ describeComponent(
|
||||
beforeEach(function () {
|
||||
this.register('service:config', configStub);
|
||||
this.register('service:session', sessionStub);
|
||||
this.register('service:notifications', notificationsStub);
|
||||
this.inject.service('config', {as: 'configService'});
|
||||
this.inject.service('session', {as: 'sessionService'});
|
||||
this.inject.service('notifications', {as: 'notifications'});
|
||||
this.set('update', function () {});
|
||||
server = new Pretender();
|
||||
});
|
||||
@ -298,6 +306,35 @@ describeComponent(
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers notifications.showAPIError for VersionMismatchError', function (done) {
|
||||
let showAPIError = sinon.spy();
|
||||
this.set('notifications.showAPIError', showAPIError);
|
||||
|
||||
stubFailedUpload(server, 400, 'VersionMismatchError');
|
||||
|
||||
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
|
||||
fileUpload(this.$('input[type="file"]'));
|
||||
|
||||
wait().then(() => {
|
||||
expect(showAPIError.calledOnce).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('doesn\'t trigger notifications.showAPIError for other errors', function (done) {
|
||||
let showAPIError = sinon.spy();
|
||||
this.set('notifications.showAPIError', showAPIError);
|
||||
|
||||
stubFailedUpload(server, 400, 'UnknownError');
|
||||
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
|
||||
fileUpload(this.$('input[type="file"]'));
|
||||
|
||||
wait().then(() => {
|
||||
expect(showAPIError.called).to.be.false;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can be reset after a failed upload', function (done) {
|
||||
stubFailedUpload(server, 400, 'UnknownError');
|
||||
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
|
||||
|
@ -6,7 +6,18 @@ import {
|
||||
} from 'ember-mocha';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import run from 'ember-runloop';
|
||||
import Pretender from 'pretender';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
import sinon from 'sinon';
|
||||
|
||||
let versionMismatchResponse = function () {
|
||||
return [400, {'Content-Type': 'application/json'}, JSON.stringify({
|
||||
errors: [{
|
||||
errorType: 'VersionMismatchError',
|
||||
statusCode: 400
|
||||
}]
|
||||
})];
|
||||
};
|
||||
|
||||
describeComponent(
|
||||
'gh-search-input',
|
||||
@ -15,6 +26,16 @@ describeComponent(
|
||||
integration: true
|
||||
},
|
||||
function () {
|
||||
let server;
|
||||
|
||||
beforeEach(function () {
|
||||
server = new Pretender();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
server.shutdown();
|
||||
});
|
||||
|
||||
it('renders', function () {
|
||||
// renders the component on the page
|
||||
this.render(hbs`{{gh-search-input}}`);
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
isUnauthorizedError
|
||||
} from 'ember-ajax/errors';
|
||||
import {
|
||||
isVersionMismatchError,
|
||||
isRequestEntityTooLargeError,
|
||||
isUnsupportedMediaTypeError
|
||||
} from 'ghost-admin/services/ajax';
|
||||
@ -125,6 +126,30 @@ describeModule(
|
||||
});
|
||||
});
|
||||
|
||||
it('handles error checking for VersionMismatchError', function (done) {
|
||||
server.get('/test/', function () {
|
||||
return [
|
||||
400,
|
||||
{'Content-Type': 'application/json'},
|
||||
JSON.stringify({
|
||||
errors: [{
|
||||
errorType: 'VersionMismatchError',
|
||||
statusCode: 400
|
||||
}]
|
||||
})
|
||||
];
|
||||
});
|
||||
|
||||
let ajax = this.subject();
|
||||
|
||||
ajax.request('/test/').then(() => {
|
||||
expect(false).to.be.true;
|
||||
}).catch((error) => {
|
||||
expect(isVersionMismatchError(error)).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles error checking for RequestEntityTooLargeError on 413 errors', function (done) {
|
||||
stubAjaxEndpoint(server, {}, 413);
|
||||
|
||||
|
@ -174,8 +174,16 @@ describeModule(
|
||||
});
|
||||
|
||||
return wait().then(() => {
|
||||
expect(server.handlers[1].numberOfCalls).to.equal(1);
|
||||
expect(service.get('notifications.notifications').length).to.equal(1);
|
||||
expect(
|
||||
server.handlers[1].numberOfCalls,
|
||||
'PUT call is made'
|
||||
).to.equal(1);
|
||||
|
||||
expect(
|
||||
service.get('notifications.alerts').length,
|
||||
'number of alerts shown'
|
||||
).to.equal(1);
|
||||
|
||||
expect(service.get('testFlag')).to.be.false;
|
||||
done();
|
||||
});
|
||||
|
@ -25,7 +25,14 @@ describeModule(
|
||||
'Unit: Controller: settings/navigation',
|
||||
{
|
||||
// Specify the other units that are required for this test.
|
||||
needs: ['service:config', 'service:notifications', 'model:navigation-item', 'service:ajax', 'service:ghostPaths']
|
||||
needs: [
|
||||
'service:config',
|
||||
'service:notifications',
|
||||
'model:navigation-item',
|
||||
'service:ajax',
|
||||
'service:ghostPaths',
|
||||
'service:upgrade-status'
|
||||
]
|
||||
},
|
||||
function () {
|
||||
it('blogUrl: captures config and ensures trailing slash', function () {
|
||||
|
@ -166,34 +166,39 @@ describeModule(
|
||||
]);
|
||||
});
|
||||
|
||||
it('#showAPIError adds single json response error', function () {
|
||||
it('#showAPIError handles single json response error', function () {
|
||||
let notifications = this.subject();
|
||||
let error = new AjaxError('Single error');
|
||||
let error = new AjaxError([{message: 'Single error'}]);
|
||||
|
||||
run(() => {
|
||||
notifications.showAPIError(error);
|
||||
});
|
||||
|
||||
let notification = notifications.get('alerts.firstObject');
|
||||
expect(get(notification, 'message')).to.equal('Single error');
|
||||
expect(get(notification, 'status')).to.equal('alert');
|
||||
expect(get(notification, 'type')).to.equal('error');
|
||||
expect(get(notification, 'key')).to.equal('api-error');
|
||||
let alert = notifications.get('alerts.firstObject');
|
||||
expect(get(alert, 'message')).to.equal('Single error');
|
||||
expect(get(alert, 'status')).to.equal('alert');
|
||||
expect(get(alert, 'type')).to.equal('error');
|
||||
expect(get(alert, 'key')).to.equal('api-error');
|
||||
});
|
||||
|
||||
// used to display validation errors returned from the server
|
||||
it('#showAPIError adds multiple json response errors', function () {
|
||||
// TODO: update once we have unique api key handling
|
||||
it('#showAPIError handles multiple json response errors', function () {
|
||||
let notifications = this.subject();
|
||||
let error = new AjaxError(['First error', 'Second error']);
|
||||
let error = new AjaxError([
|
||||
{message: 'First error'},
|
||||
{message: 'Second error'}
|
||||
]);
|
||||
|
||||
run(() => {
|
||||
notifications.showAPIError(error);
|
||||
});
|
||||
|
||||
expect(notifications.get('notifications')).to.deep.equal([
|
||||
{message: 'First error', status: 'notification', type: 'error', key: undefined},
|
||||
{message: 'Second error', status: 'notification', type: 'error', key: undefined}
|
||||
]);
|
||||
// First error is removed due to duplicate api-key
|
||||
let alert = notifications.get('alerts.firstObject');
|
||||
expect(get(alert, 'message')).to.equal('Second error');
|
||||
expect(get(alert, 'status')).to.equal('alert');
|
||||
expect(get(alert, 'type')).to.equal('error');
|
||||
expect(get(alert, 'key')).to.equal('api-error');
|
||||
});
|
||||
|
||||
it('#showAPIError displays default error text if response has no error/message', function () {
|
||||
@ -238,7 +243,7 @@ describeModule(
|
||||
|
||||
it('#showAPIError parses errors from ember-ajax correctly', function () {
|
||||
let notifications = this.subject();
|
||||
let error = new InvalidError('Test Error');
|
||||
let error = new InvalidError([{message: 'Test Error'}]);
|
||||
|
||||
run(() => {
|
||||
notifications.showAPIError(error);
|
||||
|
23
ghost/admin/tests/unit/services/upgrade-status-test.js
Normal file
23
ghost/admin/tests/unit/services/upgrade-status-test.js
Normal file
@ -0,0 +1,23 @@
|
||||
/* jshint expr:true */
|
||||
import { expect } from 'chai';
|
||||
import {
|
||||
describeModule,
|
||||
it
|
||||
} from 'ember-mocha';
|
||||
|
||||
describeModule(
|
||||
'service:upgrade-status',
|
||||
'UpgradeStatusService',
|
||||
{
|
||||
// Specify the other units that are required for this test.
|
||||
// needs: ['service:foo']
|
||||
needs: []
|
||||
},
|
||||
function() {
|
||||
// Replace this with your real tests.
|
||||
it('exists', function() {
|
||||
let service = this.subject();
|
||||
expect(service).to.be.ok;
|
||||
});
|
||||
}
|
||||
);
|
Loading…
Reference in New Issue
Block a user