From 4b2bd4d682b6f25293eef89f03d362311642b25b Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Mon, 3 Jun 2024 14:37:28 +1000 Subject: [PATCH] FEATURE: Allow "move to inbox" and "move to archive" for private messages using new bulk topic dropdown (#27236) This commit re-introduces the "Move to Inbox" and "Move to Archive" bulk topic actions, which we had in the old modal but had not yet added to the new "experimental" dropdown, which isn't really experimental at this point. Once this is merged we can remove the old modal and only rely on the new dropdown. --- .../bulk-select-topics-dropdown.gjs | 57 +++-- .../components/modal/bulk-topic-actions.gjs | 15 ++ .../bulk-select-topics-dropdown-test.js | 225 ++++++++++++++++++ config/locales/client.en.yml | 15 +- spec/fabricators/topic_fabricator.rb | 9 + spec/system/topic_bulk_select_spec.rb | 107 ++++++++- 6 files changed, 398 insertions(+), 30 deletions(-) create mode 100644 app/assets/javascripts/discourse/tests/integration/components/bulk-select-topics-dropdown-test.js diff --git a/app/assets/javascripts/discourse/app/components/bulk-select-topics-dropdown.gjs b/app/assets/javascripts/discourse/app/components/bulk-select-topics-dropdown.gjs index 9e1a6293e23..8853c2120c4 100644 --- a/app/assets/javascripts/discourse/app/components/bulk-select-topics-dropdown.gjs +++ b/app/assets/javascripts/discourse/app/components/bulk-select-topics-dropdown.gjs @@ -46,6 +46,9 @@ export default class BulkSelectTopicsDropdown extends Component { id: "update-category", icon: "pencil-alt", name: i18n("topic_bulk_actions.update_category.name"), + visible: ({ topics }) => { + return !topics.some((t) => t.isPrivateMessage); + }, }, { id: "update-notifications", @@ -72,6 +75,19 @@ export default class BulkSelectTopicsDropdown extends Component { id: "archive-topics", icon: "folder", name: i18n("topic_bulk_actions.archive_topics.name"), + visible: ({ topics }) => !topics.some((t) => t.isPrivateMessage), + }, + { + id: "archive-messages", + icon: "archive", + name: i18n("topic_bulk_actions.archive_messages.name"), + visible: ({ topics }) => topics.every((t) => t.isPrivateMessage), + }, + { + id: "move-messages-to-inbox", + icon: "envelope", + name: i18n("topic_bulk_actions.move_messages_to_inbox.name"), + visible: ({ topics }) => topics.every((t) => t.isPrivateMessage), }, { id: "unlist-topics", @@ -167,17 +183,17 @@ export default class BulkSelectTopicsDropdown extends Component { } @action - async onSelect(id) { + async onSelect(actionId) { await this.dMenu.close(); - switch (id) { + switch (actionId) { case "update-category": - this.showBulkTopicActionsModal(id, "change_category", { + this.showBulkTopicActionsModal(actionId, "change_category", { description: i18n(`topic_bulk_actions.update_category.description`), }); break; case "update-notifications": - this.showBulkTopicActionsModal(id, "notification_level", { + this.showBulkTopicActionsModal(actionId, "notification_level", { description: i18n( `topic_bulk_actions.update_notifications.description` ), @@ -191,6 +207,15 @@ export default class BulkSelectTopicsDropdown extends Component { case "archive-topics": this.showBulkTopicActionsModal("archive", "archive_topics"); break; + case "archive-messages": + this.showBulkTopicActionsModal("archive_messages", "archive_messages"); + break; + case "move-messages-to-inbox": + this.showBulkTopicActionsModal( + "move_messages_to_inbox", + "move_messages_to_inbox" + ); + break; case "unlist-topics": this.showBulkTopicActionsModal("unlist", "unlist_topics"); break; @@ -198,33 +223,37 @@ export default class BulkSelectTopicsDropdown extends Component { this.showBulkTopicActionsModal("relist", "relist_topics"); break; case "append-tags": - this.showBulkTopicActionsModal(id, "choose_append_tags"); + this.showBulkTopicActionsModal(actionId, "choose_append_tags"); break; case "replace-tags": - this.showBulkTopicActionsModal(id, "change_tags"); + this.showBulkTopicActionsModal(actionId, "change_tags"); break; case "remove-tags": - this.showBulkTopicActionsModal(id, "remove_tags"); + this.showBulkTopicActionsModal(actionId, "remove_tags"); break; case "delete-topics": this.showBulkTopicActionsModal("delete", "delete"); break; case "reset-bump-dates": - this.showBulkTopicActionsModal(id, "reset_bump_dates", { + this.showBulkTopicActionsModal(actionId, "reset_bump_dates", { description: i18n(`topic_bulk_actions.reset_bump_dates.description`), }); break; case "defer": - this.showBulkTopicActionsModal(id, "defer", { + this.showBulkTopicActionsModal(actionId, "defer", { description: i18n(`topic_bulk_actions.defer.description`), }); break; default: - if (_customOnSelection[id]) { - this.showBulkTopicActionsModal(id, _customOnSelection[id].label, { - custom: true, - setComponent: _customOnSelection[id].setComponent, - }); + if (_customOnSelection[actionId]) { + this.showBulkTopicActionsModal( + actionId, + _customOnSelection[actionId].label, + { + custom: true, + setComponent: _customOnSelection[actionId].setComponent, + } + ); } } } diff --git a/app/assets/javascripts/discourse/app/components/modal/bulk-topic-actions.gjs b/app/assets/javascripts/discourse/app/components/modal/bulk-topic-actions.gjs index dbfb1c6f0bd..acf52cbbce2 100644 --- a/app/assets/javascripts/discourse/app/components/modal/bulk-topic-actions.gjs +++ b/app/assets/javascripts/discourse/app/components/modal/bulk-topic-actions.gjs @@ -1,5 +1,6 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; +import { getOwner } from "@ember/application"; import { Input } from "@ember/component"; import { on } from "@ember/modifier"; import { action } from "@ember/object"; @@ -145,6 +146,20 @@ export default class BulkTopicActions extends Component { t.set("archived", true) ); break; + case "archive_messages": + case "move_messages_to_inbox": + let userPrivateMessages = getOwner(this).lookup( + "controller:user-private-messages" + ); + + let params = { type: this.model.action }; + + if (userPrivateMessages.isGroup) { + params.group = userPrivateMessages.groupFilter; + } + + this.performAndRefresh(params); + break; case "unlist": this.forEachPerformed({ type: "unlist" }, (t) => t.set("unlisted", true) diff --git a/app/assets/javascripts/discourse/tests/integration/components/bulk-select-topics-dropdown-test.js b/app/assets/javascripts/discourse/tests/integration/components/bulk-select-topics-dropdown-test.js new file mode 100644 index 00000000000..13e2963f8ab --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/bulk-select-topics-dropdown-test.js @@ -0,0 +1,225 @@ +import { getOwner } from "@ember/application"; +import { click, render } from "@ember/test-helpers"; +import { hbs } from "ember-cli-htmlbars"; +import { module, test } from "qunit"; +import BulkSelectHelper from "discourse/lib/bulk-select-helper"; +import { TOPIC_VISIBILITY_REASONS } from "discourse/lib/constants"; +import { setupRenderingTest } from "discourse/tests/helpers/component-test"; + +const REGULAR_TOPIC_ID = 123; +const PM_TOPIC_ID = 124; +const UNLISTED_TOPIC_ID = 125; + +function createBulkSelectHelper(testThis, opts = {}) { + const store = getOwner(testThis).lookup("service:store"); + const regularTopic = store.createRecord("topic", { + id: REGULAR_TOPIC_ID, + visible: true, + }); + const pmTopic = store.createRecord("topic", { + id: PM_TOPIC_ID, + visible: true, + archetype: "private_message", + }); + const unlistedTopic = store.createRecord("topic", { + id: UNLISTED_TOPIC_ID, + visibility_reason_id: TOPIC_VISIBILITY_REASONS.manually_unlisted, + visible: false, + }); + const topics = [regularTopic, pmTopic, unlistedTopic].filter((t) => { + if (opts.topicIds) { + return opts.topicIds.includes(t.id); + } else { + return true; + } + }); + + const bulkSelectHelper = new BulkSelectHelper(testThis); + topics.forEach((t) => { + bulkSelectHelper.selected.addObject(t); + }); + return bulkSelectHelper; +} + +module("Integration | Component | BulkSelectTopicsDropdown", function (hooks) { + setupRenderingTest(hooks); + + test("actions all topics can perform", async function (assert) { + this.currentUser.admin = true; + this.bulkSelectHelper = createBulkSelectHelper(this); + + await render( + hbs`` + ); + + await click(".bulk-select-topics-dropdown-trigger"); + assert + .dom(".fk-d-menu__inner-content .dropdown-menu__item") + .exists({ count: 7 }); + + [ + "update-notifications", + "reset-bump-dates", + "close-topics", + "append-tags", + "replace-tags", + "remove-tags", + "delete-topics", + ].forEach((action) => { + assert + .dom(`.fk-d-menu__inner-content .dropdown-menu__item .${action}`) + .exists(); + }); + }); + + test("does not allow unlisting topics that are already unlisted", async function (assert) { + this.currentUser.admin = true; + this.bulkSelectHelper = createBulkSelectHelper(this, { + topicIds: [UNLISTED_TOPIC_ID], + }); + + await render( + hbs`` + ); + + await click(".bulk-select-topics-dropdown-trigger"); + assert + .dom(".fk-d-menu__inner-content .dropdown-menu__item .unlist-topics") + .doesNotExist(); + }); + + test("does not allow relisting topics that are already visible", async function (assert) { + this.currentUser.admin = true; + this.bulkSelectHelper = createBulkSelectHelper(this, { + topicIds: [REGULAR_TOPIC_ID], + }); + + await render( + hbs`` + ); + + await click(".bulk-select-topics-dropdown-trigger"); + assert + .dom(".fk-d-menu__inner-content .dropdown-menu__item .relist-topics") + .doesNotExist(); + }); + + test("allows deferring topics if the user has the preference enabled", async function (assert) { + this.currentUser.admin = true; + this.currentUser.user_option.enable_defer = true; + this.bulkSelectHelper = createBulkSelectHelper(this); + + await render( + hbs`` + ); + + await click(".bulk-select-topics-dropdown-trigger"); + assert + .dom(".fk-d-menu__inner-content .dropdown-menu__item .defer") + .exists(); + }); + + test("does not allow tagging actions if tagging_enabled is false", async function (assert) { + this.currentUser.admin = true; + this.siteSettings.tagging_enabled = false; + this.bulkSelectHelper = createBulkSelectHelper(this); + + await render( + hbs`` + ); + + await click(".bulk-select-topics-dropdown-trigger"); + ["append-tags", "replace-tags", "remove-tags"].forEach((action) => { + assert + .dom(`.fk-d-menu__inner-content .dropdown-menu__item .${action}`) + .doesNotExist(); + }); + }); + + test("does not allow tagging actions if user cannot manage topic", async function (assert) { + this.bulkSelectHelper = createBulkSelectHelper(this); + + await render( + hbs`` + ); + + await click(".bulk-select-topics-dropdown-trigger"); + ["append-tags", "replace-tags", "remove-tags"].forEach((action) => { + assert + .dom(`.fk-d-menu__inner-content .dropdown-menu__item .${action}`) + .doesNotExist(); + }); + }); + + test("does not allow deleting topics if user is not staff", async function (assert) { + this.bulkSelectHelper = createBulkSelectHelper(this); + + await render( + hbs`` + ); + + await click(".bulk-select-topics-dropdown-trigger"); + assert + .dom(".fk-d-menu__inner-content .dropdown-menu__item .delete-topics") + .doesNotExist(); + }); + + test("does not allow unlisting or relisting PM topics", async function (assert) { + this.currentUser.admin = true; + this.bulkSelectHelper = createBulkSelectHelper(this, { + topicIds: [PM_TOPIC_ID], + }); + + await render( + hbs`` + ); + + await click(".bulk-select-topics-dropdown-trigger"); + assert + .dom(".fk-d-menu__inner-content .dropdown-menu__item .relist-topics") + .doesNotExist(); + assert + .dom(".fk-d-menu__inner-content .dropdown-menu__item .unlist-topics") + .doesNotExist(); + }); + + test("does not allow updating category for PMs", async function (assert) { + this.currentUser.admin = true; + this.bulkSelectHelper = createBulkSelectHelper(this, { + topicIds: [PM_TOPIC_ID], + }); + + await render( + hbs`` + ); + + await click(".bulk-select-topics-dropdown-trigger"); + assert + .dom(".fk-d-menu__inner-content .dropdown-menu__item .update-category") + .doesNotExist(); + }); + + test("allows moving to archive and moving to inbox for PMs", async function (assert) { + this.currentUser.admin = true; + this.bulkSelectHelper = createBulkSelectHelper(this, { + topicIds: [PM_TOPIC_ID], + }); + + await render( + hbs`` + ); + + await click(".bulk-select-topics-dropdown-trigger"); + assert + .dom(".fk-d-menu__inner-content .dropdown-menu__item .archive-messages") + .exists(); + assert + .dom( + ".fk-d-menu__inner-content .dropdown-menu__item .move-messages-to-inbox" + ) + .exists(); + assert + .dom(".fk-d-menu__inner-content .dropdown-menu__item .archive-topics") + .doesNotExist(); + }); +}); diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 5cb05278858..d6000966086 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3018,6 +3018,7 @@ en: close_topics: "Close Topics" archive_topics: "Archive Topics" move_messages_to_inbox: "Move to Inbox" + archive_messages: "Move to Archive" notification_level: "Notifications…" change_notification_level: "Change Notification Level" choose_new_category: "Choose the new category for the topics:" @@ -3078,15 +3079,19 @@ en: topic_bulk_actions: close_topics: - name: "Close Topics" + name: "Close" note: "Note" optional: (optional) archive_topics: - name: "Archive Topics" + name: "Archive" + archive_messages: + name: "Move to Archive" + move_messages_to_inbox: + name: "Move to Inbox" unlist_topics: - name: "Unlist Topics" + name: "Unlist" relist_topics: - name: "Relist Topics" + name: "Relist" remove_tags: name: "Remove Tags" append_tags: @@ -3094,7 +3099,7 @@ en: replace_tags: name: "Replace Tags" delete_topics: - name: "Delete Topics" + name: "Delete" update_category: name: "Update Category" description: "Choose the new category for the selected topics" diff --git a/spec/fabricators/topic_fabricator.rb b/spec/fabricators/topic_fabricator.rb index c693fc213c6..f2ed08be097 100644 --- a/spec/fabricators/topic_fabricator.rb +++ b/spec/fabricators/topic_fabricator.rb @@ -26,3 +26,12 @@ Fabricator(:private_message_topic, from: :topic) do ] end end + +Fabricator(:group_private_message_topic, from: :topic) do + transient :recipient_group + category_id { nil } + title { sequence(:title) { |i| "This is a private message #{i} to a group" } } + archetype "private_message" + topic_allowed_users { |t| [Fabricate.build(:topic_allowed_user, user: t[:user])] } + topic_allowed_groups { |t| [Fabricate.build(:topic_allowed_group, group: t[:recipient_group])] } +end diff --git a/spec/system/topic_bulk_select_spec.rb b/spec/system/topic_bulk_select_spec.rb index b76fe4a8109..4250ab430c0 100644 --- a/spec/system/topic_bulk_select_spec.rb +++ b/spec/system/topic_bulk_select_spec.rb @@ -11,6 +11,20 @@ describe "Topic bulk select", type: :system do let(:topic_page) { PageObjects::Pages::Topic.new } let(:topic_bulk_actions_modal) { PageObjects::Modals::TopicBulkActions.new } + def open_bulk_actions_modal(topics_to_select = nil, action) + topic_list_header.click_bulk_select_button + + if !topics_to_select + topic_list.click_topic_checkbox(topics.last) + else + topics_to_select.each { |topic| topic_list.click_topic_checkbox(topic) } + end + + topic_list_header.click_bulk_select_topics_dropdown + topic_list_header.click_bulk_button(action) + expect(topic_bulk_actions_modal).to be_open + end + context "when appending tags" do fab!(:tag1) { Fabricate(:tag) } fab!(:tag2) { Fabricate(:tag) } @@ -22,17 +36,7 @@ describe "Topic bulk select", type: :system do sign_in(admin) visit("/latest") - topic_list_header.click_bulk_select_button - - if !topics_to_select - topic_list.click_topic_checkbox(topics.last) - else - topics_to_select.each { |topic| topic_list.click_topic_checkbox(topic) } - end - - topic_list_header.click_bulk_select_topics_dropdown - topic_list_header.click_bulk_button("append-tags") - expect(topic_bulk_actions_modal).to be_open + open_bulk_actions_modal(topics_to_select, "append-tags") end context "when in mobile", mobile: true do @@ -241,4 +245,85 @@ describe "Topic bulk select", type: :system do expect(topic_list).to have_no_topics end end + + context "when working with private messages" do + fab!(:private_message_1) do + Fabricate(:private_message_topic, user: admin, recipient: user, participant_count: 2) + end + fab!(:private_message_post_1) { Fabricate(:post, topic: private_message_1, user: admin) } + fab!(:private_message_post_2) { Fabricate(:post, topic: private_message_1, user: user) } + fab!(:group) + fab!(:group_private_message) do + Fabricate(:group_private_message_topic, user: admin, recipient_group: group) + end + + before do + TopicUser.change( + admin.id, + private_message_1, + notification_level: TopicUser.notification_levels[:tracking], + ) + TopicUser.update_last_read(admin, private_message_1.id, 1, 1, 1) + GroupUser.create!(user: admin, group: group) + end + + it "allows moving private messages to the Archive" do + sign_in(admin) + visit("/u/#{admin.username}/messages") + expect(page).to have_content(private_message_1.title) + open_bulk_actions_modal([private_message_1], "archive-messages") + topic_bulk_actions_modal.click_bulk_topics_confirm + expect(page).to have_content(I18n.t("js.topics.bulk.completed")) + visit("/u/#{admin.username}/messages/archive") + expect(page).to have_content(private_message_1.title) + expect(UserArchivedMessage.exists?(user_id: admin.id, topic_id: private_message_1.id)).to eq( + true, + ) + end + + it "allows moving private messages to the Inbox" do + UserArchivedMessage.create!(user: admin, topic: private_message_1) + sign_in(admin) + visit("/u/#{admin.username}/messages/archive") + expect(page).to have_content(private_message_1.title) + open_bulk_actions_modal([private_message_1], "move-messages-to-inbox") + topic_bulk_actions_modal.click_bulk_topics_confirm + expect(page).to have_content(I18n.t("js.topics.bulk.completed")) + visit("/u/#{admin.username}/messages") + expect(page).to have_content(private_message_1.title) + end + + it "allows moving group private messages to the scoped group Archive" do + sign_in(admin) + visit("/u/#{admin.username}/messages/group/#{group.name}") + expect(page).to have_content(group_private_message.title) + open_bulk_actions_modal([group_private_message], "archive-messages") + topic_bulk_actions_modal.click_bulk_topics_confirm + expect(page).to have_content(I18n.t("js.topics.bulk.completed")) + visit("/u/#{admin.username}/messages/group/#{group.name}/archive") + expect(page).to have_content(group_private_message.title) + end + + it "allows moving group private messages to the scoped group Inbox" do + GroupArchivedMessage.create!(group: group, topic: group_private_message) + sign_in(admin) + visit("/u/#{admin.username}/messages/group/#{group.name}/archive") + expect(page).to have_content(group_private_message.title) + open_bulk_actions_modal([group_private_message], "move-messages-to-inbox") + topic_bulk_actions_modal.click_bulk_topics_confirm + expect(page).to have_content(I18n.t("js.topics.bulk.completed")) + visit("/u/#{admin.username}/messages/group/#{group.name}") + expect(page).to have_content(group_private_message.title) + end + + context "when in mobile" do + it "is working", mobile: true do + # behavior is already tested on desktop, we simply ensure + # the general workflow is working on mobile + sign_in(admin) + visit("/u/#{admin.username}/messages") + open_bulk_actions_modal([private_message_1], "archive-messages") + end + end + end end