Merge branch 'next' into timestamp

This commit is contained in:
Chi Vinh Le committed 2018-07-12 16:29:23 -03:00
commit 6467348296
112 files changed
+4411 -990

No files matched your search

+10 -5
View File
@@ -1,3 +1,4 @@
const path = require("path");
const paths = require("./paths");
module.exports = {
@@ -5,11 +6,8 @@ module.exports = {
roots: ["<rootDir>/src", "<rootDir>/scripts"],
collectCoverageFrom: ["src/**/*.{js,jsx,mjs,ts,tsx}"],
coveragePathIgnorePatterns: ["/node_modules/"],
setupFiles: ["<rootDir>/config/polyfills.js"],
testMatch: [
"**/__tests__/**/*.{js,jsx,mjs,ts,tsx}",
"**/*.spec.{js,jsx,mjs,ts,tsx}",
],
setupFiles: ["<rootDir>/config/polyfills.js", "<rootDir>/test/setupjest.ts"],
testMatch: ["**/*.spec.{js,jsx,mjs,ts,tsx}"],
testEnvironment: "node",
testURL: "http://localhost",
transform: {
@@ -40,4 +38,11 @@ module.exports = {
"ts",
"tsx",
],
snapshotSerializers: ["enzyme-to-json/serializer"],
globals: {
"ts-jest": {
useBabelrc: true,
tsConfigFile: path.resolve(__dirname, "tsconfig.jest.json"),
},
},
};
+13 -2
View File
@@ -4,7 +4,7 @@
// Copyright https://github.com/Connormiha/jest-css-modules-transform/graphs/contributors
// This oututs `module.exports = cssObject` instead of `module.exports = { default: cssObject }`;
const { sep: pathSep, resolve } = require("path");
const { sep: pathSep, resolve, basename, extname } = require("path");
const postcss = require("postcss");
const postcssNested = postcss([require("postcss-nested")]);
@@ -88,7 +88,18 @@ module.exports = {
process(src, path, config) {
const filename = path.slice(path.lastIndexOf(pathSep) + 1);
const textCSS = postcssNested.process(src).css;
const exprt = JSON.stringify(getCSSSelectors(textCSS, path));
const selectors = getCSSSelectors(textCSS, path);
const component = basename(path, extname(path));
// Prefix class names with component name.
Object.keys(selectors).forEach(k => {
selectors[k] = selectors[k]
.split(/\s+/)
.map(v => `${component}-${v}`)
.join(" ");
});
const exprt = JSON.stringify(selectors);
return `module.exports = ${exprt}`;
},
};
+1
View File
@@ -53,6 +53,7 @@ module.exports = {
appTsconfig: resolveApp("src/core/client/tsconfig.json"),
appLocales: resolveApp("src/locales"),
appThemeVariables: resolveApp("src/core/client/ui/theme/variables.ts"),
appThemeVariablesCSS: resolveApp("src/core/client/ui/theme/variables.css"),
testsSetup: resolveApp("src/setupTests.js"),
appNodeModules: resolveApp("node_modules"),
+55 -9
View File
@@ -1,27 +1,73 @@
const precss = require("precss");
const autoprefixer = require("autoprefixer");
const fontMagician = require("postcss-font-magician");
// Allow importing typescript files.
require("ts-node/register");
const kebabCase = require("lodash/kebabCase");
const mapKeys = require("lodash/mapKeys");
const mapValues = require("lodash/mapValues");
const pickBy = require("lodash/pickBy");
const flat = require("flat");
const flexbugsFixes = require("postcss-flexbugs-fixes");
const paths = require("./paths");
const autoprefixer = require("autoprefixer");
const postcssFontMagician = require("postcss-font-magician");
const postcssFlexbugsFixes = require("postcss-flexbugs-fixes");
const postcssVariables = require("postcss-css-variables");
const postcssPresetEnv = require("postcss-preset-env");
const postcssNested = require("postcss-nested");
const postcssImport = require("postcss-import");
const postcssPrependImports = require("postcss-prepend-imports");
const postcssAdvancedVariables = require("postcss-advanced-variables");
delete require.cache[paths.appThemeVariables];
const variables = require(paths.appThemeVariables);
const variables = require(paths.appThemeVariables).default;
const flatKebabVariables = mapKeys(
flat(variables, { delimiter: "-" }),
mapValues(flat(variables, { delimiter: "-" }), v => v.toString()),
(_, k) => kebabCase(k)
);
// These are the default css standard variables.
const cssVariables = pickBy(
flatKebabVariables,
(v, k) => !k.startsWith("breakpoints-")
);
// These are sass style variables used in media queries.
const mediaQueryVariables = mapValues(
pickBy(flatKebabVariables, (v, k) => k.startsWith("breakpoints-")),
// Add unit to breakpoints.
// Add 1 to support mobile first approach where we start
// with the smallest screen and gradually add styling for the
// next bigger screen. This is realized using `min-width` without
// ever using `max-width`.
v => `${Number.parseInt(v) + 1}px`
);
module.exports = {
// Necessary for external CSS imports to work
// https://github.com/facebookincubator/create-react-app/issues/2677
ident: "postcss",
plugins: [
precss({ variables: flatKebabVariables }),
fontMagician(),
flexbugsFixes,
// This allows us to define dynamic css variables.
postcssPrependImports({
path: "",
files: [paths.appThemeVariablesCSS],
}),
// Needed by above plugin.
postcssImport(),
// Support nesting.
postcssNested(),
// Sass style variables to be used in media queries.
postcssAdvancedVariables({ variables: mediaQueryVariables }),
// CSS standard variables for everything else.
postcssVariables({
variables: cssVariables,
}),
// Provides a modern CSS environment.
postcssPresetEnv(),
// Does all the font handling logic.
postcssFontMagician(),
// Fix known flexbox bugs.
postcssFlexbugsFixes,
// Vendor prefixing.
autoprefixer({
browsers: [
">1%",
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "es2015",
"module": "es2015"
}
}
+7 -2
View File
@@ -13,9 +13,14 @@ const config: Config = {
"core/client/stream/**/*.ts",
"core/client/stream/**/*.tsx",
"core/client/stream/**/*.graphql",
"core/client/server/**/*.graphql",
"core/server/**/*.graphql",
],
ignore: [
"core/**/*.d.ts",
"core/**/*.graphql.ts",
"**/test/**/*",
"core/**/*.spec.*",
],
ignore: ["core/**/*.d.ts", "core/**/*.graphql.ts"],
executor: new CommandExecutor("npm run compile:relay-stream", {
runOnInit: true,
}),
+5
View File
@@ -224,6 +224,11 @@ module.exports = {
jsx: "preserve",
noEmit: false,
},
// Overwrites the behavior of `include` and `exclude` to only
// include files that are actually being imported and which
// are necessary to compile the bundle.
onlyCompileBundledFiles: true,
},
},
],
+1090 -570
View File
File diff suppressed because it is too large. Load diff
+21 -2
View File
@@ -52,6 +52,7 @@
"devDependencies": {
"@babel/core": "7.0.0-beta.49",
"@babel/plugin-syntax-dynamic-import": "7.0.0-beta.49",
"@babel/plugin-transform-modules-commonjs": "^7.0.0-beta.52",
"@babel/polyfill": "7.0.0-beta.49",
"@babel/preset-env": "7.0.0-beta.49",
"@babel/preset-react": "7.0.0-beta.49",
@@ -62,11 +63,14 @@
"@types/convict": "^4.2.0",
"@types/cross-spawn": "^6.0.0",
"@types/dotenv": "^4.0.3",
"@types/enzyme": "^3.1.11",
"@types/enzyme-adapter-react-16": "^1.0.2",
"@types/express": "^4.16.0",
"@types/graphql": "^0.13.1",
"@types/ioredis": "^3.2.8",
"@types/jest": "^23.1.1",
"@types/joi": "^13.0.8",
"@types/jsdom": "^11.0.6",
"@types/lodash": "^4.14.109",
"@types/luxon": "^0.5.3",
"@types/mongodb": "^3.0.19",
@@ -75,12 +79,15 @@
"@types/query-string": "^6.1.0",
"@types/react-dom": "^16.0.6",
"@types/react-relay": "github:coralproject/patched#types/react-relay",
"@types/react-responsive": "^3.0.1",
"@types/react-test-renderer": "^16.0.1",
"@types/recompose": "^0.26.1",
"@types/relay-runtime": "github:coralproject/patched#types/relay-runtime",
"@types/sinon": "^5.0.1",
"@types/uuid": "^3.4.3",
"@types/ws": "^5.1.2",
"autoprefixer": "^8.6.0",
"babel-core": "^7.0.0-bridge.0",
"babel-loader": "^8.0.0-beta",
"babel-plugin-relay": "github:coralproject/patched#babel-plugin-relay",
"babel-preset-react-optimize": "^1.0.1",
@@ -94,6 +101,9 @@
"css-loader": "^0.28.11",
"docz": "^0.2.6",
"docz-theme-default": "^0.2.10",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"enzyme-to-json": "^3.3.4",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"final-form": "^4.8.1",
"flat": "^4.0.0",
@@ -104,12 +114,18 @@
"graphql-playground-middleware-express": "^1.7.0",
"html-webpack-plugin": "^3.2.0",
"jest": "^23.3.0",
"jsdom": "^11.11.0",
"loader-utils": "^1.1.0",
"npm-run-all": "^4.1.3",
"postcss-advanced-variables": "^2.3.3",
"postcss-css-variables": "^0.9.0",
"postcss-flexbugs-fixes": "^3.3.1",
"postcss-font-magician": "^2.2.1",
"postcss-import": "^11.1.0",
"postcss-loader": "^2.1.5",
"precss": "^3.1.2",
"postcss-nested": "^3.0.0",
"postcss-prepend-imports": "^1.0.1",
"postcss-preset-env": "^5.2.1",
"prettier": "^1.13.4",
"query-string": "^6.1.0",
"raw-loader": "^0.5.1",
@@ -118,14 +134,17 @@
"react-dom": "^16.4.0",
"react-final-form": "^3.6.0",
"react-relay": "github:coralproject/patched#react-relay",
"react-responsive": "^4.1.0",
"react-test-renderer": "^16.4.1",
"recompose": "^0.27.1",
"relay-compiler": "github:coralproject/patched#relay-compiler",
"relay-compiler-language-typescript": "github:coralproject/patched#relay-compiler-language-typescript",
"relay-local-schema": "^0.7.0",
"relay-runtime": "github:coralproject/patched#relay-runtime",
"relay-test-utils": "github:coralproject/patched#relay-test-utils",
"sinon": "^6.1.2",
"style-loader": "^0.21.0",
"ts-jest": "^22.4.6",
"ts-jest": "^23.0.0",
"ts-loader": "^4.3.1",
"ts-node": "^6.1.0",
"tsconfig-paths": "^3.4.0",
+3
View File
@@ -8,5 +8,8 @@ module.exports = {
production: {
plugins: [],
},
test: {
plugins: ["@babel/transform-modules-commonjs"],
},
},
};
@@ -1,5 +1,9 @@
import * as React from "react";
import { hoistStatics, InferableComponentEnhancer } from "recompose";
import {
hoistStatics,
InferableComponentEnhancer,
wrapDisplayName,
} from "recompose";
import { TalkContext, TalkContextConsumer } from "./TalkContext";
@@ -12,11 +16,17 @@ function withContext<T>(
propsCallback: (context: TalkContext) => T
): InferableComponentEnhancer<T> {
return hoistStatics<T>(
<U extends T>(WrappedComponent: React.ComponentType<U>) => (props: any) => (
<TalkContextConsumer>
{context => <WrappedComponent {...props} {...propsCallback(context)} />}
</TalkContextConsumer>
)
<U extends T>(WrappedComponent: React.ComponentType<U>) => {
const Component: React.StatelessComponent<any> = props => (
<TalkContextConsumer>
{context => (
<WrappedComponent {...props} {...propsCallback(context)} />
)}
</TalkContextConsumer>
);
Component.displayName = wrapDisplayName(WrappedComponent, "withContext");
return Component;
}
);
}
+1 -1
View File
@@ -13,7 +13,7 @@ export const VALIDATION_REQUIRED = () => (
);
export const VALIDATION_TOO_SHORT = () => (
<Localized id="framework-validation-too-short">
<Localized id="framework-validation-tooShort">
<span>This field is too short.</span>
</Localized>
);
@@ -1,24 +1,18 @@
import React, { Component } from "react";
import { QueryRenderer } from "react-relay";
import { CacheConfig, GraphQLTaggedNode, RerunParam } from "relay-runtime";
import {
QueryRenderer,
QueryRendererProps as QueryRendererPropsOrig,
} from "react-relay";
import { Omit } from "talk-framework/types";
import { TalkContextConsumer } from "../bootstrap/TalkContext";
// Taken from relay types and added Generic support for Variables and Response
export interface QueryRendererProps<V, R> {
cacheConfig?: CacheConfig;
query?: GraphQLTaggedNode | null;
render(readyState: ReadyState<R>): React.ReactElement<any> | undefined | null;
variables: V;
rerunParamExperimental?: RerunParam;
}
// Taken from relay types and added Generic support for Variables and Response
export interface ReadyState<R> {
error: Error | undefined | null;
props: R | undefined | null;
retry?(): void;
}
// Omit environment as we are passing this from the context.
export type QueryRendererProps<V, R> = Omit<
QueryRendererPropsOrig<V, R>,
"environment"
>;
/**
* TalkQueryRenderer is a wrappper around Relay's `QueryRenderer`.
@@ -1,5 +1,10 @@
import * as React from "react";
import { compose, hoistStatics, InferableComponentEnhancer } from "recompose";
import {
compose,
hoistStatics,
InferableComponentEnhancer,
wrapDisplayName,
} from "recompose";
import { Environment } from "relay-runtime";
import { withContext } from "../bootstrap";
@@ -19,6 +24,11 @@ function createMutationContainer<T extends string, I, R>(
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
hoistStatics((WrappedComponent: React.ComponentType<any>) => {
class CreateMutationContainer extends React.Component<any> {
public static displayName = wrapDisplayName(
WrappedComponent,
"createMutationContainer"
);
private commit = (input: I) => {
return commit(this.props.relayEnvironment, input);
};
@@ -7,6 +7,7 @@ export { default as QueryRenderer } from "./QueryRenderer";
export * from "./QueryRenderer";
export { default as createMutationContainer } from "./createMutationContainer";
export { default as createAndRetain } from "./createAndRetain";
export { default as wrapFetchWithLogger } from "./wrapFetchWithLogger";
export {
commitMutationPromise,
commitMutationPromiseNormalized,
@@ -6,7 +6,7 @@ import { InferableComponentEnhancerWithProps } from "recompose";
* from Relay.
*/
export default <T>(
fragmentSpec: GraphQLTaggedNode
fragmentSpec: { [P in keyof T]: GraphQLTaggedNode }
): InferableComponentEnhancerWithProps<T, { [P in keyof T]: any }> => (
component: React.ComponentType<any>
) => createFragmentContainer(component, fragmentSpec) as any;
@@ -1,6 +1,11 @@
import * as React from "react";
import { compose, hoistStatics, InferableComponentEnhancer } from "recompose";
import { CSelector, CSnapshot, Environment } from "relay-runtime";
import {
CSelector,
CSnapshot,
Environment,
GraphQLTaggedNode,
} from "relay-runtime";
import { withContext } from "../bootstrap";
@@ -25,7 +30,7 @@ export const LOCAL_ID = "client:root.local";
* must have the `LOCAL_ID`.
*/
function withLocalStateContainer<T>(
fragmentSpec: any
fragmentSpec: GraphQLTaggedNode
): InferableComponentEnhancer<{ local: T }> {
return compose(
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
@@ -33,7 +38,7 @@ function withLocalStateContainer<T>(
class LocalStateContainer extends React.Component<Props, any> {
constructor(props: Props) {
super(props);
const fragment = fragmentSpec.data().default;
const fragment = (fragmentSpec as any).data().default;
if (fragment.kind !== "Fragment") {
throw new Error("Expected fragment");
}
@@ -10,9 +10,13 @@ import { InferableComponentEnhancerWithProps } from "recompose";
* withPaginationContainer is a curried version of `createPaginationContainers`
* from Relay.
*/
export default <T, InnerProps>(
fragmentSpec: GraphQLTaggedNode,
connectionConfig: ConnectionConfig<InnerProps>
export default <T, InnerProps, FragmentVariables, QueryVariables>(
fragmentSpec: { [P in keyof T]: GraphQLTaggedNode },
connectionConfig: ConnectionConfig<
InnerProps,
FragmentVariables,
QueryVariables
>
): InferableComponentEnhancerWithProps<
T & { relay: RelayPaginationProp },
{ [P in keyof T]: any }
@@ -10,7 +10,7 @@ import { InferableComponentEnhancerWithProps } from "recompose";
* from Relay.
*/
export default <T>(
fragmentSpec: GraphQLTaggedNode,
fragmentSpec: { [P in keyof T]: GraphQLTaggedNode },
refetchQuery: GraphQLTaggedNode
): InferableComponentEnhancerWithProps<
T & { relay: RelayRefetchProp },
@@ -0,0 +1,25 @@
import { FetchFunction } from "relay-runtime";
/**
* Decorates the fetch function with error logging.
* Intended for testing purposes.
*/
export default function wrapFetchWithLogger(
fetch: FetchFunction,
logResult?: boolean
): FetchFunction {
return async (...args: any[]) => {
try {
const result = await (fetch as any)(...args);
if (logResult) {
// tslint:disable-next-line:no-console
console.log(JSON.stringify(result));
}
return result;
} catch (err) {
// tslint:disable-next-line:no-console
console.error(err);
throw err;
}
};
}
-1
View File
@@ -1,5 +1,4 @@
const path = require("path");
module.exports = {
extends: "../.babelrc.js",
plugins: [
+5 -18
View File
@@ -1,33 +1,20 @@
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";
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">
<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 +1,10 @@
.root {
width: 400px;
}
.gutterBottom {
margin-bottom: calc(2px * $spacing-unit);
margin-bottom: calc(1px * var(--spacing-unit));
}
.author {
font-weight: $font-weight-medium;
.topBar {
margin-bottom: calc(0.5px * var(--spacing-unit));
}
@@ -1,5 +1,6 @@
import { shallow } from "enzyme";
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import Comment from "./Comment";
it("renders username and body", () => {
@@ -10,9 +11,8 @@ it("renders username and body", () => {
body: "Woof",
createdAt: new Date("December 17, 1995 03:24:00").toISOString(),
};
const renderer = createRenderer();
renderer.render(<Comment {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
const wrapper = shallow(<Comment {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders with gutterBottom", () => {
@@ -24,7 +24,6 @@ it("renders with gutterBottom", () => {
createdAt: new Date("December 17, 1995 03:24:00").toISOString(),
gutterBottom: true,
};
const renderer = createRenderer();
renderer.render(<Comment {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
const wrapper = shallow(<Comment {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -5,6 +5,7 @@ import { StatelessComponent } from "react";
import { Timestamp, Typography } from "talk-ui/components";
import * as styles from "./Comment.css";
import Username from "./Username";
export interface CommentProps {
className?: string;
@@ -21,11 +22,11 @@ const Comment: StatelessComponent<CommentProps> = props => {
[styles.gutterBottom]: props.gutterBottom,
});
return (
<div className={rootClassName}>
<Typography className={styles.author} gutterBottom>
{props.author && props.author.username}
</Typography>
<Timestamp date={props.createdAt} />
<div className={rootClassName} role="article">
<div className={styles.topBar}>
{props.author && <Username>{props.author.username}</Username>}
<Timestamp date={props.createdAt} />
</div>
<Typography>{props.body}</Typography>
</div>
);
@@ -0,0 +1,8 @@
.root {
border-left: 3px solid;
padding-left: calc(1px * var(--spacing-unit));
}
.level0 {
border-color: var(--palette-secondary-darkest);
}
@@ -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;
@@ -3,10 +3,12 @@
display: block;
height: 100px;
width: 400px;
margin-bottom: calc(2px * $spacing-unit);
width: 100%;
box-sizing: border-box;
margin-bottom: calc(1px * 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,40 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import sinon, { SinonSpy } from "sinon";
import ReplyList, { ReplyListProps } from "./ReplyList";
it("renders correctly", () => {
const props: ReplyListProps = {
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: ReplyListProps = {
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,45 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { Button } 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>
<div id={`talk-comments-replyList-log--${props.commentID}`} role="log">
{props.comments.map(comment => (
<CommentContainer key={comment.id} data={comment} gutterBottom />
))}
{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>
)}
</div>
</Indent>
);
};
export default ReplyList;
@@ -0,0 +1,4 @@
.root {
width: 100%;
max-width: 400px;
}
@@ -0,0 +1,45 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import sinon from "sinon";
import Stream, { StreamProps } from "./Stream";
it("renders correctly", () => {
const props: StreamProps = {
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 = {
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.calledOnce).toBe(true);
});
const wrapperDisabledButton = shallow(<Stream {...props} disableLoadMore />);
it("disables load more button", () => {
expect(wrapperDisabledButton).toMatchSnapshot();
});
});
+38 -4
View File
@@ -1,18 +1,52 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { Button } 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} />
<div id="talk-comments-stream-log" role="log" aria-live="polite">
{props.comments.map(comment => (
<div key={comment.id}>
<CommentContainer data={comment} gutterBottom />
<ReplyListContainer comment={comment} />
</div>
))}
{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>
)}
</div>
</div>
);
};
@@ -0,0 +1,3 @@
.root {
composes: heading4 from "talk-ui/shared/typography.css";
}
@@ -0,0 +1,12 @@
import { shallow } from "enzyme";
import React from "react";
import Username from "./Username";
it("renders correctly", () => {
const props = {
children: "Marvin",
};
const wrapper = shallow(<Username {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,14 @@
import React from "react";
import { StatelessComponent } from "react";
import * as styles from "./Username.css";
export interface CommentProps {
children: string;
}
const Username: StatelessComponent<CommentProps> = props => {
return <span className={styles.root}>{props.children}</span>;
};
export default Username;
@@ -2,17 +2,19 @@
exports[`renders username and body 1`] = `
<div
className="root"
className="Comment-root"
role="article"
>
<withPropsOnChange(Typography)
className="author"
gutterBottom={true}
<div
className="Comment-topBar"
>
Marvin
</withPropsOnChange(Typography)>
<withPropsOnChange(Timestamp)
date="1995-12-17T06:24:00.000Z"
/>
<Username>
Marvin
</Username>
<withPropsOnChange(Timestamp)
date="1995-12-17T06:24:00.000Z"
/>
</div>
<withPropsOnChange(Typography)>
Woof
</withPropsOnChange(Typography)>
@@ -21,17 +23,19 @@ exports[`renders username and body 1`] = `
exports[`renders with gutterBottom 1`] = `
<div
className="root gutterBottom"
className="Comment-root Comment-gutterBottom"
role="article"
>
<withPropsOnChange(Typography)
className="author"
gutterBottom={true}
<div
className="Comment-topBar"
>
Marvin
</withPropsOnChange(Typography)>
<withPropsOnChange(Timestamp)
date="1995-12-17T06:24:00.000Z"
/>
<Username>
Marvin
</Username>
<withPropsOnChange(Timestamp)
date="1995-12-17T06:24:00.000Z"
/>
</div>
<withPropsOnChange(Typography)>
Woof
</withPropsOnChange(Typography)>
@@ -0,0 +1,72 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Indent>
<div
id="talk-comments-replyList-log--comment-id"
role="log"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
gutterBottom={true}
key="comment-1"
/>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
gutterBottom={true}
key="comment-2"
/>
</div>
</Indent>
`;
exports[`when there is more renders a load more button 1`] = `
<Indent>
<div
id="talk-comments-replyList-log--comment-id"
role="log"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
gutterBottom={true}
key="comment-1"
/>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
gutterBottom={true}
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>
</div>
</Indent>
`;
@@ -0,0 +1,202 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="Stream-root"
>
<Logo
gutterBottom={true}
/>
<withContext(createMutationContainer(PostCommentFormContainer))
assetID="asset-id"
/>
<div
aria-live="polite"
id="talk-comments-stream-log"
role="log"
>
<div
key="comment-1"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
gutterBottom={true}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-1",
}
}
/>
</div>
<div
key="comment-2"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
gutterBottom={true}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-2",
}
}
/>
</div>
</div>
</div>
`;
exports[`when there is more disables load more button 1`] = `
<div
className="Stream-root"
>
<Logo
gutterBottom={true}
/>
<withContext(createMutationContainer(PostCommentFormContainer))
assetID="asset-id"
/>
<div
aria-live="polite"
id="talk-comments-stream-log"
role="log"
>
<div
key="comment-1"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
gutterBottom={true}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-1",
}
}
/>
</div>
<div
key="comment-2"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
gutterBottom={true}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-2",
}
}
/>
</div>
<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>
</div>
</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"
/>
<div
aria-live="polite"
id="talk-comments-stream-log"
role="log"
>
<div
key="comment-1"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
gutterBottom={true}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-1",
}
}
/>
</div>
<div
key="comment-2"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
gutterBottom={true}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-2",
}
}
/>
</div>
<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>
</div>
</div>
`;
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<span
className="Username-root"
>
Marvin
</span>
`;
@@ -15,26 +15,16 @@ 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,18 @@
import { shallow } from "enzyme";
import React from "react";
import { CommentContainer } from "./CommentContainer";
it("renders username and body", () => {
const props = {
data: {
author: {
username: "Marvin",
},
body: "Woof",
},
};
const wrapper = shallow(<CommentContainer {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -9,22 +9,29 @@ import Comment, { CommentProps } from "../components/Comment";
type InnerProps = { data: Data } & Omit<CommentProps, keyof Data>;
const CommentContainer: StatelessComponent<InnerProps> = props => {
// tslint:disable-next-line:no-unused-expression
graphql`
fragment CommentContainer_comment on Comment {
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
}
createdAt
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: any = {
comment: {
id: "comment-id",
replies: {
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
},
},
relay: {
hasMore: noop,
isLoading: noop,
},
};
const wrapper = shallow(<ReplyListContainer {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders correctly when replies are null", () => {
const props: any = {
comment: {
id: "comment-id",
replies: null,
},
relay: {
hasMore: noop,
isLoading: noop,
},
};
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,132 @@
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) {
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,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders username and body 1`] = `
<Comment
author={
Object {
"username": "Marvin",
}
}
body="Woof"
/>
`;
@@ -0,0 +1,78 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<ReplyList
commentID="comment-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableShowAll={false}
onShowAll={[Function]}
/>
`;
exports[`renders correctly when replies are null 1`] = `""`;
exports[`when has more replies renders hasMore 1`] = `
<ReplyList
commentID="comment-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableShowAll={false}
hasMore={true}
onShowAll={[Function]}
/>
`;
exports[`when has more replies when showing all disables show all button 1`] = `
<ReplyList
commentID="comment-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableShowAll={true}
hasMore={true}
onShowAll={[Function]}
/>
`;
exports[`when has more replies when showing all enable show all button after loading is done 1`] = `
<ReplyList
commentID="comment-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableShowAll={false}
hasMore={true}
onShowAll={[Function]}
/>
`;
@@ -0,0 +1,80 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Stream
assetID="asset-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
isClosed={false}
onLoadMore={[Function]}
/>
`;
exports[`when has more comments renders hasMore 1`] = `
<Stream
assetID="asset-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
hasMore={true}
isClosed={false}
onLoadMore={[Function]}
/>
`;
exports[`when has more comments when loading more disables load more button 1`] = `
<Stream
assetID="asset-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={true}
hasMore={true}
isClosed={false}
onLoadMore={[Function]}
/>
`;
exports[`when has more comments when loading more enable load more button after loading is done 1`] = `
<Stream
assetID="asset-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
hasMore={true}
isClosed={false}
onLoadMore={[Function]}
/>
`;
@@ -0,0 +1,31 @@
import { shallow } from "enzyme";
import React from "react";
import { render } from "./AppQuery";
it("renders app", () => {
const data = {
props: {} as any,
error: null,
};
const wrapper = shallow(React.createElement(() => render(data)));
expect(wrapper).toMatchSnapshot();
});
it("renders loading", () => {
const data = {
props: null,
error: null,
};
const wrapper = shallow(React.createElement(() => render(data)));
expect(wrapper).toMatchSnapshot();
});
it("renders error", () => {
const data = {
props: null,
error: new Error("error"),
};
const wrapper = shallow(React.createElement(() => render(data)));
expect(wrapper).toMatchSnapshot();
});
+5 -9
View File
@@ -1,10 +1,10 @@
import * as React from "react";
import { StatelessComponent } from "react";
import { ReadyState } from "react-relay";
import {
graphql,
QueryRenderer,
ReadyState,
withLocalStateContainer,
} from "talk-framework/lib/relay";
import {
@@ -15,7 +15,7 @@ import { AppQueryLocal as Local } from "talk-stream/__generated__/AppQueryLocal.
import AppContainer from "../containers/AppContainer";
const render = ({ error, props }: ReadyState<AppQueryResponse>) => {
export const render = ({ error, props }: ReadyState<AppQueryResponse>) => {
if (error) {
return <div>{error.message}</div>;
}
@@ -33,16 +33,12 @@ const AppQuery: StatelessComponent<InnerProps> = props => {
return (
<QueryRenderer<AppQueryVariables, AppQueryResponse>
query={graphql`
query AppQuery($showAssetList: Boolean!, $assetID: ID!) {
...AppContainer
@arguments(showAssetList: $showAssetList, assetID: $assetID)
query AppQuery($assetID: ID!) {
...AppContainer @arguments(assetID: $assetID)
}
`}
variables={{
// We cast `null` to any due to restrictions of the current graphql syntax.
assetID: props.local.assetID || (null as any),
// TODO: This is set to false, as server does not support querying assets yet.
showAssetList: !props.local.assetID && false,
assetID: props.local.assetID,
}}
render={render}
/>
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders app 1`] = `
<Relay(AppContainer)
data={Object {}}
/>
`;
exports[`renders error 1`] = `
<div>
error
</div>
`;
exports[`renders loading 1`] = `
<div>
Loading
</div>
`;
@@ -0,0 +1,249 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loads more comments 1`] = `
<div
className="Flex-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"
id="talk-comments-stream-log"
role="log"
>
<div>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Markus
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:24:00.000Z"
title="2018-07-06T18:24:00"
>
2018-07-06T21:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
</div>
<div>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Lukas
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:20:00.000Z"
title="2018-07-06T18:20:00"
>
2018-07-06T21:20:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
</div>
<div>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Isabelle
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:14:00.000Z"
title="2018-07-06T18:14:00"
>
2018-07-06T21: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 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"
id="talk-comments-stream-log"
role="log"
>
<div>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Markus
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:24:00.000Z"
title="2018-07-06T18:24:00"
>
2018-07-06T21:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
</div>
<div>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Lukas
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:20:00.000Z"
title="2018-07-06T18:20:00"
>
2018-07-06T21: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,166 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders comment stream 1`] = `
<div
className="Flex-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"
id="talk-comments-stream-log"
role="log"
>
<div>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Markus
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:24:00.000Z"
title="2018-07-06T18:24:00"
>
2018-07-06T21:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
</div>
<div>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Markus
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:24:00.000Z"
title="2018-07-06T18:24:00"
>
2018-07-06T21:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
I like yoghurt
</p>
</div>
<div
className="Indent-root Indent-level0"
>
<div
id="talk-comments-replyList-log--comment-with-replies"
role="log"
>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Markus
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:24:00.000Z"
title="2018-07-06T18:24:00"
>
2018-07-06T21:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Lukas
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:20:00.000Z"
title="2018-07-06T18:20:00"
>
2018-07-06T21: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,105 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders comment stream 1`] = `
<div
className="Flex-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"
id="talk-comments-stream-log"
role="log"
>
<div>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Markus
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:24:00.000Z"
title="2018-07-06T18:24:00"
>
2018-07-06T21:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
</div>
<div>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Lukas
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:20:00.000Z"
title="2018-07-06T18:20:00"
>
2018-07-06T21:20:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,261 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders comment stream 1`] = `
<div
className="Flex-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"
id="talk-comments-stream-log"
role="log"
>
<div>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Markus
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:24:00.000Z"
title="2018-07-06T18:24:00"
>
2018-07-06T21:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
<div
className="Indent-root Indent-level0"
>
<div
id="talk-comments-replyList-log--comment-0"
role="log"
>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Lukas
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:20:00.000Z"
title="2018-07-06T18:20:00"
>
2018-07-06T21: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 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"
id="talk-comments-stream-log"
role="log"
>
<div>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Markus
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:24:00.000Z"
title="2018-07-06T18:24:00"
>
2018-07-06T21:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
<div
className="Indent-root Indent-level0"
>
<div
id="talk-comments-replyList-log--comment-0"
role="log"
>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Lukas
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:20:00.000Z"
title="2018-07-06T18:20:00"
>
2018-07-06T21:20:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
<div
className="Comment-root Comment-gutterBottom"
role="article"
>
<div
className="Comment-topBar"
>
<span
className="Username-root"
>
Isabelle
</span>
<time
className="Timestamp-root"
dateTime="2018-07-06T21:14:00.000Z"
title="2018-07-06T18:14:00"
>
2018-07-06T21:14:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Hey!
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,49 @@
import { IResolvers } from "graphql-tools";
import { createFetch } from "relay-local-schema";
import {
commitLocalUpdate,
Environment,
Network,
RecordProxy,
RecordSource,
Store,
} from "relay-runtime";
import { loadSchema } from "talk-common/graphql";
import {
createAndRetain,
LOCAL_ID,
LOCAL_TYPE,
wrapFetchWithLogger,
} from "talk-framework/lib/relay";
export interface CreateEnvironmentParams {
/** graphql resolvers */
resolvers: IResolvers<any, any>;
/** Allows to set initial state for Local state */
initLocalState?: (local: RecordProxy) => void;
/** If enabled, graphql responses will be logged to the console */
logNetwork?: boolean;
}
/**
* create Relay environment for integration tests.
*/
export default function createEnvironment(params: CreateEnvironmentParams) {
const schema = loadSchema("tenant", params.resolvers);
const environment = new Environment({
network: Network.create(
wrapFetchWithLogger(createFetch({ schema }), params.logNetwork)
),
store: new Store(new RecordSource()),
});
if (params.initLocalState) {
commitLocalUpdate(environment, s => {
const root = s.getRoot();
const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE);
root.setLinkedRecord(localRecord, "local");
params.initLocalState!(localRecord);
});
}
return environment;
}
+81
View File
@@ -0,0 +1,81 @@
export const users = [
{
id: "user-0",
username: "Markus",
},
{
id: "user-1",
username: "Lukas",
},
{
id: "user-2",
username: "Isabelle",
},
];
export const comments = [
{
id: "comment-0",
author: users[0],
body: "Joining Too",
createdAt: "2018-07-06T18:24:00",
},
{
id: "comment-1",
author: users[1],
body: "What's up?",
createdAt: "2018-07-06T18:20:00",
},
{
id: "comment-2",
author: users[2],
body: "Hey!",
createdAt: "2018-07-06T18:14:00",
},
];
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",
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();
});
+1 -2
View File
@@ -12,8 +12,7 @@
"talk-stream/*": ["./stream/*"],
"talk-framework/*": ["./framework/*"],
"talk-ui/*": ["./ui/*"],
"talk-common/*": ["../common/*"],
"talk-locales/*": ["../../locales/*"]
"talk-common/*": ["../common/*"]
}
},
"include": ["./**/*", "../../types/**/*.d.ts"],
+66 -45
View File
@@ -1,78 +1,99 @@
.root {
composes: button from "talk-ui/shared/typography.css";
padding: 5px 20px;
border-radius: $round-corners;
background-color: transparent;
/* TODO: hover styles for the default button */
&:enabled,
&:disabled {
padding: 5px 20px;
border-radius: var(--round-corners);
background-color: transparent;
/* TODO: hover styles for the default button */
}
}
.fullWidth {
.root:disabled {
opacity: 0.4;
cursor: default;
}
.fullWidth:enabled,
.fullWidth:disabled {
display: block;
width: 100%;
box-sizing: border-box;
}
.primary {
background-color: $palette-primary-main;
.primary:enabled,
.primary:disabled {
background-color: var(--palette-primary-main);
color: #fff;
}
.primary:enabled {
&:hover {
background-color: $palette-primary-light;
background-color: var(--palette-primary-light);
}
&:active {
background-color: $palette-primary-lighter;
}
&.invert {
background-color: transparent;
border: 1px solid $palette-primary-main;
color: $palette-primary-main;
&:hover {
border-color: $palette-primary-light;
color: $palette-primary-light;
}
&:active {
border-color: $palette-primary-lighter;
color: $palette-primary-lighter;
}
background-color: var(--palette-primary-lighter);
}
}
.secondary {
background-color: $palette-secondary-main;
color: #fff;
.primary:enabled.invert,
.primary:disabled.invert {
background-color: transparent;
border: 1px solid var(--palette-primary-main);
color: var(--palette-primary-main);
}
.primary:enabled.invert {
&:hover {
background-color: $palette-secondary-light;
border-color: var(--palette-primary-light);
color: var(--palette-primary-light);
}
&:active {
background-color: $palette-secondary-lighter;
border-color: var(--palette-primary-lighter);
color: var(--palette-primary-lighter);
}
}
.secondary:enabled,
.secondary:disabled {
background-color: var(--palette-secondary-main);
color: #fff;
}
.secondary:enabled {
&:hover {
background-color: var(--palette-secondary-light);
}
&:active {
background-color: var(--palette-secondary-lighter);
}
}
.secondary:enabled.invert,
.secondary:disabled.invert {
background-color: transparent;
border: 1px solid var(--palette-secondary-main);
color: var(--palette-secondary-main);
}
.secondary:enabled.invert {
&:hover {
border-color: var(--palette-secondary-light);
color: var(--palette-secondary-light);
}
&.invert {
background-color: transparent;
border: 1px solid $palette-secondary-main;
color: $palette-secondary-main;
&:hover {
border-color: $palette-secondary-light;
color: $palette-secondary-light;
}
&:active {
border-color: $palette-secondary-lighter;
color: $palette-secondary-lighter;
}
&:active {
border-color: var(--palette-secondary-lighter);
color: var(--palette-secondary-lighter);
}
}
/**
* This seems to be the best way to target modern touch device browsers.
*/
@media (-moz-touch-enabled: 1), (pointer:coarse) {
@media (-moz-touch-enabled: 1), (pointer: coarse) {
/* TODO: Remove hover styles */
}
@@ -10,11 +10,18 @@ import Button from './Button'
## Basic usage
<Playground>
<Button style={{marginRight: "10px"}}>Push Me</Button>
<Button style={{marginRight: "10px"}} anchor>I'm an Anchor Tag</Button>
<Button style={{marginRight: "10px"}} primary>Primary</Button>
<Button style={{marginRight: "10px"}} secondary>Secondary</Button>
<Button style={{marginTop: "10px"}} primary fullWidth>Full Width</Button>
<Button style={{marginTop: "10px"}} primary invert fullWidth>Full Width Invert</Button>
<Button style={{margin: "0 10px 10px 0"}}>Push Me</Button>
<Button style={{margin: "0 10px 10px 0"}} disabled>Push Me</Button>
<Button style={{margin: "0 10px 10px 0"}} anchor>I'm an Anchor Tag</Button>
<Button style={{margin: "0 10px 10px 0"}} primary>Primary</Button>
<Button style={{margin: "0 10px 10px 0"}} primary disabled>Primary</Button>
<Button style={{margin: "0 10px 10px 0"}} secondary>Secondary</Button>
<Button style={{margin: "0 10px 10px 0"}} secondary disabled>Secondary</Button>
<Button style={{margin: "0 10px 10px 0"}} primary invert>Primary</Button>
<Button style={{margin: "0 10px 10px 0"}} primary invert disabled>Primary</Button>
<Button style={{margin: "0 10px 10px 0"}} secondary invert>Secondary</Button>
<Button style={{margin: "0 10px 10px 0"}} secondary invert disabled>Secondary</Button>
<Button style={{marginBottom: "10px"}} primary fullWidth>Full Width</Button>
<Button style={{marginBottom: "10px"}} primary invert fullWidth>Full Width Invert</Button>
</Playground>
@@ -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,51 @@
.root {
display: flex;
}
.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,43 @@
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 {
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";
}
const Flex: StatelessComponent<InnerProps> = props => {
const { justifyContent, alignItems, direction, ...rest } = props;
const classObject: Record<string, boolean> = {};
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, 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>
`;
@@ -0,0 +1,2 @@
export * from "./Flex";
export { default } from "./Flex";
@@ -0,0 +1,40 @@
---
name: MatchMedia
menu: UI Kit
---
import { Playground, PropsTable } from 'docz'
import MatchMedia from './MatchMedia'
# MatchMedia
`MatchMedia` uses media queries to provide responsive rendering.
## Basic usage
<Playground>
<MatchMedia maxWidth="xs">
I'm a very small screen
</MatchMedia>
<MatchMedia minWidth="xs" maxWidth="sm">
I'm a small screen
</MatchMedia>
<MatchMedia minWidth="sm" maxWidth="md">
I'm a medium screen
</MatchMedia>
<MatchMedia minWidth="md" maxWidth="lg">
I'm a large screen
</MatchMedia>
<MatchMedia minWidth="lg" maxWidth="xl">
I'm a very large screen
</MatchMedia>
<MatchMedia minWidth="xl">
I'm a super large screen
</MatchMedia>
</Playground>
## Using render props
<Playground>
<MatchMedia minWidth="lg">
{matches => <span>{matches ? "I'm big" : "I'm small"}</span>}
</MatchMedia>
</Playground>
@@ -0,0 +1,27 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-ui/types";
import MatchMedia from "./MatchMedia";
it("renders correctly", () => {
const props: PropTypesOf<typeof MatchMedia> = {
minWidth: "xs",
maxWidth: "sm",
component: "div",
screen: true,
children: <div>Hello World</div>,
};
const wrapper = shallow(<MatchMedia {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("map new speech prop to older aural prop", () => {
const props: PropTypesOf<typeof MatchMedia> = {
speech: true,
children: <div>Hello World</div>,
};
const wrapper = shallow(<MatchMedia {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,37 @@
import React from "react";
import { ReactNode, StatelessComponent } from "react";
import Responsive from "react-responsive";
import theme from "../../theme/variables";
type Breakpoints = keyof typeof theme.breakpoints;
interface InnerProps {
minWidth?: Breakpoints;
maxWidth?: Breakpoints;
children: ReactNode | ((matches: boolean) => React.ReactNode);
className?: string;
component?:
| string
| React.SFC<any>
| React.ClassType<any, any, any>
| React.ComponentClass<any>;
all?: boolean;
print?: boolean;
screen?: boolean;
speech?: boolean;
}
const MatchMedia: StatelessComponent<InnerProps> = props => {
const { speech, minWidth, maxWidth, ...rest } = props;
const mapped = {
// TODO: Temporarily map newer speech to older aural type until
// react-responsive supports the speech prop.
aural: speech,
minWidth: minWidth ? theme.breakpoints[minWidth] + 1 : undefined,
maxWidth: maxWidth ? theme.breakpoints[maxWidth] : undefined,
};
return <Responsive {...rest} {...mapped} />;
};
export default MatchMedia;
@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`map new speech prop to older aural prop 1`] = `
<MediaQuery
aural={true}
values={Object {}}
>
<div>
Hello World
</div>
</MediaQuery>
`;
exports[`renders correctly 1`] = `
<MediaQuery
component="div"
maxWidth={640}
minWidth={321}
screen={true}
values={Object {}}
>
<div>
Hello World
</div>
</MediaQuery>
`;
@@ -0,0 +1,2 @@
export * from "./MatchMedia";
export { default } from "./MatchMedia";
@@ -2,7 +2,7 @@
exports[`uses default formatter 1`] = `
<time
className="root"
className="Timestamp-root"
dateTime="2108-12-17T06:24:00.000Z"
title="2108-12-17T06:24:00.000Z"
>
@@ -12,7 +12,7 @@ exports[`uses default formatter 1`] = `
exports[`uses formatter from context 1`] = `
<time
className="root"
className="Timestamp-root"
dateTime="2108-12-17T06:24:00.000Z"
title="2108-12-17T06:24:00.000Z"
>
@@ -22,10 +22,10 @@ exports[`uses formatter from context 1`] = `
exports[`uses formatter from props 1`] = `
<time
className="root"
className="Timestamp-root"
dateTime="2108-12-17T06:24:00.000Z"
title="2108-12-17T06:24:00.000Z"
>
My Props Formatter
My Context Formatter
</time>
`;
@@ -11,19 +11,19 @@
composes: heading2 from "talk-ui/shared/typography.css";
}
.heading3 {
.heading3 {
composes: heading3 from "talk-ui/shared/typography.css";
}
.heading4 {
.heading4 {
composes: heading4 from "talk-ui/shared/typography.css";
}
.subtitle1 {
.subtitle1 {
composes: subtitle1 from "talk-ui/shared/typography.css";
}
.subtitle2 {
.subtitle2 {
composes: subtitle2 from "talk-ui/shared/typography.css";
}
@@ -31,7 +31,7 @@
composes: body1 from "talk-ui/shared/typography.css";
}
.body2 {
.body2 {
composes: body2 from "talk-ui/shared/typography.css";
}
@@ -70,7 +70,7 @@
}
.paragraph {
margin-bottom: calc(2px * $spacing-unit);
margin-bottom: calc(1px * var(--spacing-unit));
}
.colorInherit {
@@ -78,17 +78,17 @@
}
.colorPrimary {
color: $palette-primary-main;
color: var(--palette-primary-main);
}
.colorSecondary {
color: $palette-secondary-main;
color: var(--palette-secondary-main);
}
.colorTextSecondary {
color: $palette-text-secondary;
color: var(--palette-text-secondary);
}
.colorError {
color: $palette-error-main;
color: var(--palette-error-main);
}
+2 -1
View File
@@ -1,6 +1,7 @@
export { default as BaseButton } from "./BaseButton";
export { default as Button } from "./Button";
export { default as Center } from "./Center";
export { default as Typography } from "./Typography";
export { default as Timestamp } from "./Timestamp";
export { default as UIContext } from "./UIContext";
export { default as Flex } from "./Flex";
export { default as MatchMedia } from "./MatchMedia";
+17 -17
View File
@@ -1,36 +1,36 @@
.heading1 {
font-size: calc(24rem / $rem-base);
font-weight: $font-weight-medium;
font-size: calc(24rem / var(--rem-base));
font-weight: var(--font-weight-medium);
font-family: "Manuale";
line-height: calc(32em / 24);
letter-spacing: calc(0.2em / 16);
color: $palette-text-primary;
color: var(--palette-text-primary);
}
.heading2 {
font-size: calc(20rem / $rem-base);
font-weight: $font-weight-medium;
font-size: calc(20rem / var(--rem-base));
font-weight: var(--font-weight-medium);
font-family: "Manuale";
line-height: calc(24em / 20);
color: $palette-text-primary;
color: var(--palette-text-primary);
}
.heading3 {
font-size: calc(18rem / $rem-base);
font-weight: $font-weight-medium;
font-size: calc(18rem / var(--rem-base));
font-weight: var(--font-weight-medium);
font-family: "Manuale";
line-height: calc(20em / 18);
letter-spacing: calc(0.2em / 16);
color: $palette-text-primary;
color: var(--palette-text-primary);
}
.heading4 {
font-size: calc(16rem / $rem-base);
font-weight: $font-weight-medium;
font-size: calc(16rem / var(--rem-base));
font-weight: var(--font-weight-medium);
font-family: "Manuale";
line-height: calc(18em / 16);
letter-spacing: calc(0.2em / 16);
color: $palette-text-primary;
color: var(--palette-text-primary);
}
.subtitle1 {
@@ -43,18 +43,18 @@
}
.body1 {
font-size: calc(16rem / $rem-base);
font-weight: $font-weight-regular;
font-size: calc(16rem / var(--rem-base));
font-weight: var(--font-weight-regular);
font-family: "Source Sans Pro";
line-height: calc(18em / 16);
letter-spacing: calc(0.2em / 16);
color: $palette-text-primary;
color: var(--palette-text-primary);
}
.button {
color: $palette-text-secondary;
color: var(--palette-text-secondary);
font-family: "Source Sans Pro";
font-weight: $font-weight-medium;
font-weight: var(--font-weight-medium);
font-size: 16px;
letter-spacing: calc(0.57em / 16);
}
+16
View File
@@ -0,0 +1,16 @@
/*
Variables defined in `variables.ts` are already available
flattened and in kebab case.
These are additionally variables we define.
*/
:root {
--spacing-unit: var(--spacing-unit-small);
}
@media (min-width: $breakpoints-xs) {
:root {
--spacing-unit: var(--spacing-unit-large);
}
}
+15 -6
View File
@@ -7,7 +7,7 @@ const variables = {
palette: {
/* Primary colors */
primary: {
darker: "#0D5B8F",
darkest: "#0D5B8F",
dark: "#2B7EB5",
main: "#3498DB",
light: "#67B2E4",
@@ -15,7 +15,7 @@ const variables = {
},
/* Secondary colors */
secondary: {
darker: "#404345",
darkest: "#404345",
dark: "#65696B",
main: "#787D80",
light: "#9A9DA0",
@@ -23,7 +23,7 @@ const variables = {
},
/* Success colors */
success: {
darker: "#03AB61",
darkest: "#03AB61",
dark: "#02BD6B",
main: "#00CD73",
light: "#40D996",
@@ -31,7 +31,7 @@ const variables = {
},
/* Error colors */
error: {
darker: "#F50F0C",
darkest: "#F50F0C",
dark: "#FF1F1C",
main: "#FA4643",
light: "#F26563",
@@ -58,7 +58,8 @@ const variables = {
},
},
/* gitter and spacing */
spacingUnit: 5,
spacingUnitSmall: 5,
spacingUnitLarge: 10,
/* Borders */
roundCorners: "2px",
/* Typography */
@@ -68,6 +69,14 @@ const variables = {
fontWeightLight: 300,
fontWeightRegular: 400,
fontWeightMedium: 550,
/* Breakpoints */
breakpoints: {
xs: 320,
sm: 640,
md: 1024,
lg: 1400,
xl: 1600,
},
};
module.exports = variables;
export default variables;
+3 -1
View File
@@ -29,4 +29,6 @@ export type Overwrite<T, U> = Pick<T, Diff<keyof T, keyof U>> & U;
*
* E.g. type ButtonProps = ReturnPropTypes<Button>;
*/
export type PropTypesOf<T> = T extends React.ComponentType<infer R> ? R : {};
export type PropTypesOf<T> = T extends React.ComponentType<infer R>
? R
: T extends React.Component<infer S> ? S : {};
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
presets: [
["@babel/env", { targets: "last 2 versions, ie 11", modules: "commonjs" }],
],
};
+1
View File
@@ -0,0 +1 @@
export { default as loadSchema } from "./loadSchema";
+2
View File
@@ -0,0 +1,2 @@
export { default as timeout } from "./timeout";
export { default as pascalCase } from "./pascalCase";
+5
View File
@@ -0,0 +1,5 @@
import { camelCase } from "lodash";
export default function pascalCase(value: string) {
return value[0].toUpperCase() + camelCase(value).slice(1);
}
+4
View File
@@ -0,0 +1,4 @@
/** A promisified timeout. */
export default function timeout(ms: number = 0) {
return new Promise(resolve => setTimeout(resolve, ms));
}
@@ -1,4 +1,4 @@
import loadSchema from "talk-server/graph/common/schema";
import { loadSchema } from "talk-common/graphql";
import resolvers from "talk-server/graph/management/resolvers";
export default function getManagementSchema() {
+1 -1
View File
@@ -1,4 +1,4 @@
import loadSchema from "talk-server/graph/common/schema";
import { loadSchema } from "talk-common/graphql";
import resolvers from "talk-server/graph/tenant/resolvers";
export default function getTenantSchema() {
@@ -237,7 +237,22 @@ type PageInfo {
"""
Indicates that there are more nodes after this subset.
"""
hasNextPage: Boolean!
hasNextPage: Boolean
"""
Included for legacy Relay reasons. Always set to false.
"""
hasPreviousPage: Boolean
"""
Included for legacy Relay reasons. Always set to null.
"""
startCursor: Cursor
"""
Specifies the last node's cursor for forwards pagination.
"""
endCursor: Cursor
}
"""
@@ -307,7 +322,7 @@ type Asset {
first: Int = 10
orderBy: COMMENT_SORT = CREATED_AT_DESC
after: Cursor
): CommentsConnection
): CommentsConnection!
"""
author is the authors listed in the meta tags for the Asset.
Loaded 100 of 112 files, more files were not shown because too many files have changed in this diff. Show more