diff --git a/app/assets/javascripts/discourse/app/controllers/flag.js b/app/assets/javascripts/discourse/app/controllers/flag.js index b2b81d60614..9ee9f050123 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 4a37ee0337c..3b2284ccbac 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 00000000000..95ba0726368 --- /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(); + assert.ok(exists(".flag-modal-body"), "it shows the flag modal"); +}); + +test("Flag take action dropdown exists", async (assert) => { + await visit("/t/internationalization-localization/280"); + await openFlagModal(); + await click("#radio_inappropriate"); + await selectKit(".reviewable-action-dropdown").expand(); + assert.ok( + exists("[data-value='agree_and_silence']"), + "it shows the silence action option" + ); + await click("[data-value='agree_and_silence']"); + assert.ok(exists(".silence-user-modal"), "it shows the silence modal"); +}); + +test("Can silence from take action", async (assert) => { + await visit("/t/internationalization-localization/280"); + await openFlagModal(); + await click("#radio_inappropriate"); + await selectKit(".reviewable-action-dropdown").expand(); + await click("[data-value='agree_and_silence']"); + + const silenceUntilCombobox = selectKit(".silence-until .combobox"); + await silenceUntilCombobox.expand(); + await silenceUntilCombobox.selectRowByValue("tomorrow"); + await fillIn(".silence-reason", "for breaking the rules"); + await click(".perform-silence"); + assert.equal(find(".bootbox.modal:visible").length, 0); +}); + +test("Gets dismissable warning from canceling incomplete silence from take action", async (assert) => { + await visit("/t/internationalization-localization/280"); + await openFlagModal(); + await click("#radio_inappropriate"); + await selectKit(".reviewable-action-dropdown").expand(); + await click("[data-value='agree_and_silence']"); + + const silenceUntilCombobox = selectKit(".silence-until .combobox"); + await silenceUntilCombobox.expand(); + await silenceUntilCombobox.selectRowByValue("tomorrow"); + await fillIn(".silence-reason", "for breaking the rules"); + await click(".d-modal-cancel"); + assert.equal(find(".bootbox.modal:visible").length, 1); + + await click(".modal-footer .btn-default"); + assert.equal(find(".bootbox.modal:visible").length, 0); + assert.ok(exists(".silence-user-modal"), "it shows the silence modal"); + + await click(".d-modal-cancel"); + assert.equal(find(".bootbox.modal:visible").length, 1); + + await click(".modal-footer .btn-primary"); + assert.equal(find(".bootbox.modal:visible").length, 0); +}); diff --git a/app/assets/stylesheets/common/base/reviewables.scss b/app/assets/stylesheets/common/base/reviewables.scss index 2069e5152f8..81ba13d8ae2 100644 --- a/app/assets/stylesheets/common/base/reviewables.scss +++ b/app/assets/stylesheets/common/base/reviewables.scss @@ -490,6 +490,10 @@ } } +.flag-modal .modal-inner-container .select-kit.reviewable-action-dropdown { + width: initial; +} + @media screen and (max-width: 1000px) { table.reviewable-scores { width: 100%; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 95cad878750..0f5ed522050 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2997,7 +2997,17 @@ en: flagging: title: "Thanks for helping to keep our community civil!" action: "Flag Post" - take_action: "Take Action" + take_action: "Take Action..." + take_action_options: + default: + title: "Take Action" + details: "Reach the flag threshold immediately, rather than waiting for more community flags" + suspend: + title: "Suspend User" + details: "Reach the flag threshold, and suspend the user" + silence: + title: "Silence User" + details: "Reach the flag threshold, and silence the user" notify_action: "Message" official_warning: "Official Warning" delete_spammer: "Delete Spammer"