[CORL-161, CORL-240] Message Box (#2222)

* feat: Message Box

* test: do all the testing stuff

* fix: removed old messageBox

* Update src/core/client/stream/tabs/configure/components/MessageBoxConfig.tsx

Co-Authored-By: wyattjoh <wyattjoh@gmail.com>

* fix: addressed space error
This commit is contained in:
Kiwi
2019-03-19 00:08:22 +01:00
committed by GitHub
parent 9eb5afbb2b
commit 7501155078
63 changed files with 1932 additions and 432 deletions
@@ -2,6 +2,7 @@ import { Localized } from "fluent-react/compat";
import React, { StatelessComponent, Suspense } from "react";
import { Field } from "react-final-form";
import { MarkdownEditor } from "talk-framework/components/loadables";
import {
HorizontalGutter,
Spinner,
@@ -10,7 +11,6 @@ import {
} from "talk-ui/components";
import Header from "../../../components/Header";
import LazyMarkdown from "./LazyMarkdown";
interface Props {
disabled: boolean;
@@ -39,7 +39,7 @@ const ClosedStreamMessageConfig: StatelessComponent<Props> = ({ disabled }) => (
{({ input, meta }) => (
<>
<Suspense fallback={<Spinner />}>
<LazyMarkdown
<MarkdownEditor
id="configure-general-closedStreamMessage-content"
name={input.name}
onChange={input.onChange}
@@ -14,8 +14,9 @@ import {
} from "talk-ui/components";
import OnOffField from "talk-admin/routes/configure/components/OnOffField";
import { MarkdownEditor } from "talk-framework/components/loadables";
import Header from "../../../components/Header";
import LazyMarkdown from "./LazyMarkdown";
interface Props {
disabled: boolean;
@@ -60,7 +61,7 @@ const GuidelinesConfig: StatelessComponent<Props> = ({ disabled }) => (
{({ input, meta }) => (
<>
<Suspense fallback={<Spinner />}>
<LazyMarkdown
<MarkdownEditor
id="configure-general-guidelines-content"
name={input.name}
onChange={input.onChange}
@@ -13,8 +13,9 @@ import {
} from "talk-ui/components";
import OnOffField from "talk-admin/routes/configure/components/OnOffField";
import { MarkdownEditor } from "talk-framework/components/loadables";
import Header from "../../../components/Header";
import LazyMarkdown from "./LazyMarkdown";
interface Props {
disabled: boolean;
@@ -73,7 +74,7 @@ const SitewideCommentingConfig: StatelessComponent<Props> = ({ disabled }) => (
{({ input, meta }) => (
<>
<Suspense fallback={<Spinner />}>
<LazyMarkdown
<MarkdownEditor
id="configure-general-sitewideCommenting-message"
name={input.name}
onChange={input.onChange}
@@ -3,10 +3,10 @@ import React from "react";
import { graphql } from "react-relay";
import { GeneralConfigRouteContainerQueryResponse } from "talk-admin/__generated__/GeneralConfigRouteContainerQuery.graphql";
import { loadMarkdownEditor } from "talk-framework/components/loadables";
import { withRouteConfig } from "talk-framework/lib/router";
import { Delay, Spinner } from "talk-ui/components";
import { loadMarkdownEditor } from "../components/LazyMarkdown";
import GeneralConfigContainer from "./GeneralConfigContainer";
interface Props {
+2
View File
@@ -2,6 +2,8 @@ import { merge } from "lodash";
export const settings = {
id: "settings",
moderation: "POST",
premodLinksEnable: false,
wordList: {
suspect: ["idiot", "stupid"],
banned: ["fuck"],
@@ -1,7 +1,7 @@
import React from "react";
export function loadMarkdownEditor() {
return import("talk-framework/components/loadables/MarkdownEditor" /* webpackChunkName: "markdownEditor" */);
return import("./MarkdownEditor" /* webpackChunkName: "markdownEditor" */);
}
export default React.lazy(loadMarkdownEditor);
@@ -0,0 +1,4 @@
export {
default as MarkdownEditor,
loadMarkdownEditor,
} from "./MarkdownEditor/LazyMarkdownEditory";
@@ -0,0 +1,10 @@
.root {
display: flex;
position: relative;
justify-content: flex-start;
align-items: center;
padding: calc(0.5 * var(--spacing-unit) + 1px) var(--spacing-unit);
box-sizing: border-box;
width: 100%;
background-color: var(--palette-text-primary);
}
@@ -0,0 +1,25 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import { PropTypesOf } from "talk-ui/types";
import MessageBox from "./MessageBox";
import MessageBoxIcon from "./MessageBoxIcon";
it("renders correctly", () => {
const props: PropTypesOf<typeof MessageBox> = {
className: "custom",
children: "Hello World",
};
const renderer = TestRenderer.create(<MessageBox {...props} />);
expect(renderer.toJSON()).toMatchSnapshot();
});
it("renders icon", () => {
const renderer = TestRenderer.create(
<MessageBox>
<MessageBoxIcon>alert</MessageBoxIcon>Alert MessageBox
</MessageBox>
);
expect(renderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,35 @@
import cn from "classnames";
import React, { ReactNode, StatelessComponent } from "react";
import { withStyles } from "talk-ui/hocs";
import styles from "./MessageBox.css";
interface Props {
/**
* The content of the component.
*/
children: ReactNode;
/**
* Convenient prop to override the root styling.
*/
className?: string;
/**
* Override or extend the styles applied to the component.
*/
classes: typeof styles;
}
const MessageBox: StatelessComponent<Props> = props => {
const { className, classes, children, ...rest } = props;
const rootClassName = cn(classes.root, className);
return (
<div className={rootClassName} {...rest}>
{children}
</div>
);
};
const enhanced = withStyles(styles)(MessageBox);
export default enhanced;
@@ -0,0 +1,4 @@
.root {
composes: root from "talk-stream/shared/htmlContent.css";
color: var(--palette-text-light);
}
@@ -0,0 +1,37 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import { Markdown } from "talk-framework/components";
import { withStyles } from "talk-ui/hocs";
import styles from "./MessageBoxContent.css";
interface Props {
/**
* The content of the component.
*/
children: string;
/**
* Convenient prop to override the root styling.
*/
className?: string;
/**
* Override or extend the styles applied to the component.
*/
classes: typeof styles;
}
const MessageBox: StatelessComponent<Props> = props => {
const { className, classes, children, ...rest } = props;
const rootClassName = cn(classes.root, className);
return (
<Markdown className={rootClassName} {...rest}>
{children}
</Markdown>
);
};
const enhanced = withStyles(styles)(MessageBox);
export default enhanced;
@@ -0,0 +1,9 @@
.root {
align-self: flex-start;
margin-top: 1px;
flex-shrink: 0;
color: var(--palette-text-light);
&:first-child {
margin-right: calc(0.5 * var(--spacing-unit));
}
}
@@ -0,0 +1,37 @@
import cn from "classnames";
import React, { HTMLAttributes, Ref, StatelessComponent } from "react";
import Icon, { IconProps } from "talk-ui/components/Icon";
import { withForwardRef, withStyles } from "talk-ui/hocs";
import { Omit } from "talk-ui/types";
import styles from "./MessageBoxIcon.css";
interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "color"> {
/**
* This prop can be used to add custom classnames.
* It is handled by the `withStyles `HOC.
*/
classes: typeof styles & IconProps["classes"];
size?: IconProps["size"];
/** The name of the icon to render */
children: string;
/** Internal: Forwarded Ref */
forwardRef?: Ref<HTMLSpanElement>;
}
export const MessageBoxIcon: StatelessComponent<Props> = props => {
const { classes, className, forwardRef, ...rest } = props;
const rootClassName = cn(classes.root, className);
return <Icon className={rootClassName} {...rest} ref={forwardRef} />;
};
MessageBoxIcon.defaultProps = {
size: "md",
};
const enhanced = withForwardRef(withStyles(styles)(MessageBoxIcon));
export default enhanced;
@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="MessageBox-root custom"
>
Hello World
</div>
`;
exports[`renders icon 1`] = `
<div
className="MessageBox-root"
>
<span
aria-hidden="true"
className="Icon-root Icon-md MessageBoxIcon-root"
>
alert
</span>
Alert MessageBox
</div>
`;
@@ -0,0 +1,3 @@
export { default, default as MessageBox } from "./MessageBox";
export { default as MessageBoxIcon } from "./MessageBoxIcon";
export { default as MessageBoxContent } from "./MessageBoxContent";
@@ -1,4 +1,5 @@
.root {
.messageBox {
margin-bottom: -1px;
}
.poweredBy {
@@ -8,18 +8,19 @@ import ValidationMessage from "talk-admin/routes/configure/components/Validation
import { OnSubmit } from "talk-framework/lib/form";
import { AriaInfo, Button, Flex, HorizontalGutter } from "talk-ui/components";
import MessageBoxContainer from "../containers/MessageBoxContainer";
import PostCommentSubmitStatusContainer from "../containers/PostCommentSubmitStatusContainer";
import RemainingCharactersContainer from "../containers/RemainingCharactersContainer";
import { cleanupRTEEmptyHTML, getCommentBodyValidators } from "../helpers";
import RTE from "./RTE";
import PostCommentSubmitStatusContainer from "../containers/PostCommentSubmitStatusContainer";
import styles from "./PostCommentForm.css";
interface FormProps {
body: string;
}
export interface PostCommentFormProps {
interface Props {
onSubmit: OnSubmit<FormProps>;
onChange?: (state: FormState, form: FormApi) => void;
initialValues?: FormProps;
@@ -28,103 +29,110 @@ export interface PostCommentFormProps {
disabled?: boolean;
disabledMessage?: React.ReactNode;
submitStatus: PropTypesOf<PostCommentSubmitStatusContainer>["status"];
showMessageBox?: boolean;
story: PropTypesOf<typeof MessageBoxContainer>["story"];
}
const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
{({ handleSubmit, submitting, submitError, form }) => (
<form
autoComplete="off"
onSubmit={handleSubmit}
className={styles.root}
id="comments-postCommentForm-form"
>
<FormSpy
onChange={state => props.onChange && props.onChange(state, form)}
/>
<HorizontalGutter>
<Field
name="body"
validate={getCommentBodyValidators(props.min, props.max)}
>
{({ input, meta }) => (
<>
<HorizontalGutter size="half">
<Localized id="comments-postCommentForm-rteLabel">
<AriaInfo
component="label"
htmlFor="comments-postCommentForm-field"
const PostCommentForm: StatelessComponent<Props> = props => (
<div>
{props.showMessageBox && (
<MessageBoxContainer story={props.story} className={styles.messageBox} />
)}
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
{({ handleSubmit, submitting, submitError, form }) => (
<form
autoComplete="off"
onSubmit={handleSubmit}
id="comments-postCommentForm-form"
>
<FormSpy
onChange={state => props.onChange && props.onChange(state, form)}
/>
<HorizontalGutter>
<Field
name="body"
validate={getCommentBodyValidators(props.min, props.max)}
>
{({ input, meta }) => (
<>
<HorizontalGutter size="half">
<Localized id="comments-postCommentForm-rteLabel">
<AriaInfo
component="label"
htmlFor="comments-postCommentForm-field"
>
Post a comment
</AriaInfo>
</Localized>
<Localized
id="comments-postCommentForm-rte"
attrs={{ placeholder: true }}
>
Post a comment
</AriaInfo>
</Localized>
<Localized
id="comments-postCommentForm-rte"
attrs={{ placeholder: true }}
>
<RTE
inputId="comments-postCommentForm-field"
onChange={({ html }) =>
input.onChange(cleanupRTEEmptyHTML(html))
}
value={input.value}
placeholder="Post a comment"
disabled={submitting || props.disabled}
/>
</Localized>
{props.disabled ? (
<>
{props.disabledMessage && (
<ValidationMessage fullWidth>
{props.disabledMessage}
</ValidationMessage>
)}
</>
) : (
<>
{meta.touched &&
(meta.error ||
(meta.submitError && !meta.dirtySinceLastSubmit)) && (
<RTE
inputId="comments-postCommentForm-field"
onChange={({ html }) =>
input.onChange(cleanupRTEEmptyHTML(html))
}
value={input.value}
placeholder="Post a comment"
disabled={submitting || props.disabled}
/>
</Localized>
{props.disabled ? (
<>
{props.disabledMessage && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
{props.disabledMessage}
</ValidationMessage>
)}
{submitError && (
<ValidationMessage fullWidth>
{submitError}
</ValidationMessage>
)}
<PostCommentSubmitStatusContainer
status={props.submitStatus}
/>
{props.max && (
<RemainingCharactersContainer
value={input.value}
max={props.max}
</>
) : (
<>
{meta.touched &&
(meta.error ||
(meta.submitError &&
!meta.dirtySinceLastSubmit)) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{submitError && (
<ValidationMessage fullWidth>
{submitError}
</ValidationMessage>
)}
<PostCommentSubmitStatusContainer
status={props.submitStatus}
/>
)}
</>
)}
</HorizontalGutter>
<Flex direction="column" alignItems="flex-end">
<Localized id="comments-postCommentForm-submit">
<Button
color="primary"
variant="filled"
disabled={submitting || !input.value || props.disabled}
type="submit"
>
Submit
</Button>
</Localized>
</Flex>
</>
)}
</Field>
</HorizontalGutter>
</form>
)}
</Form>
{props.max && (
<RemainingCharactersContainer
value={input.value}
max={props.max}
/>
)}
</>
)}
</HorizontalGutter>
<Flex direction="column" alignItems="flex-end">
<Localized id="comments-postCommentForm-submit">
<Button
color="primary"
variant="filled"
disabled={submitting || !input.value || props.disabled}
type="submit"
>
Submit
</Button>
</Localized>
</Flex>
</>
)}
</Field>
</HorizontalGutter>
</form>
)}
</Form>
</div>
);
export default PostCommentForm;
@@ -0,0 +1,4 @@
.messageBox {
margin-bottom: -1px;
z-index: 1;
}
@@ -0,0 +1,24 @@
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { CallOut } from "talk-ui/components";
import MessageBoxContainer from "../containers/MessageBoxContainer";
import styles from "./PostCommentFormClosed.css";
interface Props {
showMessageBox?: boolean;
story: PropTypesOf<typeof MessageBoxContainer>["story"];
children?: React.ReactNode;
}
const PostCommentFormClosed: StatelessComponent<Props> = props => (
<div>
{props.showMessageBox && (
<MessageBoxContainer story={props.story} className={styles.messageBox} />
)}
<CallOut fullWidth>{props.children}</CallOut>
</div>
);
export default PostCommentFormClosed;
@@ -1,7 +1,4 @@
.root {
}
.sitewide {
background-color: var(--palette-grey-dark);
border-color: var(--palette-grey-dark);
color: var(--palette-text-light);
@@ -0,0 +1,24 @@
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { CallOut, HorizontalGutter } from "talk-ui/components";
import MessageBoxContainer from "../containers/MessageBoxContainer";
import styles from "./PostCommentFormClosedSitewide.css";
interface Props {
showMessageBox?: boolean;
story: PropTypesOf<typeof MessageBoxContainer>["story"];
children?: React.ReactNode;
}
const PostCommentFormClosedSitewide: StatelessComponent<Props> = props => (
<HorizontalGutter size="double">
<CallOut fullWidth className={styles.root}>
{props.children}
</CallOut>
{props.showMessageBox && <MessageBoxContainer story={props.story} />}
</HorizontalGutter>
);
export default PostCommentFormClosedSitewide;
@@ -1,25 +0,0 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { PropTypesOf } from "talk-framework/types";
import PostCommentFormCollapsed from "./PostCommentFormCollapsed";
it("renders correctly when comments are closed sitewide", () => {
const props: PropTypesOf<typeof PostCommentFormCollapsed> = {
closedSitewide: true,
closedMessage: "closed site-wide",
};
const renderer = createRenderer();
renderer.render(<PostCommentFormCollapsed {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it("renders correctly when story is closed", () => {
const props: PropTypesOf<typeof PostCommentFormCollapsed> = {
closedSitewide: false,
closedMessage: "closed story",
};
const renderer = createRenderer();
renderer.render(<PostCommentFormCollapsed {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -1,21 +0,0 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import { CallOut } from "talk-ui/components";
import styles from "./PostCommentFormCollapsed.css";
interface Props {
closedSitewide?: boolean;
closedMessage?: React.ReactNode;
}
const PostCommentFormCollapsed: StatelessComponent<Props> = props => (
<CallOut
fullWidth
className={cn(styles.root, { [styles.sitewide]: props.closedSitewide })}
>
{props.closedMessage}
</CallOut>
);
export default PostCommentFormCollapsed;
@@ -1,6 +1,10 @@
.root {
}
.messageBox {
margin-bottom: -1px;
}
.textarea {
composes: bodyCopy from "talk-ui/shared/typography.css";
display: block;
@@ -1,10 +1,13 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import PostCommentFormFake from "./PostCommentFormFake";
const PostCommentFormFakeN = removeFragmentRefs(PostCommentFormFake);
it("renders correctly", () => {
const renderer = createRenderer();
renderer.render(<PostCommentFormFake />);
renderer.render(<PostCommentFormFakeN story={{}} showMessageBox />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -1,28 +1,46 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { Button, HorizontalGutter } from "talk-ui/components";
import MessageBoxContainer from "../containers/MessageBoxContainer";
import RTE from "./RTE";
import styles from "./PostCommentFormFake.css";
const PostCommentFormFake: StatelessComponent = props => (
<HorizontalGutter className={styles.root}>
<div aria-hidden="true">
<Localized
id="comments-postCommentFormFake-rte"
attrs={{ placeholder: true }}
>
<RTE placeholder="Post a comment" disabled />
interface Props {
showMessageBox?: boolean;
story: PropTypesOf<typeof MessageBoxContainer>["story"];
}
const PostCommentFormFake: StatelessComponent<Props> = props => (
<div>
{props.showMessageBox && (
<MessageBoxContainer story={props.story} className={styles.messageBox} />
)}
<HorizontalGutter className={styles.root}>
<div aria-hidden="true">
<Localized
id="comments-postCommentFormFake-rte"
attrs={{ placeholder: true }}
>
<RTE placeholder="Post a comment" disabled />
</Localized>
</div>
<Localized id="comments-postCommentFormFake-signInAndJoin">
<Button
color="primary"
variant="filled"
disabled
type="submit"
fullWidth
>
Sign in and join the conversation
</Button>
</Localized>
</div>
<Localized id="comments-postCommentFormFake-signInAndJoin">
<Button color="primary" variant="filled" disabled type="submit" fullWidth>
Sign in and join the conversation
</Button>
</Localized>
</HorizontalGutter>
</HorizontalGutter>
</div>
);
export default PostCommentFormFake;
@@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly when comments are closed sitewide 1`] = `
<withPropsOnChange(CallOut)
className="PostCommentFormCollapsed-root PostCommentFormCollapsed-sitewide"
fullWidth={true}
>
closed site-wide
</withPropsOnChange(CallOut)>
`;
exports[`renders correctly when story is closed 1`] = `
<withPropsOnChange(CallOut)
className="PostCommentFormCollapsed-root"
fullWidth={true}
>
closed story
</withPropsOnChange(CallOut)>
`;
@@ -1,38 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<ForwardRef(forwardRef)
className="PostCommentFormFake-root"
>
<div
aria-hidden="true"
<div>
<Relay(MessageBoxContainer)
className="PostCommentFormFake-messageBox"
story={Object {}}
/>
<ForwardRef(forwardRef)
className="PostCommentFormFake-root"
>
<Localized
attrs={
Object {
"placeholder": true,
<div
aria-hidden="true"
>
<Localized
attrs={
Object {
"placeholder": true,
}
}
}
id="comments-postCommentFormFake-rte"
id="comments-postCommentFormFake-rte"
>
<RTE
disabled={true}
placeholder="Post a comment"
/>
</Localized>
</div>
<Localized
id="comments-postCommentFormFake-signInAndJoin"
>
<RTE
<ForwardRef(forwardRef)
color="primary"
disabled={true}
placeholder="Post a comment"
/>
fullWidth={true}
type="submit"
variant="filled"
>
Sign in and join the conversation
</ForwardRef(forwardRef)>
</Localized>
</div>
<Localized
id="comments-postCommentFormFake-signInAndJoin"
>
<ForwardRef(forwardRef)
color="primary"
disabled={true}
fullWidth={true}
type="submit"
variant="filled"
>
Sign in and join the conversation
</ForwardRef(forwardRef)>
</Localized>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</div>
`;
@@ -0,0 +1,46 @@
import React, { StatelessComponent } from "react";
import { graphql } from "react-relay";
import { withFragmentContainer } from "talk-framework/lib/relay";
import { MessageBoxContainer_story as StoryData } from "talk-stream/__generated__/MessageBoxContainer_story.graphql";
import {
MessageBox,
MessageBoxContent,
MessageBoxIcon,
} from "talk-stream/components/MessageBox";
interface Props {
story: StoryData;
className?: string;
}
const MessageBoxContainer: StatelessComponent<Props> = ({
story,
className,
}) => {
return (
<MessageBox className={className}>
{story.settings.messageBox.icon && (
<MessageBoxIcon>{story.settings.messageBox.icon}</MessageBoxIcon>
)}
<MessageBoxContent>
{story.settings.messageBox.content || ""}
</MessageBoxContent>
</MessageBox>
);
};
const enhanced = withFragmentContainer<Props>({
story: graphql`
fragment MessageBoxContainer_story on Story {
settings {
messageBox {
content
icon
}
}
}
`,
})(MessageBoxContainer);
export default enhanced;
@@ -11,7 +11,7 @@ interface Props {
commentID: string;
}
export const PermalinkButtonContainerProps: StatelessComponent<Props> = ({
export const PermalinkButtonContainer: StatelessComponent<Props> = ({
story,
commentID,
}) => {
@@ -29,6 +29,6 @@ const enhanced = withFragmentContainer<Props>({
url
}
`,
})(PermalinkButtonContainerProps);
})(PermalinkButtonContainer);
export default enhanced;
@@ -27,6 +27,11 @@ function createDefaultProps(add: DeepPartial<Props> = {}): Props {
story: {
id: "story-id",
isClosed: false,
settings: {
messageBox: {
enabled: false,
},
},
},
sessionStorage: createPromisifiedStorage(),
settings: {
@@ -21,10 +21,9 @@ import {
withCreateCommentMutation,
} from "talk-stream/mutations";
import PostCommentForm, {
PostCommentFormProps,
} from "../components/PostCommentForm";
import PostCommentFormCollapsed from "../components/PostCommentFormCollapsed";
import PostCommentForm from "../components/PostCommentForm";
import PostCommentFormClosed from "../components/PostCommentFormClosed";
import PostCommentFormClosedSitewide from "../components/PostCommentFormClosedSitewide";
import PostCommentFormFake from "../components/PostCommentFormFake";
import {
getSubmitStatus,
@@ -42,7 +41,7 @@ interface Props {
}
interface State {
initialValues?: PostCommentFormProps["initialValues"];
initialValues?: PropTypesOf<typeof PostCommentForm>["initialValues"];
initialized: boolean;
keepFormWhenClosed: boolean;
submitStatus: SubmitStatus | null;
@@ -79,10 +78,9 @@ export class PostCommentFormContainer extends Component<Props, State> {
});
}
private handleOnSubmit: PostCommentFormProps["onSubmit"] = async (
input,
form
) => {
private handleOnSubmit: PropTypesOf<
typeof PostCommentForm
>["onSubmit"] = async (input, form) => {
try {
const submitStatus = getSubmitStatus(
await this.props.createComment({
@@ -107,7 +105,10 @@ export class PostCommentFormContainer extends Component<Props, State> {
return;
};
private handleOnChange: PostCommentFormProps["onChange"] = (state, form) => {
private handleOnChange: PropTypesOf<typeof PostCommentForm>["onChange"] = (
state,
form
) => {
if (this.state.submitStatus && state.dirty) {
this.setState({ submitStatus: null });
}
@@ -126,27 +127,39 @@ export class PostCommentFormContainer extends Component<Props, State> {
if (!this.state.initialized) {
return null;
}
if (
!this.state.keepFormWhenClosed &&
(this.props.settings.disableCommenting.enabled ||
this.props.story.isClosed)
) {
if (!this.state.keepFormWhenClosed) {
if (this.props.settings.disableCommenting.enabled) {
return (
<PostCommentFormClosedSitewide
story={this.props.story}
showMessageBox={this.props.story.settings.messageBox.enabled}
>
{this.props.settings.disableCommenting.message}
</PostCommentFormClosedSitewide>
);
}
if (this.props.story.isClosed) {
return (
<PostCommentFormClosed
story={this.props.story}
showMessageBox={this.props.story.settings.messageBox.enabled}
>
{this.props.settings.closeCommenting.message}
</PostCommentFormClosed>
);
}
}
if (!this.props.local.loggedIn) {
return (
<PostCommentFormCollapsed
closedSitewide={this.props.settings.disableCommenting.enabled}
closedMessage={
(this.props.settings.disableCommenting.enabled &&
this.props.settings.disableCommenting.message) ||
this.props.settings.closeCommenting.message
}
<PostCommentFormFake
story={this.props.story}
showMessageBox={this.props.story.settings.messageBox.enabled}
/>
);
}
if (!this.props.local.loggedIn) {
return <PostCommentFormFake />;
}
return (
<PostCommentForm
story={this.props.story}
onSubmit={this.handleOnSubmit}
onChange={this.handleOnChange}
initialValues={this.state.initialValues}
@@ -170,6 +183,7 @@ export class PostCommentFormContainer extends Component<Props, State> {
this.props.settings.closeCommenting.message
}
submitStatus={this.state.submitStatus}
showMessageBox={this.props.story.settings.messageBox.enabled}
/>
);
}
@@ -208,6 +222,12 @@ const enhanced = withContext(({ sessionStorage }) => ({
fragment PostCommentFormContainer_story on Story {
id
isClosed
...MessageBoxContainer_story
settings {
messageBox {
enabled
}
}
}
`,
})(PostCommentFormContainer)
@@ -15,7 +15,7 @@ exports[`hide reply button 1`] = `
justifyContent="space-between"
>
<ButtonsBar>
<Relay(PermalinkButtonContainerProps)
<Relay(PermalinkButtonContainer)
commentID="comment-id"
story={
Object {
@@ -109,7 +109,7 @@ exports[`renders body only 1`] = `
id="comments-commentContainer-replyButton-comment-id"
onClick={[Function]}
/>
<Relay(PermalinkButtonContainerProps)
<Relay(PermalinkButtonContainer)
commentID="comment-id"
story={
Object {
@@ -203,7 +203,7 @@ exports[`renders disabled reply when commenting has been disabled 1`] = `
id="comments-commentContainer-replyButton-comment-id"
onClick={[Function]}
/>
<Relay(PermalinkButtonContainerProps)
<Relay(PermalinkButtonContainer)
commentID="comment-id"
story={
Object {
@@ -297,7 +297,7 @@ exports[`renders disabled reply when story is closed 1`] = `
id="comments-commentContainer-replyButton-comment-id"
onClick={[Function]}
/>
<Relay(PermalinkButtonContainerProps)
<Relay(PermalinkButtonContainer)
commentID="comment-id"
story={
Object {
@@ -391,7 +391,7 @@ exports[`renders in reply to 1`] = `
id="comments-commentContainer-replyButton-comment-id"
onClick={[Function]}
/>
<Relay(PermalinkButtonContainerProps)
<Relay(PermalinkButtonContainer)
commentID="comment-id"
story={
Object {
@@ -493,7 +493,7 @@ exports[`renders username and body 1`] = `
id="comments-commentContainer-replyButton-comment-id"
onClick={[Function]}
/>
<Relay(PermalinkButtonContainerProps)
<Relay(PermalinkButtonContainer)
commentID="comment-id"
story={
Object {
@@ -587,7 +587,7 @@ exports[`shows conversation link 1`] = `
id="comments-commentContainer-replyButton-comment-id"
onClick={[Function]}
/>
<Relay(PermalinkButtonContainerProps)
<Relay(PermalinkButtonContainer)
commentID="comment-id"
story={
Object {
@@ -8,15 +8,39 @@ exports[`renders correctly 1`] = `
min={3}
onChange={[Function]}
onSubmit={[Function]}
showMessageBox={false}
story={
Object {
"id": "story-id",
"isClosed": false,
"settings": Object {
"messageBox": Object {
"enabled": false,
},
},
}
}
submitStatus={null}
/>
`;
exports[`renders when commenting has been disabled (collapsing) 1`] = `
<PostCommentFormCollapsed
closedMessage="commenting disabled"
closedSitewide={true}
/>
<PostCommentFormClosedSitewide
showMessageBox={false}
story={
Object {
"id": "story-id",
"isClosed": false,
"settings": Object {
"messageBox": Object {
"enabled": false,
},
},
}
}
>
commenting disabled
</PostCommentFormClosedSitewide>
`;
exports[`renders when commenting has been disabled (non-collapsing) 1`] = `
@@ -27,15 +51,39 @@ exports[`renders when commenting has been disabled (non-collapsing) 1`] = `
min={3}
onChange={[Function]}
onSubmit={[Function]}
showMessageBox={false}
story={
Object {
"id": "story-id",
"isClosed": false,
"settings": Object {
"messageBox": Object {
"enabled": false,
},
},
}
}
submitStatus={null}
/>
`;
exports[`renders when story has been closed (collapsing) 1`] = `
<PostCommentFormCollapsed
closedMessage="story closed"
closedSitewide={false}
/>
<PostCommentFormClosed
showMessageBox={false}
story={
Object {
"id": "story-id",
"isClosed": true,
"settings": Object {
"messageBox": Object {
"enabled": false,
},
},
}
}
>
story closed
</PostCommentFormClosed>
`;
exports[`renders when story has been closed (non-collapsing) 1`] = `
@@ -46,6 +94,18 @@ exports[`renders when story has been closed (non-collapsing) 1`] = `
min={3}
onChange={[Function]}
onSubmit={[Function]}
showMessageBox={false}
story={
Object {
"id": "story-id",
"isClosed": true,
"settings": Object {
"messageBox": Object {
"enabled": false,
},
},
}
}
submitStatus={null}
/>
`;
@@ -63,6 +123,18 @@ exports[`renders with initialValues 1`] = `
min={3}
onChange={[Function]}
onSubmit={[Function]}
showMessageBox={false}
story={
Object {
"id": "story-id",
"isClosed": false,
"settings": Object {
"messageBox": Object {
"enabled": false,
},
},
}
}
submitStatus={null}
/>
`;
@@ -13,6 +13,7 @@ import {
Typography,
} from "talk-ui/components";
import MessageBoxConfigContainer from "../containers/MessageBoxConfigContainer";
import PremodConfigContainer from "../containers/PremodConfigContainer";
import PremodLinksConfigContainer from "../containers/PremodLinksConfigContainer";
@@ -21,7 +22,8 @@ import styles from "./ConfigureCommentStream.css";
interface Props {
onSubmit: (settings: any, form: FormApi) => void;
storySettings: PropTypesOf<typeof PremodConfigContainer>["storySettings"] &
PropTypesOf<typeof PremodLinksConfigContainer>["storySettings"];
PropTypesOf<typeof PremodLinksConfigContainer>["storySettings"] &
PropTypesOf<typeof MessageBoxConfigContainer>["storySettings"];
}
const ConfigureCommentStream: StatelessComponent<Props> = ({
@@ -66,6 +68,11 @@ const ConfigureCommentStream: StatelessComponent<Props> = ({
storySettings={storySettings}
disabled={submitting}
/>
<MessageBoxConfigContainer
onInitValues={onInitValues}
storySettings={storySettings}
disabled={submitting}
/>
</HorizontalGutter>
</form>
)}
@@ -0,0 +1,3 @@
.preview {
text-transform: uppercase;
}
@@ -0,0 +1,152 @@
import { Localized } from "fluent-react/compat";
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 {
MessageBox,
MessageBoxContent,
MessageBoxIcon,
} from "talk-stream/components/MessageBox";
import {
HorizontalGutter,
Icon,
Spinner,
TileOption,
TileSelector,
Typography,
ValidationMessage,
} from "talk-ui/components";
import ToggleConfig from "./ToggleConfig";
import WidthLimitedDescription from "./WidthLimitedDescription";
import styles from "./MessageBoxConfig.css";
interface Props {
disabled: boolean;
}
const MessageBoxConfig: StatelessComponent<Props> = ({ disabled }) => (
<Field name="messageBox.enabled" type="checkbox" parse={parseBool}>
{({ input }) => (
<ToggleConfig
id={input.name}
name={input.name}
onChange={input.onChange}
onFocus={input.onFocus}
onBlur={input.onBlur}
checked={input.checked}
disabled={disabled}
title={
<Localized id="configure-messageBox-title">
<span>Enable Message Box for this Stream</span>
</Localized>
}
>
<HorizontalGutter size="oneAndAHalf">
<Localized id="configure-messageBox-description">
<WidthLimitedDescription>
Add a message to the top of the comment box for your readers. Use
this to pose a topic, ask a question or make announcements
relating to this story.
</WidthLimitedDescription>
</Localized>
{input.checked && (
<Field name="messageBox.icon">
{({ input: iconInput }) => (
<Field name="messageBox.content">
{({ input: contentInput, meta }) => (
<>
<HorizontalGutter size="half" container="section">
<Localized id="configure-messageBox-preview">
<Typography
variant="bodyCopyBold"
container="h1"
className={styles.preview}
>
PREVIEW
</Typography>
</Localized>
<MessageBox>
{iconInput.value && (
<MessageBoxIcon>{iconInput.value}</MessageBoxIcon>
)}
{/* Using a zero width join character to ensure that the space is used */}
<MessageBoxContent>
{contentInput.value || "&nbsp;"}
</MessageBoxContent>
</MessageBox>
</HorizontalGutter>
<HorizontalGutter size="half" container="fieldset">
<Localized id="configure-messageBox-selectAnIcon">
<Typography variant="bodyCopyBold" container="legend">
Select an Icon
</Typography>
</Localized>
<TileSelector
id="configure-messageBox-icon"
name={iconInput.name}
onChange={iconInput.onChange}
value={iconInput.value}
>
<TileOption value="question_answer">
<Icon size="md">question_answer</Icon>
</TileOption>
<TileOption value="today">
<Icon size="md">today</Icon>
</TileOption>
<TileOption value="help_outline">
<Icon size="md">help_outline</Icon>
</TileOption>
<TileOption value="warning">
<Icon size="md">warning</Icon>
</TileOption>
<TileOption value="chat_bubble_outline">
<Icon size="md">chat_bubble_outline</Icon>
</TileOption>
<TileOption value="">No Icon</TileOption>
</TileSelector>
</HorizontalGutter>
<HorizontalGutter size="half" container="section">
<div>
<Localized id="configure-messageBox-writeAMessage">
<Typography
variant="bodyCopyBold"
container={
<label htmlFor="configure-messageBox-content" />
}
>
Write a Message
</Typography>
</Localized>
</div>
<Suspense fallback={<Spinner />}>
<MarkdownEditor
id="configure-messageBox-content"
name={contentInput.name}
onChange={contentInput.onChange}
value={contentInput.value}
/>
</Suspense>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</HorizontalGutter>
</>
)}
</Field>
)}
</Field>
)}
</HorizontalGutter>
</ToggleConfig>
)}
</Field>
);
export default MessageBoxConfig;
@@ -50,6 +50,7 @@ const enhanced = withFragmentContainer<Props>({
settings {
...PremodConfigContainer_storySettings
...PremodLinksConfigContainer_storySettings
...MessageBoxConfigContainer_storySettings
}
}
`,
@@ -0,0 +1,39 @@
import React from "react";
import { graphql } from "react-relay";
import { withFragmentContainer } from "talk-framework/lib/relay";
import { MessageBoxConfigContainer_storySettings as StorySettingsData } from "talk-stream/__generated__/MessageBoxConfigContainer_storySettings.graphql";
import MessageBoxConfig from "../components/MessageBoxConfig";
interface Props {
storySettings: StorySettingsData;
onInitValues: (values: StorySettingsData) => void;
disabled: boolean;
}
class MessageBoxConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.storySettings);
}
public render() {
const { disabled } = this.props;
return <MessageBoxConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
storySettings: graphql`
fragment MessageBoxConfigContainer_storySettings on StorySettings {
messageBox {
enabled
content
icon
}
}
`,
})(MessageBoxConfigContainer);
export default enhanced;
@@ -52,102 +52,104 @@ exports[`renders comment stream with community guidelines 1`] = `
}
/>
</div>
<div
className="HorizontalGutter-root PostCommentFormFake-root HorizontalGutter-full"
>
<div>
<div
aria-hidden="true"
className="HorizontalGutter-root PostCommentFormFake-root HorizontalGutter-full"
>
<div>
<div
className=""
>
<div
aria-hidden="true"
>
<div>
<div
aria-hidden="true"
className="RTE-placeholder RTE-placeholder "
className=""
>
Post a comment
</div>
<div
aria-placeholder="Post a comment"
className="RTE-contentEditable RTE-content RTE-contentEditableDisabled"
contentEditable={false}
dangerouslySetInnerHTML={
Object {
"__html": "",
<div
aria-hidden="true"
className="RTE-placeholder RTE-placeholder "
>
Post a comment
</div>
<div
aria-placeholder="Post a comment"
className="RTE-contentEditable RTE-content RTE-contentEditableDisabled"
contentEditable={false}
dangerouslySetInnerHTML={
Object {
"__html": "",
}
}
}
disabled={true}
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onPaste={[Function]}
onSelect={[Function]}
/>
<div
className="RTE-toolbar RTE-toolbarDisabled RTE-toolbarBottom Toolbar-toolbar"
>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Bold"
type="button"
disabled={true}
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onPaste={[Function]}
onSelect={[Function]}
/>
<div
className="RTE-toolbar RTE-toolbarDisabled RTE-toolbarBottom Toolbar-toolbar"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Bold"
type="button"
>
format_bold
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Italic"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_bold
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Italic"
type="button"
>
format_italic
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Blockquote"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_italic
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Blockquote"
type="button"
>
format_quote
</span>
</button>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_quote
</span>
</button>
</div>
</div>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth Button-disabled"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Sign in and Join the Conversation
</button>
</div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth Button-disabled"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Sign in and Join the Conversation
</button>
</div>
<div>
<div
@@ -0,0 +1,512 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders message box when commenting disabled 1`] = `
<section
aria-labelledby="tab-COMMENTS"
className="App-tabContent"
data-testid="current-tab-pane"
id="tabPane-COMMENTS"
role="tabpanel"
>
<div
className="HorizontalGutter-root Stream-root HorizontalGutter-double"
>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign in
</button>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Register
</button>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<div
className="CallOut-root CallOut-colorRegular CallOut-fullWidth PostCommentFormClosedSitewide-root"
>
Commenting disabled
</div>
<div
className="MessageBox-root"
>
<span
aria-hidden="true"
className="Icon-root Icon-md MessageBoxIcon-root"
>
chat
</span>
<div
className="Markdown-root MessageBoxContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "<p><strong>What do you think</strong>?</p>
",
}
}
/>
</div>
</div>
<div
aria-live="polite"
className="HorizontalGutter-root HorizontalGutter-full"
data-testid="comments-stream-log"
id="talk-comments-stream-log"
role="log"
/>
</div>
</section>
`;
exports[`renders message box when logged in 1`] = `
<section
aria-labelledby="tab-COMMENTS"
className="App-tabContent"
data-testid="current-tab-pane"
id="tabPane-COMMENTS"
role="tabpanel"
>
<div
className="HorizontalGutter-root Stream-root HorizontalGutter-double"
>
<div
className="Flex-root"
>
<div
className="Flex-flex Flex-halfItemGutter Flex-wrap"
>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Signed in as
<span
className="Typography-root Typography-bodyCopyBold Typography-colorTextPrimary"
>
Markus
</span>
.
</div>
<div
className="Flex-root Typography-root Typography-bodyCopy Typography-colorTextPrimary Flex-flex"
>
<span>
Not you? 
</span>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign Out
</button>
</div>
</div>
</div>
<div>
<div
className="MessageBox-root PostCommentForm-messageBox"
>
<span
aria-hidden="true"
className="Icon-root Icon-md MessageBoxIcon-root"
>
chat
</span>
<div
className="Markdown-root MessageBoxContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "<p><strong>What do you think</strong>?</p>
",
}
}
/>
</div>
<form
autoComplete="off"
id="comments-postCommentForm-form"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<div
className="HorizontalGutter-root HorizontalGutter-half"
>
<label
className="AriaInfo-root"
htmlFor="comments-postCommentForm-field"
>
Post a comment
</label>
<div>
<div
className=""
>
<div
aria-hidden="true"
className="RTE-placeholder RTE-placeholder"
>
Post a comment
</div>
<div
aria-placeholder="Post a comment"
className="RTE-contentEditable RTE-content"
contentEditable={true}
dangerouslySetInnerHTML={
Object {
"__html": "",
}
}
disabled={false}
id="comments-postCommentForm-field"
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onPaste={[Function]}
onSelect={[Function]}
/>
<div
className="RTE-toolbar RTE-toolbarBottom Toolbar-toolbar"
>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Bold"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_bold
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Italic"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_italic
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Blockquote"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_quote
</span>
</button>
</div>
</div>
</div>
</div>
<div
className="Flex-root Flex-flex Flex-alignFlexEnd Flex-directionColumn"
>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-disabled"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Submit
</button>
</div>
</div>
</form>
</div>
<div
aria-live="polite"
className="HorizontalGutter-root HorizontalGutter-full"
data-testid="comments-stream-log"
id="talk-comments-stream-log"
role="log"
/>
</div>
</section>
`;
exports[`renders message box when not logged in 1`] = `
<section
aria-labelledby="tab-COMMENTS"
className="App-tabContent"
data-testid="current-tab-pane"
id="tabPane-COMMENTS"
role="tabpanel"
>
<div
className="HorizontalGutter-root Stream-root HorizontalGutter-double"
>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign in
</button>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Register
</button>
</div>
<div>
<div
className="MessageBox-root PostCommentFormFake-messageBox"
>
<span
aria-hidden="true"
className="Icon-root Icon-md MessageBoxIcon-root"
>
chat
</span>
<div
className="Markdown-root MessageBoxContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "<p><strong>What do you think</strong>?</p>
",
}
}
/>
</div>
<div
className="HorizontalGutter-root PostCommentFormFake-root HorizontalGutter-full"
>
<div
aria-hidden="true"
>
<div>
<div
className=""
>
<div
aria-hidden="true"
className="RTE-placeholder RTE-placeholder "
>
Post a comment
</div>
<div
aria-placeholder="Post a comment"
className="RTE-contentEditable RTE-content RTE-contentEditableDisabled"
contentEditable={false}
dangerouslySetInnerHTML={
Object {
"__html": "",
}
}
disabled={true}
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onPaste={[Function]}
onSelect={[Function]}
/>
<div
className="RTE-toolbar RTE-toolbarDisabled RTE-toolbarBottom Toolbar-toolbar"
>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Bold"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_bold
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Italic"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_italic
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Blockquote"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_quote
</span>
</button>
</div>
</div>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth Button-disabled"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Sign in and Join the Conversation
</button>
</div>
</div>
<div
aria-live="polite"
className="HorizontalGutter-root HorizontalGutter-full"
data-testid="comments-stream-log"
id="talk-comments-stream-log"
role="log"
/>
</div>
</section>
`;
exports[`renders message box when story isClosed 1`] = `
<section
aria-labelledby="tab-COMMENTS"
className="App-tabContent"
data-testid="current-tab-pane"
id="tabPane-COMMENTS"
role="tabpanel"
>
<div
className="HorizontalGutter-root Stream-root HorizontalGutter-double"
>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign in
</button>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Register
</button>
</div>
<div>
<div
className="MessageBox-root PostCommentFormClosed-messageBox"
>
<div
className="Markdown-root MessageBoxContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "<p><strong>What do you think</strong>?</p>
",
}
}
/>
</div>
<div
className="CallOut-root CallOut-colorRegular CallOut-fullWidth"
>
Story is closed
</div>
</div>
<div
aria-live="polite"
className="HorizontalGutter-root HorizontalGutter-full"
data-testid="comments-stream-log"
id="talk-comments-stream-log"
role="log"
/>
</div>
</section>
`;
@@ -70,102 +70,104 @@ exports[`renders comment stream 1`] = `
Register
</button>
</div>
<div
className="HorizontalGutter-root PostCommentFormFake-root HorizontalGutter-full"
>
<div>
<div
aria-hidden="true"
className="HorizontalGutter-root PostCommentFormFake-root HorizontalGutter-full"
>
<div>
<div
className=""
>
<div
aria-hidden="true"
>
<div>
<div
aria-hidden="true"
className="RTE-placeholder RTE-placeholder "
className=""
>
Post a comment
</div>
<div
aria-placeholder="Post a comment"
className="RTE-contentEditable RTE-content RTE-contentEditableDisabled"
contentEditable={false}
dangerouslySetInnerHTML={
Object {
"__html": "",
<div
aria-hidden="true"
className="RTE-placeholder RTE-placeholder "
>
Post a comment
</div>
<div
aria-placeholder="Post a comment"
className="RTE-contentEditable RTE-content RTE-contentEditableDisabled"
contentEditable={false}
dangerouslySetInnerHTML={
Object {
"__html": "",
}
}
}
disabled={true}
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onPaste={[Function]}
onSelect={[Function]}
/>
<div
className="RTE-toolbar RTE-toolbarDisabled RTE-toolbarBottom Toolbar-toolbar"
>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Bold"
type="button"
disabled={true}
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onPaste={[Function]}
onSelect={[Function]}
/>
<div
className="RTE-toolbar RTE-toolbarDisabled RTE-toolbarBottom Toolbar-toolbar"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Bold"
type="button"
>
format_bold
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Italic"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_bold
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Italic"
type="button"
>
format_italic
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Blockquote"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_italic
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Blockquote"
type="button"
>
format_quote
</span>
</button>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_quote
</span>
</button>
</div>
</div>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth Button-disabled"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Sign in and Join the Conversation
</button>
</div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth Button-disabled"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Sign in and Join the Conversation
</button>
</div>
<div>
<div
@@ -0,0 +1,120 @@
import { merge } from "lodash";
import sinon from "sinon";
import { waitForElement, within } from "talk-framework/testHelpers";
import { settings, storyWithNoComments, users } from "../fixtures";
import create from "./create";
async function createTestRenderer(
data: {
story?: any;
settings?: any;
loggedIn?: boolean;
} = {},
options: { muteNetworkErrors?: boolean } = {}
) {
const resolvers = {
Query: {
settings: sinon.stub().returns(merge({}, settings, data.settings)),
me: sinon.stub().returns((data.loggedIn && users[0]) || null),
story: sinon.stub().callsFake((_: any, variables: any) => {
expectAndFail(variables.id).toBe(storyWithNoComments.id);
return merge({}, storyWithNoComments, data.story);
}),
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(storyWithNoComments.id, "storyID");
localRecord.setValue(Boolean(data.loggedIn), "loggedIn");
},
});
await waitForElement(() =>
within(testRenderer.root).getByTestID("comments-stream-log")
);
const tabPane = await waitForElement(() =>
within(testRenderer.root).getByTestID("current-tab-pane")
);
return {
testRenderer,
context,
tabPane,
};
}
it("renders message box when not logged in", async () => {
const { tabPane } = await createTestRenderer({
story: {
settings: {
messageBox: {
enabled: true,
icon: "chat",
content: "**What do you think**?",
},
},
},
});
expect(within(tabPane).toJSON()).toMatchSnapshot();
});
it("renders message box when logged in", async () => {
const { tabPane } = await createTestRenderer({
story: {
settings: {
messageBox: {
enabled: true,
icon: "chat",
content: "**What do you think**?",
},
},
},
loggedIn: true,
});
expect(within(tabPane).toJSON()).toMatchSnapshot();
});
it("renders message box when commenting disabled", async () => {
const { tabPane } = await createTestRenderer({
story: {
settings: {
messageBox: {
enabled: true,
icon: "chat",
content: "**What do you think**?",
},
},
},
settings: {
disableCommenting: {
enabled: true,
message: "Commenting disabled",
},
},
});
expect(within(tabPane).toJSON()).toMatchSnapshot();
});
it("renders message box when story isClosed", async () => {
const { tabPane } = await createTestRenderer({
story: {
isClosed: true,
settings: {
messageBox: {
enabled: true,
icon: null,
content: "**What do you think**?",
},
},
},
});
expect(within(tabPane).toJSON()).toMatchSnapshot();
});
@@ -162,6 +162,53 @@ exports[`renders configure 1`] = `
</p>
</div>
</div>
<div>
<div
className="CheckBox-root"
>
<input
checked={false}
className="CheckBox-input"
disabled={false}
id="messageBox.enabled"
name="messageBox.enabled"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="checkbox"
/>
<label
className="CheckBox-label"
htmlFor="messageBox.enabled"
>
<span
className="CheckBox-labelSpan"
>
<span
className="Typography-root Typography-heading3 Typography-colorTextPrimary"
>
<span>
Enable Message Box for this Stream
</span>
</span>
</span>
</label>
</div>
<div
className="ToggleConfig-details"
>
<div
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary WidthLimitedDescription-root"
>
Add a message to the top of the comment box for your readers. Use this to pose a topic,
ask a question or make announcements relating to this story.
</p>
</div>
</div>
</div>
</div>
</form>
</div>
@@ -128,3 +128,59 @@ it("change premod links", async () => {
// Should have successfully sent with server.
expect(updateStorySettingsStub.called).toBe(true);
});
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 = merge(storyRecord, { settings: data.input.settings });
return {
story: storyRecord,
clientMutationId: data.input.clientMutationId,
};
});
const { form, applyButton } = await createTestRenderer({
Mutation: {
updateStorySettings: updateStorySettingsStub,
},
});
const enableField = within(form).getByLabelText(
"Enable Message Box for this Stream"
);
expect(applyButton.props.disabled).toBe(true);
// Let's enable premod.
enableField.props.onChange({});
expect(applyButton.props.disabled).toBe(false);
// Select icon
within(form)
.getByLabelText("question_answer")
.props.onChange({ target: { value: "question_answer" } });
// Change content.
(await waitForElement(() =>
within(form).getByLabelText("Write a Message")
)).props.onChange("*What do you think?*");
// Send form
form.props.onSubmit();
expect(applyButton.props.disabled).toBe(true);
expect(enableField.props.disabled).toBe(true);
// Wait for submission to be finished
await wait(() => {
expect(enableField.props.disabled).toBe(false);
});
// Should have successfully sent with server.
expect(updateStorySettingsStub.called).toBe(true);
});
+17
View File
@@ -7,6 +7,8 @@ import {
export const settings = {
id: "settings",
moderation: "POST",
premodLinksEnable: false,
communityGuidelines: {
enabled: false,
content: "",
@@ -295,6 +297,9 @@ export const baseStory = {
settings: {
moderation: "POST",
premodLinksEnable: false,
messageBox: {
enabled: false,
},
},
};
@@ -329,6 +334,18 @@ export const stories = denormalizeStories([
},
]);
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",
@@ -18,6 +18,12 @@
color: var(--palette-grey-main);
}
.colorDark {
background-color: var(--palette-text-primary);
border-width: 0px;
color: var(--palette-text-light);
}
.colorError {
background-color: var(--palette-error-light);
border-color: var(--palette-error-darkest);
@@ -24,7 +24,7 @@ export interface MessageProps {
/*
* Name of color, "grey" stays by default - common gray one
*/
color?: "error" | "grey" | "primary";
color?: "error" | "grey" | "primary" | "dark";
}
const Message: StatelessComponent<MessageProps> = props => {
@@ -36,6 +36,7 @@ const Message: StatelessComponent<MessageProps> = props => {
[classes.colorGrey]: color === "grey",
[classes.colorError]: color === "error",
[classes.colorPrimary]: color === "primary",
[classes.colorDark]: color === "dark",
[classes.fullWidth]: fullWidth,
},
className
@@ -1,6 +1,7 @@
.root {
align-self: flex-start;
margin-top: 1px;
flex-shrink: 0;
&:first-child {
margin-right: calc(0.5 * var(--spacing-unit));
}
@@ -0,0 +1,27 @@
.input {
cursor: pointer;
position: absolute; /* take it out of document flow */
opacity: 0; /* hide it */
}
.label {
composes: bodyCopy from "talk-ui/shared/typography.css";
display: inline-flex;
align-items: center;
position: relative;
cursor: pointer;
user-select: none;
border: 1px solid var(--palette-grey-dark);
border-radius: 2px;
color: var(--palette-grey-dark);
padding: calc(0.5 * var(--spacing-unit));
line-height: 1;
box-sizing: border-box;
height: 100%;
}
.checked {
border: 1px solid var(--palette-text-primary);
background: var(--palette-text-primary);
color: var(--palette-text-light);
}
@@ -0,0 +1,41 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import styles from "./TileOption.css";
import { SelectorChildProps } from "./TileSelector";
interface Props extends SelectorChildProps {
className?: string;
}
const TileOption: StatelessComponent<Props> = ({
id,
name,
onChange,
className,
value,
checked,
children,
}) => {
return (
<div className={className}>
<input
id={id}
name={name}
type="radio"
className={styles.input}
onChange={onChange}
value={value}
checked={checked}
/>
<label
htmlFor={id}
className={cn(styles.label, { [styles.checked]: checked })}
>
{children}
</label>
</div>
);
};
export default TileOption;
@@ -0,0 +1,2 @@
.root {
}
@@ -0,0 +1,47 @@
---
name: TileSelector
menu: UI Kit
---
import { Playground, PropsTable } from "docz";
import Container from "react-with-state-props";
import TileSelector from "./TileSelector";
import TileOption from "./TileOption";
import HorizontalGutter from "../HorizontalGutter";
import Icon from "../Icon";
# TileSelector
## Basic Use
<Playground>
<Container
state={{ value: null }}
render={props => (
<TileSelector
id="icon"
name="icon"
onChange={val => props.setValue(val)}
value={props.value}
>
<TileOption value="question_answer">
<Icon size="md">question_answer</Icon>
</TileOption>
<TileOption value="today">
<Icon size="md">today</Icon>
</TileOption>
<TileOption value="help_outline">
<Icon size="md">help_outline</Icon>
</TileOption>
<TileOption value="warning">
<Icon size="md">warning</Icon>
</TileOption>
<TileOption value="chat_bubble_outline">
<Icon size="md">chat_bubble_outline</Icon>
</TileOption>
<TileOption value="">No Icon</TileOption>
</TileSelector>
)}
/>
</Playground>
@@ -0,0 +1,52 @@
import cn from "classnames";
import React, { ReactElement, StatelessComponent, useCallback } from "react";
import { Flex } from "talk-ui/components";
import styles from "./TileSelector.css";
export interface SelectorChildProps {
id?: string;
name?: string;
value?: any;
checked?: boolean;
onChange?: React.EventHandler<React.ChangeEvent<HTMLInputElement>>;
}
interface Props {
id: string;
name: string;
onChange?: (value: any) => void;
value?: string;
className?: string;
children: Array<ReactElement<SelectorChildProps, any>>;
}
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]
);
return (
<Flex
className={cn(className, styles.root)}
itemGutter="half"
alignItems="stretch"
>
{React.Children.map(
children,
(c: ReactElement<SelectorChildProps, any>, i) =>
React.cloneElement(c, {
id: `${id}-${i}`,
name,
checked: c.props.value === value,
onChange: onItemChange,
})
)}
</Flex>
);
};
export default TileSelector;
@@ -0,0 +1,2 @@
export { default, default as TileSelector } from "./TileSelector";
export { default as Option } from "./TileOption";
+1
View File
@@ -47,3 +47,4 @@ export { default as Counter } from "./Counter";
export { Marker, Count as MarkerCount } from "./Marker";
export { default as Card } from "./Card";
export { default as PasswordField } from "./PasswordField";
export { TileSelector, Option as TileOption } from "./TileSelector";
+1 -1
View File
@@ -111,7 +111,7 @@
font-family: var(--font-family-sans-serif);
font-weight: var(--font-weight-medium);
font-size: calc(16rem / var(--rem-base));
line-height: calc(16em / 16);
line-height: calc(18em / 16);
letter-spacing: calc(-0.1em / 16);
}
@@ -846,7 +846,7 @@ type ReactionConfiguration {
type CommunityGuidelines {
"""
enable set to true will show the guidelines above the question box.
enable set to true will show the guidelines above the message box.
"""
enabled: Boolean!
@@ -2275,7 +2275,7 @@ input SettingsKarmaInput {
input SettingsCommunityGuidelinesInput {
"""
enable set to true will show the guidelines above the question box.
enable set to true will show the guidelines above the message box.
"""
enabled: Boolean
-5
View File
@@ -76,11 +76,6 @@ export interface Story extends TenantResource {
* createdAt is the date that the Story was added to the Talk database.
*/
createdAt: Date;
/**
* premodLinksEnable will put all comments that contain links into premod.
*/
premodLinksEnable?: boolean;
}
export async function createStoryIndexes(mongo: Db) {
+9
View File
@@ -145,3 +145,12 @@ configure-premod-description =
configure-premodLink-title = Pre-Moderate Comments Containing Links
configure-premodLink-description =
Moderators must approve any comment that contains a link before it is published to this stream.
configure-messageBox-title = Enable Message Box for this Stream
configure-messageBox-description =
Add a message to the top of the comment box for your readers. Use this to pose a topic,
ask a question or make announcements relating to this story.
configure-messageBox-preview = Preview
configure-messageBox-selectAnIcon = Select an Icon
configure-messageBox-noIcon = No Icon
configure-messageBox-writeAMessage = Write a Message