[chore] Ran native classes codemod for app/services (#2240)

refs https://github.com/TryGhost/Admin/pull/2227

- a continuation of #2227 that runs the native classes codemod against app/services
This commit is contained in:
Gabriel Csapo 2022-02-02 14:11:11 -08:00 committed by GitHub
parent 48e5eb0261
commit 75a138cb39
17 changed files with 283 additions and 179 deletions

View File

@ -1,4 +1,5 @@
import AjaxService from 'ember-ajax/services/ajax';
import classic from 'ember-classic-decorator';
import config from 'ghost-admin/config/environment';
import moment from 'moment';
import {AjaxError, isAjaxError, isForbiddenError} from 'ember-ajax/errors';
@ -160,29 +161,33 @@ export function isAcceptedResponse(errorOrStatus) {
return false;
}
let ajaxService = AjaxService.extend({
config: service(),
session: service(),
@classic
class ajaxService extends AjaxService {
@service
config;
@service
session;
// flag to tell our ESA authenticator not to try an invalidate DELETE request
// because it's been triggered by this service's 401 handling which means the
// DELETE would fail and get stuck in an infinite loop
// TODO: find a more elegant way to handle this
skipSessionDeletion: false,
skipSessionDeletion = false;
get headers() {
return {
'X-Ghost-Version': config.APP.version,
'App-Pragma': 'no-cache'
};
},
}
init() {
this._super(...arguments);
super.init(...arguments);
if (this.isTesting === undefined) {
this.isTesting = config.environment === 'test';
}
},
}
async _makeRequest(hash) {
// ember-ajax recognises `application/vnd.api+json` as a JSON-API request
@ -220,7 +225,7 @@ let ajaxService = AjaxService.extend({
return data;
};
const makeRequest = this._super.bind(this);
const makeRequest = super._makeRequest.bind(this);
while (retryingMs <= maxRetryingMs && !success) {
try {
@ -253,7 +258,7 @@ let ajaxService = AjaxService.extend({
}
}
}
},
}
handleResponse(status, headers, payload, request) {
if (this.isVersionMismatchError(status, headers, payload)) {
@ -291,8 +296,8 @@ let ajaxService = AjaxService.extend({
this.session.invalidate();
}
return this._super(...arguments);
},
return super.handleResponse(...arguments);
}
normalizeErrorResponse(status, headers, payload) {
if (payload && typeof payload === 'object') {
@ -313,45 +318,45 @@ let ajaxService = AjaxService.extend({
}
}
return this._super(status, headers, payload);
},
return super.normalizeErrorResponse(status, headers, payload);
}
isVersionMismatchError(status, headers, payload) {
return isVersionMismatchError(status, payload);
},
}
isServerUnreachableError(status) {
return isServerUnreachableError(status);
},
}
isRequestEntityTooLargeError(status) {
return isRequestEntityTooLargeError(status);
},
}
isUnsupportedMediaTypeError(status) {
return isUnsupportedMediaTypeError(status);
},
}
isMaintenanceError(status, headers, payload) {
return isMaintenanceError(status, payload);
},
}
isThemeValidationError(status, headers, payload) {
return isThemeValidationError(status, payload);
},
}
isHostLimitError(status, headers, payload) {
return isHostLimitError(status, payload);
},
}
isEmailError(status, headers, payload) {
return isEmailError(status, payload);
},
}
isAcceptedResponse(status) {
return isAcceptedResponse(status);
}
});
}
// we need to reopen so that internal methods use the correct contentType
ajaxService.reopen({

View File

@ -1,20 +1,29 @@
import Service, {inject as service} from '@ember/service';
import classic from 'ember-classic-decorator';
export default Service.extend({
router: service(),
config: service(),
ghostPaths: service(),
store: service(),
@classic
export default class BillingService extends Service {
@service
router;
billingRouteRoot: '#/pro',
billingWindowOpen: false,
subscription: null,
previousRoute: null,
action: null,
ownerUser: null,
@service
config;
@service
ghostPaths;
@service
store;
billingRouteRoot = '#/pro';
billingWindowOpen = false;
subscription = null;
previousRoute = null;
action = null;
ownerUser = null;
init() {
this._super(...arguments);
super.init(...arguments);
if (this.config.get('hostSettings.billing.url')) {
window.addEventListener('message', (event) => {
@ -23,7 +32,7 @@ export default Service.extend({
}
});
}
},
}
handleRouteChangeInIframe(destinationRoute) {
if (this.billingWindowOpen) {
@ -37,7 +46,7 @@ export default Service.extend({
window.history.replaceState(window.history.state, '', billingRoute);
}
}
},
}
getIframeURL() {
// initiate getting owner user in the background
@ -54,7 +63,7 @@ export default Service.extend({
}
return url;
},
}
async getOwnerUser() {
if (!this.ownerUser) {
@ -69,7 +78,7 @@ export default Service.extend({
this.set('ownerUser', user);
}
return this.ownerUser;
},
}
// Sends a route update to a child route in the BMA, because we can't control
// navigating to it otherwise
@ -86,7 +95,7 @@ export default Service.extend({
this.set('action', null);
}
},
}
// Controls billing window modal visibility and sync of the URL visible in browser
// and the URL opened on the iframe. It is responsible to non user triggered iframe opening,
@ -100,7 +109,7 @@ export default Service.extend({
this.sendRouteUpdate();
this.set('billingWindowOpen', value);
},
}
// Controls navigation to billing window modal which is triggered from the application UI.
// For example: pressing "View Billing" link in navigation menu. It's main side effect is
@ -124,9 +133,9 @@ export default Service.extend({
this.sendRouteUpdate();
this.router.transitionTo(childRoute || '/pro');
},
}
getBillingIframe() {
return document.getElementById('billing-frame');
}
});
}

View File

@ -1,4 +1,5 @@
import Service from '@ember/service';
import classic from 'ember-classic-decorator';
import config from 'ghost-admin/config/environment';
import moment from 'moment';
import {run} from '@ember/runloop';
@ -7,15 +8,16 @@ const ONE_SECOND = 1000;
// Creates a clock service to run intervals.
export default Service.extend({
second: null,
minute: null,
hour: null,
@classic
export default class ClockService extends Service {
second = null;
minute = null;
hour = null;
init() {
this._super(...arguments);
super.init(...arguments);
this.tick();
},
}
tick() {
let now = moment().utc();
@ -32,5 +34,4 @@ export default Service.extend({
}, ONE_SECOND);
}
}
});
}

View File

@ -1,23 +1,30 @@
import Ember from 'ember';
import RSVP from 'rsvp';
import Service, {inject as service} from '@ember/service';
import classic from 'ember-classic-decorator';
import timezoneData from '@tryghost/timezone-data';
import {computed} from '@ember/object';
// ember-cli-shims doesn't export _ProxyMixin
const {_ProxyMixin} = Ember;
export default Service.extend(_ProxyMixin, {
ajax: service(),
ghostPaths: service(),
session: service(),
@classic
export default class ConfigService extends Service.extend(_ProxyMixin) {
@service
ajax;
content: null,
@service
ghostPaths;
@service
session;
content = null;
init() {
this._super(...arguments);
super.init(...arguments);
this.content = {};
},
}
fetch() {
let promises = [];
@ -29,7 +36,7 @@ export default Service.extend(_ProxyMixin, {
}
return RSVP.all(promises);
},
}
fetchUnauthenticated() {
let siteUrl = this.ghostPaths.url.api('site');
@ -44,7 +51,7 @@ export default Service.extend(_ProxyMixin, {
}).then(() => {
this.notifyPropertyChange('content');
});
},
}
fetchAuthenticated() {
let configUrl = this.ghostPaths.url.api('config');
@ -53,22 +60,25 @@ export default Service.extend(_ProxyMixin, {
}).then(() => {
this.notifyPropertyChange('content');
});
},
}
availableTimezones: computed(function () {
@computed
get availableTimezones() {
return RSVP.resolve(timezoneData);
}),
}
blogDomain: computed('blogUrl', function () {
@computed('blogUrl')
get blogDomain() {
let blogUrl = this.get('blogUrl');
let blogDomain = blogUrl
.replace(/^https?:\/\//, '')
.replace(/\/?$/, '');
return blogDomain;
}),
}
emailDomain: computed('blogDomain', function () {
@computed('blogDomain')
get emailDomain() {
let blogDomain = this.blogDomain || '';
const domainExp = blogDomain.match(new RegExp('^([^/:?#]+)(?:[/:?#]|$)', 'i'));
const domain = (domainExp && domainExp[1]) || '';
@ -76,7 +86,7 @@ export default Service.extend(_ProxyMixin, {
return domain.replace(/^(www)\.(?=[^/]*\..{2,5})/, '');
}
return domain;
}),
}
getSiteUrl(path) {
const siteUrl = new URL(this.get('blogUrl'));
@ -85,4 +95,4 @@ export default Service.extend(_ProxyMixin, {
return `${siteUrl.origin}${fullPath}`;
}
});
}

View File

@ -1,23 +1,25 @@
import $ from 'jquery';
import classic from 'ember-classic-decorator';
// This is used by the dropdown initializer to manage closing & toggling
import BodyEventListener from 'ghost-admin/mixins/body-event-listener';
import Evented from '@ember/object/evented';
import Service from '@ember/service';
export default Service.extend(Evented, BodyEventListener, {
@classic
export default class DropdownService extends Service.extend(Evented, BodyEventListener) {
bodyClick(event) {
let dropdownSelector = '.ember-basic-dropdown-trigger, .ember-basic-dropdown-content';
if ($(event.target).closest(dropdownSelector).length <= 0) {
this.closeDropdowns();
}
},
}
closeDropdowns() {
this.trigger('close');
},
}
toggleDropdown(dropdownName, dropdownButton) {
this.trigger('toggle', {target: dropdownName, button: dropdownButton});
}
});
}

View File

@ -1,14 +1,18 @@
import Evented from '@ember/object/evented';
import Service from '@ember/service';
import classic from 'ember-classic-decorator';
export default Service.extend(Evented, {
@classic
export default class EventBusService extends Service.extend(Evented) {
publish() {
return this.trigger(...arguments);
},
}
subscribe() {
return this.on(...arguments);
},
}
unsubscribe() {
return this.off(...arguments);
}
});
}

View File

@ -2,6 +2,7 @@ import $ from 'jquery';
import Ember from 'ember';
import EmberError from '@ember/error';
import Service, {inject as service} from '@ember/service';
import classic from 'ember-classic-decorator';
import {computed, set} from '@ember/object';
export function feature(name, options = {}) {
@ -36,35 +37,69 @@ export function feature(name, options = {}) {
}));
}
export default Service.extend({
store: service(),
config: service(),
session: service(),
settings: service(),
notifications: service(),
lazyLoader: service(),
@classic
export default class FeatureService extends Service {
@service
store;
@service
config;
@service
session;
@service
settings;
@service
notifications;
@service
lazyLoader;
// features
emailAnalytics: feature('emailAnalytics'),
@feature('emailAnalytics')
emailAnalytics;
// user-specific flags
nightShift: feature('nightShift', {user: true, onChange: '_setAdminTheme'}),
dashboardHideGettingStarted: feature('dashboardHideGettingStarted', {user: true}),
@feature('nightShift', {user: true, onChange: '_setAdminTheme'})
nightShift;
@feature('dashboardHideGettingStarted', {user: true})
dashboardHideGettingStarted;
// labs flags
multipleProducts: feature('multipleProducts'),
oauthLogin: feature('oauthLogin'),
membersActivity: feature('membersActivity'),
urlCache: feature('urlCache'),
beforeAfterCard: feature('beforeAfterCard'),
tweetGridCard: feature('tweetGridCard'),
membersActivityFeed: feature('membersActivityFeed'),
improvedOnboarding: feature('improvedOnboarding'),
tierWelcomePages: feature('tierWelcomePages'),
@feature('multipleProducts')
multipleProducts;
_user: null,
@feature('oauthLogin')
oauthLogin;
labs: computed('settings.labs', function () {
@feature('membersActivity')
membersActivity;
@feature('urlCache')
urlCache;
@feature('beforeAfterCard')
beforeAfterCard;
@feature('tweetGridCard')
tweetGridCard;
@feature('membersActivityFeed')
membersActivityFeed;
@feature('improvedOnboarding')
improvedOnboarding;
@feature('tierWelcomePages')
tierWelcomePages;
_user = null;
@computed('settings.labs')
get labs() {
let labs = this.get('settings.labs');
try {
@ -72,9 +107,10 @@ export default Service.extend({
} catch (e) {
return {};
}
}),
}
accessibility: computed('_user.accessibility', function () {
@computed('_user.accessibility')
get accessibility() {
let accessibility = this.get('_user.accessibility');
try {
@ -82,14 +118,14 @@ export default Service.extend({
} catch (e) {
return {};
}
}),
}
fetch() {
return this.settings.fetch().then(() => {
this.set('_user', this.session.user);
return this._setAdminTheme().then(() => true);
});
},
}
update(key, value, options = {}) {
let serviceProperty = options.user ? 'accessibility' : 'labs';
@ -126,7 +162,7 @@ export default Service.extend({
return this.get(`${serviceProperty}.${key}`);
});
},
}
_setAdminTheme(enabled) {
let nightShift = enabled;
@ -142,4 +178,4 @@ export default Service.extend({
$('link[title=dark]').prop('disabled', true);
});
}
});
}

View File

@ -1,4 +1,6 @@
import Service from '@ember/service';
import classic from 'ember-classic-decorator';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
export default Service.extend(ghostPaths());
@classic
export default class GhostPathsService extends Service.extend(ghostPaths()) {}

View File

@ -1,24 +1,29 @@
import RSVP from 'rsvp';
import Service, {inject as service} from '@ember/service';
import classic from 'ember-classic-decorator';
import config from 'ghost-admin/config/environment';
export default Service.extend({
ajax: service(),
ghostPaths: service(),
@classic
export default class LazyLoaderService extends Service {
@service
ajax;
@service
ghostPaths;
// This is needed so we can disable it in unit tests
testing: undefined,
testing = undefined;
scriptPromises: null,
scriptPromises = null;
init() {
this._super(...arguments);
super.init(...arguments);
this.scriptPromises = {};
if (this.testing === undefined) {
this.testing = config.environment === 'test';
}
},
}
loadScript(key, url) {
if (this.testing) {
@ -52,7 +57,7 @@ export default Service.extend({
this.scriptPromises[key] = scriptPromise;
return scriptPromise;
},
}
loadStyle(key, url, alternate = false) {
if (this.testing || document.querySelector(`#${key}-styles`)) {
@ -82,4 +87,4 @@ export default Service.extend({
document.querySelector('head').appendChild(link);
});
}
});
}

View File

@ -1,5 +1,6 @@
import Evented from '@ember/object/evented';
import Service from '@ember/service';
import classic from 'ember-classic-decorator';
import {run} from '@ember/runloop';
const MEDIA_QUERIES = {
@ -9,18 +10,19 @@ const MEDIA_QUERIES = {
maxWidth1000: '(max-width: 1000px)'
};
export default Service.extend(Evented, {
@classic
export default class MediaQueriesService extends Service.extend(Evented) {
init() {
this._super(...arguments);
super.init(...arguments);
this._handlers = [];
this.loadQueries(MEDIA_QUERIES);
},
}
loadQueries(queries) {
Object.keys(queries).forEach((key) => {
this.loadQuery(key, queries[key]);
});
},
}
loadQuery(key, queryString) {
let query = window.matchMedia(queryString);
@ -37,13 +39,12 @@ export default Service.extend(Evented, {
});
query.addListener(handler);
this._handlers.push([query, handler]);
},
}
willDestroy() {
this._handlers.forEach(([query, handler]) => {
query.removeListener(handler);
});
this._super(...arguments);
super.willDestroy(...arguments);
}
});
}

View File

@ -1,6 +1,8 @@
import Service from '@ember/service';
import classic from 'ember-classic-decorator';
// dummy service to account for not having the ember-responsive dependency
// available for ember-light-table (we don't use it so no need for the dep)
// see https://github.com/offirgolan/ember-light-table/issues/576
export default Service.extend({});
@classic
export default class MediaService extends Service {}

View File

@ -1,17 +1,24 @@
import Service, {inject as service} from '@ember/service';
import classic from 'ember-classic-decorator';
import validator from 'validator';
import {isEmpty} from '@ember/utils';
export default Service.extend({
ajax: service(),
membersUtils: service(),
ghostPaths: service(),
@classic
export default class MemberImportValidatorService extends Service {
@service
ajax;
@service
membersUtils;
@service
ghostPaths;
check(data) {
let sampledData = this._sampleData(data);
let mapping = this._detectDataTypes(sampledData);
return mapping;
},
}
/**
* Method implements foollowing sampling logic:
@ -62,7 +69,7 @@ export default Service.extend({
}
return validatedSet;
},
}
/**
* Detects supported data types and auto-detects following two needed for validation:
@ -117,4 +124,4 @@ export default Service.extend({
return mapping;
}
});
}

View File

@ -1,5 +1,6 @@
import * as Sentry from '@sentry/browser';
import Service, {inject as service} from '@ember/service';
import classic from 'ember-classic-decorator';
import {dasherize} from '@ember/string';
import {A as emberA, isArray as isEmberArray} from '@ember/array';
import {filter} from '@ember/object/computed';
@ -20,28 +21,34 @@ import {
// to avoid stacking of multiple error messages whilst leaving enough
// specificity to re-use keys for i18n lookups
export default Service.extend({
delayedNotifications: null,
content: null,
@classic
export default class NotificationsService extends Service {
delayedNotifications = null;
content = null;
init() {
this._super(...arguments);
super.init(...arguments);
this.delayedNotifications = emberA();
this.content = emberA();
},
}
config: service(),
upgradeStatus: service(),
@service
config;
alerts: filter('content', function (notification) {
@service
upgradeStatus;
@filter('content', function (notification) {
let status = get(notification, 'status');
return status === 'alert';
}),
})
alerts;
notifications: filter('content', function (notification) {
@filter('content', function (notification) {
let status = get(notification, 'status');
return status === 'notification';
}),
})
notifications;
handleNotification(message, delayed) {
// If this is an alert message from the server, treat it as html safe
@ -70,7 +77,7 @@ export default Service.extend({
} else {
this.delayedNotifications.pushObject(message);
}
},
}
showAlert(message, options) {
options = options || {};
@ -107,7 +114,7 @@ export default Service.extend({
key: options.key,
actions: options.actions
}, options.delayed);
},
}
showNotification(message, options) {
options = options || {};
@ -121,7 +128,7 @@ export default Service.extend({
key: options.key,
actions: options.actions
}, options.delayed);
},
}
showAPIError(resp, options) {
// handle "global" errors
@ -139,7 +146,7 @@ export default Service.extend({
}
this._showAPIError(resp, options);
},
}
_showAPIError(resp, options) {
options = options || {};
@ -188,14 +195,14 @@ export default Service.extend({
options.isApiError = true;
this.showAlert(msg, options);
},
}
displayDelayed() {
this.delayedNotifications.forEach((message) => {
this.content.pushObject(message);
});
this.set('delayedNotifications', []);
},
}
closeNotification(notification) {
let content = this.content;
@ -208,19 +215,19 @@ export default Service.extend({
} else {
content.removeObject(notification);
}
},
}
closeNotifications(key) {
this._removeItems('notification', key);
},
}
closeAlerts(key) {
this._removeItems('alert', key);
},
}
clearAll() {
this.content.clear();
},
}
_removeItems(status, key) {
if (key) {
@ -239,11 +246,11 @@ export default Service.extend({
} else {
this.set('content', this.content.rejectBy('status', status));
}
},
}
// take a key and return the first two elements, eg:
// "invite.revoke.failed" => "invite.revoke"
_getKeyBase(key) {
return key.split('.').slice(0, 2).join('.');
}
});
}

View File

@ -1,13 +1,15 @@
import Service from '@ember/service';
import classic from 'ember-classic-decorator';
import erd from 'element-resize-detector';
export default Service.extend({
@classic
export default class ResizeDetectorService extends Service {
init() {
this._super(...arguments);
super.init(...arguments);
this.detector = erd({
strategy: 'scroll'
});
},
}
setup(selector, callback) {
let element = document.querySelector(selector);
@ -16,7 +18,7 @@ export default Service.extend({
console.error(`service:resize-detector - could not find element matching ${selector}`);
}
this.detector.listenTo(element, callback);
},
}
teardown(selector, callback) {
let element = document.querySelector(selector);
@ -24,4 +26,4 @@ export default Service.extend({
this.detector.removeListener(element, callback);
}
}
});
}

View File

@ -2,24 +2,27 @@ import Ember from 'ember';
import RSVP from 'rsvp';
import Service, {inject as service} from '@ember/service';
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
import classic from 'ember-classic-decorator';
import {get} from '@ember/object';
// ember-cli-shims doesn't export _ProxyMixin
const {_ProxyMixin} = Ember;
export default Service.extend(_ProxyMixin, ValidationEngine, {
store: service(),
@classic
export default class SettingsService extends Service.extend(_ProxyMixin, ValidationEngine) {
@service
store;
// will be set to the single Settings model, it's a reference so any later
// changes to the settings object in the store will be reflected
content: null,
content = null;
validationType: 'setting',
_loadingPromise: null,
validationType = 'setting';
_loadingPromise = null;
// this is an odd case where we only want to react to changes that we get
// back from the API rather than local updates
settledIcon: '',
settledIcon = '';
// the settings API endpoint is a little weird as it's singular and we have
// to pass in all types - if we ever fetch settings without all types then
@ -35,7 +38,7 @@ export default Service.extend(_ProxyMixin, ValidationEngine, {
}
return this._loadingPromise;
},
}
fetch() {
if (!this.content) {
@ -43,7 +46,7 @@ export default Service.extend(_ProxyMixin, ValidationEngine, {
} else {
return RSVP.resolve(this);
}
},
}
reload() {
return this._loadSettings().then((settings) => {
@ -51,7 +54,7 @@ export default Service.extend(_ProxyMixin, ValidationEngine, {
this.set('settledIcon', get(settings, 'icon'));
return this;
});
},
}
async save() {
let settings = this.content;
@ -63,13 +66,13 @@ export default Service.extend(_ProxyMixin, ValidationEngine, {
await settings.save();
this.set('settledIcon', settings.icon);
return settings;
},
}
rollbackAttributes() {
return this.content?.rollbackAttributes();
},
}
changedAttributes() {
return this.content?.changedAttributes();
}
});
}

View File

@ -1,11 +1,16 @@
import RSVP from 'rsvp';
import Service, {inject as service} from '@ember/service';
import classic from 'ember-classic-decorator';
const {resolve} = RSVP;
export default Service.extend({
ghostPaths: service(),
ajax: service(),
@classic
export default class SlugGeneratorService extends Service {
@service
ghostPaths;
@service
ajax;
generateSlug(slugType, textToSlugify) {
let url;
@ -23,4 +28,4 @@ export default Service.extend({
return slug;
});
}
});
}

View File

@ -1,19 +1,22 @@
import Service, {inject as service} from '@ember/service';
import classic from 'ember-classic-decorator';
import {get, set} from '@ember/object';
import {htmlSafe} from '@ember/template';
export default Service.extend({
notifications: service(),
@classic
export default class UpgradeStatusService extends Service {
@service
notifications;
isRequired: false,
message: '',
isRequired = false;
message = '';
// called when notifications are fetched during app boot for notifications
// where the `location` is not 'top' and `custom` is false
handleUpgradeNotification(notification) {
let message = get(notification, 'message');
set(this, 'message', htmlSafe(message));
},
}
// called when a MaintenanceError is encountered
maintenanceAlert() {
@ -21,7 +24,7 @@ export default Service.extend({
'Sorry, Ghost is currently undergoing maintenance, please wait a moment then try again.',
{type: 'error', key: 'api-error.under-maintenance'}
);
},
}
// called when a VersionMismatchError is encountered
requireUpgrade() {
@ -31,4 +34,4 @@ export default Service.extend({
{type: 'error', key: 'api-error.upgrade-required'}
);
}
});
}