FEATURE: Batch process topic bulk actions (#10980)

FEATURE: Batch process topic bulk actions (#10980)

Topics are processed in chunks of 30 in order to prevent timeouts.

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 4d88bec..b863206 100644
--- a/app/assets/javascripts/discourse/app/controllers/topic-bulk-actions.js
+++ b/app/assets/javascripts/discourse/app/controllers/topic-bulk-actions.js
@@ -5,6 +5,7 @@ import ModalFunctionality from "discourse/mixins/modal-functionality";
 import Topic from "discourse/models/topic";
 import Category from "discourse/models/category";
 import bootbox from "bootbox";
+import { Promise } from "rsvp";
 
 const _buttons = [];
 
@@ -84,6 +85,7 @@ export default Controller.extend(ModalFunctionality, {
 
   emptyTags: empty("tags"),
   categoryId: alias("model.category.id"),
+  processedTopicCount: 0,
 
   onShow() {
     const topics = this.get("model.topics");
@@ -101,23 +103,70 @@ export default Controller.extend(ModalFunctionality, {
   },
 
   perform(operation) {
+    this.set("processedTopicCount", 0);
+    this.send("changeBulkTemplate", "modal/bulk-progress");
     this.set("loading", true);
 
-    const topics = this.get("model.topics");
-    return Topic.bulkOperation(topics, operation)
-      .then((result) => {
-        this.set("loading", false);
-        if (result && result.topic_ids) {
-          return result.topic_ids.map((t) => topics.findBy("id", t));
-        }
-        return result;
-      })
+    return this._processChunks(operation)
       .catch(() => {
         bootbox.alert(I18n.t("generic_error"));
+      })
+      .finally(() => {
         this.set("loading", false);
       });
   },
 
+  _generateTopicChunks(allTopics) {
+    let startIndex = 0;
+    const chunkSize = 30;
+    const chunks = [];
+
+    while (startIndex < allTopics.length) {
+      let topics = allTopics.slice(startIndex, startIndex + chunkSize);
+      chunks.push(topics);
+      startIndex += chunkSize;
+    }
+
+    return chunks;
+  },
+
+  _processChunks(operation) {
+    const allTopics = this.get("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;
+      });
+    });
+
+    return new Promise((resolve, reject) => {
+      const resolveNextTask = () => {
+        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);
+      };
+
+      resolveNextTask();
+    });
+  },
+
   forEachPerformed(operation, cb) {
     this.perform(operation).then((topics) => {
       if (topics) {
diff --git a/app/assets/javascripts/discourse/app/templates/modal/bulk-progress.hbs b/app/assets/javascripts/discourse/app/templates/modal/bulk-progress.hbs
new file mode 100644
index 0000000..8fb41be
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/templates/modal/bulk-progress.hbs
@@ -0,0 +1 @@
+<p>{{html-safe (i18n "topics.bulk.progress" count=processedTopicCount)}}</p>
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 3f30571..6a44c0d 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -2199,6 +2199,9 @@ en:
         choose_append_tags: "Choose new tags to append for these topics:"
         changed_tags: "The tags of those topics were changed."
         remove_tags: "Remove Tags"
+        progress:
+          one: "Progress: <strong>%{count}</strong> topic"
+          other: "Progress: <strong>%{count}</strong> topics"
 
       none:
         unread: "You have no unread topics."

GitHub sha: cc74c3f9

This commit appears in #10980 which was approved by SamSaffron. It was merged by SamSaffron.