0
Fork 0
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:
Sodbileg Gansukh 2024-08-30 03:17:16 +08:00 committed by GitHub
parent c2ae91e4db
commit d30164df97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 90 additions and 131 deletions

View file

@ -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 = () => {

View file

@ -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}}

View file

@ -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) {

View file

@ -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">

View file

@ -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

View file

@ -16,9 +16,7 @@ export default class PostsList extends Component {
constructor() {
super(...arguments);
if (this.feature.publishFlowEndScreen) {
this.checkPublishFlowModal();
}
this.checkPublishFlowModal();
}
async checkPublishFlowModal() {

View file

@ -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>

View file

@ -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) {

View file

@ -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;

View file

@ -45,9 +45,7 @@ const ALPHA_FEATURES = [
'importMemberTier',
'lexicalIndicators',
'adminXDemo',
'contentVisibility',
'publishFlowEndScreen',
'postAnalyticsRefresh'
'contentVisibility'
];
module.exports.GA_KEYS = [...GA_FEATURES];

View file

@ -29,8 +29,6 @@ Object {
"members": true,
"newEmailAddresses": true,
"outboundLinkTagging": true,
"postAnalyticsRefresh": true,
"publishFlowEndScreen": true,
"stripeAutomaticTax": true,
"themeErrorsNotification": true,
"tipsAndDonations": true,

View file

@ -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');