mirror of
https://github.com/wassname/talk.git
synced 2026-07-29 11:28:24 +08:00
[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:
@@ -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`
|
||||
|
||||
@@ -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,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`
|
||||
|
||||
+14
@@ -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}
|
||||
|
||||
+4
@@ -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}
|
||||
/>
|
||||
`;
|
||||
|
||||
+4
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user