UX: Redesign and refactor penalty modals (#19458)
This merges the two modals code to remove duplication and implements a more consistent design.
This commit is contained in:
parent
43a8ca00b9
commit
1ad06eb764
|
@ -1,9 +1,9 @@
|
|||
import Component from "@ember/component";
|
||||
import { equal } from "@ember/object/computed";
|
||||
import discourseComputed, {
|
||||
afterRender,
|
||||
} from "discourse-common/utils/decorators";
|
||||
import Component from "@ember/component";
|
||||
import I18n from "I18n";
|
||||
import { equal } from "@ember/object/computed";
|
||||
|
||||
const ACTIONS = ["delete", "delete_replies", "edit", "none"];
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import Component from "@ember/component";
|
||||
import I18n from "I18n";
|
||||
import { action } from "@ember/object";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { equal } from "@ember/object/computed";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import I18n from "I18n";
|
||||
|
||||
const CUSTOM_REASON_KEY = "custom";
|
||||
|
|
@ -5,7 +5,7 @@ import discourseComputed from "discourse-common/utils/decorators";
|
|||
export default Component.extend({
|
||||
tagName: "",
|
||||
|
||||
@discourseComputed("type")
|
||||
@discourseComputed("penaltyType")
|
||||
penaltyField(penaltyType) {
|
||||
if (penaltyType === "suspend") {
|
||||
return "can_be_suspended";
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { next } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
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 Controller.extend(ModalFunctionality, {
|
||||
dialog: service(),
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -1,48 +0,0 @@
|
|||
import Controller from "@ember/controller";
|
||||
import PenaltyController from "admin/mixins/penalty-controller";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
|
||||
export default Controller.extend(PenaltyController, {
|
||||
silenceUntil: null,
|
||||
silencing: false,
|
||||
|
||||
onShow() {
|
||||
this.resetModal();
|
||||
this.setProperties({
|
||||
silenceUntil: null,
|
||||
silencing: false,
|
||||
otherUserIds: [],
|
||||
});
|
||||
},
|
||||
|
||||
finishedSetup() {
|
||||
this.set("silenceUntil", this.user?.next_penalty);
|
||||
},
|
||||
|
||||
@discourseComputed("silenceUntil", "reason", "silencing")
|
||||
submitDisabled(silenceUntil, reason, silencing) {
|
||||
return silencing || isEmpty(silenceUntil) || !reason || reason.length < 1;
|
||||
},
|
||||
|
||||
actions: {
|
||||
silence() {
|
||||
if (this.submitDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("silencing", true);
|
||||
this.penalize(() => {
|
||||
return this.user.silence({
|
||||
silenced_till: this.silenceUntil,
|
||||
reason: this.reason,
|
||||
message: this.message,
|
||||
post_id: this.postId,
|
||||
post_action: this.postAction,
|
||||
post_edit: this.postEdit,
|
||||
other_user_ids: this.otherUserIds,
|
||||
});
|
||||
}).finally(() => this.set("silencing", false));
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,48 +0,0 @@
|
|||
import Controller from "@ember/controller";
|
||||
import PenaltyController from "admin/mixins/penalty-controller";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
|
||||
export default Controller.extend(PenaltyController, {
|
||||
suspendUntil: null,
|
||||
suspending: false,
|
||||
|
||||
onShow() {
|
||||
this.resetModal();
|
||||
this.setProperties({
|
||||
suspendUntil: null,
|
||||
suspending: false,
|
||||
otherUserIds: [],
|
||||
});
|
||||
},
|
||||
|
||||
finishedSetup() {
|
||||
this.set("suspendUntil", this.user?.next_penalty);
|
||||
},
|
||||
|
||||
@discourseComputed("suspendUntil", "reason", "suspending")
|
||||
submitDisabled(suspendUntil, reason, suspending) {
|
||||
return suspending || isEmpty(suspendUntil) || !reason || reason.length < 1;
|
||||
},
|
||||
|
||||
actions: {
|
||||
suspend() {
|
||||
if (this.submitDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("suspending", true);
|
||||
this.penalize(() => {
|
||||
return this.user.suspend({
|
||||
suspend_until: this.suspendUntil,
|
||||
reason: this.reason,
|
||||
message: this.message,
|
||||
post_id: this.postId,
|
||||
post_action: this.postAction,
|
||||
post_edit: this.postEdit,
|
||||
other_user_ids: this.otherUserIds,
|
||||
});
|
||||
}).finally(() => this.set("suspending", false));
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,76 +0,0 @@
|
|||
import I18n from "I18n";
|
||||
import Mixin from "@ember/object/mixin";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import { Promise } from "rsvp";
|
||||
import { extractError } from "discourse/lib/ajax-error";
|
||||
import { next } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
||||
export default Mixin.create(ModalFunctionality, {
|
||||
dialog: service(),
|
||||
errorMessage: null,
|
||||
reason: null,
|
||||
message: null,
|
||||
postEdit: null,
|
||||
postAction: null,
|
||||
user: null,
|
||||
postId: null,
|
||||
successCallback: null,
|
||||
confirmClose: false,
|
||||
|
||||
resetModal() {
|
||||
this.setProperties({
|
||||
errorMessage: null,
|
||||
reason: null,
|
||||
message: null,
|
||||
loadingUser: true,
|
||||
postId: null,
|
||||
postEdit: null,
|
||||
postAction: "delete",
|
||||
before: null,
|
||||
successCallback: null,
|
||||
confirmClose: false,
|
||||
});
|
||||
},
|
||||
|
||||
beforeClose() {
|
||||
// prompt a confirmation if we have unsaved content
|
||||
if (
|
||||
!this.confirmClose &&
|
||||
((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;
|
||||
}
|
||||
},
|
||||
|
||||
penalize(cb) {
|
||||
let before = this.before;
|
||||
let promise = before ? before() : Promise.resolve();
|
||||
|
||||
return promise
|
||||
.then(() => cb())
|
||||
.then((result) => {
|
||||
this.set("confirmClose", true);
|
||||
this.send("closeModal");
|
||||
let callback = this.successCallback;
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.set("errorMessage", extractError(error));
|
||||
});
|
||||
},
|
||||
});
|
|
@ -197,6 +197,7 @@ const AdminUser = User.extend({
|
|||
canLockTrustLevel: lt("trust_level", 4),
|
||||
|
||||
canSuspend: not("staff"),
|
||||
canSilence: not("staff"),
|
||||
|
||||
@discourseComputed("suspended_till", "suspended_at")
|
||||
suspendDuration(suspendedTill, suspendedAt) {
|
||||
|
|
|
@ -41,11 +41,17 @@ export default Service.extend({
|
|||
|
||||
_showControlModal(type, user, opts) {
|
||||
opts = opts || {};
|
||||
let controller = showModal(`admin-${type}-user`, {
|
||||
|
||||
const controller = showModal(`admin-penalize-user`, {
|
||||
admin: true,
|
||||
modalClass: `${type}-user-modal`,
|
||||
});
|
||||
controller.setProperties({ postId: opts.postId, postEdit: opts.postEdit });
|
||||
|
||||
controller.setProperties({
|
||||
penaltyType: type,
|
||||
postId: opts.postId,
|
||||
postEdit: opts.postEdit,
|
||||
});
|
||||
|
||||
return (
|
||||
user.adminUserView
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<div class="penalty-reason-controls">
|
||||
{{#if (eq @penaltyType "suspend")}}
|
||||
<label class="suspend-reason-title">{{i18n "admin.user.suspend_reason_title"}}</label>
|
||||
<ComboBox @content={{this.reasons}} @value={{this.selectedReason}} @class="suspend-reason" @onChange={{this.setSelectedReason}} />
|
||||
|
||||
{{#if this.isCustomReason}}
|
||||
<TextField @value={{this.customReason}} @class="suspend-reason" @onChange={{this.setCustomReason}} />
|
||||
{{/if}}
|
||||
{{else if (eq @penaltyType "silence")}}
|
||||
<label class="silence-reason-title">{{html-safe (i18n "admin.user.silence_reason_label")}}</label>
|
||||
<TextField @value={{this.customReason}} @class="silence-reason" @onChange={{this.setCustomReason}} @placeholderKey="admin.user.silence_reason_placeholder" />
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="penalty-message-controls">
|
||||
<label>{{i18n "admin.user.suspend_message"}}</label>
|
||||
<Textarea @value={{this.message}} class="suspend-message" placeholder={{i18n "admin.user.suspend_message_placeholder"}} />
|
||||
</div>
|
|
@ -1,6 +1,6 @@
|
|||
<div class="penalty-similar-users">
|
||||
<p class="alert alert-danger">
|
||||
{{i18n "admin.user.other_matches" (hash count=this.user.similar_users_count username=this.user.username)}}
|
||||
<p class="alert alert-warning">
|
||||
{{html-safe (i18n "admin.user.other_matches" (hash count=this.user.similar_users_count username=this.user.username))}}
|
||||
</p>
|
||||
|
||||
<table class="table">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="reason-controls">
|
||||
<div class="penalty-reason-controls">
|
||||
<label>
|
||||
<div class="silence-reason-label">
|
||||
{{html-safe (i18n "admin.user.silence_reason_label")}}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
<div class="reason-controls">
|
||||
<label>
|
||||
<div class="suspend-reason-label">
|
||||
{{#if this.siteSettings.hide_suspension_reasons}}
|
||||
{{html-safe (i18n "admin.user.suspend_reason_hidden_label")}}
|
||||
{{else}}
|
||||
{{html-safe (i18n "admin.user.suspend_reason_label")}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
{{i18n "admin.user.suspend_reason_title"}}
|
||||
</label>
|
||||
<ComboBox @content={{this.reasons}} @value={{this.selectedReason}} @class="suspend-reason" @onChange={{action this.setSelectedReason}} />
|
||||
{{#if this.isCustomReason}}
|
||||
<TextField @value={{this.customReason}} @class="suspend-reason" @onChange={{action this.setCustomReason}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<label>
|
||||
<div class="suspend-message-label">
|
||||
{{i18n "admin.user.suspend_message"}}
|
||||
</div>
|
||||
</label>
|
||||
<Textarea @value={{this.message}} class="suspend-message" placeholder={{i18n "admin.user.suspend_message_placeholder"}} />
|
|
@ -0,0 +1,63 @@
|
|||
<DModalBody @title={{this.modalTitle}}>
|
||||
<ConditionalLoadingSpinner @condition={{this.loadingUser}}>
|
||||
{{#if this.errorMessage}}
|
||||
<div class="alert alert-error">{{this.errorMessage}}</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.canPenalize}}
|
||||
<div class="penalty-duration-controls">
|
||||
{{#if (eq this.penaltyType "suspend")}}
|
||||
<FutureDateInput @class="suspend-until"
|
||||
@label="admin.user.suspend_duration"
|
||||
@clearable={{false}}
|
||||
@input={{this.penalizeUntil}}
|
||||
@onChangeInput={{action (mut this.penalizeUntil)}} />
|
||||
{{else if (eq this.penaltyType "silence")}}
|
||||
<FutureDateInput @class="silence-until"
|
||||
@label="admin.user.silence_duration"
|
||||
@clearable={{false}}
|
||||
@input={{this.penalizeUntil}}
|
||||
@onChangeInput={{action (mut this.penalizeUntil)}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if (eq this.penaltyType "suspend")}}
|
||||
<div class="penalty-reason-visibility">
|
||||
{{#if this.siteSettings.hide_suspension_reasons}}
|
||||
{{html-safe (i18n "admin.user.suspend_reason_hidden_label")}}
|
||||
{{else}}
|
||||
{{html-safe (i18n "admin.user.suspend_reason_label")}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<AdminPenaltyReason @penaltyType={{this.penaltyType}} @reason={{this.reason}} @message={{this.message}} />
|
||||
|
||||
{{#if this.postId}}
|
||||
<AdminPenaltyPostAction @postId={{this.postId}} @postAction={{this.postAction}} @postEdit={{this.postEdit}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.user.similar_users}}
|
||||
<AdminPenaltySimilarUsers @penaltyType={{this.penaltyType}} @user={{this.user}} @selectedUserIds={{this.otherUserIds}} />
|
||||
{{/if}}
|
||||
|
||||
<div class="penalty-history">{{html-safe this.penaltyHistory}}</div>
|
||||
{{else}}
|
||||
{{#if (eq this.penaltyType "suspend")}}
|
||||
<div class="cant-suspend">{{i18n "admin.user.cant_suspend"}}</div>
|
||||
{{else if (eq this.penaltyType "silence")}}
|
||||
<div class="cant-silence">{{i18n "admin.user.cant_silence"}}</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</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>
|
|
@ -1,33 +0,0 @@
|
|||
<DModalBody @title="admin.user.silence_modal_title">
|
||||
<ConditionalLoadingSpinner @condition={{this.loadingUser}}>
|
||||
|
||||
{{#if this.errorMessage}}
|
||||
<div class="alert alert-error">{{this.errorMessage}}</div>
|
||||
{{/if}}
|
||||
|
||||
<AdminPenaltyHistory @user={{this.user}} />
|
||||
|
||||
<div class="until-controls">
|
||||
<label>
|
||||
<FutureDateInput @class="silence-until" @label="admin.user.silence_duration" @clearable={{false}} @input={{this.silenceUntil}} @onChangeInput={{action (mut this.silenceUntil)}} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<SilenceDetails @reason={{this.reason}} @message={{this.message}} />
|
||||
{{#if this.postId}}
|
||||
<PenaltyPostAction @postId={{this.postId}} @postAction={{this.postAction}} @postEdit={{this.postEdit}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.user.similar_users}}
|
||||
<AdminPenaltySimilarUsers @type="silence" @user={{this.user}} @selectedUserIds={{this.otherUserIds}} />
|
||||
{{/if}}
|
||||
|
||||
</ConditionalLoadingSpinner>
|
||||
|
||||
</DModalBody>
|
||||
|
||||
<div class="modal-footer">
|
||||
<DButton @class="btn-danger perform-silence" @action={{action "silence"}} @disabled={{this.submitDisabled}} @icon="microphone-slash" @label="admin.user.silence" />
|
||||
<DModalCancel @close={{route-action "closeModal"}} />
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}} @size="small" />
|
||||
</div>
|
|
@ -1,39 +0,0 @@
|
|||
<DModalBody @title="admin.user.suspend_modal_title">
|
||||
<ConditionalLoadingSpinner @condition={{this.loadingUser}}>
|
||||
|
||||
{{#if this.errorMessage}}
|
||||
<div class="alert alert-error">{{this.errorMessage}}</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.user.canSuspend}}
|
||||
<AdminPenaltyHistory @user={{this.user}} />
|
||||
|
||||
<div class="until-controls">
|
||||
<label>
|
||||
<FutureDateInput @class="suspend-until" @label="admin.user.suspend_duration" @clearable={{false}} @input={{this.suspendUntil}} @onChangeInput={{action (mut this.suspendUntil)}} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<SuspensionDetails @reason={{this.reason}} @message={{this.message}} />
|
||||
{{#if this.postId}}
|
||||
<PenaltyPostAction @postId={{this.postId}} @postAction={{this.postAction}} @postEdit={{this.postEdit}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.user.similar_users}}
|
||||
<AdminPenaltySimilarUsers @type="suspend" @user={{this.user}} @selectedUserIds={{this.otherUserIds}} />
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="cant-suspend">
|
||||
{{i18n "admin.user.cant_suspend"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
</ConditionalLoadingSpinner>
|
||||
|
||||
</DModalBody>
|
||||
|
||||
<div class="modal-footer">
|
||||
<DButton @class="btn-danger perform-suspend" @action={{action "suspend"}} @disabled={{this.submitDisabled}} @icon="ban" @label="admin.user.suspend" />
|
||||
<DModalCancel @close={{route-action "closeModal"}} />
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}} @size="small" />
|
||||
</div>
|
|
@ -1,3 +1,4 @@
|
|||
import { click, fillIn, visit } from "@ember/test-helpers";
|
||||
import {
|
||||
acceptance,
|
||||
count,
|
||||
|
@ -7,10 +8,9 @@ import {
|
|||
query,
|
||||
queryAll,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { click, fillIn, visit } from "@ember/test-helpers";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import { test } from "qunit";
|
||||
import I18n from "I18n";
|
||||
import { test } from "qunit";
|
||||
|
||||
acceptance("Admin - Suspend User", function (needs) {
|
||||
needs.user();
|
||||
|
@ -83,7 +83,7 @@ acceptance("Admin - Suspend User", function (needs) {
|
|||
await click(".suspend-user");
|
||||
|
||||
assert.strictEqual(
|
||||
count(".perform-suspend[disabled]"),
|
||||
count(".perform-penalize[disabled]"),
|
||||
1,
|
||||
"disabled by default"
|
||||
);
|
||||
|
@ -94,9 +94,9 @@ acceptance("Admin - Suspend User", function (needs) {
|
|||
await fillIn("input.suspend-reason", "for breaking the rules");
|
||||
await fillIn(".suspend-message", "this is an email reason why");
|
||||
|
||||
assert.ok(!exists(".perform-suspend[disabled]"), "no longer disabled");
|
||||
assert.ok(!exists(".perform-penalize[disabled]"), "no longer disabled");
|
||||
|
||||
await click(".perform-suspend");
|
||||
await click(".perform-penalize");
|
||||
|
||||
assert.ok(!exists(".suspend-user-modal:visible"));
|
||||
assert.ok(exists(".suspension-info"));
|
||||
|
@ -125,6 +125,48 @@ acceptance("Admin - Suspend User - timeframe choosing", function (needs) {
|
|||
await click(".suspend-user");
|
||||
await click(".future-date-input-selector-header");
|
||||
|
||||
const options = Array.from(
|
||||
queryAll(`ul.select-kit-collection li span.name`)
|
||||
).map((el) => el.innerText.trim());
|
||||
|
||||
const expected = [
|
||||
I18n.t("time_shortcut.later_today"),
|
||||
I18n.t("time_shortcut.tomorrow"),
|
||||
I18n.t("time_shortcut.later_this_week"),
|
||||
I18n.t("time_shortcut.start_of_next_business_week_alt"),
|
||||
I18n.t("time_shortcut.two_weeks"),
|
||||
I18n.t("time_shortcut.next_month"),
|
||||
I18n.t("time_shortcut.two_months"),
|
||||
I18n.t("time_shortcut.three_months"),
|
||||
I18n.t("time_shortcut.four_months"),
|
||||
I18n.t("time_shortcut.six_months"),
|
||||
I18n.t("time_shortcut.one_year"),
|
||||
I18n.t("time_shortcut.forever"),
|
||||
I18n.t("time_shortcut.custom"),
|
||||
];
|
||||
|
||||
assert.deepEqual(options, expected, "options are correct");
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Admin - Silence User", function (needs) {
|
||||
let clock = null;
|
||||
needs.user();
|
||||
|
||||
needs.hooks.beforeEach(() => {
|
||||
const timezone = loggedInUser().user_option.timezone;
|
||||
clock = fakeTime("2100-05-03T08:00:00", timezone, true); // Monday morning
|
||||
});
|
||||
|
||||
needs.hooks.afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
test("shows correct timeframe options", async function (assert) {
|
||||
await visit("/admin/users/1234/regular");
|
||||
await click(".silence-user");
|
||||
await click(".future-date-input-selector-header");
|
||||
|
||||
const options = Array.from(
|
||||
queryAll(`ul.select-kit-collection li span.name`).map((_, x) =>
|
||||
x.innerText.trim()
|
|
@ -1,53 +0,0 @@
|
|||
import {
|
||||
acceptance,
|
||||
fakeTime,
|
||||
loggedInUser,
|
||||
queryAll,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { click, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
import I18n from "I18n";
|
||||
|
||||
acceptance("Admin - Silence User", function (needs) {
|
||||
let clock = null;
|
||||
needs.user();
|
||||
|
||||
needs.hooks.beforeEach(() => {
|
||||
const timezone = loggedInUser().user_option.timezone;
|
||||
clock = fakeTime("2100-05-03T08:00:00", timezone, true); // Monday morning
|
||||
});
|
||||
|
||||
needs.hooks.afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
test("shows correct timeframe options", async function (assert) {
|
||||
await visit("/admin/users/1234/regular");
|
||||
await click(".silence-user");
|
||||
await click(".future-date-input-selector-header");
|
||||
|
||||
const options = Array.from(
|
||||
queryAll(`ul.select-kit-collection li span.name`).map((_, x) =>
|
||||
x.innerText.trim()
|
||||
)
|
||||
);
|
||||
|
||||
const expected = [
|
||||
I18n.t("time_shortcut.later_today"),
|
||||
I18n.t("time_shortcut.tomorrow"),
|
||||
I18n.t("time_shortcut.later_this_week"),
|
||||
I18n.t("time_shortcut.start_of_next_business_week_alt"),
|
||||
I18n.t("time_shortcut.two_weeks"),
|
||||
I18n.t("time_shortcut.next_month"),
|
||||
I18n.t("time_shortcut.two_months"),
|
||||
I18n.t("time_shortcut.three_months"),
|
||||
I18n.t("time_shortcut.four_months"),
|
||||
I18n.t("time_shortcut.six_months"),
|
||||
I18n.t("time_shortcut.one_year"),
|
||||
I18n.t("time_shortcut.forever"),
|
||||
I18n.t("time_shortcut.custom"),
|
||||
];
|
||||
|
||||
assert.deepEqual(options, expected, "options are correct");
|
||||
});
|
||||
});
|
|
@ -125,7 +125,7 @@ acceptance("flagging", function (needs) {
|
|||
assert.ok(exists(".modal-body"));
|
||||
await fillIn(".silence-reason", "for breaking the rules");
|
||||
|
||||
await click(".perform-silence");
|
||||
await click(".perform-penalize");
|
||||
assert.ok(!exists(".modal-body"));
|
||||
});
|
||||
|
||||
|
|
|
@ -1010,7 +1010,6 @@ a.inline-editable-field {
|
|||
@import "common/admin/settings";
|
||||
@import "common/admin/users";
|
||||
@import "common/admin/penalty";
|
||||
@import "common/admin/suspend";
|
||||
@import "common/admin/badges";
|
||||
@import "common/admin/emails";
|
||||
@import "common/admin/json_schema_editor";
|
||||
|
|
|
@ -1,11 +1,90 @@
|
|||
.silence-user-modal,
|
||||
.suspend-user-modal {
|
||||
.table {
|
||||
width: 100%;
|
||||
.penalty-duration,
|
||||
.penalty-suspend-forever,
|
||||
.suspend-reason-title,
|
||||
.penalty-reason-controls label,
|
||||
.penalty-message-controls label,
|
||||
.penalty-post-controls label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px 0px;
|
||||
.penalty-reason-visibility,
|
||||
.penalty-reason-controls,
|
||||
.penalty-similar-users {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.penalty-duration-controls,
|
||||
.penalty-reason-visibility,
|
||||
.penalty-post-controls {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.penalty-duration-controls {
|
||||
.future-date-input {
|
||||
align-items: end;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
input {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 0;
|
||||
|
||||
&:first-child {
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
details {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.future-date-input-date-picker,
|
||||
.future-date-input-time-picker {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.penalty-reason-controls {
|
||||
input,
|
||||
.combo-box {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.penalty-post-controls {
|
||||
.select-kit {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.penalty-similar-users {
|
||||
background-color: var(--primary-very-low);
|
||||
padding: 0.5em;
|
||||
|
||||
.alert {
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
.suspend-user-modal {
|
||||
.until-controls {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.suspend-reason {
|
||||
width: 100%;
|
||||
|
||||
&.combo-box {
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
.suspend-reason-label,
|
||||
.suspend-message-label {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.inline-spinner {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
.penalty-post-edit {
|
||||
margin-top: 1em;
|
||||
|
||||
textarea {
|
||||
height: 10em;
|
||||
}
|
||||
}
|
||||
.penalty-history {
|
||||
margin-bottom: 1em;
|
||||
padding-bottom: 0.5em;
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
display: flex;
|
||||
& > * {
|
||||
flex-basis: 100%;
|
||||
text-align: center;
|
||||
padding: 1em 0;
|
||||
font-weight: 600;
|
||||
label {
|
||||
font-weight: 600;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.danger {
|
||||
background-color: var(--danger);
|
||||
color: var(--secondary);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -161,7 +161,6 @@ class AdminDetailedUserSerializer < AdminUserSerializer
|
|||
def similar_users
|
||||
ActiveModel::ArraySerializer.new(
|
||||
@options[:similar_users],
|
||||
each_serializer: AdminUserListSerializer,
|
||||
each_serializer: SimilarAdminUserSerializer,
|
||||
scope: scope,
|
||||
root: false,
|
||||
|
|
|
@ -5516,7 +5516,7 @@ en:
|
|||
user:
|
||||
suspend_failed: "Something went wrong suspending this user %{error}"
|
||||
unsuspend_failed: "Something went wrong unsuspending this user %{error}"
|
||||
suspend_duration: "How long will the user be suspended for?"
|
||||
suspend_duration: "Suspend user until:"
|
||||
suspend_reason_label: "Why are you suspending? This text <b>will be visible to everyone</b> on this user's profile page, and will be shown to the user when they try to log in. Keep it short."
|
||||
suspend_reason_hidden_label: "Why are you suspending? This text will be shown to the user when they try to log in. Keep it short."
|
||||
suspend_reason: "Reason"
|
||||
|
@ -5540,7 +5540,9 @@ en:
|
|||
silence_message: "Email Message"
|
||||
silence_message_placeholder: "(leave blank to send default message)"
|
||||
suspended_until: "(until %{until})"
|
||||
suspend_forever: "Suspend forever"
|
||||
cant_suspend: "This user cannot be suspended."
|
||||
cant_silence: "This user cannot be silenced."
|
||||
|
||||
delete_posts_failed: "There was a problem deleting the posts."
|
||||
post_edits: "Post Edits"
|
||||
|
@ -5551,7 +5553,19 @@ en:
|
|||
penalty_post_edit: "Edit the post"
|
||||
penalty_post_none: "Do nothing"
|
||||
penalty_count: "Penalty Count"
|
||||
penalty_history: "Penalty History"
|
||||
|
||||
# This string uses the ICU Message Format. See https://meta.discourse.org/t/7035 for translation guidelines.
|
||||
penalty_history_MF: >-
|
||||
In the past 6 months this user has been <b>suspended
|
||||
{ SUSPENDED, plural,
|
||||
one {# time}
|
||||
other {# times}
|
||||
}</b> and <b>silenced
|
||||
{ SILENCED, plural,
|
||||
one {# time}
|
||||
other {# times}
|
||||
}</b>.
|
||||
|
||||
clear_penalty_history:
|
||||
title: "Clear Penalty History"
|
||||
description: "users with penalties cannot reach TL3"
|
||||
|
@ -5721,8 +5735,8 @@ en:
|
|||
suspended_count: "Suspended"
|
||||
last_six_months: "Last 6 months"
|
||||
other_matches:
|
||||
one: "There is %{count} other user with the same IP address. Review and select the suspicious ones to suspend along with %{username}."
|
||||
other: "There are %{count} other users with the same IP address. Review and select the suspicious ones to suspend along with %{username}."
|
||||
one: "There is <b>%{count} other user</b> with the same IP address. Review and select the suspicious ones to penalize along with %{username}."
|
||||
other: "There are <b>%{count} other users</b> with the same IP address. Review and select the suspicious ones to penalize along with %{username}."
|
||||
other_matches_list:
|
||||
username: "Username"
|
||||
trust_level: "Trust Level"
|
||||
|
|
Loading…
Reference in New Issue