Switched publish flow dropdowns to expanding blocks and added publish time options

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

- moved publish-flow modal into `components/editor-labs/modals/publish-flow` as we have enough editor-related components to keep them in one place
- updated publish flow design to use expanding blocks in place of dropdowns
  - added `openSection` property in publish-flow modal and associated action for toggling sections
  - moved "publish at" option into a separate component to keep publish-flow modal cleaner (keeps option-specific actions out of the main modal component)
  - used `{{liquid-if}}` to animate the expanding blocks
- added schedule time properties to `PublishOptions`
  - kept "is scheduled" and "scheduled at" separate so it's possible to keep the selected schedule time across selecting/deselecting the option to schedule
  - ensures schedule date is kept to the minimum 5-minute in the future across option changes
  - updated publish-management to reset the scheduled option to "Right now" when the publish-flow modal is opened if a schedule time was previously set but is now in the past
This commit is contained in:
Kevin Ansfield 2022-04-27 18:20:46 +01:00
parent cc8dc03485
commit 75395a0eb2
12 changed files with 262 additions and 105 deletions

View File

@ -0,0 +1,75 @@
<div class="flex flex-column h-100 items-center overflow-auto">
<header class="gh-publish-header" data-test-modal="publish">
<button class="gh-publish-back-button" title="Close" type="button" {{on "click" @close}}>
<span>{{svg-jar "close-stroke"}} Close</span>
</button>
</header>
<div class="gh-publish-settings-container">
<div class="gh-publish-title"><span>Another masterpiece.</span> Ready to share it with the world?</div>
<div class="gh-publish-settings">
<div class="gh-publish-setting">
<div class="gh-publish-setting-title">
{{svg-jar "send-email"}}
{{#if @data.publishOptions.emailUnavailable}}
<span>Publish on site</span>
{{else}}
<button type="button" class="gh-publish-setting-trigger" {{on "click" (fn this.toggleSection "publishType")}}>
<span>{{@data.publishOptions.selectedPublishTypeOption.display}}</span>
</button>
{{/if}}
</div>
{{#liquid-if (eq this.openSection "publishType")}}
<div class="gh-publish-setting-form">
<EditorLabs::PublishOptions::PublishType
@publishOptions={{@data.publishOptions}}
/>
</div>
{{/liquid-if}}
</div>
{{#if (not-eq @data.publishOptions.publishType "publish")}}
<div class="gh-publish-setting">
<div class="gh-publish-setting-title">
{{svg-jar "member"}}
<div class="gh-publish-setting-trigger">
235
{{#unless @data.publishOptions.onlyDefaultNewsletter}}
<span>
Charts of the Week
</span>
{{/unless}}
subscribers
</div>
</div>
</div>
{{/if}}
<div class="gh-publish-setting">
<div class="gh-publish-setting-title">
{{svg-jar "clock"}}
<button type="button" class="gh-publish-setting-trigger" {{on "click" (fn this.toggleSection "publishAt")}}>
<span>
{{#if @data.publishOptions.isScheduled}}
{{capitalize (gh-format-post-time @data.publishOptions.scheduledAtUTC draft=true)}}
{{else}}
Right now
{{/if}}
</span>
</button>
</div>
{{#liquid-if (eq this.openSection "publishAt")}}
<EditorLabs::PublishOptions::PublishAt
@publishOptions={{@data.publishOptions}}
/>
{{/liquid-if}}
</div>
</div>
<div class="gh-publish-cta">
<button type="button" class="gh-btn gh-btn-black gh-btn-large" {{on "click" (noop)}}><span>Continue &rarr;</span></button>
<button type="button" class="gh-btn gh-btn-link gh-btn-large" {{on "click" @close}}><span>Back to edit</span></button>
</div>
</div>
</div>

View File

@ -1,5 +1,6 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {tracked} from '@glimmer/tracking';
export default class PublishModalComponent extends Component {
static modalOptions = {
@ -8,10 +9,14 @@ export default class PublishModalComponent extends Component {
ignoreBackdropClick: true
};
@action
publishTypeChanged(event) {
event.preventDefault();
@tracked openSection = null;
this.args.data.publishOptions.setPublishType(event.target.value);
@action
toggleSection(section) {
if (section === this.openSection) {
this.openSection = null;
} else {
this.openSection = section;
}
}
}

View File

@ -1,6 +1,7 @@
import Component from '@glimmer/component';
import PublishFlowModal from '../modals/editor-labs/publish-flow';
import PublishFlowModal from './modals/publish-flow';
import PublishOptionsResource from 'ghost-admin/helpers/publish-options';
import moment from 'moment';
import {action, get} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
@ -13,7 +14,7 @@ export class PublishOptions {
settings = null;
store = null;
// passed in objects
// passed in models
post = null;
user = null;
@ -21,6 +22,45 @@ export class PublishOptions {
return this.setupTask.isRunning;
}
// publish date ------------------------------------------------------------
@tracked isScheduled = false;
@tracked scheduledAtUTC = this.minScheduledAt;
get minScheduledAt() {
return moment.utc().add(5, 'minutes');
}
@action
toggleScheduled(shouldSchedule) {
if (shouldSchedule === undefined) {
shouldSchedule = !this.isScheduled;
}
this.isScheduled = shouldSchedule;
if (shouldSchedule && (!this.scheduledAtUTC || this.scheduledAtUTC.isBefore(this.minScheduledAt))) {
this.scheduledAtUTC = this.minScheduledAt;
}
}
@action
setScheduledAt(date) {
if (moment.utc(date).isBefore(this.minScheduledAt)) {
return;
}
this.scheduledAtUTC = moment.utc(date);
}
@action
resetPastScheduledAt() {
if (this.scheduledAtUTC.isBefore(this.minScheduledAt)) {
this.isScheduled = false;
this.scheduledAt = null;
}
}
// publish type ------------------------------------------------------------
@tracked publishType = 'publish+send';
@ -67,15 +107,11 @@ export class PublishOptions {
}
@action
setPublishType(publishType) {
// TODO: validate publish type is allowed
this.publishType = publishType;
setPublishType(newValue) {
// TODO: validate option is allowed when setting?
this.publishType = newValue;
}
// publish date ------------------------------------------------------------
@tracked publishDate = 'now';
// newsletter --------------------------------------------------------------
newsletters = []; // set in constructor
@ -153,6 +189,8 @@ export default class PublishManagement extends Component {
event?.preventDefault();
if (!this.publishFlowModal || this.publishFlowModal.isClosing) {
this.publishOptions.resetPastScheduledAt();
this.publishFlowModal = this.modals.open(PublishFlowModal, {
publishOptions: this.publishOptions
});

View File

@ -0,0 +1,25 @@
{{!-- template-lint-disable no-invalid-interactive --}}
<fieldset>
<div class="gh-publishmenu-radio {{unless @publishOptions.isScheduled "active"}}" {{on "click" (fn @publishOptions.toggleScheduled false)}}>
<div class="gh-publishmenu-radio-button" data-test-publishmenu-published-option></div>
<div class="gh-publishmenu-radio-content">
<div class="gh-publishmenu-radio-label">Set it live now</div>
</div>
</div>
<div class="gh-publishmenu-radio {{if @publishOptions.isScheduled "active"}}" {{on "click" (fn @publishOptions.toggleScheduled true)}}>
<div class="gh-publishmenu-radio-button" data-test-publishmenu-scheduled-option></div>
<div class="gh-publishmenu-radio-content">
<div class="gh-publishmenu-radio-label">Schedule it for later</div>
<GhDateTimePicker
@date={{moment-format (moment-site-tz @publishOptions.scheduledAtUTC) "YYYY-MM-DD"}}
@time={{moment-format (moment-site-tz @publishOptions.scheduledAtUTC) "HH:mm"}}
@setDate={{this.setDate}}
@setTime={{this.setTime}}
@minDate={{@publishOptions.minScheduledAt}}
@isActive={{@publishOptions.isScheduled}}
@renderInPlace={{false}}
/>
<div class="gh-publishmenu-radio-desc">Set automatic future publish date</div>
</div>
</div>
</fieldset>

View File

@ -0,0 +1,47 @@
import Component from '@glimmer/component';
import moment from 'moment';
import {action} from '@ember/object';
export default class PublishAtOption extends Component {
@action
setDate(selectedDate) {
const newDate = moment(this.args.publishOptions.scheduledAtUTC);
const {year, month, date} = moment(selectedDate).toObject();
newDate.set({year, month, date});
this.args.publishOptions.setScheduledAt(newDate);
}
@action
setTime(time) {
if (!time) {
return;
}
if (time.match(/^\d:\d\d$/)) {
time = `0${time}`;
}
if (!time.match(/^\d\d:\d\d$/)) {
return;
}
const [hour, minute] = time.split(':').map(n => parseInt(n, 10));
if (isNaN(hour) || hour < 0 || hour > 23 || isNaN(minute) || minute < 0 || minute > 59) {
return;
}
// hour/minute will be the site timezone equivalent but we need the hour/minute
// as it would be in UTC
const conversionDate = moment();
conversionDate.set({hour, minute});
const utcDate = moment.utc(conversionDate);
const newDate = moment.utc(this.args.publishOptions.scheduledAtUTC);
newDate.set({hour: utcDate.get('hour'), minute: utcDate.get('minute')});
this.args.publishOptions.setScheduledAt(newDate);
}
}

View File

@ -0,0 +1,16 @@
<fieldset>
{{#each @publishOptions.publishTypeOptions as |option|}}
<div class="radio-button">
<input
type="radio"
name="publish-type"
id="publish-type-{{option.value}}"
value={{option.value}}
checked={{eq option.value @publishOptions.selectedPublishTypeOption.value}}
disabled={{option.disabled}}
{{on "change" this.onChange}}
>
<label for="publish-type-{{option.value}}">{{option.label}}</label>
</div>
{{/each}}
</fieldset>

View File

@ -0,0 +1,10 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
export default class PublishTypeOption extends Component {
@action
onChange(event) {
event.preventDefault();
this.args.publishOptions.setPublishType(event.target.value);
}
}

View File

@ -3,7 +3,7 @@
@selected={{this._date}}
@center={{this._date}}
@onSelect={{action "setDateInternal" value="date"}}
@renderInPlace={{true}}
@renderInPlace={{this.renderInPlaceWithFallback}}
@disabled={{this.disabled}} as |dp|
>
<dp.Trigger @tabindex="-1" data-test-date-time-picker-datepicker>

View File

@ -31,6 +31,10 @@ export default class GhDateTimePicker extends Component {
// actions
setTypedDateError() {}
get renderInPlaceWithFallback() {
return this.renderInPlace === undefined ? true : this.renderInPlace;
}
@reads('settings.timezone')
blogTimezone;

View File

@ -1,76 +0,0 @@
<div class="flex flex-column h-100 items-center">
<header class="gh-publish-header" data-test-modal="publish">
<button class="gh-publish-back-button" title="Close" type="button" {{on "click" @close}}>
<span>{{svg-jar "close-stroke"}} Close</span>
</button>
</header>
<div class="gh-publish-settings-container">
<div class="gh-publish-title"><span>Another masterpiece.</span> Ready to share it with the world?</div>
<div class="gh-publish-settings">
<div class="gh-publish-setting">
{{svg-jar "send-email"}}
{{#if @data.publishOptions.emailUnavailable}}
publish
{{else}}
<GhBasicDropdown
class="gh-publish-setting-trigger"
@verticalPosition="below"
@horizintalPosition="right"
@renderInPlace={{true}}
as |dd|
>
<dd.Trigger>
{{@data.publishOptions.selectedPublishTypeOption.display}}
</dd.Trigger>
<dd.Content class="gh-publish-setting-dropdown">
<fieldset>
{{#each @data.publishOptions.publishTypeOptions as |option|}}
<div class="radio-button">
<input
type="radio"
name="publish-type"
id="publish-type-{{option.value}}"
value={{option.value}}
checked={{eq option.value @data.publishOptions.publishType}}
disabled={{option.disabled}}
{{on "change" this.publishTypeChanged}}
>
<label for="publish-type-{{option.value}}">{{option.label}}</label>
</div>
{{/each}}
</fieldset>
</dd.Content>
</GhBasicDropdown>
{{/if}}
</div>
{{#if (not-eq @data.publishOptions.publishType "publish")}}
<div class="gh-publish-setting">
{{svg-jar "member"}}
<div class="gh-publish-setting-trigger">
235
{{#unless @data.publishOptions.onlyDefaultNewsletter}}
<span class="gh-publish-setting-trigger">
Charts of the Week
</span>
{{/unless}}
subscribers
</div>
</div>
{{/if}}
<div class="gh-publish-setting">
{{svg-jar "clock"}}
<div class="gh-publish-setting-trigger">Right now</div>
</div>
</div>
<div class="gh-publish-cta">
<button type="button" class="gh-btn gh-btn-black gh-btn-large" {{on "click" (noop)}}><span>Continue &rarr;</span></button>
<button type="button" class="gh-btn gh-btn-link gh-btn-large" {{on "click" @close}}><span>Back to edit</span></button>
</div>
</div>
</div>

View File

@ -220,6 +220,7 @@
.ember-power-datepicker-content {
min-width: 212px;
padding: 12px;
z-index: 99999;
}
.ember-power-datepicker-trigger:focus {

View File

@ -462,6 +462,7 @@
width: 640px;
margin: 0 auto 8rem;
padding-top: 11vw;
margin-bottom: 11vw;
}
.gh-publish-title {
@ -485,17 +486,22 @@
.gh-publish-setting {
display: flex;
align-items: center;
flex-direction: column;
margin-bottom: 1.6rem;
}
.gh-publish-setting svg {
.gh-publish-setting-title {
display: flex;
align-items: center;
}
.gh-publish-setting-title svg {
width: 2rem;
height: 2rem;
margin-right: 1.6rem;
}
.gh-publish-setting svg path {
.gh-publish-setting-title svg path {
stroke: var(--black);
stroke-width: 2px;
}
@ -510,7 +516,7 @@
cursor: pointer;
}
.gh-publish-setting span {
.gh-publish-setting-title span {
border-bottom: 0;
font-weight: 700;
}
@ -519,19 +525,25 @@
color: var(--midlightgrey);
}
.gh-publish-setting-dropdown {
top: 46px;
min-width: 218px;
padding: 4px 16px;
font-size: 1.4rem;
font-weight: 500;
background: var(--white);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow-m);
.gh-publish-setting fieldset {
margin: 0;
margin-top: 1.6rem;
background-color: var(--whitegrey);
}
.gh-publish-setting-dropdown fieldset {
margin-bottom: 0;
.gh-publish-setting .radio-button {
display: flex;
flex-direction: row;
align-items: center;
column-gap: 5px;
padding: 10px;
border-bottom: 1px solid var(--white);
}
.gh-publish-setting .gh-publishmenu-radio {
margin: 0;
padding: 20px 10px;
border-bottom: 1px solid var(--white);
}
.gh-publish-cta {