mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-28 05:14:12 +03:00
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:
parent
62f7600aa4
commit
72ed8f56f6
@ -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}}
|
@ -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});
|
||||||
|
}
|
||||||
|
}
|
38
ghost/admin/app/components/modal-feedback-lexical.hbs
Normal file
38
ghost/admin/app/components/modal-feedback-lexical.hbs
Normal 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. You’ve 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>
|
69
ghost/admin/app/components/modal-feedback-lexical.js
Normal file
69
ghost/admin/app/components/modal-feedback-lexical.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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({})
|
||||||
|
@ -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 () {
|
||||||
|
Loading…
Reference in New Issue
Block a user