mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-29 15:12:58 +03:00
cb59388c5b
no issue - adds `eslint-plugin-sort-imports-es6-autofix` dependency - implements ESLint's base `sort-imports` rule but has a distinction in that `import {foo} from 'bar';` is considered `multiple` rather than `single` - fixes ESLint's autofix behaviour so `eslint --fix` will actually fix the sort order - updates all unordered import rules by using `eslint --fix` With the increased number of `import` statements since Ember+ecosystem started moving towards es6 modules I've found it frustrating at times trying to search through randomly ordered import statements. Recently I've been sorting imports manually when I've added new code or touched old code so I thought I'd add an ESLint rule to codify it.
295 lines
8.6 KiB
JavaScript
295 lines
8.6 KiB
JavaScript
import Component from 'ember-component';
|
|
import EmberObject from 'ember-object';
|
|
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
|
import injectService from 'ember-service/inject';
|
|
import run from 'ember-runloop';
|
|
import {all, task} from 'ember-concurrency';
|
|
import {isEmberArray} from 'ember-array/utils';
|
|
import {isEmpty} from 'ember-utils';
|
|
|
|
// TODO: this is designed to be a more re-usable/composable upload component, it
|
|
// should be able to replace the duplicated upload logic in:
|
|
// - gh-image-uploader
|
|
// - gh-file-uploader
|
|
// - gh-koenig/cards/card-image
|
|
// - gh-koenig/cards/card-markdown
|
|
//
|
|
// In order to support the above components we'll need to introduce an
|
|
// "allowMultiple" attribute so that single-image uploads don't allow multiple
|
|
// simultaneous uploads
|
|
|
|
/**
|
|
* Result from a file upload
|
|
* @typedef {Object} UploadResult
|
|
* @property {string} fileName - file name, eg "my-image.png"
|
|
* @property {string} url - url relative to Ghost root,eg "/content/images/2017/05/my-image.png"
|
|
*/
|
|
|
|
const UploadTracker = EmberObject.extend({
|
|
file: null,
|
|
total: 0,
|
|
loaded: 0,
|
|
|
|
init() {
|
|
this.total = this.file && this.file.size || 0;
|
|
},
|
|
|
|
update({loaded, total}) {
|
|
this.total = total;
|
|
this.loaded = loaded;
|
|
}
|
|
});
|
|
|
|
export default Component.extend({
|
|
tagName: '',
|
|
|
|
ajax: injectService(),
|
|
|
|
// Public attributes
|
|
accept: '',
|
|
extensions: '',
|
|
files: null,
|
|
paramName: 'uploadimage', // TODO: is this the best default?
|
|
uploadUrl: null,
|
|
|
|
// Interal attributes
|
|
errors: null, // [{fileName: 'x', message: 'y'}, ...]
|
|
totalSize: 0,
|
|
uploadedSize: 0,
|
|
uploadPercentage: 0,
|
|
uploadUrls: null, // [{filename: 'x', url: 'y'}],
|
|
|
|
// Private
|
|
_defaultUploadUrl: '/uploads/',
|
|
_files: null,
|
|
_uploadTrackers: null,
|
|
|
|
// Closure actions
|
|
onCancel() {},
|
|
onComplete() {},
|
|
onFailed() {},
|
|
onStart() {},
|
|
onUploadFail() {},
|
|
onUploadSuccess() {},
|
|
|
|
// Optional closure actions
|
|
// validate(file) {}
|
|
|
|
init() {
|
|
this._super(...arguments);
|
|
this.set('errors', []);
|
|
this.set('uploadUrls', []);
|
|
this._uploadTrackers = [];
|
|
},
|
|
|
|
didReceiveAttrs() {
|
|
this._super(...arguments);
|
|
|
|
// set up any defaults
|
|
if (!this.get('uploadUrl')) {
|
|
this.set('uploadUrl', this._defaultUploadUrl);
|
|
}
|
|
|
|
// if we have new files, validate and start an upload
|
|
let files = this.get('files');
|
|
if (files && files !== this._files) {
|
|
if (this.get('_uploadFiles.isRunning')) {
|
|
// eslint-disable-next-line
|
|
console.error('Adding new files whilst an upload is in progress is not supported.');
|
|
}
|
|
|
|
this._files = files;
|
|
|
|
// we cancel early if any file fails client-side validation
|
|
if (this._validate()) {
|
|
this.get('_uploadFiles').perform(files);
|
|
}
|
|
}
|
|
},
|
|
|
|
_validate() {
|
|
let files = this.get('files');
|
|
let validate = this.get('validate') || this._defaultValidator.bind(this);
|
|
let ok = [];
|
|
let errors = [];
|
|
|
|
// NOTE: for...of loop results in a transpilation that errors in Edge,
|
|
// once we drop IE11 support we should be able to use native for...of
|
|
for (let i = 0; i < files.length; i++) {
|
|
let file = files[i];
|
|
let result = validate(file);
|
|
if (result === true) {
|
|
ok.push(file);
|
|
} else {
|
|
errors.push({fileName: file.name, message: result});
|
|
}
|
|
}
|
|
|
|
if (isEmpty(errors)) {
|
|
return true;
|
|
}
|
|
|
|
this.set('errors', errors);
|
|
this.onFailed(errors);
|
|
return false;
|
|
},
|
|
|
|
// we only check the file extension by default because IE doesn't always
|
|
// expose the mime-type, we'll rely on the API for final validation
|
|
_defaultValidator(file) {
|
|
let extensions = this.get('extensions');
|
|
let [, extension] = (/(?:\.([^.]+))?$/).exec(file.name);
|
|
|
|
// if extensions is falsy exit early and accept all files
|
|
if (!extensions) {
|
|
return true;
|
|
}
|
|
|
|
if (!isEmberArray(extensions)) {
|
|
extensions = extensions.split(',');
|
|
}
|
|
|
|
if (!extension || extensions.indexOf(extension.toLowerCase()) === -1) {
|
|
let validExtensions = `.${extensions.join(', .').toUpperCase()}`;
|
|
return `The image type you uploaded is not supported. Please use ${validExtensions}`;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
_uploadFiles: task(function* (files) {
|
|
let uploads = [];
|
|
|
|
this._reset();
|
|
this.onStart();
|
|
|
|
// NOTE: for...of loop results in a transpilation that errors in Edge,
|
|
// once we drop IE11 support we should be able to use native for...of
|
|
for (let i = 0; i < files.length; i++) {
|
|
uploads.push(this.get('_uploadFile').perform(files[i]));
|
|
}
|
|
|
|
// populates this.errors and this.uploadUrls
|
|
yield all(uploads);
|
|
|
|
if (!isEmpty(this.get('errors'))) {
|
|
this.onFailed(this.get('errors'));
|
|
}
|
|
|
|
this.onComplete(this.get('uploadUrls'));
|
|
}).drop(),
|
|
|
|
_uploadFile: task(function* (file) {
|
|
let ajax = this.get('ajax');
|
|
let formData = this._getFormData(file);
|
|
let url = `${ghostPaths().apiRoot}${this.get('uploadUrl')}`;
|
|
|
|
let tracker = new UploadTracker({file});
|
|
this.get('_uploadTrackers').pushObject(tracker);
|
|
|
|
try {
|
|
let response = yield ajax.post(url, {
|
|
data: formData,
|
|
processData: false,
|
|
contentType: false,
|
|
dataType: 'text',
|
|
xhr: () => {
|
|
let xhr = new window.XMLHttpRequest();
|
|
|
|
xhr.upload.addEventListener('progress', (event) => {
|
|
run(() => {
|
|
tracker.update(event);
|
|
this._updateProgress();
|
|
});
|
|
}, false);
|
|
|
|
return xhr;
|
|
}
|
|
});
|
|
|
|
// force tracker progress to 100% in case we didn't get a final event,
|
|
// eg. when using mirage
|
|
tracker.update({loaded: file.size, total: file.size});
|
|
this._updateProgress();
|
|
|
|
// TODO: is it safe to assume we'll only get a url back?
|
|
let uploadUrl = JSON.parse(response);
|
|
let result = {
|
|
fileName: file.name,
|
|
url: uploadUrl
|
|
};
|
|
|
|
this.get('uploadUrls').pushObject(result);
|
|
this.onUploadSuccess(result);
|
|
|
|
return true;
|
|
|
|
} catch (error) {
|
|
// grab custom error message if present
|
|
let message = error.errors && error.errors[0].message;
|
|
|
|
// fall back to EmberData/ember-ajax default message for error type
|
|
if (!message) {
|
|
message = error.message;
|
|
}
|
|
|
|
let result = {
|
|
fileName: file.name,
|
|
message: error.errors[0].message
|
|
};
|
|
|
|
// TODO: check for or expose known error types?
|
|
this.get('errors').pushObject(result);
|
|
this.onUploadFail(result);
|
|
}
|
|
}),
|
|
|
|
// NOTE: this is necessary because the API doesn't accept direct file uploads
|
|
_getFormData(file) {
|
|
let formData = new FormData();
|
|
formData.append(this.get('paramName'), file, file.name);
|
|
return formData;
|
|
},
|
|
|
|
// TODO: this was needed because using CPs directly resulted in infrequent updates
|
|
// - I think this was because updates were being wrapped up to save
|
|
// computation but that hypothesis needs testing
|
|
_updateProgress() {
|
|
let trackers = this._uploadTrackers;
|
|
|
|
let totalSize = trackers.reduce((total, tracker) => {
|
|
return total + tracker.get('total');
|
|
}, 0);
|
|
|
|
let uploadedSize = trackers.reduce((total, tracker) => {
|
|
return total + tracker.get('loaded');
|
|
}, 0);
|
|
|
|
this.set('totalSize', totalSize);
|
|
this.set('uploadedSize', uploadedSize);
|
|
|
|
if (totalSize === 0 || uploadedSize === 0) {
|
|
return;
|
|
}
|
|
|
|
let uploadPercentage = Math.round((uploadedSize / totalSize) * 100);
|
|
this.set('uploadPercentage', uploadPercentage);
|
|
},
|
|
|
|
_reset() {
|
|
this.set('errors', []);
|
|
this.set('totalSize', 0);
|
|
this.set('uploadedSize', 0);
|
|
this.set('uploadPercentage', 0);
|
|
this.set('uploadUrls', []);
|
|
this._uploadTrackers = [];
|
|
},
|
|
|
|
actions: {
|
|
cancel() {
|
|
this._reset();
|
|
this.onCancel();
|
|
}
|
|
}
|
|
});
|