🎨 Separated post and page list screens (#1101)

no issue
- added `page` model
- removed `page` param from Post model
- added pages screen with associated links
- added `:type` param to editor screens to work with the right models
- removed post<->page toggle and associated tour item
This commit is contained in:
Kevin Ansfield 2019-02-22 10:17:33 +07:00 committed by GitHub
parent 6701c4b328
commit f8b03f50b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 561 additions and 239 deletions

View File

@ -33,7 +33,7 @@ export default Component.extend({
let errors = this.get('errors');
let property = this.get('dateErrorProperty');
if (!isEmpty(errors.errorsFor(property))) {
if (errors && !isEmpty(errors.errorsFor(property))) {
return errors.errorsFor(property).get('firstObject').message;
}
}),
@ -42,7 +42,7 @@ export default Component.extend({
let errors = this.get('errors');
let property = this.get('timeErrorProperty');
if (!isEmpty(errors.errorsFor(property))) {
if (errors && !isEmpty(errors.errorsFor(property))) {
return errors.errorsFor(property).get('firstObject').message;
}
}),

View File

@ -137,21 +137,6 @@ export default Component.extend(SettingsMenuMixin, {
return false;
},
togglePage() {
this.toggleProperty('post.page');
// If this is a new post. Don't save the post. Defer the save
// to the user pressing the save button
if (this.get('post.isNew')) {
return;
}
this.get('savePost').perform().catch((error) => {
this.showError(error);
this.get('post').rollbackAttributes();
});
},
toggleFeatured() {
this.toggleProperty('post.featured');

View File

@ -31,7 +31,7 @@ export default Component.extend({
matchedSlugTemplate: computed('post.{page,slug}', 'activeTheme.slugTemplates.[]', function () {
let slug = this.get('post.slug');
let type = this.get('post.page') ? 'page' : 'post';
let type = this.post.constructor.modelName;
let [matchedTemplate] = this.get('activeTheme.slugTemplates').filter(function (template) {
return template.for.includes(type) && template.slug === slug;

View File

@ -33,7 +33,7 @@ export default Component.extend({
currentSearch: '',
selection: null,
posts: computedGroup('Stories'),
posts: computedGroup('Posts'),
pages: computedGroup('Pages'),
users: computedGroup('Users'),
tags: computedGroup('Tags'),
@ -42,7 +42,7 @@ export default Component.extend({
let groups = [];
if (!isEmpty(this.get('posts'))) {
groups.pushObject({groupName: 'Stories', options: this.get('posts')});
groups.pushObject({groupName: 'Posts', options: this.get('posts')});
}
if (!isEmpty(this.get('pages'))) {
@ -71,9 +71,14 @@ export default Component.extend({
return;
}
if (selected.category === 'Stories' || selected.category === 'Pages') {
if (selected.category === 'Posts') {
let id = selected.id.replace('post.', '');
this.get('router').transitionTo('editor.edit', id);
this.get('router').transitionTo('editor.edit', 'post', id);
}
if (selected.category === 'Pages') {
let id = selected.id.replace('page.', '');
this.get('router').transitionTo('editor.edit', 'page', id);
}
if (selected.category === 'Users') {
@ -132,6 +137,7 @@ export default Component.extend({
this.set('content', []);
promises.pushObject(this._loadPosts());
promises.pushObject(this._loadPages());
promises.pushObject(this._loadUsers());
promises.pushObject(this._loadTags());
@ -149,14 +155,31 @@ export default Component.extend({
_loadPosts() {
let store = this.get('store');
let postsUrl = `${store.adapterFor('post').urlForQuery({}, 'post')}/`;
let postsQuery = {fields: 'id,title,page', limit: 'all', status: 'all', filter: 'page:[true,false]'};
let postsQuery = {fields: 'id,title,page', limit: 'all', status: 'all'};
let content = this.get('content');
return this.get('ajax').request(postsUrl, {data: postsQuery}).then((posts) => {
content.pushObjects(posts.posts.map(post => ({
id: `post.${post.id}`,
title: post.title,
category: post.page ? 'Pages' : 'Stories'
category: 'Posts'
})));
}).catch((error) => {
this.get('notifications').showAPIError(error, {key: 'search.loadPosts.error'});
});
},
_loadPages() {
let store = this.get('store');
let pagesUrl = `${store.adapterFor('page').urlForQuery({}, 'page')}/`;
let pagesQuery = {fields: 'id,title,page', limit: 'all', status: 'all'};
let content = this.get('content');
return this.get('ajax').request(pagesUrl, {data: pagesQuery}).then((pages) => {
content.pushObjects(pages.pages.map(page => ({
id: `page.${page.id}`,
title: page.title,
category: 'Pages'
})));
}).catch((error) => {
this.get('notifications').showAPIError(error, {key: 'search.loadPosts.error'});

View File

@ -0,0 +1,7 @@
import PostsLoadingController from './posts-loading';
import {inject as controller} from '@ember/controller';
/* eslint-disable ghost/ember/alias-model-in-controller */
export default PostsLoadingController.extend({
postsController: controller('pages')
});

44
app/controllers/pages.js Normal file
View File

@ -0,0 +1,44 @@
import PostsController from './posts';
const TYPES = [{
name: 'All pages',
value: null
}, {
name: 'Draft pages',
value: 'draft'
}, {
name: 'Published pages',
value: 'published'
}, {
name: 'Scheduled pages',
value: 'scheduled'
}, {
name: 'Featured pages',
value: 'featured'
}];
const ORDERS = [{
name: 'Newest',
value: null
}, {
name: 'Oldest',
value: 'published_at asc'
}, {
name: 'Recently updated',
value: 'updated_at desc'
}];
/* eslint-disable ghost/ember/alias-model-in-controller */
export default PostsController.extend({
init() {
this._super(...arguments);
this.availableTypes = TYPES;
this.availableOrders = ORDERS;
},
actions: {
openEditor(page) {
this.transitionToRoute('editor.edit', 'page', page.get('id'));
}
}
});

View File

@ -1,8 +1,8 @@
/* eslint-disable ghost/ember/alias-model-in-controller */
import Controller, {inject as controller} from '@ember/controller';
import {readOnly} from '@ember/object/computed';
import {inject as service} from '@ember/service';
/* eslint-disable ghost/ember/alias-model-in-controller */
export default Controller.extend({
postsController: controller('posts'),

View File

@ -19,9 +19,6 @@ const TYPES = [{
}, {
name: 'Featured posts',
value: 'featured'
}, {
name: 'Pages',
value: 'page'
}];
const ORDERS = [{
@ -137,7 +134,7 @@ export default Controller.extend({
},
openEditor(post) {
this.transitionToRoute('editor.edit', post.get('id'));
this.transitionToRoute('editor.edit', 'post', post.get('id'));
}
}
});

5
app/models/page.js Normal file
View File

@ -0,0 +1,5 @@
import PostModel from './post';
export default PostModel.extend({
displayName: 'page'
});

View File

@ -72,6 +72,7 @@ export default Model.extend(Comparable, ValidationEngine, {
clock: service(),
settings: service(),
displayName: 'post',
validationType: 'post',
createdAtUTC: attr('moment-utc'),
@ -92,7 +93,6 @@ export default Model.extend(Comparable, ValidationEngine, {
metaDescription: attr('string'),
metaTitle: attr('string'),
mobiledoc: attr('json-string'),
page: attr('boolean', {defaultValue: false}),
plaintext: attr('string'),
publishedAtUTC: attr('moment-utc'),
slug: attr('string'),

View File

@ -32,10 +32,11 @@ Router.map(function () {
this.route('about', {path: '/about'});
this.route('posts', {path: '/'}, function () {});
this.route('pages', {path: '/pages'}, function () {});
this.route('editor', function () {
this.route('new', {path: ''});
this.route('edit', {path: ':post_id'});
this.route('new', {path: ':type'});
this.route('edit', {path: ':type/:post_id'});
});
this.route('team', {path: '/team'}, function () {

View File

@ -1,4 +1,5 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import {pluralize} from 'ember-inflector';
export default AuthenticatedRoute.extend({
beforeModel(transition) {
@ -17,11 +18,10 @@ export default AuthenticatedRoute.extend({
let query = {
id: params.post_id,
status: 'all',
filter: 'page:[true,false]',
formats: 'mobiledoc,plaintext'
};
return this.store.query('post', query)
return this.store.query(params.type, query)
.then(records => records.get('firstObject'));
},
@ -32,17 +32,26 @@ export default AuthenticatedRoute.extend({
this._super(...arguments);
return this.get('session.user').then((user) => {
let returnRoute = `${pluralize(post.constructor.modelName)}.index`;
if (user.get('isAuthorOrContributor') && !post.isAuthoredByUser(user)) {
return this.replaceWith('posts.index');
return this.replaceWith(returnRoute);
}
// If the post is not a draft and user is contributor, redirect to index
if (user.get('isContributor') && !post.get('isDraft')) {
return this.replaceWith('posts.index');
return this.replaceWith(returnRoute);
}
});
},
serialize(model) {
return {
type: model.constructor.modelName,
post_id: model.id
};
},
// there's no specific controller for this route, instead all editor
// handling is done on the editor route/controler
setupController(controller, post) {

View File

@ -0,0 +1,8 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
export default AuthenticatedRoute.extend({
beforeModel() {
this._super(...arguments);
this.replaceWith('editor.new', 'post');
}
});

View File

@ -1,9 +1,9 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
export default AuthenticatedRoute.extend({
model() {
model(params) {
return this.get('session.user').then(user => (
this.store.createRecord('post', {authors: [user]})
this.store.createRecord(params.type, {authors: [user]})
));
},

6
app/routes/pages.js Normal file
View File

@ -0,0 +1,6 @@
import PostsRoute from './posts';
export default PostsRoute.extend({
titleToken: 'Pages',
modelName: 'page'
});

View File

@ -1,4 +1,3 @@
import $ from 'jquery';
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import {assign} from '@ember/polyfills';
import {isBlank} from '@ember/utils';
@ -26,14 +25,15 @@ export default AuthenticatedRoute.extend({
}
},
titleToken: 'Content',
titleToken: 'Posts',
modelName: 'post',
perPage: 30,
_type: null,
model(params) {
return this.get('session.user').then((user) => {
return this.session.user.then((user) => {
let queryParams = {};
let filterParams = {tag: params.tag};
let paginationParams = {
@ -47,12 +47,12 @@ export default AuthenticatedRoute.extend({
filterParams.featured = true;
}
if (user.get('isAuthor')) {
if (user.isAuthor) {
// authors can only view their own posts
filterParams.authors = user.get('slug');
} else if (user.get('isContributor')) {
filterParams.authors = user.slug;
} else if (user.isContributor) {
// Contributors can only view their own draft posts
filterParams.authors = user.get('slug');
filterParams.authors = user.slug;
filterParams.status = 'draft';
} else if (params.author) {
filterParams.authors = params.author;
@ -69,10 +69,10 @@ export default AuthenticatedRoute.extend({
queryParams.formats = 'mobiledoc,plaintext';
let perPage = this.get('perPage');
let perPage = this.perPage;
let paginationSettings = assign({perPage, startingPage: 1}, paginationParams, queryParams);
return this.infinity.model('post', paginationSettings);
return this.infinity.model(this.modelName, paginationSettings);
});
},
@ -81,14 +81,14 @@ export default AuthenticatedRoute.extend({
this._super(...arguments);
if (!controller._hasLoadedTags) {
this.get('store').query('tag', {limit: 'all'}).then(() => {
this.store.query('tag', {limit: 'all'}).then(() => {
controller._hasLoadedTags = true;
});
}
this.get('session.user').then((user) => {
if (!user.get('isAuthorOrContributor') && !controller._hasLoadedAuthors) {
this.get('store').query('user', {limit: 'all'}).then(() => {
this.session.user.then((user) => {
if (!user.isAuthorOrContributor && !controller._hasLoadedAuthors) {
this.store.query('user', {limit: 'all'}).then(() => {
controller._hasLoadedAuthors = true;
});
}
@ -97,14 +97,17 @@ export default AuthenticatedRoute.extend({
actions: {
willTransition() {
if (this.get('controller')) {
if (this.controller) {
this.resetController();
}
},
queryParamsDidChange() {
// scroll back to the top
$('.content-list').scrollTop(0);
let contentList = document.querySelector('.content-list');
if (contentList) {
contentList.scrollTop = 0;
}
this._super(...arguments);
}
@ -112,29 +115,21 @@ export default AuthenticatedRoute.extend({
_getTypeFilters(type) {
let status = '[draft,scheduled,published]';
let page = '[true,false]';
switch (type) {
case 'draft':
status = 'draft';
page = false;
break;
case 'published':
status = 'published';
page = false;
break;
case 'scheduled':
status = 'scheduled';
page = false;
break;
case 'page':
page = true;
break;
}
return {
status,
page
status
};
},

3
app/serializers/page.js Normal file
View File

@ -0,0 +1,3 @@
import PostSerializer from './post';
export default PostSerializer.extend({});

View File

@ -53,10 +53,6 @@ export default Service.extend(Evented, {
id: 'using-the-editor',
title: 'Using the Ghost editor',
message: 'Ghost uses Markdown to allow you to write and format content quickly and easily. This toolbar also helps! Hit the <strong>?</strong> icon for more editor shortcuts.'
}, {
id: 'static-post',
title: 'Turning posts into pages',
message: 'Static pages are permanent pieces of content which live outside of your usual stream of posts, for example an \'about\' or \'contact\' page.'
}, {
id: 'featured-post',
title: 'Setting a featured post',

View File

@ -1,8 +1,8 @@
{{#link-to "editor.new" data-test-mobile-nav="new-story"}}{{svg-jar "pen"}}New story{{/link-to}}
{{#link-to "editor.new" "post" data-test-mobile-nav="new-post"}}{{svg-jar "pen"}}New post{{/link-to}}
{{#if (eq router.currentRouteName "posts.index")}}
{{#link-to "posts" (query-params type=null) classNames="active" data-test-mobile-nav="stories"}}{{svg-jar "content"}}Stories{{/link-to}}
{{#link-to "posts" (query-params type=null) classNames="active" data-test-mobile-nav="posts"}}{{svg-jar "content"}}Posts{{/link-to}}
{{else}}
{{#link-to "posts"}}{{svg-jar "content" data-test-mobile-nav="stories"}}Content{{/link-to}}
{{#link-to "posts"}}{{svg-jar "content" data-test-mobile-nav="posts"}}Posts{{/link-to}}
{{/if}}
{{#link-to "team" classNames="gh-nav-main-users" data-test-mobile-nav="team"}}{{svg-jar "account-group"}}Team{{/link-to}}
<div class="gh-mobile-nav-bar-more" {{action "openMobileMenu" target=ui data-test-mobile-nav="more"}}>{{svg-jar "icon" class="icon-gh"}}More</div>

View File

@ -40,13 +40,21 @@
{{gh-search-input class="gh-nav-search-input"}}
</section>
<ul class="gh-nav-list gh-nav-main">
<li>{{#link-to "editor.new" data-test-nav="new-story"}}{{svg-jar "pen"}}New story{{/link-to}}</li>
<li>{{#link-to "editor.new" "post" data-test-nav="new-post"}}{{svg-jar "pen"}}New post{{/link-to}}</li>
<li>
{{!-- clicking the Content link whilst on the content screen should reset the filter --}}
{{#if (eq router.currentRouteName "posts.index")}}
{{#link-to "posts" (query-params type=null author=null tag=null order=null) classNames="active" data-test-nav="stories"}}{{svg-jar "content"}}Stories{{/link-to}}
{{#link-to "posts" (query-params type=null author=null tag=null order=null) classNames="active" data-test-nav="posts"}}{{svg-jar "content"}}Posts{{/link-to}}
{{else}}
{{#link-to "posts" data-test-nav="stories"}}{{svg-jar "content"}}Stories{{/link-to}}
{{#link-to "posts" data-test-nav="posts"}}{{svg-jar "content"}}Posts{{/link-to}}
{{/if}}
</li>
<li>
{{!-- clicking the Content link whilst on the content screen should reset the filter --}}
{{#if (eq router.currentRouteName "pages.index")}}
{{#link-to "pages" (query-params type=null author=null tag=null order=null) classNames="active" data-test-nav="pages"}}{{svg-jar "content"}}Pages{{/link-to}}
{{else}}
{{#link-to "pages" data-test-nav="pages"}}{{svg-jar "content"}}Pages{{/link-to}}
{{/if}}
</li>
<li>{{#link-to "team" data-test-nav="team"}}{{svg-jar "account-group"}}Team{{/link-to}}</li>

View File

@ -2,7 +2,7 @@
<div id="entry-controls">
<div class="{{if isViewingSubview 'settings-menu-pane-out-left' 'settings-menu-pane-in'}} settings-menu settings-menu-pane">
<div class="settings-menu-header">
<h4>Post Settings</h4>
<h4>{{capitalize post.displayName}} Settings</h4>
<button class="close settings-menu-header-action" {{action "closeMenus" target=ui}} data-test-close-settings-menu>
{{svg-jar "close"}}<span class="hidden">Close</span>
</button>
@ -17,12 +17,12 @@
}}
<form>
<div class="form-group">
<label for="url">Post URL</label>
<label for="url">{{capitalize post.displayName}} URL</label>
{{!-- new posts don't have a preview link --}}
{{#unless post.isNew}}
{{#if post.isPublished}}
<a class="post-view-link" target="_blank" href="{{post.url}}">
View post {{svg-jar "external"}}
View {{post.displayName}} {{svg-jar "external"}}
</a>
{{else}}
<a class="post-view-link" target="_blank" href="{{post.previewUrl}}">
@ -127,20 +127,6 @@
</ul>
<div class="form-group for-checkbox">
<label class="checkbox" for="static-page" {{action "togglePage" bubbles="false"}}>
<input
type="checkbox"
checked={{post.page}}
class="gh-input post-setting-static-page"
name="static-page"
id="static-page"
onclick={{action (mut post.page) value="target.checked"}}
data-test-checkbox="static-page"
>
<span class="input-toggle-component"></span>
<p>Turn this post into a page</p>
</label>
<label class="checkbox" for="featured" {{action "toggleFeatured" bubbles="false"}}>
<input
type="checkbox"
@ -150,7 +136,7 @@
data-test-checkbox="featured"
>
<span class="input-toggle-component"></span>
<p>Feature this post</p>
<p>Feature this {{post.displayName}}</p>
</label>
</div>
@ -410,13 +396,6 @@
pane is shown
--}}
{{#if _showThrobbers}}
{{gh-tour-item "static-post"
target="label[for='static-page'] p"
throbberAttachment="middle middle"
throbberOffset="0px 33px"
popoverTriangleClass="bottom-right"
}}
{{gh-tour-item "featured-post"
target="label[for='featured'] p"
throbberAttachment="middle middle"

View File

@ -1,4 +1,4 @@
<h3 class="gh-content-entry-title">{{#link-to "editor.edit" post.id class="permalink" title="Edit this post"}}{{post.title}}{{/link-to}}</h3>
<h3 class="gh-content-entry-title">{{#link-to "editor.edit" "post" post.id class="permalink" title="Edit this post"}}{{post.title}}{{/link-to}}</h3>
<p>{{subText}}</p>
<section class="gh-content-entry-meta">

View File

@ -8,9 +8,9 @@
<div class="flex items-center pe-auto">
{{#if ui.isFullScreen}}
<div class="{{ui-text "ts"}} h9 br b--lightgrey pl3 pr4 flex items-center br2 br--left {{unless infoMessage "bg-white"}}">
{{#link-to "posts" classNames="blue link fw4 flex items-center" data-test-link="stories"}}
{{#link-to (pluralize post.displayName) classNames="blue link fw4 flex items-center" data-test-link=(pluralize post.displayName)}}
{{svg-jar "arrow-left" class="w3 fill-blue mr1 nudge-right--2"}}
Stories
{{capitalize (pluralize post.displayName)}}
{{/link-to}}
</div>
{{/if}}
@ -23,7 +23,7 @@
</span>
{{#gh-scheduled-post-countdown post=post as |post countdown|}}
<time datetime="{{post.publishedAtUTC}}" class="green f8 ml5" data-test-schedule-countdown>
Post will go live {{countdown}}.
{{capitalize post.displayName}} will go live {{countdown}}.
</time>
{{/gh-scheduled-post-countdown}}
</div>
@ -71,11 +71,11 @@
--}}
{{gh-koenig-editor
title=(readonly post.titleScratch)
titlePlaceholder="Story Title"
titlePlaceholder=(concat (capitalize post.displayName) " Title")
onTitleChange=(action "updateTitleScratch")
onTitleBlur=(action (perform saveTitle))
body=(readonly post.scratch)
bodyPlaceholder="Begin writing your story..."
bodyPlaceholder=(concat "Begin writing your " post.displayName "...")
bodyAutofocus=shouldFocusEditor
onBodyChange=(action "updateScratch")
headerOffset=editor.headerHeight

View File

@ -0,0 +1,89 @@
<section class="gh-canvas">
<header class="gh-canvas-header">
<h2 class="gh-canvas-title" data-test-screen-title>Your pages</h2>
<section class="view-actions">
{{#link-to "editor.new" "page" class="gh-btn gh-btn-green" data-test-new-page-button=true}}<span>New page</span>{{/link-to}}
</section>
</header>
<div class="gh-contentfilter">
<div class="gh-contentfilter-left">
{{#power-select
placeholder="All pages"
selected=selectedType
options=availableTypes
searchField="name"
onchange=(action (mut k))
tagName="div"
classNames="gh-contentfilter-menu gh-contentfilter-type"
triggerClass="gh-contentfilter-menu-trigger"
dropdownClass="gh-contentfilter-menu-dropdown"
matchTriggerWidth=false
data-test-type-select=true
as |type|
}}
{{type.name}}
{{/power-select}}
{{#unless session.user.isAuthorOrContributor}}
{{#power-select
placeholder="All authors"
selected=selectedAuthor
options=availableAuthors
searchField="name"
onchange=(action (mut k))
tagName="div"
classNames="gh-contentfilter-menu gh-contentfilter-author"
triggerClass="gh-contentfilter-menu-trigger"
dropdownClass="gh-contentfilter-menu-dropdown"
matchTriggerWidth=false
data-test-author-select=true
as |author|
}}
{{author.name}}
{{/power-select}}
{{/unless}}
{{#power-select
placeholder="All tags"
selected=selectedTag
options=availableTags
searchField="name"
onchange=(action (mut k))
tagName="div"
classNames="gh-contentfilter-menu gh-contentfilter-tag"
triggerClass="gh-contentfilter-menu-trigger"
dropdownClass="gh-contentfilter-menu-dropdown"
searchPlaceholder="Search tags"
matchTriggerWidth=false
data-test-tag-select=true
as |tag|
}}
{{tag.name}}
{{/power-select}}
</div>
<div class="gh-contentfilter-right">
Sort by:
{{#power-select
selected=selectedOrder
options=availableOrders
searchEnabled=false
onchange=(action (mut k))
tagName="div"
classNames="gh-contentfilter-menu gh-contentfilter-sort"
triggerClass="gh-contentfilter-menu-trigger"
dropdownClass="gh-contentfilter-menu-dropdown"
horizontalPosition="right"
matchTriggerWidth=false
data-test-order-select=true
as |order|
}}
{{order.name}}
{{/power-select}}
</div>
</div>
<div class="gh-content">
{{gh-loading-spinner}}
</div>
</section>

118
app/templates/pages.hbs Normal file
View File

@ -0,0 +1,118 @@
<section class="gh-canvas">
<header class="gh-canvas-header">
<h2 class="gh-canvas-title" data-test-screen-title>Your pages</h2>
<section class="view-actions">
{{#link-to "editor.new" "page" class="gh-btn gh-btn-green" data-test-new-page-button=true}}<span>New page</span>{{/link-to}}
</section>
</header>
<div class="gh-contentfilter">
<div class="gh-contentfilter-left">
{{#unless session.user.isContributor}}
{{#power-select
selected=selectedType
options=availableTypes
searchEnabled=false
onchange=(action "changeType")
tagName="div"
classNames="gh-contentfilter-menu gh-contentfilter-type"
triggerClass="gh-contentfilter-menu-trigger"
dropdownClass="gh-contentfilter-menu-dropdown"
matchTriggerWidth=false
data-test-type-select=true
as |type|
}}
{{type.name}}
{{/power-select}}
{{/unless}}
{{#unless session.user.isAuthorOrContributor}}
{{#power-select
selected=selectedAuthor
options=availableAuthors
searchField="name"
onchange=(action "changeAuthor")
tagName="div"
classNames="gh-contentfilter-menu gh-contentfilter-author"
triggerClass="gh-contentfilter-menu-trigger"
dropdownClass="gh-contentfilter-menu-dropdown"
searchPlaceholder="Search authors"
matchTriggerWidth=false
data-test-author-select=true
as |author|
}}
{{author.name}}
{{/power-select}}
{{/unless}}
{{#unless session.user.isContributor}}
{{#power-select
selected=selectedTag
options=availableTags
searchField="name"
onchange=(action "changeTag")
tagName="div"
classNames="gh-contentfilter-menu gh-contentfilter-tag"
triggerClass="gh-contentfilter-menu-trigger"
dropdownClass="gh-contentfilter-menu-dropdown"
searchPlaceholder="Search tags"
matchTriggerWidth=false
optionsComponent="power-select-vertical-collection-options"
data-test-tag-select=true
as |tag|
}}
{{tag.name}}
{{/power-select}}
{{/unless}}
</div>
<div class="gh-contentfilter-right">
Sort by:
{{#power-select
selected=selectedOrder
options=availableOrders
searchEnabled=false
onchange=(action "changeOrder")
tagName="div"
classNames="gh-contentfilter-menu gh-contentfilter-sort"
triggerClass="gh-contentfilter-menu-trigger"
dropdownClass="gh-contentfilter-menu-dropdown"
horizontalPosition="right"
matchTriggerWidth=false
data-test-order-select=true
as |order|
}}
{{order.name}}
{{/power-select}}
</div>
</div>
<section class="content-list">
<ol class="posts-list">
{{#each postsInfinityModel as |page|}}
{{gh-posts-list-item
post=page
onDoubleClick=(action "openEditor")
data-test-page-id=page.id}}
{{else}}
<li class="no-posts-box">
<div class="no-posts">
{{#if showingAll}}
<h3>You haven't created any pages yet!</h3>
{{#link-to "editor.new" "page"}}<button type="button" class="gh-btn gh-btn-green gh-btn-lg"><span>Create a new page</span></button>{{/link-to}}
{{else}}
<h3>No pages match the current filter</h3>
{{#link-to "pages.index" (query-params type=null author=null tag=null)}}<button type="button" class="gh-btn gh-btn-lg"><span>Show all pages</span></button>{{/link-to}}
{{/if}}
</div>
</li>
{{/each}}
</ol>
{{gh-infinity-loader
infinityModel=postsInfinityModel
scrollable=".gh-main"
triggerOffset=1000}}
</section>
{{outlet}}
</section>

View File

@ -1,8 +1,8 @@
<section class="gh-canvas">
<header class="gh-canvas-header">
<h2 class="gh-canvas-title" data-test-screen-title>Your stories</h2>
<h2 class="gh-canvas-title" data-test-screen-title>Your posts</h2>
<section class="view-actions">
{{#link-to "editor.new" class="gh-btn gh-btn-green" data-test-new-post-button=true}}<span>New story</span>{{/link-to}}
{{#link-to "editor.new" "post" class="gh-btn gh-btn-green" data-test-new-post-button=true}}<span>New post</span>{{/link-to}}
</section>
</header>

View File

@ -1,8 +1,8 @@
<section class="gh-canvas">
<header class="gh-canvas-header">
<h2 class="gh-canvas-title" data-test-screen-title>Your stories</h2>
<h2 class="gh-canvas-title" data-test-screen-title>Your posts</h2>
<section class="view-actions">
{{#link-to "editor.new" class="gh-btn gh-btn-green" data-test-new-post-button=true}}<span>New story</span>{{/link-to}}
{{#link-to "editor.new" "post" class="gh-btn gh-btn-green" data-test-new-post-button=true}}<span>New post</span>{{/link-to}}
</section>
</header>
<div class="gh-contentfilter">
@ -97,11 +97,11 @@
<li class="no-posts-box">
<div class="no-posts">
{{#if showingAll}}
<h3>You haven't written any stories yet!</h3>
{{#link-to "editor.new"}}<button type="button" class="gh-btn gh-btn-green gh-btn-lg"><span>Write a new Story</span></button>{{/link-to}}
<h3>You haven't written any posts yet!</h3>
{{#link-to "editor.new" "post"}}<button type="button" class="gh-btn gh-btn-green gh-btn-lg"><span>Write a new Post</span></button>{{/link-to}}
{{else}}
<h3>No stories match the current filter</h3>
{{#link-to "posts.index" (query-params type=null author=null tag=null)}}<button type="button" class="gh-btn gh-btn-lg"><span>Show all stories</span></button>{{/link-to}}
<h3>No posts match the current filter</h3>
{{#link-to "posts.index" (query-params type=null author=null tag=null)}}<button type="button" class="gh-btn gh-btn-lg"><span>Show all posts</span></button>{{/link-to}}
{{/if}}
</div>
</li>

View File

@ -62,7 +62,7 @@ export default Component.extend({
this._super(...arguments);
// re-register the / text input handler if the editor changes such as
// when a "New story" is clicked from the sidebar or a different post
// when a "New post" is clicked from the sidebar or a different post
// is loaded via search
if (this.editor !== this._lastEditor) {
this.editor.onTextInput({

View File

@ -4,6 +4,7 @@ import mockConfiguration from './config/configuration';
import mockIntegrations from './config/integrations';
import mockInvites from './config/invites';
import mockMembers from './config/members';
import mockPages from './config/pages';
import mockPosts from './config/posts';
import mockRoles from './config/roles';
import mockSettings from './config/settings';
@ -57,6 +58,7 @@ export function testConfig() {
mockIntegrations(this);
mockInvites(this);
mockMembers(this);
mockPages(this);
mockPosts(this);
mockRoles(this);
mockSettings(this);

119
mirage/config/pages.js Normal file
View File

@ -0,0 +1,119 @@
import moment from 'moment';
import {Response} from 'ember-cli-mirage';
import {dasherize} from '@ember/string';
import {isArray} from '@ember/array';
import {isBlank, isEmpty} from '@ember/utils';
import {paginateModelCollection} from '../utils';
function normalizeBooleanParams(arr) {
if (!isArray(arr)) {
return arr;
}
return arr.map((i) => {
if (i === 'true') {
return true;
} else if (i === 'false') {
return false;
} else {
return i;
}
});
}
// TODO: use GQL to parse filter string?
function extractFilterParam(param, filter) {
let filterRegex = new RegExp(`${param}:(.*?)(?:\\+|$)`);
let match;
let [, result] = filter.match(filterRegex) || [];
if (result && result.startsWith('[')) {
match = result.replace(/^\[|\]$/g, '').split(',');
} else if (result) {
match = [result];
}
return normalizeBooleanParams(match);
}
// NOTE: mirage requires Model objects when saving relationships, however the
// `attrs` on POST/PUT requests will contain POJOs for authors and tags so we
// need to replace them
function extractAuthors(pageAttrs, users) {
return pageAttrs.authors.map(author => users.find(author.id));
}
function extractTags(pageAttrs, tags) {
return pageAttrs.tags.map((requestTag) => {
let tag = tags.find(requestTag.id);
if (!tag) {
tag = tag.create(requestTag);
}
return tag;
});
}
export default function mockPages(server) {
server.post('/pages', function ({pages, users, tags}) {
let attrs = this.normalizedRequestAttrs();
attrs.authors = extractAuthors(attrs, users);
attrs.tags = extractTags(attrs, tags);
if (isBlank(attrs.slug) && !isBlank(attrs.title)) {
attrs.slug = dasherize(attrs.title);
}
return pages.create(attrs);
});
// TODO: handle authors filter
server.get('/pages/', function ({pages}, {queryParams}) {
let {filter, page, limit} = queryParams;
page = +page || 1;
limit = +limit || 15;
let statusFilter = extractFilterParam('status', filter);
let collection = pages.all().filter((page) => {
let matchesStatus = true;
if (!isEmpty(statusFilter)) {
matchesStatus = statusFilter.includes(page.status);
}
return matchesStatus;
});
return paginateModelCollection('pages', collection, page, limit);
});
server.get('/pages/:id/', function ({pages}, {params}) {
let {id} = params;
let page = pages.find(id);
return page || new Response(404, {}, {
errors: [{
errorType: 'NotFoundError',
message: 'Page not found.'
}]
});
});
server.put('/pages/:id/', function ({pages, users, tags}, {params}) {
let attrs = this.normalizedRequestAttrs();
let page = pages.find(params.id);
attrs.authors = extractAuthors(attrs, users);
attrs.tags = extractTags(attrs, tags);
attrs.updatedAt = moment.utc().toDate();
return page.update(attrs);
});
server.del('/pages/:id/');
}

View File

@ -77,21 +77,15 @@ export default function mockPosts(server) {
limit = +limit || 15;
let statusFilter = extractFilterParam('status', filter);
let pageFilter = extractFilterParam('page', filter);
let collection = posts.all().filter((post) => {
let matchesStatus = true;
let matchesPage = true;
if (!isEmpty(statusFilter)) {
matchesStatus = statusFilter.includes(post.status);
}
if (!isEmpty(pageFilter)) {
matchesPage = pageFilter.includes(post.page);
}
return matchesStatus && matchesPage;
return matchesStatus;
});
return paginateModelCollection('posts', collection, page, limit);

View File

@ -18,7 +18,6 @@ export default Factory.extend({
ogDescription: null,
ogImage: null,
ogTitle: null,
page: false,
plaintext(i) { return `Plaintext for post ${i}.`; },
publishedAt: '2015-12-19T16:25:07.000Z',
publishedBy: 1,

5
mirage/models/page.js Normal file
View File

@ -0,0 +1,5 @@
import PostModel from './post';
export default PostModel.extend({
});

View File

@ -18,8 +18,7 @@ describe('Acceptance: Content', function () {
});
describe('as admin', function () {
let admin, editor,
publishedPost, scheduledPost, draftPost, publishedPage, authorPost;
let admin, editor, publishedPost, scheduledPost, draftPost, authorPost;
beforeEach(async function () {
let adminRole = this.server.create('role', {name: 'Administrator'});
@ -30,9 +29,11 @@ describe('Acceptance: Content', function () {
publishedPost = this.server.create('post', {authors: [admin], status: 'published', title: 'Published Post'});
scheduledPost = this.server.create('post', {authors: [admin], status: 'scheduled', title: 'Scheduled Post'});
draftPost = this.server.create('post', {authors: [admin], status: 'draft', title: 'Draft Post'});
publishedPage = this.server.create('post', {authors: [admin], status: 'published', page: true, title: 'Published Page'});
authorPost = this.server.create('post', {authors: [editor], status: 'published', title: 'Editor Published Post'});
// pages shouldn't appear in the list
this.server.create('page', {authors: [admin], status: 'published', title: 'Published Page'});
return await authenticateSession();
});
@ -40,7 +41,7 @@ describe('Acceptance: Content', function () {
await visit('/');
// Not checking request here as it won't be the last request made
// Displays all posts + pages
expect(findAll('[data-test-post-id]').length, 'all posts count').to.equal(5);
expect(findAll('[data-test-post-id]').length, 'all posts count').to.equal(4);
// show draft posts
await selectChoose('[data-test-type-select]', 'Draft posts');
@ -48,7 +49,6 @@ describe('Acceptance: Content', function () {
// API request is correct
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"drafts" request status filter').to.have.string('status:draft');
expect(lastRequest.queryParams.filter, '"drafts" request page filter').to.have.string('page:false');
// Displays draft post
expect(findAll('[data-test-post-id]').length, 'drafts count').to.equal(1);
expect(find(`[data-test-post-id="${draftPost.id}"]`), 'draft post').to.exist;
@ -59,7 +59,6 @@ describe('Acceptance: Content', function () {
// API request is correct
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"published" request status filter').to.have.string('status:published');
expect(lastRequest.queryParams.filter, '"published" request page filter').to.have.string('page:false');
// Displays three published posts + pages
expect(findAll('[data-test-post-id]').length, 'published count').to.equal(2);
expect(find(`[data-test-post-id="${publishedPost.id}"]`), 'admin published post').to.exist;
@ -71,29 +70,16 @@ describe('Acceptance: Content', function () {
// API request is correct
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"scheduled" request status filter').to.have.string('status:scheduled');
expect(lastRequest.queryParams.filter, '"scheduled" request page filter').to.have.string('page:false');
// Displays scheduled post
expect(findAll('[data-test-post-id]').length, 'scheduled count').to.equal(1);
expect(find(`[data-test-post-id="${scheduledPost.id}"]`), 'scheduled post').to.exist;
// show pages
await selectChoose('[data-test-type-select]', 'Pages');
// API request is correct
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"pages" request status filter').to.have.string('status:[draft,scheduled,published]');
expect(lastRequest.queryParams.filter, '"pages" request page filter').to.have.string('page:true');
// Displays page
expect(findAll('[data-test-post-id]').length, 'pages count').to.equal(1);
expect(find(`[data-test-post-id="${publishedPage.id}"]`), 'page post').to.exist;
// show all posts
await selectChoose('[data-test-type-select]', 'All posts');
// API request is correct
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"all" request status filter').to.have.string('status:[draft,scheduled,published]');
expect(lastRequest.queryParams.filter, '"all" request page filter').to.have.string('page:[true,false]');
// show all posts by editor
await selectChoose('[data-test-author-select]', editor.name);
@ -101,7 +87,6 @@ describe('Acceptance: Content', function () {
// API request is correct
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter, '"editor" request status filter').to.have.string('status:[draft,scheduled,published]');
expect(lastRequest.queryParams.filter, '"editor" request page filter').to.have.string('page:[true,false]');
expect(lastRequest.queryParams.filter, '"editor" request filter param').to.have.string(`authors:${editor.slug}`);
// Displays editor post
@ -114,7 +99,7 @@ describe('Acceptance: Content', function () {
// Double-click on a post opens editor
await triggerEvent(`[data-test-post-id="${authorPost.id}"]`, 'dblclick');
expect(currentURL(), 'url after double-click').to.equal(`/editor/${authorPost.id}`);
expect(currentURL(), 'url after double-click').to.equal(`/editor/post/${authorPost.id}`);
});
// TODO: skipped due to consistently random failures on Travis

View File

@ -63,7 +63,7 @@ describe('Acceptance: Custom Post Templates', function () {
it('can change selected template', async function () {
let post = this.server.create('post', {customTemplate: 'custom-news-bulletin.hbs'});
await visit('/editor/1');
await visit('/editor/post/1');
await click('[data-test-psm-trigger]');
// template form should be shown
@ -109,7 +109,7 @@ describe('Acceptance: Custom Post Templates', function () {
this.server.create('post', {customTemplate: 'custom-news-bulletin.hbs'});
await visit('/editor/1');
await visit('/editor/post/1');
await click('[data-test-psm-trigger]');
expect(themeRequests().length, 'after first open').to.equal(1);
@ -137,7 +137,7 @@ describe('Acceptance: Custom Post Templates', function () {
it('doesn\'t show template selector', async function () {
this.server.create('post', {customTemplate: 'custom-news-bulletin.hbs'});
await visit('/editor/1');
await visit('/editor/post/1');
await click('[data-test-psm-trigger]');
// template form should be shown

View File

@ -23,7 +23,7 @@ describe('Acceptance: Editor', function () {
this.server.create('post', {authors: [author]});
await invalidateSession();
await visit('/editor/1');
await visit('/editor/post/1');
expect(currentURL(), 'currentURL').to.equal('/signin');
});
@ -34,9 +34,9 @@ describe('Acceptance: Editor', function () {
this.server.create('post', {authors: [author]});
await authenticateSession();
await visit('/editor/1');
await visit('/editor/post/1');
expect(currentURL(), 'currentURL').to.equal('/editor/1');
expect(currentURL(), 'currentURL').to.equal('/editor/post/1');
});
it('does not redirect to team page when authenticated as author', async function () {
@ -45,9 +45,9 @@ describe('Acceptance: Editor', function () {
this.server.create('post', {authors: [author]});
await authenticateSession();
await visit('/editor/1');
await visit('/editor/post/1');
expect(currentURL(), 'currentURL').to.equal('/editor/1');
expect(currentURL(), 'currentURL').to.equal('/editor/post/1');
});
it('does not redirect to team page when authenticated as editor', async function () {
@ -56,9 +56,9 @@ describe('Acceptance: Editor', function () {
this.server.create('post', {authors: [author]});
await authenticateSession();
await visit('/editor/1');
await visit('/editor/post/1');
expect(currentURL(), 'currentURL').to.equal('/editor/1');
expect(currentURL(), 'currentURL').to.equal('/editor/post/1');
});
it('displays 404 when post does not exist', async function () {
@ -66,10 +66,10 @@ describe('Acceptance: Editor', function () {
this.server.create('user', {roles: [role], slug: 'test-user'});
await authenticateSession();
await visit('/editor/1');
await visit('/editor/post/1');
expect(currentRouteName()).to.equal('error404');
expect(currentURL()).to.equal('/editor/1');
expect(currentURL()).to.equal('/editor/post/1');
});
it('when logged in as a contributor, renders a save button instead of a publish menu & hides tags input', async function () {
@ -80,9 +80,9 @@ describe('Acceptance: Editor', function () {
await authenticateSession();
// post id 1 is a draft, checking for draft behaviour now
await visit('/editor/1');
await visit('/editor/post/1');
expect(currentURL(), 'currentURL').to.equal('/editor/1');
expect(currentURL(), 'currentURL').to.equal('/editor/post/1');
// Expect publish menu to not exist
expect(
@ -100,7 +100,7 @@ describe('Acceptance: Editor', function () {
).to.not.exist;
// post id 2 is published, we should be redirected to index
await visit('/editor/2');
await visit('/editor/post/2');
expect(currentURL(), 'currentURL').to.equal('/');
});
@ -121,10 +121,10 @@ describe('Acceptance: Editor', function () {
let futureTime = moment().tz('Etc/UTC').add(10, 'minutes');
// post id 1 is a draft, checking for draft behaviour now
await visit('/editor/1');
await visit('/editor/post/1');
expect(currentURL(), 'currentURL')
.to.equal('/editor/1');
.to.equal('/editor/post/1');
// open post settings menu
await click('[data-test-psm-trigger]');
@ -235,9 +235,9 @@ describe('Acceptance: Editor', function () {
).to.equal('Unpublish');
// post id 2 is a published post, checking for published post behaviour now
await visit('/editor/2');
await visit('/editor/post/2');
expect(currentURL(), 'currentURL').to.equal('/editor/2');
expect(currentURL(), 'currentURL').to.equal('/editor/post/2');
expect(find('[data-test-date-time-picker-date-input]').value).to.equal('12/19/2015');
expect(find('[data-test-date-time-picker-time-input]').value).to.equal('16:25');
@ -280,10 +280,10 @@ describe('Acceptance: Editor', function () {
.to.equal('(GMT +12:00) International Date Line West');
// and now go back to the editor
await visit('/editor/2');
await visit('/editor/post/2');
expect(currentURL(), 'currentURL in editor')
.to.equal('/editor/2');
.to.equal('/editor/post/2');
expect(
find('[data-test-date-time-picker-date-input]').value,
@ -436,7 +436,7 @@ describe('Acceptance: Editor', function () {
let post = this.server.create('post', 1, {authors: [author], status: 'draft'});
let plusTenMin = moment().utc().add(10, 'minutes');
await visit(`/editor/${post.id}`);
await visit(`/editor/post/${post.id}`);
await click('[data-test-publishmenu-trigger]');
await click('[data-test-publishmenu-scheduled-option]');
@ -461,10 +461,10 @@ describe('Acceptance: Editor', function () {
this.server.create('post', {authors: [author]});
// post id 1 is a draft, checking for draft behaviour now
await visit('/editor/1');
await visit('/editor/post/1');
expect(currentURL(), 'currentURL')
.to.equal('/editor/1');
.to.equal('/editor/post/1');
await fillIn('[data-test-editor-title-input]', Array(260).join('a'));
await click('[data-test-publishmenu-trigger]');
@ -486,10 +486,10 @@ describe('Acceptance: Editor', function () {
// this.server.createList('post', 1);
//
// // post id 1 is a draft, checking for draft behaviour now
// await visit('/editor/1');
// await visit('/editor/post/1');
//
// expect(currentURL(), 'currentURL')
// .to.equal('/editor/1');
// .to.equal('/editor/post/1');
//
// await titleRendered();
//
@ -508,10 +508,10 @@ describe('Acceptance: Editor', function () {
// this.server.createList('post', 1);
//
// // post id 1 is a draft, checking for draft behaviour now
// await visit('/editor/1');
// await visit('/editor/post/1');
//
// expect(currentURL(), 'currentURL')
// .to.equal('/editor/1');
// .to.equal('/editor/post/1');
//
// await titleRendered();
//
@ -529,10 +529,10 @@ describe('Acceptance: Editor', function () {
this.server.create('setting', {activeTimezone: 'Europe/Dublin'});
clock.restore();
await visit('/editor/1');
await visit('/editor/post/1');
expect(currentURL(), 'currentURL')
.to.equal('/editor/1');
.to.equal('/editor/post/1');
expect(find('[data-test-date-time-picker-date-input]').value, 'scheduled date')
.to.equal(compareDateString);
expect(find('[data-test-date-time-picker-time-input]').value, 'scheduled time')
@ -552,10 +552,10 @@ describe('Acceptance: Editor', function () {
this.server.create('user', {name: 'Waldo', roles: [authorRole]});
this.server.create('post', {authors: [user1]});
await visit('/editor/1');
await visit('/editor/post/1');
expect(currentURL(), 'currentURL')
.to.equal('/editor/1');
.to.equal('/editor/post/1');
await click('button.post-settings');
@ -586,7 +586,7 @@ describe('Acceptance: Editor', function () {
expect(
currentURL(),
'url on initial visit'
).to.equal('/editor');
).to.equal('/editor/post');
await click('[data-test-editor-title-input]');
await blur('[data-test-editor-title-input]');
@ -599,13 +599,13 @@ describe('Acceptance: Editor', function () {
expect(
currentURL(),
'url after autosave'
).to.equal('/editor/1');
).to.equal('/editor/post/1');
});
it('saves post settings fields', async function () {
let post = this.server.create('post', {authors: [author]});
await visit(`/editor/${post.id}`);
await visit(`/editor/post/${post.id}`);
// TODO: implement tests for other fields

View File

@ -45,7 +45,7 @@ describe('Acceptance: Error Handling', function () {
expect(find('.gh-alert').textContent).to.match(/refresh/);
// try navigating back to the content list
await click('[data-test-link="stories"]');
await click('[data-test-link="posts"]');
expect(currentRouteName()).to.equal('editor.edit');
});
@ -123,7 +123,7 @@ describe('Acceptance: Error Handling', function () {
this.server.put('/posts/1/', htmlErrorResponse);
this.server.create('post');
await visit('/editor/1');
await visit('/editor/post/1');
await click('[data-test-publishmenu-trigger]');
await click('[data-test-publishmenu-save]');

View File

@ -60,7 +60,9 @@ describe('Integration: Component: gh-psm-template-select', function () {
it('disables template selector if slug matches post template', async function () {
this.set('post', {
slug: 'one',
page: false
constructor: {
modelName: 'post'
}
});
await render(hbs`{{gh-psm-template-select post=post}}`);
@ -73,7 +75,9 @@ describe('Integration: Component: gh-psm-template-select', function () {
it('disables template selector if slug matches page template', async function () {
this.set('post', {
slug: 'about',
page: true
constructor: {
modelName: 'page'
}
});
await render(hbs`{{gh-psm-template-select post=post}}`);

View File

@ -291,65 +291,6 @@ describe.skip('Unit: Component: post-settings-menu', function () {
});
});
describe('togglePage', function () {
it('should toggle the page property', function () {
let component = this.subject({
post: EmberObject.create({
page: false,
isNew: true
})
});
expect(component.get('post.page')).to.not.be.ok;
run(function () {
component.send('togglePage');
expect(component.get('post.page')).to.be.ok;
});
});
it('should not save the post if it is still new', function () {
let component = this.subject({
post: EmberObject.create({
page: false,
isNew: true,
save() {
this.incrementProperty('saved');
return RSVP.resolve();
}
})
});
run(function () {
component.send('togglePage');
expect(component.get('post.page')).to.be.ok;
expect(component.get('post.saved')).to.not.be.ok;
});
});
it('should save the post if it is not new', function () {
let component = this.subject({
post: EmberObject.create({
page: false,
isNew: false,
save() {
this.incrementProperty('saved');
return RSVP.resolve();
}
})
});
run(function () {
component.send('togglePage');
expect(component.get('post.page')).to.be.ok;
expect(component.get('post.saved')).to.equal(1);
});
});
});
describe('toggleFeatured', function () {
it('should toggle the featured property', function () {
let component = this.subject({