mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-28 21:33:24 +03:00
✨ detailed theme validation errors (#226)
no issue - display the detailed validation errors that we get back from gscan so that users know how to fix their themes
This commit is contained in:
parent
7f089e1e47
commit
926f0283b5
@ -2,7 +2,10 @@ import ModalComponent from 'ghost-admin/components/modals/base';
|
|||||||
import computed, {mapBy, or} from 'ember-computed';
|
import computed, {mapBy, or} from 'ember-computed';
|
||||||
import {invokeAction} from 'ember-invoke-action';
|
import {invokeAction} from 'ember-invoke-action';
|
||||||
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
||||||
import {UnsupportedMediaTypeError} from 'ghost-admin/services/ajax';
|
import {
|
||||||
|
UnsupportedMediaTypeError,
|
||||||
|
isThemeValidationError
|
||||||
|
} from 'ghost-admin/services/ajax';
|
||||||
import {isBlank} from 'ember-utils';
|
import {isBlank} from 'ember-utils';
|
||||||
import run from 'ember-runloop';
|
import run from 'ember-runloop';
|
||||||
import injectService from 'ember-service/inject';
|
import injectService from 'ember-service/inject';
|
||||||
@ -91,6 +94,12 @@ export default ModalComponent.extend({
|
|||||||
invokeAction(this, 'model.uploadSuccess', this.get('theme'));
|
invokeAction(this, 'model.uploadSuccess', this.get('theme'));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
uploadFailed(error) {
|
||||||
|
if (isThemeValidationError(error)) {
|
||||||
|
this.set('validationErrors', error.errors[0].errorDetails);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
confirm() {
|
confirm() {
|
||||||
// noop - we don't want the enter key doing anything
|
// noop - we don't want the enter key doing anything
|
||||||
},
|
},
|
||||||
@ -104,6 +113,10 @@ export default ModalComponent.extend({
|
|||||||
if (!this.get('closeDisabled')) {
|
if (!this.get('closeDisabled')) {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.set('validationErrors', null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -88,6 +88,24 @@ export function isMaintenanceError(errorOrStatus) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Theme validation error */
|
||||||
|
|
||||||
|
export function ThemeValidationError(errors) {
|
||||||
|
AjaxError.call(this, errors, 'Theme is not compatible or contains errors.');
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeValidationError.prototype = Object.create(AjaxError.prototype);
|
||||||
|
|
||||||
|
export function isThemeValidationError(errorOrStatus, payload) {
|
||||||
|
if (isAjaxError(errorOrStatus)) {
|
||||||
|
return errorOrStatus instanceof ThemeValidationError;
|
||||||
|
} else if (errorOrStatus && get(errorOrStatus, 'isAdapterError')) {
|
||||||
|
return get(errorOrStatus, 'errors.firstObject.errorType') === 'ThemeValidationError';
|
||||||
|
} else {
|
||||||
|
return get(payload || {}, 'errors.firstObject.errorType') === 'ThemeValidationError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* end: custom error types */
|
/* end: custom error types */
|
||||||
|
|
||||||
export default AjaxService.extend({
|
export default AjaxService.extend({
|
||||||
@ -119,6 +137,8 @@ export default AjaxService.extend({
|
|||||||
return new UnsupportedMediaTypeError(payload.errors);
|
return new UnsupportedMediaTypeError(payload.errors);
|
||||||
} else if (this.isMaintenanceError(status, headers, payload)) {
|
} else if (this.isMaintenanceError(status, headers, payload)) {
|
||||||
return new MaintenanceError(payload.errors);
|
return new MaintenanceError(payload.errors);
|
||||||
|
} else if (this.isThemeValidationError(status, headers, payload)) {
|
||||||
|
return new ThemeValidationError(payload.errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._super(...arguments);
|
return this._super(...arguments);
|
||||||
@ -160,5 +180,9 @@ export default AjaxService.extend({
|
|||||||
|
|
||||||
isMaintenanceError(status, headers, payload) {
|
isMaintenanceError(status, headers, payload) {
|
||||||
return isMaintenanceError(status, payload);
|
return isMaintenanceError(status, payload);
|
||||||
|
},
|
||||||
|
|
||||||
|
isThemeValidationError(status, headers, payload) {
|
||||||
|
return isThemeValidationError(status, payload);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -251,3 +251,22 @@ a.theme-list-action {
|
|||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theme-validation-errors {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-validation-errors p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-validation-errors > li {
|
||||||
|
margin-bottom: 1.2em;
|
||||||
|
list-style: none;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-validation-errors > li > ul {
|
||||||
|
margin-top: 0.2em;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
<h1>
|
<h1>
|
||||||
{{#if theme}}
|
{{#if theme}}
|
||||||
Upload successful!
|
Upload successful!
|
||||||
|
{{else if validationErrors}}
|
||||||
|
Invalid theme
|
||||||
{{else}}
|
{{else}}
|
||||||
Upload a theme
|
Upload a theme
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@ -19,6 +21,24 @@
|
|||||||
<p>
|
<p>
|
||||||
"{{fileThemeName}}" will overwrite an existing theme of the same name. Are you sure?
|
"{{fileThemeName}}" will overwrite an existing theme of the same name. Are you sure?
|
||||||
</p>
|
</p>
|
||||||
|
{{else if validationErrors}}
|
||||||
|
<ul class="theme-validation-errors">
|
||||||
|
{{#each validationErrors as |error|}}
|
||||||
|
<li>
|
||||||
|
{{#if error.details}}
|
||||||
|
{{{error.details}}}
|
||||||
|
{{else}}
|
||||||
|
{{{error.rule}}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{{#each error.failures as |failure|}}
|
||||||
|
<li><code>{{failure.ref}}</code>{{#if failure.message}}: {{failure.message}}{{/if}}</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{gh-file-uploader
|
{{gh-file-uploader
|
||||||
url=uploadUrl
|
url=uploadUrl
|
||||||
@ -29,6 +49,7 @@
|
|||||||
uploadStarted=(action "uploadStarted")
|
uploadStarted=(action "uploadStarted")
|
||||||
uploadFinished=(action "uploadFinished")
|
uploadFinished=(action "uploadFinished")
|
||||||
uploadSuccess=(action "uploadSuccess")
|
uploadSuccess=(action "uploadSuccess")
|
||||||
|
uploadFailed=(action "uploadFailed")
|
||||||
listenTo="themeUploader"}}
|
listenTo="themeUploader"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
@ -42,6 +63,11 @@
|
|||||||
Overwrite
|
Overwrite
|
||||||
</button>
|
</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if validationErrors}}
|
||||||
|
<button {{action "reset"}} class="btn btn-green">
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
{{#if canActivateTheme}}
|
{{#if canActivateTheme}}
|
||||||
<button {{action "activate"}} class="btn btn-green">
|
<button {{action "activate"}} class="btn btn-green">
|
||||||
Activate Now
|
Activate Now
|
||||||
|
@ -411,7 +411,7 @@ describe('Acceptance: Settings - General', function () {
|
|||||||
).to.match(/default Casper theme cannot be overwritten/);
|
).to.match(/default Casper theme cannot be overwritten/);
|
||||||
});
|
});
|
||||||
|
|
||||||
// theme upload handles validation errors
|
// theme upload handles upload errors
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
server.post('/themes/upload/', function () {
|
server.post('/themes/upload/', function () {
|
||||||
return new Mirage.Response(422, {}, {
|
return new Mirage.Response(422, {}, {
|
||||||
@ -433,8 +433,90 @@ describe('Acceptance: Settings - General', function () {
|
|||||||
mockThemes(server);
|
mockThemes(server);
|
||||||
});
|
});
|
||||||
|
|
||||||
// theme upload handles success then close
|
// theme upload handles validation errors
|
||||||
|
andThen(() => {
|
||||||
|
server.post('/themes/upload/', function () {
|
||||||
|
return new Mirage.Response(422, {}, {
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: 'Theme is not compatible or contains errors.',
|
||||||
|
errorType: 'ThemeValidationError',
|
||||||
|
errorDetails: [
|
||||||
|
{
|
||||||
|
level: 'error',
|
||||||
|
rule: 'Templates must contain valid Handlebars.',
|
||||||
|
failures: [
|
||||||
|
{
|
||||||
|
ref: 'index.hbs',
|
||||||
|
message: 'The partial index_meta could not be found'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ref: 'tag.hbs',
|
||||||
|
message: 'The partial index_meta could not be found'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
level: 'error',
|
||||||
|
rule: 'Assets such as CSS & JS must use the <code>{{asset}}</code> helper',
|
||||||
|
details: '<p>The listed files should be included using the <code>{{asset}}</code> helper.</p>',
|
||||||
|
failures: [
|
||||||
|
{
|
||||||
|
ref: '/assets/javascripts/ui.js'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
click('button:contains("Try Again")');
|
click('button:contains("Try Again")');
|
||||||
|
fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'bad-theme.zip', type: 'application/zip'});
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal h1').text().trim(),
|
||||||
|
'modal title after uploading invalid theme'
|
||||||
|
).to.equal('Invalid theme');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-validation-errors').text(),
|
||||||
|
'top-level errors are displayed'
|
||||||
|
).to.match(/Templates must contain valid Handlebars/);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-validation-errors').text(),
|
||||||
|
'top-level errors do not escape HTML'
|
||||||
|
).to.match(/The listed files should be included using the {{asset}} helper/);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.theme-validation-errors').text(),
|
||||||
|
'individual failures are displayed'
|
||||||
|
).to.match(/index\.hbs: The partial index_meta could not be found/);
|
||||||
|
|
||||||
|
// reset to default mirage handlers
|
||||||
|
mockThemes(server);
|
||||||
|
});
|
||||||
|
click('button:contains("Try Again")');
|
||||||
|
andThen(() => {
|
||||||
|
expect(
|
||||||
|
find('.theme-validation-errors').length,
|
||||||
|
'"Try Again" resets form after theme validation error'
|
||||||
|
).to.equal(0);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.gh-image-uploader').length,
|
||||||
|
'"Try Again" resets form after theme validation error'
|
||||||
|
).to.equal(1);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('.fullscreen-modal h1').text().trim(),
|
||||||
|
'"Try Again" resets form after theme validation error'
|
||||||
|
).to.equal('Upload a theme');
|
||||||
|
});
|
||||||
|
|
||||||
|
// theme upload handles success then close
|
||||||
fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'theme-1.zip', type: 'application/zip'});
|
fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'theme-1.zip', type: 'application/zip'});
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
expect(
|
expect(
|
||||||
|
Loading…
Reference in New Issue
Block a user