mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-08 02:52:39 -05:00
✨ Improved publishing flow (#20878)
ref DES-706 * After a user publishes or schedules a post, they are directed to the post list * If a post is sent as an email, they are directed to the Analytics page * In both cases, a confirmation modal is shown * If a post is published, they can share it directly from the confirmation modal * Added a "Share" button and some additional functions (view, edit, and delete post) to published posts in post analytics * Added a manual "Refresh" button to post analytics so that there is no need to reload the whole app to update the data --------- Co-authored-by: Sag <guptazy@gmail.com>
This commit is contained in:
parent
c2ae91e4db
commit
d30164df97
12 changed files with 90 additions and 131 deletions
|
@ -59,14 +59,6 @@ const features = [{
|
|||
title: 'Content Visibility',
|
||||
description: 'Enables content visibility in Emails',
|
||||
flag: 'contentVisibility'
|
||||
},{
|
||||
title: 'Publish Flow — End Screen',
|
||||
description: 'Enables improved publish flow',
|
||||
flag: 'publishFlowEndScreen'
|
||||
},{
|
||||
title: 'Post Analytics — Refresh',
|
||||
description: 'Adds a refresh button to the post analytics screen',
|
||||
flag: 'postAnalyticsRefresh'
|
||||
}];
|
||||
|
||||
const AlphaFeatures: React.FC = () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="flex flex-column h-100 items-center overflow-auto" data-test-modal="publish-flow">
|
||||
<header class="gh-publish-header">
|
||||
<button class="gh-btn-editor gh-publish-back-button" title="Close" type="button" {{on "click" @close}} data-test-button="close-publish-flow">
|
||||
<button class="gh-btn-editor gh-publish-back-button" title="Close" type="button" {{on "click" @close}}>
|
||||
<span>{{svg-jar "arrow-left"}} Editor</span>
|
||||
</button>
|
||||
|
||||
|
@ -45,14 +45,6 @@
|
|||
@close={{@close}}
|
||||
/>
|
||||
{{else if this.isComplete}}
|
||||
{{#unless (feature "publishFlowEndScreen")}}
|
||||
<Editor::Modals::PublishFlow::Complete
|
||||
@publishOptions={{@data.publishOptions}}
|
||||
@recipientType={{this.recipientType}}
|
||||
@postCount={{this.postCount}}
|
||||
@close={{@close}}
|
||||
/>
|
||||
{{/unless}}
|
||||
{{else}}
|
||||
<Editor::Modals::PublishFlow::Options
|
||||
@publishOptions={{@data.publishOptions}}
|
||||
|
|
|
@ -93,31 +93,30 @@ export default class PublishFlowOptions extends Component {
|
|||
|
||||
try {
|
||||
yield this.args.saveTask.perform();
|
||||
if (this.feature.publishFlowEndScreen) {
|
||||
if (this.args.publishOptions.isScheduled) {
|
||||
localStorage.setItem('ghost-last-scheduled-post', JSON.stringify({
|
||||
id: this.args.publishOptions.post.id,
|
||||
type: this.args.publishOptions.post.displayName
|
||||
}));
|
||||
if (this.args.publishOptions.post.displayName !== 'page') {
|
||||
this.router.transitionTo('posts');
|
||||
|
||||
if (this.args.publishOptions.isScheduled) {
|
||||
localStorage.setItem('ghost-last-scheduled-post', JSON.stringify({
|
||||
id: this.args.publishOptions.post.id,
|
||||
type: this.args.publishOptions.post.displayName
|
||||
}));
|
||||
if (this.args.publishOptions.post.displayName !== 'page') {
|
||||
this.router.transitionTo('posts');
|
||||
} else {
|
||||
this.router.transitionTo('pages');
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem('ghost-last-published-post', JSON.stringify({
|
||||
id: this.args.publishOptions.post.id,
|
||||
type: this.args.publishOptions.post.displayName
|
||||
}));
|
||||
if (this.args.publishOptions.post.displayName !== 'page') {
|
||||
if (this.args.publishOptions.post.hasEmail) {
|
||||
this.router.transitionTo('posts.analytics', this.args.publishOptions.post.id);
|
||||
} else {
|
||||
this.router.transitionTo('pages');
|
||||
this.router.transitionTo('posts');
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem('ghost-last-published-post', JSON.stringify({
|
||||
id: this.args.publishOptions.post.id,
|
||||
type: this.args.publishOptions.post.displayName
|
||||
}));
|
||||
if (this.args.publishOptions.post.displayName !== 'page') {
|
||||
if (this.args.publishOptions.post.hasEmail) {
|
||||
this.router.transitionTo('posts.analytics', this.args.publishOptions.post.id);
|
||||
} else {
|
||||
this.router.transitionTo('posts');
|
||||
}
|
||||
} else {
|
||||
this.router.transitionTo('pages');
|
||||
}
|
||||
this.router.transitionTo('pages');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
<img src="{{or this.post.featureImage this.post.twitterImage this.post.ogImage}}" alt="{{this.post.title}}">
|
||||
</figure>
|
||||
{{/if}}
|
||||
|
||||
|
||||
<div class="modal-body">
|
||||
<h2>{{this.post.title}}</h2>
|
||||
{{#if this.post.excerpt}}
|
||||
|
@ -103,10 +103,10 @@
|
|||
</footer>
|
||||
{{/if}}
|
||||
|
||||
<button type="button" class="close" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
||||
<button type="button" class="close" title="Close" {{on "click" @close}} data-test-button="close-publish-flow">{{svg-jar "close"}}<span class="hidden">Close</span></button>
|
||||
|
||||
{{#unless this.post.emailOnly}}
|
||||
<a href="{{this.post.url}}" target="_blank" rel="noopener noreferrer" title="View post: {{this.post.title}}">
|
||||
<a href="{{this.post.url}}" target="_blank" rel="noopener noreferrer" title="View post: {{this.post.title}}" data-test-complete-bookmark>
|
||||
<div class="gh-post-card">
|
||||
{{#if this.post.featureImage}}
|
||||
<figure class="modal-image">
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
</span>
|
||||
{{/if}}
|
||||
</p>
|
||||
<p class="gh-content-entry-status">
|
||||
<p class="gh-content-entry-status" data-test-editor-post-status>
|
||||
<span class="published">
|
||||
Published
|
||||
{{#if @post.hasEmail}}
|
||||
|
@ -87,7 +87,7 @@
|
|||
<span class="gh-content-entry-date">– Lexical</span>
|
||||
{{/if}} --}}
|
||||
</p>
|
||||
<p class="gh-content-entry-status">
|
||||
<p class="gh-content-entry-status" data-test-editor-post-status>
|
||||
{{#if @post.isScheduled}}
|
||||
<span class="scheduled">
|
||||
Scheduled
|
||||
|
|
|
@ -16,9 +16,7 @@ export default class PostsList extends Component {
|
|||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
if (this.feature.publishFlowEndScreen) {
|
||||
this.checkPublishFlowModal();
|
||||
}
|
||||
this.checkPublishFlowModal();
|
||||
}
|
||||
|
||||
async checkPublishFlowModal() {
|
||||
|
|
|
@ -34,66 +34,58 @@
|
|||
{{moment-format publishedAt "HH:mm"}}
|
||||
{{/let}}
|
||||
</div>
|
||||
{{#if (feature "publishFlowEndScreen")}}
|
||||
<div style="display: flex; gap: 8px;">
|
||||
{{#if (feature "postAnalyticsRefresh")}}
|
||||
<GhTaskButton
|
||||
@buttonText="Refresh"
|
||||
@task={{this.fetchPostTask}}
|
||||
@showIcon={{true}}
|
||||
@idleIcon="reload"
|
||||
@successText="Refreshed"
|
||||
@class="gh-btn gh-btn-icon refresh"
|
||||
@successClass="gh-btn gh-btn-icon refresh" />
|
||||
{{/if}}
|
||||
{{#unless this.post.emailOnly}}
|
||||
<button type="button" class="gh-post-list-cta share" {{on "click" this.togglePublishFlowModal}}>
|
||||
{{svg-jar "share" title="Share post"}}<span>Share</span>
|
||||
</button>
|
||||
{{/unless}}
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<GhTaskButton
|
||||
@buttonText="Refresh"
|
||||
@task={{this.fetchPostTask}}
|
||||
@showIcon={{true}}
|
||||
@idleIcon="reload"
|
||||
@successText="Refreshed"
|
||||
@class="gh-btn gh-btn-icon refresh"
|
||||
@successClass="gh-btn gh-btn-icon refresh" />
|
||||
{{#unless this.post.emailOnly}}
|
||||
<button type="button" class="gh-post-list-cta share" {{on "click" this.togglePublishFlowModal}}>
|
||||
{{svg-jar "share" title="Share post"}}<span>Share</span>
|
||||
</button>
|
||||
{{/unless}}
|
||||
|
||||
<span class="dropdown">
|
||||
<GhDropdownButton
|
||||
@dropdownName="analytics-actions-menu"
|
||||
@classNames="gh-post-list-cta gh-btn-icon icon-only gh-btn-action-icon"
|
||||
@title="Analytics Actions"
|
||||
data-test-button="analytics-actions"
|
||||
>
|
||||
<span>
|
||||
{{svg-jar "dotdotdot"}}
|
||||
<span class="hidden">Actions</span>
|
||||
</span>
|
||||
</GhDropdownButton>
|
||||
<GhDropdown
|
||||
@name="analytics-actions-menu"
|
||||
@tagName="ul"
|
||||
@classNames="gh-analytics-actions-menu dropdown-menu dropdown-triangle-top-right"
|
||||
@closeOnClick={{true}}
|
||||
>
|
||||
<li>
|
||||
<LinkTo class="edit-post" @route="lexical-editor.edit" @models={{array this.post.displayName this.post.id}}>Edit post</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<a class="view-browser" href="{{this.post.url}}" target="_blank" rel="noopener noreferrer">View in browser</a>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="delete-post mr2"
|
||||
{{on "click" this.confirmDeleteMember}}
|
||||
data-test-button="delete-post"
|
||||
>
|
||||
<span class="red">Delete post</span>
|
||||
</button>
|
||||
</li>
|
||||
</GhDropdown>
|
||||
</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<LinkTo @route="lexical-editor.edit" @models={{array this.post.displayName this.post.id}} class="gh-post-list-cta edit" title="">
|
||||
{{svg-jar "pen" title=""}}<span>Edit post</span>
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
<span class="dropdown">
|
||||
<GhDropdownButton
|
||||
@dropdownName="analytics-actions-menu"
|
||||
@classNames="gh-post-list-cta gh-btn-icon icon-only gh-btn-action-icon"
|
||||
@title="Analytics Actions"
|
||||
data-test-button="analytics-actions"
|
||||
>
|
||||
<span>
|
||||
{{svg-jar "dotdotdot"}}
|
||||
<span class="hidden">Actions</span>
|
||||
</span>
|
||||
</GhDropdownButton>
|
||||
<GhDropdown
|
||||
@name="analytics-actions-menu"
|
||||
@tagName="ul"
|
||||
@classNames="gh-analytics-actions-menu dropdown-menu dropdown-triangle-top-right"
|
||||
@closeOnClick={{true}}
|
||||
>
|
||||
<li>
|
||||
<LinkTo class="edit-post" @route="lexical-editor.edit" @models={{array this.post.displayName this.post.id}}>Edit post</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<a class="view-browser" href="{{this.post.url}}" target="_blank" rel="noopener noreferrer">View in browser</a>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="delete-post mr2"
|
||||
{{on "click" this.confirmDeleteMember}}
|
||||
data-test-button="delete-post"
|
||||
>
|
||||
<span class="red">Delete post</span>
|
||||
</button>
|
||||
</li>
|
||||
</GhDropdown>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GhCanvasHeader>
|
||||
|
|
|
@ -50,9 +50,7 @@ export default class Analytics extends Component {
|
|||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
if (this.feature.publishFlowEndScreen) {
|
||||
this.checkPublishFlowModal();
|
||||
}
|
||||
this.checkPublishFlowModal();
|
||||
}
|
||||
|
||||
openPublishFlowModal() {
|
||||
|
@ -73,11 +71,7 @@ export default class Analytics extends Component {
|
|||
}
|
||||
|
||||
get post() {
|
||||
if (this.feature.publishFlowEndScreen) {
|
||||
return this._post ?? this.args.post;
|
||||
}
|
||||
|
||||
return this.args.post;
|
||||
return this._post ?? this.args.post;
|
||||
}
|
||||
|
||||
set post(value) {
|
||||
|
|
|
@ -78,8 +78,6 @@ export default class FeatureService extends Service {
|
|||
@feature('ActivityPub') ActivityPub;
|
||||
@feature('editorExcerpt') editorExcerpt;
|
||||
@feature('contentVisibility') contentVisibility;
|
||||
@feature('publishFlowEndScreen') publishFlowEndScreen;
|
||||
@feature('postAnalyticsRefresh') postAnalyticsRefresh;
|
||||
|
||||
_user = null;
|
||||
|
||||
|
|
|
@ -45,9 +45,7 @@ const ALPHA_FEATURES = [
|
|||
'importMemberTier',
|
||||
'lexicalIndicators',
|
||||
'adminXDemo',
|
||||
'contentVisibility',
|
||||
'publishFlowEndScreen',
|
||||
'postAnalyticsRefresh'
|
||||
'contentVisibility'
|
||||
];
|
||||
|
||||
module.exports.GA_KEYS = [...GA_FEATURES];
|
||||
|
|
|
@ -29,8 +29,6 @@ Object {
|
|||
"members": true,
|
||||
"newEmailAddresses": true,
|
||||
"outboundLinkTagging": true,
|
||||
"postAnalyticsRefresh": true,
|
||||
"publishFlowEndScreen": true,
|
||||
"stripeAutomaticTax": true,
|
||||
"themeErrorsNotification": true,
|
||||
"tipsAndDonations": true,
|
||||
|
|
|
@ -11,11 +11,11 @@ const {createTier, createMember, createPostDraft, impersonateMember} = require('
|
|||
* @param {string} [hoverStatus] Optional different status when you hover the status
|
||||
*/
|
||||
const checkPostStatus = async (page, status, hoverStatus) => {
|
||||
await expect(page.locator('[data-test-editor-post-status]')).toContainText(status, {timeout: 5000});
|
||||
await expect(page.locator('[data-test-editor-post-status]').first()).toContainText(status, {timeout: 5000});
|
||||
|
||||
if (hoverStatus) {
|
||||
await page.locator('[data-test-editor-post-status]').hover();
|
||||
await expect(page.locator('[data-test-editor-post-status]')).toContainText(hoverStatus, {timeout: 5000});
|
||||
await page.locator('[data-test-editor-post-status]').first().hover();
|
||||
await expect(page.locator('[data-test-editor-post-status]').first()).toContainText(hoverStatus, {timeout: 5000});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -198,8 +198,6 @@ test.describe('Publishing', () => {
|
|||
await createPostDraft(sharedPage, postData);
|
||||
await publishPost(sharedPage, {type: 'publish+send'});
|
||||
await closePublishFlow(sharedPage);
|
||||
|
||||
await checkPostStatus(sharedPage, 'Published');
|
||||
await checkPostPublished(sharedPage, postData);
|
||||
});
|
||||
|
||||
|
@ -232,8 +230,6 @@ test.describe('Publishing', () => {
|
|||
await createPostDraft(sharedPage, postData);
|
||||
await publishPost(sharedPage, {type: 'send'});
|
||||
await closePublishFlow(sharedPage);
|
||||
await checkPostStatus(sharedPage, 'Sent to '); // can't test for 1 member for now, because depends on test ordering :( (sometimes 2 members are created)
|
||||
|
||||
await checkPostNotPublished(sharedPage, postData);
|
||||
});
|
||||
});
|
||||
|
@ -327,8 +323,9 @@ test.describe('Publishing', () => {
|
|||
await expect(publishedHeader).toContainText(date.toFormat('LLL d, yyyy'));
|
||||
|
||||
// add some extra text to the post
|
||||
await adminPage.locator('li[data-test-post-id]').first().click();
|
||||
await adminPage.locator('[data-kg="editor"]').first().click();
|
||||
await adminPage.waitForTimeout(200); //
|
||||
await adminPage.waitForTimeout(500);
|
||||
await adminPage.keyboard.type(' This is some updated text.');
|
||||
|
||||
// change some post settings
|
||||
|
@ -431,7 +428,7 @@ test.describe('Publishing', () => {
|
|||
// Schedule the post to publish asap (by setting it to 00:00, it will get auto corrected to the minimum time possible - 5 seconds in the future)
|
||||
await publishPost(sharedPage, {type: 'send', time: '00:00'});
|
||||
await closePublishFlow(sharedPage);
|
||||
await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be sent to');
|
||||
await checkPostStatus(sharedPage, 'Scheduled', 'Scheduled to be sent in a few seconds');
|
||||
const editorUrl = await sharedPage.url();
|
||||
|
||||
// Check not published yet
|
||||
|
@ -472,6 +469,7 @@ test.describe('Publishing', () => {
|
|||
await checkPostNotPublished(testsharedPage, postData);
|
||||
|
||||
// Now unschedule this post
|
||||
await sharedPage.locator('li[data-test-post-id]').first().click();
|
||||
await sharedPage.locator('[data-test-button="update-flow"]').first().click();
|
||||
await sharedPage.locator('[data-test-button="revert-to-draft"]').click();
|
||||
|
||||
|
@ -566,6 +564,7 @@ test.describe('Updating post access', () => {
|
|||
|
||||
// publish
|
||||
await publishPost(sharedPage);
|
||||
await closePublishFlow(sharedPage);
|
||||
const frontendPage = await openPublishedPostBookmark(sharedPage);
|
||||
|
||||
// non-member doesn't have access
|
||||
|
@ -607,7 +606,6 @@ test.describe('Updating post access', () => {
|
|||
await closePublishFlow(page);
|
||||
|
||||
// go to settings and change the timezone
|
||||
await page.locator('[data-test-link="posts"]').click();
|
||||
await page.locator('[data-test-nav="settings"]').click();
|
||||
await expect(page.getByTestId('timezone')).toContainText('UTC');
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue