diff --git a/app/assets/javascripts/discourse/app/components/bulk-actions/append-tags.hbs b/app/assets/javascripts/discourse/app/components/bulk-actions/append-tags.hbs new file mode 100644 index 00000000000..581c7ded898 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/bulk-actions/append-tags.hbs @@ -0,0 +1,9 @@ +

{{i18n "topics.bulk.choose_append_tags"}}

+ +

+ + \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/bulk-actions/append-tags.js b/app/assets/javascripts/discourse/app/components/bulk-actions/append-tags.js new file mode 100644 index 00000000000..6b66d271026 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/bulk-actions/append-tags.js @@ -0,0 +1,6 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; + +export default class AppendTags extends Component { + @tracked tags = []; +} diff --git a/app/assets/javascripts/discourse/app/components/bulk-actions/change-category.hbs b/app/assets/javascripts/discourse/app/components/bulk-actions/change-category.hbs new file mode 100644 index 00000000000..5c68851acf4 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/bulk-actions/change-category.hbs @@ -0,0 +1,15 @@ +

{{i18n "topics.bulk.choose_new_category"}}

+ +

+ +

+ + + + \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/bulk-actions/change-category.js b/app/assets/javascripts/discourse/app/components/bulk-actions/change-category.js new file mode 100644 index 00000000000..61c10614f4e --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/bulk-actions/change-category.js @@ -0,0 +1,17 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; + +export default class ChangeCategory extends Component { + categoryId = 0; + + @action + async changeCategory() { + await this.args.forEachPerformed( + { + type: "change_category", + category_id: this.categoryId, + }, + (t) => t.set("category_id", this.categoryId) + ); + } +} diff --git a/app/assets/javascripts/discourse/app/components/bulk-actions/change-tags.hbs b/app/assets/javascripts/discourse/app/components/bulk-actions/change-tags.hbs new file mode 100644 index 00000000000..4e46d9ff96a --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/bulk-actions/change-tags.hbs @@ -0,0 +1,9 @@ +

{{i18n "topics.bulk.choose_new_tags"}}

+ +

+ + \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/bulk-actions/change-tags.js b/app/assets/javascripts/discourse/app/components/bulk-actions/change-tags.js new file mode 100644 index 00000000000..eb80adf4565 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/bulk-actions/change-tags.js @@ -0,0 +1,6 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; + +export default class ChangeTags extends Component { + @tracked tags = []; +} diff --git a/app/assets/javascripts/discourse/app/templates/modal/bulk-notification-level.hbs b/app/assets/javascripts/discourse/app/components/bulk-actions/notification-level.hbs similarity index 91% rename from app/assets/javascripts/discourse/app/templates/modal/bulk-notification-level.hbs rename to app/assets/javascripts/discourse/app/components/bulk-actions/notification-level.hbs index 89d8aa146f9..e9ff958df96 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/bulk-notification-level.hbs +++ b/app/assets/javascripts/discourse/app/components/bulk-actions/notification-level.hbs @@ -16,6 +16,6 @@ \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/bulk-actions/notification-level.js b/app/assets/javascripts/discourse/app/components/bulk-actions/notification-level.js new file mode 100644 index 00000000000..9a8dc0e0868 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/bulk-actions/notification-level.js @@ -0,0 +1,28 @@ +import Component from "@glimmer/component"; +import I18n from "I18n"; +import { empty } from "@ember/object/computed"; +import { topicLevels } from "discourse/lib/notification-levels"; +import { action } from "@ember/object"; + +// Support for changing the notification level of various topics +export default class NotificationLevel extends Component { + notificationLevelId = null; + + @empty("notificationLevelId") disabled; + + get notificationLevels() { + return topicLevels.map((level) => ({ + id: level.id.toString(), + name: I18n.t(`topic.notifications.${level.key}.title`), + description: I18n.t(`topic.notifications.${level.key}.description`), + })); + } + + @action + changeNotificationLevel() { + this.args.performAndRefresh({ + type: "change_notification_level", + notification_level_id: this.notificationLevelId, + }); + } +} diff --git a/app/assets/javascripts/discourse/app/controllers/bulk-notification-level.js b/app/assets/javascripts/discourse/app/controllers/bulk-notification-level.js deleted file mode 100644 index 0a8655986fb..00000000000 --- a/app/assets/javascripts/discourse/app/controllers/bulk-notification-level.js +++ /dev/null @@ -1,33 +0,0 @@ -import Controller, { inject as controller } from "@ember/controller"; -import I18n from "I18n"; -import discourseComputed from "discourse-common/utils/decorators"; -import { empty } from "@ember/object/computed"; -import { topicLevels } from "discourse/lib/notification-levels"; - -// Support for changing the notification level of various topics -export default Controller.extend({ - topicBulkActions: controller(), - notificationLevelId: null, - - @discourseComputed - notificationLevels() { - return topicLevels.map((level) => { - return { - id: level.id.toString(), - name: I18n.t(`topic.notifications.${level.key}.title`), - description: I18n.t(`topic.notifications.${level.key}.description`), - }; - }); - }, - - disabled: empty("notificationLevelId"), - - actions: { - changeNotificationLevel() { - this.topicBulkActions.performAndRefresh({ - type: "change_notification_level", - notification_level_id: this.notificationLevelId, - }); - }, - }, -}); diff --git a/app/assets/javascripts/discourse/app/controllers/topic-bulk-actions.js b/app/assets/javascripts/discourse/app/controllers/topic-bulk-actions.js index 9bbc1558904..71ca054057a 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic-bulk-actions.js +++ b/app/assets/javascripts/discourse/app/controllers/topic-bulk-actions.js @@ -1,163 +1,231 @@ -import { alias, empty } from "@ember/object/computed"; import Controller, { inject as controller } from "@ember/controller"; +import { tracked } from "@glimmer/tracking"; +import { inject as service } from "@ember/service"; +import { action } from "@ember/object"; import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { Promise } from "rsvp"; import Topic from "discourse/models/topic"; +import ChangeCategory from "../components/bulk-actions/change-category"; +import NotificationLevel from "../components/bulk-actions/notification-level"; +import ChangeTags from "../components/bulk-actions/change-tags"; +import AppendTags from "../components/bulk-actions/append-tags"; -import { inject as service } from "@ember/service"; +const _customButtons = []; -const _buttons = []; - -const alwaysTrue = () => true; - -function identity() {} - -function addBulkButton(action, key, opts) { - opts = opts || {}; - - const btn = { - action, - label: `topics.bulk.${key}`, +export function _addBulkButton(opts) { + _customButtons.push({ + label: opts.label, icon: opts.icon, - buttonVisible: opts.buttonVisible || alwaysTrue, - enabledSetting: opts.enabledSetting, class: opts.class, - }; - - _buttons.push(btn); + visible: opts.visible, + action: opts.action, + }); } -// Default buttons -addBulkButton("showChangeCategory", "change_category", { - icon: "pencil-alt", - class: "btn-default", - buttonVisible: (topics) => !topics.some((t) => t.isPrivateMessage), -}); -addBulkButton("closeTopics", "close_topics", { - icon: "lock", - class: "btn-default", - buttonVisible: (topics) => !topics.some((t) => t.isPrivateMessage), -}); -addBulkButton("archiveTopics", "archive_topics", { - icon: "folder", - class: "btn-default", - buttonVisible: (topics) => !topics.some((t) => t.isPrivateMessage), -}); -addBulkButton("archiveMessages", "archive_topics", { - icon: "folder", - class: "btn-default", - buttonVisible: (topics) => topics.some((t) => t.isPrivateMessage), -}); -addBulkButton("moveMessagesToInbox", "move_messages_to_inbox", { - icon: "folder", - class: "btn-default", - buttonVisible: (topics) => topics.some((t) => t.isPrivateMessage), -}); -addBulkButton("showNotificationLevel", "notification_level", { - icon: "d-regular", - class: "btn-default", -}); -addBulkButton("deletePostTiming", "defer", { - icon: "circle", - class: "btn-default", - buttonVisible() { - return this.currentUser.user_option.enable_defer; - }, -}); -addBulkButton("unlistTopics", "unlist_topics", { - icon: "far-eye-slash", - class: "btn-default", - buttonVisible: (topics) => - topics.some((t) => t.visible) && !topics.some((t) => t.isPrivateMessage), -}); -addBulkButton("relistTopics", "relist_topics", { - icon: "far-eye", - class: "btn-default", - buttonVisible: (topics) => - topics.some((t) => !t.visible) && !topics.some((t) => t.isPrivateMessage), -}); -addBulkButton("resetBumpDateTopics", "reset_bump_dates", { - icon: "anchor", - class: "btn-default", - buttonVisible() { - return this.currentUser.canManageTopic; - }, -}); -addBulkButton("showTagTopics", "change_tags", { - icon: "tag", - class: "btn-default", - enabledSetting: "tagging_enabled", - buttonVisible() { - return this.currentUser.canManageTopic; - }, -}); -addBulkButton("showAppendTagTopics", "append_tags", { - icon: "tag", - class: "btn-default", - enabledSetting: "tagging_enabled", - buttonVisible() { - return this.currentUser.canManageTopic; - }, -}); -addBulkButton("removeTags", "remove_tags", { - icon: "tag", - class: "btn-default", - enabledSetting: "tagging_enabled", - buttonVisible() { - return this.currentUser.canManageTopic; - }, -}); -addBulkButton("deleteTopics", "delete", { - icon: "trash-alt", - class: "btn-danger delete-topics", - buttonVisible() { - return this.currentUser.staff; - }, -}); +export function clearBulkButtons() { + _customButtons.length = 0; +} // Modal for performing bulk actions on topics -export default Controller.extend(ModalFunctionality, { - userPrivateMessages: controller("user-private-messages"), - dialog: service(), - tags: null, - emptyTags: empty("tags"), - categoryId: alias("model.category.id"), - processedTopicCount: 0, - isGroup: alias("userPrivateMessages.isGroup"), - groupFilter: alias("userPrivateMessages.groupFilter"), +export default class TopicBulkActions extends Controller.extend( + ModalFunctionality +) { + @service currentUser; + @service siteSettings; + @service dialog; + @controller("user-private-messages") userPrivateMessages; + + @tracked loading = false; + @tracked showProgress = false; + @tracked processedTopicCount = 0; + @tracked activeComponent = null; + + defaultButtons = [ + { + label: "topics.bulk.change_category", + icon: "pencil-alt", + class: "btn-default", + visible: ({ topics }) => !topics.some((t) => t.isPrivateMessage), + action({ setComponent }) { + setComponent(ChangeCategory); + }, + }, + { + label: "topics.bulk.close_topics", + icon: "lock", + class: "btn-default", + 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", + 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", + visible: ({ topics }) => topics.some((t) => t.isPrivateMessage), + action: ({ performAndRefresh }) => { + let params = { type: "archive_messages" }; + if (this.userPrivateMessages.isGroup) { + params.group = this.userPrivateMessages.groupFilter; + } + performAndRefresh(params); + }, + }, + { + label: "topics.bulk.move_messages_to_inbox", + icon: "folder", + class: "btn-default", + visible: ({ topics }) => topics.some((t) => t.isPrivateMessage), + action: ({ performAndRefresh }) => { + let params = { type: "move_messages_to_inbox" }; + if (this.userPrivateMessages.isGroup) { + params.group = this.userPrivateMessages.groupFilter; + } + performAndRefresh(params); + }, + }, + { + label: "topics.bulk.notification_level", + icon: "d-regular", + class: "btn-default", + action({ setComponent }) { + setComponent(NotificationLevel); + }, + }, + { + label: "topics.bulk.defer", + icon: "circle", + class: "btn-default", + 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", + 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", + 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", + visible: ({ currentUser }) => currentUser.canManageTopic, + action({ performAndRefresh }) { + performAndRefresh({ type: "reset_bump_dates" }); + }, + }, + { + label: "topics.bulk.change_tags", + icon: "tag", + class: "btn-default", + visible: ({ currentUser, siteSettings }) => + siteSettings.tagging_enabled && currentUser.canManageTopic, + action({ setComponent }) { + setComponent(ChangeTags); + }, + }, + { + label: "topics.bulk.append_tags", + icon: "tag", + class: "btn-default", + visible: ({ currentUser, siteSettings }) => + siteSettings.tagging_enabled && currentUser.canManageTopic, + action({ setComponent }) { + setComponent(AppendTags); + }, + }, + { + label: "topics.bulk.remove_tags", + icon: "tag", + class: "btn-default", + 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", + visible: ({ currentUser }) => currentUser.staff, + action({ performAndRefresh }) { + performAndRefresh({ type: "delete" }); + }, + }, + ]; + + get buttons() { + return [...this.defaultButtons, ..._customButtons].filter(({ visible }) => { + if (visible) { + return visible({ + topics: this.model.topics, + category: this.model.category, + currentUser: this.currentUser, + siteSettings: this.siteSettings, + }); + } else { + return true; + } + }); + } onShow() { - const topics = this.get("model.topics"); - this.set( - "buttons", - _buttons.filter((b) => { - if (b.enabledSetting && !this.siteSettings[b.enabledSetting]) { - return false; - } - return b.buttonVisible.call(this, topics); - }) - ); - this.set("modal.modalClass", "topic-bulk-actions-modal small"); - this.send("changeBulkTemplate", "modal/bulk-actions-buttons"); - }, + this.modal.set("modalClass", "topic-bulk-actions-modal small"); + this.activeComponent = null; + } - perform(operation) { - this.set("processedTopicCount", 0); - if (this.get("model.topics").length > 20) { - this.send("changeBulkTemplate", "modal/bulk-progress"); + async perform(operation) { + this.loading = true; + + if (this.model.topics.length > 20) { + this.showProgress = true; } - this.set("loading", true); - - return this._processChunks(operation) - .catch(() => { - this.dialog.alert(I18n.t("generic_error")); - }) - .finally(() => { - this.set("loading", false); - }); - }, + 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; @@ -165,168 +233,70 @@ export default Controller.extend(ModalFunctionality, { const chunks = []; while (startIndex < allTopics.length) { - let topics = allTopics.slice(startIndex, startIndex + chunkSize); + const topics = allTopics.slice(startIndex, startIndex + chunkSize); chunks.push(topics); startIndex += chunkSize; } return chunks; - }, + } _processChunks(operation) { - const allTopics = this.get("model.topics"); + const allTopics = this.model.topics; const topicChunks = this._generateTopicChunks(allTopics); const topicIds = []; - const tasks = topicChunks.map((topics) => () => { - return Topic.bulkOperation(topics, operation).then((result) => { - this.set( - "processedTopicCount", - this.get("processedTopicCount") + topics.length - ); - return result; - }); + 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 = () => { + const resolveNextTask = async () => { if (tasks.length === 0) { const topics = topicIds.map((id) => allTopics.findBy("id", id)); return resolve(topics); } - tasks - .shift()() - .then((result) => { - if (result && result.topic_ids) { - topicIds.push(...result.topic_ids); - } - resolveNextTask(); - }) - .catch(reject); + const task = tasks.shift(); + + try { + const result = await task(); + if (result?.topic_ids) { + topicIds.push(...result.topic_ids); + } + resolveNextTask(); + } catch { + reject(); + } }; resolveNextTask(); }); - }, + } - forEachPerformed(operation, cb) { - this.perform(operation).then((topics) => { - if (topics) { - topics.forEach(cb); - (this.refreshClosure || identity)(); - this.send("closeModal"); - } - }); - }, + @action + setComponent(component) { + this.activeComponent = component; + } - performAndRefresh(operation) { - return this.perform(operation).then(() => { - (this.refreshClosure || identity)(); + @action + async forEachPerformed(operation, cb) { + const topics = await this.perform(operation); + + if (topics) { + topics.forEach(cb); + this.refreshClosure?.(); this.send("closeModal"); - }); - }, + } + } - actions: { - showTagTopics() { - this.set("tags", ""); - this.set("action", "changeTags"); - this.set("label", "change_tags"); - this.set("title", "choose_new_tags"); - this.send("changeBulkTemplate", "bulk-tag"); - }, + @action + async performAndRefresh(operation) { + await this.perform(operation); - changeTags() { - this.performAndRefresh({ type: "change_tags", tags: this.tags }); - }, - - showAppendTagTopics() { - this.set("tags", null); - this.set("action", "appendTags"); - this.set("label", "append_tags"); - this.set("title", "choose_append_tags"); - this.send("changeBulkTemplate", "bulk-tag"); - }, - - appendTags() { - this.performAndRefresh({ type: "append_tags", tags: this.tags }); - }, - - showChangeCategory() { - this.send("changeBulkTemplate", "modal/bulk-change-category"); - }, - - showNotificationLevel() { - this.send("changeBulkTemplate", "modal/bulk-notification-level"); - }, - - deleteTopics() { - this.performAndRefresh({ type: "delete" }); - }, - - closeTopics() { - this.forEachPerformed({ type: "close" }, (t) => t.set("closed", true)); - }, - - archiveTopics() { - this.forEachPerformed({ type: "archive" }, (t) => - t.set("archived", true) - ); - }, - - archiveMessages() { - let params = { type: "archive_messages" }; - if (this.isGroup) { - params.group = this.groupFilter; - } - this.performAndRefresh(params); - }, - - moveMessagesToInbox() { - let params = { type: "move_messages_to_inbox" }; - if (this.isGroup) { - params.group = this.groupFilter; - } - this.performAndRefresh(params); - }, - - unlistTopics() { - this.forEachPerformed({ type: "unlist" }, (t) => t.set("visible", false)); - }, - - relistTopics() { - this.forEachPerformed({ type: "relist" }, (t) => t.set("visible", true)); - }, - - resetBumpDateTopics() { - this.performAndRefresh({ type: "reset_bump_dates" }); - }, - - changeCategory() { - const categoryId = parseInt(this.newCategoryId, 10) || 0; - - this.perform({ type: "change_category", category_id: categoryId }).then( - (topics) => { - topics.forEach((t) => t.set("category_id", categoryId)); - (this.refreshClosure || identity)(); - this.send("closeModal"); - } - ); - }, - - deletePostTiming() { - this.performAndRefresh({ type: "destroy_post_timing" }); - }, - - removeTags() { - this.dialog.deleteConfirm({ - message: I18n.t("topics.bulk.confirm_remove_tags", { - count: this.get("model.topics").length, - }), - didConfirm: () => this.performAndRefresh({ type: "remove_tags" }), - }); - }, - }, -}); - -export { addBulkButton }; + this.refreshClosure?.(); + this.send("closeModal"); + } +} diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index fdf6e50149a..77152443ff0 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -120,12 +120,13 @@ import { registerModelTransformer } from "discourse/lib/model-transformers"; import { registerCustomUserNavMessagesDropdownRow } from "discourse/controllers/user-private-messages"; import { registerFullPageSearchType } from "discourse/controllers/full-page-search"; import { registerHashtagType } from "discourse/lib/hashtag-autocomplete"; +import { _addBulkButton } from "discourse/controllers/topic-bulk-actions"; // If you add any methods to the API ensure you bump up the version number // based on Semantic Versioning 2.0.0. Please update the changelog at // docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version // using the format described at https://keepachangelog.com/en/1.0.0/. -export const PLUGIN_API_VERSION = "1.7.0"; +export const PLUGIN_API_VERSION = "1.7.1"; // This helper prevents us from applying the same `modifyClass` over and over in test mode. function canModify(klass, type, resolverName, changes) { @@ -1962,15 +1963,15 @@ class PluginApi { * }) * ``` * - * @params {Object} arg - An object - * @params {string} arg.categoryId - The id of the category - * @params {string} arg.prefixType - The type of prefix to use. Can be "icon", "image", "text" or "span". - * @params {string} arg.prefixValue - The value of the prefix to use. + * @param {Object} arg - An object + * @param {string} arg.categoryId - The id of the category + * @param {string} arg.prefixType - The type of prefix to use. Can be "icon", "image", "text" or "span". + * @param {string} arg.prefixValue - The value of the prefix to use. * For "icon", pass in the name of a FontAwesome 5 icon. * For "image", pass in the src of the image. * For "text", pass in the text to display. * For "span", pass in an array containing two hex color values. Example: `[FF0000, 000000]`. - * @params {string} arg.prefixColor - The color of the prefix to use. Example: "FF0000". + * @param {string} arg.prefixColor - The color of the prefix to use. Example: "FF0000". */ registerCustomCategorySectionLinkPrefix({ categoryId, @@ -2000,10 +2001,10 @@ class PluginApi { * }); * ``` * - * @params {Object} arg - An object - * @params {string} arg.tagName - The name of the tag - * @params {string} arg.prefixValue - The name of a FontAwesome 5 icon. - * @params {string} arg.prefixColor - The color represented using hexadecimal to use for the prefix. Example: "#FF0000" or "#FFF". + * @param {Object} arg - An object + * @param {string} arg.tagName - The name of the tag + * @param {string} arg.prefixValue - The name of a FontAwesome 5 icon. + * @param {string} arg.prefixColor - The color represented using hexadecimal to use for the prefix. Example: "#FF0000" or "#FFF". */ registerCustomTagSectionLinkPrefixIcon({ tagName, @@ -2279,6 +2280,49 @@ class PluginApi { registerHashtagType(type, typeClassInstance) { registerHashtagType(type, typeClassInstance); } + + /** + * Adds a button to the bulk topic actions modal. + * + * ``` + * api.addBulkActionButton({ + * label: "super_plugin.bulk.enhance", + * icon: "magic", + * class: "btn-default", + * visible: ({ currentUser, siteSettings }) => siteSettings.super_plugin_enabled && currentUser.staff, + * async action({ setComponent }) { + * await doSomething(this.model.topics); + * setComponent(MyBulkModal); + * }, + * }); + * ``` + * + * @callback buttonVisibilityCallback + * @param {Object} opts + * @param {Topic[]} opts.topics - the selected topic for the bulk action + * @param {Category} opts.category - the category in which the action is performed (if applicable) + * @param {User} opts.currentUser + * @param {SiteSettings} opts.siteSettings + * @returns {Boolean} - whether the button should be visible or not + * + * @callback buttonAction + * @param {Object} opts + * @param {Topic[]} opts.topics - the selected topic for the bulk action + * @param {Category} opts.category - the category in which the action is performed (if applicable) + * @param {function} opts.setComponent - render a template in the bulk action modal (pass in an imported component) + * @param {function} opts.performAndRefresh + * @param {function} opts.forEachPerformed + * + * @param {Object} opts + * @param {string} opts.label + * @param {string} opts.icon + * @param {string} opts.class + * @param {buttonVisibilityCallback} opts.visible + * @param {buttonAction} opts.action + */ + addBulkActionButton(opts) { + _addBulkButton(opts); + } } // from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number diff --git a/app/assets/javascripts/discourse/app/templates/bulk-tag.hbs b/app/assets/javascripts/discourse/app/templates/bulk-tag.hbs deleted file mode 100644 index e3f75763906..00000000000 --- a/app/assets/javascripts/discourse/app/templates/bulk-tag.hbs +++ /dev/null @@ -1,9 +0,0 @@ -

{{i18n (concat "topics.bulk." this.title)}}

- -

- - \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/templates/modal/bulk-change-category.hbs b/app/assets/javascripts/discourse/app/templates/modal/bulk-change-category.hbs deleted file mode 100644 index 1469e90da43..00000000000 --- a/app/assets/javascripts/discourse/app/templates/modal/bulk-change-category.hbs +++ /dev/null @@ -1,15 +0,0 @@ -

{{i18n "topics.bulk.choose_new_category"}}

- -

- -

- - - - \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/templates/modal/bulk-progress.hbs b/app/assets/javascripts/discourse/app/templates/modal/bulk-progress.hbs deleted file mode 100644 index 69480bd60fc..00000000000 --- a/app/assets/javascripts/discourse/app/templates/modal/bulk-progress.hbs +++ /dev/null @@ -1,3 +0,0 @@ -

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

\ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/templates/modal/topic-bulk-actions.hbs b/app/assets/javascripts/discourse/app/templates/modal/topic-bulk-actions.hbs index 95d449a02b8..8f56ff8e103 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/topic-bulk-actions.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/topic-bulk-actions.hbs @@ -1,6 +1,40 @@ -

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

- {{outlet "bulkOutlet"}} +

+ {{html-safe (i18n "topics.bulk.selected" count=this.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/tests/acceptance/topic-bulk-actions-test.js b/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js index 0193efa0007..a92174220ea 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/topic-bulk-actions-test.js @@ -1,9 +1,7 @@ import { acceptance, count, - exists, invisible, - query, queryAll, updateCurrentUser, } from "discourse/tests/helpers/qunit-helpers"; @@ -35,85 +33,86 @@ acceptance("Topic - Bulk Actions", function (needs) { await click(".bulk-select-actions"); - assert.ok( - query("#discourse-modal-title").innerHTML.includes( - I18n.t("topics.bulk.actions") - ), - "it opens bulk-select modal" - ); + assert + .dom("#discourse-modal-title") + .hasText(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 + .dom(".bulk-buttons") + .includesText( + 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 + .dom(".bulk-buttons") + .includesText( + 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 + .dom(".bulk-buttons") + .includesText( + 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 + .dom(".bulk-buttons") + .includesText( + 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 + .dom(".bulk-buttons") + .includesText( + 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 + .dom(".bulk-buttons") + .includesText( + 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 + .dom(".bulk-buttons") + .includesText( + 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 + .dom(".bulk-buttons") + .includesText( + 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 + .dom(".bulk-buttons") + .includesText( + 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 + .dom(".bulk-buttons") + .includesText( + 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" - ); + 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) { @@ -127,7 +126,7 @@ acceptance("Topic - Bulk Actions", function (needs) { await click(".bulk-select-actions"); await click(".modal-body .delete-topics"); - assert.ok( + assert.true( invisible(".topic-bulk-actions-modal"), "it closes the bulk select modal" ); @@ -164,10 +163,9 @@ acceptance("Topic - Bulk Actions", function (needs) { 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.notOk( - exists(".button.bulk-select"), - "non-staff and < TL4 users cannot bulk select" - ); + assert + .dom(".button.bulk-select") + .doesNotExist("non-staff and < TL4 users cannot bulk select"); }); test("TL4 users can bulk select", async function (assert) { @@ -183,86 +181,87 @@ acceptance("Topic - Bulk Actions", function (needs) { 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 + .dom("#discourse-modal-title") + .hasText(I18n.t("topics.bulk.actions"), "it opens bulk-select modal"); - assert.ok( - query(".bulk-buttons").innerHTML.includes( - I18n.t("topics.bulk.close_topics") - ), - "it shows an option to close topics" - ); + assert + .dom(".bulk-buttons") + .includesText( + I18n.t("topics.bulk.change_category"), + "it shows an option to change category" + ); - assert.ok( - query(".bulk-buttons").innerHTML.includes( - I18n.t("topics.bulk.archive_topics") - ), - "it shows an option to archive topics" - ); + assert + .dom(".bulk-buttons") + .includesText( + I18n.t("topics.bulk.close_topics"), + "it shows an option to close topics" + ); - assert.ok( - query(".bulk-buttons").innerHTML.includes( - I18n.t("topics.bulk.notification_level") - ), - "it shows an option to update notification level" - ); + assert + .dom(".bulk-buttons") + .includesText( + I18n.t("topics.bulk.archive_topics"), + "it shows an option to archive topics" + ); - assert.notOk( - query(".bulk-buttons").innerHTML.includes(I18n.t("topics.bulk.defer")), - "it does not show an option to reset read" - ); + assert + .dom(".bulk-buttons") + .includesText( + 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.unlist_topics") - ), - "it shows an option to unlist topics" - ); + assert + .dom(".bulk-buttons") + .doesNotIncludeText( + I18n.t("topics.bulk.defer"), + "it does not show an option to reset read" + ); - assert.ok( - query(".bulk-buttons").innerHTML.includes( - I18n.t("topics.bulk.reset_bump_dates") - ), - "it shows an option to reset bump dates" - ); + assert + .dom(".bulk-buttons") + .includesText( + I18n.t("topics.bulk.unlist_topics"), + "it shows an option to unlist topics" + ); - assert.ok( - query(".bulk-buttons").innerHTML.includes( - I18n.t("topics.bulk.change_tags") - ), - "it shows an option to replace tags" - ); + assert + .dom(".bulk-buttons") + .includesText( + 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.append_tags") - ), - "it shows an option to append tags" - ); + assert + .dom(".bulk-buttons") + .includesText( + I18n.t("topics.bulk.change_tags"), + "it shows an option to replace tags" + ); - assert.ok( - query(".bulk-buttons").innerHTML.includes( - I18n.t("topics.bulk.remove_tags") - ), - "it shows an option to remove all tags" - ); + assert + .dom(".bulk-buttons") + .includesText( + I18n.t("topics.bulk.append_tags"), + "it shows an option to append tags" + ); - assert.notOk( - query(".bulk-buttons").innerHTML.includes(I18n.t("topics.bulk.delete")), - "it does not show an option to delete topics" - ); + 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" + ); }); }); diff --git a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js index a4ee666da72..732b0e3eef9 100644 --- a/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js +++ b/app/assets/javascripts/discourse/tests/helpers/qunit-helpers.js @@ -87,6 +87,7 @@ import { reset as resetLinkLookup } from "discourse/lib/link-lookup"; import { resetMentions } from "discourse/lib/link-mentions"; import { resetModelTransformers } from "discourse/lib/model-transformers"; import { cleanupTemporaryModuleRegistrations } from "./temporary-module-helper"; +import { clearBulkButtons } from "discourse/controllers/topic-bulk-actions"; export function currentUser() { return User.create(sessionFixtures["/session/current.json"].current_user); @@ -223,6 +224,7 @@ export function testCleanup(container, app) { resetMentions(); cleanupTemporaryModuleRegistrations(); cleanupCssGeneratorTags(); + clearBulkButtons(); } function cleanupCssGeneratorTags() { diff --git a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md index 68bfa888a97..700cf004bb6 100644 --- a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md +++ b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md @@ -7,6 +7,12 @@ in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.7.1] - 2023-07-18 + +### Added + +- Adds `addBulkActionButton` which adds actions to the Bulk Topic modal + ## [1.7.0] - 2023-07-17 ### Added