FEATURE: add penalty options for take action (#10926)

FEATURE: add penalty options for take action (#10926)

  • FEATURE: add penalty options for take action

Add the ability to silence or suspend users from the “take action” button when moderators are flagging posts. This allows for a more streamlined active moderation workflow, when moderating against a topic directly.

diff --git a/app/assets/javascripts/discourse/app/controllers/flag.js b/app/assets/javascripts/discourse/app/controllers/flag.js
index b2b81d6..9ee9f05 100644
--- a/app/assets/javascripts/discourse/app/controllers/flag.js
+++ b/app/assets/javascripts/discourse/app/controllers/flag.js
@@ -7,6 +7,9 @@ import ActionSummary from "discourse/models/action-summary";
 import { MAX_MESSAGE_LENGTH } from "discourse/models/post-action-type";
 import optionalService from "discourse/lib/optional-service";
 import { popupAjaxError } from "discourse/lib/ajax-error";
+import I18n from "I18n";
+import User from "discourse/models/user";
+import { Promise } from "rsvp";
 
 export default Controller.extend(ModalFunctionality, {
   adminTools: optionalService(),
@@ -17,6 +20,58 @@ export default Controller.extend(ModalFunctionality, {
   isWarning: false,
   topicActionByName: null,
   spammerDetails: null,
+  flagActions: null,
+
+  init() {
+    this._super(...arguments);
+    this.flagActions = {
+      icon: "gavel",
+      label: I18n.t("flagging.take_action"),
+      actions: [
+        {
+          id: "agree_and_keep",
+          icon: "thumbs-up",
+          label: I18n.t("flagging.take_action_options.default.title"),
+          description: I18n.t("flagging.take_action_options.default.details"),
+        },
+        {
+          id: "agree_and_suspend",
+          icon: "ban",
+          label: I18n.t("flagging.take_action_options.suspend.title"),
+          description: I18n.t("flagging.take_action_options.suspend.details"),
+          client_action: "suspend",
+        },
+        {
+          id: "agree_and_silence",
+          icon: "microphone-slash",
+          label: I18n.t("flagging.take_action_options.silence.title"),
+          description: I18n.t("flagging.take_action_options.silence.details"),
+          client_action: "silence",
+        },
+      ],
+    };
+  },
+
+  clientSuspend(performAction) {
+    this._penalize("showSuspendModal", performAction);
+  },
+
+  clientSilence(performAction) {
+    this._penalize("showSilenceModal", performAction);
+  },
+
+  async _penalize(adminToolMethod, performAction) {
+    if (this.adminTools) {
+      let createdBy = await User.findByUsername(this.model.username);
+      let postId = this.model.id;
+      let postEdit = this.model.cooked;
+      return this.adminTools[adminToolMethod](createdBy, {
+        postId,
+        postEdit,
+        before: performAction,
+      });
+    }
+  },
 
   onShow() {
     this.setProperties({
@@ -24,9 +79,8 @@ export default Controller.extend(ModalFunctionality, {
       spammerDetails: null,
     });
 
-    let adminTools = this.adminTools;
-    if (adminTools) {
-      adminTools.checkSpammer(this.get("model.user_id")).then((result) => {
+    if (this.adminTools) {
+      this.adminTools.checkSpammer(this.get("model.user_id")).then((result) => {
         this.set("spammerDetails", result);
       });
     }
@@ -133,9 +187,28 @@ export default Controller.extend(ModalFunctionality, {
       }
     },
 
-    takeAction() {
-      this.send("createFlag", { takeAction: true });
-      this.set("model.hidden", true);
+    takeAction(action) {
+      let performAction = (o = {}) => {
+        o.takeAction = true;
+        this.send("createFlag", o);
+        return Promise.resolve();
+      };
+
+      if (action.client_action) {
+        let actionMethod = this[`client${action.client_action.classify()}`];
+        if (actionMethod) {
+          return actionMethod.call(this, () =>
+            performAction({ skipClose: true })
+          );
+        } else {
+          // eslint-disable-next-line no-console
+          console.error(`No handler for ${action.client_action} found`);
+          return;
+        }
+      } else {
+        this.set("model.hidden", true);
+        return performAction();
+      }
     },
 
     createFlag(opts) {
@@ -171,7 +244,9 @@ export default Controller.extend(ModalFunctionality, {
       postAction
         .act(this.model, params)
         .then(() => {
-          this.send("closeModal");
+          if (!opts.skipClose) {
+            this.send("closeModal");
+          }
           if (params.message) {
             this.set("message", "");
           }
diff --git a/app/assets/javascripts/discourse/app/templates/modal/flag.hbs b/app/assets/javascripts/discourse/app/templates/modal/flag.hbs
index 4a37ee0..3b2284c 100644
--- a/app/assets/javascripts/discourse/app/templates/modal/flag.hbs
+++ b/app/assets/javascripts/discourse/app/templates/modal/flag.hbs
@@ -35,13 +35,10 @@
   {{/if}}
 
   {{#if canTakeAction}}
-    {{d-button
-      class="btn-danger"
-      action=(action "takeAction")
-      disabled=submitDisabled
-      title="flagging.take_action_tooltip"
-      icon="gavel"
-      label="flagging.take_action"
+    {{reviewable-bundled-action
+        bundle=flagActions
+        performAction=(action "takeAction")
+        reviewableUpdating=submitDisabled
     }}
   {{/if}}
 
diff --git a/app/assets/javascripts/discourse/tests/acceptance/flag-post-test.js b/app/assets/javascripts/discourse/tests/acceptance/flag-post-test.js
new file mode 100644
index 0000000..95ba072
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/acceptance/flag-post-test.js
@@ -0,0 +1,144 @@
+import { test } from "qunit";
+import selectKit from "discourse/tests/helpers/select-kit-helper";
+import { acceptance } from "discourse/tests/helpers/qunit-helpers";
+import userFixtures from "discourse/tests/fixtures/user-fixtures";
+
+acceptance("flagging", {
+  loggedIn: true,
+  afterEach() {
+    sandbox.restore();
+  },
+  pretend(pretenderServer, helper) {
+    const userResponse = Object.assign({}, userFixtures["/u/charlie.json"]);
+    pretenderServer.get("/u/uwe_keim.json", () => {
+      return helper.response(userResponse);
+    });
+    pretenderServer.get("/admin/users/255.json", () => {
+      return helper.response({
+        id: 255,
+        automatic: false,
+        name: "admin",
+        username: "admin",
+        user_count: 0,
+        alias_level: 99,
+        visible: true,
+        automatic_membership_email_domains: "",
+        primary_group: false,
+        title: null,
+        grant_trust_level: null,
+        has_messages: false,
+        flair_url: null,
+        flair_bg_color: null,
+        flair_color: null,
+        bio_raw: null,
+        bio_cooked: null,
+        public_admission: false,
+        allow_membership_requests: true,
+        membership_request_template: "Please add me",
+        full_name: null,
+      });
+    });
+    pretenderServer.get("/admin/users/5.json", () => {
+      return helper.response({
+        id: 5,
+        automatic: false,
+        name: "user",
+        username: "user",
+        user_count: 0,
+        alias_level: 99,
+        visible: true,
+        automatic_membership_email_domains: "",
+        primary_group: false,
+        title: null,
+        grant_trust_level: null,
+        has_messages: false,
+        flair_url: null,
+        flair_bg_color: null,
+        flair_color: null,
+        bio_raw: null,
+        bio_cooked: null,
+        public_admission: false,
+        allow_membership_requests: true,
+        membership_request_template: "Please add me",
+        full_name: null,
+      });
+    });
+    pretenderServer.put("admin/users/5/silence", () => {
+      return helper.response({
+        silenced: true,
+      });
+    });
+    pretenderServer.post("post_actions", () => {
+      return helper.response({
+        response: true,
+      });
+    });
+  },
+});
+
+async function openFlagModal() {
+  if (exists(".topic-post:first-child button.show-more-actions")) {
+    await click(".topic-post:first-child button.show-more-actions");
+  }
+
+  await click(".topic-post:first-child button.create-flag");
+}
+
+test("Flag modal opening", async (assert) => {
+  await visit("/t/internationalization-localization/280");
+  await openFlagModal();

[... diff too long, it was truncated ...]

GitHub sha: d68ad82a

This commit appears in #10926 which was approved by eviltrout and jjaffeux. It was merged by featheredtoast.