Added validation logic to in-editor subtitle (#20284)

closes https://linear.app/tryghost/issue/MOM-150

- use our validation engine to display an error state when >300 characters have been typed in the subtitle input field

---------

Co-authored-by: Sanne de Vries <sannedv@protonmail.com>
Co-authored-by: Kevin Ansfield <kevin@lookingsideways.co.uk>
This commit is contained in:
Ronald Langeveld 2024-06-03 18:02:18 +07:00 committed by GitHub
parent 9891abd61c
commit 833ac83921
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 72 additions and 22 deletions

View File

@ -59,18 +59,22 @@
{{#if (feature 'editorSubtitle')}} {{#if (feature 'editorSubtitle')}}
<GhTextarea <GhTextarea
@class="gh-editor-subtitle" @class={{concat "gh-editor-subtitle " (if @excerptErrorMessage "red")}}
@placeholder="Add a post excerpt..." @placeholder="Add a subtitle..."
@shouldFocus={{false}} @shouldFocus={{false}}
@tabindex="1" @tabindex="1"
@autoExpand=".gh-koenig-editor" @autoExpand=".gh-koenig-editor"
@value={{readonly this.excerpt}} @value={{readonly this.excerpt}}
@input={{this.updateExcerpt}} @input={{this.onExcerptInput}}
@focus-out={{optional @blurExcerpt}}
@keyDown={{this.onExcerptKeydown}} @keyDown={{this.onExcerptKeydown}}
data-test-editor-subhead-input={{true}} data-test-textarea="subtitle"
/> />
<hr class="gh-editor-title-divider"> {{#if @excerptErrorMessage}}
<div class="gh-editor-subtitle-error" data-test-error="subtitle">
{{@excerptErrorMessage}}
</div>
{{/if}}
<hr class="gh-editor-title-divider {{if @excerptErrorMessage "gh-editor-title-divider-error" ""}}">
{{/if}} {{/if}}
</div> </div>

View File

@ -153,18 +153,8 @@ export default class GhKoenigEditorReactComponent extends Component {
// Subhead ("excerpt") Actions ------------------------------------------- // Subhead ("excerpt") Actions -------------------------------------------
@action @action
updateExcerpt(event) { onExcerptInput(event) {
this.args.onExcerptChange?.(event.target.value); this.args.setExcerpt?.(event.target.value);
}
@action
focusExcerpt() {
this.args.onExcerptFocus?.();
}
@action
blurExcerpt() {
this.args.onExcerptBlur?.();
} }
@action @action

View File

@ -285,8 +285,13 @@ export default class LexicalEditorController extends Controller {
} }
@action @action
updateExcerptScratch(excerpt) { updateExcerpt(excerpt) {
this.set('post.customExcerptScratch', excerpt); this.post.customExcerpt = excerpt;
this.post.validate({property: 'customExcerpt'});
}
get excerptErrorMessage() {
return this.post.errors.errorsFor('customExcerpt')?.[0]?.message;
} }
// updates local willPublish/Schedule values, does not get applied to // updates local willPublish/Schedule values, does not get applied to

View File

@ -811,10 +811,22 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
border: none !important; border: none !important;
} }
.gh-editor-subtitle-error {
margin-top: .8rem;
color: var(--red-d1);
font-size: 1.4rem;
font-weight: 400;
}
.gh-editor-title-divider { .gh-editor-title-divider {
margin: 1.6rem 0 4.8rem; margin: 1.6rem 0 4.8rem;
} }
.gh-editor-title-divider-error {
margin: .4rem 0 4.8rem;
border-top: 1px solid var(--red);
}
.gh-editor .tk-indicator { .gh-editor .tk-indicator {
position: absolute; position: absolute;
top: 15px; top: 15px;

View File

@ -62,11 +62,12 @@
<GhKoenigEditorLexical <GhKoenigEditorLexical
@title={{readonly this.post.titleScratch}} @title={{readonly this.post.titleScratch}}
@excerpt={{readonly this.post.customExcerpt}} @excerpt={{readonly this.post.customExcerpt}}
@setExcerpt={{this.updateExcerpt}}
@excerptErrorMessage={{this.excerptErrorMessage}}
@titleAutofocus={{this.shouldFocusTitle}} @titleAutofocus={{this.shouldFocusTitle}}
@titlePlaceholder={{concat (capitalize this.post.displayName) " title"}} @titlePlaceholder={{concat (capitalize this.post.displayName) " title"}}
@titleHasTk={{this.titleHasTk}} @titleHasTk={{this.titleHasTk}}
@onTitleChange={{this.updateTitleScratch}} @onTitleChange={{this.updateTitleScratch}}
@onExcerptChange={{this.updateExcerptScratch}}
@onTitleBlur={{perform this.saveTitleTask}} @onTitleBlur={{perform this.saveTitleTask}}
@body={{readonly this.post.lexicalScratch}} @body={{readonly this.post.lexicalScratch}}
@bodyPlaceholder={{concat "Begin writing your " this.post.displayName "..."}} @bodyPlaceholder={{concat "Begin writing your " this.post.displayName "..."}}
@ -95,6 +96,7 @@
}} }}
@postType={{this.post.displayName}} @postType={{this.post.displayName}}
@registerAPI={{this.registerEditorAPI}} @registerAPI={{this.registerEditorAPI}}
@savePostTask={{this.savePostTask}}
/> />
<div class="gh-editor-wordcount-container"> <div class="gh-editor-wordcount-container">

View File

@ -62,7 +62,11 @@ export default BaseValidator.create({
customExcerpt(model) { customExcerpt(model) {
if (!validator.isLength(model.customExcerpt || '', 0, 300)) { if (!validator.isLength(model.customExcerpt || '', 0, 300)) {
if (model.feature.editorSubtitle) {
model.errors.add('customExcerpt', 'Please keep the subtitle under 300 characters.');
} else {
model.errors.add('customExcerpt', 'Excerpt cannot be longer than 300 characters.'); model.errors.add('customExcerpt', 'Excerpt cannot be longer than 300 characters.');
}
this.invalidate(); this.invalidate();
} }
}, },

View File

@ -5,6 +5,7 @@ import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-sup
import {beforeEach, describe, it} from 'mocha'; import {beforeEach, describe, it} from 'mocha';
import {blur, click, currentRouteName, currentURL, fillIn, find, findAll, triggerEvent, typeIn} from '@ember/test-helpers'; import {blur, click, currentRouteName, currentURL, fillIn, find, findAll, triggerEvent, typeIn} from '@ember/test-helpers';
import {datepickerSelect} from 'ember-power-datepicker/test-support'; import {datepickerSelect} from 'ember-power-datepicker/test-support';
import {enableLabsFlag} from '../helpers/labs-flag';
import {expect} from 'chai'; import {expect} from 'chai';
import {selectChoose} from 'ember-power-select/test-support'; import {selectChoose} from 'ember-power-select/test-support';
import {setupApplicationTest} from 'ember-mocha'; import {setupApplicationTest} from 'ember-mocha';
@ -490,6 +491,38 @@ describe('Acceptance: Editor', function () {
).to.equal(0); ).to.equal(0);
}); });
it('handles in-editor excerpt update and validation', async function () {
enableLabsFlag(this.server, 'editorSubtitle');
let post = this.server.create('post', {authors: [author], customExcerpt: 'Existing excerpt'});
await visit(`/editor/post/${post.id}`);
expect(find('[data-test-textarea="subtitle"]'), 'initial textarea').to.be.visible;
expect(find('[data-test-textarea="subtitle"]'), 'initial textarea').to.have.value('Existing excerpt');
await fillIn('[data-test-textarea="subtitle"]', 'New excerpt');
expect(find('[data-test-textarea="subtitle"]'), 'updated textarea').to.have.value('New excerpt');
await triggerEvent('[data-test-textarea="subtitle"]', 'keydown', {
key: 's',
keyCode: 83, // s
metaKey: ctrlOrCmd === 'command',
ctrlKey: ctrlOrCmd === 'ctrl'
});
expect(post.customExcerpt, 'saved excerpt').to.equal('New excerpt');
await fillIn('[data-test-textarea="subtitle"]', Array(302).join('a'));
expect(find('[data-test-error="subtitle"]'), 'subtitle error').to.exist;
expect(find('[data-test-error="subtitle"]')).to.have.trimmed.text('Please keep the subtitle under 300 characters.');
await fillIn('[data-test-textarea="subtitle"]', Array(300).join('a'));
expect(find('[data-test-error="subtitle"]'), 'subtitle error').to.not.exist;
});
// https://github.com/TryGhost/Ghost/issues/11786 // https://github.com/TryGhost/Ghost/issues/11786
// NOTE: Flaky test with moving to Lexical editor, skipping for now // NOTE: Flaky test with moving to Lexical editor, skipping for now
it.skip('save shortcut works when tags/authors field is focused', async function () { it.skip('save shortcut works when tags/authors field is focused', async function () {