mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-18 02:21:47 -05:00
Started implementing the new publishing flow
This commit is contained in:
parent
768ba8c558
commit
d24a5a0646
9 changed files with 422 additions and 3 deletions
|
@ -91,6 +91,11 @@ export default class PublishFlowOptions extends Component {
|
|||
|
||||
try {
|
||||
yield this.args.saveTask.perform();
|
||||
if (this.args.publishOptions.isScheduled) {
|
||||
window.location.href = '/ghost/#/posts/?success=true';
|
||||
} else {
|
||||
window.location.href = `/ghost/#/posts/analytics/${this.args.publishOptions.post.id}/?success=true`;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e === undefined && this.args.publishOptions.post.errors.length !== 0) {
|
||||
// validation error
|
||||
|
|
155
ghost/admin/app/components/modal-publish-flow.hbs
Normal file
155
ghost/admin/app/components/modal-publish-flow.hbs
Normal file
|
@ -0,0 +1,155 @@
|
|||
{{#if this.post.featureImage}}
|
||||
<figure class="modal-image">
|
||||
<img src="{{this.post.featureImage}}" alt="{{this.post.title}}">
|
||||
</figure>
|
||||
{{/if}}
|
||||
|
||||
<header class="modal-header">
|
||||
<h1>
|
||||
{{#unless this.post.isScheduled}}
|
||||
<span>Boom! It's out there.</span>
|
||||
<span>
|
||||
{{#if this.post.emailOnly}}
|
||||
Your email has been sent.
|
||||
{{else}}
|
||||
That's 100 posts published.
|
||||
{{/if}}
|
||||
</span>
|
||||
{{else}}
|
||||
<span>All set!</span>
|
||||
{{/unless}}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
{{!-- disable mouseDown so it doesn't trigger focus-out validations --}}
|
||||
<a class="close" href role="button" title="Close" {{action "closeModal"}} {{action (optional this.noop) on="mouseDown"}}>
|
||||
{{svg-jar "close"}}<span class="hidden">Close</span>
|
||||
</a>
|
||||
|
||||
<div class="modal-body">
|
||||
{{#if (and this.post.isPublished (not this.post.emailOnly))}}
|
||||
Keep up the good work. Now, share your post with the world!
|
||||
{{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 (or this.post.hasEmail this.post.willEmail)}}
|
||||
{{#let (members-count-fetcher query=(hash filter=this.post.fullRecipientFilter)) as |countFetcher|}}
|
||||
<strong>
|
||||
{{if (eq @recipientType "all") "all"}}
|
||||
|
||||
{{format-number countFetcher.count}}
|
||||
|
||||
{{!-- @recipientType = free/paid/all/specific --}}
|
||||
{{if (not-eq @recipientType "all") @recipientType}}
|
||||
|
||||
{{gh-pluralize countFetcher.count "subscriber" without-count=true}}
|
||||
</strong>
|
||||
|
||||
of <strong>{{this.post.newsletter.name}}</strong>
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
|
||||
{{#let (moment-site-tz post.publishedAtUTC) as |publishedAt|}}
|
||||
on
|
||||
{{moment-format publishedAt "D MMM YYYY"}}
|
||||
at
|
||||
{{moment-format publishedAt "HH:mm"}}.
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<footer class="modal-footer">
|
||||
{{#if (and this.post.isPublished (not this.post.emailOnly))}}
|
||||
<button
|
||||
class="gh-btn twitter"
|
||||
type="button"
|
||||
{{!-- {{on "click" this.close}} --}}
|
||||
{{on "mousedown" (optional this.noop)}}
|
||||
>
|
||||
<span>{{svg-jar "social-x"}}</span>
|
||||
</button>
|
||||
<button
|
||||
class="gh-btn threads"
|
||||
type="button"
|
||||
{{!-- {{on "click" this.close}} --}}
|
||||
{{on "mousedown" (optional this.noop)}}
|
||||
>
|
||||
<span>{{svg-jar "social-threads"}}</span>
|
||||
</button>
|
||||
<button
|
||||
class="gh-btn facebook"
|
||||
type="button"
|
||||
{{!-- {{on "click" this.close}} --}}
|
||||
{{on "mousedown" (optional this.noop)}}
|
||||
>
|
||||
<span>{{svg-jar "social-facebook"}}</span>
|
||||
</button>
|
||||
<button
|
||||
class="gh-btn linkedin"
|
||||
type="button"
|
||||
{{!-- {{on "click" this.close}} --}}
|
||||
{{on "mousedown" (optional this.noop)}}
|
||||
>
|
||||
<span>{{svg-jar "social-linkedin"}}</span>
|
||||
</button>
|
||||
<button
|
||||
class="gh-btn gh-btn-primary"
|
||||
type="button"
|
||||
{{!-- {{on "click" this.close}} --}}
|
||||
{{on "mousedown" (optional this.noop)}}
|
||||
>
|
||||
<span>{{svg-jar "twitter-link"}} Copy link</span>
|
||||
</button>
|
||||
{{else}}
|
||||
{{#if this.post.emailOnly}}
|
||||
{{#if this.post.isScheduled}}
|
||||
<button
|
||||
class="gh-btn"
|
||||
type="button"
|
||||
{{!-- {{on "click" this.close}} --}}
|
||||
{{on "mousedown" (optional this.noop)}}
|
||||
>
|
||||
<span>Unschedule</span>
|
||||
</button>
|
||||
{{else}}
|
||||
<button
|
||||
class="gh-btn"
|
||||
type="button"
|
||||
{{!-- {{on "click" this.close}} --}}
|
||||
{{on "mousedown" (optional this.noop)}}
|
||||
>
|
||||
<span>View in browser</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<button
|
||||
class="gh-btn"
|
||||
type="button"
|
||||
{{!-- {{on "click" this.close}} --}}
|
||||
{{on "mousedown" (optional this.noop)}}
|
||||
>
|
||||
<span>Copy preview link</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
<button
|
||||
class="gh-btn gh-btn-primary"
|
||||
type="button"
|
||||
{{!-- {{on "click" this.close}} --}}
|
||||
{{on "mousedown" (optional this.noop)}}
|
||||
>
|
||||
<span>OK</span>
|
||||
</button>
|
||||
{{/if}}
|
||||
</footer>
|
40
ghost/admin/app/components/modal-publish-flow.js
Normal file
40
ghost/admin/app/components/modal-publish-flow.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import ModalComponent from 'ghost-admin/components/modal-base';
|
||||
// import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
|
||||
import {alias} from '@ember/object/computed';
|
||||
// import {inject} from 'ghost-admin/decorators/inject';
|
||||
import {inject as service} from '@ember/service';
|
||||
// import {task, timeout} from 'ember-concurrency';
|
||||
|
||||
export default ModalComponent.extend({
|
||||
store: service(),
|
||||
|
||||
classNames: 'modal-publish-flow',
|
||||
|
||||
// signinUrl: null,
|
||||
// config: inject(),
|
||||
|
||||
post: alias('model')
|
||||
|
||||
// didInsertElement() {
|
||||
// this._super(...arguments);
|
||||
|
||||
// this._signinUrlUpdateTask.perform();
|
||||
// },
|
||||
|
||||
// actions: {
|
||||
// // noop - we don't want the enter key doing anything
|
||||
// confirm() {}
|
||||
// },
|
||||
|
||||
// copySigninUrl: task(function* () {
|
||||
// copyTextToClipboard(this.signinUrl);
|
||||
// yield timeout(1000);
|
||||
// return true;
|
||||
// }),
|
||||
|
||||
// _signinUrlUpdateTask: task(function*() {
|
||||
// const memberSigninURL = yield this.member.fetchSigninUrl.perform();
|
||||
|
||||
// this.set('signinUrl', memberSigninURL.url);
|
||||
// }).drop()
|
||||
});
|
|
@ -34,9 +34,13 @@
|
|||
{{moment-format publishedAt "HH:mm"}}
|
||||
{{/let}}
|
||||
</div>
|
||||
<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>
|
||||
<div style="display: flex; gap: 4px;">
|
||||
<button class="gh-post-list-cta edit">♻️<span>Refresh</span></button>
|
||||
<button class="gh-post-list-cta edit" {{on "click" this.togglePublishFlowModal}}>🔁<span>Share</span></button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GhCanvasHeader>
|
||||
|
@ -202,3 +206,12 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
|
||||
{{#if this.showPublishFlowModal}}
|
||||
<GhFullscreenModal
|
||||
@modal="publish-flow"
|
||||
@model={{this.post}}
|
||||
@publishOptions={{this.publishOptions}}
|
||||
@close={{this.togglePublishFlowModal}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
|
@ -1,8 +1,10 @@
|
|||
import Component from '@glimmer/component';
|
||||
import PublishOptionsResource from 'ghost-admin/helpers/publish-options';
|
||||
import {action} from '@ember/object';
|
||||
import {didCancel, task} from 'ember-concurrency';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
import {use} from 'ember-could-get-used-to-this';
|
||||
|
||||
/**
|
||||
* @typedef {import('../../services/dashboard-stats').SourceAttributionCount} SourceAttributionCount
|
||||
|
@ -24,6 +26,7 @@ export default class Analytics extends Component {
|
|||
@service utils;
|
||||
@service feature;
|
||||
@service store;
|
||||
@service router;
|
||||
|
||||
@tracked sources = null;
|
||||
@tracked links = null;
|
||||
|
@ -31,8 +34,25 @@ export default class Analytics extends Component {
|
|||
@tracked sortColumn = 'signups';
|
||||
@tracked showSuccess;
|
||||
@tracked updateLinkId;
|
||||
@tracked showPublishFlowModal = false;
|
||||
displayOptions = DISPLAY_OPTIONS;
|
||||
|
||||
@use publishOptions = new PublishOptionsResource(() => [this.args.post]);
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.checkUrlParameter();
|
||||
}
|
||||
|
||||
checkUrlParameter() {
|
||||
const currentURL = this.router.currentURL;
|
||||
const url = new URL(window.location.origin + currentURL);
|
||||
const successParam = url.searchParams.get('success');
|
||||
if (successParam === 'true') {
|
||||
this.showPublishFlowModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
get post() {
|
||||
return this.args.post;
|
||||
}
|
||||
|
@ -142,6 +162,33 @@ export default class Analytics extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
togglePublishFlowModal() {
|
||||
if (this.showPublishFlowModal) {
|
||||
const hash = window.location.hash;
|
||||
|
||||
// Extract the part before '?' and the query parameters
|
||||
const [path, queryParamsString] = hash.split('?');
|
||||
|
||||
// If there are query parameters, proceed to modify them
|
||||
if (queryParamsString) {
|
||||
const searchParams = new URLSearchParams(queryParamsString);
|
||||
|
||||
// Remove the 'success' parameter
|
||||
searchParams.delete('success');
|
||||
|
||||
// Construct the new hash fragment
|
||||
const newQueryParamsString = searchParams.toString();
|
||||
const newHash = newQueryParamsString ? `${path}?${newQueryParamsString}` : path;
|
||||
|
||||
// Update the URL without reloading the page
|
||||
window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}${newHash}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.showPublishFlowModal = !this.showPublishFlowModal;
|
||||
}
|
||||
|
||||
updateLinkData(linksData) {
|
||||
let updatedLinks;
|
||||
if (this.links?.length) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import {DEFAULT_QUERY_PARAMS} from 'ghost-admin/helpers/reset-query-params';
|
|||
import {action} from '@ember/object';
|
||||
import {inject} from 'ghost-admin/decorators/inject';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
const TYPES = [{
|
||||
|
@ -68,6 +69,8 @@ export default class PostsController extends Controller {
|
|||
@tracked tag = null;
|
||||
@tracked order = null;
|
||||
@tracked selectionList = new SelectionList(this.postsInfinityModel);
|
||||
@tracked showPublishFlowModal = false;
|
||||
@tracked latestScheduledPost = null;
|
||||
|
||||
availableTypes = TYPES;
|
||||
availableVisibilities = VISIBILITIES;
|
||||
|
@ -81,10 +84,31 @@ export default class PostsController extends Controller {
|
|||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.checkUrlParameter();
|
||||
|
||||
this.router.on('routeDidChange', () => {
|
||||
this.checkUrlParameter();
|
||||
});
|
||||
|
||||
this.getLatestScheduledPost.perform();
|
||||
|
||||
Object.assign(this, DEFAULT_QUERY_PARAMS.posts);
|
||||
}
|
||||
|
||||
checkUrlParameter() {
|
||||
const hash = window.location.hash; // Get the full hash fragment
|
||||
const queryParamsString = hash.split('?')[1]; // Get the part after '?'
|
||||
|
||||
if (queryParamsString) {
|
||||
const searchParams = new URLSearchParams(queryParamsString);
|
||||
const successParam = searchParams.get('success');
|
||||
|
||||
if (successParam === 'true') {
|
||||
this.showPublishFlowModal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get postsInfinityModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
@ -174,4 +198,15 @@ export default class PostsController extends Controller {
|
|||
openEditor(post) {
|
||||
this.router.transitionTo('lexical-editor.edit', 'post', post.id);
|
||||
}
|
||||
|
||||
@action
|
||||
togglePublishFlowModal() {
|
||||
this.showPublishFlowModal = !this.showPublishFlowModal;
|
||||
}
|
||||
|
||||
@task
|
||||
*getLatestScheduledPost() {
|
||||
const result = yield this.store.query('post', {filter: 'status:scheduled', limit: 1});
|
||||
this.latestScheduledPost = result.toArray()[0];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -880,3 +880,109 @@
|
|||
height: 20px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* Publish flow modal
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
.modal-publish-flow {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-image {
|
||||
aspect-ratio: 16 / 7.55;
|
||||
overflow: hidden;
|
||||
margin: -32px -32px 32px;
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-image img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-header h1 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
font-size: 3.2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-header h1 span:has(+ span) {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-body {
|
||||
margin-top: 16px;
|
||||
font-size: 1.8rem;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-footer .gh-btn {
|
||||
height: 48px;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-footer .gh-btn span {
|
||||
padding-inline: 16px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-footer:has(.twitter) .gh-btn-primary {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-footer .gh-btn:is(.twitter, .threads, .facebook, .linkedin) {
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-footer .gh-btn:is(.twitter, .threads, .facebook, .linkedin) span {
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-footer .gh-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-footer .gh-btn.twitter svg path {
|
||||
fill: black;
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-footer .gh-btn-primary svg {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.modal-publish-flow .modal-footer .gh-btn-primary svg path {
|
||||
fill: white
|
||||
}
|
||||
|
||||
.modal-publish-flow:has(.modal-image) .close {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.modal-publish-flow:has(.modal-image) .close:hover {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.modal-publish-flow:has(.modal-image) .close svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.modal-publish-flow:has(.modal-image) .close svg path {
|
||||
fill: white;
|
||||
}
|
||||
|
|
|
@ -59,3 +59,11 @@
|
|||
|
||||
{{outlet}}
|
||||
</section>
|
||||
|
||||
{{#if this.showPublishFlowModal}}
|
||||
<GhFullscreenModal
|
||||
@modal="publish-flow"
|
||||
@model={{this.latestScheduledPost}}
|
||||
@close={{this.togglePublishFlowModal}}
|
||||
@modifier="action wide" />
|
||||
{{/if}}
|
10
ghost/admin/public/assets/icons/social-threads.svg
Normal file
10
ghost/admin/public/assets/icons/social-threads.svg
Normal file
|
@ -0,0 +1,10 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_351_18008)">
|
||||
<path d="M13.0332 8.37917C12.96 8.34407 12.8856 8.3103 12.8103 8.27795C12.6791 5.86015 11.3579 4.47596 9.1396 4.4618C9.12955 4.46173 9.11955 4.46173 9.1095 4.46173C7.78265 4.46173 6.67913 5.0281 5.99992 6.05871L7.21993 6.89561C7.72733 6.12579 8.52364 5.96167 9.11008 5.96167C9.11686 5.96167 9.12366 5.96168 9.13036 5.96174C9.86078 5.96639 10.4119 6.17876 10.7687 6.59291C11.0283 6.89442 11.2019 7.31107 11.2879 7.8369C10.6403 7.72683 9.93993 7.69299 9.19122 7.73592C7.08214 7.8574 5.72624 9.08747 5.81731 10.7967C5.86352 11.6637 6.29544 12.4096 7.03346 12.8968C7.65745 13.3087 8.46111 13.5101 9.29635 13.4645C10.3994 13.4041 11.2647 12.9832 11.8684 12.2137C12.3268 11.6293 12.6168 10.872 12.7448 9.91782C13.2705 10.2351 13.6601 10.6525 13.8753 11.1544C14.2411 12.0075 14.2624 13.4094 13.1186 14.5523C12.1164 15.5535 10.9117 15.9866 9.09104 16C7.07147 15.9851 5.54409 15.3374 4.55103 14.0749C3.62111 12.8928 3.14053 11.1854 3.1226 9C3.14053 6.8146 3.62111 5.10714 4.55103 3.92503C5.54409 2.66262 7.07144 2.01495 9.09101 1.99994C11.1252 2.01506 12.6792 2.66585 13.7103 3.93435C14.2159 4.55641 14.597 5.3387 14.8483 6.25081L16.278 5.86936C15.9734 4.74665 15.4942 3.7792 14.842 2.97686C13.5201 1.35059 11.5869 0.517279 9.096 0.5H9.08603C6.60019 0.517219 4.68862 1.3537 3.40443 2.98619C2.26168 4.4389 1.67221 6.46024 1.65241 8.99402L1.65234 9L1.65241 9.00598C1.67221 11.5397 2.26168 13.5611 3.40443 15.0138C4.68862 16.6463 6.60019 17.4828 9.08603 17.5H9.096C11.306 17.4847 12.8638 16.9061 14.1472 15.6239C15.8262 13.9465 15.7756 11.8439 15.2222 10.5531C14.8252 9.62749 14.0683 8.8757 13.0332 8.37917ZM9.21739 11.9668C8.29301 12.0188 7.33268 11.6039 7.28533 10.7152C7.25023 10.0563 7.75426 9.32105 9.27412 9.23347C9.44817 9.22343 9.61897 9.21852 9.78676 9.21852C10.3388 9.21852 10.8553 9.27215 11.3248 9.3748C11.1497 11.562 10.1224 11.9171 9.21739 11.9668Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_351_18008">
|
||||
<rect width="17" height="17" fill="white" transform="translate(0.5 0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
Loading…
Add table
Reference in a new issue