DEV: Convert `flag` modal to component-based API (#23279)
# Topic Flag <img width="587" alt="Screenshot 2023-08-28 at 10 53 12 AM" src="https://github.com/discourse/discourse/assets/50783505/6ffe4e47-05a6-406c-9d1b-899ff4d5c2c9"> # Post Flag <img width="620" alt="Screenshot 2023-08-28 at 10 52 44 AM" src="https://github.com/discourse/discourse/assets/50783505/1f893916-b62f-4825-a337-4c0e0e4ce3af"> # Chat Flag <img width="648" alt="Screenshot 2023-08-28 at 10 52 31 AM" src="https://github.com/discourse/discourse/assets/50783505/e79444e8-a8b1-4f05-9b47-03d425bc9085">
This commit is contained in:
parent
cf71f8358e
commit
026cd3e532
|
@ -20,14 +20,21 @@
|
||||||
@value={{this.message}}
|
@value={{this.message}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="custom-message-length {{this.customMessageLengthClasses}}"
|
class={{concat-class
|
||||||
>{{this.customMessageLength}}</div>
|
"custom-message-length"
|
||||||
|
this.customMessageLengthClasses
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{{this.customMessageLength}}
|
||||||
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{{#if this.staffFlagsAvailable}}
|
{{#if @includeSeparator}}
|
||||||
<hr />
|
<hr />
|
||||||
|
{{/if}}
|
||||||
|
{{#if this.staffFlagsAvailable}}
|
||||||
<h3>{{i18n "flagging.notify_staff"}}</h3>
|
<h3>{{i18n "flagging.notify_staff"}}</h3>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
@ -53,8 +60,13 @@
|
||||||
@value={{this.message}}
|
@value={{this.message}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="custom-message-length {{this.customMessageLengthClasses}}"
|
class={{concat-class
|
||||||
>{{this.customMessageLength}}</div>
|
"custom-message-length"
|
||||||
|
this.customMessageLengthClasses
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{{this.customMessageLength}}
|
||||||
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
<DModal
|
||||||
|
class="flag-modal"
|
||||||
|
@bodyClass="flag-modal-body"
|
||||||
|
@title={{i18n this.title}}
|
||||||
|
@submitOnEnter={{false}}
|
||||||
|
@closeModal={{@closeModal}}
|
||||||
|
{{on "keydown" this.onKeydown}}
|
||||||
|
>
|
||||||
|
<:body>
|
||||||
|
<form>
|
||||||
|
<FlagSelection
|
||||||
|
@nameKey={{this.selected.name_key}}
|
||||||
|
@flags={{this.flagsAvailable}}
|
||||||
|
as |f|
|
||||||
|
>
|
||||||
|
<FlagActionType
|
||||||
|
@flag={{f}}
|
||||||
|
@message={{this.message}}
|
||||||
|
@isWarning={{this.isWarning}}
|
||||||
|
@selectedFlag={{this.selected}}
|
||||||
|
@username={{@model.flagModel.username}}
|
||||||
|
@staffFlagsAvailable={{this.staffFlagsAvailable}}
|
||||||
|
@changePostActionType={{this.changePostActionType}}
|
||||||
|
@includeSeparator={{this.includeSeparator}}
|
||||||
|
/>
|
||||||
|
</FlagSelection>
|
||||||
|
</form>
|
||||||
|
<PluginOutlet
|
||||||
|
@name="flag-modal-bottom"
|
||||||
|
@connectorTagName="div"
|
||||||
|
@outletArgs={{hash post=@model.flagModel}}
|
||||||
|
/>
|
||||||
|
</:body>
|
||||||
|
<:footer>
|
||||||
|
<DButton
|
||||||
|
class="btn-primary"
|
||||||
|
@action={{this.createFlag}}
|
||||||
|
@disabled={{not this.submitEnabled}}
|
||||||
|
@title="flagging.submit_tooltip"
|
||||||
|
@icon={{if this.selected.is_custom_flag "envelope" "flag"}}
|
||||||
|
@label={{this.submitLabel}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{#if this.canSendWarning}}
|
||||||
|
<DButton
|
||||||
|
class="btn-danger"
|
||||||
|
@action={{this.createFlagAsWarning}}
|
||||||
|
@disabled={{not this.submitEnabled}}
|
||||||
|
@icon="exclamation-triangle"
|
||||||
|
@label="flagging.official_warning"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.canTakeAction}}
|
||||||
|
<ReviewableBundledAction
|
||||||
|
@bundle={{this.flagActions}}
|
||||||
|
@performAction={{this.takeAction}}
|
||||||
|
@reviewableUpdating={{not this.submitEnabled}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DButton
|
||||||
|
class="btn-danger"
|
||||||
|
@action={{this.flagForReview}}
|
||||||
|
@disabled={{not this.submitEnabled this.notifyModeratorsFlag}}
|
||||||
|
@icon="exclamation-triangle"
|
||||||
|
@label="flagging.flag_for_review"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.showDeleteSpammer}}
|
||||||
|
<DButton
|
||||||
|
class="btn-danger"
|
||||||
|
@action={{this.deleteSpammer}}
|
||||||
|
@disabled={{not this.submitEnabled}}
|
||||||
|
@icon="exclamation-triangle"
|
||||||
|
@label="flagging.delete_spammer"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</:footer>
|
||||||
|
</DModal>
|
|
@ -0,0 +1,208 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import I18n from "I18n";
|
||||||
|
import { MAX_MESSAGE_LENGTH } from "discourse/models/post-action-type";
|
||||||
|
import User from "discourse/models/user";
|
||||||
|
import { reload } from "discourse/helpers/page-reloader";
|
||||||
|
|
||||||
|
const NOTIFY_MODERATORS_ID = 7;
|
||||||
|
|
||||||
|
export default class Flag extends Component {
|
||||||
|
@service adminTools;
|
||||||
|
@service currentUser;
|
||||||
|
@service siteSettings;
|
||||||
|
@service site;
|
||||||
|
@service appEvents;
|
||||||
|
|
||||||
|
@tracked userDetails;
|
||||||
|
@tracked selected;
|
||||||
|
@tracked message;
|
||||||
|
@tracked isWarning = false;
|
||||||
|
@tracked spammerDetails;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
this.adminTools
|
||||||
|
?.checkSpammer(this.args.model.flagModel.user_id)
|
||||||
|
.then((result) => (this.spammerDetails = result));
|
||||||
|
}
|
||||||
|
|
||||||
|
get flagActions() {
|
||||||
|
return {
|
||||||
|
icon: "gavel",
|
||||||
|
label: I18n.t("flagging.take_action"),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: "agree_and_keep",
|
||||||
|
icon: "thumbs-up",
|
||||||
|
label: I18n.t("flagging.take_action_options.default.title"),
|
||||||
|
description: I18n.t("flagging.take_action_options.default.details"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "agree_and_suspend",
|
||||||
|
icon: "ban",
|
||||||
|
label: I18n.t("flagging.take_action_options.suspend.title"),
|
||||||
|
description: I18n.t("flagging.take_action_options.suspend.details"),
|
||||||
|
client_action: "suspend",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "agree_and_silence",
|
||||||
|
icon: "microphone-slash",
|
||||||
|
label: I18n.t("flagging.take_action_options.silence.title"),
|
||||||
|
description: I18n.t("flagging.take_action_options.silence.details"),
|
||||||
|
client_action: "silence",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get canSendWarning() {
|
||||||
|
return (
|
||||||
|
!this.args.model.flagTarget.targetsTopic() &&
|
||||||
|
this.currentUser.staff &&
|
||||||
|
this.selected?.name_key === "notify_user"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get submitLabel() {
|
||||||
|
if (this.selected?.is_custom_flag) {
|
||||||
|
return this.args.model.flagTarget.customSubmitLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.args.model.flagTarget.submitLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
get includeSeparator() {
|
||||||
|
return (
|
||||||
|
this.staffFlagsAvailable ||
|
||||||
|
this.args.model.flagTarget.includeSeparator?.()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
return this.args.model.flagTarget.title();
|
||||||
|
}
|
||||||
|
|
||||||
|
get flagsAvailable() {
|
||||||
|
return this.args.model.flagTarget.flagsAvailable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get staffFlagsAvailable() {
|
||||||
|
return this.args.model.flagModel.flagsAvailable?.length > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
get submitEnabled() {
|
||||||
|
if (!this.selected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.selected.is_custom_flag) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const len = this.message?.length || 0;
|
||||||
|
return (
|
||||||
|
len >= this.siteSettings.min_personal_message_post_length &&
|
||||||
|
len <= MAX_MESSAGE_LENGTH
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get notifyModeratorsFlag() {
|
||||||
|
return this.flagsAvailable.find((f) => f.id === NOTIFY_MODERATORS_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
get canTakeAction() {
|
||||||
|
return (
|
||||||
|
!this.args.model.flagTarget.targetsTopic() &&
|
||||||
|
!this.selected?.is_custom_flag &&
|
||||||
|
this.currentUser.staff
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onKeydown(event) {
|
||||||
|
if (
|
||||||
|
this.submitEnabled &&
|
||||||
|
event.key === "Enter" &&
|
||||||
|
(event.ctrlKey || event.metaKey)
|
||||||
|
) {
|
||||||
|
this.createFlag();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async penalize(adminToolMethod, performAction) {
|
||||||
|
if (!this.adminTools) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdBy = await User.findByUsername(
|
||||||
|
this.args.model.flagModel.username
|
||||||
|
);
|
||||||
|
const opts = { before: performAction };
|
||||||
|
|
||||||
|
if (this.args.model.flagTarget.editable()) {
|
||||||
|
opts.postId = this.args.model.flagModel.id;
|
||||||
|
opts.postEdit = this.args.model.flagModel.cooked;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.adminTools[adminToolMethod](createdBy, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async deleteSpammer() {
|
||||||
|
if (this.spammerDetails) {
|
||||||
|
await this.spammerDetails.deleteUser();
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async takeAction(actionable) {
|
||||||
|
if (actionable.client_action === "suspend") {
|
||||||
|
await this.penalize("showSuspendModal", () =>
|
||||||
|
this.createFlag({ takeAction: true, skipClose: true })
|
||||||
|
);
|
||||||
|
} else if (actionable.client_action === "silence") {
|
||||||
|
await this.penalize("showSilenceModal", () =>
|
||||||
|
this.createFlag({ takeAction: true, skipClose: true })
|
||||||
|
);
|
||||||
|
} else if (actionable.client_action) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(`No handler for ${actionable.client_action} found`);
|
||||||
|
} else {
|
||||||
|
this.args.model.setHidden();
|
||||||
|
this.createFlag({ takeAction: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
createFlag(opts = {}) {
|
||||||
|
if (this.selected.is_custom_flag) {
|
||||||
|
opts.message = this.message;
|
||||||
|
}
|
||||||
|
this.args.model.flagTarget.create(this, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
createFlagAsWarning() {
|
||||||
|
this.createFlag({ isWarning: true });
|
||||||
|
this.args.model.setHidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
flagForReview() {
|
||||||
|
this.selected ||= this.notifyModeratorsFlag;
|
||||||
|
this.createFlag({ queue_for_review: true });
|
||||||
|
this.args.model.setHidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
changePostActionType(actionType) {
|
||||||
|
this.selected = actionType;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,13 +17,12 @@
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
<DButton
|
<DButton
|
||||||
@class={{concat
|
@class={{concat-class
|
||||||
"reviewable-action "
|
"reviewable-action"
|
||||||
(dasherize this.first.id)
|
(dasherize this.first.id)
|
||||||
" "
|
|
||||||
this.first.button_class
|
this.first.button_class
|
||||||
}}
|
}}
|
||||||
@action={{action "perform" this.first}}
|
@action={{action "performFirst"}}
|
||||||
@translatedLabel={{this.first.label}}
|
@translatedLabel={{this.first.label}}
|
||||||
@disabled={{this.reviewableUpdating}}
|
@disabled={{this.reviewableUpdating}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -18,11 +18,11 @@ export default Component.extend({
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
performById(id) {
|
performById(id) {
|
||||||
this.attrs.performAction(this.get("bundle.actions").findBy("id", id));
|
this.performAction(this.get("bundle.actions").findBy("id", id));
|
||||||
},
|
},
|
||||||
|
|
||||||
perform(action) {
|
performFirst() {
|
||||||
this.attrs.performAction(action);
|
this.performAction(this.first);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,261 +0,0 @@
|
||||||
import { schedule } from "@ember/runloop";
|
|
||||||
import Controller from "@ember/controller";
|
|
||||||
import I18n from "I18n";
|
|
||||||
import { MAX_MESSAGE_LENGTH } from "discourse/models/post-action-type";
|
|
||||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
|
||||||
import { Promise } from "rsvp";
|
|
||||||
import User from "discourse/models/user";
|
|
||||||
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
|
||||||
import { not } from "@ember/object/computed";
|
|
||||||
import optionalService from "discourse/lib/optional-service";
|
|
||||||
import { classify } from "@ember/string";
|
|
||||||
|
|
||||||
export default Controller.extend(ModalFunctionality, {
|
|
||||||
adminTools: optionalService(),
|
|
||||||
userDetails: null,
|
|
||||||
selected: null,
|
|
||||||
message: null,
|
|
||||||
isWarning: false,
|
|
||||||
topicActionByName: null,
|
|
||||||
spammerDetails: null,
|
|
||||||
flagActions: null,
|
|
||||||
flagTarget: null,
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this._super(...arguments);
|
|
||||||
this.flagActions = {
|
|
||||||
icon: "gavel",
|
|
||||||
label: I18n.t("flagging.take_action"),
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
id: "agree_and_keep",
|
|
||||||
icon: "thumbs-up",
|
|
||||||
label: I18n.t("flagging.take_action_options.default.title"),
|
|
||||||
description: I18n.t("flagging.take_action_options.default.details"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "agree_and_suspend",
|
|
||||||
icon: "ban",
|
|
||||||
label: I18n.t("flagging.take_action_options.suspend.title"),
|
|
||||||
description: I18n.t("flagging.take_action_options.suspend.details"),
|
|
||||||
client_action: "suspend",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "agree_and_silence",
|
|
||||||
icon: "microphone-slash",
|
|
||||||
label: I18n.t("flagging.take_action_options.silence.title"),
|
|
||||||
description: I18n.t("flagging.take_action_options.silence.details"),
|
|
||||||
client_action: "silence",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
@bind
|
|
||||||
keyDown(event) {
|
|
||||||
// CTRL+ENTER or CMD+ENTER
|
|
||||||
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
||||||
if (this.submitEnabled) {
|
|
||||||
this.send("createFlag");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
clientSuspend(performAction) {
|
|
||||||
this._penalize("showSuspendModal", performAction);
|
|
||||||
},
|
|
||||||
|
|
||||||
clientSilence(performAction) {
|
|
||||||
this._penalize("showSilenceModal", performAction);
|
|
||||||
},
|
|
||||||
|
|
||||||
_penalize(adminToolMethod, performAction) {
|
|
||||||
if (this.adminTools) {
|
|
||||||
return User.findByUsername(this.model.username).then((createdBy) => {
|
|
||||||
const opts = { before: performAction };
|
|
||||||
|
|
||||||
if (this.flagTarget.editable()) {
|
|
||||||
opts.postId = this.model.id;
|
|
||||||
opts.postEdit = this.model.cooked;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.adminTools[adminToolMethod](createdBy, opts);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onShow() {
|
|
||||||
this.setProperties({
|
|
||||||
selected: null,
|
|
||||||
spammerDetails: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.adminTools) {
|
|
||||||
this.adminTools.checkSpammer(this.get("model.user_id")).then((result) => {
|
|
||||||
this.set("spammerDetails", result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
schedule("afterRender", () => {
|
|
||||||
const element = document.querySelector(".flag-modal");
|
|
||||||
element.addEventListener("keydown", this.keyDown);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onClose() {
|
|
||||||
const element = document.querySelector(".flag-modal");
|
|
||||||
element.removeEventListener("keydown", this.keyDown);
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("spammerDetails.canDelete", "selected.name_key")
|
|
||||||
showDeleteSpammer(canDeleteSpammer, nameKey) {
|
|
||||||
return canDeleteSpammer && nameKey === "spam";
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("flagTarget")
|
|
||||||
title(flagTarget) {
|
|
||||||
return flagTarget.title();
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed(
|
|
||||||
"post",
|
|
||||||
"flagTarget",
|
|
||||||
"model.actions_summary.@each.can_act"
|
|
||||||
)
|
|
||||||
flagsAvailable() {
|
|
||||||
return this.flagTarget.flagsAvailable(this, this.site, this.model);
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed(
|
|
||||||
"post",
|
|
||||||
"flagTarget",
|
|
||||||
"model.actions_summary.@each.can_act"
|
|
||||||
)
|
|
||||||
staffFlagsAvailable() {
|
|
||||||
return (
|
|
||||||
this.get("model.flagsAvailable") &&
|
|
||||||
this.get("model.flagsAvailable").length > 1
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("selected.is_custom_flag", "message.length")
|
|
||||||
submitEnabled() {
|
|
||||||
const selected = this.selected;
|
|
||||||
if (!selected) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selected.get("is_custom_flag")) {
|
|
||||||
const len = this.get("message.length") || 0;
|
|
||||||
return (
|
|
||||||
len >= this.siteSettings.min_personal_message_post_length &&
|
|
||||||
len <= MAX_MESSAGE_LENGTH
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
submitDisabled: not("submitEnabled"),
|
|
||||||
cantFlagForReview: not("notifyModeratorsFlag"),
|
|
||||||
|
|
||||||
@discourseComputed("flagsAvailable")
|
|
||||||
notifyModeratorsFlag(flagsAvailable) {
|
|
||||||
const notifyModeratorsID = 7;
|
|
||||||
return flagsAvailable.find((f) => f.id === notifyModeratorsID);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Staff accounts can "take action"
|
|
||||||
@discourseComputed("flagTarget", "selected.is_custom_flag")
|
|
||||||
canTakeAction(flagTarget, isCustomFlag) {
|
|
||||||
return (
|
|
||||||
!flagTarget.targetsTopic() &&
|
|
||||||
!isCustomFlag &&
|
|
||||||
this.currentUser.get("staff")
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("selected.is_custom_flag")
|
|
||||||
submitIcon(isCustomFlag) {
|
|
||||||
return isCustomFlag ? "envelope" : "flag";
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("selected.is_custom_flag", "flagTarget")
|
|
||||||
submitLabel(isCustomFlag, flagTarget) {
|
|
||||||
if (isCustomFlag) {
|
|
||||||
return flagTarget.customSubmitLabel();
|
|
||||||
}
|
|
||||||
|
|
||||||
return flagTarget.submitLabel();
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
deleteSpammer() {
|
|
||||||
let details = this.spammerDetails;
|
|
||||||
if (details) {
|
|
||||||
details.deleteUser().then(() => window.location.reload());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
takeAction(action) {
|
|
||||||
let performAction = (o = {}) => {
|
|
||||||
o.takeAction = true;
|
|
||||||
this.send("createFlag", o);
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (action.client_action) {
|
|
||||||
let actionMethod = this[`client${classify(action.client_action)}`];
|
|
||||||
if (actionMethod) {
|
|
||||||
return actionMethod.call(this, () =>
|
|
||||||
performAction({ skipClose: true })
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(`No handler for ${action.client_action} found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.set("model.hidden", true);
|
|
||||||
return performAction();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
createFlag(opts) {
|
|
||||||
const params = opts || {};
|
|
||||||
|
|
||||||
if (this.get("selected.is_custom_flag")) {
|
|
||||||
params.message = this.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.flagTarget.create(this, params);
|
|
||||||
},
|
|
||||||
|
|
||||||
createFlagAsWarning() {
|
|
||||||
this.send("createFlag", { isWarning: true });
|
|
||||||
this.set("model.hidden", true);
|
|
||||||
},
|
|
||||||
|
|
||||||
flagForReview() {
|
|
||||||
if (!this.selected) {
|
|
||||||
this.set("selected", this.get("notifyModeratorsFlag"));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.send("createFlag", { queue_for_review: true });
|
|
||||||
this.set("model.hidden", true);
|
|
||||||
},
|
|
||||||
|
|
||||||
changePostActionType(action) {
|
|
||||||
this.set("selected", action);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("flagTarget", "selected.name_key")
|
|
||||||
canSendWarning(flagTarget, nameKey) {
|
|
||||||
return (
|
|
||||||
!flagTarget.targetsTopic() &&
|
|
||||||
this.currentUser.get("staff") &&
|
|
||||||
nameKey === "notify_user"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -9,41 +9,24 @@ export default class Flag {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
create(controller, opts) {
|
create(flagModal, opts) {
|
||||||
// an instance of ActionSummary
|
// an instance of ActionSummary
|
||||||
let postAction = this.postActionFor(controller);
|
const postAction = this.postActionFor(flagModal);
|
||||||
|
flagModal.appEvents.trigger(
|
||||||
controller.appEvents.trigger(
|
|
||||||
this.flagCreatedEvent,
|
this.flagCreatedEvent,
|
||||||
controller.model,
|
flagModal.args.model.flagModel,
|
||||||
postAction,
|
postAction,
|
||||||
opts
|
opts
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.send("hideModal");
|
flagModal.args.closeModal();
|
||||||
|
|
||||||
postAction
|
postAction
|
||||||
.act(controller.model, opts)
|
.act(flagModal.args.model.flagModel, opts)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (controller.isDestroying || controller.isDestroyed) {
|
flagModal.appEvents.trigger("post-stream:refresh", {
|
||||||
return;
|
id: flagModal.args.model.flagModel.id,
|
||||||
}
|
|
||||||
|
|
||||||
if (!opts.skipClose) {
|
|
||||||
controller.send("closeModal");
|
|
||||||
}
|
|
||||||
if (opts.message) {
|
|
||||||
controller.set("message", "");
|
|
||||||
}
|
|
||||||
controller.appEvents.trigger("post-stream:refresh", {
|
|
||||||
id: controller.get("model.id"),
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => popupAjaxError(error));
|
||||||
if (!controller.isDestroying && !controller.isDestroyed) {
|
|
||||||
controller.send("closeModal");
|
|
||||||
}
|
|
||||||
popupAjaxError(error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,8 @@ export default class PostFlag extends Flag {
|
||||||
return "post:flag-created";
|
return "post:flag-created";
|
||||||
}
|
}
|
||||||
|
|
||||||
flagsAvailable(_flagController, _site, model) {
|
flagsAvailable(flagModal) {
|
||||||
let flagsAvailable = model.flagsAvailable;
|
let flagsAvailable = flagModal.args.model.flagModel.flagsAvailable;
|
||||||
|
|
||||||
// "message user" option should be at the top
|
// "message user" option should be at the top
|
||||||
const notifyUserIndex = flagsAvailable.indexOf(
|
const notifyUserIndex = flagsAvailable.indexOf(
|
||||||
|
@ -34,9 +34,10 @@ export default class PostFlag extends Flag {
|
||||||
return flagsAvailable;
|
return flagsAvailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
postActionFor(controller) {
|
postActionFor(flagModal) {
|
||||||
return controller
|
return flagModal.args.model.flagModel.actions_summary.findBy(
|
||||||
.get("model.actions_summary")
|
"id",
|
||||||
.findBy("id", controller.get("selected.id"));
|
flagModal.selected.id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,24 +23,24 @@ export default class TopicFlag extends Flag {
|
||||||
return "topic:flag-created";
|
return "topic:flag-created";
|
||||||
}
|
}
|
||||||
|
|
||||||
flagsAvailable(flagController, site, model) {
|
flagsAvailable(flagModal) {
|
||||||
let lookup = EmberObject.create();
|
let lookup = EmberObject.create();
|
||||||
|
|
||||||
model.actions_summary.forEach((a) => {
|
flagModal.args.model.flagModel.actions_summary.forEach((a) => {
|
||||||
a.flagTopic = model;
|
a.flagTopic = flagModal.args.model.flagModel;
|
||||||
a.actionType = site.topicFlagTypeById(a.id);
|
a.actionType = flagModal.site.topicFlagTypeById(a.id);
|
||||||
lookup.set(a.actionType.name_key, ActionSummary.create(a));
|
lookup.set(a.actionType.name_key, ActionSummary.create(a));
|
||||||
});
|
});
|
||||||
flagController.set("topicActionByName", lookup);
|
flagModal.topicActionByName = lookup;
|
||||||
|
|
||||||
return site.topic_flag_types.filter((item) => {
|
return flagModal.site.topic_flag_types.filter((item) => {
|
||||||
return model.actions_summary.some((a) => {
|
return flagModal.args.model.flagModel.actions_summary.some((a) => {
|
||||||
return a.id === item.id && a.can_act;
|
return a.id === item.id && a.can_act;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
postActionFor(controller) {
|
postActionFor(flagModal) {
|
||||||
return controller.get(`topicActionByName.${controller.selected.name_key}`);
|
return flagModal.topicActionByName[flagModal.selected.name_key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import EditSlowModeModal from "discourse/components/modal/edit-slow-mode";
|
||||||
import ChangeTimestampModal from "discourse/components/modal/change-timestamp";
|
import ChangeTimestampModal from "discourse/components/modal/change-timestamp";
|
||||||
import EditTopicTimerModal from "discourse/components/modal/edit-topic-timer";
|
import EditTopicTimerModal from "discourse/components/modal/edit-topic-timer";
|
||||||
import FeatureTopicModal from "discourse/components/modal/feature-topic";
|
import FeatureTopicModal from "discourse/components/modal/feature-topic";
|
||||||
|
import FlagModal from "discourse/components/modal/flag";
|
||||||
|
|
||||||
const SCROLL_DELAY = 500;
|
const SCROLL_DELAY = 500;
|
||||||
|
|
||||||
|
@ -103,15 +104,25 @@ const TopicRoute = DiscourseRoute.extend({
|
||||||
|
|
||||||
@action
|
@action
|
||||||
showFlags(model) {
|
showFlags(model) {
|
||||||
let controller = showModal("flag", { model });
|
this.modal.show(FlagModal, {
|
||||||
controller.setProperties({ flagTarget: new PostFlag() });
|
model: {
|
||||||
|
flagTarget: new PostFlag(),
|
||||||
|
flagModel: model,
|
||||||
|
setHidden: () => model.set("hidden", true),
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
showFlagTopic() {
|
showFlagTopic() {
|
||||||
const model = this.modelFor("topic");
|
const model = this.modelFor("topic");
|
||||||
let controller = showModal("flag", { model });
|
this.modal.show(FlagModal, {
|
||||||
controller.setProperties({ flagTarget: new TopicFlag() });
|
model: {
|
||||||
|
flagTarget: new TopicFlag(),
|
||||||
|
flagModel: model,
|
||||||
|
setHidden: () => model.set("hidden", true),
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|
|
@ -18,7 +18,6 @@ const KNOWN_LEGACY_MODALS = [
|
||||||
"create-account",
|
"create-account",
|
||||||
"create-invite-bulk",
|
"create-invite-bulk",
|
||||||
"create-invite",
|
"create-invite",
|
||||||
"flag",
|
|
||||||
"grant-badge",
|
"grant-badge",
|
||||||
"group-default-notifications",
|
"group-default-notifications",
|
||||||
"login",
|
"login",
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
<DModalBody
|
|
||||||
@class="flag-modal-body"
|
|
||||||
@title={{this.title}}
|
|
||||||
@submitOnEnter={{false}}
|
|
||||||
>
|
|
||||||
<form>
|
|
||||||
<FlagSelection
|
|
||||||
@nameKey={{this.selected.name_key}}
|
|
||||||
@flags={{this.flagsAvailable}}
|
|
||||||
as |f|
|
|
||||||
>
|
|
||||||
<FlagActionType
|
|
||||||
@flag={{f}}
|
|
||||||
@message={{this.message}}
|
|
||||||
@isWarning={{this.isWarning}}
|
|
||||||
@selectedFlag={{this.selected}}
|
|
||||||
@username={{this.model.username}}
|
|
||||||
@staffFlagsAvailable={{this.staffFlagsAvailable}}
|
|
||||||
@changePostActionType={{action "changePostActionType"}}
|
|
||||||
/>
|
|
||||||
</FlagSelection>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<PluginOutlet
|
|
||||||
@name="flag-modal-bottom"
|
|
||||||
@connectorTagName="div"
|
|
||||||
@outletArgs={{hash post=this.model}}
|
|
||||||
/>
|
|
||||||
</DModalBody>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<DButton
|
|
||||||
@class="btn-primary"
|
|
||||||
@action={{action "createFlag"}}
|
|
||||||
@disabled={{this.submitDisabled}}
|
|
||||||
@title="flagging.submit_tooltip"
|
|
||||||
@icon={{this.submitIcon}}
|
|
||||||
@label={{this.submitLabel}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{{#if this.canSendWarning}}
|
|
||||||
<DButton
|
|
||||||
@class="btn-danger"
|
|
||||||
@action={{action "createFlagAsWarning"}}
|
|
||||||
@disabled={{this.submitDisabled}}
|
|
||||||
@icon="exclamation-triangle"
|
|
||||||
@label="flagging.official_warning"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.canTakeAction}}
|
|
||||||
<ReviewableBundledAction
|
|
||||||
@bundle={{this.flagActions}}
|
|
||||||
@performAction={{action "takeAction"}}
|
|
||||||
@reviewableUpdating={{this.submitDisabled}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DButton
|
|
||||||
@class="btn-danger"
|
|
||||||
@action={{action "flagForReview"}}
|
|
||||||
@disabled={{or this.submitDisabled this.cantFlagForReview}}
|
|
||||||
@icon="exclamation-triangle"
|
|
||||||
@label="flagging.flag_for_review"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if this.showDeleteSpammer}}
|
|
||||||
<DButton
|
|
||||||
@class="btn-danger"
|
|
||||||
@action={{action "deleteSpammer"}}
|
|
||||||
@disabled={{this.submitDisabled}}
|
|
||||||
@icon="exclamation-triangle"
|
|
||||||
@label="flagging.delete_spammer"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
|
@ -158,31 +158,31 @@ acceptance("flagging", function (needs) {
|
||||||
await visit("/t/internationalization-localization/280");
|
await visit("/t/internationalization-localization/280");
|
||||||
await openFlagModal();
|
await openFlagModal();
|
||||||
|
|
||||||
const modal = query("#discourse-modal");
|
const modal = query(".d-modal");
|
||||||
await pressEnter(modal, "ctrlKey");
|
await pressEnter(modal, "ctrlKey");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
exists("#discourse-modal:visible"),
|
exists(".d-modal:visible"),
|
||||||
"The modal wasn't closed because the accept button was disabled"
|
"The modal wasn't closed because the accept button was disabled"
|
||||||
);
|
);
|
||||||
|
|
||||||
await click("#radio_inappropriate"); // this enables the accept button
|
await click("#radio_inappropriate"); // this enables the accept button
|
||||||
await pressEnter(modal, "ctrlKey");
|
await pressEnter(modal, "ctrlKey");
|
||||||
assert.ok(!exists("#discourse-modal:visible"), "The modal was closed");
|
assert.ok(!exists(".d-modal:visible"), "The modal was closed");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("CMD or WINDOWS-KEY + ENTER accepts the modal", async function (assert) {
|
test("CMD or WINDOWS-KEY + ENTER accepts the modal", async function (assert) {
|
||||||
await visit("/t/internationalization-localization/280");
|
await visit("/t/internationalization-localization/280");
|
||||||
await openFlagModal();
|
await openFlagModal();
|
||||||
|
|
||||||
const modal = query("#discourse-modal");
|
const modal = query(".d-modal");
|
||||||
await pressEnter(modal, "metaKey");
|
await pressEnter(modal, "metaKey");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
exists("#discourse-modal:visible"),
|
exists(".d-modal:visible"),
|
||||||
"The modal wasn't closed because the accept button was disabled"
|
"The modal wasn't closed because the accept button was disabled"
|
||||||
);
|
);
|
||||||
|
|
||||||
await click("#radio_inappropriate"); // this enables the accept button
|
await click("#radio_inappropriate"); // this enables the accept button
|
||||||
await pressEnter(modal, "ctrlKey");
|
await pressEnter(modal, "ctrlKey");
|
||||||
assert.ok(!exists("#discourse-modal:visible"), "The modal was closed");
|
assert.ok(!exists(".d-modal:visible"), "The modal was closed");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,6 +24,10 @@ export default class ChatMessageFlag {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
includeSeparator() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
_rewriteFlagDescriptions(flags) {
|
_rewriteFlagDescriptions(flags) {
|
||||||
return flags.map((flag) => {
|
return flags.map((flag) => {
|
||||||
flag.set(
|
flag.set(
|
||||||
|
@ -34,11 +38,13 @@ export default class ChatMessageFlag {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
flagsAvailable(_controller, site, model) {
|
flagsAvailable(flagModal) {
|
||||||
let flagsAvailable = site.flagTypes;
|
let flagsAvailable = flagModal.site.flagTypes;
|
||||||
|
|
||||||
flagsAvailable = flagsAvailable.filter((flag) => {
|
flagsAvailable = flagsAvailable.filter((flag) => {
|
||||||
return model.availableFlags.includes(flag.name_key);
|
return flagModal.args.model.flagModel.availableFlags.includes(
|
||||||
|
flag.name_key
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// "message user" option should be at the top
|
// "message user" option should be at the top
|
||||||
|
@ -55,37 +61,19 @@ export default class ChatMessageFlag {
|
||||||
return this._rewriteFlagDescriptions(flagsAvailable);
|
return this._rewriteFlagDescriptions(flagsAvailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
create(controller, opts) {
|
create(flagModal, opts) {
|
||||||
controller.send("hideModal");
|
flagModal.args.closeModal();
|
||||||
|
|
||||||
return ajax("/chat/flag", {
|
return ajax("/chat/flag", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
data: {
|
data: {
|
||||||
chat_message_id: controller.get("model.id"),
|
chat_message_id: flagModal.args.model.flagModel.id,
|
||||||
flag_type_id: controller.get("selected.id"),
|
flag_type_id: flagModal.selected.id,
|
||||||
message: opts.message,
|
message: opts.message,
|
||||||
is_warning: opts.isWarning,
|
is_warning: opts.isWarning,
|
||||||
take_action: opts.takeAction,
|
take_action: opts.takeAction,
|
||||||
queue_for_review: opts.queue_for_review,
|
queue_for_review: opts.queue_for_review,
|
||||||
},
|
},
|
||||||
})
|
}).catch((error) => popupAjaxError(error));
|
||||||
.then(() => {
|
|
||||||
if (controller.isDestroying || controller.isDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!opts.skipClose) {
|
|
||||||
controller.send("closeModal");
|
|
||||||
}
|
|
||||||
if (opts.message) {
|
|
||||||
controller.set("message", "");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (!controller.isDestroying && !controller.isDestroyed) {
|
|
||||||
controller.send("closeModal");
|
|
||||||
}
|
|
||||||
popupAjaxError(error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import showModal from "discourse/lib/show-modal";
|
|
||||||
import ChatMessageFlag from "discourse/plugins/chat/discourse/lib/chat-message-flag";
|
import ChatMessageFlag from "discourse/plugins/chat/discourse/lib/chat-message-flag";
|
||||||
import Bookmark from "discourse/models/bookmark";
|
import Bookmark from "discourse/models/bookmark";
|
||||||
import BookmarkModal from "discourse/components/modal/bookmark";
|
import BookmarkModal from "discourse/components/modal/bookmark";
|
||||||
|
@ -18,6 +17,7 @@ import { tracked } from "@glimmer/tracking";
|
||||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||||
import { MESSAGE_CONTEXT_THREAD } from "discourse/plugins/chat/discourse/components/chat-message";
|
import { MESSAGE_CONTEXT_THREAD } from "discourse/plugins/chat/discourse/components/chat-message";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
import FlagModal from "discourse/components/modal/flag";
|
||||||
|
|
||||||
const removedSecondaryActions = new Set();
|
const removedSecondaryActions = new Set();
|
||||||
|
|
||||||
|
@ -338,8 +338,13 @@ export default class ChatMessageInteractor {
|
||||||
const model = new ChatMessage(this.message.channel, this.message);
|
const model = new ChatMessage(this.message.channel, this.message);
|
||||||
model.username = this.message.user?.username;
|
model.username = this.message.user?.username;
|
||||||
model.user_id = this.message.user?.id;
|
model.user_id = this.message.user?.id;
|
||||||
const controller = showModal("flag", { model });
|
this.modal.show(FlagModal, {
|
||||||
controller.set("flagTarget", new ChatMessageFlag());
|
model: {
|
||||||
|
flagTarget: new ChatMessageFlag(),
|
||||||
|
flagModel: model,
|
||||||
|
setHidden: () => model.set("hidden", true),
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|
Loading…
Reference in New Issue