Use token input to allow selection of multiple authors in PSM
requires https://github.com/TryGhost/Ghost/pull/9426 - fixed default token component display in {{gh-token-input}} - if no `tokenComponent` is passed to `{{gh-token-input}}` then it should default to the ember-drag-drop `draggable-object` component but instead it didn't output anything - put `draggable-object` in quotes because `{{component}}` needs a component name rather than an object - rename `option` attribute to `content` to match the default `{{draggable-object}}` interface - add embedded `authors` attr to the Post model - ensure authors is populated when starting new post - add validation for empty authors list - swap author dropdown for a token input in PSM - show all post authors in posts list - update tests for `authors` - always provide through an authors array - fix mirage serialisation for paginated responses (embedded records were not being serialised) - unify tags and author inputs design - remove highlight of primary tags - highlight internal tags - remove unnecessary/redundant title attributes on tags - use SVG icon for "remove option" button in token inputs
This commit is contained in:
parent
d361aa4b36
commit
38b138d759
|
@ -22,9 +22,7 @@ export default Component.extend(SettingsMenuMixin, {
|
|||
settings: service(),
|
||||
ui: service(),
|
||||
|
||||
authors: null,
|
||||
post: null,
|
||||
selectedAuthor: null,
|
||||
|
||||
_showSettingsMenu: false,
|
||||
_showThrobbers: false,
|
||||
|
@ -92,24 +90,9 @@ export default Component.extend(SettingsMenuMixin, {
|
|||
return seoURL;
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.authors = this.authors || [];
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.get('store').query('user', {limit: 'all'}).then((users) => {
|
||||
if (!this.get('isDestroyed')) {
|
||||
this.set('authors', users.sortBy('name'));
|
||||
}
|
||||
});
|
||||
|
||||
this.get('post.author').then((author) => {
|
||||
this.set('selectedAuthor', author);
|
||||
});
|
||||
|
||||
// HACK: ugly method of working around the CSS animations so that we
|
||||
// can add throbbers only when the animation has finished
|
||||
// TODO: use liquid-fire to handle PSM slide-in and replace tabs manager
|
||||
|
@ -485,25 +468,24 @@ export default Component.extend(SettingsMenuMixin, {
|
|||
});
|
||||
},
|
||||
|
||||
changeAuthor(newAuthor) {
|
||||
let author = this.get('post.author');
|
||||
changeAuthors(newAuthors) {
|
||||
let post = this.get('post');
|
||||
|
||||
// return if nothing changed
|
||||
if (newAuthor.get('id') === author.get('id')) {
|
||||
if (newAuthors.mapBy('id').join() === post.get('authors').mapBy('id').join()) {
|
||||
return;
|
||||
}
|
||||
|
||||
post.set('author', newAuthor);
|
||||
post.set('authors', newAuthors);
|
||||
post.validate({property: 'authors'});
|
||||
|
||||
// if this is a new post (never been saved before), don't try to save it
|
||||
if (this.get('post.isNew')) {
|
||||
if (post.get('isNew')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.get('savePost').perform().catch((error) => {
|
||||
this.showError(error);
|
||||
this.set('selectedAuthor', author);
|
||||
post.rollbackAttributes();
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import $ from 'jquery';
|
||||
import Component from '@ember/component';
|
||||
import Ember from 'ember';
|
||||
import {alias, equal} from '@ember/object/computed';
|
||||
import {computed} from '@ember/object';
|
||||
import {htmlSafe} from '@ember/string';
|
||||
import {isBlank} from '@ember/utils';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
const {Handlebars} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
ghostPaths: service(),
|
||||
|
||||
|
@ -29,19 +25,10 @@ export default Component.extend({
|
|||
isPublished: equal('post.status', 'published'),
|
||||
isScheduled: equal('post.status', 'scheduled'),
|
||||
|
||||
authorName: computed('post.author.{name,email}', function () {
|
||||
return this.get('post.author.name') || this.get('post.author.email');
|
||||
}),
|
||||
authorNames: computed('post.authors.[]', function () {
|
||||
let authors = this.get('post.authors');
|
||||
|
||||
authorAvatar: computed('post.author.profileImage', function () {
|
||||
let defaultImage = '/img/user-image.png';
|
||||
return this.get('post.author.profileImage') || `${this.get('ghostPaths.assetRoot')}${defaultImage}`;
|
||||
}),
|
||||
|
||||
authorAvatarBackground: computed('authorAvatar', function () {
|
||||
let authorAvatar = this.get('authorAvatar');
|
||||
let safeUrl = Handlebars.Utils.escapeExpression(authorAvatar);
|
||||
return htmlSafe(`background-image: url(${safeUrl})`);
|
||||
return authors.map(author => author.get('name') || author.get('email')).join(', ');
|
||||
}),
|
||||
|
||||
// HACK: this is intentionally awful due to time constraints
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import Component from '@ember/component';
|
||||
import {computed} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
|
||||
store: service(),
|
||||
|
||||
// public attrs
|
||||
selectedAuthors: null,
|
||||
tagName: '',
|
||||
triggerId: '',
|
||||
|
||||
// closure actions
|
||||
updateAuthors() {},
|
||||
|
||||
// live-query of all users for author input autocomplete
|
||||
availableAuthors: computed(function () {
|
||||
return this.get('store').filter('user', {limit: 'all'}, () => true);
|
||||
}),
|
||||
|
||||
availableAuthorNames: computed('availableAuthors.@each.name', function () {
|
||||
return this.get('availableAuthors').map(author => author.get('name').toLowerCase());
|
||||
}),
|
||||
|
||||
actions: {
|
||||
updateAuthors(newAuthors) {
|
||||
this.updateAuthors(newAuthors);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
/* global key */
|
||||
import Component from '@ember/component';
|
||||
import Ember from 'ember';
|
||||
import {A} from '@ember/array';
|
||||
import {A, isArray} from '@ember/array';
|
||||
import {
|
||||
advanceSelectableOption,
|
||||
defaultMatcher,
|
||||
|
@ -21,6 +21,7 @@ const TAB = 9;
|
|||
export default Component.extend({
|
||||
|
||||
// public attrs
|
||||
allowCreation: true,
|
||||
closeOnSelect: false,
|
||||
labelField: 'name',
|
||||
matcher: defaultMatcher,
|
||||
|
@ -85,6 +86,10 @@ export default Component.extend({
|
|||
}),
|
||||
|
||||
shouldShowCreateOption(term, options) {
|
||||
if (!this.get('allowCreation')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.get('showCreateWhen')) {
|
||||
return this.get('showCreateWhen')(term, options);
|
||||
} else {
|
||||
|
@ -140,6 +145,11 @@ export default Component.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
// guard against return being pressed when nothing is selected
|
||||
if (!isArray(selection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let suggestion = selection.find(option => option.__isSuggestion__);
|
||||
|
||||
if (suggestion) {
|
||||
|
|
|
@ -7,29 +7,19 @@ export default DraggableObject.extend({
|
|||
attributeBindings: ['title'],
|
||||
classNames: ['tag-token'],
|
||||
classNameBindings: [
|
||||
'primary:tag-token--primary',
|
||||
'internal:tag-token--internal'
|
||||
],
|
||||
|
||||
content: readOnly('option'),
|
||||
internal: readOnly('option.isInternal'),
|
||||
internal: readOnly('content.isInternal'),
|
||||
|
||||
primary: computed('idx', 'internal', function () {
|
||||
return !this.get('internal') && this.get('idx') === 0;
|
||||
}),
|
||||
|
||||
title: computed('option.name', 'primary', 'internal', function () {
|
||||
let name = this.get('option.name');
|
||||
|
||||
title: computed('internal', function () {
|
||||
if (this.get('internal')) {
|
||||
return `${name} (internal)`;
|
||||
return `Internal tag`;
|
||||
}
|
||||
|
||||
if (this.get('primary')) {
|
||||
return `${name} (primary tag)`;
|
||||
}
|
||||
|
||||
return name;
|
||||
})
|
||||
|
||||
});
|
||||
|
|
|
@ -7,6 +7,10 @@ import {isBlank} from '@ember/utils';
|
|||
export default EmberPowerSelectMultipleTrigger.extend({
|
||||
|
||||
actions: {
|
||||
chooseOption(option) {
|
||||
this.get('select').actions.choose(option);
|
||||
},
|
||||
|
||||
handleOptionMouseDown(event) {
|
||||
let action = this.get('extra.optionMouseDown');
|
||||
if (action) {
|
||||
|
|
|
@ -77,7 +77,6 @@ export default Model.extend(Comparable, ValidationEngine, {
|
|||
|
||||
validationType: 'post',
|
||||
|
||||
authorId: attr('string'),
|
||||
createdAtUTC: attr('moment-utc'),
|
||||
customExcerpt: attr('string'),
|
||||
featured: attr('boolean', {defaultValue: false}),
|
||||
|
@ -107,7 +106,10 @@ export default Model.extend(Comparable, ValidationEngine, {
|
|||
url: attr('string'),
|
||||
uuid: attr('string'),
|
||||
|
||||
author: belongsTo('user', {async: true}),
|
||||
authors: hasMany('user', {
|
||||
embedded: 'always',
|
||||
async: false
|
||||
}),
|
||||
createdBy: belongsTo('user', {async: true}),
|
||||
publishedBy: belongsTo('user', {async: true}),
|
||||
tags: hasMany('tag', {
|
||||
|
@ -115,6 +117,10 @@ export default Model.extend(Comparable, ValidationEngine, {
|
|||
async: false
|
||||
}),
|
||||
|
||||
primaryAuthor: computed('authors.[]', function () {
|
||||
return this.get('authors.firstObject');
|
||||
}),
|
||||
|
||||
init() {
|
||||
// HACK: we can't use the defaultValue property on the mobiledoc attr
|
||||
// because it won't have access to `this` for the feature check so we do
|
||||
|
@ -282,7 +288,7 @@ export default Model.extend(Comparable, ValidationEngine, {
|
|||
},
|
||||
|
||||
isAuthoredByUser(user) {
|
||||
return user.get('id') === this.get('authorId');
|
||||
return this.get('authors').includes(user);
|
||||
},
|
||||
|
||||
// a custom sort function is needed in order to sort the posts list the same way the server would:
|
||||
|
|
|
@ -3,7 +3,7 @@ import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
|||
export default AuthenticatedRoute.extend({
|
||||
model() {
|
||||
return this.get('session.user').then(user => (
|
||||
this.store.createRecord('post', {author: user})
|
||||
this.store.createRecord('post', {authors: [user]})
|
||||
));
|
||||
},
|
||||
|
||||
|
|
|
@ -6,23 +6,13 @@ import {pluralize} from 'ember-inflector';
|
|||
export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
|
||||
// settings for the EmbeddedRecordsMixin.
|
||||
attrs: {
|
||||
authors: {embedded: 'always'},
|
||||
tags: {embedded: 'always'},
|
||||
publishedAtUTC: {key: 'published_at'},
|
||||
createdAtUTC: {key: 'created_at'},
|
||||
updatedAtUTC: {key: 'updated_at'}
|
||||
},
|
||||
|
||||
normalize(model, hash, prop) {
|
||||
// this is to enable us to still access the raw authorId
|
||||
// without requiring an extra get request (since it is an
|
||||
// async relationship).
|
||||
if ((prop === 'post' || prop === 'posts') && hash.author !== undefined) {
|
||||
hash.author_id = hash.author;
|
||||
}
|
||||
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
normalizeSingleResponse(store, primaryModelClass, payload) {
|
||||
let root = this.keyForAttribute(primaryModelClass.modelName);
|
||||
let pluralizedRoot = pluralize(primaryModelClass.modelName);
|
||||
|
@ -62,6 +52,8 @@ export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
|
|||
delete data.author_id;
|
||||
// Read-only virtual property.
|
||||
delete data.url;
|
||||
// Deprecated property (replaced with data.authors)
|
||||
delete data.author;
|
||||
|
||||
hash[root] = [data];
|
||||
}
|
||||
|
|
|
@ -165,11 +165,26 @@
|
|||
|
||||
.ember-power-select-multiple-option {
|
||||
margin: 2px;
|
||||
padding: 2px 4px;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid #3fb0ef;
|
||||
border-radius: 3px;
|
||||
border: 0;
|
||||
background: #3eb0ef;
|
||||
font-size: 0.93em;
|
||||
color: white;
|
||||
background: #3fb0ef;
|
||||
}
|
||||
|
||||
.ember-power-select-multiple-remove-btn:not(:hover) {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.ember-power-select-multiple-remove-btn svg {
|
||||
width: 0.6em;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.ember-power-select-multiple-remove-btn svg path {
|
||||
stroke: white;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.ember-power-select-trigger-multiple-input {
|
||||
|
@ -187,6 +202,13 @@
|
|||
}
|
||||
|
||||
/* Tag input */
|
||||
.tag-token--primary {
|
||||
background: color(#3eb0ef lightness(-20%));
|
||||
|
||||
.tag-token--internal {
|
||||
background: #ebf7fd;
|
||||
color: #3fb0ef;
|
||||
}
|
||||
|
||||
.tag-token--internal svg path {
|
||||
stroke: #3fb0ef;
|
||||
fill: #3fb0ef;
|
||||
}
|
||||
|
|
|
@ -148,6 +148,7 @@ select {
|
|||
|
||||
.gh-input.error,
|
||||
.error .gh-input,
|
||||
.error .ember-power-select-multiple-trigger,
|
||||
.gh-select.error,
|
||||
select.error {
|
||||
border-color: var(--red);
|
||||
|
|
|
@ -88,24 +88,11 @@
|
|||
{{/gh-form-group}}
|
||||
|
||||
{{#unless session.user.isAuthorOrContributor}}
|
||||
<div class="form-group for-select">
|
||||
<label for="author-list">Author</label>
|
||||
<span class="gh-input-icon gh-icon-user">
|
||||
{{svg-jar "user-circle"}}
|
||||
<span class="gh-select" tabindex="0">
|
||||
{{one-way-select
|
||||
selectedAuthor
|
||||
id="author-list"
|
||||
name="post-setting-author"
|
||||
options=authors
|
||||
optionValuePath="id"
|
||||
optionLabelPath="name"
|
||||
update=(action "changeAuthor")
|
||||
}}
|
||||
{{svg-jar "arrow-down-small"}}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{{#gh-form-group class="for-select" errors=post.errors hasValidated=post.hasValidated property="authors" data-test-input="authors"}}
|
||||
<label for="author-list">Authors</label>
|
||||
{{gh-psm-authors-input selectedAuthors=post.authors updateAuthors=(action "changeAuthors") triggerId="author-list"}}
|
||||
{{gh-error-message errors=post.errors property="authors" data-test-error="authors"}}
|
||||
{{/gh-form-group}}
|
||||
{{/unless}}
|
||||
|
||||
<ul class="nav-list nav-list-block">
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<span class="gh-content-status-published">Published</span>
|
||||
{{/if}}
|
||||
|
||||
by <span class="gh-content-entry-author">{{authorName}}</span> —
|
||||
by <span class="gh-content-entry-author">{{authorNames}}</span> —
|
||||
|
||||
{{#if isPublished}}
|
||||
{{gh-format-post-time post.publishedAtUTC published=true}}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{{gh-token-input
|
||||
options=availableAuthors
|
||||
selected=selectedAuthors
|
||||
onchange=(action "updateAuthors")
|
||||
allowCreation=false
|
||||
renderInPlace=true
|
||||
triggerId=triggerId
|
||||
}}
|
|
@ -7,11 +7,11 @@
|
|||
sortEndAction=(action "reorderItems")
|
||||
}}
|
||||
{{#each select.selected as |opt idx|}}
|
||||
{{#component (or extra.tokenComponent draggable-object)
|
||||
{{#component (or extra.tokenComponent "draggable-object")
|
||||
tagName="li"
|
||||
class="ember-power-select-multiple-option"
|
||||
select=select
|
||||
option=(readonly opt)
|
||||
content=(readonly opt)
|
||||
idx=idx
|
||||
isSortable=true
|
||||
mouseDown=(action "handleOptionMouseDown")
|
||||
|
@ -26,8 +26,10 @@
|
|||
<span role="button"
|
||||
aria-label="remove element"
|
||||
class="ember-power-select-multiple-remove-btn"
|
||||
data-selected-index={{idx}}>
|
||||
×
|
||||
data-selected-index={{idx}}
|
||||
onmousedown={{action "chooseOption" opt}}
|
||||
>
|
||||
{{svg-jar "close" data-selected-index=idx}}
|
||||
</span>
|
||||
{{/unless}}
|
||||
{{/component}}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {isBlank, isEmpty, isPresent} from '@ember/utils';
|
|||
export default BaseValidator.create({
|
||||
properties: [
|
||||
'title',
|
||||
'authors',
|
||||
'customExcerpt',
|
||||
'codeinjectionHead',
|
||||
'codeinjectionFoot',
|
||||
|
@ -33,6 +34,15 @@ export default BaseValidator.create({
|
|||
}
|
||||
},
|
||||
|
||||
authors(model) {
|
||||
let authors = model.get('authors');
|
||||
|
||||
if (isEmpty(authors)) {
|
||||
model.get('errors').add('authors', 'At least one author is required.');
|
||||
this.invalidate();
|
||||
}
|
||||
},
|
||||
|
||||
customExcerpt(model) {
|
||||
let customExcerpt = model.get('customExcerpt');
|
||||
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
import {Response} from 'ember-cli-mirage';
|
||||
import {dasherize} from '@ember/string';
|
||||
import {isBlank} from '@ember/utils';
|
||||
import {paginateModelArray} from '../utils';
|
||||
import {paginateModelCollection} from '../utils';
|
||||
|
||||
export default function mockPosts(server) {
|
||||
server.post('/posts', function ({posts}) {
|
||||
server.post('/posts', function ({posts, users}) {
|
||||
let attrs = this.normalizedRequestAttrs();
|
||||
let authors = [];
|
||||
|
||||
// mirage expects `author` to be a reference but we only have an ID
|
||||
attrs.authorId = attrs.author;
|
||||
delete attrs.author;
|
||||
// NOTE: this is necessary so that ember-cli-mirage has a valid user
|
||||
// schema object rather than a plain object
|
||||
// TODO: should ember-cli-mirage be handling this automatically?
|
||||
attrs.authors.forEach((author) => {
|
||||
authors.push(users.find(author.id));
|
||||
});
|
||||
|
||||
attrs.authors = authors;
|
||||
|
||||
if (isBlank(attrs.slug) && !isBlank(attrs.title)) {
|
||||
attrs.slug = dasherize(attrs.title);
|
||||
|
@ -24,7 +30,6 @@ export default function mockPosts(server) {
|
|||
let limit = +queryParams.limit || 15;
|
||||
let {status, staticPages} = queryParams;
|
||||
let query = {};
|
||||
let models;
|
||||
|
||||
if (status && status !== 'all') {
|
||||
query.status = status;
|
||||
|
@ -38,9 +43,9 @@ export default function mockPosts(server) {
|
|||
query.page = true;
|
||||
}
|
||||
|
||||
models = posts.where(query).models;
|
||||
let collection = posts.where(query);
|
||||
|
||||
return paginateModelArray('posts', models, page, limit);
|
||||
return paginateModelCollection('posts', collection, page, limit);
|
||||
});
|
||||
|
||||
server.get('/posts/:id/', function ({posts}, {params}) {
|
||||
|
@ -55,17 +60,21 @@ export default function mockPosts(server) {
|
|||
});
|
||||
});
|
||||
|
||||
// Handle embedded author in post
|
||||
server.put('/posts/:id/', function ({posts}, request) {
|
||||
let post = this.normalizedRequestAttrs();
|
||||
let {author} = post;
|
||||
delete post.author;
|
||||
server.put('/posts/:id/', function ({posts, users}, {params}) {
|
||||
let attrs = this.normalizedRequestAttrs();
|
||||
let post = posts.find(params.id);
|
||||
let authors = [];
|
||||
|
||||
let savedPost = posts.find(request.params.id).update(post);
|
||||
savedPost.authorId = author;
|
||||
savedPost.save();
|
||||
// NOTE: this is necessary so that ember-cli-mirage has a valid user
|
||||
// schema object rather than a plain object
|
||||
// TODO: should ember-cli-mirage be handling this automatically?
|
||||
attrs.authors.forEach((author) => {
|
||||
authors.push(users.find(author.id));
|
||||
});
|
||||
|
||||
return savedPost;
|
||||
attrs.authors = authors;
|
||||
|
||||
return post.update(attrs);
|
||||
});
|
||||
|
||||
server.del('/posts/:id/');
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {Response} from 'ember-cli-mirage';
|
||||
import {paginateModelArray} from '../utils';
|
||||
import {paginateModelCollection} from '../utils';
|
||||
|
||||
export default function mockUsers(server) {
|
||||
// /users/me = Always return the user with ID=1
|
||||
|
@ -20,7 +20,7 @@ export default function mockUsers(server) {
|
|||
|
||||
// NOTE: this is naive and only set up to work with queries that are
|
||||
// actually used - if you use a different filter in the app, add it here!
|
||||
let {models} = users.where(function (user) {
|
||||
let collection = users.where(function (user) {
|
||||
let statusMatch = true;
|
||||
|
||||
if (queryParams.filter === 'status:-inactive') {
|
||||
|
@ -34,7 +34,7 @@ export default function mockUsers(server) {
|
|||
return statusMatch;
|
||||
});
|
||||
|
||||
return paginateModelArray('users', models, page, queryParams.limit);
|
||||
return paginateModelCollection('users', collection, page, queryParams.limit);
|
||||
});
|
||||
|
||||
server.get('/users/slug/:slug/', function ({users}, {params, queryParams}) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {Factory, faker} from 'ember-cli-mirage';
|
||||
import {isEmpty} from '@ember/utils';
|
||||
|
||||
export default Factory.extend({
|
||||
codeinjectionFoot: null,
|
||||
|
@ -21,14 +22,29 @@ export default Factory.extend({
|
|||
plaintext(i) { return `Plaintext for post ${i}.`; },
|
||||
publishedAt: '2015-12-19T16:25:07.000Z',
|
||||
publishedBy: 1,
|
||||
slug(i) { return `post-${i}`; },
|
||||
status(i) { return faker.list.cycle('draft', 'published', 'scheduled')(i); },
|
||||
tags() { return []; },
|
||||
title(i) { return `Post ${i}`; },
|
||||
twitterDescription: null,
|
||||
twitterImage: null,
|
||||
twitterTitle: null,
|
||||
updatedAt: '2015-10-19T16:25:07.756Z',
|
||||
updatedBy: 1,
|
||||
uuid(i) { return `post-${i}`; }
|
||||
uuid(i) { return `post-${i}`; },
|
||||
|
||||
authors() { return []; },
|
||||
tags() { return []; },
|
||||
|
||||
afterCreate(post, server) {
|
||||
if (isEmpty(post.authors)) {
|
||||
let user = server.schema.users.find(1);
|
||||
|
||||
if (!user) {
|
||||
let role = server.schema.roles.find({name: 'Administrator'}) || server.create('role', {name: 'Administrator'});
|
||||
user = server.create('user', {roles: [role]});
|
||||
}
|
||||
|
||||
post.authors = [user];
|
||||
post.save();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -19,6 +19,7 @@ export default Factory.extend({
|
|||
updatedAt: '2015-11-02T16:12:05.000Z',
|
||||
updatedBy: '1',
|
||||
website: 'http://example.com',
|
||||
|
||||
posts() { return []; },
|
||||
roles() { return []; }
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {Model, belongsTo, hasMany} from 'ember-cli-mirage';
|
||||
import {Model, hasMany} from 'ember-cli-mirage';
|
||||
|
||||
export default Model.extend({
|
||||
author: belongsTo('user'),
|
||||
tags: hasMany()
|
||||
tags: hasMany(),
|
||||
authors: hasMany('user')
|
||||
});
|
||||
|
|
|
@ -6,5 +6,5 @@ export default Model.extend({
|
|||
postCount: false,
|
||||
|
||||
roles: hasMany(),
|
||||
posts: hasMany('post', {inverse: 'author'})
|
||||
posts: hasMany()
|
||||
});
|
||||
|
|
|
@ -1,28 +1,19 @@
|
|||
import BaseSerializer from './application';
|
||||
import {RestSerializer} from 'ember-cli-mirage';
|
||||
|
||||
export default BaseSerializer.extend({
|
||||
embed: true,
|
||||
|
||||
include(request) {
|
||||
let includes = [];
|
||||
|
||||
if (request.queryParams.include && request.queryParams.include.indexOf('tags') >= 0) {
|
||||
return ['tags'];
|
||||
includes.push('tags');
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
|
||||
serialize(object, request) {
|
||||
if (this.isCollection(object)) {
|
||||
return BaseSerializer.prototype.serialize.apply(this, arguments);
|
||||
if (request.queryParams.include && request.queryParams.include.indexOf('authors') >= 0) {
|
||||
includes.push('authors');
|
||||
}
|
||||
|
||||
let {post} = RestSerializer.prototype.serialize.call(this, object, request);
|
||||
|
||||
if (object.author) {
|
||||
post.author = object.author.id;
|
||||
}
|
||||
|
||||
return {posts: [post]};
|
||||
return includes;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ export default BaseSerializer.extend({
|
|||
|
||||
serialize(object, request) {
|
||||
if (this.isCollection(object)) {
|
||||
return BaseSerializer.prototype.serialize.apply(this, arguments);
|
||||
return BaseSerializer.prototype.serialize.call(this, object, request);
|
||||
}
|
||||
|
||||
let {user} = RestSerializer.prototype.serialize.call(this, object, request);
|
||||
|
|
|
@ -5,13 +5,13 @@ export function paginatedResponse(modelName) {
|
|||
return function (schema, request) {
|
||||
let page = +request.queryParams.page || 1;
|
||||
let limit = +request.queryParams.limit || 15;
|
||||
let allModels = this.serialize(schema[modelName].all())[modelName];
|
||||
let collection = schema[modelName].all();
|
||||
|
||||
return paginateModelArray(modelName, allModels, page, limit);
|
||||
return paginateModelCollection(modelName, collection, page, limit);
|
||||
};
|
||||
}
|
||||
|
||||
export function paginateModelArray(modelName, allModels, page, limit) {
|
||||
export function paginateModelCollection(modelName, collection, page, limit) {
|
||||
let pages, next, prev, models;
|
||||
|
||||
if (limit === 'all') {
|
||||
|
@ -22,31 +22,34 @@ export function paginateModelArray(modelName, allModels, page, limit) {
|
|||
let start = (page - 1) * limit;
|
||||
let end = start + limit;
|
||||
|
||||
pages = Math.ceil(allModels.length / limit);
|
||||
models = allModels.slice(start, end);
|
||||
pages = Math.ceil(collection.models.length / limit);
|
||||
models = collection.models.slice(start, end);
|
||||
|
||||
if (start > 0) {
|
||||
prev = page - 1;
|
||||
}
|
||||
|
||||
if (end < allModels.length) {
|
||||
if (end < collection.models.length) {
|
||||
next = page + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
meta: {
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
pages,
|
||||
total: allModels.length,
|
||||
next: next || null,
|
||||
prev: prev || null
|
||||
}
|
||||
},
|
||||
[modelName]: models || allModels
|
||||
collection.meta = {
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
pages,
|
||||
total: collection.models.length,
|
||||
next: next || null,
|
||||
prev: prev || null
|
||||
}
|
||||
};
|
||||
|
||||
if (models) {
|
||||
collection.models = models;
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
export function maintenanceResponse() {
|
||||
|
|
|
@ -35,11 +35,11 @@ describe('Acceptance: Content', function () {
|
|||
let editorRole = server.create('role', {name: 'Editor'});
|
||||
editor = server.create('user', {roles: [editorRole]});
|
||||
|
||||
publishedPost = server.create('post', {author: admin, status: 'published', title: 'Published Post'});
|
||||
scheduledPost = server.create('post', {author: admin, status: 'scheduled', title: 'Scheduled Post'});
|
||||
draftPost = server.create('post', {author: admin, status: 'draft', title: 'Draft Post'});
|
||||
publishedPage = server.create('post', {author: admin, status: 'published', page: true, title: 'Published Page'});
|
||||
authorPost = server.create('post', {author: editor, status: 'published', title: 'Editor Published Post'});
|
||||
publishedPost = server.create('post', {authors: [admin], status: 'published', title: 'Published Post'});
|
||||
scheduledPost = server.create('post', {authors: [admin], status: 'scheduled', title: 'Scheduled Post'});
|
||||
draftPost = server.create('post', {authors: [admin], status: 'draft', title: 'Draft Post'});
|
||||
publishedPage = server.create('post', {authors: [admin], status: 'published', page: true, title: 'Published Page'});
|
||||
authorPost = server.create('post', {authors: [editor], status: 'published', title: 'Editor Published Post'});
|
||||
|
||||
return authenticateSession(application);
|
||||
});
|
||||
|
@ -138,8 +138,8 @@ describe('Acceptance: Content', function () {
|
|||
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'});
|
||||
authorPost = server.create('post', {authors: [author], status: 'published', title: 'Author Post'});
|
||||
server.create('post', {authors: [admin], status: 'scheduled', title: 'Admin Post'});
|
||||
|
||||
return authenticateSession(application);
|
||||
});
|
||||
|
@ -169,9 +169,9 @@ describe('Acceptance: Content', function () {
|
|||
let admin = server.create('user', {roles: [adminRole]});
|
||||
|
||||
// Create posts
|
||||
contributorPost = server.create('post', {authorId: contributor.id, status: 'draft', title: 'Contributor Post Draft'});
|
||||
server.create('post', {authorId: contributor.id, status: 'published', title: 'Contributor Published Post'});
|
||||
server.create('post', {authorId: admin.id, status: 'scheduled', title: 'Admin Post'});
|
||||
contributorPost = server.create('post', {authors: [contributor], status: 'draft', title: 'Contributor Post Draft'});
|
||||
server.create('post', {authors: [contributor], status: 'published', title: 'Contributor Published Post'});
|
||||
server.create('post', {authors: [admin], status: 'scheduled', title: 'Admin Post'});
|
||||
|
||||
return authenticateSession(application);
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import startApp from '../helpers/start-app';
|
|||
import {afterEach, beforeEach, describe, it} from 'mocha';
|
||||
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
|
||||
import {expect} from 'chai';
|
||||
// import {selectChoose} from 'ember-power-select/test-support';
|
||||
|
||||
describe('Acceptance: Editor', function () {
|
||||
let application;
|
||||
|
@ -20,7 +21,7 @@ describe('Acceptance: Editor', function () {
|
|||
|
||||
it('redirects to signin when not authenticated', async function () {
|
||||
let author = server.create('user'); // necesary for post-author association
|
||||
server.create('post', {author});
|
||||
server.create('post', {authors: [author]});
|
||||
|
||||
invalidateSession(application);
|
||||
await visit('/editor/1');
|
||||
|
@ -31,7 +32,7 @@ describe('Acceptance: Editor', function () {
|
|||
it('does not redirect to team page when authenticated as contributor', async function () {
|
||||
let role = server.create('role', {name: 'Contributor'});
|
||||
let author = server.create('user', {roles: [role], slug: 'test-user'});
|
||||
server.create('post', {author});
|
||||
server.create('post', {authors: [author]});
|
||||
|
||||
authenticateSession(application);
|
||||
await visit('/editor/1');
|
||||
|
@ -42,7 +43,7 @@ describe('Acceptance: Editor', function () {
|
|||
it('does not redirect to team page when authenticated as author', async function () {
|
||||
let role = server.create('role', {name: 'Author'});
|
||||
let author = server.create('user', {roles: [role], slug: 'test-user'});
|
||||
server.create('post', {author});
|
||||
server.create('post', {authors: [author]});
|
||||
|
||||
authenticateSession(application);
|
||||
await visit('/editor/1');
|
||||
|
@ -53,7 +54,7 @@ describe('Acceptance: Editor', function () {
|
|||
it('does not redirect to team page when authenticated as editor', async function () {
|
||||
let role = server.create('role', {name: 'Editor'});
|
||||
let author = server.create('user', {roles: [role], slug: 'test-user'});
|
||||
server.create('post', {author});
|
||||
server.create('post', {authors: [author]});
|
||||
|
||||
authenticateSession(application);
|
||||
await visit('/editor/1');
|
||||
|
@ -75,7 +76,7 @@ describe('Acceptance: Editor', function () {
|
|||
it('when logged in as a contributor, renders a save button instead of a publish menu & hides tags input', async function () {
|
||||
let role = server.create('role', {name: 'Contributor'});
|
||||
let author = server.create('user', {roles: [role]});
|
||||
server.createList('post', 2, {author});
|
||||
server.createList('post', 2, {authors: [author]});
|
||||
server.loadFixtures('settings');
|
||||
authenticateSession(application);
|
||||
|
||||
|
@ -117,7 +118,7 @@ describe('Acceptance: Editor', function () {
|
|||
});
|
||||
|
||||
it('renders the editor correctly, PSM Publish Date and Save Button', async function () {
|
||||
let [post1] = server.createList('post', 2, {author});
|
||||
let [post1] = server.createList('post', 2, {authors: [author]});
|
||||
let futureTime = moment().tz('Etc/UTC').add(10, 'minutes');
|
||||
|
||||
// post id 1 is a draft, checking for draft behaviour now
|
||||
|
@ -433,7 +434,7 @@ describe('Acceptance: Editor', function () {
|
|||
});
|
||||
});
|
||||
|
||||
let post = server.create('post', 1, {author});
|
||||
let post = server.create('post', 1, {authors: [author]});
|
||||
let plusTenMin = moment().utc().add(10, 'minutes');
|
||||
|
||||
await visit(`/editor/${post.id}`);
|
||||
|
@ -457,7 +458,7 @@ describe('Acceptance: Editor', function () {
|
|||
});
|
||||
|
||||
it('handles title validation errors correctly', async function () {
|
||||
server.create('post', {author});
|
||||
server.create('post', {authors: [author]});
|
||||
|
||||
// post id 1 is a draft, checking for draft behaviour now
|
||||
await visit('/editor/1');
|
||||
|
@ -524,7 +525,7 @@ describe('Acceptance: Editor', function () {
|
|||
let compareDate = moment().tz('Etc/UTC').add(4, 'minutes');
|
||||
let compareDateString = compareDate.format('MM/DD/YYYY');
|
||||
let compareTimeString = compareDate.format('HH:mm');
|
||||
server.create('post', {publishedAt: moment.utc().add(4, 'minutes'), status: 'scheduled', author});
|
||||
server.create('post', {publishedAt: moment.utc().add(4, 'minutes'), status: 'scheduled', authors: [author]});
|
||||
server.create('setting', {activeTimezone: 'Europe/Dublin'});
|
||||
clock.restore();
|
||||
|
||||
|
@ -544,10 +545,12 @@ describe('Acceptance: Editor', function () {
|
|||
.to.contain('Post will be published in');
|
||||
});
|
||||
|
||||
it('shows author list and allows switching of author in PSM', async function () {
|
||||
let role = server.create('role', {name: 'Author'});
|
||||
let otherAuthor = server.create('user', {name: 'Waldo', roles: [role]});
|
||||
server.create('post', {author});
|
||||
it('shows author token input and allows changing of authors in PSM', async function () {
|
||||
let adminRole = server.create('role', {name: 'Adminstrator'});
|
||||
let authorRole = server.create('role', {name: 'Author'});
|
||||
let user1 = server.create('user', {name: 'Primary', roles: [adminRole]});
|
||||
server.create('user', {name: 'Waldo', roles: [authorRole]});
|
||||
server.create('post', {authors: [user1]});
|
||||
|
||||
await visit('/editor/1');
|
||||
|
||||
|
@ -556,13 +559,18 @@ describe('Acceptance: Editor', function () {
|
|||
|
||||
await click('button.post-settings');
|
||||
|
||||
expect(find('select[name="post-setting-author"]').val()).to.equal('1');
|
||||
expect(find('select[name="post-setting-author"] option[value="2"]')).to.be.ok;
|
||||
let tokens = find('[data-test-input="authors"] .ember-power-select-multiple-option');
|
||||
|
||||
await fillIn('select[name="post-setting-author"]', '2');
|
||||
expect(tokens.length).to.equal(1);
|
||||
expect(tokens[0].textContent.trim()).to.have.string('Primary');
|
||||
|
||||
expect(find('select[name="post-setting-author"]').val()).to.equal('2');
|
||||
expect(server.db.posts[0].authorId).to.equal(otherAuthor.id);
|
||||
await selectChoose('[data-test-input="authors"]', 'Waldo');
|
||||
|
||||
let savedAuthors = server.schema.posts.find('1').authors.models;
|
||||
|
||||
expect(savedAuthors.length).to.equal(2);
|
||||
expect(savedAuthors[0].name).to.equal('Primary');
|
||||
expect(savedAuthors[1].name).to.equal('Waldo');
|
||||
});
|
||||
|
||||
it('autosaves when title loses focus', async function () {
|
||||
|
@ -594,7 +602,7 @@ describe('Acceptance: Editor', function () {
|
|||
});
|
||||
|
||||
it('saves post settings fields', async function () {
|
||||
let post = server.create('post', {author});
|
||||
let post = server.create('post', {authors: [author]});
|
||||
|
||||
await visit(`/editor/${post.id}`);
|
||||
|
||||
|
@ -810,7 +818,7 @@ describe('Acceptance: Editor', function () {
|
|||
});
|
||||
|
||||
it('has unsplash icon when server doesn\'t return unsplash settings key', async function () {
|
||||
server.createList('post', 1, {author});
|
||||
server.createList('post', 1, {authors: [author]});
|
||||
|
||||
await visit('/editor/1');
|
||||
|
||||
|
|
|
@ -409,7 +409,12 @@ describe('Acceptance: Team', function () {
|
|||
it('can delete users', async function () {
|
||||
let user1 = server.create('user');
|
||||
let user2 = server.create('user');
|
||||
server.create('post', {author: user2});
|
||||
let post = server.create('post', {authors: [user2]});
|
||||
|
||||
// we don't have a full many-to-many relationship in mirage so we
|
||||
// need to add the inverse manually
|
||||
user2.posts = [post];
|
||||
user2.save();
|
||||
|
||||
await visit('/team');
|
||||
await click(`[data-test-user-id="${user1.id}"]`);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import EmberObject from '@ember/object';
|
||||
import {describe, it} from 'mocha';
|
||||
import {run} from '@ember/runloop';
|
||||
import {setupModelTest} from 'ember-mocha';
|
||||
|
@ -54,17 +53,19 @@ describe('Unit: Model: post', function () {
|
|||
});
|
||||
|
||||
it('isAuthoredByUser is correct', function () {
|
||||
let model = this.subject({
|
||||
authorId: 'abcd1234'
|
||||
});
|
||||
let user = EmberObject.create({id: 'abcd1234'});
|
||||
let user1 = this.store().createRecord('user', {id: 'abcd1234'});
|
||||
let user2 = this.store().createRecord('user', {id: 'wxyz9876'});
|
||||
|
||||
expect(model.isAuthoredByUser(user)).to.be.ok;
|
||||
let model = this.subject({
|
||||
authors: [user1]
|
||||
});
|
||||
|
||||
expect(model.isAuthoredByUser(user1)).to.be.ok;
|
||||
|
||||
run(function () {
|
||||
model.set('authorId', 'wxyz9876');
|
||||
model.set('authors', [user2]);
|
||||
|
||||
expect(model.isAuthoredByUser(user)).to.not.be.ok;
|
||||
expect(model.isAuthoredByUser(user1)).to.not.be.ok;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue