Routable tags

refs #5845
- Updates tag settings screen to match content screen behaviour. Each now tag has it's own route that is link-able from other areas of the app
- Updates a number of places where jQuery event handler code was not wrapped in Ember's run loop
This commit is contained in:
Kevin Ansfield 2015-10-15 13:03:26 +01:00
parent 56de42b280
commit 89b7ff3320
27 changed files with 1276 additions and 305 deletions

View File

@ -18,20 +18,17 @@ export default Ember.Component.extend({
didInsertElement: function () {
var options = this.getProperties('lineNumbers', 'indentUnit', 'mode', 'theme'),
self = this,
editor;
editor = new CodeMirror(this.get('element'), options);
editor.getDoc().setValue(this.get('value'));
// events
editor.on('focus', function () {
self.set('isFocused', true);
editor.on('focus', Ember.run.bind(this, 'set', 'isFocused', true));
editor.on('blur', Ember.run.bind(this, 'set', 'isFocused', false));
editor.on('change', () => {
Ember.run(this, function () {
this.set('value', editor.getDoc().getValue());
});
editor.on('blur', function () {
self.set('isFocused', false);
});
editor.on('change', function () {
self.set('value', editor.getDoc().getValue());
});
this.set('editor', editor);

View File

@ -37,10 +37,12 @@ export default Ember.Component.extend(DropdownMixin, {
}
this.$().on('animationend webkitAnimationEnd oanimationend MSAnimationEnd', function (event) {
if (event.originalEvent.animationName === 'fade-out') {
if (self.get('closing')) {
self.set('isOpen', false);
self.set('closing', false);
Ember.run(self, function () {
if (this.get('closing')) {
this.set('isOpen', false);
this.set('closing', false);
}
});
}
});
},

View File

@ -0,0 +1,127 @@
/* global key */
import Ember from 'ember';
import boundOneWay from 'ghost/utils/bound-one-way';
const {get} = Ember;
export default Ember.Component.extend({
tag: null,
scratchName: boundOneWay('tag.name'),
scratchSlug: boundOneWay('tag.slug'),
scratchDescription: boundOneWay('tag.description'),
scratchMetaTitle: boundOneWay('tag.meta_title'),
scratchMetaDescription: boundOneWay('tag.meta_description'),
isViewingSubview: false,
config: Ember.inject.service(),
title: Ember.computed('tag.isNew', function () {
if (this.get('tag.isNew')) {
return 'New Tag';
} else {
return 'Tag Settings';
}
}),
seoTitle: Ember.computed('scratchName', 'scratchMetaTitle', function () {
let metaTitle = this.get('scratchMetaTitle') || '';
metaTitle = metaTitle.length > 0 ? metaTitle : this.get('scratchName');
if (metaTitle && metaTitle.length > 70) {
metaTitle = metaTitle.substring(0, 70).trim();
metaTitle = Ember.Handlebars.Utils.escapeExpression(metaTitle);
metaTitle = Ember.String.htmlSafe(metaTitle + '…');
}
return metaTitle;
}),
seoURL: Ember.computed('scratchSlug', function () {
const blogUrl = this.get('config.blogUrl'),
seoSlug = this.get('scratchSlug') || '';
let 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 = Ember.String.htmlSafe(seoURL + '…');
}
return seoURL;
}),
seoDescription: Ember.computed('scratchDescription', 'scratchMetaDescription', function () {
let metaDescription = this.get('scratchMetaDescription') || '';
metaDescription = metaDescription.length > 0 ? metaDescription : this.get('scratchDescription');
if (metaDescription && metaDescription.length > 156) {
metaDescription = metaDescription.substring(0, 156).trim();
metaDescription = Ember.Handlebars.Utils.escapeExpression(metaDescription);
metaDescription = Ember.String.htmlSafe(metaDescription + '…');
}
return metaDescription;
}),
didReceiveAttrs: function (attrs) {
if (get(attrs, 'newAttrs.tag.value.id') !== get(attrs, 'oldAttrs.tag.value.id')) {
this.reset();
}
},
reset: function () {
this.set('isViewingSubview', false);
if (this.$()) {
this.$('.settings-menu-pane').scrollTop(0);
}
},
focusIn: function () {
key.setScope('tag-settings-form');
},
focusOut: function () {
key.setScope('default');
},
actions: {
setProperty: function (property, value) {
this.attrs.setProperty(property, value);
},
setCoverImage: function (image) {
this.attrs.setProperty('image', image);
},
clearCoverImage: function () {
this.attrs.setProperty('image', '');
},
setUploaderReference: function () {
// noop
},
openMeta: function () {
this.set('isViewingSubview', true);
},
closeMeta: function () {
this.set('isViewingSubview', false);
},
deleteTag: function () {
this.sendAction('openModal', 'delete-tag', this.get('tag'));
}
}
});

View File

@ -10,29 +10,6 @@ export default Ember.Component.extend({
return this.get('image') || '';
}),
/**
* Sets up the uploader on render
*/
setup: function () {
var $this = this.$(),
self = this;
// this.set('uploaderReference', uploader.call($this, {
// editor: true,
// fileStorage: this.get('config.fileStorage')
// }));
$this.on('uploadsuccess', function (event, result) {
if (result && result !== '' && result !== 'http://') {
self.sendAction('uploaded', result);
}
});
$this.on('imagecleared', function () {
self.sendAction('canceled');
});
},
// removes event listeners from the uploader
removeListeners: function () {
var $this = this.$();
@ -41,17 +18,6 @@ export default Ember.Component.extend({
$this.find('.js-cancel').off();
},
// didInsertElement: function () {
// Ember.run.scheduleOnce('afterRender', this, this.setup());
// },
didInsertElement: function () {
this.send('initUploader');
},
willDestroyElement: function () {
this.removeListeners();
},
// NOTE: because the uploader is sometimes in the same place in the DOM
// between transitions Glimmer will re-use the existing elements including
// those that arealready decorated by jQuery. The following works around
@ -77,13 +43,20 @@ export default Ember.Component.extend({
}
},
didInsertElement: function () {
this.send('initUploader');
},
willDestroyElement: function () {
this.removeListeners();
},
actions: {
initUploader: function () {
var ref,
el,
el = this.$(),
self = this;
el = this.$();
ref = uploader.call(el, {
editor: true,
fileStorage: this.get('config.fileStorage')
@ -91,13 +64,13 @@ export default Ember.Component.extend({
el.on('uploadsuccess', function (event, result) {
if (result && result !== '' && result !== 'http://') {
self.sendAction('uploaded', result);
Ember.run(self, function () {
this.sendAction('uploaded', result);
});
}
});
el.on('imagecleared', function () {
self.sendAction('canceled');
});
el.on('imagecleared', Ember.run.bind(self, 'sendAction', 'canceled'));
this.sendAction('initUploader', ref);
}

View File

@ -2,6 +2,7 @@ import Ember from 'ember';
export default Ember.Controller.extend({
notifications: Ember.inject.service(),
application: Ember.inject.controller(),
postInflection: Ember.computed('model.post_count', function () {
return this.get('model.post_count') > 1 ? 'posts' : 'post';
@ -9,13 +10,18 @@ export default Ember.Controller.extend({
actions: {
confirmAccept: function () {
var tag = this.get('model'),
self = this;
var tag = this.get('model');
this.send('closeMenus');
tag.destroyRecord().catch(function (error) {
self.get('notifications').showAPIError(error, {key: 'tag.delete'});
tag.destroyRecord().then(() => {
let currentRoute = this.get('application.currentRouteName') || '';
if (currentRoute.match(/^settings\.tags/)) {
this.transitionToRoute('settings.tags.index');
}
}).catch((error) => {
this.get('notifications').showAPIError(error, {key: 'tag.delete'});
});
},

View File

@ -1,153 +1,21 @@
import Ember from 'ember';
import SettingsMenuMixin from 'ghost/mixins/settings-menu-controller';
import boundOneWay from 'ghost/utils/bound-one-way';
export default Ember.Controller.extend(SettingsMenuMixin, {
tags: Ember.computed.alias('model'),
export default Ember.Controller.extend({
activeTag: null,
activeTagNameScratch: boundOneWay('activeTag.name'),
activeTagSlugScratch: boundOneWay('activeTag.slug'),
activeTagDescriptionScratch: boundOneWay('activeTag.description'),
activeTagMetaTitleScratch: boundOneWay('activeTag.meta_title'),
activeTagMetaDescriptionScratch: boundOneWay('activeTag.meta_description'),
tagListFocused: Ember.computed.equal('keyboardFocus', 'tagList'),
tagContentFocused: Ember.computed.equal('keyboardFocus', 'tagContent'),
application: Ember.inject.controller(),
config: Ember.inject.service(),
notifications: Ember.inject.service(),
tags: Ember.computed.sort('model', function (a, b) {
const idA = +a.get('id'),
idB = +b.get('id');
uploaderReference: null,
// This observer loads and resets the uploader whenever the active tag changes,
// ensuring that we can reuse the whole settings menu.
updateUploader: Ember.observer('activeTag.image', 'uploaderReference', function () {
var uploader = this.get('uploaderReference'),
image = this.get('activeTag.image');
if (uploader && uploader[0]) {
if (image) {
uploader[0].uploaderUi.initWithImage();
} else {
uploader[0].uploaderUi.reset();
}
}
}),
saveActiveTagProperty: function (propKey, newValue) {
var activeTag = this.get('activeTag'),
currentValue = activeTag.get(propKey),
self = this;
newValue = newValue.trim();
// Quit if there was no change
if (newValue === currentValue) {
return;
if (idA > idB) {
return 1;
} else if (idA < idB) {
return -1;
}
activeTag.set(propKey, newValue);
activeTag.get('hasValidated').addObject(propKey);
return 0;
})
activeTag.save().catch(function (error) {
if (error) {
self.get('notifications').showAPIError(error, {key: 'tag.save'});
}
});
},
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 = Ember.String.htmlSafe(metaTitle + '&hellip;');
}
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 = Ember.String.htmlSafe(seoURL + '&hellip;');
}
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 = Ember.String.htmlSafe(metaDescription + '&hellip;');
}
return metaDescription;
}),
actions: {
newTag: function () {
this.set('activeTag', this.store.createRecord('tag', {post_count: 0}));
this.get('activeTag.errors').clear();
this.send('openSettingsMenu');
},
editTag: function (tag) {
tag.validate();
this.set('activeTag', tag);
this.send('openSettingsMenu');
},
saveActiveTagName: function (name) {
this.saveActiveTagProperty('name', name);
},
saveActiveTagSlug: function (slug) {
this.saveActiveTagProperty('slug', slug);
},
saveActiveTagDescription: function (description) {
this.saveActiveTagProperty('description', description);
},
saveActiveTagMetaTitle: function (metaTitle) {
this.saveActiveTagProperty('meta_title', metaTitle);
},
saveActiveTagMetaDescription: function (metaDescription) {
this.saveActiveTagProperty('meta_description', metaDescription);
},
setCoverImage: function (image) {
this.saveActiveTagProperty('image', image);
},
clearCoverImage: function () {
this.saveActiveTagProperty('image', '');
},
closeNavMenu: function () {
this.get('application').send('closeNavMenu');
},
setUploaderReference: function (ref) {
this.set('uploaderReference', ref);
}
}
});

View File

@ -0,0 +1,40 @@
import Ember from 'ember';
const {computed} = Ember,
{alias} = computed;
export default Ember.Controller.extend({
tag: alias('model'),
saveTagProperty: function (propKey, newValue) {
const tag = this.get('tag'),
currentValue = tag.get(propKey);
newValue = newValue.trim();
// Quit if there was no change
if (newValue === currentValue) {
return;
}
tag.set(propKey, newValue);
// TODO: This is required until .validate/.save mark fields as validated
tag.get('hasValidated').addObject(propKey);
tag.save().then((savedTag) => {
// replace 'new' route with 'tag' route
this.replaceWith('settings.tags.tag', savedTag);
}).catch((error) => {
if (error) {
this.notifications.showAPIError(error, {key: 'tag.save'});
}
});
},
actions: {
setProperty: function (propKey, value) {
this.saveTagProperty(propKey, value);
}
}
});

View File

@ -60,7 +60,9 @@ export default Ember.Mixin.create({
key(shortcut, scope, function (event) {
// stop things like ctrl+s from actually opening a save dialogue
event.preventDefault();
self.send(action, options);
Ember.run(self, function () {
this.send(action, options);
});
});
});
},

View File

@ -43,7 +43,10 @@ Router.map(function () {
});
this.route('settings.general', {path: '/settings/general'});
this.route('settings.tags', {path: '/settings/tags'});
this.route('settings.tags', {path: '/settings/tags'}, function () {
this.route('tag', {path: ':tag_id'});
this.route('new');
});
this.route('settings.labs', {path: '/settings/labs'});
this.route('settings.code-injection', {path: '/settings/code-injection'});
this.route('settings.navigation', {path: '/settings/navigation'});

View File

@ -1,8 +1,10 @@
import Ember from 'ember';
import AuthenticatedRoute from 'ghost/routes/authenticated';
import CurrentUserSettings from 'ghost/mixins/current-user-settings';
import PaginationRouteMixin from 'ghost/mixins/pagination-route';
import ShortcutsRoute from 'ghost/mixins/shortcuts-route';
import PaginationRoute from 'ghost/mixins/pagination-route';
export default AuthenticatedRoute.extend(CurrentUserSettings, PaginationRouteMixin, {
export default AuthenticatedRoute.extend(CurrentUserSettings, PaginationRoute, ShortcutsRoute, {
titleToken: 'Settings - Tags',
paginationModel: 'tag',
@ -11,6 +13,14 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, PaginationRouteMix
limit: 15
},
shortcuts: {
'up, k': 'moveUp',
'down, j': 'moveDown',
left: 'focusList',
right: 'focusContent',
c: 'newTag'
},
beforeModel: function () {
this._super(...arguments);
@ -20,22 +30,70 @@ export default AuthenticatedRoute.extend(CurrentUserSettings, PaginationRouteMix
model: function () {
this.store.unloadAll('tag');
this.loadFirstPage();
return this.store.filter('tag', function (tag) {
return this.loadFirstPage().then(() => {
return this.store.filter('tag', (tag) => {
return !tag.get('isNew');
});
},
renderTemplate: function (controller, model) {
this._super(controller, model);
this.render('settings/tags/settings-menu', {
into: 'application',
outlet: 'settings-menu'
});
},
deactivate: function () {
this.send('resetPagination');
},
stepThroughTags: function (step) {
let currentTag = this.modelFor('settings.tags.tag'),
tags = this.get('controller.tags'),
length = tags.get('length');
if (currentTag && length) {
let newPosition = tags.indexOf(currentTag) + step;
if (newPosition >= length) {
return;
} else if (newPosition < 0) {
return;
}
this.transitionTo('settings.tags.tag', tags.objectAt(newPosition));
}
},
scrollContent: function (amount) {
let content = Ember.$('.tag-settings-pane'),
scrolled = content.scrollTop();
content.scrollTop(scrolled + 50 * amount);
},
actions: {
moveUp: function () {
if (this.controller.get('tagContentFocused')) {
this.scrollContent(-1);
} else {
this.stepThroughTags(-1);
}
},
moveDown: function () {
if (this.controller.get('tagContentFocused')) {
this.scrollContent(1);
} else {
this.stepThroughTags(1);
}
},
focusList: function () {
this.set('controller.keyboardFocus', 'tagList');
},
focusContent: function () {
this.set('controller.keyboardFocus', 'tagContent');
},
newTag: function () {
this.transitionTo('settings.tags.new');
}
}
});

View File

@ -0,0 +1,13 @@
import AuthenticatedRoute from 'ghost/routes/authenticated';
export default AuthenticatedRoute.extend({
beforeModel: function () {
const firstTag = this.modelFor('settings.tags').get('firstObject');
if (firstTag) {
this.transitionTo('settings.tags.tag', firstTag);
}
}
});

View File

@ -0,0 +1,15 @@
import AuthenticatedRoute from 'ghost/routes/authenticated';
export default AuthenticatedRoute.extend({
controllerName: 'settings.tags.tag',
model: function () {
return this.store.createRecord('tag');
},
renderTemplate: function () {
this.render('settings.tags.tag');
}
});

View File

@ -0,0 +1,9 @@
import AuthenticatedRoute from 'ghost/routes/authenticated';
export default AuthenticatedRoute.extend({
model: function (params) {
return this.store.findRecord('tag', params.tag_id);
}
});

View File

@ -267,7 +267,9 @@
/* This has to be a pseudo element to sit over the top of everything else in the content list */
.content-list.keyboard-focused:before {
.content-list.keyboard-focused:before,
.tag-list-content.keyboard-focused:before,
.tag-settings.keyboard-focused:before {
content: "";
position: absolute;
top: 0;

View File

@ -13,15 +13,14 @@
}
.settings-tag .tag-edit-button {
display: block;
padding: 20px;
width: calc(100% + 45px);
text-align: left;
}
.settings-tag .tag-edit-button:hover,
.settings-tag .tag-edit-button:focus,
.settings-tag .tag-edit-button:active {
background: color(#dfe1e3 lightness(+10%));
.settings-tag .tag-edit-button.active {
border-left: 3px solid;
}
.settings-tag:last-of-type:hover .tag-edit-button {
@ -29,9 +28,12 @@
}
.settings-tag .label {
position: relative;
top: -2px;
margin-left: 2px;
display: inline-block;
overflow: hidden;
max-width: 100%;
vertical-align: middle;
text-overflow: ellipsis;
white-space: nowrap;
}
.settings-tag .label-alt {
@ -58,3 +60,40 @@
color: color(#dfe1e3 lightness(-10%));
font-size: 16px;
}
/* Tag List (Left pane)
/* ---------------------------------------------------------- */
.tag-list {
position: absolute;
top: 0;
bottom: 0;
left: 0;
overflow: auto;
max-width: calc(100% - 350px);
width: 66%;
border-right: #dfe1e3 1px solid;
background: #fff;
}
/* Tag Settings (Right pane)
/* ---------------------------------------------------------- */
.tag-settings {
position: absolute;
top: 0;
right: 0;
bottom: 0;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
min-width: 350px;
width: 34%;
border: none;
background: #fff;
transform: none;
}
.tag-settings .no-posts h3 {
text-align: center;
}

View File

@ -0,0 +1,78 @@
<div class="{{if isViewingSubview 'settings-menu-pane-out-left' 'settings-menu-pane-in'}} settings-menu settings-menu-pane tag-settings-pane">
<div class="settings-menu-header">
<h4>{{title}}</h4>
</div>
<div class="settings-menu-content">
{{gh-uploader uploaded="setCoverImage" canceled="clearCoverImage" description="Add tag image" image=tag.image initUploader="setUploaderReference" tagName="section"}}
<form>
{{#gh-form-group errors=tag.errors hasValidated=tag.hasValidated property="name"}}
<label for="tag-name">Name</label>
{{gh-input id="tag-name" name="name" type="text" value=scratchName focus-out=(action 'setProperty' 'name')}}
{{gh-error-message errors=tag.errors property="name"}}
{{/gh-form-group}}
{{#gh-form-group errors=tag.errors hasValidated=tag.hasValidated property="slug"}}
<label for="tag-slug">URL</label>
{{gh-input id="tag-slug" name="slug" type="text" value=scratchSlug focus-out=(action 'setProperty' 'slug')}}
{{gh-url-preview prefix="tag" slug=scratchSlug tagName="p" classNames="description"}}
{{gh-error-message errors=activeTag.errors property="slug"}}
{{/gh-form-group}}
{{#gh-form-group errors=tag.errors hasValidated=tag.hasValidated property="description"}}
<label for="tag-description">Description</label>
{{gh-textarea id="tag-description" name="description" value=scratchDescription focus-out=(action 'setProperty' 'description')}}
{{gh-error-message errors=tag.errors property="description"}}
<p>Maximum: <b>200</b> characters. Youve used {{gh-count-down-characters scratchDescription 200}}</p>
{{/gh-form-group}}
<ul class="nav-list nav-list-block">
<li class="nav-list-item" {{action 'openMeta'}}>
<button type="button" class="meta-data-button">
<b>Meta Data</b>
<span>Extra content for SEO and social media.</span>
</button>
<i class="icon-arrow-right"></i>
</li>
</ul>
{{#unless tag.isNew}}
<button type="button" class="btn btn-link btn-sm tag-delete-button" {{action "deleteTag"}}><i class="icon-trash"></i> Delete Tag</button>
{{/unless}}
</form>
</div>
</div>{{! .settings-menu-pane }}
<div class="{{if isViewingSubview 'settings-menu-pane-in' 'settings-menu-pane-out-right'}} settings-menu settings-menu-pane tag-meta-settings-pane">
<div class="settings-menu-header subview">
<button {{action "closeMeta"}} class="back icon-arrow-left settings-menu-header-action"><span class="hidden">Back</span></button>
<h4>Meta Data</h4>
<div style="width:23px;">{{!flexbox space-between}}</div>
</div>
<div class="settings-menu-content">
<form>
{{#gh-form-group errors=tag.errors hasValidated=tag.hasValidated property="meta_title"}}
<label for="meta-title">Meta Title</label>
{{gh-input id="meta-title" name="meta_title" type="text" value=scratchMetaTitle focus-out=(action 'setProperty' 'meta_title')}}
{{gh-error-message errors=tag.errors property="meta_title"}}
<p>Recommended: <b>70</b> characters. Youve used {{gh-count-down-characters scratchMetaTitle 70}}</p>
{{/gh-form-group}}
{{#gh-form-group errors=tag.errors hasValidated=tag.hasValidated property="meta_description"}}
<label for="meta-description">Meta Description</label>
{{gh-textarea id="meta-description" name="meta_description" value=scratchMetaDescription focus-out=(action 'setProperty' 'meta_description')}}
{{gh-error-message errors=tag.errors property="meta_description"}}
<p>Recommended: <b>156</b> characters. Youve used {{gh-count-down-characters scratchMetaDescription 156}}</p>
{{/gh-form-group}}
<div class="form-group">
<label>Search Engine Result Preview</label>
<div class="seo-preview">
<div class="seo-preview-title">{{seoTitle}}</div>
<div class="seo-preview-link">{{seoURL}}</div>
<div class="seo-preview-description">{{seoDescription}}</div>
</div>
</div>
</form>
</div>
</div>

View File

@ -2,25 +2,32 @@
<header class="view-header">
{{#gh-view-title openMobileMenu="openMobileMenu"}}<span>Tags</span>{{/gh-view-title}}
<section class="view-actions">
<button type="button" class="btn btn-green" {{action "newTag"}}>New Tag</button>
{{#link-to "settings.tags.new" class="btn btn-green" title="New Tag"}}New Tag{{/link-to}}
{{!-- <button type="button" class="btn btn-green" {{action "newTag"}}>New Tag</button> --}}
</section>
</header>
<div class="view-container">
{{#gh-infinite-scroll
fetch="loadNextPage"
isLoading=isLoading
tagName="section"
classNames="view-container settings-tags"
classNames="tag-list"
}}
<section class="tag-list-content settings-tags {{if tagListFocused 'keyboard-focused'}}">
{{#each tags as |tag|}}
<div class="settings-tag">
<button class="tag-edit-button" {{action "editTag" tag}}>
{{#link-to 'settings.tags.tag' tag.id class="tag-edit-button"}}
<span class="tag-title">{{tag.name}}</span>
<span class="label label-default">/{{tag.slug}}</span>
<p class="tag-description">{{tag.description}}</p>
<span class="tags-count">{{tag.post_count}}</span>
</button>
{{/link-to}}
</div>
{{/each}}
{{/gh-infinite-scroll}}
</section>
{{/gh-infinite-scroll}}
<section class="settings-menu-container tag-settings {{if tagContentFocused 'keyboard-focused'}}">
{{outlet}}
</section>
</div>
</section>

View File

@ -0,0 +1,6 @@
<div class="no-posts-box">
<div class="no-posts">
<h3>You haven't added any Tags yet!</h3>
{{#link-to "settings.tags.new"}}<button type="button" class="btn btn-green btn-lg" title="New Tag">Add a Tag</button>{{/link-to}}
</div>
</div>

View File

@ -0,0 +1 @@
{{gh-tag-settings-form tag=tag setProperty=(action "setProperty") openModal="openModal"}}

View File

@ -50,11 +50,7 @@ export default function () {
}),
setTitle: function (title) {
if (Ember.testing) {
this._title = title;
} else {
window.document.title = title;
}
}
});
}

View File

@ -10,60 +10,9 @@ import Ember from 'ember';
import startApp from '../../helpers/start-app';
import Pretender from 'pretender';
import { invalidateSession, authenticateSession } from 'ghost/tests/helpers/ember-simple-auth';
import requiredSettings from '../../fixtures/settings';
const {run} = Ember,
// TODO: Pull this into a fixture or similar when required elsewhere
requiredSettings = [{
created_at: '2015-09-11T09:44:30.805Z',
created_by: 1,
id: 5,
key: 'title',
type: 'blog',
updated_at: '2015-10-04T16:26:05.195Z',
updated_by: 1,
uuid: '39e16daf-43fa-4bf0-87d4-44948ba8bf4c',
value: 'The Daily Awesome'
}, {
created_at: '2015-09-11T09:44:30.806Z',
created_by: 1,
id: 6,
key: 'description',
type: 'blog',
updated_at: '2015-10-04T16:26:05.198Z',
updated_by: 1,
uuid: 'e6c8b636-6925-4c4a-a5d9-1dc0870fb8ea',
value: 'Thoughts, stories and ideas.'
}, {
created_at: '2015-09-11T09:44:30.809Z',
created_by: 1,
id: 10,
key: 'postsPerPage',
type: 'blog',
updated_at: '2015-10-04T16:26:05.211Z',
updated_by: 1,
uuid: '775e6ca1-bcc3-4347-a53d-15d5d76c04a4',
value: '5'
}, {
created_at: '2015-09-11T09:44:30.809Z',
created_by: 1,
id: 13,
key: 'ghost_head',
type: 'blog',
updated_at: '2015-09-23T13:32:49.858Z',
updated_by: 1,
uuid: 'df7f3151-bc08-4a77-be9d-dd315b630d51',
value: ''
}, {
created_at: '2015-09-11T09:44:30.809Z',
created_by: 1,
id: 14,
key: 'ghost_foot',
type: 'blog',
updated_at: '2015-09-23T13:32:49.858Z',
updated_by: 1,
uuid: '0649d45e-828b-4dd0-8381-3dff6d1d5ddb',
value: ''
}];
const {run} = Ember;
describe('Acceptance: Settings - Navigation', function () {
let application,
@ -72,7 +21,7 @@ describe('Acceptance: Settings - Navigation', function () {
beforeEach(function () {
application = startApp();
store = application.__container__.lookup('store:main');
store = application.__container__.lookup('service:store');
server = new Pretender(function () {
// TODO: This needs to either be fleshed out to include all user data, or be killed with fire
// as it needs to be loaded with all authenticated page loads

View File

@ -0,0 +1,375 @@
/* jshint expr:true */
import {
describe,
it,
beforeEach,
afterEach
} from 'mocha';
import { expect } from 'chai';
import Ember from 'ember';
import startApp from '../../helpers/start-app';
import Pretender from 'pretender';
import { invalidateSession, authenticateSession } from 'ghost/tests/helpers/ember-simple-auth';
import requiredSettings from '../../fixtures/settings';
const {run} = Ember,
// Grabbed from keymaster's testing code because Ember's `keyEvent` helper
// is for some reason not triggering the events in a way that keymaster detects:
// https://github.com/madrobby/keymaster/blob/master/test/keymaster.html#L31
modifierMap = {
16:'shiftKey',
18:'altKey',
17:'ctrlKey',
91:'metaKey'
},
keydown = function (code, modifiers, el) {
let event = document.createEvent('Event');
event.initEvent('keydown', true, true);
event.keyCode = code;
if (modifiers && modifiers.length > 0) {
for (let i in modifiers) {
event[modifierMap[modifiers[i]]] = true;
}
}
(el || document).dispatchEvent(event);
},
keyup = function (code, el) {
let event = document.createEvent('Event');
event.initEvent('keyup', true, true);
event.keyCode = code;
(el || document).dispatchEvent(event);
};
describe('Acceptance: Settings - Tags', function () {
let application,
store,
server,
roleName;
beforeEach(function () {
application = startApp();
store = application.__container__.lookup('service:store');
server = new Pretender(function () {
// TODO: This needs to either be fleshed out to include all user data, or be killed with fire
// as it needs to be loaded with all authenticated page loads
this.get('/ghost/api/v0.1/users/me', function () {
return [200, {'Content-Type': 'application/json'}, JSON.stringify({users: [{
id: '1',
roles: [{
id: 1,
name: roleName,
slug: 'barry'
}]
}]})];
});
this.get('/ghost/api/v0.1/settings/', function (_request) {
let response = {meta: {filters: 'blog,theme'}};
response.settings = requiredSettings;
return [200, {'Content-Type': 'application/json'}, JSON.stringify(response)];
});
// TODO: This will be needed for all authenticated page loads
// - is there some way to make this a default?
this.get('/ghost/api/v0.1/notifications/', function (_request) {
return [200, {'Content-Type': 'application/json'}, JSON.stringify({notifications: []})];
});
this.get('/ghost/api/v0.1/tags/', function (_request) {
let response = {};
response.meta = {
pagination: {
page: 1,
limit: 15,
pages: 1,
total: 2,
next: null,
prev: null
}
};
response.tags = [
{
id: 1,
parent: null,
uuid: 'e2016ef1-4b51-46ff-9388-c6f066fc2e6c',
image: '/content/images/2015/10/tag-1.jpg',
name: 'Tag One',
slug: 'tag-one',
description: 'Description one.',
meta_title: 'Meta Title One',
meta_description: 'Meta description one.',
created_at: '2015-09-11T09:44:29.871Z',
created_by: 1,
updated_at: '2015-10-19T16:25:07.756Z',
updated_by: 1,
hidden: false,
post_count: 1
},
{
id: 2,
parent: null,
uuid: '0cade0f9-7a3f-4fd1-a80a-3a1ab7028340',
image: '/content/images/2015/10/tag-2.jpg',
name: 'Tag Two',
slug: 'tag-two',
description: 'Description two.',
meta_title: 'Meta Title Two',
meta_description: 'Meta description two.',
created_at: '2015-09-11T09:44:29.871Z',
created_by: 1,
updated_at: '2015-10-19T16:25:07.756Z',
updated_by: 1,
hidden: false,
post_count: 2
}
];
return [200, {'Content-Type': 'application/json'}, JSON.stringify(response)];
});
this.put('/ghost/api/v0.1/tags/2/', function (_request) {
let response = {};
response.tag = {
id: 2,
parent: null,
uuid: '0cade0f9-7a3f-4fd1-a80a-3a1ab7028340',
image: '/content/images/2015/10/tag-2.jpg',
name: 'Saved Tag',
slug: 'tag-two',
description: 'Description two.',
meta_title: 'Meta Title Two',
meta_description: 'Meta description two.',
created_at: '2015-09-11T09:44:29.871Z',
created_by: 1,
updated_at: '2015-10-19T16:25:07.756Z',
updated_by: 1,
hidden: false,
post_count: 2
};
return [200, {'Content-Type': 'application/json'}, JSON.stringify(response)];
});
this.post('/ghost/api/v0.1/tags/', function (_request) {
let response = {};
response.tag = {
id: 3,
parent: null,
uuid: 'de9f4636-0398-4e23-a963-e073d12bc511',
image: '/content/images/2015/10/tag-3.jpg',
name: 'Tag Three',
slug: 'tag-three',
description: 'Description three.',
meta_title: 'Meta Title Three',
meta_description: 'Meta description three.',
created_at: '2015-09-11T09:44:29.871Z',
created_by: 1,
updated_at: '2015-10-19T16:25:07.756Z',
updated_by: 1,
hidden: false,
post_count: 2
};
return [200, {'Content-Type': 'application/json'}, JSON.stringify(response)];
});
this.delete('/ghost/api/v0.1/tags/3/', function (_request) {
let response = {tags: []};
response.tags.push({
id: 3,
parent: null,
uuid: 'de9f4636-0398-4e23-a963-e073d12bc511',
image: '/content/images/2015/10/tag-3.jpg',
name: 'Tag Three',
slug: 'tag-three',
description: 'Description three.',
meta_title: 'Meta Title Three',
meta_description: 'Meta description three.',
created_at: '2015-09-11T09:44:29.871Z',
created_by: 1,
updated_at: '2015-10-19T16:25:07.756Z',
updated_by: 1,
hidden: false,
post_count: 2
});
return [200, {'Content-Type': 'application/json'}, JSON.stringify(response)];
});
});
});
afterEach(function () {
Ember.run(application, 'destroy');
});
it('redirects to signin when not authenticated', function () {
invalidateSession(application);
visit('/settings/tags');
andThen(() => {
expect(currentURL()).to.equal('/signin');
});
});
it('redirects to team page when authenticated as author', function () {
roleName = 'Author';
authenticateSession(application);
visit('/settings/navigation');
andThen(() => {
expect(currentURL()).to.match(/^\/team\//);
});
});
describe('when logged in', function () {
beforeEach(function () {
roleName = 'Administrator';
authenticateSession(application);
});
it('it renders, can be navigated, can edit, create & delete tags', function () {
visit('/settings/tags');
andThen(() => {
// it redirects to first tag
expect(currentURL(), 'currentURL').to.equal('/settings/tags/1');
// it has correct page title
expect(document.title, 'page title').to.equal('Settings - Tags - Test Blog');
// it highlights nav menu
expect($('.gh-nav-settings-tags').hasClass('active'), 'highlights nav menu item')
.to.be.true;
// it lists all tags
expect(find('.settings-tags .settings-tag').length, 'tag list count')
.to.equal(2);
expect(find('.settings-tags .settings-tag:first .tag-title').text(), 'tag list item title')
.to.equal('Tag One');
// it highlights selected tag
expect(find('a[href="/settings/tags/1"]').hasClass('active'), 'highlights selected tag')
.to.be.true;
// it shows selected tag form
expect(find('.tag-settings-pane h4').text(), 'settings pane title')
.to.equal('Tag Settings');
expect(find('.tag-settings-pane input[name="name"]').val(), 'loads correct tag into form')
.to.equal('Tag One');
});
// click the second tag in the list
click('.tag-edit-button:last');
andThen(() => {
// it navigates to selected tag
expect(currentURL(), 'url after clicking tag').to.equal('/settings/tags/2');
// it highlights selected tag
expect(find('a[href="/settings/tags/2"]').hasClass('active'), 'highlights selected tag')
.to.be.true;
// it shows selected tag form
expect(find('.tag-settings-pane input[name="name"]').val(), 'loads correct tag into form')
.to.equal('Tag Two');
});
andThen(() => {
// simulate up arrow press
run(() => {
keydown(38);
keyup(38);
});
// it navigates to previous tag
expect(currentURL(), 'url after keyboard up arrow').to.equal('/settings/tags/1');
// it highlights selected tag
expect(find('a[href="/settings/tags/1"]').hasClass('active'), 'selects previous tag')
.to.be.true;
});
andThen(() => {
// simulate down arrow press
run(() => {
keydown(40);
keyup(40);
});
// it navigates to previous tag
expect(currentURL(), 'url after keyboard down arrow').to.equal('/settings/tags/2');
// it highlights selected tag
expect(find('a[href="/settings/tags/2"]').hasClass('active'), 'selects next tag')
.to.be.true;
});
// trigger save
fillIn('.tag-settings-pane input[name="name"]', 'New Name');
triggerEvent('.tag-settings-pane input[name="name"]', 'blur');
andThen(() => {
// check we update with the data returned from the server
expect(find('.settings-tags .settings-tag:last .tag-title').text(), 'tag list updates on save')
.to.equal('Saved Tag');
expect(find('.tag-settings-pane input[name="name"]').val(), 'settings form updates on save')
.to.equal('Saved Tag');
});
// start new tag
click('.view-actions .btn-green');
andThen(() => {
// it navigates to the new tag route
expect(currentURL(), 'new tag URL').to.equal('/settings/tags/new');
// it displays the new tag form
expect(find('.tag-settings-pane h4').text(), 'settings pane title')
.to.equal('New Tag');
// all fields start blank
find('.tag-settings-pane input, .tag-settings-pane textarea').each(function () {
expect($(this).val(), `input field for ${$(this).attr('name')}`)
.to.be.blank;
});
});
// save new tag
fillIn('.tag-settings-pane input[name="name"]', 'New Tag');
triggerEvent('.tag-settings-pane input[name="name"]', 'blur');
andThen(() => {
// it redirects to the new tag's URL
expect(currentURL(), 'URL after tag creation').to.equal('/settings/tags/3');
// it adds the tag to the list and selects
expect(find('.settings-tags .settings-tag').length, 'tag list count after creation')
.to.equal(3);
expect(find('.settings-tags .settings-tag:last .tag-title').text(), 'new tag list item title')
.to.equal('Tag Three');
expect(find('a[href="/settings/tags/3"]').hasClass('active'), 'highlights new tag')
.to.be.true;
});
// delete tag
click('.tag-delete-button');
click('.modal-container .btn-red');
andThen(() => {
// it redirects to the first tag
expect(currentURL(), 'URL after tag deletion').to.equal('/settings/tags/1');
// it removes the tag from the list
expect(find('.settings-tags .settings-tag').length, 'tag list count after deletion')
.to.equal(2);
});
});
it('has infinite scroll pagination of tags list');
});
});

51
ghost/admin/tests/fixtures/settings.js vendored Normal file
View File

@ -0,0 +1,51 @@
export default [{
created_at: '2015-09-11T09:44:30.805Z',
created_by: 1,
id: 5,
key: 'title',
type: 'blog',
updated_at: '2015-10-04T16:26:05.195Z',
updated_by: 1,
uuid: '39e16daf-43fa-4bf0-87d4-44948ba8bf4c',
value: 'Test Blog'
}, {
created_at: '2015-09-11T09:44:30.806Z',
created_by: 1,
id: 6,
key: 'description',
type: 'blog',
updated_at: '2015-10-04T16:26:05.198Z',
updated_by: 1,
uuid: 'e6c8b636-6925-4c4a-a5d9-1dc0870fb8ea',
value: 'Thoughts, stories and ideas.'
}, {
created_at: '2015-09-11T09:44:30.809Z',
created_by: 1,
id: 10,
key: 'postsPerPage',
type: 'blog',
updated_at: '2015-10-04T16:26:05.211Z',
updated_by: 1,
uuid: '775e6ca1-bcc3-4347-a53d-15d5d76c04a4',
value: '5'
}, {
created_at: '2015-09-11T09:44:30.809Z',
created_by: 1,
id: 13,
key: 'ghost_head',
type: 'blog',
updated_at: '2015-09-23T13:32:49.858Z',
updated_by: 1,
uuid: 'df7f3151-bc08-4a77-be9d-dd315b630d51',
value: ''
}, {
created_at: '2015-09-11T09:44:30.809Z',
created_by: 1,
id: 14,
key: 'ghost_foot',
type: 'blog',
updated_at: '2015-09-23T13:32:49.858Z',
updated_by: 1,
uuid: '0649d45e-828b-4dd0-8381-3dff6d1d5ddb',
value: ''
}];

View File

@ -13,7 +13,7 @@
<meta name="env-fileStorage" content="true" />
<meta name="env-apps" content="false" />
<meta name="env-blogUrl" content="http://localhost:7357/" />
<meta name="env-blogTitle" content="The Daily Awesome" />
<meta name="env-blogTitle" content="Test Blog" />
<meta name="env-routeKeywords" content="{&quot;tag&quot;:&quot;tag&quot;,&quot;author&quot;:&quot;author&quot;,&quot;page&quot;:&quot;page&quot;,&quot;preview&quot;:&quot;p&quot;,&quot;private&quot;:&quot;private&quot;}" />
<meta name="env-clientId" content="ghost-admin" />
<meta name="env-clientSecret" content="5076dc643873" />

View File

@ -0,0 +1,53 @@
/* jshint expr:true */
import { expect } from 'chai';
import {
describeComponent,
it
} from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
import Ember from 'ember';
const {run} = Ember;
describeComponent(
'gh-cm-editor',
'Integration: Component: gh-cm-editor',
{
integration: true
},
function () {
it('handles editor events', function () {
this.set('text', '');
this.render(hbs`{{gh-cm-editor class="gh-input" value=text}}`);
let input = this.$('.gh-input');
expect(input.hasClass('focused'), 'has focused class on first render')
.to.be.false;
run(() => {
input.find('textarea').trigger('focus');
});
expect(input.hasClass('focused'), 'has focused class after focus')
.to.be.true;
run(() => {
input.find('textarea').trigger('blur');
});
expect(input.hasClass('focused'), 'loses focused class on blur')
.to.be.false;
run(() => {
// access CodeMirror directly as it doesn't pick up changes
// to the textarea
let cm = input.find('.CodeMirror').get(0).CodeMirror;
cm.setValue('Testing');
});
expect(this.get('text'), 'text value after CM editor change')
.to.equal('Testing');
});
}
);

View File

@ -0,0 +1,301 @@
/* jshint expr:true */
import { expect } from 'chai';
import {
describeComponent,
it
} from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
import Ember from 'ember';
import DS from 'ember-data';
const {run} = Ember,
configStub = Ember.Service.extend({
blogUrl: 'http://localhost:2368'
});
describeComponent(
'gh-tag-settings-form',
'Integration: Component: gh-tag-settings-form',
{
integration: true
},
function () {
beforeEach(function () {
let tag = Ember.Object.create({
id: 1,
name: 'Test',
slug: 'test',
description: 'Description.',
meta_title: 'Meta Title',
meta_description: 'Meta description',
errors: DS.Errors.create(),
hasValidated: []
});
this.set('tag', tag);
this.set('actions.setProperty', function (property, value) {
// this should be overridden if a call is expected
console.error(`setProperty called '${property}: ${value}'`);
});
this.register('service:config', configStub);
this.inject.service('config', {as: 'config'});
});
it('renders', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
`);
expect(this.$()).to.have.length(1);
});
it('has the correct title', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
`);
expect(this.$('.tag-settings-pane h4').text(), 'existing tag title').to.equal('Tag Settings');
this.set('tag.isNew', true);
expect(this.$('.tag-settings-pane h4').text(), 'new tag title').to.equal('New Tag');
});
it('renders main settings', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
`);
expect(this.$('.image-uploader').length, 'displays image uploader').to.equal(1);
expect(this.$('input[name="name"]').val(), 'name field value').to.equal('Test');
expect(this.$('input[name="slug"]').val(), 'slug field value').to.equal('test');
expect(this.$('textarea[name="description"]').val(), 'description field value').to.equal('Description.');
expect(this.$('input[name="meta_title"]').val(), 'meta_title field value').to.equal('Meta Title');
expect(this.$('textarea[name="meta_description"]').val(), 'meta_description field value').to.equal('Meta description');
});
it('can switch between main/meta settings', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
`);
expect(this.$('.tag-settings-pane').hasClass('settings-menu-pane-in'), 'main settings are displayed by default').to.be.true;
expect(this.$('.tag-meta-settings-pane').hasClass('settings-menu-pane-out-right'), 'meta settings are hidden by default').to.be.true;
run(() => {
this.$('.meta-data-button').click();
});
expect(this.$('.tag-settings-pane').hasClass('settings-menu-pane-out-left'), 'main settings are hidden after clicking Meta Data button').to.be.true;
expect(this.$('.tag-meta-settings-pane').hasClass('settings-menu-pane-in'), 'meta settings are displayed after clicking Meta Data button').to.be.true;
run(() => {
this.$('.back').click();
});
expect(this.$('.tag-settings-pane').hasClass('settings-menu-pane-in'), 'main settings are displayed after clicking "back"').to.be.true;
expect(this.$('.tag-meta-settings-pane').hasClass('settings-menu-pane-out-right'), 'meta settings are hidden after clicking "back"').to.be.true;
});
it('has one-way binding for properties', function () {
this.set('actions.setProperty', function () {
// noop
});
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
`);
run(() => {
this.$('input[name="name"]').val('New name');
this.$('input[name="slug"]').val('new-slug');
this.$('textarea[name="description"]').val('New description');
this.$('input[name="meta_title"]').val('New meta_title');
this.$('textarea[name="meta_description"]').val('New meta_description');
});
expect(this.get('tag.name'), 'tag name').to.equal('Test');
expect(this.get('tag.slug'), 'tag slug').to.equal('test');
expect(this.get('tag.description'), 'tag description').to.equal('Description.');
expect(this.get('tag.meta_title'), 'tag meta_title').to.equal('Meta Title');
expect(this.get('tag.meta_description'), 'tag meta_description').to.equal('Meta description');
});
it('triggers setProperty action on blur of all fields', function () {
let expectedProperty = '',
expectedValue = '';
this.set('actions.setProperty', function (property, value) {
expect(property, 'property').to.equal(expectedProperty);
expect(value, 'value').to.equal(expectedValue);
});
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
`);
expectedProperty = 'name';
expectedValue = 'new-slug';
run(() => {
this.$('input[name="name"]').val('New name');
});
expectedProperty = 'url';
expectedValue = 'new-slug';
run(() => {
this.$('input[name="slug"]').val('new-slug');
});
expectedProperty = 'description';
expectedValue = 'New description';
run(() => {
this.$('textarea[name="description"]').val('New description');
});
expectedProperty = 'meta_title';
expectedValue = 'New meta_title';
run(() => {
this.$('input[name="meta_title"]').val('New meta_title');
});
expectedProperty = 'meta_description';
expectedValue = 'New meta_description';
run(() => {
this.$('textarea[name="meta_description"]').val('New meta_description');
});
});
it('displays error messages for validated fields', function () {
let errors = this.get('tag.errors'),
hasValidated = this.get('tag.hasValidated');
errors.add('name', 'must be present');
hasValidated.push('name');
errors.add('slug', 'must be present');
hasValidated.push('slug');
errors.add('description', 'is too long');
hasValidated.push('description');
errors.add('meta_title', 'is too long');
hasValidated.push('meta_title');
errors.add('meta_description', 'is too long');
hasValidated.push('meta_description');
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
`);
let nameFormGroup = this.$('input[name="name"]').closest('.form-group');
expect(nameFormGroup.hasClass('error'), 'name form group has error state').to.be.true;
expect(nameFormGroup.find('.response').length, 'name form group has error message').to.equal(1);
let slugFormGroup = this.$('input[name="slug"]').closest('.form-group');
expect(slugFormGroup.hasClass('error'), 'slug form group has error state').to.be.true;
expect(slugFormGroup.find('.response').length, 'slug form group has error message').to.equal(1);
let descriptionFormGroup = this.$('textarea[name="description"]').closest('.form-group');
expect(descriptionFormGroup.hasClass('error'), 'description form group has error state').to.be.true;
let metaTitleFormGroup = this.$('input[name="meta_title"]').closest('.form-group');
expect(metaTitleFormGroup.hasClass('error'), 'meta_title form group has error state').to.be.true;
expect(metaTitleFormGroup.find('.response').length, 'meta_title form group has error message').to.equal(1);
let metaDescriptionFormGroup = this.$('textarea[name="meta_description"]').closest('.form-group');
expect(metaDescriptionFormGroup.hasClass('error'), 'meta_description form group has error state').to.be.true;
expect(metaDescriptionFormGroup.find('.response').length, 'meta_description form group has error message').to.equal(1);
});
it('displays char count for text fields', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
`);
let descriptionFormGroup = this.$('textarea[name="description"]').closest('.form-group');
expect(descriptionFormGroup.find('.word-count').text(), 'description char count').to.equal('12');
let metaDescriptionFormGroup = this.$('textarea[name="meta_description"]').closest('.form-group');
expect(metaDescriptionFormGroup.find('.word-count').text(), 'description char count').to.equal('16');
});
it('renders SEO title preview', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
`);
expect(this.$('.seo-preview-title').text(), 'displays meta title if present').to.equal('Meta Title');
run(() => {
this.set('tag.meta_title', '');
});
expect(this.$('.seo-preview-title').text(), 'falls back to tag name without meta_title').to.equal('Test');
run(() => {
this.set('tag.name', (new Array(151).join('x')));
});
let expectedLength = 70 + '…'.length;
expect(this.$('.seo-preview-title').text().length, 'cuts title to max 70 chars').to.equal(expectedLength);
});
it('renders SEO URL preview', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
`);
expect(this.$('.seo-preview-link').text(), 'adds url and tag prefix').to.equal('http://localhost:2368/tag/test/');
run(() => {
this.set('tag.slug', (new Array(151).join('x')));
});
let expectedLength = 70 + '…'.length;
expect(this.$('.seo-preview-link').text().length, 'cuts slug to max 70 chars').to.equal(expectedLength);
});
it('renders SEO description preview', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
`);
expect(this.$('.seo-preview-description').text(), 'displays meta description if present').to.equal('Meta description');
run(() => {
this.set('tag.meta_description', '');
});
expect(this.$('.seo-preview-description').text(), 'falls back to tag description without meta_description').to.equal('Description.');
run(() => {
this.set('tag.description', (new Array(200).join('x')));
});
let expectedLength = 156 + '…'.length;
expect(this.$('.seo-preview-description').text().length, 'cuts description to max 156 chars').to.equal(expectedLength);
});
it('resets if a new tag is received', function () {
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
`);
run(() => {
this.$('.meta-data-button').click();
});
expect(this.$('.tag-meta-settings-pane').hasClass('settings-menu-pane-in'), 'meta data pane is shown').to.be.true;
run(() => {
this.set('tag', Ember.Object.create({id: '2'}));
});
expect(this.$('.tag-settings-pane').hasClass('settings-menu-pane-in'), 'resets to main settings').to.be.true;
});
it('triggers delete tag modal on delete click', function (done) {
this.set('actions.openModal', (modalName, model) => {
expect(modalName, 'passed modal name').to.equal('delete-tag');
expect(model, 'passed model').to.equal(this.get('tag'));
done();
});
this.render(hbs`
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty') openModal='openModal'}}
`);
run(() => {
this.$('.tag-delete-button').click();
});
});
}
);