2023-04-13 17:17:36 +03:00
|
|
|
import AddPostTagsModal from './modals/add-tag';
|
2023-04-07 12:48:14 +03:00
|
|
|
import Component from '@glimmer/component';
|
2023-04-11 17:37:42 +03:00
|
|
|
import DeletePostsModal from './modals/delete-posts';
|
2023-04-12 12:58:46 +03:00
|
|
|
import EditPostsAccessModal from './modals/edit-posts-access';
|
2023-04-12 16:28:09 +03:00
|
|
|
import UnpublishPostsModal from './modals/unpublish-posts';
|
|
|
|
import nql from '@tryghost/nql';
|
2023-04-13 15:49:52 +03:00
|
|
|
import tpl from '@tryghost/tpl';
|
2023-04-07 12:48:14 +03:00
|
|
|
import {action} from '@ember/object';
|
|
|
|
import {inject as service} from '@ember/service';
|
2023-04-11 17:37:42 +03:00
|
|
|
import {task} from 'ember-concurrency';
|
2023-04-07 12:48:14 +03:00
|
|
|
|
2023-04-13 15:49:52 +03:00
|
|
|
const messages = {
|
|
|
|
deleted: {
|
|
|
|
single: 'Post deleted successfully',
|
|
|
|
multiple: '{count} posts deleted successfully'
|
|
|
|
},
|
|
|
|
unpublished: {
|
|
|
|
single: 'Post successfully reverted to a draft',
|
|
|
|
multiple: '{count} posts successfully reverted to drafts'
|
|
|
|
},
|
|
|
|
featured: {
|
|
|
|
single: 'Post featured successfully',
|
|
|
|
multiple: '{count} posts featured successfully'
|
|
|
|
},
|
|
|
|
unfeatured: {
|
|
|
|
single: 'Post unfeatured successfully',
|
|
|
|
multiple: '{count} posts unfeatured successfully'
|
|
|
|
},
|
|
|
|
accessUpdated: {
|
|
|
|
single: 'Post access successfully updated',
|
|
|
|
multiple: 'Post access successfully updated for {count} posts'
|
2023-04-13 17:17:36 +03:00
|
|
|
},
|
|
|
|
tagsAdded: {
|
|
|
|
single: 'Tags added successfully',
|
|
|
|
multiple: 'Tags added successfully to {count} posts'
|
2023-04-13 15:49:52 +03:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-04-07 12:48:14 +03:00
|
|
|
export default class PostsContextMenu extends Component {
|
|
|
|
@service ajax;
|
|
|
|
@service ghostPaths;
|
2023-04-11 17:32:11 +03:00
|
|
|
@service session;
|
2023-04-11 17:37:42 +03:00
|
|
|
@service infinity;
|
2023-04-12 12:58:46 +03:00
|
|
|
@service store;
|
2023-04-13 15:49:52 +03:00
|
|
|
@service notifications;
|
2023-04-07 12:48:14 +03:00
|
|
|
|
|
|
|
get menu() {
|
|
|
|
return this.args.menu;
|
|
|
|
}
|
|
|
|
|
|
|
|
get selectionList() {
|
|
|
|
return this.menu.selectionList;
|
|
|
|
}
|
|
|
|
|
2023-04-13 15:49:52 +03:00
|
|
|
#getToastMessage(type) {
|
|
|
|
if (this.selectionList.isSingle) {
|
|
|
|
return tpl(messages[type].single);
|
|
|
|
}
|
|
|
|
return tpl(messages[type].multiple, {count: this.selectionList.count});
|
|
|
|
}
|
|
|
|
|
2023-04-13 16:04:06 +03:00
|
|
|
@action
|
|
|
|
async featurePosts() {
|
|
|
|
this.menu.performTask(this.featurePostsTask);
|
|
|
|
}
|
|
|
|
|
|
|
|
@action
|
|
|
|
async unfeaturePosts() {
|
|
|
|
this.menu.performTask(this.unfeaturePostsTask);
|
|
|
|
}
|
|
|
|
|
2023-04-13 17:17:36 +03:00
|
|
|
@action
|
|
|
|
async addTagToPosts() {
|
|
|
|
await this.menu.openModal(AddPostTagsModal, {
|
|
|
|
selectionList: this.selectionList,
|
|
|
|
confirm: this.addTagToPostsTask
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-04-07 12:48:14 +03:00
|
|
|
@action
|
2023-04-11 17:37:42 +03:00
|
|
|
async deletePosts() {
|
2023-04-13 16:04:06 +03:00
|
|
|
this.menu.openModal(DeletePostsModal, {
|
2023-04-12 16:53:17 +03:00
|
|
|
selectionList: this.selectionList,
|
2023-04-11 17:37:42 +03:00
|
|
|
confirm: this.deletePostsTask
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-04-12 16:28:09 +03:00
|
|
|
@action
|
|
|
|
async unpublishPosts() {
|
2023-04-13 16:04:06 +03:00
|
|
|
await this.menu.openModal(UnpublishPostsModal, {
|
2023-04-12 16:53:17 +03:00
|
|
|
selectionList: this.selectionList,
|
2023-04-12 16:28:09 +03:00
|
|
|
confirm: this.unpublishPostsTask
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-04-12 12:58:46 +03:00
|
|
|
@action
|
|
|
|
async editPostsAccess() {
|
2023-04-13 16:04:06 +03:00
|
|
|
this.menu.openModal(EditPostsAccessModal, {
|
2023-04-12 16:53:17 +03:00
|
|
|
selectionList: this.selectionList,
|
2023-04-12 12:58:46 +03:00
|
|
|
confirm: this.editPostsAccessTask
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-04-13 17:17:36 +03:00
|
|
|
@task
|
|
|
|
*addTagToPostsTask(tags) {
|
|
|
|
const updatedModels = this.selectionList.availableModels;
|
|
|
|
|
|
|
|
yield this.performBulkEdit('addTag', {tags: tags.map(tag => tag.id)});
|
|
|
|
|
|
|
|
this.notifications.showNotification(this.#getToastMessage('tagsAdded'), {type: 'success'});
|
|
|
|
|
|
|
|
// Update the models on the client side
|
|
|
|
for (const post of updatedModels) {
|
|
|
|
const newTags = post.tags.toArray().map((t) => {
|
|
|
|
return {
|
|
|
|
...t.serialize({includeId: true}),
|
|
|
|
type: 'tag'
|
|
|
|
};
|
|
|
|
});
|
|
|
|
for (const tag of tags) {
|
|
|
|
if (!newTags.find(t => t.id === tag.id)) {
|
|
|
|
newTags.push({
|
|
|
|
...tag.serialize({includeId: true}),
|
|
|
|
type: 'tag'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We need to do it this way to prevent marking the model as dirty
|
|
|
|
this.store.push({
|
|
|
|
data: {
|
|
|
|
id: post.id,
|
|
|
|
type: 'post',
|
|
|
|
relationships: {
|
|
|
|
tags: {
|
|
|
|
data: newTags
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove posts that no longer match the filter
|
|
|
|
this.updateFilteredPosts();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-04-11 17:37:42 +03:00
|
|
|
@task
|
2023-04-13 16:04:06 +03:00
|
|
|
*deletePostsTask() {
|
2023-04-11 17:37:42 +03:00
|
|
|
const deletedModels = this.selectionList.availableModels;
|
|
|
|
yield this.performBulkDestroy();
|
2023-04-13 15:49:52 +03:00
|
|
|
this.notifications.showNotification(this.#getToastMessage('deleted'), {type: 'success'});
|
|
|
|
|
2023-04-11 17:37:42 +03:00
|
|
|
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);
|
|
|
|
this.selectionList.clearSelection();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-04-12 16:28:09 +03:00
|
|
|
@task
|
2023-04-13 16:04:06 +03:00
|
|
|
*unpublishPostsTask() {
|
2023-04-12 16:28:09 +03:00
|
|
|
const updatedModels = this.selectionList.availableModels;
|
|
|
|
yield this.performBulkEdit('unpublish');
|
2023-04-13 15:49:52 +03:00
|
|
|
this.notifications.showNotification(this.#getToastMessage('unpublished'), {type: 'success'});
|
2023-04-12 16:28:09 +03:00
|
|
|
|
|
|
|
// Update the models on the client side
|
|
|
|
for (const post of updatedModels) {
|
|
|
|
if (post.status === 'published' || post.status === 'sent') {
|
|
|
|
// We need to do it this way to prevent marking the model as dirty
|
|
|
|
this.store.push({
|
|
|
|
data: {
|
|
|
|
id: post.id,
|
|
|
|
type: 'post',
|
|
|
|
attributes: {
|
|
|
|
status: 'draft'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove posts that no longer match the filter
|
|
|
|
this.updateFilteredPosts();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
updateFilteredPosts() {
|
|
|
|
const updatedModels = this.selectionList.availableModels;
|
|
|
|
const filter = this.selectionList.allFilter;
|
|
|
|
const filterNql = nql(filter);
|
|
|
|
|
|
|
|
const remainingModels = this.selectionList.infinityModel.content.filter((model) => {
|
|
|
|
if (!updatedModels.find(u => u.id === model.id)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return filterNql.queryJSON(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);
|
|
|
|
}
|
|
|
|
|
2023-04-12 12:58:46 +03:00
|
|
|
@task
|
|
|
|
*editPostsAccessTask(close, {visibility, tiers}) {
|
|
|
|
const updatedModels = this.selectionList.availableModels;
|
|
|
|
yield this.performBulkEdit('access', {visibility, tiers});
|
2023-04-13 15:49:52 +03:00
|
|
|
this.notifications.showNotification(this.#getToastMessage('accessUpdated'), {type: 'success'});
|
2023-04-12 12:58:46 +03:00
|
|
|
|
|
|
|
// Update the models on the client side
|
|
|
|
for (const post of updatedModels) {
|
|
|
|
// We need to do it this way to prevent marking the model as dirty
|
|
|
|
this.store.push({
|
|
|
|
data: {
|
|
|
|
id: post.id,
|
|
|
|
type: 'post',
|
|
|
|
attributes: {
|
|
|
|
visibility
|
|
|
|
},
|
|
|
|
relationships: {
|
|
|
|
links: {
|
|
|
|
data: tiers
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-04-12 16:28:09 +03:00
|
|
|
// Remove posts that no longer match the filter
|
|
|
|
this.updateFilteredPosts();
|
|
|
|
|
2023-04-12 12:58:46 +03:00
|
|
|
close();
|
2023-04-13 16:04:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
@task
|
|
|
|
*featurePostsTask() {
|
|
|
|
const updatedModels = this.selectionList.availableModels;
|
|
|
|
yield this.performBulkEdit('feature');
|
|
|
|
|
|
|
|
this.notifications.showNotification(this.#getToastMessage('featured'), {type: 'success'});
|
|
|
|
|
2023-04-13 16:09:17 +03:00
|
|
|
// Update the models on the client side
|
|
|
|
for (const post of updatedModels) {
|
2023-04-13 16:04:06 +03:00
|
|
|
// We need to do it this way to prevent marking the model as dirty
|
|
|
|
this.store.push({
|
|
|
|
data: {
|
|
|
|
id: post.id,
|
|
|
|
type: 'post',
|
|
|
|
attributes: {
|
|
|
|
featured: true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove posts that no longer match the filter
|
|
|
|
this.updateFilteredPosts();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@task
|
|
|
|
*unfeaturePostsTask() {
|
|
|
|
const updatedModels = this.selectionList.availableModels;
|
|
|
|
yield this.performBulkEdit('unfeature');
|
|
|
|
|
|
|
|
this.notifications.showNotification(this.#getToastMessage('unfeatured'), {type: 'success'});
|
|
|
|
|
|
|
|
// Update the models on the client side
|
|
|
|
for (const post of updatedModels) {
|
|
|
|
// We need to do it this way to prevent marking the model as dirty
|
|
|
|
this.store.push({
|
|
|
|
data: {
|
|
|
|
id: post.id,
|
|
|
|
type: 'post',
|
|
|
|
attributes: {
|
|
|
|
featured: false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove posts that no longer match the filter
|
|
|
|
this.updateFilteredPosts();
|
2023-04-12 12:58:46 +03:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-04-11 17:37:42 +03:00
|
|
|
async performBulkDestroy() {
|
|
|
|
const filter = this.selectionList.filter;
|
|
|
|
let bulkUpdateUrl = this.ghostPaths.url.api(`posts`) + `?filter=${encodeURIComponent(filter)}`;
|
|
|
|
return await this.ajax.delete(bulkUpdateUrl);
|
2023-04-07 12:48:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
async performBulkEdit(_action, meta = {}) {
|
|
|
|
const filter = this.selectionList.filter;
|
|
|
|
let bulkUpdateUrl = this.ghostPaths.url.api(`posts/bulk`) + `?filter=${encodeURIComponent(filter)}`;
|
|
|
|
return await this.ajax.put(bulkUpdateUrl, {
|
|
|
|
data: {
|
|
|
|
bulk: {
|
|
|
|
action: _action,
|
|
|
|
meta
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
get shouldFeatureSelection() {
|
2023-04-12 16:37:53 +03:00
|
|
|
let featuredCount = 0;
|
|
|
|
for (const m of this.selectionList.availableModels) {
|
|
|
|
if (m.featured) {
|
|
|
|
featuredCount += 1;
|
|
|
|
}
|
2023-04-07 12:48:14 +03:00
|
|
|
}
|
2023-04-12 16:37:53 +03:00
|
|
|
return featuredCount <= this.selectionList.availableModels.length / 2;
|
2023-04-07 12:48:14 +03:00
|
|
|
}
|
|
|
|
|
2023-04-11 09:25:49 +03:00
|
|
|
get canFeatureSelection() {
|
2023-04-12 16:37:53 +03:00
|
|
|
for (const m of this.selectionList.availableModels) {
|
|
|
|
if (m.get('status') !== 'sent') {
|
|
|
|
return true;
|
|
|
|
}
|
2023-04-11 09:25:49 +03:00
|
|
|
}
|
2023-04-12 16:37:53 +03:00
|
|
|
return false;
|
2023-04-12 16:28:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
get canUnpublishSelection() {
|
|
|
|
for (const m of this.selectionList.availableModels) {
|
|
|
|
if (['published', 'sent'].includes(m.status)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
2023-04-11 09:25:49 +03:00
|
|
|
}
|
2023-04-07 12:48:14 +03:00
|
|
|
}
|