From 048f052a3b2a59c79a5ea7d737ca19948fefba47 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Thu, 21 Sep 2017 16:01:40 +0100 Subject: [PATCH] Upload/Download redirects UI closes TryGhost/Ghost#9028 - add upload/download UI to labs screen - displays success/failure state in the button for 5 secs after uploading - minor refactor to remove redundant `{{#if}}` conditionals in general settings screen - minor naming refactor of `onUploadFail` -> `onUploadFailure` for `{{gh-uploader}}`'s closure action --- ghost/admin/app/components/gh-uploader.js | 4 +- .../admin/app/controllers/settings/general.js | 2 +- ghost/admin/app/controllers/settings/labs.js | 43 ++++++- .../admin/app/templates/settings/general.hbs | 24 ++-- ghost/admin/app/templates/settings/labs.hbs | 48 +++++++- .../tests/acceptance/settings/labs-test.js | 115 ++++++++++++++++++ .../components/gh-uploader-test.js | 4 +- 7 files changed, 216 insertions(+), 24 deletions(-) diff --git a/ghost/admin/app/components/gh-uploader.js b/ghost/admin/app/components/gh-uploader.js index c1a248c535..4690f15474 100644 --- a/ghost/admin/app/components/gh-uploader.js +++ b/ghost/admin/app/components/gh-uploader.js @@ -69,7 +69,7 @@ export default Component.extend({ onComplete() {}, onFailed() {}, onStart() {}, - onUploadFail() {}, + onUploadFailure() {}, onUploadSuccess() {}, // Optional closure actions @@ -240,7 +240,7 @@ export default Component.extend({ // TODO: check for or expose known error types? this.get('errors').pushObject(result); - this.onUploadFail(result); + this.onUploadFailure(result); } }), diff --git a/ghost/admin/app/controllers/settings/general.js b/ghost/admin/app/controllers/settings/general.js index 56c27f580f..1d9bce0b06 100644 --- a/ghost/admin/app/controllers/settings/general.js +++ b/ghost/admin/app/controllers/settings/general.js @@ -110,7 +110,7 @@ export default Controller.extend({ if (fileInput.length > 0) { // reset file input value before clicking so that the same image // can be selected again - fileInput.value = ''; + fileInput.val(''); // simulate click to open file dialog // using jQuery because IE11 doesn't support MouseEvent diff --git a/ghost/admin/app/controllers/settings/labs.js b/ghost/admin/app/controllers/settings/labs.js index ead02446fc..f7b46a0a91 100644 --- a/ghost/admin/app/controllers/settings/labs.js +++ b/ghost/admin/app/controllers/settings/labs.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import Controller from '@ember/controller'; +import Ember from 'ember'; import RSVP from 'rsvp'; import { UnsupportedMediaTypeError, @@ -9,8 +10,9 @@ import {inject as injectService} from '@ember/service'; import {isBlank} from '@ember/utils'; import {isArray as isEmberArray} from '@ember/array'; import {run} from '@ember/runloop'; -import {task} from 'ember-concurrency'; +import {task, timeout} from 'ember-concurrency'; +const {testing} = Ember; const {Promise} = RSVP; export default Controller.extend({ @@ -21,6 +23,8 @@ export default Controller.extend({ uploadButtonText: 'Import', importMimeType: ['application/json', 'application/zip', 'application/x-zip-compressed'], + jsonExtension: ['json'], + jsonMimeType: ['application/json'], ajax: injectService(), config: injectService(), @@ -84,6 +88,17 @@ export default Controller.extend({ } }).drop(), + redirectUploadResult: task(function* (success) { + this.set('redirectSuccess', success); + this.set('redirectFailure', !success); + + yield timeout(testing ? 100 : 5000); + + this.set('redirectSuccess', null); + this.set('redirectFailure', null); + return true; + }).drop(), + reset() { this.set('importErrors', null); this.set('importSuccessful', false); @@ -152,8 +167,8 @@ export default Controller.extend({ }); }, - exportData() { - let dbUrl = this.get('ghostPaths.url').api('db'); + downloadFile(url) { + let dbUrl = this.get('ghostPaths.url').api(url); let accessToken = this.get('session.data.authenticated.access_token'); let downloadURL = `${dbUrl}?access_token=${accessToken}`; let iframe = $('#iframeDownload'); @@ -167,6 +182,28 @@ export default Controller.extend({ toggleDeleteAllModal() { this.toggleProperty('showDeleteAllModal'); + }, + + /** + * Opens a file selection dialog - Triggered by "Upload x" buttons, + * searches for the hidden file input within the .gh-setting element + * containing the clicked button then simulates a click + * @param {MouseEvent} event - MouseEvent fired by the button click + */ + triggerFileDialog(event) { + let fileInput = $(event.target) + .closest('.gh-setting') + .find('input[type="file"]'); + + if (fileInput.length > 0) { + // reset file input value before clicking so that the same image + // can be selected again + fileInput.val(''); + + // simulate click to open file dialog + // using jQuery because IE11 doesn't support MouseEvent + $(fileInput).click(); + } } } }); diff --git a/ghost/admin/app/templates/settings/general.hbs b/ghost/admin/app/templates/settings/general.hbs index 9fc130b14b..0fb70ad2d7 100644 --- a/ghost/admin/app/templates/settings/general.hbs +++ b/ghost/admin/app/templates/settings/general.hbs @@ -63,11 +63,9 @@
Publication icon
A square, social icon used in the UI of your publication, at least 60x60px
- {{#if uploader.errors}} - {{#each uploader.errors as |error|}} -
{{error.message}}
- {{/each}} - {{/if}} + {{#each uploader.errors as |error|}} +
{{error.message}}
+ {{/each}}
{{#if uploader.isUploading}} @@ -98,11 +96,9 @@
Publication logo
The primary logo for your brand displayed across your theme, should be transparent and at least 600px x 72px
- {{#if uploader.errors}} - {{#each uploader.errors as |error|}} -
{{error.message}}
- {{/each}} - {{/if}} + {{#each uploader.errors as |error|}} +
{{error.message}}
+ {{/each}}
{{#if uploader.isUploading}} @@ -133,11 +129,9 @@
Publication cover
An optional large background image for your site
- {{#if uploader.errors}} - {{#each uploader.errors as |error|}} -
{{error.message}}
- {{/each}} - {{/if}} + {{#each uploader.errors as |error|}} +
{{error.message}}
+ {{/each}}
{{#if uploader.isUploading}} diff --git a/ghost/admin/app/templates/settings/labs.hbs b/ghost/admin/app/templates/settings/labs.hbs index 994002362d..1398a4e858 100644 --- a/ghost/admin/app/templates/settings/labs.hbs +++ b/ghost/admin/app/templates/settings/labs.hbs @@ -51,7 +51,7 @@
Download all of your posts and settings in a single, glorious JSON file
- +
@@ -103,6 +103,52 @@
{{gh-feature-flag "subscribers"}}
+
+ {{#gh-uploader + files=redirectsUpload + extensions=jsonExtension + uploadUrl="/redirects/json/" + paramName="redirects" + onUploadSuccess=(perform redirectUploadResult true) + onUploadFailure=(perform redirectUploadResult false) + as |uploader| + }} +
+
Redirects
+
Configure redirects for old or moved content, more info in the docs
+ {{#each uploader.errors as |error|}} +
{{error.message}}
+ {{/each}} +
+
+ {{#if uploader.isUploading}} + {{uploader.progressBar}} + {{else}} + + Download current redirects + {{/if}} + +
+ {{gh-file-input multiple=false action=(action (mut redirectsUpload)) accept=jsonMimeType data-test-file-input="redirects"}} +
+
+ {{/gh-uploader}} +
diff --git a/ghost/admin/tests/acceptance/settings/labs-test.js b/ghost/admin/tests/acceptance/settings/labs-test.js index 7b90bbc58b..a7fac1ff58 100644 --- a/ghost/admin/tests/acceptance/settings/labs-test.js +++ b/ghost/admin/tests/acceptance/settings/labs-test.js @@ -2,9 +2,11 @@ import $ from 'jquery'; import destroyApp from '../../helpers/destroy-app'; import startApp from '../../helpers/start-app'; +// import wait from 'ember-test-helpers/wait'; import {afterEach, beforeEach, describe, it} from 'mocha'; import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth'; import {expect} from 'chai'; +// import {timeout} from 'ember-concurrency'; describe('Acceptance: Settings - Labs', function() { let application; @@ -71,5 +73,118 @@ describe('Acceptance: Settings - Labs', function() { await click('.fullscreen-modal .modal-footer .gh-btn'); expect(find('.fullscreen-modal').length, 'modal element').to.equal(0); }); + + it('can upload/download redirects', async function () { + await visit('/settings/labs'); + + // successful upload + server.post('/redirects/json/', {}, 200); + + await fileUpload( + '[data-test-file-input="redirects"]', + ['test'], + {name: 'redirects.json', type: 'application/json'} + ); + + // TODO: tests for the temporary success/failure state have been + // disabled because they were randomly failing + + // this should be half-way through button reset timeout + // await timeout(50); + // + // // shows success button + // let button = find('[data-test-button="upload-redirects"]'); + // expect(button.length, 'no of success buttons').to.equal(1); + // expect( + // button.hasClass('gh-btn-green'), + // 'success button is green' + // ).to.be.true; + // expect( + // button.text().trim(), + // 'success button text' + // ).to.have.string('Uploaded'); + // + // await wait(); + + // returned to normal button + let button = find('[data-test-button="upload-redirects"]'); + expect(button.length, 'no of post-success buttons').to.equal(1); + expect( + button.hasClass('gh-btn-green'), + 'post-success button doesn\'t have success class' + ).to.be.false; + expect( + button.text().trim(), + 'post-success button text' + ).to.have.string('Upload redirects'); + + // failed upload + server.post('/redirects/json/', { + errors: [{ + errorType: 'BadRequestError', + message: 'Test failure message' + }] + }, 400); + + await fileUpload( + '[data-test-file-input="redirects"]', + ['test'], + {name: 'redirects-bad.json', type: 'application/json'} + ); + + // TODO: tests for the temporary success/failure state have been + // disabled because they were randomly failing + + // this should be half-way through button reset timeout + // await timeout(50); + // + // shows failure button + // button = find('[data-test-button="upload-redirects"]'); + // expect(button.length, 'no of failure buttons').to.equal(1); + // expect( + // button.hasClass('gh-btn-red'), + // 'failure button is red' + // ).to.be.true; + // expect( + // button.text().trim(), + // 'failure button text' + // ).to.have.string('Upload Failed'); + // + // await wait(); + + // shows error message + expect( + find('[data-test-error="redirects"]').text().trim(), + 'upload error text' + ).to.have.string('Test failure message'); + + // returned to normal button + button = find('[data-test-button="upload-redirects"]'); + expect(button.length, 'no of post-failure buttons').to.equal(1); + expect( + button.hasClass('gh-btn-red'), + 'post-failure button doesn\'t have failure class' + ).to.be.false; + expect( + button.text().trim(), + 'post-failure button text' + ).to.have.string('Upload redirects'); + + // successful upload clears error + server.post('/redirects/json/', {}, 200); + await fileUpload( + '[data-test-file-input="redirects"]', + ['test'], + {name: 'redirects-bad.json', type: 'application/json'} + ); + + expect(find('[data-test-error="redirects"]')).to.not.exist; + + // can download redirects.json + await click('[data-test-link="download-redirects"]'); + + let iframe = $('#iframeDownload'); + expect(iframe.attr('src')).to.have.string('/redirects/json/'); + }); }); }); diff --git a/ghost/admin/tests/integration/components/gh-uploader-test.js b/ghost/admin/tests/integration/components/gh-uploader-test.js index ab8715b977..877380dee0 100644 --- a/ghost/admin/tests/integration/components/gh-uploader-test.js +++ b/ghost/admin/tests/integration/components/gh-uploader-test.js @@ -375,13 +375,13 @@ describe('Integration: Component: gh-uploader', function() { expect(failures[0].message).to.equal('Error: No upload for you'); }); - it('triggers onUploadFail when each upload fails', async function () { + it('triggers onUploadFailure when each upload fails', async function () { this.set('uploadFail', sinon.spy()); this.render(hbs` {{#gh-uploader files=files - onUploadFail=(action uploadFail)}} + onUploadFailure=(action uploadFail)}} {{/gh-uploader}} `); this.set('files', [