mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-22 10:21:36 +03:00
fa84808048
no issue Since `ember-moment@10.0` it's not been necessary to use the `ember-cli-moment-shim` package, with `moment` instead being usable directly via `ember-auto-import`. Getting rid of the shim package is necessary for compatibility with `embroider`, Ember's new build tooling. - dropped `ember-cli-moment-shim` dependency - added `moment-timezone` dependency and updated all imports to reflect the different package - worked around `ember-power-calendar` having `ember-cli-moment-shim` as a sub-dependency - added empty in-repo-addon `ember-power-calendar-moment` to avoid `ember-power-calendar` complaining about a missing package - added `ember-power-calendar-utils` in-repo-addon that is a copy of `ember-power-calendar-moment` but without the build-time renaming of the tree for better compatibility with embroider
229 lines
7.4 KiB
JavaScript
229 lines
7.4 KiB
JavaScript
import ModalComponent from 'ghost-admin/components/modal-base';
|
|
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
|
import moment from 'moment-timezone';
|
|
import unparse from '@tryghost/members-csv/lib/unparse';
|
|
import {
|
|
AcceptedResponse,
|
|
isRequestEntityTooLargeError,
|
|
isUnsupportedMediaTypeError,
|
|
isVersionMismatchError
|
|
} from 'ghost-admin/services/ajax';
|
|
import {computed} from '@ember/object';
|
|
import {htmlSafe} from '@ember/template';
|
|
import {isBlank} from '@ember/utils';
|
|
import {inject as service} from '@ember/service';
|
|
|
|
export default ModalComponent.extend({
|
|
config: service(),
|
|
ajax: service(),
|
|
notifications: service(),
|
|
store: service(),
|
|
|
|
state: 'INIT',
|
|
|
|
file: null,
|
|
mappingResult: null,
|
|
mappingFileData: null,
|
|
paramName: 'membersfile',
|
|
importResponse: null,
|
|
errorMessage: null,
|
|
errorHeader: null,
|
|
showMappingErrors: false,
|
|
showTryAgainButton: true,
|
|
|
|
// Allowed actions
|
|
confirm: () => {},
|
|
|
|
uploadUrl: computed(function () {
|
|
return `${ghostPaths().apiRoot}/members/upload/`;
|
|
}),
|
|
|
|
formData: computed('file', function () {
|
|
let formData = new FormData();
|
|
|
|
formData.append(this.paramName, this.file);
|
|
|
|
if (this.mappingResult.labels) {
|
|
this.mappingResult.labels.forEach((label) => {
|
|
formData.append('labels', label.name);
|
|
});
|
|
}
|
|
|
|
if (this.mappingResult.mapping) {
|
|
let mapping = this.mappingResult.mapping.toJSON();
|
|
for (let [key, val] of Object.entries(mapping)) {
|
|
formData.append(`mapping[${key}]`, val);
|
|
}
|
|
}
|
|
|
|
return formData;
|
|
}),
|
|
|
|
actions: {
|
|
setFile(file) {
|
|
this.set('file', file);
|
|
this.set('state', 'MAPPING');
|
|
},
|
|
|
|
setMappingResult(mappingResult) {
|
|
this.set('mappingResult', mappingResult);
|
|
},
|
|
|
|
setMappingFileData(mappingFileData) {
|
|
this.set('mappingFileData', mappingFileData);
|
|
},
|
|
|
|
upload() {
|
|
if (this.file && !this.mappingResult.error) {
|
|
this.generateRequest();
|
|
this.set('showMappingErrors', false);
|
|
} else {
|
|
this.set('showMappingErrors', true);
|
|
}
|
|
},
|
|
|
|
reset() {
|
|
this.set('showMappingErrors', false);
|
|
this.set('errorMessage', null);
|
|
this.set('errorHeader', null);
|
|
this.set('file', null);
|
|
this.set('mapping', null);
|
|
this.set('state', 'INIT');
|
|
this.set('showTryAgainButton', true);
|
|
},
|
|
|
|
closeModal() {
|
|
if (this.state !== 'UPLOADING') {
|
|
this._super(...arguments);
|
|
}
|
|
},
|
|
|
|
// noop - we don't want the enter key doing anything
|
|
confirm() {}
|
|
},
|
|
|
|
generateRequest() {
|
|
let ajax = this.ajax;
|
|
let formData = this.formData;
|
|
let url = this.uploadUrl;
|
|
|
|
this.set('state', 'UPLOADING');
|
|
ajax.post(url, {
|
|
data: formData,
|
|
processData: false,
|
|
contentType: false,
|
|
dataType: 'text'
|
|
}).then((importResponse) => {
|
|
if (importResponse instanceof AcceptedResponse) {
|
|
this.set('state', 'PROCESSING');
|
|
} else {
|
|
this._uploadSuccess(JSON.parse(importResponse));
|
|
this.set('state', 'COMPLETE');
|
|
}
|
|
}).catch((error) => {
|
|
this._uploadError(error);
|
|
this.set('state', 'ERROR');
|
|
});
|
|
},
|
|
|
|
_uploadSuccess(importResponse) {
|
|
let importedCount = importResponse.meta.stats.imported;
|
|
const erroredMembers = importResponse.meta.stats.invalid;
|
|
let errorCount = erroredMembers.length;
|
|
const errorList = {};
|
|
|
|
const errorsWithFormattedMessages = erroredMembers.map((row) => {
|
|
const formattedError = row.error
|
|
.replace(
|
|
'Value in [members.email] cannot be blank.',
|
|
'Missing email address'
|
|
)
|
|
.replace(
|
|
'Value in [members.note] exceeds maximum length of 2000 characters.',
|
|
'Note is too long'
|
|
)
|
|
.replace(
|
|
'Value in [members.subscribed] must be one of true, false, 0 or 1.',
|
|
'Value of "Subscribed to emails" must be "true" or "false"'
|
|
)
|
|
.replace(
|
|
'Validation (isEmail) failed for email',
|
|
'Invalid email address'
|
|
)
|
|
.replace(
|
|
/No such customer:[^,]*/,
|
|
'Could not find Stripe customer'
|
|
);
|
|
formattedError.split(',').forEach((errorMssg) => {
|
|
if (errorList[errorMssg]) {
|
|
errorList[errorMssg].count = errorList[errorMssg].count + 1;
|
|
} else {
|
|
errorList[errorMssg] = {
|
|
message: errorMssg,
|
|
count: 1
|
|
};
|
|
}
|
|
});
|
|
return {
|
|
...row,
|
|
error: formattedError
|
|
};
|
|
});
|
|
|
|
let errorCsv = unparse(errorsWithFormattedMessages);
|
|
let errorCsvBlob = new Blob([errorCsv], {type: 'text/csv'});
|
|
let errorCsvUrl = URL.createObjectURL(errorCsvBlob);
|
|
let errorCsvName = importResponse.meta.import_label ? `${importResponse.meta.import_label.name} - Errors.csv` : `Import ${moment().format('YYYY-MM-DD HH:mm')} - Errors.csv`;
|
|
|
|
this.set('importResponse', {
|
|
importedCount,
|
|
errorCount,
|
|
errorCsvUrl,
|
|
errorCsvName,
|
|
errorList: Object.values(errorList)
|
|
});
|
|
|
|
// insert auto-created import label into store immediately if present
|
|
// ready for filtering the members list
|
|
if (importResponse.meta.import_label) {
|
|
this.store.pushPayload({
|
|
labels: [importResponse.meta.import_label]
|
|
});
|
|
}
|
|
|
|
// invoke the passed in confirm action to refresh member data
|
|
// @TODO wtf does confirm mean?
|
|
this.confirm({label: importResponse.meta.import_label});
|
|
},
|
|
|
|
_uploadError(error) {
|
|
let message;
|
|
let header = 'Import error';
|
|
|
|
if (isVersionMismatchError(error)) {
|
|
this.notifications.showAPIError(error);
|
|
}
|
|
|
|
if (isUnsupportedMediaTypeError(error)) {
|
|
message = 'The file type you uploaded is not supported.';
|
|
} else if (isRequestEntityTooLargeError(error)) {
|
|
message = 'The file you uploaded was larger than the maximum file size your server allows.';
|
|
} else if (error.payload && error.payload.errors && !isBlank(error.payload.errors[0].message)) {
|
|
message = htmlSafe(error.payload.errors[0].message);
|
|
|
|
if (error.payload.errors[0].message.match(/great deliverability/gi)) {
|
|
header = 'Woah there cowboy, that\'s a big list';
|
|
this.set('showTryAgainButton', false);
|
|
// NOTE: confirm makes sure to refresh the members data in the background
|
|
this.confirm();
|
|
}
|
|
} else {
|
|
console.error(error); // eslint-disable-line
|
|
message = 'Something went wrong :(';
|
|
}
|
|
|
|
this.set('errorMessage', message);
|
|
this.set('errorHeader', header);
|
|
}
|
|
});
|