⚡️ Add "Excerpt" field to post settings menu (#810)
refs TryGhost/Ghost#8793 - add `customExcerpt` attr to Post model + reorder attrs to be alphabetical - add "Excerpt" field to PSM - add validation for `customExcerpt` length (max 300 chars) - add style adjustments for custom excerpt UI
This commit is contained in:
parent
91ac2dee40
commit
45210fb8bf
|
@ -30,6 +30,7 @@ export default Component.extend(SettingsMenuMixin, {
|
||||||
|
|
||||||
model: null,
|
model: null,
|
||||||
slugValue: boundOneWay('model.slug'),
|
slugValue: boundOneWay('model.slug'),
|
||||||
|
customExcerptScratch: alias('model.customExcerptScratch'),
|
||||||
metaTitleScratch: alias('model.metaTitleScratch'),
|
metaTitleScratch: alias('model.metaTitleScratch'),
|
||||||
metaDescriptionScratch: alias('model.metaDescriptionScratch'),
|
metaDescriptionScratch: alias('model.metaDescriptionScratch'),
|
||||||
|
|
||||||
|
@ -245,6 +246,21 @@ export default Component.extend(SettingsMenuMixin, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setCustomExcerpt(excerpt) {
|
||||||
|
let model = this.get('model');
|
||||||
|
let currentExcerpt = model.get('customExcerpt');
|
||||||
|
|
||||||
|
if (excerpt === currentExcerpt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
model.set('customExcerpt', excerpt);
|
||||||
|
|
||||||
|
return model.validate({property: 'customExcerpt'}).then(() => {
|
||||||
|
return model.save();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
setMetaTitle(metaTitle) {
|
setMetaTitle(metaTitle) {
|
||||||
// Grab the model and current stored meta title
|
// Grab the model and current stored meta title
|
||||||
let model = this.get('model');
|
let model = this.get('model');
|
||||||
|
|
|
@ -158,6 +158,7 @@ export default Mixin.create({
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set('model.title', this.get('model.titleScratch'));
|
this.set('model.title', this.get('model.titleScratch'));
|
||||||
|
this.set('model.customExcerpt', this.get('model.customExcerptScratch'));
|
||||||
this.set('model.metaTitle', this.get('model.metaTitleScratch'));
|
this.set('model.metaTitle', this.get('model.metaTitleScratch'));
|
||||||
this.set('model.metaDescription', this.get('model.metaDescriptionScratch'));
|
this.set('model.metaDescription', this.get('model.metaDescriptionScratch'));
|
||||||
|
|
||||||
|
|
|
@ -73,32 +73,33 @@ export default Model.extend(Comparable, ValidationEngine, {
|
||||||
|
|
||||||
validationType: 'post',
|
validationType: 'post',
|
||||||
|
|
||||||
uuid: attr('string'),
|
|
||||||
title: attr('string', {defaultValue: ''}),
|
|
||||||
slug: attr('string'),
|
|
||||||
mobiledoc: attr('json-string', {defaultValue: () => BLANK_DOC}),
|
|
||||||
html: attr('string'),
|
|
||||||
featureImage: attr('string'),
|
|
||||||
featured: attr('boolean', {defaultValue: false}),
|
|
||||||
page: attr('boolean', {defaultValue: false}),
|
|
||||||
plaintext: attr('string'),
|
|
||||||
status: attr('string', {defaultValue: 'draft'}),
|
|
||||||
locale: attr('string'),
|
|
||||||
metaTitle: attr('string'),
|
|
||||||
metaDescription: attr('string'),
|
|
||||||
author: belongsTo('user', {async: true}),
|
author: belongsTo('user', {async: true}),
|
||||||
authorId: attr('string'),
|
authorId: attr('string'),
|
||||||
updatedAtUTC: attr('moment-utc'),
|
|
||||||
updatedBy: attr(),
|
|
||||||
publishedAtUTC: attr('moment-utc'),
|
|
||||||
publishedBy: belongsTo('user', {async: true}),
|
|
||||||
createdAtUTC: attr('moment-utc'),
|
createdAtUTC: attr('moment-utc'),
|
||||||
createdBy: attr(),
|
createdBy: attr(),
|
||||||
|
customExcerpt: attr(),
|
||||||
|
featured: attr('boolean', {defaultValue: false}),
|
||||||
|
featureImage: attr('string'),
|
||||||
|
html: attr('string'),
|
||||||
|
locale: attr('string'),
|
||||||
|
metaDescription: attr('string'),
|
||||||
|
metaTitle: attr('string'),
|
||||||
|
mobiledoc: attr('json-string', {defaultValue: () => BLANK_DOC}),
|
||||||
|
page: attr('boolean', {defaultValue: false}),
|
||||||
|
plaintext: attr('string'),
|
||||||
|
publishedAtUTC: attr('moment-utc'),
|
||||||
|
publishedBy: belongsTo('user', {async: true}),
|
||||||
|
slug: attr('string'),
|
||||||
|
status: attr('string', {defaultValue: 'draft'}),
|
||||||
tags: hasMany('tag', {
|
tags: hasMany('tag', {
|
||||||
embedded: 'always',
|
embedded: 'always',
|
||||||
async: false
|
async: false
|
||||||
}),
|
}),
|
||||||
|
title: attr('string', {defaultValue: ''}),
|
||||||
|
updatedAtUTC: attr('moment-utc'),
|
||||||
|
updatedBy: attr(),
|
||||||
url: attr('string'),
|
url: attr('string'),
|
||||||
|
uuid: attr('string'),
|
||||||
|
|
||||||
scratch: null,
|
scratch: null,
|
||||||
titleScratch: null,
|
titleScratch: null,
|
||||||
|
@ -114,6 +115,7 @@ export default Model.extend(Comparable, ValidationEngine, {
|
||||||
publishedAtBlogDate: '',
|
publishedAtBlogDate: '',
|
||||||
publishedAtBlogTime: '',
|
publishedAtBlogTime: '',
|
||||||
|
|
||||||
|
customExcerptScratch: boundOneWay('customExcerpt'),
|
||||||
metaTitleScratch: boundOneWay('metaTitle'),
|
metaTitleScratch: boundOneWay('metaTitle'),
|
||||||
metaDescriptionScratch: boundOneWay('metaDescription'),
|
metaDescriptionScratch: boundOneWay('metaDescription'),
|
||||||
|
|
||||||
|
|
|
@ -179,6 +179,15 @@
|
||||||
stroke: color(var(--red) lightness(-10%));
|
stroke: color(var(--red) lightness(-10%));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-menu-content .selectize-input {
|
||||||
|
padding: 7px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-setting-custom-excerpt {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Background
|
/* Background
|
||||||
/* ---------------------------------------------------------- */
|
/* ---------------------------------------------------------- */
|
||||||
|
|
|
@ -73,6 +73,12 @@
|
||||||
plugins="remove_button, drag_drop"}}
|
plugins="remove_button, drag_drop"}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="customExcerpt"}}
|
||||||
|
<label for="custom-excerpt">Excerpt</label>
|
||||||
|
{{gh-textarea customExcerptScratch class="post-setting-custom-excerpt" id="custom-excerpt" name="post-setting-custom-excerpt" focusOut=(action "setCustomExcerpt" customExcerptScratch) stopEnterKeyDownPropagation="true" update=(action (mut customExcerptScratch)) data-test-field="custom-excerpt"}}
|
||||||
|
{{gh-error-message errors=model.errors property="customExcerpt" data-test-error="custom-excerpt"}}
|
||||||
|
{{/gh-form-group}}
|
||||||
|
|
||||||
{{#unless session.user.isAuthor}}
|
{{#unless session.user.isAuthor}}
|
||||||
<div class="form-group for-select">
|
<div class="form-group for-select">
|
||||||
<label for="author-list">Author</label>
|
<label for="author-list">Author</label>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import moment from 'moment';
|
||||||
import {isEmpty, isPresent} from 'ember-utils';
|
import {isEmpty, isPresent} from 'ember-utils';
|
||||||
|
|
||||||
export default BaseValidator.create({
|
export default BaseValidator.create({
|
||||||
properties: ['title', 'metaTitle', 'metaDescription', 'publishedAtBlogTime', 'publishedAtBlogDate'],
|
properties: ['title', 'customExcerpt', 'metaTitle', 'metaDescription', 'publishedAtBlogTime', 'publishedAtBlogDate'],
|
||||||
|
|
||||||
title(model) {
|
title(model) {
|
||||||
let title = model.get('title');
|
let title = model.get('title');
|
||||||
|
@ -19,6 +19,15 @@ export default BaseValidator.create({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
customExcerpt(model) {
|
||||||
|
let customExcerpt = model.get('customExcerpt');
|
||||||
|
|
||||||
|
if (!validator.isLength(customExcerpt, 0, 300)) {
|
||||||
|
model.get('errors').add('customExcerpt', 'Excerpt cannot be longer than 300 characters.');
|
||||||
|
this.invalidate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
metaTitle(model) {
|
metaTitle(model) {
|
||||||
let metaTitle = model.get('metaTitle');
|
let metaTitle = model.get('metaTitle');
|
||||||
|
|
||||||
|
|
|
@ -548,5 +548,37 @@ describe('Acceptance: Editor', function() {
|
||||||
'url after autosave'
|
'url after autosave'
|
||||||
).to.equal('/editor/1');
|
).to.equal('/editor/1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('saves post settings fields', async function () {
|
||||||
|
let post = server.create('post');
|
||||||
|
|
||||||
|
await visit(`/editor/${post.id}`);
|
||||||
|
|
||||||
|
// TODO: implement tests for other fields
|
||||||
|
|
||||||
|
// changing custom excerpt auto-saves
|
||||||
|
await click(testSelector('psm-trigger'));
|
||||||
|
await fillIn(testSelector('field', 'custom-excerpt'), 'Testing excerpt');
|
||||||
|
await triggerEvent(testSelector('field', 'custom-excerpt'), 'blur');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
server.db.posts[0].custom_excerpt,
|
||||||
|
'saved excerpt'
|
||||||
|
).to.equal('Testing excerpt');
|
||||||
|
|
||||||
|
// excerpt has validation
|
||||||
|
await fillIn(testSelector('field', 'custom-excerpt'), Array(302).join('a'));
|
||||||
|
await triggerEvent(testSelector('field', 'custom-excerpt'), 'blur');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find(testSelector('error', 'custom-excerpt')).text().trim(),
|
||||||
|
'excerpt too long error'
|
||||||
|
).to.match(/cannot be longer than 300/);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
server.db.posts[0].custom_excerpt,
|
||||||
|
'saved excerpt after validation error'
|
||||||
|
).to.equal('Testing excerpt');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue