Add lexical feedback (#16772)

no refs
- add lexical feedback modal in the editor, labs, and publish workflows
- modal is a basic textarea form

---------

Co-authored-by: Djordje Vlaisavljevic <dzvlais@gmail.com>
This commit is contained in:
Steve Larson 2023-05-10 17:40:55 -05:00 committed by GitHub
parent 62f7600aa4
commit 72ed8f56f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 237 additions and 5 deletions

View File

@ -96,7 +96,7 @@
</button> </button>
</p> </p>
{{else}} {{else}}
<p class="gh-publish-confirmation"> <p class="gh-publish-confirmation gh-publish-confirmation-with-feedback">
<button <button
type="button" type="button"
class="gh-back-to-editor" class="gh-back-to-editor"
@ -105,8 +105,17 @@
> >
<span>Back to editor</span> <span>Back to editor</span>
</button> </button>
{{#if (feature 'lexicalEditor')}}
<button
type="button"
class="gh-publish-confirmation-feedback"
{{on "click" this.openFeedbackLexical}}
data-test-button="lexical-feedback"
>
<span>Beta feedback?</span>
</button>
{{/if}}
</p> </p>
{{/if}} {{/if}}
{{/if}} {{/if}}
{{/let}} {{/let}}

View File

@ -0,0 +1,16 @@
import Component from '@glimmer/component';
import FeedbackLexicalModal from '../../../modal-feedback-lexical';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class PublishFlowComplete extends Component {
@service modals;
@tracked showFeedbackLexicalModal = false;
@action
async openFeedbackLexical() {
await this.modals.open(FeedbackLexicalModal, {post: this.args.publishOptions.post});
}
}

View File

@ -0,0 +1,38 @@
<div class="modal-content" data-test-modal="lexical-feedback">
<header class="modal-header">
<h1>Editor beta feedback</h1>
</header>
<a class="close" href="" role="button" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span
class="hidden">Close</span></a>
<div class="modal-body gh-modal-feedback-lexical">
{{!-- <p>Have any issues? Feedback? Let us know!</p> --}}
<form>
<GhFormGroup>
<label for="feedback-lexical">Have any issues? Feedback? Let us know below!</label>
<GhTextarea
@id="feedback-lexical"
@name="feedback-lexical"
@value={{this.feedbackMessage}}
@placeholder="I've noticed that..."
@shouldFocus={{true}}
data-test-lexical-feedback-textarea
/>
{{!--
<GhErrorMessage @errors={{this.member.errors}} @property="note" /> --}}
{{!-- <p> Maximum: <b>500</b> characters. Youve used
{{gh-count-down-characters this.feedbackMessage 500}}</p> --}}
</GhFormGroup>
</form>
</div>
<div class="modal-footer">
<button class="gh-btn" type="button" {{action "closeModal" }}><span>Cancel</span></button>
<GhTaskButton
@buttonText="Send feedback"
@task={{this.submitFeedback}}
@class="gh-btn gh-btn-black gh-btn-icon"
data-test-button="submit-lexical-feedback"
/>
</div>
</div>

View File

@ -0,0 +1,69 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
export default class FeedbackLexicalModalComponent extends Component {
@service ajax;
@service ghostPaths;
@service session;
@service notifications;
@inject config;
constructor(...args) {
super(...args);
this.feedbackMessage = this.args.feedbackMessage;
}
@action
closeModal() {
this.args.close();
}
@task({drop: true})
*submitFeedback() {
let url = `https://submit-form.com/us6uBWv8`;
let postData;
if (this.args.data?.post) {
postData = {
PostId: this.args.data.post?.id,
PostTitle: this.args.data.post?.title
};
}
let ghostData = {
Site: this.config.blogUrl,
StaffMember: this.session.user.name,
StaffMemberEmail: this.session.user.email,
StaffAccessLevel: this.session.user.role?.description,
UserAgent: navigator.userAgent,
Version: this.config.version,
Feedback: this.feedbackMessage
};
let data = {
...ghostData,
...postData
};
let response = yield this.ajax.post(url, {data});
if (response.status < 200 || response.status >= 300) {
throw new Error('api failed ' + response.status + ' ' + response.statusText);
}
this.args.close();
this.notifications.showNotification('Feedback sent',
{
icon: 'send-email',
description: 'Thank you!'
}
);
return response;
}
}

View File

@ -2,6 +2,7 @@ import ConfirmEditorLeaveModal from '../components/modals/editor/confirm-leave';
import Controller, {inject as controller} from '@ember/controller'; import Controller, {inject as controller} from '@ember/controller';
import DeletePostModal from '../components/modals/delete-post'; import DeletePostModal from '../components/modals/delete-post';
import DeleteSnippetModal from '../components/editor/modals/delete-snippet'; import DeleteSnippetModal from '../components/editor/modals/delete-snippet';
import FeedbackLexicalModal from '../components/modal-feedback-lexical';
import PostModel from 'ghost-admin/models/post'; import PostModel from 'ghost-admin/models/post';
import PublishLimitModal from '../components/modals/limits/publish-limit'; import PublishLimitModal from '../components/modals/limits/publish-limit';
import ReAuthenticateModal from '../components/editor/modals/re-authenticate'; import ReAuthenticateModal from '../components/editor/modals/re-authenticate';
@ -23,6 +24,7 @@ import {isArray as isEmberArray} from '@ember/array';
import {isHostLimitError, isServerUnreachableError, isVersionMismatchError} from 'ghost-admin/services/ajax'; import {isHostLimitError, isServerUnreachableError, isVersionMismatchError} from 'ghost-admin/services/ajax';
import {isInvalidError} from 'ember-ajax/errors'; import {isInvalidError} from 'ember-ajax/errors';
import {inject as service} from '@ember/service'; import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
const DEFAULT_TITLE = '(Untitled)'; const DEFAULT_TITLE = '(Untitled)';
@ -113,6 +115,8 @@ export default class LexicalEditorController extends Controller {
@inject config; @inject config;
@tracked showFeedbackLexicalModal = false;
/* public properties -----------------------------------------------------*/ /* public properties -----------------------------------------------------*/
shouldFocusTitle = false; shouldFocusTitle = false;
@ -408,6 +412,11 @@ export default class LexicalEditorController extends Controller {
}); });
} }
@action
async openFeedbackLexical() {
await this.modals.open(FeedbackLexicalModal);
}
/* Public tasks ----------------------------------------------------------*/ /* Public tasks ----------------------------------------------------------*/
// separate task for autosave so that it doesn't override a manual save // separate task for autosave so that it doesn't override a manual save

View File

@ -3,6 +3,7 @@ import {inject as service} from '@ember/service';
/* eslint-disable ghost/ember/alias-model-in-controller */ /* eslint-disable ghost/ember/alias-model-in-controller */
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import DeleteAllModal from '../../components/settings/labs/delete-all-content-modal'; import DeleteAllModal from '../../components/settings/labs/delete-all-content-modal';
import FeedbackLexicalModal from '../../components/modal-feedback-lexical';
import ImportContentModal from '../../components/modal-import-content'; import ImportContentModal from '../../components/modal-import-content';
import RSVP from 'rsvp'; import RSVP from 'rsvp';
import config from 'ghost-admin/config/environment'; import config from 'ghost-admin/config/environment';
@ -17,6 +18,7 @@ import {isBlank} from '@ember/utils';
import {isArray as isEmberArray} from '@ember/array'; import {isArray as isEmberArray} from '@ember/array';
import {run} from '@ember/runloop'; import {run} from '@ember/runloop';
import {task, timeout} from 'ember-concurrency'; import {task, timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
const {Promise} = RSVP; const {Promise} = RSVP;
@ -50,6 +52,8 @@ export default class LabsController extends Controller {
@inject config; @inject config;
@tracked showFeedbackLexicalModal = false;
importErrors = null; importErrors = null;
importSuccessful = false; importSuccessful = false;
showEarlyAccessModal = false; showEarlyAccessModal = false;
@ -160,6 +164,11 @@ export default class LabsController extends Controller {
this.toggleProperty('showEarlyAccessModal'); this.toggleProperty('showEarlyAccessModal');
} }
@action
async openFeedbackLexical() {
await this.modals.open(FeedbackLexicalModal);
}
/** /**
* Opens a file selection dialog - Triggered by "Upload x" buttons, * Opens a file selection dialog - Triggered by "Upload x" buttons,
* searches for the hidden file input within the .gh-setting element * searches for the hidden file input within the .gh-setting element

View File

@ -732,6 +732,18 @@
font-size: 1.6rem; font-size: 1.6rem;
} }
.gh-publish-confirmation-with-feedback {
display: flex;
justify-content: space-between;
}
.gh-publish-confirmation-feedback {
color: var(--green);
font-size: 1.6rem;
font-weight: 400;
letter-spacing: .4px;
}
.gh-revert-to-draft { .gh-revert-to-draft {
color: var(--green-d1); color: var(--green-d1);
font-weight: 500; font-weight: 500;

View File

@ -387,6 +387,10 @@
font-weight: 400; font-weight: 400;
} }
.gh-editor-feedback {
color: var(--green);
}
.gh-editor-status { .gh-editor-status {
color: var(--midgrey); color: var(--midgrey);
font-size: 1.3rem; font-size: 1.3rem;

View File

@ -94,6 +94,16 @@
/> />
<div class="gh-editor-wordcount-container"> <div class="gh-editor-wordcount-container">
{{#if (feature 'lexicalEditor')}}
<button
type="button"
class="gh-editor-feedback"
{{on "click" this.openFeedbackLexical}}
data-test-button="lexical-editor-feedback"
>
<span>Feedback?</span>
</button>
{{/if}}
<div class="gh-editor-wordcount"> <div class="gh-editor-wordcount">
{{gh-pluralize this.wordCount.wordCount "word"}} {{gh-pluralize this.wordCount.wordCount "word"}}
</div> </div>

View File

@ -206,7 +206,11 @@
<div> <div>
<h4 class="gh-expandable-title">Lexical editor</h4> <h4 class="gh-expandable-title">Lexical editor</h4>
<p class="gh-expandable-description"> <p class="gh-expandable-description">
Makes lexical editor the default when creating new posts/pages. {{#if (feature 'lexicalEditor')}}
<span>Makes lexical editor the default when creating new posts/pages. Any issues? Feedback?</span><button class="green ml1" role="button" {{on "click" this.openFeedbackLexical}} data-test-button="lexical-feedback">Let us know</button>
{{else}}
<span>Makes lexical editor the default when creating new posts/pages.</span>
{{/if}}
</p> </p>
</div> </div>
<div class="for-switch"> <div class="for-switch">

View File

@ -1,7 +1,9 @@
import loginAsRole from '../../helpers/login-as-role'; import loginAsRole from '../../helpers/login-as-role';
import {BLANK_DOC} from 'koenig-editor/components/koenig-editor'; import {BLANK_DOC} from 'koenig-editor/components/koenig-editor';
import {currentURL} from '@ember/test-helpers'; import {currentURL} from '@ember/test-helpers';
import {enableLabsFlag} from '../../helpers/labs-flag';
import {expect} from 'chai'; import {expect} from 'chai';
import {find} from '@ember/test-helpers';
import {setupApplicationTest} from 'ember-mocha'; import {setupApplicationTest} from 'ember-mocha';
import {setupMirage} from 'ember-cli-mirage/test-support'; import {setupMirage} from 'ember-cli-mirage/test-support';
import {visit} from '../../helpers/visit'; import {visit} from '../../helpers/visit';
@ -18,6 +20,8 @@ describe('Acceptance: Lexical editor', function () {
config.attrs.editor = {url: 'https://cdn.pkg/editor.js'}; config.attrs.editor = {url: 'https://cdn.pkg/editor.js'};
config.save(); config.save();
enableLabsFlag(this.server, 'lexicalEditor');
// stub loaded external module to avoid loading of external dep // stub loaded external module to avoid loading of external dep
window['@tryghost/koenig-lexical'] = { window['@tryghost/koenig-lexical'] = {
KoenigComposer: () => null, KoenigComposer: () => null,
@ -47,6 +51,14 @@ describe('Acceptance: Lexical editor', function () {
expect(currentURL(), 'currentURL').to.equal('/lexical-editor/post/'); expect(currentURL(), 'currentURL').to.equal('/lexical-editor/post/');
}); });
it('shows feedback link in lexical editor', async function () {
await loginAsRole('Administrator', this.server);
await visit('/lexical-editor/post/');
expect(currentURL(), 'currentURL').to.equal('/lexical-editor/post/');
expect(find('.gh-editor-feedback'), 'feedback button').to.exist;
});
it('redirects mobiledoc editor to lexical editor when post.lexical is present', async function () { it('redirects mobiledoc editor to lexical editor when post.lexical is present', async function () {
const post = this.server.create('post', { const post = this.server.create('post', {
lexical: JSON.stringify({}) lexical: JSON.stringify({})

View File

@ -1,13 +1,12 @@
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {beforeEach, describe, it} from 'mocha'; import {beforeEach, describe, it} from 'mocha';
import {click, currentURL, fillIn, find, findAll} from '@ember/test-helpers'; import {click, currentURL, fillIn, find, findAll} from '@ember/test-helpers';
import {enableLabsFlag} from '../../helpers/labs-flag';
import {expect} from 'chai'; import {expect} from 'chai';
import {fileUpload} from '../../helpers/file-upload'; import {fileUpload} from '../../helpers/file-upload';
import {setupApplicationTest} from 'ember-mocha'; import {setupApplicationTest} from 'ember-mocha';
import {setupMirage} from 'ember-cli-mirage/test-support'; import {setupMirage} from 'ember-cli-mirage/test-support';
import {visit} from '../../helpers/visit'; import {visit} from '../../helpers/visit';
// import wait from 'ember-test-helpers/wait';
// import {timeout} from 'ember-concurrency';
describe('Acceptance: Settings - Labs', function () { describe('Acceptance: Settings - Labs', function () {
let hooks = setupApplicationTest(); let hooks = setupApplicationTest();
@ -314,6 +313,47 @@ describe('Acceptance: Settings - Labs', function () {
let iframe = document.querySelector('#iframeDownload'); let iframe = document.querySelector('#iframeDownload');
expect(iframe.getAttribute('src')).to.have.string('/settings/routes/yaml/'); expect(iframe.getAttribute('src')).to.have.string('/settings/routes/yaml/');
}); });
it('displays lexical feedback button when the labs setting is enabled', async function () {
enableLabsFlag(this.server, 'lexicalEditor');
await visit('/settings/labs');
expect(find('[data-test-button="lexical-feedback"]')).to.exist;
});
it('does not display lexical feedback button when the labs setting is disabled', async function () {
await visit('/settings/labs');
expect(find('[data-test-button="lexical-feedback"]')).to.not.exist;
});
it('allows the user to launch the feedback modal', async function () {
enableLabsFlag(this.server, 'lexicalEditor');
await visit('/settings/labs');
await click('[data-test-button="lexical-feedback"]');
expect(find('[data-test-modal="lexical-feedback"]')).to.exist;
});
it('allows the user to send lexical feedback', async function () {
enableLabsFlag(this.server, 'lexicalEditor');
// mock successful request
this.server.post('https://submit-form.com/us6uBWv8', {}, 200);
await visit('/settings/labs');
await click('[data-test-button="lexical-feedback"]');
expect(find('[data-test-modal="lexical-feedback"]')).to.exist;
await fillIn('[data-test-lexical-feedback-textarea]', 'This is test feedback');
await click('[data-test-button="submit-lexical-feedback"]');
// successful request will close the modal and show a notification toast
expect(find('[data-test-modal="lexical-feedback"]')).to.not.exist;
expect(find('[data-test-text="notification-content"]')).to.exist;
});
}); });
describe('When logged in as Owner', function () { describe('When logged in as Owner', function () {