From 0b413e2aa141a6948ab569b8027df724e1e1c78d Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Tue, 23 Jul 2024 11:39:27 +1000 Subject: [PATCH] FEATURE: Use new topic bulk actions menu for all sites (#28003) This commit promotes the new topic bulk action menu introduced in 89883b2f51d2c484a3d8371060171d03d35bc978 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. --- .../components/modal/topic-bulk-actions.hbs | 44 --- .../components/modal/topic-bulk-actions.js | 312 ------------------ .../discourse/app/components/topic-list.hbs | 1 - .../discourse/app/components/topic-list.js | 16 - .../app/components/topic-list/list.gjs | 5 - .../topic-list/topic-list-header-column.gjs | 29 +- .../topic-list/topic-list-header.gjs | 3 +- .../app/controllers/full-page-search.js | 15 - .../discourse/app/lib/plugin-api.gjs | 2 - .../topic-list-header-column.hbr | 12 +- .../app/raw-templates/topic-list-header.hbr | 8 +- .../app/templates/full-page-search.hbs | 17 +- .../mobile-topic-bulk-actions-test.js | 130 -------- .../tests/acceptance/search-full-test.js | 17 - .../acceptance/topic-bulk-actions-test.js | 265 --------------- .../acceptance/user-activity-read-test.js | 5 +- .../acceptance/user-activity-topic-test.js | 5 +- .../discourse/tests/helpers/qunit-helpers.js | 2 - app/serializers/current_user_serializer.rb | 5 - config/locales/server.en.yml | 1 - config/site_settings.yml | 6 - spec/system/search_spec.rb | 61 +--- spec/system/topic_bulk_select_spec.rb | 1 - 23 files changed, 34 insertions(+), 928 deletions(-) delete mode 100644 app/assets/javascripts/discourse/app/components/modal/topic-bulk-actions.hbs delete mode 100644 app/assets/javascripts/discourse/app/components/modal/topic-bulk-actions.js delete mode 100644 app/assets/javascripts/discourse/tests/acceptance/mobile-topic-bulk-actions-test.js delete mode 100644 app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js diff --git a/app/assets/javascripts/discourse/app/components/modal/topic-bulk-actions.hbs b/app/assets/javascripts/discourse/app/components/modal/topic-bulk-actions.hbs deleted file mode 100644 index 3077ed6860a..00000000000 --- a/app/assets/javascripts/discourse/app/components/modal/topic-bulk-actions.hbs +++ /dev/null @@ -1,44 +0,0 @@ - -

- {{html-safe (i18n "topics.bulk.selected" count=@model.topics.length)}} -

- - {{#if this.showProgress}} -

- {{html-safe (i18n "topics.bulk.progress" count=this.processedTopicCount)}} -

- {{else if this.activeComponent}} - - {{else}} -
- {{#each this.buttons as |button|}} - - {{/each}} -
- {{/if}} -
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/modal/topic-bulk-actions.js b/app/assets/javascripts/discourse/app/components/modal/topic-bulk-actions.js deleted file mode 100644 index 457bc07107c..00000000000 --- a/app/assets/javascripts/discourse/app/components/modal/topic-bulk-actions.js +++ /dev/null @@ -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(); - } -} diff --git a/app/assets/javascripts/discourse/app/components/topic-list.hbs b/app/assets/javascripts/discourse/app/components/topic-list.hbs index 9f5f54cdbc1..ae89d49c9d2 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list.hbs +++ b/app/assets/javascripts/discourse/app/components/topic-list.hbs @@ -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 diff --git a/app/assets/javascripts/discourse/app/components/topic-list.js b/app/assets/javascripts/discourse/app/components/topic-list.js index 1f5432c1f27..09ddc2db3f2 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list.js +++ b/app/assets/javascripts/discourse/app/components/topic-list.js @@ -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); diff --git a/app/assets/javascripts/discourse/app/components/topic-list/list.gjs b/app/assets/javascripts/discourse/app/components/topic-list/list.gjs index fe71c3d97c6..fa0ac5e52c9 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list/list.gjs +++ b/app/assets/javascripts/discourse/app/components/topic-list/list.gjs @@ -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}} diff --git a/app/assets/javascripts/discourse/app/components/topic-list/topic-list-header-column.gjs b/app/assets/javascripts/discourse/app/components/topic-list/topic-list-header-column.gjs index 66838dcee3f..bd9bacfd4c6 100644 --- a/app/assets/javascripts/discourse/app/components/topic-list/topic-list-header-column.gjs +++ b/app/assets/javascripts/discourse/app/components/topic-list/topic-list-header-column.gjs @@ -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"}} {{/if}} {{#if @bulkSelectEnabled}} {{#if @canDoBulkActions}} - {{#if @experimentalTopicBulkActionsEnabled}} - - {{else}} - - {{/if}} + {{/if}} {{/if}} @@ -37,7 +37,6 @@ const TopicListHeader =