diff --git a/core/client/controllers/settings/tags.js b/core/client/controllers/settings/tags.js index 92041fd6a2..5b83e9bfd3 100644 --- a/core/client/controllers/settings/tags.js +++ b/core/client/controllers/settings/tags.js @@ -4,13 +4,14 @@ import boundOneWay from 'ghost/utils/bound-one-way'; var TagsController = Ember.ArrayController.extend(PaginationMixin, { tags: Ember.computed.alias('model'), + needs: 'application', + activeTag: null, activeTagNameScratch: boundOneWay('activeTag.name'), activeTagSlugScratch: boundOneWay('activeTag.slug'), activeTagDescriptionScratch: boundOneWay('activeTag.description'), - - // Tag properties that should not be set to the empty string - requiredTagProperties: ['name', 'slug'], + activeTagMetaTitleScratch: boundOneWay('activeTag.meta_title'), + activeTagMetaDescriptionScratch: boundOneWay('activeTag.meta_description'), init: function (options) { options = options || {}; @@ -18,18 +19,30 @@ var TagsController = Ember.ArrayController.extend(PaginationMixin, { this._super(options); }, + isViewingSubview: Ember.computed('controllers.application.showSettingsMenu', function (key, value) { + // Not viewing a subview if we can't even see the PSM + if (!this.get('controllers.application.showSettingsMenu')) { + return false; + } + if (arguments.length > 1) { + return value; + } + + return false; + }), + + showErrors: function (errors) { + errors = Ember.isArray(errors) ? errors : [errors]; + this.notifications.showErrors(errors); + }, + saveActiveTagProperty: function (propKey, newValue) { var activeTag = this.get('activeTag'), currentValue = activeTag.get(propKey), - requiredTagProps = this.get('requiredTagProperties'), - self = this, - tagName; + self = this; newValue = newValue.trim(); - // Quit if value is empty for a required property - if (!newValue && requiredTagProps.contains(propKey)) { - return; - } + // Quit if there was no change if (newValue === currentValue) { return; @@ -37,19 +50,59 @@ var TagsController = Ember.ArrayController.extend(PaginationMixin, { activeTag.set(propKey, newValue); - tagName = activeTag.get('name'); - // don't save a new tag until it has a name - if (!tagName) { - return; - } + this.notifications.closePassive(); - activeTag.save().then(function () { - self.notifications.showSuccess('Saved ' + tagName); - }).catch(function (error) { - self.notifications.showAPIError(error); + activeTag.save().catch(function (errors) { + self.showErrors(errors); }); }, + seoTitle: Ember.computed('scratch', 'activeTagNameScratch', 'activeTagMetaTitleScratch', function () { + var metaTitle = this.get('activeTagMetaTitleScratch') || ''; + + metaTitle = metaTitle.length > 0 ? metaTitle : this.get('activeTagNameScratch'); + + if (metaTitle && metaTitle.length > 70) { + metaTitle = metaTitle.substring(0, 70).trim(); + metaTitle = Ember.Handlebars.Utils.escapeExpression(metaTitle); + metaTitle = new Ember.Handlebars.SafeString(metaTitle + '…'); + } + + return metaTitle; + }), + + seoURL: Ember.computed('activeTagSlugScratch', function () { + var blogUrl = this.get('config').blogUrl, + seoSlug = this.get('activeTagSlugScratch') ? this.get('activeTagSlugScratch') : '', + seoURL = blogUrl + '/tag/' + seoSlug; + + // only append a slash to the URL if the slug exists + if (seoSlug) { + seoURL += '/'; + } + + if (seoURL.length > 70) { + seoURL = seoURL.substring(0, 70).trim(); + seoURL = new Ember.Handlebars.SafeString(seoURL + '…'); + } + + return seoURL; + }), + + seoDescription: Ember.computed('scratch', 'activeTagDescriptionScratch', 'activeTagMetaDescriptionScratch', function () { + var metaDescription = this.get('activeTagMetaDescriptionScratch') || ''; + + metaDescription = metaDescription.length > 0 ? metaDescription : this.get('activeTagDescriptionScratch'); + + if (metaDescription && metaDescription.length > 156) { + metaDescription = metaDescription.substring(0, 156).trim(); + metaDescription = Ember.Handlebars.Utils.escapeExpression(metaDescription); + metaDescription = new Ember.Handlebars.SafeString(metaDescription + '…'); + } + + return metaDescription; + }), + actions: { newTag: function () { this.set('activeTag', this.store.createRecord('tag')); @@ -84,6 +137,22 @@ var TagsController = Ember.ArrayController.extend(PaginationMixin, { saveActiveTagDescription: function (description) { this.saveActiveTagProperty('description', description); + }, + + saveActiveTagMetaTitle: function (metaTitle) { + this.saveActiveTagProperty('meta_title', metaTitle); + }, + + saveActiveTagMetaDescription: function (metaDescription) { + this.saveActiveTagProperty('meta_description', metaDescription); + }, + + showSubview: function () { + this.set('isViewingSubview', true); + }, + + closeSubview: function () { + this.set('isViewingSubview', false); } } }); diff --git a/core/client/mixins/validation-engine.js b/core/client/mixins/validation-engine.js index 34b951855c..089f1ac06f 100644 --- a/core/client/mixins/validation-engine.js +++ b/core/client/mixins/validation-engine.js @@ -9,6 +9,7 @@ import ForgotValidator from 'ghost/validators/forgotten'; import SettingValidator from 'ghost/validators/setting'; import ResetValidator from 'ghost/validators/reset'; import UserValidator from 'ghost/validators/user'; +import TagSettingsValidator from 'ghost/validators/tag-settings'; // our extensions to the validator library ValidatorExtensions.init(); @@ -71,7 +72,8 @@ var ValidationEngine = Ember.Mixin.create({ forgotten: ForgotValidator, setting: SettingValidator, reset: ResetValidator, - user: UserValidator + user: UserValidator, + tag: TagSettingsValidator }, /** diff --git a/core/client/models/tag.js b/core/client/models/tag.js index 2ee7407b5c..bc16f8291d 100644 --- a/core/client/models/tag.js +++ b/core/client/models/tag.js @@ -1,6 +1,9 @@ +import ValidationEngine from 'ghost/mixins/validation-engine'; import NProgressSaveMixin from 'ghost/mixins/nprogress-save'; -var Tag = DS.Model.extend(NProgressSaveMixin, { +var Tag = DS.Model.extend(NProgressSaveMixin, ValidationEngine, { + validationType: 'tag', + uuid: DS.attr('string'), name: DS.attr('string'), slug: DS.attr('string'), diff --git a/core/client/templates/settings/tags/settings-menu.hbs b/core/client/templates/settings/tags/settings-menu.hbs index 4c530b3110..0ae8c811bd 100644 --- a/core/client/templates/settings/tags/settings-menu.hbs +++ b/core/client/templates/settings/tags/settings-menu.hbs @@ -1,6 +1,6 @@
-
-
+{{#gh-tabs-manager selected="showSubview" class="settings-menu-container"}} +

Tag Settings

+ {{/gh-tab}} + + {{#unless activeTag.isNew}} - + {{/unless}}
-
-
+
{{! .settings-menu-pane }} + +
+ {{#gh-tab-pane}} +
+ +

Meta Data

+
+ +
+
+
+ + {{gh-input type="text" value=activeTagMetaTitleScratch focus-out="saveActiveTagMetaTitle"}} +

Recommended: 70 characters. You’ve used {{gh-count-down-characters activeTagMetaTitleScratch 70}}

+
+ +
+ + {{gh-textarea value=activeTagMetaDescriptionScratch focus-out="saveActiveTagMetaDescription"}} +

Recommended: 156 characters. You’ve used {{gh-count-down-characters activeTagMetaDescriptionScratch 156}}

+
+ +
+ +
+
{{seoTitle}}
+ +
{{seoDescription}}
+
+ +
{{! .settings-menu-content }} + {{/gh-tab-pane}} +
{{! .settings-menu-pane }} +{{/gh-tabs-manager}} \ No newline at end of file diff --git a/core/client/validators/tag-settings.js b/core/client/validators/tag-settings.js new file mode 100644 index 0000000000..883b40bdfe --- /dev/null +++ b/core/client/validators/tag-settings.js @@ -0,0 +1,28 @@ +var TagSettingsValidator = Ember.Object.create({ + check: function (model) { + var validationErrors = [], + data = model.getProperties('name', 'meta_title', 'meta_description'); + + if (validator.empty(data.name)) { + validationErrors.push({ + message: 'You must specify a name for the tag.' + }); + } + + if (!validator.isLength(data.meta_title, 0, 150)) { + validationErrors.push({ + message: 'Meta Title cannot be longer than 150 characters.' + }); + } + + if (!validator.isLength(data.meta_description, 0, 200)) { + validationErrors.push({ + message: 'Meta Description cannot be longer than 200 characters.' + }); + } + + return validationErrors; + } +}); + +export default TagSettingsValidator;