[next] Implement Comment History Pagination (#2008)

* refactor: profile

* feat: add pagination to comment history

* feat: add getMeSourceID helper

* feat: update profile in CreateCommentMutation

* fix: clear query response cache on mutation

* test: add integration tests for profile

* test: add unit tests
This commit is contained in:
Kiwi
2018-10-19 19:54:40 +02:00
committed by Wyatt Johnson
parent bc4d746291
commit 2e6237b9d9
27 changed files with 1521 additions and 132 deletions
+5 -4
View File
@@ -1,11 +1,12 @@
import { Environment, ROOT_ID } from "relay-runtime";
import { Environment } from "relay-runtime";
import getMeSourceID from "./getMeSourceID";
export default function getMe(environment: Environment) {
const source = environment.getStore().getSource();
const root = source.get(ROOT_ID)!;
if (!root.me) {
const meID = getMeSourceID(environment);
if (!meID) {
return null;
}
const meID = root.me.__ref;
return source.get(meID)!;
}
@@ -0,0 +1,10 @@
import { Environment, ROOT_ID } from "relay-runtime";
export default function getMeSourceID(environment: Environment): string | null {
const source = environment.getStore().getSource();
const root = source.get(ROOT_ID)!;
if (!root.me) {
return null;
}
return root.me.__ref;
}
@@ -1,3 +1,4 @@
export { default as getMe } from "./getMe";
export { default as getMeSourceID } from "./getMeSourceID";
export { default as getURLWithCommentID } from "./getURLWithCommentID";
export { default as urls } from "./urls";
@@ -19,6 +19,7 @@ export default function createNetwork(tokenGetter: TokenGetter) {
cacheMiddleware({
size: 100, // max 100 requests
ttl: 900000, // 15 minutes
clearOnMutation: true,
}),
urlMiddleware({
url: req => Promise.resolve(graphqlURL),
@@ -28,7 +28,10 @@ export function denormalizeComments(commentList: any[]) {
export function denormalizeAsset(asset: any) {
const commentNodes =
(asset.comments &&
asset.comments.edges.map((edge: any) => denormalizeComment(edge))) ||
asset.comments.edges.map((edge: any) => ({
...edge,
node: denormalizeComment(edge.node),
}))) ||
[];
const commentsPageInfo = (asset.comments && asset.comments.pageInfo) || {
endCursor: null,
@@ -5,7 +5,7 @@ import {
RecordSourceSelectorProxy,
} from "relay-runtime";
import { getMe } from "talk-framework/helpers";
import { getMe, getMeSourceID } from "talk-framework/helpers";
import { TalkContext } from "talk-framework/lib/bootstrap";
import {
commitMutationPromiseNormalized,
@@ -21,6 +21,7 @@ export type CreateCommentInput = Omit<
> & { local?: boolean };
function sharedUpdater(
environment: Environment,
store: RecordSourceSelectorProxy,
input: CreateCommentInput
) {
@@ -30,7 +31,7 @@ function sharedUpdater(
} else {
update(store, input);
}
updateProfile(store, input);
updateProfile(environment, store, input);
}
function updateAsset(
@@ -120,6 +121,7 @@ function localUpdate(
* updateProfile integrates new comment into the profile.
*/
function updateProfile(
environment: Environment,
store: RecordSourceSelectorProxy,
input: CreateCommentInput
) {
@@ -128,12 +130,17 @@ function updateProfile(
// Get the edge of the newly created comment.
const newEdge = payload.getLinkedRecord("edge")!;
const newComment = newEdge.getLinkedRecord("node");
// TODO: update profile comments connection after we
// integrated pagination.
// tslint:disable-next-line:no-unused-expression
newComment;
const meProxy = store.get(getMeSourceID(environment)!);
const con = ConnectionHandler.getConnection(
meProxy,
"CommentHistory_comments"
);
// Note: Currently this is always null, until Relay comes
// with better data retaintion and data from store support.
if (con) {
ConnectionHandler.insertEdgeBefore(con, newEdge);
}
}
const mutation = graphql`
@@ -196,11 +203,11 @@ function commit(
},
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
optimisticUpdater: store => {
sharedUpdater(store, input);
sharedUpdater(environment, store, input);
store.get(id)!.setValue(true, "pending");
},
updater: store => {
sharedUpdater(store, input);
sharedUpdater(environment, store, input);
},
});
}
@@ -0,0 +1,47 @@
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 CommentHistory from "./CommentHistory";
const CommentHistoryN = removeFragmentRefs(CommentHistory);
it("renders correctly", () => {
const props: PropTypesOf<typeof CommentHistoryN> = {
asset: {},
comments: [{ id: "comment-1" }, { id: "comment-2" }],
onLoadMore: noop,
hasMore: false,
disableLoadMore: false,
};
const wrapper = shallow(<CommentHistoryN {...props} />);
expect(wrapper).toMatchSnapshot();
});
describe("has more", () => {
it("renders correctly", () => {
const props: PropTypesOf<typeof CommentHistoryN> = {
asset: {},
comments: [{ id: "comment-1" }, { id: "comment-2" }],
onLoadMore: noop,
hasMore: true,
disableLoadMore: false,
};
const wrapper = shallow(<CommentHistoryN {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("disables load more", () => {
const props: PropTypesOf<typeof CommentHistoryN> = {
asset: {},
comments: [{ id: "comment-1" }, { id: "comment-2" }],
onLoadMore: noop,
hasMore: true,
disableLoadMore: true,
};
const wrapper = shallow(<CommentHistoryN {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
@@ -0,0 +1,50 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { Button, HorizontalGutter, Typography } from "talk-ui/components";
import HistoryCommentContainer from "../containers/HistoryCommentContainer";
interface CommentHistoryProps {
asset: PropTypesOf<typeof HistoryCommentContainer>["asset"];
comments: Array<
{ id: string } & PropTypesOf<typeof HistoryCommentContainer>["comment"]
>;
onLoadMore?: () => void;
hasMore?: boolean;
disableLoadMore?: boolean;
}
const CommentHistory: StatelessComponent<CommentHistoryProps> = props => {
return (
<HorizontalGutter size="double">
<Localized id="profile-historyComment-commentHistory">
<Typography variant="heading3">Comment History</Typography>
</Localized>
{props.comments.map(comment => (
<HistoryCommentContainer
key={comment.id}
asset={props.asset}
comment={comment}
/>
))}
{props.hasMore && (
<Localized id="profile-commentHistory-loadMore">
<Button
id={"talk-profile-commentHistory-loadMore"}
onClick={props.onLoadMore}
variant="outlined"
fullWidth
disabled={props.disableLoadMore}
aria-controls="talk-profile-commentHistory-log"
>
Load More
</Button>
</Localized>
)}
</HorizontalGutter>
);
};
export default CommentHistory;
@@ -1,36 +0,0 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { HorizontalGutter, Typography } from "talk-ui/components";
import HistoryComment from "./HistoryComment";
interface Comment {
id: string;
body: string | null;
createdAt: any;
replyCount: number | null;
asset: {
title: string | null;
};
conversationURL: string;
onGotoConversation: (e: React.MouseEvent) => void;
}
interface CommentsHistoryProps {
comments: Comment[];
}
const CommentsHistory: StatelessComponent<CommentsHistoryProps> = props => {
return (
<HorizontalGutter size="double">
<Localized id="profile-historyComment-commentHistory">
<Typography variant="heading3">Comment History</Typography>
</Localized>
{props.comments.map(comment => (
<HistoryComment key={comment.id} {...comment} />
))}
</HorizontalGutter>
);
};
export default CommentsHistory;
@@ -0,0 +1,25 @@
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 HistoryComment from "./HistoryComment";
const HistoryCommentN = removeFragmentRefs(HistoryComment);
it("renders correctly", () => {
const props: PropTypesOf<typeof HistoryCommentN> = {
body: "Hello World",
createdAt: "2018-07-06T18:24:00.000Z",
replyCount: 4,
asset: {
title: "Asset Title",
},
conversationURL: "http://localhost/conversation",
onGotoConversation: noop,
};
const wrapper = shallow(<HistoryCommentN {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,18 @@
import { shallow } from "enzyme";
import React from "react";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import { PropTypesOf } from "talk-framework/types";
import Profile from "./Profile";
const ProfileN = removeFragmentRefs(Profile);
it("renders correctly", () => {
const props: PropTypesOf<typeof ProfileN> = {
asset: {},
me: {},
};
const wrapper = shallow(<ProfileN {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -3,21 +3,19 @@ import { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import UserBoxContainer from "talk-stream/containers/UserBoxContainer";
import { HorizontalGutter } from "talk-ui/components";
import CommentsHistoryContainer from "../containers/CommentsHistoryContainer";
import CommentHistoryContainer from "../containers/CommentHistoryContainer";
export interface ProfileProps {
asset: PropTypesOf<typeof CommentsHistoryContainer>["asset"];
asset: PropTypesOf<typeof CommentHistoryContainer>["asset"];
me: PropTypesOf<typeof UserBoxContainer>["me"] &
PropTypesOf<typeof CommentsHistoryContainer>["me"];
PropTypesOf<typeof CommentHistoryContainer>["me"];
}
const Profile: StatelessComponent<ProfileProps> = props => {
return (
<HorizontalGutter size="double">
<UserBoxContainer me={props.me} />
{props.me && (
<CommentsHistoryContainer me={props.me} asset={props.asset} />
)}
<CommentHistoryContainer me={props.me} asset={props.asset} />
</HorizontalGutter>
);
};
@@ -0,0 +1,131 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`has more disables load more 1`] = `
<withPropsOnChange(HorizontalGutter)
size="double"
>
<Localized
id="profile-historyComment-commentHistory"
>
<withPropsOnChange(Typography)
variant="heading3"
>
Comment History
</withPropsOnChange(Typography)>
</Localized>
<withContext(createMutationContainer(Relay(HistoryCommentContainer)))
asset={Object {}}
comment={
Object {
"id": "comment-1",
}
}
key="comment-1"
/>
<withContext(createMutationContainer(Relay(HistoryCommentContainer)))
asset={Object {}}
comment={
Object {
"id": "comment-2",
}
}
key="comment-2"
/>
<Localized
id="profile-commentHistory-loadMore"
>
<withPropsOnChange(Button)
aria-controls="talk-profile-commentHistory-log"
disabled={true}
fullWidth={true}
id="talk-profile-commentHistory-loadMore"
onClick={[Function]}
variant="outlined"
>
Load More
</withPropsOnChange(Button)>
</Localized>
</withPropsOnChange(HorizontalGutter)>
`;
exports[`has more renders correctly 1`] = `
<withPropsOnChange(HorizontalGutter)
size="double"
>
<Localized
id="profile-historyComment-commentHistory"
>
<withPropsOnChange(Typography)
variant="heading3"
>
Comment History
</withPropsOnChange(Typography)>
</Localized>
<withContext(createMutationContainer(Relay(HistoryCommentContainer)))
asset={Object {}}
comment={
Object {
"id": "comment-1",
}
}
key="comment-1"
/>
<withContext(createMutationContainer(Relay(HistoryCommentContainer)))
asset={Object {}}
comment={
Object {
"id": "comment-2",
}
}
key="comment-2"
/>
<Localized
id="profile-commentHistory-loadMore"
>
<withPropsOnChange(Button)
aria-controls="talk-profile-commentHistory-log"
disabled={false}
fullWidth={true}
id="talk-profile-commentHistory-loadMore"
onClick={[Function]}
variant="outlined"
>
Load More
</withPropsOnChange(Button)>
</Localized>
</withPropsOnChange(HorizontalGutter)>
`;
exports[`renders correctly 1`] = `
<withPropsOnChange(HorizontalGutter)
size="double"
>
<Localized
id="profile-historyComment-commentHistory"
>
<withPropsOnChange(Typography)
variant="heading3"
>
Comment History
</withPropsOnChange(Typography)>
</Localized>
<withContext(createMutationContainer(Relay(HistoryCommentContainer)))
asset={Object {}}
comment={
Object {
"id": "comment-1",
}
}
key="comment-1"
/>
<withContext(createMutationContainer(Relay(HistoryCommentContainer)))
asset={Object {}}
comment={
Object {
"id": "comment-2",
}
}
key="comment-2"
/>
</withPropsOnChange(HorizontalGutter)>
`;
@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(HorizontalGutter)>
<Localized
$title="Asset Title"
id="profile-historyComment-story"
>
<withPropsOnChange(Typography)
variant="heading4"
>
Story: {$title}
</withPropsOnChange(Typography)>
</Localized>
<Timestamp>
2018-07-06T18:24:00.000Z
</Timestamp>
<withPropsOnChange(Typography)
container="div"
variant="bodyCopy"
>
<HTMLContent>
Hello World
</HTMLContent>
</withPropsOnChange(Typography)>
<withPropsOnChange(Flex)
alignItems="center"
direction="row"
itemGutter={true}
>
<div
className="HistoryComment-replies"
>
<withPropsOnChange(Icon)
className="HistoryComment-icon"
>
reply
</withPropsOnChange(Icon)>
<Localized
$replyCount={4}
id="profile-historyComment-replies"
>
<span>
Replies {$replyCount}
</span>
</Localized>
</div>
<withPropsOnChange(Button)
anchor={true}
href="http://localhost/conversation"
onClick={[Function]}
target="_parent"
variant="underlined"
>
<withPropsOnChange(Icon)>
launch
</withPropsOnChange(Icon)>
<Localized
id="profile-historyComment-viewConversation"
>
<span>
View Conversation
</span>
</Localized>
</withPropsOnChange(Button)>
</withPropsOnChange(Flex)>
<MatchMediaWithContext
lteWidth="xs"
>
<hr
className="HistoryComment-divider"
/>
</MatchMediaWithContext>
</withPropsOnChange(HorizontalGutter)>
`;
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(HorizontalGutter)
size="double"
>
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
me={Object {}}
/>
<Relay(CommentHistoryContainer)
asset={Object {}}
me={Object {}}
/>
</withPropsOnChange(HorizontalGutter)>
`;
@@ -0,0 +1,124 @@
import React from "react";
import { graphql, RelayPaginationProp } from "react-relay";
import { withPaginationContainer } from "talk-framework/lib/relay";
import { CommentHistoryContainer_asset as AssetData } from "talk-stream/__generated__/CommentHistoryContainer_asset.graphql";
import { CommentHistoryContainer_me as MeData } from "talk-stream/__generated__/CommentHistoryContainer_me.graphql";
import { CommentHistoryContainerPaginationQueryVariables } from "talk-stream/__generated__/CommentHistoryContainerPaginationQuery.graphql";
import CommentHistory from "../components/CommentHistory";
interface CommentHistoryContainerProps {
me: MeData;
asset: AssetData;
relay: RelayPaginationProp;
}
export class CommentHistoryContainer extends React.Component<
CommentHistoryContainerProps
> {
public state = {
disableLoadMore: false,
};
public render() {
const comments = this.props.me.comments.edges.map(edge => edge.node);
return (
<CommentHistory
asset={this.props.asset}
comments={comments}
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<
CommentHistoryContainerProps,
CommentHistoryContainerPaginationQueryVariables,
FragmentVariables
>(
{
asset: graphql`
fragment CommentHistoryContainer_asset on Asset {
...HistoryCommentContainer_asset
}
`,
me: graphql`
fragment CommentHistoryContainer_me on User
@argumentDefinitions(
count: { type: "Int!", defaultValue: 5 }
cursor: { type: "Cursor" }
) {
comments(first: $count, after: $cursor)
@connection(key: "CommentHistory_comments") {
edges {
node {
id
...HistoryCommentContainer_comment
}
}
}
}
`,
},
{
direction: "forward",
getConnectionFromProps(props) {
return props.me && props.me.comments;
},
// 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 CommentHistoryContainerPaginationQuery(
$count: Int!
$cursor: Cursor
) {
me {
...CommentHistoryContainer_me
@arguments(count: $count, cursor: $cursor)
}
}
`,
}
)(CommentHistoryContainer);
export default enhanced;
@@ -1,70 +0,0 @@
import React from "react";
import { graphql } from "react-relay";
import { getURLWithCommentID } from "talk-framework/helpers";
import { withFragmentContainer } from "talk-framework/lib/relay";
import { CommentsHistoryContainer_asset as AssetData } from "talk-stream/__generated__/CommentsHistoryContainer_asset.graphql";
import { CommentsHistoryContainer_me as CommentsData } from "talk-stream/__generated__/CommentsHistoryContainer_me.graphql";
import {
SetCommentIDMutation,
withSetCommentIDMutation,
} from "talk-stream/mutations";
import CommentsHistory from "../components/CommentsHistory";
interface CommentsHistoryContainerProps {
setCommentID: SetCommentIDMutation;
me: CommentsData;
asset: AssetData;
}
export class CommentsHistoryContainer extends React.Component<
CommentsHistoryContainerProps
> {
private onGoToConversation = (id: string) => {
this.props.setCommentID({ id });
};
public render() {
const comments = this.props.me.comments.edges.map(edge => ({
...edge.node,
conversationURL: getURLWithCommentID(edge.node.asset.url, edge.node.id),
onGotoConversation: (e: React.MouseEvent) => {
if (this.props.asset.id === edge.node.asset.id) {
this.onGoToConversation(edge.node.id);
e.preventDefault();
}
},
}));
return <CommentsHistory comments={comments} />;
}
}
const enhanced = withSetCommentIDMutation(
withFragmentContainer<CommentsHistoryContainerProps>({
asset: graphql`
fragment CommentsHistoryContainer_asset on Asset {
id
}
`,
me: graphql`
fragment CommentsHistoryContainer_me on User {
comments {
edges {
node {
id
body
createdAt
replyCount
asset {
id
title
url
}
}
}
}
}
`,
})(CommentsHistoryContainer)
);
export default enhanced;
@@ -0,0 +1,66 @@
import React from "react";
import { graphql } from "react-relay";
import { getURLWithCommentID } from "talk-framework/helpers";
import { withFragmentContainer } from "talk-framework/lib/relay";
import { HistoryCommentContainer_asset as AssetData } from "talk-stream/__generated__/HistoryCommentContainer_asset.graphql";
import { HistoryCommentContainer_comment as CommentData } from "talk-stream/__generated__/HistoryCommentContainer_comment.graphql";
import {
SetCommentIDMutation,
withSetCommentIDMutation,
} from "talk-stream/mutations";
import HistoryComment from "../components/HistoryComment";
interface HistoryCommentContainerProps {
setCommentID: SetCommentIDMutation;
asset: AssetData;
comment: CommentData;
}
export class HistoryCommentContainer extends React.Component<
HistoryCommentContainerProps
> {
private handleGoToConversation = (e: React.MouseEvent) => {
if (this.props.asset.id === this.props.comment.asset.id) {
this.props.setCommentID({ id: this.props.comment.id });
e.preventDefault();
}
};
public render() {
return (
<HistoryComment
{...this.props.comment}
conversationURL={getURLWithCommentID(
this.props.comment.asset.url,
this.props.comment.id
)}
onGotoConversation={this.handleGoToConversation}
/>
);
}
}
const enhanced = withSetCommentIDMutation(
withFragmentContainer<HistoryCommentContainerProps>({
asset: graphql`
fragment HistoryCommentContainer_asset on Asset {
id
}
`,
comment: graphql`
fragment HistoryCommentContainer_comment on Comment {
id
body
createdAt
replyCount
asset {
id
title
url
}
}
`,
})(HistoryCommentContainer)
);
export default enhanced;
@@ -20,13 +20,13 @@ export class StreamContainer extends React.Component<ProfileContainerProps> {
const enhanced = withFragmentContainer<ProfileContainerProps>({
asset: graphql`
fragment ProfileContainer_asset on Asset {
...CommentsHistoryContainer_asset
...CommentHistoryContainer_asset
}
`,
me: graphql`
fragment ProfileContainer_me on User {
...UserBoxContainer_me
...CommentsHistoryContainer_me
...CommentHistoryContainer_me
}
`,
})(StreamContainer);
@@ -64,10 +64,6 @@ const ProfileQuery: StatelessComponent<InnerProps> = ({
assetID,
assetURL,
}}
cacheConfig={{
// TODO: enable caching after mutations are adapted.
force: true,
}}
render={render}
/>
);
+34
View File
@@ -242,6 +242,20 @@ export const assets = denormalizeAssets([
},
},
},
{
...baseAsset,
id: "asset-2",
url: "http://localhost/assets/asset-2",
comments: {
edges: [
{ node: comments[2], cursor: comments[2].createdAt },
{ node: comments[3], cursor: comments[3].createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
},
]);
export const assetWithReplies = denormalizeAsset({
@@ -293,3 +307,23 @@ export const assetWithDeepestReplies = denormalizeAsset({
},
},
});
export const meWithComments = {
id: "me-with-comments",
username: "Markus",
comments: {
edges: [
{
node: { ...assets[0].comments.edges[0].node, asset: assets[0] },
cursor: comments[0].createdAt,
},
{
node: { ...assets[1].comments.edges[0].node, asset: assets[1] },
cursor: comments[1].createdAt,
},
],
pageInfo: {
hasNextPage: false,
},
},
};
@@ -0,0 +1,522 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loads more comments 1`] = `
<div
className="HorizontalGutter-root App-root HorizontalGutter-full"
>
<ul
className="TabBar-root TabBar-primary"
role="tablist"
>
<li
className="Tab-root"
id="tab-COMMENTS"
role="presentation"
>
<button
aria-controls="tabPane-COMMENTS"
aria-selected={false}
className="BaseButton-root Tab-button Tab-primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
2 Comments
</button>
</li>
<li
className="Tab-root"
id="tab-PROFILE"
role="presentation"
>
<button
aria-controls="tabPane-PROFILE"
aria-selected={true}
className="BaseButton-root Tab-button Tab-primary Tab-active"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
My Profile
</button>
</li>
</ul>
<section
aria-labelledby="tab-PROFILE"
className="App-tabContent"
id="tabPane-PROFILE"
role="tabpanel"
>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<div
className="Flex-root"
>
<div
className="Flex-flex Flex-halfItemGutter Flex-wrap"
>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Signed in as
<span
className="Typography-root Typography-bodyCopyBold Typography-colorTextPrimary"
>
Markus
</span>
.
</div>
<div
className="Flex-root Typography-root Typography-bodyCopy Typography-colorTextPrimary Flex-flex"
>
<span>
Not you? 
</span>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign Out
</button>
</div>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<h1
className="Typography-root Typography-heading3 Typography-colorTextPrimary"
>
Comment History
</h1>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Joining Too",
}
}
/>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionRow"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantUnderlined"
href="http://localhost/assets/asset-1?commentID=comment-0"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
launch
</span>
<span>
View Conversation
</span>
</a>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "What's up?",
}
}
/>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionRow"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantUnderlined"
href="http://localhost/assets/asset-1?commentID=comment-1"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
launch
</span>
<span>
View Conversation
</span>
</a>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Hey!",
}
}
/>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionRow"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantUnderlined"
href="http://localhost/assets/asset-1?commentID=comment-2"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
launch
</span>
<span>
View Conversation
</span>
</a>
</div>
</div>
</div>
</div>
</section>
</div>
`;
exports[`renders comment stream 1`] = `
<div
className="HorizontalGutter-root App-root HorizontalGutter-full"
>
<ul
className="TabBar-root TabBar-primary"
role="tablist"
>
<li
className="Tab-root"
id="tab-COMMENTS"
role="presentation"
>
<button
aria-controls="tabPane-COMMENTS"
aria-selected={false}
className="BaseButton-root Tab-button Tab-primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
2 Comments
</button>
</li>
<li
className="Tab-root"
id="tab-PROFILE"
role="presentation"
>
<button
aria-controls="tabPane-PROFILE"
aria-selected={true}
className="BaseButton-root Tab-button Tab-primary Tab-active"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
My Profile
</button>
</li>
</ul>
<section
aria-labelledby="tab-PROFILE"
className="App-tabContent"
id="tabPane-PROFILE"
role="tabpanel"
>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<div
className="Flex-root"
>
<div
className="Flex-flex Flex-halfItemGutter Flex-wrap"
>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Signed in as
<span
className="Typography-root Typography-bodyCopyBold Typography-colorTextPrimary"
>
Markus
</span>
.
</div>
<div
className="Flex-root Typography-root Typography-bodyCopy Typography-colorTextPrimary Flex-flex"
>
<span>
Not you? 
</span>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign Out
</button>
</div>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<h1
className="Typography-root Typography-heading3 Typography-colorTextPrimary"
>
Comment History
</h1>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Joining Too",
}
}
/>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionRow"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantUnderlined"
href="http://localhost/assets/asset-1?commentID=comment-0"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
launch
</span>
<span>
View Conversation
</span>
</a>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "What's up?",
}
}
/>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionRow"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantUnderlined"
href="http://localhost/assets/asset-1?commentID=comment-1"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
launch
</span>
<span>
View Conversation
</span>
</a>
</div>
</div>
<button
aria-controls="talk-profile-commentHistory-log"
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantOutlined Button-fullWidth"
disabled={false}
id="talk-profile-commentHistory-loadMore"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Load More
</button>
</div>
</div>
</section>
</div>
`;
@@ -0,0 +1,226 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders profile 1`] = `
<div
className="HorizontalGutter-root App-root HorizontalGutter-full"
>
<ul
className="TabBar-root TabBar-primary"
role="tablist"
>
<li
className="Tab-root"
id="tab-COMMENTS"
role="presentation"
>
<button
aria-controls="tabPane-COMMENTS"
aria-selected={false}
className="BaseButton-root Tab-button Tab-primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
2 Comments
</button>
</li>
<li
className="Tab-root"
id="tab-PROFILE"
role="presentation"
>
<button
aria-controls="tabPane-PROFILE"
aria-selected={true}
className="BaseButton-root Tab-button Tab-primary Tab-active"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
My Profile
</button>
</li>
</ul>
<section
aria-labelledby="tab-PROFILE"
className="App-tabContent"
id="tabPane-PROFILE"
role="tabpanel"
>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<div
className="Flex-root"
>
<div
className="Flex-flex Flex-halfItemGutter Flex-wrap"
>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Signed in as
<span
className="Typography-root Typography-bodyCopyBold Typography-colorTextPrimary"
>
Markus
</span>
.
</div>
<div
className="Flex-root Typography-root Typography-bodyCopy Typography-colorTextPrimary Flex-flex"
>
<span>
Not you? 
</span>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign Out
</button>
</div>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<h1
className="Typography-root Typography-heading3 Typography-colorTextPrimary"
>
Comment History
</h1>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Joining Too",
}
}
/>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionRow"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantUnderlined"
href="http://localhost/assets/asset-1?commentID=comment-0"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
launch
</span>
<span>
View Conversation
</span>
</a>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Hey!",
}
}
/>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionRow"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantUnderlined"
href="http://localhost/assets/asset-2?commentID=comment-2"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
launch
</span>
<span>
View Conversation
</span>
</a>
</div>
</div>
</div>
</div>
</section>
</div>
`;
@@ -0,0 +1,13 @@
import createTopLevel, { CreateParams } from "../create";
export default function create(params: CreateParams) {
return createTopLevel({
...params,
initLocalState: (localRecord, source, environment) => {
if (params.initLocalState) {
localRecord.setValue("PROFILE", "activeTab");
params.initLocalState(localRecord, source, environment);
}
},
});
}
@@ -0,0 +1,92 @@
import { ReactTestRenderer } from "react-test-renderer";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { createSinonStub } from "talk-framework/testHelpers";
import { assets, comments, meWithComments } from "../fixtures";
import create from "./create";
let testRenderer: ReactTestRenderer;
beforeEach(() => {
const meStub = {
...meWithComments,
comments: createSinonStub(
s => s.throws(),
s =>
s.withArgs({ first: 5, orderBy: "CREATED_AT_DESC" }).returns({
edges: [
{
node: { ...comments[0], asset: assets[0] },
cursor: comments[0].createdAt,
},
{
node: { ...comments[1], asset: assets[0] },
cursor: comments[1].createdAt,
},
],
pageInfo: {
endCursor: comments[1].createdAt,
hasNextPage: true,
},
}),
s =>
s
.withArgs({
first: 10,
orderBy: "CREATED_AT_DESC",
after: comments[1].createdAt,
})
.returns({
edges: [
{
node: { ...comments[2], asset: assets[0] },
cursor: comments[2].createdAt,
},
],
pageInfo: {
endCursor: comments[2].createdAt,
hasNextPage: false,
},
})
),
};
const resolvers = {
Query: {
asset: createSinonStub(
s => s.throws(),
s =>
s
.withArgs(undefined, { id: assets[0].id, url: null })
.returns(assets[0])
),
me: sinon.stub().returns(meStub),
},
};
({ testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(assets[0].id, "assetID");
},
}));
});
it("renders comment stream", async () => {
// Wait for loading.
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("loads more comments", async () => {
testRenderer.root
.findByProps({ id: "talk-profile-commentHistory-loadMore" })
.props.onClick();
// Wait for loading.
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,39 @@
import { ReactTestRenderer } from "react-test-renderer";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { createSinonStub } from "talk-framework/testHelpers";
import { assets, meWithComments } from "../fixtures";
import create from "./create";
let testRenderer: ReactTestRenderer;
beforeEach(() => {
const resolvers = {
Query: {
asset: createSinonStub(
s => s.throws(),
s =>
s
.withArgs(undefined, { id: assets[0].id, url: null })
.returns(assets[0])
),
me: sinon.stub().returns(meWithComments),
},
};
({ testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(assets[0].id, "assetID");
},
}));
});
it("renders profile", async () => {
// Wait for loading.
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
+1
View File
@@ -92,3 +92,4 @@ profile-historyComment-commentHistory = Comment History
profile-historyComment-story = Story: {$title}
profile-profileQuery-errorLoadingProfile = Error loading profile
profile-profileQuery-assetNotFound = Asset not found
profile-commentHistory-loadMore = Load More