0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-17 23:44:39 -05:00

Added "Save" button to editor for scheduled and published posts

refs https://github.com/TryGhost/Team/issues/1597

- added "Save" button to editor for scheduled and published posts when any edits have been made
- shows "Saving..." then "Saved" for 3 seconds before disappearing
  - replaces "Saving..." indicator shown in status bar on the left
- added `showIcon` argument to `<GhTaskButton>` so it can be used for text style buttons
- changed editor status behaviour to only show "Scheduled" by default with the full text shown on hover
This commit is contained in:
Kevin Ansfield 2022-05-10 10:04:14 +01:00
parent 8a303fe411
commit 756f5094b4
8 changed files with 82 additions and 29 deletions

View file

@ -10,6 +10,19 @@
<span>Publish</span>
</button>
{{else}}
{{#if (or @hasUnsavedChanges this.saveButtonTaskGroup.isRunning)}}
<GhTaskButton
@task={{this.saveTask}}
@runningText="Saving..."
@class="gh-btn gh-btn-editor gh-publishmenu-trigger"
@idleClass="green"
@runningClass="midlightgrey"
@successClass="midlightgrey"
@failureClass="red"
@showIcon={{false}}
/>
{{/if}}
<button
type="button"
class="gh-btn gh-btn-editor darkgrey gh-publishmenu-trigger"

View file

@ -3,13 +3,15 @@ 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 envConfig from 'ghost-admin/config/environment';
import moment from 'moment';
import {action, get} from '@ember/object';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
import {task, taskGroup, timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
import {use} from 'ember-could-get-used-to-this';
const SHOW_SAVE_STATUS_DURATION = 3000;
const CONFIRM_EMAIL_POLL_LENGTH = 1000;
const CONFIRM_EMAIL_MAX_POLL_LENGTH = 15 * 1000;
@ -374,7 +376,7 @@ export default class PublishManagement extends Component {
this.publishFlowModal = this.modals.open(PublishFlowModal, {
publishOptions: this.publishOptions,
saveTask: this.saveTask
saveTask: this.publishTask
});
}
}
@ -388,28 +390,28 @@ export default class PublishManagement extends Component {
if (!this.updateFlowModal || this.updateFlowModal.isClosing) {
this.updateFlowModal = this.modals.open(UpdateFlowModal, {
publishOptions: this.publishOptions,
saveTask: this.saveTask,
saveTask: this.publishTask,
revertToDraftTask: this.revertToDraftTask
});
}
}
@task
*saveTask({taskName = 'saveTask'} = {}) {
*publishTask({taskName = 'saveTask'} = {}) {
const willEmail = this.publishOptions.willEmail;
// clean up blank editor cards
// apply cloned mobiledoc
// apply scratch values
// generate slug if needed (should never happen - publish flow can't be opened on new posts)
yield this.args.beforeSave();
yield this.args.beforePublish();
// apply publish options (with undo on failure)
// save with the required query params for emailing
const result = yield this.publishOptions[taskName].perform();
// perform any post-save cleanup for the editor
yield this.args.afterSave(result);
yield this.args.afterPublish(result);
// if emailed, wait until it has been submitted so we can show a failure message if needed
if (willEmail && this.publishOptions.post.email) {
@ -419,6 +421,21 @@ export default class PublishManagement extends Component {
return result;
}
// used by the non-publish "Save" button shown for scheduled/published posts
@task({group: 'saveButtonTaskGroup'})
*saveTask() {
yield this.args.saveTask.perform();
this.saveButtonTimeoutTask.perform();
return true;
}
@task({group: 'saveButtonTaskGroup'})
*saveButtonTimeoutTask() {
yield timeout(envConfig.environment === 'test' ? 1 : SHOW_SAVE_STATUS_DURATION);
}
@taskGroup saveButtonTaskGroup;
@task
*confirmEmailTask() {
const post = this.publishOptions.post;

View file

@ -1,13 +1,16 @@
<div data-test-editor-post-status ...attributes>
{{#if this.isSaving}}
<div role="tooltip" {{on "mouseover" this.onMouseover}} {{on "mouseleave" this.onMouseleave}} data-test-editor-post-status ...attributes>
{{#if (and this.isSaving @post.isDraft)}}
Saving...
{{else if @post.isSent}}
Sent to {{gh-pluralize @post.email.emailCount "member"}}
{{else if (and @post.emailOnly @post.isScheduled)}}
<time datetime="{{@post.publishedAtUTC}}" class="ml1 green f8" data-test-schedule-countdown>
Will be sent to <GhRecipientFilterCount @filter={{@post.emailRecipientFilter}} />
{{this.scheduledTime}}
</time>
Scheduled
{{#if this.isHovered}}
<time datetime="{{@post.publishedAtUTC}}" class="ml1 green f8" data-test-schedule-countdown>
to be sent to <GhRecipientFilterCount @filter={{@post.emailRecipientFilter}} />
{{this.scheduledTime}}
</time>
{{/if}}
{{else if (or @post.isPublished @post.pastScheduledTime)}}
Published
{{#if (or (eq @post.email.status "submitting") (eq @post.email.status "submitting"))}}
@ -17,11 +20,14 @@
{{/if}}
{{else if @post.isScheduled}}
<time datetime="{{@post.publishedAtUTC}}" class="ml1 green f8" data-test-schedule-countdown>
Will be published
{{#if (and @post.emailRecipientFilter (not @post.email))}}
and sent to <GhRecipientFilterCount @filter={{@post.emailRecipientFilter}} />
Scheduled
{{#if this.isHovered}}
to be published
{{#if (and @post.emailRecipientFilter (not @post.email))}}
and sent to <GhRecipientFilterCount @filter={{@post.emailRecipientFilter}} />
{{/if}}
{{this.scheduledTime}}
{{/if}}
{{this.scheduledTime}}
</time>
{{else if @post.isNew}}
New

View file

@ -1,7 +1,7 @@
import Component from '@glimmer/component';
import config from 'ghost-admin/config/environment';
import {action, get} from '@ember/object';
import {formatPostTime} from 'ghost-admin/helpers/gh-format-post-time';
import {get} from '@ember/object';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
@ -10,6 +10,8 @@ export default class GhEditorPostStatusComponent extends Component {
@service clock;
@service settings;
@tracked isHovered = false;
@tracked _isSaving = false;
// this.args.isSaving will only be true briefly whilst the post is saving,
@ -34,6 +36,16 @@ export default class GhEditorPostStatusComponent extends Component {
);
}
@action
onMouseover() {
this.isHovered = true;
}
@action
onMouseleave() {
this.isHovered = false;
}
@task({drop: true})
*showSavingMessage() {
this._isSaving = true;

View file

@ -6,8 +6,8 @@
isFailure=this.isFailure
)}}
{{else}}
{{#if this.isRunning}}<span data-test-task-button-state="running">{{svg-jar "spinner" class="gh-icon-spinner"}}{{this.runningText}}</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 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">{{svg-jar "check-circle"}} {{this.successText}}</span>{{/if}}
{{#if this.isFailure}}<span data-test-task-button-state="failure">{{svg-jar "retry"}} {{this.failureText}}</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

@ -34,6 +34,7 @@ const GhTaskButton = Component.extend({
buttonText: 'Save',
idleClass: '',
runningClass: '',
showIcon: true,
showSuccess: true, // set to false if you want the spinner to show until a transition occurs
autoReset: true, // set to false if you want don't want task button to reset after timeout
successText: 'Saved',

View file

@ -48,8 +48,10 @@
{{#if (feature "publishingFlow")}}
<EditorLabs::PublishManagement
@post={{this.post}}
@beforeSave={{perform this.beforeSaveTask}}
@afterSave={{this.afterSave}}
@hasUnsavedChanges={{this.hasDirtyAttributes}}
@beforePublish={{perform this.beforeSaveTask}}
@afterPublish={{this.afterSave}}
@saveTask={{this.saveTask}}
/>
{{else}}
<GhPublishmenu

View file

@ -361,8 +361,9 @@ describe('Acceptance: Editor', function () {
).to.not.exist;
// expect countdown to show warning that post is scheduled to be published
await triggerEvent('[data-test-editor-post-status]', 'mouseover');
expect(find('[data-test-schedule-countdown]').textContent.trim(), 'notification countdown')
.to.match(/Will be published\s+in (4|5) minutes/);
.to.match(/to be published\s+in (4|5) minutes/);
expect(
find('[data-test-publishmenu-trigger]').textContent.trim(),
@ -372,7 +373,7 @@ describe('Acceptance: Editor', function () {
expect(
find('[data-test-editor-post-status]').textContent.trim(),
'scheduled post status'
).to.match(/Will be published\s+in (4|5) minutes/);
).to.match(/to be published\s+in (4|5) minutes/);
// Re-schedule
await click('[data-test-publishmenu-trigger]');
@ -399,7 +400,7 @@ describe('Acceptance: Editor', function () {
expect(
find('[data-test-editor-post-status]').textContent.trim(),
'scheduled status text'
).to.match(/Will be published\s+in (4|5) minutes/);
).to.match(/to be published\s+in (4|5) minutes/);
// unschedule
await click('[data-test-publishmenu-trigger]');
@ -556,8 +557,9 @@ describe('Acceptance: Editor', function () {
expect(find('[data-test-publishmenu-trigger]').textContent.trim(), 'text in save button for scheduled post')
.to.equal('Scheduled');
// expect countdown to show warning, that post is scheduled to be published
await triggerEvent('[data-test-editor-post-status]', 'mouseover');
expect(find('[data-test-schedule-countdown]').textContent.trim(), 'notification countdown')
.to.match(/Will be published\s+in (4|5) minutes/);
.to.match(/to be published\s+in (4|5) minutes/);
});
it('shows author token input and allows changing of authors in PSM', async function () {
@ -891,11 +893,11 @@ describe('Acceptance: Editor', function () {
await selectChoose('[data-test-distribution-action-select]', 'send');
await click('[data-test-publishmenu-scheduled-option]');
await datepickerSelect('[data-test-publishmenu-draft] [data-test-date-time-picker-datepicker]', new Date(scheduledTime.format().replace(/\+.*$/, '')));
// Expect 4 free and 2 paid recipients here
// Expect 4 free and 2 paid recipients here
expect(find('[data-test-email-count="free-members"]')).to.contain.text('4');
expect(find('[data-test-email-count="paid-members"]')).to.contain.text('2');
await click('[data-test-publishmenu-save]');
await click('[data-test-button="confirm-schedule"]');