New content screen prototype (#503)

refs https://github.com/TryGhost/Ghost/issues/7860

- remove preview pane from content screen
- add basic post status filters
- replace custom infinite scroll with ember-infinity and increase trigger threshold for improved scroll behaviour

Commits:
* basic content list + filter using existing infinite scroll and pagination
* swap our custom pagination + infinite loader for `ember-infinity`
* minor cleanups
* reset scroll position when changing filter
* fix tests
* remove client-side sorting step as we no longer have a live collection
* remove unused `mobile-index-route`
* add acceptance tests for content screen filters
This commit is contained in:
Kevin Ansfield 2017-01-25 20:05:28 +00:00 committed by Katharina Irrgang
parent 74b020e0e3
commit c16d633d4b
27 changed files with 504 additions and 598 deletions

View File

@ -1,11 +0,0 @@
import Component from 'ember-component';
import {reads} from 'ember-computed';
import injectService from 'ember-service/inject';
export default Component.extend({
tagName: 'section',
classNames: ['gh-view', 'content-view-container'],
mediaQueries: injectService(),
previewIsHidden: reads('mediaQueries.maxWidth900')
});

View File

@ -1,21 +1,16 @@
import $ from 'jquery';
import Ember from 'ember';
import Component from 'ember-component';
import {htmlSafe} from 'ember-string';
import computed, {alias, equal} from 'ember-computed';
import injectService from 'ember-service/inject';
import ActiveLinkMixin from 'ember-cli-active-link-wrapper/mixins/active-link';
import {invokeAction} from 'ember-invoke-action';
// ember-cli-shims doesn't export these
const {Handlebars, ObjectProxy, PromiseProxyMixin} = Ember;
const ObjectPromiseProxy = ObjectProxy.extend(PromiseProxyMixin);
export default Component.extend(ActiveLinkMixin, {
export default Component.extend({
tagName: 'li',
classNameBindings: ['isFeatured:featured', 'isPage:page'],
post: null,
previewIsHidden: false,
@ -54,45 +49,5 @@ export default Component.extend(ActiveLinkMixin, {
doubleClick() {
this.sendAction('onDoubleClick', this.get('post'));
},
didInsertElement() {
this._super(...arguments);
this.addObserver('active', this, this.scrollIntoView);
},
willDestroyElement() {
this._super(...arguments);
this.removeObserver('active', this, this.scrollIntoView);
if (this.get('post.isDeleted') && this.get('onDelete')) {
invokeAction(this, 'onDelete');
}
},
scrollIntoView() {
if (!this.get('active')) {
return;
}
let element = this.$();
let offset = element.offset().top;
let elementHeight = element.height();
let container = $('.js-content-scrollbox');
let containerHeight = container.height();
let currentScroll = container.scrollTop();
let isBelowTop, isAboveBottom, isOnScreen;
isAboveBottom = offset < containerHeight;
isBelowTop = offset > elementHeight;
isOnScreen = isBelowTop && isAboveBottom;
if (!isOnScreen) {
// Scroll so that element is centered in container
// 40 is the amount of padding on the container
container.clearQueue().animate({
scrollTop: currentScroll + offset - 40 - containerHeight / 2
});
}
}
});

View File

@ -1,26 +1,5 @@
import Ember from 'ember';
import Controller from 'ember-controller';
import computed, {equal} from 'ember-computed';
import injectService from 'ember-service/inject';
const {compare} = Ember;
export default Controller.extend({
feature: injectService(),
showDeletePostModal: false,
// See PostsRoute's shortcuts
postListFocused: equal('keyboardFocus', 'postList'),
postContentFocused: equal('keyboardFocus', 'postContent'),
sortedPosts: computed('model.@each.{status,publishedAtUTC,isNew,updatedAtUTC}', function () {
return this.get('model').toArray().sort(compare);
}),
actions: {
toggleDeletePostModal() {
this.toggleProperty('showDeletePostModal');
}
}
});

View File

@ -0,0 +1,23 @@
import Controller from 'ember-controller';
import computed from 'ember-computed';
import injectService from 'ember-service/inject';
export default Controller.extend({
queryParams: ['type'],
type: null,
session: injectService(),
showDeletePostModal: false,
showingAll: computed('type', function () {
return this.get('type') === null;
}),
actions: {
toggleDeletePostModal() {
this.toggleProperty('showDeletePostModal');
}
}
});

View File

@ -30,12 +30,17 @@ export default Mixin.create({
}),
init() {
// don't merge defaults if paginationSettings is a CP
if (!this.paginationSettings.isDescriptor) {
let paginationSettings = this.get('paginationSettings');
let settings = assign({}, defaultPaginationSettings, paginationSettings);
this._super(...arguments);
this.set('paginationSettings', settings);
}
this.set('paginationMeta', {});
this._super(...arguments);
},
reportLoadError(error) {

View File

@ -31,9 +31,7 @@ GhostRouter.map(function () {
this.route('reset', {path: '/reset/:token'});
this.route('about', {path: '/about'});
this.route('posts', {path: '/'}, function () {
this.route('post', {path: ':post_id'});
});
this.route('posts', {path: '/'}, function() {});
this.route('editor', function () {
this.route('new', {path: ''});

View File

@ -1,35 +0,0 @@
import Route from 'ember-route';
import {addObserver, removeObserver} from 'ember-metal/observer';
import injectService from 'ember-service/inject';
function K() {
return this;
}
// Routes that extend MobileIndexRoute need to implement
// desktopTransition, a function which is called when
// the user resizes to desktop levels.
export default Route.extend({
desktopTransition: K,
_callDesktopTransition: null,
mediaQueries: injectService(),
activate() {
this._super(...arguments);
this._callDesktopTransition = () => {
if (!this.get('mediaQueries.isMobile')) {
this.desktopTransition();
}
};
addObserver(this, 'mediaQueries.isMobile', this._callDesktopTransition);
},
deactivate() {
this._super(...arguments);
if (this._callDesktopTransition) {
removeObserver(this, 'mediaQueries.isMobile', this._callDesktopTransition);
this._callDesktopTransition = null;
}
}
});

View File

@ -1,100 +1,5 @@
import $ from 'jquery';
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import ShortcutsRoute from 'ghost-admin/mixins/shortcuts-route';
import PaginationMixin from 'ghost-admin/mixins/pagination';
export default AuthenticatedRoute.extend(ShortcutsRoute, PaginationMixin, {
titleToken: 'Content',
paginationModel: 'post',
paginationSettings: {
status: 'all',
staticPages: 'all'
},
model() {
let paginationSettings = this.get('paginationSettings');
return this.get('session.user').then((user) => {
if (user.get('isAuthor')) {
paginationSettings.filter = paginationSettings.filter
? `${paginationSettings.filter}+author:${user.get('slug')}` : `author:${user.get('slug')}`;
}
return this.loadFirstPage().then(() => {
// using `.filter` allows the template to auto-update when new models are pulled in from the server.
// we just need to 'return true' to allow all models by default.
return this.store.filter('post', (post) => {
if (user.get('isAuthor')) {
return post.isAuthoredByUser(user);
}
return true;
});
});
});
},
stepThroughPosts(step) {
let currentPost = this.get('controller.currentPost');
let posts = this.get('controller.sortedPosts');
let length = posts.get('length');
let newPosition = posts.indexOf(currentPost) + step;
// if we are on the first or last item
// just do nothing (desired behavior is to not
// loop around)
if (newPosition >= length) {
return;
} else if (newPosition < 0) {
return;
}
this.transitionTo('posts.post', posts.objectAt(newPosition));
},
scrollContent(amount) {
let content = $('.js-content-preview');
let scrolled = content.scrollTop();
content.scrollTop(scrolled + 50 * amount);
},
shortcuts: {
'up, k': 'moveUp',
'down, j': 'moveDown',
left: 'focusList',
right: 'focusContent',
c: 'newPost'
},
actions: {
focusList() {
this.controller.set('keyboardFocus', 'postList');
},
focusContent() {
this.controller.set('keyboardFocus', 'postContent');
},
newPost() {
this.transitionTo('editor.new');
},
moveUp() {
if (this.controller.get('postContentFocused')) {
this.scrollContent(-1);
} else {
this.stepThroughPosts(-1);
}
},
moveDown() {
if (this.controller.get('postContentFocused')) {
this.scrollContent(1);
} else {
this.stepThroughPosts(1);
}
}
}
export default AuthenticatedRoute.extend({
titleToken: 'Content'
});

View File

@ -1,53 +1,104 @@
import {reads} from 'ember-computed';
import injectService from 'ember-service/inject';
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import ShortcutsRoute from 'ghost-admin/mixins/shortcuts-route';
import InfinityRoute from 'ember-infinity/mixins/route';
import computed from 'ember-computed';
import {assign} from 'ember-platform';
import $ from 'jquery';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
import MobileIndexRoute from 'ghost-admin/routes/mobile-index-route';
export default AuthenticatedRoute.extend(InfinityRoute, ShortcutsRoute, {
export default MobileIndexRoute.extend(AuthenticatedRouteMixin, {
noPosts: false,
perPageParam: 'limit',
totalPagesParam: 'meta.pagination.pages',
mediaQueries: injectService(),
isMobile: reads('mediaQueries.isMobile'),
_type: null,
// Transition to a specific post if we're not on mobile
beforeModel() {
this._super(...arguments);
if (!this.get('isMobile')) {
return this.goToPost();
}
},
setupController(controller) {
controller.set('noPosts', this.get('noPosts'));
this._super(...arguments);
},
goToPost() {
// the store has been populated by PostsRoute
let posts = this.store.peekAll('post');
let post;
model(params) {
this.set('_type', params.type);
let filterSettings = this.get('filterSettings');
return this.get('session.user').then((user) => {
post = posts.find(function (post) {
// Authors can only see posts they've written
if (user.get('isAuthor')) {
return post.isAuthoredByUser(user);
filterSettings.filter = filterSettings.filter
? `${filterSettings.filter}+author:${user.get('slug')}` : `author:${user.get('slug')}`;
}
return true;
});
let paginationSettings = assign({perPage: 15, startingPage: 1}, filterSettings);
if (post) {
return this.transitionTo('posts.post', post);
}
this.set('noPosts', true);
return this.infinityModel('post', paginationSettings);
});
},
// Mobile posts route callback
desktopTransition() {
this.goToPost();
filterSettings: computed('_type', function () {
let type = this.get('_type');
let status = 'all';
let staticPages = 'all';
switch (type) {
case 'draft':
status = 'draft';
staticPages = false;
break;
case 'published':
status = 'published';
staticPages = false;
break;
case 'scheduled':
status = 'scheduled';
staticPages = false;
break;
case 'page':
staticPages = true;
break;
}
return {
status,
staticPages
};
}),
stepThroughPosts(step) {
let currentPost = this.get('controller.currentPost');
let posts = this.get('controller.sortedPosts');
let length = posts.get('length');
let newPosition = posts.indexOf(currentPost) + step;
// if we are on the first or last item
// just do nothing (desired behavior is to not
// loop around)
if (newPosition >= length) {
return;
} else if (newPosition < 0) {
return;
}
// TODO: highlight post
// this.transitionTo('posts.post', posts.objectAt(newPosition));
},
shortcuts: {
'up, k': 'moveUp',
'down, j': 'moveDown',
c: 'newPost'
},
actions: {
queryParamsDidChange() {
this.refresh();
// reset the scroll position
$('.content-list-content').scrollTop(0);
},
newPost() {
this.transitionTo('editor.new');
},
moveUp() {
this.stepThroughPosts(-1);
},
moveDown() {
this.stepThroughPosts(1);
}
}
});

View File

@ -1,64 +0,0 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import ShortcutsRoute from 'ghost-admin/mixins/shortcuts-route';
export default AuthenticatedRoute.extend(ShortcutsRoute, {
model(params) {
/* eslint-disable camelcase */
let post = this.store.peekRecord('post', params.post_id);
let query = {
id: params.post_id,
status: 'all',
staticPages: 'all'
};
/* eslint-enable camelcase */
if (post) {
return post;
}
return this.store.query('post', query).then((records) => {
let post = records.get('firstObject');
if (post) {
return post;
}
return this.replaceWith('posts.index');
});
},
afterModel(post) {
return this.get('session.user').then((user) => {
if (user.get('isAuthor') && !post.isAuthoredByUser(user)) {
return this.replaceWith('posts.index');
}
});
},
setupController(controller, model) {
this._super(controller, model);
this.controllerFor('posts').set('currentPost', model);
},
shortcuts: {
'enter, o': 'openEditor',
'command+backspace, ctrl+backspace': 'deletePost'
},
actions: {
openEditor(post) {
post = post || this.get('controller.model');
if (!post) {
return;
}
this.transitionTo('editor.edit', post.get('id'));
},
deletePost() {
this.controllerFor('posts').send('toggleDeletePostModal');
}
}
});

View File

@ -48,3 +48,12 @@
transform: rotate(360deg);
}
}
/* Infinite scroll */
.infinity-loader {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 2em 0;
}

View File

@ -1,26 +1,35 @@
/* Content /ghost/
/* Show/Hide on Mobile // TODO: What the fuck does that mean?
/* Header
/* ---------------------------------------------------------- */
.content-list.show-menu {
display: block;
.gh-content-view-container .view-header {
border-bottom: none;
}
.content-list.show-content {
display: none;
.basic-filter {
display: flex;
justify-content: space-between;
border-bottom: #dfe1e3 1px solid;
}
.content-preview.show-menu {
display: none;
.basic-filter > ul {
display: flex;
flex-direction: row;
margin-bottom: 0;
}
.content-preview.show-content {
display: block;
.basic-filter > ul li {
list-style: none;
display: inline-block;
margin-right: 1em;
}
.basic-filter > ul a.active {
text-decoration: underline;
}
/* Content List (Left pane)
/* Content List
/* ---------------------------------------------------------- */
.content-list {
@ -28,7 +37,7 @@
top: 0;
bottom: 0;
left: 0;
width: 30%;
width: 100%;
border-right: #dfe1e3 1px solid;
background: #fff;
}
@ -163,80 +172,6 @@
}
/* Preview (Right pane)
/* ---------------------------------------------------------- */
.content-preview-title a {
position: relative;
color: var(--darkgrey);
text-decoration: none;
}
.content-preview {
position: absolute;
top: 0;
right: 0;
bottom: 0;
overflow: auto;
-webkit-overflow-scrolling: touch;
width: 70%;
background: #fff;
}
@media (max-width: 900px) {
.content-preview {
display: none;
overflow: visible;
width: 100%;
border: none;
}
}
.content-preview-content {
padding: 5vw 6vw;
word-break: break-word;
hyphens: auto;
}
@media (max-width: 900px) {
.content-preview-content {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
}
.content-preview-content .wrapper {
margin: 0 auto;
max-width: 700px;
}
.content-preview .post-controls {
position: absolute;
top: 20px;
right: 25px;
}
.content-preview .post-controls .post-edit {
padding-top: 0;
padding-right: 0;
border: none;
font-size: 18px;
}
.content-preview .post-controls .post-edit:hover {
color: var(--darkgrey);
}
.content-preview img {
width: 100%;
height: auto;
}
/* Empty State
/* ---------------------------------------------------------- */
@ -270,7 +205,6 @@
/* This has to be a pseudo element to sit over the top of everything else in the content list */
.content-list.keyboard-focused:before,
.tag-list-content.keyboard-focused:before,
.tag-settings.keyboard-focused:before {
content: "";
@ -283,7 +217,3 @@
animation: keyboard-focus-style-fade-out 1.5s 1 forwards;
pointer-events: none;
}
.content-preview.keyboard-focused {
animation: keyboard-focus-style-fade-out 1.5s 1 forwards;
}

View File

@ -1 +0,0 @@
{{yield previewIsHidden}}

View File

@ -1,4 +1,4 @@
{{#link-to (if previewIsHidden 'editor.edit' 'posts.post') post.id class="permalink" title="Edit this post"}}
{{#link-to "editor.edit" post.id class="permalink" title="Edit this post"}}
<h3 class="entry-title">{{post.title}}</h3>
<section class="entry-meta">
<span class="avatar" style={{authorAvatarBackground}}>

View File

@ -0,0 +1,8 @@
{{#if hasBlock}}
{{yield}}
{{else}}
{{#if infinityModel.reachedInfinity}}
{{else}}
<div class="gh-loading-spinner"></div>
{{/if}}
{{/if}}

View File

@ -1,30 +1,42 @@
{{#gh-content-view-container as |previewIsHidden|}}
<header class="view-header">
<section class="gh-view gh-content-view-container">
<header class="view-header">
{{#gh-view-title openMobileMenu="openMobileMenu"}}<span>Content</span>{{/gh-view-title}}
<section class="view-actions">
{{#link-to "editor.new" class="btn btn-green" title="New Post"}}New Post{{/link-to}}
{{#link-to "editor.new" class="btn btn-green" title="New Post" data-test-new-post-button=true}}
New Post
{{/link-to}}
</section>
</header>
</header>
<div class="view-container">
<section class="content-list js-content-list {{if postListFocused 'keyboard-focused'}}">
{{#gh-infinite-scroll tagName="section" classNames="content-list-content js-content-scrollbox" fetch="loadNextPage" as |checkScroll|}}
<ol class="posts-list">
{{#each sortedPosts key="id" as |post|}}
{{gh-posts-list-item post=post previewIsHidden=previewIsHidden onDoubleClick="openEditor" onDelete=(action checkScroll)}}
{{/each}}
</ol>
{{/gh-infinite-scroll}}
<section class="basic-filter">
<ul>
<li>
{{#link-to "posts.index" (query-params type=null) data-test-all-filter-link=true}}
All
{{/link-to}}
</li>
<li>
{{#link-to "posts.index" (query-params type="draft") data-test-drafts-filter-link=true}}
Drafts
{{/link-to}}
</li>
<li>
{{#link-to "posts.index" (query-params type="published") data-test-published-filter-link=true}}
Published
{{/link-to}}
</li>
<li>
{{#link-to "posts.index" (query-params type="scheduled") data-test-scheduled-filter-link=true}}
Scheduled
{{/link-to}}
</li>
<li>
{{#link-to "posts.index" (query-params type="page") data-test-pages-filter-link=true}}
Pages
{{/link-to}}
</li>
</ul>
</section>
<section class="content-preview js-content-preview {{if postContentFocused 'keyboard-focused'}}">
{{outlet}}
</section>
</div>
{{/gh-content-view-container}}
{{#if showDeletePostModal}}
{{gh-fullscreen-modal "delete-post"
model=currentPost
close=(action "toggleDeletePostModal")
modifier="action wide"}}
{{/if}}
</section>

View File

@ -1,8 +1,40 @@
<div class="no-posts-box">
{{#if noPosts}}
<div class="view-container">
<section class="content-list js-content-list {{if postListFocused 'keyboard-focused'}}">
<section class="content-list-content">
<ol class="posts-list">
{{#each model as |post|}}
{{gh-posts-list-item
post=post
onDoubleClick="openEditor"
data-test-posts-list-item-id=post.id}}
{{else}}
<li class="no-posts-box">
<div class="no-posts">
{{#if showingAll}}
<h3>You Haven't Written Any Posts Yet!</h3>
{{#link-to "editor.new"}}<button type="button" class="btn btn-green btn-lg" title="New Post">Write a new Post</button>{{/link-to}}
</div>
{{else}}
<h3>No posts that match the current filter</h3>
{{#link-to "posts.index" (query-params type=null)}}<button type="button" class="btn btn-lg">Show all posts</button>{{/link-to}}
{{/if}}
</div>
</li>
{{/each}}
{{infinity-loader
infinityModel=model
scrollable=".content-list-content"
triggerOffset=1000}}
</ol>
</section>
</section>
</div>
{{#if showDeletePostModal}}
{{gh-fullscreen-modal "delete-post"
model=currentPost
close=(action "toggleDeletePostModal")
modifier="action wide"}}
{{/if}}
{{outlet}}

View File

@ -1,14 +0,0 @@
<section class="post-controls">
{{#link-to "editor.edit" model.id class="btn btn-minor post-edit" title="Edit this post"}}<i class="icon-edit"></i>{{/link-to}}
</section>
{{#gh-content-preview-content tagName="section" content=model}}
<div class="wrapper">
<h1 class="content-preview-title">
{{#link-to "editor.edit" model.id}}
{{model.title}}
{{/link-to}}
</h1>
{{gh-format-html model.html}}
</div>
{{/gh-content-preview-content}}

View File

@ -64,7 +64,7 @@ codemirrorAssets = function () {
module.exports = function (defaults) {
var app = new EmberApp(defaults, {
babel: {
"ember-cli-babel": {
optional: ['es6.spec.symbols'],
includePolyfill: true
},

View File

@ -1,6 +1,6 @@
import {Response} from 'ember-cli-mirage';
import {isBlank} from 'ember-utils';
import {paginatedResponse} from '../utils';
import {paginateModelArray} from '../utils';
import {dasherize} from 'ember-string';
export default function mockPosts(server) {
@ -11,12 +11,33 @@ export default function mockPosts(server) {
attrs.slug = dasherize(attrs.title);
}
// NOTE: this does not use the post factory to fill in blank fields
return posts.create(attrs);
});
// TODO: handle status/staticPages/author params
server.get('/posts/', paginatedResponse('posts'));
// TODO: handle author filter
server.get('/posts/', function ({posts}, {queryParams}) {
let page = +queryParams.page || 1;
let limit = +queryParams.limit || 15;
let {status, staticPages} = queryParams;
let query = {};
let models;
if (status && status !== 'all') {
query.status = status;
}
if (staticPages === 'false') {
query.page = false;
}
if (staticPages === 'true') {
query.page = true;
}
models = posts.where(query).models;
return paginateModelArray('posts', models, page, limit);
});
server.get('/posts/:id/', function ({posts}, {params}) {
let {id} = params;

View File

@ -4,11 +4,16 @@ import {Response} from 'ember-cli-mirage';
export function paginatedResponse(modelName) {
return function (schema, request) {
let page = +request.queryParams.page || 1;
let limit = request.queryParams.limit || 15;
let pages, next, prev, models;
let limit = +request.queryParams.limit || 15;
let allModels = this.serialize(schema[modelName].all())[modelName];
return paginateModelArray(modelName, allModels, page, limit);
};
}
export function paginateModelArray(modelName, allModels, page, limit) {
let pages, next, prev, models;
if (limit === 'all') {
pages = 1;
} else {
@ -42,7 +47,6 @@ export function paginatedResponse(modelName) {
},
[modelName]: models || allModels
};
};
}
export function maintenanceResponse() {

View File

@ -63,6 +63,7 @@
"ember-data": "2.11.0",
"ember-data-filter": "1.13.0",
"ember-export-application-global": "1.1.1",
"ember-infinity": "0.2.8",
"ember-invoke-action": "1.4.0",
"ember-light-table": "1.8.1",
"ember-load": "0.0.11",
@ -75,6 +76,7 @@
"ember-simple-auth": "1.1.0",
"ember-sinon": "0.6.0",
"ember-sortable": "1.9.1",
"ember-test-selectors": "0.2.0",
"ember-wormhole": "0.5.1",
"emberx-file-input": "1.1.1",
"eslint-plugin-ember-suave": "1.0.0",

View File

@ -33,6 +33,7 @@ describe('Acceptance: Authentication', function () {
beforeEach(function () {
originalReplaceLocation = windowProxy.replaceLocation;
windowProxy.replaceLocation = function (url) {
url = url.replace(/^\/ghost\//, '/');
visit(url);
};

View File

@ -0,0 +1,160 @@
import {describe, it, beforeEach, afterEach} from 'mocha';
import {expect} from 'chai';
import startApp from '../helpers/start-app';
import destroyApp from '../helpers/destroy-app';
import {
invalidateSession,
authenticateSession
} from 'ghost-admin/tests/helpers/ember-simple-auth';
import testSelector from 'ember-test-selectors';
describe('Acceptance: Content', function() {
let application;
beforeEach(function() {
application = startApp();
});
afterEach(function() {
destroyApp(application);
});
it('redirects to signin when not authenticated', function () {
invalidateSession(application);
visit('/');
andThen(function () {
expect(currentURL()).to.equal('/signin');
});
});
describe('as admin', function () {
let publishedPost, scheduledPost, draftPost, publishedPage, authorPost;
beforeEach(function () {
let adminRole = server.create('role', {name: 'Administrator'});
let admin = server.create('user', {roles: [adminRole]});
let editorRole = server.create('role', {name: 'Editor'});
let editor = server.create('user', {roles: [editorRole]});
publishedPost = server.create('post', {authorId: admin.id, status: 'published', title: 'Published Post'});
scheduledPost = server.create('post', {authorId: admin.id, status: 'scheduled', title: 'Scheduled Post'});
draftPost = server.create('post', {authorId: admin.id, status: 'draft', title: 'Draft Post'});
publishedPage = server.create('post', {authorId: admin.id, status: 'published', page: true, title: 'Published Page'});
authorPost = server.create('post', {authorId: editor.id, status: 'published', title: 'Editor Published Post'});
return authenticateSession(application);
});
it('displays and filters posts', function () {
visit('/');
andThen(() => {
// All filter is active by default
expect(find(testSelector('all-filter-link'))).to.have.class('active');
// Not checking request here as it won't be the last request made
// Displays all posts + pages
expect(find(testSelector('posts-list-item-id')).length, 'all posts count').to.equal(5);
});
click(testSelector('drafts-filter-link'));
andThen(() => {
// Filter link is highlighted
expect(find(testSelector('drafts-filter-link'))).to.have.class('active');
// API request is correct
let [lastRequest] = server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.status, '"drafts" request status param').to.equal('draft');
expect(lastRequest.queryParams.staticPages, '"drafts" request staticPages param').to.equal('false');
// Displays draft post
expect(find(testSelector('posts-list-item-id')).length, 'drafts count').to.equal(1);
expect(find(testSelector('posts-list-item-id', draftPost.id)), 'draft post').to.exist;
});
click(testSelector('published-filter-link'));
andThen(() => {
// Filter link is highlighted
expect(find(testSelector('published-filter-link'))).to.have.class('active');
// API request is correct
let [lastRequest] = server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.status, '"published" request status param').to.equal('published');
expect(lastRequest.queryParams.staticPages, '"published" request staticPages param').to.equal('false');
// Displays three published posts + pages
expect(find(testSelector('posts-list-item-id')).length, 'published count').to.equal(2);
expect(find(testSelector('posts-list-item-id', publishedPost.id)), 'admin published post').to.exist;
expect(find(testSelector('posts-list-item-id', authorPost.id)), 'author published post').to.exist;
});
click(testSelector('scheduled-filter-link'));
andThen(() => {
// Filter link is highlighted
expect(find(testSelector('scheduled-filter-link'))).to.have.class('active');
// API request is correct
let [lastRequest] = server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.status, '"scheduled" request status param').to.equal('scheduled');
expect(lastRequest.queryParams.staticPages, '"scheduled" request staticPages param').to.equal('false');
// Displays scheduled post
expect(find(testSelector('posts-list-item-id')).length, 'scheduled count').to.equal(1);
expect(find(testSelector('posts-list-item-id', scheduledPost.id)), 'scheduled post').to.exist;
});
click(testSelector('pages-filter-link'));
andThen(() => {
// Filter link is highlighted
expect(find(testSelector('pages-filter-link'))).to.have.class('active');
// API request is correct
let [lastRequest] = server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.status, '"pages" request status param').to.equal('all');
expect(lastRequest.queryParams.staticPages, '"pages" request staticPages param').to.equal('true');
// Displays page
expect(find(testSelector('posts-list-item-id')).length, 'pages count').to.equal(1);
expect(find(testSelector('posts-list-item-id', publishedPage.id)), 'page post').to.exist;
});
click(testSelector('all-filter-link'));
andThen(() => {
// API request is correct
let [lastRequest] = server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.status, '"all" request status param').to.equal('all');
expect(lastRequest.queryParams.staticPages, '"all" request staticPages param').to.equal('all');
});
});
});
describe('as author', function () {
let author, authorPost;
beforeEach(function () {
let authorRole = server.create('role', {name: 'Author'});
author = server.create('user', {roles: [authorRole]});
let adminRole = server.create('role', {name: 'Administrator'});
let admin = server.create('user', {roles: [adminRole]});
// create posts
authorPost = server.create('post', {authorId: author.id, status: 'published', title: 'Author Post'});
server.create('post', {authorId: admin.id, status: 'scheduled', title: 'Admin Post'});
return authenticateSession(application);
});
it('only fetches the author\'s posts', function () {
visit('/');
// trigger a filter request so we can grab the posts API request easily
click(testSelector('published-filter-link'));
andThen(() => {
// API request includes author filter
let [lastRequest] = server.pretender.handledRequests.slice(-1);
expect(lastRequest.queryParams.filter).to.equal(`author:${author.slug}`);
// only author's post is shown
expect(find(testSelector('posts-list-item-id')).length, 'post count').to.equal(1);
expect(find(testSelector('posts-list-item-id', authorPost.id)), 'author post').to.exist;
});
});
});
});

View File

@ -1,84 +0,0 @@
/* jshint expr:true */
/* eslint-disable camelcase */
import {
describe,
it,
beforeEach,
afterEach
} from 'mocha';
import {expect} from 'chai';
import startApp from '../../helpers/start-app';
import destroyApp from '../../helpers/destroy-app';
import {authenticateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
import {errorOverride, errorReset} from 'ghost-admin/tests/helpers/adapter-error';
import {Response} from 'ember-cli-mirage';
describe('Acceptance: Posts - Post', function() {
let application;
beforeEach(function() {
application = startApp();
});
afterEach(function() {
destroyApp(application);
});
describe('when logged in', function () {
beforeEach(function () {
let role = server.create('role', {name: 'Administrator'});
server.create('user', {roles: [role]});
return authenticateSession(application);
});
it('can visit post route', function () {
let posts = server.createList('post', 6);
visit('/');
andThen(() => {
expect(find('.posts-list li').length, 'post list count').to.equal(6);
// if we're in "desktop" size, we should redirect and highlight
if (find('.content-preview:visible').length) {
expect(currentURL(), 'currentURL').to.equal(`/${posts[0].id}`);
// expect(find('.posts-list li').first().hasClass('active'), 'highlights latest post').to.be.true;
expect(find('.posts-list li:nth-child(1) .status span').first().hasClass('scheduled'), 'first post in list is a scheduled one')
.to.be.true;
expect(find('.posts-list li:nth-child(3) .status span').first().hasClass('draft'), 'third post in list is a draft')
.to.be.true;
expect(find('.posts-list li:nth-child(5) .status time').first().hasClass('published'), 'fifth post in list is a published one')
.to.be.true;
}
});
// check if we can edit the post
click('.post-edit');
andThen(() => {
expect(currentURL(), 'currentURL to editor')
.to.equal('/editor/1');
});
// TODO: test the right order of the listes posts
// and fix the faker import to ensure correct ordering
});
it('redirects to 404 when post does not exist', function () {
server.get('/posts/200/', function () {
return new Response(404, {'Content-Type': 'application/json'}, {errors: [{message: 'Post not found.', errorType: 'NotFoundError'}]});
});
errorOverride();
visit('/200');
andThen(() => {
errorReset();
expect(currentPath()).to.equal('error404');
expect(currentURL()).to.equal('/200');
});
});
});
});

View File

@ -38,7 +38,6 @@ describe('Acceptance: Version Mismatch', function() {
visit('/');
click('.posts-list li:nth-of-type(2) a'); // select second post
click('.post-edit'); // preview edit button
click('.js-publish-button'); // "Save post"
andThen(() => {

View File

@ -1315,7 +1315,7 @@ commander@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.0.0.tgz#d1b86f901f8b64bd941bdeadaf924530393be928"
commander@2.3.0, commander@^2.1.0:
commander@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873"
@ -1325,7 +1325,7 @@ commander@2.8.x:
dependencies:
graceful-readlink ">= 1.0.0"
commander@^2.5.0, commander@^2.6.0, commander@^2.9.0:
commander@^2.1.0, commander@^2.5.0, commander@^2.6.0, commander@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
dependencies:
@ -1897,7 +1897,7 @@ ember-cli-htmlbars-inline-precompile@0.3.6:
ember-cli-htmlbars "^1.0.0"
hash-for-dep "^1.0.2"
ember-cli-htmlbars@1.1.1, ember-cli-htmlbars@^1.0.0, ember-cli-htmlbars@^1.0.10, ember-cli-htmlbars@^1.0.11, ember-cli-htmlbars@^1.0.3, ember-cli-htmlbars@^1.1.0, ember-cli-htmlbars@^1.1.1:
ember-cli-htmlbars@1.1.1, ember-cli-htmlbars@^1.0.0, ember-cli-htmlbars@^1.0.1, ember-cli-htmlbars@^1.0.10, ember-cli-htmlbars@^1.0.11, ember-cli-htmlbars@^1.0.3, ember-cli-htmlbars@^1.1.0, ember-cli-htmlbars@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-1.1.1.tgz#8776cf59796dac8f32e8625fc6d1ea45ffa55de1"
dependencies:
@ -2289,6 +2289,15 @@ ember-in-viewport@2.1.1:
ember-cli-babel "^5.1.6"
ember-getowner-polyfill "^1.1.1"
ember-infinity@0.2.8:
version "0.2.8"
resolved "https://registry.yarnpkg.com/ember-infinity/-/ember-infinity-0.2.8.tgz#813a24d0828446a44d09c21fee5adf371897d8dd"
dependencies:
ember-cli-babel "^5.1.5"
ember-cli-htmlbars "^1.0.1"
ember-cli-version-checker "^1.0.2"
ember-version-is "0.0.3"
ember-inflector@^1.9.2, ember-inflector@^1.9.4:
version "1.11.0"
resolved "https://registry.yarnpkg.com/ember-inflector/-/ember-inflector-1.11.0.tgz#99baae18e2bee53cfa97d8db1d739280289a174f"
@ -2459,6 +2468,12 @@ ember-test-helpers@^0.6.0-beta.1:
version "0.6.0"
resolved "https://registry.yarnpkg.com/ember-test-helpers/-/ember-test-helpers-0.6.0.tgz#1f644fd303437ef4d19430a3e18447d57a97cf36"
ember-test-selectors@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/ember-test-selectors/-/ember-test-selectors-0.2.0.tgz#d0305d01a96e3a0ad6a53f08f2d7b74975d3296e"
dependencies:
ember-cli-babel "^5.1.7"
ember-text-measurer@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/ember-text-measurer/-/ember-text-measurer-0.3.3.tgz#0762809a71c2e1f2e60ab00c53c6eb1b63c9f963"
@ -2506,6 +2521,12 @@ ember-try@^0.2.6:
semver "^5.1.0"
sync-exec "^0.6.2"
ember-version-is@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/ember-version-is/-/ember-version-is-0.0.3.tgz#7d54ec39ed5e03f0df11cf8a5e22dc20b0810b1a"
dependencies:
ember-cli-babel "^5.0.0"
ember-weakmap@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ember-weakmap/-/ember-weakmap-2.0.0.tgz#71c6819a8bfd0b077ae17ca1d9053fc5db06e4ac"
@ -5505,7 +5526,19 @@ read@1, read@~1.0.1, read@~1.0.7:
dependencies:
mute-stream "~0.0.4"
"readable-stream@1 || 2", readable-stream@^2, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, readable-stream@^2.2.2:
"readable-stream@1 || 2", readable-stream@^2, readable-stream@^2.0.2, readable-stream@~2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0"
dependencies:
buffer-shims "^1.0.0"
core-util-is "~1.0.0"
inherits "~2.0.1"
isarray "~1.0.0"
process-nextick-args "~1.0.6"
string_decoder "~0.10.x"
util-deprecate "~1.0.1"
"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e"
dependencies:
@ -5537,18 +5570,6 @@ readable-stream@~2.0.5:
string_decoder "~0.10.x"
util-deprecate "~1.0.1"
readable-stream@~2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0"
dependencies:
buffer-shims "^1.0.0"
core-util-is "~1.0.0"
inherits "~2.0.1"
isarray "~1.0.0"
process-nextick-args "~1.0.6"
string_decoder "~0.10.x"
util-deprecate "~1.0.1"
readdir-scoped-modules@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747"