2015-05-24 07:47:23 +02:00
|
|
|
import Ember from 'ember';
|
2017-08-22 09:53:26 +02:00
|
|
|
import Mixin from '@ember/object/mixin';
|
2017-05-29 20:50:03 +02:00
|
|
|
import PostModel from 'ghost-admin/models/post';
|
|
|
|
import boundOneWay from 'ghost-admin/utils/bound-one-way';
|
|
|
|
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
2017-06-13 17:04:09 +02:00
|
|
|
import isNumber from 'ghost-admin/utils/isNumber';
|
2017-05-29 20:50:03 +02:00
|
|
|
import moment from 'moment';
|
2017-08-22 09:53:26 +02:00
|
|
|
import {computed} from '@ember/object';
|
|
|
|
import {htmlSafe} from '@ember/string';
|
|
|
|
import {inject as injectController} from '@ember/controller';
|
|
|
|
import {inject as injectService} from '@ember/service';
|
|
|
|
import {isBlank} from '@ember/utils';
|
|
|
|
import {isArray as isEmberArray} from '@ember/array';
|
2016-09-26 18:59:04 +02:00
|
|
|
import {isInvalidError} from 'ember-ajax/errors';
|
2017-05-29 20:50:03 +02:00
|
|
|
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
|
2017-08-22 09:53:26 +02:00
|
|
|
import {mapBy, reads} from '@ember/object/computed';
|
2017-06-13 17:04:09 +02:00
|
|
|
import {task, taskGroup, timeout} from 'ember-concurrency';
|
2014-06-08 08:02:21 +02:00
|
|
|
|
2017-03-28 21:41:25 +02:00
|
|
|
// ember-cli-shims doesn't export Ember.testing
|
|
|
|
const {testing} = Ember;
|
|
|
|
|
2014-06-06 03:18:03 +02:00
|
|
|
// this array will hold properties we need to watch
|
2015-09-03 13:06:50 +02:00
|
|
|
// to know if the model has been changed (`controller.hasDirtyAttributes`)
|
2015-10-28 12:36:45 +01:00
|
|
|
const watchedProps = ['model.scratch', 'model.titleScratch', 'model.hasDirtyAttributes', 'model.tags.[]'];
|
2014-06-06 03:18:03 +02:00
|
|
|
|
2017-07-10 13:33:05 +02:00
|
|
|
const DEFAULT_TITLE = '(Untitled)';
|
2017-03-28 21:41:25 +02:00
|
|
|
|
2017-07-10 17:09:50 +02:00
|
|
|
// time in ms to save after last content edit
|
|
|
|
const AUTOSAVE_TIMEOUT = 3000;
|
|
|
|
// time in ms to force a save if the user is continuously typing
|
|
|
|
const TIMEDSAVE_TIMEOUT = 60000;
|
|
|
|
|
2014-10-14 19:29:54 +02:00
|
|
|
PostModel.eachAttribute(function (name) {
|
2015-10-28 12:36:45 +01:00
|
|
|
watchedProps.push(`model.${name}`);
|
2014-06-06 03:18:03 +02:00
|
|
|
});
|
|
|
|
|
2015-10-28 12:36:45 +01:00
|
|
|
export default Mixin.create({
|
2014-12-30 03:11:24 +01:00
|
|
|
|
2015-11-18 11:50:48 +01:00
|
|
|
showLeaveEditorModal: false,
|
|
|
|
showReAuthenticateModal: false,
|
2017-04-19 11:46:42 +02:00
|
|
|
showDeletePostModal: false,
|
2017-07-22 01:11:24 +02:00
|
|
|
shouldFocusEditor: true,
|
2015-11-18 11:50:48 +01:00
|
|
|
|
2017-03-10 15:30:01 +01:00
|
|
|
application: injectController(),
|
2016-06-30 12:21:47 +02:00
|
|
|
notifications: injectService(),
|
|
|
|
clock: injectService(),
|
2016-08-11 08:58:38 +02:00
|
|
|
slugGenerator: injectService(),
|
2017-08-14 14:30:00 +02:00
|
|
|
ui: injectService(),
|
2015-05-26 04:10:50 +02:00
|
|
|
|
2017-04-25 13:32:27 +02:00
|
|
|
wordcount: 0,
|
2016-10-24 12:55:55 +02:00
|
|
|
cards: [], // for apps
|
|
|
|
atoms: [], // for apps
|
|
|
|
toolbar: [], // for apps
|
|
|
|
apiRoot: ghostPaths().apiRoot,
|
|
|
|
assetPath: ghostPaths().assetRoot,
|
2017-04-07 22:05:43 +02:00
|
|
|
editor: null,
|
2017-04-10 11:10:53 +02:00
|
|
|
editorMenuIsOpen: false,
|
2017-04-19 15:20:42 +02:00
|
|
|
|
|
|
|
navIsClosed: reads('application.autoNav'),
|
|
|
|
|
2015-10-28 12:36:45 +01:00
|
|
|
init() {
|
2015-11-20 17:40:41 +01:00
|
|
|
this._super(...arguments);
|
|
|
|
window.onbeforeunload = () => {
|
|
|
|
return this.get('hasDirtyAttributes') ? this.unloadDirtyMessage() : null;
|
2014-06-21 20:58:06 +02:00
|
|
|
};
|
|
|
|
},
|
2015-05-26 04:10:50 +02:00
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
_canAutosave: computed('model.isDraft', function () {
|
2017-07-10 17:09:50 +02:00
|
|
|
return !testing && this.get('model.isDraft');
|
2017-04-11 15:39:45 +02:00
|
|
|
}),
|
|
|
|
|
|
|
|
// save 3 seconds after the last edit
|
|
|
|
_autosave: task(function* () {
|
2017-09-28 14:53:22 +02:00
|
|
|
if (!this.get('_canAutosave')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-07-22 01:11:24 +02:00
|
|
|
// force an instant save on first body edit for new posts
|
2017-09-28 14:53:22 +02:00
|
|
|
if (this.get('model.isNew')) {
|
2017-07-22 01:11:24 +02:00
|
|
|
return this.get('autosave').perform();
|
|
|
|
}
|
|
|
|
|
2017-07-10 17:09:50 +02:00
|
|
|
yield timeout(AUTOSAVE_TIMEOUT);
|
2017-09-28 14:53:22 +02:00
|
|
|
this.get('autosave').perform();
|
2017-04-11 15:39:45 +02:00
|
|
|
}).restartable(),
|
2015-03-17 19:03:41 +01:00
|
|
|
|
2017-04-19 18:09:31 +02:00
|
|
|
// save at 60 seconds even if the user doesn't stop typing
|
2017-04-11 15:39:45 +02:00
|
|
|
_timedSave: task(function* () {
|
2017-09-28 14:53:22 +02:00
|
|
|
if (!this.get('_canAutosave')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-04-11 15:39:45 +02:00
|
|
|
// eslint-disable-next-line no-constant-condition
|
|
|
|
while (!testing && true) {
|
2017-07-10 17:09:50 +02:00
|
|
|
yield timeout(TIMEDSAVE_TIMEOUT);
|
2017-09-28 14:53:22 +02:00
|
|
|
this.get('autosave').perform();
|
2017-04-11 15:39:45 +02:00
|
|
|
}
|
2017-04-19 18:09:31 +02:00
|
|
|
}).drop(),
|
2017-04-11 15:39:45 +02:00
|
|
|
|
|
|
|
// separate task for autosave so that it doesn't override a manual save
|
|
|
|
autosave: task(function* () {
|
|
|
|
if (!this.get('save.isRunning')) {
|
2017-07-22 16:25:00 +02:00
|
|
|
return yield this.get('save').perform({
|
2015-05-10 00:23:14 +02:00
|
|
|
silent: true,
|
|
|
|
backgroundSave: true
|
2017-04-11 15:39:45 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}).drop(),
|
2015-03-17 19:03:41 +01:00
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
_autosaveRunning: computed('_autosave.isRunning', '_timedSave.isRunning', function () {
|
|
|
|
let autosave = this.get('_autosave.isRunning');
|
|
|
|
let timedsave = this.get('_timedSave.isRunning');
|
|
|
|
|
|
|
|
return autosave || timedsave;
|
|
|
|
}),
|
|
|
|
|
2017-06-13 17:04:09 +02:00
|
|
|
// updateSlug and save should always be enqueued so that we don't run into
|
|
|
|
// problems with concurrency, for example when Cmd-S is pressed whilst the
|
|
|
|
// cursor is in the slug field - that would previously trigger a simultaneous
|
|
|
|
// slug update and save resulting in ember data errors and inconsistent save
|
|
|
|
// results
|
|
|
|
saveTasks: taskGroup().enqueue(),
|
|
|
|
|
2017-04-19 18:09:31 +02:00
|
|
|
// save tasks cancels autosave before running, although this cancels the
|
|
|
|
// _xSave tasks that will also cancel the autosave task
|
2017-07-22 16:25:00 +02:00
|
|
|
save: task(function* (options = {}) {
|
2017-04-11 15:39:45 +02:00
|
|
|
let prevStatus = this.get('model.status');
|
|
|
|
let isNew = this.get('model.isNew');
|
2017-07-22 16:25:00 +02:00
|
|
|
let status;
|
2017-04-11 15:39:45 +02:00
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
this.send('cancelAutosave');
|
2017-04-11 15:39:45 +02:00
|
|
|
|
2017-04-19 18:09:31 +02:00
|
|
|
if (options.backgroundSave && !this.get('hasDirtyAttributes')) {
|
2017-07-22 16:25:00 +02:00
|
|
|
return;
|
2017-04-19 18:09:31 +02:00
|
|
|
}
|
|
|
|
|
2017-04-11 15:39:45 +02:00
|
|
|
if (options.backgroundSave) {
|
|
|
|
// do not allow a post's status to be set to published by a background save
|
|
|
|
status = 'draft';
|
|
|
|
} else {
|
|
|
|
if (this.get('post.pastScheduledTime')) {
|
|
|
|
status = (!this.get('willSchedule') && !this.get('willPublish')) ? 'draft' : 'published';
|
|
|
|
} else {
|
|
|
|
if (this.get('willPublish') && !this.get('model.isScheduled') && !this.get('statusFreeze')) {
|
|
|
|
status = 'published';
|
|
|
|
} else if (this.get('willSchedule') && !this.get('model.isPublished') && !this.get('statusFreeze')) {
|
|
|
|
status = 'scheduled';
|
|
|
|
} else {
|
|
|
|
status = 'draft';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the properties that are indirected
|
|
|
|
// set mobiledoc equal to what's in the editor, minus the image markers.
|
|
|
|
this.set('model.mobiledoc', this.get('model.scratch'));
|
|
|
|
this.set('model.status', status);
|
|
|
|
|
|
|
|
// Set a default title
|
|
|
|
if (!this.get('model.titleScratch').trim()) {
|
2017-07-10 13:33:05 +02:00
|
|
|
this.set('model.titleScratch', DEFAULT_TITLE);
|
2017-04-11 15:39:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
this.set('model.title', this.get('model.titleScratch'));
|
2017-08-01 10:24:46 +02:00
|
|
|
this.set('model.customExcerpt', this.get('model.customExcerptScratch'));
|
2017-08-02 11:32:51 +02:00
|
|
|
this.set('model.footerInjection', this.get('model.footerExcerptScratch'));
|
|
|
|
this.set('model.headerInjection', this.get('model.headerExcerptScratch'));
|
2017-04-11 15:39:45 +02:00
|
|
|
this.set('model.metaTitle', this.get('model.metaTitleScratch'));
|
|
|
|
this.set('model.metaDescription', this.get('model.metaDescriptionScratch'));
|
2017-08-03 13:45:14 +02:00
|
|
|
this.set('model.ogTitle', this.get('model.ogTitleScratch'));
|
|
|
|
this.set('model.ogDescription', this.get('model.ogDescriptionScratch'));
|
|
|
|
this.set('model.twitterTitle', this.get('model.twitterTitleScratch'));
|
|
|
|
this.set('model.twitterDescription', this.get('model.twitterDescriptionScratch'));
|
2017-04-11 15:39:45 +02:00
|
|
|
|
|
|
|
if (!this.get('model.slug')) {
|
2017-07-22 16:25:00 +02:00
|
|
|
this.get('saveTitle').cancelAll();
|
2017-04-11 15:39:45 +02:00
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
yield this.get('generateSlug').perform();
|
2017-04-11 15:39:45 +02:00
|
|
|
}
|
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
try {
|
|
|
|
let model = yield this.get('model').save(options);
|
|
|
|
|
|
|
|
if (!options.silent) {
|
|
|
|
this.showSaveNotification(prevStatus, model.get('status'), isNew ? true : false);
|
|
|
|
}
|
2017-04-11 15:39:45 +02:00
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
this.get('model').set('statusScratch', null);
|
2017-04-11 15:39:45 +02:00
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
// redirect to edit route if saving a new record
|
|
|
|
if (isNew && model.get('id')) {
|
|
|
|
if (!this.get('leaveEditorTransition')) {
|
2017-07-10 17:09:50 +02:00
|
|
|
this.replaceRoute('editor.edit', model);
|
|
|
|
}
|
2017-07-22 16:25:00 +02:00
|
|
|
return true;
|
|
|
|
}
|
2017-07-10 17:09:50 +02:00
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
return model;
|
2017-04-11 15:39:45 +02:00
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
} catch (error) {
|
2017-04-11 15:39:45 +02:00
|
|
|
// re-throw if we have a general server error
|
|
|
|
if (error && !isInvalidError(error)) {
|
|
|
|
this.send('error', error);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.set('model.status', prevStatus);
|
|
|
|
|
|
|
|
if (!options.silent) {
|
2017-07-22 16:25:00 +02:00
|
|
|
let errorOrMessages = error || this.get('model.errors.messages');
|
|
|
|
this.showErrorAlert(prevStatus, this.get('model.status'), errorOrMessages);
|
2017-04-11 15:39:45 +02:00
|
|
|
// simulate a validation error for upstream tasks
|
|
|
|
throw undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.get('model');
|
2017-07-22 16:25:00 +02:00
|
|
|
}
|
|
|
|
}).group('saveTasks'),
|
2017-04-11 15:39:45 +02:00
|
|
|
|
2017-06-13 17:04:09 +02:00
|
|
|
/*
|
|
|
|
* triggered by a user manually changing slug
|
|
|
|
*/
|
|
|
|
updateSlug: task(function* (_newSlug) {
|
|
|
|
let slug = this.get('model.slug');
|
|
|
|
let newSlug, serverSlug;
|
|
|
|
|
|
|
|
newSlug = _newSlug || slug;
|
|
|
|
newSlug = newSlug && newSlug.trim();
|
|
|
|
|
|
|
|
// Ignore unchanged slugs or candidate slugs that are empty
|
|
|
|
if (!newSlug || slug === newSlug) {
|
|
|
|
// reset the input to its previous state
|
|
|
|
this.set('slugValue', slug);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
serverSlug = yield this.get('slugGenerator').generateSlug('post', newSlug);
|
|
|
|
|
|
|
|
// If after getting the sanitized and unique slug back from the API
|
|
|
|
// we end up with a slug that matches the existing slug, abort the change
|
|
|
|
if (serverSlug === slug) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Because the server transforms the candidate slug by stripping
|
|
|
|
// certain characters and appending a number onto the end of slugs
|
|
|
|
// to enforce uniqueness, there are cases where we can get back a
|
|
|
|
// candidate slug that is a duplicate of the original except for
|
|
|
|
// the trailing incrementor (e.g., this-is-a-slug and this-is-a-slug-2)
|
|
|
|
|
|
|
|
// get the last token out of the slug candidate and see if it's a number
|
|
|
|
let slugTokens = serverSlug.split('-');
|
|
|
|
let check = Number(slugTokens.pop());
|
|
|
|
|
|
|
|
// if the candidate slug is the same as the existing slug except
|
|
|
|
// for the incrementor then the existing slug should be used
|
|
|
|
if (isNumber(check) && check > 0) {
|
|
|
|
if (slug === slugTokens.join('-') && serverSlug !== newSlug) {
|
|
|
|
this.set('slugValue', slug);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.set('model.slug', serverSlug);
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
return yield this.get('model').save();
|
|
|
|
}).group('saveTasks'),
|
|
|
|
|
2014-06-08 08:02:21 +02:00
|
|
|
/**
|
|
|
|
* By default, a post will not change its publish state.
|
|
|
|
* Only with a user-set value (via setSaveType action)
|
|
|
|
* can the post's status change.
|
|
|
|
*/
|
2014-12-30 03:11:24 +01:00
|
|
|
willPublish: boundOneWay('model.isPublished'),
|
Scheduler UI
refs TryGhost/Ghost#6413 and TryGhost/Ghost#6870
needs TryGhost/Ghost#6861
- **Post Settings Menu (PSM)**:'Publish Date' input accepts a date from now, min. 2 minutes to allow scheduler processing on the server. Also, there will always be some delay between typing the date and clicking on the 'Schedule Post' button. If the user types a future date for an already published post, the date will be reseted and he sees the message, that the post needs to be unpublished first. Once, the date is accepted, the label will change to 'Scheduled Date'.
- adds a CP 'timeScheduled' to post model, which will return `true` if the publish time is currently in the future.
- **Changes to the button flow in editor**:
- if the the CP `timeScheduled` returns true, a different drop-down-menu will be shown: 'Schedule Post' replaces 'Publish Now' and 'Unschedule' replaces 'Unpublish'.
- Covering the _edge cases_, especially when a scheduled post is about to be published, while the user is in the editor.
- First, a new CP `scheduleCountdown` will return the remaining time, when the estimated publish time is 15 minutes from now. A notification with this live-ticker is shown next to the save button. Once, we reach a 2 minutes limit, another CP `statusFreeze` will return true and causes the save button to only show `Unschedule` in a red state, until we reach the publish time
- Once the publish time is reached, a CP `scheduledWillPublish` causes the buttons and the existing code to pretend we're already dealing with a publish post. At the moment, there's no way to make a background-fetch of the now serverside-scheduled post model from the server, so Ember doesn't know about the changed state at that time.
- Changes in the editor, which are done during this 'status freeze'-process will be saved back correctly, once the user hits 'Update Post' after the buttons changed back. A click on 'Unpublish' will change the status back to a draft.
- The user will get a regular 'toaster' notification that the post has been published.
- adds CP `isScheduled` for scheduled posts
- adds CP `offset` to component `gh-posts-list-item` and helper `gh-format-time-scheduled` to show schedule date in content overview.
- sets timeout in `gh-spin-button` to 10ms for `Ember.testing`
- changes error message in `gh-editor-base-controller` to be in one line, seperated with a `:`
TODOs:
- [x] new sort order for posts (1. scheduled, 2. draft, 3. published) (refs TryGhost/Ghost#6932)
- [ ] Move posts sorting from posts controller to model and refactor to use `Ember.comparable` mixin
- [x] Flows for draft -> scheduled -> published like described in TryGhost/Ghost#6870 incl. edge cases and button behaviour
- [x] Tests
- [x] new PSM behaviour for time/date in future
- [x] display publishedAt date with timezone offset on posts overview
2016-02-02 08:04:40 +01:00
|
|
|
willSchedule: boundOneWay('model.isScheduled'),
|
2014-06-08 08:02:21 +02:00
|
|
|
|
2015-09-03 13:06:50 +02:00
|
|
|
// set by the editor route and `hasDirtyAttributes`. useful when checking
|
|
|
|
// whether the number of tags has changed for `hasDirtyAttributes`.
|
2014-06-06 03:18:03 +02:00
|
|
|
previousTagNames: null,
|
|
|
|
|
2016-08-12 17:07:46 +02:00
|
|
|
tagNames: mapBy('model.tags', 'name'),
|
2014-06-06 03:18:03 +02:00
|
|
|
|
2015-10-28 12:36:45 +01:00
|
|
|
postOrPage: computed('model.page', function () {
|
2015-01-08 19:23:48 +01:00
|
|
|
return this.get('model.page') ? 'Page' : 'Post';
|
|
|
|
}),
|
|
|
|
|
Scheduler UI
refs TryGhost/Ghost#6413 and TryGhost/Ghost#6870
needs TryGhost/Ghost#6861
- **Post Settings Menu (PSM)**:'Publish Date' input accepts a date from now, min. 2 minutes to allow scheduler processing on the server. Also, there will always be some delay between typing the date and clicking on the 'Schedule Post' button. If the user types a future date for an already published post, the date will be reseted and he sees the message, that the post needs to be unpublished first. Once, the date is accepted, the label will change to 'Scheduled Date'.
- adds a CP 'timeScheduled' to post model, which will return `true` if the publish time is currently in the future.
- **Changes to the button flow in editor**:
- if the the CP `timeScheduled` returns true, a different drop-down-menu will be shown: 'Schedule Post' replaces 'Publish Now' and 'Unschedule' replaces 'Unpublish'.
- Covering the _edge cases_, especially when a scheduled post is about to be published, while the user is in the editor.
- First, a new CP `scheduleCountdown` will return the remaining time, when the estimated publish time is 15 minutes from now. A notification with this live-ticker is shown next to the save button. Once, we reach a 2 minutes limit, another CP `statusFreeze` will return true and causes the save button to only show `Unschedule` in a red state, until we reach the publish time
- Once the publish time is reached, a CP `scheduledWillPublish` causes the buttons and the existing code to pretend we're already dealing with a publish post. At the moment, there's no way to make a background-fetch of the now serverside-scheduled post model from the server, so Ember doesn't know about the changed state at that time.
- Changes in the editor, which are done during this 'status freeze'-process will be saved back correctly, once the user hits 'Update Post' after the buttons changed back. A click on 'Unpublish' will change the status back to a draft.
- The user will get a regular 'toaster' notification that the post has been published.
- adds CP `isScheduled` for scheduled posts
- adds CP `offset` to component `gh-posts-list-item` and helper `gh-format-time-scheduled` to show schedule date in content overview.
- sets timeout in `gh-spin-button` to 10ms for `Ember.testing`
- changes error message in `gh-editor-base-controller` to be in one line, seperated with a `:`
TODOs:
- [x] new sort order for posts (1. scheduled, 2. draft, 3. published) (refs TryGhost/Ghost#6932)
- [ ] Move posts sorting from posts controller to model and refactor to use `Ember.comparable` mixin
- [x] Flows for draft -> scheduled -> published like described in TryGhost/Ghost#6870 incl. edge cases and button behaviour
- [x] Tests
- [x] new PSM behaviour for time/date in future
- [x] display publishedAt date with timezone offset on posts overview
2016-02-02 08:04:40 +01:00
|
|
|
// countdown timer to show the time left until publish time for a scheduled post
|
|
|
|
// starts 15 minutes before scheduled time
|
2017-04-11 15:39:45 +02:00
|
|
|
scheduleCountdown: computed('model.{publishedAtUTC,isScheduled}', 'clock.second', function () {
|
|
|
|
let isScheduled = this.get('model.isScheduled');
|
|
|
|
let publishTime = this.get('model.publishedAtUTC') || moment.utc();
|
|
|
|
let timeUntilPublished = publishTime.diff(moment.utc(), 'minutes', true);
|
|
|
|
let isPublishedSoon = timeUntilPublished > 0 && timeUntilPublished < 15;
|
Scheduler UI
refs TryGhost/Ghost#6413 and TryGhost/Ghost#6870
needs TryGhost/Ghost#6861
- **Post Settings Menu (PSM)**:'Publish Date' input accepts a date from now, min. 2 minutes to allow scheduler processing on the server. Also, there will always be some delay between typing the date and clicking on the 'Schedule Post' button. If the user types a future date for an already published post, the date will be reseted and he sees the message, that the post needs to be unpublished first. Once, the date is accepted, the label will change to 'Scheduled Date'.
- adds a CP 'timeScheduled' to post model, which will return `true` if the publish time is currently in the future.
- **Changes to the button flow in editor**:
- if the the CP `timeScheduled` returns true, a different drop-down-menu will be shown: 'Schedule Post' replaces 'Publish Now' and 'Unschedule' replaces 'Unpublish'.
- Covering the _edge cases_, especially when a scheduled post is about to be published, while the user is in the editor.
- First, a new CP `scheduleCountdown` will return the remaining time, when the estimated publish time is 15 minutes from now. A notification with this live-ticker is shown next to the save button. Once, we reach a 2 minutes limit, another CP `statusFreeze` will return true and causes the save button to only show `Unschedule` in a red state, until we reach the publish time
- Once the publish time is reached, a CP `scheduledWillPublish` causes the buttons and the existing code to pretend we're already dealing with a publish post. At the moment, there's no way to make a background-fetch of the now serverside-scheduled post model from the server, so Ember doesn't know about the changed state at that time.
- Changes in the editor, which are done during this 'status freeze'-process will be saved back correctly, once the user hits 'Update Post' after the buttons changed back. A click on 'Unpublish' will change the status back to a draft.
- The user will get a regular 'toaster' notification that the post has been published.
- adds CP `isScheduled` for scheduled posts
- adds CP `offset` to component `gh-posts-list-item` and helper `gh-format-time-scheduled` to show schedule date in content overview.
- sets timeout in `gh-spin-button` to 10ms for `Ember.testing`
- changes error message in `gh-editor-base-controller` to be in one line, seperated with a `:`
TODOs:
- [x] new sort order for posts (1. scheduled, 2. draft, 3. published) (refs TryGhost/Ghost#6932)
- [ ] Move posts sorting from posts controller to model and refactor to use `Ember.comparable` mixin
- [x] Flows for draft -> scheduled -> published like described in TryGhost/Ghost#6870 incl. edge cases and button behaviour
- [x] Tests
- [x] new PSM behaviour for time/date in future
- [x] display publishedAt date with timezone offset on posts overview
2016-02-02 08:04:40 +01:00
|
|
|
|
2017-04-11 15:39:45 +02:00
|
|
|
// force a recompute
|
Scheduler UI
refs TryGhost/Ghost#6413 and TryGhost/Ghost#6870
needs TryGhost/Ghost#6861
- **Post Settings Menu (PSM)**:'Publish Date' input accepts a date from now, min. 2 minutes to allow scheduler processing on the server. Also, there will always be some delay between typing the date and clicking on the 'Schedule Post' button. If the user types a future date for an already published post, the date will be reseted and he sees the message, that the post needs to be unpublished first. Once, the date is accepted, the label will change to 'Scheduled Date'.
- adds a CP 'timeScheduled' to post model, which will return `true` if the publish time is currently in the future.
- **Changes to the button flow in editor**:
- if the the CP `timeScheduled` returns true, a different drop-down-menu will be shown: 'Schedule Post' replaces 'Publish Now' and 'Unschedule' replaces 'Unpublish'.
- Covering the _edge cases_, especially when a scheduled post is about to be published, while the user is in the editor.
- First, a new CP `scheduleCountdown` will return the remaining time, when the estimated publish time is 15 minutes from now. A notification with this live-ticker is shown next to the save button. Once, we reach a 2 minutes limit, another CP `statusFreeze` will return true and causes the save button to only show `Unschedule` in a red state, until we reach the publish time
- Once the publish time is reached, a CP `scheduledWillPublish` causes the buttons and the existing code to pretend we're already dealing with a publish post. At the moment, there's no way to make a background-fetch of the now serverside-scheduled post model from the server, so Ember doesn't know about the changed state at that time.
- Changes in the editor, which are done during this 'status freeze'-process will be saved back correctly, once the user hits 'Update Post' after the buttons changed back. A click on 'Unpublish' will change the status back to a draft.
- The user will get a regular 'toaster' notification that the post has been published.
- adds CP `isScheduled` for scheduled posts
- adds CP `offset` to component `gh-posts-list-item` and helper `gh-format-time-scheduled` to show schedule date in content overview.
- sets timeout in `gh-spin-button` to 10ms for `Ember.testing`
- changes error message in `gh-editor-base-controller` to be in one line, seperated with a `:`
TODOs:
- [x] new sort order for posts (1. scheduled, 2. draft, 3. published) (refs TryGhost/Ghost#6932)
- [ ] Move posts sorting from posts controller to model and refactor to use `Ember.comparable` mixin
- [x] Flows for draft -> scheduled -> published like described in TryGhost/Ghost#6870 incl. edge cases and button behaviour
- [x] Tests
- [x] new PSM behaviour for time/date in future
- [x] display publishedAt date with timezone offset on posts overview
2016-02-02 08:04:40 +01:00
|
|
|
this.get('clock.second');
|
|
|
|
|
2017-04-11 15:39:45 +02:00
|
|
|
if (isScheduled && isPublishedSoon) {
|
Scheduler UI
refs TryGhost/Ghost#6413 and TryGhost/Ghost#6870
needs TryGhost/Ghost#6861
- **Post Settings Menu (PSM)**:'Publish Date' input accepts a date from now, min. 2 minutes to allow scheduler processing on the server. Also, there will always be some delay between typing the date and clicking on the 'Schedule Post' button. If the user types a future date for an already published post, the date will be reseted and he sees the message, that the post needs to be unpublished first. Once, the date is accepted, the label will change to 'Scheduled Date'.
- adds a CP 'timeScheduled' to post model, which will return `true` if the publish time is currently in the future.
- **Changes to the button flow in editor**:
- if the the CP `timeScheduled` returns true, a different drop-down-menu will be shown: 'Schedule Post' replaces 'Publish Now' and 'Unschedule' replaces 'Unpublish'.
- Covering the _edge cases_, especially when a scheduled post is about to be published, while the user is in the editor.
- First, a new CP `scheduleCountdown` will return the remaining time, when the estimated publish time is 15 minutes from now. A notification with this live-ticker is shown next to the save button. Once, we reach a 2 minutes limit, another CP `statusFreeze` will return true and causes the save button to only show `Unschedule` in a red state, until we reach the publish time
- Once the publish time is reached, a CP `scheduledWillPublish` causes the buttons and the existing code to pretend we're already dealing with a publish post. At the moment, there's no way to make a background-fetch of the now serverside-scheduled post model from the server, so Ember doesn't know about the changed state at that time.
- Changes in the editor, which are done during this 'status freeze'-process will be saved back correctly, once the user hits 'Update Post' after the buttons changed back. A click on 'Unpublish' will change the status back to a draft.
- The user will get a regular 'toaster' notification that the post has been published.
- adds CP `isScheduled` for scheduled posts
- adds CP `offset` to component `gh-posts-list-item` and helper `gh-format-time-scheduled` to show schedule date in content overview.
- sets timeout in `gh-spin-button` to 10ms for `Ember.testing`
- changes error message in `gh-editor-base-controller` to be in one line, seperated with a `:`
TODOs:
- [x] new sort order for posts (1. scheduled, 2. draft, 3. published) (refs TryGhost/Ghost#6932)
- [ ] Move posts sorting from posts controller to model and refactor to use `Ember.comparable` mixin
- [x] Flows for draft -> scheduled -> published like described in TryGhost/Ghost#6870 incl. edge cases and button behaviour
- [x] Tests
- [x] new PSM behaviour for time/date in future
- [x] display publishedAt date with timezone offset on posts overview
2016-02-02 08:04:40 +01:00
|
|
|
return moment(publishTime).fromNow();
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
|
2014-06-06 03:18:03 +02:00
|
|
|
// compares previousTagNames to tagNames
|
2015-10-28 12:36:45 +01:00
|
|
|
tagNamesEqual() {
|
2016-08-12 17:07:46 +02:00
|
|
|
let tagNames = this.get('tagNames') || [];
|
|
|
|
let previousTagNames = this.get('previousTagNames') || [];
|
2015-10-28 12:36:45 +01:00
|
|
|
let hashCurrent,
|
2014-06-06 03:18:03 +02:00
|
|
|
hashPrevious;
|
|
|
|
|
|
|
|
// beware! even if they have the same length,
|
|
|
|
// that doesn't mean they're the same.
|
|
|
|
if (tagNames.length !== previousTagNames.length) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// instead of comparing with slow, nested for loops,
|
|
|
|
// perform join on each array and compare the strings
|
|
|
|
hashCurrent = tagNames.join('');
|
|
|
|
hashPrevious = previousTagNames.join('');
|
|
|
|
|
|
|
|
return hashCurrent === hashPrevious;
|
|
|
|
},
|
|
|
|
|
2014-11-04 04:31:10 +01:00
|
|
|
// a hook created in editor-base-route's setupController
|
2015-10-28 12:36:45 +01:00
|
|
|
modelSaved() {
|
|
|
|
let model = this.get('model');
|
2014-06-28 05:01:56 +02:00
|
|
|
|
|
|
|
// safer to updateTags on save in one place
|
|
|
|
// rather than in all other places save is called
|
|
|
|
model.updateTags();
|
|
|
|
|
2015-09-03 13:06:50 +02:00
|
|
|
// set previousTagNames to current tagNames for hasDirtyAttributes check
|
2014-06-28 05:01:56 +02:00
|
|
|
this.set('previousTagNames', this.get('tagNames'));
|
|
|
|
|
2015-09-03 13:06:50 +02:00
|
|
|
// `updateTags` triggers `hasDirtyAttributes => true`.
|
2014-06-28 05:01:56 +02:00
|
|
|
// for a saved model it would otherwise be false.
|
2014-09-04 20:12:03 +02:00
|
|
|
|
|
|
|
// if the two "scratch" properties (title and content) match the model, then
|
2015-09-03 13:06:50 +02:00
|
|
|
// it's ok to set hasDirtyAttributes to false
|
2016-11-14 14:16:51 +01:00
|
|
|
if (model.get('titleScratch') === model.get('title')
|
|
|
|
&& JSON.stringify(model.get('scratch')) === JSON.stringify(model.get('mobiledoc'))) {
|
2015-09-03 13:06:50 +02:00
|
|
|
this.set('hasDirtyAttributes', false);
|
2014-09-04 20:12:03 +02:00
|
|
|
}
|
2014-06-28 05:01:56 +02:00
|
|
|
},
|
|
|
|
|
2014-06-06 03:18:03 +02:00
|
|
|
// an ugly hack, but necessary to watch all the model's properties
|
|
|
|
// and more, without having to be explicit and do it manually
|
2015-10-28 12:36:45 +01:00
|
|
|
hasDirtyAttributes: computed.apply(Ember, watchedProps.concat({
|
|
|
|
get() {
|
|
|
|
let model = this.get('model');
|
2016-02-24 05:39:54 +01:00
|
|
|
|
|
|
|
if (!model) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-04-11 15:39:45 +02:00
|
|
|
let mobiledoc = JSON.stringify(model.get('mobiledoc'));
|
|
|
|
let scratch = JSON.stringify(model.get('scratch'));
|
2015-10-28 12:36:45 +01:00
|
|
|
let title = model.get('title');
|
|
|
|
let titleScratch = model.get('titleScratch');
|
|
|
|
let changedAttributes;
|
2015-06-03 04:56:42 +02:00
|
|
|
|
|
|
|
if (!this.tagNamesEqual()) {
|
|
|
|
return true;
|
|
|
|
}
|
2014-06-06 03:18:03 +02:00
|
|
|
|
2015-06-03 04:56:42 +02:00
|
|
|
if (titleScratch !== title) {
|
|
|
|
return true;
|
|
|
|
}
|
2014-06-06 03:18:03 +02:00
|
|
|
|
2015-06-03 04:56:42 +02:00
|
|
|
// since `scratch` is not model property, we need to check
|
2016-09-26 15:04:20 +02:00
|
|
|
// it explicitly against the model's mobiledoc attribute
|
|
|
|
if (mobiledoc !== scratch) {
|
2015-06-03 04:56:42 +02:00
|
|
|
return true;
|
|
|
|
}
|
2014-08-07 05:39:19 +02:00
|
|
|
|
2015-06-03 04:56:42 +02:00
|
|
|
// if the Adapter failed to save the model isError will be true
|
|
|
|
// and we should consider the model still dirty.
|
|
|
|
if (model.get('isError')) {
|
|
|
|
return true;
|
|
|
|
}
|
2014-06-06 03:18:03 +02:00
|
|
|
|
2015-09-03 13:06:50 +02:00
|
|
|
// models created on the client always return `hasDirtyAttributes: true`,
|
2015-06-03 04:56:42 +02:00
|
|
|
// so we need to see which properties have actually changed.
|
|
|
|
if (model.get('isNew')) {
|
2015-09-03 13:06:50 +02:00
|
|
|
changedAttributes = Object.keys(model.changedAttributes());
|
2014-11-29 20:42:57 +01:00
|
|
|
|
2015-06-03 04:56:42 +02:00
|
|
|
if (changedAttributes.length) {
|
|
|
|
return true;
|
|
|
|
}
|
2014-06-06 03:18:03 +02:00
|
|
|
|
2015-06-03 04:56:42 +02:00
|
|
|
return false;
|
2014-06-06 03:18:03 +02:00
|
|
|
}
|
|
|
|
|
2015-06-03 04:56:42 +02:00
|
|
|
// even though we use the `scratch` prop to show edits,
|
2015-09-03 13:06:50 +02:00
|
|
|
// which does *not* change the model's `hasDirtyAttributes` property,
|
|
|
|
// `hasDirtyAttributes` will tell us if the other props have changed,
|
2015-06-03 04:56:42 +02:00
|
|
|
// as long as the model is not new (model.isNew === false).
|
2015-09-03 13:06:50 +02:00
|
|
|
return model.get('hasDirtyAttributes');
|
2015-06-03 04:56:42 +02:00
|
|
|
},
|
2015-10-28 12:36:45 +01:00
|
|
|
set(key, value) {
|
2015-06-03 04:56:42 +02:00
|
|
|
return value;
|
2014-06-06 03:18:03 +02:00
|
|
|
}
|
|
|
|
})),
|
|
|
|
|
|
|
|
// used on window.onbeforeunload
|
2015-10-28 12:36:45 +01:00
|
|
|
unloadDirtyMessage() {
|
2016-11-14 14:16:51 +01:00
|
|
|
return '==============================\n\n'
|
|
|
|
+ 'Hey there! It looks like you\'re in the middle of writing'
|
|
|
|
+ ' something and you haven\'t saved all of your content.'
|
|
|
|
+ '\n\nSave before you go!\n\n'
|
|
|
|
+ '==============================';
|
2014-06-06 03:18:03 +02:00
|
|
|
},
|
|
|
|
|
2014-10-24 23:09:50 +02:00
|
|
|
// TODO: This has to be moved to the I18n localization file.
|
|
|
|
// This structure is supposed to be close to the i18n-localization which will be used soon.
|
2014-06-26 11:42:29 +02:00
|
|
|
messageMap: {
|
|
|
|
errors: {
|
|
|
|
post: {
|
|
|
|
published: {
|
Scheduler UI
refs TryGhost/Ghost#6413 and TryGhost/Ghost#6870
needs TryGhost/Ghost#6861
- **Post Settings Menu (PSM)**:'Publish Date' input accepts a date from now, min. 2 minutes to allow scheduler processing on the server. Also, there will always be some delay between typing the date and clicking on the 'Schedule Post' button. If the user types a future date for an already published post, the date will be reseted and he sees the message, that the post needs to be unpublished first. Once, the date is accepted, the label will change to 'Scheduled Date'.
- adds a CP 'timeScheduled' to post model, which will return `true` if the publish time is currently in the future.
- **Changes to the button flow in editor**:
- if the the CP `timeScheduled` returns true, a different drop-down-menu will be shown: 'Schedule Post' replaces 'Publish Now' and 'Unschedule' replaces 'Unpublish'.
- Covering the _edge cases_, especially when a scheduled post is about to be published, while the user is in the editor.
- First, a new CP `scheduleCountdown` will return the remaining time, when the estimated publish time is 15 minutes from now. A notification with this live-ticker is shown next to the save button. Once, we reach a 2 minutes limit, another CP `statusFreeze` will return true and causes the save button to only show `Unschedule` in a red state, until we reach the publish time
- Once the publish time is reached, a CP `scheduledWillPublish` causes the buttons and the existing code to pretend we're already dealing with a publish post. At the moment, there's no way to make a background-fetch of the now serverside-scheduled post model from the server, so Ember doesn't know about the changed state at that time.
- Changes in the editor, which are done during this 'status freeze'-process will be saved back correctly, once the user hits 'Update Post' after the buttons changed back. A click on 'Unpublish' will change the status back to a draft.
- The user will get a regular 'toaster' notification that the post has been published.
- adds CP `isScheduled` for scheduled posts
- adds CP `offset` to component `gh-posts-list-item` and helper `gh-format-time-scheduled` to show schedule date in content overview.
- sets timeout in `gh-spin-button` to 10ms for `Ember.testing`
- changes error message in `gh-editor-base-controller` to be in one line, seperated with a `:`
TODOs:
- [x] new sort order for posts (1. scheduled, 2. draft, 3. published) (refs TryGhost/Ghost#6932)
- [ ] Move posts sorting from posts controller to model and refactor to use `Ember.comparable` mixin
- [x] Flows for draft -> scheduled -> published like described in TryGhost/Ghost#6870 incl. edge cases and button behaviour
- [x] Tests
- [x] new PSM behaviour for time/date in future
- [x] display publishedAt date with timezone offset on posts overview
2016-02-02 08:04:40 +01:00
|
|
|
published: 'Update failed',
|
|
|
|
draft: 'Saving failed',
|
|
|
|
scheduled: 'Scheduling failed'
|
2014-06-26 11:42:29 +02:00
|
|
|
},
|
|
|
|
draft: {
|
Scheduler UI
refs TryGhost/Ghost#6413 and TryGhost/Ghost#6870
needs TryGhost/Ghost#6861
- **Post Settings Menu (PSM)**:'Publish Date' input accepts a date from now, min. 2 minutes to allow scheduler processing on the server. Also, there will always be some delay between typing the date and clicking on the 'Schedule Post' button. If the user types a future date for an already published post, the date will be reseted and he sees the message, that the post needs to be unpublished first. Once, the date is accepted, the label will change to 'Scheduled Date'.
- adds a CP 'timeScheduled' to post model, which will return `true` if the publish time is currently in the future.
- **Changes to the button flow in editor**:
- if the the CP `timeScheduled` returns true, a different drop-down-menu will be shown: 'Schedule Post' replaces 'Publish Now' and 'Unschedule' replaces 'Unpublish'.
- Covering the _edge cases_, especially when a scheduled post is about to be published, while the user is in the editor.
- First, a new CP `scheduleCountdown` will return the remaining time, when the estimated publish time is 15 minutes from now. A notification with this live-ticker is shown next to the save button. Once, we reach a 2 minutes limit, another CP `statusFreeze` will return true and causes the save button to only show `Unschedule` in a red state, until we reach the publish time
- Once the publish time is reached, a CP `scheduledWillPublish` causes the buttons and the existing code to pretend we're already dealing with a publish post. At the moment, there's no way to make a background-fetch of the now serverside-scheduled post model from the server, so Ember doesn't know about the changed state at that time.
- Changes in the editor, which are done during this 'status freeze'-process will be saved back correctly, once the user hits 'Update Post' after the buttons changed back. A click on 'Unpublish' will change the status back to a draft.
- The user will get a regular 'toaster' notification that the post has been published.
- adds CP `isScheduled` for scheduled posts
- adds CP `offset` to component `gh-posts-list-item` and helper `gh-format-time-scheduled` to show schedule date in content overview.
- sets timeout in `gh-spin-button` to 10ms for `Ember.testing`
- changes error message in `gh-editor-base-controller` to be in one line, seperated with a `:`
TODOs:
- [x] new sort order for posts (1. scheduled, 2. draft, 3. published) (refs TryGhost/Ghost#6932)
- [ ] Move posts sorting from posts controller to model and refactor to use `Ember.comparable` mixin
- [x] Flows for draft -> scheduled -> published like described in TryGhost/Ghost#6870 incl. edge cases and button behaviour
- [x] Tests
- [x] new PSM behaviour for time/date in future
- [x] display publishedAt date with timezone offset on posts overview
2016-02-02 08:04:40 +01:00
|
|
|
published: 'Publish failed',
|
|
|
|
draft: 'Saving failed',
|
|
|
|
scheduled: 'Scheduling failed'
|
|
|
|
},
|
|
|
|
scheduled: {
|
|
|
|
scheduled: 'Updated failed',
|
|
|
|
draft: 'Unscheduling failed',
|
|
|
|
published: 'Publish failed'
|
2014-06-26 11:42:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
success: {
|
|
|
|
post: {
|
|
|
|
published: {
|
2014-10-08 16:53:20 +02:00
|
|
|
published: 'Updated.',
|
Scheduler UI
refs TryGhost/Ghost#6413 and TryGhost/Ghost#6870
needs TryGhost/Ghost#6861
- **Post Settings Menu (PSM)**:'Publish Date' input accepts a date from now, min. 2 minutes to allow scheduler processing on the server. Also, there will always be some delay between typing the date and clicking on the 'Schedule Post' button. If the user types a future date for an already published post, the date will be reseted and he sees the message, that the post needs to be unpublished first. Once, the date is accepted, the label will change to 'Scheduled Date'.
- adds a CP 'timeScheduled' to post model, which will return `true` if the publish time is currently in the future.
- **Changes to the button flow in editor**:
- if the the CP `timeScheduled` returns true, a different drop-down-menu will be shown: 'Schedule Post' replaces 'Publish Now' and 'Unschedule' replaces 'Unpublish'.
- Covering the _edge cases_, especially when a scheduled post is about to be published, while the user is in the editor.
- First, a new CP `scheduleCountdown` will return the remaining time, when the estimated publish time is 15 minutes from now. A notification with this live-ticker is shown next to the save button. Once, we reach a 2 minutes limit, another CP `statusFreeze` will return true and causes the save button to only show `Unschedule` in a red state, until we reach the publish time
- Once the publish time is reached, a CP `scheduledWillPublish` causes the buttons and the existing code to pretend we're already dealing with a publish post. At the moment, there's no way to make a background-fetch of the now serverside-scheduled post model from the server, so Ember doesn't know about the changed state at that time.
- Changes in the editor, which are done during this 'status freeze'-process will be saved back correctly, once the user hits 'Update Post' after the buttons changed back. A click on 'Unpublish' will change the status back to a draft.
- The user will get a regular 'toaster' notification that the post has been published.
- adds CP `isScheduled` for scheduled posts
- adds CP `offset` to component `gh-posts-list-item` and helper `gh-format-time-scheduled` to show schedule date in content overview.
- sets timeout in `gh-spin-button` to 10ms for `Ember.testing`
- changes error message in `gh-editor-base-controller` to be in one line, seperated with a `:`
TODOs:
- [x] new sort order for posts (1. scheduled, 2. draft, 3. published) (refs TryGhost/Ghost#6932)
- [ ] Move posts sorting from posts controller to model and refactor to use `Ember.comparable` mixin
- [x] Flows for draft -> scheduled -> published like described in TryGhost/Ghost#6870 incl. edge cases and button behaviour
- [x] Tests
- [x] new PSM behaviour for time/date in future
- [x] display publishedAt date with timezone offset on posts overview
2016-02-02 08:04:40 +01:00
|
|
|
draft: 'Saved.',
|
|
|
|
scheduled: 'Scheduled.'
|
2014-06-26 11:42:29 +02:00
|
|
|
},
|
|
|
|
draft: {
|
2014-10-08 16:53:20 +02:00
|
|
|
published: 'Published!',
|
Scheduler UI
refs TryGhost/Ghost#6413 and TryGhost/Ghost#6870
needs TryGhost/Ghost#6861
- **Post Settings Menu (PSM)**:'Publish Date' input accepts a date from now, min. 2 minutes to allow scheduler processing on the server. Also, there will always be some delay between typing the date and clicking on the 'Schedule Post' button. If the user types a future date for an already published post, the date will be reseted and he sees the message, that the post needs to be unpublished first. Once, the date is accepted, the label will change to 'Scheduled Date'.
- adds a CP 'timeScheduled' to post model, which will return `true` if the publish time is currently in the future.
- **Changes to the button flow in editor**:
- if the the CP `timeScheduled` returns true, a different drop-down-menu will be shown: 'Schedule Post' replaces 'Publish Now' and 'Unschedule' replaces 'Unpublish'.
- Covering the _edge cases_, especially when a scheduled post is about to be published, while the user is in the editor.
- First, a new CP `scheduleCountdown` will return the remaining time, when the estimated publish time is 15 minutes from now. A notification with this live-ticker is shown next to the save button. Once, we reach a 2 minutes limit, another CP `statusFreeze` will return true and causes the save button to only show `Unschedule` in a red state, until we reach the publish time
- Once the publish time is reached, a CP `scheduledWillPublish` causes the buttons and the existing code to pretend we're already dealing with a publish post. At the moment, there's no way to make a background-fetch of the now serverside-scheduled post model from the server, so Ember doesn't know about the changed state at that time.
- Changes in the editor, which are done during this 'status freeze'-process will be saved back correctly, once the user hits 'Update Post' after the buttons changed back. A click on 'Unpublish' will change the status back to a draft.
- The user will get a regular 'toaster' notification that the post has been published.
- adds CP `isScheduled` for scheduled posts
- adds CP `offset` to component `gh-posts-list-item` and helper `gh-format-time-scheduled` to show schedule date in content overview.
- sets timeout in `gh-spin-button` to 10ms for `Ember.testing`
- changes error message in `gh-editor-base-controller` to be in one line, seperated with a `:`
TODOs:
- [x] new sort order for posts (1. scheduled, 2. draft, 3. published) (refs TryGhost/Ghost#6932)
- [ ] Move posts sorting from posts controller to model and refactor to use `Ember.comparable` mixin
- [x] Flows for draft -> scheduled -> published like described in TryGhost/Ghost#6870 incl. edge cases and button behaviour
- [x] Tests
- [x] new PSM behaviour for time/date in future
- [x] display publishedAt date with timezone offset on posts overview
2016-02-02 08:04:40 +01:00
|
|
|
draft: 'Saved.',
|
|
|
|
scheduled: 'Scheduled.'
|
|
|
|
},
|
|
|
|
scheduled: {
|
|
|
|
scheduled: 'Updated.',
|
|
|
|
draft: 'Unscheduled.',
|
|
|
|
published: 'Published!'
|
2014-06-26 11:42:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-06-18 23:56:18 +02:00
|
|
|
// TODO: Update for new notification click-action API
|
2015-10-28 12:36:45 +01:00
|
|
|
showSaveNotification(prevStatus, status, delay) {
|
|
|
|
let message = this.messageMap.success.post[prevStatus][status];
|
|
|
|
let notifications = this.get('notifications');
|
2016-01-12 21:29:57 +01:00
|
|
|
let type, path;
|
2014-06-26 11:42:29 +02:00
|
|
|
|
2014-12-15 11:11:29 +01:00
|
|
|
if (status === 'published') {
|
2016-01-12 21:29:57 +01:00
|
|
|
type = this.get('postOrPage');
|
|
|
|
path = this.get('model.absoluteUrl');
|
|
|
|
} else {
|
|
|
|
type = 'Preview';
|
|
|
|
path = this.get('model.previewUrl');
|
2014-12-15 11:11:29 +01:00
|
|
|
}
|
2015-05-26 04:10:50 +02:00
|
|
|
|
2016-01-12 21:29:57 +01:00
|
|
|
message += ` <a href="${path}" target="_blank">View ${type}</a>`;
|
|
|
|
|
2015-06-18 23:56:18 +02:00
|
|
|
notifications.showNotification(message.htmlSafe(), {delayed: delay});
|
2014-06-26 11:42:29 +02:00
|
|
|
},
|
|
|
|
|
2016-07-19 01:23:43 +02:00
|
|
|
showErrorAlert(prevStatus, status, error, delay) {
|
2015-10-28 12:36:45 +01:00
|
|
|
let message = this.messageMap.errors.post[prevStatus][status];
|
|
|
|
let notifications = this.get('notifications');
|
2016-07-19 01:23:43 +02:00
|
|
|
let errorMessage;
|
2015-08-29 21:02:06 +02:00
|
|
|
|
|
|
|
function isString(str) {
|
2016-11-14 14:16:51 +01:00
|
|
|
/* global toString */
|
2015-08-29 21:02:06 +02:00
|
|
|
return toString.call(str) === '[object String]';
|
|
|
|
}
|
|
|
|
|
2016-07-19 01:23:43 +02:00
|
|
|
if (error && isString(error)) {
|
|
|
|
errorMessage = error;
|
2016-07-22 11:14:32 +02:00
|
|
|
} else if (error && isEmberArray(error)) {
|
|
|
|
// This is here because validation errors are returned as an array
|
|
|
|
// TODO: remove this once validations are fixed
|
|
|
|
errorMessage = error[0];
|
2016-07-19 01:23:43 +02:00
|
|
|
} else if (error && error.errors && error.errors[0].message) {
|
|
|
|
errorMessage = error.errors[0].message;
|
2015-08-29 21:02:06 +02:00
|
|
|
} else {
|
2016-07-19 01:23:43 +02:00
|
|
|
errorMessage = 'Unknown Error';
|
2015-08-29 21:02:06 +02:00
|
|
|
}
|
2014-06-26 11:42:29 +02:00
|
|
|
|
2016-07-19 01:23:43 +02:00
|
|
|
message += `: ${errorMessage}`;
|
2016-06-11 18:52:36 +02:00
|
|
|
message = htmlSafe(message);
|
2014-06-26 11:42:29 +02:00
|
|
|
|
2015-10-28 12:36:45 +01:00
|
|
|
notifications.showAlert(message, {type: 'error', delayed: delay, key: 'post.save'});
|
2014-06-26 11:42:29 +02:00
|
|
|
},
|
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
saveTitle: task(function* () {
|
2017-06-01 23:00:57 +02:00
|
|
|
let model = this.get('model');
|
2017-07-22 16:25:00 +02:00
|
|
|
let currentTitle = model.get('title');
|
|
|
|
let newTitle = model.get('titleScratch').trim();
|
2017-06-01 23:00:57 +02:00
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
if (currentTitle && newTitle && newTitle === currentTitle) {
|
|
|
|
return;
|
|
|
|
}
|
2016-08-11 08:58:38 +02:00
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
// this is necessary to force a save when the title is blank
|
|
|
|
this.set('hasDirtyAttributes', true);
|
2016-08-11 08:58:38 +02:00
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
// generate a slug if a post is new and doesn't have a title yet or
|
|
|
|
// if the title is still '(Untitled)'
|
|
|
|
if ((model.get('isNew') && !currentTitle) || currentTitle === DEFAULT_TITLE) {
|
2017-07-22 01:11:24 +02:00
|
|
|
yield this.get('generateSlug').perform();
|
|
|
|
}
|
2017-07-22 16:25:00 +02:00
|
|
|
|
|
|
|
if (this.get('model.isDraft')) {
|
|
|
|
yield this.get('autosave').perform();
|
|
|
|
}
|
|
|
|
}),
|
2016-08-11 08:58:38 +02:00
|
|
|
|
|
|
|
generateSlug: task(function* () {
|
|
|
|
let title = this.get('model.titleScratch');
|
|
|
|
|
|
|
|
// Only set an "untitled" slug once per post
|
2017-07-10 13:33:05 +02:00
|
|
|
if (title === DEFAULT_TITLE && this.get('model.slug')) {
|
2016-08-11 08:58:38 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
let slug = yield this.get('slugGenerator').generateSlug('post', title);
|
|
|
|
|
|
|
|
if (!isBlank(slug)) {
|
|
|
|
this.set('model.slug', slug);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
// Nothing to do (would be nice to log this somewhere though),
|
|
|
|
// but a rejected promise needs to be handled here so that a resolved
|
|
|
|
// promise is returned.
|
|
|
|
if (isVersionMismatchError(error)) {
|
|
|
|
this.get('notifications').showAPIError(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}).enqueue(),
|
|
|
|
|
2014-06-08 08:02:21 +02:00
|
|
|
actions: {
|
2017-04-11 15:39:45 +02:00
|
|
|
updateScratch(value) {
|
|
|
|
this.set('model.scratch', value);
|
2017-05-08 12:35:42 +02:00
|
|
|
|
2017-04-19 18:09:31 +02:00
|
|
|
// save 3 seconds after last edit
|
2017-04-11 15:39:45 +02:00
|
|
|
this.get('_autosave').perform();
|
2017-04-19 18:09:31 +02:00
|
|
|
// force save at 60 seconds
|
2017-04-11 15:39:45 +02:00
|
|
|
this.get('_timedSave').perform();
|
|
|
|
},
|
2015-12-14 13:52:53 +01:00
|
|
|
|
2017-04-11 15:39:45 +02:00
|
|
|
cancelAutosave() {
|
|
|
|
this.get('_autosave').cancelAll();
|
|
|
|
this.get('_timedSave').cancelAll();
|
2015-12-14 13:52:53 +01:00
|
|
|
},
|
|
|
|
|
2015-10-28 12:36:45 +01:00
|
|
|
save(options) {
|
2017-04-11 15:39:45 +02:00
|
|
|
return this.get('save').perform(options);
|
2014-06-08 08:02:21 +02:00
|
|
|
},
|
|
|
|
|
2015-10-28 12:36:45 +01:00
|
|
|
setSaveType(newType) {
|
2014-06-08 08:02:21 +02:00
|
|
|
if (newType === 'publish') {
|
|
|
|
this.set('willPublish', true);
|
Scheduler UI
refs TryGhost/Ghost#6413 and TryGhost/Ghost#6870
needs TryGhost/Ghost#6861
- **Post Settings Menu (PSM)**:'Publish Date' input accepts a date from now, min. 2 minutes to allow scheduler processing on the server. Also, there will always be some delay between typing the date and clicking on the 'Schedule Post' button. If the user types a future date for an already published post, the date will be reseted and he sees the message, that the post needs to be unpublished first. Once, the date is accepted, the label will change to 'Scheduled Date'.
- adds a CP 'timeScheduled' to post model, which will return `true` if the publish time is currently in the future.
- **Changes to the button flow in editor**:
- if the the CP `timeScheduled` returns true, a different drop-down-menu will be shown: 'Schedule Post' replaces 'Publish Now' and 'Unschedule' replaces 'Unpublish'.
- Covering the _edge cases_, especially when a scheduled post is about to be published, while the user is in the editor.
- First, a new CP `scheduleCountdown` will return the remaining time, when the estimated publish time is 15 minutes from now. A notification with this live-ticker is shown next to the save button. Once, we reach a 2 minutes limit, another CP `statusFreeze` will return true and causes the save button to only show `Unschedule` in a red state, until we reach the publish time
- Once the publish time is reached, a CP `scheduledWillPublish` causes the buttons and the existing code to pretend we're already dealing with a publish post. At the moment, there's no way to make a background-fetch of the now serverside-scheduled post model from the server, so Ember doesn't know about the changed state at that time.
- Changes in the editor, which are done during this 'status freeze'-process will be saved back correctly, once the user hits 'Update Post' after the buttons changed back. A click on 'Unpublish' will change the status back to a draft.
- The user will get a regular 'toaster' notification that the post has been published.
- adds CP `isScheduled` for scheduled posts
- adds CP `offset` to component `gh-posts-list-item` and helper `gh-format-time-scheduled` to show schedule date in content overview.
- sets timeout in `gh-spin-button` to 10ms for `Ember.testing`
- changes error message in `gh-editor-base-controller` to be in one line, seperated with a `:`
TODOs:
- [x] new sort order for posts (1. scheduled, 2. draft, 3. published) (refs TryGhost/Ghost#6932)
- [ ] Move posts sorting from posts controller to model and refactor to use `Ember.comparable` mixin
- [x] Flows for draft -> scheduled -> published like described in TryGhost/Ghost#6870 incl. edge cases and button behaviour
- [x] Tests
- [x] new PSM behaviour for time/date in future
- [x] display publishedAt date with timezone offset on posts overview
2016-02-02 08:04:40 +01:00
|
|
|
this.set('willSchedule', false);
|
2014-06-08 08:02:21 +02:00
|
|
|
} else if (newType === 'draft') {
|
|
|
|
this.set('willPublish', false);
|
Scheduler UI
refs TryGhost/Ghost#6413 and TryGhost/Ghost#6870
needs TryGhost/Ghost#6861
- **Post Settings Menu (PSM)**:'Publish Date' input accepts a date from now, min. 2 minutes to allow scheduler processing on the server. Also, there will always be some delay between typing the date and clicking on the 'Schedule Post' button. If the user types a future date for an already published post, the date will be reseted and he sees the message, that the post needs to be unpublished first. Once, the date is accepted, the label will change to 'Scheduled Date'.
- adds a CP 'timeScheduled' to post model, which will return `true` if the publish time is currently in the future.
- **Changes to the button flow in editor**:
- if the the CP `timeScheduled` returns true, a different drop-down-menu will be shown: 'Schedule Post' replaces 'Publish Now' and 'Unschedule' replaces 'Unpublish'.
- Covering the _edge cases_, especially when a scheduled post is about to be published, while the user is in the editor.
- First, a new CP `scheduleCountdown` will return the remaining time, when the estimated publish time is 15 minutes from now. A notification with this live-ticker is shown next to the save button. Once, we reach a 2 minutes limit, another CP `statusFreeze` will return true and causes the save button to only show `Unschedule` in a red state, until we reach the publish time
- Once the publish time is reached, a CP `scheduledWillPublish` causes the buttons and the existing code to pretend we're already dealing with a publish post. At the moment, there's no way to make a background-fetch of the now serverside-scheduled post model from the server, so Ember doesn't know about the changed state at that time.
- Changes in the editor, which are done during this 'status freeze'-process will be saved back correctly, once the user hits 'Update Post' after the buttons changed back. A click on 'Unpublish' will change the status back to a draft.
- The user will get a regular 'toaster' notification that the post has been published.
- adds CP `isScheduled` for scheduled posts
- adds CP `offset` to component `gh-posts-list-item` and helper `gh-format-time-scheduled` to show schedule date in content overview.
- sets timeout in `gh-spin-button` to 10ms for `Ember.testing`
- changes error message in `gh-editor-base-controller` to be in one line, seperated with a `:`
TODOs:
- [x] new sort order for posts (1. scheduled, 2. draft, 3. published) (refs TryGhost/Ghost#6932)
- [ ] Move posts sorting from posts controller to model and refactor to use `Ember.comparable` mixin
- [x] Flows for draft -> scheduled -> published like described in TryGhost/Ghost#6870 incl. edge cases and button behaviour
- [x] Tests
- [x] new PSM behaviour for time/date in future
- [x] display publishedAt date with timezone offset on posts overview
2016-02-02 08:04:40 +01:00
|
|
|
this.set('willSchedule', false);
|
|
|
|
} else if (newType === 'schedule') {
|
|
|
|
this.set('willSchedule', true);
|
|
|
|
this.set('willPublish', false);
|
2014-06-08 08:02:21 +02:00
|
|
|
}
|
2014-06-06 03:18:03 +02:00
|
|
|
},
|
|
|
|
|
2015-11-18 11:50:48 +01:00
|
|
|
toggleLeaveEditorModal(transition) {
|
2017-07-22 16:25:00 +02:00
|
|
|
let leaveTransition = this.get('leaveEditorTransition');
|
|
|
|
|
|
|
|
if (!transition && this.get('showLeaveEditorModal')) {
|
2017-09-28 17:27:33 +02:00
|
|
|
this.set('leaveEditorTransition', null);
|
2017-07-22 16:25:00 +02:00
|
|
|
this.set('showLeaveEditorModal', false);
|
|
|
|
return;
|
2017-07-10 17:09:50 +02:00
|
|
|
}
|
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
if (!leaveTransition || transition.targetName === leaveTransition.targetName) {
|
|
|
|
this.set('leaveEditorTransition', transition);
|
|
|
|
|
|
|
|
// if a save is running, wait for it to finish then transition
|
|
|
|
if (this.get('saveTasks.isRunning')) {
|
|
|
|
return this.get('saveTasks.last').then(() => {
|
|
|
|
transition.retry();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// if an autosave is scheduled, cancel it, save then transition
|
|
|
|
if (this.get('_autosaveRunning')) {
|
|
|
|
this.send('cancelAutosave');
|
|
|
|
this.get('autosave').cancelAll();
|
|
|
|
|
|
|
|
return this.get('autosave').perform().then(() => {
|
|
|
|
transition.retry();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// we genuinely have unsaved data, show the modal
|
|
|
|
this.set('showLeaveEditorModal', true);
|
|
|
|
}
|
2015-11-18 11:50:48 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
leaveEditor() {
|
|
|
|
let transition = this.get('leaveEditorTransition');
|
|
|
|
let model = this.get('model');
|
|
|
|
|
|
|
|
if (!transition) {
|
|
|
|
this.get('notifications').showAlert('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// definitely want to clear the data store and post of any unsaved, client-generated tags
|
|
|
|
model.updateTags();
|
|
|
|
|
|
|
|
if (model.get('isNew')) {
|
|
|
|
// the user doesn't want to save the new, unsaved post, so delete it.
|
|
|
|
model.deleteRecord();
|
|
|
|
} else {
|
|
|
|
// roll back changes on model props
|
|
|
|
model.rollbackAttributes();
|
|
|
|
}
|
|
|
|
|
|
|
|
// setting hasDirtyAttributes to false here allows willTransition on the editor route to succeed
|
|
|
|
this.set('hasDirtyAttributes', false);
|
|
|
|
|
|
|
|
// since the transition is now certain to complete, we can unset window.onbeforeunload here
|
|
|
|
window.onbeforeunload = null;
|
|
|
|
|
|
|
|
return transition.retry();
|
|
|
|
},
|
|
|
|
|
2017-07-22 16:25:00 +02:00
|
|
|
updateTitle(newTitle) {
|
|
|
|
this.set('model.titleScratch', newTitle);
|
2016-04-10 22:57:00 +02:00
|
|
|
},
|
|
|
|
|
2017-04-19 11:46:42 +02:00
|
|
|
toggleDeletePostModal() {
|
|
|
|
if (!this.get('model.isNew')) {
|
|
|
|
this.toggleProperty('showDeletePostModal');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-11-18 11:50:48 +01:00
|
|
|
toggleReAuthenticateModal() {
|
|
|
|
this.toggleProperty('showReAuthenticateModal');
|
2017-04-07 22:05:43 +02:00
|
|
|
},
|
|
|
|
|
2017-05-08 11:44:02 +02:00
|
|
|
setWordcount(wordcount) {
|
2017-04-25 13:32:27 +02:00
|
|
|
this.set('wordcount', wordcount);
|
2014-06-08 08:02:21 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|