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