mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-02 08:13:34 +03:00
9bdb25d184
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`
602 lines
16 KiB
JavaScript
602 lines
16 KiB
JavaScript
import Component from '@ember/component';
|
||
import boundOneWay from 'ghost-admin/utils/bound-one-way';
|
||
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';
|
||
|
||
@classic
|
||
@tagName('')
|
||
export default class GhPostSettingsMenu extends Component {
|
||
@service feature;
|
||
@service store;
|
||
@service ajax;
|
||
@service ghostPaths;
|
||
@service notifications;
|
||
@service slugGenerator;
|
||
@service session;
|
||
@service settings;
|
||
@service ui;
|
||
|
||
@inject config;
|
||
|
||
post = null;
|
||
isViewingSubview = false;
|
||
|
||
@alias('post.canonicalUrlScratch')
|
||
canonicalUrlScratch;
|
||
|
||
@alias('post.customExcerptScratch')
|
||
customExcerptScratch;
|
||
|
||
@alias('post.codeinjectionFootScratch')
|
||
codeinjectionFootScratch;
|
||
|
||
@alias('post.codeinjectionHeadScratch')
|
||
codeinjectionHeadScratch;
|
||
|
||
@alias('post.metaDescriptionScratch')
|
||
metaDescriptionScratch;
|
||
|
||
@alias('post.metaTitleScratch')
|
||
metaTitleScratch;
|
||
|
||
@alias('post.ogDescriptionScratch')
|
||
ogDescriptionScratch;
|
||
|
||
@alias('post.ogTitleScratch')
|
||
ogTitleScratch;
|
||
|
||
@alias('post.twitterDescriptionScratch')
|
||
twitterDescriptionScratch;
|
||
|
||
@alias('post.twitterTitleScratch')
|
||
twitterTitleScratch;
|
||
|
||
@boundOneWay('post.slug')
|
||
slugValue;
|
||
|
||
@boundOneWay('post.uuid')
|
||
uuidValue;
|
||
|
||
@or('metaDescriptionScratch', 'customExcerptScratch', 'post.excerpt')
|
||
seoDescription;
|
||
|
||
@or(
|
||
'ogDescriptionScratch',
|
||
'customExcerptScratch',
|
||
'seoDescription',
|
||
'post.excerpt',
|
||
'settings.description',
|
||
''
|
||
)
|
||
facebookDescription;
|
||
|
||
@or(
|
||
'post.ogImage',
|
||
'post.featureImage',
|
||
'settings.ogImage',
|
||
'settings.coverImage'
|
||
)
|
||
facebookImage;
|
||
|
||
@or('ogTitleScratch', 'seoTitle')
|
||
facebookTitle;
|
||
|
||
@or(
|
||
'twitterDescriptionScratch',
|
||
'customExcerptScratch',
|
||
'seoDescription',
|
||
'post.excerpt',
|
||
'settings.description',
|
||
''
|
||
)
|
||
twitterDescription;
|
||
|
||
@or(
|
||
'post.twitterImage',
|
||
'post.featureImage',
|
||
'settings.twitterImage',
|
||
'settings.coverImage'
|
||
)
|
||
twitterImage;
|
||
|
||
@or('twitterTitleScratch', 'seoTitle')
|
||
twitterTitle;
|
||
|
||
@or(
|
||
'session.user.isOwnerOnly',
|
||
'session.user.isAdminOnly',
|
||
'session.user.isEditor'
|
||
)
|
||
showVisibilityInput;
|
||
|
||
@or(
|
||
'session.user.isOwnerOnly',
|
||
'session.user.isAdminOnly',
|
||
'session.user.isEditor'
|
||
)
|
||
showEmailNewsletter;
|
||
|
||
@computed('metaTitleScratch', 'post.titleScratch')
|
||
get seoTitle() {
|
||
return this.metaTitleScratch || this.post.titleScratch || '(Untitled)';
|
||
}
|
||
|
||
@computed('post.{slug,canonicalUrl}', 'config.blogUrl')
|
||
get seoURL() {
|
||
const urlParts = [];
|
||
|
||
if (this.post.canonicalUrl) {
|
||
try {
|
||
const canonicalUrl = new URL(this.post.canonicalUrl);
|
||
urlParts.push(canonicalUrl.host);
|
||
urlParts.push(...canonicalUrl.pathname.split('/').reject(p => !p));
|
||
} catch (e) {
|
||
// no-op, invalid URL
|
||
}
|
||
} else {
|
||
const blogUrl = new URL(this.config.blogUrl);
|
||
urlParts.push(blogUrl.host);
|
||
urlParts.push(...blogUrl.pathname.split('/').reject(p => !p));
|
||
urlParts.push(this.post.slug);
|
||
}
|
||
|
||
return urlParts.join(' › ');
|
||
}
|
||
|
||
willDestroyElement() {
|
||
super.willDestroyElement(...arguments);
|
||
|
||
let post = this.post;
|
||
let errors = post.get('errors');
|
||
|
||
// reset the publish date if it has an error
|
||
if (errors.has('publishedAtBlogDate') || errors.has('publishedAtBlogTime')) {
|
||
post.set('publishedAtBlogTZ', post.get('publishedAtUTC'));
|
||
post.validate({attribute: 'publishedAtBlog'});
|
||
}
|
||
|
||
this.setSidebarWidthVariable(0);
|
||
}
|
||
|
||
@action
|
||
showSubview(subview) {
|
||
this.set('isViewingSubview', true);
|
||
this.set('subview', subview);
|
||
}
|
||
|
||
@action
|
||
closeSubview() {
|
||
this.set('isViewingSubview', false);
|
||
this.set('subview', null);
|
||
}
|
||
|
||
@action
|
||
discardEnter() {
|
||
return false;
|
||
}
|
||
|
||
@action
|
||
toggleFeatured() {
|
||
this.toggleProperty('post.featured');
|
||
|
||
// If this is a new post. Don't save the post. Defer the save
|
||
// to the user pressing the save button
|
||
if (this.get('post.isNew')) {
|
||
return;
|
||
}
|
||
|
||
this.savePostTask.perform().catch((error) => {
|
||
this.showError(error);
|
||
this.post.rollbackAttributes();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* triggered by user manually changing slug
|
||
*/
|
||
@action
|
||
updateSlug(newSlug) {
|
||
return this.updateSlugTask
|
||
.perform(newSlug)
|
||
.catch((error) => {
|
||
this.showError(error);
|
||
this.post.rollbackAttributes();
|
||
});
|
||
}
|
||
|
||
@action
|
||
setPublishedAtBlogDate(date) {
|
||
// date is a Date object that contains the correct date string in the blog timezone
|
||
let post = this.post;
|
||
let dateString = moment.tz(date, this.settings.get('timezone')).format('YYYY-MM-DD');
|
||
|
||
post.get('errors').remove('publishedAtBlogDate');
|
||
|
||
if (post.get('isNew') || date === post.get('publishedAtBlogDate')) {
|
||
post.validate({property: 'publishedAtBlog'});
|
||
} else {
|
||
post.set('publishedAtBlogDate', dateString);
|
||
return this.savePostTask.perform();
|
||
}
|
||
}
|
||
|
||
@action
|
||
async setVisibility(segment) {
|
||
this.post.set('tiers', segment);
|
||
try {
|
||
await this.post.validate({property: 'visibility'});
|
||
await this.post.validate({property: 'tiers'});
|
||
if (this.post.get('isDraft') && this.post.changedAttributes().tiers) {
|
||
await this.savePostTask.perform();
|
||
}
|
||
} catch (e) {
|
||
if (!e) {
|
||
// validation error
|
||
return;
|
||
}
|
||
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
@action
|
||
setPublishedAtBlogTime(time) {
|
||
let post = this.post;
|
||
|
||
post.get('errors').remove('publishedAtBlogDate');
|
||
|
||
if (post.get('isNew') || time === post.get('publishedAtBlogTime')) {
|
||
post.validate({property: 'publishedAtBlog'});
|
||
} else {
|
||
post.set('publishedAtBlogTime', time);
|
||
return this.savePostTask.perform();
|
||
}
|
||
}
|
||
|
||
@action
|
||
setCustomExcerpt(excerpt) {
|
||
let post = this.post;
|
||
let currentExcerpt = post.get('customExcerpt');
|
||
|
||
if (excerpt === currentExcerpt) {
|
||
return;
|
||
}
|
||
|
||
post.set('customExcerpt', excerpt);
|
||
|
||
return post.validate({property: 'customExcerpt'}).then(() => this.savePostTask.perform());
|
||
}
|
||
|
||
@action
|
||
setHeaderInjection(code) {
|
||
let post = this.post;
|
||
let currentCode = post.get('codeinjectionHead');
|
||
|
||
if (code === currentCode) {
|
||
return;
|
||
}
|
||
|
||
post.set('codeinjectionHead', code);
|
||
|
||
return post.validate({property: 'codeinjectionHead'}).then(() => this.savePostTask.perform());
|
||
}
|
||
|
||
@action
|
||
setFooterInjection(code) {
|
||
let post = this.post;
|
||
let currentCode = post.get('codeinjectionFoot');
|
||
|
||
if (code === currentCode) {
|
||
return;
|
||
}
|
||
|
||
post.set('codeinjectionFoot', code);
|
||
|
||
return post.validate({property: 'codeinjectionFoot'}).then(() => this.savePostTask.perform());
|
||
}
|
||
|
||
@action
|
||
setMetaTitle(metaTitle) {
|
||
// Grab the post and current stored meta title
|
||
let post = this.post;
|
||
let currentTitle = post.get('metaTitle');
|
||
|
||
// If the title entered matches the stored meta title, do nothing
|
||
if (currentTitle === metaTitle) {
|
||
return;
|
||
}
|
||
|
||
// If the title entered is different, set it as the new meta title
|
||
post.set('metaTitle', metaTitle);
|
||
|
||
// Make sure the meta title is valid and if so, save it into the post
|
||
return post.validate({property: 'metaTitle'}).then(() => {
|
||
if (post.get('isNew')) {
|
||
return;
|
||
}
|
||
|
||
return this.savePostTask.perform();
|
||
});
|
||
}
|
||
|
||
@action
|
||
setMetaDescription(metaDescription) {
|
||
// Grab the post and current stored meta description
|
||
let post = this.post;
|
||
let currentDescription = post.get('metaDescription');
|
||
|
||
// If the title entered matches the stored meta title, do nothing
|
||
if (currentDescription === metaDescription) {
|
||
return;
|
||
}
|
||
|
||
// If the title entered is different, set it as the new meta title
|
||
post.set('metaDescription', metaDescription);
|
||
|
||
// Make sure the meta title is valid and if so, save it into the post
|
||
return post.validate({property: 'metaDescription'}).then(() => {
|
||
if (post.get('isNew')) {
|
||
return;
|
||
}
|
||
|
||
return this.savePostTask.perform();
|
||
});
|
||
}
|
||
|
||
@action
|
||
setCanonicalUrl(value) {
|
||
// Grab the post and current stored meta description
|
||
let post = this.post;
|
||
let currentCanonicalUrl = post.canonicalUrl;
|
||
|
||
// If the value entered matches the stored value, do nothing
|
||
if (currentCanonicalUrl === value) {
|
||
return;
|
||
}
|
||
|
||
// If the value supplied is different, set it as the new value
|
||
post.set('canonicalUrl', value);
|
||
|
||
// Make sure the value is valid and if so, save it into the post
|
||
return post.validate({property: 'canonicalUrl'}).then(() => {
|
||
if (post.get('isNew')) {
|
||
return;
|
||
}
|
||
|
||
return this.savePostTask.perform();
|
||
});
|
||
}
|
||
|
||
@action
|
||
setOgTitle(ogTitle) {
|
||
// Grab the post and current stored facebook title
|
||
let post = this.post;
|
||
let currentTitle = post.get('ogTitle');
|
||
|
||
// If the title entered matches the stored facebook title, do nothing
|
||
if (currentTitle === ogTitle) {
|
||
return;
|
||
}
|
||
|
||
// If the title entered is different, set it as the new facebook title
|
||
post.set('ogTitle', ogTitle);
|
||
|
||
// Make sure the facebook title is valid and if so, save it into the post
|
||
return post.validate({property: 'ogTitle'}).then(() => {
|
||
if (post.get('isNew')) {
|
||
return;
|
||
}
|
||
|
||
return this.savePostTask.perform();
|
||
});
|
||
}
|
||
|
||
@action
|
||
setOgDescription(ogDescription) {
|
||
// Grab the post and current stored facebook description
|
||
let post = this.post;
|
||
let currentDescription = post.get('ogDescription');
|
||
|
||
// If the title entered matches the stored facebook description, do nothing
|
||
if (currentDescription === ogDescription) {
|
||
return;
|
||
}
|
||
|
||
// If the description entered is different, set it as the new facebook description
|
||
post.set('ogDescription', ogDescription);
|
||
|
||
// Make sure the facebook description is valid and if so, save it into the post
|
||
return post.validate({property: 'ogDescription'}).then(() => {
|
||
if (post.get('isNew')) {
|
||
return;
|
||
}
|
||
|
||
return this.savePostTask.perform();
|
||
});
|
||
}
|
||
|
||
@action
|
||
setTwitterTitle(twitterTitle) {
|
||
// Grab the post and current stored twitter title
|
||
let post = this.post;
|
||
let currentTitle = post.get('twitterTitle');
|
||
|
||
// If the title entered matches the stored twitter title, do nothing
|
||
if (currentTitle === twitterTitle) {
|
||
return;
|
||
}
|
||
|
||
// If the title entered is different, set it as the new twitter title
|
||
post.set('twitterTitle', twitterTitle);
|
||
|
||
// Make sure the twitter title is valid and if so, save it into the post
|
||
return post.validate({property: 'twitterTitle'}).then(() => {
|
||
if (post.get('isNew')) {
|
||
return;
|
||
}
|
||
|
||
return this.savePostTask.perform();
|
||
});
|
||
}
|
||
|
||
@action
|
||
setTwitterDescription(twitterDescription) {
|
||
// Grab the post and current stored twitter description
|
||
let post = this.post;
|
||
let currentDescription = post.get('twitterDescription');
|
||
|
||
// If the description entered matches the stored twitter description, do nothing
|
||
if (currentDescription === twitterDescription) {
|
||
return;
|
||
}
|
||
|
||
// If the description entered is different, set it as the new twitter description
|
||
post.set('twitterDescription', twitterDescription);
|
||
|
||
// Make sure the twitter description is valid and if so, save it into the post
|
||
return post.validate({property: 'twitterDescription'}).then(() => {
|
||
if (post.get('isNew')) {
|
||
return;
|
||
}
|
||
|
||
return this.savePostTask.perform();
|
||
});
|
||
}
|
||
|
||
@action
|
||
setCoverImage(image) {
|
||
this.set('post.featureImage', image);
|
||
|
||
if (this.get('post.isNew')) {
|
||
return;
|
||
}
|
||
|
||
this.savePostTask.perform().catch((error) => {
|
||
this.showError(error);
|
||
this.post.rollbackAttributes();
|
||
});
|
||
}
|
||
|
||
@action
|
||
clearCoverImage() {
|
||
this.set('post.featureImage', '');
|
||
|
||
if (this.get('post.isNew')) {
|
||
return;
|
||
}
|
||
|
||
this.savePostTask.perform().catch((error) => {
|
||
this.showError(error);
|
||
this.post.rollbackAttributes();
|
||
});
|
||
}
|
||
|
||
@action
|
||
setOgImage(image) {
|
||
this.set('post.ogImage', image);
|
||
|
||
if (this.get('post.isNew')) {
|
||
return;
|
||
}
|
||
|
||
this.savePostTask.perform().catch((error) => {
|
||
this.showError(error);
|
||
this.post.rollbackAttributes();
|
||
});
|
||
}
|
||
|
||
@action
|
||
clearOgImage() {
|
||
this.set('post.ogImage', '');
|
||
|
||
if (this.get('post.isNew')) {
|
||
return;
|
||
}
|
||
|
||
this.savePostTask.perform().catch((error) => {
|
||
this.showError(error);
|
||
this.post.rollbackAttributes();
|
||
});
|
||
}
|
||
|
||
@action
|
||
setTwitterImage(image) {
|
||
this.set('post.twitterImage', image);
|
||
|
||
if (this.get('post.isNew')) {
|
||
return;
|
||
}
|
||
|
||
this.savePostTask.perform().catch((error) => {
|
||
this.showError(error);
|
||
this.post.rollbackAttributes();
|
||
});
|
||
}
|
||
|
||
@action
|
||
clearTwitterImage() {
|
||
this.set('post.twitterImage', '');
|
||
|
||
if (this.get('post.isNew')) {
|
||
return;
|
||
}
|
||
|
||
this.savePostTask.perform().catch((error) => {
|
||
this.showError(error);
|
||
this.post.rollbackAttributes();
|
||
});
|
||
}
|
||
|
||
@action
|
||
changeAuthors(newAuthors) {
|
||
let post = this.post;
|
||
|
||
// return if nothing changed
|
||
if (newAuthors.mapBy('id').join() === post.get('authors').mapBy('id').join()) {
|
||
return;
|
||
}
|
||
|
||
post.set('authors', newAuthors);
|
||
post.validate({property: 'authors'});
|
||
|
||
// if this is a new post (never been saved before), don't try to save it
|
||
if (post.get('isNew')) {
|
||
return;
|
||
}
|
||
|
||
this.savePostTask.perform().catch((error) => {
|
||
this.showError(error);
|
||
post.rollbackAttributes();
|
||
});
|
||
}
|
||
|
||
@action
|
||
deletePostInternal() {
|
||
if (this.deletePost) {
|
||
this.deletePost();
|
||
}
|
||
}
|
||
|
||
@action
|
||
setSidebarWidthFromElement(element) {
|
||
const width = element.getBoundingClientRect().width;
|
||
this.setSidebarWidthVariable(width);
|
||
}
|
||
|
||
showError(error) {
|
||
// TODO: remove null check once ValidationEngine has been removed
|
||
if (error) {
|
||
this.notifications.showAPIError(error);
|
||
}
|
||
}
|
||
|
||
setSidebarWidthVariable(width) {
|
||
document.documentElement.style.setProperty('--editor-sidebar-width', `${width}px`);
|
||
}
|
||
}
|