mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-25 02:31:59 -05:00
Migrated notifications to Octane patterns
no issue - migrated `notifications` service from EmberObject to true native class - switched to tracked properties and use of `TrackedArray` - swapped computed properties to getters - dropped unnecessary usage of `get` and `set` - migrated alert/notification related components to Glimmer components
This commit is contained in:
parent
0c07e26cc4
commit
e021843e3f
10 changed files with 105 additions and 152 deletions
|
@ -1216,3 +1216,5 @@ add|ember-template-lint|no-action|20|150|20|150|d6149e8bd18677704c261b7d3e9afeaf
|
|||
remove|ember-template-lint|no-action|20|150|20|150|d07a2c2968b7c337172eb3d189fcd004f05034d7|1652054400000|1662422400000|1665014400000|app/components/modal-portal-settings.hbs
|
||||
remove|ember-template-lint|no-action|2|17|2|17|c31e149422ee6e80dcaf0561a993a93ecd166c90|1652054400000|1662422400000|1665014400000|app/components/gh-canvas-header.hbs
|
||||
remove|ember-template-lint|no-action|3|19|3|19|80972d8846ff5e8dd8ef7cc359093dfd822a3dd3|1652054400000|1662422400000|1665014400000|app/components/gh-canvas-header.hbs
|
||||
remove|ember-template-lint|no-action|4|83|4|83|671e1cde0a4afb412c681bb728239241c2cc822f|1652054400000|1662422400000|1665014400000|app/components/gh-alert.hbs
|
||||
remove|ember-template-lint|no-action|27|90|27|90|671e1cde0a4afb412c681bb728239241c2cc822f|1652054400000|1662422400000|1665014400000|app/components/gh-notification.hbs
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<div class="gh-alert-content">
|
||||
{{this.message.message}}
|
||||
</div>
|
||||
<button class="gh-alert-close" data-test-button="close-notification" type="button" {{action "closeNotification"}}>
|
||||
{{svg-jar "close-stroke"}}<span class="hidden">Close</span>
|
||||
</button>
|
||||
<article class="gh-alert {{this.typeClass}}" ...attributes>
|
||||
<div class="gh-alert-content">
|
||||
{{@message.message}}
|
||||
</div>
|
||||
<button class="gh-alert-close" data-test-button="close-notification" type="button" {{on "click" this.closeNotification}}>
|
||||
{{svg-jar "close-stroke"}}<span class="hidden">Close</span>
|
||||
</button>
|
||||
</article>
|
|
@ -1,19 +1,12 @@
|
|||
import Component from '@ember/component';
|
||||
import classic from 'ember-classic-decorator';
|
||||
import {action, computed} from '@ember/object';
|
||||
import {classNameBindings, classNames, tagName} from '@ember-decorators/component';
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
@classic
|
||||
@classNameBindings('typeClass')
|
||||
@classNames('gh-alert')
|
||||
@tagName('article')
|
||||
export default class GhAlert extends Component {
|
||||
@service notifications;
|
||||
|
||||
@computed('message.type')
|
||||
get typeClass() {
|
||||
let type = this.get('message.type');
|
||||
let type = this.args.message.type;
|
||||
let classes = '';
|
||||
let typeMapping;
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
{{#each this.messages as |message|}}
|
||||
<GhAlert @message={{message}} />
|
||||
{{/each}}
|
||||
<aside class="gh-alerts" ...attributes>
|
||||
{{#each this.notifications.alerts as |message|}}
|
||||
<GhAlert @message={{message}} />
|
||||
{{/each}}
|
||||
</aside>
|
|
@ -1,15 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
import classic from 'ember-classic-decorator';
|
||||
import {alias} from '@ember/object/computed';
|
||||
import {classNames, tagName} from '@ember-decorators/component';
|
||||
import Component from '@glimmer/component';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
@classic
|
||||
@classNames('gh-alerts')
|
||||
@tagName('aside')
|
||||
export default class GhAlerts extends Component {
|
||||
@service notifications;
|
||||
|
||||
@alias('notifications.alerts')
|
||||
messages;
|
||||
}
|
||||
|
|
|
@ -1,29 +1,31 @@
|
|||
<div class="gh-notification-icon">
|
||||
{{#if this.message.icon}}
|
||||
{{svg-jar this.message.icon}}
|
||||
{{else}}
|
||||
{{#if (eq this.message.type "success")}}
|
||||
{{svg-jar "check-circle"}}
|
||||
{{else if (eq this.message.type "error")}}
|
||||
{{svg-jar "warning-stroke"}}
|
||||
{{else if (eq this.message.type "warn")}}
|
||||
{{svg-jar "warning-stroke"}}
|
||||
<article class="gh-notification gh-notification-passive {{this.typeClass}}" {{on "animationend" this.closeOnFadeOut}} ...attributes>
|
||||
<div class="gh-notification-icon">
|
||||
{{#if @message.icon}}
|
||||
{{svg-jar @message.icon}}
|
||||
{{else}}
|
||||
{{svg-jar "check-circle"}}
|
||||
{{#if (eq @message.type "success")}}
|
||||
{{svg-jar "check-circle"}}
|
||||
{{else if (eq @message.type "error")}}
|
||||
{{svg-jar "warning-stroke"}}
|
||||
{{else if (eq @message.type "warn")}}
|
||||
{{svg-jar "warning-stroke"}}
|
||||
{{else}}
|
||||
{{svg-jar "check-circle"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-notification-content" data-test-text="notification-content">
|
||||
<span class="gh-notification-title">{{this.message.message}}</span>
|
||||
|
||||
{{#if this.message.description}}
|
||||
<p>{{this.message.description}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-notification-content" data-test-text="notification-content">
|
||||
<span class="gh-notification-title">{{@message.message}}</span>
|
||||
|
||||
{{#if this.message.actions}}
|
||||
<span class="gh-notification-actions">{{this.message.actions}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<button class="gh-notification-close" data-test-button="close-notification" type="button" {{action "closeNotification"}}>
|
||||
{{svg-jar "close"}}<span class="hidden">Close</span>
|
||||
</button>
|
||||
{{#if @message.description}}
|
||||
<p>{{@message.description}}</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if @message.actions}}
|
||||
<span class="gh-notification-actions">{{@message.actions}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<button class="gh-notification-close" data-test-button="close-notification" type="button" {{on "click" this.closeNotification}}>
|
||||
{{svg-jar "close"}}<span class="hidden">Close</span>
|
||||
</button>
|
||||
</article>
|
|
@ -1,30 +1,20 @@
|
|||
import Component from '@ember/component';
|
||||
import classic from 'ember-classic-decorator';
|
||||
import {action, computed} from '@ember/object';
|
||||
import {classNameBindings, classNames, tagName} from '@ember-decorators/component';
|
||||
import {run} from '@ember/runloop';
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
@classic
|
||||
@tagName('article')
|
||||
@classNames('gh-notification', 'gh-notification-passive')
|
||||
@classNameBindings('typeClass')
|
||||
export default class GhNotification extends Component {
|
||||
@service notifications;
|
||||
|
||||
message = null;
|
||||
|
||||
@computed('message.type')
|
||||
get typeClass() {
|
||||
let type = this.get('message.type');
|
||||
let classes = '';
|
||||
let typeMapping;
|
||||
|
||||
typeMapping = {
|
||||
const typeMapping = {
|
||||
error: 'red',
|
||||
warn: 'yellow'
|
||||
};
|
||||
|
||||
const type = this.args.message.type;
|
||||
let classes = '';
|
||||
if (typeMapping[type] !== undefined) {
|
||||
classes += `gh-notification-${typeMapping[type]}`;
|
||||
}
|
||||
|
@ -32,25 +22,15 @@ export default class GhNotification extends Component {
|
|||
return classes;
|
||||
}
|
||||
|
||||
didInsertElement() {
|
||||
super.didInsertElement(...arguments);
|
||||
|
||||
this._animationEndHandler = run.bind(this, function () {
|
||||
if (event.animationName === 'fade-out') {
|
||||
this.notifications.closeNotification(this.message);
|
||||
}
|
||||
});
|
||||
|
||||
this.element.addEventListener('animationend', this._animationEndHandler);
|
||||
}
|
||||
|
||||
willDestroyElement() {
|
||||
super.willDestroyElement(...arguments);
|
||||
this.element.removeEventListener('animationend', this._animationEndHandler);
|
||||
@action
|
||||
closeOnFadeOut(event) {
|
||||
if (event.animationName === 'fade-out') {
|
||||
this.closeNotification();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
closeNotification() {
|
||||
this.notifications.closeNotification(this.message);
|
||||
this.notifications.closeNotification(this.args.message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
{{#each this.messages as |message|}}
|
||||
<GhNotification @message={{message}} />
|
||||
{{/each}}
|
||||
<aside class="gh-notifications" ...attributes>
|
||||
{{#each this.notifications.notifications as |message|}}
|
||||
<GhNotification @message={{message}} />
|
||||
{{/each}}
|
||||
</aside>
|
|
@ -1,15 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
import classic from 'ember-classic-decorator';
|
||||
import {alias} from '@ember/object/computed';
|
||||
import {classNames, tagName} from '@ember-decorators/component';
|
||||
import Component from '@glimmer/component';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
@classic
|
||||
@tagName('aside')
|
||||
@classNames('gh-notifications')
|
||||
export default class GhNotifications extends Component {
|
||||
@service notifications;
|
||||
|
||||
@alias('notifications.notifications')
|
||||
messages;
|
||||
}
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import * as Sentry from '@sentry/browser';
|
||||
import Service, {inject as service} from '@ember/service';
|
||||
import classic from 'ember-classic-decorator';
|
||||
import {TrackedArray} from 'tracked-built-ins';
|
||||
import {dasherize} from '@ember/string';
|
||||
import {A as emberA, isArray as isEmberArray} from '@ember/array';
|
||||
import {filter} from '@ember/object/computed';
|
||||
import {get, set} from '@ember/object';
|
||||
import {htmlSafe} from '@ember/template';
|
||||
import {isArray} from '@ember/array';
|
||||
import {isBlank} from '@ember/utils';
|
||||
import {
|
||||
isMaintenanceError,
|
||||
isVersionMismatchError
|
||||
} from 'ghost-admin/services/ajax';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
// Notification keys take the form of "noun.verb.message", eg:
|
||||
//
|
||||
|
@ -21,73 +20,62 @@ import {
|
|||
// to avoid stacking of multiple error messages whilst leaving enough
|
||||
// specificity to re-use keys for i18n lookups
|
||||
|
||||
@classic
|
||||
export default class NotificationsService extends Service {
|
||||
delayedNotifications = null;
|
||||
content = null;
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
this.delayedNotifications = emberA();
|
||||
this.content = emberA();
|
||||
}
|
||||
|
||||
@service config;
|
||||
@service upgradeStatus;
|
||||
|
||||
@filter('content', function (notification) {
|
||||
let status = get(notification, 'status');
|
||||
return status === 'alert';
|
||||
})
|
||||
alerts;
|
||||
@tracked delayedNotifications = new TrackedArray([]);
|
||||
@tracked content = new TrackedArray([]);
|
||||
|
||||
@filter('content', function (notification) {
|
||||
let status = get(notification, 'status');
|
||||
return status === 'notification';
|
||||
})
|
||||
notifications;
|
||||
get alerts() {
|
||||
return this.content.filter(n => n.status === 'alert');
|
||||
}
|
||||
|
||||
get notifications() {
|
||||
return this.content.filter(n => n.status === 'notification');
|
||||
}
|
||||
|
||||
handleNotification(message, delayed) {
|
||||
// If this is an alert message from the server, treat it as html safe
|
||||
if (message.constructor.modelName === 'notification' && message.get('status') === 'alert') {
|
||||
message.set('message', htmlSafe(message.get('message')));
|
||||
if (message.constructor.modelName === 'notification' && message.status === 'alert') {
|
||||
message.message = htmlSafe(message.message);
|
||||
}
|
||||
|
||||
if (!get(message, 'status')) {
|
||||
set(message, 'status', 'notification');
|
||||
if (!message.status) {
|
||||
message.status = 'notification';
|
||||
}
|
||||
|
||||
// close existing duplicate alerts/notifications to avoid stacking
|
||||
if (get(message, 'key')) {
|
||||
this._removeItems(get(message, 'status'), get(message, 'key'));
|
||||
if (message.key) {
|
||||
this._removeItems(message.status, message.key);
|
||||
}
|
||||
|
||||
// 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');
|
||||
let newText = message.message.string || message.message;
|
||||
this.content = new TrackedArray(this.content.reject((notification) => {
|
||||
let existingText = notification.message.string || notification.message;
|
||||
return existingText === newText;
|
||||
}));
|
||||
|
||||
if (!delayed) {
|
||||
this.content.pushObject(message);
|
||||
this.content.push(message);
|
||||
} else {
|
||||
this.delayedNotifications.pushObject(message);
|
||||
this.delayedNotifications.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
showAlert(message, options) {
|
||||
showAlert(message, options = {}) {
|
||||
options = options || {};
|
||||
|
||||
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 displayedMessage = message.string || message;
|
||||
|
||||
const contexts = {
|
||||
ghost: {
|
||||
displayed_message: displayedMessage,
|
||||
ghost_error_code: get(options, 'ghostErrorCode'),
|
||||
ghost_error_code: options.ghostErrorCode,
|
||||
full_error: message,
|
||||
source: 'showAlert'
|
||||
}
|
||||
|
@ -136,7 +124,7 @@ export default class NotificationsService extends Service {
|
|||
}
|
||||
|
||||
// loop over ember-ajax errors object
|
||||
if (resp && resp.payload && isEmberArray(resp.payload.errors)) {
|
||||
if (resp && resp.payload && isArray(resp.payload.errors)) {
|
||||
return resp.payload.errors.forEach((error) => {
|
||||
this._showAPIError(error, options);
|
||||
});
|
||||
|
@ -152,8 +140,8 @@ export default class NotificationsService extends Service {
|
|||
// 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'));
|
||||
if (!options.key && !isBlank(resp?.title)) {
|
||||
options.key = dasherize(resp?.title);
|
||||
}
|
||||
options.key = ['api-error', options.key].compact().join('.');
|
||||
|
||||
|
@ -161,14 +149,14 @@ export default class NotificationsService extends Service {
|
|||
|
||||
if (resp instanceof String) {
|
||||
msg = resp;
|
||||
} else if (!isBlank(get(resp, 'detail'))) {
|
||||
} else if (!isBlank(resp?.detail)) {
|
||||
msg = resp.detail;
|
||||
} else if (!isBlank(get(resp, 'message'))) {
|
||||
} else if (!isBlank(resp?.message)) {
|
||||
msg = resp.message;
|
||||
}
|
||||
|
||||
if (!isBlank(get(resp, 'context'))) {
|
||||
msg = `${msg} ${get(resp, 'context')}`;
|
||||
if (!isBlank(resp?.context)) {
|
||||
msg = `${msg} ${resp.context}`;
|
||||
}
|
||||
|
||||
if (this.config.get('sentry_dsn')) {
|
||||
|
@ -177,7 +165,7 @@ export default class NotificationsService extends Service {
|
|||
Sentry.captureException(reportedError, {
|
||||
contexts: {
|
||||
ghost: {
|
||||
ghost_error_code: get(resp, 'ghostErrorCode'),
|
||||
ghost_error_code: resp.ghostErrorCode,
|
||||
displayed_message: msg,
|
||||
full_error: resp,
|
||||
source: 'showAPIError'
|
||||
|
@ -196,9 +184,9 @@ export default class NotificationsService extends Service {
|
|||
|
||||
displayDelayed() {
|
||||
this.delayedNotifications.forEach((message) => {
|
||||
this.content.pushObject(message);
|
||||
this.content.push(message);
|
||||
});
|
||||
this.set('delayedNotifications', []);
|
||||
this.delayedNotifications = new TrackedArray([]);
|
||||
}
|
||||
|
||||
closeNotification(notification) {
|
||||
|
@ -223,7 +211,7 @@ export default class NotificationsService extends Service {
|
|||
}
|
||||
|
||||
clearAll() {
|
||||
this.content.clear();
|
||||
this.content = new TrackedArray([]);
|
||||
}
|
||||
|
||||
_removeItems(status, key) {
|
||||
|
@ -234,14 +222,14 @@ export default class NotificationsService extends Service {
|
|||
let escapedKeyBase = keyBase.replace('.', '\\.');
|
||||
let keyRegex = new RegExp(`^${escapedKeyBase}`);
|
||||
|
||||
this.set('content', this.content.reject((item) => {
|
||||
let itemKey = get(item, 'key');
|
||||
let itemStatus = get(item, 'status');
|
||||
this.content = new TrackedArray(this.content.reject((item) => {
|
||||
let itemKey = item.key;
|
||||
let itemStatus = item.status;
|
||||
|
||||
return itemStatus === status && (itemKey && itemKey.match(keyRegex));
|
||||
}));
|
||||
} else {
|
||||
this.set('content', this.content.rejectBy('status', status));
|
||||
this.content = new TrackedArray(this.content.rejectBy('status', status));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue