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