mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
[CORL-443] Add user drawer to the community section of the moderation area (#2401)
* Move the user history drawer into shared location that all admin routes access CORL-443 * Move the moderate card to the shared components for admin CORL-443 * Add a user drawer to the community area of the admin section CORL-443 * Touch up missing tabs in UserRow.css CORL-443 * Create unit tests around the user drawer CORL-443 * Move toxicity label to new shared component location to fix rebase CORL-443 * Update comment fixture generation to include reason metadata, action counts CORL-443 * Rename userDrawerID to userDrawerUserID CORL-443 * Clean up imports on user drawer unit tests CORL-443 * Add coral-test to the jest config paths CORL-443 * Add todo around creating predictable date times for test fixtures CORL-443 * Move testRenderer construction outside of act() operations CORL-443
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.category {
|
||||
color: var(--palette-error-darkest);
|
||||
}
|
||||
+2
-3
@@ -6,15 +6,14 @@ import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import { Button, CallOut, Typography } from "coral-ui/components/";
|
||||
import { ModerateCardContainer } from "coral-admin/components/ModerateCard";
|
||||
import { Button, CallOut, Typography } from "coral-ui/components";
|
||||
|
||||
import { UserHistoryAllComments_settings } from "coral-admin/__generated__/UserHistoryAllComments_settings.graphql";
|
||||
import { UserHistoryAllComments_user } from "coral-admin/__generated__/UserHistoryAllComments_user.graphql";
|
||||
import { UserHistoryAllComments_viewer } from "coral-admin/__generated__/UserHistoryAllComments_viewer.graphql";
|
||||
import { UserHistoryAllCommentsPaginationQueryVariables } from "coral-admin/__generated__/UserHistoryAllCommentsPaginationQuery.graphql";
|
||||
|
||||
import { ModerateCardContainer } from "../ModerateCard";
|
||||
|
||||
import styles from "./UserHistoryAllComments.css";
|
||||
|
||||
interface Props {
|
||||
+1
-2
@@ -6,6 +6,7 @@ import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import { ModerateCardContainer } from "coral-admin/components/ModerateCard";
|
||||
import { Button, CallOut, Typography } from "coral-ui/components";
|
||||
|
||||
import { UserHistoryRejectedComments_settings } from "coral-admin/__generated__/UserHistoryRejectedComments_settings.graphql";
|
||||
@@ -13,8 +14,6 @@ import { UserHistoryRejectedComments_user } from "coral-admin/__generated__/User
|
||||
import { UserHistoryRejectedComments_viewer } from "coral-admin/__generated__/UserHistoryRejectedComments_viewer.graphql";
|
||||
import { UserHistoryRejectedCommentsPaginationQueryVariables } from "coral-admin/__generated__/UserHistoryRejectedCommentsPaginationQuery.graphql";
|
||||
|
||||
import { ModerateCardContainer } from "../ModerateCard";
|
||||
|
||||
import styles from "./UserHistoryRejectedComments.css";
|
||||
|
||||
interface Props {
|
||||
@@ -21,3 +21,11 @@
|
||||
.statusColumn {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.usernameButton {
|
||||
padding: 0px;
|
||||
font-family: var(--font-family-sans-serif);
|
||||
font-weight: var(--font-weight-regular);
|
||||
font-size: calc(14rem / var(--rem-base));
|
||||
line-height: calc(14em / 14);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
|
||||
import NotAvailable from "coral-admin/components/NotAvailable";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import { TableCell, TableRow, TextLink } from "coral-ui/components";
|
||||
import { Button, TableCell, TableRow, TextLink } from "coral-ui/components";
|
||||
|
||||
import UserRole from "./UserRole";
|
||||
import UserStatus from "./UserStatus";
|
||||
@@ -17,28 +17,47 @@ interface Props {
|
||||
user: PropTypesOf<typeof UserRole>["user"] &
|
||||
PropTypesOf<typeof UserStatus>["user"];
|
||||
viewer: PropTypesOf<typeof UserRole>["viewer"];
|
||||
onUsernameClicked?: (userID: string) => void;
|
||||
}
|
||||
|
||||
const UserRow: FunctionComponent<Props> = props => (
|
||||
<TableRow>
|
||||
<TableCell className={styles.usernameColumn}>
|
||||
{props.username || <NotAvailable />}
|
||||
</TableCell>
|
||||
<TableCell className={styles.emailColumn}>
|
||||
{<TextLink href={`mailto:${props.email}`}>{props.email}</TextLink> || (
|
||||
<NotAvailable />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={styles.memberSinceColumn}>
|
||||
{props.memberSince}
|
||||
</TableCell>
|
||||
<TableCell className={styles.roleColumn}>
|
||||
<UserRole user={props.user} viewer={props.viewer} />
|
||||
</TableCell>
|
||||
<TableCell className={styles.statusColumn}>
|
||||
<UserStatus user={props.user} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
const UserRow: FunctionComponent<Props> = ({
|
||||
userID,
|
||||
username,
|
||||
email,
|
||||
memberSince,
|
||||
user,
|
||||
viewer,
|
||||
onUsernameClicked,
|
||||
}) => {
|
||||
const usernameClicked = useCallback(() => {
|
||||
if (!onUsernameClicked) {
|
||||
return;
|
||||
}
|
||||
|
||||
onUsernameClicked(userID);
|
||||
}, [userID, onUsernameClicked]);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell className={styles.usernameColumn}>
|
||||
<Button onClick={usernameClicked} className={styles.usernameButton}>
|
||||
{username || <NotAvailable />}
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell className={styles.emailColumn}>
|
||||
{<TextLink href={`mailto:${email}`}>{email}</TextLink> || (
|
||||
<NotAvailable />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={styles.memberSinceColumn}>{memberSince}</TableCell>
|
||||
<TableCell className={styles.roleColumn}>
|
||||
<UserRole user={user} viewer={viewer} />
|
||||
</TableCell>
|
||||
<TableCell className={styles.statusColumn}>
|
||||
<UserStatus user={user} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserRow;
|
||||
|
||||
@@ -11,6 +11,7 @@ import UserRow from "./UserRow";
|
||||
interface Props {
|
||||
user: UserData;
|
||||
viewer: ViewerData;
|
||||
onUsernameClicked?: (userID: string) => void;
|
||||
}
|
||||
|
||||
const UserRowContainer: FunctionComponent<Props> = props => {
|
||||
@@ -27,6 +28,7 @@ const UserRowContainer: FunctionComponent<Props> = props => {
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
}).format(new Date(props.user.createdAt))}
|
||||
onUsernameClicked={props.onUsernameClicked}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
|
||||
import AutoLoadMore from "coral-admin/components/AutoLoadMore";
|
||||
import UserHistoryDrawerContainer from "coral-admin/components/UserHistoryDrawer/UserHistoryDrawerContainer";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -28,56 +29,86 @@ interface Props {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const UserTable: FunctionComponent<Props> = props => (
|
||||
<>
|
||||
<HorizontalGutter size="double">
|
||||
<Table fullWidth>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<Localized id="community-column-username">
|
||||
<TableCell className={styles.usernameColumn}>Username</TableCell>
|
||||
</Localized>
|
||||
<Localized id="community-column-email">
|
||||
<TableCell className={styles.emailColumn}>
|
||||
Email Address
|
||||
</TableCell>
|
||||
</Localized>
|
||||
<Localized id="community-column-memberSince">
|
||||
<TableCell className={styles.memberSinceColumn}>
|
||||
Member Since
|
||||
</TableCell>
|
||||
</Localized>
|
||||
<Localized id="community-column-role">
|
||||
<TableCell className={styles.roleColumn}>Role</TableCell>
|
||||
</Localized>
|
||||
<Localized id="community-column-status">
|
||||
<TableCell className={styles.statusColumn}>Status</TableCell>
|
||||
</Localized>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{!props.loading &&
|
||||
props.users.map(u => (
|
||||
<UserRowContainer key={u.id} user={u} viewer={props.viewer!} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{!props.loading && props.users.length === 0 && <EmptyMessage />}
|
||||
{props.loading && (
|
||||
<Flex justifyContent="center">
|
||||
<Spinner />
|
||||
</Flex>
|
||||
)}
|
||||
{props.hasMore && (
|
||||
<Flex justifyContent="center">
|
||||
<AutoLoadMore
|
||||
disableLoadMore={props.disableLoadMore}
|
||||
onLoadMore={props.onLoadMore}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
</>
|
||||
);
|
||||
const UserTable: FunctionComponent<Props> = props => {
|
||||
const [userDrawerUserID, setUserDrawerUserID] = useState("");
|
||||
const [userDrawerVisible, setUserDrawerVisible] = useState(false);
|
||||
|
||||
const onShowUserDrawer = useCallback(
|
||||
(userID: string) => {
|
||||
setUserDrawerUserID(userID);
|
||||
setUserDrawerVisible(true);
|
||||
},
|
||||
[setUserDrawerUserID, setUserDrawerVisible]
|
||||
);
|
||||
|
||||
const onHideUserDrawer = useCallback(() => {
|
||||
setUserDrawerVisible(false);
|
||||
setUserDrawerUserID("");
|
||||
}, [setUserDrawerUserID, setUserDrawerVisible]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HorizontalGutter size="double">
|
||||
<Table fullWidth>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<Localized id="community-column-username">
|
||||
<TableCell className={styles.usernameColumn}>
|
||||
Username
|
||||
</TableCell>
|
||||
</Localized>
|
||||
<Localized id="community-column-email">
|
||||
<TableCell className={styles.emailColumn}>
|
||||
Email Address
|
||||
</TableCell>
|
||||
</Localized>
|
||||
<Localized id="community-column-memberSince">
|
||||
<TableCell className={styles.memberSinceColumn}>
|
||||
Member Since
|
||||
</TableCell>
|
||||
</Localized>
|
||||
<Localized id="community-column-role">
|
||||
<TableCell className={styles.roleColumn}>Role</TableCell>
|
||||
</Localized>
|
||||
<Localized id="community-column-status">
|
||||
<TableCell className={styles.statusColumn}>Status</TableCell>
|
||||
</Localized>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{!props.loading &&
|
||||
props.users.map(u => (
|
||||
<UserRowContainer
|
||||
key={u.id}
|
||||
user={u}
|
||||
viewer={props.viewer!}
|
||||
onUsernameClicked={onShowUserDrawer}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{!props.loading && props.users.length === 0 && <EmptyMessage />}
|
||||
{props.loading && (
|
||||
<Flex justifyContent="center">
|
||||
<Spinner />
|
||||
</Flex>
|
||||
)}
|
||||
{props.hasMore && (
|
||||
<Flex justifyContent="center">
|
||||
<AutoLoadMore
|
||||
disableLoadMore={props.disableLoadMore}
|
||||
onLoadMore={props.onLoadMore}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
<UserHistoryDrawerContainer
|
||||
userID={userDrawerUserID}
|
||||
open={userDrawerVisible}
|
||||
onClose={onHideUserDrawer}
|
||||
/>
|
||||
</HorizontalGutter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserTable;
|
||||
|
||||
@@ -3,12 +3,11 @@ import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
||||
|
||||
import AutoLoadMore from "coral-admin/components/AutoLoadMore";
|
||||
import ModerateCardContainer from "coral-admin/components/ModerateCard";
|
||||
import UserHistoryDrawerContainer from "coral-admin/components/UserHistoryDrawer/UserHistoryDrawerContainer";
|
||||
import { Button, Flex, HorizontalGutter } from "coral-ui/components";
|
||||
import { PropTypesOf } from "coral-ui/types";
|
||||
|
||||
import ModerateCardContainer from "../ModerateCard";
|
||||
import UserHistoryDrawerContainer from "../UserHistoryDrawer/UserHistoryDrawerContainer";
|
||||
|
||||
import styles from "./Queue.css";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -359,7 +359,18 @@ exports[`renders community 1`] = `
|
||||
<td
|
||||
className="TableCell-root UserRow-usernameColumn TableCell-body"
|
||||
>
|
||||
Markus
|
||||
<button
|
||||
className="BaseButton-root Button-root UserRow-usernameButton Button-sizeRegular Button-colorRegular Button-variantRegular"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Markus
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root UserRow-emailColumn TableCell-body"
|
||||
@@ -421,7 +432,18 @@ exports[`renders community 1`] = `
|
||||
<td
|
||||
className="TableCell-root UserRow-usernameColumn TableCell-body"
|
||||
>
|
||||
Lukas
|
||||
<button
|
||||
className="BaseButton-root Button-root UserRow-usernameButton Button-sizeRegular Button-colorRegular Button-variantRegular"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Lukas
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root UserRow-emailColumn TableCell-body"
|
||||
@@ -514,7 +536,18 @@ exports[`renders community 1`] = `
|
||||
<td
|
||||
className="TableCell-root UserRow-usernameColumn TableCell-body"
|
||||
>
|
||||
Huy
|
||||
<button
|
||||
className="BaseButton-root Button-root UserRow-usernameButton Button-sizeRegular Button-colorRegular Button-variantRegular"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Huy
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root UserRow-emailColumn TableCell-body"
|
||||
@@ -607,7 +640,18 @@ exports[`renders community 1`] = `
|
||||
<td
|
||||
className="TableCell-root UserRow-usernameColumn TableCell-body"
|
||||
>
|
||||
Isabelle
|
||||
<button
|
||||
className="BaseButton-root Button-root UserRow-usernameButton Button-sizeRegular Button-colorRegular Button-variantRegular"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Isabelle
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root UserRow-emailColumn TableCell-body"
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
createSettings,
|
||||
createStory,
|
||||
createUser,
|
||||
} from "coral-test/helpers/fixture";
|
||||
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import { GQLResolver, GQLUser, GQLUSER_ROLE } from "coral-framework/schema";
|
||||
import {
|
||||
act,
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import create from "./create";
|
||||
|
||||
const viewer = createUser();
|
||||
viewer.role = GQLUSER_ROLE.ADMIN;
|
||||
const settings = createSettings();
|
||||
|
||||
async function createTestRenderer(
|
||||
user: GQLUser,
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context, subscriptionHandler } = create(
|
||||
{
|
||||
...params,
|
||||
resolvers: pureMerge(
|
||||
createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () => settings,
|
||||
viewer: () => viewer,
|
||||
user: () => user,
|
||||
},
|
||||
}),
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
},
|
||||
},
|
||||
user
|
||||
);
|
||||
|
||||
return { testRenderer, context, subscriptionHandler };
|
||||
}
|
||||
|
||||
it("User drawer is open for user, user name is visible", async () => {
|
||||
const story = createStory();
|
||||
const user = story.comments.edges[0].node.author!;
|
||||
const { testRenderer } = await createTestRenderer(user);
|
||||
|
||||
await act(async () => {
|
||||
const { getByText } = within(testRenderer.root);
|
||||
await waitForElement(() => getByText(user.id, { exact: false }));
|
||||
});
|
||||
});
|
||||
|
||||
it("User drawer is open for user, user name is visible", async () => {
|
||||
const story = createStory();
|
||||
const user = story.comments.edges[0].node.author!;
|
||||
const { testRenderer } = await createTestRenderer(user);
|
||||
|
||||
await act(async () => {
|
||||
const { getByText } = within(testRenderer.root);
|
||||
await waitForElement(() => getByText(user.username!, { exact: false }));
|
||||
});
|
||||
});
|
||||
|
||||
it("All comments selected, comment is visible in all comments", async () => {
|
||||
const story = createStory();
|
||||
const user = story.comments.edges[0].node.author!;
|
||||
const comment = user.allComments.edges[0].node;
|
||||
const { testRenderer } = await createTestRenderer(user);
|
||||
|
||||
await act(async () => {
|
||||
const { getByText } = within(testRenderer.root);
|
||||
|
||||
await waitForElement(() => getByText(comment.body!, { exact: false }));
|
||||
});
|
||||
});
|
||||
|
||||
it("Select rejected comments, rejected comment is visible.", async () => {
|
||||
const story = createStory();
|
||||
const user = story.comments.edges[0].node.author!;
|
||||
const rejectedComment = user.rejectedComments.edges[0].node;
|
||||
const { testRenderer } = await createTestRenderer(user);
|
||||
|
||||
await act(async () => {
|
||||
const { getByText } = within(testRenderer.root);
|
||||
const rejectedTab = await waitForElement(() =>
|
||||
getByText("rejected", {
|
||||
selector: "button",
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
|
||||
rejectedTab.props.onClick();
|
||||
|
||||
await waitForElement(() =>
|
||||
getByText(rejectedComment.body!, { exact: false })
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
|
||||
import UserHistoryDrawerContainer from "coral-admin/components/UserHistoryDrawer/UserHistoryDrawerContainer";
|
||||
import { GQLUser } from "coral-framework/schema";
|
||||
import {
|
||||
createTestRenderer as createTestRendererGeneric,
|
||||
CreateTestRendererParams,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
export default function create(
|
||||
params: CreateTestRendererParams,
|
||||
user: GQLUser
|
||||
) {
|
||||
return createTestRendererGeneric(
|
||||
"userDrawer",
|
||||
<UserHistoryDrawerContainer
|
||||
userID={user.id}
|
||||
open
|
||||
onClose={() => {
|
||||
return;
|
||||
}}
|
||||
/>,
|
||||
{
|
||||
...params,
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user