mirror of
https://github.com/wassname/talk.git
synced 2026-07-28 11:27:05 +08:00
[CORL-156] Manage user suspension status (#2419)
* wire up suspension modal * show user suspended callout on stream * prevent comment actions for suspended users * set default to 3 hour suspension * add message field to suspension * allow custom message for suspension * show suspend success modal * fix type errors * remove unused code * update styles * fix fixture for streams * add suspension ui tests * fix types * remove warnings? * remove snapshot * fix merge conflicts * allow custom email message when banning users * fix typo * correct message type * use final-form in suspend modal * refactor suspend modal to use final-form * refactor ban modal to use final-form * refactor userStatusChangeContainer to use useCallback * feat: improve translated form * fix: addressed issue caused by i18n refactor * update getMessage to accept arguments, remove format method * translate suspend info * change hour format * make message a mandatory input for suspend and ban user * fix types in user table * Revert "fix types in user table" This reverts commit d396e90b88bb1bd354c5cdbdd72b6d8f1ab72929. * fix types for user table * fix: small review tweaks
This commit is contained in:
committed by
Wyatt Johnson
parent
4e548e8fbf
commit
5df2de6afc
@@ -1,7 +1,7 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { DURATION_UNIT } from "coral-framework/components";
|
||||
import { reduceSeconds } from "coral-framework/lib/i18n";
|
||||
|
||||
interface From {
|
||||
start: Date;
|
||||
@@ -13,48 +13,23 @@ export interface SuspensionActionProps {
|
||||
from: From;
|
||||
}
|
||||
|
||||
const UNITS = [
|
||||
DURATION_UNIT.WEEKS,
|
||||
DURATION_UNIT.DAYS,
|
||||
DURATION_UNIT.HOURS,
|
||||
DURATION_UNIT.MINUTES,
|
||||
DURATION_UNIT.SECONDS,
|
||||
];
|
||||
|
||||
const UNIT_MAP = {
|
||||
[DURATION_UNIT.SECONDS]: "second",
|
||||
[DURATION_UNIT.MINUTES]: "minute",
|
||||
[DURATION_UNIT.HOURS]: "hour",
|
||||
[DURATION_UNIT.DAYS]: "day",
|
||||
[DURATION_UNIT.WEEKS]: "week",
|
||||
};
|
||||
|
||||
function reduceValue(value: number) {
|
||||
// Find the largest match for the smallest number.
|
||||
const unit: keyof typeof UNIT_MAP =
|
||||
UNITS.find(compare => value >= compare) || DURATION_UNIT.SECONDS;
|
||||
|
||||
return {
|
||||
value: Math.round((value / unit) * 100) / 100,
|
||||
unit: UNIT_MAP[unit],
|
||||
};
|
||||
}
|
||||
|
||||
const SuspensionAction: FunctionComponent<SuspensionActionProps> = ({
|
||||
action,
|
||||
from,
|
||||
}) => {
|
||||
if (action === "created") {
|
||||
const seconds = (from.finish.getTime() - from.start.getTime()) / 1000;
|
||||
const { value, unit } = reduceValue(seconds);
|
||||
const { scaled, unit } = reduceSeconds(seconds);
|
||||
|
||||
return (
|
||||
<Localized
|
||||
id="moderate-user-drawer-suspension"
|
||||
$unit={unit}
|
||||
$value={value}
|
||||
$value={scaled}
|
||||
>
|
||||
<span>Suspension, {seconds} seconds</span>
|
||||
<span>
|
||||
Suspension, {scaled} {unit}
|
||||
</span>
|
||||
</Localized>
|
||||
);
|
||||
} else if (action === "removed") {
|
||||
|
||||
@@ -2,11 +2,12 @@ import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { ReadyState } from "react-relay";
|
||||
|
||||
import { UserHistoryDrawerQuery as QueryTypes } from "coral-admin/__generated__/UserHistoryDrawerQuery.graphql";
|
||||
import { UserStatusChangeContainer } from "coral-admin/components/UserStatus";
|
||||
import { CopyButton } from "coral-framework/components";
|
||||
import { useCoralContext } from "coral-framework/lib/bootstrap";
|
||||
import { graphql, QueryRenderer } from "coral-framework/lib/relay";
|
||||
|
||||
import { UserHistoryDrawerQuery as QueryTypes } from "coral-admin/__generated__/UserHistoryDrawerQuery.graphql";
|
||||
import { UserStatusChangeContainer } from "coral-admin/components/UserStatus";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
@@ -49,6 +50,12 @@ const UserHistoryDrawerQuery: FunctionComponent<Props> = ({
|
||||
createdAt
|
||||
...UserStatusChangeContainer_user
|
||||
}
|
||||
settings {
|
||||
organization {
|
||||
name
|
||||
}
|
||||
...UserStatusChangeContainer_settings
|
||||
}
|
||||
}
|
||||
`}
|
||||
variables={{ userID }}
|
||||
@@ -74,7 +81,7 @@ const UserHistoryDrawerQuery: FunctionComponent<Props> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const user = props.user;
|
||||
const { user, settings } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -85,10 +92,20 @@ const UserHistoryDrawerQuery: FunctionComponent<Props> = ({
|
||||
<span>{user.username}</span>
|
||||
</Flex>
|
||||
<div className={styles.userStatus}>
|
||||
<div className={styles.userStatusLabel}>Status:</div>
|
||||
<div className={styles.userStatusChange}>
|
||||
<UserStatusChangeContainer user={user} />
|
||||
</div>
|
||||
<Flex alignItems="center">
|
||||
<div className={styles.userStatusLabel}>
|
||||
<Typography variant="bodyCopyBold" container="div">
|
||||
<Flex alignItems="center" itemGutter="half">
|
||||
<Localized id="moderate-user-drawer-status-label">
|
||||
Status:
|
||||
</Localized>
|
||||
</Flex>
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={styles.userStatusChange}>
|
||||
<UserStatusChangeContainer settings={settings} user={user} />
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
<div className={styles.userDetails}>
|
||||
<Flex alignItems="center" className={styles.userDetail}>
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
.card {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.textArea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: calc(12 * var(--mini-unit));
|
||||
padding: calc(0.5 * var(--mini-unit));
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import NotAvailable from "coral-admin/components/NotAvailable";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardCloseButton,
|
||||
CheckBox,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
Modal,
|
||||
Typography,
|
||||
} from "coral-ui/components";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback, useMemo } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import NotAvailable from "coral-admin/components/NotAvailable";
|
||||
import { GetMessage, withGetMessage } from "coral-framework/lib/i18n";
|
||||
|
||||
import styles from "./BanModal.css";
|
||||
|
||||
@@ -18,7 +21,8 @@ interface Props {
|
||||
username: string | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
onConfirm: (message?: string) => void;
|
||||
getMessage: GetMessage;
|
||||
}
|
||||
|
||||
const BanModal: FunctionComponent<Props> = ({
|
||||
@@ -26,51 +30,107 @@ const BanModal: FunctionComponent<Props> = ({
|
||||
onClose,
|
||||
onConfirm,
|
||||
username,
|
||||
}) => (
|
||||
<Modal open={open} onClose={onClose} aria-labelledby="banModal-title">
|
||||
{({ firstFocusableRef, lastFocusableRef }) => (
|
||||
<Card className={styles.card}>
|
||||
<CardCloseButton onClick={onClose} ref={firstFocusableRef} />
|
||||
<HorizontalGutter size="double">
|
||||
<HorizontalGutter>
|
||||
<Localized
|
||||
id="community-banModal-areYouSure"
|
||||
strong={<strong />}
|
||||
$username={username || <NotAvailable />}
|
||||
>
|
||||
<Typography variant="header2" id="banModal-title">
|
||||
Are you sure you want to ban{" "}
|
||||
<strong>{username || <NotAvailable />}</strong>?
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Localized id="community-banModal-consequence">
|
||||
<Typography>
|
||||
Once banned, this user will no longer be able to comment, use
|
||||
reactions, or report comments.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
<Flex justifyContent="flex-end" itemGutter="half">
|
||||
<Localized id="community-banModal-cancel">
|
||||
<Button variant="outlined" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
<Localized id="community-banModal-banUser">
|
||||
<Button
|
||||
variant="filled"
|
||||
color="primary"
|
||||
onClick={onConfirm}
|
||||
ref={lastFocusableRef}
|
||||
>
|
||||
Ban User
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</Card>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
getMessage,
|
||||
}) => {
|
||||
const getDefaultMessage = useMemo((): string => {
|
||||
return getMessage(
|
||||
"community-banModal-emailTemplate",
|
||||
"Someone with access to your account has violated our community guidelines. As a result, your account has been banned. You will no longer be able to comment, react or report comments",
|
||||
{
|
||||
username,
|
||||
}
|
||||
);
|
||||
}, [getMessage, username]);
|
||||
|
||||
export default BanModal;
|
||||
const onFormSubmit = useCallback(
|
||||
({ emailMessage }) => {
|
||||
onConfirm(emailMessage);
|
||||
},
|
||||
[onConfirm]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} aria-labelledby="banModal-title">
|
||||
{({ firstFocusableRef, lastFocusableRef }) => (
|
||||
<Card className={styles.card}>
|
||||
<CardCloseButton onClick={onClose} ref={firstFocusableRef} />
|
||||
<HorizontalGutter size="double">
|
||||
<HorizontalGutter>
|
||||
<Localized
|
||||
id="community-banModal-areYouSure"
|
||||
strong={<strong />}
|
||||
$username={username || <NotAvailable />}
|
||||
>
|
||||
<Typography variant="header2" id="banModal-title">
|
||||
Are you sure you want to ban{" "}
|
||||
<strong>{username || <NotAvailable />}</strong>?
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Localized id="community-banModal-consequence">
|
||||
<Typography>
|
||||
Once banned, this user will no longer be able to comment, use
|
||||
reactions, or report comments.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
<Form
|
||||
onSubmit={onFormSubmit}
|
||||
initialValues={{
|
||||
showMessage: false,
|
||||
emailMessage: getDefaultMessage,
|
||||
}}
|
||||
>
|
||||
{({ handleSubmit }) => (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Field name="showMessage">
|
||||
{({ input }) => (
|
||||
<Localized id="community-banModal-customize">
|
||||
<CheckBox {...input} id="banModal-showMessage">
|
||||
Customize ban email message
|
||||
</CheckBox>
|
||||
</Localized>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="showMessage" subscription={{ value: true }}>
|
||||
{({ input: { value } }) =>
|
||||
value ? (
|
||||
<Field
|
||||
className={styles.textArea}
|
||||
id="banModal-message"
|
||||
component="textarea"
|
||||
name="emailMessage"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
</Field>
|
||||
|
||||
<Flex justifyContent="flex-end" itemGutter="half">
|
||||
<Localized id="community-banModal-cancel">
|
||||
<Button variant="outlined" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
<Localized id="community-banModal-banUser">
|
||||
<Button
|
||||
variant="filled"
|
||||
color="primary"
|
||||
type="submit"
|
||||
ref={lastFocusableRef}
|
||||
>
|
||||
Ban User
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</HorizontalGutter>
|
||||
</Card>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withGetMessage(BanModal);
|
||||
|
||||
export default enhanced;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { RemoveUserSuspensionMutation as MutationTypes } from "coral-admin/__generated__/RemoveUserSuspensionMutation.graphql";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
lookup,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLUser, GQLUSER_STATUS } from "coral-framework/schema";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const RemoveUserSuspensionMutation = createMutation(
|
||||
"removeUserSuspension",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) => {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RemoveUserSuspensionMutation(
|
||||
$input: RemoveUserSuspensionInput!
|
||||
) {
|
||||
removeUserSuspension(input: $input) {
|
||||
user {
|
||||
id
|
||||
status {
|
||||
current
|
||||
suspension {
|
||||
active
|
||||
}
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
removeUserSuspension: {
|
||||
user: {
|
||||
id: input.userID,
|
||||
status: {
|
||||
current: lookup<GQLUser>(
|
||||
environment,
|
||||
input.userID
|
||||
)!.status.current.concat(GQLUSER_STATUS.SUSPENDED),
|
||||
suspension: {
|
||||
active: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default RemoveUserSuspensionMutation;
|
||||
@@ -0,0 +1,207 @@
|
||||
import { Mutator } from "final-form";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import {
|
||||
GetMessage,
|
||||
ScaledUnit,
|
||||
withGetMessage,
|
||||
} from "coral-framework/lib/i18n";
|
||||
|
||||
import {
|
||||
Button,
|
||||
CheckBox,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
RadioButton,
|
||||
} from "coral-ui/components";
|
||||
|
||||
import styles from "./SuspendModal.css";
|
||||
|
||||
interface Props {
|
||||
username: string | null;
|
||||
onCancel: () => void;
|
||||
getMessage: GetMessage;
|
||||
organizationName: string;
|
||||
onSubmit: (duration: ScaledUnit, message: string) => void;
|
||||
}
|
||||
|
||||
const DURATIONS: ScaledUnit[] = [
|
||||
{ original: 3600, value: "3600", unit: "hour", scaled: 1 }, // 1 hour
|
||||
{ original: 10800, value: "10800", unit: "hour", scaled: 3 }, // 3 hours
|
||||
{ original: 86400, value: "86400", unit: "hour", scaled: 24 }, // 24 hours
|
||||
{ original: 604800, value: "604800", unit: "day", scaled: 7 }, // 7 days
|
||||
];
|
||||
|
||||
const DEFAULT_DURATION = DURATIONS[0]; // 1 hour
|
||||
|
||||
const SuspendForm: FunctionComponent<Props> = ({
|
||||
onCancel,
|
||||
username,
|
||||
getMessage,
|
||||
onSubmit,
|
||||
organizationName,
|
||||
}) => {
|
||||
const getMessageWithDuration = useCallback(
|
||||
({ scaled, unit }: Pick<ScaledUnit, "scaled" | "unit">): string => {
|
||||
return getMessage(
|
||||
"community-suspendModal-emailTemplate",
|
||||
`your account has been temporarily suspended. During the suspension, you will be unable to comment, flag or engage with fellow commenters. Please rejoin the conversation in ${scaled} ${unit}`,
|
||||
{
|
||||
username,
|
||||
organizationName,
|
||||
value: scaled,
|
||||
unit,
|
||||
}
|
||||
);
|
||||
},
|
||||
[username, organizationName]
|
||||
);
|
||||
|
||||
const onFormSubmit = useCallback(
|
||||
({ duration, emailMessage }) => {
|
||||
const unit = DURATIONS.find(d => d.value === duration);
|
||||
onSubmit(unit!, emailMessage);
|
||||
},
|
||||
[onSubmit]
|
||||
);
|
||||
|
||||
const setMessageValue: Mutator = useCallback(
|
||||
([name, newValue], state, { changeValue }) => {
|
||||
if (state.lastFormState) {
|
||||
const { duration, emailMessage } = state.lastFormState.values;
|
||||
const unit = DURATIONS.find(d => d.value === duration);
|
||||
const expectedEmailMessage = getMessageWithDuration(unit!);
|
||||
if (expectedEmailMessage === emailMessage) {
|
||||
changeValue(state, name, () => newValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
[getMessageWithDuration]
|
||||
);
|
||||
|
||||
const resetMessageValue: Mutator = (
|
||||
[name, checked],
|
||||
state,
|
||||
{ changeValue }
|
||||
) => {
|
||||
if (state.lastFormState && !checked) {
|
||||
const { duration, emailMessage } = state.lastFormState.values;
|
||||
const unit = DURATIONS.find(d => d.value === duration);
|
||||
const expectedEmailMessage = getMessageWithDuration(unit!);
|
||||
if (expectedEmailMessage !== emailMessage) {
|
||||
changeValue(state, name, () => expectedEmailMessage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
onSubmit={onFormSubmit}
|
||||
mutators={{
|
||||
setMessageValue,
|
||||
resetMessageValue,
|
||||
}}
|
||||
initialValues={{
|
||||
duration: DEFAULT_DURATION.value,
|
||||
emailMessage: getMessageWithDuration(DEFAULT_DURATION),
|
||||
}}
|
||||
>
|
||||
{({ handleSubmit, form }) => (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<HorizontalGutter spacing={2}>
|
||||
<div>
|
||||
{DURATIONS.map(({ original, value, scaled, unit }) => (
|
||||
<Field
|
||||
key={value}
|
||||
name="duration"
|
||||
type="radio"
|
||||
component="input"
|
||||
value={value}
|
||||
>
|
||||
{({ input }) => (
|
||||
<Localized
|
||||
id="framework-timeago-time"
|
||||
$value={scaled}
|
||||
$unit={unit}
|
||||
>
|
||||
<RadioButton
|
||||
id={`duration-${value}`}
|
||||
{...input}
|
||||
onChange={event => {
|
||||
form.mutators.setMessageValue(
|
||||
"emailMessage",
|
||||
getMessageWithDuration({ scaled, unit })
|
||||
);
|
||||
input.onChange(event);
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{scaled} {unit}
|
||||
</span>
|
||||
</RadioButton>
|
||||
</Localized>
|
||||
)}
|
||||
</Field>
|
||||
))}
|
||||
</div>
|
||||
<Field type="checkbox" name="editMessage">
|
||||
{({ input }) => (
|
||||
<Localized id="community-suspendModal-customize">
|
||||
<CheckBox
|
||||
id="suspendModal-editMessage"
|
||||
{...input}
|
||||
onChange={event => {
|
||||
form.mutators.resetMessageValue(
|
||||
"emailMessage",
|
||||
!input.checked
|
||||
);
|
||||
input.onChange(event);
|
||||
}}
|
||||
>
|
||||
Customize suspension email message
|
||||
</CheckBox>
|
||||
</Localized>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="editMessage" subscription={{ value: true }}>
|
||||
{({ input: { value } }) =>
|
||||
value ? (
|
||||
<Field
|
||||
className={styles.textArea}
|
||||
id="suspendModal-message"
|
||||
component="textarea"
|
||||
name="emailMessage"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
</Field>
|
||||
</HorizontalGutter>
|
||||
<Flex
|
||||
className={styles.footer}
|
||||
justifyContent="flex-end"
|
||||
itemGutter="half"
|
||||
>
|
||||
<Localized id="community-suspendModal-cancel">
|
||||
<Button variant="outlined" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
<Localized id="community-suspendModal-suspendUser">
|
||||
<Button variant="filled" color="primary" type="submit">
|
||||
Suspend User
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withGetMessage(SuspendForm);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,22 @@
|
||||
.card {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.radioButton {
|
||||
margin: 0 var(--spacing-1) 0 0 !important;
|
||||
}
|
||||
|
||||
.textArea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: calc(12 * var(--mini-unit));
|
||||
padding: calc(0.5 * var(--mini-unit));
|
||||
}
|
||||
|
||||
.header {
|
||||
margin: 0 0 var(--spacing-3) 0 !important;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: var(--spacing-3);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
|
||||
import {
|
||||
GetMessage,
|
||||
ScaledUnit,
|
||||
withGetMessage,
|
||||
} from "coral-framework/lib/i18n";
|
||||
|
||||
import NotAvailable from "coral-admin/components/NotAvailable";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardCloseButton,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
Modal,
|
||||
Typography,
|
||||
} from "coral-ui/components";
|
||||
|
||||
import SuspendForm from "./SuspendForm";
|
||||
|
||||
import styles from "./SuspendModal.css";
|
||||
|
||||
interface Props {
|
||||
username: string | null;
|
||||
getMessage: GetMessage;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (timeout: number, message: string) => void;
|
||||
organizationName: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
const SuspendModal: FunctionComponent<Props> = ({
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
getMessage,
|
||||
username,
|
||||
success,
|
||||
organizationName,
|
||||
}) => {
|
||||
const [successDuration, setSuccessDuration] = useState("");
|
||||
const onFormSubmit = useCallback(
|
||||
({ original, scaled, unit }: ScaledUnit, message: string) => {
|
||||
setSuccessDuration(
|
||||
getMessage("framework-timeago-time", `${scaled} ${unit}`, {
|
||||
value: scaled,
|
||||
unit,
|
||||
})
|
||||
);
|
||||
onConfirm(original, message);
|
||||
},
|
||||
[onConfirm]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal open={open} onClose={onClose} aria-labelledby="suspendModal-title">
|
||||
{({ firstFocusableRef, lastFocusableRef }) => (
|
||||
<Card className={styles.card}>
|
||||
<CardCloseButton onClick={onClose} ref={firstFocusableRef} />
|
||||
{success && (
|
||||
<HorizontalGutter spacing={2}>
|
||||
<HorizontalGutter>
|
||||
<Localized
|
||||
id="community-suspendModal-success"
|
||||
$username={username}
|
||||
strong={<strong />}
|
||||
$duration={successDuration}
|
||||
>
|
||||
<Typography>
|
||||
<strong>{username}</strong> has been suspended for{" "}
|
||||
<strong>{successDuration}</strong>
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
|
||||
<Flex justifyContent="flex-end" itemGutter="half">
|
||||
<Localized id="community-suspendModal-success-close">
|
||||
<Button variant="filled" color="primary" onClick={onClose}>
|
||||
Ok
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
)}
|
||||
{!success && (
|
||||
<HorizontalGutter spacing={2}>
|
||||
<Localized
|
||||
id="community-suspendModal-areYouSure"
|
||||
strong={<strong />}
|
||||
$username={username || <NotAvailable />}
|
||||
>
|
||||
<Typography
|
||||
className={styles.header}
|
||||
variant="header2"
|
||||
id="suspendModal-title"
|
||||
>
|
||||
Suspend <strong>{username || <NotAvailable />}</strong>?
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Localized id="community-suspendModal-consequence">
|
||||
<Typography>
|
||||
While suspended, this user will no longer be able to
|
||||
comment, use reactions, or report comments.
|
||||
</Typography>
|
||||
</Localized>
|
||||
|
||||
<Localized id="community-suspendModal-selectDuration">
|
||||
<Typography variant="header3">
|
||||
Select suspension length
|
||||
</Typography>
|
||||
</Localized>
|
||||
|
||||
<SuspendForm
|
||||
username={username}
|
||||
onCancel={onClose}
|
||||
organizationName={organizationName}
|
||||
onSubmit={onFormSubmit}
|
||||
/>
|
||||
</HorizontalGutter>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withGetMessage(SuspendModal);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { SuspendUserMutation as MutationTypes } from "coral-admin/__generated__/SuspendUserMutation.graphql";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
lookup,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLUser, GQLUSER_STATUS } from "coral-framework/schema";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const SuspendUserMutation = createMutation(
|
||||
"suspendUser",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) => {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation SuspendUserMutation($input: SuspendUserInput!) {
|
||||
suspendUser(input: $input) {
|
||||
user {
|
||||
id
|
||||
status {
|
||||
current
|
||||
suspension {
|
||||
active
|
||||
}
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
suspendUser: {
|
||||
user: {
|
||||
id: input.userID,
|
||||
status: {
|
||||
current: lookup<GQLUser>(
|
||||
environment,
|
||||
input.userID
|
||||
)!.status.current.concat(GQLUSER_STATUS.SUSPENDED),
|
||||
suspension: {
|
||||
active: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default SuspendUserMutation;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
|
||||
import { UserStatusChangeContainer_settings as SettingsData } from "coral-admin/__generated__/UserStatusChangeContainer_settings.graphql";
|
||||
import { UserStatusChangeContainer_user as UserData } from "coral-admin/__generated__/UserStatusChangeContainer_user.graphql";
|
||||
import {
|
||||
graphql,
|
||||
@@ -12,21 +13,27 @@ import ButtonPadding from "../ButtonPadding";
|
||||
import BanModal from "./BanModal";
|
||||
import BanUserMutation from "./BanUserMutation";
|
||||
import RemoveUserBanMutation from "./RemoveUserBanMutation";
|
||||
import RemoveUserSuspensionMutation from "./RemoveUserSuspensionMutation";
|
||||
import SuspendModal from "./SuspendModal";
|
||||
import SuspendUserMutation from "./SuspendUserMutation";
|
||||
import UserStatusChange from "./UserStatusChange";
|
||||
import UserStatusContainer from "./UserStatusContainer";
|
||||
|
||||
interface Props {
|
||||
user: UserData;
|
||||
fullWidth?: boolean;
|
||||
settings: SettingsData;
|
||||
}
|
||||
|
||||
const UserStatusChangeContainer: FunctionComponent<Props> = ({
|
||||
user,
|
||||
fullWidth = false,
|
||||
}) => {
|
||||
const UserStatusChangeContainer: FunctionComponent<Props> = props => {
|
||||
const { user, settings, fullWidth } = props;
|
||||
const banUser = useMutation(BanUserMutation);
|
||||
const suspendUser = useMutation(SuspendUserMutation);
|
||||
const removeUserBan = useMutation(RemoveUserBanMutation);
|
||||
const removeUserSuspension = useMutation(RemoveUserSuspensionMutation);
|
||||
const [showBanned, setShowBanned] = useState<boolean>(false);
|
||||
const [showSuspend, setShowSuspend] = useState<boolean>(false);
|
||||
const [showSuspendSuccess, setShowSuspendSuccess] = useState<boolean>(false);
|
||||
const handleBan = useCallback(() => {
|
||||
if (user.status.ban.active) {
|
||||
return;
|
||||
@@ -43,14 +50,43 @@ const UserStatusChangeContainer: FunctionComponent<Props> = ({
|
||||
if (user.status.suspension.active) {
|
||||
return;
|
||||
}
|
||||
// TODO: (cvle)
|
||||
}, [user]);
|
||||
setShowSuspend(true);
|
||||
}, [user, setShowSuspend]);
|
||||
const handleRemoveSuspension = useCallback(() => {
|
||||
if (!user.status.suspension.active) {
|
||||
return;
|
||||
}
|
||||
// TODO: (cvle)
|
||||
}, [user]);
|
||||
removeUserSuspension({ userID: user.id });
|
||||
}, [user, removeUserSuspension]);
|
||||
|
||||
const handleSuspendModalClose = useCallback(() => {
|
||||
setShowSuspend(false);
|
||||
setShowSuspendSuccess(false);
|
||||
}, [setShowBanned, setShowSuspendSuccess]);
|
||||
|
||||
const handleBanModalClose = useCallback(() => {
|
||||
setShowBanned(false);
|
||||
}, [setShowBanned]);
|
||||
|
||||
const handleSuspendConfirm = useCallback(
|
||||
(timeout, message) => {
|
||||
suspendUser({
|
||||
userID: user.id,
|
||||
timeout,
|
||||
message,
|
||||
});
|
||||
setShowSuspendSuccess(true);
|
||||
},
|
||||
[user, suspendUser, setShowSuspendSuccess]
|
||||
);
|
||||
|
||||
const handleBanConfirm = useCallback(
|
||||
message => {
|
||||
banUser({ userID: user.id, message });
|
||||
setShowBanned(false);
|
||||
},
|
||||
[user, setShowBanned]
|
||||
);
|
||||
|
||||
if (user.role !== GQLUSER_ROLE.COMMENTER) {
|
||||
return (
|
||||
@@ -73,14 +109,19 @@ const UserStatusChangeContainer: FunctionComponent<Props> = ({
|
||||
>
|
||||
<UserStatusContainer user={user} />
|
||||
</UserStatusChange>
|
||||
<SuspendModal
|
||||
username={user.username}
|
||||
open={showSuspend || showSuspendSuccess}
|
||||
success={showSuspendSuccess}
|
||||
onClose={handleSuspendModalClose}
|
||||
organizationName={settings.organization.name}
|
||||
onConfirm={handleSuspendConfirm}
|
||||
/>
|
||||
<BanModal
|
||||
username={user.username}
|
||||
open={showBanned}
|
||||
onClose={() => setShowBanned(false)}
|
||||
onConfirm={() => {
|
||||
banUser({ userID: user.id });
|
||||
setShowBanned(false);
|
||||
}}
|
||||
onClose={handleBanModalClose}
|
||||
onConfirm={handleBanConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -89,7 +130,6 @@ const UserStatusChangeContainer: FunctionComponent<Props> = ({
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
user: graphql`
|
||||
fragment UserStatusChangeContainer_user on User {
|
||||
...UserStatusContainer_user
|
||||
id
|
||||
role
|
||||
username
|
||||
@@ -101,6 +141,14 @@ const enhanced = withFragmentContainer<Props>({
|
||||
active
|
||||
}
|
||||
}
|
||||
...UserStatusContainer_user
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment UserStatusChangeContainer_settings on Settings {
|
||||
organization {
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(UserStatusChangeContainer);
|
||||
|
||||
@@ -16,6 +16,7 @@ interface Props {
|
||||
user: PropTypesOf<typeof UserRole>["user"] &
|
||||
PropTypesOf<typeof UserStatus>["user"];
|
||||
viewer: PropTypesOf<typeof UserRole>["viewer"];
|
||||
settings: PropTypesOf<typeof UserStatus>["settings"];
|
||||
onUsernameClicked?: (userID: string) => void;
|
||||
}
|
||||
|
||||
@@ -27,6 +28,7 @@ const UserRow: FunctionComponent<Props> = ({
|
||||
user,
|
||||
viewer,
|
||||
onUsernameClicked,
|
||||
settings,
|
||||
}) => {
|
||||
const usernameClicked = useCallback(() => {
|
||||
if (!onUsernameClicked) {
|
||||
@@ -53,7 +55,7 @@ const UserRow: FunctionComponent<Props> = ({
|
||||
<UserRole user={user} viewer={viewer} />
|
||||
</TableCell>
|
||||
<TableCell className={styles.statusColumn}>
|
||||
<UserStatus user={user} fullWidth />
|
||||
<UserStatus user={user} settings={settings} fullWidth />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { UserRowContainer_settings as SettingsData } from "coral-admin/__generated__/UserRowContainer_settings.graphql";
|
||||
import { UserRowContainer_user as UserData } from "coral-admin/__generated__/UserRowContainer_user.graphql";
|
||||
import { UserRowContainer_viewer as ViewerData } from "coral-admin/__generated__/UserRowContainer_viewer.graphql";
|
||||
import { useCoralContext } from "coral-framework/lib/bootstrap";
|
||||
@@ -11,6 +12,7 @@ import UserRow from "./UserRow";
|
||||
interface Props {
|
||||
user: UserData;
|
||||
viewer: ViewerData;
|
||||
settings: SettingsData;
|
||||
onUsernameClicked?: (userID: string) => void;
|
||||
}
|
||||
|
||||
@@ -19,6 +21,7 @@ const UserRowContainer: FunctionComponent<Props> = props => {
|
||||
return (
|
||||
<UserRow
|
||||
user={props.user}
|
||||
settings={props.settings}
|
||||
viewer={props.viewer}
|
||||
userID={props.user.id}
|
||||
username={props.user.username!}
|
||||
@@ -39,6 +42,11 @@ const enhanced = withFragmentContainer<Props>({
|
||||
...UserRoleChangeContainer_viewer
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment UserRowContainer_settings on Settings {
|
||||
...UserStatusChangeContainer_settings
|
||||
}
|
||||
`,
|
||||
user: graphql`
|
||||
fragment UserRowContainer_user on User {
|
||||
...UserStatusChangeContainer_user
|
||||
|
||||
@@ -22,6 +22,7 @@ import styles from "./UserTable.css";
|
||||
|
||||
interface Props {
|
||||
viewer: PropTypesOf<typeof UserRowContainer>["viewer"] | null;
|
||||
settings: PropTypesOf<typeof UserRowContainer>["settings"] | null;
|
||||
users: Array<{ id: string } & PropTypesOf<typeof UserRowContainer>["user"]>;
|
||||
onLoadMore: () => void;
|
||||
hasMore: boolean;
|
||||
@@ -29,7 +30,11 @@ interface Props {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const UserTable: FunctionComponent<Props> = props => {
|
||||
const UserTable: FunctionComponent<Props> = ({
|
||||
viewer,
|
||||
settings,
|
||||
...props
|
||||
}) => {
|
||||
const [userDrawerUserID, setUserDrawerUserID] = useState("");
|
||||
const [userDrawerVisible, setUserDrawerVisible] = useState(false);
|
||||
|
||||
@@ -45,7 +50,6 @@ const UserTable: FunctionComponent<Props> = props => {
|
||||
setUserDrawerVisible(false);
|
||||
setUserDrawerUserID("");
|
||||
}, [setUserDrawerUserID, setUserDrawerVisible]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HorizontalGutter size="double">
|
||||
@@ -77,11 +81,14 @@ const UserTable: FunctionComponent<Props> = props => {
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{!props.loading &&
|
||||
settings &&
|
||||
viewer &&
|
||||
props.users.map(u => (
|
||||
<UserRowContainer
|
||||
key={u.id}
|
||||
user={u}
|
||||
viewer={props.viewer!}
|
||||
settings={settings}
|
||||
viewer={viewer}
|
||||
onUsernameClicked={onShowUserDrawer}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -52,6 +52,7 @@ const UserTableContainer: FunctionComponent<Props> = props => {
|
||||
/>
|
||||
<UserTable
|
||||
viewer={props.query && props.query.viewer}
|
||||
settings={props.query && props.query.settings}
|
||||
loading={!props.query || isRefetching}
|
||||
users={users}
|
||||
onLoadMore={loadMore}
|
||||
@@ -87,6 +88,7 @@ const enhanced = withPaginationContainer<
|
||||
}
|
||||
settings {
|
||||
...InviteUsersContainer_settings
|
||||
...UserRowContainer_settings
|
||||
}
|
||||
users(
|
||||
first: $count
|
||||
|
||||
@@ -1,120 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ban user 1`] = `
|
||||
<div
|
||||
aria-labelledby="banModal-title"
|
||||
className="Modal-root"
|
||||
onKeyDown={[Function]}
|
||||
role="modal"
|
||||
>
|
||||
<div
|
||||
className="Backdrop-root Backdrop-active"
|
||||
data-testid="backdrop"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="Modal-scroll"
|
||||
>
|
||||
<div
|
||||
className="Modal-alignContainer1"
|
||||
>
|
||||
<div
|
||||
className="Modal-alignContainer2"
|
||||
>
|
||||
<div
|
||||
className="Modal-wrapper"
|
||||
>
|
||||
<div
|
||||
onFocus={[Function]}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<div
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
className="Card-root BanModal-card"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root CloseButton-root"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md CloseButton-icon"
|
||||
>
|
||||
close
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<h1
|
||||
className="Box-root Typography-root Typography-header2 Typography-colorTextPrimary"
|
||||
id="banModal-title"
|
||||
>
|
||||
Are you sure you want to ban
|
||||
<strong>
|
||||
Isabelle
|
||||
</strong>
|
||||
?
|
||||
</h1>
|
||||
<p
|
||||
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Once banned, this user will no longer be able to comment, use
|
||||
reactions, or report comments.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-halfItemGutter Flex-justifyFlexEnd gutter"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantOutlined"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Ban User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onFocus={[Function]}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders community 1`] = `
|
||||
<div
|
||||
className="MainLayout-root Community-root"
|
||||
|
||||
@@ -404,6 +404,264 @@ it("can't change staff, moderator and admin status", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("suspend user", async () => {
|
||||
const user = users.commenters[0];
|
||||
|
||||
const resolvers = createResolversStub<GQLResolver>({
|
||||
Mutation: {
|
||||
suspendUser: ({ variables }) => {
|
||||
expectAndFail(variables).toMatchObject({
|
||||
userID: user.id,
|
||||
});
|
||||
const userRecord = pureMerge<typeof user>(user, {
|
||||
status: {
|
||||
current: user.status.current.concat(GQLUSER_STATUS.SUSPENDED),
|
||||
suspension: { active: true },
|
||||
},
|
||||
});
|
||||
return {
|
||||
user: userRecord,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { container, testRenderer } = await createTestRenderer({
|
||||
resolvers,
|
||||
});
|
||||
|
||||
const userRow = within(container).getByText(user.username!, {
|
||||
selector: "tr",
|
||||
});
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(userRow)
|
||||
.getByLabelText("Change user status")
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
const popup = within(userRow).getByLabelText(
|
||||
"A dropdown to change the user status"
|
||||
);
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Suspend User", { selector: "button" })
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
const modal = within(testRenderer.root).getByLabelText("Suspend", {
|
||||
exact: false,
|
||||
});
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(modal)
|
||||
.getByType("form")
|
||||
.props.onSubmit();
|
||||
});
|
||||
within(userRow).getByText("Suspended");
|
||||
expect(resolvers.Mutation!.suspendUser!.called).toBe(true);
|
||||
});
|
||||
|
||||
it("remove user suspension", async () => {
|
||||
const user = users.suspendedCommenter;
|
||||
const resolvers = createResolversStub<GQLResolver>({
|
||||
Mutation: {
|
||||
removeUserSuspension: ({ variables }) => {
|
||||
expectAndFail(variables).toMatchObject({
|
||||
userID: user.id,
|
||||
});
|
||||
const userRecord = pureMerge<typeof user>(user, {
|
||||
status: {
|
||||
current: [GQLUSER_STATUS.ACTIVE],
|
||||
suspension: { active: false },
|
||||
},
|
||||
});
|
||||
return {
|
||||
user: userRecord,
|
||||
};
|
||||
},
|
||||
},
|
||||
Query: {
|
||||
users: () => ({
|
||||
edges: [
|
||||
{
|
||||
node: user,
|
||||
cursor: user.createdAt,
|
||||
},
|
||||
],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = await createTestRenderer({
|
||||
resolvers,
|
||||
});
|
||||
|
||||
const userRow = within(container).getByText(user.username!, {
|
||||
selector: "tr",
|
||||
});
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(userRow)
|
||||
.getByLabelText("Change user status")
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
const popup = within(userRow).getByLabelText(
|
||||
"A dropdown to change the user status"
|
||||
);
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Remove Suspension", { selector: "button" })
|
||||
.props.onClick();
|
||||
});
|
||||
expect(resolvers.Mutation!.removeUserSuspension!.called).toBe(true);
|
||||
|
||||
await waitForElement(() => within(userRow).getByText("Active"));
|
||||
});
|
||||
|
||||
it("suspend user with custom timeout", async () => {
|
||||
const user = users.commenters[0];
|
||||
|
||||
const resolvers = createResolversStub<GQLResolver>({
|
||||
Mutation: {
|
||||
suspendUser: ({ variables }) => {
|
||||
expectAndFail(variables).toMatchObject({
|
||||
userID: user.id,
|
||||
timeout: 604800,
|
||||
});
|
||||
const userRecord = pureMerge<typeof user>(user, {
|
||||
status: {
|
||||
current: user.status.current.concat(GQLUSER_STATUS.SUSPENDED),
|
||||
suspension: { active: true },
|
||||
},
|
||||
});
|
||||
return {
|
||||
user: userRecord,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { container, testRenderer } = await createTestRenderer({
|
||||
resolvers,
|
||||
});
|
||||
|
||||
const userRow = within(container).getByText(user.username!, {
|
||||
selector: "tr",
|
||||
});
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(userRow)
|
||||
.getByLabelText("Change user status")
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
const popup = within(userRow).getByLabelText(
|
||||
"A dropdown to change the user status"
|
||||
);
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Suspend User", { selector: "button" })
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
const modal = within(testRenderer.root).getByLabelText("Suspend", {
|
||||
exact: false,
|
||||
});
|
||||
|
||||
const changedDuration = within(modal).getByID("duration-604800");
|
||||
|
||||
TestRenderer.act(() => {
|
||||
changedDuration.props.onChange(changedDuration.props.value.toString());
|
||||
});
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(modal)
|
||||
.getByType("form")
|
||||
.props.onSubmit();
|
||||
});
|
||||
within(userRow).getByText("Suspended");
|
||||
expect(resolvers.Mutation!.suspendUser!.called).toBe(true);
|
||||
});
|
||||
|
||||
it("suspend user with custom message", async () => {
|
||||
const user = users.commenters[0];
|
||||
|
||||
const resolvers = createResolversStub<GQLResolver>({
|
||||
Mutation: {
|
||||
suspendUser: ({ variables }) => {
|
||||
expectAndFail(variables).toMatchObject({
|
||||
userID: user.id,
|
||||
message: "YOU WERE SUSPENDED FOR BEHAVING BADLY",
|
||||
});
|
||||
const userRecord = pureMerge<typeof user>(user, {
|
||||
status: {
|
||||
current: user.status.current.concat(GQLUSER_STATUS.SUSPENDED),
|
||||
suspension: { active: true },
|
||||
},
|
||||
});
|
||||
return {
|
||||
user: userRecord,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { container, testRenderer } = await createTestRenderer({
|
||||
resolvers,
|
||||
});
|
||||
|
||||
const userRow = within(container).getByText(user.username!, {
|
||||
selector: "tr",
|
||||
});
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(userRow)
|
||||
.getByLabelText("Change user status")
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
const popup = within(userRow).getByLabelText(
|
||||
"A dropdown to change the user status"
|
||||
);
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Suspend User", { selector: "button" })
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
const modal = within(testRenderer.root).getByLabelText("Suspend", {
|
||||
exact: false,
|
||||
});
|
||||
|
||||
const toggleEdit = within(modal).getByID("suspendModal-editMessage");
|
||||
|
||||
TestRenderer.act(() => {
|
||||
toggleEdit.props.onChange(true);
|
||||
});
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(modal)
|
||||
.getByID("suspendModal-message")
|
||||
.props.onChange("YOU WERE SUSPENDED FOR BEHAVING BADLY");
|
||||
});
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(modal)
|
||||
.getByType("form")
|
||||
.props.onSubmit();
|
||||
});
|
||||
|
||||
within(userRow).getByText("Suspended");
|
||||
expect(resolvers.Mutation!.suspendUser!.called).toBe(true);
|
||||
});
|
||||
|
||||
it("ban user", async () => {
|
||||
const user = users.commenters[0];
|
||||
|
||||
@@ -457,12 +715,85 @@ it("ban user", async () => {
|
||||
}
|
||||
);
|
||||
|
||||
expect(within(modal).toJSON()).toMatchSnapshot();
|
||||
TestRenderer.act(() => {
|
||||
within(modal)
|
||||
.getByType("form")
|
||||
.props.onSubmit();
|
||||
});
|
||||
within(userRow).getByText("Banned");
|
||||
expect(resolvers.Mutation!.banUser!.called).toBe(true);
|
||||
});
|
||||
|
||||
it("ban user with custom message", async () => {
|
||||
const user = users.commenters[0];
|
||||
|
||||
const resolvers = createResolversStub<GQLResolver>({
|
||||
Mutation: {
|
||||
banUser: ({ variables }) => {
|
||||
expectAndFail(variables).toMatchObject({
|
||||
userID: user.id,
|
||||
message: "YOU WERE BANNED FOR BREAKING THE RULES",
|
||||
});
|
||||
const userRecord = pureMerge<typeof user>(user, {
|
||||
status: {
|
||||
current: user.status.current.concat(GQLUSER_STATUS.BANNED),
|
||||
ban: { active: true },
|
||||
},
|
||||
});
|
||||
return {
|
||||
user: userRecord,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { container, testRenderer } = await createTestRenderer({
|
||||
resolvers,
|
||||
});
|
||||
|
||||
const userRow = within(container).getByText(user.username!, {
|
||||
selector: "tr",
|
||||
});
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(userRow)
|
||||
.getByLabelText("Change user status")
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
const popup = within(userRow).getByLabelText(
|
||||
"A dropdown to change the user status"
|
||||
);
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Ban User", { selector: "button" })
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
const modal = within(testRenderer.root).getByLabelText(
|
||||
"Are you sure you want to ban",
|
||||
{
|
||||
exact: false,
|
||||
}
|
||||
);
|
||||
|
||||
const toggleMessage = within(modal).getByID("banModal-showMessage");
|
||||
|
||||
TestRenderer.act(() => {
|
||||
toggleMessage.props.onChange(true);
|
||||
});
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(modal)
|
||||
.getByText("Ban User")
|
||||
.props.onClick();
|
||||
.getByID("banModal-message")
|
||||
.props.onChange("YOU WERE BANNED FOR BREAKING THE RULES");
|
||||
});
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(modal)
|
||||
.getByType("form")
|
||||
.props.onSubmit();
|
||||
});
|
||||
within(userRow).getByText("Banned");
|
||||
expect(resolvers.Mutation!.banUser!.called).toBe(true);
|
||||
|
||||
@@ -355,6 +355,24 @@ export const users = {
|
||||
],
|
||||
baseUser
|
||||
),
|
||||
suspendedCommenter: createFixture<GQLUser>(
|
||||
{
|
||||
id: "user-suspended-0",
|
||||
username: "lol1111",
|
||||
email: "lol1111@test.com",
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
ignoreable: true,
|
||||
status: {
|
||||
current: [GQLUSER_STATUS.SUSPENDED],
|
||||
ban: { active: false },
|
||||
suspension: {
|
||||
active: true,
|
||||
until: new Date(Date.now() + 600000).toISOString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
baseUser
|
||||
),
|
||||
bannedCommenter: createFixture<GQLUser>(
|
||||
{
|
||||
id: "user-banned-0",
|
||||
|
||||
Reference in New Issue
Block a user