Skip to content
This repository has been archived by the owner on Nov 28, 2022. It is now read-only.

Commit

Permalink
✨ new publish menu and date/time picker (#588)
Browse files Browse the repository at this point in the history
closes TryGhost/Ghost#8249

- replaces the old split-button publish/schedule/update button with a less confusing menu system
- adds a `{{gh-date-time-picker}}` component that contains a datepicker with separate time input
- replaces the date text input in the post settings menu with `{{gh-date-time-picker}}`
  - disabled when post is scheduled, only way to update a scheduled time is via the publish menu
  - validates date is in the past when draft/published so there's no confusion with scheduling
- displays saving status in top-left of editor screen
- refactor editor (auto)saving processes to use ember-concurrency

Other minor changes:
- adds `post.publishedAtBlog{TZ,Date,Time}` properties to Post model to allow working with `publishedAt` datetime in the selected blog timezone rather than UTC
- adds a `beforeSave` hook to `validation-engine` that is called after successful validation and before the Ember Data save call is made
- adds validation of `publishedAtBlog{Date,Time}` to post validator
- prevent gh-task-button showing last task state on first render
- fixes bug where clicking into and out of the published date input in the PSM without making any changes saves a published date for draft posts
  • Loading branch information
kevinansfield authored and ErisDS committed Apr 11, 2017
1 parent 0a09c24 commit 34cb65d
Show file tree
Hide file tree
Showing 48 changed files with 1,628 additions and 894 deletions.
111 changes: 111 additions & 0 deletions app/components/gh-date-time-picker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import Component from 'ember-component';
import computed, {reads, or} from 'ember-computed';
import injectService from 'ember-service/inject';
import moment from 'moment';
import {isBlank, isEmpty} from 'ember-utils';

export default Component.extend({
settings: injectService(),

tagName: '',

date: '',
time: '',
errors: null,
dateErrorProperty: null,
timeErrorProperty: null,

_time: '',
_previousTime: '',
_minDate: null,
_maxDate: null,

blogTimezone: reads('settings.activeTimezone'),
hasError: or('dateError', 'timeError'),

timezone: computed('blogTimezone', function () {
let blogTimezone = this.get('blogTimezone');
return moment.utc().tz(blogTimezone).format('z');
}),

dateError: computed('errors.[]', 'dateErrorProperty', function () {
let errors = this.get('errors');
let property = this.get('dateErrorProperty');

if (!isEmpty(errors.errorsFor(property))) {
return errors.errorsFor(property).get('firstObject').message;
}
}),

timeError: computed('errors.[]', 'timeErrorProperty', function () {
let errors = this.get('errors');
let property = this.get('timeErrorProperty');

if (!isEmpty(errors.errorsFor(property))) {
return errors.errorsFor(property).get('firstObject').message;
}
}),

didReceiveAttrs() {
let date = this.get('date');
let time = this.get('time');
let minDate = this.get('minDate');
let maxDate = this.get('maxDate');
let blogTimezone = this.get('blogTimezone');

if (!isBlank(date)) {
this.set('_date', moment(date));
} else {
this.set('_date', moment().tz(blogTimezone));
}

if (isBlank(time)) {
this.set('_time', this.get('_date').format('HH:mm'));
} else {
this.set('_time', this.get('time'));
}
this.set('_previousTime', this.get('_time'));

// unless min/max date is at midnight moment will diable that day
if (minDate === 'now') {
this.set('_minDate', moment(moment().format('YYYY-MM-DD')));
} else if (!isBlank(minDate)) {
this.set('_minDate', moment(moment(minDate).format('YYYY-MM-DD')));
} else {
this.set('_minDate', null);
}

if (maxDate === 'now') {
this.set('_maxDate', moment(moment().format('YYYY-MM-DD')));
} else if (!isBlank(maxDate)) {
this.set('_maxDate', moment(moment(maxDate).format('YYYY-MM-DD')));
} else {
this.set('_maxDate', null);
}
},

actions: {
// if date or time is set and the other property is blank set that too
// so that we don't get "can't be blank" errors
setDate(date) {
if (date !== this.get('_date')) {
this.get('setDate')(date);

if (isBlank(this.get('time'))) {
this.get('setTime')(this.get('_time'));
}
}
},

setTime(time) {
if (time !== this.get('_previousTime')) {
this.get('setTime')(time);
this.set('_previousTime', time);

if (isBlank(this.get('date'))) {
this.get('setDate')(this.get('_date'));
}
}
}
}
});
6 changes: 5 additions & 1 deletion app/components/gh-dropdown-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import DropdownMixin from 'ghost-admin/mixins/dropdown-mixin';

export default Component.extend(DropdownMixin, {
tagName: 'button',
attributeBindings: 'role',
attributeBindings: ['href', 'role'],
role: 'button',

// matches with the dropdown this button toggles
Expand All @@ -16,5 +16,9 @@ export default Component.extend(DropdownMixin, {
click(event) {
this._super(event);
this.get('dropdown').toggleDropdown(this.get('dropdownName'), this);

if (this.get('tagName') === 'a') {
return false;
}
}
});
38 changes: 38 additions & 0 deletions app/components/gh-editor-post-status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Component from 'ember-component';
import {task, timeout} from 'ember-concurrency';
import computed, {reads} from 'ember-computed';

// TODO: reduce when in testing mode
const SAVE_TIMEOUT_MS = 3000;

export default Component.extend({
post: null,
isScheduled: reads('post.isScheduled'),
isSaving: false,

'data-test-editor-post-status': true,

_isSaving: false,

isPublished: computed('post.{isPublished,pastScheduledTime}', function () {
let isPublished = this.get('post.isPublished');
let pastScheduledTime = this.get('post.pastScheduledTime');

return isPublished || pastScheduledTime;
}),

// isSaving will only be true briefly whilst the post is saving,
// we want to ensure that the "Saving..." message is shown for at least
// a few seconds so that it's noticeable
didReceiveAttrs() {
if (this.get('isSaving')) {
this.get('showSavingMessage').perform();
}
},

showSavingMessage: task(function* () {
this.set('_isSaving', true);
yield timeout(SAVE_TIMEOUT_MS);
this.set('_isSaving', false);
}).drop()
});
106 changes: 0 additions & 106 deletions app/components/gh-editor-save-button.js

This file was deleted.

96 changes: 18 additions & 78 deletions app/components/gh-post-settings-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {guidFor} from 'ember-metal/utils';
import injectService from 'ember-service/inject';
import {htmlSafe} from 'ember-string';
import {invokeAction} from 'ember-invoke-action';
import {parseDateString} from 'ghost-admin/utils/date-formatting';
import SettingsMenuMixin from 'ghost-admin/mixins/settings-menu-component';
import boundOneWay from 'ghost-admin/utils/bound-one-way';
import isNumber from 'ghost-admin/utils/isNumber';
Expand Down Expand Up @@ -211,90 +210,31 @@ export default Component.extend(SettingsMenuMixin, {
});
},

/**
* Parse user's set published date.
* Action sent by post settings menu view.
* (#1351)
*/
setPublishedAtUTC(userInput) {
if (!userInput) {
// Clear out the publishedAtUTC field for a draft
if (this.get('model.isDraft')) {
this.set('model.publishedAtUTC', null);
}
return;
}

// The user inputs a date which he expects to be in his timezone. Therefore, from now on
// we have to work with the timezone offset which we get from the settings Service.
let blogTimezone = this.get('settings.activeTimezone');
let newPublishedAt = parseDateString(userInput, blogTimezone);
let publishedAtUTC = moment.utc(this.get('model.publishedAtUTC'));
let errMessage = '';
let newPublishedAtUTC;

// Clear previous errors
this.get('model.errors').remove('post-setting-date');

// Validate new Published date
if (!newPublishedAt.isValid()) {
errMessage = 'Published Date must be a valid date with format: '
+ 'DD MMM YY @ HH:mm (e.g. 6 Dec 14 @ 15:00)';
}
setPublishedAtBlogDate(date) {
let post = this.get('model');
let dateString = moment(date).format('YYYY-MM-DD');

// Date is a valid date, so now make it UTC
newPublishedAtUTC = moment.utc(newPublishedAt);

if (newPublishedAtUTC.diff(moment.utc(new Date()), 'hours', true) > 0) {

// We have to check that the time from now is not shorter than 2 minutes,
// otherwise we'll have issues with the serverside scheduling procedure
if (newPublishedAtUTC.diff(moment.utc(new Date()), 'minutes', true) < 2) {
errMessage = 'Must be at least 2 minutes from now.';
} else {
// in case the post is already published and the user sets the date
// afterwards to a future time, we stop here, and he has to unpublish
// his post first
if (this.get('model.isPublished')) {
errMessage = 'Your post is already published.';
// this is the indicator for the different save button layout
this.set('timeScheduled', false);
}
// everything fine, we can start the schedule workflow and change
// the save buttons according to it
this.set('timeScheduled', true);
}
// if the post is already scheduled and the user changes the date back into the
// past, we'll set the status of the post back to draft, so he can start all over
// again
} else if (this.get('model.isScheduled')) {
this.set('model.status', 'draft');
}
post.get('errors').remove('publishedAtBlogDate');

// If errors, notify and exit.
if (errMessage) {
this.get('model.errors').add('post-setting-date', errMessage);
return;
if (post.get('isNew') || date === post.get('publishedAtBlogDate')) {
post.validate({property: 'publishedAtBlog'});
} else {
post.set('publishedAtBlogDate', dateString);
return post.save();
}
},

// Do nothing if the user didn't actually change the date
if (publishedAtUTC && publishedAtUTC.isSame(newPublishedAtUTC)) {
return;
}
setPublishedAtBlogTime(time) {
let post = this.get('model');

// Validation complete
this.set('model.publishedAtUTC', newPublishedAtUTC);
post.get('errors').remove('publishedAtBlogDate');

// If this is a new post. Don't save the model. Defer the save
// to the user pressing the save button
if (this.get('model.isNew')) {
return;
if (post.get('isNew') || time === post.get('publishedAtBlogTime')) {
post.validate({property: 'publishedAtBlog'});
} else {
post.set('publishedAtBlogTime', time);
return post.save();
}

this.get('model').save().catch((error) => {
this.showError(error);
this.get('model').rollbackAttributes();
});
},

setMetaTitle(metaTitle) {
Expand Down
Loading

0 comments on commit 34cb65d

Please sign in to comment.