From 2f02ec55665d3b185bd1dfae969a1f1c1fe15efc Mon Sep 17 00:00:00 2001 From: Sodbileg Gansukh Date: Tue, 24 Sep 2024 15:32:45 +0800 Subject: [PATCH] Added unschedule bulk action to posts (#20945) ref DES-675 --- .../components/posts-list/context-menu.hbs | 7 +++ .../app/components/posts-list/context-menu.js | 52 ++++++++++++++++++- .../posts-list/modals/unschedule-posts.hbs | 27 ++++++++++ .../app/styles/components/modals-new.css | 1 + ghost/admin/tests/acceptance/content-test.js | 40 ++++++++++++++ .../src/PostsBulkUnscheduledEvent.ts | 13 +++++ ghost/post-events/src/index.ts | 1 + ghost/post-events/test/post-events.test.ts | 7 +++ ghost/posts-service/lib/PostsService.js | 14 +++++ 9 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 ghost/admin/app/components/posts-list/modals/unschedule-posts.hbs create mode 100644 ghost/post-events/src/PostsBulkUnscheduledEvent.ts diff --git a/ghost/admin/app/components/posts-list/context-menu.hbs b/ghost/admin/app/components/posts-list/context-menu.hbs index a7277b4161..6646408e0a 100644 --- a/ghost/admin/app/components/posts-list/context-menu.hbs +++ b/ghost/admin/app/components/posts-list/context-menu.hbs @@ -21,6 +21,13 @@ {{/if}} + {{#if this.canUnscheduleSelection}} +
  • + +
  • + {{/if}} {{/if}} {{#if this.canFeatureSelection}} {{#if this.shouldFeatureSelection }} diff --git a/ghost/admin/app/components/posts-list/context-menu.js b/ghost/admin/app/components/posts-list/context-menu.js index 8bac60d8b5..e689acbd42 100644 --- a/ghost/admin/app/components/posts-list/context-menu.js +++ b/ghost/admin/app/components/posts-list/context-menu.js @@ -3,6 +3,7 @@ import Component from '@glimmer/component'; import DeletePostsModal from './modals/delete-posts'; import EditPostsAccessModal from './modals/edit-posts-access'; import UnpublishPostsModal from './modals/unpublish-posts'; +import UnschedulePostsModal from './modals/unschedule-posts'; import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard'; import nql from '@tryghost/nql'; import {action} from '@ember/object'; @@ -29,6 +30,10 @@ const messages = { single: '{Type} reverted to a draft', multiple: '{count} {type}s reverted to drafts' }, + unscheduled: { + single: '{Type} unscheduled', + multiple: '{count} {type}s unscheduled' + }, accessUpdated: { single: '{Type} access updated', multiple: '{Type} access updated for {count} {type}s' @@ -128,6 +133,15 @@ export default class PostsContextMenu extends Component { }); } + @action + async unschedulePosts() { + await this.menu.openModal(UnschedulePostsModal, { + type: this.type, + selectionList: this.selectionList, + confirm: this.unschedulePostsTask + }); + } + @action async editPostsAccess() { this.menu.openModal(EditPostsAccessModal, { @@ -240,7 +254,7 @@ export default class PostsContextMenu extends Component { // 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; } @@ -271,6 +285,33 @@ export default class PostsContextMenu extends Component { return true; } + @task + *unschedulePostsTask() { + const updatedModels = this.selectionList.availableModels; + yield this.performBulkEdit('unschedule'); + this.notifications.showNotification(this.#getToastMessage('unscheduled'), {type: 'success'}); + + // Update the models on the client side + for (const post of updatedModels) { + if (post.status === 'scheduled') { + // We need to do it this way to prevent marking the model as dirty + this.store.push({ + data: { + id: post.id, + type: this.type, + attributes: { + status: 'draft', + published_at: null + } + } + }); + } + } + + this.updateFilteredPosts(); + return true; + } + updateFilteredPosts() { const updatedModels = this.selectionList.availableModels; const filter = this.selectionList.allFilter; @@ -489,6 +530,15 @@ export default class PostsContextMenu extends Component { return false; } + get canUnscheduleSelection() { + for (const m of this.selectionList.availableModels) { + if (m.status === 'scheduled') { + return true; + } + } + return false; + } + get canCopySelection() { return this.selectionList.availableModels.length === 1; } diff --git a/ghost/admin/app/components/posts-list/modals/unschedule-posts.hbs b/ghost/admin/app/components/posts-list/modals/unschedule-posts.hbs new file mode 100644 index 0000000000..53d0677f94 --- /dev/null +++ b/ghost/admin/app/components/posts-list/modals/unschedule-posts.hbs @@ -0,0 +1,27 @@ + diff --git a/ghost/admin/app/styles/components/modals-new.css b/ghost/admin/app/styles/components/modals-new.css index e9ffdbfbf7..66032a15a3 100644 --- a/ghost/admin/app/styles/components/modals-new.css +++ b/ghost/admin/app/styles/components/modals-new.css @@ -218,6 +218,7 @@ line-height: 1.15em; font-weight: 600; letter-spacing: -0.025em; + text-wrap: pretty; } .modal-header.icon-center { diff --git a/ghost/admin/tests/acceptance/content-test.js b/ghost/admin/tests/acceptance/content-test.js index 44cf74b19d..2e486af714 100644 --- a/ghost/admin/tests/acceptance/content-test.js +++ b/ghost/admin/tests/acceptance/content-test.js @@ -539,6 +539,46 @@ describe('Acceptance: Posts / Pages', function () { expect(postFourContainer.querySelector('.gh-content-entry-status').textContent, 'postThree status').to.contain('Draft'); }); + it('can unschedule', async function () { + await visit('/posts'); + + // get all posts + const posts = findAll('[data-test-post-id]'); + expect(posts.length, 'all posts count').to.equal(4); + + const postOneContainer = posts[0].parentElement; // scheduled post + + await click(postOneContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'}); + + expect(postOneContainer.getAttribute('data-selected'), 'postOne 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(postOneContainer, 'contextmenu'); + + let contextMenu = find('.gh-posts-context-menu'); // this is a