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
  ...
This commit is contained in:
Belén Curcio
2018-07-16 13:42:54 -03:00
114 changed files with 4399 additions and 1049 deletions
+40
View File
@@ -0,0 +1,40 @@
const path = require("path");
module.exports = {
displayName: "client",
rootDir: "../",
roots: ["<rootDir>/src/core/client"],
collectCoverageFrom: ["**/*.{js,jsx,mjs,ts,tsx}"],
coveragePathIgnorePatterns: ["/node_modules/"],
setupFiles: [
"<rootDir>/config/polyfills.js",
"<rootDir>/src/core/client/test/setup.ts",
],
testMatch: ["**/*.spec.{js,jsx,mjs,ts,tsx}"],
testEnvironment: "node",
testURL: "http://localhost",
transform: {
"^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest",
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
"^(?!.*\\.(js|jsx|mjs|ts|tsx|css|json|ftl)$)":
"<rootDir>/config/jest/fileTransform.js",
},
transformIgnorePatterns: [
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|ts|tsx)$",
],
moduleNameMapper: {
"^talk-admin/(.*)$": "<rootDir>/src/core/client/admin/$1",
"^talk-ui/(.*)$": "<rootDir>/src/core/client/ui/$1",
"^talk-stream/(.*)$": "<rootDir>/src/core/client/stream/$1",
"^talk-framework/(.*)$": "<rootDir>/src/core/client/framework/$1",
"^talk-common/(.*)$": "<rootDir>/src/core/common/$1",
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
snapshotSerializers: ["enzyme-to-json/serializer"],
globals: {
"ts-jest": {
useBabelrc: true,
tsConfigFile: path.resolve(__dirname, "tsconfig.jest.json"),
},
},
};
+19
View File
@@ -0,0 +1,19 @@
module.exports = {
displayName: "server",
rootDir: "../",
roots: ["<rootDir>/src"],
collectCoverageFrom: ["**/*.{js,jsx,mjs,ts,tsx}"],
coveragePathIgnorePatterns: ["/node_modules/"],
testMatch: ["**/*.spec.{js,jsx,mjs,ts,tsx}"],
testPathIgnorePatterns: ["/node_modules/", "/client/"],
testEnvironment: "node",
testURL: "http://localhost",
transform: {
"^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest",
},
moduleNameMapper: {
"^talk-server/(.*)$": "<rootDir>/src/core/server/$1",
"^talk-common/(.*)$": "<rootDir>/src/core/common/$1",
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
};
+3 -40
View File
@@ -1,43 +1,6 @@
const paths = require("./paths");
module.exports = {
rootDir: "../",
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}",
],
testEnvironment: "node",
testURL: "http://localhost",
transform: {
"^.+\\.(js|jsx|mjs|ts|tsx)$": "<rootDir>/node_modules/ts-jest",
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
"^(?!.*\\.(js|jsx|mjs|css|json|ftl)$)":
"<rootDir>/config/jest/fileTransform.js",
},
transformIgnorePatterns: [
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|ts|tsx)$",
],
moduleNameMapper: {
"^talk-admin/(.*)$": "<rootDir>/src/core/client/admin/$1",
"^talk-ui/(.*)$": "<rootDir>/src/core/client/ui/$1",
"^talk-stream/(.*)$": "<rootDir>/src/core/client/stream/$1",
"^talk-framework/(.*)$": "<rootDir>/src/core/client/framework/$1",
"^talk-common/(.*)$": "<rootDir>/src/core/common/$1",
"^talk-server/(.*)$": "<rootDir>/src/core/server/$1",
},
moduleFileExtensions: [
"web.js",
"js",
"json",
"web.jsx",
"jsx",
"node",
"mjs",
"ts",
"tsx",
projects: [
"<rootDir>/jest-client.config.js",
"<rootDir>/jest-server.config.js",
],
};
+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,
},
},
],
+1075 -564
View File
File diff suppressed because it is too large Load Diff
+21 -2
View File
@@ -57,6 +57,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",
@@ -67,11 +68,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",
@@ -80,12 +84,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",
@@ -99,6 +106,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",
@@ -109,12 +119,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",
@@ -123,14 +139,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 -23
View File
@@ -1,38 +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;
comment?: any | null;
asset: {} | null;
}
const App: StatelessComponent<AppProps> = props => {
console.log(props);
if (props.comment) {
return <div>Comment</div>;
}
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(2 px * $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",
};
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", () => {
body: "Woof",
gutterBottom: true,
};
const renderer = createRenderer();
renderer.render(<Comment {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
const wrapper = shallow(<Comment {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -4,6 +4,7 @@ import { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
import * as styles from "./Comment.css";
import PermalinkPopover from "./PermalinkPopover";
import Username from "./Username";
export interface CommentProps {
id: string;
@@ -15,17 +16,15 @@ export interface CommentProps {
gutterBottom?: boolean;
}
// make a permalink popover
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>
<div className={rootClassName} role="article">
<div className={styles.topBar}>
{props.author && <Username>{props.author.username}</Username>}
</div>
<Typography>{props.body}</Typography>
<div className={cn("talk-comment-footer")}>
<PermalinkPopover commentId={props.id} />
@@ -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,14 +2,16 @@
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)>
<Username>
Marvin
</Username>
</div>
<withPropsOnChange(Typography)>
Woof
</withPropsOnChange(Typography)>
@@ -18,14 +20,16 @@ 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)>
<Username>
Marvin
</Username>
</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,28 @@ 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
}
`;
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 {
id
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: 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($assetID: ID!, $showAssetList: Boolean!) {
...AppContainer
@arguments(assetID: $assetID, showAssetList: $showAssetList)
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,214 @@
// 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>
</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>
</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>
</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>
</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>
</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,138 @@
// 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>
</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>
</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>
</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>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,91 @@
// 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>
</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>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,226 @@
// 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>
</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>
</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>
</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>
</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>
</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();
});
+5
View File
@@ -0,0 +1,5 @@
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
// React 16 Enzyme adapter
Enzyme.configure({ adapter: new Adapter() });
+27
View File
@@ -0,0 +1,27 @@
import { JSDOM } from "jsdom";
declare var global: any;
const jsdom = new JSDOM("<!doctype html><html><body></body></html>");
const { window } = jsdom;
function copyProps(src: any, target: any) {
const props = Object.getOwnPropertyNames(src)
.filter(prop => typeof target[prop] === "undefined")
.reduce(
(result, prop) => ({
...result,
[prop]: Object.getOwnPropertyDescriptor(src, prop),
}),
{}
);
Object.defineProperties(target, props);
}
global.window = window;
global.document = (window as any).document;
global.navigator = {
userAgent: "node.js",
};
copyProps(window, global);
+24
View File
@@ -0,0 +1,24 @@
import "./enzyme";
import "./jsdom";
// TODO: Remove when fixed.
// Mock React.createContext because of https://github.com/airbnb/enzyme/issues/1509.
function mockReact() {
const originalReact = require.requireActual("react");
return {
...originalReact,
createContext: jest.fn(defaultValue => {
let value = defaultValue;
const Provider = (props: any) => {
value = props.value;
return props.children;
};
const Consumer = (props: any) => props.children(value);
return {
Provider,
Consumer,
};
}),
};
}
jest.mock("react", () => mockReact());
+9 -33
View File
@@ -5,40 +5,16 @@
"module": "esnext",
"jsx": "preserve",
"allowJs": false,
"lib": [
"dom",
"es7",
"scripthost",
"es2015",
"esnext.asynciterable"
],
"lib": ["dom", "es7", "scripthost", "es2015", "esnext.asynciterable"],
"baseUrl": "./",
"paths": {
"talk-admin/*": [
"./admin/*"
],
"talk-stream/*": [
"./stream/*"
],
"talk-framework/*": [
"./framework/*"
],
"talk-ui/*": [
"./ui/*"
],
"talk-common/*": [
"../common/*"
],
"talk-locales/*": [
"../../locales/*"
]
"talk-admin/*": ["./admin/*"],
"talk-stream/*": ["./stream/*"],
"talk-framework/*": ["./framework/*"],
"talk-ui/*": ["./ui/*"],
"talk-common/*": ["../common/*"]
}
},
"include": [
"./**/*",
"../../types/**/*.d.ts"
],
"exclude": [
"node_modules"
]
}
"include": ["./**/*", "../../types/**/*.d.ts"],
"exclude": ["node_modules"]
}
+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";
@@ -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);
}
+5 -1
View File
@@ -1,6 +1,10 @@
export { default as BaseButton } from "./BaseButton";
export { default as Button } from "./Button";
export { default as Center } from "./Center";
export { default as Typography } from "./Typography";
<<<<<<< HEAD
export { default as Popover } from "./Popover";
export { default as TextField } from "./TextField";
=======
export { default as Flex } from "./Flex";
export { default as MatchMedia } from "./MatchMedia";
>>>>>>> cac0afa61e79b19ddcae873f543a6910b83c92e7
+25 -21
View File
@@ -1,59 +1,63 @@
.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 {}
.subtitle1 {
}
.subtitle2 {}
.subtitle2 {
}
.body2 {}
.body2 {
}
.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);
}
.overline {}
.overline {
}
+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);
}

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