FEATURE: Use new topic bulk actions menu for all sites (#28003)

This commit promotes the new topic bulk action
menu introduced in 89883b2f51
to the main method of bulk selecting and performing
actions on topics. The site setting flag gating this
feature is deleted, and the old bulk select code is
deleted as well.

The new modal shows a loading spinner while operations
are taking place, allows selecting the action from a dropdown
instead of having a 2-step modal flow,
and also supports additional options for some operations, e.g.
allowing Close silently.
This commit is contained in:
Martin Brennan 2024-07-23 11:39:27 +10:00 committed by GitHub
parent a027ec4663
commit 0b413e2aa1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 34 additions and 928 deletions

View File

@ -1,44 +0,0 @@
<DModal
@title={{i18n "topics.bulk.actions"}}
@closeModal={{@closeModal}}
class="topic-bulk-actions-modal -large"
>
<p>
{{html-safe (i18n "topics.bulk.selected" count=@model.topics.length)}}
</p>
{{#if this.showProgress}}
<p>
{{html-safe (i18n "topics.bulk.progress" count=this.processedTopicCount)}}
</p>
{{else if this.activeComponent}}
<this.activeComponent
@loading={{this.loading}}
@topics={{@model.topics}}
@category={{@model.category}}
@setComponent={{this.setComponent}}
@forEachPerformed={{this.forEachPerformed}}
@performAndRefresh={{this.performAndRefresh}}
/>
{{else}}
<div class="bulk-buttons">
{{#each this.buttons as |button|}}
<DButton
@action={{fn
button.action
(hash
topics=@model.topics
category=@model.category
setComponent=this.setComponent
performAndRefresh=this.performAndRefresh
forEachPerformed=this.forEachPerformed
)
}}
@label={{button.label}}
@icon={{button.icon}}
class={{button.class}}
/>
{{/each}}
</div>
{{/if}}
</DModal>

View File

@ -1,312 +0,0 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { getOwner } from "@ember/application";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { Promise } from "rsvp";
import Topic from "discourse/models/topic";
import I18n from "discourse-i18n";
import AppendTags from "../bulk-actions/append-tags";
import ChangeCategory from "../bulk-actions/change-category";
import ChangeTags from "../bulk-actions/change-tags";
import NotificationLevel from "../bulk-actions/notification-level";
const _customButtons = [];
export function _addBulkButton(opts) {
_customButtons.push({
label: opts.label,
icon: opts.icon,
class: opts.class,
visible: opts.visible,
action: opts.action,
});
}
export function clearBulkButtons() {
_customButtons.length = 0;
}
// Modal for performing bulk actions on topics
export default class TopicBulkActions extends Component {
@service currentUser;
@service siteSettings;
@service dialog;
@tracked loading = false;
@tracked showProgress = false;
@tracked processedTopicCount = 0;
@tracked activeComponent = null;
defaultButtons = [
{
label: "topics.bulk.change_category",
icon: "pencil-alt",
class: "btn-default bulk-actions__change-category",
visible: ({ topics }) => !topics.some((t) => t.isPrivateMessage),
action({ setComponent }) {
setComponent(ChangeCategory);
},
},
{
label: "topics.bulk.close_topics",
icon: "lock",
class: "btn-default bulk-actions__close-topics",
visible: ({ topics }) => !topics.some((t) => t.isPrivateMessage),
action({ forEachPerformed }) {
forEachPerformed({ type: "close" }, (t) => t.set("closed", true));
},
},
{
label: "topics.bulk.archive_topics",
icon: "folder",
class: "btn-default bulk-actions__archive-topics",
visible: ({ topics }) => !topics.some((t) => t.isPrivateMessage),
action({ forEachPerformed }) {
forEachPerformed({ type: "archive" }, (t) => t.set("archived", true));
},
},
{
label: "topics.bulk.archive_topics",
icon: "folder",
class: "btn-default bulk-actions__archive-topics",
visible: ({ topics }) => topics.some((t) => t.isPrivateMessage),
action: ({ performAndRefresh }) => {
const userPrivateMessages = getOwner(this).lookup(
"controller:user-private-messages"
);
let params = { type: "archive_messages" };
if (userPrivateMessages.isGroup) {
params.group = userPrivateMessages.groupFilter;
}
performAndRefresh(params);
},
},
{
label: "topics.bulk.move_messages_to_inbox",
icon: "folder",
class: "btn-default bulk-actions__move-messages-to-inbox",
visible: ({ topics }) => topics.some((t) => t.isPrivateMessage),
action: ({ performAndRefresh }) => {
const userPrivateMessages = getOwner(this).lookup(
"controller:user-private-messages"
);
let params = { type: "move_messages_to_inbox" };
if (userPrivateMessages.isGroup) {
params.group = userPrivateMessages.groupFilter;
}
performAndRefresh(params);
},
},
{
label: "topics.bulk.notification_level",
icon: "d-regular",
class: "btn-default bulk-actions__notification-level",
action({ setComponent }) {
setComponent(NotificationLevel);
},
},
{
label: "topics.bulk.defer",
icon: "circle",
class: "btn-default bulk-actions__defer",
visible: ({ currentUser }) => currentUser.user_option.enable_defer,
action({ performAndRefresh }) {
performAndRefresh({ type: "destroy_post_timing" });
},
},
{
label: "topics.bulk.unlist_topics",
icon: "far-eye-slash",
class: "btn-default bulk-actions__unlist",
visible: ({ topics }) =>
topics.some((t) => t.visible) &&
!topics.some((t) => t.isPrivateMessage),
action({ forEachPerformed }) {
forEachPerformed({ type: "unlist" }, (t) => t.set("visible", false));
},
},
{
label: "topics.bulk.relist_topics",
icon: "far-eye",
class: "btn-default bulk-actions__relist",
visible: ({ topics }) =>
topics.some((t) => !t.visible) &&
!topics.some((t) => t.isPrivateMessage),
action({ forEachPerformed }) {
forEachPerformed({ type: "relist" }, (t) => t.set("visible", true));
},
},
{
label: "topics.bulk.reset_bump_dates",
icon: "anchor",
class: "btn-default bulk-actions__reset-bump-dates",
visible: ({ currentUser }) => currentUser.canManageTopic,
action({ performAndRefresh }) {
performAndRefresh({ type: "reset_bump_dates" });
},
},
{
label: "topics.bulk.change_tags",
icon: "tag",
class: "btn-default bulk-actions__change-tags",
visible: ({ currentUser, siteSettings }) =>
siteSettings.tagging_enabled && currentUser.canManageTopic,
action({ setComponent }) {
setComponent(ChangeTags);
},
},
{
label: "topics.bulk.append_tags",
icon: "tag",
class: "btn-default bulk-actions__append-tags",
visible: ({ currentUser, siteSettings }) =>
siteSettings.tagging_enabled && currentUser.canManageTopic,
action({ setComponent }) {
setComponent(AppendTags);
},
},
{
label: "topics.bulk.remove_tags",
icon: "tag",
class: "btn-default bulk-actions__remove-tags",
visible: ({ currentUser, siteSettings }) =>
siteSettings.tagging_enabled && currentUser.canManageTopic,
action: ({ performAndRefresh, topics }) => {
this.dialog.deleteConfirm({
message: I18n.t("topics.bulk.confirm_remove_tags", {
count: topics.length,
}),
didConfirm: () => performAndRefresh({ type: "remove_tags" }),
});
},
},
{
label: "topics.bulk.delete",
icon: "trash-alt",
class: "btn-danger delete-topics bulk-actions__delete",
visible: ({ currentUser }) => currentUser.staff,
action({ performAndRefresh }) {
performAndRefresh({ type: "delete" });
},
},
];
constructor() {
super(...arguments);
if (this.args.model.initialAction === "set-component") {
this.setComponent(this.args.model.initialComponent);
}
}
get buttons() {
return [...this.defaultButtons, ..._customButtons].filter(({ visible }) => {
if (visible) {
return visible({
topics: this.args.model.topics,
category: this.args.model.category,
currentUser: this.currentUser,
siteSettings: this.siteSettings,
});
} else {
return true;
}
});
}
async perform(operation) {
this.loading = true;
if (this.args.model.topics.length > 20) {
this.showProgress = true;
}
try {
return this._processChunks(operation);
} catch {
this.dialog.alert(I18n.t("generic_error"));
} finally {
this.loading = false;
this.processedTopicCount = 0;
this.showProgress = false;
}
}
_generateTopicChunks(allTopics) {
let startIndex = 0;
const chunkSize = 30;
const chunks = [];
while (startIndex < allTopics.length) {
const topics = allTopics.slice(startIndex, startIndex + chunkSize);
chunks.push(topics);
startIndex += chunkSize;
}
return chunks;
}
_processChunks(operation) {
const allTopics = this.args.model.topics;
const topicChunks = this._generateTopicChunks(allTopics);
const topicIds = [];
const tasks = topicChunks.map((topics) => async () => {
const result = await Topic.bulkOperation(topics, operation);
this.processedTopicCount = this.processedTopicCount + topics.length;
return result;
});
return new Promise((resolve, reject) => {
const resolveNextTask = async () => {
if (tasks.length === 0) {
const topics = topicIds.map((id) => allTopics.findBy("id", id));
return resolve(topics);
}
const task = tasks.shift();
try {
const result = await task();
if (result?.topic_ids) {
topicIds.push(...result.topic_ids);
}
resolveNextTask();
} catch {
reject();
}
};
resolveNextTask();
});
}
@action
setComponent(component) {
this.activeComponent = component;
}
@action
async forEachPerformed(operation, cb) {
const topics = await this.perform(operation);
if (topics) {
topics.forEach(cb);
this.args.model.refreshClosure?.();
this.args.closeModal();
}
}
@action
async performAndRefresh(operation) {
await this.perform(operation);
this.args.model.refreshClosure?.();
this.args.closeModal();
}
}

View File

@ -15,7 +15,6 @@
listTitle=this.listTitle
bulkSelectEnabled=this.bulkSelectEnabled
bulkSelectHelper=this.bulkSelectHelper
experimentalTopicBulkActionsEnabled=this.experimentalTopicBulkActionsEnabled
canDoBulkActions=this.canDoBulkActions
showTopicsAndRepliesToggle=this.showTopicsAndRepliesToggle
newListSubset=this.newListSubset

View File

@ -5,7 +5,6 @@ import { on } from "@ember/object/evented";
import { service } from "@ember/service";
import LoadMore from "discourse/mixins/load-more";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import TopicBulkActions from "./modal/topic-bulk-actions";
export default Component.extend(LoadMore, {
modal: service(),
@ -50,11 +49,6 @@ export default Component.extend(LoadMore, {
);
},
@discourseComputed
experimentalTopicBulkActionsEnabled() {
return this.currentUser?.use_experimental_topic_bulk_actions;
},
@discourseComputed
sortable() {
return !!this.changeSort;
@ -196,16 +190,6 @@ export default Component.extend(LoadMore, {
this.rerender();
});
onClick("button.bulk-select-actions", () => {
this.modal.show(TopicBulkActions, {
model: {
topics: this.bulkSelectHelper.selected,
category: this.category,
refreshClosure: () => this.router.refresh(),
},
});
});
onClick("button.topics-replies-toggle", (element) => {
if (element.classList.contains("--all")) {
this.changeNewListSubset(null);

View File

@ -32,10 +32,6 @@ export default class TopicList extends Component {
return !this.bulkSelectEnabled && this.args.canBulkSelect;
}
get experimentalTopicBulkActionsEnabled() {
return this.currentUser?.use_experimental_topic_bulk_actions;
}
get sortable() {
return !!this.args.changeSort;
}
@ -118,7 +114,6 @@ export default class TopicList extends Component {
@listTitle={{or @listTitle "topic.title"}}
@bulkSelectEnabled={{this.bulkSelectEnabled}}
@bulkSelectHelper={{@bulkSelectHelper}}
@experimentalTopicBulkActionsEnabled={{this.experimentalTopicBulkActionsEnabled}}
@canDoBulkActions={{this.canDoBulkActions}}
@showTopicsAndRepliesToggle={{@showTopicsAndRepliesToggle}}
@newListSubset={{@newListSubset}}

View File

@ -2,7 +2,6 @@ import Component from "@glimmer/component";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
import TopicBulkActions from "discourse/components/modal/topic-bulk-actions";
import NewListHeaderControls from "discourse/components/topic-list/new-list-header-controls";
import TopicBulkSelectDropdown from "discourse/components/topic-list/topic-bulk-select-dropdown";
import concatClass from "discourse/helpers/concat-class";
@ -48,17 +47,6 @@ export default class TopicListHeaderColumn extends Component {
.forEach((el) => el.click());
}
@action
bulkSelectActions() {
this.modal.show(TopicBulkActions, {
model: {
topics: this.args.bulkSelectHelper.selected,
category: this.category,
refreshClosure: () => this.router.refresh(),
},
});
}
@action
onClick() {
this.args.changeSort(this.args.order);
@ -100,24 +88,17 @@ export default class TopicListHeaderColumn extends Component {
title={{i18n "topics.bulk.toggle"}}
class="btn-flat bulk-select"
>
{{icon (if @experimentalTopicBulkActionsEnabled "tasks" "list")}}
{{icon "tasks"}}
</button>
{{/if}}
{{#if @bulkSelectEnabled}}
<span class="bulk-select-topics">
{{#if @canDoBulkActions}}
{{#if @experimentalTopicBulkActionsEnabled}}
<TopicBulkSelectDropdown
@bulkSelectHelper={{@bulkSelectHelper}}
@afterBulkActionComplete={{this.afterBulkActionComplete}}
/>
{{else}}
<button
{{on "click" this.bulkSelectActions}}
class="btn btn-icon no-text bulk-select-actions"
>{{icon "cog"}}&#8203;</button>
{{/if}}
<TopicBulkSelectDropdown
@bulkSelectHelper={{@bulkSelectHelper}}
@afterBulkActionComplete={{this.afterBulkActionComplete}}
/>
{{/if}}
<button

View File

@ -16,7 +16,7 @@ const TopicListHeader = <template>
title={{i18n "topics.bulk.toggle"}}
class="btn-flat bulk-select"
>
{{icon (if @experimentalTopicBulkActionsEnabled "tasks" "list")}}
{{icon "tasks"}}
</button>
{{/if}}
</th>
@ -37,7 +37,6 @@ const TopicListHeader = <template>
@newListSubset={{@newListSubset}}
@newRepliesCount={{@newRepliesCount}}
@newTopicsCount={{@newTopicsCount}}
@experimentalTopicBulkActionsEnabled={{@experimentalTopicBulkActionsEnabled}}
@bulkSelectHelper={{@bulkSelectHelper}}
@changeNewListSubset={{@changeNewListSubset}}
/>

View File

@ -4,7 +4,6 @@ import { gt, or } from "@ember/object/computed";
import { service } from "@ember/service";
import { isEmpty } from "@ember/utils";
import { Promise } from "rsvp";
import TopicBulkActions from "discourse/components/modal/topic-bulk-actions";
import { ajax } from "discourse/lib/ajax";
import BulkSelectHelper from "discourse/lib/bulk-select-helper";
import { search as searchCategoryTag } from "discourse/lib/category-tag-search";
@ -232,11 +231,6 @@ export default Controller.extend({
}
},
@discourseComputed("currentUser.use_experimental_topic_bulk_actions")
useNewBulkActions() {
return this.currentUser?.use_experimental_topic_bulk_actions;
},
@discourseComputed("q")
showLikeCount(q) {
return q?.includes("order:likes");
@ -549,15 +543,6 @@ export default Controller.extend({
this.bulkSelectHelper.selected.clear();
},
showBulkActions() {
this.modal.show(TopicBulkActions, {
model: {
topics: this.bulkSelectHelper.selected,
refreshClosure: this._search,
},
});
},
search(options = {}) {
if (this.searching) {
return;

View File

@ -14,7 +14,6 @@ import { forceDropdownForMenuPanels as glimmerForceDropdownForMenuPanels } from
import { addGlobalNotice } from "discourse/components/global-notice";
import { headerButtonsDAG } from "discourse/components/header";
import { headerIconsDAG } from "discourse/components/header/icons";
import { _addBulkButton } from "discourse/components/modal/topic-bulk-actions";
import MountWidget, {
addWidgetCleanCallback,
} from "discourse/components/mount-widget";
@ -3036,7 +3035,6 @@ class PluginApi {
* @param {string} opts.actionType - type of the action, either performanAndRefresh or setComponent
*/
addBulkActionButton(opts) {
_addBulkButton(opts);
addBulkDropdownButton(opts);
}

View File

@ -1,20 +1,12 @@
<th data-sort-order='{{order}}' class='{{view.className}} topic-list-data' scope="col" {{#if view.ariaSort}}aria-sort='{{view.ariaSort}}'{{/if}}>
{{~#if canBulkSelect}}
{{~#if showBulkToggle}}
{{~#if experimentalTopicBulkActionsEnabled }}
{{raw "flat-button" class="bulk-select" icon="tasks" title="topics.bulk.toggle"}}
{{else}}
{{raw "flat-button" class="bulk-select" icon="list" title="topics.bulk.toggle"}}
{{/if ~}}
{{raw "flat-button" class="bulk-select" icon="tasks" title="topics.bulk.toggle"}}
{{/if ~}}
{{~#if bulkSelectEnabled}}
<span class='bulk-select-topics'>
{{~#if canDoBulkActions}}
{{~#if experimentalTopicBulkActionsEnabled }}
{{raw "topic-bulk-select-dropdown" bulkSelectHelper=bulkSelectHelper}}
{{else}}
<button class='btn btn-icon no-text bulk-select-actions'>{{d-icon "cog"}}&#8203;</button>
{{/if ~}}
{{raw "topic-bulk-select-dropdown" bulkSelectHelper=bulkSelectHelper}}
{{/if ~}}
<button class='btn btn-default bulk-select-all'>{{i18n "topics.bulk.select_all"}}</button>
<button class='btn btn-default bulk-clear-all'>{{i18n "topics.bulk.clear_all"}}</button>

View File

@ -2,15 +2,11 @@
{{#if bulkSelectEnabled}}
<th class="bulk-select topic-list-data">
{{#if canBulkSelect}}
{{#if experimentalTopicBulkActionsEnabled }}
{{raw "flat-button" class="bulk-select" icon="tasks" title="topics.bulk.toggle"}}
{{else}}
{{raw "flat-button" class="bulk-select" icon="list" title="topics.bulk.toggle"}}
{{/if}}
{{raw "flat-button" class="bulk-select" icon="tasks" title="topics.bulk.toggle"}}
{{/if}}
</th>
{{/if}}
{{raw "topic-list-header-column" order='default' name=listTitle bulkSelectEnabled=bulkSelectEnabled showBulkToggle=toggleInTitle canBulkSelect=canBulkSelect canDoBulkActions=canDoBulkActions showTopicsAndRepliesToggle=showTopicsAndRepliesToggle newListSubset=newListSubset newRepliesCount=newRepliesCount newTopicsCount=newTopicsCount experimentalTopicBulkActionsEnabled=experimentalTopicBulkActionsEnabled bulkSelectHelper=bulkSelectHelper }}
{{raw "topic-list-header-column" order='default' name=listTitle bulkSelectEnabled=bulkSelectEnabled showBulkToggle=toggleInTitle canBulkSelect=canBulkSelect canDoBulkActions=canDoBulkActions showTopicsAndRepliesToggle=showTopicsAndRepliesToggle newListSubset=newListSubset newRepliesCount=newRepliesCount newTopicsCount=newTopicsCount bulkSelectHelper=bulkSelectHelper }}
{{raw-plugin-outlet name="topic-list-header-after-main-link"}}
{{#if showPosters}}
{{raw "topic-list-header-column" name='posters' screenreaderOnly='true'}}

View File

@ -110,19 +110,10 @@
class="btn-default bulk-select"
/>
{{#if this.bulkSelectHelper.selected}}
{{#if this.useNewBulkActions}}
<TopicList::TopicBulkSelectDropdown
@bulkSelectHelper={{this.bulkSelectHelper}}
@afterBulkActionComplete={{this.afterBulkActionComplete}}
/>
{{else}}
<DButton
@selected={{this.bulkSelectHelper.selected}}
@action={{action "showBulkActions"}}
@icon="wrench"
class="btn-default bulk-select-btn"
/>
{{/if}}
<TopicList::TopicBulkSelectDropdown
@bulkSelectHelper={{this.bulkSelectHelper}}
@afterBulkActionComplete={{this.afterBulkActionComplete}}
/>
{{/if}}
{{/if}}

View File

@ -1,130 +0,0 @@
import { click, visit } from "@ember/test-helpers";
import { test } from "qunit";
import {
acceptance,
query,
queryAll,
updateCurrentUser,
} from "discourse/tests/helpers/qunit-helpers";
import I18n from "discourse-i18n";
acceptance("Topic - Bulk Actions - Mobile", function (needs) {
needs.user();
needs.mobileView();
needs.settings({ tagging_enabled: true });
needs.pretender((server, helper) => {
server.put("/topics/bulk", () => {
return helper.response({
topic_ids: [],
});
});
});
test("bulk select - modal", async function (assert) {
updateCurrentUser({ moderator: true, user_option: { enable_defer: true } });
await visit("/latest");
await click("button.bulk-select");
await click(queryAll("input.bulk-select")[0]);
await click(queryAll("input.bulk-select")[1]);
await click(".bulk-select-actions");
assert.ok(
query("#discourse-modal-title").innerHTML.includes(
I18n.t("topics.bulk.actions")
),
"it opens bulk-select modal"
);
assert.ok(
query(".bulk-buttons").innerHTML.includes(
I18n.t("topics.bulk.change_category")
),
"it shows an option to change category"
);
assert.ok(
query(".bulk-buttons").innerHTML.includes(
I18n.t("topics.bulk.close_topics")
),
"it shows an option to close topics"
);
assert.ok(
query(".bulk-buttons").innerHTML.includes(
I18n.t("topics.bulk.archive_topics")
),
"it shows an option to archive topics"
);
assert.ok(
query(".bulk-buttons").innerHTML.includes(
I18n.t("topics.bulk.notification_level")
),
"it shows an option to update notification level"
);
assert.ok(
query(".bulk-buttons").innerHTML.includes(I18n.t("topics.bulk.defer")),
"it shows an option to reset read"
);
assert.ok(
query(".bulk-buttons").innerHTML.includes(
I18n.t("topics.bulk.unlist_topics")
),
"it shows an option to unlist topics"
);
assert.ok(
query(".bulk-buttons").innerHTML.includes(
I18n.t("topics.bulk.reset_bump_dates")
),
"it shows an option to reset bump dates"
);
assert.ok(
query(".bulk-buttons").innerHTML.includes(
I18n.t("topics.bulk.change_tags")
),
"it shows an option to replace tags"
);
assert.ok(
query(".bulk-buttons").innerHTML.includes(
I18n.t("topics.bulk.append_tags")
),
"it shows an option to append tags"
);
assert.ok(
query(".bulk-buttons").innerHTML.includes(
I18n.t("topics.bulk.remove_tags")
),
"it shows an option to remove all tags"
);
assert.ok(
query(".bulk-buttons").innerHTML.includes(I18n.t("topics.bulk.delete")),
"it shows an option to delete topics"
);
});
test("bulk select - delete topics", async function (assert) {
updateCurrentUser({ moderator: true });
await visit("/latest");
await click("button.bulk-select");
await click(queryAll("input.bulk-select")[0]);
await click(queryAll("input.bulk-select")[1]);
await click(".bulk-select-actions");
await click(".d-modal__body .delete-topics");
assert
.dom(".topic-bulk-actions-modal")
.doesNotExist("it closes the bulk select modal");
});
});

View File

@ -8,7 +8,6 @@ import {
import { acceptance, selectDate } from "discourse/tests/helpers/qunit-helpers";
import selectKit from "discourse/tests/helpers/select-kit-helper";
let lastBody;
let searchResultClickTracked = false;
acceptance("Search - Full Page", function (needs) {
@ -87,11 +86,6 @@ acceptance("Search - Full Page", function (needs) {
});
});
server.put("/topics/bulk", (request) => {
lastBody = helper.parsePostData(request.requestBody);
return helper.response({ topic_ids: [130] });
});
server.post("/search/click", () => {
searchResultClickTracked = true;
return helper.response({ success: "OK" });
@ -541,17 +535,6 @@ acceptance("Search - Full Page", function (needs) {
.isVisible("clicking on element expands filters");
});
test("bulk operations work", async function (assert) {
await visit("/search");
await fillIn(".search-query", "discourse");
await click(".search-cta");
await click(".bulk-select"); // toggle bulk
await click(".bulk-select-visible .btn:nth-child(2)"); // select all
await click(".bulk-select-btn"); // show bulk actions
await click(".topic-bulk-actions-modal .btn.bulk-actions__close-topics");
assert.deepEqual(lastBody["topic_ids[]"], ["130"]);
});
test("adds visited class to visited topics", async function (assert) {
await visit("/search");

View File

@ -1,265 +0,0 @@
import { click, triggerEvent, visit } from "@ember/test-helpers";
import { test } from "qunit";
import {
acceptance,
count,
queryAll,
updateCurrentUser,
} from "discourse/tests/helpers/qunit-helpers";
import I18n from "discourse-i18n";
acceptance("Topic - Bulk Actions", function (needs) {
needs.user();
needs.settings({ tagging_enabled: true });
needs.pretender((server, helper) => {
server.put("/topics/bulk", () => {
return helper.response({
topic_ids: [],
});
});
});
test("bulk select - modal", async function (assert) {
updateCurrentUser({
moderator: true,
user_option: { enable_defer: true },
});
await visit("/latest");
await click("button.bulk-select");
await click(queryAll("input.bulk-select")[0]);
await click(queryAll("input.bulk-select")[1]);
await click(".bulk-select-actions");
assert
.dom("#discourse-modal-title")
.hasText(I18n.t("topics.bulk.actions"), "it opens bulk-select modal");
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.change_category"),
"it shows an option to change category"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.close_topics"),
"it shows an option to close topics"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.archive_topics"),
"it shows an option to archive topics"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.notification_level"),
"it shows an option to update notification level"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.defer"),
"it shows an option to reset read"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.unlist_topics"),
"it shows an option to unlist topics"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.reset_bump_dates"),
"it shows an option to reset bump dates"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.change_tags"),
"it shows an option to replace tags"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.append_tags"),
"it shows an option to append tags"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.remove_tags"),
"it shows an option to remove all tags"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.delete"),
"it shows an option to delete topics"
);
});
test("bulk select - delete topics", async function (assert) {
updateCurrentUser({ moderator: true });
await visit("/latest");
await click("button.bulk-select");
await click(queryAll("input.bulk-select")[0]);
await click(queryAll("input.bulk-select")[1]);
await click(".bulk-select-actions");
await click(".d-modal__body .delete-topics");
assert
.dom(".topic-bulk-actions-modal")
.doesNotExist("it closes the bulk select modal");
});
test("bulk select - Shift click selection", async function (assert) {
updateCurrentUser({ moderator: true });
await visit("/latest");
await click("button.bulk-select");
await click(queryAll("input.bulk-select")[0]);
await triggerEvent(queryAll("input.bulk-select")[3], "click", {
shiftKey: true,
});
assert.strictEqual(
count("input.bulk-select:checked"),
4,
"Shift click selects a range"
);
await click("button.bulk-clear-all");
await click(queryAll("input.bulk-select")[5]);
await triggerEvent(queryAll("input.bulk-select")[1], "click", {
shiftKey: true,
});
assert.strictEqual(
count("input.bulk-select:checked"),
5,
"Bottom-up Shift click range selection works"
);
});
test("bulk select is not available for users who are not staff or TL4", async function (assert) {
updateCurrentUser({ moderator: false, admin: false, trust_level: 1 });
await visit("/latest");
assert
.dom(".button.bulk-select")
.doesNotExist("non-staff and < TL4 users cannot bulk select");
});
test("TL4 users can bulk select", async function (assert) {
updateCurrentUser({
moderator: false,
admin: false,
trust_level: 4,
user_option: { enable_defer: false },
});
await visit("/latest");
await click("button.bulk-select");
await click(queryAll("input.bulk-select")[0]);
await click(queryAll("input.bulk-select")[1]);
await click(".bulk-select-actions");
assert
.dom("#discourse-modal-title")
.hasText(I18n.t("topics.bulk.actions"), "it opens bulk-select modal");
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.change_category"),
"it shows an option to change category"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.close_topics"),
"it shows an option to close topics"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.archive_topics"),
"it shows an option to archive topics"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.notification_level"),
"it shows an option to update notification level"
);
assert
.dom(".bulk-buttons")
.doesNotIncludeText(
I18n.t("topics.bulk.defer"),
"it does not show an option to reset read"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.unlist_topics"),
"it shows an option to unlist topics"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.reset_bump_dates"),
"it shows an option to reset bump dates"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.change_tags"),
"it shows an option to replace tags"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.append_tags"),
"it shows an option to append tags"
);
assert
.dom(".bulk-buttons")
.includesText(
I18n.t("topics.bulk.remove_tags"),
"it shows an option to remove all tags"
);
assert
.dom(".bulk-buttons")
.doesNotIncludeText(
I18n.t("topics.bulk.delete"),
"it does not show an option to delete topics"
);
});
});

View File

@ -22,9 +22,8 @@ acceptance("User Activity / Read - bulk actions", function (needs) {
await click("button.bulk-select");
await click(queryAll("input.bulk-select")[0]);
await click(queryAll("input.bulk-select")[1]);
await click("button.bulk-select-actions");
await click("div.bulk-buttons button.bulk-actions__close-topics");
await click(".bulk-select-topics-dropdown-trigger");
await click(".dropdown-menu__item .close-topics");
assert
.dom("div.bulk-buttons")

View File

@ -26,9 +26,8 @@ acceptance("User Activity / Topics - bulk actions", function (needs) {
await click("button.bulk-select");
await click(queryAll("input.bulk-select")[0]);
await click(queryAll("input.bulk-select")[1]);
await click("button.bulk-select-actions");
await click("div.bulk-buttons button:nth-child(2)"); // the Close Topics button
await click(".bulk-select-topics-dropdown-trigger");
await click(".dropdown-menu__item .close-topics");
assert.notOk(
exists("div.bulk-buttons"),

View File

@ -20,7 +20,6 @@ import {
import { clearToolbarCallbacks } from "discourse/components/d-editor";
import { clearExtraHeaderButtons as clearExtraGlimmerHeaderButtons } from "discourse/components/header";
import { clearExtraHeaderIcons as clearExtraGlimmerHeaderIcons } from "discourse/components/header/icons";
import { clearBulkButtons } from "discourse/components/modal/topic-bulk-actions";
import { resetWidgetCleanCallbacks } from "discourse/components/mount-widget";
import { resetDecorators as resetPluginOutletDecorators } from "discourse/components/plugin-connector";
import { resetItemSelectCallbacks } from "discourse/components/search-menu/results/assistant-item";
@ -244,7 +243,6 @@ export function testCleanup(container, app) {
resetMentions();
cleanupTemporaryModuleRegistrations();
cleanupCssGeneratorTags();
clearBulkButtons();
resetBeforeAuthCompleteCallbacks();
clearPopupMenuOptions();
clearAdditionalAdminSidebarSectionLinks();

View File

@ -72,7 +72,6 @@ class CurrentUserSerializer < BasicUserSerializer
:sidebar_category_ids,
:sidebar_sections,
:new_new_view_enabled?,
:use_experimental_topic_bulk_actions?,
:use_admin_sidebar,
:can_view_raw_email,
:use_glimmer_topic_list?,
@ -323,10 +322,6 @@ class CurrentUserSerializer < BasicUserSerializer
Reviewable.unseen_reviewable_count(object)
end
def use_experimental_topic_bulk_actions?
scope.user.in_any_groups?(SiteSetting.experimental_topic_bulk_actions_enabled_groups_map)
end
def can_view_raw_email
scope.user.in_any_groups?(SiteSetting.view_raw_email_allowed_groups_map)
end

View File

@ -2679,7 +2679,6 @@ en:
experimental_form_templates: "EXPERIMENTAL: Enable the form templates feature. <b>After enabled,</b> manage the templates at <a href='%{base_path}/admin/customize/form-templates'>Customize / Templates</a>."
admin_sidebar_enabled_groups: "Enable sidebar navigation for the admin UI for the specified groups, which replaces the top-level admin navigation buttons."
lazy_load_categories_groups: "EXPERIMENTAL: Lazy load category information only for users of these groups. This improves performance on sites with many categories."
experimental_topic_bulk_actions_enabled_groups: "EXPERIMENTAL: Enable the new bulk actions dropdown."
page_loading_indicator: "Configure the loading indicator which appears during page navigations within Discourse. 'Spinner' is a full page indicator. 'Slider' shows a narrow bar at the top of the screen."
show_user_menu_avatars: "Show user avatars in the user menu"

View File

@ -2425,12 +2425,6 @@ developer:
default: ""
client: true
hidden: true
experimental_topic_bulk_actions_enabled_groups:
default: ""
type: group_list
list_type: compact
allow_any: false
refresh: true
experimental_flags_admin_page_enabled_groups:
default: ""
type: group_list

View File

@ -143,51 +143,22 @@ describe "Search", type: :system do
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
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
end

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
describe "Topic bulk select", type: :system do
before { SiteSetting.experimental_topic_bulk_actions_enabled_groups = "1" }
fab!(:topics) { Fabricate.times(10, :post).map(&:topic) }
fab!(:admin)
fab!(:user)