mirror of
https://github.com/wassname/talk.git
synced 2026-09-11 12:51:33 +08:00
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:
@@ -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>
|
||||
);
|
||||
|
||||
+23
@@ -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;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,5 +1,4 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
extends: "../.babelrc.js",
|
||||
plugins: [
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import Enzyme from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
|
||||
// React 16 Enzyme adapter
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
@@ -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);
|
||||
@@ -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());
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user