mirror of
https://github.com/wassname/talk.git
synced 2026-08-05 13:30:26 +08:00
[CORL-139, CORL-140] Community (#2239)
* feat: Add table ui component * feat: community user table * feat: filters and role change * fix: add some comments * fix: user viewer * fix: snapshots * test: add tests * fix: better popover experience * fix: test * chore: use enum * feat: prevent server side setting your own role * fix: cleanup
This commit is contained in:
@@ -49,10 +49,12 @@ it("redirect to redirectPath when already logged in", async () => {
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(createAccessToken(), "accessToken");
|
||||
localRecord.setValue("/admin/community", "redirectPath");
|
||||
localRecord.setValue("/admin/moderate/pending", "redirectPath");
|
||||
},
|
||||
});
|
||||
await wait(() =>
|
||||
expect(window.location.toString()).toBe("http://localhost/admin/community")
|
||||
expect(window.location.toString()).toBe(
|
||||
"http://localhost/admin/moderate/pending"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders community 1`] = `
|
||||
<div
|
||||
className="MainLayout-root Community-root"
|
||||
data-testid="community-container"
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-doubleItemGutter"
|
||||
>
|
||||
<fieldset
|
||||
className="FieldSet-root"
|
||||
>
|
||||
<legend
|
||||
className="Typography-root Typography-bodyCopyBold Typography-colorTextPrimary UserTableFilter-legend"
|
||||
>
|
||||
Search
|
||||
</legend>
|
||||
<div
|
||||
className="TextField-root"
|
||||
>
|
||||
<input
|
||||
aria-label="Search by username or email address"
|
||||
className="TextField-input UserTableFilter-textField TextField-colorRegular"
|
||||
placeholder="Search by username or email address..."
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
className="FieldSet-root"
|
||||
>
|
||||
<legend
|
||||
className="Typography-root Typography-bodyCopyBold Typography-colorTextPrimary UserTableFilter-legend"
|
||||
>
|
||||
Show Me
|
||||
</legend>
|
||||
<span
|
||||
className="SelectField-root"
|
||||
>
|
||||
<select
|
||||
aria-label="Search by role"
|
||||
className="SelectField-select"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
value=""
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
Everyone
|
||||
</option>
|
||||
<optgroup
|
||||
label="Audience"
|
||||
>
|
||||
<option
|
||||
value="COMMENTER"
|
||||
>
|
||||
Commenters
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup
|
||||
label="Organization"
|
||||
>
|
||||
<option
|
||||
value="ADMIN"
|
||||
>
|
||||
Admins
|
||||
</option>
|
||||
<option
|
||||
value="MODERATOR"
|
||||
>
|
||||
Moderators
|
||||
</option>
|
||||
<option
|
||||
value="STAFF"
|
||||
>
|
||||
Staff
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
className="SelectField-afterWrapper"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
expand_more
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<table
|
||||
className="Table-root Table-fullWidth"
|
||||
>
|
||||
<thead
|
||||
className="TableHead-root"
|
||||
>
|
||||
<tr
|
||||
className="TableRow-root"
|
||||
>
|
||||
<th
|
||||
className="TableCell-root UserTable-usernameColumn TableCell-header"
|
||||
>
|
||||
Username
|
||||
</th>
|
||||
<th
|
||||
className="TableCell-root UserTable-emailColumn TableCell-header"
|
||||
>
|
||||
Email
|
||||
</th>
|
||||
<th
|
||||
className="TableCell-root UserTable-memberSinceColumn TableCell-header"
|
||||
>
|
||||
Member Since
|
||||
</th>
|
||||
<th
|
||||
className="TableCell-root UserTable-roleColumn TableCell-header"
|
||||
>
|
||||
Role
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
className="TableBody-root"
|
||||
>
|
||||
<tr
|
||||
className="TableRow-root TableRow-body"
|
||||
>
|
||||
<td
|
||||
className="TableCell-root TableCell-body"
|
||||
>
|
||||
Markus
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root TableCell-body"
|
||||
>
|
||||
<a
|
||||
className="TextLink-root"
|
||||
href="mailto:markus@test.com"
|
||||
>
|
||||
markus@test.com
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root TableCell-body"
|
||||
>
|
||||
07/06/18
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root TableCell-body"
|
||||
>
|
||||
<span
|
||||
className="RoleText-root"
|
||||
>
|
||||
Admin
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
className="TableRow-root TableRow-body"
|
||||
>
|
||||
<td
|
||||
className="TableCell-root TableCell-body"
|
||||
>
|
||||
Lukas
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root TableCell-body"
|
||||
>
|
||||
<a
|
||||
className="TextLink-root"
|
||||
href="mailto:lukas@test.com"
|
||||
>
|
||||
lukas@test.com
|
||||
</a>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root TableCell-body"
|
||||
>
|
||||
07/06/18
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root TableCell-body"
|
||||
>
|
||||
<div
|
||||
className="Popover-root"
|
||||
>
|
||||
<button
|
||||
aria-label="Change role"
|
||||
className="BaseButton-root Button-root RoleChange-button Button-sizeSmall Button-colorRegular Button-variantRegular"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="RoleText-root"
|
||||
>
|
||||
Moderator
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-lg ButtonIcon-root"
|
||||
>
|
||||
arrow_drop_down
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
aria-hidden={true}
|
||||
aria-labelledby="userMenu-ariainfo"
|
||||
id="userMenu"
|
||||
role="popup"
|
||||
>
|
||||
<div
|
||||
className="AriaInfo-root"
|
||||
id="userMenu-ariainfo"
|
||||
>
|
||||
A dropdown to change the user role
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders empty community 1`] = `
|
||||
<div
|
||||
className="MainLayout-root Community-root"
|
||||
data-testid="community-container"
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-doubleItemGutter"
|
||||
>
|
||||
<fieldset
|
||||
className="FieldSet-root"
|
||||
>
|
||||
<legend
|
||||
className="Typography-root Typography-bodyCopyBold Typography-colorTextPrimary UserTableFilter-legend"
|
||||
>
|
||||
Search
|
||||
</legend>
|
||||
<div
|
||||
className="TextField-root"
|
||||
>
|
||||
<input
|
||||
aria-label="Search by username or email address"
|
||||
className="TextField-input UserTableFilter-textField TextField-colorRegular"
|
||||
placeholder="Search by username or email address..."
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
className="FieldSet-root"
|
||||
>
|
||||
<legend
|
||||
className="Typography-root Typography-bodyCopyBold Typography-colorTextPrimary UserTableFilter-legend"
|
||||
>
|
||||
Show Me
|
||||
</legend>
|
||||
<span
|
||||
className="SelectField-root"
|
||||
>
|
||||
<select
|
||||
aria-label="Search by role"
|
||||
className="SelectField-select"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
value=""
|
||||
>
|
||||
<option
|
||||
value=""
|
||||
>
|
||||
Everyone
|
||||
</option>
|
||||
<optgroup
|
||||
label="Audience"
|
||||
>
|
||||
<option
|
||||
value="COMMENTER"
|
||||
>
|
||||
Commenters
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup
|
||||
label="Organization"
|
||||
>
|
||||
<option
|
||||
value="ADMIN"
|
||||
>
|
||||
Admins
|
||||
</option>
|
||||
<option
|
||||
value="MODERATOR"
|
||||
>
|
||||
Moderators
|
||||
</option>
|
||||
<option
|
||||
value="STAFF"
|
||||
>
|
||||
Staff
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
className="SelectField-afterWrapper"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
expand_more
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<table
|
||||
className="Table-root Table-fullWidth"
|
||||
>
|
||||
<thead
|
||||
className="TableHead-root"
|
||||
>
|
||||
<tr
|
||||
className="TableRow-root"
|
||||
>
|
||||
<th
|
||||
className="TableCell-root UserTable-usernameColumn TableCell-header"
|
||||
>
|
||||
Username
|
||||
</th>
|
||||
<th
|
||||
className="TableCell-root UserTable-emailColumn TableCell-header"
|
||||
>
|
||||
Email
|
||||
</th>
|
||||
<th
|
||||
className="TableCell-root UserTable-memberSinceColumn TableCell-header"
|
||||
>
|
||||
Member Since
|
||||
</th>
|
||||
<th
|
||||
className="TableCell-root UserTable-roleColumn TableCell-header"
|
||||
>
|
||||
Role
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
className="TableBody-root"
|
||||
/>
|
||||
</table>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopyBold Typography-colorTextPrimary Typography-alignCenter EmptyMessage-root"
|
||||
>
|
||||
We could not find anyone in your community matching your criteria.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,190 @@
|
||||
import { get, merge } from "lodash";
|
||||
import TestRenderer from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
|
||||
import {
|
||||
createSinonStub,
|
||||
replaceHistoryLocation,
|
||||
waitForElement,
|
||||
waitUntilThrow,
|
||||
within,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import create from "../create";
|
||||
import {
|
||||
communityUsers,
|
||||
emptyCommunityUsers,
|
||||
settings,
|
||||
users,
|
||||
} from "../fixtures";
|
||||
|
||||
beforeEach(async () => {
|
||||
replaceHistoryLocation("http://localhost/admin/community");
|
||||
});
|
||||
|
||||
const createTestRenderer = async (resolver: any = {}) => {
|
||||
const resolvers = {
|
||||
...resolver,
|
||||
Query: {
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(resolver, "Query.settings"))),
|
||||
users: sinon.stub().callsFake((_, data) => {
|
||||
expectAndFail(data.role).toBeFalsy();
|
||||
return communityUsers;
|
||||
}),
|
||||
viewer: sinon.stub().returns(users[0]),
|
||||
...resolver.Query,
|
||||
},
|
||||
};
|
||||
const { testRenderer } = create({
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
},
|
||||
});
|
||||
const container = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("community-container")
|
||||
);
|
||||
return { testRenderer, container };
|
||||
};
|
||||
|
||||
it("renders community", async () => {
|
||||
const { container } = await createTestRenderer();
|
||||
expect(within(container).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders empty community", async () => {
|
||||
const { container } = await createTestRenderer({
|
||||
Query: {
|
||||
users: sinon.stub().returns(emptyCommunityUsers),
|
||||
},
|
||||
});
|
||||
expect(within(container).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("filter by role", async () => {
|
||||
const { container } = await createTestRenderer({
|
||||
Query: {
|
||||
users: createSinonStub(
|
||||
s => s.onFirstCall().returns(communityUsers),
|
||||
s =>
|
||||
s.onSecondCall().callsFake((_, data) => {
|
||||
expectAndFail(data.role).toBe("COMMENTER");
|
||||
return emptyCommunityUsers;
|
||||
})
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
const selectField = within(container).getByLabelText("Search by role");
|
||||
const commentersOption = within(selectField).getByText("Commenters");
|
||||
|
||||
TestRenderer.act(() => {
|
||||
selectField.props.onChange({
|
||||
target: { value: commentersOption.props.value.toString() },
|
||||
});
|
||||
// TODO: Fix act warnings until await Promise.resolve();
|
||||
// or whatever comes out at https://github.com/facebook/react/issues/14769
|
||||
});
|
||||
|
||||
await waitForElement(() =>
|
||||
within(container).getByText("We could not find anyone", { exact: false })
|
||||
);
|
||||
});
|
||||
|
||||
it("can't change viewer role", async () => {
|
||||
const viewer = users[0];
|
||||
const { container } = await createTestRenderer();
|
||||
|
||||
const viewerRow = within(container).getByText(viewer.username, {
|
||||
selector: "tr",
|
||||
});
|
||||
expect(() => within(viewerRow).getByLabelText("Change role")).toThrow();
|
||||
});
|
||||
|
||||
it("change user role", async () => {
|
||||
const user = users[1];
|
||||
const updateUserRole = sinon.stub().callsFake((_: any, data: any) => {
|
||||
expectAndFail(data.input).toMatchObject({
|
||||
userID: user.id,
|
||||
role: "STAFF",
|
||||
});
|
||||
const userRecord = merge({}, user, { role: data.input.role });
|
||||
return {
|
||||
user: userRecord,
|
||||
clientMutationId: data.input.clientMutationId,
|
||||
};
|
||||
});
|
||||
|
||||
const { container } = await createTestRenderer({
|
||||
Mutation: { updateUserRole },
|
||||
});
|
||||
|
||||
const userRow = within(container).getByText(user.username, {
|
||||
selector: "tr",
|
||||
});
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(userRow)
|
||||
.getByLabelText("Change role")
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
const popup = within(userRow).getByLabelText(
|
||||
"A dropdown to change the user role"
|
||||
);
|
||||
|
||||
TestRenderer.act(() => {
|
||||
within(popup)
|
||||
.getByText("Staff")
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
within(userRow).getByText("Staff");
|
||||
expect(updateUserRole.called).toBe(true);
|
||||
});
|
||||
|
||||
it("can't change role as a moderator", async () => {
|
||||
const viewer = users[1];
|
||||
const { container } = await createTestRenderer({
|
||||
Query: {
|
||||
viewer: sinon.stub().returns(viewer),
|
||||
},
|
||||
});
|
||||
expect(() => within(container).getByLabelText("Change role")).toThrow();
|
||||
});
|
||||
|
||||
it("load more", async () => {
|
||||
const { container } = await createTestRenderer({
|
||||
Query: {
|
||||
users: createSinonStub(
|
||||
s =>
|
||||
s.onFirstCall().returns({
|
||||
edges: [
|
||||
{ node: users[0], cursor: users[0].createdAt },
|
||||
{ node: users[1], cursor: users[1].createdAt },
|
||||
],
|
||||
pageInfo: { endCursor: users[1].createdAt, hasNextPage: true },
|
||||
}),
|
||||
s =>
|
||||
s.onSecondCall().returns({
|
||||
edges: [{ node: users[2], cursor: users[2].createdAt }],
|
||||
pageInfo: { endCursor: users[2].createdAt, hasNextPage: false },
|
||||
})
|
||||
),
|
||||
},
|
||||
});
|
||||
const loadMore = within(container).getByText("Load More");
|
||||
TestRenderer.act(() => {
|
||||
loadMore.props.onClick();
|
||||
});
|
||||
|
||||
// Wait for load more to disappear.
|
||||
await waitUntilThrow(() => within(container).getByText("Load More"));
|
||||
|
||||
// Make sure third user was added.
|
||||
within(container).getByText(users[2].username);
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { IResolvers } from "graphql-tools";
|
||||
import React from "react";
|
||||
import TestRenderer from "react-test-renderer";
|
||||
import TestRenderer, { ReactTestRenderer } from "react-test-renderer";
|
||||
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
|
||||
|
||||
import EntryContainer from "talk-admin/containers/EntryContainer";
|
||||
@@ -41,6 +41,7 @@ export default function create(params: CreateParams) {
|
||||
|
||||
const context: TalkContext = {
|
||||
relayEnvironment: environment,
|
||||
locales: ["en-US"],
|
||||
localeBundles: [createFluentBundle()],
|
||||
localStorage: createPromisifiedStorage(),
|
||||
sessionStorage: createPromisifiedStorage(),
|
||||
@@ -52,12 +53,15 @@ export default function create(params: CreateParams) {
|
||||
clearSession: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<TalkContextProvider value={context}>
|
||||
<EntryContainer />
|
||||
</TalkContextProvider>,
|
||||
{ createNodeMock }
|
||||
);
|
||||
let testRenderer: ReactTestRenderer;
|
||||
TestRenderer.act(() => {
|
||||
testRenderer = TestRenderer.create(
|
||||
<TalkContextProvider value={context}>
|
||||
<EntryContainer />
|
||||
</TalkContextProvider>,
|
||||
{ createNodeMock }
|
||||
);
|
||||
});
|
||||
|
||||
return { context, testRenderer };
|
||||
return { context, testRenderer: testRenderer! };
|
||||
}
|
||||
|
||||
@@ -246,27 +246,32 @@ export const moderationActions = [
|
||||
},
|
||||
];
|
||||
|
||||
export const baseUser = {
|
||||
profiles: [{ __typename: "LocalProfile" }],
|
||||
createdAt: "2018-07-06T18:24:00.000Z",
|
||||
};
|
||||
|
||||
export const users = [
|
||||
{
|
||||
...baseUser,
|
||||
id: "user-0",
|
||||
username: "Markus",
|
||||
email: "markus@test.com",
|
||||
role: "ADMIN",
|
||||
profiles: [{ __typename: "LocalProfile" }],
|
||||
},
|
||||
{
|
||||
...baseUser,
|
||||
id: "user-1",
|
||||
username: "Lukas",
|
||||
email: "lukas@test.com",
|
||||
role: "MODERATOR",
|
||||
profiles: [{ __typename: "LocalProfile" }],
|
||||
},
|
||||
{
|
||||
...baseUser,
|
||||
id: "user-2",
|
||||
username: "Isabelle",
|
||||
email: "isabelle@test.com",
|
||||
role: "COMMENTER",
|
||||
profiles: [{ __typename: "LocalProfile" }],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -372,3 +377,16 @@ export const emptyRejectedComments = {
|
||||
edges: [],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
};
|
||||
|
||||
export const communityUsers = {
|
||||
edges: [
|
||||
{ node: users[0], cursor: users[0].createdAt },
|
||||
{ node: users[1], cursor: users[1].createdAt },
|
||||
],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
};
|
||||
|
||||
export const emptyCommunityUsers = {
|
||||
edges: [],
|
||||
pageInfo: { endCursor: null, hasNextPage: false },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user