Added custom excerpt to post revisions (#20323)
closes https://linear.app/tryghost/issue/MOM-170 When the subtitle field is included in the editor it creates a disconnect with post revisions if the underlying custom excerpt data is not included so we'd like to both preview and restore the subtitle when the in-editor subtitle field is enabled. - added `post_revisions.custom_excerpt` column to schema - added migration to add `post_revisions.custom_excerpt` to existing databases - added migration to populate `post_revisions.custom_excerpt` with the current `post.custom_excerpt` value from the associated record - ensures no data is inadvertently lost when restoring an old version - using current data matches what would have happened previously where custom_excerpt was never overwritten when restoring an old version - updated post revisions handling to accept the `custom_excerpt` field - updated Admin's revision preview and restoration to display and set the `custom_excerpt` field
This commit is contained in:
parent
d40ef32ca8
commit
b447a26832
@ -20,7 +20,15 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-editor-title" data-test-post-history-preview-title>{{this.currentTitle}}</div>
|
||||
<div class="gh-editor-title" data-test-post-history-preview-title>
|
||||
{{this.currentTitle}}
|
||||
</div>
|
||||
{{#if (feature "editorSubtitle")}}
|
||||
<div class="gh-editor-subtitle" data-test-post-history-preview-subtitle>
|
||||
{{this.selectedRevision.custom_excerpt}}
|
||||
</div>
|
||||
<hr class="gh-editor-title-divider">
|
||||
{{/if}}
|
||||
<KoenigLexicalEditor
|
||||
@lexical={{this.selectedRevision.lexical}}
|
||||
@cardConfig={{this.cardConfig}}
|
||||
|
@ -51,6 +51,7 @@ export default class ModalPostHistory extends Component {
|
||||
latest: index === 0,
|
||||
createdAt: revision.get('createdAt'),
|
||||
title: revision.get('title'),
|
||||
custom_excerpt: revision.get('customExcerpt'),
|
||||
feature_image: revision.get('featureImage'),
|
||||
feature_image_alt: revision.get('featureImageAlt'),
|
||||
feature_image_caption: revision.get('featureImageCaption'),
|
||||
|
@ -4,6 +4,7 @@ import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default class RestoreRevisionModal extends Component {
|
||||
@service feature;
|
||||
@service notifications;
|
||||
|
||||
get title() {
|
||||
@ -38,6 +39,10 @@ export default class RestoreRevisionModal extends Component {
|
||||
post.featureImageAlt = revision.feature_image_alt;
|
||||
post.featureImageCaption = revision.feature_image_caption;
|
||||
|
||||
if (this.feature.editorSubtitle) {
|
||||
post.customExcerpt = revision.custom_excerpt;
|
||||
}
|
||||
|
||||
yield post.save({adapterOptions: {saveRevision: true}});
|
||||
|
||||
updateTitle();
|
||||
|
@ -3,6 +3,7 @@ import Model, {attr, belongsTo} from '@ember-data/model';
|
||||
export default class PostRevisionModel extends Model {
|
||||
@attr('string') lexical;
|
||||
@attr('string') title;
|
||||
@attr('string') customExcerpt;
|
||||
@attr('string') featureImage;
|
||||
@attr('string') featureImageAlt;
|
||||
@attr('string') featureImageCaption;
|
||||
|
@ -5,15 +5,6 @@ import {EmbeddedRecordsMixin} from '@ember-data/serializer/rest';
|
||||
export default class PostRevisionSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
|
||||
// settings for the EmbeddedRecordsMixin.
|
||||
attrs = {
|
||||
author: {embedded: 'always'},
|
||||
lexical: {key: 'lexical'},
|
||||
title: {key: 'title'},
|
||||
createdAt: {key: 'created_at'},
|
||||
postStatus: {key: 'post_status'},
|
||||
reason: {key: 'reason'},
|
||||
featureImage: {key: 'feature_image'},
|
||||
featureImageAlt: {key: 'feature_image_alt'},
|
||||
featureImageCaption: {key: 'feature_image_caption'},
|
||||
postIdLocal: {key: 'post_id'}
|
||||
author: {embedded: 'always'}
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import loginAsRole from '../../helpers/login-as-role';
|
||||
import moment from 'moment-timezone';
|
||||
import {click, find, findAll} from '@ember/test-helpers';
|
||||
import {enableLabsFlag} from '../../helpers/labs-flag';
|
||||
import {expect} from 'chai';
|
||||
import {setupApplicationTest} from 'ember-mocha';
|
||||
import {setupMirage} from 'ember-cli-mirage/test-support';
|
||||
@ -19,14 +20,16 @@ describe('Acceptance: Post revisions', function () {
|
||||
it('can restore a draft post revision', async function () {
|
||||
const post = this.server.create('post', {
|
||||
title: 'Current Title',
|
||||
customExcerpt: 'Current excerpt',
|
||||
status: 'draft'
|
||||
});
|
||||
this.server.create('post-revision', {
|
||||
post,
|
||||
title: post.title,
|
||||
featureImage: post.featureImage,
|
||||
featureImageAlt: post.featureImageAlt,
|
||||
featureImageCaption: post.featureImageCaption,
|
||||
customExcerpt: 'New subtitle',
|
||||
featureImage: 'https://example.com/new-image.jpg',
|
||||
featureImageAlt: 'New feature alt text',
|
||||
featureImageCaption: 'New feature caption',
|
||||
postStatus: 'draft',
|
||||
author: post.authors.models[0],
|
||||
createdAt: moment(post.updatedAt).subtract(1, 'hour'),
|
||||
@ -35,9 +38,10 @@ describe('Acceptance: Post revisions', function () {
|
||||
this.server.create('post-revision', {
|
||||
post,
|
||||
title: 'Old Title',
|
||||
featureImage: post.featureImage,
|
||||
featureImageAlt: post.featureImageAlt,
|
||||
featureImageCaption: post.featureImageCaption,
|
||||
customExcerpt: 'Old subtitle',
|
||||
featureImage: 'https://example.com/old-image.jpg',
|
||||
featureImageAlt: 'Old feature alt text',
|
||||
featureImageCaption: 'Old feature caption',
|
||||
postStatus: 'draft',
|
||||
author: post.authors.models[0],
|
||||
createdAt: moment(post.updatedAt).subtract(1, 'day'),
|
||||
@ -62,6 +66,12 @@ describe('Acceptance: Post revisions', function () {
|
||||
|
||||
// latest post is previewed by default
|
||||
expect(find('[data-test-post-history-preview-title]')).to.have.trimmed.text('Current Title');
|
||||
expect(find('[data-test-post-history-preview-feature-image]')).to.have.attribute('src', 'https://example.com/new-image.jpg');
|
||||
expect(find('[data-test-post-history-preview-feature-image]')).to.have.attribute('alt', 'New feature alt text');
|
||||
expect(find('[data-test-post-history-preview-feature-image-caption]')).to.have.trimmed.text('New feature caption');
|
||||
|
||||
// subtitle is not visible (needs feature flag)
|
||||
expect(find('[data-test-post-history-preview-subtitle]')).to.not.exist;
|
||||
|
||||
// previous post can be previewed
|
||||
await click('[data-test-revision-item="1"] [data-test-button="preview-revision"]');
|
||||
@ -74,5 +84,62 @@ describe('Acceptance: Post revisions', function () {
|
||||
await click('[data-test-modal="restore-revision"] [data-test-button="restore"]');
|
||||
expect(find('[data-test-modal="restore-revision"]')).to.not.exist;
|
||||
expect(find('[data-test-editor-title-input]')).to.have.value('Old Title');
|
||||
// post has been saved with correct data
|
||||
expect(post.attrs.title).to.equal('Old Title');
|
||||
expect(post.attrs.featureImage).to.equal('https://example.com/old-image.jpg');
|
||||
expect(post.attrs.featureImageAlt).to.equal('Old feature alt text');
|
||||
expect(post.attrs.featureImageCaption).to.equal('Old feature caption');
|
||||
|
||||
// subtitle (customExcerpt) is not restored (needs feature flag)
|
||||
expect(post.attrs.customExcerpt).to.equal('Current excerpt');
|
||||
});
|
||||
|
||||
it('can preview and restore subtitle (with editorSubtitle feature flag)', async function () {
|
||||
enableLabsFlag(this.server, 'editorSubtitle');
|
||||
|
||||
const post = this.server.create('post', {
|
||||
title: 'Current Title',
|
||||
customExcerpt: 'Current subtitle',
|
||||
status: 'draft'
|
||||
});
|
||||
this.server.create('post-revision', {
|
||||
post,
|
||||
title: post.title,
|
||||
customExcerpt: 'New subtitle',
|
||||
postStatus: 'draft',
|
||||
author: post.authors.models[0],
|
||||
createdAt: moment(post.updatedAt).subtract(1, 'hour'),
|
||||
reason: 'explicit_save'
|
||||
});
|
||||
this.server.create('post-revision', {
|
||||
post,
|
||||
title: 'Old Title',
|
||||
customExcerpt: 'Old subtitle',
|
||||
postStatus: 'draft',
|
||||
author: post.authors.models[0],
|
||||
createdAt: moment(post.updatedAt).subtract(1, 'day'),
|
||||
reason: 'initial_revision'
|
||||
});
|
||||
|
||||
await visit(`/editor/post/${post.id}`);
|
||||
|
||||
// open post history menu
|
||||
await click('[data-test-psm-trigger]');
|
||||
await click('[data-test-toggle="post-history"]');
|
||||
|
||||
// subtitle is visible
|
||||
expect(find('[data-test-post-history-preview-subtitle]')).to.exist;
|
||||
expect(find('[data-test-post-history-preview-subtitle]')).to.have.trimmed.text('New subtitle');
|
||||
|
||||
// previous post can be previewed
|
||||
await click('[data-test-revision-item="1"] [data-test-button="preview-revision"]');
|
||||
expect(find('[data-test-post-history-preview-subtitle]')).to.have.trimmed.text('Old subtitle');
|
||||
|
||||
// previous post can be restored
|
||||
await click('[data-test-revision-item="1"] [data-test-button="restore-revision"]');
|
||||
await click('[data-test-modal="restore-revision"] [data-test-button="restore"]');
|
||||
|
||||
// post has been saved with correct data
|
||||
expect(post.attrs.customExcerpt).to.equal('Old subtitle');
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,7 @@
|
||||
const {createAddColumnMigration} = require('../../utils');
|
||||
|
||||
module.exports = createAddColumnMigration('post_revisions', 'custom_excerpt', {
|
||||
type: 'string',
|
||||
maxlength: 2000,
|
||||
nullable: true
|
||||
});
|
@ -0,0 +1,32 @@
|
||||
const logging = require('@tryghost/logging');
|
||||
const {createTransactionalMigration} = require('../../utils');
|
||||
const DatabaseInfo = require('@tryghost/database-info');
|
||||
|
||||
module.exports = createTransactionalMigration(
|
||||
async function up(knex) {
|
||||
logging.info('Populating post_revisions.custom_excerpt with post.excerpt');
|
||||
|
||||
if (DatabaseInfo.isSQLite(knex)) {
|
||||
// SQLite doesn't support JOINs in UPDATE queries
|
||||
await knex.raw(`
|
||||
UPDATE post_revisions
|
||||
SET custom_excerpt = (
|
||||
SELECT posts.custom_excerpt
|
||||
FROM posts
|
||||
WHERE post_revisions.post_id = posts.id
|
||||
)
|
||||
`);
|
||||
} else {
|
||||
await knex.raw(`
|
||||
UPDATE post_revisions
|
||||
JOIN posts ON post_revisions.post_id = posts.id
|
||||
SET post_revisions.custom_excerpt = posts.custom_excerpt
|
||||
`);
|
||||
}
|
||||
|
||||
logging.info('Finished populating post_revisions.custom_excerpt');
|
||||
},
|
||||
async function down() {
|
||||
// Not required
|
||||
}
|
||||
);
|
@ -413,7 +413,8 @@ module.exports = {
|
||||
reason: {type: 'string', maxlength: 50, nullable: true},
|
||||
feature_image: {type: 'string', maxlength: 2000, nullable: true},
|
||||
feature_image_alt: {type: 'string', maxlength: 191, nullable: true, validations: {isLength: {max: 125}}},
|
||||
feature_image_caption: {type: 'text', maxlength: 65535, nullable: true}
|
||||
feature_image_caption: {type: 'text', maxlength: 65535, nullable: true},
|
||||
custom_excerpt: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 300}}}
|
||||
},
|
||||
members: {
|
||||
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
||||
|
@ -973,6 +973,7 @@ Post = ghostBookshelf.Model.extend({
|
||||
feature_image_alt: model.get('posts_meta')?.feature_image_alt,
|
||||
feature_image_caption: model.get('posts_meta')?.feature_image_caption,
|
||||
title: model.get('title'),
|
||||
custom_excerpt: model.get('custom_excerpt'),
|
||||
post_status: model.get('status')
|
||||
};
|
||||
|
||||
|
@ -35,7 +35,7 @@ const validateRouteSettings = require('../../../../../core/server/services/route
|
||||
*/
|
||||
describe('DB version integrity', function () {
|
||||
// Only these variables should need updating
|
||||
const currentSchemaHash = '8e52be4cf84cae82a406e5a153ecbd43';
|
||||
const currentSchemaHash = 'e6fff125e9a6cd6167acaf9983a21319';
|
||||
const currentFixturesHash = 'a489d615989eab1023d4b8af0ecee7fd';
|
||||
const currentSettingsHash = '5c957ceb48c4878767d7d3db484c592d';
|
||||
const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';
|
||||
|
@ -7,6 +7,7 @@ type PostLike = {
|
||||
feature_image_alt: string | null;
|
||||
feature_image_caption: string | null;
|
||||
title: string;
|
||||
custom_excerpt: string | null;
|
||||
reason: string;
|
||||
post_status: string;
|
||||
}
|
||||
@ -19,6 +20,7 @@ type Revision = {
|
||||
feature_image_alt: string | null;
|
||||
feature_image_caption: string | null;
|
||||
title: string;
|
||||
custom_excerpt: string | null;
|
||||
post_status: string;
|
||||
reason: string;
|
||||
created_at_ts: number;
|
||||
@ -71,8 +73,9 @@ export class PostRevisions {
|
||||
const featuredImagedHasChanged = latestRevision.feature_image !== current.feature_image;
|
||||
const lexicalHasChanged = latestRevision.lexical !== current.lexical;
|
||||
const titleHasChanged = latestRevision.title !== current.title;
|
||||
const customExcerptHasChanged = latestRevision.custom_excerpt !== current.custom_excerpt;
|
||||
// CASE: we only want to save a revision if something has changed since the previous revision
|
||||
if (lexicalHasChanged || titleHasChanged || featuredImagedHasChanged) {
|
||||
if (lexicalHasChanged || titleHasChanged || featuredImagedHasChanged || customExcerptHasChanged) {
|
||||
// CASE: user has explicitly requested a revision by hitting cmd+s or leaving the editor
|
||||
if (forceRevision) {
|
||||
return {value: true, reason: 'explicit_save'};
|
||||
@ -119,6 +122,7 @@ export class PostRevisions {
|
||||
feature_image_alt: input.feature_image_alt,
|
||||
feature_image_caption: input.feature_image_caption,
|
||||
title: input.title,
|
||||
custom_excerpt: input.custom_excerpt,
|
||||
reason: input.reason,
|
||||
post_status: input.post_status
|
||||
};
|
||||
|
@ -17,6 +17,7 @@ function makePostLike(data: any = {}) {
|
||||
feature_image_alt: data.feature_image_alt || null,
|
||||
feature_image_caption: data.feature_image_caption || null,
|
||||
title: 'Title',
|
||||
custom_excerpt: 'Subtitle',
|
||||
reason: 'reason',
|
||||
post_status: 'published'
|
||||
}, data);
|
||||
@ -33,6 +34,7 @@ function makeRevision(data: any = {}) {
|
||||
feature_image_alt: data.feature_image_alt || null,
|
||||
feature_image_caption: data.feature_image_caption || null,
|
||||
title: 'Title',
|
||||
custom_excerpt: 'Subtitle',
|
||||
reason: 'reason',
|
||||
post_status: 'published'
|
||||
}, data);
|
||||
@ -100,6 +102,26 @@ describe('PostRevisions', function () {
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should return true if the current and previous custom_excerpt values are different and forceRevision is true', function () {
|
||||
const postRevisions = new PostRevisions({config, model: {}});
|
||||
|
||||
const expected = {value: true, reason: 'explicit_save'};
|
||||
const actual = postRevisions.shouldGenerateRevision(makePostLike({
|
||||
lexical: 'blah',
|
||||
html: 'blah',
|
||||
title: 'blah',
|
||||
custom_excerpt: 'blah2'
|
||||
}), [{
|
||||
lexical: 'blah',
|
||||
custom_excerpt: 'not blah'
|
||||
}].map(makeRevision), {
|
||||
forceRevision: true,
|
||||
isPublished: false
|
||||
});
|
||||
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should return true if the current and previous feature_image values are different and forceRevision is true', function () {
|
||||
const postRevisions = new PostRevisions({config, model: {}});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user