ghost/admin/app/components/gh-post-settings-menu.js
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';
import {tracked} from '@glimmer/tracking';
@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 themeManagement;
@service ui;
@inject config;
@tracked showPostHistory = false;
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')
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;
@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(' › ');
}
get canViewPostHistory() {
// Can only view history for lexical posts
if (this.post.lexical === null) {
return false;
}
// Can view history for all unpublished/unsent posts
if (!this.post.isPublished && !this.post.isSent) {
return true;
}
// Cannot view history for published posts if there isn't a web version
if (this.post.emailOnly) {
return false;
}
return true;
}
get themeMissingShowTitleAndFeatureImage() {
return !this.themeManagement.activeTheme.hasPageBuilderFeature('show_title_and_feature_image');
}
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.post.featured = !this.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.post.isNew) {
return;
}
this.savePostTask.perform().catch((error) => {
this.showError(error);
this.post.rollbackAttributes();
});
}
@action
toggleShowTitleAndFeatureImage(event) {
this.post.showTitleAndFeatureImage = event.target.checked;
// If this is a new post. Don't save the post. Defer the save
// to the user pressing the save button
if (this.post.isNew) {
return;
}
this.savePostTask.perform().catch((error) => {
this.showError(error);
this.post.rollbackAttributes();
});
}
@action
openPostHistory() {
this.showPostHistory = true;
}
@action
closePostHistory() {
this.showPostHistory = false;
}
/**
* 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`);
document.documentElement.style.setProperty('--kg-breakout-adjustment', `${width}px`);
}
}