2021-11-08 15:34:23 +03:00
|
|
|
import * as Sentry from '@sentry/browser';
|
2017-10-30 12:38:01 +03:00
|
|
|
import Service, {inject as service} from '@ember/service';
|
2021-05-12 14:33:36 +03:00
|
|
|
import {dasherize} from '@ember/string';
|
2017-08-22 10:53:26 +03:00
|
|
|
import {A as emberA, isArray as isEmberArray} from '@ember/array';
|
|
|
|
import {filter} from '@ember/object/computed';
|
2017-10-30 12:38:01 +03:00
|
|
|
import {get, set} from '@ember/object';
|
2021-05-12 14:33:36 +03:00
|
|
|
import {htmlSafe} from '@ember/template';
|
2017-08-22 10:53:26 +03:00
|
|
|
import {isBlank} from '@ember/utils';
|
2016-07-08 16:54:36 +03:00
|
|
|
import {
|
|
|
|
isMaintenanceError,
|
|
|
|
isVersionMismatchError
|
|
|
|
} from 'ghost-admin/services/ajax';
|
2016-01-18 18:37:14 +03:00
|
|
|
|
2015-10-07 17:44:23 +03:00
|
|
|
// Notification keys take the form of "noun.verb.message", eg:
|
|
|
|
//
|
|
|
|
// "invite.resend.api-error"
|
|
|
|
// "user.invite.already-invited"
|
|
|
|
//
|
|
|
|
// The "noun.verb" part will be used as the "key base" in duplicate checks
|
|
|
|
// to avoid stacking of multiple error messages whilst leaving enough
|
|
|
|
// specificity to re-use keys for i18n lookups
|
|
|
|
|
2015-10-28 14:36:45 +03:00
|
|
|
export default Service.extend({
|
2019-01-02 12:58:55 +03:00
|
|
|
delayedNotifications: null,
|
|
|
|
content: null,
|
|
|
|
|
|
|
|
init() {
|
|
|
|
this._super(...arguments);
|
|
|
|
this.delayedNotifications = emberA();
|
|
|
|
this.content = emberA();
|
|
|
|
},
|
2014-06-30 01:45:03 +04:00
|
|
|
|
2021-05-26 19:01:18 +03:00
|
|
|
config: service(),
|
2017-10-30 12:38:01 +03:00
|
|
|
upgradeStatus: service(),
|
2016-06-30 17:45:02 +03:00
|
|
|
|
2015-10-28 14:36:45 +03:00
|
|
|
alerts: filter('content', function (notification) {
|
|
|
|
let status = get(notification, 'status');
|
2015-06-19 00:56:18 +03:00
|
|
|
return status === 'alert';
|
|
|
|
}),
|
2014-07-09 08:17:30 +04:00
|
|
|
|
2015-10-28 14:36:45 +03:00
|
|
|
notifications: filter('content', function (notification) {
|
|
|
|
let status = get(notification, 'status');
|
2015-06-19 00:56:18 +03:00
|
|
|
return status === 'notification';
|
|
|
|
}),
|
2014-07-09 08:17:30 +04:00
|
|
|
|
2015-10-28 14:36:45 +03:00
|
|
|
handleNotification(message, delayed) {
|
2015-06-19 00:56:18 +03:00
|
|
|
// If this is an alert message from the server, treat it as html safe
|
2020-01-07 16:23:15 +03:00
|
|
|
if (message.constructor.modelName === 'notification' && message.get('status') === 'alert') {
|
2019-03-12 20:50:45 +03:00
|
|
|
message.set('message', htmlSafe(message.get('message')));
|
2014-07-09 08:17:30 +04:00
|
|
|
}
|
|
|
|
|
2015-10-28 14:36:45 +03:00
|
|
|
if (!get(message, 'status')) {
|
|
|
|
set(message, 'status', 'notification');
|
2014-06-30 01:45:03 +04:00
|
|
|
}
|
|
|
|
|
2015-10-07 17:44:23 +03:00
|
|
|
// close existing duplicate alerts/notifications to avoid stacking
|
2015-10-28 14:36:45 +03:00
|
|
|
if (get(message, 'key')) {
|
|
|
|
this._removeItems(get(message, 'status'), get(message, 'key'));
|
2015-10-07 17:44:23 +03:00
|
|
|
}
|
|
|
|
|
2019-03-12 20:50:45 +03:00
|
|
|
// close existing alerts/notifications which have the same text to avoid stacking
|
|
|
|
let newText = get(message, 'message').string || get(message, 'message');
|
|
|
|
this.set('content', this.content.reject((notification) => {
|
|
|
|
let existingText = get(notification, 'message').string || get(notification, 'message');
|
|
|
|
return existingText === newText;
|
|
|
|
}));
|
|
|
|
|
2014-06-24 14:00:28 +04:00
|
|
|
if (!delayed) {
|
2019-03-06 16:53:54 +03:00
|
|
|
this.content.pushObject(message);
|
2014-06-24 14:00:28 +04:00
|
|
|
} else {
|
2019-03-06 16:53:54 +03:00
|
|
|
this.delayedNotifications.pushObject(message);
|
2014-06-24 14:00:28 +04:00
|
|
|
}
|
|
|
|
},
|
2015-05-26 05:10:50 +03:00
|
|
|
|
2015-10-28 14:36:45 +03:00
|
|
|
showAlert(message, options) {
|
2014-08-01 09:40:49 +04:00
|
|
|
options = options || {};
|
|
|
|
|
2021-11-08 15:34:23 +03:00
|
|
|
if (!options.isApiError) {
|
|
|
|
if (this.config.get('sentry_dsn')) {
|
|
|
|
// message could be a htmlSafe object rather than a string
|
|
|
|
const displayedMessage = get(message, 'string') || message;
|
|
|
|
|
|
|
|
const contexts = {
|
|
|
|
ghost: {
|
|
|
|
displayed_message: displayedMessage,
|
|
|
|
ghost_error_code: get(options, 'ghostErrorCode'),
|
|
|
|
full_error: message,
|
|
|
|
source: 'showAlert'
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Sentry.captureException(displayedMessage, {
|
|
|
|
contexts,
|
|
|
|
tags: {
|
|
|
|
shown_to_user: true
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-19 00:56:18 +03:00
|
|
|
this.handleNotification({
|
2015-10-28 14:36:45 +03:00
|
|
|
message,
|
2015-06-19 00:56:18 +03:00
|
|
|
status: 'alert',
|
2020-02-27 12:19:29 +03:00
|
|
|
description: options.description,
|
|
|
|
icon: options.icon,
|
2015-10-07 17:44:23 +03:00
|
|
|
type: options.type,
|
2020-02-27 12:19:29 +03:00
|
|
|
key: options.key,
|
|
|
|
actions: options.actions
|
2015-06-19 00:56:18 +03:00
|
|
|
}, options.delayed);
|
|
|
|
},
|
|
|
|
|
2015-10-28 14:36:45 +03:00
|
|
|
showNotification(message, options) {
|
2015-06-19 00:56:18 +03:00
|
|
|
options = options || {};
|
|
|
|
|
2014-06-24 14:00:28 +04:00
|
|
|
this.handleNotification({
|
2015-10-28 14:36:45 +03:00
|
|
|
message,
|
2015-06-19 00:56:18 +03:00
|
|
|
status: 'notification',
|
2020-02-27 12:19:29 +03:00
|
|
|
description: options.description,
|
|
|
|
icon: options.icon,
|
2015-10-07 17:44:23 +03:00
|
|
|
type: options.type,
|
2020-02-27 12:19:29 +03:00
|
|
|
key: options.key,
|
|
|
|
actions: options.actions
|
2014-08-01 09:40:49 +04:00
|
|
|
}, options.delayed);
|
2014-03-22 16:08:15 +04:00
|
|
|
},
|
2015-05-26 05:10:50 +03:00
|
|
|
|
2015-10-28 14:36:45 +03:00
|
|
|
showAPIError(resp, options) {
|
2016-06-30 17:45:02 +03:00
|
|
|
// handle "global" errors
|
|
|
|
if (isVersionMismatchError(resp)) {
|
2019-03-06 16:53:54 +03:00
|
|
|
return this.upgradeStatus.requireUpgrade();
|
2016-07-08 16:54:36 +03:00
|
|
|
} else if (isMaintenanceError(resp)) {
|
2019-03-06 16:53:54 +03:00
|
|
|
return this.upgradeStatus.maintenanceAlert();
|
2016-06-30 17:45:02 +03:00
|
|
|
}
|
|
|
|
|
2016-09-26 19:59:04 +03:00
|
|
|
// loop over ember-ajax errors object
|
2017-11-04 01:59:39 +03:00
|
|
|
if (resp && resp.payload && isEmberArray(resp.payload.errors)) {
|
|
|
|
return resp.payload.errors.forEach((error) => {
|
2016-06-30 17:45:02 +03:00
|
|
|
this._showAPIError(error, options);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
this._showAPIError(resp, options);
|
|
|
|
},
|
|
|
|
|
|
|
|
_showAPIError(resp, options) {
|
2014-08-01 09:40:49 +04:00
|
|
|
options = options || {};
|
2015-06-19 00:56:18 +03:00
|
|
|
options.type = options.type || 'error';
|
2014-08-01 09:40:49 +04:00
|
|
|
|
2016-06-14 14:46:24 +03:00
|
|
|
// if possible use the title to get a unique key
|
|
|
|
// - we only show one alert for each key so if we get multiple errors
|
|
|
|
// only the last one will be shown
|
|
|
|
if (!options.key && !isBlank(get(resp, 'title'))) {
|
|
|
|
options.key = dasherize(get(resp, 'title'));
|
2014-08-01 09:40:49 +04:00
|
|
|
}
|
2016-06-14 14:46:24 +03:00
|
|
|
options.key = ['api-error', options.key].compact().join('.');
|
2014-08-01 09:40:49 +04:00
|
|
|
|
2016-06-14 14:46:24 +03:00
|
|
|
let msg = options.defaultErrorText || 'There was a problem on the server, please try again.';
|
2014-05-15 03:36:13 +04:00
|
|
|
|
2016-06-14 14:46:24 +03:00
|
|
|
if (resp instanceof String) {
|
|
|
|
msg = resp;
|
|
|
|
} else if (!isBlank(get(resp, 'detail'))) {
|
|
|
|
msg = resp.detail;
|
|
|
|
} else if (!isBlank(get(resp, 'message'))) {
|
|
|
|
msg = resp.message;
|
2014-08-01 09:40:49 +04:00
|
|
|
}
|
2016-06-14 14:46:24 +03:00
|
|
|
|
2019-03-06 14:45:47 +03:00
|
|
|
if (!isBlank(get(resp, 'context'))) {
|
|
|
|
msg = `${msg} ${get(resp, 'context')}`;
|
|
|
|
}
|
|
|
|
|
2021-11-08 15:34:23 +03:00
|
|
|
if (this.config.get('sentry_dsn')) {
|
|
|
|
const reportedError = resp instanceof Error ? resp : msg;
|
|
|
|
|
|
|
|
Sentry.captureException(reportedError, {
|
|
|
|
contexts: {
|
|
|
|
ghost: {
|
|
|
|
ghost_error_code: get(resp, 'ghostErrorCode'),
|
|
|
|
displayed_message: msg,
|
|
|
|
full_error: resp,
|
|
|
|
source: 'showAPIError'
|
|
|
|
}
|
|
|
|
},
|
|
|
|
tags: {
|
|
|
|
shown_to_user: true
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
options.isApiError = true;
|
|
|
|
|
2016-06-14 14:46:24 +03:00
|
|
|
this.showAlert(msg, options);
|
2014-06-24 14:00:28 +04:00
|
|
|
},
|
2015-05-26 05:10:50 +03:00
|
|
|
|
2015-10-28 14:36:45 +03:00
|
|
|
displayDelayed() {
|
|
|
|
this.delayedNotifications.forEach((message) => {
|
2019-03-06 16:53:54 +03:00
|
|
|
this.content.pushObject(message);
|
2014-03-22 16:08:15 +04:00
|
|
|
});
|
2019-03-25 15:28:14 +03:00
|
|
|
this.set('delayedNotifications', []);
|
2014-06-19 23:44:44 +04:00
|
|
|
},
|
2015-05-26 05:10:50 +03:00
|
|
|
|
2015-10-28 14:36:45 +03:00
|
|
|
closeNotification(notification) {
|
2019-03-06 16:53:54 +03:00
|
|
|
let content = this.content;
|
2014-06-30 01:45:03 +04:00
|
|
|
|
2020-01-07 16:23:15 +03:00
|
|
|
if (notification.constructor.modelName === 'notification') {
|
2014-06-30 01:45:03 +04:00
|
|
|
notification.deleteRecord();
|
2015-10-28 14:36:45 +03:00
|
|
|
notification.save().finally(() => {
|
2015-05-26 05:10:50 +03:00
|
|
|
content.removeObject(notification);
|
2014-06-30 01:45:03 +04:00
|
|
|
});
|
|
|
|
} else {
|
2015-05-26 05:10:50 +03:00
|
|
|
content.removeObject(notification);
|
2014-06-30 01:45:03 +04:00
|
|
|
}
|
|
|
|
},
|
2015-05-26 05:10:50 +03:00
|
|
|
|
2015-10-28 14:36:45 +03:00
|
|
|
closeNotifications(key) {
|
2015-10-07 17:44:23 +03:00
|
|
|
this._removeItems('notification', key);
|
|
|
|
},
|
|
|
|
|
2015-10-28 14:36:45 +03:00
|
|
|
closeAlerts(key) {
|
2015-10-07 17:44:23 +03:00
|
|
|
this._removeItems('alert', key);
|
2014-06-30 01:45:03 +04:00
|
|
|
},
|
2015-05-26 05:10:50 +03:00
|
|
|
|
2015-10-28 14:36:45 +03:00
|
|
|
clearAll() {
|
2019-03-06 16:53:54 +03:00
|
|
|
this.content.clear();
|
2015-10-07 17:44:23 +03:00
|
|
|
},
|
|
|
|
|
2015-10-28 14:36:45 +03:00
|
|
|
_removeItems(status, key) {
|
2015-10-07 17:44:23 +03:00
|
|
|
if (key) {
|
2015-10-28 14:36:45 +03:00
|
|
|
let keyBase = this._getKeyBase(key);
|
|
|
|
// TODO: keys should only have . special char but we should
|
|
|
|
// probably use a better regexp escaping function/polyfill
|
|
|
|
let escapedKeyBase = keyBase.replace('.', '\\.');
|
|
|
|
let keyRegex = new RegExp(`^${escapedKeyBase}`);
|
2015-10-07 17:44:23 +03:00
|
|
|
|
2019-03-06 16:53:54 +03:00
|
|
|
this.set('content', this.content.reject((item) => {
|
2015-10-28 14:36:45 +03:00
|
|
|
let itemKey = get(item, 'key');
|
|
|
|
let itemStatus = get(item, 'status');
|
2015-10-07 17:44:23 +03:00
|
|
|
|
|
|
|
return itemStatus === status && (itemKey && itemKey.match(keyRegex));
|
|
|
|
}));
|
|
|
|
} else {
|
2019-03-06 16:53:54 +03:00
|
|
|
this.set('content', this.content.rejectBy('status', status));
|
2015-10-07 17:44:23 +03:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// take a key and return the first two elements, eg:
|
|
|
|
// "invite.revoke.failed" => "invite.revoke"
|
2015-10-28 14:36:45 +03:00
|
|
|
_getKeyBase(key) {
|
2015-10-07 17:44:23 +03:00
|
|
|
return key.split('.').slice(0, 2).join('.');
|
2014-03-22 16:08:15 +04:00
|
|
|
}
|
|
|
|
});
|