0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

Added scheduled and published states to new publish flow

closes https://github.com/TryGhost/Team/issues/1589
closes https://github.com/TryGhost/Team/issues/1590

When a post is already scheduled or published the "Publish" button changes to "Update" and when clicked shows details about the last publish / upcoming schedule along with buttons to save any changes or revert to a draft.

- added separate `update-flow` modal with the save/revert options to keep the main `publish-flow` cleaner and focused just on the draft->publish flow
This commit is contained in:
Kevin Ansfield 2022-05-05 15:59:34 +01:00
parent 27cb5a9fec
commit 8fb0d6ebb2
6 changed files with 196 additions and 26 deletions

View file

@ -1,5 +1,5 @@
<div class="flex flex-column h-100 items-center overflow-auto">
<header class="gh-publish-header" data-test-modal="publish">
<header class="gh-publish-header" data-test-modal="publish-flow">
<button class="gh-publish-back-button" title="Close" type="button" {{on "click" @close}}>
<span>{{svg-jar "close-stroke"}} Close</span>
</button>

View file

@ -26,10 +26,10 @@
<strong>{{pluralize post.email.emailCount "member"}}</strong>
and was
{{#if post.emailOnly}}
and was <strong>not</strong>
{{else}}
and was
<strong>not</strong>
{{/if}}
{{/if}}

View file

@ -0,0 +1,77 @@
<div class="flex flex-column h-100 items-center overflow-auto">
<header class="gh-publish-header" data-test-modal="update-flow">
<button class="gh-publish-back-button" title="Close" type="button" {{on "click" @close}}>
<span>{{svg-jar "close-stroke"}} Close</span>
</button>
</header>
{{#let @data.publishOptions.post as |post|}}
<div class="gh-publish-settings-container">
<div class="gh-publish-title">
This {{post.displayName}} is
<span>{{post.status}}</span>
</div>
<p>
{{#let (moment-site-tz post.publishedAtUTC) as |publishedAt|}}
On
<strong>
{{moment-format publishedAt "D MMM YYYY"}}
at
{{moment-format publishedAt "HH:mm"}}
</strong>
your
{{/let}}
{{post.displayName}}
{{if post.isScheduled "will be" "was"}}
{{#if (or post.isPublished (and post.isScheduled post.emailRecipientFilter (not post.email)))}}
{{#if post.emailOnly}}
sent to
{{else}}
published and sent to
{{/if}}
<strong>{{pluralize post.email.emailCount "member"}}</strong>.
{{else}}
published.
{{/if}}
</p>
{{#if (and post.isScheduled post.email)}}
<p>
This post was previously emailed to
<strong>{{pluralize post.email.emailCount "member"}}</strong> on
{{#let (moment-site-tz post.email.createdAtUTC) as |sentAt|}}
<strong>
{{moment-format sentAt "D MMM YYYY"}}
at
{{moment-format sentAt "HH:mm"}}.
</strong>
{{/let}}
</p>
{{/if}}
<div class="gh-publish-cta">
<GhTaskButton
@task={{this.saveTask}}
@buttonText="Save changes"
@runningText="Saving"
@successText="Saved"
@class="gh-btn gh-btn-icon gh-btn-primary gh-btn-large mr4"
/>
<GhTaskButton
@task={{this.revertToDraftTask}}
@buttonText="Revert to draft"
@runningText="Reverting"
@successText="Reverted"
@class="gh-btn gh-btn-icon gh-btn-secondary gh-btn-large"
/>
</div>
</div>
{{/let}}
</div>

View file

@ -0,0 +1,24 @@
import Component from '@glimmer/component';
import {task} from 'ember-concurrency';
export default class UpdateFlowModalComponent extends Component {
static modalOptions = {
className: 'fullscreen-modal-total-overlay',
omitBackdrop: true,
ignoreBackdropClick: true
};
@task
*saveTask() {
yield this.args.data.saveTask.perform();
this.args.close();
return true;
}
@task
*revertToDraftTask() {
yield this.args.data.revertToDraftTask.perform();
this.args.close();
return true;
}
}

View file

@ -1,10 +1,21 @@
<button
type="button"
class="gh-btn gh-btn-editor darkgrey gh-publishmenu-trigger"
{{on "click" this.openPublishFlow}}
{{on-key "cmd+shift+p"}}
disabled={{this.publishOptions.isLoading}}
data-test-button="publish-flow"
>
<span>Publish</span>
</button>
{{#if @post.isDraft}}
<button
type="button"
class="gh-btn gh-btn-editor darkgrey gh-publishmenu-trigger"
{{on "click" this.openPublishFlow}}
{{on-key "cmd+shift+p"}}
disabled={{this.publishOptions.isLoading}}
data-test-button="publish-flow"
>
<span>Publish</span>
</button>
{{else}}
<button
type="button"
class="gh-btn gh-btn-editor darkgrey gh-publishmenu-trigger"
{{on "click" this.openUpdateFlow}}
data-test-button="update-flow"
>
<span>Update</span>
</button>
{{/if}}

View file

@ -2,6 +2,7 @@ import Component from '@glimmer/component';
import EmailFailedError from 'ghost-admin/errors/email-failed-error';
import PublishFlowModal from './modals/publish-flow';
import PublishOptionsResource from 'ghost-admin/helpers/publish-options';
import UpdateFlowModal from './modals/update-flow';
import moment from 'moment';
import {action, get} from '@ember/object';
import {inject as service} from '@ember/service';
@ -29,7 +30,10 @@ export class PublishOptions {
}
get willEmail() {
return this.publishType !== 'publish' && this.recipientFilter;
return this.publishType !== 'publish'
&& this.recipientFilter
&& this.post.isDraft
&& !this.post.email;
}
get willPublish() {
@ -225,8 +229,6 @@ export class PublishOptions {
// saving ------------------------------------------------------------------
revertableModelProperties = ['status', 'publishedAtUTC', 'emailOnly'];
@task({drop: true})
*saveTask() {
this._applyModelChanges();
@ -246,6 +248,26 @@ export class PublishOptions {
}
}
@task({drop: true})
*revertToDraftTask() {
const originalStatus = this.post.status;
const originalPublishedAtUTC = this.post.publishedAtUTC;
try {
if (this.post.isScheduled) {
this.post.publishedAtUTC = null;
}
this.post.status = 'draft';
return yield this.post.save();
} catch (e) {
this.post.status = originalStatus;
this.post.publishedAtUTC = originalPublishedAtUTC;
throw e;
}
}
// Publishing/scheduling is a side-effect of changing model properties.
// We don't want to get into a situation where we've applied these changes
// but they haven't been saved because that would result in confusing UI.
@ -256,21 +278,31 @@ export class PublishOptions {
_applyModelChanges() {
// store backup of original values in case we need to revert
this._originalModelValues = {};
this.revertableModelProperties.forEach((property) => {
// this only applies to the full publish flow which is only available for drafts
if (!this.post.isDraft) {
return;
}
const revertableModelProperties = ['status', 'publishedAtUTC', 'emailOnly'];
revertableModelProperties.forEach((property) => {
this._originalModelValues[property] = this.post[property];
});
this.post.status = this.isScheduled ? 'scheduled' : 'published';
if (this.post.isScheduled) {
if (this.isScheduled) {
this.post.publishedAtUTC = this.scheduledAtUTC;
}
this.post.emailOnly = this.publishType === 'email';
if (this.willEmail) {
this.post.emailOnly = this.publishType === 'email';
}
}
_revertModelChanges() {
this.revertableModelProperties.forEach((property) => {
Object.keys(this._originalModelValues).forEach((property) => {
this.post[property] = this._originalModelValues[property];
});
}
@ -279,8 +311,9 @@ export class PublishOptions {
/* Component -----------------------------------------------------------------*/
// This component exists for the duration of the editor screen being open.
// It's used to store the selected publish options and control the
// publishing flow modal.
// It's used to store the selected publish options, control the publishing flow
// modal display, and provide an editor-specific save behaviour wrapper around
// PublishOptions saving.
export default class PublishManagement extends Component {
@service modals;
@ -288,6 +321,7 @@ export default class PublishManagement extends Component {
@use publishOptions = new PublishOptionsResource(() => [this.args.post]);
publishFlowModal = null;
updateFlowModal = null;
willDestroy() {
super.willDestroy(...arguments);
@ -298,6 +332,8 @@ export default class PublishManagement extends Component {
openPublishFlow(event) {
event?.preventDefault();
this.updateFlowModal?.close();
if (!this.publishFlowModal || this.publishFlowModal.isClosing) {
this.publishOptions.resetPastScheduledAt();
@ -308,8 +344,25 @@ export default class PublishManagement extends Component {
}
}
@action
openUpdateFlow(event) {
event?.preventDefault();
this.publishFlowModal?.close();
if (!this.updateFlowModal || this.updateFlowModal.isClosing) {
this.updateFlowModal = this.modals.open(UpdateFlowModal, {
publishOptions: this.publishOptions,
saveTask: this.saveTask,
revertToDraftTask: this.revertToDraftTask
});
}
}
@task
*saveTask() {
*saveTask({taskName = 'saveTask'} = {}) {
const willEmail = this.publishOptions.willEmail;
// clean up blank editor cards
// apply cloned mobiledoc
// apply scratch values
@ -318,13 +371,13 @@ export default class PublishManagement extends Component {
// apply publish options (with undo on failure)
// save with the required query params for emailing
const result = yield this.publishOptions.saveTask.perform();
const result = yield this.publishOptions[taskName].perform();
// perform any post-save cleanup for the editor
yield this.args.afterSave(result);
// if emailed, wait until it has been submitted so we can show a failure message if needed
if (this.publishOptions.post.email) {
if (willEmail && this.publishOptions.post.email) {
yield this.confirmEmailTask.perform();
}
@ -354,4 +407,9 @@ export default class PublishManagement extends Component {
return true;
}
@task
*revertToDraftTask() {
return yield this.saveTask.perform({taskName: 'revertToDraftTask'});
}
}