[next] Decision History (#2103)

* feat: decision history components

* feat: implement decision history container

* test: add unit tests

* test: integration test

* test: fix remaining snapshots

* test: add more concepts from react-testing-library

* fix: wrap long username into the next line

* fix: fallback to word-break: break-all;
This commit is contained in:
Kiwi
2018-11-30 20:51:45 +01:00
committed by GitHub
parent a5c98cd587
commit 5b62082693
98 changed files with 2252 additions and 8 deletions
+6
View File
@@ -25882,6 +25882,12 @@
"browser-process-hrtime": "^0.1.2"
}
},
"wait-for-expect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-1.1.0.tgz",
"integrity": "sha512-vQDokqxyMyknfX3luCDn16bSaRcOyH6gGuUXMIbxBLeTo6nWuEWYqMTT9a+44FmW8c2m6TRWBdNvBBjA1hwEKg==",
"dev": true
},
"walker": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz",
+1
View File
@@ -275,6 +275,7 @@
"typescript": "^3.0.3",
"typescript-snapshots-plugin": "^1.2.0",
"uglifyjs-webpack-plugin": "^1.2.5",
"wait-for-expect": "^1.1.0",
"webpack": "4.12.0",
"webpack-cli": "^3.0.2",
"webpack-dev-server": "^3.1.10",
+1 -1
View File
@@ -6,7 +6,7 @@
.container {
max-width: 1280px;
margin: 0 auto;
padding: 0 calc(2 * var(--spacing-unit));
padding: 0 var(--spacing-unit) 0 calc(2 * var(--spacing-unit));
height: calc(6 * var(--spacing-unit));
box-sizing: border-box;
}
@@ -0,0 +1,9 @@
.popover {
z-index: var(--zindex-popover);
width: 350px;
}
.historyIcon {
color: var(--palette-text-secondary);
margin-right: calc(1.5 * var(--spacing-unit));
}
@@ -0,0 +1,55 @@
import React from "react";
import { oncePerFrame } from "talk-common/utils";
import { BaseButton, ClickOutside, Icon, Popover } from "talk-ui/components";
import DecisionHistoryQuery from "../views/decisionHistory/queries/DecisionHistoryQuery";
import styles from "./DecisionHistoryButton.css";
class DecisionHistoryButton extends React.Component {
// Helper that prevents calling toggleVisibility more then once per frame.
// In essence this means we'll process an event only once.
// This might happen, when clicking on the button which will
// cause its onClick to happen as well as onClickOutside.
private toggleVisibilityOncePerFrame = oncePerFrame(
(toggleVisibility: () => void) => toggleVisibility()
);
public render() {
const popoverID = `decision-history-popover`;
return (
<Popover
data-test="decisionHistory-popover"
id={popoverID}
placement="bottom-end"
description="A dialog showing a permalink to the comment"
classes={{ popover: styles.popover }}
body={({ toggleVisibility }) => (
<ClickOutside
onClickOutside={() =>
this.toggleVisibilityOncePerFrame(toggleVisibility)
}
>
<div>
<DecisionHistoryQuery />
</div>
</ClickOutside>
)}
>
{({ toggleVisibility, forwardRef, visible }) => (
<BaseButton
onClick={() => this.toggleVisibilityOncePerFrame(toggleVisibility)}
aria-controls={popoverID}
forwardRef={forwardRef}
className={styles.historyIcon}
data-test="decisionHistory-toggle"
>
<Icon size="lg">history</Icon>
</BaseButton>
)}
</Popover>
);
}
}
export default DecisionHistoryButton;
@@ -26,3 +26,8 @@
text-decoration: none;
color: var(--palette-text-light);
}
.historyIcon {
color: var(--palette-text-secondary);
padding: var(--spacing-unit) var(--spacing-unit);
}
@@ -4,7 +4,9 @@ import React, { StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import SignOutButtonContainer from "../containers/SignOutButtonContainer";
import DecisionHistoryButton from "./DecisionHistoryButton";
import styles from "./Navigation.css";
import NavigationDivider from "./NavigationDivider";
import NavigationLink from "./NavigationLink";
const Navigation: StatelessComponent = () => (
@@ -24,6 +26,8 @@ const Navigation: StatelessComponent = () => (
</Localized>
</Flex>
<Flex alignItems="center">
<DecisionHistoryButton />
<NavigationDivider />
<SignOutButtonContainer id="navigation-signOutButton" />
</Flex>
</Flex>
@@ -0,0 +1,4 @@
.root {
height: 75%;
border-right: 1px solid var(--palette-divider);
}
@@ -0,0 +1,9 @@
import { shallow } from "enzyme";
import React from "react";
import NavigationDivider from "./NavigationDivider";
it("renders correctly", () => {
const wrapper = shallow(<NavigationDivider />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,9 @@
import React, { StatelessComponent } from "react";
import styles from "./NavigationDivider.css";
const NavigationDivider: StatelessComponent<{}> = () => (
<div className={styles.root} />
);
export default NavigationDivider;
@@ -1,7 +1,7 @@
.root {
color: var(--palette-text-secondary);
font-family: var(--font-family-sans-serif);
font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-medium);
font-size: calc(18rem / var(--rem-base));
line-height: calc(20em / 18);
letter-spacing: calc(-0.1em / 18);
@@ -0,0 +1,5 @@
.root {
font-weight: var(--font-weight-bold);
font-size: calc(18rem / var(--rem-base));
letter-spacing: calc(-0.2em / 18);
}
@@ -3,6 +3,8 @@ import React, { StatelessComponent } from "react";
import { Button } from "talk-ui/components";
import styles from "./SignOutButton.css";
interface Props {
id?: string;
onClick: React.EventHandler<React.MouseEvent>;
@@ -10,7 +12,7 @@ interface Props {
const SignOutButton: StatelessComponent<Props> = props => (
<Localized id="navigation-signOutButton">
<Button id={props.id} onClick={props.onClick}>
<Button id={props.id} onClick={props.onClick} className={styles.root}>
Sign Out
</Button>
</Localized>
@@ -48,6 +48,8 @@ exports[`renders correctly 1`] = `
<withPropsOnChange(Flex)
alignItems="center"
>
<DecisionHistoryButton />
<NavigationDivider />
<withContext(createMutationContainer(RedirectAppContainer))
id="navigation-signOutButton"
/>
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="NavigationDivider-root"
/>
`;
@@ -5,6 +5,7 @@ exports[`renders correctly 1`] = `
id="navigation-signOutButton"
>
<withPropsOnChange(Button)
className="SignOutButton-root"
id="id"
onClick={[Function]}
>
@@ -0,0 +1,472 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loads more 1`] = `
<div
data-test="decisionHistory-container"
>
<div
className="Flex-root Title-root Flex-flex Flex-alignCenter"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
history
</span>
<span>
 
</span>
<span
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary Title-text"
>
Your Decision History
</span>
</div>
<div
className="Main-root"
>
<ul
className="DecisionList-root"
>
<li
className="DecisionItem-root"
>
<div
className="Flex-root Flex-flex"
>
<div
className="DecisionItem-leftCol"
>
<span
aria-hidden="true"
className="Icon-root AcceptedIcon-root Icon-sm"
>
check
</span>
</div>
<div>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary Info-root"
>
Accepted comment by
<strong>
dany
</strong>
</p>
<div
className="Flex-root Footer-root Flex-flex Flex-alignBaseline"
>
<span
className="Typography-root Typography-timestamp Typography-colorTextPrimary"
>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-11-29T16:01:51.897Z"
title="2018-11-29T16:01:51.897Z"
>
2018-11-29T16:01:51.897Z
</time>
</span>
<div
className="DotDivider-root"
>
</div>
<a
className="TextLink-root GoToCommentLink-root"
href="#"
onClick={[Function]}
>
<span>
Go to comment
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
chevron_right
</span>
</a>
</div>
</div>
</div>
</li>
<li
className="DecisionItem-root"
>
<div
className="Flex-root Flex-flex"
>
<div
className="DecisionItem-leftCol"
>
<span
aria-hidden="true"
className="Icon-root RejectedIcon-root Icon-sm"
>
cancel
</span>
</div>
<div>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary Info-root"
>
Rejected comment by
<strong>
dany
</strong>
</p>
<div
className="Flex-root Footer-root Flex-flex Flex-alignBaseline"
>
<span
className="Typography-root Typography-timestamp Typography-colorTextPrimary"
>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-11-29T16:01:45.644Z"
title="2018-11-29T16:01:45.644Z"
>
2018-11-29T16:01:45.644Z
</time>
</span>
<div
className="DotDivider-root"
>
</div>
<a
className="TextLink-root GoToCommentLink-root"
href="#"
onClick={[Function]}
>
<span>
Go to comment
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
chevron_right
</span>
</a>
</div>
</div>
</div>
</li>
<li
className="DecisionItem-root"
>
<div
className="Flex-root Flex-flex"
>
<div
className="DecisionItem-leftCol"
>
<span
aria-hidden="true"
className="Icon-root AcceptedIcon-root Icon-sm"
>
check
</span>
</div>
<div>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary Info-root"
>
Accepted comment by
<strong>
dany
</strong>
</p>
<div
className="Flex-root Footer-root Flex-flex Flex-alignBaseline"
>
<span
className="Typography-root Typography-timestamp Typography-colorTextPrimary"
>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-11-29T16:01:42.060Z"
title="2018-11-29T16:01:42.060Z"
>
2018-11-29T16:01:42.060Z
</time>
</span>
<div
className="DotDivider-root"
>
</div>
<a
className="TextLink-root GoToCommentLink-root"
href="#"
onClick={[Function]}
>
<span>
Go to comment
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
chevron_right
</span>
</a>
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
`;
exports[`opens popover when clicked on button showing loading state 1`] = `
<div
data-test="decisionHistory-loading-container"
>
<div
className="Flex-root Title-root Flex-flex Flex-alignCenter"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
history
</span>
<span>
 
</span>
<span
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary Title-text"
>
Your Decision History
</span>
</div>
<div
className="Main-root"
>
<div
className="Flex-root DecisionHistoryLoading-container Flex-flex Flex-justifyCenter Flex-alignCenter"
/>
</div>
</div>
`;
exports[`render popover content 1`] = `
<div
data-test="decisionHistory-container"
>
<div
className="Flex-root Title-root Flex-flex Flex-alignCenter"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
history
</span>
<span>
 
</span>
<span
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary Title-text"
>
Your Decision History
</span>
</div>
<div
className="Main-root"
>
<ul
className="DecisionList-root"
>
<li
className="DecisionItem-root"
>
<div
className="Flex-root Flex-flex"
>
<div
className="DecisionItem-leftCol"
>
<span
aria-hidden="true"
className="Icon-root AcceptedIcon-root Icon-sm"
>
check
</span>
</div>
<div>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary Info-root"
>
Accepted comment by
<strong>
addy
</strong>
</p>
<div
className="Flex-root Footer-root Flex-flex Flex-alignBaseline"
>
<span
className="Typography-root Typography-timestamp Typography-colorTextPrimary"
>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-11-29T16:01:51.897Z"
title="2018-11-29T16:01:51.897Z"
>
2018-11-29T16:01:51.897Z
</time>
</span>
<div
className="DotDivider-root"
>
</div>
<a
className="TextLink-root GoToCommentLink-root"
href="#"
onClick={[Function]}
>
<span>
Go to comment
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
chevron_right
</span>
</a>
</div>
</div>
</div>
</li>
<li
className="DecisionItem-root"
>
<div
className="Flex-root Flex-flex"
>
<div
className="DecisionItem-leftCol"
>
<span
aria-hidden="true"
className="Icon-root RejectedIcon-root Icon-sm"
>
cancel
</span>
</div>
<div>
<p
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary Info-root"
>
Rejected comment by
<strong>
addy
</strong>
</p>
<div
className="Flex-root Footer-root Flex-flex Flex-alignBaseline"
>
<span
className="Typography-root Typography-timestamp Typography-colorTextPrimary"
>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-11-29T16:01:45.644Z"
title="2018-11-29T16:01:45.644Z"
>
2018-11-29T16:01:45.644Z
</time>
</span>
<div
className="DotDivider-root"
>
</div>
<a
className="TextLink-root GoToCommentLink-root"
href="#"
onClick={[Function]}
>
<span>
Go to comment
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
chevron_right
</span>
</a>
</div>
</div>
</div>
</li>
</ul>
<button
className="BaseButton-root ShowMoreButton-root"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Show More
</button>
</div>
</div>
`;
exports[`renders decision history popover button 1`] = `
<div
className="Popover-root"
data-test="decisionHistory-popover"
>
<button
aria-controls="decision-history-popover"
className="BaseButton-root DecisionHistoryButton-historyIcon"
data-test="decisionHistory-toggle"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg"
>
history
</span>
</button>
<div
aria-hidden={true}
aria-labelledby="decision-history-popover-ariainfo"
id="decision-history-popover"
role="popup"
>
<div
className="AriaInfo-root"
id="decision-history-popover-ariainfo"
>
A dialog showing a permalink to the comment
</div>
</div>
</div>
`;
@@ -0,0 +1,136 @@
import { get, merge } from "lodash";
import sinon from "sinon";
import {
createSinonStub,
getByTestID,
getByText,
limitSnapshotTo,
replaceHistoryLocation,
wait,
waitForElement,
} from "talk-framework/testHelpers";
import create from "../create";
import { moderationActions, settings } from "../fixtures";
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/configure/auth");
});
const commentModerationActionHistory = createSinonStub(
s => s.throws(),
s =>
s.withArgs({ first: 5 }).returns({
edges: [
{
node: moderationActions[0],
cursor: moderationActions[0].createdAt,
},
{
node: moderationActions[1],
cursor: moderationActions[1].createdAt,
},
],
pageInfo: {
endCursor: moderationActions[1].createdAt,
hasNextPage: true,
},
}),
s =>
s
.withArgs({
first: 10,
after: moderationActions[1].createdAt,
})
.returns({
edges: [
{
node: moderationActions[2],
cursor: moderationActions[2].createdAt,
},
],
pageInfo: {
endCursor: moderationActions[2].createdAt,
hasNextPage: false,
},
})
);
const createTestRenderer = async (resolver: any = {}) => {
const resolvers = {
...resolver,
Query: {
...resolver.Query,
me: sinon.stub().returns({
id: "me",
commentModerationActionHistory,
}),
settings: sinon
.stub()
.returns(merge(settings, get(resolver, "Query.settings"))),
},
};
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(true, "loggedIn");
},
});
await waitForElement(() =>
getByTestID("decisionHistory-toggle", testRenderer.root)
);
return testRenderer;
};
async function createTestRendererAndOpenPopover() {
const testRenderer = await createTestRenderer();
const toggle = testRenderer.root.findByProps({
"data-test": "decisionHistory-toggle",
})!;
toggle.props.onClick();
return testRenderer;
}
it("renders decision history popover button", async () => {
const testRenderer = await createTestRenderer();
expect(
limitSnapshotTo("decisionHistory-popover", testRenderer.toJSON())
).toMatchSnapshot();
});
it("opens popover when clicked on button showing loading state", async () => {
const testRenderer = await createTestRendererAndOpenPopover();
expect(
limitSnapshotTo("decisionHistory-loading-container", testRenderer.toJSON())
).toMatchSnapshot();
});
it("render popover content", async () => {
const testRenderer = await createTestRendererAndOpenPopover();
await waitForElement(() =>
getByTestID("decisionHistory-container", testRenderer.root)
);
expect(
limitSnapshotTo("decisionHistory-container", testRenderer.toJSON())
).toMatchSnapshot();
});
it("loads more", async () => {
const testRenderer = await createTestRendererAndOpenPopover();
const decisionHistoryContainer = await waitForElement(() =>
getByTestID("decisionHistory-container", testRenderer.root)
);
const ShowMoreButton = getByText("Show More", decisionHistoryContainer)!;
expect(ShowMoreButton.props.disabled).toBeFalsy();
ShowMoreButton.props.onClick();
expect(ShowMoreButton.props.disabled).toBeTruthy();
await wait(() => {
expect(() => getByText("Show More", decisionHistoryContainer)).toThrow();
});
expect(
limitSnapshotTo("decisionHistory-container", testRenderer.toJSON())
).toMatchSnapshot();
});
+83
View File
@@ -48,3 +48,86 @@ export const settings = {
},
},
};
export const moderationActions = [
{
id: "07e8f815-e165-4b5d-b438-7163415c8cf7",
revision: {
id: "4210dc8b-c212-4f74-9381-913e8c52e51a",
comment: {
author: {
username: "luke2",
id: "4383c3d3-bb9b-40b9-847a-240f3cf6c6af",
},
id: "1b41be9f-510f-41f3-a1df-5a431dc98bf3",
},
},
createdAt: "2018-11-29T16:01:51.897Z",
status: "ACCEPTED",
__typename: "CommentModerationAction",
},
{
id: "6869314b-47ef-4cf9-b8ce-42b12bca8231",
revision: {
id: "4210dc8b-c212-4f74-9381-913e8c52e51a",
comment: {
author: {
username: "addy",
id: "4383c3d3-bb9b-40b9-847a-240f3cf6c6af",
},
id: "1b41be9f-510f-41f3-a1df-5a431dc98bf3",
},
},
createdAt: "2018-11-29T16:01:45.644Z",
status: "REJECTED",
__typename: "CommentModerationAction",
},
{
id: "caebbf7f-4813-42c0-ac3c-46b1be8199e0",
revision: {
id: "4210dc8b-c212-4f74-9381-913e8c52e51a",
comment: {
author: {
username: "dany",
id: "4383c3d3-bb9b-40b9-847a-240f3cf6c6af",
},
id: "1b41be9f-510f-41f3-a1df-5a431dc98bf3",
},
},
createdAt: "2018-11-29T16:01:42.060Z",
status: "ACCEPTED",
__typename: "CommentModerationAction",
},
{
id: "b2f92717-e4a8-4075-a543-95f7c5eaefb2",
revision: {
id: "4210dc8b-c212-4f74-9381-913e8c52e51a",
comment: {
author: {
username: "admin",
id: "4383c3d3-bb9b-40b9-847a-240f3cf6c6af",
},
id: "1b41be9f-510f-41f3-a1df-5a431dc98bf3",
},
},
createdAt: "2018-11-29T16:01:34.539Z",
status: "REJECTED",
__typename: "CommentModerationAction",
},
{
id: "9fb2ff3c-7105-4357-99e1-36cdeea49c75",
revision: {
id: "4210dc8b-c212-4f74-9381-913e8c52e51a",
comment: {
author: {
username: "mod245",
id: "4383c3d3-bb9b-40b9-847a-240f3cf6c6af",
},
id: "1b41be9f-510f-41f3-a1df-5a431dc98bf3",
},
},
createdAt: "2018-11-29T16:01:30.648Z",
status: "ACCEPTED",
__typename: "CommentModerationAction",
},
];
@@ -0,0 +1,18 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import AcceptedComment from "./AcceptedComment";
it("renders correctly", () => {
const props: PropTypesOf<typeof AcceptedComment> = {
href: "#",
username: "InTheExpensiveSeats",
date: "2018-07-06T18:24:00.000Z",
onGotoComment: noop,
};
const wrapper = shallow(<AcceptedComment {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,43 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import AcceptedIcon from "./AcceptedIcon";
import DecisionItem from "./DecisionItem";
import DotDivider from "./DotDivider";
import Footer from "./Footer";
import GoToCommentLink from "./GoToCommentLink";
import Info from "./Info";
import Timestamp from "./Timestamp";
import { Typography } from "talk-ui/components";
interface Props {
href: string;
username: string;
date: string;
onGotoComment?: React.EventHandler<React.MouseEvent>;
}
const Username: StatelessComponent<{ username: string }> = ({ username }) => (
<strong>{username}</strong>
);
const ApprovedComment: StatelessComponent<Props> = props => (
<DecisionItem icon={<AcceptedIcon />}>
<Localized
id="decisionHistory-acceptedCommentBy"
username={<Username username={props.username} />}
>
<Info>{"Accepted comment by <username></username>"}</Info>
</Localized>
<Footer>
<Typography variant="timestamp">
<Timestamp>{props.date}</Timestamp>
</Typography>
<DotDivider />
<GoToCommentLink href={props.href} onClick={props.onGotoComment} />
</Footer>
</DecisionItem>
);
export default ApprovedComment;
@@ -0,0 +1,3 @@
.root {
color: var(--palette-success-main);
}
@@ -0,0 +1,9 @@
import { shallow } from "enzyme";
import React from "react";
import AcceptedIcon from "./AcceptedIcon";
it("renders correctly", () => {
const wrapper = shallow(<AcceptedIcon />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,11 @@
import React, { StatelessComponent } from "react";
import { Icon } from "talk-ui/components";
import styles from "./AcceptedIcon.css";
const AcceptedIcon: StatelessComponent = () => (
<Icon className={styles.root}>check</Icon>
);
export default AcceptedIcon;
@@ -0,0 +1,54 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import { PropTypesOf } from "talk-framework/types";
import DecisionHistory from "./DecisionHistory";
const DecisionHistoryN = removeFragmentRefs(DecisionHistory);
it("renders correctly", () => {
const props: PropTypesOf<typeof DecisionHistoryN> = {
actions: [{ id: "1" }, { id: "2" }, { id: "3" }],
onLoadMore: noop,
hasMore: false,
disableLoadMore: false,
};
const wrapper = shallow(<DecisionHistoryN {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders empty state", () => {
const props: PropTypesOf<typeof DecisionHistoryN> = {
actions: [],
onLoadMore: noop,
hasMore: false,
disableLoadMore: false,
};
const wrapper = shallow(<DecisionHistoryN {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders hasMore", () => {
const props: PropTypesOf<typeof DecisionHistoryN> = {
actions: [{ id: "1" }, { id: "2" }, { id: "3" }],
onLoadMore: noop,
hasMore: true,
disableLoadMore: false,
};
const wrapper = shallow(<DecisionHistoryN {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders disable load more", () => {
const props: PropTypesOf<typeof DecisionHistoryN> = {
actions: [{ id: "1" }, { id: "2" }, { id: "3" }],
onLoadMore: noop,
hasMore: true,
disableLoadMore: true,
};
const wrapper = shallow(<DecisionHistoryN {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,41 @@
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import DecisionHistoryItemContainer from "../containers/DecisionHistoryItemContainer";
import DecisionList from "./DecisionList";
import Empty from "./Empty";
import Main from "./Main";
import ShowMoreButton from "./ShowMoreButton";
import Title from "./Title";
interface Props {
actions: Array<
{ id: string } & PropTypesOf<typeof DecisionHistoryItemContainer>["action"]
>;
onLoadMore?: () => void;
hasMore?: boolean;
disableLoadMore?: boolean;
}
const DecisionHistory: StatelessComponent<Props> = props => (
<div data-test="decisionHistory-container">
<Title />
<Main>
<DecisionList>
{props.actions.length === 0 && <Empty />}
{props.actions.map(action => (
<DecisionHistoryItemContainer key={action.id} action={action} />
))}
</DecisionList>
{props.hasMore && (
<ShowMoreButton
onClick={props.onLoadMore}
disabled={props.disableLoadMore}
/>
)}
</Main>
</div>
);
export default DecisionHistory;
@@ -0,0 +1,4 @@
.container {
padding: var(--spacing-unit);
min-height: 30px;
}
@@ -0,0 +1,9 @@
import { shallow } from "enzyme";
import React from "react";
import DecisionHistoryLoading from "./DecisionHistoryLoading";
it("renders correctly", () => {
const wrapper = shallow(<DecisionHistoryLoading />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,26 @@
import React, { StatelessComponent } from "react";
import { Delay, Flex, Spinner } from "talk-ui/components";
import styles from "./DecisionHistoryLoading.css";
import Main from "./Main";
import Title from "./Title";
const DecisionHistoryLoading: StatelessComponent = () => (
<div data-test="decisionHistory-loading-container">
<Title />
<Main>
<Flex
justifyContent="center"
alignItems="center"
className={styles.container}
>
<Delay>
<Spinner size="sm" />
</Delay>
</Flex>
</Main>
</div>
);
export default DecisionHistoryLoading;
@@ -0,0 +1,13 @@
.root {
padding: calc(0.5 * var(--spacing-unit)) calc(0.5 * var(--spacing-unit))
calc(0.5 * var(--spacing-unit)) 0;
}
.leftCol {
display: flex;
justify-content: center;
flex-grow: 0;
flex-shrink: 0;
width: calc(3 * var(--spacing-unit));
padding-top: 3px;
}
@@ -0,0 +1,15 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import DecisionItem from "./DecisionItem";
it("renders correctly", () => {
const props: PropTypesOf<typeof DecisionItem> = {
icon: "icon",
children: "children",
};
const wrapper = shallow(<DecisionItem {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,21 @@
import React, { StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import styles from "./DecisionItem.css";
interface Props {
icon: React.ReactNode;
children: React.ReactNode;
}
const DecisionItem: StatelessComponent<Props> = props => (
<li className={styles.root}>
<Flex>
<div className={styles.leftCol}>{props.icon}</div>
<div>{props.children}</div>
</Flex>
</li>
);
export default DecisionItem;
@@ -0,0 +1,12 @@
.root {
margin: 0;
padding: 0;
list-style-type: none;
& > * {
border-bottom: 1px solid var(--palette-divider);
}
& > *:last-child {
border-bottom: 0;
}
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import DecisionList from "./DecisionList";
it("renders correctly", () => {
const props: PropTypesOf<typeof DecisionList> = {
children: "children",
};
const wrapper = shallow(<DecisionList {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,9 @@
import React, { StatelessComponent } from "react";
import styles from "./DecisionList.css";
const DecisionList: StatelessComponent = props => (
<ul className={styles.root}>{props.children}</ul>
);
export default DecisionList;
@@ -0,0 +1,7 @@
.root {
height: 100%;
font-size: calc(12rem / var(--rem-base));
line-height: calc(12em / 12);
color: var(--palette-divider);
padding: 0 calc(0.5 * var(--spacing-unit));
}
@@ -0,0 +1,9 @@
import { shallow } from "enzyme";
import React from "react";
import DotDivider from "./DotDivider";
it("renders correctly", () => {
const wrapper = shallow(<DotDivider />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,9 @@
import React, { StatelessComponent } from "react";
import styles from "./DotDivider.css";
const Footer: StatelessComponent<{}> = () => (
<div className={styles.root}></div>
);
export default Footer;
@@ -0,0 +1,3 @@
.root {
padding: calc(1.5 * var(--spacing-unit)) calc(2 * var(--spacing-unit));
}
@@ -0,0 +1,9 @@
import { shallow } from "enzyme";
import React from "react";
import Empty from "./Empty";
it("renders correctly", () => {
const wrapper = shallow(<Empty />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,18 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Flex, Typography } from "talk-ui/components";
import styles from "./Empty.css";
const Empty: StatelessComponent<{}> = () => (
<Flex justifyContent="center" alignItems="center" className={styles.root}>
<Localized id="decisionHistory-youWillSeeAList">
<Typography>
You will see a list of your post moderation actions here.
</Typography>
</Localized>
</Flex>
);
export default Empty;
@@ -0,0 +1,2 @@
.root {
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Footer from "./Footer";
it("renders correctly", () => {
const props: PropTypesOf<typeof Footer> = {
children: "children",
};
const wrapper = shallow(<Footer {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,17 @@
import React, { StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import styles from "./Footer.css";
interface Props {
children: React.ReactNode;
}
const Footer: StatelessComponent<Props> = props => (
<Flex className={styles.root} alignItems="baseline">
{props.children}
</Flex>
);
export default Footer;
@@ -0,0 +1,9 @@
.root {
font-size: calc(12rem / var(--rem-base));
font-weight: var(--font-weight-regular);
font-family: var(--font-family-sans-serif);
line-height: calc(14em / 12);
letter-spacing: calc(0.2em / 12);
color: var(--palette-primary-main);
text-decoration: none;
}
@@ -0,0 +1,16 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import GoToCommentLink from "./GoToCommentLink";
it("renders correctly", () => {
const props: PropTypesOf<typeof GoToCommentLink> = {
href: "#",
onClick: noop,
};
const wrapper = shallow(<GoToCommentLink {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,22 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Icon, TextLink } from "talk-ui/components";
import styles from "./GoToCommentLink.css";
interface Props {
href?: string;
onClick?: React.EventHandler<React.MouseEvent>;
}
const GoToCommentLink: StatelessComponent<Props> = props => (
<TextLink className={styles.root} onClick={props.onClick} href={props.href}>
<Localized id="decisionHistory-goToComment">
<span>Go to comment</span>
</Localized>{" "}
<Icon>chevron_right</Icon>
</TextLink>
);
export default GoToCommentLink;
@@ -0,0 +1,13 @@
.root {
font-size: calc(14rem / var(--rem-base));
line-height: calc(16em / 12);
/*
Fallback begin
Unfortunately Firefox / IE does not support
`word-break: break-word` yet.
*/
word-break: break-all;
/* Fallback end */
word-break: break-word;
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Info from "./Info";
it("renders correctly", () => {
const props: PropTypesOf<typeof Info> = {
children: "children",
};
const wrapper = shallow(<Info {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,15 @@
import React, { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
import styles from "./Info.css";
interface Props {
children: React.ReactNode;
}
const Info: StatelessComponent<Props> = props => (
<Typography className={styles.root}>{props.children}</Typography>
);
export default Info;
@@ -0,0 +1,5 @@
.root {
max-height: calc(26 * var(--spacing-unit));
overflow: hidden;
overflow-y: scroll;
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Main from "./Main";
it("renders correctly", () => {
const props: PropTypesOf<typeof Main> = {
children: "children",
};
const wrapper = shallow(<Main {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,9 @@
import React, { StatelessComponent } from "react";
import styles from "./Main.css";
const Main: StatelessComponent = props => (
<div className={styles.root}>{props.children}</div>
);
export default Main;
@@ -0,0 +1,18 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import RejectedComment from "./RejectedComment";
it("renders correctly", () => {
const props: PropTypesOf<typeof RejectedComment> = {
href: "#",
username: "InTheExpensiveSeats",
date: "2018-07-06T18:24:00.000Z",
onGotoComment: noop,
};
const wrapper = shallow(<RejectedComment {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,43 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import DecisionItem from "./DecisionItem";
import DotDivider from "./DotDivider";
import Footer from "./Footer";
import GoToCommentLink from "./GoToCommentLink";
import Info from "./Info";
import RejectedIcon from "./RejectedIcon";
import Timestamp from "./Timestamp";
import { Typography } from "talk-ui/components";
interface Props {
href: string;
username: string;
date: string;
onGotoComment?: React.EventHandler<React.MouseEvent>;
}
const Username: StatelessComponent<{ username: string }> = ({ username }) => (
<strong>{username}</strong>
);
const RejectedComment: StatelessComponent<Props> = props => (
<DecisionItem icon={<RejectedIcon />}>
<Localized
id="decisionHistory-rejectedCommentBy"
username={<Username username={props.username} />}
>
<Info>{"Rejected comment by <username></username>"}</Info>
</Localized>
<Footer>
<Typography variant="timestamp">
<Timestamp>{props.date}</Timestamp>
</Typography>
<DotDivider />
<GoToCommentLink href={props.href} onClick={props.onGotoComment} />
</Footer>
</DecisionItem>
);
export default RejectedComment;
@@ -0,0 +1,3 @@
.root {
color: var(--palette-error-main);
}
@@ -0,0 +1,9 @@
import { shallow } from "enzyme";
import React from "react";
import RejectedIcon from "./RejectedIcon";
it("renders correctly", () => {
const wrapper = shallow(<RejectedIcon />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,11 @@
import React, { StatelessComponent } from "react";
import { Icon } from "talk-ui/components";
import styles from "./RejectedIcon.css";
const RejectedIcon: StatelessComponent = () => (
<Icon className={styles.root}>cancel</Icon>
);
export default RejectedIcon;
@@ -0,0 +1,16 @@
.root {
text-transform: uppercase;
font-size: calc(12rem / var(--rem-base));
font-weight: var(--font-weight-medium);
font-family: var(--font-family-sans-serif);
line-height: calc(2 * var(--spacing-unit));
letter-spacing: calc(0.2em / 12);
color: var(--palette-grey-main);
width: 100%;
border-top: 1px solid var(--palette-divider);
text-align: center;
&:disabled {
color: var(--palette-grey-lighter);
}
}
@@ -0,0 +1,16 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import ShowMoreButton from "./ShowMoreButton";
it("renders correctly", () => {
const props: PropTypesOf<typeof ShowMoreButton> = {
disabled: false,
onClick: noop,
};
const wrapper = shallow(<ShowMoreButton {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,25 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { BaseButton } from "talk-ui/components";
import styles from "./ShowMoreButton.css";
interface Props {
disabled?: boolean;
onClick?: () => void;
}
const ShowMoreButton: StatelessComponent<Props> = props => (
<Localized id="decisionHistory-showMoreButton">
<BaseButton
className={styles.root}
onClick={props.onClick}
disabled={props.disabled}
>
Show More
</BaseButton>
</Localized>
);
export default ShowMoreButton;
@@ -0,0 +1,8 @@
.root {
color: var(--palette-text-secondary);
font-family: var(--font-family-sans-serif);
font-weight: var(--font-weight-medium);
font-size: calc(12rem / var(--rem-base));
line-height: calc(14em / 12);
letter-spacing: 0;
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Timestamp from "./Timestamp";
it("renders correctly", () => {
const props: PropTypesOf<typeof Timestamp> = {
children: "2018-07-06T18:24:00.000Z",
};
const wrapper = shallow(<Timestamp {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,15 @@
import React, { StatelessComponent } from "react";
import { RelativeTime } from "talk-ui/components";
import styles from "./Timestamp.css";
interface Props {
children: string;
}
const DecisionHistory: StatelessComponent<Props> = props => (
<RelativeTime className={styles.root} date={props.children} />
);
export default DecisionHistory;
@@ -0,0 +1,16 @@
.root {
padding: 0 calc(0.5 * var(--spacing-unit));
background-color: #f2f2f2;
color: var(--palette-text-primary);
border-bottom: 1px solid var(--palette-grey-lighter);
}
.text {
text-transform: uppercase;
font-size: calc(12rem / var(--rem-base));
font-weight: var(--font-weight-medium);
font-family: var(--font-family-sans-serif);
line-height: calc(12em / 12);
letter-spacing: calc(0.2em / 12);
color: var(--palette-text-primary);
}
@@ -0,0 +1,9 @@
import { shallow } from "enzyme";
import React from "react";
import Title from "./Title";
it("renders correctly", () => {
const wrapper = shallow(<Title />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,19 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Flex, Icon, Typography } from "talk-ui/components";
import styles from "./Title.css";
const Title: StatelessComponent = () => (
<Flex className={styles.root} alignItems="center">
<Icon>history</Icon>{" "}
<Localized id="decisionHistory-yourDecisionHistory">
<Typography container="span" className={styles.text}>
Your Decision History
</Typography>
</Localized>
</Flex>
);
export default Title;
@@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<DecisionItem
icon={<AcceptedIcon />}
>
<Localized
id="decisionHistory-acceptedCommentBy"
username={
<Username
username="InTheExpensiveSeats"
/>
}
>
<Info>
Accepted comment by &lt;username&gt;&lt;/username&gt;
</Info>
</Localized>
<Footer>
<withPropsOnChange(Typography)
variant="timestamp"
>
<DecisionHistory>
2018-07-06T18:24:00.000Z
</DecisionHistory>
</withPropsOnChange(Typography)>
<Footer />
<GoToCommentLink
href="#"
onClick={[Function]}
/>
</Footer>
</DecisionItem>
`;
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Icon)
className="AcceptedIcon-root"
>
check
</withPropsOnChange(Icon)>
`;
@@ -0,0 +1,130 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
data-test="decisionHistory-container"
>
<Title />
<Main>
<DecisionList>
<Relay(DecisionHistoryItemContainer)
action={
Object {
"id": "1",
}
}
key="1"
/>
<Relay(DecisionHistoryItemContainer)
action={
Object {
"id": "2",
}
}
key="2"
/>
<Relay(DecisionHistoryItemContainer)
action={
Object {
"id": "3",
}
}
key="3"
/>
</DecisionList>
</Main>
</div>
`;
exports[`renders disable load more 1`] = `
<div
data-test="decisionHistory-container"
>
<Title />
<Main>
<DecisionList>
<Relay(DecisionHistoryItemContainer)
action={
Object {
"id": "1",
}
}
key="1"
/>
<Relay(DecisionHistoryItemContainer)
action={
Object {
"id": "2",
}
}
key="2"
/>
<Relay(DecisionHistoryItemContainer)
action={
Object {
"id": "3",
}
}
key="3"
/>
</DecisionList>
<ShowMoreButton
disabled={true}
onClick={[Function]}
/>
</Main>
</div>
`;
exports[`renders empty state 1`] = `
<div
data-test="decisionHistory-container"
>
<Title />
<Main>
<DecisionList>
<Empty />
</DecisionList>
</Main>
</div>
`;
exports[`renders hasMore 1`] = `
<div
data-test="decisionHistory-container"
>
<Title />
<Main>
<DecisionList>
<Relay(DecisionHistoryItemContainer)
action={
Object {
"id": "1",
}
}
key="1"
/>
<Relay(DecisionHistoryItemContainer)
action={
Object {
"id": "2",
}
}
key="2"
/>
<Relay(DecisionHistoryItemContainer)
action={
Object {
"id": "3",
}
}
key="3"
/>
</DecisionList>
<ShowMoreButton
disabled={false}
onClick={[Function]}
/>
</Main>
</div>
`;
@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
data-test="decisionHistory-loading-container"
>
<Title />
<Main>
<withPropsOnChange(Flex)
alignItems="center"
className="DecisionHistoryLoading-container"
justifyContent="center"
>
<Delay
ms={500}
>
<withPropsOnChange(Spinner)
size="sm"
/>
</Delay>
</withPropsOnChange(Flex)>
</Main>
</div>
`;
@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<li
className="DecisionItem-root"
>
<withPropsOnChange(Flex)>
<div
className="DecisionItem-leftCol"
>
icon
</div>
<div>
children
</div>
</withPropsOnChange(Flex)>
</li>
`;
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<ul
className="DecisionList-root"
>
children
</ul>
`;
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="DotDivider-root"
>
</div>
`;
@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Flex)
alignItems="center"
className="Empty-root"
justifyContent="center"
>
<Localized
id="decisionHistory-youWillSeeAList"
>
<withPropsOnChange(Typography)>
You will see a list of your post moderation actions here.
</withPropsOnChange(Typography)>
</Localized>
</withPropsOnChange(Flex)>
`;
@@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Flex)
alignItems="baseline"
className="Footer-root"
>
children
</withPropsOnChange(Flex)>
`;
@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(TextLinkProps)
className="GoToCommentLink-root"
href="#"
onClick={[Function]}
>
<Localized
id="decisionHistory-goToComment"
>
<span>
Go to comment
</span>
</Localized>
<withPropsOnChange(Icon)>
chevron_right
</withPropsOnChange(Icon)>
</withPropsOnChange(TextLinkProps)>
`;
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Typography)
className="Info-root"
>
children
</withPropsOnChange(Typography)>
`;
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="Main-root"
>
children
</div>
`;
@@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<DecisionItem
icon={<RejectedIcon />}
>
<Localized
id="decisionHistory-rejectedCommentBy"
username={
<Username
username="InTheExpensiveSeats"
/>
}
>
<Info>
Rejected comment by &lt;username&gt;&lt;/username&gt;
</Info>
</Localized>
<Footer>
<withPropsOnChange(Typography)
variant="timestamp"
>
<DecisionHistory>
2018-07-06T18:24:00.000Z
</DecisionHistory>
</withPropsOnChange(Typography)>
<Footer />
<GoToCommentLink
href="#"
onClick={[Function]}
/>
</Footer>
</DecisionItem>
`;
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Icon)
className="RejectedIcon-root"
>
cancel
</withPropsOnChange(Icon)>
`;
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Localized
id="decisionHistory-showMoreButton"
>
<withPropsOnChange(WithMouseHover)
className="ShowMoreButton-root"
disabled={false}
onClick={[Function]}
>
Show More
</withPropsOnChange(WithMouseHover)>
</Localized>
`;
@@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(RelativeTime)
className="Timestamp-root"
date="2018-07-06T18:24:00.000Z"
/>
`;
@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Flex)
alignItems="center"
className="Title-root"
>
<withPropsOnChange(Icon)>
history
</withPropsOnChange(Icon)>
<Localized
id="decisionHistory-yourDecisionHistory"
>
<withPropsOnChange(Typography)
className="Title-text"
container="span"
>
Your Decision History
</withPropsOnChange(Typography)>
</Localized>
</withPropsOnChange(Flex)>
`;
@@ -0,0 +1,118 @@
import React from "react";
import { graphql, RelayPaginationProp } from "react-relay";
import { DecisionHistoryContainer_me as MeData } from "talk-admin/__generated__/DecisionHistoryContainer_me.graphql";
import { DecisionHistoryContainerPaginationQueryVariables } from "talk-admin/__generated__/DecisionHistoryContainerPaginationQuery.graphql";
import { withPaginationContainer } from "talk-framework/lib/relay";
import DecisionHistory from "../components/DecisionHistory";
interface DecisionHistoryContainerProps {
me: MeData;
relay: RelayPaginationProp;
}
export class DecisionHistoryContainer extends React.Component<
DecisionHistoryContainerProps
> {
public state = {
disableLoadMore: false,
};
public render() {
const actions = this.props.me.commentModerationActionHistory.edges.map(
edge => edge.node
);
return (
<DecisionHistory
actions={actions}
onLoadMore={this.loadMore}
hasMore={this.props.relay.hasMore()}
disableLoadMore={this.state.disableLoadMore}
/>
);
}
private loadMore = () => {
if (!this.props.relay.hasMore() || this.props.relay.isLoading()) {
return;
}
this.setState({ disableLoadMore: true });
this.props.relay.loadMore(
10, // Fetch the next 10 feed items
error => {
this.setState({ disableLoadMore: false });
if (error) {
// tslint:disable-next-line:no-console
console.error(error);
}
}
);
};
}
// TODO: (cvle) This should be autogenerated.
interface FragmentVariables {
count: number;
cursor?: string;
}
const enhanced = withPaginationContainer<
DecisionHistoryContainerProps,
DecisionHistoryContainerPaginationQueryVariables,
FragmentVariables
>(
{
me: graphql`
fragment DecisionHistoryContainer_me on User
@argumentDefinitions(
count: { type: "Int!", defaultValue: 5 }
cursor: { type: "Cursor" }
) {
commentModerationActionHistory(first: $count, after: $cursor)
@connection(key: "DecisionHistory_commentModerationActionHistory") {
edges {
node {
id
...DecisionHistoryItemContainer_action
}
}
}
}
`,
},
{
direction: "forward",
getConnectionFromProps(props) {
return props.me && props.me.commentModerationActionHistory;
},
// This is also the default implementation of `getFragmentVariables` if it isn't provided.
getFragmentVariables(prevVars, totalCount) {
return {
...prevVars,
count: totalCount,
};
},
getVariables(props, { count, cursor }, fragmentVariables) {
return {
count,
cursor,
};
},
query: graphql`
# Pagination query to be fetched upon calling 'loadMore'.
# Notice that we re-use our fragment, and the shape of this query matches our fragment spec.
query DecisionHistoryContainerPaginationQuery(
$count: Int!
$cursor: Cursor
) {
me {
...DecisionHistoryContainer_me
@arguments(count: $count, cursor: $cursor)
}
}
`,
}
)(DecisionHistoryContainer);
export default enhanced;
@@ -0,0 +1,66 @@
import React from "react";
import { graphql } from "react-relay";
import { DecisionHistoryItemContainer_action as ActionData } from "talk-admin/__generated__/DecisionHistoryItemContainer_action.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import AcceptedComment from "../components/AcceptedComment";
import RejectedComment from "../components/RejectedComment";
interface DecisionHistoryItemContainerProps {
action: ActionData;
}
class DecisionHistoryItemContainer extends React.Component<
DecisionHistoryItemContainerProps
> {
private handleGoToComment = (e: React.MouseEvent) => {
return;
};
public render() {
const username =
(this.props.action.revision.comment.author &&
this.props.action.revision.comment.author.username) ||
"Unknown"; // TODO: (cvle) Figure out what to display, when username is not available.
if (this.props.action.status === "ACCEPTED") {
return (
<AcceptedComment
href="#"
username={username}
date={this.props.action.createdAt}
onGotoComment={this.handleGoToComment}
/>
);
} else if (this.props.action.status === "REJECTED") {
return (
<RejectedComment
href="#"
username={username}
date={this.props.action.createdAt}
onGotoComment={this.handleGoToComment}
/>
);
}
return null;
}
}
const enhanced = withFragmentContainer<DecisionHistoryItemContainerProps>({
action: graphql`
fragment DecisionHistoryItemContainer_action on CommentModerationAction {
id
revision {
id
comment {
author {
username
}
}
}
createdAt
status
}
`,
})(DecisionHistoryItemContainer);
export default enhanced;
@@ -0,0 +1,36 @@
import React, { Component } from "react";
import { graphql, QueryRenderer } from "talk-framework/lib/relay";
import { DecisionHistoryQuery as QueryTypes } from "talk-admin/__generated__/DecisionHistoryQuery.graphql";
import DecisionHistoryLoading from "../components/DecisionHistoryLoading";
import DecisionHistoryContainer from "../containers/DecisionHistoryContainer";
class DecisionHistoryQuery extends Component {
public render() {
return (
<QueryRenderer<QueryTypes>
query={graphql`
query DecisionHistoryQuery {
me {
...DecisionHistoryContainer_me
}
}
`}
variables={{}}
render={({ error, props }) => {
if (error) {
return <div>{error.message}</div>;
}
if (!props || !props.me) {
return <DecisionHistoryLoading />;
}
return <DecisionHistoryContainer me={props.me} />;
}}
/>
);
}
}
export default DecisionHistoryQuery;
@@ -0,0 +1,5 @@
import { ReactTestInstance } from "react-test-renderer";
export default function getByTestID(id: string, instance: ReactTestInstance) {
return instance.findByProps({ "data-test": id });
}
@@ -0,0 +1,20 @@
import React from "react";
import { ReactTestInstance } from "react-test-renderer";
export default function getByText(text: string, instance: ReactTestInstance) {
return instance.find(i => {
if (!i.props.children) {
return false;
}
const children = React.Children.toArray(i.props.children);
for (const c of children) {
if (
typeof c === "string" &&
c.toLowerCase().includes(text.toLowerCase())
) {
return true;
}
}
return false;
});
}
@@ -13,3 +13,7 @@ export * from "./denormalize";
export { default as replaceHistoryLocation } from "./replaceHistoryLocation";
export { default as limitSnapshotTo } from "./limitSnapshotTo";
export { default as inputPredicate } from "./inputPredicate";
export { default as getByTestID } from "./getByTestID";
export { default as getByText } from "./getByText";
export { default as wait } from "./wait";
export { default as waitForElement } from "./waitForElement";
@@ -0,0 +1,13 @@
import waitForExpect from "wait-for-expect";
interface Options {
timeout?: number;
interval?: number;
}
export default async function wait(
callback: () => void,
{ timeout = 4500, interval = 50 }: Options = {}
) {
return waitForExpect(callback, timeout, interval);
}
@@ -0,0 +1,22 @@
import { ReactTestInstance } from "react-test-renderer";
import wait from "./wait";
interface Options {
timeout?: number;
interval?: number;
}
export default async function waitForElement(
callback: () => ReactTestInstance | null,
options?: Options
): Promise<ReactTestInstance> {
let result: ReactTestInstance | null = null;
await wait(() => {
result = callback();
if (!result) {
throw new Error();
}
}, options);
return result!;
}
@@ -1,4 +1,5 @@
.root {
padding: calc(0.5 * var(--spacing-unit));
}
.textField {
@@ -7,7 +7,6 @@
box-sizing: border-box;
box-shadow: var(--elevation-main);
border-radius: var(--round-corners);
padding: calc(0.5 * var(--spacing-unit));
}
.top {
@@ -43,7 +43,6 @@ interface PopoverProps {
children: (props: ChildrenRenderProps) => React.ReactNode;
description: string;
id: string;
onClose?: () => void;
className?: string;
placement?: Placement;
classes: typeof styles;
@@ -97,6 +96,7 @@ class Popover extends React.Component<PopoverProps> {
className,
placement,
classes,
...rest
} = this.props;
const { visible } = this.state;
@@ -108,7 +108,7 @@ class Popover extends React.Component<PopoverProps> {
});
return (
<div className={cn(classes.root, className)}>
<div className={cn(classes.root, className)} {...rest}>
<Manager>
<Reference>
{(props: PopperArrowProps) =>
@@ -3,6 +3,8 @@ import React, { StatelessComponent } from "react";
import { withStyles } from "talk-ui/hocs";
import styles from "./Spinner.css";
type Size = "sm" | "md";
export interface SpinnerProps {
/**
* Convenient prop to override the root styling.
@@ -12,18 +14,32 @@ export interface SpinnerProps {
* Override or extend the styles applied to the component.
*/
classes: typeof styles;
size?: Size;
}
function calculateSize(size: Size): number {
switch (size) {
case "sm":
return 30;
case "md":
return 40;
default:
throw new Error(`Unknown ${size}`);
}
}
const Spinner: StatelessComponent<SpinnerProps> = props => {
const { className, classes } = props;
const rootClassName = cn(classes.spinner, className);
const s = calculateSize(props.size!);
return (
<svg
className={rootClassName}
width="40px"
height="40px"
width={`${s}px`}
height={`${s}px`}
viewBox="0 0 66 66"
xmlns="http://www.w3.org/2000/svg"
>
@@ -40,5 +56,9 @@ const Spinner: StatelessComponent<SpinnerProps> = props => {
);
};
Spinner.defaultProps = {
size: "md",
};
const enhanced = withStyles(styles)(Spinner);
export default enhanced;
+3
View File
@@ -84,6 +84,9 @@ const variables = {
lg: 1400,
xl: 1600,
},
zindex: {
popover: 300,
},
};
export default variables;
+10
View File
@@ -109,3 +109,13 @@ configure-auth-oidc-authorizationURL = Authorization URL
configure-auth-oidc-tokenURL = Token URL
configure-auth-oidc-jwksURI = JWKS URI
configure-auth-oidc-useLoginOn = Use OpenID Connect login on
## Decision History
decisionHistory-youWillSeeAList =
You will see a list of your post moderation actions here.
decisionHistory-showMoreButton =
Show More
decisionHistory-yourDecisionHistory = Your Decision History
decisionHistory-rejectedCommentBy = Rejected comment by <username></username>
decisionHistory-acceptedCommentBy = Accepted comment by <username></username>
decisionHistory-goToComment = Go to comment