[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:
Tessa Thornton
2019-08-02 21:16:21 +00:00
committed by Wyatt Johnson
parent 4e548e8fbf
commit 5df2de6afc
42 changed files with 1511 additions and 276 deletions
@@ -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);
+18
View File
@@ -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",