Publish modal updates (#20817)

The new modal for the updated publishing flow has an entirely new
layout, based on feedback previously received. In addition, this PR
includes a few tweaks to the underlying logic.
This commit is contained in:
Daniël van der Winden 2024-08-22 16:49:29 +02:00 committed by GitHub
parent cd7c27d3ad
commit 7c992825ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 328 additions and 128 deletions

View File

@ -96,10 +96,14 @@ export default class PublishFlowOptions extends Component {
if (this.feature.publishFlowEndScreen) {
if (this.args.publishOptions.isScheduled) {
localStorage.setItem('ghost-last-scheduled-post', this.args.publishOptions.post.id);
this.router.transitionTo('posts', {queryParams: {type: 'scheduled'}});
this.router.transitionTo('posts');
} else {
localStorage.setItem('ghost-last-published-post', this.args.publishOptions.post.id);
this.router.transitionTo('posts.analytics', this.args.publishOptions.post.id);
if (this.args.publishOptions.post.emailOnly) {
this.router.transitionTo('posts.analytics', this.args.publishOptions.post.id);
} else {
this.router.transitionTo('posts');
}
}
}
} catch (e) {

View File

@ -6,8 +6,41 @@
isFailure=this.isFailure
)}}
{{else}}
{{#if this.isRunning}}<span data-test-task-button-state="running">{{#if this.showIcon}}{{svg-jar "spinner" class="gh-icon-spinner"}} {{/if}}{{this.runningText}}</span>{{/if}}
{{#if this.isIdle}}<span data-test-task-button-state="idle">{{this.buttonText}}</span>{{/if}}
{{#if this.isSuccess}}<span {{did-insert this.handleReset}} data-test-task-button-state="success">{{#if this.showIcon}}{{svg-jar "check-circle"}} {{/if}}{{this.successText}}</span>{{/if}}
{{#if this.isFailure}}<span data-test-task-button-state="failure">{{#if this.showIcon}}{{svg-jar "retry"}} {{/if}}{{this.failureText}}</span>{{/if}}
{{#if this.isRunning}}
<span data-test-task-button-state="running">
{{#if this.showIcon}}
{{svg-jar "spinner" class="gh-icon-spinner"}}
{{/if}}
{{this.runningText}}
</span>
{{/if}}
{{#if (and this.isIdle (not (or this.isRunning this.isSuccess this.isFailure)))}}
<span data-test-task-button-state="idle">
{{#if this.showIcon}}
{{#if this.idleIcon}}
{{svg-jar this.idleIcon}}
{{/if}}
{{/if}}
{{this.buttonText}}
</span>
{{/if}}
{{#if this.isSuccess}}
<span {{did-insert this.handleReset}} data-test-task-button-state="success">
{{#if this.showIcon}}
{{svg-jar "check-circle"}}
{{/if}}
{{this.successText}}
</span>
{{/if}}
{{#if this.isFailure}}
<span data-test-task-button-state="failure">
{{#if this.showIcon}}
{{svg-jar "retry"}}
{{/if}}
{{this.failureText}}
</span>
{{/if}}
{{/if}}

View File

@ -33,6 +33,7 @@ const GhTaskButton = Component.extend({
defaultClick: false,
buttonText: 'Save',
idleClass: '',
idleIcon: '',
runningClass: '',
showIcon: true,
showSuccess: true, // set to false if you want the spinner to show until a transition occurs

View File

@ -1,18 +1,4 @@
<div class="modal-content">
{{#if this.post.featureImage}}
<figure class="modal-image">
<img src="{{this.post.featureImage}}" alt="{{this.post.title}}">
</figure>
{{else if this.post.twitterImage}}
<figure class="modal-image">
<img src="{{this.post.twitterImage}}" alt="{{this.post.title}}">
</figure>
{{else if this.post.ogImage}}
<figure class="modal-image">
<img src="{{this.post.ogImage}}" alt="{{this.post.title}}">
</figure>
{{/if}}
<header class="modal-header">
<h1>
{{#if this.post.isScheduled}}
@ -22,7 +8,7 @@
{{#if this.showPostCount}}
Boom! It's out there.
{{else}}
Your post is out there.
Your post is published.
{{/if}}
</span>
<span>
@ -32,7 +18,7 @@
{{#if this.showPostCount}}
That's {{format-number this.postCount}} {{gh-pluralize this.postCount "post" without-count=true}} published.
{{else}}
Share it with the world!
Spread the word!
{{/if}}
{{/if}}
</span>
@ -40,93 +26,156 @@
</h1>
</header>
<button type="button" class="close" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
<div class="modal-body">
{{#if (and this.post.isPublished (not this.post.emailOnly))}}
{{#if this.showPostCount}}
Keep up the good work. Now, share your post with the world!
{{else}}
Spread the word to your audience and increase your reach.
{{/if}}
{{#if (and this.post.isPublished (not this.post.emailOnly))}}
{{else}}
<div class="modal-body email">
{{#if this.post.isSent}}
It
{{else}}
{{#if this.post.isSent}}
It
{{else}}
{{if this.post.emailOnly "Your email" "Your post"}}
{{/if}}
{{if this.post.isScheduled "will be" "was"}}
{{#if this.post.emailOnly}}
sent to
{{else if this.post.willEmail}}
published on your site, and sent to
{{else}}
published on your site
{{/if}}
{{if this.post.emailOnly "Your email" "Your post"}}
{{/if}}
{{if this.post.isScheduled "will be" "was"}}
{{#if this.post.emailOnly}}
sent to
{{else if this.post.willEmail}}
published on your site, and sent to
{{else}}
published on your site
{{/if}}
{{#if (or this.post.hasEmail this.post.willEmail)}}
{{#let (members-count-fetcher query=(hash filter=this.post.fullRecipientFilter)) as |countFetcher|}}
<strong class="nowrap">
{{if (eq @recipientType "all") "all"}}
{{#if (or this.post.hasEmail this.post.willEmail)}}
{{#let (members-count-fetcher query=(hash filter=this.post.fullRecipientFilter)) as |countFetcher|}}
<strong class="nowrap">
{{if (eq @recipientType "all") "all"}}
{{format-number countFetcher.count}}
{{format-number countFetcher.count}}
{{!-- @recipientType = free/paid/all/specific --}}
{{if (not-eq @recipientType "all") @recipientType}}
{{!-- @recipientType = free/paid/all/specific --}}
{{if (not-eq @recipientType "all") @recipientType}}
{{gh-pluralize countFetcher.count "subscriber" without-count=true}}
</strong>
{{gh-pluralize countFetcher.count "subscriber" without-count=true}}
</strong>
of <strong>{{this.post.newsletter.name}}</strong>
{{/let}}
{{/if}}
{{#let (moment-site-tz this.post.publishedAtUTC) as |publishedAt|}}
on
{{moment-format publishedAt "D MMM YYYY"}}
at
{{moment-format publishedAt "HH:mm"}}.
of <strong>{{this.post.newsletter.name}}</strong>
{{/let}}
{{/if}}
{{#let (moment-site-tz this.post.publishedAtUTC) as |publishedAt|}}
on
<strong>{{moment-format publishedAt "D MMM YYYY"}}</strong>
at
<strong>{{moment-format publishedAt "HH:mm"}}</strong>.
{{/let}}
</div>
<footer class="modal-footer">
{{#if (and this.post.isPublished (not this.post.emailOnly))}}
<a href="https://twitter.com/intent/tweet?text={{this.encodedTitle}}&url={{this.encodedUrl}}" class="gh-btn twitter" target="_blank" rel="noopener noreferrer">
<span>{{svg-jar "social-x"}}</span>
</a>
<a href="https://threads.net/intent/post?text={{this.encodedTitleAndUrl}}" class="gh-btn threads" target="_blank" rel="noopener noreferrer">
<span>{{svg-jar "social-threads"}}</span>
</a>
<a href="https://www.facebook.com/sharer/sharer.php?u={{this.encodedUrl}}" class="gh-btn facebook" target="_blank" rel="noopener noreferrer">
<span>{{svg-jar "social-facebook"}}</span>
</a>
<a href="http://www.linkedin.com/shareArticle?mini=true&title={{this.encodedTitle}}&url={{this.encodedUrl}}" class="gh-btn linkedin" target="_blank" rel="noopener noreferrer">
<span>{{svg-jar "social-linkedin"}}</span>
</a>
<GhTaskButton
@buttonText="Copy link"
@task={{this.handleCopyLink}}
@showIcon={{true}}
@successText="Link copied"
@class="gh-btn gh-btn-primary gh-btn-icon copy-link" />
{{else}}
{{#if (and this.post.isScheduled (not this.post.emailOnly))}}
<GhTaskButton
@buttonText="Copy preview link"
@task={{this.handleCopyPreviewLink}}
@successText="Link copied"
@class="gh-btn gh-btn-icon copy-preview-link" />
{{/if}}
{{#if this.post.emailOnly}}
<footer class="modal-footer-email">
<button
class="gh-btn gh-btn-primary dismiss"
type="button"
{{on "click" @close}}
{{on "mousedown" (optional this.noop)}}
>
<span>OK</span>
<span>Close</span>
</button>
</footer>
{{/if}}
{{/if}}
<button type="button" class="close" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
{{#unless this.post.emailOnly}}
<div class="gh-post-card">
{{#if this.post.featureImage}}
<figure class="modal-image">
<img src="{{this.post.featureImage}}" alt="{{this.post.title}}">
</figure>
{{else if this.post.twitterImage}}
<figure class="modal-image">
<img src="{{this.post.twitterImage}}" alt="{{this.post.title}}">
</figure>
{{else if this.post.ogImage}}
<figure class="modal-image">
<img src="{{this.post.ogImage}}" alt="{{this.post.title}}">
</figure>
{{/if}}
</footer>
<div class="modal-body">
<h2>{{this.post.title}}</h2>
{{#if this.post.excerpt}}
<p class="post-excerpt">{{this.post.excerpt}}</p>
{{/if}}
<div class="gh-post-details">
{{#if (get-setting "icon")}}
<div class="gh-post-bookmark-site-icon">
<img src={{get-setting "icon"}} alt="" role="presentation" />
</div>
{{/if}}
{{#if (get-setting "title")}}
<div class="gh-post-bookmark-site-name">{{get-setting "title"}}</div>
{{/if}}
<div class="gh-post-bookmark-authors">{{this.authorNames}}</div>
</div>
</div>
<footer class="modal-footer">
{{#if (and this.post.isPublished (not this.post.emailOnly))}}
<div class="share-buttons">
<a href="https://twitter.com/intent/tweet?text={{this.encodedTitle}}&url={{this.encodedUrl}}" class="gh-btn twitter" target="_blank" rel="noopener noreferrer" title="Share on Twitter">
<span>{{svg-jar "social-x"}}</span>
</a>
<a href="https://threads.net/intent/post?text={{this.encodedTitleAndUrl}}" class="gh-btn threads" target="_blank" rel="noopener noreferrer" title="Share on Threads">
<span>{{svg-jar "social-threads"}}</span>
</a>
<a href="https://www.facebook.com/sharer/sharer.php?u={{this.encodedUrl}}" class="gh-btn facebook" target="_blank" rel="noopener noreferrer" title="Share on Facebook">
<span>{{svg-jar "social-facebook"}}</span>
</a>
<a href="http://www.linkedin.com/shareArticle?mini=true&title={{this.encodedTitle}}&url={{this.encodedUrl}}" class="gh-btn linkedin" target="_blank" rel="noopener noreferrer" title="Share on LinkedIn">
<span>{{svg-jar "social-linkedin"}}</span>
</a>
</div>
<GhTaskButton
@showIcon={{true}}
@idleIcon="link"
@buttonText="Copy link"
@title="Copy link"
@task={{this.handleCopyLink}}
@successText="Link copied"
@class="gh-btn gh-btn-primary gh-btn-icon copy-link" />
{{else}}
{{#if (and this.post.isScheduled (not this.post.emailOnly))}}
<GhTaskButton
@buttonText="Copy preview link"
@task={{this.handleCopyPreviewLink}}
@showIcon={{true}}
@idleIcon="link"
@successText="Preview link copied"
@class="gh-btn gh-btn-icon copy-preview-link" />
<button
class="gh-btn gh-btn-primary dismiss"
type="button"
{{on "click" @close}}
{{on "mousedown" (optional this.noop)}}
>
<span>Close</span>
</button>
{{else}}
<button
class="gh-btn gh-btn-primary dismiss"
type="button"
{{on "click" @close}}
{{on "mousedown" (optional this.noop)}}
>
<span>Close</span>
</button>
{{/if}}
{{/if}}
</footer>
</div>
{{/unless}}
</div>

View File

@ -1,5 +1,6 @@
import Component from '@glimmer/component';
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
import {authorNames} from '../helpers/author-names';
import {capitalize} from '@ember/string';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
@ -38,6 +39,10 @@ export default class PostSuccessModal extends Component {
return encodeURIComponent(`${this.post.title} ${this.post.url}`);
}
get authorNames() {
return authorNames([this.post.authors]);
}
@task
*handleCopyLink() {
copyTextToClipboard(this.post.url);

View File

@ -2,13 +2,17 @@ import Component from '@glimmer/component';
import PostSuccessModal from '../modal-post-success';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class PostsList extends Component {
@service store;
@service modals;
@service feature;
@tracked postCount = 0;
latestScheduledPost = null;
latestPublishedPost = null;
constructor() {
super(...arguments);
@ -25,6 +29,17 @@ export default class PostsList extends Component {
});
localStorage.removeItem('ghost-last-scheduled-post');
}
if (localStorage.getItem('ghost-last-published-post')) {
await this.getlatestPublishedPost.perform();
await this.getPostCount.perform();
this.modals.open(PostSuccessModal, {
post: this.latestPublishedPost,
postCount: this.postCount,
showPostCount: true
});
localStorage.removeItem('ghost-last-published-post');
}
}
get list() {
@ -36,4 +51,16 @@ export default class PostsList extends Component {
const result = yield this.store.query('post', {filter: `id:${localStorage.getItem('ghost-last-scheduled-post')}`, limit: 1});
this.latestScheduledPost = result.toArray()[0];
}
}
@task
*getlatestPublishedPost() {
const result = yield this.store.query('post', {filter: `id:${localStorage.getItem('ghost-last-published-post')}`, limit: 1});
this.latestPublishedPost = result.toArray()[0];
}
@task
*getPostCount() {
const result = yield this.store.query('post', {filter: 'status:published', limit: 1, page: 1});
this.postCount = result.meta.pagination.total;
}
}

View File

@ -895,10 +895,16 @@
border-radius: var(--radius);
}
.modal-post-success .gh-post-card {
border-radius: 8px;
border: 1px solid var(--lightgrey-l1);
box-sizing: border-box;
margin: 24px 0 0 0;
}
.modal-post-success .modal-image {
aspect-ratio: 16 / 7.55;
overflow: hidden;
margin: calc(var(--padding) * -1) calc(var(--padding) * -1) var(--padding);
}
.modal-post-success .modal-image img {
@ -906,7 +912,8 @@
width: 100%;
height: 100%;
object-fit: cover;
border-radius: var(--radius) var(--radius) 0 0;
border-radius: 7px 7px 0 0;
box-sizing: border-box;
}
.modal-post-success .modal-header {
@ -932,6 +939,48 @@
line-height: 1.4;
letter-spacing: -0.01em;
text-wrap: pretty;
padding: 16px 24px;
}
.modal-post-success .modal-body p {
font-size: 1.6rem;
line-height: 1.5em;
}
.modal-post-success .modal-body p.post-excerpt {
-webkit-line-clamp: 2;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0 0 16px 0;
}
.modal-post-success .email {
font-size: 1.6rem;
padding: 0;
}
.modal-post-success .modal-body .gh-post-details {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 16px;
font-size: 1.4rem;
}
.modal-post-success .modal-body .gh-post-details .gh-post-bookmark-site-name {
font-weight: 600;
flex-shrink: 0;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.modal-post-success .modal-body .gh-post-bookmark-site-icon {
width: 24px;
height: 24px;
}
.modal-post-success .modal-body strong.nowrap {
@ -939,17 +988,44 @@
}
.modal-post-success .modal-footer {
background: var(--whitegrey-l2);
margin-top: 0;
gap: 16px;
padding: 16px 24px;
border-top: 1px solid var(--lightgrey-l1);
border-radius: 0 0 8px 8px;
}
.modal-post-success .modal-footer-email {
background: var(--white);
margin-top: 36px;
gap: 16px;
padding: 0;
border: none;
border-radius: 0;
text-align: right;
}
.modal-post-success .modal-footer .share-buttons {
display: flex;
justify-content: space-evenly;
align-items: center;
gap: 16px;
margin-top: var(--padding);
}
.modal-post-success .modal-footer .gh-btn {
display: flex;
justify-content: center;
align-items: center;
min-width: 64px;
height: 40px;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
}
.modal-post-success .modal-footer .copy-preview-link {
background: var(--white);
border: 1px solid var(--lightgrey-l1);
}
.modal-post-success .modal-footer .gh-btn:not(:first-child) {
@ -959,6 +1035,8 @@
.modal-post-success .modal-footer .gh-btn span {
padding-inline: 18px;
font-size: 1.4rem;
height: 100%;
align-content: center;
}
.modal-post-success .modal-footer .gh-btn-primary {
@ -971,6 +1049,12 @@
.modal-post-success .modal-footer .gh-btn:is(.twitter, .threads, .facebook, .linkedin) {
width: 56px;
background: var(--white);
border: 1px solid var(--lightgrey-l1);
}
.modal-post-success .modal-footer .gh-btn:is(.twitter, .threads, .facebook, .linkedin, .copy-link, .copy-preview-link):hover {
background: var(--whitegrey-l1);
}
.modal-post-success .modal-footer .gh-btn:is(.twitter, .threads, .facebook, .linkedin) span {
@ -991,27 +1075,18 @@
right: 24px;
}
.modal-post-success:has(.modal-image) .close {
display: flex;
justify-content: center;
align-items: center;
width: 32px;
height: 32px;
top: 16px;
right: 16px;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 50%;
}
@media (max-width: 500px) {
.modal-post-success .modal-header h1 {
font-size: 2.4rem;
}
.modal-post-success .modal-footer {
display: flex;
flex-direction: column;
}
.modal-post-success:has(.modal-image) .close:hover {
background-color: rgba(0, 0, 0, 0.3);
}
.modal-post-success:has(.modal-image) .close svg {
width: 14px;
height: 14px;
}
.modal-post-success:has(.modal-image) .close svg path {
fill: white;
}
.modal-post-success .modal-footer .copy-link,
.modal-post-success .modal-footer .copy-preview-link {
width: 100%!important;
}
}

View File

@ -725,7 +725,7 @@
.gh-canvas-title.gh-post-title {
max-width: 880px;
padding: 20px 0 12px;
padding: 20px 0 0 0;
font-size: 3.2rem;
line-height: 1.2em;
overflow: inherit;
@ -1481,6 +1481,11 @@
transition: all .1s linear;
}
.gh-post-list-cta:hover {
border-color: var(--lightgrey-l2);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
.gh-list-data .gh-post-list-cta {
justify-content: center;
width: 56px;
@ -1491,6 +1496,10 @@
margin-right: 0;
}
.gh-post-analytics-header .share svg {
margin-top: -2px;
}
.gh-post-list-cta.is-hovered {
border-color: var(--whitegrey-d2);
}

View File

@ -1,4 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>link</title>
<path stroke="currentColor" d="m14.5 12.5.086.086a2 2 0 0 0 2.828 0l3.965-3.964a3.01 3.01 0 0 0 0-4.243l-1.758-1.757a3.008 3.008 0 0 0-4.242 0l-3.965 3.964a2 2 0 0 0 0 2.829l.086.085m-2 2-.086-.085a2 2 0 0 0-2.828 0l-3.965 3.964a3.01 3.01 0 0 0 0 4.243l1.758 1.757a3.008 3.008 0 0 0 4.242 0l3.965-3.964a2 2 0 0 0 0-2.829L12.5 14.5m-4.389 1.389 7.778-7.778" fill="none"/>
</svg>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" id="Hyperlink-3--Streamline-Ultimate.svg" height="24" width="24"><desc>Hyperlink 3 Streamline Icon: https://streamlinehq.com</desc><path d="m9.364 18.5 -0.932 0.932a4.5 4.5 0 0 1 -6.364 -6.364l4.773 -4.774a4.5 4.5 0 0 1 6.825 5.825" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="m14.818 5.567 0.75 -0.75a4.5 4.5 0 0 1 6.364 6.364l-4.773 4.773a4.5 4.5 0 0 1 -6.824 -5.826" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path></svg>

Before

Width:  |  Height:  |  Size: 485 B

After

Width:  |  Height:  |  Size: 615 B