Many enhancements to the flagging / suspending interface.
This commit is contained in:
parent
f7df68c9a3
commit
8ff4104555
|
@ -4,8 +4,6 @@ import computed from 'ember-addons/ember-computed-decorators';
|
|||
export default Ember.Component.extend({
|
||||
adminTools: Ember.inject.service(),
|
||||
expanded: false,
|
||||
suspended: false,
|
||||
|
||||
tagName: 'div',
|
||||
classNameBindings: [
|
||||
':flagged-post',
|
||||
|
@ -21,12 +19,7 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
removeAfter(promise) {
|
||||
return promise.then(() => {
|
||||
this.attrs.removePost();
|
||||
}).catch(error => {
|
||||
if (error._discourse_displayed) { return; }
|
||||
bootbox.alert(I18n.t("admin.flags.error"));
|
||||
});
|
||||
return promise.then(() => this.attrs.removePost());
|
||||
},
|
||||
|
||||
_spawnModal(name, model, modalClass) {
|
||||
|
@ -36,7 +29,7 @@ export default Ember.Component.extend({
|
|||
|
||||
actions: {
|
||||
removeAfter(promise) {
|
||||
this.removeAfter(promise);
|
||||
return this.removeAfter(promise);
|
||||
},
|
||||
|
||||
disagree() {
|
||||
|
@ -58,18 +51,6 @@ export default Ember.Component.extend({
|
|||
filter: 'post',
|
||||
post_id: this.get('flaggedPost.id')
|
||||
});
|
||||
},
|
||||
|
||||
showSuspendModal() {
|
||||
let post = this.get('flaggedPost');
|
||||
let user = post.get('user');
|
||||
this.get('adminTools').showSuspendModal(
|
||||
user,
|
||||
{
|
||||
post,
|
||||
successCallback: result => this.set('suspended', result.suspended)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const ACTIONS = ['delete', 'edit', 'none'];
|
||||
export default Ember.Component.extend({
|
||||
postAction: null,
|
||||
postEdit: null,
|
||||
|
||||
@computed
|
||||
penaltyActions() {
|
||||
return ACTIONS.map(id => {
|
||||
return { id, name: I18n.t(`admin.user.penalty_post_${id}`) };
|
||||
});
|
||||
},
|
||||
|
||||
editing: Ember.computed.equal('postAction', 'edit'),
|
||||
|
||||
actions: {
|
||||
penaltyChanged() {
|
||||
let postAction = this.get('postAction');
|
||||
|
||||
// If we switch to edit mode, jump to the edit textarea
|
||||
if (postAction === 'edit') {
|
||||
Ember.run.scheduleOnce('afterRender', () => {
|
||||
let $elem = this.$();
|
||||
let body = $elem.closest('.modal-body');
|
||||
body.scrollTop(body.height());
|
||||
$elem.find('.post-editor').focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,26 +1,14 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import PenaltyController from 'admin/mixins/penalty-controller';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
export default Ember.Controller.extend(PenaltyController, {
|
||||
silenceUntil: null,
|
||||
reason: null,
|
||||
message: null,
|
||||
silencing: false,
|
||||
user: null,
|
||||
post: null,
|
||||
successCallback: null,
|
||||
|
||||
onShow() {
|
||||
this.setProperties({
|
||||
silenceUntil: null,
|
||||
reason: null,
|
||||
message: null,
|
||||
silencing: false,
|
||||
loadingUser: true,
|
||||
post: null,
|
||||
successCallback: null,
|
||||
});
|
||||
this.resetModal();
|
||||
this.setProperties({ silenceUntil: null, silencing: false });
|
||||
},
|
||||
|
||||
@computed('silenceUntil', 'reason', 'silencing')
|
||||
|
@ -33,18 +21,16 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
if (this.get('submitDisabled')) { return; }
|
||||
|
||||
this.set('silencing', true);
|
||||
this.get('user').silence({
|
||||
silenced_till: this.get('silenceUntil'),
|
||||
reason: this.get('reason'),
|
||||
message: this.get('message'),
|
||||
post_id: this.get('post.id')
|
||||
}).then(result => {
|
||||
this.send('closeModal');
|
||||
let callback = this.get('successCallback');
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
}).catch(popupAjaxError).finally(() => this.set('silencing', false));
|
||||
this.penalize(() => {
|
||||
return this.get('user').silence({
|
||||
silenced_till: this.get('silenceUntil'),
|
||||
reason: this.get('reason'),
|
||||
message: this.get('message'),
|
||||
post_id: this.get('post.id'),
|
||||
post_action: this.get('postAction'),
|
||||
post_edit: this.get('postEdit')
|
||||
});
|
||||
}).finally(() => this.set('silencing', false));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,26 +1,13 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import PenaltyController from 'admin/mixins/penalty-controller';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
export default Ember.Controller.extend(PenaltyController, {
|
||||
suspendUntil: null,
|
||||
reason: null,
|
||||
message: null,
|
||||
suspending: false,
|
||||
user: null,
|
||||
post: null,
|
||||
successCallback: null,
|
||||
|
||||
onShow() {
|
||||
this.setProperties({
|
||||
suspendUntil: null,
|
||||
reason: null,
|
||||
message: null,
|
||||
suspending: false,
|
||||
loadingUser: true,
|
||||
post: null,
|
||||
successCallback: null,
|
||||
});
|
||||
this.resetModal();
|
||||
this.setProperties({ suspendUntil: null, suspending: false });
|
||||
},
|
||||
|
||||
@computed('suspendUntil', 'reason', 'suspending')
|
||||
|
@ -33,19 +20,17 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
if (this.get('submitDisabled')) { return; }
|
||||
|
||||
this.set('suspending', true);
|
||||
this.get('user').suspend({
|
||||
suspend_until: this.get('suspendUntil'),
|
||||
reason: this.get('reason'),
|
||||
message: this.get('message'),
|
||||
post_id: this.get('post.id')
|
||||
}).then(result => {
|
||||
this.send('closeModal');
|
||||
let callback = this.get('successCallback');
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
}).catch(popupAjaxError).finally(() => this.set('suspending', false));
|
||||
|
||||
this.penalize(() => {
|
||||
return this.get('user').suspend({
|
||||
suspend_until: this.get('suspendUntil'),
|
||||
reason: this.get('reason'),
|
||||
message: this.get('message'),
|
||||
post_id: this.get('post.id'),
|
||||
post_action: this.get('postAction'),
|
||||
post_edit: this.get('postEdit')
|
||||
});
|
||||
}).finally(() => this.set('suspending', false));
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Mixin.create(ModalFunctionality, {
|
||||
reason: null,
|
||||
message: null,
|
||||
postEdit: null,
|
||||
postAction: null,
|
||||
user: null,
|
||||
post: null,
|
||||
successCallback: null,
|
||||
|
||||
resetModal() {
|
||||
this.setProperties({
|
||||
reason: null,
|
||||
message: null,
|
||||
loadingUser: true,
|
||||
post: null,
|
||||
postEdit: null,
|
||||
postAction: 'delete',
|
||||
before: null,
|
||||
successCallback: null
|
||||
});
|
||||
},
|
||||
|
||||
penalize(cb) {
|
||||
let before = this.get('before');
|
||||
let promise = before ? before() : Ember.RSVP.resolve();
|
||||
|
||||
return promise
|
||||
.then(() => cb())
|
||||
.then(result => {
|
||||
this.send('closeModal');
|
||||
let callback = this.get('successCallback');
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
});
|
|
@ -52,7 +52,10 @@ export default Ember.Service.extend({
|
|||
modalClass: `${type}-user-modal`
|
||||
});
|
||||
if (opts.post) {
|
||||
controller.set('post', opts.post);
|
||||
controller.setProperties({
|
||||
post: opts.post,
|
||||
postEdit: opts.post.get('raw')
|
||||
});
|
||||
}
|
||||
|
||||
return (user.adminUserView ?
|
||||
|
@ -62,6 +65,7 @@ export default Ember.Service.extend({
|
|||
controller.setProperties({
|
||||
user: loadedUser,
|
||||
loadingUser: false,
|
||||
before: opts.before,
|
||||
successCallback: opts.successCallback
|
||||
});
|
||||
});
|
||||
|
|
|
@ -68,12 +68,6 @@
|
|||
|
||||
{{flag-user-lists flaggedPost=flaggedPost showResolvedBy=showResolvedBy}}
|
||||
|
||||
{{#if suspended}}
|
||||
<div class='suspended-message'>
|
||||
{{i18n "admin.flags.suspended_for_post"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class='flagged-post-controls'>
|
||||
{{#if canAct}}
|
||||
{{admin-agree-flag-dropdown
|
||||
|
@ -106,15 +100,6 @@
|
|||
{{admin-delete-flag-dropdown
|
||||
post=flaggedPost
|
||||
removeAfter=(action "removeAfter")}}
|
||||
|
||||
{{#unless suspended}}
|
||||
{{d-button
|
||||
class="btn-danger suspend-user"
|
||||
icon="ban"
|
||||
label="admin.flags.suspend_user"
|
||||
title="admin.flags.suspend_user_title"
|
||||
action=(action "showSuspendModal")}}
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
{{d-button
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<div class='penalty-post-controls'>
|
||||
<label>
|
||||
<div class='penalty-post-label'>
|
||||
{{{i18n 'admin.user.penalty_post_actions'}}}
|
||||
</div>
|
||||
</label>
|
||||
{{combo-box value=postAction content=penaltyActions onSelect=(action "penaltyChanged")}}
|
||||
</div>
|
||||
|
||||
{{#if editing}}
|
||||
<div class='penalty-post-edit'>
|
||||
{{textarea
|
||||
value=postEdit
|
||||
class="post-editor"}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -12,6 +12,12 @@
|
|||
</div>
|
||||
|
||||
{{silence-details reason=reason message=message}}
|
||||
{{#if post}}
|
||||
{{penalty-post-action
|
||||
post=post
|
||||
postAction=postAction
|
||||
postEdit=postEdit}}
|
||||
{{/if}}
|
||||
|
||||
{{/conditional-loading-spinner}}
|
||||
|
||||
|
|
|
@ -13,6 +13,13 @@
|
|||
</div>
|
||||
{{suspension-details reason=reason message=message}}
|
||||
|
||||
{{#if post}}
|
||||
{{penalty-post-action
|
||||
post=post
|
||||
postAction=postAction
|
||||
postEdit=postEdit}}
|
||||
{{/if}}
|
||||
|
||||
{{else}}
|
||||
<div class='cant-suspend'>
|
||||
{{i18n "admin.user.cant_suspend"}}
|
||||
|
|
|
@ -54,9 +54,11 @@ export function throwAjaxError(undoCallback) {
|
|||
}
|
||||
|
||||
export function popupAjaxError(error) {
|
||||
if (error && error._discourse_displayed) { return; }
|
||||
bootbox.alert(extractError(error));
|
||||
|
||||
error._discourse_displayed = true;
|
||||
|
||||
// We re-throw in a catch to not swallow the exception
|
||||
throw error;
|
||||
}
|
||||
|
|
|
@ -55,6 +55,22 @@ export default DropdownSelectBox.extend({
|
|||
label: I18n.t("admin.flags.agree_flag"),
|
||||
});
|
||||
|
||||
content.push({
|
||||
icon: 'ban',
|
||||
id: 'confirm-agree-suspend',
|
||||
description: I18n.t('admin.flags.agree_flag_suspend_title'),
|
||||
action: () => this.send("showSuspendModal"),
|
||||
label: I18n.t("admin.flags.agree_flag_suspend"),
|
||||
});
|
||||
|
||||
content.push({
|
||||
icon: 'microphone-slash',
|
||||
id: 'confirm-agree-silence',
|
||||
description: I18n.t('admin.flags.agree_flag_silence_title'),
|
||||
action: () => this.send("showSilenceModal"),
|
||||
label: I18n.t("admin.flags.agree_flag_silence"),
|
||||
});
|
||||
|
||||
if (canDeleteSpammer) {
|
||||
content.push({
|
||||
title: I18n.t("admin.flags.delete_spammer_title"),
|
||||
|
@ -79,6 +95,28 @@ export default DropdownSelectBox.extend({
|
|||
this.attrs.removeAfter(spammerDetails.deleteUser());
|
||||
},
|
||||
|
||||
showSuspendModal() {
|
||||
let post = this.get('post');
|
||||
let user = post.get('user');
|
||||
this.get('adminTools').showSuspendModal(user, {
|
||||
post,
|
||||
before: () => {
|
||||
return this.attrs.removeAfter(post.agreeFlags('suspended'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showSilenceModal() {
|
||||
let post = this.get('post');
|
||||
let user = post.get('user');
|
||||
this.get('adminTools').showSilenceModal(user, {
|
||||
post,
|
||||
before: () => {
|
||||
return this.attrs.removeAfter(post.agreeFlags('silenced'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
perform(action) {
|
||||
let flaggedPost = this.get("post");
|
||||
this.attrs.removeAfter(flaggedPost.agreeFlags(action));
|
||||
|
|
|
@ -21,3 +21,13 @@
|
|||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
.penalty-post-edit {
|
||||
margin-top: 1em;
|
||||
|
||||
textarea {
|
||||
height: 10em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,6 +93,8 @@ class Admin::UsersController < Admin::AdminController
|
|||
suspended_at: DateTime.now
|
||||
)
|
||||
|
||||
perform_post_action
|
||||
|
||||
render_json_dump(
|
||||
suspension: {
|
||||
suspended: true,
|
||||
|
@ -297,6 +299,7 @@ class Admin::UsersController < Admin::AdminController
|
|||
user_history_id: silencer.user_history.id
|
||||
)
|
||||
end
|
||||
perform_post_action
|
||||
|
||||
render_json_dump(
|
||||
silence: {
|
||||
|
@ -467,6 +470,27 @@ class Admin::UsersController < Admin::AdminController
|
|||
|
||||
private
|
||||
|
||||
def perform_post_action
|
||||
return unless params[:post_id].present? &&
|
||||
params[:post_action].present?
|
||||
|
||||
if post = Post.where(id: params[:post_id]).first
|
||||
case params[:post_action]
|
||||
when 'delete'
|
||||
PostDestroyer.new(current_user, post).destroy
|
||||
when 'edit'
|
||||
revisor = PostRevisor.new(post)
|
||||
|
||||
# Take what the moderator edited in as gospel
|
||||
revisor.revise!(
|
||||
current_user,
|
||||
{ raw: params[:post_edit] },
|
||||
skip_validations: true, skip_revision: true
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_user
|
||||
@user = User.find_by(id: params[:user_id])
|
||||
end
|
||||
|
|
|
@ -2695,7 +2695,12 @@ en:
|
|||
agree_flag_hide_post_title: "Hide this post and automatically send the user a message urging them to edit it."
|
||||
agree_flag_restore_post: "Agree and Restore Post"
|
||||
agree_flag_restore_post_title: "Restore the post so that all users can see it."
|
||||
agree_flag: "Agree and Keep Post"
|
||||
agree_flag_suspend: "Suspend User"
|
||||
agree_flag_suspend_title: "Agree with flag and suspend the user."
|
||||
agree_flag_silence: "Silence User"
|
||||
agree_flag_silence_title: "Agree with flag and silence the user."
|
||||
|
||||
agree_flag: "Keep Post"
|
||||
agree_flag_title: "Agree with flag and keep the post unchanged."
|
||||
ignore_flag: "Ignore"
|
||||
ignore_flag_title: "Remove this flag; it requires no action at this time."
|
||||
|
@ -2729,7 +2734,6 @@ en:
|
|||
system: "System"
|
||||
error: "Something went wrong"
|
||||
reply_message: "Reply"
|
||||
suspended_for_post: "The user was suspended for this post."
|
||||
no_results: "There are no flagged posts."
|
||||
topic_flagged: "This <strong>topic</strong> has been flagged."
|
||||
show_full: "show full post"
|
||||
|
@ -3392,6 +3396,10 @@ en:
|
|||
suspended_until: "(until %{until})"
|
||||
cant_suspend: "This user cannot be suspended."
|
||||
delete_all_posts: "Delete all posts"
|
||||
penalty_post_actions: "What would you like to do with the associated post?"
|
||||
penalty_post_delete: "Delete the post"
|
||||
penalty_post_edit: "Edit the post"
|
||||
penalty_post_none: "Do nothing"
|
||||
|
||||
# keys ending with _MF use message format, see https://meta.discourse.org/t/message-format-support-for-localization/7035 for details
|
||||
delete_all_posts_confirm_MF: "You are about to delete {POSTS, plural, one {1 post} other {# posts}} and {TOPICS, plural, one {1 topic} other {# topics}}. Are you sure?"
|
||||
|
|
|
@ -31,6 +31,7 @@ module FlagQuery
|
|||
posts = SqlBuilder.new("
|
||||
SELECT p.id,
|
||||
p.cooked,
|
||||
p.raw,
|
||||
p.user_id,
|
||||
p.topic_id,
|
||||
p.post_number,
|
||||
|
|
|
@ -148,24 +148,42 @@ describe Admin::UsersController do
|
|||
expect(log.details).to match(/because I said so/)
|
||||
end
|
||||
|
||||
it "can have an associated post" do
|
||||
post = Fabricate(:post)
|
||||
|
||||
put(
|
||||
:suspend,
|
||||
params: {
|
||||
user_id: user.id,
|
||||
context "with an associated post" do
|
||||
let(:post) { Fabricate(:post) }
|
||||
let(:suspend_params) do
|
||||
{ user_id: user.id,
|
||||
suspend_until: 5.hours.from_now,
|
||||
reason: "because of this post",
|
||||
post_id: post.id,
|
||||
format: :json
|
||||
}
|
||||
)
|
||||
expect(response).to be_success
|
||||
format: :json }
|
||||
end
|
||||
|
||||
log = UserHistory.where(target_user_id: user.id).order('id desc').first
|
||||
expect(log).to be_present
|
||||
expect(log.post_id).to eq(post.id)
|
||||
it "can have an associated post" do
|
||||
put(:suspend, params: suspend_params)
|
||||
expect(response).to be_success
|
||||
|
||||
log = UserHistory.where(target_user_id: user.id).order('id desc').first
|
||||
expect(log).to be_present
|
||||
expect(log.post_id).to eq(post.id)
|
||||
end
|
||||
|
||||
it "can delete an associated post" do
|
||||
put(:suspend, params: suspend_params.merge(post_action: 'delete'))
|
||||
post.reload
|
||||
expect(post.deleted_at).to be_present
|
||||
expect(response).to be_success
|
||||
end
|
||||
|
||||
it "can edit an associated post" do
|
||||
put(:suspend, params: suspend_params.merge(
|
||||
post_action: 'edit',
|
||||
post_edit: 'this is the edited content'
|
||||
))
|
||||
post.reload
|
||||
expect(post.deleted_at).to be_blank
|
||||
expect(post.raw).to eq("this is the edited content")
|
||||
expect(response).to be_success
|
||||
end
|
||||
end
|
||||
|
||||
it "can send a message to the user" do
|
||||
|
|
|
@ -104,15 +104,6 @@ QUnit.test("flagged posts - delete + deleteSpammer", assert => {
|
|||
});
|
||||
});
|
||||
|
||||
QUnit.test("flagged posts - suspend", assert => {
|
||||
visit("/admin/flags/active");
|
||||
click('.suspend-user');
|
||||
andThen(() => {
|
||||
assert.equal(find('.suspend-user-modal:visible').length, 1);
|
||||
assert.equal(find('.suspend-user-modal .cant-suspend').length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("topics with flags", assert => {
|
||||
visit("/admin/flags/topics");
|
||||
andThen(() => {
|
||||
|
|
Loading…
Reference in New Issue