diff --git a/ghost/admin/app/components/gh-context-menu.js b/ghost/admin/app/components/gh-context-menu.js
index 8b3de5a54b..205e488bb1 100644
--- a/ghost/admin/app/components/gh-context-menu.js
+++ b/ghost/admin/app/components/gh-context-menu.js
@@ -1,5 +1,5 @@
import Component from '@glimmer/component';
-import SelectionList from '../utils/selection-list';
+import SelectionList from './posts-list/selection-list';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
diff --git a/ghost/admin/app/components/posts-list/context-menu.js b/ghost/admin/app/components/posts-list/context-menu.js
index f945cfda40..0e9b248661 100644
--- a/ghost/admin/app/components/posts-list/context-menu.js
+++ b/ghost/admin/app/components/posts-list/context-menu.js
@@ -216,11 +216,14 @@ export default class PostsContextMenu extends Component {
yield this.performBulkDestroy();
this.notifications.showNotification(this.#getToastMessage('deleted'), {type: 'success'});
- const remainingModels = this.selectionList.infinityModel.content.filter((model) => {
- return !deletedModels.includes(model);
- });
- // Deleteobjects method from infintiymodel is broken for all models except the first page, so we cannot use this
- this.infinity.replace(this.selectionList.infinityModel, remainingModels);
+ for (const key in this.selectionList.infinityModel) {
+ const remainingModels = this.selectionList.infinityModel[key].content.filter((model) => {
+ return !deletedModels.includes(model);
+ });
+ // Deleteobjects method from infintiymodel is broken for all models except the first page, so we cannot use this
+ this.infinity.replace(this.selectionList.infinityModel[key], remainingModels);
+ }
+
this.selectionList.clearSelection({force: true});
return true;
}
@@ -247,9 +250,7 @@ export default class PostsContextMenu extends Component {
}
}
- // Remove posts that no longer match the filter
this.updateFilteredPosts();
-
return true;
}
@@ -282,14 +283,17 @@ export default class PostsContextMenu extends Component {
]
});
- const remainingModels = this.selectionList.infinityModel.content.filter((model) => {
- if (!updatedModels.find(u => u.id === model.id)) {
- return true;
- }
- return filterNql.queryJSON(model.serialize({includeId: true}));
- });
- // Deleteobjects method from infintiymodel is broken for all models except the first page, so we cannot use this
- this.infinity.replace(this.selectionList.infinityModel, remainingModels);
+ // TODO: something is wrong in here
+ for (const key in this.selectionList.infinityModel) {
+ const remainingModels = this.selectionList.infinityModel[key].content.filter((model) => {
+ if (!updatedModels.find(u => u.id === model.id)) {
+ return true;
+ }
+ return filterNql.queryJSON(model.serialize({includeId: true}));
+ });
+ // Deleteobjects method from infintiymodel is broken for all models except the first page, so we cannot use this
+ this.infinity.replace(this.selectionList.infinityModel[key], remainingModels);
+ }
this.selectionList.clearUnavailableItems();
}
@@ -386,8 +390,10 @@ export default class PostsContextMenu extends Component {
const data = result[this.type === 'post' ? 'posts' : 'pages'][0];
const model = this.store.peekRecord(this.type, data.id);
- // Update infinity list
- this.selectionList.infinityModel.content.unshiftObject(model);
+ // Update infinity draft posts content - copied posts are always drafts
+ if (this.selectionList.infinityModel.draftPosts) {
+ this.selectionList.infinityModel.draftPosts.content.unshiftObject(model);
+ }
// Show notification
this.notifications.showNotification(this.#getToastMessage('duplicated'), {type: 'success'});
diff --git a/ghost/admin/app/components/posts-list/list.hbs b/ghost/admin/app/components/posts-list/list.hbs
index 4755c76d62..b1ab3acd99 100644
--- a/ghost/admin/app/components/posts-list/list.hbs
+++ b/ghost/admin/app/components/posts-list/list.hbs
@@ -1,14 +1,39 @@
- {{#each @model as |post|}}
-
-
-
+ {{!-- always order as scheduled, draft, remainder --}}
+ {{#if (or @model.scheduledPosts (or @model.draftPosts @model.publishedAndSentPosts))}}
+ {{#if @model.scheduledPosts}}
+ {{#each @model.scheduledPosts as |post|}}
+
+
+
+ {{/each}}
+ {{/if}}
+ {{#if (and @model.draftPosts (or (not @model.scheduledPosts) (and @model.scheduledPosts @model.scheduledPosts.reachedInfinity)))}}
+ {{#each @model.draftPosts as |post|}}
+
+
+
+ {{/each}}
+ {{/if}}
+ {{#if (and @model.publishedAndSentPosts (and (or (not @model.scheduledPosts) @model.scheduledPosts.reachedInfinity) (or (not @model.draftPosts) @model.draftPosts.reachedInfinity)))}}
+ {{#each @model.publishedAndSentPosts as |post|}}
+
+
+
+ {{/each}}
+ {{/if}}
{{else}}
{{yield}}
- {{/each}}
+ {{/if}}
{{!-- The currently selected item or items are passed to the context menu --}}
diff --git a/ghost/admin/app/utils/selection-list.js b/ghost/admin/app/components/posts-list/selection-list.js
similarity index 67%
rename from ghost/admin/app/utils/selection-list.js
rename to ghost/admin/app/components/posts-list/selection-list.js
index b409d6da4b..c389cd9545 100644
--- a/ghost/admin/app/utils/selection-list.js
+++ b/ghost/admin/app/components/posts-list/selection-list.js
@@ -18,7 +18,11 @@ export default class SelectionList {
#clearOnNextUnfreeze = false;
constructor(infinityModel) {
- this.infinityModel = infinityModel ?? {content: []};
+ this.infinityModel = infinityModel ?? {
+ draftPosts: {
+ content: []
+ }
+ };
}
freeze() {
@@ -41,7 +45,12 @@ export default class SelectionList {
* Returns an NQL filter for all items, not the selection
*/
get allFilter() {
- return this.infinityModel.extraParams?.filter ?? '';
+ const models = this.infinityModel;
+ // grab filter from the first key in the infinityModel object (they should all be identical)
+ for (const key in models) {
+ return models[key].extraParams?.allFilter ?? '';
+ }
+ return '';
}
/**
@@ -81,10 +90,13 @@ export default class SelectionList {
* Keep in mind that when using CMD + A, we don't have all items in memory!
*/
get availableModels() {
+ const models = this.infinityModel;
const arr = [];
- for (const item of this.infinityModel.content) {
- if (this.isSelected(item.id)) {
- arr.push(item);
+ for (const key in models) {
+ for (const item of models[key].content) {
+ if (this.isSelected(item.id)) {
+ arr.push(item);
+ }
}
}
return arr;
@@ -102,7 +114,13 @@ export default class SelectionList {
if (!this.inverted) {
return this.selectedIds.size;
}
- return Math.max((this.infinityModel.meta?.pagination?.total ?? 0) - this.selectedIds.size, 1);
+
+ const models = this.infinityModel;
+ let total;
+ for (const key in models) {
+ total += models[key].meta?.pagination?.total;
+ }
+ return Math.max((total ?? 0) - this.selectedIds.size, 1);
}
isSelected(id) {
@@ -147,9 +165,12 @@ export default class SelectionList {
clearUnavailableItems() {
const newSelection = new Set();
- for (const item of this.infinityModel.content) {
- if (this.selectedIds.has(item.id)) {
- newSelection.add(item.id);
+ const models = this.infinityModel;
+ for (const key in models) {
+ for (const item of models[key].content) {
+ if (this.selectedIds.has(item.id)) {
+ newSelection.add(item.id);
+ }
}
}
this.selectedIds = newSelection;
@@ -181,37 +202,40 @@ export default class SelectionList {
// todo
let running = false;
- for (const item of this.infinityModel.content) {
- // Exlusing the last selected item
- if (item.id === this.lastSelectedId || item.id === id) {
- if (!running) {
- running = true;
+ const models = this.infinityModel;
+ for (const key in models) {
+ for (const item of this.models[key].content) {
+ // Exlusing the last selected item
+ if (item.id === this.lastSelectedId || item.id === id) {
+ if (!running) {
+ running = true;
- // Skip last selected on its own
- if (item.id === this.lastSelectedId) {
- continue;
- }
- } else {
- // Still include id
- if (item.id === id) {
- this.lastShiftSelectionGroup.add(item.id);
-
- if (this.inverted) {
- this.selectedIds.delete(item.id);
- } else {
- this.selectedIds.add(item.id);
+ // Skip last selected on its own
+ if (item.id === this.lastSelectedId) {
+ continue;
}
- }
- break;
- }
- }
+ } else {
+ // Still include id
+ if (item.id === id) {
+ this.lastShiftSelectionGroup.add(item.id);
- if (running) {
- this.lastShiftSelectionGroup.add(item.id);
- if (this.inverted) {
- this.selectedIds.delete(item.id);
- } else {
- this.selectedIds.add(item.id);
+ if (this.inverted) {
+ this.selectedIds.delete(item.id);
+ } else {
+ this.selectedIds.add(item.id);
+ }
+ }
+ break;
+ }
+ }
+
+ if (running) {
+ this.lastShiftSelectionGroup.add(item.id);
+ if (this.inverted) {
+ this.selectedIds.delete(item.id);
+ } else {
+ this.selectedIds.add(item.id);
+ }
}
}
}
diff --git a/ghost/admin/app/controllers/posts.js b/ghost/admin/app/controllers/posts.js
index 014cad0f47..1a8dd66e2a 100644
--- a/ghost/admin/app/controllers/posts.js
+++ b/ghost/admin/app/controllers/posts.js
@@ -1,5 +1,5 @@
import Controller from '@ember/controller';
-import SelectionList from 'ghost-admin/utils/selection-list';
+import SelectionList from 'ghost-admin/components/posts-list/selection-list';
import {DEFAULT_QUERY_PARAMS} from 'ghost-admin/helpers/reset-query-params';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
diff --git a/ghost/admin/app/routes/posts.js b/ghost/admin/app/routes/posts.js
index 93e7d5d4ab..1df44770ec 100644
--- a/ghost/admin/app/routes/posts.js
+++ b/ghost/admin/app/routes/posts.js
@@ -1,4 +1,5 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
+import RSVP from 'rsvp';
import {action} from '@ember/object';
import {assign} from '@ember/polyfills';
import {isBlank} from '@ember/utils';
@@ -39,43 +40,53 @@ export default class PostsRoute extends AuthenticatedRoute {
model(params) {
const user = this.session.user;
- let queryParams = {};
let filterParams = {tag: params.tag, visibility: params.visibility};
let paginationParams = {
perPageParam: 'limit',
totalPagesParam: 'meta.pagination.pages'
};
-
+
+ // type filters are actually mapping statuses
assign(filterParams, this._getTypeFilters(params.type));
-
+
if (params.type === 'featured') {
filterParams.featured = true;
}
-
+
+ // authors and contributors can only view their own posts
if (user.isAuthor) {
- // authors can only view their own posts
filterParams.authors = user.slug;
} else if (user.isContributor) {
- // Contributors can only view their own draft posts
filterParams.authors = user.slug;
- // filterParams.status = 'draft';
+ // otherwise we need to filter by author if present
} else if (params.author) {
filterParams.authors = params.author;
}
-
- let filter = this._filterString(filterParams);
- if (!isBlank(filter)) {
- queryParams.filter = filter;
- }
-
- if (!isBlank(params.order)) {
- queryParams.order = params.order;
- }
-
+
let perPage = this.perPage;
- let paginationSettings = assign({perPage, startingPage: 1}, paginationParams, queryParams);
+
+ const filterStatuses = filterParams.status;
+ let queryParams = {allFilter: this._filterString({...filterParams})}; // pass along the parent filter so it's easier to apply the params filter to each infinity model
+ let models = {};
+ if (filterStatuses.includes('scheduled')) {
+ let scheduledPostsParams = {...queryParams, order: params.order || 'published_at desc', filter: this._filterString({...filterParams, status: 'scheduled'})};
+ models.scheduledPosts = this.infinity.model('post', assign({perPage, startingPage: 1}, paginationParams, scheduledPostsParams));
+ }
+ if (filterStatuses.includes('draft')) {
+ let draftPostsParams = {...queryParams, order: params.order || 'updated_at desc', filter: this._filterString({...filterParams, status: 'draft'})};
+ models.draftPosts = this.infinity.model('post', assign({perPage, startingPage: 1}, paginationParams, draftPostsParams));
+ }
+ if (filterStatuses.includes('published') || filterStatuses.includes('sent')) {
+ let publishedAndSentPostsParams;
+ if (filterStatuses.includes('published') && filterStatuses.includes('sent')) {
+ publishedAndSentPostsParams = {...queryParams, order: params.order || 'published_at desc', filter: this._filterString({...filterParams, status: '[published,sent]'})};
+ } else {
+ publishedAndSentPostsParams = {...queryParams, order: params.order || 'published_at desc', filter: this._filterString({...filterParams, status: filterStatuses.includes('published') ? 'published' : 'sent'})};
+ }
+ models.publishedAndSentPosts = this.infinity.model('post', assign({perPage, startingPage: 1}, paginationParams, publishedAndSentPostsParams));
+ }
- return this.infinity.model(this.modelName, paginationSettings);
+ return RSVP.hash(models);
}
// trigger a background load of all tags and authors for use in filter dropdowns
@@ -120,6 +131,12 @@ export default class PostsRoute extends AuthenticatedRoute {
};
}
+ /**
+ * Returns an object containing the status filter based on the given type.
+ *
+ * @param {string} type - The type of filter to generate (draft, published, scheduled, sent).
+ * @returns {Object} - An object containing the status filter.
+ */
_getTypeFilters(type) {
let status = '[draft,scheduled,published,sent]';
diff --git a/ghost/admin/app/templates/posts.hbs b/ghost/admin/app/templates/posts.hbs
index f0d0b6bbe8..4187b57418 100644
--- a/ghost/admin/app/templates/posts.hbs
+++ b/ghost/admin/app/templates/posts.hbs
@@ -30,7 +30,7 @@
@@ -43,7 +43,7 @@
{{else}}
No posts match the current filter
-
+
Show all posts
{{/if}}
@@ -51,11 +51,26 @@
+ {{!-- only show one infinity loader wheel at a time - always order as scheduled, draft, remainder --}}
+ {{#if @model.scheduledPosts}}
-
+ {{/if}}
+ {{#if (and @model.draftPosts (or (not @model.scheduledPosts) (and @model.scheduledPosts @model.scheduledPosts.reachedInfinity)))}}
+
+ {{/if}}
+ {{#if (and @model.publishedAndSentPosts (and (or (not @model.scheduledPosts) @model.scheduledPosts.reachedInfinity) (or (not @model.draftPosts) @model.draftPosts.reachedInfinity)))}}
+
+ {{/if}}
+
{{outlet}}
diff --git a/ghost/admin/mirage/config/posts.js b/ghost/admin/mirage/config/posts.js
index a12863bfe7..2836e0613d 100644
--- a/ghost/admin/mirage/config/posts.js
+++ b/ghost/admin/mirage/config/posts.js
@@ -23,7 +23,6 @@ function extractTags(postAttrs, tags) {
});
}
-// TODO: handle authors filter
export function getPosts({posts}, {queryParams}) {
let {filter, page, limit} = queryParams;
@@ -31,15 +30,27 @@ export function getPosts({posts}, {queryParams}) {
limit = +limit || 15;
let statusFilter = extractFilterParam('status', filter);
+ let authorsFilter = extractFilterParam('authors', filter);
+ let visibilityFilter = extractFilterParam('visibility', filter);
let collection = posts.all().filter((post) => {
let matchesStatus = true;
+ let matchesAuthors = true;
+ let matchesVisibility = true;
if (!isEmpty(statusFilter)) {
matchesStatus = statusFilter.includes(post.status);
}
- return matchesStatus;
+ if (!isEmpty(authorsFilter)) {
+ matchesAuthors = authorsFilter.includes(post.authors.models[0].slug);
+ }
+
+ if (!isEmpty(visibilityFilter)) {
+ matchesVisibility = visibilityFilter.includes(post.visibility);
+ }
+
+ return matchesStatus && matchesAuthors && matchesVisibility;
});
return paginateModelCollection('posts', collection, page, limit);
@@ -59,7 +70,6 @@ export default function mockPosts(server) {
return posts.create(attrs);
});
- // TODO: handle authors filter
server.get('/posts/', getPosts);
server.get('/posts/:id/', function ({posts}, {params}) {
@@ -100,6 +110,13 @@ export default function mockPosts(server) {
posts.find(ids).destroy();
});
+ server.post('/posts/:id/copy/', function ({posts}, {params}) {
+ let post = posts.find(params.id);
+ let attrs = post.attrs;
+
+ return posts.create(attrs);
+ });
+
server.put('/posts/bulk/', function ({tags}, {requestBody}) {
const bulk = JSON.parse(requestBody).bulk;
const action = bulk.action;
@@ -115,7 +132,7 @@ export default function mockPosts(server) {
tags.create(tag);
}
});
- // TODO: update the actual posts in the mock db
+ // TODO: update the actual posts in the mock db if wanting to write tests where we navigate around (refresh model)
// const postsToUpdate = posts.find(ids);
// getting the posts is fine, but within this we CANNOT manipulate them (???) not even iterate with .forEach
}
diff --git a/ghost/admin/tests/acceptance/content-test.js b/ghost/admin/tests/acceptance/content-test.js
index 5fbbf3f743..0fa898abab 100644
--- a/ghost/admin/tests/acceptance/content-test.js
+++ b/ghost/admin/tests/acceptance/content-test.js
@@ -17,11 +17,15 @@ const findButton = (text, buttons) => {
return Array.from(buttons).find(button => button.innerText.trim() === text);
};
+// NOTE: With accommodations for faster loading of posts in the UI, the requests to fetch the posts have been split into separate requests based
+// on the status of the post. This means that the tests for filtering by status will have multiple requests to check against.
describe('Acceptance: Content', function () {
let hooks = setupApplicationTest();
setupMirage(hooks);
beforeEach(async function () {
+ // console.log(`this.server`, this.server);
+ // console.log(`this.server.db`, this.server.db);
this.server.loadFixtures('configs');
});
@@ -32,6 +36,70 @@ describe('Acceptance: Content', function () {
expect(currentURL()).to.equal('/signin');
});
+ describe('as contributor', function () {
+ beforeEach(async function () {
+ let contributorRole = this.server.create('role', {name: 'Contributor'});
+ this.server.create('user', {roles: [contributorRole]});
+
+ return await authenticateSession();
+ });
+
+ // NOTE: This test seems to fail if run AFTER the 'can change access' test in the 'as admin' section; router seems to fail, did not look into it further
+ it('shows posts list and allows post creation', async function () {
+ await visit('/posts');
+
+ // has an empty state
+ expect(findAll('[data-test-post-id]')).to.have.length(0);
+ expect(find('[data-test-no-posts-box]')).to.exist;
+ expect(find('[data-test-link="write-a-new-post"]')).to.exist;
+
+ await click('[data-test-link="write-a-new-post"]');
+
+ expect(currentURL()).to.equal('/editor/post');
+
+ await fillIn('[data-test-editor-title-input]', 'First contributor post');
+ await blur('[data-test-editor-title-input]');
+
+ expect(currentURL()).to.equal('/editor/post/1');
+
+ await click('[data-test-link="posts"]');
+
+ expect(findAll('[data-test-post-id]')).to.have.length(1);
+ expect(find('[data-test-no-posts-box]')).to.not.exist;
+ });
+ });
+
+ describe('as author', function () {
+ let author, authorPost;
+
+ beforeEach(async function () {
+ let authorRole = this.server.create('role', {name: 'Author'});
+ author = this.server.create('user', {roles: [authorRole]});
+ let adminRole = this.server.create('role', {name: 'Administrator'});
+ let admin = this.server.create('user', {roles: [adminRole]});
+
+ // create posts
+ authorPost = this.server.create('post', {authors: [author], status: 'published', title: 'Author Post'});
+ this.server.create('post', {authors: [admin], status: 'scheduled', title: 'Admin Post'});
+
+ return await authenticateSession();
+ });
+
+ it('only fetches the author\'s posts', async function () {
+ await visit('/posts');
+ // trigger a filter request so we can grab the posts API request easily
+ await selectChoose('[data-test-type-select]', 'Published posts');
+
+ // API request includes author filter
+ let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
+ expect(lastRequest.queryParams.filter).to.have.string(`authors:${author.slug}`);
+
+ // only author's post is shown
+ expect(findAll('[data-test-post-id]').length, 'post count').to.equal(1);
+ expect(find(`[data-test-post-id="${authorPost.id}"]`), 'author post').to.exist;
+ });
+ });
+
describe('as admin', function () {
let admin, editor, publishedPost, scheduledPost, draftPost, authorPost;
@@ -41,11 +109,10 @@ describe('Acceptance: Content', function () {
let editorRole = this.server.create('role', {name: 'Editor'});
editor = this.server.create('user', {roles: [editorRole]});
- publishedPost = this.server.create('post', {authors: [admin], status: 'published', title: 'Published Post'});
+ publishedPost = this.server.create('post', {authors: [admin], status: 'published', title: 'Published Post', visibility: 'paid'});
scheduledPost = this.server.create('post', {authors: [admin], status: 'scheduled', title: 'Scheduled Post'});
- // draftPost = this.server.create('post', {authors: [admin], status: 'draft', title: 'Draft Post', visibility: 'paid'});
draftPost = this.server.create('post', {authors: [admin], status: 'draft', title: 'Draft Post'});
- authorPost = this.server.create('post', {authors: [editor], status: 'published', title: 'Editor Published Post', visibiity: 'paid'});
+ 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'});
@@ -61,7 +128,17 @@ describe('Acceptance: Content', function () {
// displays all posts by default (all statuses) [no pages]
expect(posts.length, 'all posts count').to.equal(4);
- // note: atm the mirage backend doesn't support ordering of the results set
+ // make sure display is scheduled > draft > published/sent
+ expect(posts[0].querySelector('.gh-content-entry-title').textContent, 'post 1 title').to.contain('Scheduled Post');
+ expect(posts[1].querySelector('.gh-content-entry-title').textContent, 'post 2 title').to.contain('Draft Post');
+ expect(posts[2].querySelector('.gh-content-entry-title').textContent, 'post 3 title').to.contain('Published Post');
+ expect(posts[3].querySelector('.gh-content-entry-title').textContent, 'post 4 title').to.contain('Editor Published Post');
+
+ // check API requests
+ let lastRequests = this.server.pretender.handledRequests.filter(request => request.url.includes('/posts/'));
+ expect(lastRequests[0].queryParams.filter, 'scheduled request filter').to.have.string('status:scheduled');
+ expect(lastRequests[1].queryParams.filter, 'drafts request filter').to.have.string('status:draft');
+ expect(lastRequests[2].queryParams.filter, 'published request filter').to.have.string('status:[published,sent]');
});
it('can filter by status', async function () {
@@ -97,13 +174,6 @@ describe('Acceptance: Content', function () {
// 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 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,sent]');
});
it('can filter by author', async function () {
@@ -114,20 +184,31 @@ describe('Acceptance: Content', function () {
// API request is correct
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
- expect(lastRequest.queryParams.filter, '"editor" request status filter')
+ expect(lastRequest.queryParams.allFilter, '"editor" request status filter')
.to.have.string('status:[draft,scheduled,published,sent]');
- expect(lastRequest.queryParams.filter, '"editor" request filter param')
+ expect(lastRequest.queryParams.allFilter, '"editor" request filter param')
.to.have.string(`authors:${editor.slug}`);
+
+ // Displays editor post
+ expect(findAll('[data-test-post-id]').length, 'editor count').to.equal(1);
});
it('can filter by visibility', async function () {
await visit('/posts');
await selectChoose('[data-test-visibility-select]', 'Paid members-only');
-
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
- expect(lastRequest.queryParams.filter, '"visibility" request filter param')
- .to.have.string('visibility:[paid,tiers]+status:[draft,scheduled,published,sent]');
+ expect(lastRequest.queryParams.allFilter, '"visibility" request filter param')
+ .to.have.string('visibility:[paid,tiers]');
+ let posts = findAll('[data-test-post-id]');
+ expect(posts.length, 'all posts count').to.equal(1);
+
+ await selectChoose('[data-test-visibility-select]', 'Public');
+ [lastRequest] = this.server.pretender.handledRequests.slice(-1);
+ expect(lastRequest.queryParams.allFilter, '"visibility" request filter param')
+ .to.have.string('visibility:public');
+ posts = findAll('[data-test-post-id]');
+ expect(posts.length, 'all posts count').to.equal(3);
});
it('can filter by tag', async function () {
@@ -150,14 +231,13 @@ describe('Acceptance: Content', function () {
await selectChoose('[data-test-tag-select]', 'B - Second');
// affirm request
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
- expect(lastRequest.queryParams.filter, 'request filter').to.have.string('tag:second');
+ expect(lastRequest.queryParams.allFilter, '"tag" request filter param').to.have.string('tag:second');
});
});
describe('context menu actions', function () {
describe('single post', function () {
- // has a duplicate option
- it.skip('can duplicate a post', async function () {
+ it('can duplicate a post', async function () {
await visit('/posts');
// get the post
@@ -165,13 +245,11 @@ describe('Acceptance: Content', function () {
expect(post, 'post').to.exist;
await triggerEvent(post, 'contextmenu');
- // await this.pauseTest();
let contextMenu = find('.gh-posts-context-menu'); // this is a
element
let buttons = contextMenu.querySelectorAll('button');
- // should have three options for a published post
expect(contextMenu, 'context menu').to.exist;
expect(buttons.length, 'context menu buttons').to.equal(5);
expect(buttons[0].innerText.trim(), 'context menu button 1').to.contain('Unpublish');
@@ -183,19 +261,15 @@ describe('Acceptance: Content', function () {
// duplicate the post
await click(buttons[3]);
- // API request is correct
- // POST /ghost/api/admin/posts/{id}/copy/?formats=mobiledoc,lexical
-
- // TODO: probably missing endpoint in mirage...
-
- // let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
- // console.log(`lastRequest`, lastRequest);
- // expect(lastRequest.url, 'request url').to.match(new RegExp(`/posts/${publishedPost.id}/copy/`));
+ const posts = findAll('[data-test-post-id]');
+ expect(posts.length, 'all posts count').to.equal(5);
+ let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
+ expect(lastRequest.url, 'request url').to.match(new RegExp(`/posts/${publishedPost.id}/copy/`));
});
});
describe('multiple posts', function () {
- it('can feature and unfeature posts', async function () {
+ it('can feature and unfeature', async function () {
await visit('/posts');
// get all posts
@@ -226,7 +300,7 @@ describe('Acceptance: Content', function () {
// API request is correct - note, we don't mock the actual model updates
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
- expect(lastRequest.queryParams.filter, 'feature request id').to.equal(`id:['3','4']`);
+ expect(lastRequest.queryParams.filter, 'feature request id').to.equal(`id:['${publishedPost.id}','${authorPost.id}']`);
expect(JSON.parse(lastRequest.requestBody).bulk.action, 'feature request action').to.equal('feature');
// ensure ui shows these are now featured
@@ -247,7 +321,7 @@ describe('Acceptance: Content', function () {
// API request is correct - note, we don't mock the actual model updates
[lastRequest] = this.server.pretender.handledRequests.slice(-1);
- expect(lastRequest.queryParams.filter, 'unfeature request id').to.equal(`id:['3','4']`);
+ expect(lastRequest.queryParams.filter, 'unfeature request id').to.equal(`id:['${publishedPost.id}','${authorPost.id}']`);
expect(JSON.parse(lastRequest.requestBody).bulk.action, 'unfeature request action').to.equal('unfeature');
// ensure ui shows these are now unfeatured
@@ -255,7 +329,7 @@ describe('Acceptance: Content', function () {
expect(postFourContainer.querySelector('.gh-featured-post'), 'postFour featured').to.not.exist;
});
- it('can add a tag to multiple posts', async function () {
+ it('can add a tag', async function () {
await visit('/posts');
// get all posts
@@ -295,13 +369,12 @@ describe('Acceptance: Content', function () {
// API request is correct - note, we don't mock the actual model updates
let [lastRequest] = this.server.pretender.handledRequests.slice(-2);
- expect(lastRequest.queryParams.filter, 'add tag request id').to.equal(`id:['3','4']`);
+ expect(lastRequest.queryParams.filter, 'add tag request id').to.equal(`id:['${publishedPost.id}','${authorPost.id}']`);
expect(JSON.parse(lastRequest.requestBody).bulk.action, 'add tag request action').to.equal('addTag');
});
- // NOTE: we do not seem to be loading the settings properly into the membersutil service, such that the members
- // service doesn't think members are enabled
- it.skip('can change access to multiple posts', async function () {
+ // TODO: Skip for now. This causes the member creation test to fail ('New member' text doesn't show... ???).
+ it.skip('can change access', async function () {
await visit('/posts');
// get all posts
@@ -317,26 +390,38 @@ describe('Acceptance: Content', function () {
expect(postFourContainer.getAttribute('data-selected'), 'postFour selected').to.exist;
expect(postThreeContainer.getAttribute('data-selected'), 'postThree selected').to.exist;
- // NOTE: right clicks don't seem to work in these tests
- // contextmenu is the event triggered - https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event
await triggerEvent(postFourContainer, 'contextmenu');
-
let contextMenu = find('.gh-posts-context-menu'); // this is a element
expect(contextMenu, 'context menu').to.exist;
-
- // TODO: the change access button is not showing; need to debug the UI to see what field it expects
- // change access to the posts
let buttons = contextMenu.querySelectorAll('button');
let changeAccessButton = findButton('Change access', buttons);
+
+ expect(changeAccessButton, 'change access button').not.to.exist;
+
+ const settingsService = this.owner.lookup('service:settings');
+ await settingsService.set('membersEnabled', true);
+
+ await triggerEvent(postFourContainer, 'contextmenu');
+ contextMenu = find('.gh-posts-context-menu'); // this is a element
+ expect(contextMenu, 'context menu').to.exist;
+ buttons = contextMenu.querySelectorAll('button');
+ changeAccessButton = findButton('Change access', buttons);
expect(changeAccessButton, 'change access button').to.exist;
await click(changeAccessButton);
-
+
const changeAccessModal = find('[data-test-modal="edit-posts-access"]');
- expect(changeAccessModal, 'change access modal').to.exist;
+ const selectElement = changeAccessModal.querySelector('select');
+ await fillIn(selectElement, 'members');
+ await click('[data-test-button="confirm"]');
+
+ // check API request
+ let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
+ expect(lastRequest.queryParams.filter, 'change access request id').to.equal(`id:['${publishedPost.id}','${authorPost.id}']`);
+ expect(JSON.parse(lastRequest.requestBody).bulk.action, 'change access request action').to.equal('access');
});
- it('can unpublish posts', async function () {
+ it('can unpublish', async function () {
await visit('/posts');
// get all posts
@@ -372,7 +457,7 @@ describe('Acceptance: Content', function () {
// API request is correct - note, we don't mock the actual model updates
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
- expect(lastRequest.queryParams.filter, 'unpublish request id').to.equal(`id:['3','4']`);
+ expect(lastRequest.queryParams.filter, 'unpublish request id').to.equal(`id:['${publishedPost.id}','${authorPost.id}']`);
expect(JSON.parse(lastRequest.requestBody).bulk.action, 'unpublish request action').to.equal('unpublish');
// ensure ui shows these are now unpublished
@@ -380,7 +465,7 @@ describe('Acceptance: Content', function () {
expect(postFourContainer.querySelector('.gh-content-entry-status').textContent, 'postThree status').to.contain('Draft');
});
- it('can delete posts', async function () {
+ it('can delete', async function () {
await visit('/posts');
// get all posts
@@ -416,7 +501,7 @@ describe('Acceptance: Content', function () {
// API request is correct - note, we don't mock the actual model updates
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
- expect(lastRequest.queryParams.filter, 'delete request id').to.equal(`id:['3','4']`);
+ expect(lastRequest.queryParams.filter, 'delete request id').to.equal(`id:['${publishedPost.id}','${authorPost.id}']`);
expect(lastRequest.method, 'delete request method').to.equal('DELETE');
// ensure ui shows these are now deleted
@@ -508,67 +593,4 @@ describe('Acceptance: Content', function () {
expect(find('[data-test-screen-title]').innerText).to.match(/Scheduled/);
});
});
-
- describe('as author', function () {
- let author, authorPost;
-
- beforeEach(async function () {
- let authorRole = this.server.create('role', {name: 'Author'});
- author = this.server.create('user', {roles: [authorRole]});
- let adminRole = this.server.create('role', {name: 'Administrator'});
- let admin = this.server.create('user', {roles: [adminRole]});
-
- // create posts
- authorPost = this.server.create('post', {authors: [author], status: 'published', title: 'Author Post'});
- this.server.create('post', {authors: [admin], status: 'scheduled', title: 'Admin Post'});
-
- return await authenticateSession();
- });
-
- it('only fetches the author\'s posts', async function () {
- await visit('/posts');
- // trigger a filter request so we can grab the posts API request easily
- await selectChoose('[data-test-type-select]', 'Published posts');
-
- // API request includes author filter
- let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
- expect(lastRequest.queryParams.filter).to.have.string(`authors:${author.slug}`);
-
- // only author's post is shown
- expect(findAll('[data-test-post-id]').length, 'post count').to.equal(1);
- expect(find(`[data-test-post-id="${authorPost.id}"]`), 'author post').to.exist;
- });
- });
-
- describe('as contributor', function () {
- beforeEach(async function () {
- let contributorRole = this.server.create('role', {name: 'Contributor'});
- this.server.create('user', {roles: [contributorRole]});
-
- return await authenticateSession();
- });
-
- it('shows posts list and allows post creation', async function () {
- await visit('/posts');
-
- // has an empty state
- expect(findAll('[data-test-post-id]')).to.have.length(0);
- expect(find('[data-test-no-posts-box]')).to.exist;
- expect(find('[data-test-link="write-a-new-post"]')).to.exist;
-
- await click('[data-test-link="write-a-new-post"]');
-
- expect(currentURL()).to.equal('/editor/post');
-
- await fillIn('[data-test-editor-title-input]', 'First contributor post');
- await blur('[data-test-editor-title-input]');
-
- expect(currentURL()).to.equal('/editor/post/1');
-
- await click('[data-test-link="posts"]');
-
- expect(findAll('[data-test-post-id]')).to.have.length(1);
- expect(find('[data-test-no-posts-box]')).to.not.exist;
- });
- });
});
\ No newline at end of file