Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ For more information about each release including git tags and artifacts, see [R

### Added

- View and edit policy penalty severity in the Policies dashboard and policy form ([#1248](https://github.com/roostorg/coop/pull/1248) by [@jess-upscrolled](https://github.com/jess-upscrolled))
- List parameters used in the action in the item action history view ([#1173](https://github.com/roostorg/coop/pull/1173) by [@maarkN](https://github.com/maarkN), closes [#833](https://github.com/roostorg/coop/issues/833))
- Manual Review Analytics with average handle time per moderator ([#1022](https://github.com/roostorg/coop/pull/1022) by [@juanmrad](https://github.com/juanmrad), closes [#380](https://github.com/roostorg/coop/issues/380))
- Support for text-only NCMEC reports ([#866](https://github.com/roostorg/coop/pull/866), [#881](https://github.com/roostorg/coop/pull/881) by [@calebmcquaid](https://github.com/calebmcquaid), closes [#661](https://github.com/roostorg/coop/issues/661))
Expand Down
9 changes: 9 additions & 0 deletions client/src/graphql/generated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions client/src/webpages/dashboard/policies/PoliciesDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import {
useGQLPoliciesWithModelsQuery,
} from '../../../graphql/generated';
import { userHasPermissions } from '../../../routing/permissions';
import { titleCaseEnumString } from '../../../utils/string';
import { Tree, treeFromList, TreeNode } from '../../../utils/tree';
import { getSeverityColor } from '../../../utils/userPenalty';
import { ModalInfo } from '../types/ModalInfo';

export type Policy = {
Expand All @@ -52,6 +54,7 @@ gql`
policyType
userStrikeCount
applyUserStrikeCountConfigToChildren
penalty
}

query Policies {
Expand Down Expand Up @@ -219,6 +222,13 @@ export default function PoliciesDashboard() {
<div className="text-base font-bold text-start">
{policy.value?.name}
</div>
<div
className={`px-2 py-0.5 text-xs font-medium border rounded ${getSeverityColor(
policy.value.penalty,
)}`}
>
{titleCaseEnumString(policy.value.penalty)}
</div>
<div className="flex items-center gap-2 text-slate-400">
ID: <CopyTextComponent value={policy.value.id} />
</div>
Expand Down
33 changes: 31 additions & 2 deletions client/src/webpages/dashboard/policies/PolicyForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button } from '@/coop-ui/Button';
import { treeFromList } from '@/utils/tree';
import { Input } from 'antd';
import { Input, Select } from 'antd';
import {
Check as CheckmarkFilled,
Plus as PlusFilled,
Expand All @@ -21,9 +21,13 @@ import {
useGQLUpdatePolicyMutation,
type GQLPolicy,
} from '../../../graphql/generated';
import { titleCaseEnumString } from '../../../utils/string';
import { UserPenaltySeverityOrder } from '../../../utils/userPenalty';
import MarkdownTextInput from './MarkdownTextInput';
import { Policy } from './PoliciesDashboard';

const { Option } = Select;

export type PolicyInputModalInfo = {
onClose: () => void;
existingPolicy?: Policy;
Expand All @@ -39,6 +43,9 @@ export default function PolicyForm() {
const navigate = useNavigate();
const [policyName, setPolicyName] = useState<string | undefined>(undefined);
const [policyText, setPolicyText] = useState<string | undefined>(undefined);
const [policyPenalty, setPolicyPenalty] = useState<GQLUserPenaltySeverity>(
GQLUserPenaltySeverity.None,
);
const [parent, setParent] = useState<
{ id: string; name: string } | undefined
>(undefined);
Expand All @@ -51,7 +58,10 @@ export default function PolicyForm() {
const [showEnforcementGuidelines, setShowEnforcementGuidelines] =
useState<boolean>(false);
const [existingPolicy, setExistingPolicy] = useState<
| Pick<GQLPolicy, 'id' | 'name' | 'enforcementGuidelines' | 'policyText'>
| Pick<
GQLPolicy,
'id' | 'name' | 'enforcementGuidelines' | 'policyText' | 'penalty'
>
| undefined
>(undefined);
const { data, loading } = useGQLPoliciesQuery();
Expand Down Expand Up @@ -85,6 +95,7 @@ export default function PolicyForm() {
if (existingPolicy) {
setPolicyName(existingPolicy.name);
setPolicyText(existingPolicy.policyText ?? undefined);
setPolicyPenalty(existingPolicy.penalty);
setEnforcementGuidelines(
existingPolicy.enforcementGuidelines ?? undefined,
);
Expand Down Expand Up @@ -138,6 +149,21 @@ export default function PolicyForm() {
onChange={(event) => setPolicyName(event.target.value)}
/>
</div>
<div className="flex flex-col items-start gap-2">
<div className="font-semibold">Penalty</div>
<Select
className="w-40"
aria-label="Penalty"
value={policyPenalty}
onChange={(value: GQLUserPenaltySeverity) => setPolicyPenalty(value)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
>
{UserPenaltySeverityOrder.map((severity) => (
<Option key={severity} value={severity}>
{titleCaseEnumString(severity)}
</Option>
))}
</Select>
</div>
</div>
);

Expand Down Expand Up @@ -228,6 +254,7 @@ export default function PolicyForm() {
existingPolicy.enforcementGuidelines ?? undefined,
);
setPolicyText(existingPolicy.policyText ?? undefined);
setPolicyPenalty(existingPolicy.penalty);
}}
>
<TrashCanFilled className="w-6 h-6 mr-2" />
Expand All @@ -253,6 +280,7 @@ export default function PolicyForm() {
enforcementGuidelines,
name: policyName,
parentId: parent?.id ?? undefined,
penalty: policyPenalty,
},
},
onCompleted: () => setShowSuccess(true),
Expand All @@ -268,6 +296,7 @@ export default function PolicyForm() {
enforcementGuidelines,
name: policyName,
parentId: parent?.id ?? undefined,
penalty: policyPenalty,
},
],
},
Expand Down
2 changes: 2 additions & 0 deletions docs/user/administration.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ Policies are the set of rules and guidelines that a platform uses to govern the

![Policy Dashboard showing 4 policies: Fraud, Nudity, Scams, Spam. There's a button to create new policies as well as options to add sub-policies for each existing policy and edit or delete them](../images/policies.png)

When you create or edit a policy, you can set its **Penalty** to one of: **None**, **Low**, **Medium**, **High**, or **Severe**. The Policies dashboard shows each policy's penalty next to its name. Penalty is included with the policy in [action webhook payloads](../api/actions.md) so your platform can weigh how serious a given policy violation is.

Policies added in Coop's UI are visible to reviewers directly in the [Job view](review-console.md#job-view) of the Review Console.

## User management
Expand Down
2 changes: 2 additions & 0 deletions docs/user/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ Policies can have sub-policies; for example, a _Spam_ policy could have sub-poli

It is often useful (and in some cases required, i.e. by the EU's Digital Services Act) to tie every Action you take to one or more specific Policies. For example, you could _Delete_ a comment under your _Hate Speech_ policy, or you could _Delete_ it under your _Spam_ policy. Coop allows you to track those differences and measure how many Actions you've taken for each Policy. That way, you can see how effectively you're enforcing each Policy over time, identify Policies for which your enforcement is poor or degrading, and report performance metrics to your leadership (or to regulators, i.e. in the form of a DSA Transparency Report).

Each Policy also has a **Penalty** severity (**None**, **Low**, **Medium**, **High**, or **Severe**) that you set when creating or editing the policy. Penalty appears on the Policies dashboard and is sent with the policy in [action webhook payloads](../api/actions.md), so your platform can treat violations of more serious policies differently from milder ones.

You can create and manage your Policies in the **Policies** dashboard, and you can fetch them programmatically through the [Policies API](../api/policies.md). Policies added in Coop's UI are also visible to reviewers directly in the [Job view](review-console.md#job-view) of the Review Console.

## Jobs
Expand Down
8 changes: 8 additions & 0 deletions server/graphql/generated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions server/graphql/modules/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const typeDefs = /* GraphQL */ `
policyType: PolicyType
userStrikeCount: Int
applyUserStrikeCountConfigToChildren: Boolean
penalty: UserPenaltySeverity!
}

enum PolicyType {
Expand Down Expand Up @@ -55,6 +56,7 @@ const typeDefs = /* GraphQL */ `
parentId: ID
parentName: String
policyType: PolicyType
penalty: UserPenaltySeverity
}

input UpdatePolicyInput {
Expand All @@ -66,6 +68,7 @@ const typeDefs = /* GraphQL */ `
policyType: PolicyType
userStrikeCount: Int
applyUserStrikeCountConfigToChildren: Boolean
penalty: UserPenaltySeverity
}

type Mutation {
Expand Down Expand Up @@ -127,6 +130,7 @@ const Mutation: GQLMutationResolvers = {
policyText: policy.policyText ?? null,
enforcementGuidelines: policy.enforcementGuidelines ?? null,
policyType: policy.policyType ?? null,
penalty: policy.penalty ?? null,
},
orgId: user.orgId,
invokedBy: {
Expand Down Expand Up @@ -172,6 +176,7 @@ const Mutation: GQLMutationResolvers = {
policyType,
userStrikeCount,
applyUserStrikeCountConfigToChildren,
penalty,
} = input;

const updatedPolicy =
Expand All @@ -185,6 +190,7 @@ const Mutation: GQLMutationResolvers = {
enforcementGuidelines,
userStrikeCount,
applyUserStrikeCountConfigToChildren,
penalty,
},
orgId: user.orgId,
invokedBy: {
Expand Down
Loading
Loading