[CORL-130] Handle non-visible comment status (#2213)

* feat: support blue color in message

* feat: Show message when comment is in review

* feat: handle non-visible comment status

* test: add feature test for handling invisible comment status

* fix: apply review

* chore: better comment :-)

* Update isVisible.ts
This commit is contained in:
Kiwi
2019-03-05 19:15:33 +01:00
committed by GitHub
parent 4e043638f6
commit 37959f9398
60 changed files with 874 additions and 318 deletions
@@ -4,16 +4,14 @@ import { ConnectionHandler, Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { AcceptCommentMutation as MutationTypes } from "talk-admin/__generated__/AcceptCommentMutation.graphql";
import { getQueueConnection } from "talk-admin/helpers";
export type AcceptCommentInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
export type AcceptCommentInput = MutationInput<MutationTypes>;
const mutation = graphql`
mutation AcceptCommentMutation($input: AcceptCommentInput!) {
@@ -79,4 +77,4 @@ export const withAcceptCommentMutation = createMutationContainer(
export type AcceptCommentMutation = (
input: AcceptCommentInput
) => Promise<MutationTypes["response"]["acceptComment"]>;
) => MutationResponsePromise<MutationTypes, "acceptComment">;
@@ -4,6 +4,7 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { RegenerateSSOKeyMutation as MutationTypes } from "talk-admin/__generated__/RegenerateSSOKeyMutation.graphql";
@@ -44,6 +45,7 @@ export const withRegenerateSSOKeyMutation = createMutationContainer(
commit
);
export type RegenerateSSOKeyMutation = () => Promise<
MutationTypes["response"]["regenerateSSOKey"]
export type RegenerateSSOKeyMutation = () => MutationResponsePromise<
MutationTypes,
"regenerateSSOKey"
>;
@@ -4,16 +4,14 @@ import { ConnectionHandler, Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { RejectCommentMutation as MutationTypes } from "talk-admin/__generated__/RejectCommentMutation.graphql";
import { getQueueConnection } from "talk-admin/helpers";
export type RejectCommentInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
export type RejectCommentInput = MutationInput<MutationTypes>;
const mutation = graphql`
mutation RejectCommentMutation($input: RejectCommentInput!) {
@@ -78,4 +76,4 @@ export const withRejectCommentMutation = createMutationContainer(
export type RejectCommentMutation = (
input: RejectCommentInput
) => Promise<MutationTypes["response"]["rejectComment"]>;
) => MutationResponsePromise<MutationTypes, "rejectComment">;
@@ -4,15 +4,13 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { SetEmailMutation as MutationTypes } from "talk-admin/__generated__/SetEmailMutation.graphql";
export type SetEmailInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
export type SetEmailInput = MutationInput<MutationTypes>;
const mutation = graphql`
mutation SetEmailMutation($input: SetEmailInput!) {
@@ -43,4 +41,4 @@ export const withSetEmailMutation = createMutationContainer("setEmail", commit);
export type SetEmailMutation = (
input: SetEmailInput
) => Promise<MutationTypes["response"]["setEmail"]>;
) => MutationResponsePromise<MutationTypes, "setEmail">;
@@ -4,15 +4,13 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { SetPasswordMutation as MutationTypes } from "talk-admin/__generated__/SetPasswordMutation.graphql";
export type SetPasswordInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
export type SetPasswordInput = MutationInput<MutationTypes>;
const mutation = graphql`
mutation SetPasswordMutation($input: SetPasswordInput!) {
@@ -48,4 +46,4 @@ export const withSetPasswordMutation = createMutationContainer(
export type SetPasswordMutation = (
input: SetPasswordInput
) => Promise<MutationTypes["response"]["setPassword"]>;
) => MutationResponsePromise<MutationTypes, "setPassword">;
@@ -4,15 +4,13 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { SetUsernameMutation as MutationTypes } from "talk-admin/__generated__/SetUsernameMutation.graphql";
export type SetUsernameInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
export type SetUsernameInput = MutationInput<MutationTypes>;
const mutation = graphql`
mutation SetUsernameMutation($input: SetUsernameInput!) {
@@ -46,4 +44,4 @@ export const withSetUsernameMutation = createMutationContainer(
export type SetUsernameMutation = (
input: SetUsernameInput
) => Promise<MutationTypes["response"]["setUsername"]>;
) => MutationResponsePromise<MutationTypes, "setUsername">;
@@ -4,15 +4,13 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { UpdateSettingsMutation as MutationTypes } from "talk-admin/__generated__/UpdateSettingsMutation.graphql";
export type UpdateSettingsInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
export type UpdateSettingsInput = MutationInput<MutationTypes>;
const mutation = graphql`
mutation UpdateSettingsMutation($input: UpdateSettingsInput!) {
@@ -53,4 +51,4 @@ export const withUpdateSettingsMutation = createMutationContainer(
export type UpdateSettingsMutation = (
input: UpdateSettingsInput
) => Promise<MutationTypes["response"]["updateSettings"]>;
) => MutationResponsePromise<MutationTypes, "updateSettings">;
@@ -6,9 +6,24 @@ import {
wrapDisplayName,
} from "recompose";
import { Environment } from "relay-runtime";
import { Omit } from "talk-framework/types";
import { TalkContext, withContext } from "../bootstrap";
export type MutationInput<
T extends { variables: { input: { clientMutationId: string } } }
> = Omit<T["variables"]["input"], "clientMutationId">;
export type MutationResponse<
T extends { response: { [P in U]: any } },
U extends string
> = Exclude<T["response"][U], null>;
export type MutationResponsePromise<
T extends { response: { [P in U]: any } },
U extends string
> = Promise<MutationResponse<T, U>>;
/**
* createMutationContainer creates a HOC that
* injects a property with the name specified in `propName`
+6 -1
View File
@@ -5,7 +5,12 @@ export { default as withLocalStateContainer } from "./withLocalStateContainer";
export * from "./withLocalStateContainer";
export { default as QueryRenderer } from "./QueryRenderer";
export * from "./QueryRenderer";
export { default as createMutationContainer } from "./createMutationContainer";
export {
default as createMutationContainer,
MutationInput,
MutationResponse,
MutationResponsePromise,
} from "./createMutationContainer";
export { default as createFetchContainer } from "./createFetchContainer";
export { default as createAndRetain } from "./createAndRetain";
export { default as wrapFetchWithLogger } from "./wrapFetchWithLogger";
+4
View File
@@ -4,3 +4,7 @@ export {
export {
default as prependCommentEdgeToProfile,
} from "./prependCommentEdgeToProfile";
export { default as isVisible } from "./isVisible";
export { default as isInReview } from "./isInReview";
export { default as isRejected } from "./isRejected";
export { default as isRolePriviledged } from "./isRolePriviledged";
@@ -0,0 +1,5 @@
// TODO: use generated schema types.
const inReviewStatuses = ["PREMOD", "SYSTEM_WITHHELD"];
export default function isInReview(status: any) {
return inReviewStatuses.includes(status);
}
@@ -0,0 +1,4 @@
// TODO: use generated schema types.
export default function isRejected(status: any) {
return status === "REJECTED";
}
@@ -0,0 +1,5 @@
// TODO: use generated schema types.
const priviledgedRoles = ["ADMIN", "MODERATOR"];
export default function isRolePriviledged(role: any) {
return priviledgedRoles.includes(role);
}
@@ -0,0 +1,6 @@
// TODO: use generated schema types.
const visibleStatuses = ["NONE", "ACCEPTED"];
export default function isVisible(status: any) {
return visibleStatuses.includes(status);
}
@@ -5,15 +5,13 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { CreateCommentDontAgreeMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentDontAgreeMutation.graphql";
export type CreateCommentDontAgreeInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
export type CreateCommentDontAgreeInput = MutationInput<MutationTypes>;
const mutation = graphql`
mutation CreateCommentDontAgreeMutation(
@@ -52,4 +50,4 @@ export const withCreateCommentDontAgreeMutation = createMutationContainer(
export type CreateCommentDontAgreeMutation = (
input: CreateCommentDontAgreeInput
) => Promise<MutationTypes["response"]["createCommentDontAgree"]>;
) => MutationResponsePromise<MutationTypes, "createCommentDontAgree">;
@@ -5,15 +5,13 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { CreateCommentFlagMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentFlagMutation.graphql";
export type CreateCommentFlagInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
export type CreateCommentFlagInput = MutationInput<MutationTypes>;
const mutation = graphql`
mutation CreateCommentFlagMutation($input: CreateCommentFlagInput!) {
@@ -55,4 +53,4 @@ export const withCreateCommentFlagMutation = createMutationContainer(
export type CreateCommentFlagMutation = (
input: CreateCommentFlagInput
) => Promise<MutationTypes["response"]["createCommentFlag"]>;
) => MutationResponsePromise<MutationTypes, "createCommentFlag">;
@@ -2,6 +2,7 @@ import { graphql } from "react-relay";
import {
ConnectionHandler,
Environment,
RecordProxy,
RecordSourceSelectorProxy,
} from "relay-runtime";
@@ -10,32 +11,38 @@ import { TalkContext } from "talk-framework/lib/bootstrap";
import {
commitMutationPromiseNormalized,
createMutationContainer,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { CreateCommentMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentMutation.graphql";
import {
incrementStoryCommentCounts,
isRolePriviledged,
isVisible,
prependCommentEdgeToProfile,
} from "../helpers";
export type CreateCommentInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
export type CreateCommentInput = MutationInput<MutationTypes>;
function sharedUpdater(
environment: Environment,
store: RecordSourceSelectorProxy,
input: CreateCommentInput
) {
const commentEdge = store
.getRootField("createComment")!
.getLinkedRecord("edge")!;
const status = commentEdge.getLinkedRecord("node")!.getValue("status");
// If comment is not visible, we don't need to add it.
if (!isVisible(status)) {
return;
}
incrementStoryCommentCounts(store, input.storyID);
prependCommentEdgeToProfile(
environment,
store,
store.getRootField("createComment")!.getLinkedRecord("edge")!
);
addCommentToStory(store, input);
prependCommentEdgeToProfile(environment, store, commentEdge);
addCommentToStory(store, input, commentEdge);
}
/**
@@ -43,14 +50,9 @@ function sharedUpdater(
*/
function addCommentToStory(
store: RecordSourceSelectorProxy,
input: CreateCommentInput
input: CreateCommentInput,
commentEdge: RecordProxy
) {
// Get the payload returned from the server.
const payload = store.getRootField("createComment")!;
// Get the edge of the newly created comment.
const newEdge = payload.getLinkedRecord("edge")!;
// Get stream proxy.
const streamProxy = store.get(input.storyID);
const connectionKey = "Stream_comments";
@@ -63,11 +65,23 @@ function addCommentToStory(
filters
);
if (con) {
ConnectionHandler.insertEdgeBefore(con, newEdge);
ConnectionHandler.insertEdgeBefore(con, commentEdge);
}
}
}
/** These are needed to be included when querying for the stream. */
// tslint:disable-next-line:no-unused-expression
graphql`
fragment CreateCommentMutation_story on Story {
moderation
}
fragment CreateCommentMutation_me on User {
role
}
`;
/** end */
const mutation = graphql`
mutation CreateCommentMutation($input: CreateCommentInput!) {
createComment(input: $input) {
@@ -75,6 +89,7 @@ const mutation = graphql`
cursor
node {
...StreamContainer_comment @relay(mask: false)
status
}
}
clientMutationId
@@ -87,11 +102,24 @@ let clientMutationId = 0;
function commit(
environment: Environment,
input: CreateCommentInput,
{ uuidGenerator }: TalkContext
{ uuidGenerator, relayEnvironment }: TalkContext
) {
const me = getMe(environment)!;
const currentDate = new Date().toISOString();
const id = uuidGenerator();
const story = relayEnvironment
.getStore()
.getSource()
.get(input.storyID);
if (!story || !story.moderation) {
throw new Error("Moderation mode of the story was not included");
}
// TODO: Generate and use schema types.
const expectPremoderation =
!isRolePriviledged(me.role) && story.moderation === "PRE";
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
@@ -108,6 +136,7 @@ function commit(
node: {
id,
createdAt: currentDate,
status: "NONE",
author: {
id: me.id,
username: me.username,
@@ -127,6 +156,10 @@ function commit(
},
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
optimisticUpdater: store => {
// Skip optimistic update if comment is probably premoderated.
if (expectPremoderation) {
return;
}
sharedUpdater(environment, store, input);
store.get(id)!.setValue(true, "pending");
},
@@ -143,4 +176,4 @@ export const withCreateCommentMutation = createMutationContainer(
export type CreateCommentMutation = (
input: CreateCommentInput
) => Promise<MutationTypes["response"]["createComment"]>;
) => MutationResponsePromise<MutationTypes, "createComment">;
@@ -5,15 +5,13 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { CreateCommentReactionMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentReactionMutation.graphql";
export type CreateCommentReactionInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
export type CreateCommentReactionInput = MutationInput<MutationTypes>;
const mutation = graphql`
mutation CreateCommentReactionMutation($input: CreateCommentReactionInput!) {
@@ -68,4 +66,4 @@ export const withCreateCommentReactionMutation = createMutationContainer(
export type CreateCommentReactionMutation = (
input: CreateCommentReactionInput
) => Promise<MutationTypes["response"]["createCommentReaction"]>;
) => MutationResponsePromise<MutationTypes, "createCommentReaction">;
@@ -2,6 +2,7 @@ import { graphql } from "react-relay";
import {
ConnectionHandler,
Environment,
RecordProxy,
RecordSourceSelectorProxy,
} from "relay-runtime";
@@ -10,35 +11,43 @@ import { TalkContext } from "talk-framework/lib/bootstrap";
import {
commitMutationPromiseNormalized,
createMutationContainer,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { CreateCommentReplyMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentReplyMutation.graphql";
import {
incrementStoryCommentCounts,
isRolePriviledged,
isVisible,
prependCommentEdgeToProfile,
} from "../helpers";
export type CreateCommentReplyInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
> & { local?: boolean };
export type CreateCommentReplyInput = MutationInput<MutationTypes> & {
local?: boolean;
};
function sharedUpdater(
environment: Environment,
store: RecordSourceSelectorProxy,
input: CreateCommentReplyInput
) {
const commentEdge = store
.getRootField("createCommentReply")!
.getLinkedRecord("edge")!;
const status = commentEdge.getLinkedRecord("node")!.getValue("status");
// If comment is not visible, we don't need to add it.
if (!isVisible(status)) {
return;
}
incrementStoryCommentCounts(store, input.storyID);
prependCommentEdgeToProfile(
environment,
store,
store.getRootField("createCommentReply")!.getLinkedRecord("edge")!
);
prependCommentEdgeToProfile(environment, store, commentEdge);
if (input.local) {
addLocalCommentReplyToStory(store, input);
addLocalCommentReplyToStory(store, input, commentEdge);
} else {
addCommentReplyToStory(store, input);
addCommentReplyToStory(store, input, commentEdge);
}
}
@@ -47,14 +56,9 @@ function sharedUpdater(
*/
function addCommentReplyToStory(
store: RecordSourceSelectorProxy,
input: CreateCommentReplyInput
input: CreateCommentReplyInput,
commentEdge: RecordProxy
) {
// Get the payload returned from the server.
const payload = store.getRootField("createCommentReply")!;
// Get the edge of the newly created comment.
const newEdge = payload.getLinkedRecord("edge")!;
// Get parent proxy.
const parentProxy = store.get(input.parentID);
const connectionKey = "ReplyList_replies";
@@ -67,7 +71,7 @@ function addCommentReplyToStory(
filters
);
if (con) {
ConnectionHandler.insertEdgeAfter(con, newEdge);
ConnectionHandler.insertEdgeAfter(con, commentEdge);
}
}
}
@@ -77,14 +81,10 @@ function addCommentReplyToStory(
*/
function addLocalCommentReplyToStory(
store: RecordSourceSelectorProxy,
input: CreateCommentReplyInput
input: CreateCommentReplyInput,
commentEdge: RecordProxy
) {
// Get the payload returned from the server.
const payload = store.getRootField("createCommentReply")!;
// Get the edge of the newly created comment.
const newEdge = payload.getLinkedRecord("edge")!;
const newComment = newEdge.getLinkedRecord("node");
const newComment = commentEdge.getLinkedRecord("node");
// Get parent proxy.
const parentProxy = store.get(input.parentID!);
@@ -98,6 +98,17 @@ function addLocalCommentReplyToStory(
}
}
/** These are needed to be included when querying for the stream. */
// tslint:disable-next-line:no-unused-expression
graphql`
fragment CreateCommentReplyMutation_story on Story {
moderation
}
fragment CreateCommentReplyMutation_me on User {
role
}
`;
/** end */
const mutation = graphql`
mutation CreateCommentReplyMutation($input: CreateCommentReplyInput!) {
createCommentReply(input: $input) {
@@ -105,6 +116,7 @@ const mutation = graphql`
cursor
node {
...StreamContainer_comment @relay(mask: false)
status
}
}
clientMutationId
@@ -117,11 +129,24 @@ let clientMutationId = 0;
function commit(
environment: Environment,
input: CreateCommentReplyInput,
{ uuidGenerator }: TalkContext
{ uuidGenerator, relayEnvironment }: TalkContext
) {
const me = getMe(environment)!;
const currentDate = new Date().toISOString();
const id = uuidGenerator();
const story = relayEnvironment
.getStore()
.getSource()
.get(input.storyID);
if (!story || !story.moderation) {
throw new Error("Moderation mode of the story was not included");
}
// TODO: Generate and use schema types.
const expectPremoderation =
!isRolePriviledged(me.role) && story.moderation === "PRE";
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
@@ -140,6 +165,7 @@ function commit(
node: {
id,
createdAt: currentDate,
status: "NONE",
author: {
id: me.id,
username: me.username,
@@ -159,6 +185,10 @@ function commit(
},
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
optimisticUpdater: store => {
// Skip optimistic update if comment is probably premoderated.
if (expectPremoderation) {
return;
}
sharedUpdater(environment, store, input);
store.get(id)!.setValue(true, "pending");
},
@@ -175,4 +205,4 @@ export const withCreateCommentReplyMutation = createMutationContainer(
export type CreateCommentReplyMutation = (
input: CreateCommentReplyInput
) => Promise<MutationTypes["response"]["createCommentReply"]>;
) => MutationResponsePromise<MutationTypes, "createCommentReply">;
@@ -5,21 +5,20 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { EditCommentMutation as MutationTypes } from "talk-stream/__generated__/EditCommentMutation.graphql";
export type EditCommentInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
export type EditCommentInput = MutationInput<MutationTypes>;
const mutation = graphql`
mutation EditCommentMutation($input: EditCommentInput!) {
editComment(input: $input) {
comment {
body
status
revision {
id
}
@@ -63,4 +62,4 @@ export const withEditCommentMutation = createMutationContainer(
export type EditCommentMutation = (
input: EditCommentInput
) => Promise<MutationTypes["response"]["editComment"]>;
) => MutationResponsePromise<MutationTypes, "editComment">;
@@ -5,15 +5,13 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { RemoveCommentReactionMutation as MutationTypes } from "talk-stream/__generated__/RemoveCommentReactionMutation.graphql";
export type RemoveCommentReactionInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
export type RemoveCommentReactionInput = MutationInput<MutationTypes>;
const mutation = graphql`
mutation RemoveCommentReactionMutation($input: RemoveCommentReactionInput!) {
@@ -67,4 +65,4 @@ export const withRemoveCommentReactionMutation = createMutationContainer(
export type RemoveCommentReactionMutation = (
input: RemoveCommentReactionInput
) => Promise<MutationTypes["response"]["removeCommentReaction"]>;
) => MutationResponsePromise<MutationTypes, "removeCommentReaction">;
@@ -2,6 +2,7 @@ import { FormApi, FormState } from "final-form";
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field, Form, FormSpy } from "react-final-form";
import { PropTypesOf } from "talk-framework/types";
import ValidationMessage from "talk-admin/routes/configure/components/ValidationMessage";
import { OnSubmit } from "talk-framework/lib/form";
@@ -11,6 +12,7 @@ import RemainingCharactersContainer from "../containers/RemainingCharactersConta
import { cleanupRTEEmptyHTML, getCommentBodyValidators } from "../helpers";
import RTE from "./RTE";
import PostCommentSubmitStatusContainer from "../containers/PostCommentSubmitStatusContainer";
import styles from "./PostCommentForm.css";
interface FormProps {
@@ -25,6 +27,7 @@ export interface PostCommentFormProps {
max: number | null;
disabled?: boolean;
disabledMessage?: React.ReactNode;
submitStatus: PropTypesOf<PostCommentSubmitStatusContainer>["status"];
}
const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
@@ -91,6 +94,9 @@ const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
{submitError}
</ValidationMessage>
)}
<PostCommentSubmitStatusContainer
status={props.submitStatus}
/>
{props.max && (
<RemainingCharactersContainer
value={input.value}
@@ -0,0 +1,3 @@
.flex {
width: 100%;
}
@@ -0,0 +1,33 @@
import React, { StatelessComponent } from "react";
import { Button, Flex, Message } from "talk-ui/components";
import { Localized } from "fluent-react/compat";
import styles from "./PostCommentInReviewMessage.css";
export interface PostCommentInReviewProps {
onDismiss: () => void;
}
const PostCommentInReview: StatelessComponent<
PostCommentInReviewProps
> = props => {
return (
<Message color="primary" fullWidth>
<Flex justifyContent="space-between" className={styles.flex}>
<Localized id="comments-submitStatus-submittedAndWillBeReviewed">
<div>
Your comment has been submitted and will be reviewed by a moderator
</div>
</Localized>
<Localized id="comments-submitStatus-dismiss">
<Button onClick={props.onDismiss} variant="underlined" color="light">
Dismiss
</Button>
</Localized>
</Flex>
</Message>
);
};
export default PostCommentInReview;
@@ -0,0 +1,3 @@
.callout {
min-height: calc(10 * var(--spacing-unit));
}
@@ -0,0 +1,49 @@
import React from "react";
import { Button, CallOut, Flex, HorizontalGutter } from "talk-ui/components";
import { SubmitStatus } from "../helpers/getSubmitStatus";
import { Localized } from "fluent-react/compat";
import styles from "./ReplyEditSubmitStatus.css";
interface Props {
status: SubmitStatus;
onDismiss: () => void;
}
function getMessage(status: SubmitStatus) {
switch (status) {
case "RETRY":
throw new Error(`Invalid status ${status}`);
case "REJECTED":
// TODO: Show a different message when rejected?
case "IN_REVIEW":
return (
<Localized id="comments-submitStatus-submittedAndWillBeReviewed">
<CallOut className={styles.callout} color="primary" fullWidth>
Your comment has been submitted and will be reviewed by a moderator
</CallOut>
</Localized>
);
case "APPROVED":
case null:
return null;
default:
throw new Error(`Unknown status ${status}`);
}
}
export default function ReplyEditSubmitStatus(props: Props) {
return (
<HorizontalGutter>
{getMessage(props.status)}
<Flex justifyContent="flex-end">
<Localized id="comments-submitStatus-dismiss">
<Button onClick={props.onDismiss} variant="outlined">
Dismiss
</Button>
</Localized>
</Flex>
</HorizontalGutter>
);
}
@@ -23,6 +23,7 @@ function createDefaultProps(add: DeepPartial<Props> = {}): Props {
},
comment: {
id: "comment-id",
status: "NONE",
author: {
id: "author-id",
username: "Marvin",
@@ -25,6 +25,7 @@ import Comment, {
ShowConversationLink,
} from "../components/Comment";
import ReplyButton from "../components/Comment/ReplyButton";
import { isCommentVisible } from "../helpers";
import EditCommentFormContainer from "./EditCommentFormContainer";
import PermalinkButtonContainer from "./PermalinkButtonContainer";
import ReplyCommentFormContainer from "./ReplyCommentFormContainer";
@@ -159,6 +160,12 @@ export class CommentContainer extends Component<Props, State> {
</div>
);
}
// Comment is not visible, so don't render it.
// This is the case when the comment was just edited and
// the comment status has changed.
if (!isCommentVisible(comment)) {
return null;
}
return (
<div data-testid={`comment-${comment.id}`}>
<HorizontalGutter>
@@ -282,6 +289,7 @@ const enhanced = withSetCommentIDMutation(
}
body
createdAt
status
editing {
edited
editableUntil
@@ -23,7 +23,12 @@ import {
import EditCommentForm, {
EditCommentFormProps,
} from "../components/EditCommentForm";
import { shouldTriggerSettingsRefresh } from "../helpers";
import ReplyEditSubmitStatus from "../components/ReplyEditSubmitStatus";
import {
getSubmitStatus,
shouldTriggerSettingsRefresh,
SubmitStatus,
} from "../helpers";
interface Props {
editComment: EditCommentMutation;
@@ -39,6 +44,7 @@ interface State {
initialValues?: EditCommentFormProps["initialValues"];
initialized: boolean;
expired: boolean;
submitStatus: SubmitStatus | null;
}
export class EditCommentFormContainer extends Component<Props, State> {
@@ -47,6 +53,7 @@ export class EditCommentFormContainer extends Component<Props, State> {
public state: State = {
initialized: false,
expired: !isBeforeDate(this.props.comment.editing.editableUntil),
submitStatus: null,
};
constructor(props: Props) {
@@ -84,13 +91,19 @@ export class EditCommentFormContainer extends Component<Props, State> {
form
) => {
try {
await this.props.editComment({
commentID: this.props.comment.id,
body: input.body,
});
if (this.props.onClose) {
this.props.onClose();
const submitStatus = getSubmitStatus(
await this.props.editComment({
commentID: this.props.comment.id,
body: input.body,
})
);
if (submitStatus !== "RETRY") {
if (submitStatus === "APPROVED" && this.props.onClose) {
this.props.onClose();
return;
}
}
this.setState({ submitStatus });
} catch (error) {
if (error instanceof InvalidRequestError) {
if (shouldTriggerSettingsRefresh(error.code)) {
@@ -101,10 +114,18 @@ export class EditCommentFormContainer extends Component<Props, State> {
// tslint:disable-next-line:no-console
console.error(error);
}
return undefined;
return;
};
public render() {
if (this.state.submitStatus && this.state.submitStatus !== "RETRY") {
return (
<ReplyEditSubmitStatus
status={this.state.submitStatus}
onDismiss={this.handleOnCancelOrClose}
/>
);
}
return (
<EditCommentForm
id={this.props.comment.id}
@@ -83,7 +83,7 @@ it("save values", async () => {
it("creates a comment", async () => {
const storyID = "story-id";
const input = { body: "Hello World!" };
const createCommentStub = sinon.stub();
const createCommentStub = sinon.stub().returns({ edge: { node: {} } });
const form = { reset: noop };
const formMock = sinon.mock(form);
formMock
@@ -26,7 +26,11 @@ import PostCommentForm, {
} from "../components/PostCommentForm";
import PostCommentFormCollapsed from "../components/PostCommentFormCollapsed";
import PostCommentFormFake from "../components/PostCommentFormFake";
import { shouldTriggerSettingsRefresh } from "../helpers";
import {
getSubmitStatus,
shouldTriggerSettingsRefresh,
SubmitStatus,
} from "../helpers";
interface Props {
createComment: CreateCommentMutation;
@@ -41,6 +45,7 @@ interface State {
initialValues?: PostCommentFormProps["initialValues"];
initialized: boolean;
keepFormWhenClosed: boolean;
submitStatus: SubmitStatus | null;
}
const contextKey = "postCommentFormBody";
@@ -52,6 +57,7 @@ export class PostCommentFormContainer extends Component<Props, State> {
this.props.local.loggedIn &&
!this.props.story.isClosed &&
!this.props.settings.disableCommenting.enabled,
submitStatus: null,
};
constructor(props: Props) {
@@ -78,11 +84,16 @@ export class PostCommentFormContainer extends Component<Props, State> {
form
) => {
try {
await this.props.createComment({
storyID: this.props.story.id,
...input,
});
form.reset({});
const submitStatus = getSubmitStatus(
await this.props.createComment({
storyID: this.props.story.id,
...input,
})
);
if (submitStatus !== "RETRY") {
form.reset({});
}
this.setState({ submitStatus });
} catch (error) {
if (error instanceof InvalidRequestError) {
if (shouldTriggerSettingsRefresh(error.code)) {
@@ -93,10 +104,13 @@ export class PostCommentFormContainer extends Component<Props, State> {
// tslint:disable-next-line:no-console
console.error(error);
}
return undefined;
return;
};
private handleOnChange: PostCommentFormProps["onChange"] = (state, form) => {
if (this.state.submitStatus && state.dirty) {
this.setState({ submitStatus: null });
}
if (state.values.body) {
this.props.sessionStorage.setItem(contextKey, state.values.body);
} else {
@@ -155,6 +169,7 @@ export class PostCommentFormContainer extends Component<Props, State> {
this.props.settings.disableCommenting.message) ||
this.props.settings.closedMessage
}
submitStatus={this.state.submitStatus}
/>
);
}
@@ -0,0 +1,48 @@
import React from "react";
import PostCommentInReviewMessage from "../components/PostCommentInReviewMessage";
import { SubmitStatus } from "../helpers/getSubmitStatus";
interface Props {
status: SubmitStatus | null;
}
interface State {
dismissed: boolean;
}
export default class PostCommentSubmitStatusContainer extends React.Component<
Props
> {
public state: State = {
dismissed: false,
};
public componentWillReceiveProps(nextProps: Props) {
if (this.props.status !== nextProps.status) {
this.setState({ dismissed: false });
}
}
private handleOnDismiss = () => {
this.setState({ dismissed: true });
};
public render() {
switch (this.props.status) {
case "RETRY":
throw new Error("Not implemented");
case "REJECTED":
// TODO: Show a different message when rejected?
case "IN_REVIEW":
return this.state.dismissed ? null : (
<PostCommentInReviewMessage onDismiss={this.handleOnDismiss} />
);
case "APPROVED":
case null:
return null;
default:
throw new Error(`Unknown status ${this.props.status}`);
}
}
}
@@ -104,7 +104,7 @@ it("save values", async () => {
it("creates a comment", async () => {
const storyID = "story-id";
const input = { body: "Hello World!", local: false };
const createCommentStub = sinon.stub();
const createCommentStub = sinon.stub().returns({ edge: { node: {} } });
const form = { reset: noop };
const onCloseStub = sinon.stub();
@@ -22,7 +22,12 @@ import {
import ReplyCommentForm, {
ReplyCommentFormProps,
} from "../components/ReplyCommentForm";
import { shouldTriggerSettingsRefresh } from "../helpers";
import ReplyEditSubmitStatus from "../components/ReplyEditSubmitStatus";
import {
getSubmitStatus,
shouldTriggerSettingsRefresh,
SubmitStatus,
} from "../helpers";
interface Props {
createCommentReply: CreateCommentReplyMutation;
@@ -39,10 +44,14 @@ interface Props {
interface State {
initialValues?: ReplyCommentFormProps["initialValues"];
initialized: boolean;
submitStatus: SubmitStatus | null;
}
export class ReplyCommentFormContainer extends Component<Props, State> {
public state: State = { initialized: false };
public state: State = {
initialized: false,
submitStatus: null,
};
private contextKey = `replyCommentFormBody-${this.props.comment.id}`;
constructor(props: Props) {
@@ -70,30 +79,32 @@ export class ReplyCommentFormContainer extends Component<Props, State> {
});
}
private handleOnCancel = () => {
private handleOnCancelOrDismiss = () => {
this.props.sessionStorage.removeItem(this.contextKey);
if (this.props.onClose) {
this.props.onClose();
}
};
private handleOnSubmit: ReplyCommentFormProps["onSubmit"] = async (
input,
form
) => {
private handleOnSubmit: ReplyCommentFormProps["onSubmit"] = async input => {
try {
await this.props.createCommentReply({
storyID: this.props.story.id,
parentID: this.props.comment.id,
parentRevisionID: this.props.comment.revision.id,
local: this.props.localReply,
...input,
});
this.props.sessionStorage.removeItem(this.contextKey);
if (this.props.onClose) {
this.props.onClose();
const submitStatus = getSubmitStatus(
await this.props.createCommentReply({
storyID: this.props.story.id,
parentID: this.props.comment.id,
parentRevisionID: this.props.comment.revision.id,
local: this.props.localReply,
...input,
})
);
if (submitStatus !== "RETRY") {
this.props.sessionStorage.removeItem(this.contextKey);
if (submitStatus === "APPROVED" && this.props.onClose) {
this.props.onClose();
return;
}
}
this.setState({ submitStatus });
} catch (error) {
if (error instanceof InvalidRequestError) {
if (shouldTriggerSettingsRefresh(error.code)) {
@@ -104,7 +115,7 @@ export class ReplyCommentFormContainer extends Component<Props, State> {
// tslint:disable-next-line:no-console
console.error(error);
}
return undefined;
return;
};
private handleOnChange: ReplyCommentFormProps["onChange"] = (state, form) => {
@@ -123,13 +134,21 @@ export class ReplyCommentFormContainer extends Component<Props, State> {
if (!this.state.initialized) {
return null;
}
if (this.state.submitStatus && this.state.submitStatus !== "RETRY") {
return (
<ReplyEditSubmitStatus
status={this.state.submitStatus}
onDismiss={this.handleOnCancelOrDismiss}
/>
);
}
return (
<ReplyCommentForm
id={this.props.comment.id}
onSubmit={this.handleOnSubmit}
onChange={this.handleOnChange}
initialValues={this.state.initialValues}
onCancel={this.handleOnCancel}
onCancel={this.handleOnCancelOrDismiss}
rteRef={this.handleRTERef}
parentUsername={
this.props.comment.author && this.props.comment.author.username
@@ -18,6 +18,7 @@ it("renders correctly", () => {
},
comment: {
id: "comment-id",
status: "NONE",
replies: {
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
},
@@ -48,6 +49,7 @@ it("renders correctly when replies are empty", () => {
},
comment: {
id: "comment-id",
status: "NONE",
replies: { edges: [] },
},
relay: {
@@ -77,6 +79,7 @@ describe("when has more replies", () => {
},
comment: {
id: "comment-id",
status: "NONE",
replies: {
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
},
@@ -17,6 +17,7 @@ import { ReplyListContainer5_comment as Comment5Data } from "talk-stream/__gener
import { StatelessComponent } from "enzyme";
import { FragmentKeys } from "talk-framework/lib/relay/types";
import ReplyList from "../components/ReplyList";
import { isCommentVisible } from "../helpers";
import LocalReplyListContainer from "./LocalReplyListContainer";
type UnpackArray<T> = T extends ReadonlyArray<infer U> ? U : any;
@@ -56,19 +57,27 @@ export class ReplyListContainer extends React.Component<Props> {
) {
return null;
}
const comments = this.props.comment.replies.edges.map(edge => ({
...edge.node,
replyListElement: this.props.ReplyListComponent && (
<this.props.ReplyListComponent
me={this.props.me}
comment={edge.node}
story={this.props.story}
settings={this.props.settings}
/>
),
// ReplyListContainer5 contains replyCount.
showConversationLink: ((edge.node as any) as ReplyNode5).replyCount > 0,
}));
// Don't render replies if comment is not visible.
// We have this comment here because it's
// status just changed due to an edit.
// We keep it around for the CommentContainer to show
// some information to the user.
const comments = !isCommentVisible(this.props.comment)
? []
: this.props.comment.replies.edges.map(edge => ({
...edge.node,
replyListElement: this.props.ReplyListComponent && (
<this.props.ReplyListComponent
me={this.props.me}
comment={edge.node}
story={this.props.story}
settings={this.props.settings}
/>
),
// ReplyListContainer5 contains replyCount.
showConversationLink:
((edge.node as any) as ReplyNode5).replyCount > 0,
}));
return (
<ReplyList
me={this.props.me}
@@ -182,6 +191,7 @@ const ReplyListContainer5 = createReplyListContainer(
orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC }
) {
id
status
replies(first: $count, after: $cursor, orderBy: $orderBy)
@connection(key: "ReplyList_replies") {
edges {
@@ -244,6 +254,7 @@ const ReplyListContainer4 = createReplyListContainer(
orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC }
) {
id
status
replies(first: $count, after: $cursor, orderBy: $orderBy)
@connection(key: "ReplyList_replies") {
edges {
@@ -304,6 +315,7 @@ const ReplyListContainer3 = createReplyListContainer(
orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC }
) {
id
status
replies(first: $count, after: $cursor, orderBy: $orderBy)
@connection(key: "ReplyList_replies") {
edges {
@@ -364,6 +376,7 @@ const ReplyListContainer2 = createReplyListContainer(
orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC }
) {
id
status
replies(first: $count, after: $cursor, orderBy: $orderBy)
@connection(key: "ReplyList_replies") {
edges {
@@ -424,6 +437,7 @@ const ReplyListContainer1 = createReplyListContainer(
orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC }
) {
id
status
replies(first: $count, after: $cursor, orderBy: $orderBy)
@connection(key: "ReplyList_replies") {
edges {
@@ -130,6 +130,8 @@ const enhanced = withPaginationContainer<
...CommentContainer_story
...ReplyListContainer1_story
...StoryClosedTimeoutContainer_story
...CreateCommentReplyMutation_story
...CreateCommentMutation_story
}
`,
me: graphql`
@@ -137,6 +139,8 @@ const enhanced = withPaginationContainer<
...ReplyListContainer1_me
...CommentContainer_me
...UserBoxContainer_me
...CreateCommentReplyMutation_me
...CreateCommentMutation_me
}
`,
settings: graphql`
@@ -40,6 +40,7 @@ exports[`hide reply button 1`] = `
"id": "comment-id",
"parent": null,
"pending": false,
"status": "NONE",
}
}
me={null}
@@ -69,6 +70,7 @@ exports[`hide reply button 1`] = `
"id": "comment-id",
"parent": null,
"pending": false,
"status": "NONE",
}
}
me={null}
@@ -132,6 +134,7 @@ exports[`renders body only 1`] = `
"id": "comment-id",
"parent": null,
"pending": false,
"status": "NONE",
}
}
me={null}
@@ -161,6 +164,7 @@ exports[`renders body only 1`] = `
"id": "comment-id",
"parent": null,
"pending": false,
"status": "NONE",
}
}
me={null}
@@ -224,6 +228,7 @@ exports[`renders disabled reply when commenting has been disabled 1`] = `
"id": "comment-id",
"parent": null,
"pending": false,
"status": "NONE",
}
}
me={null}
@@ -253,6 +258,7 @@ exports[`renders disabled reply when commenting has been disabled 1`] = `
"id": "comment-id",
"parent": null,
"pending": false,
"status": "NONE",
}
}
me={null}
@@ -316,6 +322,7 @@ exports[`renders disabled reply when story is closed 1`] = `
"id": "comment-id",
"parent": null,
"pending": false,
"status": "NONE",
}
}
me={null}
@@ -345,6 +352,7 @@ exports[`renders disabled reply when story is closed 1`] = `
"id": "comment-id",
"parent": null,
"pending": false,
"status": "NONE",
}
}
me={null}
@@ -412,6 +420,7 @@ exports[`renders in reply to 1`] = `
},
},
"pending": false,
"status": "NONE",
}
}
me={null}
@@ -445,6 +454,7 @@ exports[`renders in reply to 1`] = `
},
},
"pending": false,
"status": "NONE",
}
}
me={null}
@@ -508,6 +518,7 @@ exports[`renders username and body 1`] = `
"id": "comment-id",
"parent": null,
"pending": false,
"status": "NONE",
}
}
me={null}
@@ -537,6 +548,7 @@ exports[`renders username and body 1`] = `
"id": "comment-id",
"parent": null,
"pending": false,
"status": "NONE",
}
}
me={null}
@@ -600,6 +612,7 @@ exports[`shows conversation link 1`] = `
"id": "comment-id",
"parent": null,
"pending": false,
"status": "NONE",
}
}
me={null}
@@ -629,6 +642,7 @@ exports[`shows conversation link 1`] = `
"id": "comment-id",
"parent": null,
"pending": false,
"status": "NONE",
}
}
me={null}
@@ -8,6 +8,7 @@ exports[`renders correctly 1`] = `
min={3}
onChange={[Function]}
onSubmit={[Function]}
submitStatus={null}
/>
`;
@@ -26,6 +27,7 @@ exports[`renders when commenting has been disabled (non-collapsing) 1`] = `
min={3}
onChange={[Function]}
onSubmit={[Function]}
submitStatus={null}
/>
`;
@@ -44,6 +46,7 @@ exports[`renders when story has been closed (non-collapsing) 1`] = `
min={3}
onChange={[Function]}
onSubmit={[Function]}
submitStatus={null}
/>
`;
@@ -60,5 +63,6 @@ exports[`renders with initialValues 1`] = `
min={3}
onChange={[Function]}
onSubmit={[Function]}
submitStatus={null}
/>
`;
@@ -19,6 +19,7 @@ exports[`renders correctly 1`] = `
},
],
},
"status": "NONE",
}
}
comments={
@@ -117,6 +118,7 @@ exports[`when has more replies renders hasMore 1`] = `
},
],
},
"status": "NONE",
}
}
comments={
@@ -174,6 +176,7 @@ exports[`when has more replies when showing all disables show all button 1`] = `
},
],
},
"status": "NONE",
}
}
comments={
@@ -231,6 +234,7 @@ exports[`when has more replies when showing all enable show all button after loa
},
],
},
"status": "NONE",
}
}
comments={
@@ -0,0 +1,23 @@
import { MutationResponse } from "talk-framework/lib/relay";
import { CreateCommentMutation } from "talk-stream/__generated__/CreateCommentMutation.graphql";
import { CreateCommentReplyMutation } from "talk-stream/__generated__/CreateCommentReplyMutation.graphql";
import { EditCommentMutation } from "talk-stream/__generated__/EditCommentMutation.graphql";
import { isInReview, isRejected } from "talk-stream/helpers";
export type SubmitStatus = "APPROVED" | "RETRY" | "IN_REVIEW" | "REJECTED";
export default function getSubmitStatus(
response:
| MutationResponse<CreateCommentMutation, "createComment">
| MutationResponse<CreateCommentReplyMutation, "createCommentReply">
| MutationResponse<EditCommentMutation, "editComment">
): SubmitStatus {
const node = "edge" in response ? response.edge.node : response.comment;
if (isInReview(node.status)) {
return "IN_REVIEW";
}
if (isRejected(node.status)) {
return "REJECTED";
}
return "APPROVED";
}
@@ -7,3 +7,5 @@ export {
default as shouldTriggerSettingsRefresh,
} from "./shouldTriggerSettingsRefresh";
export { default as getHTMLText } from "./getHTMLText";
export { default as getSubmitStatus, SubmitStatus } from "./getSubmitStatus";
export { default as isCommentVisible } from "./isCommentVisible";
@@ -0,0 +1,9 @@
import { COMMENT_STATUS } from "talk-stream/__generated__/CreateCommentMutation.graphql";
const VisibleStatus = ["ACCEPTED", "NONE"];
export default function isCommentVisible(comment: {
status: COMMENT_STATUS;
}): boolean {
return VisibleStatus.includes(comment.status);
}
@@ -2,7 +2,7 @@
exports[`cancel edit 1`] = `
<div
data-testid="comment-comment-0"
data-testid="comment-comment-with-replies"
>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -62,7 +62,7 @@ exports[`cancel edit 1`] = `
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Joining Too",
"__html": "I like yoghurt",
}
}
/>
@@ -75,7 +75,7 @@ exports[`cancel edit 1`] = `
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-0"
id="comments-commentContainer-replyButton-comment-with-replies"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
@@ -92,7 +92,7 @@ exports[`cancel edit 1`] = `
className="Popover-root"
>
<button
aria-controls="permalink-popover-comment-0"
aria-controls="permalink-popover-comment-with-replies"
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
onBlur={[Function]}
onClick={[Function]}
@@ -108,13 +108,13 @@ exports[`cancel edit 1`] = `
</button>
<div
aria-hidden={true}
aria-labelledby="permalink-popover-comment-0-ariainfo"
id="permalink-popover-comment-0"
aria-labelledby="permalink-popover-comment-with-replies-ariainfo"
id="permalink-popover-comment-with-replies"
role="popup"
>
<div
className="AriaInfo-root"
id="permalink-popover-comment-0-ariainfo"
id="permalink-popover-comment-with-replies-ariainfo"
>
A dialog showing a permalink to the comment
</div>
@@ -142,7 +142,7 @@ exports[`cancel edit 1`] = `
className="Popover-root"
>
<button
aria-controls="report-popover-comment-0"
aria-controls="report-popover-comment-with-replies"
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
onBlur={[Function]}
@@ -159,13 +159,13 @@ exports[`cancel edit 1`] = `
</button>
<div
aria-hidden={true}
aria-labelledby="report-popover-comment-0-ariainfo"
id="report-popover-comment-0"
aria-labelledby="report-popover-comment-with-replies-ariainfo"
id="report-popover-comment-with-replies"
role="popup"
>
<div
className="AriaInfo-root"
id="report-popover-comment-0-ariainfo"
id="report-popover-comment-with-replies-ariainfo"
>
A dialog for reporting comments
</div>
@@ -183,7 +183,7 @@ exports[`cancel edit 1`] = `
exports[`edit a comment and handle server error: edit form 1`] = `
<div
data-testid="comment-comment-0"
data-testid="comment-comment-with-replies"
>
<form
autoComplete="off"
@@ -215,7 +215,7 @@ exports[`edit a comment and handle server error: edit form 1`] = `
>
<label
className="AriaInfo-root"
htmlFor="comments-editCommentForm-rte-comment-0"
htmlFor="comments-editCommentForm-rte-comment-with-replies"
>
Edit comment
</label>
@@ -229,11 +229,11 @@ exports[`edit a comment and handle server error: edit form 1`] = `
contentEditable={true}
dangerouslySetInnerHTML={
Object {
"__html": "Joining Too",
"__html": "I like yoghurt",
}
}
disabled={false}
id="comments-editCommentForm-rte-comment-0"
id="comments-editCommentForm-rte-comment-with-replies"
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
@@ -349,7 +349,7 @@ exports[`edit a comment and handle server error: edit form 1`] = `
exports[`edit a comment: edit form 1`] = `
<div
data-testid="comment-comment-0"
data-testid="comment-comment-with-replies"
>
<form
autoComplete="off"
@@ -381,7 +381,7 @@ exports[`edit a comment: edit form 1`] = `
>
<label
className="AriaInfo-root"
htmlFor="comments-editCommentForm-rte-comment-0"
htmlFor="comments-editCommentForm-rte-comment-with-replies"
>
Edit comment
</label>
@@ -395,11 +395,11 @@ exports[`edit a comment: edit form 1`] = `
contentEditable={true}
dangerouslySetInnerHTML={
Object {
"__html": "Joining Too",
"__html": "I like yoghurt",
}
}
disabled={false}
id="comments-editCommentForm-rte-comment-0"
id="comments-editCommentForm-rte-comment-with-replies"
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
@@ -515,7 +515,7 @@ exports[`edit a comment: edit form 1`] = `
exports[`edit a comment: optimistic response 1`] = `
<div
data-testid="comment-comment-0"
data-testid="comment-comment-with-replies"
>
<form
autoComplete="off"
@@ -547,7 +547,7 @@ exports[`edit a comment: optimistic response 1`] = `
>
<label
className="AriaInfo-root"
htmlFor="comments-editCommentForm-rte-comment-0"
htmlFor="comments-editCommentForm-rte-comment-with-replies"
>
Edit comment
</label>
@@ -565,7 +565,7 @@ exports[`edit a comment: optimistic response 1`] = `
}
}
disabled={true}
id="comments-editCommentForm-rte-comment-0"
id="comments-editCommentForm-rte-comment-with-replies"
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
@@ -681,7 +681,7 @@ exports[`edit a comment: optimistic response 1`] = `
exports[`edit a comment: render comment with edit button 1`] = `
<div
data-testid="comment-comment-0"
data-testid="comment-comment-with-replies"
>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -741,7 +741,7 @@ exports[`edit a comment: render comment with edit button 1`] = `
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Joining Too",
"__html": "I like yoghurt",
}
}
/>
@@ -754,7 +754,7 @@ exports[`edit a comment: render comment with edit button 1`] = `
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-0"
id="comments-commentContainer-replyButton-comment-with-replies"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
@@ -771,7 +771,7 @@ exports[`edit a comment: render comment with edit button 1`] = `
className="Popover-root"
>
<button
aria-controls="permalink-popover-comment-0"
aria-controls="permalink-popover-comment-with-replies"
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
onBlur={[Function]}
onClick={[Function]}
@@ -787,13 +787,13 @@ exports[`edit a comment: render comment with edit button 1`] = `
</button>
<div
aria-hidden={true}
aria-labelledby="permalink-popover-comment-0-ariainfo"
id="permalink-popover-comment-0"
aria-labelledby="permalink-popover-comment-with-replies-ariainfo"
id="permalink-popover-comment-with-replies"
role="popup"
>
<div
className="AriaInfo-root"
id="permalink-popover-comment-0-ariainfo"
id="permalink-popover-comment-with-replies-ariainfo"
>
A dialog showing a permalink to the comment
</div>
@@ -821,7 +821,7 @@ exports[`edit a comment: render comment with edit button 1`] = `
className="Popover-root"
>
<button
aria-controls="report-popover-comment-0"
aria-controls="report-popover-comment-with-replies"
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
onBlur={[Function]}
@@ -838,13 +838,13 @@ exports[`edit a comment: render comment with edit button 1`] = `
</button>
<div
aria-hidden={true}
aria-labelledby="report-popover-comment-0-ariainfo"
id="report-popover-comment-0"
aria-labelledby="report-popover-comment-with-replies-ariainfo"
id="report-popover-comment-with-replies"
role="popup"
>
<div
className="AriaInfo-root"
id="report-popover-comment-0-ariainfo"
id="report-popover-comment-with-replies-ariainfo"
>
A dialog for reporting comments
</div>
@@ -862,7 +862,7 @@ exports[`edit a comment: render comment with edit button 1`] = `
exports[`edit a comment: server response 1`] = `
<div
data-testid="comment-comment-0"
data-testid="comment-comment-with-replies"
>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -944,7 +944,7 @@ exports[`edit a comment: server response 1`] = `
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-0"
id="comments-commentContainer-replyButton-comment-with-replies"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
@@ -961,7 +961,7 @@ exports[`edit a comment: server response 1`] = `
className="Popover-root"
>
<button
aria-controls="permalink-popover-comment-0"
aria-controls="permalink-popover-comment-with-replies"
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
onBlur={[Function]}
onClick={[Function]}
@@ -977,13 +977,13 @@ exports[`edit a comment: server response 1`] = `
</button>
<div
aria-hidden={true}
aria-labelledby="permalink-popover-comment-0-ariainfo"
id="permalink-popover-comment-0"
aria-labelledby="permalink-popover-comment-with-replies-ariainfo"
id="permalink-popover-comment-with-replies"
role="popup"
>
<div
className="AriaInfo-root"
id="permalink-popover-comment-0-ariainfo"
id="permalink-popover-comment-with-replies-ariainfo"
>
A dialog showing a permalink to the comment
</div>
@@ -1011,7 +1011,7 @@ exports[`edit a comment: server response 1`] = `
className="Popover-root"
>
<button
aria-controls="report-popover-comment-0"
aria-controls="report-popover-comment-with-replies"
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
onBlur={[Function]}
@@ -1028,13 +1028,13 @@ exports[`edit a comment: server response 1`] = `
</button>
<div
aria-hidden={true}
aria-labelledby="report-popover-comment-0-ariainfo"
id="report-popover-comment-0"
aria-labelledby="report-popover-comment-with-replies-ariainfo"
id="report-popover-comment-with-replies"
role="popup"
>
<div
className="AriaInfo-root"
id="report-popover-comment-0-ariainfo"
id="report-popover-comment-with-replies-ariainfo"
>
A dialog for reporting comments
</div>
@@ -1052,7 +1052,7 @@ exports[`edit a comment: server response 1`] = `
exports[`shows expiry message: edit form closed 1`] = `
<div
data-testid="comment-comment-0"
data-testid="comment-comment-with-replies"
>
<div
className="HorizontalGutter-root HorizontalGutter-full"
@@ -1112,7 +1112,7 @@ exports[`shows expiry message: edit form closed 1`] = `
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Joining Too",
"__html": "I like yoghurt",
}
}
/>
@@ -1125,7 +1125,7 @@ exports[`shows expiry message: edit form closed 1`] = `
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-0"
id="comments-commentContainer-replyButton-comment-with-replies"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
@@ -1142,7 +1142,7 @@ exports[`shows expiry message: edit form closed 1`] = `
className="Popover-root"
>
<button
aria-controls="permalink-popover-comment-0"
aria-controls="permalink-popover-comment-with-replies"
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
onBlur={[Function]}
onClick={[Function]}
@@ -1158,13 +1158,13 @@ exports[`shows expiry message: edit form closed 1`] = `
</button>
<div
aria-hidden={true}
aria-labelledby="permalink-popover-comment-0-ariainfo"
id="permalink-popover-comment-0"
aria-labelledby="permalink-popover-comment-with-replies-ariainfo"
id="permalink-popover-comment-with-replies"
role="popup"
>
<div
className="AriaInfo-root"
id="permalink-popover-comment-0-ariainfo"
id="permalink-popover-comment-with-replies-ariainfo"
>
A dialog showing a permalink to the comment
</div>
@@ -1192,7 +1192,7 @@ exports[`shows expiry message: edit form closed 1`] = `
className="Popover-root"
>
<button
aria-controls="report-popover-comment-0"
aria-controls="report-popover-comment-with-replies"
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
onBlur={[Function]}
@@ -1209,13 +1209,13 @@ exports[`shows expiry message: edit form closed 1`] = `
</button>
<div
aria-hidden={true}
aria-labelledby="report-popover-comment-0-ariainfo"
id="report-popover-comment-0"
aria-labelledby="report-popover-comment-with-replies-ariainfo"
id="report-popover-comment-with-replies"
role="popup"
>
<div
className="AriaInfo-root"
id="report-popover-comment-0-ariainfo"
id="report-popover-comment-with-replies-ariainfo"
>
A dialog for reporting comments
</div>
@@ -1233,7 +1233,7 @@ exports[`shows expiry message: edit form closed 1`] = `
exports[`shows expiry message: edit time expired 1`] = `
<div
data-testid="comment-comment-0"
data-testid="comment-comment-with-replies"
>
<form
autoComplete="off"
@@ -1265,7 +1265,7 @@ exports[`shows expiry message: edit time expired 1`] = `
>
<label
className="AriaInfo-root"
htmlFor="comments-editCommentForm-rte-comment-0"
htmlFor="comments-editCommentForm-rte-comment-with-replies"
>
Edit comment
</label>
@@ -1279,11 +1279,11 @@ exports[`shows expiry message: edit time expired 1`] = `
contentEditable={false}
dangerouslySetInnerHTML={
Object {
"__html": "Joining Too",
"__html": "I like yoghurt",
}
}
disabled={true}
id="comments-editCommentForm-rte-comment-0"
id="comments-editCommentForm-rte-comment-with-replies"
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
@@ -9,12 +9,17 @@ import {
within,
} from "talk-framework/testHelpers";
import { settings, stories, users } from "../fixtures";
import {
commentWithReplies,
settings,
storyWithReplies,
users,
} from "../fixtures";
import create from "./create";
function createTestRenderer(
resolver: any = {},
options: { muteNetworkErrors?: boolean } = {}
options: { muteNetworkErrors?: boolean; status?: string } = {}
) {
const resolvers = {
Query: {
@@ -22,35 +27,35 @@ function createTestRenderer(
s => s.throws(),
s =>
s
.withArgs(undefined, { id: stories[0].id, url: null })
.returns(stories[0])
.withArgs(undefined, { id: storyWithReplies.id, url: null })
.returns(storyWithReplies)
),
me: sinon.stub().returns(users[0]),
settings: sinon.stub().returns(settings),
},
Mutation: {
editComment: sinon.stub().callsFake((_, data) => {
expect(data).toEqual({
expect(data).toMatchObject({
input: {
commentID: stories[0].comments.edges[0].node.id,
commentID: commentWithReplies.id,
body: "Edited!",
clientMutationId: "0",
},
});
return {
// TODO: add a type assertion here to ensure that if the type changes, that the test will fail
comment: {
id: stories[0].comments.edges[0].node.id,
id: commentWithReplies.id,
body: "Edited! (from server)",
status: options.status ? options.status : commentWithReplies.status,
editing: {
edited: true,
},
revision: {
id: stories[0].comments.edges[0].node.revision.id,
id: commentWithReplies.revision.id,
},
},
clientMutationId: "0",
clientMutationId: data.input.clientMutationId,
};
}),
},
@@ -63,7 +68,7 @@ function createTestRenderer(
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(stories[0].id, "storyID");
localRecord.setValue(storyWithReplies.id, "storyID");
localRecord.setValue(true, "loggedIn");
},
});
@@ -75,12 +80,11 @@ afterAll(() => {
});
it("edit a comment", async () => {
const commentData = stories[0].comments.edges[0].node;
timekeeper.freeze(commentData.createdAt);
timekeeper.freeze(commentWithReplies.createdAt);
const testRenderer = createTestRenderer();
const comment = await waitForElement(() =>
within(testRenderer.root).getByTestID(`comment-${commentData.id}`)
within(testRenderer.root).getByTestID(`comment-${commentWithReplies.id}`)
);
expect(within(comment).toJSON()).toMatchSnapshot(
"render comment with edit button"
@@ -93,7 +97,9 @@ it("edit a comment", async () => {
expect(within(comment).toJSON()).toMatchSnapshot("edit form");
testRenderer.root
.findByProps({ inputId: `comments-editCommentForm-rte-${commentData.id}` })
.findByProps({
inputId: `comments-editCommentForm-rte-${commentWithReplies.id}`,
})
.props.onChange({ html: "Edited!" });
within(comment)
@@ -112,13 +118,60 @@ it("edit a comment", async () => {
expect(within(comment).toJSON()).toMatchSnapshot("server response");
});
it("edit a comment and handle non-visible comment state", async () => {
const testRenderer = createTestRenderer({}, { status: "SYSTEM_WITHHELD" });
const comment = await waitForElement(() =>
within(testRenderer.root).getByTestID(`comment-${commentWithReplies.id}`)
);
// Open edit form.
within(comment)
.getByText("Edit")
.props.onClick();
testRenderer.root
.findByProps({
inputId: `comments-editCommentForm-rte-${commentWithReplies.id}`,
})
.props.onChange({ html: "Edited!" });
within(comment)
.getByType("form")
.props.onSubmit();
// Test after server response.
await waitForElement(() =>
within(comment).getByText("will be reviewed", { exact: false })
);
await within(comment)
.getByText("Dismiss")
.props.onClick();
expect(
within(testRenderer.root).queryByText("will be reviewed", { exact: false })
).toBeNull();
// The comment must disappear.
expect(
within(testRenderer.root).queryByTestID(`comment-${commentWithReplies.id}`)
).toBeNull();
// The whole reply list must disappear too.
expect(
within(
within(testRenderer.root).getByTestID(
`commentReplyList-${commentWithReplies.id}`
)
).queryAllByTestID(/^comment-.*$/).length
).toBe(0);
});
it("cancel edit", async () => {
const commentData = stories[0].comments.edges[0].node;
timekeeper.freeze(commentData.createdAt);
timekeeper.freeze(commentWithReplies.createdAt);
const testRenderer = createTestRenderer();
const comment = await waitForElement(() =>
within(testRenderer.root).getByTestID(`comment-${commentData.id}`)
within(testRenderer.root).getByTestID(`comment-${commentWithReplies.id}`)
);
// Open edit form.
@@ -135,12 +188,11 @@ it("cancel edit", async () => {
});
it("shows expiry message", async () => {
const commentData = stories[0].comments.edges[0].node;
timekeeper.freeze(commentData.createdAt);
timekeeper.freeze(commentWithReplies.createdAt);
const testRenderer = createTestRenderer();
const comment = await waitForElement(() =>
within(testRenderer.root).getByTestID(`comment-${commentData.id}`)
within(testRenderer.root).getByTestID(`comment-${commentWithReplies.id}`)
);
jest.useFakeTimers();
@@ -164,8 +216,7 @@ it("shows expiry message", async () => {
});
it("edit a comment and handle server error", async () => {
const commentData = stories[0].comments.edges[0].node;
timekeeper.freeze(commentData.createdAt);
timekeeper.freeze(commentWithReplies.createdAt);
const testRenderer = createTestRenderer(
{
Mutation: {
@@ -178,7 +229,7 @@ it("edit a comment and handle server error", async () => {
);
const comment = await waitForElement(() =>
within(testRenderer.root).getByTestID(`comment-${commentData.id}`)
within(testRenderer.root).getByTestID(`comment-${commentWithReplies.id}`)
);
// Open edit form.
@@ -188,7 +239,9 @@ it("edit a comment and handle server error", async () => {
expect(within(comment).toJSON()).toMatchSnapshot("edit form");
testRenderer.root
.findByProps({ inputId: `comments-editCommentForm-rte-${commentData.id}` })
.findByProps({
inputId: `comments-editCommentForm-rte-${commentWithReplies.id}`,
})
.props.onChange({ html: "Edited!" });
within(comment)
@@ -11,6 +11,7 @@ import {
} from "talk-framework/testHelpers";
import RTE from "@coralproject/rte";
import { ReactTestInstance } from "react-test-renderer";
import { baseComment, settings, stories, users } from "../fixtures";
import create from "./create";
@@ -71,11 +72,10 @@ it("post a comment", async () => {
const { rte, form, tabPane } = await createTestRenderer({
Mutation: {
createComment: sinon.stub().callsFake((_, data) => {
expect(data).toEqual({
expect(data).toMatchObject({
input: {
storyID: stories[0].id,
body: "<b>Hello world!</b>",
clientMutationId: "0",
},
});
return {
@@ -88,7 +88,7 @@ it("post a comment", async () => {
body: "<b>Hello world! (from server)</b>",
},
},
clientMutationId: "0",
clientMutationId: data.input.clientMutationId,
};
}),
},
@@ -113,6 +113,60 @@ it("post a comment", async () => {
);
});
const postACommentAndHandleNonVisibleComment = async (
dismiss: (form: ReactTestInstance, rte: ReactTestInstance) => void
) => {
const { rte, form } = await createTestRenderer({
Mutation: {
createComment: sinon.stub().callsFake((_, data) => {
expect(data).toMatchObject({
input: {
storyID: stories[0].id,
body: "<b>Hello world!</b>",
},
});
return {
edge: {
cursor: null,
node: {
...baseComment,
id: "comment-x",
status: "SYSTEM_WITHHELD",
author: users[0],
body: "<b>Hello world!</b>",
},
},
clientMutationId: data.input.clientMutationId,
};
}),
},
});
rte.props.onChange({ html: "<b>Hello world!</b>" });
form.props.onSubmit();
// Test after server response.
await waitForElement(() =>
within(form).getByText("will be reviewed", { exact: false })
);
dismiss(form, rte);
expect(
within(form).queryByText("will be reviewed", { exact: false })
).toBeNull();
};
it("post a comment and handle non-visible comment state (dismiss by click)", async () =>
await postACommentAndHandleNonVisibleComment((form, rte) => {
within(form)
.getByText("Dismiss")
.props.onClick();
}));
it("post a comment and handle non-visible comment state (dismiss by typing)", async () =>
await postACommentAndHandleNonVisibleComment((form, rte) => {
rte.props.onChange({ html: "Typing..." });
}));
it("post a comment and handle server error", async () => {
const { form, rte, tabPane } = await createTestRenderer(
{
@@ -31,32 +31,26 @@ beforeEach(() => {
),
},
Mutation: {
createCommentReply: createSinonStub(
s => s.throws(),
s =>
s
.withArgs(undefined, {
input: {
storyID: storyWithDeepestReplies.id,
parentID: "comment-with-deepest-replies-5",
parentRevisionID: "revision-0",
body: "<b>Hello world!</b>",
clientMutationId: "0",
},
})
.returns({
edge: {
cursor: null,
node: {
...baseComment,
id: "comment-x",
author: users[0],
body: "<b>Hello world! (from server)</b>",
},
},
clientMutationId: "0",
})
),
createCommentReply: sinon.stub().callsFake((_: any, data: any) => {
expect(data.input).toMatchObject({
storyID: storyWithDeepestReplies.id,
parentID: "comment-with-deepest-replies-5",
parentRevisionID: "revision-0",
body: "<b>Hello world!</b>",
});
return {
edge: {
cursor: null,
node: {
...baseComment,
id: "comment-x",
author: users[0],
body: "<b>Hello world! (from server)</b>",
},
},
clientMutationId: data.input.clientMutationId,
};
}),
},
};
@@ -75,13 +75,12 @@ it("post a reply", async () => {
const { testRenderer, comment, rte, form } = await createTestRenderer({
Mutation: {
createCommentReply: sinon.stub().callsFake((_, data) => {
expect(data).toEqual({
expect(data).toMatchObject({
input: {
storyID: stories[0].id,
parentID: stories[0].comments.edges[0].node.id,
parentRevisionID: stories[0].comments.edges[0].node.revision.id,
body: "<b>Hello world!</b>",
clientMutationId: "0",
},
});
return {
@@ -94,7 +93,7 @@ it("post a reply", async () => {
body: "<b>Hello world! (from server)</b>",
},
},
clientMutationId: "0",
clientMutationId: data.input.clientMutationId,
};
}),
},
@@ -124,6 +123,50 @@ it("post a reply", async () => {
);
});
it("post a reply and handle non-visible comment state", async () => {
const { comment, rte, form } = await createTestRenderer({
Mutation: {
createCommentReply: sinon.stub().callsFake((_, data) => {
expect(data).toMatchObject({
input: {
storyID: stories[0].id,
parentID: stories[0].comments.edges[0].node.id,
parentRevisionID: stories[0].comments.edges[0].node.revision.id,
body: "<b>Hello world!</b>",
},
});
return {
edge: {
cursor: null,
node: {
...baseComment,
id: "comment-x",
status: "SYSTEM_WITHHELD",
author: users[0],
body: "<b>Hello world!</b>",
},
},
clientMutationId: data.input.clientMutationId,
};
}),
},
});
// Write reply .
rte.props.onChange({ html: "<b>Hello world!</b>" });
form.props.onSubmit();
// Test after server response.
await waitForElement(() =>
within(comment).getByText("will be reviewed", { exact: false })
);
await within(comment)
.getByText("Dismiss")
.props.onClick();
expect(
within(comment).queryByText("will be reviewed", { exact: false })
).toBeNull();
});
it("post a reply and handle server error", async () => {
const { rte, form, comment } = await createTestRenderer(
{
@@ -20,10 +20,9 @@ function createTestRenderer() {
},
Mutation: {
createCommentReaction: sinon.stub().callsFake((_: any, data: any) => {
expect(data.input).toEqual({
expect(data.input).toMatchObject({
commentID: stories[0].comments.edges[0].node.id,
commentRevisionID: stories[0].comments.edges[0].node.revision.id,
clientMutationId: "0",
});
return {
comment: {
@@ -32,13 +31,12 @@ function createTestRenderer() {
myActionPresence: { reaction: true },
actionCounts: { reaction: { total: 1 } },
},
clientMutationId: "0",
clientMutationId: data.input.clientMutationId,
};
}),
removeCommentReaction: sinon.stub().callsFake((_: any, data: any) => {
expect(data.input).toEqual({
expect(data.input).toMatchObject({
commentID: stories[0].comments.edges[0].node.id,
clientMutationId: "0",
});
return {
comment: {
@@ -47,7 +45,7 @@ function createTestRenderer() {
myActionPresence: { reaction: false },
actionCounts: { reaction: { total: 0 } },
},
clientMutationId: "0",
clientMutationId: data.input.clientMutationId,
};
}),
},
@@ -27,34 +27,32 @@ function createTestRenderer(
},
Mutation: {
createCommentFlag: sinon.stub().callsFake((_: any, data: any) => {
expect(data.input).toEqual({
expect(data.input).toMatchObject({
commentID: stories[0].comments.edges[0].node.id,
commentRevisionID: stories[0].comments.edges[0].node.revision.id,
reason: "COMMENT_REPORTED_OFFENSIVE",
additionalDetails: "More info",
clientMutationId: "0",
});
return {
comment: {
id: stories[0].comments.edges[0].node.id,
myActionPresence: { flag: true },
},
clientMutationId: "0",
clientMutationId: data.input.clientMutationId,
};
}),
createCommentDontAgree: sinon.stub().callsFake((_: any, data: any) => {
expect(data.input).toEqual({
expect(data.input).toMatchObject({
commentID: stories[0].comments.edges[0].node.id,
commentRevisionID: stories[0].comments.edges[0].node.revision.id,
additionalDetails: "More info",
clientMutationId: "0",
});
return {
comment: {
id: stories[0].comments.edges[0].node.id,
myActionPresence: { dontAgree: true },
},
clientMutationId: "0",
clientMutationId: data.input.clientMutationId,
};
}),
...resolver.Mutation,
+5
View File
@@ -70,14 +70,17 @@ export const users = [
{
id: "user-0",
username: "Markus",
role: "COMMENTER",
},
{
id: "user-1",
username: "Lukas",
role: "COMMENTER",
},
{
id: "user-2",
username: "Isabelle",
role: "COMMENTER",
},
];
@@ -87,6 +90,7 @@ export const baseComment = {
revision: {
id: "revision-0",
},
status: "NONE",
createdAt: "2018-07-06T18:24:00.000Z",
replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } },
replyCount: 0,
@@ -274,6 +278,7 @@ export const baseStory = {
metadata: {
title: "title",
},
moderation: "POST",
isClosed: false,
comments: {
edges: [],
@@ -48,6 +48,9 @@
&.colorRegular {
color: var(--palette-grey-main);
}
&.colorLight {
color: var(--palette-text-light);
}
&.colorPrimary {
color: var(--palette-primary-main);
}
@@ -71,6 +74,11 @@
color: var(--palette-grey-lighter);
}
}
&.colorLight {
&.mouseHover {
opacity: 0.8;
}
}
&.colorPrimary {
&.mouseHover {
color: var(--palette-primary-light);
@@ -346,6 +354,9 @@
&.colorRegular {
color: var(--palette-grey-main);
}
&.colorLight {
color: var(--palette-text-light);
}
&.colorPrimary {
color: var(--palette-primary-main);
}
@@ -369,6 +380,11 @@
color: var(--palette-grey-lighter);
}
}
&.colorLight {
&.mouseHover {
opacity: 0.8;
}
}
&.colorPrimary {
&.mouseHover {
color: var(--palette-primary-light);
@@ -26,7 +26,7 @@ interface Props extends Omit<BaseButtonProps, "ref"> {
size?: "small" | "regular" | "large";
/** Color of the button */
color?: "regular" | "primary" | "error" | "success" | "brand";
color?: "regular" | "primary" | "error" | "success" | "brand" | "light";
/** Variant of the button */
variant?: "regular" | "filled" | "outlined" | "ghost" | "underlined";
@@ -69,6 +69,7 @@ export class Button extends React.Component<Props> {
[classes.sizeSmall]: size === "small",
[classes.sizeLarge]: size === "large",
[classes.colorRegular]: color === "regular",
[classes.colorLight]: color === "light",
[classes.colorPrimary]: color === "primary",
[classes.colorError]: color === "error",
[classes.colorSuccess]: color === "success",
@@ -21,6 +21,16 @@
.colorError {
background-color: var(--palette-error-light);
border-color: var(--palette-error-darkest);
border-width: 0px;
border-left-width: calc(0.5 * var(--spacing-unit));
color: var(--palette-text-light);
}
.colorPrimary {
background-color: var(--palette-primary-light);
border-color: var(--palette-primary-darkest);
border-width: 0px;
border-left-width: calc(0.5 * var(--spacing-unit));
color: var(--palette-text-light);
}
@@ -3,26 +3,36 @@ name: Message
menu: UI Kit
---
import { Playground } from 'docz'
import Message from './Message'
import MessageIcon from './MessageIcon'
import HorizontalGutter from '../HorizontalGutter'
import { Playground } from "docz";
import Message from "./Message";
import MessageIcon from "./MessageIcon";
import HorizontalGutter from "../HorizontalGutter";
# Message
## Basic Use
<Playground>
<HorizontalGutter>
<Message>This is a message</Message>
<Message fullWidth>Contrary to popular belief, Lorem Ipsum is not simply random text.</Message>
<Message fullWidth>
Contrary to popular belief, Lorem Ipsum is not simply random text.
</Message>
<Message fullWidth color="error">
Contrary to popular belief, Lorem Ipsum is not simply random text.
</Message>
<Message fullWidth color="primary">
Contrary to popular belief, Lorem Ipsum is not simply random text.
</Message>
</HorizontalGutter>
</Playground>
## Usage with icon
<Playground>
<HorizontalGutter>
<Message><MessageIcon size="sm">alarm</MessageIcon>Edit: 1 min 23 secs Remaining</Message>
<Message>
<MessageIcon size="sm">alarm</MessageIcon>Edit: 1 min 23 secs Remaining
</Message>
</HorizontalGutter>
</Playground>
@@ -24,7 +24,7 @@ export interface MessageProps {
/*
* Name of color, "grey" stays by default - common gray one
*/
color?: "error" | "grey";
color?: "error" | "grey" | "primary";
}
const Message: StatelessComponent<MessageProps> = props => {
@@ -35,6 +35,7 @@ const Message: StatelessComponent<MessageProps> = props => {
{
[classes.colorGrey]: color === "grey",
[classes.colorError]: color === "error",
[classes.colorPrimary]: color === "primary",
[classes.fullWidth]: fullWidth,
},
className
@@ -13,12 +13,3 @@ export default function withForwardRef<P extends { forwardRef?: Ref<any> }>(
);
return React.forwardRef<any, P>(forwardRef) as any;
}
/*
// Stub, currently doesn't do anything except adding types.
export default function withForwardRef<P extends { forwardRef?: Ref<any> }>(
BaseComponent: React.ComponentType<P>
): React.ComponentType<P> {
return BaseComponent;
}
*/
@@ -14,5 +14,6 @@ export const Story: GQLStoryTypeResolver<story.Story> = {
closedAt: (s, input, ctx) => getStoryClosedAt(ctx.tenant, s),
commentActionCounts: s => decodeActionCounts(s.commentCounts.action),
commentCounts: s => s.commentCounts.status,
moderation: (s, input, ctx) => ctx.tenant.moderation,
moderationQueues: storyModerationInputResolver,
};
@@ -1581,6 +1581,11 @@ type Story {
createdAt is the date that the Story was created at.
"""
createdAt: Time!
"""
moderation determines whether or not this is a PRE or POST moderated story.
"""
moderation: MODERATION_MODE!
}
################################################################################
@@ -1697,7 +1702,7 @@ type CreateCommentPayload {
"""
edge is the possibly created comment edge.
"""
edge: CommentEdge
edge: CommentEdge!
"""
clientMutationId is required for Relay support.
@@ -1747,7 +1752,7 @@ type CreateCommentReplyPayload {
"""
edge is the possibly created comment edge.
"""
edge: CommentEdge
edge: CommentEdge!
"""
clientMutationId is required for Relay support.
@@ -1787,7 +1792,7 @@ type EditCommentPayload {
"""
comment is the possibly edited comment.
"""
comment: Comment
comment: Comment!
"""
clientMutationId is required for Relay support.
+5
View File
@@ -124,3 +124,8 @@ comments-reportPopover-receivedMessage =
comments-reportPopover-dismiss = Dismiss
## Submit Status
comments-submitStatus-dismiss = Dismiss
comments-submitStatus-submittedAndWillBeReviewed =
Your comment has been submitted and will be reviewed by a moderator