DEV: Convert `penalize-user` modal to component-based API (#22960)

<img width="681" alt="Screenshot 2023-08-03 at 12 55 08 PM" src="https://github.com/discourse/discourse/assets/50783505/79cc045a-523d-45a2-8c33-04b556331358">

<img width="763" alt="Screenshot 2023-08-03 at 12 55 05 PM" src="https://github.com/discourse/discourse/assets/50783505/7196a97f-e4f4-4870-b8ac-77255d604c27">

<img width="711" alt="Screenshot 2023-08-03 at 12 55 11 PM" src="https://github.com/discourse/discourse/assets/50783505/a916a85d-8bdb-41fb-8210-1e0c06cf7cf1">
This commit is contained in:
Isaac Janzen 2023-08-14 13:02:54 -05:00 committed by GitHub
parent ba46b34581
commit a5542eeab0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 167 additions and 232 deletions

View File

@ -1,12 +1,13 @@
<DModalBody @title={{this.modalTitle}}>
<ConditionalLoadingSpinner @condition={{this.loadingUser}}>
{{#if this.errorMessage}}
<div class="alert alert-error">{{this.errorMessage}}</div>
{{/if}}
<DModal
class="{{@model.penaltyType}}-user-modal"
@title={{i18n this.modalTitle}}
@closeModal={{this.warnBeforeClosing}}
@flash={{this.flash}}
>
<:body>
{{#if this.canPenalize}}
<div class="penalty-duration-controls">
{{#if (eq this.penaltyType "suspend")}}
{{#if (eq @model.penaltyType "suspend")}}
<FutureDateInput
@class="suspend-until"
@label="admin.user.suspend_duration"
@ -14,7 +15,7 @@
@input={{this.penalizeUntil}}
@onChangeInput={{action (mut this.penalizeUntil)}}
/>
{{else if (eq this.penaltyType "silence")}}
{{else if (eq @model.penaltyType "silence")}}
<FutureDateInput
@class="silence-until"
@label="admin.user.silence_duration"
@ -24,8 +25,7 @@
/>
{{/if}}
</div>
{{#if (eq this.penaltyType "suspend")}}
{{#if (eq @model.penaltyType "suspend")}}
<div class="penalty-reason-visibility">
{{#if this.siteSettings.hide_suspension_reasons}}
{{html-safe (i18n "admin.user.suspend_reason_hidden_label")}}
@ -34,48 +34,46 @@
{{/if}}
</div>
{{/if}}
<AdminPenaltyReason
@penaltyType={{this.penaltyType}}
@penaltyType={{@model.penaltyType}}
@reason={{this.reason}}
@message={{this.message}}
/>
{{#if this.postId}}
{{#if @model.postId}}
<AdminPenaltyPostAction
@postId={{this.postId}}
@postId={{@model.postId}}
@postAction={{this.postAction}}
@postEdit={{this.postEdit}}
/>
{{/if}}
{{#if this.user.similar_users}}
<AdminPenaltySimilarUsers
@penaltyType={{this.penaltyType}}
@penaltyType={{@model.penaltyType}}
@user={{this.user}}
@selectedUserIds={{this.otherUserIds}}
/>
{{/if}}
{{else}}
{{#if (eq this.penaltyType "suspend")}}
{{#if (eq @model.penaltyType "suspend")}}
<div class="cant-suspend">{{i18n "admin.user.cant_suspend"}}</div>
{{else if (eq this.penaltyType "silence")}}
{{else if (eq @model.penaltyType "silence")}}
<div class="cant-silence">{{i18n "admin.user.cant_silence"}}</div>
{{/if}}
{{/if}}
<div class="penalty-history">{{html-safe this.penaltyHistory}}</div>
</ConditionalLoadingSpinner>
</DModalBody>
<div class="modal-footer">
<DButton
@class="btn-danger perform-penalize"
@action={{this.penalizeUser}}
@disabled={{this.submitDisabled}}
@icon="ban"
@label={{this.buttonLabel}}
/>
<DModalCancel @close={{route-action "closeModal"}} />
<ConditionalLoadingSpinner @condition={{this.loading}} @size="small" />
</div>
</:body>
<:footer>
<DButton
class="btn-danger perform-penalize"
@action={{this.penalizeUser}}
@disabled={{this.submitDisabled}}
@icon="ban"
@label={{this.buttonLabel}}
/>
<DButton
class="btn-flat d-modal-cancel"
@action={{this.warnBeforeClosing}}
@label="cancel"
/>
</:footer>
</DModal>

View File

@ -0,0 +1,118 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { isEmpty } from "@ember/utils";
import { inject as service } from "@ember/service";
import { extractError } from "discourse/lib/ajax-error";
import I18n from "I18n";
export default class PenalizeUser extends Component {
@service dialog;
@service siteSettings;
@tracked penalizeUntil = this.args.model.user.next_penalty;
@tracked confirmClose = false;
@tracked otherUserIds = [];
@tracked postAction = "delete";
@tracked postEdit = this.args.model.postEdit;
@tracked flash;
@tracked reason;
@tracked message;
get modalTitle() {
if (this.args.model.penaltyType === "suspend") {
return "admin.user.suspend_modal_title";
} else if (this.args.model.penaltyType === "silence") {
return "admin.user.silence_modal_title";
}
}
get buttonLabel() {
if (this.args.model.penaltyType === "suspend") {
return "admin.user.suspend";
} else if (this.args.model.penaltyType === "silence") {
return "admin.user.silence";
}
}
get penaltyHistory() {
return I18n.messageFormat("admin.user.penalty_history_MF", {
SUSPENDED: this.args.model.user.penalty_counts?.suspended,
SILENCED: this.args.model.user.penalty_counts?.silenced,
});
}
get canPenalize() {
if (this.args.model.penaltyType === "suspend") {
return this.args.model.user.canSuspend;
} else if (this.args.model.penaltyType === "silence") {
return this.args.model.user.canSilence;
}
return false;
}
get submitDisabled() {
return (
this.penalizing ||
isEmpty(this.penalizeUntil) ||
!this.reason ||
this.reason.length < 1
);
}
@action
async penalizeUser() {
if (this.submitDisabled) {
return;
}
this.penalizing = true;
this.confirmClose = true;
if (this.before) {
this.before();
}
let result;
try {
const opts = {
reason: this.reason,
message: this.message,
post_id: this.args.model.postId,
post_action: this.postAction,
post_edit: this.postEdit,
other_user_ids: this.otherUserIds,
};
if (this.args.model.penaltyType === "suspend") {
opts.suspend_until = this.penalizeUntil;
result = await this.args.model.user.suspend(opts);
} else if (this.args.model.penaltyType === "silence") {
opts.silenced_till = this.penalizeUntil;
result = await this.args.model.user.silence(opts);
} else {
// eslint-disable-next-line no-console
console.error("Unknown penalty type:", this.args.model.penaltyType);
}
this.args.closeModal();
if (this.successCallback) {
await this.successCallback(result);
}
} catch {
this.flash = extractError(result);
} finally {
this.penalizing = false;
}
}
@action
warnBeforeClosing() {
if (!this.confirmClose && (this.reason?.length || this.message?.length)) {
this.dialog.confirm({
message: I18n.t("admin.user.confirm_cancel_penalty"),
didConfirm: () => this.args.closeModal(),
});
return false;
}
this.args.closeModal();
}
}

View File

@ -1,170 +0,0 @@
import { inject as service } from "@ember/service";
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { next } from "@ember/runloop";
import { isEmpty } from "@ember/utils";
import discourseComputed from "discourse-common/utils/decorators";
import { extractError } from "discourse/lib/ajax-error";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import I18n from "I18n";
export default class AdminPenalizeUserController extends Controller.extend(
ModalFunctionality
) {
@service dialog;
loadingUser = false;
errorMessage = null;
penaltyType = null;
penalizeUntil = null;
reason = null;
message = null;
postId = null;
postAction = null;
postEdit = null;
user = null;
otherUserIds = null;
loading = false;
confirmClose = false;
onShow() {
this.setProperties({
loadingUser: true,
errorMessage: null,
penaltyType: null,
penalizeUntil: null,
reason: null,
message: null,
postId: null,
postAction: "delete",
postEdit: null,
user: null,
otherUserIds: [],
loading: false,
errorMessage: null,
reason: null,
message: null,
confirmClose: false,
});
}
finishedSetup() {
this.set("penalizeUntil", this.user?.next_penalty);
}
beforeClose() {
if (this.confirmClose) {
return true;
}
if (
(this.reason && this.reason.length > 1) ||
(this.message && this.message.length > 1)
) {
this.send("hideModal");
this.dialog.confirm({
message: I18n.t("admin.user.confirm_cancel_penalty"),
didConfirm: () => {
next(() => {
this.set("confirmClose", true);
this.send("closeModal");
});
},
didCancel: () => this.send("reopenModal"),
});
return false;
}
}
@discourseComputed("penaltyType")
modalTitle(penaltyType) {
if (penaltyType === "suspend") {
return "admin.user.suspend_modal_title";
} else if (penaltyType === "silence") {
return "admin.user.silence_modal_title";
}
}
@discourseComputed("penaltyType")
buttonLabel(penaltyType) {
if (penaltyType === "suspend") {
return "admin.user.suspend";
} else if (penaltyType === "silence") {
return "admin.user.silence";
}
}
@discourseComputed(
"user.penalty_counts.suspended",
"user.penalty_counts.silenced"
)
penaltyHistory(suspendedCount, silencedCount) {
return I18n.messageFormat("admin.user.penalty_history_MF", {
SUSPENDED: suspendedCount,
SILENCED: silencedCount,
});
}
@discourseComputed("penaltyType", "user.canSuspend", "user.canSilence")
canPenalize(penaltyType, canSuspend, canSilence) {
if (penaltyType === "suspend") {
return canSuspend;
} else if (penaltyType === "silence") {
return canSilence;
}
return false;
}
@discourseComputed("penalizing", "penalizeUntil", "reason")
submitDisabled(penalizing, penalizeUntil, reason) {
return penalizing || isEmpty(penalizeUntil) || !reason || reason.length < 1;
}
@action
async penalizeUser() {
if (this.submitDisabled) {
return;
}
this.set("penalizing", true);
this.set("confirmClose", true);
if (this.before) {
this.before();
}
let result;
try {
const opts = {
reason: this.reason,
message: this.message,
post_id: this.postId,
post_action: this.postAction,
post_edit: this.postEdit,
other_user_ids: this.otherUserIds,
};
if (this.penaltyType === "suspend") {
opts.suspend_until = this.penalizeUntil;
result = await this.user.suspend(opts);
} else if (this.penaltyType === "silence") {
opts.silenced_till = this.penalizeUntil;
result = await this.user.silence(opts);
} else {
// eslint-disable-next-line no-console
console.error("Unknown penalty type:", this.penaltyType);
}
this.send("closeModal");
if (this.successCallback) {
await this.successCallback(result);
}
} catch {
this.set("errorMessage", extractError(result));
} finally {
this.set("penalizing", false);
}
}
}

View File

@ -4,14 +4,16 @@ import { Promise } from "rsvp";
import Service, { inject as service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import { getOwner } from "discourse-common/lib/get-owner";
import showModal from "discourse/lib/show-modal";
import { htmlSafe } from "@ember/template";
import { action } from "@ember/object";
import PenalizeUserModal from "admin/components/modal/penalize-user";
// A service that can act as a bridge between the front end Discourse application
// and the admin application. Use this if you need front end code to access admin
// modules. Inject it optionally, and if it exists go to town!
export default class AdminToolsService extends Service {
@service dialog;
@service modal;
showActionLogs(target, filters) {
const controller = getOwner(target).lookup(
@ -39,42 +41,30 @@ export default class AdminToolsService extends Service {
};
}
_showControlModal(type, user, opts) {
@action
async showControlModal(type, user, opts) {
opts = opts || {};
const controller = showModal(`admin-penalize-user`, {
admin: true,
modalClass: `${type}-user-modal`,
});
controller.setProperties({
penaltyType: type,
postId: opts.postId,
postEdit: opts.postEdit,
});
return (
user.adminUserView
? Promise.resolve(user)
: AdminUser.find(user.get("id"))
).then((loadedUser) => {
controller.setProperties({
const loadedUser = user.adminUserView
? user
: await AdminUser.find(user.get("id"));
this.modal.show(PenalizeUserModal, {
model: {
penaltyType: type,
postId: opts.postId,
postEdit: opts.postEdit,
user: loadedUser,
loadingUser: false,
before: opts.before,
successCallback: opts.successCallback,
});
controller.finishedSetup();
},
});
}
showSilenceModal(user, opts) {
this._showControlModal("silence", user, opts);
this.showControlModal("silence", user, opts);
}
showSuspendModal(user, opts) {
this._showControlModal("suspend", user, opts);
this.showControlModal("suspend", user, opts);
}
_deleteSpammer(adminUser) {

View File

@ -45,7 +45,6 @@ const KNOWN_LEGACY_MODALS = [
"tag-upload",
"topic-summary",
"user-status",
"admin-penalize-user",
"admin-reseed",
"admin-theme-item",
"admin-color-scheme-select-base",

View File

@ -65,9 +65,9 @@ acceptance("Admin - Suspend User", function (needs) {
await click(".d-modal-cancel");
assert.strictEqual(count(".dialog-body:visible"), 1);
assert.ok(!exists(".suspend-user-modal:visible"));
await click(".dialog-footer .btn-primary");
assert.ok(!exists(".suspend-user-modal:visible"));
assert.ok(!exists(".dialog-body:visible"));
});