From 5b6208269392ef1a072c1c38f7384d19fb9b2cb8 Mon Sep 17 00:00:00 2001 From: Kiwi Date: Fri, 30 Nov 2018 20:51:45 +0100 Subject: [PATCH] [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; --- package-lock.json | 6 + package.json | 1 + src/core/client/admin/components/AppBar.css | 2 +- .../components/DecisionHistoryButton.css | 9 + .../components/DecisionHistoryButton.tsx | 55 ++ .../client/admin/components/Navigation.css | 5 + .../client/admin/components/Navigation.tsx | 4 + .../admin/components/NavigationDivider.css | 4 + .../components/NavigationDivider.spec.tsx | 9 + .../admin/components/NavigationDivider.tsx | 9 + .../admin/components/NavigationLink.css | 2 +- .../client/admin/components/SignOutButton.css | 5 + .../client/admin/components/SignOutButton.tsx | 4 +- .../__snapshots__/Navigation.spec.tsx.snap | 2 + .../NavigationDivider.spec.tsx.snap | 7 + .../__snapshots__/SignOutButton.spec.tsx.snap | 1 + .../decisionHistory.spec.tsx.snap | 472 ++++++++++++++++++ .../decisionHistory/decisionHistory.spec.tsx | 136 +++++ src/core/client/admin/test/fixtures.ts | 83 +++ .../components/AcceptedComment.spec.tsx | 18 + .../components/AcceptedComment.tsx | 43 ++ .../components/AcceptedIcon.css | 3 + .../components/AcceptedIcon.spec.tsx | 9 + .../components/AcceptedIcon.tsx | 11 + .../components/DecisionHistory.spec.tsx | 54 ++ .../components/DecisionHistory.tsx | 41 ++ .../components/DecisionHistoryLoading.css | 4 + .../DecisionHistoryLoading.spec.tsx | 9 + .../components/DecisionHistoryLoading.tsx | 26 + .../components/DecisionItem.css | 13 + .../components/DecisionItem.spec.tsx | 15 + .../components/DecisionItem.tsx | 21 + .../components/DecisionList.css | 12 + .../components/DecisionList.spec.tsx | 14 + .../components/DecisionList.tsx | 9 + .../decisionHistory/components/DotDivider.css | 7 + .../components/DotDivider.spec.tsx | 9 + .../decisionHistory/components/DotDivider.tsx | 9 + .../decisionHistory/components/Empty.css | 3 + .../decisionHistory/components/Empty.spec.tsx | 9 + .../decisionHistory/components/Empty.tsx | 18 + .../decisionHistory/components/Footer.css | 2 + .../components/Footer.spec.tsx | 14 + .../decisionHistory/components/Footer.tsx | 17 + .../components/GoToCommentLink.css | 9 + .../components/GoToCommentLink.spec.tsx | 16 + .../components/GoToCommentLink.tsx | 22 + .../views/decisionHistory/components/Info.css | 13 + .../decisionHistory/components/Info.spec.tsx | 14 + .../views/decisionHistory/components/Info.tsx | 15 + .../views/decisionHistory/components/Main.css | 5 + .../decisionHistory/components/Main.spec.tsx | 14 + .../views/decisionHistory/components/Main.tsx | 9 + .../components/RejectedComment.spec.tsx | 18 + .../components/RejectedComment.tsx | 43 ++ .../components/RejectedIcon.css | 3 + .../components/RejectedIcon.spec.tsx | 9 + .../components/RejectedIcon.tsx | 11 + .../components/ShowMoreButton.css | 16 + .../components/ShowMoreButton.spec.tsx | 16 + .../components/ShowMoreButton.tsx | 25 + .../decisionHistory/components/Timestamp.css | 8 + .../components/Timestamp.spec.tsx | 14 + .../decisionHistory/components/Timestamp.tsx | 15 + .../decisionHistory/components/Title.css | 16 + .../decisionHistory/components/Title.spec.tsx | 9 + .../decisionHistory/components/Title.tsx | 19 + .../AcceptedComment.spec.tsx.snap | 34 ++ .../__snapshots__/AcceptedIcon.spec.tsx.snap | 9 + .../DecisionHistory.spec.tsx.snap | 130 +++++ .../DecisionHistoryLoading.spec.tsx.snap | 24 + .../__snapshots__/DecisionItem.spec.tsx.snap | 18 + .../__snapshots__/DecisionList.spec.tsx.snap | 9 + .../__snapshots__/DotDivider.spec.tsx.snap | 9 + .../__snapshots__/Empty.spec.tsx.snap | 17 + .../__snapshots__/Footer.spec.tsx.snap | 10 + .../GoToCommentLink.spec.tsx.snap | 21 + .../__snapshots__/Info.spec.tsx.snap | 9 + .../__snapshots__/Main.spec.tsx.snap | 9 + .../RejectedComment.spec.tsx.snap | 34 ++ .../__snapshots__/RejectedIcon.spec.tsx.snap | 9 + .../ShowMoreButton.spec.tsx.snap | 15 + .../__snapshots__/Timestamp.spec.tsx.snap | 8 + .../__snapshots__/Title.spec.tsx.snap | 23 + .../containers/DecisionHistoryContainer.tsx | 118 +++++ .../DecisionHistoryItemContainer.tsx | 66 +++ .../queries/DecisionHistoryQuery.tsx | 36 ++ .../framework/testHelpers/getByTestID.ts | 5 + .../client/framework/testHelpers/getByText.ts | 20 + .../client/framework/testHelpers/index.ts | 4 + src/core/client/framework/testHelpers/wait.ts | 13 + .../framework/testHelpers/waitForElement.ts | 22 + .../PermalinkButton/PermalinkPopover.css | 1 + .../client/ui/components/Popover/Popover.css | 1 - .../client/ui/components/Popover/Popover.tsx | 4 +- .../client/ui/components/Spinner/Spinner.tsx | 24 +- src/core/client/ui/theme/variables.ts | 3 + src/locales/en-US/admin.ftl | 10 + 98 files changed, 2252 insertions(+), 8 deletions(-) create mode 100644 src/core/client/admin/components/DecisionHistoryButton.css create mode 100644 src/core/client/admin/components/DecisionHistoryButton.tsx create mode 100644 src/core/client/admin/components/NavigationDivider.css create mode 100644 src/core/client/admin/components/NavigationDivider.spec.tsx create mode 100644 src/core/client/admin/components/NavigationDivider.tsx create mode 100644 src/core/client/admin/components/SignOutButton.css create mode 100644 src/core/client/admin/components/__snapshots__/NavigationDivider.spec.tsx.snap create mode 100644 src/core/client/admin/test/decisionHistory/__snapshots__/decisionHistory.spec.tsx.snap create mode 100644 src/core/client/admin/test/decisionHistory/decisionHistory.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/AcceptedComment.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/AcceptedComment.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/AcceptedIcon.css create mode 100644 src/core/client/admin/views/decisionHistory/components/AcceptedIcon.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/AcceptedIcon.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/DecisionHistory.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/DecisionHistory.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/DecisionHistoryLoading.css create mode 100644 src/core/client/admin/views/decisionHistory/components/DecisionHistoryLoading.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/DecisionHistoryLoading.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/DecisionItem.css create mode 100644 src/core/client/admin/views/decisionHistory/components/DecisionItem.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/DecisionItem.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/DecisionList.css create mode 100644 src/core/client/admin/views/decisionHistory/components/DecisionList.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/DecisionList.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/DotDivider.css create mode 100644 src/core/client/admin/views/decisionHistory/components/DotDivider.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/DotDivider.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/Empty.css create mode 100644 src/core/client/admin/views/decisionHistory/components/Empty.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/Empty.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/Footer.css create mode 100644 src/core/client/admin/views/decisionHistory/components/Footer.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/Footer.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/GoToCommentLink.css create mode 100644 src/core/client/admin/views/decisionHistory/components/GoToCommentLink.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/GoToCommentLink.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/Info.css create mode 100644 src/core/client/admin/views/decisionHistory/components/Info.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/Info.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/Main.css create mode 100644 src/core/client/admin/views/decisionHistory/components/Main.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/Main.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/RejectedComment.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/RejectedComment.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/RejectedIcon.css create mode 100644 src/core/client/admin/views/decisionHistory/components/RejectedIcon.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/RejectedIcon.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/ShowMoreButton.css create mode 100644 src/core/client/admin/views/decisionHistory/components/ShowMoreButton.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/ShowMoreButton.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/Timestamp.css create mode 100644 src/core/client/admin/views/decisionHistory/components/Timestamp.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/Timestamp.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/Title.css create mode 100644 src/core/client/admin/views/decisionHistory/components/Title.spec.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/Title.tsx create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/AcceptedComment.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/AcceptedIcon.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionHistory.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionHistoryLoading.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionItem.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionList.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/DotDivider.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/Empty.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/Footer.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/GoToCommentLink.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/Info.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/Main.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/RejectedComment.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/RejectedIcon.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/ShowMoreButton.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/Timestamp.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/components/__snapshots__/Title.spec.tsx.snap create mode 100644 src/core/client/admin/views/decisionHistory/containers/DecisionHistoryContainer.tsx create mode 100644 src/core/client/admin/views/decisionHistory/containers/DecisionHistoryItemContainer.tsx create mode 100644 src/core/client/admin/views/decisionHistory/queries/DecisionHistoryQuery.tsx create mode 100644 src/core/client/framework/testHelpers/getByTestID.ts create mode 100644 src/core/client/framework/testHelpers/getByText.ts create mode 100644 src/core/client/framework/testHelpers/wait.ts create mode 100644 src/core/client/framework/testHelpers/waitForElement.ts diff --git a/package-lock.json b/package-lock.json index b3bdf869e..806f7d05e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 65156f224..659bf7469 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/core/client/admin/components/AppBar.css b/src/core/client/admin/components/AppBar.css index effb9c43b..8eb154452 100644 --- a/src/core/client/admin/components/AppBar.css +++ b/src/core/client/admin/components/AppBar.css @@ -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; } diff --git a/src/core/client/admin/components/DecisionHistoryButton.css b/src/core/client/admin/components/DecisionHistoryButton.css new file mode 100644 index 000000000..001a0a329 --- /dev/null +++ b/src/core/client/admin/components/DecisionHistoryButton.css @@ -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)); +} diff --git a/src/core/client/admin/components/DecisionHistoryButton.tsx b/src/core/client/admin/components/DecisionHistoryButton.tsx new file mode 100644 index 000000000..c248cc6c3 --- /dev/null +++ b/src/core/client/admin/components/DecisionHistoryButton.tsx @@ -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 ( + ( + + this.toggleVisibilityOncePerFrame(toggleVisibility) + } + > +
+ +
+
+ )} + > + {({ toggleVisibility, forwardRef, visible }) => ( + this.toggleVisibilityOncePerFrame(toggleVisibility)} + aria-controls={popoverID} + forwardRef={forwardRef} + className={styles.historyIcon} + data-test="decisionHistory-toggle" + > + history + + )} +
+ ); + } +} + +export default DecisionHistoryButton; diff --git a/src/core/client/admin/components/Navigation.css b/src/core/client/admin/components/Navigation.css index fd02200e2..661276f43 100644 --- a/src/core/client/admin/components/Navigation.css +++ b/src/core/client/admin/components/Navigation.css @@ -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); +} diff --git a/src/core/client/admin/components/Navigation.tsx b/src/core/client/admin/components/Navigation.tsx index 18112c2af..9f90c6a3e 100644 --- a/src/core/client/admin/components/Navigation.tsx +++ b/src/core/client/admin/components/Navigation.tsx @@ -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 = () => ( + + diff --git a/src/core/client/admin/components/NavigationDivider.css b/src/core/client/admin/components/NavigationDivider.css new file mode 100644 index 000000000..db5d684a2 --- /dev/null +++ b/src/core/client/admin/components/NavigationDivider.css @@ -0,0 +1,4 @@ +.root { + height: 75%; + border-right: 1px solid var(--palette-divider); +} diff --git a/src/core/client/admin/components/NavigationDivider.spec.tsx b/src/core/client/admin/components/NavigationDivider.spec.tsx new file mode 100644 index 000000000..07eb79537 --- /dev/null +++ b/src/core/client/admin/components/NavigationDivider.spec.tsx @@ -0,0 +1,9 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import NavigationDivider from "./NavigationDivider"; + +it("renders correctly", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/components/NavigationDivider.tsx b/src/core/client/admin/components/NavigationDivider.tsx new file mode 100644 index 000000000..f85a43c7c --- /dev/null +++ b/src/core/client/admin/components/NavigationDivider.tsx @@ -0,0 +1,9 @@ +import React, { StatelessComponent } from "react"; + +import styles from "./NavigationDivider.css"; + +const NavigationDivider: StatelessComponent<{}> = () => ( +
+); + +export default NavigationDivider; diff --git a/src/core/client/admin/components/NavigationLink.css b/src/core/client/admin/components/NavigationLink.css index 80cd64d7d..9155c5f83 100644 --- a/src/core/client/admin/components/NavigationLink.css +++ b/src/core/client/admin/components/NavigationLink.css @@ -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); diff --git a/src/core/client/admin/components/SignOutButton.css b/src/core/client/admin/components/SignOutButton.css new file mode 100644 index 000000000..8e62c2bc6 --- /dev/null +++ b/src/core/client/admin/components/SignOutButton.css @@ -0,0 +1,5 @@ +.root { + font-weight: var(--font-weight-bold); + font-size: calc(18rem / var(--rem-base)); + letter-spacing: calc(-0.2em / 18); +} diff --git a/src/core/client/admin/components/SignOutButton.tsx b/src/core/client/admin/components/SignOutButton.tsx index c4f0bd8c9..e50927d72 100644 --- a/src/core/client/admin/components/SignOutButton.tsx +++ b/src/core/client/admin/components/SignOutButton.tsx @@ -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; @@ -10,7 +12,7 @@ interface Props { const SignOutButton: StatelessComponent = props => ( - diff --git a/src/core/client/admin/components/__snapshots__/Navigation.spec.tsx.snap b/src/core/client/admin/components/__snapshots__/Navigation.spec.tsx.snap index 1d44e5052..2ae96297a 100644 --- a/src/core/client/admin/components/__snapshots__/Navigation.spec.tsx.snap +++ b/src/core/client/admin/components/__snapshots__/Navigation.spec.tsx.snap @@ -48,6 +48,8 @@ exports[`renders correctly 1`] = ` + + diff --git a/src/core/client/admin/components/__snapshots__/NavigationDivider.spec.tsx.snap b/src/core/client/admin/components/__snapshots__/NavigationDivider.spec.tsx.snap new file mode 100644 index 000000000..2c3bd27f6 --- /dev/null +++ b/src/core/client/admin/components/__snapshots__/NavigationDivider.spec.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+`; diff --git a/src/core/client/admin/components/__snapshots__/SignOutButton.spec.tsx.snap b/src/core/client/admin/components/__snapshots__/SignOutButton.spec.tsx.snap index a44d3d67c..23ffa5fa0 100644 --- a/src/core/client/admin/components/__snapshots__/SignOutButton.spec.tsx.snap +++ b/src/core/client/admin/components/__snapshots__/SignOutButton.spec.tsx.snap @@ -5,6 +5,7 @@ exports[`renders correctly 1`] = ` id="navigation-signOutButton" > diff --git a/src/core/client/admin/test/decisionHistory/__snapshots__/decisionHistory.spec.tsx.snap b/src/core/client/admin/test/decisionHistory/__snapshots__/decisionHistory.spec.tsx.snap new file mode 100644 index 000000000..decfa1756 --- /dev/null +++ b/src/core/client/admin/test/decisionHistory/__snapshots__/decisionHistory.spec.tsx.snap @@ -0,0 +1,472 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`loads more 1`] = ` +
+
+ + +   + + + Your Decision History + +
+
+ +
+
+`; + +exports[`opens popover when clicked on button showing loading state 1`] = ` +
+
+ + +   + + + Your Decision History + +
+
+
+
+
+`; + +exports[`render popover content 1`] = ` +
+
+ + +   + + + Your Decision History + +
+
+ + +
+
+`; + +exports[`renders decision history popover button 1`] = ` +
+ +
+
+ A dialog showing a permalink to the comment +
+
+
+`; diff --git a/src/core/client/admin/test/decisionHistory/decisionHistory.spec.tsx b/src/core/client/admin/test/decisionHistory/decisionHistory.spec.tsx new file mode 100644 index 000000000..1b40addac --- /dev/null +++ b/src/core/client/admin/test/decisionHistory/decisionHistory.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/test/fixtures.ts b/src/core/client/admin/test/fixtures.ts index c33ddd43a..e1669ce41 100644 --- a/src/core/client/admin/test/fixtures.ts +++ b/src/core/client/admin/test/fixtures.ts @@ -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", + }, +]; diff --git a/src/core/client/admin/views/decisionHistory/components/AcceptedComment.spec.tsx b/src/core/client/admin/views/decisionHistory/components/AcceptedComment.spec.tsx new file mode 100644 index 000000000..f28b46015 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/AcceptedComment.spec.tsx @@ -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 = { + href: "#", + username: "InTheExpensiveSeats", + date: "2018-07-06T18:24:00.000Z", + onGotoComment: noop, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/AcceptedComment.tsx b/src/core/client/admin/views/decisionHistory/components/AcceptedComment.tsx new file mode 100644 index 000000000..ad30b4767 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/AcceptedComment.tsx @@ -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; +} + +const Username: StatelessComponent<{ username: string }> = ({ username }) => ( + {username} +); + +const ApprovedComment: StatelessComponent = props => ( + }> + } + > + {"Accepted comment by "} + +
+ + {props.date} + + + +
+
+); + +export default ApprovedComment; diff --git a/src/core/client/admin/views/decisionHistory/components/AcceptedIcon.css b/src/core/client/admin/views/decisionHistory/components/AcceptedIcon.css new file mode 100644 index 000000000..50efdc1dc --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/AcceptedIcon.css @@ -0,0 +1,3 @@ +.root { + color: var(--palette-success-main); +} diff --git a/src/core/client/admin/views/decisionHistory/components/AcceptedIcon.spec.tsx b/src/core/client/admin/views/decisionHistory/components/AcceptedIcon.spec.tsx new file mode 100644 index 000000000..c1dec6d58 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/AcceptedIcon.spec.tsx @@ -0,0 +1,9 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import AcceptedIcon from "./AcceptedIcon"; + +it("renders correctly", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/AcceptedIcon.tsx b/src/core/client/admin/views/decisionHistory/components/AcceptedIcon.tsx new file mode 100644 index 000000000..076e6467c --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/AcceptedIcon.tsx @@ -0,0 +1,11 @@ +import React, { StatelessComponent } from "react"; + +import { Icon } from "talk-ui/components"; + +import styles from "./AcceptedIcon.css"; + +const AcceptedIcon: StatelessComponent = () => ( + check +); + +export default AcceptedIcon; diff --git a/src/core/client/admin/views/decisionHistory/components/DecisionHistory.spec.tsx b/src/core/client/admin/views/decisionHistory/components/DecisionHistory.spec.tsx new file mode 100644 index 000000000..bbef181a3 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/DecisionHistory.spec.tsx @@ -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 = { + actions: [{ id: "1" }, { id: "2" }, { id: "3" }], + onLoadMore: noop, + hasMore: false, + disableLoadMore: false, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); + +it("renders empty state", () => { + const props: PropTypesOf = { + actions: [], + onLoadMore: noop, + hasMore: false, + disableLoadMore: false, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); + +it("renders hasMore", () => { + const props: PropTypesOf = { + actions: [{ id: "1" }, { id: "2" }, { id: "3" }], + onLoadMore: noop, + hasMore: true, + disableLoadMore: false, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); + +it("renders disable load more", () => { + const props: PropTypesOf = { + actions: [{ id: "1" }, { id: "2" }, { id: "3" }], + onLoadMore: noop, + hasMore: true, + disableLoadMore: true, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/DecisionHistory.tsx b/src/core/client/admin/views/decisionHistory/components/DecisionHistory.tsx new file mode 100644 index 000000000..ea090ae32 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/DecisionHistory.tsx @@ -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["action"] + >; + onLoadMore?: () => void; + hasMore?: boolean; + disableLoadMore?: boolean; +} + +const DecisionHistory: StatelessComponent = props => ( +
+ + <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; diff --git a/src/core/client/admin/views/decisionHistory/components/DecisionHistoryLoading.css b/src/core/client/admin/views/decisionHistory/components/DecisionHistoryLoading.css new file mode 100644 index 000000000..7b77839a1 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/DecisionHistoryLoading.css @@ -0,0 +1,4 @@ +.container { + padding: var(--spacing-unit); + min-height: 30px; +} diff --git a/src/core/client/admin/views/decisionHistory/components/DecisionHistoryLoading.spec.tsx b/src/core/client/admin/views/decisionHistory/components/DecisionHistoryLoading.spec.tsx new file mode 100644 index 000000000..d8047d34e --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/DecisionHistoryLoading.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/DecisionHistoryLoading.tsx b/src/core/client/admin/views/decisionHistory/components/DecisionHistoryLoading.tsx new file mode 100644 index 000000000..987f7e8ce --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/DecisionHistoryLoading.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/components/DecisionItem.css b/src/core/client/admin/views/decisionHistory/components/DecisionItem.css new file mode 100644 index 000000000..5216bdf50 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/DecisionItem.css @@ -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; +} diff --git a/src/core/client/admin/views/decisionHistory/components/DecisionItem.spec.tsx b/src/core/client/admin/views/decisionHistory/components/DecisionItem.spec.tsx new file mode 100644 index 000000000..5c8063609 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/DecisionItem.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/DecisionItem.tsx b/src/core/client/admin/views/decisionHistory/components/DecisionItem.tsx new file mode 100644 index 000000000..422e7fd75 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/DecisionItem.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/components/DecisionList.css b/src/core/client/admin/views/decisionHistory/components/DecisionList.css new file mode 100644 index 000000000..92219280f --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/DecisionList.css @@ -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; + } +} diff --git a/src/core/client/admin/views/decisionHistory/components/DecisionList.spec.tsx b/src/core/client/admin/views/decisionHistory/components/DecisionList.spec.tsx new file mode 100644 index 000000000..2a1103b47 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/DecisionList.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/DecisionList.tsx b/src/core/client/admin/views/decisionHistory/components/DecisionList.tsx new file mode 100644 index 000000000..a06dac070 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/DecisionList.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/components/DotDivider.css b/src/core/client/admin/views/decisionHistory/components/DotDivider.css new file mode 100644 index 000000000..2c8a678ff --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/DotDivider.css @@ -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)); +} diff --git a/src/core/client/admin/views/decisionHistory/components/DotDivider.spec.tsx b/src/core/client/admin/views/decisionHistory/components/DotDivider.spec.tsx new file mode 100644 index 000000000..26e75f123 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/DotDivider.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/DotDivider.tsx b/src/core/client/admin/views/decisionHistory/components/DotDivider.tsx new file mode 100644 index 000000000..b433d3e00 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/DotDivider.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/components/Empty.css b/src/core/client/admin/views/decisionHistory/components/Empty.css new file mode 100644 index 000000000..e9b985a5f --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Empty.css @@ -0,0 +1,3 @@ +.root { + padding: calc(1.5 * var(--spacing-unit)) calc(2 * var(--spacing-unit)); +} diff --git a/src/core/client/admin/views/decisionHistory/components/Empty.spec.tsx b/src/core/client/admin/views/decisionHistory/components/Empty.spec.tsx new file mode 100644 index 000000000..3cf25937c --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Empty.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/Empty.tsx b/src/core/client/admin/views/decisionHistory/components/Empty.tsx new file mode 100644 index 000000000..71d313a13 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Empty.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/components/Footer.css b/src/core/client/admin/views/decisionHistory/components/Footer.css new file mode 100644 index 000000000..c3a2af639 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Footer.css @@ -0,0 +1,2 @@ +.root { +} diff --git a/src/core/client/admin/views/decisionHistory/components/Footer.spec.tsx b/src/core/client/admin/views/decisionHistory/components/Footer.spec.tsx new file mode 100644 index 000000000..eea6261c3 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Footer.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/Footer.tsx b/src/core/client/admin/views/decisionHistory/components/Footer.tsx new file mode 100644 index 000000000..7f4fff81c --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Footer.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/components/GoToCommentLink.css b/src/core/client/admin/views/decisionHistory/components/GoToCommentLink.css new file mode 100644 index 000000000..519ed50fb --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/GoToCommentLink.css @@ -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; +} diff --git a/src/core/client/admin/views/decisionHistory/components/GoToCommentLink.spec.tsx b/src/core/client/admin/views/decisionHistory/components/GoToCommentLink.spec.tsx new file mode 100644 index 000000000..31984f27f --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/GoToCommentLink.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/GoToCommentLink.tsx b/src/core/client/admin/views/decisionHistory/components/GoToCommentLink.tsx new file mode 100644 index 000000000..96e3ec809 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/GoToCommentLink.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/components/Info.css b/src/core/client/admin/views/decisionHistory/components/Info.css new file mode 100644 index 000000000..daa3c0e8f --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Info.css @@ -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; +} diff --git a/src/core/client/admin/views/decisionHistory/components/Info.spec.tsx b/src/core/client/admin/views/decisionHistory/components/Info.spec.tsx new file mode 100644 index 000000000..005ca6fa9 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Info.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/Info.tsx b/src/core/client/admin/views/decisionHistory/components/Info.tsx new file mode 100644 index 000000000..aee38accf --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Info.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/components/Main.css b/src/core/client/admin/views/decisionHistory/components/Main.css new file mode 100644 index 000000000..062a053aa --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Main.css @@ -0,0 +1,5 @@ +.root { + max-height: calc(26 * var(--spacing-unit)); + overflow: hidden; + overflow-y: scroll; +} diff --git a/src/core/client/admin/views/decisionHistory/components/Main.spec.tsx b/src/core/client/admin/views/decisionHistory/components/Main.spec.tsx new file mode 100644 index 000000000..edfbee57b --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Main.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/Main.tsx b/src/core/client/admin/views/decisionHistory/components/Main.tsx new file mode 100644 index 000000000..568b32798 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Main.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/components/RejectedComment.spec.tsx b/src/core/client/admin/views/decisionHistory/components/RejectedComment.spec.tsx new file mode 100644 index 000000000..2319c383d --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/RejectedComment.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/RejectedComment.tsx b/src/core/client/admin/views/decisionHistory/components/RejectedComment.tsx new file mode 100644 index 000000000..bbaddcd38 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/RejectedComment.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/components/RejectedIcon.css b/src/core/client/admin/views/decisionHistory/components/RejectedIcon.css new file mode 100644 index 000000000..488befa65 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/RejectedIcon.css @@ -0,0 +1,3 @@ +.root { + color: var(--palette-error-main); +} diff --git a/src/core/client/admin/views/decisionHistory/components/RejectedIcon.spec.tsx b/src/core/client/admin/views/decisionHistory/components/RejectedIcon.spec.tsx new file mode 100644 index 000000000..238ee1ce0 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/RejectedIcon.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/RejectedIcon.tsx b/src/core/client/admin/views/decisionHistory/components/RejectedIcon.tsx new file mode 100644 index 000000000..3d0453986 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/RejectedIcon.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/components/ShowMoreButton.css b/src/core/client/admin/views/decisionHistory/components/ShowMoreButton.css new file mode 100644 index 000000000..3ce579659 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/ShowMoreButton.css @@ -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); + } +} diff --git a/src/core/client/admin/views/decisionHistory/components/ShowMoreButton.spec.tsx b/src/core/client/admin/views/decisionHistory/components/ShowMoreButton.spec.tsx new file mode 100644 index 000000000..e51b0fbb5 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/ShowMoreButton.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/ShowMoreButton.tsx b/src/core/client/admin/views/decisionHistory/components/ShowMoreButton.tsx new file mode 100644 index 000000000..ac587b235 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/ShowMoreButton.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/components/Timestamp.css b/src/core/client/admin/views/decisionHistory/components/Timestamp.css new file mode 100644 index 000000000..45c88b4cf --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Timestamp.css @@ -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; +} diff --git a/src/core/client/admin/views/decisionHistory/components/Timestamp.spec.tsx b/src/core/client/admin/views/decisionHistory/components/Timestamp.spec.tsx new file mode 100644 index 000000000..bc9c50d1f --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Timestamp.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/Timestamp.tsx b/src/core/client/admin/views/decisionHistory/components/Timestamp.tsx new file mode 100644 index 000000000..8e001775e --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Timestamp.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/components/Title.css b/src/core/client/admin/views/decisionHistory/components/Title.css new file mode 100644 index 000000000..60df8148b --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Title.css @@ -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); +} diff --git a/src/core/client/admin/views/decisionHistory/components/Title.spec.tsx b/src/core/client/admin/views/decisionHistory/components/Title.spec.tsx new file mode 100644 index 000000000..13a8a3350 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Title.spec.tsx @@ -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(); +}); diff --git a/src/core/client/admin/views/decisionHistory/components/Title.tsx b/src/core/client/admin/views/decisionHistory/components/Title.tsx new file mode 100644 index 000000000..9d2e252a3 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/Title.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/AcceptedComment.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/AcceptedComment.spec.tsx.snap new file mode 100644 index 000000000..73da65ade --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/AcceptedComment.spec.tsx.snap @@ -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 <username></username> + </Info> + </Localized> + <Footer> + <withPropsOnChange(Typography) + variant="timestamp" + > + <DecisionHistory> + 2018-07-06T18:24:00.000Z + </DecisionHistory> + </withPropsOnChange(Typography)> + <Footer /> + <GoToCommentLink + href="#" + onClick={[Function]} + /> + </Footer> +</DecisionItem> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/AcceptedIcon.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/AcceptedIcon.spec.tsx.snap new file mode 100644 index 000000000..2b5623b90 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/AcceptedIcon.spec.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<withPropsOnChange(Icon) + className="AcceptedIcon-root" +> + check +</withPropsOnChange(Icon)> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionHistory.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionHistory.spec.tsx.snap new file mode 100644 index 000000000..412f7ff66 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionHistory.spec.tsx.snap @@ -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> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionHistoryLoading.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionHistoryLoading.spec.tsx.snap new file mode 100644 index 000000000..4bc7efda0 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionHistoryLoading.spec.tsx.snap @@ -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> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionItem.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionItem.spec.tsx.snap new file mode 100644 index 000000000..20dd8d17d --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionItem.spec.tsx.snap @@ -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> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionList.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionList.spec.tsx.snap new file mode 100644 index 000000000..57e7f4d20 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/DecisionList.spec.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<ul + className="DecisionList-root" +> + children +</ul> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/DotDivider.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/DotDivider.spec.tsx.snap new file mode 100644 index 000000000..8178ec67b --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/DotDivider.spec.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<div + className="DotDivider-root" +> + • +</div> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/Empty.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/Empty.spec.tsx.snap new file mode 100644 index 000000000..e15533cea --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/Empty.spec.tsx.snap @@ -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)> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/Footer.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/Footer.spec.tsx.snap new file mode 100644 index 000000000..5b74b302c --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/Footer.spec.tsx.snap @@ -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)> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/GoToCommentLink.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/GoToCommentLink.spec.tsx.snap new file mode 100644 index 000000000..f91cab384 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/GoToCommentLink.spec.tsx.snap @@ -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)> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/Info.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/Info.spec.tsx.snap new file mode 100644 index 000000000..339d7e695 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/Info.spec.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<withPropsOnChange(Typography) + className="Info-root" +> + children +</withPropsOnChange(Typography)> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/Main.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/Main.spec.tsx.snap new file mode 100644 index 000000000..c4fce236d --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/Main.spec.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<div + className="Main-root" +> + children +</div> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/RejectedComment.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/RejectedComment.spec.tsx.snap new file mode 100644 index 000000000..726c384a5 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/RejectedComment.spec.tsx.snap @@ -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 <username></username> + </Info> + </Localized> + <Footer> + <withPropsOnChange(Typography) + variant="timestamp" + > + <DecisionHistory> + 2018-07-06T18:24:00.000Z + </DecisionHistory> + </withPropsOnChange(Typography)> + <Footer /> + <GoToCommentLink + href="#" + onClick={[Function]} + /> + </Footer> +</DecisionItem> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/RejectedIcon.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/RejectedIcon.spec.tsx.snap new file mode 100644 index 000000000..288505090 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/RejectedIcon.spec.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +<withPropsOnChange(Icon) + className="RejectedIcon-root" +> + cancel +</withPropsOnChange(Icon)> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/ShowMoreButton.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/ShowMoreButton.spec.tsx.snap new file mode 100644 index 000000000..9e9480e48 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/ShowMoreButton.spec.tsx.snap @@ -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> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/Timestamp.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/Timestamp.spec.tsx.snap new file mode 100644 index 000000000..f0cdefdae --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/Timestamp.spec.tsx.snap @@ -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" +/> +`; diff --git a/src/core/client/admin/views/decisionHistory/components/__snapshots__/Title.spec.tsx.snap b/src/core/client/admin/views/decisionHistory/components/__snapshots__/Title.spec.tsx.snap new file mode 100644 index 000000000..693dd705c --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/components/__snapshots__/Title.spec.tsx.snap @@ -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)> +`; diff --git a/src/core/client/admin/views/decisionHistory/containers/DecisionHistoryContainer.tsx b/src/core/client/admin/views/decisionHistory/containers/DecisionHistoryContainer.tsx new file mode 100644 index 000000000..f75d2db0c --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/containers/DecisionHistoryContainer.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/containers/DecisionHistoryItemContainer.tsx b/src/core/client/admin/views/decisionHistory/containers/DecisionHistoryItemContainer.tsx new file mode 100644 index 000000000..50d202efe --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/containers/DecisionHistoryItemContainer.tsx @@ -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; diff --git a/src/core/client/admin/views/decisionHistory/queries/DecisionHistoryQuery.tsx b/src/core/client/admin/views/decisionHistory/queries/DecisionHistoryQuery.tsx new file mode 100644 index 000000000..3826902a3 --- /dev/null +++ b/src/core/client/admin/views/decisionHistory/queries/DecisionHistoryQuery.tsx @@ -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; diff --git a/src/core/client/framework/testHelpers/getByTestID.ts b/src/core/client/framework/testHelpers/getByTestID.ts new file mode 100644 index 000000000..653d0899f --- /dev/null +++ b/src/core/client/framework/testHelpers/getByTestID.ts @@ -0,0 +1,5 @@ +import { ReactTestInstance } from "react-test-renderer"; + +export default function getByTestID(id: string, instance: ReactTestInstance) { + return instance.findByProps({ "data-test": id }); +} diff --git a/src/core/client/framework/testHelpers/getByText.ts b/src/core/client/framework/testHelpers/getByText.ts new file mode 100644 index 000000000..60132154c --- /dev/null +++ b/src/core/client/framework/testHelpers/getByText.ts @@ -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; + }); +} diff --git a/src/core/client/framework/testHelpers/index.ts b/src/core/client/framework/testHelpers/index.ts index c9b807f90..0593be40f 100644 --- a/src/core/client/framework/testHelpers/index.ts +++ b/src/core/client/framework/testHelpers/index.ts @@ -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"; diff --git a/src/core/client/framework/testHelpers/wait.ts b/src/core/client/framework/testHelpers/wait.ts new file mode 100644 index 000000000..29454b281 --- /dev/null +++ b/src/core/client/framework/testHelpers/wait.ts @@ -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); +} diff --git a/src/core/client/framework/testHelpers/waitForElement.ts b/src/core/client/framework/testHelpers/waitForElement.ts new file mode 100644 index 000000000..38640be11 --- /dev/null +++ b/src/core/client/framework/testHelpers/waitForElement.ts @@ -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!; +} diff --git a/src/core/client/stream/tabs/comments/components/PermalinkButton/PermalinkPopover.css b/src/core/client/stream/tabs/comments/components/PermalinkButton/PermalinkPopover.css index 3fad6d3be..85c883743 100644 --- a/src/core/client/stream/tabs/comments/components/PermalinkButton/PermalinkPopover.css +++ b/src/core/client/stream/tabs/comments/components/PermalinkButton/PermalinkPopover.css @@ -1,4 +1,5 @@ .root { + padding: calc(0.5 * var(--spacing-unit)); } .textField { diff --git a/src/core/client/ui/components/Popover/Popover.css b/src/core/client/ui/components/Popover/Popover.css index 3c7488ce6..ed8fe6c1a 100644 --- a/src/core/client/ui/components/Popover/Popover.css +++ b/src/core/client/ui/components/Popover/Popover.css @@ -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 { diff --git a/src/core/client/ui/components/Popover/Popover.tsx b/src/core/client/ui/components/Popover/Popover.tsx index 30b2f6a3b..111bfcf61 100644 --- a/src/core/client/ui/components/Popover/Popover.tsx +++ b/src/core/client/ui/components/Popover/Popover.tsx @@ -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) => diff --git a/src/core/client/ui/components/Spinner/Spinner.tsx b/src/core/client/ui/components/Spinner/Spinner.tsx index 090980260..c9fbbd99b 100644 --- a/src/core/client/ui/components/Spinner/Spinner.tsx +++ b/src/core/client/ui/components/Spinner/Spinner.tsx @@ -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; diff --git a/src/core/client/ui/theme/variables.ts b/src/core/client/ui/theme/variables.ts index b8ba80e38..31abf7df3 100644 --- a/src/core/client/ui/theme/variables.ts +++ b/src/core/client/ui/theme/variables.ts @@ -84,6 +84,9 @@ const variables = { lg: 1400, xl: 1600, }, + zindex: { + popover: 300, + }, }; export default variables; diff --git a/src/locales/en-US/admin.ftl b/src/locales/en-US/admin.ftl index f9e0fe00f..31bffddb1 100644 --- a/src/locales/en-US/admin.ftl +++ b/src/locales/en-US/admin.ftl @@ -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