From ff5af6c29bf0fb7ebf9bd4494505d5c51185ee5f Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Sun, 20 Apr 2014 08:48:34 -0600 Subject: [PATCH] Create Post Settings Menu and its functionality on the Post controller. closes #2419 - Added blur-text-field component, which fires actions on focusOut - Added utils/date-formatting for moment & date functionality consolidation - Added functionality to PostsPostController - Added fixtures: posts/3 & posts/4, posts/slug/test%20title/ - Added Post model saving - Set posts.post as controller for EditorRoute - Added PostSettingsMenuView and template - Added "showErrors" convenience method to notifications --- core/client/assets/css/ember-hacks.css | 6 +- core/client/components/blur-text-field.js | 13 ++ core/client/controllers/posts/post.js | 172 ++++++++++++++++++- core/client/fixtures/init.js | 6 +- core/client/models/post.js | 55 +++++- core/client/routes/editor.js | 10 +- core/client/routes/posts.js | 7 +- core/client/routes/posts/post.js | 5 +- core/client/templates/-floating-header.hbs | 40 +---- core/client/templates/-publish-bar.hbs | 38 +--- core/client/templates/post-settings-menu.hbs | 32 ++++ core/client/utils/date-formatting.js | 21 +++ core/client/utils/notifications.js | 7 +- core/client/views/post-item-view.js | 2 +- core/client/views/post-settings-menu-view.js | 18 ++ 15 files changed, 343 insertions(+), 89 deletions(-) create mode 100644 core/client/components/blur-text-field.js create mode 100644 core/client/templates/post-settings-menu.hbs create mode 100644 core/client/utils/date-formatting.js create mode 100644 core/client/views/post-settings-menu-view.js diff --git a/core/client/assets/css/ember-hacks.css b/core/client/assets/css/ember-hacks.css index ecd98ecffa..8ee77b8adf 100644 --- a/core/client/assets/css/ember-hacks.css +++ b/core/client/assets/css/ember-hacks.css @@ -3,10 +3,6 @@ The contents should be solved properly or moved into ghost-ui package. */ -.post-settings-menu { - display: none !important; -} - #entry-markdown, .entry-preview, .CodeMirror.cm-s-default { @@ -56,4 +52,4 @@ to { opacity: 1; } -} \ No newline at end of file +} diff --git a/core/client/components/blur-text-field.js b/core/client/components/blur-text-field.js new file mode 100644 index 0000000000..4f1798a31f --- /dev/null +++ b/core/client/components/blur-text-field.js @@ -0,0 +1,13 @@ +var BlurTextField = Ember.TextField.extend({ + selectOnClick: false, + click: function (event) { + if (this.get('selectOnClick')) { + event.currentTarget.select(); + } + }, + focusOut: function () { + this.sendAction('action', this.get('value')); + } +}); + +export default BlurTextField; diff --git a/core/client/controllers/posts/post.js b/core/client/controllers/posts/post.js index f81406d23b..ac10e1fa5e 100644 --- a/core/client/controllers/posts/post.js +++ b/core/client/controllers/posts/post.js @@ -1,8 +1,176 @@ +import {parseDateString, formatDate} from 'ghost/utils/date-formatting'; + var equal = Ember.computed.equal; var PostController = Ember.ObjectController.extend({ + isPublished: equal('status', 'published'), - isDraft: equal('status', 'draft') + isDraft: equal('status', 'draft'), + isEditingSettings: false, + isStaticPage: function (key, val) { + if (arguments.length > 1) { + this.set('model.page', val ? 1 : 0); + this.get('model').save('page').then(function () { + this.notifications.showSuccess('Succesfully converted ' + (val ? 'to static page' : 'to post')); + }, this.notifications.showErrors); + } + return !!this.get('model.page'); + }.property('model.page'), + + isOnServer: function () { + return this.get('model.id') !== undefined; + }.property('model.id'), + + newSlugBinding: Ember.Binding.oneWay('model.slug'), + slugPlaceholder: null, + // Requests a new slug when the title was changed + updateSlugPlaceholder: function () { + var model, + self = this, + title = this.get('title'); + + // If there's a title present we want to + // validate it against existing slugs in the db + // and then update the placeholder value. + if (title) { + model = self.get('model'); + model.generateSlug().then(function (slug) { + self.set('slugPlaceholder', slug); + }, function () { + self.notifications.showWarn('Unable to generate a slug for "' + title + '"'); + }); + } else { + // If there's no title set placeholder to blank + // and don't make an ajax request to server + // for a proper slug (as there won't be any). + self.set('slugPlaceholder', ''); + } + }.observes('model.title'), + + publishedAt: null, + publishedAtChanged: function () { + this.set('publishedAt', formatDate(this.get('model.published_at'))); + }.observes('model.published_at'), + + actions: { + editSettings: function () { + this.toggleProperty('isEditingSettings'); + if (this.get('isEditingSettings')) { + //Stop editing if the user clicks outside the settings view + Ember.run.next(this, function () { + var self = this; + // @TODO has a race condition with click on the editSettings action + $(document).one('click', function () { + self.toggleProperty('isEditingSettings'); + }); + }); + } + }, + updateSlug: function () { + var newSlug = this.get('newSlug'), + slug = this.get('model.slug'), + placeholder = this.get('slugPlaceholder'), + self = this; + + newSlug = (!newSlug && placeholder) ? placeholder : newSlug; + + // Ignore unchanged slugs + if (slug === newSlug) { + return; + } + //reset to model's slug on empty string + if (!newSlug) { + this.set('newSlug', slug); + return; + } + + //Validation complete + this.set('model.slug', newSlug); + + // If the model doesn't currently + // exist on the server + // then just update the model's value + if (!this.get('isOnServer')) { + return; + } + + this.get('model').save('slug').then(function () { + self.notifications.showSuccess('Permalink successfully changed to ' + this.get('model.slug') + '.'); + }, this.notifications.showErrors); + }, + + updatePublishedAt: function (userInput) { + var errMessage = '', + newPubDate = formatDate(parseDateString(userInput)), + pubDate = this.get('publishedAt'), + newPubDateMoment, + pubDateMoment; + + // if there is no new pub date, mark that until the post is published, + // when we'll fill in with the current time. + if (!newPubDate) { + this.set('publishedAt', ''); + return; + } + + // Check for missing time stamp on new data + // If no time specified, add a 12:00 + if (newPubDate && !newPubDate.slice(-5).match(/\d+:\d\d/)) { + newPubDate += " 12:00"; + } + + newPubDateMoment = parseDateString(newPubDate); + + // If there was a published date already set + if (pubDate) { + // Check for missing time stamp on current model + // If no time specified, add a 12:00 + if (!pubDate.slice(-5).match(/\d+:\d\d/)) { + pubDate += " 12:00"; + } + + pubDateMoment = parseDateString(pubDate); + + // Quit if the new date is the same + if (pubDateMoment.isSame(newPubDateMoment)) { + return; + } + } + + // Validate new Published date + if (!newPubDateMoment.isValid() || newPubDate.substr(0, 12) === "Invalid date") { + errMessage = 'Published Date must be a valid date with format: DD MMM YY @ HH:mm (e.g. 6 Dec 14 @ 15:00)'; + } + + if (newPubDateMoment.diff(new Date(), 'h') > 0) { + errMessage = 'Published Date cannot currently be in the future.'; + } + + if (errMessage) { + // Show error message + this.notifications.showError(errMessage); + //Hack to push a "change" when it's actually staying + // the same. + //This alerts the listener on post-settings-menu + this.notifyPropertyChange('publishedAt'); + return; + } + + //Validation complete + this.set('model.published_at', newPubDateMoment.toDate()); + + // If the model doesn't currently + // exist on the server + // then just update the model's value + if (!this.get('isOnServer')) { + return; + } + + this.get('model').save('published_at').then(function () { + this.notifications.showSuccess('Publish date successfully changed to ' + this.get('publishedAt') + '.'); + }, this.notifications.showErrors); + } + } }); -export default PostController; \ No newline at end of file +export default PostController; diff --git a/core/client/fixtures/init.js b/core/client/fixtures/init.js index a603924f52..bd4a7f3d59 100644 --- a/core/client/fixtures/init.js +++ b/core/client/fixtures/init.js @@ -34,6 +34,10 @@ var defineFixtures = function (status) { ic.ajax.defineFixture('/ghost/api/v0.1/posts', posts(status)); ic.ajax.defineFixture('/ghost/api/v0.1/posts/1', post(1, status)); ic.ajax.defineFixture('/ghost/api/v0.1/posts/2', post(2, status)); + ic.ajax.defineFixture('/ghost/api/v0.1/posts/3', post(3, status)); + ic.ajax.defineFixture('/ghost/api/v0.1/posts/4', post(4, status)); + ic.ajax.defineFixture('/ghost/api/v0.1/posts/slug/test%20title/', response('generated-slug', status)); + ic.ajax.defineFixture('/ghost/api/v0.1/signin', user(status)); ic.ajax.defineFixture('/ghost/api/v0.1/users/me/', user(status)); ic.ajax.defineFixture('/ghost/changepw/', response({ @@ -47,4 +51,4 @@ var defineFixtures = function (status) { })); }; -export default defineFixtures; \ No newline at end of file +export default defineFixtures; diff --git a/core/client/models/post.js b/core/client/models/post.js index 1c101ee55f..4c31a37dc6 100644 --- a/core/client/models/post.js +++ b/core/client/models/post.js @@ -1,5 +1,56 @@ -var PostModel = Ember.Object.extend({ +import BaseModel from 'ghost/models/base'; +var PostModel = BaseModel.extend({ + url: BaseModel.apiRoot + '/posts/', + + generateSlug: function () { + // @TODO Make this request use this.get('title') once we're an actual user + var url = this.get('url') + 'slug/' + encodeURIComponent('test title') + '/'; + return ic.ajax.request(url, { + type: 'GET' + }); + }, + + save: function (properties) { + var url = this.url, + self = this, + type, + validationErrors = this.validate(); + + if (validationErrors.length) { + return Ember.RSVP.Promise(function (resolve, reject) { + return reject(validationErrors); + }); + } + + //If specific properties are being saved, + //this is an edit. Otherwise, it's an add. + if (properties && properties.length > 0) { + type = 'PUT'; + url += this.get('id'); + } else { + type = 'POST'; + properties = Ember.keys(this); + } + + return ic.ajax.request(url, { + type: type, + data: this.getProperties(properties) + }).then(function (model) { + return self.setProperties(model); + }); + }, + validate: function () { + var validationErrors = []; + + if (!(this.get('title') && this.get('title').length)) { + validationErrors.push({ + message: "You must specify a title for the post." + }); + } + + return validationErrors; + } }); -export default PostModel; \ No newline at end of file +export default PostModel; diff --git a/core/client/routes/editor.js b/core/client/routes/editor.js index 5bda901bdd..baf4b4e1c4 100644 --- a/core/client/routes/editor.js +++ b/core/client/routes/editor.js @@ -1,13 +1,15 @@ import ajax from 'ghost/utils/ajax'; import styleBody from 'ghost/mixins/style-body'; import AuthenticatedRoute from 'ghost/routes/authenticated'; - +import Post from 'ghost/models/post'; var EditorRoute = AuthenticatedRoute.extend(styleBody, { classNames: ['editor'], - + controllerName: 'posts.post', model: function (params) { - return ajax('/ghost/api/v0.1/posts/' + params.post_id); + return ajax('/ghost/api/v0.1/posts/' + params.post_id).then(function (post) { + return Post.create(post); + }); } }); -export default EditorRoute; \ No newline at end of file +export default EditorRoute; diff --git a/core/client/routes/posts.js b/core/client/routes/posts.js index 85e871b1ff..278b15a989 100644 --- a/core/client/routes/posts.js +++ b/core/client/routes/posts.js @@ -1,13 +1,16 @@ import ajax from 'ghost/utils/ajax'; import styleBody from 'ghost/mixins/style-body'; import AuthenticatedRoute from 'ghost/routes/authenticated'; +import Post from 'ghost/models/post'; var PostsRoute = AuthenticatedRoute.extend(styleBody, { classNames: ['manage'], model: function () { return ajax('/ghost/api/v0.1/posts').then(function (response) { - return response.posts; + return response.posts.map(function (post) { + return Post.create(post); + }); }); }, @@ -18,4 +21,4 @@ var PostsRoute = AuthenticatedRoute.extend(styleBody, { } }); -export default PostsRoute; \ No newline at end of file +export default PostsRoute; diff --git a/core/client/routes/posts/post.js b/core/client/routes/posts/post.js index 550a9268ee..d857d854fa 100644 --- a/core/client/routes/posts/post.js +++ b/core/client/routes/posts/post.js @@ -1,7 +1,10 @@ /*global ajax */ +import Post from 'ghost/models/post'; var PostsPostRoute = Ember.Route.extend({ model: function (params) { - return ajax('/ghost/api/v0.1/posts/' + params.post_id); + return ajax('/ghost/api/v0.1/posts/' + params.post_id).then(function (post) { + return Post.create(post); + }); } }); diff --git a/core/client/templates/-floating-header.hbs b/core/client/templates/-floating-header.hbs index b5b25209e0..d450468c74 100644 --- a/core/client/templates/-floating-header.hbs +++ b/core/client/templates/-floating-header.hbs @@ -14,40 +14,8 @@ {{#link-to "editor" this class="post-edit" title="Edit Post"}} {{/link-to}} - - + + + {{view "post-settings-menu-view"}} - \ No newline at end of file + diff --git a/core/client/templates/-publish-bar.hbs b/core/client/templates/-publish-bar.hbs index 7d641d71d1..77f77a48a1 100644 --- a/core/client/templates/-publish-bar.hbs +++ b/core/client/templates/-publish-bar.hbs @@ -10,39 +10,9 @@
- -
-
- - - - - - - - - - - - - -
- - - -
- - - -
- Static Page - - - -
-
- Delete This Post -
+ + + {{view "post-settings-menu-view"}}
@@ -56,4 +26,4 @@
- \ No newline at end of file + diff --git a/core/client/templates/post-settings-menu.hbs b/core/client/templates/post-settings-menu.hbs new file mode 100644 index 0000000000..8fb62d243e --- /dev/null +++ b/core/client/templates/post-settings-menu.hbs @@ -0,0 +1,32 @@ +
+ + + + + + + + + + + + + + + +
+ + + {{blur-text-field class="post-setting-slug" id="url" value=newSlug action="updateSlug" placeholder=slugPlaceholder selectOnClick="true"}} +
+ + + {{blur-text-field class="post-setting-date" value=view.publishedAt action="updatePublishedAt" placeholder=view.datePlaceholder}} +
+ + + {{input type="checkbox" name="static-page" id="static-page" class="post-setting-static-page" checked=isStaticPage}} + +
+
+Delete This Post diff --git a/core/client/utils/date-formatting.js b/core/client/utils/date-formatting.js new file mode 100644 index 0000000000..1f8213a465 --- /dev/null +++ b/core/client/utils/date-formatting.js @@ -0,0 +1,21 @@ +/* global moment */ +var parseDateFormats = ["DD MMM YY HH:mm", + "DD MMM YYYY HH:mm", + "DD/MM/YY HH:mm", + "DD/MM/YYYY HH:mm", + "DD-MM-YY HH:mm", + "DD-MM-YYYY HH:mm", + "YYYY-MM-DD HH:mm"], + displayDateFormat = 'DD MMM YY @ HH:mm'; + +//Parses a string to a Moment +var parseDateString = function (value) { + return value ? moment(value, parseDateFormats) : ''; +}; + +//Formats a Date or Moment +var formatDate = function (value) { + return value ? moment(value).format(displayDateFormat) : ''; +}; + +export {parseDateString, formatDate}; diff --git a/core/client/utils/notifications.js b/core/client/utils/notifications.js index beb2190b3b..1ae22e5b77 100644 --- a/core/client/utils/notifications.js +++ b/core/client/utils/notifications.js @@ -15,6 +15,11 @@ var Notifications = Ember.ArrayProxy.extend({ message: message }); }, + showErrors: function (errors) { + for (var i = 0; i < errors.length; i += 1) { + this.showError(errors[i].message || errors[i]); + } + }, showInfo: function (message) { this.pushObject({ type: 'info', @@ -35,4 +40,4 @@ var Notifications = Ember.ArrayProxy.extend({ } }); -export default Notifications; \ No newline at end of file +export default Notifications; diff --git a/core/client/views/post-item-view.js b/core/client/views/post-item-view.js index 5bb22846ca..90851a0aaa 100644 --- a/core/client/views/post-item-view.js +++ b/core/client/views/post-item-view.js @@ -2,7 +2,7 @@ import itemView from 'ghost/views/item-view'; var PostItemView = itemView.extend({ openEditor: function () { - this.get('controller').send('openEditor', this.get('context')); // send action to handle transition to editor route + this.get('controller').send('openEditor', this.get('controller.model')); // send action to handle transition to editor route }.on("doubleClick") }); diff --git a/core/client/views/post-settings-menu-view.js b/core/client/views/post-settings-menu-view.js new file mode 100644 index 0000000000..4af1942940 --- /dev/null +++ b/core/client/views/post-settings-menu-view.js @@ -0,0 +1,18 @@ +/* global moment */ +import {formatDate} from 'ghost/utils/date-formatting'; + +var PostSettingsMenuView = Ember.View.extend({ + templateName: 'post-settings-menu', + classNames: ['post-settings-menu', 'menu-drop-right', 'overlay'], + classNameBindings: ['controller.isEditingSettings::hidden'], + publishedAtBinding: Ember.Binding.oneWay('controller.publishedAt'), + click: function (event) { + //Stop click propagation to prevent window closing + event.stopPropagation(); + }, + datePlaceholder: function () { + return formatDate(moment()); + }.property('controller.publishedAt') +}); + +export default PostSettingsMenuView;