0
Fork 0
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:
Kevin Ansfield 2022-05-25 12:16:31 +01:00
parent 0c07e26cc4
commit e021843e3f
10 changed files with 105 additions and 152 deletions

View file

@ -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

View file

@ -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>

View file

@ -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;

View file

@ -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>

View file

@ -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;
}

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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>

View file

@ -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;
}

View file

@ -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));
}
}