mirror of
https://github.com/discourse/discourse.git
synced 2024-11-21 16:38:15 -06:00
FEATURE: created edit and delete flags (#27484)
Allow admins to create edit and delete flags.
This commit is contained in:
parent
a86590ffd6
commit
c3fadc7330
@ -6,9 +6,10 @@ import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import AdminConfigHeader from "admin/components/admin-config-header";
|
||||
import AdminFlagItem from "admin/components/admin-flag-item";
|
||||
|
||||
export default class AdminFlags extends Component {
|
||||
export default class AdminConfigAreasFlags extends Component {
|
||||
@service site;
|
||||
@tracked flags = this.site.flagTypes;
|
||||
|
||||
@ -46,19 +47,39 @@ export default class AdminFlags extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
deleteFlagCallback(flag) {
|
||||
return ajax(`/admin/config/flags/${flag.id}`, {
|
||||
type: "DELETE",
|
||||
})
|
||||
.then(() => {
|
||||
this.flags.removeObject(flag);
|
||||
})
|
||||
.catch((error) => popupAjaxError(error));
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="container admin-flags">
|
||||
<h1>{{i18n "admin.flags.title"}}</h1>
|
||||
<table class="flags grid">
|
||||
<AdminConfigHeader
|
||||
@name="flags"
|
||||
@heading="admin.config_areas.flags.header"
|
||||
@subheading="admin.config_areas.flags.subheader"
|
||||
@primaryActionRoute="adminConfig.flags.new"
|
||||
@primaryActionCssClass="admin-flags__header-add-flag"
|
||||
@primaryActionIcon="plus"
|
||||
@primaryActionLabel="admin.config_areas.flags.add"
|
||||
/>
|
||||
<table class="admin-flags__items grid">
|
||||
<thead>
|
||||
<th>{{i18n "admin.flags.description"}}</th>
|
||||
<th>{{i18n "admin.flags.enabled"}}</th>
|
||||
<th>{{i18n "admin.config_areas.flags.description"}}</th>
|
||||
<th>{{i18n "admin.config_areas.flags.enabled"}}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.flags as |flag|}}
|
||||
<AdminFlagItem
|
||||
@flag={{flag}}
|
||||
@moveFlagCallback={{this.moveFlagCallback}}
|
||||
@deleteFlagCallback={{this.deleteFlagCallback}}
|
||||
@isFirstFlag={{this.isFirstFlag flag}}
|
||||
@isLastFlag={{this.isLastFlag flag}}
|
||||
/>
|
@ -0,0 +1,34 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import dIcon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
||||
export default class AdminFlagItem extends Component {
|
||||
get headerCssClass() {
|
||||
return `admin-${this.args.name}__header`;
|
||||
}
|
||||
<template>
|
||||
<div class={{this.headerCssClass}}>
|
||||
<h2>{{i18n @heading}}</h2>
|
||||
{{#if @primaryActionRoute}}
|
||||
<LinkTo
|
||||
@route={{@primaryActionRoute}}
|
||||
class={{concatClass
|
||||
"btn-primary"
|
||||
"btn"
|
||||
"btn-icon-text"
|
||||
@primaryActionCssClass
|
||||
}}
|
||||
>
|
||||
{{dIcon @primaryActionIcon}}
|
||||
{{i18n @primaryActionLabel}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
|
||||
{{#if @subheading}}
|
||||
<h3>{{i18n @subheading}}</h3>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -3,7 +3,9 @@ import { tracked } from "@glimmer/tracking";
|
||||
import { fn } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { not } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DToggleSwitch from "discourse/components/d-toggle-switch";
|
||||
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||
@ -14,22 +16,48 @@ import i18n from "discourse-common/helpers/i18n";
|
||||
import DMenu from "float-kit/components/d-menu";
|
||||
|
||||
export default class AdminFlagItem extends Component {
|
||||
@service dialog;
|
||||
@service router;
|
||||
|
||||
@tracked enabled = this.args.flag.enabled;
|
||||
|
||||
get canMove() {
|
||||
return this.args.flag.id !== SYSTEM_FLAG_IDS.notify_user;
|
||||
}
|
||||
|
||||
get canEdit() {
|
||||
return (
|
||||
!Object.values(SYSTEM_FLAG_IDS).includes(this.args.flag.id) &&
|
||||
!this.args.flag.is_used
|
||||
);
|
||||
}
|
||||
|
||||
get editTitle() {
|
||||
return this.canEdit
|
||||
? "admin.config_areas.flags.form.edit_flag"
|
||||
: "admin.config_areas.flags.form.non_editable";
|
||||
}
|
||||
|
||||
get deleteTitle() {
|
||||
return this.canEdit
|
||||
? "admin.config_areas.flags.form.edit_flag"
|
||||
: "admin.config_areas.flags.form.non_editable";
|
||||
}
|
||||
|
||||
@action
|
||||
toggleFlagEnabled(flag) {
|
||||
this.enabled = !this.enabled;
|
||||
|
||||
return ajax(`/admin/config/flags/${flag.id}/toggle`, {
|
||||
type: "PUT",
|
||||
}).catch((error) => {
|
||||
this.enabled = !this.enabled;
|
||||
return popupAjaxError(error);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
this.args.flag.enabled = this.enabled;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.enabled = !this.enabled;
|
||||
return popupAjaxError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
@ -48,6 +76,23 @@ export default class AdminFlagItem extends Component {
|
||||
this.args.moveFlagCallback(this.args.flag, "down");
|
||||
this.dMenu.close();
|
||||
}
|
||||
@action
|
||||
edit() {
|
||||
this.router.transitionTo("adminConfig.flags.edit", this.args.flag);
|
||||
}
|
||||
|
||||
@action
|
||||
delete() {
|
||||
this.dialog.yesNoConfirm({
|
||||
message: i18n("admin.config_areas.flags.delete_confirm", {
|
||||
name: this.args.flag.name,
|
||||
}),
|
||||
didConfirm: () => {
|
||||
this.args.deleteFlagCallback(this.args.flag);
|
||||
},
|
||||
});
|
||||
this.dMenu.close();
|
||||
}
|
||||
|
||||
<template>
|
||||
<tr class="admin-flag-item {{@flag.name_key}}">
|
||||
@ -64,10 +109,19 @@ export default class AdminFlagItem extends Component {
|
||||
class="admin-flag-item__toggle {{@flag.name_key}}"
|
||||
{{on "click" (fn this.toggleFlagEnabled @flag)}}
|
||||
/>
|
||||
|
||||
<DButton
|
||||
class="btn btn-secondary admin-flag-item__edit"
|
||||
@action={{this.edit}}
|
||||
@label="admin.config_areas.flags.edit"
|
||||
@disabled={{not this.canEdit}}
|
||||
@title={{this.editTitle}}
|
||||
/>
|
||||
|
||||
{{#if this.canMove}}
|
||||
<DMenu
|
||||
@identifier="flag-menu"
|
||||
@title={{i18n "admin.flags.more_options.title"}}
|
||||
@title={{i18n "admin.config_areas.flags.more_options.title"}}
|
||||
@icon="ellipsis-v"
|
||||
@onRegisterApi={{this.onRegisterApi}}
|
||||
>
|
||||
@ -76,9 +130,9 @@ export default class AdminFlagItem extends Component {
|
||||
{{#unless @isFirstFlag}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="admin.flags.more_options.move_up"
|
||||
@label="admin.config_areas.flags.more_options.move_up"
|
||||
@icon="arrow-up"
|
||||
@class="btn-transparent move-up"
|
||||
@class="btn-transparent admin-flag-item__move-up"
|
||||
@action={{this.moveUp}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
@ -86,13 +140,24 @@ export default class AdminFlagItem extends Component {
|
||||
{{#unless @isLastFlag}}
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="admin.flags.more_options.move_down"
|
||||
@label="admin.config_areas.flags.more_options.move_down"
|
||||
@icon="arrow-down"
|
||||
@class="btn-transparent move-down"
|
||||
@class="btn-transparent admin-flag-item__move-down"
|
||||
@action={{this.moveDown}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{/unless}}
|
||||
|
||||
<dropdown.item>
|
||||
<DButton
|
||||
@label="admin.config_areas.flags.delete"
|
||||
@icon="trash-alt"
|
||||
class="btn-transparent admin-flag-item__delete"
|
||||
@action={{this.delete}}
|
||||
@disabled={{not this.canEdit}}
|
||||
@title={{this.deleteTitle}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
</DropdownMenu>
|
||||
</:content>
|
||||
</DMenu>
|
||||
|
@ -0,0 +1,190 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { Input } from "@ember/component";
|
||||
import { hash } from "@ember/helper";
|
||||
import { TextArea } from "@ember/legacy-built-in-components";
|
||||
import { action } from "@ember/object";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { service } from "@ember/service";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { not } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import dIcon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
|
||||
import MultiSelect from "select-kit/components/multi-select";
|
||||
|
||||
export default class AdminFlagsForm extends Component {
|
||||
@service router;
|
||||
@service site;
|
||||
|
||||
@tracked enabled = true;
|
||||
@tracked name;
|
||||
@tracked description;
|
||||
@tracked appliesTo;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
if (this.isUpdate) {
|
||||
this.name = this.args.flag.name;
|
||||
this.description = this.args.flag.description;
|
||||
this.appliesTo = this.args.flag.applies_to;
|
||||
this.enabled = this.args.flag.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
get isUpdate() {
|
||||
return this.args.flag;
|
||||
}
|
||||
|
||||
get isValid() {
|
||||
return (
|
||||
!isEmpty(this.name) &&
|
||||
!isEmpty(this.description) &&
|
||||
!isEmpty(this.appliesTo)
|
||||
);
|
||||
}
|
||||
|
||||
get header() {
|
||||
return this.isUpdate
|
||||
? "admin.config_areas.flags.form.edit_header"
|
||||
: "admin.config_areas.flags.form.add_header";
|
||||
}
|
||||
|
||||
get appliesToValues() {
|
||||
return this.site.valid_flag_applies_to_types.map((type) => {
|
||||
return {
|
||||
name: I18n.t(
|
||||
`admin.config_areas.flags.form.${type
|
||||
.toLowerCase()
|
||||
.replace("::", "_")}`
|
||||
),
|
||||
id: type,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
save() {
|
||||
this.isUpdate ? this.update() : this.create();
|
||||
}
|
||||
|
||||
@bind
|
||||
create() {
|
||||
return ajax(`/admin/config/flags`, {
|
||||
type: "POST",
|
||||
data: this.#formData,
|
||||
})
|
||||
.then((response) => {
|
||||
this.site.flagTypes.push(response.flag);
|
||||
this.router.transitionTo("adminConfig.flags");
|
||||
})
|
||||
.catch((error) => {
|
||||
return popupAjaxError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
update() {
|
||||
return ajax(`/admin/config/flags/${this.args.flag.id}`, {
|
||||
type: "PUT",
|
||||
data: this.#formData,
|
||||
})
|
||||
.then((response) => {
|
||||
this.args.flag.name = response.flag.name;
|
||||
this.args.flag.description = response.flag.description;
|
||||
this.args.flag.applies_to = response.flag.applies_to;
|
||||
this.args.flag.enabled = response.flag.enabled;
|
||||
this.router.transitionTo("adminConfig.flags");
|
||||
})
|
||||
.catch((error) => {
|
||||
return popupAjaxError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
get #formData() {
|
||||
return {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
applies_to: this.appliesTo,
|
||||
enabled: this.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="admin-config-area">
|
||||
<h2>{{i18n "admin.config_areas.flags.header"}}</h2>
|
||||
<LinkTo
|
||||
@route="adminConfig.flags"
|
||||
class="btn-default btn btn-icon-text btn-back"
|
||||
>
|
||||
{{dIcon "chevron-left"}}
|
||||
{{i18n "admin.config_areas.flags.back"}}
|
||||
</LinkTo>
|
||||
<div class="admin-config-area__primary-content admin-flag-form">
|
||||
<AdminConfigAreaCard @heading={{this.header}}>
|
||||
<div class="control-group">
|
||||
<label for="name">
|
||||
{{i18n "admin.config_areas.flags.form.name"}}
|
||||
</label>
|
||||
<Input
|
||||
name="name"
|
||||
@type="text"
|
||||
@value={{this.name}}
|
||||
maxlength="200"
|
||||
class="admin-flag-form__name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="description">
|
||||
{{i18n "admin.config_areas.flags.form.description"}}
|
||||
</label>
|
||||
<TextArea
|
||||
@value={{this.description}}
|
||||
maxlength="1000"
|
||||
class="admin-flag-form__description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="applies-to">
|
||||
{{i18n "admin.config_areas.flags.form.applies_to"}}
|
||||
</label>
|
||||
<MultiSelect
|
||||
@value={{this.appliesTo}}
|
||||
@content={{this.appliesToValues}}
|
||||
@options={{hash allowAny=false}}
|
||||
class="admin-flag-form__applies-to"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="checkbox-label admin-flag-form__enabled">
|
||||
<Input @type="checkbox" @checked={{this.enabled}} />
|
||||
{{i18n "admin.config_areas.flags.form.enabled"}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info admin_flag_form__info">
|
||||
{{dIcon "info-circle"}}
|
||||
{{i18n "admin.config_areas.flags.form.alert"}}
|
||||
</div>
|
||||
|
||||
<DButton
|
||||
@action={{this.save}}
|
||||
@label="admin.config_areas.flags.form.save"
|
||||
@ariaLabel="admin.config_areas.flags.form.save"
|
||||
@disabled={{not this.isValid}}
|
||||
class="btn-primary admin-flag-form__save"
|
||||
/>
|
||||
</AdminConfigAreaCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import Route from "@ember/routing/route";
|
||||
import { service } from "@ember/service";
|
||||
|
||||
export default class AdminConfigFlagsEditRoute extends Route {
|
||||
@service site;
|
||||
|
||||
model(params) {
|
||||
return this.site.flagTypes.findBy("id", parseInt(params.flag_id, 10));
|
||||
}
|
||||
}
|
@ -215,6 +215,8 @@ export default function () {
|
||||
function () {
|
||||
this.route("flags", function () {
|
||||
this.route("index", { path: "/" });
|
||||
this.route("new");
|
||||
this.route("edit", { path: "/:flag_id" });
|
||||
});
|
||||
|
||||
this.route("about");
|
||||
|
@ -0,0 +1 @@
|
||||
<AdminFlagsForm @flag={{@model}} />
|
@ -1 +1 @@
|
||||
<AdminFlags />
|
||||
<AdminConfigAreas::Flags />
|
@ -0,0 +1 @@
|
||||
<AdminFlagsForm />
|
@ -1,10 +1,11 @@
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { or } from "truth-helpers";
|
||||
import dIcon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
|
||||
<template>
|
||||
<LinkTo class="btn btn-flat back-button" @route={{@route}}>
|
||||
{{dIcon "chevron-left"}}
|
||||
{{i18n "back_button"}}
|
||||
{{i18n (or @label "back_button")}}
|
||||
</LinkTo>
|
||||
</template>
|
||||
|
@ -20,6 +20,10 @@ export default class PostFlag extends Flag {
|
||||
flagsAvailable(flagModal) {
|
||||
let flagsAvailable = flagModal.args.model.flagModel.flagsAvailable;
|
||||
|
||||
flagsAvailable = flagsAvailable.filter((flag) => {
|
||||
return flag.applies_to.includes("Post");
|
||||
});
|
||||
|
||||
// "message user" option should be at the top
|
||||
const notifyUserIndex = flagsAvailable.indexOf(
|
||||
flagsAvailable.filterBy("name_key", "notify_user")[0]
|
||||
|
@ -590,7 +590,8 @@ export default {
|
||||
icon: null,
|
||||
id: 3,
|
||||
is_custom_flag: false,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
applies_to: ["Post", "Chat::Message"]
|
||||
},
|
||||
{
|
||||
name_key: "inappropriate",
|
||||
@ -603,7 +604,8 @@ export default {
|
||||
icon: null,
|
||||
id: 4,
|
||||
is_custom_flag: false,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
applies_to: ["Post", "Topic", "Chat::Message"]
|
||||
},
|
||||
{
|
||||
name_key: "vote",
|
||||
@ -626,7 +628,8 @@ export default {
|
||||
icon: null,
|
||||
id: 8,
|
||||
is_custom_flag: false,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
applies_to: ["Post", "Topic", "Chat::Message"]
|
||||
},
|
||||
{
|
||||
name_key: "notify_user",
|
||||
@ -639,7 +642,8 @@ export default {
|
||||
icon: null,
|
||||
id: 6,
|
||||
is_custom_flag: true,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
applies_to: ["Post", "Topic", "Chat::Message"]
|
||||
},
|
||||
{
|
||||
name_key: "notify_moderators",
|
||||
@ -651,7 +655,8 @@ export default {
|
||||
icon: null,
|
||||
id: 7,
|
||||
is_custom_flag: true,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
applies_to: ["Post", "Topic", "Chat::Message"]
|
||||
},
|
||||
],
|
||||
topic_flag_types: [
|
||||
@ -664,7 +669,8 @@ export default {
|
||||
icon: null,
|
||||
id: 4,
|
||||
is_custom_flag: false,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
applies_to: ["Post", "Topic", "Chat::Message"]
|
||||
},
|
||||
{
|
||||
name_key: "spam",
|
||||
@ -675,7 +681,8 @@ export default {
|
||||
icon: null,
|
||||
id: 8,
|
||||
is_custom_flag: false,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
applies_to: ["Post", "Topic", "Chat::Message"]
|
||||
},
|
||||
{
|
||||
name_key: "notify_moderators",
|
||||
@ -686,7 +693,8 @@ export default {
|
||||
icon: null,
|
||||
id: 7,
|
||||
is_custom_flag: true,
|
||||
enabled: true
|
||||
enabled: true,
|
||||
applies_to: ["Post", "Topic", "Chat::Message"]
|
||||
},
|
||||
],
|
||||
archetypes: [
|
||||
|
@ -1,3 +1,8 @@
|
||||
.admin-config-area {
|
||||
.btn-back {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
.admin-config-area-card {
|
||||
padding: 20px;
|
||||
border: 1px solid var(--primary-low);
|
||||
@ -22,4 +27,11 @@
|
||||
font-size: var(--font-down-1);
|
||||
padding: 10px 10px;
|
||||
}
|
||||
&__control-group-horizontal {
|
||||
display: flex;
|
||||
margin-bottom: 18px;
|
||||
label {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,11 +10,67 @@
|
||||
&__options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.d-toggle-switch--label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.d-toggle-switch {
|
||||
margin-right: 2em;
|
||||
}
|
||||
.btn-secondary {
|
||||
padding: 0.25em 0.325em;
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
.flag-menu-trigger {
|
||||
padding: 0.25em 0.325em;
|
||||
}
|
||||
|
||||
&__delete.btn,
|
||||
&__delete.btn:hover {
|
||||
border-top: 1px solid var(--primary-low);
|
||||
color: var(--danger);
|
||||
svg {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-flag-form {
|
||||
&__enabled,
|
||||
&__applies-to {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
&__save {
|
||||
margin-top: 1em;
|
||||
}
|
||||
&__info {
|
||||
color: var(--primary-high);
|
||||
svg {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
}
|
||||
&__description {
|
||||
width: 60%;
|
||||
}
|
||||
&__applies-to.select-kit.multi-select {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-flags__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.btn-primary {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 1em;
|
||||
font-size: var(--font-0);
|
||||
font-weight: normal;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,17 @@
|
||||
.admin-contents table.grid tr.admin-flag-item {
|
||||
grid-template-columns: auto min-content;
|
||||
|
||||
.d-toggle-switch {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-config-area__primary-content {
|
||||
.admin-flag-form {
|
||||
&__name,
|
||||
&__applies-to.select-kit.multi-select,
|
||||
&__description {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
class Admin::Config::FlagsController < Admin::AdminController
|
||||
def toggle
|
||||
with_service(ToggleFlag) do
|
||||
with_service(Flags::ToggleFlag) do
|
||||
on_success do
|
||||
Discourse.request_refresh!
|
||||
render(json: success_json)
|
||||
@ -19,8 +19,45 @@ class Admin::Config::FlagsController < Admin::AdminController
|
||||
def index
|
||||
end
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def create
|
||||
with_service(Flags::CreateFlag) do
|
||||
on_success do
|
||||
Discourse.request_refresh!
|
||||
render json: result.flag, serializer: FlagSerializer
|
||||
end
|
||||
on_failure { render(json: failed_json, status: 422) }
|
||||
on_failed_policy(:invalid_access) { raise Discourse::InvalidAccess }
|
||||
on_failed_contract do |contract|
|
||||
render(json: failed_json.merge(errors: contract.errors.full_messages), status: 400)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
with_service(Flags::UpdateFlag) do
|
||||
on_success do
|
||||
Discourse.request_refresh!
|
||||
render json: result.flag, serializer: FlagSerializer
|
||||
end
|
||||
on_failure { render(json: failed_json, status: 422) }
|
||||
on_model_not_found(:message) { raise Discourse::NotFound }
|
||||
on_failed_policy(:not_system) { render_json_error(I18n.t("flags.errors.system")) }
|
||||
on_failed_policy(:not_used) { render_json_error(I18n.t("flags.errors.used")) }
|
||||
on_failed_policy(:invalid_access) { raise Discourse::InvalidAccess }
|
||||
on_failed_contract do |contract|
|
||||
render(json: failed_json.merge(errors: contract.errors.full_messages), status: 400)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def reorder
|
||||
with_service(ReorderFlag) do
|
||||
with_service(Flags::ReorderFlag) do
|
||||
on_success do
|
||||
Discourse.request_refresh!
|
||||
render(json: success_json)
|
||||
@ -34,4 +71,20 @@ class Admin::Config::FlagsController < Admin::AdminController
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
with_service(Flags::DestroyFlag) do
|
||||
on_success do
|
||||
Discourse.request_refresh!
|
||||
render(json: success_json)
|
||||
end
|
||||
on_failure { render(json: failed_json, status: 422) }
|
||||
on_failed_policy(:not_system) { render_json_error(I18n.t("flags.errors.system")) }
|
||||
on_failed_policy(:not_used) { render_json_error(I18n.t("flags.errors.used")) }
|
||||
on_failed_policy(:invalid_access) { raise Discourse::InvalidAccess }
|
||||
on_failed_contract do |contract|
|
||||
render(json: failed_json.merge(errors: contract.errors.full_messages), status: 400)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,7 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Flag < ActiveRecord::Base
|
||||
DEFAULT_VALID_APPLIES_TO = %w[Post Topic]
|
||||
MAX_SYSTEM_FLAG_ID = 1000
|
||||
MAX_NAME_LENGTH = 200
|
||||
MAX_DESCRIPTION_LENGTH = 1000
|
||||
scope :enabled, -> { where(enabled: true) }
|
||||
scope :system, -> { where("id < 1000") }
|
||||
|
||||
@ -17,6 +20,10 @@ class Flag < ActiveRecord::Base
|
||||
ReviewableScore.exists?(reviewable_score_type: self.id)
|
||||
end
|
||||
|
||||
def self.valid_applies_to_types
|
||||
Set.new(DEFAULT_VALID_APPLIES_TO | DiscoursePluginRegistry.flag_applies_to_types)
|
||||
end
|
||||
|
||||
def self.reset_flag_settings!
|
||||
# Flags are memoized for better performance. After the update, we need to reload them in all processes.
|
||||
PostActionType.reload_types
|
||||
|
@ -117,6 +117,14 @@ class PostActionType < ActiveRecord::Base
|
||||
all_flags.pluck(:id, :name).to_h
|
||||
end
|
||||
|
||||
def descriptions
|
||||
all_flags.pluck(:id, :description).to_h
|
||||
end
|
||||
|
||||
def applies_to
|
||||
all_flags.pluck(:id, :applies_to).to_h
|
||||
end
|
||||
|
||||
def is_flag?(sym)
|
||||
flag_types.valid?(sym)
|
||||
end
|
||||
|
@ -196,6 +196,11 @@ class Reviewable < ActiveRecord::Base
|
||||
update(score: self.score + rs.score, latest_score: rs.created_at, force_review: force_review)
|
||||
topic.update(reviewable_score: topic.reviewable_score + rs.score) if topic
|
||||
|
||||
# Flags are cached for performance reasons.
|
||||
# However, when the reviewable item is created, we need to clear the cache to mark flag as used.
|
||||
# Used flags cannot be deleted or update by admins, only disabled.
|
||||
Flag.reset_flag_settings! if PostActionType.notify_flag_type_ids.include?(reviewable_score_type)
|
||||
|
||||
DiscourseEvent.trigger(:reviewable_score_updated, self)
|
||||
|
||||
rs
|
||||
|
@ -10,6 +10,8 @@ class PostActionTypeSerializer < ApplicationSerializer
|
||||
:is_flag,
|
||||
:is_custom_flag,
|
||||
:enabled,
|
||||
:applies_to,
|
||||
:is_used,
|
||||
)
|
||||
|
||||
include ConfigurableUrls
|
||||
@ -27,7 +29,14 @@ class PostActionTypeSerializer < ApplicationSerializer
|
||||
end
|
||||
|
||||
def description
|
||||
i18n("description", vars: { tos_url:, base_path: Discourse.base_path })
|
||||
i18n(
|
||||
"description",
|
||||
vars: {
|
||||
tos_url:,
|
||||
base_path: Discourse.base_path,
|
||||
},
|
||||
default: object.class.descriptions[object.id],
|
||||
)
|
||||
end
|
||||
|
||||
def short_description
|
||||
@ -42,6 +51,15 @@ class PostActionTypeSerializer < ApplicationSerializer
|
||||
!!PostActionType.enabled_flag_types[object.id]
|
||||
end
|
||||
|
||||
def applies_to
|
||||
Array.wrap(PostActionType.applies_to[object.id])
|
||||
end
|
||||
|
||||
def is_used
|
||||
PostAction.exists?(post_action_type_id: object.id) ||
|
||||
ReviewableScore.exists?(reviewable_score_type: object.id)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def i18n(field, default: nil, vars: nil)
|
||||
|
@ -10,8 +10,7 @@ class ReviewableScoreTypeSerializer < ApplicationSerializer
|
||||
# Allow us to share post action type translations for backwards compatibility
|
||||
def title
|
||||
I18n.t("post_action_types.#{type}.title", default: nil) ||
|
||||
I18n.t("reviewable_score_types.#{type}.title", default: nil) ||
|
||||
PostActionType.flag_settings.names[id]
|
||||
I18n.t("reviewable_score_types.#{type}.title", default: nil) || PostActionType.names[id]
|
||||
end
|
||||
|
||||
def reviewable_priority
|
||||
|
@ -48,6 +48,7 @@ class SiteSerializer < ApplicationSerializer
|
||||
:privacy_policy_url,
|
||||
:system_user_avatar_template,
|
||||
:lazy_load_categories,
|
||||
:valid_flag_applies_to_types,
|
||||
)
|
||||
|
||||
has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer
|
||||
@ -350,6 +351,14 @@ class SiteSerializer < ApplicationSerializer
|
||||
scope.can_lazy_load_categories?
|
||||
end
|
||||
|
||||
def valid_flag_applies_to_types
|
||||
Flag.valid_applies_to_types
|
||||
end
|
||||
|
||||
def include_valid_flag_applies_to_types?
|
||||
scope.is_admin?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ordered_flags(flags)
|
||||
|
58
app/services/flags/create_flag.rb
Normal file
58
app/services/flags/create_flag.rb
Normal file
@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Flags::CreateFlag
|
||||
include Service::Base
|
||||
|
||||
contract
|
||||
policy :invalid_access
|
||||
model :flag, :instantiate_flag
|
||||
|
||||
transaction do
|
||||
step :create
|
||||
step :log
|
||||
end
|
||||
|
||||
class Contract
|
||||
attribute :name, :string
|
||||
attribute :description, :string
|
||||
attribute :enabled, :boolean
|
||||
attribute :applies_to
|
||||
validates :name, presence: true
|
||||
validates :description, presence: true
|
||||
validates :name, length: { maximum: Flag::MAX_NAME_LENGTH }
|
||||
validates :description, length: { maximum: Flag::MAX_DESCRIPTION_LENGTH }
|
||||
validates :applies_to, inclusion: { in: Flag.valid_applies_to_types }, allow_nil: false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def instantiate_flag(name:, description:, applies_to:, enabled:)
|
||||
Flag.new(
|
||||
name: name,
|
||||
description: description,
|
||||
applies_to: applies_to,
|
||||
enabled: enabled,
|
||||
notify_type: true,
|
||||
)
|
||||
end
|
||||
|
||||
def invalid_access(guardian:)
|
||||
guardian.can_create_flag?
|
||||
end
|
||||
|
||||
def create(flag:)
|
||||
flag.save!
|
||||
end
|
||||
|
||||
def log(guardian:, flag:)
|
||||
StaffActionLogger.new(guardian.user).log_custom(
|
||||
"create_flag",
|
||||
{
|
||||
name: flag.name,
|
||||
description: flag.description,
|
||||
applies_to: flag.applies_to,
|
||||
enabled: flag.enabled,
|
||||
},
|
||||
)
|
||||
end
|
||||
end
|
49
app/services/flags/destroy_flag.rb
Normal file
49
app/services/flags/destroy_flag.rb
Normal file
@ -0,0 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Flags::DestroyFlag
|
||||
include Service::Base
|
||||
|
||||
model :flag
|
||||
policy :not_system
|
||||
policy :not_used
|
||||
policy :invalid_access
|
||||
|
||||
transaction do
|
||||
step :destroy
|
||||
step :log
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_flag(id:)
|
||||
Flag.find(id)
|
||||
end
|
||||
|
||||
def not_system(flag:)
|
||||
!flag.system?
|
||||
end
|
||||
|
||||
def not_used(flag:)
|
||||
!flag.used?
|
||||
end
|
||||
|
||||
def invalid_access(guardian:, flag:)
|
||||
guardian.can_edit_flag?(flag)
|
||||
end
|
||||
|
||||
def destroy(flag:)
|
||||
flag.destroy!
|
||||
end
|
||||
|
||||
def log(guardian:, flag:)
|
||||
StaffActionLogger.new(guardian.user).log_custom(
|
||||
"delete_flag",
|
||||
{
|
||||
name: flag.name,
|
||||
description: flag.description,
|
||||
applies_to: flag.applies_to,
|
||||
enabled: flag.enabled,
|
||||
},
|
||||
)
|
||||
end
|
||||
end
|
@ -2,7 +2,7 @@
|
||||
|
||||
VALID_DIRECTIONS = %w[up down]
|
||||
|
||||
class ReorderFlag
|
||||
class Flags::ReorderFlag
|
||||
include Service::Base
|
||||
|
||||
contract
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ToggleFlag
|
||||
class Flags::ToggleFlag
|
||||
include Service::Base
|
||||
|
||||
contract
|
62
app/services/flags/update_flag.rb
Normal file
62
app/services/flags/update_flag.rb
Normal file
@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Flags::UpdateFlag
|
||||
include Service::Base
|
||||
|
||||
contract
|
||||
model :flag
|
||||
policy :not_system
|
||||
policy :not_used
|
||||
policy :invalid_access
|
||||
|
||||
transaction do
|
||||
step :update
|
||||
step :log
|
||||
end
|
||||
|
||||
class Contract
|
||||
attribute :name, :string
|
||||
attribute :description, :string
|
||||
attribute :enabled, :boolean
|
||||
attribute :applies_to
|
||||
validates :name, presence: true
|
||||
validates :description, presence: true
|
||||
validates :name, length: { maximum: Flag::MAX_NAME_LENGTH }
|
||||
validates :description, length: { maximum: Flag::MAX_DESCRIPTION_LENGTH }
|
||||
validates :applies_to, inclusion: { in: Flag.valid_applies_to_types }, allow_nil: false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_flag(id:)
|
||||
Flag.find(id)
|
||||
end
|
||||
|
||||
def not_system(flag:)
|
||||
!flag.system?
|
||||
end
|
||||
|
||||
def not_used(flag:)
|
||||
!flag.used?
|
||||
end
|
||||
|
||||
def invalid_access(guardian:, flag:)
|
||||
guardian.can_edit_flag?(flag)
|
||||
end
|
||||
|
||||
def update(flag:, name:, description:, applies_to:, enabled:)
|
||||
flag.update!(name: name, description: description, applies_to: applies_to, enabled: enabled)
|
||||
end
|
||||
|
||||
def log(guardian:, flag:)
|
||||
StaffActionLogger.new(guardian.user).log_custom(
|
||||
"update_flag",
|
||||
{
|
||||
name: flag.name,
|
||||
description: flag.description,
|
||||
applies_to: flag.applies_to,
|
||||
enabled: flag.enabled,
|
||||
},
|
||||
)
|
||||
end
|
||||
end
|
@ -5048,7 +5048,6 @@ en:
|
||||
title: "More options"
|
||||
move_up: "Move up"
|
||||
move_down: "Move down"
|
||||
|
||||
groups:
|
||||
new:
|
||||
title: "New Group"
|
||||
@ -5480,6 +5479,36 @@ en:
|
||||
contact_information_saved: "Contact information saved"
|
||||
your_organization_saved: "Your organization saved"
|
||||
saved: "saved!"
|
||||
flags:
|
||||
header: "Moderation Flags"
|
||||
subheader: "The flagging system in Discourse helps you and your moderator team manage content and user behavior, keeping your community respectful and healthy. The defaults are suitable for most communities and you don’t have to change them. However, if your site has particular requirements you can disable flags you don’t need and add your own custom flags."
|
||||
description: "Description"
|
||||
enabled: "Enabled?"
|
||||
add: "Add Flag"
|
||||
edit: "Edit"
|
||||
back: "Back to flags"
|
||||
delete: "Delete"
|
||||
delete_confirm: 'Are you sure you want to delete "%{name}"?'
|
||||
form:
|
||||
add_header: "Add flag"
|
||||
edit_header: "Edit flag"
|
||||
save: "Save"
|
||||
name: "Name"
|
||||
description: "Description"
|
||||
applies_to: "Display this flag on"
|
||||
topic: "topics"
|
||||
post: "posts"
|
||||
chat_message: "chat messages"
|
||||
enabled: "Enable this custom flag after saving"
|
||||
alert: "Once a custom flag has been used, it can only be disabled but not edited or deleted."
|
||||
edit_flag: "Edit flag"
|
||||
non_editable: "You cannot edit this flag because it is a system flag or has already been used in the review system, however you can still disable it."
|
||||
delete_flag: "Delete flag"
|
||||
non_deletable: "You cannot delete this flag because it is a system flag or has already been used in the review system, however you can still disable it."
|
||||
more_options:
|
||||
title: "More options"
|
||||
move_up: "Move up"
|
||||
move_down: "Move down"
|
||||
plugins:
|
||||
title: "Plugins"
|
||||
installed: "Installed Plugins"
|
||||
@ -6157,6 +6186,9 @@ en:
|
||||
delete_watched_word_group: "delete watched word group"
|
||||
toggle_flag: "toggle flag"
|
||||
move_flag: "move flag"
|
||||
create_flag: "create flag"
|
||||
update_flag: "update flag"
|
||||
delete_flag: "delete flag"
|
||||
screened_emails:
|
||||
title: "Screened Emails"
|
||||
description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed."
|
||||
|
@ -1253,6 +1253,8 @@ en:
|
||||
errors:
|
||||
already_handled: "Flag was already handled"
|
||||
wrong_move: "Flag cannot be moved"
|
||||
system: "System flag cannot be updated or deleted."
|
||||
used: "Flag cannot be updated or deleted because has already been used."
|
||||
reports:
|
||||
default:
|
||||
labels:
|
||||
|
@ -386,9 +386,10 @@ Discourse::Application.routes.draw do
|
||||
end
|
||||
end
|
||||
namespace :config, constraints: StaffConstraint.new do
|
||||
resources :flags, only: %i[index] do
|
||||
resources :flags, only: %i[index new create update destroy] do
|
||||
put "toggle"
|
||||
put "reorder/:direction" => "flags#reorder"
|
||||
member { get "/" => "flags#edit" }
|
||||
end
|
||||
|
||||
resources :about, constraints: AdminConstraint.new, only: %i[index] do
|
||||
|
@ -123,6 +123,8 @@ class DiscoursePluginRegistry
|
||||
|
||||
define_filtered_register :problem_checks
|
||||
|
||||
define_filtered_register :flag_applies_to_types
|
||||
|
||||
def self.register_auth_provider(auth_provider)
|
||||
self.auth_providers << auth_provider
|
||||
end
|
||||
|
@ -5,6 +5,10 @@ module FlagGuardian
|
||||
@user.admin? && !flag.system? && !flag.used?
|
||||
end
|
||||
|
||||
def can_create_flag?
|
||||
@user.admin?
|
||||
end
|
||||
|
||||
def can_toggle_flag?
|
||||
@user.admin?
|
||||
end
|
||||
|
@ -37,7 +37,7 @@ export default class ChatMessageFlag {
|
||||
"description",
|
||||
I18n.t(`chat.flags.${flag.name_key}`, {
|
||||
basePath: getURL(""),
|
||||
defaultValue: "",
|
||||
defaultValue: flag.description,
|
||||
})
|
||||
);
|
||||
return flag;
|
||||
@ -48,8 +48,9 @@ export default class ChatMessageFlag {
|
||||
let flagsAvailable = flagModal.site.flagTypes;
|
||||
|
||||
flagsAvailable = flagsAvailable.filter((flag) => {
|
||||
return flagModal.args.model.flagModel.availableFlags.includes(
|
||||
flag.name_key
|
||||
return (
|
||||
flagModal.args.model.flagModel.availableFlags.includes(flag.name_key) &&
|
||||
flag.applies_to.includes("Chat::Message")
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -49,6 +49,7 @@ after_initialize do
|
||||
|
||||
register_user_custom_field_type(Chat::LAST_CHAT_CHANNEL_ID, :integer)
|
||||
DiscoursePluginRegistry.serialized_current_user_fields << Chat::LAST_CHAT_CHANNEL_ID
|
||||
DiscoursePluginRegistry.register_flag_applies_to_type("Chat::Message", self)
|
||||
|
||||
UserUpdater::OPTION_ATTR.push(:chat_enabled)
|
||||
UserUpdater::OPTION_ATTR.push(:only_chat_push_notifications)
|
||||
|
@ -356,6 +356,12 @@
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"applies_to": {
|
||||
"type": "array"
|
||||
},
|
||||
"is_used": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -366,7 +372,9 @@
|
||||
"short_description",
|
||||
"is_flag",
|
||||
"is_custom_flag",
|
||||
"enabled"
|
||||
"enabled",
|
||||
"applies_to",
|
||||
"is_used"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -400,6 +408,12 @@
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"applies_to": {
|
||||
"type": "array"
|
||||
},
|
||||
"is_used": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -410,7 +424,9 @@
|
||||
"short_description",
|
||||
"is_flag",
|
||||
"is_custom_flag",
|
||||
"enabled"
|
||||
"enabled",
|
||||
"applies_to",
|
||||
"is_used"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -823,6 +839,9 @@
|
||||
"denied_emojis" : {
|
||||
"type": "array"
|
||||
},
|
||||
"valid_flag_applies_to_types" : {
|
||||
"type": "array"
|
||||
},
|
||||
"navigation_menu_site_top_tags": {
|
||||
"type": "array"
|
||||
}
|
||||
|
87
spec/services/flags/create_flag_spec.rb
Normal file
87
spec/services/flags/create_flag_spec.rb
Normal file
@ -0,0 +1,87 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe(Flags::CreateFlag) do
|
||||
subject(:result) do
|
||||
described_class.call(
|
||||
guardian: current_user.guardian,
|
||||
name: name,
|
||||
description: description,
|
||||
applies_to: applies_to,
|
||||
enabled: enabled,
|
||||
)
|
||||
end
|
||||
|
||||
let(:name) { "custom flag name" }
|
||||
let(:description) { "custom flag description" }
|
||||
let(:applies_to) { ["Topic"] }
|
||||
let(:enabled) { true }
|
||||
|
||||
context "when user is not allowed to perform the action" do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
|
||||
it { is_expected.to fail_a_policy(:invalid_access) }
|
||||
end
|
||||
|
||||
context "when applies to is invalid" do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
let(:applies_to) { ["User"] }
|
||||
|
||||
it { is_expected.to fail_a_contract }
|
||||
end
|
||||
|
||||
context "when title is empty" do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
let(:name) { nil }
|
||||
|
||||
it { is_expected.to fail_a_contract }
|
||||
end
|
||||
|
||||
context "when title is too long" do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
let(:name) { "a" * 201 }
|
||||
|
||||
it { is_expected.to fail_a_contract }
|
||||
end
|
||||
|
||||
context "when description is empty" do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
let(:description) { nil }
|
||||
|
||||
it { is_expected.to fail_a_contract }
|
||||
end
|
||||
|
||||
context "when description is too long" do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
let(:description) { "a" * 1001 }
|
||||
|
||||
it { is_expected.to fail_a_contract }
|
||||
end
|
||||
|
||||
context "when user is allowed to perform the action" do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
|
||||
after { Flag.destroy_by(name: "custom flag name") }
|
||||
|
||||
it "sets the service result as successful" do
|
||||
expect(result).to be_a_success
|
||||
end
|
||||
|
||||
it "creates the flag" do
|
||||
result
|
||||
flag = Flag.last
|
||||
expect(flag.name).to eq("custom flag name")
|
||||
expect(flag.description).to eq("custom flag description")
|
||||
expect(flag.applies_to).to eq(["Topic"])
|
||||
expect(flag.enabled).to be true
|
||||
end
|
||||
|
||||
it "logs the action" do
|
||||
expect { result }.to change { UserHistory.count }.by(1)
|
||||
expect(UserHistory.last).to have_attributes(
|
||||
custom_type: "create_flag",
|
||||
details:
|
||||
"name: custom flag name\ndescription: custom flag description\napplies_to: [\"Topic\"]\nenabled: true",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
37
spec/services/flags/destroy_flag_spec.rb
Normal file
37
spec/services/flags/destroy_flag_spec.rb
Normal file
@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe(Flags::DestroyFlag) do
|
||||
fab!(:flag)
|
||||
|
||||
subject(:result) { described_class.call(id: flag.id, guardian: current_user.guardian) }
|
||||
|
||||
after { flag.destroy }
|
||||
|
||||
context "when user is not allowed to perform the action" do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
|
||||
it { is_expected.to fail_a_policy(:invalid_access) }
|
||||
end
|
||||
|
||||
context "when user is allowed to perform the action" do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
|
||||
it "sets the service result as successful" do
|
||||
expect(result).to be_a_success
|
||||
end
|
||||
|
||||
it "destroys the flag" do
|
||||
result
|
||||
expect { flag.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it "logs the action" do
|
||||
expect { result }.to change { UserHistory.count }.by(1)
|
||||
expect(UserHistory.last).to have_attributes(
|
||||
custom_type: "delete_flag",
|
||||
details:
|
||||
"name: offtopic\ndescription: \napplies_to: [\"Post\", \"Chat::Message\"]\nenabled: true",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe(ReorderFlag) do
|
||||
RSpec.describe(Flags::ReorderFlag) do
|
||||
subject(:result) do
|
||||
described_class.call(flag_id: flag.id, guardian: current_user.guardian, direction: direction)
|
||||
end
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe(ToggleFlag) do
|
||||
RSpec.describe(Flags::ToggleFlag) do
|
||||
subject(:result) { described_class.call(flag_id: flag.id, guardian: current_user.guardian) }
|
||||
|
||||
let(:flag) { Flag.system.last }
|
89
spec/services/flags/update_flag_spec.rb
Normal file
89
spec/services/flags/update_flag_spec.rb
Normal file
@ -0,0 +1,89 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe(Flags::UpdateFlag) do
|
||||
fab!(:flag)
|
||||
|
||||
subject(:result) do
|
||||
described_class.call(
|
||||
id: flag.id,
|
||||
guardian: current_user.guardian,
|
||||
name: name,
|
||||
description: description,
|
||||
applies_to: applies_to,
|
||||
enabled: enabled,
|
||||
)
|
||||
end
|
||||
|
||||
after { flag.destroy }
|
||||
|
||||
let(:name) { "edited custom flag name" }
|
||||
let(:description) { "edited custom flag description" }
|
||||
let(:applies_to) { ["Topic"] }
|
||||
let(:enabled) { false }
|
||||
|
||||
context "when user is not allowed to perform the action" do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
|
||||
it { is_expected.to fail_a_policy(:invalid_access) }
|
||||
end
|
||||
|
||||
context "when applies to is invalid" do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
let(:applies_to) { ["User"] }
|
||||
|
||||
it { is_expected.to fail_a_contract }
|
||||
end
|
||||
|
||||
context "when title is empty" do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
let(:name) { nil }
|
||||
|
||||
it { is_expected.to fail_a_contract }
|
||||
end
|
||||
|
||||
context "when title is too long" do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
let(:name) { "a" * 201 }
|
||||
|
||||
it { is_expected.to fail_a_contract }
|
||||
end
|
||||
|
||||
context "when description is empty" do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
let(:description) { nil }
|
||||
|
||||
it { is_expected.to fail_a_contract }
|
||||
end
|
||||
|
||||
context "when description is too long" do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
let(:description) { "a" * 1001 }
|
||||
|
||||
it { is_expected.to fail_a_contract }
|
||||
end
|
||||
|
||||
context "when user is allowed to perform the action" do
|
||||
fab!(:current_user) { Fabricate(:admin) }
|
||||
|
||||
it "sets the service result as successful" do
|
||||
expect(result).to be_a_success
|
||||
end
|
||||
|
||||
it "updates the flag" do
|
||||
result
|
||||
expect(flag.reload.name).to eq("edited custom flag name")
|
||||
expect(flag.description).to eq("edited custom flag description")
|
||||
expect(flag.applies_to).to eq(["Topic"])
|
||||
expect(flag.enabled).to be false
|
||||
end
|
||||
|
||||
it "logs the action" do
|
||||
expect { result }.to change { UserHistory.count }.by(1)
|
||||
expect(UserHistory.last).to have_attributes(
|
||||
custom_type: "update_flag",
|
||||
details:
|
||||
"name: edited custom flag name\ndescription: edited custom flag description\napplies_to: [\"Topic\"]\nenabled: false",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
@ -7,6 +7,7 @@ describe "Admin Flags Page", type: :system do
|
||||
|
||||
let(:topic_page) { PageObjects::Pages::Topic.new }
|
||||
let(:admin_flags_page) { PageObjects::Pages::AdminFlags.new }
|
||||
let(:admin_flag_form_page) { PageObjects::Pages::AdminFlagForm.new }
|
||||
|
||||
before { sign_in(admin) }
|
||||
|
||||
@ -55,6 +56,55 @@ describe "Admin Flags Page", type: :system do
|
||||
)
|
||||
end
|
||||
|
||||
it "allows admin to create, edit and delete flags" do
|
||||
topic_page.visit_topic(post.topic)
|
||||
topic_page.open_flag_topic_modal
|
||||
expect(all(".flag-action-type-details strong").map(&:text)).to eq(
|
||||
["It's Inappropriate", "It's Spam", "It's Illegal", "Something Else"],
|
||||
)
|
||||
|
||||
visit "/admin/config/flags"
|
||||
|
||||
admin_flags_page.click_add_flag
|
||||
|
||||
expect(admin_flag_form_page).to have_disabled_save_button
|
||||
|
||||
admin_flag_form_page.fill_in_name("Vulgar")
|
||||
admin_flag_form_page.fill_in_description("New flag description")
|
||||
admin_flag_form_page.fill_in_applies_to("Topic")
|
||||
admin_flag_form_page.fill_in_applies_to("Post")
|
||||
admin_flag_form_page.click_save
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
topic_page.open_flag_topic_modal
|
||||
expect(all(".flag-action-type-details strong").map(&:text)).to eq(
|
||||
["It's Inappropriate", "It's Spam", "It's Illegal", "Something Else", "Vulgar"],
|
||||
)
|
||||
|
||||
visit "/admin/config/flags"
|
||||
|
||||
admin_flags_page.click_edit_flag("vulgar")
|
||||
|
||||
admin_flag_form_page.fill_in_name("Tasteless")
|
||||
admin_flag_form_page.click_save
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
topic_page.open_flag_topic_modal
|
||||
expect(all(".flag-action-type-details strong").map(&:text)).to eq(
|
||||
["It's Inappropriate", "It's Spam", "It's Illegal", "Something Else", "Tasteless"],
|
||||
)
|
||||
|
||||
visit "/admin/config/flags"
|
||||
admin_flags_page.click_delete_flag("tasteless")
|
||||
admin_flags_page.confirm_delete
|
||||
|
||||
topic_page.visit_topic(post.topic)
|
||||
topic_page.open_flag_topic_modal
|
||||
expect(all(".flag-action-type-details strong").map(&:text)).to eq(
|
||||
["It's Inappropriate", "It's Spam", "It's Illegal", "Something Else"],
|
||||
)
|
||||
end
|
||||
|
||||
it "does not allow to move notify user flag" do
|
||||
visit "/admin/config/flags"
|
||||
expect(page).not_to have_css(".notify_user .flag-menu-trigger")
|
||||
@ -66,6 +116,17 @@ describe "Admin Flags Page", type: :system do
|
||||
expect(page).not_to have_css(".dropdown-menu__item .move-down")
|
||||
end
|
||||
|
||||
it "does not allow to system flag to be edited" do
|
||||
visit "/admin/config/flags"
|
||||
expect(page).to have_css(".off_topic .admin-flag-item__edit[disabled]")
|
||||
end
|
||||
|
||||
it "does not allow to system flag to be deleted" do
|
||||
visit "/admin/config/flags"
|
||||
admin_flags_page.open_flag_menu("notify_moderators")
|
||||
expect(page).to have_css(".admin-flag-item__delete[disabled]")
|
||||
end
|
||||
|
||||
it "does not allow top flag to move up" do
|
||||
visit "/admin/config/flags"
|
||||
admin_flags_page.open_flag_menu("off_topic")
|
||||
|
30
spec/system/page_objects/pages/admin_flag_form.rb
Normal file
30
spec/system/page_objects/pages/admin_flag_form.rb
Normal file
@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class AdminFlagForm < PageObjects::Pages::Base
|
||||
def has_disabled_save_button?
|
||||
find_button("Save", disabled: true)
|
||||
end
|
||||
|
||||
def fill_in_name(name)
|
||||
find(".admin-flag-form__name").fill_in(with: name)
|
||||
end
|
||||
|
||||
def fill_in_description(description)
|
||||
find(".admin-flag-form__description").fill_in(with: description)
|
||||
end
|
||||
|
||||
def fill_in_applies_to(applies_to)
|
||||
dropdown = PageObjects::Components::SelectKit.new(".admin-flag-form__applies-to")
|
||||
dropdown.expand
|
||||
dropdown.select_row_by_value(applies_to)
|
||||
dropdown.collapse
|
||||
end
|
||||
|
||||
def click_save
|
||||
find(".admin-flag-form__save").click
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -13,12 +13,29 @@ module PageObjects
|
||||
|
||||
def move_down(key)
|
||||
open_flag_menu(key)
|
||||
find(".dropdown-menu__item .move-down").click
|
||||
find(".admin-flag-item__move-down").click
|
||||
end
|
||||
|
||||
def move_up(key)
|
||||
open_flag_menu(key)
|
||||
find(".dropdown-menu__item .move-up").click
|
||||
find(".admin-flag-item__move-up").click
|
||||
end
|
||||
|
||||
def click_add_flag
|
||||
find(".admin-flags__header-add-flag").click
|
||||
end
|
||||
|
||||
def click_edit_flag(key)
|
||||
find(".#{key} .admin-flag-item__edit").click
|
||||
end
|
||||
|
||||
def click_delete_flag(key)
|
||||
find(".#{key} .flag-menu-trigger").click
|
||||
find(".admin-flag-item__delete").click
|
||||
end
|
||||
|
||||
def confirm_delete
|
||||
find(".dialog-footer .btn-primary").click
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user