+ <:body>
+
+
+
+
+
+
+
+
+
+ {{#if this.showCustomReason}}
+
+
+
+
+ {{/if}}
+
+
+
+
+
+
+ <:footer>
+
+
+
+
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/app/components/modal/revise-and-reject-post-reviewable.js b/app/assets/javascripts/discourse/app/components/modal/revise-and-reject-post-reviewable.js
new file mode 100644
index 00000000000..b0cffb5f109
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/modal/revise-and-reject-post-reviewable.js
@@ -0,0 +1,62 @@
+import Component from "@glimmer/component";
+import { tracked } from "@glimmer/tracking";
+import { action } from "@ember/object";
+import { inject as service } from "@ember/service";
+import { isEmpty } from "@ember/utils";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import I18n from "I18n";
+
+const OTHER_REASON = "other_reason";
+
+export default class ReviseAndRejectPostReviewable extends Component {
+ @service siteSettings;
+
+ @tracked reason;
+ @tracked customReason;
+ @tracked feedback;
+ @tracked submitting = false;
+
+ get configuredReasons() {
+ const reasons = this.siteSettings.reviewable_revision_reasons
+ .split("|")
+ .filter(Boolean)
+ .map((reason) => ({ id: reason, name: reason }))
+ .concat([
+ {
+ id: OTHER_REASON,
+ name: I18n.t("review.revise_and_reject_post.other_reason"),
+ },
+ ]);
+ return reasons;
+ }
+
+ get showCustomReason() {
+ return this.reason === OTHER_REASON;
+ }
+
+ get sendPMDisabled() {
+ return (
+ isEmpty(this.reason) ||
+ (this.reason === OTHER_REASON && isEmpty(this.customReason)) ||
+ this.submitting
+ );
+ }
+
+ @action
+ async rejectAndSendPM() {
+ this.submitting = true;
+
+ try {
+ await this.args.model.performConfirmed(this.args.model.action, {
+ revise_reason: this.reason,
+ revise_custom_reason: this.customReason,
+ revise_feedback: this.feedback,
+ });
+ this.args.closeModal();
+ } catch (error) {
+ popupAjaxError(error);
+ } finally {
+ this.submitting = false;
+ }
+ }
+}
diff --git a/app/assets/javascripts/discourse/app/components/reviewable-item.js b/app/assets/javascripts/discourse/app/components/reviewable-item.js
index 9005a0af0fe..8050c122bcb 100644
--- a/app/assets/javascripts/discourse/app/components/reviewable-item.js
+++ b/app/assets/javascripts/discourse/app/components/reviewable-item.js
@@ -4,6 +4,7 @@ import { action, set } from "@ember/object";
import { inject as service } from "@ember/service";
import { classify, dasherize } from "@ember/string";
import ExplainReviewableModal from "discourse/components/modal/explain-reviewable";
+import ReviseAndRejectPostReviewable from "discourse/components/modal/revise-and-reject-post-reviewable";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import optionalService from "discourse/lib/optional-service";
@@ -15,7 +16,13 @@ import I18n from "I18n";
let _components = {};
const pluginReviewableParams = {};
-const actionModalClassMap = {};
+
+// The mappings defined here are default core mappings, and cannot be overridden
+// by plugins.
+const defaultActionModalClassMap = {
+ revise_and_reject_post: ReviseAndRejectPostReviewable,
+};
+const actionModalClassMap = { ...defaultActionModalClassMap };
export function addPluginReviewableParam(reviewableType, param) {
pluginReviewableParams[reviewableType]
@@ -24,6 +31,11 @@ export function addPluginReviewableParam(reviewableType, param) {
}
export function registerReviewableActionModal(actionName, modalClass) {
+ if (Object.keys(defaultActionModalClassMap).includes(actionName)) {
+ throw new Error(
+ `Cannot override default action modal class for ${actionName} (mapped to ${defaultActionModalClassMap[actionName].name})!`
+ );
+ }
actionModalClassMap[actionName] = modalClass;
}
@@ -135,7 +147,7 @@ export default Component.extend({
},
@bind
- _performConfirmed(performableAction) {
+ _performConfirmed(performableAction, additionalData = {}) {
let reviewable = this.reviewable;
let performAction = () => {
@@ -145,6 +157,7 @@ export default Component.extend({
const data = {
send_email: reviewable.sendEmail,
reject_reason: reviewable.rejectReason,
+ ...additionalData,
};
(pluginReviewableParams[reviewable.type] || []).forEach((param) => {
diff --git a/app/assets/stylesheets/common/base/_index.scss b/app/assets/stylesheets/common/base/_index.scss
index 387651c4906..a77ffbd4ce9 100644
--- a/app/assets/stylesheets/common/base/_index.scss
+++ b/app/assets/stylesheets/common/base/_index.scss
@@ -41,6 +41,7 @@
@import "request_access";
@import "request-group-membership-form";
@import "reviewables";
+@import "revise-and-reject-post-reviewable";
@import "rtl";
@import "search-menu";
@import "search";
diff --git a/app/assets/stylesheets/common/base/revise-and-reject-post-reviewable.scss b/app/assets/stylesheets/common/base/revise-and-reject-post-reviewable.scss
new file mode 100644
index 00000000000..5548e835bae
--- /dev/null
+++ b/app/assets/stylesheets/common/base/revise-and-reject-post-reviewable.scss
@@ -0,0 +1,46 @@
+.modal.revise-and-reject-reviewable {
+ .modal-inner-container {
+ max-width: 30em;
+ }
+
+ .modal-body {
+ .control-label {
+ font-weight: 700;
+ }
+
+ .select-kit {
+ width: 100%;
+ summary {
+ height: 100%;
+ }
+ }
+ }
+
+ .revise-and-reject-reviewable__optional {
+ margin-left: 0.5em;
+ color: var(--primary-low-mid);
+ }
+
+ .revise-and-reject-reviewable__custom-reason {
+ width: 100%;
+ }
+
+ .revise-and-reject-reviewable__queued-post {
+ @extend .reviewable-item;
+
+ padding: 1em;
+ margin: 0 0 1em 0;
+
+ .post-topic .title-text {
+ font-size: var(--font-up-1);
+ }
+
+ .post-body {
+ margin: 0;
+
+ p {
+ margin: 0;
+ }
+ }
+ }
+}
diff --git a/app/controllers/reviewables_controller.rb b/app/controllers/reviewables_controller.rb
index a00b65a5a85..455cb1ed554 100644
--- a/app/controllers/reviewables_controller.rb
+++ b/app/controllers/reviewables_controller.rb
@@ -222,11 +222,8 @@ class ReviewablesController < ApplicationController
return render_json_error(error)
end
- if reviewable.type == "ReviewableUser"
- args.merge!(
- reject_reason: params[:reject_reason],
- send_email: params[:send_email] != "false",
- )
+ if reviewable.type_class.respond_to?(:additional_args)
+ args.merge!(reviewable.type_class.additional_args(params) || {})
end
plugin_params =
diff --git a/app/models/reviewable.rb b/app/models/reviewable.rb
index f489156671d..1dbe09130e2 100644
--- a/app/models/reviewable.rb
+++ b/app/models/reviewable.rb
@@ -547,6 +547,10 @@ class Reviewable < ActiveRecord::Base
TYPE_TO_BASIC_SERIALIZER[self.type.to_sym] || BasicReviewableSerializer
end
+ def type_class
+ Reviewable.sti_class_for(self.type)
+ end
+
def self.lookup_serializer_for(type)
"#{type}Serializer".constantize
rescue NameError
diff --git a/app/models/reviewable_queued_post.rb b/app/models/reviewable_queued_post.rb
index 827087f2efb..d16826888b7 100644
--- a/app/models/reviewable_queued_post.rb
+++ b/app/models/reviewable_queued_post.rb
@@ -16,6 +16,16 @@ class ReviewableQueuedPost < Reviewable
after_commit :compute_user_stats, only: %i[create update]
+ def self.additional_args(params)
+ return {} if params[:revise_reason].blank?
+
+ {
+ revise_reason: params[:revise_reason],
+ revise_feedback: params[:revise_feedback],
+ revise_custom_reason: params[:revise_custom_reason],
+ }
+ end
+
def updatable_reviewable_scores
# Approvals are possible for already rejected queued posts. We need the
# scores to be updated when this happens.
@@ -57,6 +67,10 @@ class ReviewableQueuedPost < Reviewable
a.label = "reviewables.actions.reject_post.title"
end
end
+
+ actions.add(:revise_and_reject_post) do |a|
+ a.label = "reviewables.actions.revise_and_reject_post.title"
+ end
end
actions.add(:delete) if guardian.can_delete?(self)
@@ -147,6 +161,24 @@ class ReviewableQueuedPost < Reviewable
create_result(:success, :rejected)
end
+ def perform_revise_and_reject_post(performed_by, args)
+ pm_translation_args = {
+ topic_title: self.topic.title,
+ topic_url: self.topic.url,
+ reason: args[:revise_custom_reason].presence || args[:revise_reason],
+ feedback: args[:revise_feedback],
+ original_post: self.payload["raw"],
+ site_name: SiteSetting.title,
+ }
+ SystemMessage.create_from_system_user(
+ self.target_created_by,
+ :reviewable_queued_post_revise_and_reject,
+ pm_translation_args,
+ )
+ StaffActionLogger.new(performed_by).log_post_rejected(self, DateTime.now) if performed_by.staff?
+ create_result(:success, :rejected)
+ end
+
def perform_delete(performed_by, args)
create_result(:success, :deleted)
end
diff --git a/app/models/reviewable_user.rb b/app/models/reviewable_user.rb
index d7d2d8da098..85f8903357a 100644
--- a/app/models/reviewable_user.rb
+++ b/app/models/reviewable_user.rb
@@ -5,6 +5,10 @@ class ReviewableUser < Reviewable
create(created_by_id: Discourse.system_user.id, target: user)
end
+ def self.additional_args(params)
+ { reject_reason: params[:reject_reason], send_email: params[:send_email] != "false" }
+ end
+
def build_actions(actions, guardian, args)
return unless pending?
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 81c15088f7b..812b1133bb7 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -484,6 +484,14 @@ en:
type_bonus:
name: "type bonus"
title: "Certain reviewable types can be assigned a bonus by staff to make them a higher priority."
+ revise_and_reject_post:
+ title: "Revise"
+ reason: "Reason"
+ send_pm: "Send PM"
+ feedback: "Feedback"
+ custom_reason: "Give a clear description of the reason"
+ other_reason: "Other..."
+ optional: "optional"
stale_help: "This reviewable has been resolved by