[next] Bugfixes (#2272)

* feat: suspending, banning, now propogation

* feat: new mutation api with hooks support

* [CORL-343] Center Spinner in Stream

* [CORL-344] Fix moderation card styling

* [CORL-338] Fix permalink reply bug

* [CORL-337] Fix community guidelines box width

* [CORL-341] Toggle reply form view when clicking on reply

* test: add tests

* [CORL-333] Fix bug: removing message box icon; [CORL-336] Fix bug: allow resetting custom css
This commit is contained in:
Kiwi
2019-04-23 22:29:58 +02:00
committed by Wyatt Johnson
parent 5150cdf60e
commit a92dcd6224
29 changed files with 745 additions and 439 deletions
@@ -46,7 +46,7 @@ const BanUserMutation = createMutation(
current: lookup<GQLUser>(
environment,
input.userID
)!.status!.current!.concat([GQLUSER_STATUS.BANNED]),
)!.status.current.concat([GQLUSER_STATUS.BANNED]),
ban: {
active: true,
},
@@ -46,7 +46,7 @@ const RemoveUserBanMutation = createMutation(
current: lookup<GQLUser>(
environment,
input.userID
)!.status!.current!.filter(s => s !== GQLUSER_STATUS.BANNED),
)!.status.current.filter(s => s !== GQLUSER_STATUS.BANNED),
ban: {
active: false,
},
@@ -2,6 +2,7 @@ import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { formatEmpty, parseEmptyAsNull } from "talk-framework/lib/form";
import {
FormField,
HorizontalGutter,
@@ -33,7 +34,7 @@ const CustomCSSConfig: StatelessComponent<Props> = ({ disabled }) => (
styles. Can be internal or external.
</Typography>
</Localized>
<Field name="customCSSURL">
<Field name="customCSSURL" parse={parseEmptyAsNull} format={formatEmpty}>
{({ input, meta }) => (
<>
<TextField
@@ -1,4 +1,6 @@
.root {
composes: root from "talk-stream/shared/htmlContent.css";
mark {
background-color: var(--palette-highlight);
padding: 0 2px;
@@ -26,6 +26,8 @@ import {
users,
} from "../fixtures";
const viewer = users.admins[0];
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/community");
});
@@ -43,7 +45,7 @@ const createTestRenderer = async (
expectAndFail(variables.role).toBeFalsy();
return communityUsers;
},
viewer: () => users.admins[0],
viewer: () => viewer,
},
}),
params.resolvers
@@ -113,7 +115,6 @@ it("filter by role", async () => {
});
it("can't change viewer role", async () => {
const viewer = users.admins[0];
const { container } = await createTestRenderer();
const viewerRow = within(container).getByText(viewer.username!, {
@@ -169,11 +170,11 @@ it("change user role", async () => {
});
it("can't change role as a moderator", async () => {
const viewer = users.moderators[0];
const moderator = users.moderators[0];
const { container } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
viewer: () => viewer,
viewer: () => moderator,
},
}),
});
@@ -190,8 +191,8 @@ it("load more", async () => {
return {
edges: [
{
node: users.admins[0],
cursor: users.admins[0].createdAt,
node: viewer,
cursor: viewer.createdAt,
},
{
node: users.commenters[0],
@@ -105,6 +105,43 @@ it("change custom css", async () => {
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
it("remove custom css", async () => {
const resolvers = createResolversStub<GQLResolver>({
Query: {
settings: () =>
pureMerge<typeof settings>(settings, {
customCSSURL: "./custom.css",
}),
},
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.customCSSURL).toBeNull();
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const { configureContainer, advancedContainer } = await createTestRenderer({
resolvers,
});
const customCSSField = within(advancedContainer).getByLabelText("Custom CSS");
// Let's change the customCSS field.
customCSSField.props.onChange("");
// Send form
within(configureContainer)
.getByType("form")
.props.onSubmit();
// Wait for submission to be finished
await wait(() => {
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
});
it("change permitted domains to be empty", async () => {
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
@@ -1,8 +0,0 @@
import { Environment } from "relay-runtime";
export default function getStory(environment: Environment, id: string) {
return environment
.getStore()
.getSource()
.get(id);
}
@@ -1,15 +0,0 @@
import { Environment } from "relay-runtime";
import getStory from "./getStory";
export default function getStorySettings(environment: Environment, id: string) {
const story = getStory(environment, id);
if (!story) {
return null;
}
const storySettingsRef = story.settings.__ref;
return environment
.getStore()
.getSource()
.get(storySettingsRef);
}
@@ -1,12 +1,13 @@
import { Environment } from "relay-runtime";
import { lookup } from "talk-framework/lib/relay";
import { GQLUser } from "talk-framework/schema";
import getViewerSourceID from "./getViewerSourceID";
export default function getViewer(environment: Environment) {
const source = environment.getStore().getSource();
const viewerID = getViewerSourceID(environment);
if (!viewerID) {
return null;
}
return source.get(viewerID)!;
return lookup<GQLUser>(environment, viewerID)!;
}
@@ -7,5 +7,3 @@ export { default as redirectOAuth2 } from "./redirectOAuth2";
export {
default as getParamsFromHashAndClearIt,
} from "./getParamsFromHashAndClearIt";
export { default as getStory } from "./getStory";
export { default as getStorySettings } from "./getStorySettings";
@@ -6,7 +6,7 @@ import { Environment, RelayInMemoryRecordSource } from "relay-runtime";
*/
type RecordSourceProxy<T> = T extends object
? {
readonly [P in keyof T]?: T[P] extends Array<infer U>
readonly [P in keyof T]: T[P] extends Array<infer U>
? ReadonlyArray<RecordSourceProxy<U>>
: T[P] extends ReadonlyArray<infer V>
? ReadonlyArray<RecordSourceProxy<V>>
@@ -1,33 +1,48 @@
export function denormalizeComment(comment: any, parents: any[] = []) {
const replyNodes =
import { GQLComment, GQLCommentEdge, GQLStory } from "talk-framework/schema";
import createFixture, { Fixture } from "./createFixture";
export function denormalizeComment(
comment: Fixture<GQLComment>,
parents: Array<Fixture<GQLCommentEdge>> = []
): GQLComment {
const replyEdges =
(comment.replies &&
comment.replies.edges.map((edge: any) =>
denormalizeComment(edge, [...parents, comment])
)) ||
comment.replies.edges &&
comment.replies.edges.map(edge => ({
...edge,
node:
edge.node &&
denormalizeComment(edge.node, [
...parents,
{ node: comment, cursor: comment.createdAt },
]),
}))) ||
[];
const repliesPageInfo = (comment.replies && comment.replies.pageInfo) || {
endCursor: null,
hasNextPage: false,
};
return {
return createFixture<GQLComment>({
...comment,
replies: { edges: replyNodes, pageInfo: repliesPageInfo },
replyCount: replyNodes.length,
replies: { edges: replyEdges, pageInfo: repliesPageInfo },
replyCount: replyEdges.length,
parentCount: parents.length,
parents: {
edges: parents,
pageInfo: { startCursor: null, hasPreviousPage: false },
},
};
});
}
export function denormalizeComments(commentList: any[]) {
export function denormalizeComments(commentList: Array<Fixture<GQLComment>>) {
return commentList.map(c => denormalizeComment(c));
}
export function denormalizeStory(story: any) {
export function denormalizeStory(story: Fixture<GQLStory>) {
const commentNodes =
(story.comments &&
story.comments.edges &&
story.comments.edges.map((edge: any) => ({
...edge,
node: denormalizeComment(edge.node),
@@ -46,6 +61,6 @@ export function denormalizeStory(story: any) {
};
}
export function denormalizeStories(storyList: any[]) {
export function denormalizeStories(storyList: Array<Fixture<GQLStory>>) {
return storyList.map(a => denormalizeStory(a));
}
@@ -6,15 +6,16 @@ import {
RecordSourceSelectorProxy,
} from "relay-runtime";
import { getStorySettings, getViewer } from "talk-framework/helpers";
import { getViewer } from "talk-framework/helpers";
import { TalkContext } from "talk-framework/lib/bootstrap";
import {
commitMutationPromiseNormalized,
createMutationContainer,
lookup,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { GQLUSER_ROLE } from "talk-framework/schema";
import { GQLStory, GQLUSER_ROLE } from "talk-framework/schema";
import { CreateCommentMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentMutation.graphql";
import {
@@ -111,7 +112,8 @@ function commit(
const currentDate = new Date().toISOString();
const id = uuidGenerator();
const storySettings = getStorySettings(relayEnvironment, input.storyID);
const storySettings = lookup<GQLStory>(relayEnvironment, input.storyID)!
.settings;
if (!storySettings || !storySettings.moderation) {
throw new Error("Moderation mode of the story was not included");
}
@@ -6,15 +6,16 @@ import {
RecordSourceSelectorProxy,
} from "relay-runtime";
import { getStorySettings, getViewer } from "talk-framework/helpers";
import { getViewer } from "talk-framework/helpers";
import { TalkContext } from "talk-framework/lib/bootstrap";
import {
commitMutationPromiseNormalized,
createMutationContainer,
lookup,
MutationInput,
MutationResponsePromise,
} from "talk-framework/lib/relay";
import { GQLUSER_ROLE } from "talk-framework/schema";
import { GQLStory, GQLUSER_ROLE } from "talk-framework/schema";
import { CreateCommentReplyMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentReplyMutation.graphql";
import {
@@ -137,8 +138,8 @@ function commit(
const viewer = getViewer(environment)!;
const currentDate = new Date().toISOString();
const id = uuidGenerator();
const storySettings = getStorySettings(relayEnvironment, input.storyID);
const storySettings = lookup<GQLStory>(relayEnvironment, input.storyID)!
.settings;
if (!storySettings || !storySettings.moderation) {
throw new Error("Moderation mode of the story was not included");
}
@@ -9,7 +9,7 @@ interface Props {
const CommunityGuidelines: StatelessComponent<Props> = props => {
return (
<CallOut color="primary">
<CallOut color="primary" fullWidth>
<Markdown>{props.children}</Markdown>
</CallOut>
);
@@ -4,7 +4,7 @@ import { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import UserBoxContainer from "talk-stream/containers/UserBoxContainer";
import { Button, HorizontalGutter, Spinner } from "talk-ui/components";
import { Button, Flex, HorizontalGutter, Spinner } from "talk-ui/components";
import CommentContainer from "../containers/CommentContainer";
import CommunityGuidelinesContainer from "../containers/CommunityGuidelinesContainer";
@@ -52,7 +52,11 @@ const Stream: StatelessComponent<StreamProps> = props => {
{props.comments.length > 0 && (
<SortMenu orderBy={props.orderBy} onChange={props.onChangeOrderBy} />
)}
{props.refetching && <Spinner />}
{props.refetching && (
<Flex justifyContent="center">
<Spinner />
</Flex>
)}
{!props.refetching && (
<HorizontalGutter
id="talk-comments-stream-log"
@@ -3,6 +3,7 @@
exports[`renders correctly 1`] = `
<withPropsOnChange(CallOut)
color="primary"
fullWidth={true}
>
<Markdown>
**bold**
@@ -88,10 +88,10 @@ export class CommentContainer extends Component<Props, State> {
);
}
private openReplyDialog = () => {
private toggleReplyDialog = () => {
if (this.props.viewer) {
this.setState(state => ({
showReplyDialog: true,
showReplyDialog: !state.showReplyDialog,
}));
} else {
this.props.showAuthPopup({ view: "SIGN_IN" });
@@ -206,7 +206,7 @@ export class CommentContainer extends Component<Props, State> {
id={`comments-commentContainer-replyButton-${
comment.id
}`}
onClick={this.openReplyDialog}
onClick={this.toggleReplyDialog}
active={showReplyDialog}
disabled={
settings.disableCommenting.enabled || story.isClosed
@@ -8,7 +8,7 @@ import {
} from "talk-framework/lib/relay";
import { StreamQuery as QueryTypes } from "talk-stream/__generated__/StreamQuery.graphql";
import { StreamQueryLocal as Local } from "talk-stream/__generated__/StreamQueryLocal.graphql";
import { Delay, Spinner } from "talk-ui/components";
import { Delay, Flex, Spinner } from "talk-ui/components";
import StreamContainer from "../containers/StreamContainer";
interface Props {
@@ -43,7 +43,9 @@ export const render = (
return (
<Delay>
<Spinner />
<Flex justifyContent="center">
<Spinner />
</Flex>
</Delay>
);
};
@@ -10,7 +10,11 @@ exports[`renders loading 1`] = `
<Delay
ms={500}
>
<withPropsOnChange(Spinner) />
<ForwardRef(forwardRef)
justifyContent="center"
>
<withPropsOnChange(Spinner) />
</ForwardRef(forwardRef)>
</Delay>
`;
@@ -68,6 +68,8 @@ const enhanced = withContext(ctx => ({
fragment PermalinkViewContainer_story on Story {
...ConversationThreadContainer_story
...ReplyListContainer1_story
...CreateCommentMutation_story
...CreateCommentReplyMutation_story
}
`,
comment: graphql`
@@ -82,6 +84,8 @@ const enhanced = withContext(ctx => ({
...ConversationThreadContainer_viewer
...ReplyListContainer1_viewer
...UserBoxContainer_viewer
...CreateCommentMutation_viewer
...CreateCommentReplyMutation_viewer
}
`,
settings: graphql`
@@ -3,7 +3,11 @@ import React, { StatelessComponent, Suspense } from "react";
import { Field } from "react-final-form";
import { MarkdownEditor } from "talk-framework/components/loadables";
import { parseBool } from "talk-framework/lib/form";
import {
formatEmpty,
parseBool,
parseEmptyAsNull,
} from "talk-framework/lib/form";
import {
MessageBox,
MessageBoxContent,
@@ -54,7 +58,11 @@ const MessageBoxConfig: StatelessComponent<Props> = ({ disabled }) => (
</WidthLimitedDescription>
</Localized>
{input.checked && (
<Field name="messageBox.icon">
<Field
name="messageBox.icon"
parse={parseEmptyAsNull}
format={formatEmpty}
>
{({ input: iconInput }) => (
<Field name="messageBox.content">
{({ input: contentInput, meta }) => (
@@ -40,7 +40,7 @@ exports[`renders comment stream with community guidelines 1`] = `
</button>
</div>
<div
className="CallOut-root CallOut-colorPrimary"
className="CallOut-root CallOut-colorPrimary CallOut-fullWidth"
>
<div
className="Markdown-root"
@@ -0,0 +1,139 @@
import RTE from "@coralproject/rte";
import { pureMerge } from "talk-common/utils";
import { GQLResolver } from "talk-framework/schema";
import {
createResolversStub,
CreateTestRendererParams,
findParentWithType,
waitForElement,
within,
} from "talk-framework/testHelpers";
import {
baseComment,
commenters,
comments,
settings,
stories,
} from "../fixtures";
import create from "./create";
const commentFixture = comments[0];
const storyFixture = {
...stories[0],
comments: {
pageInfo: {
hasNextPage: false,
},
edges: [
{
node: commentFixture,
cursor: commentFixture.createdAt,
},
],
},
};
const createTestRenderer = async (
params: CreateTestRendererParams<GQLResolver> = {}
) => {
const { testRenderer, context } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => commenters[0],
comment: ({ variables }) => {
expectAndFail(variables.id).toBe(commentFixture.id);
return commentFixture;
},
story: ({ variables }) => {
expectAndFail(variables.id).toBe(storyFixture.id);
return storyFixture;
},
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(storyFixture.id, "storyID");
localRecord.setValue(commentFixture.id, "commentID");
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const comment = await waitForElement(() =>
within(testRenderer.root).getByTestID("comment-comment-0")
);
// Open reply form.
within(comment)
.getByText("Reply", { selector: "button" })
.props.onClick();
const rte = await waitForElement(
() =>
findParentWithType(
within(comment).getByLabelText("Write a reply"),
// We'll use the RTE component here as an exception because the
// jsdom does not support all of what is needed for rendering the
// Rich Text Editor.
RTE
)!
);
const form = findParentWithType(rte, "form")!;
return {
testRenderer,
context,
comment,
rte,
form,
};
};
it("post a reply", async () => {
const { testRenderer, rte, form } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Mutation: {
createCommentReply: ({ variables }) => {
expectAndFail(variables).toMatchObject({
storyID: storyFixture.id,
parentID: storyFixture.comments.edges[0].node.id,
parentRevisionID: storyFixture.comments.edges[0].node.revision.id,
body: "<b>Hello world!</b>",
});
return {
edge: {
cursor: "",
node: {
...baseComment,
id: "comment-x",
author: commenters[0],
body: "<b>Hello world! (from server)</b>",
},
},
};
},
},
}),
});
// Write reply .
rte.props.onChange({ html: "<b>Hello world!</b>" });
form.props.onSubmit();
const commentReplyList = within(testRenderer.root).getByTestID(
"commentReplyList-comment-0"
);
// Test after server response.
await waitForElement(() =>
within(commentReplyList).getByText("(from server)", { exact: false })
);
});
@@ -18,7 +18,7 @@ import { baseComment, commenters, settings, stories } from "../fixtures";
import create from "./create";
async function createTestRenderer(
resolver: any,
resolver: any = {},
options: { muteNetworkErrors?: boolean } = {}
) {
const resolvers = {
@@ -50,9 +50,11 @@ async function createTestRenderer(
);
// Open reply form.
within(comment)
.getByText("Reply", { selector: "button" })
.props.onClick();
const replyButton = within(comment).getByText("Reply", {
selector: "button",
});
replyButton.props.onClick();
const rte = await waitForElement(
() =>
@@ -70,11 +72,18 @@ async function createTestRenderer(
testRenderer,
context,
comment,
replyButton,
rte,
form,
};
}
it("hides form when reclicking on the reply button", async () => {
const { comment, replyButton } = await createTestRenderer();
replyButton.props.onClick();
expect(within(comment).queryByLabelText("Write a reply")).toBeNull();
});
it("post a reply", async () => {
const { testRenderer, comment, rte, form } = await createTestRenderer({
Mutation: {
@@ -1,9 +1,14 @@
import { cloneDeep } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import {
GQLResolver,
MutationToUpdateStorySettingsResolver,
} from "talk-framework/schema";
import {
createMutationResolverStub,
createResolversStub,
CreateTestRendererParams,
findParentWithType,
replaceHistoryLocation,
wait,
waitForElement,
within,
@@ -12,30 +17,36 @@ import {
import { moderators, settings, stories } from "../fixtures";
import create from "./create";
async function createTestRenderer(
resolver: any = {},
options: { muteNetworkErrors?: boolean; status?: string } = {}
) {
const resolvers = {
Query: {
settings: sinon.stub().returns(settings),
story: sinon.stub().callsFake((_: any, variables: any) => {
expectAndFail(variables).toEqual({ id: stories[0].id, url: null });
return stories[0];
}),
viewer: sinon.stub().returns(moderators[0]),
...resolver.Query,
},
...resolver,
};
const viewer = moderators[0];
const story = stories[0];
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/community");
});
const createTestRenderer = async (
params: CreateTestRendererParams<GQLResolver> = {}
) => {
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(stories[0].id, "storyID");
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
story: ({ variables }) => {
expectAndFail(variables).toEqual({ id: story.id, url: null });
return story;
},
viewer: () => viewer,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(story.id, "storyID");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
@@ -46,24 +57,23 @@ async function createTestRenderer(
const form = findParentWithType(applyButton, "form")!;
return { testRenderer, tabPane, applyButton, form };
}
};
it("change premod", async () => {
let storyRecord = cloneDeep(stories[0]);
const updateStorySettingsStub = sinon
.stub()
.callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.moderation).toEqual("PRE");
storyRecord = pureMerge(storyRecord, { settings: data.input.settings });
return {
story: storyRecord,
clientMutationId: data.input.clientMutationId,
};
});
const updateStorySettingsStub = createMutationResolverStub<
MutationToUpdateStorySettingsResolver
>(({ variables }) => {
expectAndFail(variables.settings.moderation).toEqual("PRE");
return {
story: pureMerge(story, { settings: variables.settings }),
};
});
const { form, applyButton } = await createTestRenderer({
Mutation: {
updateStorySettings: updateStorySettingsStub,
},
resolvers: createResolversStub<GQLResolver>({
Mutation: {
updateStorySettings: updateStorySettingsStub,
},
}),
});
const premodField = within(form).getByLabelText("Enable Pre-Moderation");
@@ -89,21 +99,20 @@ it("change premod", async () => {
});
it("change premod links", async () => {
let storyRecord = cloneDeep(stories[0]);
const updateStorySettingsStub = sinon
.stub()
.callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.premodLinksEnable).toEqual(true);
storyRecord = pureMerge(storyRecord, { settings: data.input.settings });
return {
story: storyRecord,
clientMutationId: data.input.clientMutationId,
};
});
const updateStorySettingsStub = createMutationResolverStub<
MutationToUpdateStorySettingsResolver
>(({ variables }) => {
expectAndFail(variables.settings.premodLinksEnable).toEqual(true);
return {
story: pureMerge(story, { settings: variables.settings }),
};
});
const { form, applyButton } = await createTestRenderer({
Mutation: {
updateStorySettings: updateStorySettingsStub,
},
resolvers: createResolversStub<GQLResolver>({
Mutation: {
updateStorySettings: updateStorySettingsStub,
},
}),
});
const premodLinksField = within(form).getByLabelText(
@@ -131,25 +140,24 @@ it("change premod links", async () => {
});
it("change message box", async () => {
let storyRecord = cloneDeep(stories[0]);
const updateStorySettingsStub = sinon
.stub()
.callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.messageBox).toEqual({
enabled: true,
content: "*What do you think?*",
icon: "question_answer",
});
storyRecord = pureMerge(storyRecord, { settings: data.input.settings });
return {
story: storyRecord,
clientMutationId: data.input.clientMutationId,
};
const updateStorySettingsStub = createMutationResolverStub<
MutationToUpdateStorySettingsResolver
>(({ variables }) => {
expectAndFail(variables.settings.messageBox).toEqual({
enabled: true,
content: "*What do you think?*",
icon: "question_answer",
});
return {
story: pureMerge(story, { settings: variables.settings }),
};
});
const { form, applyButton } = await createTestRenderer({
Mutation: {
updateStorySettings: updateStorySettingsStub,
},
resolvers: createResolversStub<GQLResolver>({
Mutation: {
updateStorySettings: updateStorySettingsStub,
},
}),
});
const enableField = within(form).getByLabelText(
@@ -162,9 +170,9 @@ it("change message box", async () => {
expect(applyButton.props.disabled).toBe(false);
// Select icon
within(form)
.getByLabelText("question_answer")
.props.onChange({ target: { value: "question_answer" } });
const iconButton = within(form).getByLabelText("question_answer");
iconButton.props.onChange({ target: { value: iconButton.props.value } });
// Change content.
(await waitForElement(() =>
@@ -185,3 +193,52 @@ it("change message box", async () => {
// Should have successfully sent with server.
expect(updateStorySettingsStub.called).toBe(true);
});
it("remove message icon", async () => {
const updateStorySettingsStub = createMutationResolverStub<
MutationToUpdateStorySettingsResolver
>(({ variables }) => {
expectAndFail(variables.settings.messageBox).toEqual({
enabled: true,
content: "*What do you think?*",
icon: null,
});
return {
story: pureMerge(story, { settings: variables.settings }),
};
});
const { form, applyButton } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
story: () =>
pureMerge<typeof story>(story, {
settings: {
messageBox: {
enabled: true,
content: "*What do you think?*",
icon: "question_answer",
},
},
}),
},
Mutation: {
updateStorySettings: updateStorySettingsStub,
},
}),
});
// Select icon
const noIconButton = within(form).getByLabelText("No Icon");
noIconButton.props.onChange({ target: { value: noIconButton.props.value } });
// Send form
form.props.onSubmit();
expect(applyButton.props.disabled).toBe(true);
// Wait for submission to be finished
await wait(() => {
expect(updateStorySettingsStub.called).toBe(true);
});
});
+334 -290
View File
@@ -1,14 +1,24 @@
import { GQLUSER_ROLE } from "talk-framework/schema";
import {
GQLComment,
GQLCOMMENT_STATUS,
GQLMODERATION_MODE,
GQLSettings,
GQLStory,
GQLUser,
GQLUSER_ROLE,
} from "talk-framework/schema";
import {
createFixture,
createFixtures,
denormalizeComment,
denormalizeComments,
denormalizeStories,
denormalizeStory,
} from "talk-framework/testHelpers";
export const settings = {
export const settings = createFixture<GQLSettings>({
id: "settings",
moderation: "POST",
moderation: GQLMODERATION_MODE.POST,
premodLinksEnable: false,
communityGuidelines: {
enabled: false,
@@ -21,9 +31,8 @@ export const settings = {
closeCommenting: {
auto: false,
message: "Story is closed",
timeout: null,
timeout: undefined,
},
closedAt: null,
auth: {
integrations: {
facebook: {
@@ -71,9 +80,9 @@ export const settings = {
charCount: {
enabled: false,
},
};
});
export const commenters = [
export const commenters = createFixtures<GQLUser>([
{
id: "user-0",
username: "Markus",
@@ -94,15 +103,15 @@ export const commenters = [
username: "Markus",
role: GQLUSER_ROLE.COMMENTER,
},
];
]);
export const baseComment = {
export const baseComment = createFixture<GQLComment>({
author: commenters[0],
body: "Comment Body",
revision: {
id: "revision-0",
},
status: "NONE",
status: GQLCOMMENT_STATUS.NONE,
createdAt: "2018-07-06T18:24:00.000Z",
replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } },
replyCount: 0,
@@ -116,178 +125,188 @@ export const baseComment = {
},
},
tags: [],
};
export const comments = denormalizeComments([
{
...baseComment,
id: "comment-0",
author: commenters[0],
body: "Joining Too",
},
{
...baseComment,
id: "comment-1",
author: commenters[1],
body: "What's up?",
},
{
...baseComment,
id: "comment-2",
author: commenters[2],
body: "Hey!",
},
{
...baseComment,
id: "comment-3",
author: commenters[2],
body: "Comment Body 3",
},
{
...baseComment,
id: "comment-4",
author: commenters[2],
body: "Comment Body 4",
},
{
...baseComment,
id: "comment-5",
author: commenters[2],
body: "Comment Body 5",
},
]);
export const commentWithReplies = denormalizeComment({
...baseComment,
id: "comment-with-replies",
author: commenters[0],
body: "I like yoghurt",
replies: {
edges: [
{ node: comments[3], cursor: comments[3].createdAt },
{ node: comments[4], cursor: comments[4].createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
replyCount: 2,
});
export const commentWithDeepReplies = denormalizeComment({
...baseComment,
id: "comment-with-deep-replies",
author: commenters[0],
body: "I like yoghurt",
replies: {
edges: [
{ node: commentWithReplies, cursor: commentWithReplies.createdAt },
{ node: comments[5], cursor: comments[5].createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
replyCount: 2,
});
export const commentWithDeepestReplies = denormalizeComment({
...baseComment,
id: "comment-with-deepest-replies",
body: "body 0",
replyCount: 1,
replies: {
...baseComment.replies,
edges: [
export const comments = denormalizeComments(
createFixtures<GQLComment>(
[
{
cursor: baseComment.createdAt,
node: {
...baseComment,
id: "comment-with-deepest-replies-1",
body: "body 1",
replyCount: 1,
replies: {
...baseComment.replies,
edges: [
{
cursor: baseComment.createdAt,
node: {
...baseComment,
id: "comment-with-deepest-replies-2",
body: "body 2",
replyCount: 1,
replies: {
...baseComment.replies,
edges: [
{
cursor: baseComment.createdAt,
node: {
...baseComment,
id: "comment-with-deepest-replies-3",
body: "body 3",
replyCount: 1,
replies: {
...baseComment.replies,
edges: [
{
cursor: baseComment.createdAt,
node: {
...baseComment,
id: "comment-with-deepest-replies-4",
body: "body 4",
replyCount: 1,
replies: {
...baseComment.replies,
edges: [
{
cursor: baseComment.createdAt,
node: {
...baseComment,
id: "comment-with-deepest-replies-5",
body: "body 5",
replyCount: 1,
replies: {
...baseComment.replies,
edges: [
{
cursor: baseComment.createdAt,
node: {
...baseComment,
id:
"comment-with-deepest-replies-6",
body: "body 6",
replyCount: 1,
replies: {
...baseComment.replies,
edges: [],
},
},
},
],
},
},
},
],
},
},
},
],
},
},
},
],
},
},
},
],
},
},
id: "comment-0",
author: commenters[0],
body: "Joining Too",
},
{
id: "comment-1",
author: commenters[1],
body: "What's up?",
},
{
id: "comment-2",
author: commenters[2],
body: "Hey!",
},
{
id: "comment-3",
author: commenters[2],
body: "Comment Body 3",
},
{
id: "comment-4",
author: commenters[2],
body: "Comment Body 4",
},
{
id: "comment-5",
author: commenters[2],
body: "Comment Body 5",
},
],
},
});
baseComment
)
);
export const baseStory = {
export const commentWithReplies = denormalizeComment(
createFixture<GQLComment>(
{
id: "comment-with-replies",
author: commenters[0],
body: "I like yoghurt",
replies: {
edges: [
{ node: comments[3], cursor: comments[3].createdAt },
{ node: comments[4], cursor: comments[4].createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
replyCount: 2,
},
baseComment
)
);
export const commentWithDeepReplies = denormalizeComment(
createFixture<GQLComment>(
{
id: "comment-with-deep-replies",
author: commenters[0],
body: "I like yoghurt",
replies: {
edges: [
{ node: commentWithReplies, cursor: commentWithReplies.createdAt },
{ node: comments[5], cursor: comments[5].createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
replyCount: 2,
},
baseComment
)
);
export const commentWithDeepestReplies = denormalizeComment(
createFixture<GQLComment>({
...baseComment,
id: "comment-with-deepest-replies",
body: "body 0",
replyCount: 1,
replies: {
...baseComment.replies,
edges: [
{
cursor: baseComment.createdAt,
node: {
...baseComment,
id: "comment-with-deepest-replies-1",
body: "body 1",
replyCount: 1,
replies: {
...baseComment.replies,
edges: [
{
cursor: baseComment.createdAt,
node: {
...baseComment,
id: "comment-with-deepest-replies-2",
body: "body 2",
replyCount: 1,
replies: {
...baseComment.replies,
edges: [
{
cursor: baseComment.createdAt,
node: {
...baseComment,
id: "comment-with-deepest-replies-3",
body: "body 3",
replyCount: 1,
replies: {
...baseComment.replies,
edges: [
{
cursor: baseComment.createdAt,
node: {
...baseComment,
id: "comment-with-deepest-replies-4",
body: "body 4",
replyCount: 1,
replies: {
...baseComment.replies,
edges: [
{
cursor: baseComment.createdAt,
node: {
...baseComment,
id:
"comment-with-deepest-replies-5",
body: "body 5",
replyCount: 1,
replies: {
...baseComment.replies,
edges: [
{
cursor: baseComment.createdAt,
node: {
...baseComment,
id:
"comment-with-deepest-replies-6",
body: "body 6",
replyCount: 1,
replies: {
...baseComment.replies,
edges: [],
},
},
},
],
},
},
},
],
},
},
},
],
},
},
},
],
},
},
},
],
},
},
},
],
},
})
);
export const baseStory = createFixture<GQLStory>({
metadata: {
title: "title",
},
@@ -302,140 +321,165 @@ export const baseStory = {
totalVisible: 0,
},
settings: {
moderation: "POST",
moderation: GQLMODERATION_MODE.POST,
premodLinksEnable: false,
messageBox: {
enabled: false,
},
},
};
});
export const moderators = [
export const moderators = createFixtures<GQLUser>([
{
id: "me-as-moderator",
username: "Moderator",
role: GQLUSER_ROLE.MODERATOR,
},
];
export const commentsFromStaff = denormalizeComments([
{
...baseComment,
id: "comment-from-staff-0",
author: moderators[0],
body: "Joining Too",
tags: [{ name: "Staff" }],
},
]);
export const stories = denormalizeStories([
{
...baseStory,
id: "story-1",
url: "http://localhost/stories/story-1",
comments: {
edges: [
{ node: comments[0], cursor: comments[0].createdAt },
{ node: comments[1], cursor: comments[1].createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
},
{
...baseStory,
id: "story-2",
url: "http://localhost/stories/story-2",
comments: {
edges: [
{ node: comments[2], cursor: comments[2].createdAt },
{ node: comments[3], cursor: comments[3].createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
},
{
...baseStory,
id: "story-3",
url: "http://localhost/stories/story-3",
comments: {
edges: [
{ node: comments[0], cursor: comments[0].createdAt },
{ node: commentsFromStaff[0], cursor: commentsFromStaff[0].createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
},
]);
export const storyWithNoComments = denormalizeStory({
...baseStory,
id: "story-with-no-comments",
url: "http://localhost/stories/story-with-no-comments",
comments: {
edges: [],
pageInfo: {
hasNextPage: false,
},
},
});
export const storyWithReplies = denormalizeStory({
...baseStory,
id: "story-with-replies",
url: "http://localhost/stories/story-with-replies",
comments: {
edges: [
{ node: comments[0], cursor: comments[0].createdAt },
{ node: commentWithReplies, cursor: commentWithReplies.createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
});
export const storyWithDeepReplies = denormalizeStory({
...baseStory,
id: "story-with-deep-replies",
url: "http://localhost/stories/story-with-replies",
comments: {
edges: [
{ node: comments[0], cursor: comments[0].createdAt },
export const commentsFromStaff = denormalizeComments(
createFixtures<GQLComment>(
[
{
node: commentWithDeepReplies,
cursor: commentWithDeepReplies.createdAt,
id: "comment-from-staff-0",
author: moderators[0],
body: "Joining Too",
tags: [{ name: "Staff" }],
},
],
pageInfo: {
hasNextPage: false,
},
},
});
baseComment
)
);
export const storyWithDeepestReplies = denormalizeStory({
...baseStory,
id: "story-with-deepest-replies",
url: "http://localhost/stories/story-with-replies",
comments: {
edges: [
export const stories = denormalizeStories(
createFixtures<GQLStory>(
[
{
node: commentWithDeepestReplies,
cursor: commentWithDeepestReplies.createdAt,
id: "story-1",
url: "http://localhost/stories/story-1",
comments: {
edges: [
{ node: comments[0], cursor: comments[0].createdAt },
{ node: comments[1], cursor: comments[1].createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
},
{
id: "story-2",
url: "http://localhost/stories/story-2",
comments: {
edges: [
{ node: comments[2], cursor: comments[2].createdAt },
{ node: comments[3], cursor: comments[3].createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
},
{
id: "story-3",
url: "http://localhost/stories/story-3",
comments: {
edges: [
{ node: comments[0], cursor: comments[0].createdAt },
{
node: commentsFromStaff[0],
cursor: commentsFromStaff[0].createdAt,
},
],
pageInfo: {
hasNextPage: false,
},
},
},
],
pageInfo: {
hasNextPage: false,
},
},
});
baseStory
)
);
export const viewerWithComments = {
export const storyWithNoComments = denormalizeStory(
createFixture<GQLStory>(
{
id: "story-with-no-comments",
url: "http://localhost/stories/story-with-no-comments",
comments: {
edges: [],
pageInfo: {
hasNextPage: false,
},
},
},
baseStory
)
);
export const storyWithReplies = denormalizeStory(
createFixture<GQLStory>(
{
id: "story-with-replies",
url: "http://localhost/stories/story-with-replies",
comments: {
edges: [
{ node: comments[0], cursor: comments[0].createdAt },
{ node: commentWithReplies, cursor: commentWithReplies.createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
},
baseStory
)
);
export const storyWithDeepReplies = denormalizeStory(
createFixture<GQLStory>(
{
id: "story-with-deep-replies",
url: "http://localhost/stories/story-with-replies",
comments: {
edges: [
{ node: comments[0], cursor: comments[0].createdAt },
{
node: commentWithDeepReplies,
cursor: commentWithDeepReplies.createdAt,
},
],
pageInfo: {
hasNextPage: false,
},
},
},
baseStory
)
);
export const storyWithDeepestReplies = denormalizeStory(
createFixture<GQLStory>(
{
id: "story-with-deepest-replies",
url: "http://localhost/stories/story-with-replies",
comments: {
edges: [
{
node: commentWithDeepestReplies,
cursor: commentWithDeepestReplies.createdAt,
},
],
pageInfo: {
hasNextPage: false,
},
},
},
baseStory
)
);
export const viewerWithComments = createFixture<GQLUser>({
id: "me-with-comments",
username: "Markus",
role: GQLUSER_ROLE.COMMENTER,
@@ -454,4 +498,4 @@ export const viewerWithComments = {
hasNextPage: false,
},
},
};
});
@@ -26,7 +26,7 @@ const TileSelector: StatelessComponent<Props> = props => {
const { id, name, value, className, children, onChange } = props;
const onItemChange = useCallback(
(evt: React.ChangeEvent<HTMLInputElement>) =>
onChange && onChange(evt.target.value || null),
onChange && onChange(evt.target.value),
[onChange]
);
return (
+1 -2
View File
@@ -1,4 +1,3 @@
import { isNull, omitBy } from "lodash";
import { Db, MongoError } from "mongodb";
import uuid from "uuid";
@@ -337,7 +336,7 @@ export async function updateStorySettings(
// Only update fields that have been updated.
const update = {
$set: {
...omitBy(dotize({ settings: input }, { embedArrays: true }), isNull),
...dotize({ settings: input }, { embedArrays: true }),
// Always update the updated at time.
updatedAt: now,
},