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