feat: added support for --only flag

Merge pull request #1731 from coralproject/next-watcher-flags

Watcher --only
Use JSDocs comments (#1727)


Merge branch 'next' into prevent-compile-loop-relay
Merge pull request #1726 from coralproject/prevent-compile-loop-relay

[next] Adapt relay watch config
[next] Remove nodemon (#1725)

* Remove old nodemon configs

* Remove nodemon

[next] Jest implementation for React Components (#1733)

* Make jest testing work with custom path and css modules

* Add first test

* feat: added unit tests to ci

* fix: updated package-lock.json

* Update cssTransform.js

* Update cssTransform.js

* Fix test in ci

Adapt files.exclude (#1736)


Permalink ui

Adding Copy to clipboard functionality

WIP

clean request

wip

progress

progress

work in progress

wip

ui functionality

Translations :/

wip

Merge branch 'permalink' of github.com:coralproject/talk into permalink

* 'permalink' of github.com:coralproject/talk: (42 commits)
  [next] Support server side jest testing (#1747)
  Update snapshots
  Add comments
  Remove precss
  Move react-responsive to dev deps
  Remove comment
  Mobile first approach
  Support standard css variables, dynamically set spacing-unit
  Add docs
  Fully implement Flex and MatchMedia
  Responsive Components <3
  fix: linting
  fix: adjusted pageInfo
  Remove obsoloe snapshot
  Move jsdom to dev deps
  Mark comments as always returning a value
  Add comment
  Fix unit tests
  Translate, concept for translation and id strings
  Add aria props
  ...

Adding Attachment and Popover Component

merge conflicts

progress

Any

Working

Support for refs

Ready

Merge branch 'permalink' of github.com:coralproject/talk into permalink

* 'permalink' of github.com:coralproject/talk: (101 commits)
  Ready
  Support for refs
  Working
  Any
  progress
  merge conflicts
  Make timeagoFormatter optional
  More colors
  Colors
  Short circuit endless respawn
  fix: new mongo parser
  Remove jest from watcher config, as it doesnt run well inside
  Add jest set
  Apply suggestions
  Move react-timeago to dev
  Upgrade docz
  Cleanup docz scripts
  Support watcher sets
  [next] Support server side jest testing (#1747)
  Make filter only a pure function
  ...
This commit is contained in:
Belén Curcio
2018-07-16 18:36:21 -03:00
parent 044e1c2863
commit b9a8fdb77b
183 changed files with 8963 additions and 2233 deletions
+3
View File
@@ -8,5 +8,8 @@ module.exports = {
production: {
plugins: [],
},
test: {
plugins: ["@babel/transform-modules-commonjs"],
},
},
};
@@ -1,7 +1,9 @@
import { LocalizationProvider } from "fluent-react/compat";
import { MessageContext } from "fluent/compat";
import React, { StatelessComponent } from "react";
import { Formatter } from "react-timeago";
import { Environment } from "relay-runtime";
import { UIContext } from "talk-ui/components";
export interface TalkContext {
// relayEnvironment for our relay framework.
@@ -9,6 +11,9 @@ export interface TalkContext {
// localMessages for our i18n framework.
localeMessages: MessageContext[];
// formatter for timeago.
timeagoFormatter?: Formatter;
}
const { Provider, Consumer } = React.createContext<TalkContext>({} as any);
@@ -27,7 +32,9 @@ export const TalkContextProvider: StatelessComponent<{
}> = ({ value, children }) => (
<Provider value={value}>
<LocalizationProvider messages={value.localeMessages}>
{children}
<UIContext.Provider value={{ timeagoFormatter: value.timeagoFormatter }}>
{children}
</UIContext.Provider>
</LocalizationProvider>
</Provider>
);
@@ -1,4 +1,7 @@
import { Localized } from "fluent-react/compat";
import { noop } from "lodash";
import React from "react";
import { Formatter } from "react-timeago";
import { Environment, Network, RecordSource, Store } from "relay-runtime";
import { generateMessages, LocalesData, negotiateLanguages } from "../i18n";
@@ -16,6 +19,25 @@ interface CreateContextArguments {
init?: ((context: TalkContext) => void | Promise<void>);
}
/**
* timeagoFormatter integrates timeago into our translation
* framework. It gets injected into the UIContext.
*/
export const timeagoFormatter: Formatter = (value, unit, suffix) => {
// We use 'in' instead of 'from now' for language consistency
const ourSuffix = suffix === "from now" ? "in" : suffix;
return (
<Localized
id="framework-timeago"
$value={value}
$unit={unit}
$suffix={ourSuffix}
>
<span>now</span>
</Localized>
);
};
/**
* `createContext` manages the dependencies of our framework
* and returns a `TalkContext` that can be passed to the
@@ -46,6 +68,7 @@ export default async function createContext({
const context = {
relayEnvironment,
localeMessages,
timeagoFormatter,
};
// Run custom initializations.
@@ -1,5 +1,9 @@
import * as React from "react";
import { hoistStatics, InferableComponentEnhancer } from "recompose";
import {
hoistStatics,
InferableComponentEnhancer,
wrapDisplayName,
} from "recompose";
import { TalkContext, TalkContextConsumer } from "./TalkContext";
@@ -12,11 +16,17 @@ function withContext<T>(
propsCallback: (context: TalkContext) => T
): InferableComponentEnhancer<T> {
return hoistStatics<T>(
<U extends T>(WrappedComponent: React.ComponentType<U>) => (props: any) => (
<TalkContextConsumer>
{context => <WrappedComponent {...props} {...propsCallback(context)} />}
</TalkContextConsumer>
)
<U extends T>(WrappedComponent: React.ComponentType<U>) => {
const Component: React.StatelessComponent<any> = props => (
<TalkContextConsumer>
{context => (
<WrappedComponent {...props} {...propsCallback(context)} />
)}
</TalkContextConsumer>
);
Component.displayName = wrapDisplayName(WrappedComponent, "withContext");
return Component;
}
);
}
+1 -1
View File
@@ -13,7 +13,7 @@ export const VALIDATION_REQUIRED = () => (
);
export const VALIDATION_TOO_SHORT = () => (
<Localized id="framework-validation-too-short">
<Localized id="framework-validation-tooShort">
<span>This field is too short.</span>
</Localized>
);
@@ -1,24 +1,18 @@
import React, { Component } from "react";
import { QueryRenderer } from "react-relay";
import { CacheConfig, GraphQLTaggedNode, RerunParam } from "relay-runtime";
import {
QueryRenderer,
QueryRendererProps as QueryRendererPropsOrig,
} from "react-relay";
import { Omit } from "talk-framework/types";
import { TalkContextConsumer } from "../bootstrap/TalkContext";
// Taken from relay types and added Generic support for Variables and Response
export interface QueryRendererProps<V, R> {
cacheConfig?: CacheConfig;
query?: GraphQLTaggedNode | null;
render(readyState: ReadyState<R>): React.ReactElement<any> | undefined | null;
variables: V;
rerunParamExperimental?: RerunParam;
}
// Taken from relay types and added Generic support for Variables and Response
export interface ReadyState<R> {
error: Error | undefined | null;
props: R | undefined | null;
retry?(): void;
}
// Omit environment as we are passing this from the context.
export type QueryRendererProps<V, R> = Omit<
QueryRendererPropsOrig<V, R>,
"environment"
>;
/**
* TalkQueryRenderer is a wrappper around Relay's `QueryRenderer`.
@@ -1,5 +1,10 @@
import * as React from "react";
import { compose, hoistStatics, InferableComponentEnhancer } from "recompose";
import {
compose,
hoistStatics,
InferableComponentEnhancer,
wrapDisplayName,
} from "recompose";
import { Environment } from "relay-runtime";
import { withContext } from "../bootstrap";
@@ -19,6 +24,11 @@ function createMutationContainer<T extends string, I, R>(
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
hoistStatics((WrappedComponent: React.ComponentType<any>) => {
class CreateMutationContainer extends React.Component<any> {
public static displayName = wrapDisplayName(
WrappedComponent,
"createMutationContainer"
);
private commit = (input: I) => {
return commit(this.props.relayEnvironment, input);
};
@@ -7,6 +7,7 @@ export { default as QueryRenderer } from "./QueryRenderer";
export * from "./QueryRenderer";
export { default as createMutationContainer } from "./createMutationContainer";
export { default as createAndRetain } from "./createAndRetain";
export { default as wrapFetchWithLogger } from "./wrapFetchWithLogger";
export {
commitMutationPromise,
commitMutationPromiseNormalized,
@@ -6,7 +6,7 @@ import { InferableComponentEnhancerWithProps } from "recompose";
* from Relay.
*/
export default <T>(
fragmentSpec: GraphQLTaggedNode
fragmentSpec: { [P in keyof T]: GraphQLTaggedNode }
): InferableComponentEnhancerWithProps<T, { [P in keyof T]: any }> => (
component: React.ComponentType<any>
) => createFragmentContainer(component, fragmentSpec) as any;
@@ -1,6 +1,11 @@
import * as React from "react";
import { compose, hoistStatics, InferableComponentEnhancer } from "recompose";
import { CSelector, CSnapshot, Environment } from "relay-runtime";
import {
CSelector,
CSnapshot,
Environment,
GraphQLTaggedNode,
} from "relay-runtime";
import { withContext } from "../bootstrap";
@@ -25,7 +30,7 @@ export const LOCAL_ID = "client:root.local";
* must have the `LOCAL_ID`.
*/
function withLocalStateContainer<T>(
fragmentSpec: any
fragmentSpec: GraphQLTaggedNode
): InferableComponentEnhancer<{ local: T }> {
return compose(
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
@@ -33,7 +38,7 @@ function withLocalStateContainer<T>(
class LocalStateContainer extends React.Component<Props, any> {
constructor(props: Props) {
super(props);
const fragment = fragmentSpec.data().default;
const fragment = (fragmentSpec as any).data().default;
if (fragment.kind !== "Fragment") {
throw new Error("Expected fragment");
}
@@ -10,9 +10,13 @@ import { InferableComponentEnhancerWithProps } from "recompose";
* withPaginationContainer is a curried version of `createPaginationContainers`
* from Relay.
*/
export default <T, InnerProps>(
fragmentSpec: GraphQLTaggedNode,
connectionConfig: ConnectionConfig<InnerProps>
export default <T, InnerProps, FragmentVariables, QueryVariables>(
fragmentSpec: { [P in keyof T]: GraphQLTaggedNode },
connectionConfig: ConnectionConfig<
InnerProps,
FragmentVariables,
QueryVariables
>
): InferableComponentEnhancerWithProps<
T & { relay: RelayPaginationProp },
{ [P in keyof T]: any }
@@ -10,7 +10,7 @@ import { InferableComponentEnhancerWithProps } from "recompose";
* from Relay.
*/
export default <T>(
fragmentSpec: GraphQLTaggedNode,
fragmentSpec: { [P in keyof T]: GraphQLTaggedNode },
refetchQuery: GraphQLTaggedNode
): InferableComponentEnhancerWithProps<
T & { relay: RelayRefetchProp },
@@ -0,0 +1,25 @@
import { FetchFunction } from "relay-runtime";
/**
* Decorates the fetch function with error logging.
* Intended for testing purposes.
*/
export default function wrapFetchWithLogger(
fetch: FetchFunction,
logResult?: boolean
): FetchFunction {
return async (...args: any[]) => {
try {
const result = await (fetch as any)(...args);
if (logResult) {
// tslint:disable-next-line:no-console
console.log(JSON.stringify(result));
}
return result;
} catch (err) {
// tslint:disable-next-line:no-console
console.error(err);
throw err;
}
};
}
-1
View File
@@ -1,5 +1,4 @@
const path = require("path");
module.exports = {
extends: "../.babelrc.js",
plugins: [
+14
View File
@@ -0,0 +1,14 @@
/* Here we add global stylings for body and document */
:global {
body {
margin: "0";
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
}
}
.root {
}
@@ -0,0 +1,22 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import App from "./App";
it("renders correctly", () => {
const props: PropTypesOf<typeof App> = {
asset: {},
};
const wrapper = shallow(<App {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders correctly when asset is null", () => {
const props: PropTypesOf<typeof App> = {
asset: null,
};
const wrapper = shallow(<App {...props} />);
expect(wrapper).toMatchSnapshot();
});
+7 -18
View File
@@ -1,33 +1,22 @@
import * as React from "react";
import { StatelessComponent } from "react";
import { Center } from "talk-ui/components";
import { Flex } from "talk-ui/components";
import AssetListContainer from "../containers/AssetListContainer";
import PostCommentFormContainer from "../containers/PostCommentFormContainer";
import StreamContainer from "../containers/StreamContainer";
import Logo from "./Logo";
import * as styles from "./App.css";
export interface AppProps {
assets?: any | null;
asset?: {
id: string;
isClosed: boolean;
comments: any | null;
} | null;
asset: {} | null;
}
const App: StatelessComponent<AppProps> = props => {
if (props.assets) {
return <AssetListContainer assets={props.assets} />;
}
if (props.asset) {
return (
<Center>
<Logo gutterBottom />
<StreamContainer comments={props.asset.comments} />
<PostCommentFormContainer assetID={props.asset.id} />
</Center>
<Flex justifyContent="center" className={styles.root}>
<StreamContainer asset={props.asset} />
</Flex>
);
}
return <div>Asset not found </div>;
@@ -1,16 +0,0 @@
import * as React from "react";
import { StatelessComponent } from "react";
export interface AssetListProps {
assets: ReadonlyArray<{ id: string; title: string | null }>;
}
const AssetList: StatelessComponent<AssetListProps> = props => {
return (
<div>
{props.assets.map(asset => <div key={asset.id}>{asset.title}</div>)}
</div>
);
};
export default AssetList;
@@ -1,11 +0,0 @@
.root {
width: 400px;
}
.gutterBottom {
margin-bottom: calc(2px * $spacing-unit);
}
.author {
font-weight: $font-weight-medium;
}
@@ -1,32 +0,0 @@
import cn from "classnames";
import React from "react";
import { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
import * as styles from "./Comment.css";
export interface CommentProps {
className?: string;
author: {
username: string;
} | null;
body: string | null;
gutterBottom?: boolean;
}
const Comment: StatelessComponent<CommentProps> = props => {
const rootClassName = cn(styles.root, props.className, {
[styles.gutterBottom]: props.gutterBottom,
});
return (
<div className={rootClassName}>
<Typography className={styles.author} gutterBottom>
{props.author && props.author.username}
</Typography>
<Typography>{props.body}</Typography>
</div>
);
};
export default Comment;
@@ -0,0 +1,19 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Comment from "./Comment";
it("renders username and body", () => {
const props: PropTypesOf<typeof Comment> = {
id: "comment-id",
author: {
username: "Marvin",
},
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
};
const wrapper = shallow(<Comment {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,43 @@
import { Localized } from "fluent-react/compat";
import React from "react";
import { StatelessComponent } from "react";
import { Button, Popover, Typography } from "talk-ui/components";
import PermalinkPopover from "../PermalinkPopover";
import Timestamp from "./Timestamp";
import TopBar from "./TopBar";
import Username from "./Username";
export interface CommentProps {
id: string;
className?: string;
author: {
username: string;
} | null;
body: string | null;
createdAt: string;
}
const Comment: StatelessComponent<CommentProps> = props => {
return (
<div role="article">
<TopBar>
{props.author && <Username>{props.author.username}</Username>}
<Timestamp>{props.createdAt}</Timestamp>
</TopBar>
<Typography>{props.body}</Typography>
<div>
<Popover body={<PermalinkPopover commentId={props.id} />}>
{({ toggleShow, ref }) => (
<Button onClick={toggleShow} innerRef={ref} primary>
<Localized id="comments-permalink-share">
<span>Share</span>
</Localized>
</Button>
)}
</Popover>
</div>
</div>
);
};
export default Comment;
@@ -0,0 +1,3 @@
.root {
composes: timestamp from "talk-ui/shared/typography.css";
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Timestamp from "./Timestamp";
it("renders correctly", () => {
const props: PropTypesOf<typeof Timestamp> = {
children: "1995-12-17T03:24:00.000Z",
};
const wrapper = shallow(<Timestamp {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,16 @@
import React from "react";
import { StatelessComponent } from "react";
import { RelativeTime } from "talk-ui/components";
import * as styles from "./Timestamp.css";
export interface TimestampProps {
children: string;
}
const Timestamp: StatelessComponent<TimestampProps> = props => (
<RelativeTime className={styles.root} date={props.children} />
);
export default Timestamp;
@@ -0,0 +1,3 @@
.root {
margin-bottom: calc(0.5 * var(--spacing-unit));
}
@@ -0,0 +1,45 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import { PropTypesOf } from "talk-framework/types";
import { UIContext, UIContextProps } from "talk-ui/components";
import TopBar from "./TopBar";
it("renders correctly on small screens", () => {
const props: PropTypesOf<typeof TopBar> = {
children: <div>Hello World</div>,
};
const context: UIContextProps = {
mediaQueryValues: {
width: 320,
},
};
const testRenderer = TestRenderer.create(
<UIContext.Provider value={context}>
<TopBar {...props} />
</UIContext.Provider>
);
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("renders correctly on big screens", () => {
const props: PropTypesOf<typeof TopBar> = {
children: <div>Hello World</div>,
};
const context: UIContextProps = {
mediaQueryValues: {
width: 1600,
},
};
const testRenderer = TestRenderer.create(
<UIContext.Provider value={context}>
<TopBar {...props} />
</UIContext.Provider>
);
expect(testRenderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,32 @@
import cn from "classnames";
import React from "react";
import { StatelessComponent } from "react";
import { Flex, MatchMedia } from "talk-ui/components";
import * as styles from "./TopBar.css";
export interface TopBarProps {
className?: string;
children: React.ReactNode;
}
const TopBar: StatelessComponent<TopBarProps> = props => {
const rootClassName = cn(styles.root, props.className);
return (
<MatchMedia minWidth="xs">
{matches => (
<Flex
className={rootClassName}
alignItems="baseline"
direction={matches ? "row" : "column"}
itemGutter={matches ? true : "half"}
>
{props.children}
</Flex>
)}
</MatchMedia>
);
};
export default TopBar;
@@ -0,0 +1,3 @@
.root {
line-height: 1;
}
@@ -0,0 +1,45 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import { PropTypesOf } from "talk-framework/types";
import { UIContext, UIContextProps } from "talk-ui/components";
import Username from "./Username";
it("renders correctly on small screens", () => {
const props: PropTypesOf<typeof Username> = {
children: "Marvin",
};
const context: UIContextProps = {
mediaQueryValues: {
width: 320,
},
};
const testRenderer = TestRenderer.create(
<UIContext.Provider value={context}>
<Username {...props} />
</UIContext.Provider>
);
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("renders correctly on big screens", () => {
const props: PropTypesOf<typeof Username> = {
children: "Marvin",
};
const context: UIContextProps = {
mediaQueryValues: {
width: 1600,
},
};
const testRenderer = TestRenderer.create(
<UIContext.Provider value={context}>
<Username {...props} />
</UIContext.Provider>
);
expect(testRenderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,28 @@
import React from "react";
import { StatelessComponent } from "react";
import { MatchMedia, Typography } from "talk-ui/components";
import * as styles from "./Username.css";
export interface UsernameProps {
children: string;
}
const Username: StatelessComponent<UsernameProps> = props => {
return (
<MatchMedia minWidth="xs">
{matches => (
<Typography
variant={matches ? "heading2" : "heading3"}
className={styles.root}
component="span"
>
{props.children}
</Typography>
)}
</MatchMedia>
);
};
export default Username;
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders username and body 1`] = `
<div
role="article"
>
<TopBar>
<Username>
Marvin
</Username>
<Timestamp>
1995-12-17T03:24:00.000Z
</Timestamp>
</TopBar>
<withPropsOnChange(Typography)>
Woof
</withPropsOnChange(Typography)>
</div>
`;
@@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(RelativeTime)
className="Timestamp-root"
date="1995-12-17T03:24:00.000Z"
/>
`;
@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly on big screens 1`] = `
<div
className="Flex-root TopBar-root Flex-itemGutter Flex-alignBaseline Flex-directionRow"
>
<div>
Hello World
</div>
</div>
`;
exports[`renders correctly on small screens 1`] = `
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<div>
Hello World
</div>
</div>
`;
@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly on big screens 1`] = `
<span
className="Typography-root Typography-heading2 Username-root"
>
Marvin
</span>
`;
exports[`renders correctly on small screens 1`] = `
<span
className="Typography-root Typography-heading3 Username-root"
>
Marvin
</span>
`;
@@ -0,0 +1 @@
export { default, default as Comment, CommentProps } from "./Comment";
@@ -0,0 +1,8 @@
.root {
border-left: 3px solid;
padding-left: var(--spacing-unit);
}
.level0 {
border-color: var(--palette-secondary-darkest);
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Indent from "./Indent";
it("renders correctly", () => {
const props: PropTypesOf<typeof Indent> = {
children: <div>Hello World</div>,
};
const wrapper = shallow(<Indent {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,15 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import * as styles from "./Indent.css";
export interface IndentProps {
level?: number;
children: React.ReactNode;
}
const Indent: StatelessComponent<IndentProps> = props => {
return <div className={cn(styles.root, styles.level0)}>{props.children}</div>;
};
export default Indent;
@@ -0,0 +1,13 @@
.textField {
margin-right: 5px;
}
.root {
background-color: #ffffff;
border: 1px solid #c9cacb;
box-sizing: border-box;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.25);
border-radius: 1px;
padding: 6px 10px;
display: flex;
}
@@ -0,0 +1,60 @@
import { Localized } from "fluent-react/compat";
import React, { CSSProperties } from "react";
import CopyToClipboard from "react-copy-to-clipboard";
import { RefHandler } from "react-popper";
import { Button, TextField } from "talk-ui/components";
import * as styles from "./PermalinkPopover.css";
interface InnerProps {
commentId: string;
style?: CSSProperties;
innerRef?: RefHandler;
}
interface State {
copied: boolean;
}
class PermalinkPopover extends React.Component<InnerProps> {
public state: State = {
copied: false,
};
public onCopy = async () => {
await this.toggleCopied();
setTimeout(() => {
this.toggleCopied();
}, 800);
};
public toggleCopied = () => {
this.setState((state: State) => ({
copied: !state.copied,
}));
};
public render() {
const { commentId, style, innerRef } = this.props;
const { copied } = this.state;
return (
<div className={styles.root} style={style} ref={innerRef}>
<TextField defaultValue={commentId} className={styles.textField} />
<CopyToClipboard text={commentId} onCopy={this.onCopy}>
<Button primary>
{copied ? (
<Localized id="comments-permalink-copied">
<span>Copied!</span>
</Localized>
) : (
<Localized id="comments-permalink-copy">
<span>Copy</span>
</Localized>
)}
</Button>
</CopyToClipboard>
</div>
);
}
}
export default PermalinkPopover;
@@ -3,10 +3,12 @@
display: block;
height: 100px;
width: 400px;
margin-bottom: calc(2px * $spacing-unit);
width: 100%;
box-sizing: border-box;
margin-bottom: var(--spacing-unit);
}
.postButton {
float: right;
.postButtonContainer {
display: flex;
justify-content: flex-end;
}
@@ -39,11 +39,13 @@ const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
</div>
)}
</Field>
<Localized id="postCommentForm-submit">
<Button className={styles.postButton} disabled={submitting} primary>
Post
</Button>
</Localized>
<div className={styles.postButtonContainer}>
<Localized id="comments-postCommentForm-post">
<Button disabled={submitting} primary>
Post
</Button>
</Localized>
</div>
</form>
)}
</Form>
@@ -0,0 +1,42 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import sinon, { SinonSpy } from "sinon";
import { PropTypesOf } from "talk-framework/types";
import ReplyList from "./ReplyList";
it("renders correctly", () => {
const props: PropTypesOf<typeof ReplyList> = {
commentID: "comment-id",
comments: [{ id: "comment-1" }, { id: "comment-2" }],
onShowAll: noop,
hasMore: false,
disableShowAll: false,
};
const wrapper = shallow(<ReplyList {...props} />);
expect(wrapper).toMatchSnapshot();
});
describe("when there is more", () => {
const props: PropTypesOf<typeof ReplyList> = {
commentID: "comment-id",
comments: [{ id: "comment-1" }, { id: "comment-2" }],
onShowAll: sinon.spy(),
hasMore: true,
disableShowAll: false,
};
const wrapper = shallow(<ReplyList {...props} />);
it("renders a load more button", () => {
expect(wrapper).toMatchSnapshot();
});
it("calls onLoadMore", () => {
wrapper
.find("#talk-comments-replyList-showAll--comment-id")
.simulate("click");
expect((props.onShowAll as SinonSpy).calledOnce).toBe(true);
});
});
@@ -0,0 +1,50 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { Button, Flex } from "talk-ui/components";
import CommentContainer from "../containers/CommentContainer";
import Indent from "./Indent";
export interface ReplyListProps {
commentID: string;
comments: ReadonlyArray<{ id: string }>;
onShowAll: () => void;
hasMore: boolean;
disableShowAll: boolean;
}
const ReplyList: StatelessComponent<ReplyListProps> = props => {
return (
<Indent>
<Flex
direction="column"
id={`talk-comments-replyList-log--${props.commentID}`}
role="log"
itemGutter
>
{props.comments.map(comment => (
<CommentContainer key={comment.id} data={comment} />
))}
{props.hasMore && (
<Localized id="comments-replyList-showAll">
<Button
id={`talk-comments-replyList-showAll--${props.commentID}`}
aria-controls={`talk-comments-replyList-log--${props.commentID}`}
onClick={props.onShowAll}
disabled={props.disableShowAll}
secondary
invert
fullWidth
>
Show All Replies
</Button>
</Localized>
)}
</Flex>
</Indent>
);
};
export default ReplyList;
@@ -0,0 +1,4 @@
.root {
width: 100%;
max-width: 400px;
}
@@ -0,0 +1,47 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import sinon, { SinonSpy } from "sinon";
import { PropTypesOf } from "talk-framework/types";
import Stream from "./Stream";
it("renders correctly", () => {
const props: PropTypesOf<typeof Stream> = {
assetID: "asset-id",
isClosed: false,
comments: [{ id: "comment-1" }, { id: "comment-2" }],
onLoadMore: noop,
disableLoadMore: false,
hasMore: false,
};
const wrapper = shallow(<Stream {...props} />);
expect(wrapper).toMatchSnapshot();
});
describe("when there is more", () => {
const props: PropTypesOf<typeof Stream> = {
assetID: "asset-id",
isClosed: false,
comments: [{ id: "comment-1" }, { id: "comment-2" }],
onLoadMore: sinon.spy(),
disableLoadMore: false,
hasMore: true,
};
const wrapper = shallow(<Stream {...props} />);
it("renders a load more button", () => {
expect(wrapper).toMatchSnapshot();
});
it("calls onLoadMore", () => {
wrapper.find("#talk-comments-stream-loadMore").simulate("click");
expect((props.onLoadMore as SinonSpy).calledOnce).toBe(true);
});
const wrapperDisabledButton = shallow(<Stream {...props} disableLoadMore />);
it("disables load more button", () => {
expect(wrapperDisabledButton).toMatchSnapshot();
});
});
+44 -4
View File
@@ -1,18 +1,58 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { Button, Flex } from "talk-ui/components";
import CommentContainer from "../containers/CommentContainer";
import PostCommentFormContainer from "../containers/PostCommentFormContainer";
import ReplyListContainer from "../containers/ReplyListContainer";
import Logo from "./Logo";
import * as styles from "./Stream.css";
export interface StreamProps {
assetID: string;
isClosed: boolean;
comments: ReadonlyArray<{ id: string }>;
onLoadMore: () => void;
hasMore: boolean;
disableLoadMore: boolean;
}
const Stream: StatelessComponent<StreamProps> = props => {
return (
<div>
{props.comments.map(comment => (
<CommentContainer key={comment.id} data={comment} gutterBottom />
))}
<div className={styles.root}>
<Logo gutterBottom />
<PostCommentFormContainer assetID={props.assetID} />
<Flex
direction="column"
id="talk-comments-stream-log"
role="log"
aria-live="polite"
itemGutter
>
{props.comments.map(comment => (
<Flex direction="column" key={comment.id} itemGutter>
<CommentContainer data={comment} />
<ReplyListContainer comment={comment} />
</Flex>
))}
{props.hasMore && (
<Localized id="comments-stream-loadMore">
<Button
id={"talk-comments-stream-loadMore"}
onClick={props.onLoadMore}
secondary
invert
fullWidth
disabled={props.disableLoadMore}
aria-controls="talk-comments-stream-log"
>
Load More
</Button>
</Localized>
)}
</Flex>
</div>
);
};
@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Flex
className="App-root"
justifyContent="center"
>
<Relay(StreamContainer)
asset={Object {}}
/>
</Flex>
`;
exports[`renders correctly when asset is null 1`] = `
<div>
Asset not found
</div>
`;
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="Indent-root Indent-level0"
>
<div>
Hello World
</div>
</div>
`;
@@ -0,0 +1,72 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Indent>
<Flex
direction="column"
id="talk-comments-replyList-log--comment-id"
itemGutter={true}
role="log"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
key="comment-1"
/>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
key="comment-2"
/>
</Flex>
</Indent>
`;
exports[`when there is more renders a load more button 1`] = `
<Indent>
<Flex
direction="column"
id="talk-comments-replyList-log--comment-id"
itemGutter={true}
role="log"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
key="comment-1"
/>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
key="comment-2"
/>
<Localized
id="comments-replyList-showAll"
>
<withPropsOnChange(Button)
aria-controls="talk-comments-replyList-log--comment-id"
disabled={false}
fullWidth={true}
id="talk-comments-replyList-showAll--comment-id"
invert={true}
onClick={[Function]}
secondary={true}
>
Show All Replies
</withPropsOnChange(Button)>
</Localized>
</Flex>
</Indent>
`;
@@ -0,0 +1,214 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="Stream-root"
>
<Logo
gutterBottom={true}
/>
<withContext(createMutationContainer(PostCommentFormContainer))
assetID="asset-id"
/>
<Flex
aria-live="polite"
direction="column"
id="talk-comments-stream-log"
itemGutter={true}
role="log"
>
<Flex
direction="column"
itemGutter={true}
key="comment-1"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-1",
}
}
/>
</Flex>
<Flex
direction="column"
itemGutter={true}
key="comment-2"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-2",
}
}
/>
</Flex>
</Flex>
</div>
`;
exports[`when there is more disables load more button 1`] = `
<div
className="Stream-root"
>
<Logo
gutterBottom={true}
/>
<withContext(createMutationContainer(PostCommentFormContainer))
assetID="asset-id"
/>
<Flex
aria-live="polite"
direction="column"
id="talk-comments-stream-log"
itemGutter={true}
role="log"
>
<Flex
direction="column"
itemGutter={true}
key="comment-1"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-1",
}
}
/>
</Flex>
<Flex
direction="column"
itemGutter={true}
key="comment-2"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-2",
}
}
/>
</Flex>
<Localized
id="comments-stream-loadMore"
>
<withPropsOnChange(Button)
aria-controls="talk-comments-stream-log"
disabled={true}
fullWidth={true}
id="talk-comments-stream-loadMore"
invert={true}
onClick={[Function]}
secondary={true}
>
Load More
</withPropsOnChange(Button)>
</Localized>
</Flex>
</div>
`;
exports[`when there is more renders a load more button 1`] = `
<div
className="Stream-root"
>
<Logo
gutterBottom={true}
/>
<withContext(createMutationContainer(PostCommentFormContainer))
assetID="asset-id"
/>
<Flex
aria-live="polite"
direction="column"
id="talk-comments-stream-log"
itemGutter={true}
role="log"
>
<Flex
direction="column"
itemGutter={true}
key="comment-1"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-1",
}
}
/>
</Flex>
<Flex
direction="column"
itemGutter={true}
key="comment-2"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-2",
}
}
/>
</Flex>
<Localized
id="comments-stream-loadMore"
>
<withPropsOnChange(Button)
aria-controls="talk-comments-stream-log"
disabled={false}
fullWidth={true}
id="talk-comments-stream-loadMore"
invert={true}
onClick={[Function]}
secondary={true}
>
Load More
</withPropsOnChange(Button)>
</Localized>
</Flex>
</div>
`;
@@ -0,0 +1,16 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import { AppContainer } from "./AppContainer";
it("renders correctly", () => {
const props: PropTypesOf<typeof AppContainer> = {
data: {
asset: {},
},
};
const wrapper = shallow(<AppContainer {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -11,30 +11,20 @@ interface InnerProps {
data: Data;
}
const AppContainer: StatelessComponent<InnerProps> = props => {
export const AppContainer: StatelessComponent<InnerProps> = props => {
return <App {...props.data} />;
};
const enhanced = withFragmentContainer<{ data: Data }>(
graphql`
const enhanced = withFragmentContainer<{ data: Data }>({
data: graphql`
fragment AppContainer on Query
@argumentDefinitions(
assetID: { type: "ID!" }
showAssetList: { type: "Boolean!" }
) {
assets @include(if: $showAssetList) {
...AssetListContainer_assets
}
asset(id: $assetID) @skip(if: $showAssetList) {
id
isClosed
comments {
...StreamContainer_comments
}
@argumentDefinitions(assetID: { type: "ID!" }) {
asset(id: $assetID) {
...StreamContainer_asset
}
}
`
)(AppContainer);
`,
})(AppContainer);
export type AppContainerProps = PropTypesOf<typeof enhanced>;
export default enhanced;
@@ -1,33 +0,0 @@
import React, { StatelessComponent } from "react";
import { graphql } from "react-relay";
import { withFragmentContainer } from "talk-framework/lib/relay";
import { PropTypesOf } from "talk-framework/types";
import { AssetListContainer_assets as Data } from "talk-stream/__generated__/AssetListContainer_assets.graphql";
import AssetList from "../components/AssetList";
interface InnerProps {
assets: Data;
}
const AssetListContainer: StatelessComponent<InnerProps> = props => {
const assets = props.assets.edges.map(edge => edge.node);
return <AssetList assets={assets} />;
};
const enhanced = withFragmentContainer<{ assets: Data }>(
graphql`
fragment AssetListContainer_assets on AssetsConnection {
edges {
node {
id
title
}
}
}
`
)(AssetListContainer);
export type AssetListContainerProps = PropTypesOf<typeof enhanced>;
export default enhanced;
@@ -0,0 +1,21 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import { CommentContainer } from "./CommentContainer";
it("renders username and body", () => {
const props: PropTypesOf<typeof CommentContainer> = {
data: {
author: {
username: "Marvin",
},
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
},
};
const wrapper = shallow(<CommentContainer {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -2,28 +2,39 @@ import React, { StatelessComponent } from "react";
import { graphql } from "react-relay";
import withFragmentContainer from "talk-framework/lib/relay/withFragmentContainer";
import { Omit, PropTypesOf } from "talk-framework/types";
import { PropTypesOf } from "talk-framework/types";
import { CommentContainer as Data } from "talk-stream/__generated__/CommentContainer.graphql";
import Comment, { CommentProps } from "../components/Comment";
import Comment from "../components/Comment";
type InnerProps = { data: Data } & Omit<CommentProps, keyof Data>;
interface InnerProps {
data: Data;
}
const CommentContainer: StatelessComponent<InnerProps> = props => {
// tslint:disable-next-line:no-unused-expression
graphql`
fragment CommentContainer_comment on Comment {
id
author {
username
}
body
createdAt
}
`;
export const CommentContainer: StatelessComponent<InnerProps> = props => {
const { data, ...rest } = props;
return <Comment {...rest} {...props.data} />;
};
const enhanced = withFragmentContainer<{ data: Data }>(
graphql`
const enhanced = withFragmentContainer<{ data: Data }>({
data: graphql`
fragment CommentContainer on Comment {
author {
username
}
body
...CommentContainer_comment @relay(mask: false)
}
`
)(CommentContainer);
`,
})(CommentContainer);
export type CommentContainerProps = PropTypesOf<typeof enhanced>;
export default enhanced;
@@ -25,6 +25,8 @@ class PostCommentFormContainer extends Component<InnerProps> {
if (error instanceof BadUserInputError) {
return error.invalidArgsLocalized;
}
// tslint:disable-next-line:no-console
console.error(error);
}
return undefined;
};
@@ -0,0 +1,86 @@
import { shallow, ShallowWrapper } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import ReplyList from "../components/ReplyList";
import { ReplyListContainer } from "./ReplyListContainer";
it("renders correctly", () => {
const props: PropTypesOf<typeof ReplyListContainer> = {
comment: {
id: "comment-id",
replies: {
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
},
},
relay: {
hasMore: noop,
isLoading: noop,
} as any,
};
const wrapper = shallow(<ReplyListContainer {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders correctly when replies are null", () => {
const props: PropTypesOf<typeof ReplyListContainer> = {
comment: {
id: "comment-id",
replies: null,
},
relay: {
hasMore: noop,
isLoading: noop,
} as any,
};
const wrapper = shallow(<ReplyListContainer {...props} />);
expect(wrapper).toMatchSnapshot();
});
describe("when has more replies", () => {
let finishLoading: ((error?: Error) => void) | null = null;
const props: PropTypesOf<ReplyListContainer> = {
comment: {
id: "comment-id",
replies: {
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
},
},
relay: {
hasMore: () => true,
isLoading: () => false,
loadMore: (_: any, callback: () => void) => (finishLoading = callback),
} as any,
};
let wrapper: ShallowWrapper;
beforeAll(() => (wrapper = shallow(<ReplyListContainer {...props} />)));
it("renders hasMore", () => {
expect(wrapper).toMatchSnapshot();
});
describe("when showing all", () => {
beforeAll(() => {
wrapper
.find(ReplyList)
.props()
.onShowAll();
});
it("calls relay loadMore", () => {
expect(finishLoading).not.toBeNull();
});
it("disables show all button", () => {
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
it("enable show all button after loading is done", () => {
finishLoading!();
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
});
});
@@ -0,0 +1,135 @@
import React from "react";
import { graphql, RelayPaginationProp } from "react-relay";
import { withPaginationContainer } from "talk-framework/lib/relay";
import { PropTypesOf } from "talk-framework/types";
import { ReplyListContainer_comment as Data } from "talk-stream/__generated__/ReplyListContainer_comment.graphql";
import {
COMMENT_SORT,
ReplyListContainerPaginationQueryVariables,
} from "talk-stream/__generated__/ReplyListContainerPaginationQuery.graphql";
import ReplyList from "../components/ReplyList";
export interface InnerProps {
comment: Data;
relay: RelayPaginationProp;
}
export class ReplyListContainer extends React.Component<InnerProps> {
public state = {
disableShowAll: false,
};
public render() {
if (
this.props.comment.replies === null ||
this.props.comment.replies.edges.length === 0
) {
return null;
}
const comments = this.props.comment.replies.edges.map(edge => edge.node);
return (
<ReplyList
commentID={this.props.comment.id}
comments={comments}
onShowAll={this.showAll}
hasMore={this.props.relay.hasMore()}
disableShowAll={this.state.disableShowAll}
/>
);
}
private showAll = () => {
if (!this.props.relay.hasMore() || this.props.relay.isLoading()) {
return;
}
this.setState({ disableShowAll: true });
this.props.relay.loadMore(
999999999, // Fetch All Replies
error => {
this.setState({ disableShowAll: false });
if (error) {
// tslint:disable-next-line:no-console
console.error(error);
}
}
);
};
}
// TODO: (cvle) This should be autogenerated.
interface FragmentVariables {
count: number;
cursor?: string;
orderBy: COMMENT_SORT;
}
const enhanced = withPaginationContainer<
{ comment: Data },
InnerProps,
FragmentVariables,
ReplyListContainerPaginationQueryVariables
>(
{
comment: graphql`
fragment ReplyListContainer_comment on Comment
@argumentDefinitions(
count: { type: "Int!", defaultValue: 5 }
cursor: { type: "Cursor" }
orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC }
) {
id
replies(first: $count, after: $cursor, orderBy: $orderBy)
@connection(key: "ReplyList_replies") {
edges {
node {
id
...CommentContainer
}
}
}
}
`,
},
{
direction: "forward",
getConnectionFromProps(props) {
return props.comment && props.comment.replies;
},
// This is also the default implementation of `getFragmentVariables` if it isn't provided.
getFragmentVariables(prevVars, totalCount) {
return {
...prevVars,
count: totalCount,
};
},
getVariables(props, { count, cursor }, fragmentVariables) {
return {
count,
cursor,
orderBy: fragmentVariables.orderBy,
commentID: props.comment.id,
};
},
query: graphql`
# Pagination query to be fetched upon calling 'loadMore'.
# Notice that we re-use our fragment, and the shape of this query matches our fragment spec.
query ReplyListContainerPaginationQuery(
$count: Int!
$cursor: Cursor
$orderBy: COMMENT_SORT!
$commentID: ID!
) {
comment(id: $commentID) {
...ReplyListContainer_comment
@arguments(count: $count, cursor: $cursor, orderBy: $orderBy)
}
}
`,
}
)(ReplyListContainer);
export type ReplyListContainerProps = PropTypesOf<typeof enhanced>;
export default enhanced;
@@ -0,0 +1,73 @@
import { shallow, ShallowWrapper } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Stream from "../components/Stream";
import { StreamContainer } from "./StreamContainer";
it("renders correctly", () => {
const props: PropTypesOf<StreamContainer> = {
asset: {
id: "asset-id",
isClosed: false,
comments: {
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
},
},
relay: {
hasMore: noop,
isLoading: noop,
} as any,
};
const wrapper = shallow(<StreamContainer {...props} />);
expect(wrapper).toMatchSnapshot();
});
describe("when has more comments", () => {
let finishLoading: ((error?: Error) => void) | null = null;
const props: PropTypesOf<StreamContainer> = {
asset: {
id: "asset-id",
isClosed: false,
comments: {
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
},
},
relay: {
hasMore: () => true,
isLoading: () => false,
loadMore: (_: any, callback: () => void) => (finishLoading = callback),
} as any,
};
let wrapper: ShallowWrapper;
beforeAll(() => (wrapper = shallow(<StreamContainer {...props} />)));
it("renders hasMore", () => {
expect(wrapper).toMatchSnapshot();
});
describe("when loading more", () => {
beforeAll(() => {
wrapper
.find(Stream)
.props()
.onLoadMore();
});
it("calls relay loadMore", () => {
expect(finishLoading).not.toBeNull();
});
it("disables load more button", () => {
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
it("enable load more button after loading is done", () => {
finishLoading!();
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
});
});
@@ -1,32 +1,132 @@
import React, { StatelessComponent } from "react";
import { graphql } from "react-relay";
import React from "react";
import { graphql, RelayPaginationProp } from "react-relay";
import { withFragmentContainer } from "talk-framework/lib/relay";
import { withPaginationContainer } from "talk-framework/lib/relay";
import { PropTypesOf } from "talk-framework/types";
import { StreamContainer_comments as Data } from "talk-stream/__generated__/StreamContainer_comments.graphql";
import { StreamContainer_asset as Data } from "talk-stream/__generated__/StreamContainer_asset.graphql";
import {
COMMENT_SORT,
StreamContainerPaginationQueryVariables,
} from "talk-stream/__generated__/StreamContainerPaginationQuery.graphql";
import Stream from "../components/Stream";
interface InnerProps {
comments: Data;
asset: Data;
relay: RelayPaginationProp;
}
const StreamContainer: StatelessComponent<InnerProps> = props => {
const comments = props.comments.edges.map(edge => edge.node);
return <Stream comments={comments} />;
};
export class StreamContainer extends React.Component<InnerProps> {
public state = {
disableLoadMore: false,
};
const enhanced = withFragmentContainer<{ comments: Data }>(
graphql`
fragment StreamContainer_comments on CommentsConnection {
edges {
node {
id
...CommentContainer
public render() {
const comments = this.props.asset.comments.edges.map(edge => edge.node);
return (
<Stream
assetID={this.props.asset.id}
isClosed={this.props.asset.isClosed}
comments={comments}
onLoadMore={this.loadMore}
hasMore={this.props.relay.hasMore()}
disableLoadMore={this.state.disableLoadMore}
/>
);
}
private loadMore = () => {
if (!this.props.relay.hasMore() || this.props.relay.isLoading()) {
return;
}
this.setState({ disableLoadMore: true });
this.props.relay.loadMore(
10, // Fetch the next 10 feed items
error => {
this.setState({ disableLoadMore: false });
if (error) {
// tslint:disable-next-line:no-console
console.error(error);
}
}
}
`
);
};
}
// TODO: (cvle) This should be autogenerated.
interface FragmentVariables {
count: number;
cursor?: string;
orderBy: COMMENT_SORT;
}
const enhanced = withPaginationContainer<
{ asset: Data },
InnerProps,
FragmentVariables,
StreamContainerPaginationQueryVariables
>(
{
asset: graphql`
fragment StreamContainer_asset on Asset
@argumentDefinitions(
count: { type: "Int!", defaultValue: 5 }
cursor: { type: "Cursor" }
orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_DESC }
) {
id
isClosed
comments(first: $count, after: $cursor, orderBy: $orderBy)
@connection(key: "Stream_comments") {
edges {
node {
id
...CommentContainer
...ReplyListContainer_comment
}
}
}
}
`,
},
{
direction: "forward",
getConnectionFromProps(props) {
return props.asset && props.asset.comments;
},
// This is also the default implementation of `getFragmentVariables` if it isn't provided.
getFragmentVariables(prevVars, totalCount) {
return {
...prevVars,
count: totalCount,
};
},
getVariables(props, { count, cursor }, fragmentVariables) {
return {
count,
cursor,
orderBy: fragmentVariables.orderBy,
// assetID isn't specified as an @argument for the fragment, but it should be a
// variable available for the fragment under the query root.
assetID: props.asset.id,
};
},
query: graphql`
# Pagination query to be fetched upon calling 'loadMore'.
# Notice that we re-use our fragment, and the shape of this query matches our fragment spec.
query StreamContainerPaginationQuery(
$count: Int!
$cursor: Cursor
$orderBy: COMMENT_SORT!
$assetID: ID!
) {
asset(id: $assetID) {
...StreamContainer_asset
@arguments(count: $count, cursor: $cursor, orderBy: $orderBy)
}
}
`,
}
)(StreamContainer);
export type StreamContainerProps = PropTypesOf<typeof enhanced>;
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<App
asset={Object {}}
/>
`;
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders username and body 1`] = `
<Comment
author={
Object {
"username": "Marvin",
}
}
body="Woof"
createdAt="1995-12-17T03:24:00.000Z"
/>
`;
@@ -0,0 +1,78 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<ReplyList
commentID="comment-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableShowAll={false}
onShowAll={[Function]}
/>
`;
exports[`renders correctly when replies are null 1`] = `""`;
exports[`when has more replies renders hasMore 1`] = `
<ReplyList
commentID="comment-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableShowAll={false}
hasMore={true}
onShowAll={[Function]}
/>
`;
exports[`when has more replies when showing all disables show all button 1`] = `
<ReplyList
commentID="comment-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableShowAll={true}
hasMore={true}
onShowAll={[Function]}
/>
`;
exports[`when has more replies when showing all enable show all button after loading is done 1`] = `
<ReplyList
commentID="comment-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableShowAll={false}
hasMore={true}
onShowAll={[Function]}
/>
`;
@@ -0,0 +1,80 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Stream
assetID="asset-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
isClosed={false}
onLoadMore={[Function]}
/>
`;
exports[`when has more comments renders hasMore 1`] = `
<Stream
assetID="asset-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
hasMore={true}
isClosed={false}
onLoadMore={[Function]}
/>
`;
exports[`when has more comments when loading more disables load more button 1`] = `
<Stream
assetID="asset-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={true}
hasMore={true}
isClosed={false}
onLoadMore={[Function]}
/>
`;
exports[`when has more comments when loading more enable load more button after loading is done 1`] = `
<Stream
assetID="asset-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
hasMore={true}
isClosed={false}
onLoadMore={[Function]}
/>
`;
@@ -0,0 +1,31 @@
import { shallow } from "enzyme";
import React from "react";
import { render } from "./AppQuery";
it("renders app", () => {
const data = {
props: {} as any,
error: null,
};
const wrapper = shallow(React.createElement(() => render(data)));
expect(wrapper).toMatchSnapshot();
});
it("renders loading", () => {
const data = {
props: null,
error: null,
};
const wrapper = shallow(React.createElement(() => render(data)));
expect(wrapper).toMatchSnapshot();
});
it("renders error", () => {
const data = {
props: null,
error: new Error("error"),
};
const wrapper = shallow(React.createElement(() => render(data)));
expect(wrapper).toMatchSnapshot();
});
+5 -9
View File
@@ -1,10 +1,10 @@
import * as React from "react";
import { StatelessComponent } from "react";
import { ReadyState } from "react-relay";
import {
graphql,
QueryRenderer,
ReadyState,
withLocalStateContainer,
} from "talk-framework/lib/relay";
import {
@@ -15,7 +15,7 @@ import { AppQueryLocal as Local } from "talk-stream/__generated__/AppQueryLocal.
import AppContainer from "../containers/AppContainer";
const render = ({ error, props }: ReadyState<AppQueryResponse>) => {
export const render = ({ error, props }: ReadyState<AppQueryResponse>) => {
if (error) {
return <div>{error.message}</div>;
}
@@ -33,16 +33,12 @@ const AppQuery: StatelessComponent<InnerProps> = props => {
return (
<QueryRenderer<AppQueryVariables, AppQueryResponse>
query={graphql`
query AppQuery($showAssetList: Boolean!, $assetID: ID!) {
...AppContainer
@arguments(showAssetList: $showAssetList, assetID: $assetID)
query AppQuery($assetID: ID!) {
...AppContainer @arguments(assetID: $assetID)
}
`}
variables={{
// We cast `null` to any due to restrictions of the current graphql syntax.
assetID: props.local.assetID || (null as any),
// TODO: This is set to false, as server does not support querying assets yet.
showAssetList: !props.local.assetID && false,
assetID: props.local.assetID,
}}
render={render}
/>
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders app 1`] = `
<Relay(AppContainer)
data={Object {}}
/>
`;
exports[`renders error 1`] = `
<div>
error
</div>
`;
exports[`renders loading 1`] = `
<div>
Loading
</div>
`;
@@ -0,0 +1,256 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loads more comments 1`] = `
<div
className="Flex-root App-root Flex-justifyCenter"
>
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div>
<textarea
className="PostCommentForm-textarea"
name="body"
onChange={[Function]}
value=""
/>
</div>
<div
className="PostCommentForm-postButtonContainer"
>
<button
className="BaseButton-root Button-root Button-primary"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
>
Post
</button>
</div>
</form>
<div
aria-live="polite"
className="Flex-root Flex-itemGutter Flex-directionColumn"
id="talk-comments-stream-log"
role="log"
>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
</div>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Lukas
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:20:00.000Z"
title="2018-07-06T18:20:00.000Z"
>
2018-07-06T18:20:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
</div>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Isabelle
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:14:00.000Z"
title="2018-07-06T18:14:00.000Z"
>
2018-07-06T18:14:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Hey!
</p>
</div>
</div>
</div>
</div>
</div>
`;
exports[`renders comment stream 1`] = `
<div
className="Flex-root App-root Flex-justifyCenter"
>
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div>
<textarea
className="PostCommentForm-textarea"
name="body"
onChange={[Function]}
value=""
/>
</div>
<div
className="PostCommentForm-postButtonContainer"
>
<button
className="BaseButton-root Button-root Button-primary"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
>
Post
</button>
</div>
</form>
<div
aria-live="polite"
className="Flex-root Flex-itemGutter Flex-directionColumn"
id="talk-comments-stream-log"
role="log"
>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
</div>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Lukas
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:20:00.000Z"
title="2018-07-06T18:20:00.000Z"
>
2018-07-06T18:20:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
</div>
<button
aria-controls="talk-comments-stream-log"
className="BaseButton-root Button-root Button-invert Button-fullWidth Button-secondary"
disabled={false}
id="talk-comments-stream-loadMore"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
>
Load More
</button>
</div>
</div>
</div>
`;
@@ -0,0 +1,168 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders comment stream 1`] = `
<div
className="Flex-root App-root Flex-justifyCenter"
>
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div>
<textarea
className="PostCommentForm-textarea"
name="body"
onChange={[Function]}
value=""
/>
</div>
<div
className="PostCommentForm-postButtonContainer"
>
<button
className="BaseButton-root Button-root Button-primary"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
>
Post
</button>
</div>
</form>
<div
aria-live="polite"
className="Flex-root Flex-itemGutter Flex-directionColumn"
id="talk-comments-stream-log"
role="log"
>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
</div>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
I like yoghurt
</p>
</div>
<div
className="Indent-root Indent-level0"
>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
id="talk-comments-replyList-log--comment-with-replies"
role="log"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Lukas
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:20:00.000Z"
title="2018-07-06T18:20:00.000Z"
>
2018-07-06T18:20:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,108 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders comment stream 1`] = `
<div
className="Flex-root App-root Flex-justifyCenter"
>
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div>
<textarea
className="PostCommentForm-textarea"
name="body"
onChange={[Function]}
value=""
/>
</div>
<div
className="PostCommentForm-postButtonContainer"
>
<button
className="BaseButton-root Button-root Button-primary"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
>
Post
</button>
</div>
</form>
<div
aria-live="polite"
className="Flex-root Flex-itemGutter Flex-directionColumn"
id="talk-comments-stream-log"
role="log"
>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
</div>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Lukas
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:20:00.000Z"
title="2018-07-06T18:20:00.000Z"
>
2018-07-06T18:20:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,264 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders comment stream 1`] = `
<div
className="Flex-root App-root Flex-justifyCenter"
>
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div>
<textarea
className="PostCommentForm-textarea"
name="body"
onChange={[Function]}
value=""
/>
</div>
<div
className="PostCommentForm-postButtonContainer"
>
<button
className="BaseButton-root Button-root Button-primary"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
>
Post
</button>
</div>
</form>
<div
aria-live="polite"
className="Flex-root Flex-itemGutter Flex-directionColumn"
id="talk-comments-stream-log"
role="log"
>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
<div
className="Indent-root Indent-level0"
>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
id="talk-comments-replyList-log--comment-0"
role="log"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Lukas
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:20:00.000Z"
title="2018-07-06T18:20:00.000Z"
>
2018-07-06T18:20:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
<button
aria-controls="talk-comments-replyList-log--comment-0"
className="BaseButton-root Button-root Button-invert Button-fullWidth Button-secondary"
disabled={false}
id="talk-comments-replyList-showAll--comment-0"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
>
Show All Replies
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`show all replies 1`] = `
<div
className="Flex-root App-root Flex-justifyCenter"
>
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div>
<textarea
className="PostCommentForm-textarea"
name="body"
onChange={[Function]}
value=""
/>
</div>
<div
className="PostCommentForm-postButtonContainer"
>
<button
className="BaseButton-root Button-root Button-primary"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
>
Post
</button>
</div>
</form>
<div
aria-live="polite"
className="Flex-root Flex-itemGutter Flex-directionColumn"
id="talk-comments-stream-log"
role="log"
>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
<div
className="Indent-root Indent-level0"
>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
id="talk-comments-replyList-log--comment-0"
role="log"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Lukas
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:20:00.000Z"
title="2018-07-06T18:20:00.000Z"
>
2018-07-06T18:20:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Isabelle
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:14:00.000Z"
title="2018-07-06T18:14:00.000Z"
>
2018-07-06T18:14:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Hey!
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,49 @@
import { IResolvers } from "graphql-tools";
import { createFetch } from "relay-local-schema";
import {
commitLocalUpdate,
Environment,
Network,
RecordProxy,
RecordSource,
Store,
} from "relay-runtime";
import { loadSchema } from "talk-common/graphql";
import {
createAndRetain,
LOCAL_ID,
LOCAL_TYPE,
wrapFetchWithLogger,
} from "talk-framework/lib/relay";
export interface CreateEnvironmentParams {
/** graphql resolvers */
resolvers: IResolvers<any, any>;
/** Allows to set initial state for Local state */
initLocalState?: (local: RecordProxy) => void;
/** If enabled, graphql responses will be logged to the console */
logNetwork?: boolean;
}
/**
* create Relay environment for integration tests.
*/
export default function createEnvironment(params: CreateEnvironmentParams) {
const schema = loadSchema("tenant", params.resolvers);
const environment = new Environment({
network: Network.create(
wrapFetchWithLogger(createFetch({ schema }), params.logNetwork)
),
store: new Store(new RecordSource()),
});
if (params.initLocalState) {
commitLocalUpdate(environment, s => {
const root = s.getRoot();
const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE);
root.setLinkedRecord(localRecord, "local");
params.initLocalState!(localRecord);
});
}
return environment;
}
+81
View File
@@ -0,0 +1,81 @@
export const users = [
{
id: "user-0",
username: "Markus",
},
{
id: "user-1",
username: "Lukas",
},
{
id: "user-2",
username: "Isabelle",
},
];
export const comments = [
{
id: "comment-0",
author: users[0],
body: "Joining Too",
createdAt: "2018-07-06T18:24:00.000Z",
},
{
id: "comment-1",
author: users[1],
body: "What's up?",
createdAt: "2018-07-06T18:20:00.000Z",
},
{
id: "comment-2",
author: users[2],
body: "Hey!",
createdAt: "2018-07-06T18:14:00.000Z",
},
];
export const assets = [
{
id: "asset-1",
isClosed: false,
comments: {
edges: [
{ node: comments[0], cursor: comments[0].createdAt },
{ node: comments[1], cursor: comments[1].createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
},
];
export const commentWithReplies = {
id: "comment-with-replies",
author: users[0],
body: "I like yoghurt",
createdAt: "2018-07-06T18:24:00.000Z",
replies: {
edges: [
{ node: comments[0], cursor: comments[0].createdAt },
{ node: comments[1], cursor: comments[1].createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
};
export const assetWithReplies = {
id: "asset-with-replies",
isClosed: false,
comments: {
edges: [
{ node: comments[0], cursor: comments[0].createdAt },
{ node: commentWithReplies, cursor: commentWithReplies.createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
};
@@ -0,0 +1,98 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import { RecordProxy } from "relay-runtime";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import AppQuery from "talk-stream/queries/AppQuery";
import createEnvironment from "./createEnvironment";
import { assets, comments } from "./fixtures";
const connectionStub = sinon.stub().throws();
connectionStub.withArgs({ first: 5, orderBy: "CREATED_AT_DESC" }).returns({
edges: [
{
node: comments[0],
cursor: comments[0].createdAt,
},
{
node: comments[1],
cursor: comments[1].createdAt,
},
],
pageInfo: {
endCursor: comments[1].createdAt,
hasNextPage: true,
},
});
connectionStub
.withArgs({
first: 10,
orderBy: "CREATED_AT_DESC",
after: comments[1].createdAt,
})
.returns({
edges: [
{
node: comments[2],
cursor: comments[2].createdAt,
},
],
pageInfo: {
endCursor: comments[2].createdAt,
hasNextPage: false,
},
});
const assetStub = {
...assets[0],
comments: connectionStub,
};
const resolvers = {
Query: {
asset: sinon
.stub()
.throws()
.withArgs(undefined, { id: assetStub.id })
.returns(assetStub),
},
};
const environment = createEnvironment({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: (localRecord: RecordProxy) => {
localRecord.setValue(assetStub.id, "assetID");
},
});
const context: TalkContext = {
relayEnvironment: environment,
localeMessages: [],
};
const testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>
<AppQuery />
</TalkContextProvider>
);
it("renders comment stream", async () => {
// Wait for loading.
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("loads more comments", async () => {
testRenderer.root
.findByProps({ id: "talk-comments-stream-loadMore" })
.props.onClick();
// Wait for loading.
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,47 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import { RecordProxy } from "relay-runtime";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import AppQuery from "talk-stream/queries/AppQuery";
import createEnvironment from "./createEnvironment";
import { assetWithReplies } from "./fixtures";
const resolvers = {
Query: {
asset: sinon
.stub()
.throws()
.withArgs(undefined, { id: assetWithReplies.id })
.returns(assetWithReplies),
},
};
const environment = createEnvironment({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: (localRecord: RecordProxy) => {
localRecord.setValue(assetWithReplies.id, "assetID");
},
});
const context: TalkContext = {
relayEnvironment: environment,
localeMessages: [],
};
const testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>
<AppQuery />
</TalkContextProvider>
);
it("renders comment stream", async () => {
// Wait for loading.
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,47 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import { RecordProxy } from "relay-runtime";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import AppQuery from "talk-stream/queries/AppQuery";
import createEnvironment from "./createEnvironment";
import { assets } from "./fixtures";
const resolvers = {
Query: {
asset: sinon
.stub()
.throws()
.withArgs(undefined, { id: assets[0].id })
.returns(assets[0]),
},
};
const environment = createEnvironment({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: (localRecord: RecordProxy) => {
localRecord.setValue(assets[0].id, "assetID");
},
});
const context: TalkContext = {
relayEnvironment: environment,
localeMessages: [],
};
const testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>
<AppQuery />
</TalkContextProvider>
);
it("renders comment stream", async () => {
// Wait for loading.
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,114 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import { RecordProxy } from "relay-runtime";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import AppQuery from "talk-stream/queries/AppQuery";
import createEnvironment from "./createEnvironment";
import { assets, comments } from "./fixtures";
const connectionStub = sinon.stub().throws();
connectionStub.withArgs({ first: 5, orderBy: "CREATED_AT_ASC" }).returns({
edges: [
{
node: comments[1],
cursor: comments[1].createdAt,
},
],
pageInfo: {
endCursor: comments[1].createdAt,
hasNextPage: true,
},
});
connectionStub
.withArgs({
first: sinon.match(n => n > 10000),
orderBy: "CREATED_AT_ASC",
after: comments[1].createdAt,
})
.returns({
edges: [
{
node: comments[2],
cursor: comments[2].createdAt,
},
],
pageInfo: {
endCursor: comments[2].createdAt,
hasNextPage: false,
},
});
const commentStub = {
...comments[0],
replies: connectionStub,
};
const assetStub = {
...assets[0],
comments: {
pageInfo: {
hasNextPage: false,
},
edges: [
{
node: commentStub,
cursor: commentStub.createdAt,
},
],
},
};
const resolvers = {
Query: {
comment: sinon
.stub()
.throws()
.withArgs(undefined, { id: commentStub.id })
.returns(commentStub),
asset: sinon
.stub()
.throws()
.withArgs(undefined, { id: assetStub.id })
.returns(assetStub),
},
};
const environment = createEnvironment({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: (localRecord: RecordProxy) => {
localRecord.setValue(assetStub.id, "assetID");
},
});
const context: TalkContext = {
relayEnvironment: environment,
localeMessages: [],
};
const testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>
<AppQuery />
</TalkContextProvider>
);
it("renders comment stream", async () => {
// Wait for loading.
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("show all replies", async () => {
testRenderer.root
.findByProps({ id: `talk-comments-replyList-showAll--${comments[0].id}` })
.props.onClick();
// Wait for loading.
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
+5
View File
@@ -0,0 +1,5 @@
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
// React 16 Enzyme adapter
Enzyme.configure({ adapter: new Adapter() });
+27
View File
@@ -0,0 +1,27 @@
import { JSDOM } from "jsdom";
declare var global: any;
const jsdom = new JSDOM("<!doctype html><html><body></body></html>");
const { window } = jsdom;
function copyProps(src: any, target: any) {
const props = Object.getOwnPropertyNames(src)
.filter(prop => typeof target[prop] === "undefined")
.reduce(
(result, prop) => ({
...result,
[prop]: Object.getOwnPropertyDescriptor(src, prop),
}),
{}
);
Object.defineProperties(target, props);
}
global.window = window;
global.document = (window as any).document;
global.navigator = {
userAgent: "node.js",
};
copyProps(window, global);
+24
View File
@@ -0,0 +1,24 @@
import "./enzyme";
import "./jsdom";
// TODO: Remove when fixed.
// Mock React.createContext because of https://github.com/airbnb/enzyme/issues/1509.
function mockReact() {
const originalReact = require.requireActual("react");
return {
...originalReact,
createContext: jest.fn(defaultValue => {
let value = defaultValue;
const Provider = (props: any) => {
value = props.value;
return props.children;
};
const Consumer = (props: any) => props.children(value);
return {
Provider,
Consumer,
};
}),
};
}
jest.mock("react", () => mockReact());
+9 -33
View File
@@ -5,40 +5,16 @@
"module": "esnext",
"jsx": "preserve",
"allowJs": false,
"lib": [
"dom",
"es7",
"scripthost",
"es2015",
"esnext.asynciterable"
],
"lib": ["dom", "es7", "scripthost", "es2015", "esnext.asynciterable"],
"baseUrl": "./",
"paths": {
"talk-admin/*": [
"./admin/*"
],
"talk-stream/*": [
"./stream/*"
],
"talk-framework/*": [
"./framework/*"
],
"talk-ui/*": [
"./ui/*"
],
"talk-common/*": [
"../common/*"
],
"talk-locales/*": [
"../../locales/*"
]
"talk-admin/*": ["./admin/*"],
"talk-stream/*": ["./stream/*"],
"talk-framework/*": ["./framework/*"],
"talk-ui/*": ["./ui/*"],
"talk-common/*": ["../common/*"]
}
},
"include": [
"./**/*",
"../../types/**/*.d.ts"
],
"exclude": [
"node_modules"
]
}
"include": ["./**/*", "../../types/**/*.d.ts"],
"exclude": ["node_modules"]
}
+6 -13
View File
@@ -1,20 +1,13 @@
{
"extends": [
"../../../tslint.json",
"tslint-react"
],
"extends": ["../../../tslint.json", "tslint-react"],
"rules": {
"jsx-curly-spacing": false,
"jsx-no-multiline-js": false,
"jsx-boolean-value": [
true,
"never"
]
"jsx-boolean-value": [true, "never"]
},
"jsRules": {
"jsx-curly-spacing": false,
"jsx-no-multiline-js": false,
"jsx-boolean-value": [
true,
"never"
]
"jsx-boolean-value": [true, "never"]
}
}
}
@@ -0,0 +1,2 @@
.root {
}
@@ -0,0 +1,62 @@
import React, { CSSProperties } from "react";
import { Manager, Popper, Reference, RefHandler } from "react-popper";
interface RenderProps {
ref: RefHandler;
style?: CSSProperties;
}
interface InnerProps {
body: React.ReactElement<any> | null;
children: (props: RenderProps) => React.ReactElement<any>;
className?: string;
placement?:
| "auto-start"
| "auto"
| "auto-end"
| "top-start"
| "top"
| "top-end"
| "right-start"
| "right"
| "right-end"
| "bottom-end"
| "bottom"
| "bottom-start"
| "left-end"
| "left"
| "left-start";
}
interface Props {
ref: any;
style: CSSProperties;
}
class Attachment extends React.Component<InnerProps> {
public render() {
const { children, body, placement = "top" } = this.props;
return (
<Manager>
<Reference>{(props: Props) => children({ ref: props.ref })}</Reference>
<Popper
placement={placement}
modifiers={{ preventOverflow: { enabled: false } }}
eventsEnabled
positionFixed={false}
>
{(props: Props) =>
body
? React.cloneElement(body, {
innerRef: props.ref,
style: props.style,
})
: null
}
</Popper>
</Manager>
);
}
}
export default Attachment;
@@ -0,0 +1,2 @@
export * from "./Attachment";
export { default } from "./Attachment";
@@ -19,6 +19,8 @@ interface InnerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** This is passed by the `withKeyboardFocus` HOC */
keyboardFocus: boolean;
innerRef?: React.RefObject<HTMLDivElement> | any;
}
/**
@@ -31,6 +33,7 @@ const BaseButton: StatelessComponent<InnerProps> = ({
classes,
keyboardFocus,
type: typeProp,
innerRef,
...rest
}) => {
let Element = "button";
@@ -53,7 +56,7 @@ const BaseButton: StatelessComponent<InnerProps> = ({
[classes.keyboardFocus]: keyboardFocus,
});
return <Element {...rest} className={rootClassName} />;
return <Element {...rest} className={rootClassName} ref={innerRef} />;
};
const enhanced = withStyles(styles)(withKeyboardFocus(BaseButton));
+66 -45
View File
@@ -1,78 +1,99 @@
.root {
composes: button from "talk-ui/shared/typography.css";
padding: 5px 20px;
border-radius: $round-corners;
background-color: transparent;
/* TODO: hover styles for the default button */
&:enabled,
&:disabled {
padding: 5px 20px;
border-radius: var(--round-corners);
background-color: transparent;
/* TODO: hover styles for the default button */
}
}
.fullWidth {
.root:disabled {
opacity: 0.4;
cursor: default;
}
.fullWidth:enabled,
.fullWidth:disabled {
display: block;
width: 100%;
box-sizing: border-box;
}
.primary {
background-color: $palette-primary-main;
.primary:enabled,
.primary:disabled {
background-color: var(--palette-primary-main);
color: #fff;
}
.primary:enabled {
&:hover {
background-color: $palette-primary-light;
background-color: var(--palette-primary-light);
}
&:active {
background-color: $palette-primary-lighter;
}
&.invert {
background-color: transparent;
border: 1px solid $palette-primary-main;
color: $palette-primary-main;
&:hover {
border-color: $palette-primary-light;
color: $palette-primary-light;
}
&:active {
border-color: $palette-primary-lighter;
color: $palette-primary-lighter;
}
background-color: var(--palette-primary-lighter);
}
}
.secondary {
background-color: $palette-secondary-main;
color: #fff;
.primary:enabled.invert,
.primary:disabled.invert {
background-color: transparent;
border: 1px solid var(--palette-primary-main);
color: var(--palette-primary-main);
}
.primary:enabled.invert {
&:hover {
background-color: $palette-secondary-light;
border-color: var(--palette-primary-light);
color: var(--palette-primary-light);
}
&:active {
background-color: $palette-secondary-lighter;
border-color: var(--palette-primary-lighter);
color: var(--palette-primary-lighter);
}
}
.secondary:enabled,
.secondary:disabled {
background-color: var(--palette-secondary-main);
color: #fff;
}
.secondary:enabled {
&:hover {
background-color: var(--palette-secondary-light);
}
&:active {
background-color: var(--palette-secondary-lighter);
}
}
.secondary:enabled.invert,
.secondary:disabled.invert {
background-color: transparent;
border: 1px solid var(--palette-secondary-main);
color: var(--palette-secondary-main);
}
.secondary:enabled.invert {
&:hover {
border-color: var(--palette-secondary-light);
color: var(--palette-secondary-light);
}
&.invert {
background-color: transparent;
border: 1px solid $palette-secondary-main;
color: $palette-secondary-main;
&:hover {
border-color: $palette-secondary-light;
color: $palette-secondary-light;
}
&:active {
border-color: $palette-secondary-lighter;
color: $palette-secondary-lighter;
}
&:active {
border-color: var(--palette-secondary-lighter);
color: var(--palette-secondary-lighter);
}
}
/**
* This seems to be the best way to target modern touch device browsers.
*/
@media (-moz-touch-enabled: 1), (pointer:coarse) {
@media (-moz-touch-enabled: 1), (pointer: coarse) {
/* TODO: Remove hover styles */
}
@@ -10,11 +10,18 @@ import Button from './Button'
## Basic usage
<Playground>
<Button style={{marginRight: "10px"}}>Push Me</Button>
<Button style={{marginRight: "10px"}} anchor>I'm an Anchor Tag</Button>
<Button style={{marginRight: "10px"}} primary>Primary</Button>
<Button style={{marginRight: "10px"}} secondary>Secondary</Button>
<Button style={{marginTop: "10px"}} primary fullWidth>Full Width</Button>
<Button style={{marginTop: "10px"}} primary invert fullWidth>Full Width Invert</Button>
<Button style={{margin: "0 10px 10px 0"}}>Push Me</Button>
<Button style={{margin: "0 10px 10px 0"}} disabled>Push Me</Button>
<Button style={{margin: "0 10px 10px 0"}} anchor>I'm an Anchor Tag</Button>
<Button style={{margin: "0 10px 10px 0"}} primary>Primary</Button>
<Button style={{margin: "0 10px 10px 0"}} primary disabled>Primary</Button>
<Button style={{margin: "0 10px 10px 0"}} secondary>Secondary</Button>
<Button style={{margin: "0 10px 10px 0"}} secondary disabled>Secondary</Button>
<Button style={{margin: "0 10px 10px 0"}} primary invert>Primary</Button>
<Button style={{margin: "0 10px 10px 0"}} primary invert disabled>Primary</Button>
<Button style={{margin: "0 10px 10px 0"}} secondary invert>Secondary</Button>
<Button style={{margin: "0 10px 10px 0"}} secondary invert disabled>Secondary</Button>
<Button style={{marginBottom: "10px"}} primary fullWidth>Full Width</Button>
<Button style={{marginBottom: "10px"}} primary invert fullWidth>Full Width Invert</Button>
</Playground>
@@ -28,6 +28,8 @@ interface InnerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** If set renders a button with secondary colors */
secondary?: boolean;
innerRef?: React.RefObject<HTMLDivElement> | any;
}
class Button extends React.Component<InnerProps> {
@@ -39,6 +41,7 @@ class Button extends React.Component<InnerProps> {
invert,
primary,
secondary,
innerRef,
...rest
} = this.props;
@@ -53,6 +56,7 @@ class Button extends React.Component<InnerProps> {
<BaseButton
className={rootClassName}
classes={pick(classes, "keyboardFocus")}
innerRef={innerRef}
{...rest}
/>
);
@@ -1,6 +0,0 @@
.root {
display: flex;
align-items: center;
flex-direction: column;
}
@@ -1,30 +0,0 @@
import cn from "classnames";
import * as React from "react";
import { ReactNode, StatelessComponent } from "react";
import { withStyles } from "talk-ui/hocs";
import { PropTypesOf } from "talk-ui/types";
import * as styles from "./Center.css";
interface InnerProps {
/**
* This prop can be used to add custom classnames.
* It is handled by the `withStyles `HOC.
*/
classes: Partial<typeof styles>;
className?: string;
children: ReactNode;
}
const Center: StatelessComponent<InnerProps> = props => {
return (
<div className={cn(props.className, props.classes.root)}>
{props.children}
</div>
);
};
const enhanced = withStyles(styles)(Center);
export type CenterProps = PropTypesOf<typeof enhanced>;
export default enhanced;
@@ -1,2 +0,0 @@
export * from "./Center";
export { default } from "./Center";
@@ -0,0 +1,99 @@
.root {
display: flex;
}
.halfItemGutter {
& > * {
margin: 0 calc(0.5 * var(--spacing-unit)) 0 0;
}
&.directionRowReverse {
& > * {
margin: 0 0 0 calc(0.5 * var(--spacing-unit));
}
}
&.directionColumn {
& > * {
margin: 0 0 calc(0.5 * var(--spacing-unit)) 0;
}
}
&.directionColumnReverese {
& > * {
margin: calc(0.5 * var(--spacing-unit)) 0 0 0;
}
}
& > *:last-child {
margin: 0;
}
}
.itemGutter {
& > * {
margin: 0 var(--spacing-unit) 0 0;
}
&.directionRowReverse {
& > * {
margin: 0 0 0 var(--spacing-unit);
}
}
&.directionColumn {
& > * {
margin: 0 0 var(--spacing-unit) 0;
}
}
&.directionColumnReverese {
& > * {
margin: var(--spacing-unit) 0 0 0;
}
}
& > *:last-child {
margin: 0;
}
}
.justifyFlexStart {
justify-content: flex-start;
}
.justifyFlexEnd {
justify-content: flex-end;
}
.justifyCenter {
justify-content: center;
}
.justifySpaceBetween {
justify-content: space-between;
}
.justifySpaceAround {
justify-content: space-around;
}
.justifySpaceEvenly {
justify-content: space-evenly;
}
.alignFlexStart {
align-items: flex-start;
}
.alignFlexEnd {
align-items: flex-end;
}
.alignCenter {
align-items: center;
}
.alignBaseline {
align-items: baseline;
}
.alignStretch {
align-items: stretch;
}
.directionRow {
flex-direction: row;
}
.directionColumn {
flex-direction: column;
}
.directionRowReverse {
flex-direction: row-reverse;
}
.directionColumnReverse {
flex-direction: column-reverse;
}
@@ -0,0 +1,16 @@
---
name: Flex
menu: UI Kit
---
import { Playground, PropsTable } from 'docz'
import Flex from './Flex'
# Flex
`Flex` is a wrapper around `flexbox`.
## Basic usage
<Playground>
<Flex justifyContent="center">I'm centered</Flex>
</Playground>
@@ -0,0 +1,20 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-ui/types";
import Flex from "./Flex";
it("renders correctly", () => {
const props: PropTypesOf<typeof Flex> = {
justifyContent: "center",
alignItems: "center",
direction: "row",
};
const wrapper = shallow(
<Flex {...props}>
<div>Hello World</div>
</Flex>
);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,57 @@
import cn from "classnames";
import React from "react";
import { StatelessComponent } from "react";
import { pascalCase } from "talk-common/utils";
import * as styles from "./Flex.css";
interface InnerProps {
id?: string;
role?: string;
justifyContent?:
| "flex-start"
| "flex-end"
| "center"
| "space-around"
| "space-between"
| "space-evenly";
alignItems?: "flex-start" | "flex-end" | "center" | "baseline" | "stretch";
direction?: "row" | "column" | "row-reverse" | "column-reverse";
itemGutter?: boolean | "half";
className?: string;
}
const Flex: StatelessComponent<InnerProps> = props => {
const {
className,
justifyContent,
alignItems,
direction,
itemGutter,
...rest
} = props;
const classObject: Record<string, boolean> = {
[styles.itemGutter]: itemGutter === true,
[styles.halfItemGutter]: itemGutter === "half",
};
if (justifyContent) {
classObject[(styles as any)[`justify${pascalCase(justifyContent)}`]] = true;
}
if (alignItems) {
classObject[(styles as any)[`align${pascalCase(alignItems)}`]] = true;
}
if (direction) {
classObject[(styles as any)[`direction${pascalCase(direction)}`]] = true;
}
const classNames: string = cn(styles.root, className, classObject);
return <div className={classNames} {...rest} />;
};
export default Flex;
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="Flex-root Flex-justifyCenter Flex-alignCenter Flex-directionRow"
>
<div>
Hello World
</div>
</div>
`;

Some files were not shown because too many files have changed in this diff Show More