Fixed hosting management screen not loading after sign-in process (#15763)

refs https://github.com/TryGhost/Team/issues/2110

- dynamically defined properties on the config service did not have
autotracking set up properly if they were accessed in any way before the
property was defined, this caused problems in a number of areas because
we have both "unauthed" and "authed" sets of config and when not logged
in we had parts of the app checking for authed config properties that
don't exist until after sign-in and subsequent config re-fetch
- renamed `config` service to `configManager` and updated to only
contain methods for fetching config data
- added a `config` instance initializer that sets up a `TrackedObject`
instance with some custom properties/methods and registers it on
`config:main`
- uses application instance initializer rather than a standard
initializer because standard initializers are only called once when
setting up the test suite so we'd end up with config leaking across
tests
- added an `@inject` decorator that when used takes the property name
and injects whatever is registered at `${propertyName}:main`, this
allows us to use dependency injection for any object rather than just
services or controllers
- using `application.inject()` in the initializer was initially used but
that only works for objects that extend from `EmberObject`, the
injections weren't available in native-class glimmer components so this
decorator keeps the injection syntax consistent
  - swapped all `@service config` uses to `@inject config`
This commit is contained in:
Kevin Ansfield 2022-11-03 11:14:36 +00:00 committed by GitHub
parent c00e098915
commit 9bdb25d184
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 458 additions and 260 deletions

View File

@ -2,6 +2,7 @@ import Component from '@glimmer/component';
import validator from 'validator';
import {action} from '@ember/object';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
@ -20,13 +21,14 @@ html {
// TODO: remove duplication with <ModalPostEmailPreview>
export default class ModalPostPreviewEmailComponent extends Component {
@service ajax;
@service config;
@service feature;
@service ghostPaths;
@service session;
@service settings;
@service store;
@inject config;
@tracked html = '';
@tracked subject = '';
@tracked memberSegment = 'status:free';

View File

@ -4,13 +4,16 @@ import {
IMAGE_MIME_TYPES
} from 'ghost-admin/components/gh-image-uploader';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class ModalPostPreviewSocialComponent extends Component {
@service config;
@service settings;
@service ghostPaths;
@inject config;
@tracked editingFacebookTitle = false;
@tracked editingFacebookDescription = false;
@tracked editingTwitterTitle = false;

View File

@ -1,16 +1,18 @@
import Component from '@ember/component';
import classic from 'ember-classic-decorator';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
@classic
export default class GhBillingIframe extends Component {
@service billing;
@service config;
@service ghostPaths;
@service ajax;
@service notifications;
@inject config;
isOwner = null;
fetchingSubscription = false;

View File

@ -1,17 +1,19 @@
import Component from '@ember/component';
import classic from 'ember-classic-decorator';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {reads} from '@ember/object/computed';
import {inject as service} from '@ember/service';
@classic
export default class GhBillingUpdateButton extends Component {
@service router;
@service config;
@service ghostPaths;
@service ajax;
@service billing;
@inject config;
subscription = null;
@reads('billing.subscription.isActiveTrial')

View File

@ -1,10 +1,10 @@
import Component from '@ember/component';
import classic from 'ember-classic-decorator';
import {inject as service} from '@ember/service';
import {inject} from 'ghost-admin/decorators/inject';
import {tagName} from '@ember-decorators/component';
@classic
@tagName('')
export default class GhBlogUrl extends Component {
@service config;
@inject config;
}

View File

@ -8,6 +8,7 @@ import {
} from 'ghost-admin/services/ajax';
import {computed, get} from '@ember/object';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {isArray} from '@ember/array';
import {isBlank} from '@ember/utils';
import {run} from '@ember/runloop';
@ -23,7 +24,6 @@ export const ICON_PARAMS = {purpose: 'icon'};
export default Component.extend({
ajax: service(),
config: service(),
notifications: service(),
settings: service(),
@ -64,6 +64,8 @@ export default Component.extend({
uploadSuccess: () => {},
uploadFailed: () => {},
config: inject(),
// TODO: this wouldn't be necessary if the server could accept direct
// file uploads
formData: computed('file', function () {

View File

@ -7,6 +7,7 @@ import {action, computed} from '@ember/object';
import {assign} from '@ember/polyfills';
import {classNameBindings, classNames} from '@ember-decorators/component';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {isEmpty, typeOf} from '@ember/utils';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
@ -21,10 +22,11 @@ import {inject as service} from '@ember/service';
'_isSplitScreen:gh-markdown-editor-side-by-side'
)
export default class GhMarkdownEditor extends Component.extend(ShortcutsMixin) {
@service config;
@service notifications;
@service settings;
@inject config;
// Public attributes
autofocus = false;

View File

@ -1,5 +1,6 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
@ -16,7 +17,8 @@ export default class extends Component {
@service ghostPaths;
@service ajax;
@service store;
@service config;
@inject config;
@tracked showTierModal = false;
@tracked tierModel = null;

View File

@ -1,18 +1,20 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {bind} from '@ember/runloop';
import {inject} from 'ghost-admin/decorators/inject';
import {isEmpty} from '@ember/utils';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class DesignMenuComponent extends Component {
@service config;
@service customThemeSettings;
@service router;
@service settings;
@service store;
@service themeManagement;
@inject config;
@tracked openSection = null;
themes = this.store.peekAll('theme');

View File

@ -2,16 +2,18 @@ import Component from '@ember/component';
import calculatePosition from 'ember-basic-dropdown/utils/calculate-position';
import classic from 'ember-classic-decorator';
import {and, match} from '@ember/object/computed';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
@classic
export default class Footer extends Component {
@service config;
@service session;
@service router;
@service whatsNew;
@service feature;
@inject config;
@and('config.clientExtensions.dropdown', 'session.user.isOwnerOnly')
showDropdownExtension;

View File

@ -7,6 +7,7 @@ import {action} from '@ember/object';
import {and, equal, match, or, reads} from '@ember/object/computed';
import {getOwner} from '@ember/application';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tagName} from '@ember-decorators/component';
import {task} from 'ember-concurrency';
@ -15,7 +16,6 @@ import {task} from 'ember-concurrency';
@tagName('')
export default class Main extends Component.extend(ShortcutsMixin) {
@service billing;
@service config;
@service customViews;
@service feature;
@service ghostPaths;
@ -28,6 +28,8 @@ export default class Main extends Component.extend(ShortcutsMixin) {
@service membersStats;
@service settings;
@inject config;
iconStyle = '';
iconClass = '';
memberCountLoading = true;

View File

@ -2,6 +2,7 @@ import Component from '@ember/component';
import classic from 'ember-classic-decorator';
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
import {action, computed} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tagName} from '@ember-decorators/component';
import {task, timeout} from 'ember-concurrency';
@ -9,10 +10,11 @@ import {task, timeout} from 'ember-concurrency';
@classic
@tagName('')
export default class GhPortalLinks extends Component {
@service config;
@service store;
@service settings;
@inject config;
isLink = true;
prices = null;
copiedPrice = null;
@ -52,9 +54,8 @@ export default class GhPortalLinks extends Component {
return [];
}
init() {
super.init(...arguments);
this.siteUrl = this.config.blogUrl;
get siteUrl() {
return this.config.blogUrl;
}
@action

View File

@ -4,6 +4,7 @@ import classic from 'ember-classic-decorator';
import moment from 'moment-timezone';
import {action, computed} from '@ember/object';
import {alias, or} from '@ember/object/computed';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tagName} from '@ember-decorators/component';
@ -12,7 +13,6 @@ import {tagName} from '@ember-decorators/component';
export default class GhPostSettingsMenu extends Component {
@service feature;
@service store;
@service config;
@service ajax;
@service ghostPaths;
@service notifications;
@ -21,6 +21,8 @@ export default class GhPostSettingsMenu extends Component {
@service settings;
@service ui;
@inject config;
post = null;
isViewingSubview = false;

View File

@ -5,6 +5,7 @@ import validator from 'validator';
import {action, computed} from '@ember/object';
import {alias, not, oneWay, or} from '@ember/object/computed';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
@ -18,7 +19,8 @@ export default class Email extends Component {
@service notifications;
@service session;
@service settings;
@service config;
@inject config;
post = null;
sendTestEmailError = '';

View File

@ -1,11 +1,11 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {inject} from 'ghost-admin/decorators/inject';
import {task, timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class GhSiteIframeComponent extends Component {
@service config;
@inject config;
@tracked isInvisible = this.args.invisibleUntilLoaded;

View File

@ -1,6 +1,7 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {getSymbol} from 'ghost-admin/utils/currency';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
@ -9,7 +10,8 @@ export default class extends Component {
@service ghostPaths;
@service ajax;
@service store;
@service config;
@inject config;
@tracked showTierModal = false;

View File

@ -2,7 +2,7 @@ import Component from '@ember/component';
import classic from 'ember-classic-decorator';
import {classNames} from '@ember-decorators/component';
import {computed} from '@ember/object';
import {inject as service} from '@ember/service';
import {inject} from 'ghost-admin/decorators/inject';
/*
Example usage:
@ -11,7 +11,7 @@ Example usage:
@classic
@classNames('ghost-url-preview')
export default class GhUrlPreview extends Component {
@service config;
@inject config;
prefix = null;
slug = null;

View File

@ -3,7 +3,7 @@ import Component from '@glimmer/component';
import React, {Suspense} from 'react';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {inject} from 'ghost-admin/decorators/inject';
class ErrorHandler extends React.Component {
state = {
@ -90,7 +90,7 @@ const KoenigEditor = (props) => {
};
export default class KoenigLexicalEditor extends Component {
@service config;
@inject config;
@action
onError(error) {
@ -143,9 +143,9 @@ export default class KoenigLexicalEditor extends Component {
<div className={['koenig-react-editor', this.args.className].filter(Boolean).join(' ')}>
<ErrorHandler>
<Suspense fallback={<p className="koenig-react-editor-loading">Loading editor...</p>}>
<KoenigComposer
initialEditorState={this.args.lexical}
onError={this.onError}
<KoenigComposer
initialEditorState={this.args.lexical}
onError={this.onError}
imageUploadFunction={{imageUploader, uploadProgressPercentage}} >
<KoenigEditor onChange={this.args.onChange} />
</KoenigComposer>

View File

@ -1,6 +1,7 @@
import ModalBase from 'ghost-admin/components/modal-base';
import classic from 'ember-classic-decorator';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
@ -9,13 +10,13 @@ import {tracked} from '@glimmer/tracking';
@classic
export default class ModalFreeMembershipSettings extends ModalBase {
@service settings;
@service config;
@inject config;
@tracked freeSignupRedirect;
@tracked siteUrl;
init() {
super.init(...arguments);
this.siteUrl = this.config.blogUrl;
get siteUrl() {
return this.config.blogUrl;
}
@action

View File

@ -1,16 +1,18 @@
import ModalComponent from 'ghost-admin/components/modal-base';
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
import {alias} from '@ember/object/computed';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
export default ModalComponent.extend({
config: service(),
store: service(),
classNames: 'modal-impersonate-member',
signinUrl: null,
config: inject(),
member: alias('model'),
didInsertElement() {

View File

@ -10,11 +10,11 @@ import {
} from 'ghost-admin/services/ajax';
import {computed} from '@ember/object';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {isBlank} from '@ember/utils';
import {inject as service} from '@ember/service';
export default ModalComponent.extend({
config: service(),
ajax: service(),
notifications: service(),
store: service(),
@ -34,6 +34,8 @@ export default ModalComponent.extend({
// Allowed actions
confirm: () => {},
config: inject(),
uploadUrl: computed(function () {
return `${ghostPaths().apiRoot}/members/upload/`;
}),

View File

@ -3,12 +3,12 @@ import ModalComponent from 'ghost-admin/components/modal-base';
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
import {action, computed} from '@ember/object';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
const ICON_EXTENSIONS = ['gif', 'jpg', 'jpeg', 'png', 'svg'];
export default ModalComponent.extend({
config: service(),
modals: service(),
membersUtils: service(),
settings: service(),
@ -31,6 +31,8 @@ export default ModalComponent.extend({
confirm() {},
config: inject(),
backgroundStyle: computed('settings.accentColor', function () {
let color = this.settings.accentColor || '#ffffff';
return htmlSafe(`background-color: ${color}`);

View File

@ -1,13 +1,13 @@
import ModalComponent from 'ghost-admin/components/modal-base';
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
import {reads} from '@ember/object/computed';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
export default ModalComponent.extend(ValidationEngine, {
config: service(),
notifications: service(),
session: service(),
@ -15,6 +15,8 @@ export default ModalComponent.extend(ValidationEngine, {
authenticationError: null,
config: inject(),
identification: reads('session.user.email'),
actions: {

View File

@ -5,6 +5,7 @@ import {action} from '@ember/object';
import {currencies, getCurrencyOptions, getSymbol} from 'ghost-admin/utils/currency';
import {A as emberA} from '@ember/array';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
@ -22,8 +23,10 @@ const CURRENCIES = currencies.map((currency) => {
export default class ModalTierPrice extends ModalBase {
@service feature;
@service settings;
@service config;
@service membersUtils;
@inject config;
@tracked model;
@tracked tier;
@tracked periodVal;

View File

@ -1,5 +1,6 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
@ -17,11 +18,12 @@ html {
export default class EmailPreviewModal extends Component {
@service ajax;
@service config;
@service ghostPaths;
@service settings;
@service store;
@inject config;
static modalOptions = {
className: 'fullscreen-modal-full-overlay fullscreen-modal-email-preview'
};
@ -58,11 +60,11 @@ export default class EmailPreviewModal extends Component {
if (!this.newsletter && this.args.data.newsletter) {
this.newsletter = this.args.data.newsletter;
}
if (!this.newsletter) {
const newsletters = (await this.store.query('newsletter', {filter: 'status:active', limit: 1})).toArray();
const defaultNewsletter = newsletters[0];
this.newsletter = defaultNewsletter;
this.newsletter = defaultNewsletter;
}
if (html && subject) {

View File

@ -1,11 +1,11 @@
import Component from '@glimmer/component';
import config from 'ghost-admin/config/environment';
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
import {inject as service} from '@ember/service';
import {inject} from 'ghost-admin/decorators/inject';
import {task, timeout} from 'ember-concurrency';
export default class LinkOfferModal extends Component {
@service config;
@inject config;
constructor() {
super(...arguments);

View File

@ -1,11 +1,13 @@
import Component from '@glimmer/component';
import config from 'ghost-admin/config/environment';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
export default class AboutModal extends Component {
@service config;
@service upgradeStatus;
@inject config;
constructor() {
super(...arguments);
if (this.isTesting === undefined) {

View File

@ -3,7 +3,6 @@ import {action} from '@ember/object';
import {inject as service} from '@ember/service';
export default class Analytics extends Component {
@service config;
@service settings;
@service feature;

View File

@ -1,6 +1,7 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {currencies} from 'ghost-admin/utils/currency';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
@ -11,13 +12,14 @@ const RETRY_PRODUCT_SAVE_MAX_POLL = 15 * RETRY_PRODUCT_SAVE_POLL_LENGTH;
const NO_OF_TOP_CURRENCIES = 5;
export default class StripeSettingsForm extends Component {
@service config;
@service ghostPaths;
@service ajax;
@service settings;
@service membersUtils;
@service store;
@inject config;
@tracked hasActiveStripeSubscriptions = false;
@tracked showDisconnectStripeConnectModal = false;
@tracked stripeConnectError = null;

View File

@ -1,5 +1,6 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
@ -7,10 +8,11 @@ const US = {flag: '🇺🇸', name: 'US', baseUrl: 'https://api.mailgun.net/v3'}
const EU = {flag: '🇪🇺', name: 'EU', baseUrl: 'https://api.eu.mailgun.net/v3'};
export default class Newsletters extends Component {
@service config;
@service settings;
@service feature;
@inject config;
// set recipientsSelectValue as a static property because within this
// component's lifecycle it's not always derived from the settings values.
// e.g. can be set to "segment" when the filter is empty which is not derivable

View File

@ -2,6 +2,7 @@ import Component from '@glimmer/component';
import Ember from 'ember';
import {action} from '@ember/object';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
import {slugify} from '@tryghost/string';
@ -12,9 +13,10 @@ const {Handlebars} = Ember;
export default class TagForm extends Component {
@service feature;
@service config;
@service settings;
@inject config;
@tracked metadataOpen = false;
@tracked twitterMetadataOpen = false;
@tracked facebookMetadataOpen = false;

View File

@ -1,15 +1,17 @@
import Controller from '@ember/controller';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
export default class ApplicationController extends Controller {
@service billing;
@service explore;
@service config;
@service router;
@service session;
@service settings;
@service ui;
@inject config;
get showBilling() {
return this.config.hostSettings?.billing?.enabled;
}

View File

@ -13,6 +13,7 @@ import {alias, mapBy} from '@ember/object/computed';
import {capitalize} from '@ember/string';
import {dropTask, enqueueTask, restartableTask, task, taskGroup, timeout} from 'ember-concurrency';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {isBlank} from '@ember/utils';
import {isArray as isEmberArray} from '@ember/array';
import {isHostLimitError, isServerUnreachableError, isVersionMismatchError} from 'ghost-admin/services/ajax';
@ -94,7 +95,6 @@ const messageMap = {
export default class EditorController extends Controller {
@controller application;
@service config;
@service feature;
@service membersCountCache;
@service modals;
@ -105,6 +105,8 @@ export default class EditorController extends Controller {
@service settings;
@service ui;
@inject config;
/* public properties -----------------------------------------------------*/
shouldFocusTitle = false;

View File

@ -13,6 +13,7 @@ import {alias, mapBy} from '@ember/object/computed';
import {capitalize} from '@ember/string';
import {dropTask, enqueueTask, restartableTask, task, taskGroup, timeout} from 'ember-concurrency';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {isBlank} from '@ember/utils';
import {isArray as isEmberArray} from '@ember/array';
import {isHostLimitError, isServerUnreachableError, isVersionMismatchError} from 'ghost-admin/services/ajax';
@ -94,7 +95,6 @@ const messageMap = {
export default class LexicalEditorController extends Controller {
@controller application;
@service config;
@service feature;
@service membersCountCache;
@service modals;
@ -105,6 +105,8 @@ export default class LexicalEditorController extends Controller {
@service settings;
@service ui;
@inject config;
/* public properties -----------------------------------------------------*/
shouldFocusTitle = false;

View File

@ -8,6 +8,7 @@ import moment from 'moment-timezone';
import {A} from '@ember/array';
import {action} from '@ember/object';
import {ghPluralize} from 'ghost-admin/helpers/gh-pluralize';
import {inject} from 'ghost-admin/decorators/inject';
import {resetQueryParams} from 'ghost-admin/helpers/reset-query-params';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
@ -26,7 +27,6 @@ const PAID_PARAMS = [{
export default class MembersController extends Controller {
@service ajax;
@service config;
@service ellaSparse;
@service feature;
@service ghostPaths;
@ -37,6 +37,8 @@ export default class MembersController extends Controller {
@service utils;
@service settings;
@inject config;
queryParams = [
'label',
{paidParam: 'paid'},

View File

@ -5,6 +5,7 @@ import config from 'ghost-admin/config/environment';
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
import {action} from '@ember/object';
import {getSymbol} from 'ghost-admin/utils/currency';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {slugify} from '@tryghost/string';
import {task} from 'ember-concurrency';
@ -13,7 +14,7 @@ import {tracked} from '@glimmer/tracking';
export default class OffersController extends Controller {
@controller offers;
@service config;
@service settings;
@service store;
@service modals;
@ -21,6 +22,8 @@ export default class OffersController extends Controller {
@service membersUtils;
@service notifications;
@inject config;
@tracked cadences = [];
@tracked tiers = [];
@tracked portalPreviewUrl = '';

View File

@ -1,11 +1,14 @@
import Controller, {inject as controller} from '@ember/controller';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
export default class PostsLoadingController extends Controller {
@controller('posts') postsController;
@service session;
@service ui;
@service config;
@inject config;
get availableTypes() {
return this.postsController.availableTypes;

View File

@ -1,6 +1,7 @@
import Controller from '@ember/controller';
import {DEFAULT_QUERY_PARAMS} from 'ghost-admin/helpers/reset-query-params';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
@ -47,12 +48,13 @@ const ORDERS = [{
}];
export default class PostsController extends Controller {
@service config;
@service feature;
@service router;
@service session;
@service store;
@inject config;
// default values for these are set in constructor and defined in `helpers/reset-query-params`
queryParams = ['type', 'visibility', 'author', 'tag', 'order'];

View File

@ -2,6 +2,7 @@
import Controller from '@ember/controller';
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
@ -11,7 +12,8 @@ export default class ResetController extends Controller.extend(ValidationEngine)
@service notifications;
@service session;
@service ajax;
@service config;
@inject config;
@tracked newPassword = '';
@tracked ne2Password = '';

View File

@ -1,16 +1,18 @@
import Controller from '@ember/controller';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class SettingsDesignIndexController extends Controller {
@service config;
@service customThemeSettings;
@service notifications;
@service settings;
@service themeManagement;
@inject config;
@tracked previewSize = 'desktop';
get isDesktopPreview() {

View File

@ -9,6 +9,7 @@ import {
IMAGE_MIME_TYPES
} from 'ghost-admin/components/gh-image-uploader';
import {TrackedObject} from 'tracked-built-ins';
import {inject} from 'ghost-admin/decorators/inject';
import {run} from '@ember/runloop';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
@ -22,7 +23,6 @@ function randomPassword() {
@classic
export default class GeneralController extends Controller {
@service config;
@service ghostPaths;
@service notifications;
@service session;
@ -30,12 +30,17 @@ export default class GeneralController extends Controller {
@service frontend;
@service ui;
@inject config;
@tracked scratchValues = new TrackedObject();
availableTimezones = this.config.availableTimezones;
imageExtensions = IMAGE_EXTENSIONS;
imageMimeTypes = IMAGE_MIME_TYPES;
get availableTimezones() {
return this.config.availableTimezones;
}
@computed('config.blogUrl', 'settings.publicHash')
get privateRSSUrl() {
let blogUrl = this.config.blogUrl;
@ -127,7 +132,6 @@ export default class GeneralController extends Controller {
@task
*saveTask() {
let notifications = this.notifications;
let config = this.config;
try {
let changedAttrs = this.settings.changedAttributes();
@ -135,7 +139,7 @@ export default class GeneralController extends Controller {
this.clearScratchValues();
config.set('blogTitle', settings.title);
this.config.blogTitle = settings.title;
if (changedAttrs.password) {
this.frontend.loginIfNeeded();

View File

@ -9,15 +9,17 @@ import {
} from 'ghost-admin/components/gh-image-uploader';
import {action} from '@ember/object';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class IntegrationController extends Controller {
@service config;
@service ghostPaths;
@service modals;
@inject config;
imageExtensions = IMAGE_EXTENSIONS;
imageMimeTypes = IMAGE_MIME_TYPES;

View File

@ -1,11 +1,13 @@
import Controller from '@ember/controller';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
export default class IntegrationsController extends Controller {
@service settings;
@service store;
@service config;
@inject config;
_allIntegrations = this.store.peekAll('integration');

View File

@ -11,6 +11,7 @@ import {
isUnsupportedMediaTypeError
} from 'ghost-admin/services/ajax';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {isBlank} from '@ember/utils';
import {isArray as isEmberArray} from '@ember/array';
import {run} from '@ember/runloop';
@ -38,7 +39,6 @@ const YAML_MIME_TYPE = [
@classic
export default class LabsController extends Controller {
@service ajax;
@service config;
@service feature;
@service ghostPaths;
@service modals;
@ -47,6 +47,8 @@ export default class LabsController extends Controller {
@service settings;
@service utils;
@inject config;
importErrors = null;
importSuccessful = false;
showEarlyAccessModal = false;
@ -119,7 +121,7 @@ export default class LabsController extends Controller {
// reload settings
return this.settings.reload().then((settings) => {
this.feature.fetch();
this.config.set('blogTitle', settings.title);
this.config.blogTitle = settings.title;
});
});
}).catch((response) => {

View File

@ -3,6 +3,7 @@ import Controller from '@ember/controller';
import envConfig from 'ghost-admin/config/environment';
import {action} from '@ember/object';
import {currencies, getCurrencyOptions, getSymbol} from 'ghost-admin/utils/currency';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
@ -16,7 +17,6 @@ const CURRENCIES = currencies.map((currency) => {
});
export default class MembersAccessController extends Controller {
@service config;
@service feature;
@service membersUtils;
@service modals;
@ -24,6 +24,8 @@ export default class MembersAccessController extends Controller {
@service store;
@service session;
@inject config;
@tracked showPortalSettings = false;
@tracked showStripeConnect = false;
@tracked showTierModal = false;

View File

@ -2,17 +2,19 @@ import Controller from '@ember/controller';
import NavigationItem from 'ghost-admin/models/navigation-item';
import RSVP from 'rsvp';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class NavigationController extends Controller {
@service config;
@service ghostPaths;
@service notifications;
@service session;
@service settings;
@inject config;
@tracked dirtyAttributes = false;
@tracked newNavItem = NavigationItem.create({isNew: true});
@tracked newSecondaryNavItem = NavigationItem.create({isNew: true, isSecondary: true});

View File

@ -11,6 +11,7 @@ import isNumber from 'ghost-admin/utils/isNumber';
import windowProxy from 'ghost-admin/utils/window-proxy';
import {TrackedObject} from 'tracked-built-ins';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
import {task, taskGroup, timeout} from 'ember-concurrency';
@ -18,7 +19,6 @@ import {tracked} from '@glimmer/tracking';
export default class UserController extends Controller {
@service ajax;
@service config;
@service ghostPaths;
@service membersUtils;
@service modals;
@ -27,6 +27,8 @@ export default class UserController extends Controller {
@service slugGenerator;
@service utils;
@inject config;
@tracked dirtyAttributes = false;
@tracked personalToken = null;
@tracked personalTokenRegenerated = false;

View File

@ -1,23 +1,24 @@
import Controller from '@ember/controller';
import EmberObject, {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class TierController extends Controller {
@service config;
@service membersUtils;
@service settings;
@inject config;
@tracked showLeaveSettingsModal = false;
@tracked showPriceModal = false;
@tracked priceModel = null;
@tracked showUnsavedChangesModal = false;
@tracked paidSignupRedirect;
constructor() {
super(...arguments);
this.siteUrl = this.config.blogUrl;
get siteUrl() {
return this.config.blogUrl;
}
get tier() {

View File

@ -1,12 +1,14 @@
import Controller from '@ember/controller';
import {action} from '@ember/object';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class TiersController extends Controller {
@service settings;
@service config;
@inject config;
@tracked iconStyle = '';
@tracked showFreeMembershipModal = false;

View File

@ -5,6 +5,7 @@ import Controller, {inject as controller} from '@ember/controller';
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
import {action} from '@ember/object';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {isInvalidError} from 'ember-ajax/errors';
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
import {task} from 'ember-concurrency';
@ -14,12 +15,13 @@ export default class SetupController extends Controller.extend(ValidationEngine)
@controller application;
@service ajax;
@service config;
@service ghostPaths;
@service notifications;
@service router;
@service session;
@inject config;
// ValidationEngine settings
validationType = 'setup';
@ -82,7 +84,6 @@ export default class SetupController extends Controller.extend(ValidationEngine)
_passwordSetup() {
let setupProperties = ['blogTitle', 'name', 'email', 'password'];
let data = this.getProperties(setupProperties);
let config = this.config;
let method = this.blogCreated ? 'put' : 'post';
this.set('flowErrors', '');
@ -102,7 +103,7 @@ export default class SetupController extends Controller.extend(ValidationEngine)
}]
}
}).then((result) => {
config.set('blogTitle', data.blogTitle);
this.config.blogTitle = data.blogTitle;
// don't try to login again if we are already logged in
if (this.get('session.isAuthenticated')) {

View File

@ -5,6 +5,7 @@ import Controller, {inject as controller} from '@ember/controller';
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
import {action} from '@ember/object';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {isArray as isEmberArray} from '@ember/array';
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
import {inject as service} from '@ember/service';
@ -15,12 +16,13 @@ export default class SigninController extends Controller.extend(ValidationEngine
@controller application;
@service ajax;
@service config;
@service ghostPaths;
@service notifications;
@service session;
@service settings;
@inject config;
@tracked submitting = false;
@tracked loggingIn = false;
@tracked flowErrors = '';

View File

@ -1,5 +1,6 @@
import Controller from '@ember/controller';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {isArray as isEmberArray} from '@ember/array';
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
import {inject as service} from '@ember/service';
@ -8,12 +9,13 @@ import {tracked} from '@glimmer/tracking';
export default class SignupController extends Controller {
@service ajax;
@service config;
@service ghostPaths;
@service notifications;
@service session;
@service settings;
@inject config;
@tracked flowErrors = '';
get signupDetails() {

View File

@ -0,0 +1,48 @@
import {computed, defineProperty} from '@ember/object';
import {getOwner} from '@ember/application';
function isElementDescriptor(args) {
let [maybeTarget, maybeKey, maybeDesc] = args;
return (
// Ensure we have the right number of args
args.length === 3 &&
// Make sure the target is a class or object (prototype)
(typeof maybeTarget === 'function' ||
(typeof maybeTarget === 'object' && maybeTarget !== null)) &&
// Make sure the key is a string
typeof maybeKey === 'string' &&
// Make sure the descriptor is the right shape
((typeof maybeDesc === 'object' && maybeDesc !== null) || maybeDesc === undefined)
);
}
export function inject(...args) {
let elementDescriptor;
let name;
if (isElementDescriptor(args)) {
elementDescriptor = args;
} else if (typeof args[0] === 'string') {
name = args[0];
}
const getInjection = function (propertyName) {
const owner = getOwner(this) || this.container;
return owner.lookup(`${name || propertyName}:main`);
};
const decorator = computed({
get: getInjection,
set(keyName, value) {
defineProperty(this, keyName, null, value);
return;
}
});
if (elementDescriptor) {
return decorator(elementDescriptor[0], elementDescriptor[1], elementDescriptor[2]);
} else {
return decorator;
}
}

View File

@ -1,11 +1,9 @@
import Helper from '@ember/component/helper';
import classic from 'ember-classic-decorator';
import {htmlSafe} from '@ember/template';
import {inject as service} from '@ember/service';
import {inject} from 'ghost-admin/decorators/inject';
@classic
export default class AccentColorBackgroundHelper extends Helper {
@service config;
@inject config;
compute() {
const color = this.config.accent_color;

View File

@ -1,8 +1,8 @@
import Helper from '@ember/component/helper';
import {inject as service} from '@ember/service';
import {inject} from 'ghost-admin/decorators/inject';
export default class EnableDeveloperExperimentsHelper extends Helper {
@service config;
@inject config;
compute() {
return this.config.enableDeveloperExperiments;

View File

@ -1,8 +1,8 @@
import Helper from '@ember/component/helper';
import {inject as service} from '@ember/service';
import {inject} from 'ghost-admin/decorators/inject';
export default class FullEmailAddressHelper extends Helper {
@service config;
@inject config;
compute([email = '']) {
if (email.indexOf('@') > -1) {

View File

@ -1,15 +1,17 @@
import PublishOptions from '../utils/publish-options';
import {Resource} from 'ember-could-get-used-to-this';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class PublishOptionsResource extends Resource {
@service config;
@service limit;
@service session;
@service settings;
@service store;
@inject config;
@tracked publishOptions;
get value() {

View File

@ -1,11 +1,9 @@
import Helper from '@ember/component/helper';
import classic from 'ember-classic-decorator';
import {htmlSafe} from '@ember/template';
import {inject as service} from '@ember/service';
import {inject} from 'ghost-admin/decorators/inject';
@classic
export default class SiteIconStyleHelper extends Helper {
@service config;
@inject config;
compute() {
const icon = this.config.icon || 'https://static.ghost.org/v4.0.0/images/ghost-orb-2.png';

View File

@ -0,0 +1,55 @@
import timezoneData from '@tryghost/timezone-data';
import {TrackedObject} from 'tracked-built-ins';
export function initialize(applicationInstance) {
const config = new TrackedObject({});
Object.defineProperty(config, 'availableTimezones', {
get() {
return timezoneData;
},
enumerable: true
});
Object.defineProperty(config, 'blogDomain', {
get() {
const blogDomain = this.blogUrl
.replace(/^https?:\/\//, '')
.replace(/\/?$/, '');
return blogDomain;
},
enumerable: true
});
Object.defineProperty(config, 'emailDomain', {
get() {
const blogDomain = this.blogDomain || '';
const domainExp = blogDomain.match(new RegExp('^([^/:?#]+)(?:[/:?#]|$)', 'i'));
const domain = (domainExp && domainExp[1]) || '';
if (domain.startsWith('www.')) {
return domain.replace(/^(www)\.(?=[^/]*\..{2,5})/, '');
}
return domain;
},
enumerable: true
});
Object.defineProperty(config, 'getSiteUrl', {
value: function (path) {
const siteUrl = new URL(this.blogUrl);
const subdir = siteUrl.pathname.endsWith('/') ? siteUrl.pathname : `${siteUrl.pathname}/`;
const fullPath = `${subdir}${path.replace(/^\//, '')}`;
return `${siteUrl.origin}${fullPath}`;
}
});
applicationInstance.register('config:main', config, {instantiate: false});
}
export default {
name: 'config',
initialize
};

View File

@ -7,6 +7,7 @@ import {BLANK_DOC as BLANK_MOBILEDOC} from 'koenig-editor/components/koenig-edit
import {compare, isBlank} from '@ember/utils';
import {computed, observer} from '@ember/object';
import {equal, filterBy, reads} from '@ember/object/computed';
import {inject} from 'ghost-admin/decorators/inject';
import {on} from '@ember/object/evented';
import {inject as service} from '@ember/service';
@ -67,7 +68,6 @@ function publishedAtCompare(postA, postB) {
}
export default Model.extend(Comparable, ValidationEngine, {
config: service(),
session: service(),
feature: service(),
ghostPaths: service(),
@ -75,6 +75,8 @@ export default Model.extend(Comparable, ValidationEngine, {
settings: service(),
membersUtils: service(),
config: inject(),
displayName: 'post',
validationType: 'post',

View File

@ -4,6 +4,7 @@ import ValidationEngine from 'ghost-admin/mixins/validation-engine';
import {attr, hasMany} from '@ember-data/model';
import {computed} from '@ember/object';
import {equal, or} from '@ember/object/computed';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
@ -44,7 +45,8 @@ export default BaseModel.extend(ValidationEngine, {
ajax: service(),
session: service(),
notifications: service(),
config: service(),
config: inject(),
// TODO: Once client-side permissions are in place,
// remove the hard role check.

View File

@ -4,6 +4,7 @@ import ShortcutsRoute from 'ghost-admin/mixins/shortcuts-route';
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
import windowProxy from 'ghost-admin/utils/window-proxy';
import {InitSentryForEmber} from '@sentry/ember';
import {inject} from 'ghost-admin/decorators/inject';
import {
isAjaxError,
isNotFoundError,
@ -27,7 +28,7 @@ shortcuts[`${ctrlOrCmd}+s`] = {action: 'save', scope: 'all'};
export default Route.extend(ShortcutsRoute, {
ajax: service(),
config: service(),
configManager: service(),
feature: service(),
ghostPaths: service(),
notifications: service(),
@ -52,6 +53,8 @@ export default Route.extend(ShortcutsRoute, {
this.ui.initBodyDragHandlers();
},
config: inject(),
async beforeModel() {
await this.session.setup();
return this.prepareApp();
@ -153,7 +156,7 @@ export default Route.extend(ShortcutsRoute, {
},
async prepareApp() {
await this.config.fetchUnauthenticated();
await this.configManager.fetchUnauthenticated();
// init Sentry here rather than app.js so that we can use API-supplied
// sentry_dsn and sentry_env rather than building it into release assets

View File

@ -1,10 +1,12 @@
import Route from '@ember/routing/route';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
export default class DesignsandboxRoute extends Route {
@service config;
@service store;
@inject config;
beforeModel() {
super.beforeModel(...arguments);
if (!this.config.enableDeveloperExperiments) {

View File

@ -1,10 +1,13 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import {inject} from 'ghost-admin/decorators/inject';
import {pluralize} from 'ember-inflector';
import {inject as service} from '@ember/service';
export default class EditRoute extends AuthenticatedRoute {
@service router;
@inject config;
beforeModel(transition) {
super.beforeModel(...arguments);

View File

@ -1,10 +1,10 @@
import $ from 'jquery';
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import {inject} from 'ghost-admin/decorators/inject';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
export default AuthenticatedRoute.extend({
config: service(),
feature: service(),
notifications: service(),
router: service(),
@ -12,6 +12,8 @@ export default AuthenticatedRoute.extend({
classNames: ['editor'],
config: inject(),
beforeModel() {
if (!this.config.editor?.url) {
return this.router.transitionTo('posts');

View File

@ -1,11 +1,13 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
export default class ProRoute extends AuthenticatedRoute {
@service billing;
@service session;
@service config;
@inject config;
queryParams = {
action: {refreshModel: true}

View File

@ -2,13 +2,15 @@ import AdminRoute from 'ghost-admin/routes/admin';
import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes';
import RSVP from 'rsvp';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
export default class AnalyticsSettingsRoute extends AdminRoute {
@service modals;
@service config;
@service settings;
@inject config;
model() {
return RSVP.hash({
settings: this.settings.reload()

View File

@ -2,13 +2,15 @@ import AdminRoute from 'ghost-admin/routes/admin';
import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes';
import RSVP from 'rsvp';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
export default class GeneralSettingsRoute extends AdminRoute {
@service modals;
@service config;
@service settings;
@inject config;
model() {
return RSVP.hash({
settings: this.settings.reload()

View File

@ -1,9 +1,11 @@
import AdminRoute from 'ghost-admin/routes/admin';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
export default class ZapierRoute extends AdminRoute {
@service router;
@service config;
@inject config;
constructor() {
super(...arguments);

View File

@ -1,11 +1,13 @@
import Route from '@ember/routing/route';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
export default class SetupRoute extends Route {
@service ghostPaths;
@service session;
@service ajax;
@service config;
@inject config;
// use the beforeModel hook to check to see whether or not setup has been
// previously completed. If it has, stop the transition into the setup page.

View File

@ -2,6 +2,7 @@ import EmberObject from '@ember/object';
import UnauthenticatedRoute from 'ghost-admin/routes/unauthenticated';
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
import classic from 'ember-classic-decorator';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
@ -23,7 +24,8 @@ export default class SignupRoute extends UnauthenticatedRoute {
@service notifications;
@service session;
@service ajax;
@service config;
@inject config;
beforeModel() {
if (this.session.isAuthenticated) {

View File

@ -1,13 +1,6 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import {inject as service} from '@ember/service';
export default class SiteRoute extends AuthenticatedRoute {
@service config;
@service settings;
@service ui;
_hasLoggedIn = false;
model() {
return (new Date()).valueOf();
}

View File

@ -5,6 +5,7 @@ import moment from 'moment-timezone';
import {AjaxError, isAjaxError, isForbiddenError} from 'ember-ajax/errors';
import {captureMessage} from '@sentry/ember';
import {get} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {isArray as isEmberArray} from '@ember/array';
import {isNone} from '@ember/utils';
import {inject as service} from '@ember/service';
@ -163,9 +164,10 @@ export function isAcceptedResponse(errorOrStatus) {
@classic
class ajaxService extends AjaxService {
@service config;
@service session;
@inject config;
// 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

View File

@ -1,14 +1,15 @@
import Service, {inject as service} from '@ember/service';
import classic from 'ember-classic-decorator';
import {inject} from 'ghost-admin/decorators/inject';
@classic
export default class BillingService extends Service {
@service router;
@service config;
@service ghostPaths;
@service router;
@service store;
@inject config;
billingRouteRoot = '#/pro';
billingWindowOpen = false;
subscription = null;

View File

@ -0,0 +1,44 @@
import RSVP from 'rsvp';
import Service, {inject as service} from '@ember/service';
import {inject} from 'ghost-admin/decorators/inject';
import {setProperties} from '@ember/object';
export default class ConfigManagerService extends Service {
@service ajax;
@service ghostPaths;
@service session;
@inject config;
fetch() {
let promises = [];
promises.push(this.fetchUnauthenticated());
if (this.session.isAuthenticated) {
promises.push(this.fetchAuthenticated());
}
return RSVP.all(promises);
}
async fetchUnauthenticated() {
const siteUrl = this.ghostPaths.url.api('site');
const {site} = await this.ajax.request(siteUrl);
// normalize url to non-trailing-slash
site.blogUrl = site.url.replace(/\/$/, '');
site.blogTitle = site.title;
delete site.url;
delete site.title;
setProperties(this.config, site);
}
async fetchAuthenticated() {
const configUrl = this.ghostPaths.url.api('config');
const {config} = await this.ajax.request(configUrl);
setProperties(this.config, config);
}
}

View File

@ -1,90 +0,0 @@
import RSVP from 'rsvp';
import Service, {inject as service} from '@ember/service';
import timezoneData from '@tryghost/timezone-data';
import {TrackedObject} from 'tracked-built-ins';
import {tracked} from '@glimmer/tracking';
export default class ConfigService extends Service {
@service ajax;
@service ghostPaths;
@service session;
@tracked content = new TrackedObject();
availableTimezones = timezoneData;
fetch() {
let promises = [];
promises.push(this.fetchUnauthenticated());
if (this.session.isAuthenticated) {
promises.push(this.fetchAuthenticated());
}
return RSVP.all(promises);
}
async fetchUnauthenticated() {
const siteUrl = this.ghostPaths.url.api('site');
const {site} = await this.ajax.request(siteUrl);
// normalize url to non-trailing-slash
site.blogUrl = site.url.replace(/\/$/, '');
site.blogTitle = site.title;
delete site.url;
delete site.title;
Object.assign(this.content, site);
this._defineProperties(site);
}
async fetchAuthenticated() {
const configUrl = this.ghostPaths.url.api('config');
const {config} = await this.ajax.request(configUrl);
Object.assign(this.content, config);
this._defineProperties(config);
}
get blogDomain() {
const blogDomain = this.blogUrl
.replace(/^https?:\/\//, '')
.replace(/\/?$/, '');
return blogDomain;
}
get emailDomain() {
const blogDomain = this.blogDomain || '';
const domainExp = blogDomain.match(new RegExp('^([^/:?#]+)(?:[/:?#]|$)', 'i'));
const domain = (domainExp && domainExp[1]) || '';
if (domain.startsWith('www.')) {
return domain.replace(/^(www)\.(?=[^/]*\..{2,5})/, '');
}
return domain;
}
getSiteUrl(path) {
const siteUrl = new URL(this.blogUrl);
const subdir = siteUrl.pathname.endsWith('/') ? siteUrl.pathname : `${siteUrl.pathname}/`;
const fullPath = `${subdir}${path.replace(/^\//, '')}`;
return `${siteUrl.origin}${fullPath}`;
}
_defineProperties(obj) {
for (const name of Object.keys(obj)) {
if (!Object.prototype.hasOwnProperty.call(this, name)) {
Object.defineProperty(this, name, {
get() {
return this.content[name];
},
set(newValue) {
this.content[name] = newValue;
}
});
}
}
}
}

View File

@ -4,6 +4,7 @@ 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';
import {inject} from 'ghost-admin/decorators/inject';
export function feature(name, options = {}) {
let {user, onChange} = options;
@ -39,14 +40,13 @@ export function feature(name, options = {}) {
@classic
export default class FeatureService extends Service {
@service store;
@service config;
@service lazyLoader;
@service notifications;
@service session;
@service settings;
@service store;
@service notifications;
@service lazyLoader;
@inject config;
// features
@feature('emailAnalytics') emailAnalytics;

View File

@ -1,12 +1,14 @@
import Service, {inject as service} from '@ember/service';
import fetch from 'fetch';
import validator from 'validator';
import {inject} from 'ghost-admin/decorators/inject';
export default class FrontendService extends Service {
@service settings;
@service config;
@service ajax;
@inject config;
_hasLoggedIn = false;
_lastPassword = null;

View File

@ -2,6 +2,7 @@ import LimitService from '@tryghost/limit-service';
import RSVP from 'rsvp';
import Service, {inject as service} from '@ember/service';
import {bind} from '@ember/runloop';
import {inject} from 'ghost-admin/decorators/inject';
class LimitError {
constructor({errorType, errorDetails, message}) {
@ -24,10 +25,11 @@ class HostLimitError extends LimitError {
}
export default class LimitsService extends Service {
@service config;
@service store;
@service membersStats;
@inject config;
constructor() {
super(...arguments);
@ -41,9 +43,7 @@ export default class LimitsService extends Service {
let helpLink;
if (this.config.hostSettings?.billing?.enabled === true
&& this.config.hostSettings?.billing?.url
) {
if (this.config.hostSettings?.billing?.enabled === true && this.config.hostSettings?.billing?.url) {
helpLink = this.config.hostSettings.billing?.url;
} else {
helpLink = 'https://ghost.org/help/';

View File

@ -1,12 +1,14 @@
import Service, {inject as service} from '@ember/service';
import {inject} from 'ghost-admin/decorators/inject';
export default class MembersUtilsService extends Service {
@service config;
@service settings;
@service feature;
@service session;
@service store;
@inject config;
paidTiers = null;
get isMembersEnabled() {

View File

@ -3,6 +3,7 @@ import Service, {inject as service} from '@ember/service';
import {TrackedArray} from 'tracked-built-ins';
import {dasherize} from '@ember/string';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {isArray} from '@ember/array';
import {isBlank} from '@ember/utils';
import {
@ -38,9 +39,10 @@ const GENERIC_ERROR_NAMES = [
export const GENERIC_ERROR_MESSAGE = 'An unexpected error occurred, please try again.';
export default class NotificationsService extends Service {
@service config;
@service upgradeStatus;
@inject config;
@tracked delayedNotifications = new TrackedArray([]);
@tracked content = new TrackedArray([]);

View File

@ -2,13 +2,14 @@ import ESASessionService from 'ember-simple-auth/services/session';
import RSVP from 'rsvp';
import {configureScope} from '@sentry/ember';
import {getOwner} from '@ember/application';
import {inject} from 'ghost-admin/decorators/inject';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class SessionService extends ESASessionService {
@service config;
@service configManager;
@service('store') dataStore;
@service feature;
@service notifications;
@ -20,6 +21,8 @@ export default class SessionService extends ESASessionService {
@service whatsNew;
@service membersUtils;
@inject config;
@tracked user = null;
skipAuthSuccessHandler = false;
@ -36,7 +39,7 @@ export default class SessionService extends ESASessionService {
async postAuthPreparation() {
await RSVP.all([
this.config.fetchAuthenticated(),
this.configManager.fetchAuthenticated(),
this.feature.fetch(),
this.settings.fetch(),
this.membersUtils.fetch()
@ -112,7 +115,7 @@ export default class SessionService extends ESASessionService {
// TODO: this feels hacky, find a better way than using .send
triggerAuthorizationFailed() {
getOwner(this).lookup(`route:${this.router.currentRouteName}`).send('authorizationFailed');
getOwner(this).lookup(`route:${this.router.currentRouteName}`)?.send('authorizationFailed');
}
loadServerNotifications() {

View File

@ -1,7 +1,8 @@
import Service, {inject as service} from '@ember/service';
import Service from '@ember/service';
import fetch from 'fetch';
import {TrackedArray} from 'tracked-built-ins';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {isEmpty} from '@ember/utils';
import {task, taskGroup, timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
@ -11,7 +12,7 @@ const API_VERSION = 'v2';
const DEBOUNCE_MS = 600;
export default class TenorService extends Service {
@service config;
@inject config;
@tracked columnCount = 4;
@tracked columns = null;

View File

@ -1,6 +1,7 @@
import Service, {inject as service} from '@ember/service';
import config from 'ghost-admin/config/environment';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {isEmpty} from '@ember/utils';
import {isThemeValidationError} from 'ghost-admin/services/ajax';
import {task} from 'ember-concurrency';
@ -8,7 +9,6 @@ import {tracked} from '@glimmer/tracking';
export default class ThemeManagementService extends Service {
@service ajax;
@service config;
@service customThemeSettings;
@service limit;
@service modals;
@ -16,6 +16,8 @@ export default class ThemeManagementService extends Service {
@service store;
@service frontend;
@inject config;
@tracked isUploading;
@tracked previewType = 'homepage';
@tracked previewHtml;

View File

@ -5,6 +5,7 @@ import {
lightenToContrastThreshold
} from '@tryghost/color-utils';
import {action, get} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {isEmpty} from '@ember/utils';
import {tracked} from '@glimmer/tracking';
@ -39,13 +40,14 @@ function updateBodyClasses(transition) {
}
export default class UiService extends Service {
@service config;
@service dropdown;
@service feature;
@service mediaQueries;
@service router;
@service settings;
@inject config;
@tracked contextualNavMenu = null;
@tracked isFullScreen = false;
@tracked mainClass = '';

View File

@ -11,7 +11,6 @@ const API_VERSION = 'v1';
const DEBOUNCE_MS = 600;
export default Service.extend({
config: service(),
settings: service(),
columnCount: 3,

View File

@ -1,10 +1,11 @@
import Route from '@ember/routing/route';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
Route.reopen({
config: service(),
billing: service(),
router: service(),
config: inject(),
actions: {
willTransition(transition) {

View File

@ -1,5 +1,6 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {isBlank} from '@ember/utils';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
@ -8,12 +9,13 @@ import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class KoenigCardButtonComponent extends Component {
@service config;
@service feature;
@service store;
@service membersUtils;
@service ui;
@inject config;
@tracked buttonFocused = false;
@tracked contentFocused = false;
@tracked offers = null;

View File

@ -3,6 +3,7 @@ import Browser from 'mobiledoc-kit/utils/browser';
import Component from '@glimmer/component';
import {EmojiButton} from '@joeattardi/emoji-button';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {isBlank} from '@ember/utils';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
@ -12,12 +13,13 @@ import {tracked} from '@glimmer/tracking';
const storageKey = 'gh-kg-callout-emoji';
export default class KoenigCardCalloutComponent extends Component {
@service config;
@service feature;
@service store;
@service membersUtils;
@service ui;
@inject config;
get isEmpty() {
return isBlank(this.args.payload.calloutText) && isBlank(this.args.payload.calloutEmoji);
}

View File

@ -3,6 +3,7 @@ import Component from '@glimmer/component';
import {action} from '@ember/object';
import {formatTextReplacementHtml} from './koenig-text-replacement-html-input';
import {guidFor} from '@ember/object/internals';
import {inject} from 'ghost-admin/decorators/inject';
import {isBlank} from '@ember/utils';
import {run} from '@ember/runloop';
import {schedule} from '@ember/runloop';
@ -12,12 +13,13 @@ import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class KoenigCardEmailCtaComponent extends Component {
@service config;
@service feature;
@service store;
@service membersUtils;
@service ui;
@inject config;
@tracked buttonFocused = false;
@tracked contentFocused = false;
@tracked offers = null;

View File

@ -2,18 +2,20 @@ import Browser from 'mobiledoc-kit/utils/browser';
import Component from '@glimmer/component';
import {IMAGE_EXTENSIONS, IMAGE_MIME_TYPES} from 'ghost-admin/components/gh-image-uploader';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {isBlank} from '@ember/utils';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
import {set} from '@ember/object';
export default class KoenigCardHeaderComponent extends Component {
@service config;
@service feature;
@service store;
@service membersUtils;
@service ui;
@inject config;
imageExtensions = IMAGE_EXTENSIONS;
imageMimeTypes = IMAGE_MIME_TYPES;

View File

@ -5,6 +5,7 @@ import {
IMAGE_MIME_TYPES
} from 'ghost-admin/components/gh-image-uploader';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {isBlank} from '@ember/utils';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
@ -12,12 +13,13 @@ import {set} from '@ember/object';
import {tracked} from '@glimmer/tracking';
export default class KoenigCardProductComponent extends Component {
@service config;
@service feature;
@service store;
@service membersUtils;
@service ui;
@inject config;
@tracked files = null;
imageExtensions = IMAGE_EXTENSIONS;
imageMimeTypes = IMAGE_MIME_TYPES;

View File

@ -2,18 +2,20 @@ import Browser from 'mobiledoc-kit/utils/browser';
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {formatTextReplacementHtml} from './koenig-text-replacement-html-input';
import {inject} from 'ghost-admin/decorators/inject';
import {isBlank} from '@ember/utils';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
import {set} from '@ember/object';
export default class KoenigCardToggleComponent extends Component {
@service config;
@service feature;
@service store;
@service membersUtils;
@service ui;
@inject config;
get formattedHeading() {
return formatTextReplacementHtml(this.args.payload.heading);
}

View File

@ -7,8 +7,8 @@ import {action, computed} from '@ember/object';
import {attributeBindings, classNames} from '@ember-decorators/component';
import {getLinkMarkupFromRange} from '../utils/markup-utils';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
// pixels that should be added to the `left` property of the tick adjustment styles
// TODO: handle via CSS?
@ -18,7 +18,7 @@ const TICK_ADJUSTMENT = 8;
@attributeBindings('style')
@classNames('kg-input-bar', 'absolute', 'z-999')
export default class KoenigLinkInput extends Component {
@service config;
@inject config;
// public attrs
editor = null;

View File

@ -5,8 +5,8 @@ import {action, computed} from '@ember/object';
import {attributeBindings, classNames} from '@ember-decorators/component';
import {getEventTargetMatchingTag} from 'mobiledoc-kit/utils/element-utils';
import {htmlSafe} from '@ember/template';
import {inject} from 'ghost-admin/decorators/inject';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
// gap between link and toolbar bottom
const TOOLBAR_MARGIN = 8;
@ -22,7 +22,7 @@ const DELAY = 120;
@attributeBindings('style')
@classNames('absolute', 'z-999')
export default class KoenigLinkToolbar extends Component {
@service config;
@inject config;
// public attrs
container = null;

View File

@ -1,13 +1,15 @@
import Helper from '@ember/component/helper';
import {get} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {isArray} from '@ember/array';
import {inject as service} from '@ember/service';
export default class CardIsAvailableHelper extends Helper {
@service config;
@service feature;
@service settings;
@inject config;
compute([card], {postType} = {}) {
let cardIsAvailable = true;

View File

@ -1,4 +1,3 @@
import Service from '@ember/service';
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
@ -9,10 +8,10 @@ describe('Unit: Component: gh-url-preview', function () {
setupRenderingTest();
beforeEach(function () {
let configStub = Service.extend({
let configStub = {
blogUrl: 'http://my-ghost-blog.com'
});
this.owner.register('service:config', configStub);
};
this.owner.register('config:main', configStub, {instantiate: false});
});
it('generates the correct preview URL with a prefix', async function () {

View File

@ -5,7 +5,7 @@ import {expect} from 'chai';
import {settled} from '@ember/test-helpers';
import {setupTest} from 'ember-mocha';
describe('Integration: Service: config', function () {
describe('Integration: Service: config-manager', function () {
setupTest();
let server;
@ -19,8 +19,8 @@ describe('Integration: Service: config', function () {
});
it('returns a list of timezones in the expected format', function () {
const service = this.owner.lookup('service:config');
const timezones = service.availableTimezones;
const injection = this.owner.lookup('config:main');
const timezones = injection.availableTimezones;
expect(timezones.length).to.equal(66);
expect(timezones[0].name).to.equal('Pacific/Pago_Pago');
@ -51,13 +51,14 @@ describe('Integration: Service: config', function () {
];
});
};
let service = this.owner.lookup('service:config');
const service = this.owner.lookup('service:config-manager');
const injection = this.owner.lookup('config:main');
stubBlogUrl('http://localhost:2368/');
service.fetch().then(() => {
expect(
service.get('blogUrl'), 'trailing-slash'
injection.blogUrl, 'trailing-slash'
).to.equal('http://localhost:2368');
});
@ -66,7 +67,7 @@ describe('Integration: Service: config', function () {
service.fetch().then(() => {
expect(
service.get('blogUrl'), 'non-trailing-slash'
injection.blogUrl, 'non-trailing-slash'
).to.equal('http://localhost:2368');
done();

View File

@ -117,7 +117,7 @@ describe('Integration: Service: feature', function () {
await session.populateUser();
let service = this.owner.lookup('service:feature');
service.get('config').set('testFlag', false);
service.config.testFlag = false;
return service.fetch().then(() => {
expect(service.get('labs.testFlag')).to.be.false;
@ -135,7 +135,7 @@ describe('Integration: Service: feature', function () {
await session.populateUser();
let service = this.owner.lookup('service:feature');
service.get('config').set('testFlag', true);
service.config.testFlag = true;
return service.fetch().then(() => {
expect(service.get('labs.testFlag')).to.be.false;
@ -153,7 +153,7 @@ describe('Integration: Service: feature', function () {
await session.populateUser();
let service = this.owner.lookup('service:feature');
service.get('config').set('testFlag', false);
service.config.testFlag = false;
return service.fetch().then(() => {
expect(service.get('labs.testFlag')).to.be.true;
@ -171,7 +171,7 @@ describe('Integration: Service: feature', function () {
await session.populateUser();
let service = this.owner.lookup('service:feature');
service.get('config').set('testFlag', true);
service.config.testFlag = true;
return service.fetch().then(() => {
expect(service.get('labs.testFlag')).to.be.true;
@ -223,7 +223,7 @@ describe('Integration: Service: feature', function () {
await session.populateUser();
let service = this.owner.lookup('service:feature');
service.get('config').set('testFlag', false);
service.config.testFlag = false;
return service.fetch().then(() => {
expect(service.get('testFlag')).to.be.false;
@ -274,7 +274,7 @@ describe('Integration: Service: feature', function () {
await session.populateUser();
let service = this.owner.lookup('service:feature');
service.get('config').set('testFlag', false);
service.config.testFlag = false;
return service.fetch().then(() => {
expect(service.get('testFlag')).to.be.false;
@ -339,25 +339,25 @@ describe('Integration: Service: feature', function () {
addTestFlag();
let session = this.owner.lookup('service:session');
await session.populateUser();
let sessionService = this.owner.lookup('service:session');
await sessionService.populateUser();
let service = this.owner.lookup('service:feature');
service.get('config').set('testFlag', false);
let featureService = this.owner.lookup('service:feature');
featureService.config.testFlag = false;
return service.fetch().then(() => {
expect(service.get('testFlag'), 'testFlag before set').to.be.false;
return featureService.fetch().then(() => {
expect(featureService.get('testFlag'), 'testFlag before set').to.be.false;
run(() => {
expect(() => {
service.set('testFlag', true);
featureService.set('testFlag', true);
}, EmberError, 'threw validation error');
});
return settled().then(() => {
// ensure validation is happening before the API is hit
expect(server.handlers[2].numberOfCalls).to.equal(0);
expect(service.get('testFlag')).to.be.false;
expect(featureService.get('testFlag')).to.be.false;
});
});
});