From 25c931cc0570d70d8d47dab390f931a7b27a445f Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 9 Jul 2018 16:31:57 -0300 Subject: [PATCH] Implement Replies --- config/watcher.ts | 7 +- config/webpack.config.dev.js | 5 + src/core/client/stream/components/Indent.tsx | 16 ++ .../stream/components/ReplyList.spec.tsx | 36 ++++ .../client/stream/components/ReplyList.tsx | 37 ++++ .../client/stream/components/Stream.spec.tsx | 8 +- src/core/client/stream/components/Stream.tsx | 19 +- .../__snapshots__/ReplyList.spec.tsx.snap | 56 ++++++ .../__snapshots__/Stream.spec.tsx.snap | 102 +++++++--- .../stream/containers/CommentContainer.tsx | 15 +- .../containers/ReplyListContainer.spec.tsx | 37 ++++ .../stream/containers/ReplyListContainer.tsx | 125 ++++++++++++ .../containers/StreamContainer.spec.tsx | 2 +- .../stream/containers/StreamContainer.tsx | 6 +- .../ReplyListContainer.spec.tsx.snap | 20 ++ .../StreamContainer.spec.tsx.snap | 4 +- .../test/__snapshots__/loadMore.spec.tsx.snap | 132 +++++++------ .../__snapshots__/renderReplies.spec.tsx.snap | 111 +++++++++++ .../__snapshots__/renderStream.spec.tsx.snap | 52 ++--- .../showAllReplies.spec.tsx.snap | 181 ++++++++++++++++++ .../client/stream/test/createEnvironment.ts | 1 - src/core/client/stream/test/fixtures.ts | 30 +++ src/core/client/stream/test/loadMore.spec.tsx | 2 +- .../client/stream/test/renderReplies.spec.tsx | 42 ++++ .../stream/test/showAllReplies.spec.tsx | 100 ++++++++++ 25 files changed, 1007 insertions(+), 139 deletions(-) create mode 100644 src/core/client/stream/components/Indent.tsx create mode 100644 src/core/client/stream/components/ReplyList.spec.tsx create mode 100644 src/core/client/stream/components/ReplyList.tsx create mode 100644 src/core/client/stream/components/__snapshots__/ReplyList.spec.tsx.snap create mode 100644 src/core/client/stream/containers/ReplyListContainer.spec.tsx create mode 100644 src/core/client/stream/containers/ReplyListContainer.tsx create mode 100644 src/core/client/stream/containers/__snapshots__/ReplyListContainer.spec.tsx.snap create mode 100644 src/core/client/stream/test/__snapshots__/renderReplies.spec.tsx.snap create mode 100644 src/core/client/stream/test/__snapshots__/showAllReplies.spec.tsx.snap create mode 100644 src/core/client/stream/test/renderReplies.spec.tsx create mode 100644 src/core/client/stream/test/showAllReplies.spec.tsx diff --git a/config/watcher.ts b/config/watcher.ts index 0d3bf23c1..9ff074e51 100644 --- a/config/watcher.ts +++ b/config/watcher.ts @@ -15,7 +15,12 @@ const config: Config = { "core/client/stream/**/*.graphql", "core/client/server/**/*.graphql", ], - ignore: ["core/**/*.d.ts", "core/**/*.graphql.ts"], + ignore: [ + "core/**/*.d.ts", + "core/**/*.graphql.ts", + "**/test/**/*", + "core/**/*.spec.*", + ], executor: new CommandExecutor("npm run compile:relay-stream", { runOnInit: true, }), diff --git a/config/webpack.config.dev.js b/config/webpack.config.dev.js index 80872c5a4..fcbb75fe2 100644 --- a/config/webpack.config.dev.js +++ b/config/webpack.config.dev.js @@ -224,6 +224,11 @@ module.exports = { jsx: "preserve", noEmit: false, }, + + // Overwrites the behavior of `include` and `exclude` to only + // include files that are actually being imported and which + // are necessary to compile the bundle. + onlyCompileBundledFiles: true, }, }, ], diff --git a/src/core/client/stream/components/Indent.tsx b/src/core/client/stream/components/Indent.tsx new file mode 100644 index 000000000..49222acff --- /dev/null +++ b/src/core/client/stream/components/Indent.tsx @@ -0,0 +1,16 @@ +import React, { StatelessComponent } from "react"; + +export interface IndentProps { + level?: number; + children: React.ReactNode; +} + +const Indent: StatelessComponent = props => { + return ( +
+ {props.children} +
+ ); +}; + +export default Indent; diff --git a/src/core/client/stream/components/ReplyList.spec.tsx b/src/core/client/stream/components/ReplyList.spec.tsx new file mode 100644 index 000000000..2e5d6a984 --- /dev/null +++ b/src/core/client/stream/components/ReplyList.spec.tsx @@ -0,0 +1,36 @@ +import { shallow } from "enzyme"; +import { noop } from "lodash"; +import React from "react"; +import sinon from "sinon"; + +import ReplyList from "./ReplyList"; + +it("renders correctly", () => { + const props = { + commentID: "comment-id", + comments: [{ id: "comment-1" }, { id: "comment-2" }], + onLoadMore: noop, + hasMore: false, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); + +describe("when there is more", () => { + const props = { + commentID: "comment-id", + comments: [{ id: "comment-1" }, { id: "comment-2" }], + onLoadMore: sinon.spy(), + hasMore: true, + }; + + const wrapper = shallow(); + it("renders a load more button", () => { + expect(wrapper).toMatchSnapshot(); + }); + + it("calls onLoadMore", () => { + wrapper.find("#talk-reply-list--show-all--comment-id").simulate("click"); + expect(props.onLoadMore.calledOnce).toBe(true); + }); +}); diff --git a/src/core/client/stream/components/ReplyList.tsx b/src/core/client/stream/components/ReplyList.tsx new file mode 100644 index 000000000..ce9aa13c5 --- /dev/null +++ b/src/core/client/stream/components/ReplyList.tsx @@ -0,0 +1,37 @@ +import * as React from "react"; +import { StatelessComponent } from "react"; + +import { Button } from "talk-ui/components"; + +import CommentContainer from "../containers/CommentContainer"; +import Indent from "./Indent"; + +export interface ReplyListProps { + commentID: string; + comments: ReadonlyArray<{ id: string }>; + onLoadMore: () => void; + hasMore: boolean; +} + +const ReplyList: StatelessComponent = props => { + return ( + + {props.comments.map(comment => ( + + ))} + {props.hasMore && ( + + )} + + ); +}; + +export default ReplyList; diff --git a/src/core/client/stream/components/Stream.spec.tsx b/src/core/client/stream/components/Stream.spec.tsx index 0c632d661..09ae7dfe3 100644 --- a/src/core/client/stream/components/Stream.spec.tsx +++ b/src/core/client/stream/components/Stream.spec.tsx @@ -7,7 +7,7 @@ import Stream from "./Stream"; it("renders correctly", () => { const props = { - id: "asset-id", + assetID: "asset-id", isClosed: false, comments: [{ id: "comment-1" }, { id: "comment-2" }], onLoadMore: noop, @@ -19,7 +19,7 @@ it("renders correctly", () => { it("renders when comments is null", () => { const props = { - id: "asset-id", + assetID: "asset-id", isClosed: false, comments: null, onLoadMore: noop, @@ -31,7 +31,7 @@ it("renders when comments is null", () => { describe("when there is more", () => { const props = { - id: "asset-id", + assetID: "asset-id", isClosed: false, comments: [{ id: "comment-1" }, { id: "comment-2" }], onLoadMore: sinon.spy(), @@ -44,7 +44,7 @@ describe("when there is more", () => { }); it("calls onLoadMore", () => { - wrapper.find("#talk-stream--loadmore").simulate("click"); + wrapper.find("#talk-stream--load-more").simulate("click"); expect(props.onLoadMore.calledOnce).toBe(true); }); }); diff --git a/src/core/client/stream/components/Stream.tsx b/src/core/client/stream/components/Stream.tsx index e884cacc2..c8eff6337 100644 --- a/src/core/client/stream/components/Stream.tsx +++ b/src/core/client/stream/components/Stream.tsx @@ -1,13 +1,15 @@ import * as React from "react"; import { StatelessComponent } from "react"; -import Logo from "talk-stream/components/Logo"; -import CommentContainer from "talk-stream/containers/CommentContainer"; -import PostCommentFormContainer from "talk-stream/containers/PostCommentFormContainer"; import { Button } from "talk-ui/components"; +import CommentContainer from "../containers/CommentContainer"; +import PostCommentFormContainer from "../containers/PostCommentFormContainer"; +import ReplyListContainer from "../containers/ReplyListContainer"; +import Logo from "./Logo"; + export interface StreamProps { - id: string; + assetID: string; isClosed: boolean; comments: ReadonlyArray<{ id: string }> | null; onLoadMore: () => void; @@ -22,13 +24,16 @@ const Stream: StatelessComponent = props => { return (
- + {props.comments.map(comment => ( - +
+ + +
))} {props.hasMore && (
+
+ > + + +
`; @@ -43,27 +63,47 @@ exports[`when there is more renders a load more button 1`] = ` - - + + + +
+ > + + +
; +// tslint:disable-next-line:no-unused-expression +graphql` + fragment CommentContainer_comment on Comment { + author { + username + } + body + } +`; + export const CommentContainer: StatelessComponent = props => { const { data, ...rest } = props; return ; @@ -17,10 +27,7 @@ export const CommentContainer: StatelessComponent = props => { const enhanced = withFragmentContainer<{ data: Data }>({ data: graphql` fragment CommentContainer on Comment { - author { - username - } - body + ...CommentContainer_comment @relay(mask: false) } `, })(CommentContainer); diff --git a/src/core/client/stream/containers/ReplyListContainer.spec.tsx b/src/core/client/stream/containers/ReplyListContainer.spec.tsx new file mode 100644 index 000000000..d2fbb7534 --- /dev/null +++ b/src/core/client/stream/containers/ReplyListContainer.spec.tsx @@ -0,0 +1,37 @@ +import { shallow } from "enzyme"; +import { noop } from "lodash"; +import React from "react"; + +import { ReplyListContainer } from "./ReplyListContainer"; + +it("renders correctly", () => { + const props: any = { + comment: { + id: "comment-id", + replies: { + edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }], + }, + }, + relay: { + hasMore: noop, + isLoading: noop, + }, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); + +it("renders correctly when replies are null", () => { + const props: any = { + comment: { + id: "comment-id", + replies: null, + }, + relay: { + hasMore: noop, + isLoading: noop, + }, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/stream/containers/ReplyListContainer.tsx b/src/core/client/stream/containers/ReplyListContainer.tsx new file mode 100644 index 000000000..47f940f54 --- /dev/null +++ b/src/core/client/stream/containers/ReplyListContainer.tsx @@ -0,0 +1,125 @@ +import React from "react"; +import { graphql, RelayPaginationProp } from "react-relay"; + +import { withPaginationContainer } from "talk-framework/lib/relay"; +import { PropTypesOf } from "talk-framework/types"; +import { ReplyListContainer_comment as Data } from "talk-stream/__generated__/ReplyListContainer_comment.graphql"; +import { + COMMENT_SORT, + ReplyListContainerPaginationQueryVariables, +} from "talk-stream/__generated__/ReplyListContainerPaginationQuery.graphql"; + +import ReplyList from "../components/ReplyList"; + +export interface InnerProps { + comment: Data; + relay: RelayPaginationProp; +} + +export class ReplyListContainer extends React.Component { + public render() { + if (this.props.comment.replies === null) { + return null; + } + const comments = this.props.comment.replies.edges.map(edge => edge.node); + return ( + + ); + } + + private loadMore = () => { + if (!this.props.relay.hasMore() || this.props.relay.isLoading()) { + return; + } + + this.props.relay.loadMore( + 999999999, // Fetch All Replies + error => { + if (error) { + // tslint:disable-next-line:no-console + console.error(error); + } + } + ); + }; +} + +// TODO: (cvle) This should be autogenerated. +interface FragmentVariables { + count: number; + cursor?: string; + orderBy: COMMENT_SORT; +} + +const enhanced = withPaginationContainer< + { comment: Data }, + InnerProps, + FragmentVariables, + ReplyListContainerPaginationQueryVariables +>( + { + comment: graphql` + fragment ReplyListContainer_comment on Comment + @argumentDefinitions( + count: { type: "Int!", defaultValue: 5 } + cursor: { type: "Cursor" } + orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC } + ) { + id + replies(first: $count, after: $cursor, orderBy: $orderBy) + @connection(key: "ReplyList_replies") { + edges { + node { + id + ...CommentContainer + } + } + } + } + `, + }, + { + direction: "forward", + getConnectionFromProps(props) { + return props.comment && props.comment.replies; + }, + // 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, + orderBy: fragmentVariables.orderBy, + commentID: props.comment.id, + }; + }, + 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 ReplyListContainerPaginationQuery( + $count: Int! + $cursor: Cursor + $orderBy: COMMENT_SORT! + $commentID: ID! + ) { + comment(id: $commentID) { + ...ReplyListContainer_comment + @arguments(count: $count, cursor: $cursor, orderBy: $orderBy) + } + } + `, + } +)(ReplyListContainer); + +export type ReplyListContainerProps = PropTypesOf; +export default enhanced; diff --git a/src/core/client/stream/containers/StreamContainer.spec.tsx b/src/core/client/stream/containers/StreamContainer.spec.tsx index 9e3bfebce..3ab0875d2 100644 --- a/src/core/client/stream/containers/StreamContainer.spec.tsx +++ b/src/core/client/stream/containers/StreamContainer.spec.tsx @@ -4,7 +4,7 @@ import React from "react"; import { StreamContainer } from "./StreamContainer"; -it("renders username and body", () => { +it("renders correctly", () => { const props: any = { asset: { id: "asset-id", diff --git a/src/core/client/stream/containers/StreamContainer.tsx b/src/core/client/stream/containers/StreamContainer.tsx index c948c5d84..e263337a2 100644 --- a/src/core/client/stream/containers/StreamContainer.tsx +++ b/src/core/client/stream/containers/StreamContainer.tsx @@ -11,7 +11,7 @@ import { import Stream from "../components/Stream"; -export interface InnerProps { +interface InnerProps { asset: Data; relay: RelayPaginationProp; } @@ -23,7 +23,8 @@ export class StreamContainer extends React.Component { : null; return ( +`; + +exports[`renders correctly when replies are null 1`] = `""`; diff --git a/src/core/client/stream/containers/__snapshots__/StreamContainer.spec.tsx.snap b/src/core/client/stream/containers/__snapshots__/StreamContainer.spec.tsx.snap index 5f9e72622..743ef5627 100644 --- a/src/core/client/stream/containers/__snapshots__/StreamContainer.spec.tsx.snap +++ b/src/core/client/stream/containers/__snapshots__/StreamContainer.spec.tsx.snap @@ -1,7 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders username and body 1`] = ` +exports[`renders correctly 1`] = ` diff --git a/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap index 075fd6e54..cbecb8711 100644 --- a/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap @@ -36,47 +36,53 @@ exports[`loads more comments 1`] = ` -
-

+

- Markus -

-

- Joining Too -

+

+ Markus +

+

+ Joining Too +

+
-
-

+

- Lukas -

-

- What's up? -

+

+ Lukas +

+

+ What's up? +

+
-
-

+

- Isabelle -

-

- Hey! -

+

+ Isabelle +

+

+ Hey! +

+
@@ -118,37 +124,41 @@ exports[`renders comment stream 1`] = ` -
-

+

- Markus -

-

- Joining Too -

+

+ Markus +

+

+ Joining Too +

+
-
-

+

- Lukas -

-

- What's up? -

+

+ Lukas +

+

+ What's up? +

+