FEATURE: Use new topic bulk actions dropdown on search page (#27303)

We want to get rid of the old topic bulk actions modal
and use the new dropdown (currently gated behind
experimental_topic_bulk_actions_enabled_groups). To do
this we need to use the new dropdown in all places in the
UI.

This commit changes the full page search UI to use the new
topic bulk actions dropdown if experimental_topic_bulk_actions_enabled_groups
is enabled, and makes some minor refactors to make this work.
Also add a spec for both the old and new functionality.
This commit is contained in:
Martin Brennan 2024-06-07 10:41:42 +10:00 committed by GitHub
parent 0739431cc0
commit 36dbf06fe9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 144 additions and 41 deletions

View File

@ -6,4 +6,5 @@
@action={{fn @performAndRefresh (hash type="append_tags" tags=this.tags)}} @action={{fn @performAndRefresh (hash type="append_tags" tags=this.tags)}}
@disabled={{not this.tags}} @disabled={{not this.tags}}
@label="topics.bulk.append_tags" @label="topics.bulk.append_tags"
class="topic-bulk-actions__append-tags"
/> />

View File

@ -36,7 +36,6 @@ export function addBulkDropdownButton(opts) {
export default class BulkSelectTopicsDropdown extends Component { export default class BulkSelectTopicsDropdown extends Component {
@service modal; @service modal;
@service router;
@service currentUser; @service currentUser;
@service siteSettings; @service siteSettings;
@ -174,7 +173,7 @@ export default class BulkSelectTopicsDropdown extends Component {
title, title,
description, description,
bulkSelectHelper: this.args.bulkSelectHelper, bulkSelectHelper: this.args.bulkSelectHelper,
refreshClosure: () => this.router.refresh(), refreshClosure: () => this.args.afterBulkActionComplete(),
allowSilent, allowSilent,
initialAction, initialAction,
initialActionLabel, initialActionLabel,

View File

@ -42,7 +42,7 @@ export default class TopicBulkActions extends Component {
{ {
label: "topics.bulk.change_category", label: "topics.bulk.change_category",
icon: "pencil-alt", icon: "pencil-alt",
class: "btn-default", class: "btn-default bulk-actions__change-category",
visible: ({ topics }) => !topics.some((t) => t.isPrivateMessage), visible: ({ topics }) => !topics.some((t) => t.isPrivateMessage),
action({ setComponent }) { action({ setComponent }) {
setComponent(ChangeCategory); setComponent(ChangeCategory);
@ -60,7 +60,7 @@ export default class TopicBulkActions extends Component {
{ {
label: "topics.bulk.archive_topics", label: "topics.bulk.archive_topics",
icon: "folder", icon: "folder",
class: "btn-default", class: "btn-default bulk-actions__archive-topics",
visible: ({ topics }) => !topics.some((t) => t.isPrivateMessage), visible: ({ topics }) => !topics.some((t) => t.isPrivateMessage),
action({ forEachPerformed }) { action({ forEachPerformed }) {
forEachPerformed({ type: "archive" }, (t) => t.set("archived", true)); forEachPerformed({ type: "archive" }, (t) => t.set("archived", true));
@ -69,7 +69,7 @@ export default class TopicBulkActions extends Component {
{ {
label: "topics.bulk.archive_topics", label: "topics.bulk.archive_topics",
icon: "folder", icon: "folder",
class: "btn-default", class: "btn-default bulk-actions__archive-topics",
visible: ({ topics }) => topics.some((t) => t.isPrivateMessage), visible: ({ topics }) => topics.some((t) => t.isPrivateMessage),
action: ({ performAndRefresh }) => { action: ({ performAndRefresh }) => {
const userPrivateMessages = getOwner(this).lookup( const userPrivateMessages = getOwner(this).lookup(
@ -87,7 +87,7 @@ export default class TopicBulkActions extends Component {
{ {
label: "topics.bulk.move_messages_to_inbox", label: "topics.bulk.move_messages_to_inbox",
icon: "folder", icon: "folder",
class: "btn-default", class: "btn-default bulk-actions__move-messages-to-inbox",
visible: ({ topics }) => topics.some((t) => t.isPrivateMessage), visible: ({ topics }) => topics.some((t) => t.isPrivateMessage),
action: ({ performAndRefresh }) => { action: ({ performAndRefresh }) => {
const userPrivateMessages = getOwner(this).lookup( const userPrivateMessages = getOwner(this).lookup(
@ -105,7 +105,7 @@ export default class TopicBulkActions extends Component {
{ {
label: "topics.bulk.notification_level", label: "topics.bulk.notification_level",
icon: "d-regular", icon: "d-regular",
class: "btn-default", class: "btn-default bulk-actions__notification-level",
action({ setComponent }) { action({ setComponent }) {
setComponent(NotificationLevel); setComponent(NotificationLevel);
}, },
@ -113,7 +113,7 @@ export default class TopicBulkActions extends Component {
{ {
label: "topics.bulk.defer", label: "topics.bulk.defer",
icon: "circle", icon: "circle",
class: "btn-default", class: "btn-default bulk-actions__defer",
visible: ({ currentUser }) => currentUser.user_option.enable_defer, visible: ({ currentUser }) => currentUser.user_option.enable_defer,
action({ performAndRefresh }) { action({ performAndRefresh }) {
performAndRefresh({ type: "destroy_post_timing" }); performAndRefresh({ type: "destroy_post_timing" });
@ -122,7 +122,7 @@ export default class TopicBulkActions extends Component {
{ {
label: "topics.bulk.unlist_topics", label: "topics.bulk.unlist_topics",
icon: "far-eye-slash", icon: "far-eye-slash",
class: "btn-default", class: "btn-default bulk-actions__unlist",
visible: ({ topics }) => visible: ({ topics }) =>
topics.some((t) => t.visible) && topics.some((t) => t.visible) &&
!topics.some((t) => t.isPrivateMessage), !topics.some((t) => t.isPrivateMessage),
@ -133,7 +133,7 @@ export default class TopicBulkActions extends Component {
{ {
label: "topics.bulk.relist_topics", label: "topics.bulk.relist_topics",
icon: "far-eye", icon: "far-eye",
class: "btn-default", class: "btn-default bulk-actions__relist",
visible: ({ topics }) => visible: ({ topics }) =>
topics.some((t) => !t.visible) && topics.some((t) => !t.visible) &&
!topics.some((t) => t.isPrivateMessage), !topics.some((t) => t.isPrivateMessage),
@ -144,7 +144,7 @@ export default class TopicBulkActions extends Component {
{ {
label: "topics.bulk.reset_bump_dates", label: "topics.bulk.reset_bump_dates",
icon: "anchor", icon: "anchor",
class: "btn-default", class: "btn-default bulk-actions__reset-bump-dates",
visible: ({ currentUser }) => currentUser.canManageTopic, visible: ({ currentUser }) => currentUser.canManageTopic,
action({ performAndRefresh }) { action({ performAndRefresh }) {
performAndRefresh({ type: "reset_bump_dates" }); performAndRefresh({ type: "reset_bump_dates" });
@ -153,7 +153,7 @@ export default class TopicBulkActions extends Component {
{ {
label: "topics.bulk.change_tags", label: "topics.bulk.change_tags",
icon: "tag", icon: "tag",
class: "btn-default", class: "btn-default bulk-actions__change-tags",
visible: ({ currentUser, siteSettings }) => visible: ({ currentUser, siteSettings }) =>
siteSettings.tagging_enabled && currentUser.canManageTopic, siteSettings.tagging_enabled && currentUser.canManageTopic,
action({ setComponent }) { action({ setComponent }) {
@ -163,7 +163,7 @@ export default class TopicBulkActions extends Component {
{ {
label: "topics.bulk.append_tags", label: "topics.bulk.append_tags",
icon: "tag", icon: "tag",
class: "btn-default", class: "btn-default bulk-actions__append-tags",
visible: ({ currentUser, siteSettings }) => visible: ({ currentUser, siteSettings }) =>
siteSettings.tagging_enabled && currentUser.canManageTopic, siteSettings.tagging_enabled && currentUser.canManageTopic,
action({ setComponent }) { action({ setComponent }) {
@ -173,7 +173,7 @@ export default class TopicBulkActions extends Component {
{ {
label: "topics.bulk.remove_tags", label: "topics.bulk.remove_tags",
icon: "tag", icon: "tag",
class: "btn-default", class: "btn-default bulk-actions__remove-tags",
visible: ({ currentUser, siteSettings }) => visible: ({ currentUser, siteSettings }) =>
siteSettings.tagging_enabled && currentUser.canManageTopic, siteSettings.tagging_enabled && currentUser.canManageTopic,
action: ({ performAndRefresh, topics }) => { action: ({ performAndRefresh, topics }) => {
@ -188,7 +188,7 @@ export default class TopicBulkActions extends Component {
{ {
label: "topics.bulk.delete", label: "topics.bulk.delete",
icon: "trash-alt", icon: "trash-alt",
class: "btn-danger delete-topics", class: "btn-danger delete-topics bulk-actions__delete",
visible: ({ currentUser }) => currentUser.staff, visible: ({ currentUser }) => currentUser.staff,
action({ performAndRefresh }) { action({ performAndRefresh }) {
performAndRefresh({ type: "delete" }); performAndRefresh({ type: "delete" });

View File

@ -4,7 +4,7 @@
</a> </a>
</div> </div>
<div class="fps-topic"> <div class="fps-topic" data-topic-id={{this.post.topic.id}}>
<div class="topic"> <div class="topic">
{{#if this.bulkSelectEnabled}} {{#if this.bulkSelectEnabled}}
<TrackSelected <TrackSelected

View File

@ -9,7 +9,10 @@ const TopicBulkSelectDropdown = <template>
count=@bulkSelectHelper.selected.length count=@bulkSelectHelper.selected.length
}} }}
</span> </span>
<BulkSelectTopicsDropdown @bulkSelectHelper={{@bulkSelectHelper}} /> <BulkSelectTopicsDropdown
@bulkSelectHelper={{@bulkSelectHelper}}
@afterBulkActionComplete={{@afterBulkActionComplete}}
/>
</div> </div>
</template>; </template>;

View File

@ -72,6 +72,11 @@ export default class TopicListHeaderColumn extends Component {
} }
} }
@action
afterBulkActionComplete() {
return this.router.refresh();
}
<template> <template>
<th <th
{{(if @sortable (modifier on "click" this.onClick))}} {{(if @sortable (modifier on "click" this.onClick))}}
@ -108,6 +113,7 @@ export default class TopicListHeaderColumn extends Component {
{{#if @experimentalTopicBulkActionsEnabled}} {{#if @experimentalTopicBulkActionsEnabled}}
<TopicBulkSelectDropdown <TopicBulkSelectDropdown
@bulkSelectHelper={{@bulkSelectHelper}} @bulkSelectHelper={{@bulkSelectHelper}}
@afterBulkActionComplete={{this.afterBulkActionComplete}}
/> />
{{else}} {{else}}
<button <button

View File

@ -6,6 +6,7 @@ import { isEmpty } from "@ember/utils";
import { Promise } from "rsvp"; import { Promise } from "rsvp";
import TopicBulkActions from "discourse/components/modal/topic-bulk-actions"; import TopicBulkActions from "discourse/components/modal/topic-bulk-actions";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import BulkSelectHelper from "discourse/lib/bulk-select-helper";
import { search as searchCategoryTag } from "discourse/lib/category-tag-search"; import { search as searchCategoryTag } from "discourse/lib/category-tag-search";
import { setTransient } from "discourse/lib/page-tracker"; import { setTransient } from "discourse/lib/page-tracker";
import { import {
@ -59,6 +60,7 @@ export default Controller.extend({
appEvents: service(), appEvents: service(),
siteSettings: service(), siteSettings: service(),
searchPreferencesManager: service(), searchPreferencesManager: service(),
currentUser: service(),
bulkSelectEnabled: null, bulkSelectEnabled: null,
loading: false, loading: false,
@ -82,7 +84,6 @@ export default Controller.extend({
resultCount: null, resultCount: null,
searchTypes: null, searchTypes: null,
additionalSearchResults: [], additionalSearchResults: [],
selected: [],
error: null, error: null,
init() { init() {
@ -113,6 +114,8 @@ export default Controller.extend({
}); });
this.set("searchTypes", searchTypes); this.set("searchTypes", searchTypes);
this.bulkSelectHelper = new BulkSelectHelper(this);
}, },
@discourseComputed("resultCount") @discourseComputed("resultCount")
@ -229,6 +232,11 @@ export default Controller.extend({
} }
}, },
@discourseComputed("currentUser.use_experimental_topic_bulk_actions")
useNewBulkActions() {
return this.currentUser?.use_experimental_topic_bulk_actions;
},
@discourseComputed("q") @discourseComputed("q")
showLikeCount(q) { showLikeCount(q) {
return q?.includes("order:likes"); return q?.includes("order:likes");
@ -282,9 +290,12 @@ export default Controller.extend({
return this.currentUser && this.currentUser.staff && hasResults; return this.currentUser && this.currentUser.staff && hasResults;
}, },
hasSelection: gt("selected.length", 0), hasSelection: gt("bulkSelectHelper.selected.length", 0),
@discourseComputed("selected.length", "searchResultPosts.length") @discourseComputed(
"bulkSelectHelper.selected.length",
"searchResultPosts.length"
)
hasUnselectedResults(selectionCount, postsCount) { hasUnselectedResults(selectionCount, postsCount) {
return selectionCount < postsCount; return selectionCount < postsCount;
}, },
@ -350,7 +361,7 @@ export default Controller.extend({
if (args.page === 1) { if (args.page === 1) {
this.set("bulkSelectEnabled", false); this.set("bulkSelectEnabled", false);
this.selected.clear(); this.bulkSelectHelper.selected.clear();
this.set("searching", true); this.set("searching", true);
scrollTop(); scrollTop();
} else { } else {
@ -465,8 +476,13 @@ export default Controller.extend({
searching: false, searching: false,
page: 1, page: 1,
resultCount: null, resultCount: null,
selected: [],
}); });
this.bulkSelectHelper.clear();
},
@action
afterBulkActionComplete() {
return Promise.resolve(this._search());
}, },
@action @action
@ -502,7 +518,9 @@ export default Controller.extend({
actions: { actions: {
selectAll() { selectAll() {
this.selected.addObjects(this.get("searchResultPosts").mapBy("topic")); this.bulkSelectHelper.selected.addObjects(
this.get("searchResultPosts").mapBy("topic")
);
// Doing this the proper way is a HUGE pain, // Doing this the proper way is a HUGE pain,
// we can hack this to work by observing each on the array // we can hack this to work by observing each on the array
@ -517,7 +535,7 @@ export default Controller.extend({
}, },
clearAll() { clearAll() {
this.selected.clear(); this.bulkSelectHelper.selected.clear();
document document
.querySelectorAll(".fps-result input[type=checkbox]") .querySelectorAll(".fps-result input[type=checkbox]")
@ -528,13 +546,13 @@ export default Controller.extend({
toggleBulkSelect() { toggleBulkSelect() {
this.toggleProperty("bulkSelectEnabled"); this.toggleProperty("bulkSelectEnabled");
this.selected.clear(); this.bulkSelectHelper.selected.clear();
}, },
showBulkActions() { showBulkActions() {
this.modal.show(TopicBulkActions, { this.modal.show(TopicBulkActions, {
model: { model: {
topics: this.selected, topics: this.bulkSelectHelper.selected,
refreshClosure: this._search, refreshClosure: this._search,
}, },
}); });

View File

@ -1,13 +1,21 @@
import EmberObject from "@ember/object"; import EmberObject, { action } from "@ember/object";
import { service } from "@ember/service";
import BulkSelectTopicsDropdown from "discourse/components/bulk-select-topics-dropdown"; import BulkSelectTopicsDropdown from "discourse/components/bulk-select-topics-dropdown";
import rawRenderGlimmer from "discourse/lib/raw-render-glimmer"; import rawRenderGlimmer from "discourse/lib/raw-render-glimmer";
import i18n from "discourse-common/helpers/i18n"; import i18n from "discourse-common/helpers/i18n";
export default class extends EmberObject { export default class extends EmberObject {
@service router;
get selectedCount() { get selectedCount() {
return this.bulkSelectHelper.selected.length; return this.bulkSelectHelper.selected.length;
} }
@action
afterBulkAction() {
return this.router.refresh();
}
get html() { get html() {
return rawRenderGlimmer( return rawRenderGlimmer(
this, this,
@ -18,11 +26,13 @@ export default class extends EmberObject {
</span> </span>
<BulkSelectTopicsDropdown <BulkSelectTopicsDropdown
@bulkSelectHelper={{@data.bulkSelectHelper}} @bulkSelectHelper={{@data.bulkSelectHelper}}
@afterBulkActionComplete={{@data.afterBulkAction}}
/> />
</template>, </template>,
{ {
bulkSelectHelper: this.bulkSelectHelper, bulkSelectHelper: this.bulkSelectHelper,
selectedCount: this.selectedCount, selectedCount: this.selectedCount,
afterBulkAction: this.afterBulkAction,
} }
); );
} }

View File

@ -109,13 +109,20 @@
@action={{action "toggleBulkSelect"}} @action={{action "toggleBulkSelect"}}
class="btn-default bulk-select" class="btn-default bulk-select"
/> />
{{#if this.selected}} {{#if this.bulkSelectHelper.selected}}
<DButton {{#if this.useNewBulkActions}}
@selected={{this.selected}} <TopicList::TopicBulkSelectDropdown
@action={{action "showBulkActions"}} @bulkSelectHelper={{this.bulkSelectHelper}}
@icon="wrench" @afterBulkActionComplete={{this.afterBulkActionComplete}}
class="btn-default bulk-select-btn" />
/> {{else}}
<DButton
@selected={{this.bulkSelectHelper.selected}}
@action={{action "showBulkActions"}}
@icon="wrench"
class="btn-default bulk-select-btn"
/>
{{/if}}
{{/if}} {{/if}}
{{/if}} {{/if}}
@ -125,7 +132,7 @@
@icon="check-square" @icon="check-square"
@action={{action "selectAll"}} @action={{action "selectAll"}}
@label="search.select_all" @label="search.select_all"
class="btn-default" class="btn-default bulk-select-all"
/> />
{{/if}} {{/if}}
@ -134,7 +141,7 @@
@icon="far-square" @icon="far-square"
@action={{action "clearAll"}} @action={{action "clearAll"}}
@label="search.clear_all" @label="search.clear_all"
class="btn-default" class="btn-default bulk-select-clear"
/> />
{{/if}} {{/if}}
{{/if}} {{/if}}
@ -172,7 +179,7 @@
<SearchResultEntries <SearchResultEntries
@posts={{this.searchResultPosts}} @posts={{this.searchResultPosts}}
@bulkSelectEnabled={{this.bulkSelectEnabled}} @bulkSelectEnabled={{this.bulkSelectEnabled}}
@selected={{this.selected}} @selected={{this.bulkSelectHelper.selected}}
@highlightQuery={{this.highlightQuery}} @highlightQuery={{this.highlightQuery}}
@searchLogId={{this.model.grouped_search_result.search_log_id}} @searchLogId={{this.model.grouped_search_result.search_log_id}}
/> />

View File

@ -32,10 +32,6 @@ module PageObjects
find(bulk_select_dropdown_item(name)).click find(bulk_select_dropdown_item(name)).click
end end
def click_close_topics_button
find(bulk_select_dropdown_item("close-topics")).click
end
def has_bulk_select_modal? def has_bulk_select_modal?
page.has_css?("#discourse-modal-title") page.has_css?("#discourse-modal-title")
end end

View File

@ -7,6 +7,8 @@ describe "Search", type: :system do
fab!(:topic2) { Fabricate(:topic, title: "Another test topic") } fab!(:topic2) { Fabricate(:topic, title: "Another test topic") }
fab!(:post2) { Fabricate(:post, topic: topic2, raw: "This is another test post in a test topic") } fab!(:post2) { Fabricate(:post, topic: topic2, raw: "This is another test post in a test topic") }
let(:topic_bulk_actions_modal) { PageObjects::Modals::TopicBulkActions.new }
describe "when using full page search on mobile" do describe "when using full page search on mobile" do
before do before do
SearchIndexer.enable SearchIndexer.enable
@ -127,4 +129,65 @@ describe "Search", type: :system do
expect(log.search_type).to eq(SearchLog.search_types[:header]) expect(log.search_type).to eq(SearchLog.search_types[:header])
end end
end end
describe "bulk actions" do
fab!(:admin)
fab!(:tag1) { Fabricate(:tag) }
before do
SearchIndexer.enable
SearchIndexer.index(topic, force: true)
SearchIndexer.index(topic2, force: true)
sign_in(admin)
end
after { SearchIndexer.disable }
context "when experimental_topic_bulk_actions_enabled_groups is enabled" do
before do
SiteSetting.experimental_topic_bulk_actions_enabled_groups =
Group::AUTO_GROUPS[:trust_level_1]
end
it "allows the user to perform bulk actions on the topic search results" do
visit("/search?q=test")
expect(page).to have_content(topic.title)
find(".search-info .bulk-select").click
find(".fps-result .fps-topic[data-topic-id=\"#{topic.id}\"] .bulk-select input").click
find(".search-info .bulk-select-topics-dropdown-trigger").click
find(".bulk-select-topics-dropdown-content .append-tags").click
expect(topic_bulk_actions_modal).to be_open
tag_selector = PageObjects::Components::SelectKit.new(".tag-chooser")
tag_selector.search(tag1.name)
tag_selector.select_row_by_value(tag1.name)
tag_selector.collapse
topic_bulk_actions_modal.click_bulk_topics_confirm
expect(
find(".fps-result .fps-topic[data-topic-id=\"#{topic.id}\"] .discourse-tags"),
).to have_content(tag1.name)
end
end
context "when experimental_topic_bulk_actions_enabled_groups is not enabled" do
before { SiteSetting.experimental_topic_bulk_actions_enabled_groups = "" }
it "allows the user to perform bulk actions on the topic search results" do
visit("/search?q=test")
expect(page).to have_content(topic.title)
find(".search-info .bulk-select").click
find(".fps-result .fps-topic[data-topic-id=\"#{topic.id}\"] .bulk-select input").click
find(".search-info .bulk-select-btn").click
expect(topic_bulk_actions_modal).to be_open
find(".bulk-buttons .bulk-actions__append-tags").click
tag_selector = PageObjects::Components::SelectKit.new(".tag-chooser")
tag_selector.search(tag1.name)
tag_selector.select_row_by_value(tag1.name)
tag_selector.collapse
find(".topic-bulk-actions__append-tags").click
expect(
find(".fps-result .fps-topic[data-topic-id=\"#{topic.id}\"] .discourse-tags"),
).to have_content(tag1.name)
end
end
end
end end