2019-10-04 12:33:10 +03:00
|
|
|
import ModalComponent from 'ghost-admin/components/modal-base';
|
|
|
|
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
2022-09-23 20:15:08 +03:00
|
|
|
import moment from 'moment-timezone';
|
2020-12-09 22:32:31 +03:00
|
|
|
import unparse from '@tryghost/members-csv/lib/unparse';
|
2020-06-05 13:57:07 +03:00
|
|
|
import {
|
2020-12-09 22:32:31 +03:00
|
|
|
AcceptedResponse,
|
2022-11-17 22:41:39 +03:00
|
|
|
isDataImportError,
|
|
|
|
isHostLimitError,
|
2020-06-05 13:57:07 +03:00
|
|
|
isRequestEntityTooLargeError,
|
|
|
|
isUnsupportedMediaTypeError,
|
|
|
|
isVersionMismatchError
|
|
|
|
} from 'ghost-admin/services/ajax';
|
2019-10-04 12:33:10 +03:00
|
|
|
import {computed} from '@ember/object';
|
2021-05-12 14:33:36 +03:00
|
|
|
import {htmlSafe} from '@ember/template';
|
2022-11-03 14:14:36 +03:00
|
|
|
import {inject} from 'ghost-admin/decorators/inject';
|
2020-06-05 13:57:07 +03:00
|
|
|
import {inject as service} from '@ember/service';
|
2019-10-04 12:33:10 +03:00
|
|
|
|
|
|
|
export default ModalComponent.extend({
|
2020-06-05 13:57:07 +03:00
|
|
|
ajax: service(),
|
|
|
|
notifications: service(),
|
2020-07-23 16:14:50 +03:00
|
|
|
store: service(),
|
2020-06-05 13:57:07 +03:00
|
|
|
|
2020-12-09 22:32:31 +03:00
|
|
|
state: 'INIT',
|
2020-07-09 08:31:28 +03:00
|
|
|
|
2020-06-05 13:57:07 +03:00
|
|
|
file: null,
|
2020-12-09 22:32:31 +03:00
|
|
|
mappingResult: null,
|
2020-12-10 14:03:56 +03:00
|
|
|
mappingFileData: null,
|
2020-06-05 13:57:07 +03:00
|
|
|
paramName: 'membersfile',
|
2020-06-12 11:01:46 +03:00
|
|
|
importResponse: null,
|
2020-12-09 22:32:31 +03:00
|
|
|
errorMessage: null,
|
2021-07-27 15:19:26 +03:00
|
|
|
errorHeader: null,
|
2020-12-09 22:32:31 +03:00
|
|
|
showMappingErrors: false,
|
2021-08-19 20:19:23 +03:00
|
|
|
showTryAgainButton: true,
|
2019-10-04 12:33:10 +03:00
|
|
|
|
|
|
|
// Allowed actions
|
|
|
|
confirm: () => {},
|
|
|
|
|
2022-11-03 14:14:36 +03:00
|
|
|
config: inject(),
|
|
|
|
|
2019-10-04 12:33:10 +03:00
|
|
|
uploadUrl: computed(function () {
|
2020-06-16 09:13:06 +03:00
|
|
|
return `${ghostPaths().apiRoot}/members/upload/`;
|
2019-10-04 12:33:10 +03:00
|
|
|
}),
|
|
|
|
|
2020-06-05 13:57:07 +03:00
|
|
|
formData: computed('file', function () {
|
|
|
|
let formData = new FormData();
|
|
|
|
|
2020-12-09 22:32:31 +03:00
|
|
|
formData.append(this.paramName, this.file);
|
2020-06-05 13:57:07 +03:00
|
|
|
|
2020-12-09 22:32:31 +03:00
|
|
|
if (this.mappingResult.labels) {
|
|
|
|
this.mappingResult.labels.forEach((label) => {
|
2020-06-05 13:57:07 +03:00
|
|
|
formData.append('labels', label.name);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-12-09 22:32:31 +03:00
|
|
|
if (this.mappingResult.mapping) {
|
|
|
|
let mapping = this.mappingResult.mapping.toJSON();
|
|
|
|
for (let [key, val] of Object.entries(mapping)) {
|
|
|
|
formData.append(`mapping[${key}]`, val);
|
2020-07-03 07:54:21 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-05 13:57:07 +03:00
|
|
|
return formData;
|
|
|
|
}),
|
|
|
|
|
2019-10-04 12:33:10 +03:00
|
|
|
actions: {
|
2020-12-09 22:32:31 +03:00
|
|
|
setFile(file) {
|
|
|
|
this.set('file', file);
|
|
|
|
this.set('state', 'MAPPING');
|
2019-10-04 12:33:10 +03:00
|
|
|
},
|
|
|
|
|
2020-12-09 22:32:31 +03:00
|
|
|
setMappingResult(mappingResult) {
|
|
|
|
this.set('mappingResult', mappingResult);
|
2019-10-04 12:33:10 +03:00
|
|
|
},
|
|
|
|
|
2020-12-10 14:03:56 +03:00
|
|
|
setMappingFileData(mappingFileData) {
|
|
|
|
this.set('mappingFileData', mappingFileData);
|
|
|
|
},
|
|
|
|
|
2020-06-05 13:57:07 +03:00
|
|
|
upload() {
|
2020-12-09 22:32:31 +03:00
|
|
|
if (this.file && !this.mappingResult.error) {
|
2020-07-13 11:30:32 +03:00
|
|
|
this.generateRequest();
|
2020-12-09 22:32:31 +03:00
|
|
|
this.set('showMappingErrors', false);
|
2020-07-13 14:55:07 +03:00
|
|
|
} else {
|
2020-12-09 22:32:31 +03:00
|
|
|
this.set('showMappingErrors', true);
|
2020-06-05 13:57:07 +03:00
|
|
|
}
|
2019-10-04 12:33:10 +03:00
|
|
|
},
|
|
|
|
|
2020-12-09 22:32:31 +03:00
|
|
|
reset() {
|
|
|
|
this.set('showMappingErrors', false);
|
|
|
|
this.set('errorMessage', null);
|
2021-07-27 15:19:26 +03:00
|
|
|
this.set('errorHeader', null);
|
2020-12-09 22:32:31 +03:00
|
|
|
this.set('file', null);
|
|
|
|
this.set('mapping', null);
|
|
|
|
this.set('state', 'INIT');
|
2021-08-19 20:19:23 +03:00
|
|
|
this.set('showTryAgainButton', true);
|
2019-10-04 12:33:10 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
closeModal() {
|
2020-12-09 22:32:31 +03:00
|
|
|
if (this.state !== 'UPLOADING') {
|
2019-10-04 12:33:10 +03:00
|
|
|
this._super(...arguments);
|
|
|
|
}
|
2020-07-03 07:54:21 +03:00
|
|
|
},
|
|
|
|
|
2020-12-09 22:32:31 +03:00
|
|
|
// noop - we don't want the enter key doing anything
|
|
|
|
confirm() {}
|
2020-06-05 13:57:07 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
generateRequest() {
|
|
|
|
let ajax = this.ajax;
|
|
|
|
let formData = this.formData;
|
|
|
|
let url = this.uploadUrl;
|
|
|
|
|
2020-12-09 22:32:31 +03:00
|
|
|
this.set('state', 'UPLOADING');
|
2020-06-05 13:57:07 +03:00
|
|
|
ajax.post(url, {
|
|
|
|
data: formData,
|
|
|
|
processData: false,
|
|
|
|
contentType: false,
|
2020-08-27 15:32:26 +03:00
|
|
|
dataType: 'text'
|
2020-06-12 11:01:46 +03:00
|
|
|
}).then((importResponse) => {
|
2020-12-09 22:32:31 +03:00
|
|
|
if (importResponse instanceof AcceptedResponse) {
|
|
|
|
this.set('state', 'PROCESSING');
|
|
|
|
} else {
|
|
|
|
this._uploadSuccess(JSON.parse(importResponse));
|
|
|
|
this.set('state', 'COMPLETE');
|
|
|
|
}
|
2020-06-05 13:57:07 +03:00
|
|
|
}).catch((error) => {
|
2020-12-09 22:32:31 +03:00
|
|
|
this._uploadError(error);
|
|
|
|
this.set('state', 'ERROR');
|
2020-06-05 13:57:07 +03:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2020-06-12 11:01:46 +03:00
|
|
|
_uploadSuccess(importResponse) {
|
2020-12-09 22:32:31 +03:00
|
|
|
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.',
|
2020-12-10 14:15:07 +03:00
|
|
|
'Missing email address'
|
2020-12-09 22:32:31 +03:00
|
|
|
)
|
|
|
|
.replace(
|
|
|
|
'Value in [members.note] exceeds maximum length of 2000 characters.',
|
2020-12-10 14:15:07 +03:00
|
|
|
'Note is too long'
|
2020-12-09 22:32:31 +03:00
|
|
|
)
|
|
|
|
.replace(
|
|
|
|
'Value in [members.subscribed] must be one of true, false, 0 or 1.',
|
2020-12-10 14:03:56 +03:00
|
|
|
'Value of "Subscribed to emails" must be "true" or "false"'
|
2020-12-09 22:32:31 +03:00
|
|
|
)
|
|
|
|
.replace(
|
|
|
|
'Validation (isEmail) failed for email',
|
2020-12-10 14:15:07 +03:00
|
|
|
'Invalid email address'
|
2020-12-09 22:32:31 +03:00
|
|
|
)
|
|
|
|
.replace(
|
|
|
|
/No such customer:[^,]*/,
|
2020-12-10 14:15:07 +03:00
|
|
|
'Could not find Stripe customer'
|
2020-12-09 22:32:31 +03:00
|
|
|
);
|
|
|
|
formattedError.split(',').forEach((errorMssg) => {
|
|
|
|
if (errorList[errorMssg]) {
|
|
|
|
errorList[errorMssg].count = errorList[errorMssg].count + 1;
|
|
|
|
} else {
|
|
|
|
errorList[errorMssg] = {
|
|
|
|
message: errorMssg,
|
|
|
|
count: 1
|
|
|
|
};
|
2020-07-09 09:39:42 +03:00
|
|
|
}
|
|
|
|
});
|
2020-12-09 22:32:31 +03:00
|
|
|
return {
|
|
|
|
...row,
|
|
|
|
error: formattedError
|
|
|
|
};
|
|
|
|
});
|
2020-07-09 09:39:42 +03:00
|
|
|
|
2020-12-09 22:32:31 +03:00
|
|
|
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)
|
|
|
|
});
|
2020-07-23 16:14:50 +03:00
|
|
|
|
|
|
|
// 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]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-06-05 13:57:07 +03:00
|
|
|
// invoke the passed in confirm action to refresh member data
|
2020-12-09 22:32:31 +03:00
|
|
|
// @TODO wtf does confirm mean?
|
2020-07-23 16:14:50 +03:00
|
|
|
this.confirm({label: importResponse.meta.import_label});
|
2020-06-05 13:57:07 +03:00
|
|
|
},
|
|
|
|
|
2020-12-09 22:32:31 +03:00
|
|
|
_uploadError(error) {
|
2020-06-05 13:57:07 +03:00
|
|
|
let message;
|
2021-07-27 15:19:26 +03:00
|
|
|
let header = 'Import error';
|
2020-06-05 13:57:07 +03:00
|
|
|
|
|
|
|
if (isVersionMismatchError(error)) {
|
|
|
|
this.notifications.showAPIError(error);
|
|
|
|
}
|
|
|
|
|
2022-11-17 22:41:39 +03:00
|
|
|
// Handle all the specific errors that we know about
|
2020-06-05 13:57:07 +03:00
|
|
|
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.';
|
2022-11-17 22:41:39 +03:00
|
|
|
} else if (isDataImportError(error, error.payload)) {
|
|
|
|
message = htmlSafe(error.payload.errors[0].message);
|
|
|
|
} else if (isHostLimitError(error) && error?.payload?.errors?.[0]?.code === 'EMAIL_VERIFICATION_NEEDED') {
|
2020-06-05 13:57:07 +03:00
|
|
|
message = htmlSafe(error.payload.errors[0].message);
|
2021-07-27 15:19:26 +03:00
|
|
|
|
2022-11-17 22:41:39 +03:00
|
|
|
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 { // Generic fallback error
|
|
|
|
message = 'An unexpected error occurred, please try again';
|
|
|
|
|
2020-07-23 16:14:50 +03:00
|
|
|
console.error(error); // eslint-disable-line
|
2022-11-17 22:41:39 +03:00
|
|
|
if (error?.payload?.errors?.[0]?.id) {
|
|
|
|
console.error(`Error ID: ${error.payload.errors[0].id}`); // eslint-disable-line
|
|
|
|
}
|
2020-06-05 13:57:07 +03:00
|
|
|
}
|
|
|
|
|
2020-12-09 22:32:31 +03:00
|
|
|
this.set('errorMessage', message);
|
2021-07-27 15:19:26 +03:00
|
|
|
this.set('errorHeader', header);
|
2019-10-04 12:33:10 +03:00
|
|
|
}
|
|
|
|
});
|