UX: Nicer selection of suspend duration

This commit is contained in:
Robin Ward 2017-09-13 16:44:47 -04:00
parent 677b016387
commit 6bce3004d9
14 changed files with 51 additions and 43 deletions

View File

@ -3,34 +3,32 @@ import computed from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error'; import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend(ModalFunctionality, { export default Ember.Controller.extend(ModalFunctionality, {
duration: null, suspendUntil: null,
reason: null, reason: null,
message: null, message: null,
loading: false, loading: false,
onShow() { onShow() {
this.setProperties({ this.setProperties({
duration: null, suspendUntil: null,
reason: null, reason: null,
message: null, message: null,
loading: false loading: false
}); });
}, },
@computed('reason', 'loading') @computed('suspendUntil', 'reason', 'loading')
submitDisabled(reason, loading) { submitDisabled(suspendUntil, reason, loading) {
return (loading || !reason || reason.length < 1); return (loading || Ember.isEmpty(suspendUntil) || !reason || reason.length < 1);
}, },
actions: { actions: {
suspend() { suspend() {
if (this.get('submitDisabled')) { return; } if (this.get('submitDisabled')) { return; }
let duration = parseInt(this.get('duration'), 10);
if (duration > 0) {
this.set('loading', true); this.set('loading', true);
this.get('model').suspend({ this.get('model').suspend({
duration, suspend_until: this.get('suspendUntil'),
reason: this.get('reason'), reason: this.get('reason'),
message: this.get('message') message: this.get('message')
}).then(() => { }).then(() => {
@ -38,6 +36,5 @@ export default Ember.Controller.extend(ModalFunctionality, {
}).catch(popupAjaxError).finally(() => this.set('loading', false)); }).catch(popupAjaxError).finally(() => this.set('loading', false));
} }
} }
}
}); });

View File

@ -1,9 +1,10 @@
{{#d-modal-body title="admin.user.suspend_modal_title"}} {{#d-modal-body title="admin.user.suspend_modal_title"}}
<div class='duration-controls'> <div class='until-controls'>
<label> <label>
{{i18n 'admin.user.suspend_duration'}} {{auto-update-input
{{text-field value=duration maxlength="5" autofocus="autofocus" class="suspend-duration"}} class="suspend-until"
{{i18n 'admin.user.suspend_duration_units'}} label="admin.user.suspend_duration"
input=suspendUntil}}
</label> </label>
</div> </div>

View File

@ -298,7 +298,12 @@
<div class="user-suspended display-row {{if model.isSuspended 'highlight-danger'}}"> <div class="user-suspended display-row {{if model.isSuspended 'highlight-danger'}}">
<div class='field'>{{i18n 'admin.user.suspended'}}</div> <div class='field'>{{i18n 'admin.user.suspended'}}</div>
<div class='value'>{{i18n-yes-no model.isSuspended}}</div> <div class='value'>
{{i18n-yes-no model.isSuspended}}
{{#if model.isSuspended}}
{{i18n "admin.user.suspended_until" until=model.suspendedTillDate}}
{{/if}}
</div>
<div class='controls'> <div class='controls'>
{{#if model.isSuspended}} {{#if model.isSuspended}}
{{d-button {{d-button
@ -306,7 +311,6 @@
action=(action "unsuspend") action=(action "unsuspend")
icon="ban" icon="ban"
label="admin.user.unsuspend"}} label="admin.user.unsuspend"}}
{{suspendDuration}}
{{i18n 'admin.user.suspended_explanation'}} {{i18n 'admin.user.suspended_explanation'}}
{{else}} {{else}}
{{#if model.canSuspend}} {{#if model.canSuspend}}

View File

@ -124,7 +124,7 @@ export default Ember.Component.extend(bufferedRender({
if (val && val.length && castInteger) { if (val && val.length && castInteger) {
val = parseInt(val, 10); val = parseInt(val, 10);
} }
this.set('value', val); Ember.run(() => this.set('value', val));
}); });
Ember.run.scheduleOnce('afterRender', this, this._triggerChange); Ember.run.scheduleOnce('afterRender', this, this._triggerChange);

View File

@ -13,6 +13,7 @@ export default Ember.Component.extend({
time: null, time: null,
isCustom: Ember.computed.equal('selection', PICK_DATE_AND_TIME), isCustom: Ember.computed.equal('selection', PICK_DATE_AND_TIME),
isBasedOnLastPost: Ember.computed.equal('selection', SET_BASED_ON_LAST_POST), isBasedOnLastPost: Ember.computed.equal('selection', SET_BASED_ON_LAST_POST),
displayLabel: null,
init() { init() {
this._super(); this._super();
@ -64,6 +65,10 @@ export default Ember.Component.extend({
} }
}, },
didReceiveAttrs() {
this.set('displayLabel', I18n.t(this.get('label') || 'topic.topic_status_update.when'));
},
@computed("statusType", "input", "isCustom", "date", "time", "willCloseImmediately", "categoryId") @computed("statusType", "input", "isCustom", "date", "time", "willCloseImmediately", "categoryId")
showTopicStatusInfo(statusType, input, isCustom, date, time, willCloseImmediately, categoryId) { showTopicStatusInfo(statusType, input, isCustom, date, time, willCloseImmediately, categoryId) {
if (!statusType || willCloseImmediately) return false; if (!statusType || willCloseImmediately) return false;

View File

@ -25,5 +25,3 @@ registerUnbound('format-date', function(val, params) {
return new Handlebars.SafeString(autoUpdatingRelativeAge(date, {format: format, title: title, leaveAgo: leaveAgo})); return new Handlebars.SafeString(autoUpdatingRelativeAge(date, {format: format, title: title, leaveAgo: leaveAgo}));
} }
}); });

View File

@ -1,6 +1,6 @@
<div class="auto-update-input"> <div class="auto-update-input">
<div class="control-group"> <div class="control-group">
<label>{{i18n "topic.topic_status_update.when"}}</label> <label>{{displayLabel}}</label>
{{auto-update-input-selector {{auto-update-input-selector
valueAttribute="id" valueAttribute="id"

View File

@ -1,6 +1,6 @@
.suspend-user-modal { .suspend-user-modal {
.duration-controls { .until-controls {
margin-bottom: 1em; margin-bottom: 1em;
} }

View File

@ -53,7 +53,7 @@ class Admin::UsersController < Admin::AdminController
def suspend def suspend
guardian.ensure_can_suspend!(@user) guardian.ensure_can_suspend!(@user)
@user.suspended_till = params[:duration].to_i.days.from_now @user.suspended_till = params[:suspend_until]
@user.suspended_at = DateTime.now @user.suspended_at = DateTime.now
message = params[:message] message = params[:message]

View File

@ -17,6 +17,7 @@ class AdminDetailedUserSerializer < AdminUserSerializer
:can_be_deleted, :can_be_deleted,
:can_be_anonymized, :can_be_anonymized,
:suspend_reason, :suspend_reason,
:suspended_till,
:primary_group_id, :primary_group_id,
:badge_count, :badge_count,
:warnings_received_count, :warnings_received_count,

View File

@ -3264,7 +3264,6 @@ en:
suspend_failed: "Something went wrong suspending this user {{error}}" suspend_failed: "Something went wrong suspending this user {{error}}"
unsuspend_failed: "Something went wrong unsuspending this user {{error}}" unsuspend_failed: "Something went wrong unsuspending this user {{error}}"
suspend_duration: "How long will the user be suspended for?" suspend_duration: "How long will the user be suspended for?"
suspend_duration_units: "(days)"
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_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_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" suspend_reason: "Reason"
@ -3272,6 +3271,7 @@ en:
suspend_message: "Email Message" suspend_message: "Email Message"
suspend_message_placeholder: "Optionally, provide more information about the suspension and it will be emailed to the user." suspend_message_placeholder: "Optionally, provide more information about the suspension and it will be emailed to the user."
suspended_by: "Suspended by" suspended_by: "Suspended by"
suspended_until: "(until %{until})"
delete_all_posts: "Delete all posts" delete_all_posts: "Delete all posts"
# keys ending with _MF use message format, see https://meta.discourse.org/t/message-format-support-for-localization/7035 for details # keys ending with _MF use message format, see https://meta.discourse.org/t/message-format-support-for-localization/7035 for details

View File

@ -128,7 +128,7 @@ describe Admin::UsersController do
put( put(
:suspend, :suspend,
user_id: user.id, user_id: user.id,
duration: 10, suspend_until: 5.hours.from_now,
reason: "because I said so", reason: "because I said so",
format: :json format: :json
) )
@ -149,15 +149,14 @@ describe Admin::UsersController do
:critical_user_email, :critical_user_email,
has_entries( has_entries(
type: :account_suspended, type: :account_suspended,
user_id: user.id, user_id: user.id
message: "long reason"
) )
) )
put( put(
:suspend, :suspend,
user_id: user.id, user_id: user.id,
duration: 10, suspend_until: 10.days.from_now,
reason: "short reason", reason: "short reason",
message: "long reason", message: "long reason",
format: :json format: :json
@ -168,10 +167,12 @@ describe Admin::UsersController do
expect(log).to be_present expect(log).to be_present
expect(log.details).to match(/short reason/) expect(log.details).to match(/short reason/)
expect(log.details).to match(/long reason/) expect(log.details).to match(/long reason/)
end
it "also revoke any api keys" do it "also revoke any api keys" do
User.any_instance.expects(:revoke_api_key) User.any_instance.expects(:revoke_api_key)
put :suspend, params: { user_id: evil_trout.id }, format: :json put :suspend, params: { user_id: evil_trout.id }, format: :json
expect(log.context).to match(/long reason/)
end end
end end

View File

@ -201,10 +201,9 @@ describe Jobs::UserEmail do
post, post,
:user_mentioned, :user_mentioned,
notification, notification,
notification.notification_type, notification_type: notification.notification_type,
notification.data_hash, notification_data_hash: notification.data_hash
nil, )
nil)
expect(message).to eq nil expect(message).to eq nil
expect(err.skipped_reason).to match(/notification.*already/) expect(err.skipped_reason).to match(/notification.*already/)

View File

@ -43,12 +43,14 @@ QUnit.test("suspend, then unsuspend a user", assert => {
andThen(() => { andThen(() => {
assert.equal(find('.perform-suspend[disabled]').length, 1, 'disabled by default'); assert.equal(find('.perform-suspend[disabled]').length, 1, 'disabled by default');
find('.suspend-until .combobox').select2('val', 'tomorrow');
find('.suspend-until .combobox').trigger('change', 'tomorrow');
}); });
fillIn('.suspend-duration', 12);
fillIn('.suspend-reason', "for breaking the rules"); fillIn('.suspend-reason', "for breaking the rules");
fillIn('.suspend-message', "this is an email reason why"); fillIn('.suspend-message', "this is an email reason why");
andThen(() => { andThen(() => {
assert.equal(find('.perform-suspend[disabled]').length, 0); assert.equal(find('.perform-suspend[disabled]').length, 0, 'no longer disabled');
}); });
click('.perform-suspend'); click('.perform-suspend');
andThen(() => { andThen(() => {