feat: added support for --only flag

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

Watcher --only
Use JSDocs comments (#1727)


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

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

* Remove old nodemon configs

* Remove nodemon

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

* Make jest testing work with custom path and css modules

* Add first test

* feat: added unit tests to ci

* fix: updated package-lock.json

* Update cssTransform.js

* Update cssTransform.js

* Fix test in ci

Adapt files.exclude (#1736)


Permalink ui

Adding Copy to clipboard functionality

WIP

clean request

wip

progress

progress

work in progress

wip

ui functionality

Translations :/

wip

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

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

Adding Attachment and Popover Component

merge conflicts

progress

Any

Working

Support for refs

Ready
This commit is contained in:
Belén Curcio
2018-07-16 18:31:06 -03:00
parent 044e1c2863
commit b1cb624e4f
183 changed files with 8963 additions and 2233 deletions
+17
View File
@@ -43,6 +43,20 @@ jobs:
name: Perform linting
command: npm run lint
# unit_tests will run the unit tests.
unit_tests:
<<: *job_defaults
steps:
- checkout
- attach_workspace:
at: ~/coralproject/talk
- run:
name: Compile schemas and types
command: npm run compile
- run:
name: Perform testing
command: npm run test
# build will build the static assets and server typescript files.
build:
<<: *job_defaults
@@ -74,6 +88,9 @@ workflows:
- lint:
requires:
- npm_dependencies
- unit_tests:
requires:
- npm_dependencies
- build:
requires:
- npm_dependencies
+1
View File
@@ -0,0 +1 @@
node_modules
+3 -5
View File
@@ -9,12 +9,10 @@
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"node_modules": true,
"dist": true,
".vscode": true,
"package-lock.json": true
".vs": true
},
"tslint.exclude": "**/node_modules/**",
"tslint.autoFixOnSave": true,
"tslint.jsEnable": true,
"typescript.tsdk": "node_modules/typescript/lib"
}
}
+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 -35
View File
@@ -1,38 +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|test).{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: {
"^react-native$": "react-native-web",
},
moduleFileExtensions: [
"web.js",
"js",
"json",
"web.jsx",
"jsx",
"node",
"mjs",
"ts",
"tsx",
projects: [
"<rootDir>/jest-client.config.js",
"<rootDir>/jest-server.config.js",
],
};
+99 -8
View File
@@ -1,14 +1,105 @@
"use strict";
// This is a custom Jest transformer turning style imports into empty objects.
// http://facebook.github.io/jest/docs/en/webpack.html
// Adapted version of https://github.com/Connormiha/jest-css-modules-transform
// 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, basename, extname } = require("path");
const postcss = require("postcss");
const postcssNested = postcss([require("postcss-nested")]);
const REG_EXP_NAME_BREAK_CHAR = /[\s,.{/#[:]/;
const getCSSSelectors = (css, path) => {
const end = css.length;
let i = 0;
let char;
let bracketsCount = 0;
const result = {};
while (i < end) {
if (i === -1) {
throw Error(`Parse error ${path}`);
}
if (css.indexOf("/*", i) === i) {
i = css.indexOf("*/", i + 2);
// Unclosed comment. Break to avoid infinity loop
if (i === -1) {
// Don't parse, but save collected result
return result;
}
continue;
}
char = css[i];
if (char === "{") {
bracketsCount++;
i++;
continue;
}
if (char === "}") {
bracketsCount--;
i++;
continue;
}
if (char === '"' || char === "'") {
do {
i = css.indexOf(char, i + 1);
// Syntax error since this line. Don't parse, but save collected result
if (i === -1) {
return result;
}
} while (css[i - 1] === "\\");
i++;
continue;
}
if (bracketsCount > 0) {
i++;
continue;
}
if (char === "." || char === "#") {
i++;
const startWord = i;
while (!REG_EXP_NAME_BREAK_CHAR.test(css[i])) {
i++;
}
const word = css.slice(startWord, i);
result[word] = word;
continue;
}
if (css.indexOf("@keyframes", i) === i) {
i += 10;
while (REG_EXP_NAME_BREAK_CHAR.test(css[i])) {
i++;
}
const startWord = i;
while (!REG_EXP_NAME_BREAK_CHAR.test(css[i])) {
i++;
}
const word = css.slice(startWord, i);
result[word] = word;
continue;
}
i++;
}
return result;
};
module.exports = {
process() {
return "module.exports = {};";
},
getCacheKey() {
// The output is always the same.
return "cssTransform";
process(src, path, config) {
const filename = path.slice(path.lastIndexOf(pathSep) + 1);
const textCSS = postcssNested.process(src).css;
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}`;
},
};
-8
View File
@@ -1,8 +0,0 @@
{
"exec": "npm-run-all compile:relay-stream",
"ext": "ts,tsx,graphql",
"watch": [
"./src/core/client/stream",
"./src/core/**/*.graphql"
]
}
-10
View File
@@ -1,10 +0,0 @@
{
"exec": "npm run start:development",
"ext": "ts,graphql",
"watch": [
"./src"
],
"ignore": [
"./src/client"
]
}
+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"
}
}
+23 -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"],
executor: new CommandExecutor("npm run compile:relay-stream", {
runOnInit: true,
}),
@@ -35,6 +40,22 @@ const config: Config = {
paths: [],
executor: new LongRunningExecutor("npm run start:webpackDevServer"),
},
runDocz: {
paths: [],
executor: new LongRunningExecutor("npm run docz -- dev"),
},
},
defaultSet: "client",
sets: {
server: ["runServer"],
client: [
"runServer",
"runWebpackDevServer",
"compileCSSTypes",
"compileRelayStream",
],
docz: ["runDocz", "compileCSSTypes"],
compile: ["compileCSSTypes", "compileRelayStream"],
},
};
+7 -1
View File
@@ -180,7 +180,7 @@ module.exports = {
// All available locales can be loadable on demand.
// To restrict available locales set:
// availableLocales: ["en-US"]
// availableLocales: ["en-US"],
},
},
],
@@ -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,
},
},
],
@@ -242,6 +247,7 @@ module.exports = {
options: {
modules: true,
importLoaders: 1,
localIdentName: "[name]-[local]-[hash:base64:5]",
},
},
{
+2 -1
View File
@@ -9,6 +9,7 @@ const paths = require("./paths");
const protocol = process.env.HTTPS === "true" ? "https" : "http";
const host = process.env.HOST || "0.0.0.0";
const serverPort = process.env.PORT || 3000;
const doczPort = process.env.DOCZ_PORT || 3030;
module.exports = function(proxy, allowedHost) {
return {
@@ -81,8 +82,8 @@ module.exports = function(proxy, allowedHost) {
disableDotRule: true,
},
public: allowedHost,
// Proxy to the graphql server.
proxy: proxy || {
// Proxy to the graphql server.
"/api": {
target: `http://localhost:${serverPort}`,
},
+1 -1
View File
@@ -14,7 +14,7 @@ export default {
source: "./src",
typescript: true,
host: process.env.HOST || "0.0.0.0",
port: parseInt(process.env.DOCZ_PORT, 10) || 3000,
port: parseInt(process.env.DOCZ_PORT, 10) || 3030,
modifyBundlerConfig: config => {
config.module.rules.push({
test: /\.css$/,
+3883 -1539
View File
File diff suppressed because it is too large Load Diff
+62 -34
View File
@@ -5,10 +5,11 @@
"scripts": {
"start": "node dist/index.js",
"test": "node scripts/test.js --env=jsdom",
"build": "npm-run-all --parallel compile:* && npm-run-all --parallel build:*",
"build": "npm-run-all compile --parallel build:*",
"build:client": "node ./scripts/build.js",
"build:server": "tsc -p ./src/tsconfig.json",
"watch": "NODE_ENV=development ts-node ./scripts/watcher/bin/watcher.ts ./config/watcher.ts",
"watch": "NODE_ENV=development ts-node ./scripts/watcher/bin/watcher.ts --config ./config/watcher.ts",
"compile": "npm-run-all --parallel compile:*",
"compile:css-types": "tcm src/core/client/",
"compile:relay-stream": "relay-compiler --src ./src/core/client/stream --schema $(ts-node ./scripts/schemaPath.ts tenant) --language typescript --artifactDirectory ./src/core/client/stream/__generated__ --no-watchman",
"start:development": "NODE_ENV=development ts-node --project ./src/tsconfig.json -r tsconfig-paths/register ./src/index.ts",
@@ -18,14 +19,16 @@
"lint:server": "tslint --project ./src/tsconfig.json",
"lint:client": "tslint --project ./src/core/client/tsconfig.json",
"lint:scripts": "tslint --project ./tsconfig.json",
"docz:watch": "docz dev"
"docz": "docz"
},
"author": "",
"license": "Apache-2.0",
"dependencies": {
"@types/react-copy-to-clipboard": "^4.2.5",
"@types/react-tooltip": "^3.3.5",
"apollo-server-express": "^1.3.6",
"bunyan": "^1.8.12",
"convict": "^4.3.0",
"convict": "^4.3.1",
"dataloader": "^1.4.0",
"dotenv": "^6.0.0",
"dotenv-expand": "^4.2.0",
@@ -36,20 +39,21 @@
"graphql": "^0.13.2",
"graphql-config": "^2.0.1",
"graphql-redis-subscriptions": "^1.5.0",
"graphql-tools": "^3.0.2",
"graphql-tools": "^3.0.5",
"ioredis": "^3.2.2",
"joi": "^13.4.0",
"lodash": "^4.17.10",
"luxon": "^1.2.1",
"mongodb": "^3.0.10",
"luxon": "^1.3.1",
"mongodb": "^3.1.1",
"passport": "^0.4.0",
"performance-now": "^2.1.0",
"subscriptions-transport-ws": "^0.9.11",
"uuid": "^3.2.1"
"subscriptions-transport-ws": "^0.9.12",
"uuid": "^3.3.2"
},
"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.53",
"@babel/polyfill": "7.0.0-beta.49",
"@babel/preset-env": "7.0.0-beta.49",
"@babel/preset-react": "7.0.0-beta.49",
@@ -60,24 +64,32 @@
"@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/graphql": "^0.13.3",
"@types/ioredis": "^3.2.12",
"@types/jest": "^23.1.5",
"@types/joi": "^13.0.8",
"@types/lodash": "^4.14.109",
"@types/jsdom": "^11.0.6",
"@types/lodash": "^4.14.111",
"@types/luxon": "^0.5.3",
"@types/mongodb": "^3.0.19",
"@types/node": "^10.3.1",
"@types/mongodb": "^3.1.1",
"@types/node": "^10.5.2",
"@types/passport": "^0.4.5",
"@types/query-string": "^6.1.0",
"@types/react-dom": "^16.0.6",
"@types/react-relay": "^1.3.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/sane": "^2.0.0",
"@types/sinon": "^5.0.1",
"@types/uuid": "^3.4.3",
"@types/ws": "^5.1.2",
"autoprefixer": "^8.6.0",
"autoprefixer": "^8.6.5",
"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",
@@ -89,43 +101,59 @@
"copy-webpack-plugin": "^4.5.1",
"cross-spawn": "^6.0.5",
"css-loader": "^0.28.11",
"docz": "^0.2.6",
"docz-theme-default": "^0.2.10",
"docz": "^0.5.8",
"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",
"flat": "^4.1.0",
"fluent": "^0.6.4",
"fluent-intl-polyfill": "^0.1.0",
"fluent-langneg": "^0.1.0",
"fluent-react": "^0.7.0",
"graphql-playground-middleware-express": "^1.7.0",
"graphql-playground-middleware-express": "^1.7.2",
"html-webpack-plugin": "^3.2.0",
"jest": "^23.2.0",
"jest": "^23.4.1",
"jsdom": "^11.11.0",
"loader-utils": "^1.1.0",
"nodemon": "^1.17.5",
"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-loader": "^2.1.5",
"precss": "^3.1.2",
"prettier": "^1.13.4",
"postcss-import": "^11.1.0",
"postcss-loader": "^2.1.6",
"postcss-nested": "^3.0.0",
"postcss-prepend-imports": "^1.0.1",
"postcss-preset-env": "^5.2.1",
"prettier": "^1.13.7",
"pstree.remy": "^1.1.0",
"query-string": "^6.1.0",
"raw-loader": "^0.5.1",
"react": "^16.4.0",
"react-dev-utils": "6.0.0-next.3e165448",
"react-dom": "^16.4.0",
"react-final-form": "^3.6.0",
"react-final-form": "^3.6.4",
"react-relay": "github:coralproject/patched#react-relay",
"react-responsive": "^4.1.0",
"react-test-renderer": "^16.4.1",
"react-timeago": "^4.1.9",
"react-copy-to-clipboard": "^5.0.1",
"react-popper": "^1.0.0-beta.6",
"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",
"sane": "^2.5.2",
"sinon": "^6.1.3",
"style-loader": "^0.21.0",
"ts-jest": "^22.4.6",
"ts-loader": "^4.3.1",
"ts-node": "^6.1.0",
"tsconfig-paths": "^3.4.0",
"ts-jest": "^23.0.0",
"ts-loader": "^4.4.2",
"ts-node": "^6.2.0",
"tsconfig-paths": "^3.4.2",
"tsconfig-paths-webpack-plugin": "^3.1.4",
"tslint": "^5.10.0",
"tslint-config-prettier": "^1.13.0",
@@ -133,12 +161,12 @@
"tslint-plugin-prettier": "^1.3.0",
"tslint-react": "^3.6.0",
"typed-css-modules": "^0.3.4",
"typescript": "^2.9.1",
"typescript": "^2.9.2",
"uglifyjs-webpack-plugin": "^1.2.5",
"webpack": "4.12.0",
"webpack-cli": "^3.0.2",
"webpack-dev-server": "^3.1.4",
"webpack-hot-client": "^4.0.3",
"webpack-hot-client": "^4.1.1",
"webpack-manifest-plugin": "^2.0.3"
}
}
}
+1 -1
View File
@@ -19,11 +19,11 @@ const paths = require("../config/paths");
const jest = require("jest");
let argv = process.argv.slice(2);
argv.push("--config", paths.appJestConfig);
// Watch unless on CI or in coverage mode
if (!process.env.CI && argv.indexOf("--coverage") < 0) {
argv.push("--watch");
argv.push("--config", paths.appJestConfig);
}
jest.run(argv);
+41 -26
View File
@@ -1,14 +1,22 @@
import chokidar from "chokidar";
import path from "path";
import { Watcher, WatchOptions } from "./types";
function prependRootDir(
prepend: string,
paths: ReadonlyArray<string>
): string[] {
const prependFunc = (p: string) => path.resolve(prepend, p);
return paths.map(prependFunc);
}
export default class ChokidarWatcher implements Watcher {
public watch(
rootDir: string,
paths: ReadonlyArray<string>,
options: WatchOptions = {}
): AsyncIterable<string> {
const client = chokidar.watch(paths as string[], {
ignored: options.ignore,
});
const resolvedPaths = prependRootDir(rootDir, paths);
// An array to hold all changes, that has not yet been yield.
const queue: string[] = [];
@@ -19,31 +27,38 @@ export default class ChokidarWatcher implements Watcher {
| ({ resolve: (result: string) => void; reject: (error: Error) => void })
| null = null;
// Listen for errors
client.on("error", (error: Error) => {
// Resolve pending request.
if (pending) {
pending.reject(error);
pending = null;
return;
}
if (!firstError) {
firstError = error;
}
});
// Only start client if we have something to watch.
if (paths.length) {
const client = chokidar.watch(resolvedPaths, {
ignored: options.ignore && prependRootDir(rootDir, options.ignore),
});
// Listen for changes
client.on("change", (pathFile: string) => {
// Resolve pending request.
if (pending) {
pending.resolve(pathFile);
pending = null;
return;
}
// Listen for errors
client.on("error", (error: Error) => {
// Resolve pending request.
if (pending) {
pending.reject(error);
pending = null;
return;
}
if (!firstError) {
firstError = error;
}
});
// There is no pending request, save it into the queue.
queue.unshift(pathFile);
});
// Listen for changes
client.on("change", (pathFile: string) => {
// Resolve pending request.
if (pending) {
pending.resolve(pathFile);
pending = null;
return;
}
// There is no pending request, save it into the queue.
queue.unshift(pathFile);
});
}
return {
[Symbol.asyncIterator]() {
return {
+6 -5
View File
@@ -1,3 +1,4 @@
import chalk from "chalk";
import spawn from "cross-spawn";
import { Cancelable, debounce } from "lodash";
@@ -5,13 +6,13 @@ import { Executor } from "./types";
interface CommandExecutorOptions {
args?: ReadonlyArray<string>;
// If true, allow spawning multiple processes.
/** If true, allow spawning multiple processes. */
spawnMutiple?: boolean;
// Specify the period in which the process is started at max once.
/** Specify the period in which the process is started at max once. */
debounce?: number | false;
// If true, will run command upon initialization.
/** If true, will run command upon initialization. */
runOnInit?: boolean;
}
@@ -65,9 +66,9 @@ export default class CommandExecutor implements Executor {
child.on("close", (code: number) => {
this.isRunning = false;
if (code !== 0) {
if (code !== 0 && code !== null) {
// tslint:disable-next-line: no-console
console.log(`We had an error building ${code}`);
console.log(chalk.red(`Command exited with ${code}`));
}
if (this.shouldRespawn) {
this.spawnProcessPotentiallyDebounced();
+41 -15
View File
@@ -1,12 +1,15 @@
import chalk from "chalk";
import { ChildProcess } from "child_process";
import spawn from "cross-spawn";
import { Cancelable, debounce } from "lodash";
import psTree from "pstree.remy";
import { Executor } from "./types";
interface LongRunningExecutorOptions {
args?: ReadonlyArray<string>;
// Specify the period in which the process is restarted at max once.
/** Specify the period in which the process is restarted at max once. */
debounce?: number;
}
@@ -31,9 +34,6 @@ export default class LongRunningExecutor implements Executor {
this.isRunning = true;
this.process = spawn(this.cmd, this.args as string[], {
stdio: "inherit",
// Have all child processes in their own group.
// See `process.kill` below.
detached: true,
shell: !this.args,
});
@@ -42,27 +42,53 @@ export default class LongRunningExecutor implements Executor {
if (code !== 0 && code !== null) {
// tslint:disable-next-line: no-console
console.error(`Exit code returned ${code}`);
console.log(chalk.red(`Command exited with ${code}`));
return;
}
if (this.shouldRestart) {
this.shouldRestart = false;
this.spawnProcess();
}
});
}
private restart(): void {
private restart() {
this.shouldRestart = true;
// Using the `-` will kill all child procceses in the group.
// See: https://azimi.me/2014/12/31/kill-child_process-node-js.html
process.kill(-this.process!.pid, "SIGTERM");
return this.internalKill();
}
private kill(): void {
private kill() {
this.shouldRestart = false;
// Using the `-` will kill all child procceses in the group.
// See: https://azimi.me/2014/12/31/kill-child_process-node-js.html
process.kill(-this.process!.pid, "SIGTERM");
return this.internalKill();
}
private async internalKill(): Promise<void> {
return new Promise<void>((resolve, reject) => {
const signal = "SIGTERM";
if (process.platform === "win32") {
// Force kill (/F) the whole child tree (/T) by PID
spawn.sync("taskkill", [
"/pid",
this.process!.pid.toString(),
"/T",
"/F",
]);
resolve();
return;
}
psTree(this.process!.pid, (err, kids) => {
if (err) {
reject(err);
}
spawn.sync("kill", [
`-${signal}`,
this.process!.pid.toString(),
...kids.map(p => p.PID.toString()),
]);
resolve();
});
});
}
// This is called before watching starts.
@@ -71,10 +97,10 @@ export default class LongRunningExecutor implements Executor {
}
// This is called before exiting.
public onCleanup() {
public async onCleanup() {
this.restartDebounced.cancel();
if (this.isRunning) {
this.kill();
await this.kill();
}
}
+101
View File
@@ -0,0 +1,101 @@
import chalk from "chalk";
import { execSync } from "child_process";
import sane from "sane";
import { Watcher, WatchOptions } from "./types";
interface SaneWatcherOptions {
/**
* Set to true to use watchman, false to disabled, and undefined
* for automatic detection.
*/
watchman?: boolean;
/** Use polling, this might be required for network file systems. */
poll?: boolean;
}
function canUseWatchman(): boolean {
try {
execSync("watchman --version", { stdio: ["ignore"] });
return true;
// tslint:disable-next-line:no-empty
} catch (e) {}
return false;
}
export default class SaneWatcher implements Watcher {
private watchman?: boolean;
private poll: boolean;
constructor(options: SaneWatcherOptions = {}) {
this.watchman = options.watchman;
this.poll = options.poll || false;
// Autodetect watchman.
if (this.watchman === undefined && canUseWatchman()) {
this.watchman = true;
// tslint:disable-next-line:no-console
console.log(chalk.grey(`Watchman detected`));
}
}
public watch(
rootDir: string,
paths: ReadonlyArray<string>,
options: WatchOptions = {}
): AsyncIterable<string> {
// An array to hold all changes, that has not yet been yield.
const queue: string[] = [];
// If this is set, a pending promise is waiting for the next result.
let pending: ({ resolve: (result: string) => void }) | null = null;
// Only start client if we have something to watch.
if (paths.length) {
// Setup client
const client = sane(rootDir, {
glob: paths as string[],
ignored: options.ignore as string[],
watchman: this.watchman,
poll: this.poll,
});
// Listen for changes
client.on("change", (pathFile: string) => {
// Resolve pending request.
if (pending) {
pending.resolve(pathFile);
pending = null;
return;
}
// There is no pending request, save it into the queue.
queue.unshift(pathFile);
});
}
return {
[Symbol.asyncIterator]() {
return {
next: () =>
new Promise<IteratorResult<string>>((resolve, reject) => {
const wrapped = {
resolve: (pathFile: string) =>
resolve({
done: false,
value: pathFile,
}),
};
if (queue.length) {
wrapped.resolve(queue.pop()!);
return;
}
// We need to wait for the next change event.
pending = wrapped;
}),
};
},
};
}
}
+30 -10
View File
@@ -4,16 +4,36 @@ import program from "commander";
import path from "path";
import watch from "../";
program
async function run(
args: ReadonlyArray<string>,
options: Record<string, string>
) {
const only = args;
const { config: configFile = "" } = options;
if (!configFile) {
throw new Error("Config file not specified");
}
// tslint:disable-next-line:no-var-requires
let config: any = require(path.resolve(configFile));
if (config.__esModule) {
config = config.default;
}
try {
await watch(config, { only });
} catch (err) {
// tslint:disable-next-line:no-console
console.error(err);
process.exit(1);
}
}
const cmd = program
.version("0.1.0")
.usage("<configFile>")
.arguments("<configFile>")
.usage("[watchers or sets]")
.option("-c, --config <configFile>", "Use given config file")
.description("Run watchers defined in <configFile>")
.action(configFile => {
let config: any = require(path.resolve(configFile));
if (config.__esModule) {
config = config.default;
}
watch(config);
})
.parse(process.argv);
run(cmd.args, cmd.opts());
+22 -6
View File
@@ -5,24 +5,31 @@ export interface WatchOptions {
}
export interface Watcher {
onInit?(): void | Promise<void>;
onCleanup?(): void | Promise<void>;
watch(
rootDir: string,
paths: ReadonlyArray<string>,
options?: WatchOptions
): AsyncIterable<string>;
}
export interface Executor {
onInit?(): void;
onCleanup?(): void;
onInit?(): void | Promise<void>;
onCleanup?(): void | Promise<void>;
execute(filePath: string): void;
}
export interface Options {
only?: ReadonlyArray<string>;
}
export interface Config {
rootDir?: string;
backend?: Watcher;
watchers: {
[key: string]: WatchConfig;
};
watchers: Record<string, WatchConfig>;
defaultSet?: string;
sets?: Record<string, ReadonlyArray<string>>;
}
export interface WatchConfig {
@@ -47,4 +54,13 @@ export const configSchema = Joi.object({
executor: Joi.object(),
})
),
});
defaultSet: Joi.string().optional(),
sets: Joi.object()
.pattern(
/.*/,
Joi.array()
.items(Joi.string())
.unique()
)
.optional(),
}).with("defaultSet", "sets");
+6
View File
@@ -0,0 +1,6 @@
declare module "pstree.remy" {
export default function psTree(
pid: number,
callback: (err: Error, kids: Array<{ PID: number }>) => void
): void;
}
+76 -28
View File
@@ -1,58 +1,106 @@
import chalk from "chalk";
import Joi from "joi";
import path from "path";
import { pickBy } from "lodash";
import ChokidarWatcher from "./ChokidarWatcher";
import { Config, configSchema, WatchConfig, Watcher } from "./types";
import SaneWatcher from "./SaneWatcher";
import { Config, configSchema, Options, WatchConfig, Watcher } from "./types";
// Polyfill the asyncIterator symbol.
if (Symbol.asyncIterator === undefined) {
(Symbol as any).asyncIterator = Symbol.for("asyncIterator");
}
async function beginWatch(watcher: Watcher, key: string, config: WatchConfig) {
async function beginWatch(
watcher: Watcher,
key: string,
config: WatchConfig,
rootDir: string
) {
const { paths, ignore, executor } = config;
if (executor.onInit) {
executor.onInit();
await executor.onInit();
}
for await (const filePath of watcher.watch(paths, { ignore })) {
for await (const filePath of watcher.watch(rootDir, paths, { ignore })) {
// tslint:disable-next-line:no-console
console.log(`Execute "${key}"`);
console.log(chalk.cyanBright(`Execute "${key}"`));
executor.execute(filePath);
}
}
function prependRootDir(prepend: string, cfg: WatchConfig): WatchConfig {
const prependFunc = (p: string) => path.resolve(prepend, p);
return {
...cfg,
paths: cfg.paths.map(prependFunc),
ignore: cfg.ignore ? cfg.ignore.map(prependFunc) : undefined,
};
}
function setupCleanup(config: Config) {
function setupCleanup(watcher: Watcher, config: Config) {
["SIGINT", "SIGTERM"].forEach(signal =>
process.once(signal as any, () => {
process.once(signal as any, async () => {
const cleanups = [];
if (watcher.onCleanup) {
cleanups.push(watcher.onCleanup());
}
for (const key of Object.keys(config.watchers)) {
if (config.watchers[key].executor.onCleanup) {
config.watchers[key].executor.onCleanup!();
cleanups.push(config.watchers[key].executor.onCleanup!());
}
}
await Promise.all(cleanups);
process.exit(0);
})
);
}
export default async function watch(config: Config) {
function resolveSets(
sets: Record<string, ReadonlyArray<string>>,
value: ReadonlyArray<string>
) {
const resolved: string[] = [];
value.forEach(v => {
if (v in sets) {
resolved.push(...sets[v]);
return;
}
resolved.push(v);
});
return resolved;
}
function filterOnly(
watchers: Config["watchers"],
only: ReadonlyArray<string>,
sets?: Record<string, ReadonlyArray<string>>
): Config["watchers"] {
const resolved = sets ? resolveSets(sets, only) : only;
const unknown = resolved.filter(r => !(r in watchers));
if (unknown.length) {
throw new Error(`Watcher Configuration or Set for ${unknown} not found`);
}
return pickBy(watchers, (value, key) => {
if (resolved.indexOf(key) === -1) {
// tslint:disable-next-line:no-console
console.log(chalk.grey(`Disabled watcher "${key}"`));
return false;
}
return true;
}) as Config["watchers"];
}
export default async function watch(config: Config, options: Options = {}) {
Joi.assert(config, configSchema);
const watcher = config.backend || new ChokidarWatcher();
setupCleanup(config);
for (const key of Object.keys(config.watchers)) {
const watcher: Watcher = config.backend || new SaneWatcher();
const rootDir = config.rootDir || process.cwd();
const defaultSet = config.defaultSet && [config.defaultSet];
const only = options.only && options.only.length ? options.only : defaultSet;
let watchersConfigs = config.watchers;
if (only) {
watchersConfigs = filterOnly(watchersConfigs, only, config.sets);
}
setupCleanup(watcher, config);
if (watcher.onInit) {
await watcher.onInit();
}
for (const key of Object.keys(watchersConfigs)) {
// tslint:disable-next-line:no-console
console.log(`Start watcher "${key}"`);
const watcherConfig = config.rootDir
? prependRootDir(config.rootDir, config.watchers[key])
: config.watchers[key];
beginWatch(watcher, key, watcherConfig);
console.log(chalk.cyanBright(`Start watcher "${key}"`));
const watcherConfig = watchersConfigs[key];
beginWatch(watcher, key, watcherConfig, rootDir);
}
}
+3
View File
@@ -8,5 +8,8 @@ module.exports = {
production: {
plugins: [],
},
test: {
plugins: ["@babel/transform-modules-commonjs"],
},
},
};
@@ -1,7 +1,9 @@
import { LocalizationProvider } from "fluent-react/compat";
import { MessageContext } from "fluent/compat";
import React, { StatelessComponent } from "react";
import { Formatter } from "react-timeago";
import { Environment } from "relay-runtime";
import { UIContext } from "talk-ui/components";
export interface TalkContext {
// relayEnvironment for our relay framework.
@@ -9,6 +11,9 @@ export interface TalkContext {
// localMessages for our i18n framework.
localeMessages: MessageContext[];
// formatter for timeago.
timeagoFormatter?: Formatter;
}
const { Provider, Consumer } = React.createContext<TalkContext>({} as any);
@@ -27,7 +32,9 @@ export const TalkContextProvider: StatelessComponent<{
}> = ({ value, children }) => (
<Provider value={value}>
<LocalizationProvider messages={value.localeMessages}>
{children}
<UIContext.Provider value={{ timeagoFormatter: value.timeagoFormatter }}>
{children}
</UIContext.Provider>
</LocalizationProvider>
</Provider>
);
@@ -1,4 +1,7 @@
import { Localized } from "fluent-react/compat";
import { noop } from "lodash";
import React from "react";
import { Formatter } from "react-timeago";
import { Environment, Network, RecordSource, Store } from "relay-runtime";
import { generateMessages, LocalesData, negotiateLanguages } from "../i18n";
@@ -16,6 +19,25 @@ interface CreateContextArguments {
init?: ((context: TalkContext) => void | Promise<void>);
}
/**
* timeagoFormatter integrates timeago into our translation
* framework. It gets injected into the UIContext.
*/
export const timeagoFormatter: Formatter = (value, unit, suffix) => {
// We use 'in' instead of 'from now' for language consistency
const ourSuffix = suffix === "from now" ? "in" : suffix;
return (
<Localized
id="framework-timeago"
$value={value}
$unit={unit}
$suffix={ourSuffix}
>
<span>now</span>
</Localized>
);
};
/**
* `createContext` manages the dependencies of our framework
* and returns a `TalkContext` that can be passed to the
@@ -46,6 +68,7 @@ export default async function createContext({
const context = {
relayEnvironment,
localeMessages,
timeagoFormatter,
};
// Run custom initializations.
@@ -1,5 +1,9 @@
import * as React from "react";
import { hoistStatics, InferableComponentEnhancer } from "recompose";
import {
hoistStatics,
InferableComponentEnhancer,
wrapDisplayName,
} from "recompose";
import { TalkContext, TalkContextConsumer } from "./TalkContext";
@@ -12,11 +16,17 @@ function withContext<T>(
propsCallback: (context: TalkContext) => T
): InferableComponentEnhancer<T> {
return hoistStatics<T>(
<U extends T>(WrappedComponent: React.ComponentType<U>) => (props: any) => (
<TalkContextConsumer>
{context => <WrappedComponent {...props} {...propsCallback(context)} />}
</TalkContextConsumer>
)
<U extends T>(WrappedComponent: React.ComponentType<U>) => {
const Component: React.StatelessComponent<any> = props => (
<TalkContextConsumer>
{context => (
<WrappedComponent {...props} {...propsCallback(context)} />
)}
</TalkContextConsumer>
);
Component.displayName = wrapDisplayName(WrappedComponent, "withContext");
return Component;
}
);
}
+1 -1
View File
@@ -13,7 +13,7 @@ export const VALIDATION_REQUIRED = () => (
);
export const VALIDATION_TOO_SHORT = () => (
<Localized id="framework-validation-too-short">
<Localized id="framework-validation-tooShort">
<span>This field is too short.</span>
</Localized>
);
@@ -1,24 +1,18 @@
import React, { Component } from "react";
import { QueryRenderer } from "react-relay";
import { CacheConfig, GraphQLTaggedNode, RerunParam } from "relay-runtime";
import {
QueryRenderer,
QueryRendererProps as QueryRendererPropsOrig,
} from "react-relay";
import { Omit } from "talk-framework/types";
import { TalkContextConsumer } from "../bootstrap/TalkContext";
// Taken from relay types and added Generic support for Variables and Response
export interface QueryRendererProps<V, R> {
cacheConfig?: CacheConfig;
query?: GraphQLTaggedNode | null;
render(readyState: ReadyState<R>): React.ReactElement<any> | undefined | null;
variables: V;
rerunParamExperimental?: RerunParam;
}
// Taken from relay types and added Generic support for Variables and Response
export interface ReadyState<R> {
error: Error | undefined | null;
props: R | undefined | null;
retry?(): void;
}
// Omit environment as we are passing this from the context.
export type QueryRendererProps<V, R> = Omit<
QueryRendererPropsOrig<V, R>,
"environment"
>;
/**
* TalkQueryRenderer is a wrappper around Relay's `QueryRenderer`.
@@ -1,5 +1,10 @@
import * as React from "react";
import { compose, hoistStatics, InferableComponentEnhancer } from "recompose";
import {
compose,
hoistStatics,
InferableComponentEnhancer,
wrapDisplayName,
} from "recompose";
import { Environment } from "relay-runtime";
import { withContext } from "../bootstrap";
@@ -19,6 +24,11 @@ function createMutationContainer<T extends string, I, R>(
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
hoistStatics((WrappedComponent: React.ComponentType<any>) => {
class CreateMutationContainer extends React.Component<any> {
public static displayName = wrapDisplayName(
WrappedComponent,
"createMutationContainer"
);
private commit = (input: I) => {
return commit(this.props.relayEnvironment, input);
};
@@ -7,6 +7,7 @@ export { default as QueryRenderer } from "./QueryRenderer";
export * from "./QueryRenderer";
export { default as createMutationContainer } from "./createMutationContainer";
export { default as createAndRetain } from "./createAndRetain";
export { default as wrapFetchWithLogger } from "./wrapFetchWithLogger";
export {
commitMutationPromise,
commitMutationPromiseNormalized,
@@ -6,7 +6,7 @@ import { InferableComponentEnhancerWithProps } from "recompose";
* from Relay.
*/
export default <T>(
fragmentSpec: GraphQLTaggedNode
fragmentSpec: { [P in keyof T]: GraphQLTaggedNode }
): InferableComponentEnhancerWithProps<T, { [P in keyof T]: any }> => (
component: React.ComponentType<any>
) => createFragmentContainer(component, fragmentSpec) as any;
@@ -1,6 +1,11 @@
import * as React from "react";
import { compose, hoistStatics, InferableComponentEnhancer } from "recompose";
import { CSelector, CSnapshot, Environment } from "relay-runtime";
import {
CSelector,
CSnapshot,
Environment,
GraphQLTaggedNode,
} from "relay-runtime";
import { withContext } from "../bootstrap";
@@ -25,7 +30,7 @@ export const LOCAL_ID = "client:root.local";
* must have the `LOCAL_ID`.
*/
function withLocalStateContainer<T>(
fragmentSpec: any
fragmentSpec: GraphQLTaggedNode
): InferableComponentEnhancer<{ local: T }> {
return compose(
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
@@ -33,7 +38,7 @@ function withLocalStateContainer<T>(
class LocalStateContainer extends React.Component<Props, any> {
constructor(props: Props) {
super(props);
const fragment = fragmentSpec.data().default;
const fragment = (fragmentSpec as any).data().default;
if (fragment.kind !== "Fragment") {
throw new Error("Expected fragment");
}
@@ -10,9 +10,13 @@ import { InferableComponentEnhancerWithProps } from "recompose";
* withPaginationContainer is a curried version of `createPaginationContainers`
* from Relay.
*/
export default <T, InnerProps>(
fragmentSpec: GraphQLTaggedNode,
connectionConfig: ConnectionConfig<InnerProps>
export default <T, InnerProps, FragmentVariables, QueryVariables>(
fragmentSpec: { [P in keyof T]: GraphQLTaggedNode },
connectionConfig: ConnectionConfig<
InnerProps,
FragmentVariables,
QueryVariables
>
): InferableComponentEnhancerWithProps<
T & { relay: RelayPaginationProp },
{ [P in keyof T]: any }
@@ -10,7 +10,7 @@ import { InferableComponentEnhancerWithProps } from "recompose";
* from Relay.
*/
export default <T>(
fragmentSpec: GraphQLTaggedNode,
fragmentSpec: { [P in keyof T]: GraphQLTaggedNode },
refetchQuery: GraphQLTaggedNode
): InferableComponentEnhancerWithProps<
T & { relay: RelayRefetchProp },
@@ -0,0 +1,25 @@
import { FetchFunction } from "relay-runtime";
/**
* Decorates the fetch function with error logging.
* Intended for testing purposes.
*/
export default function wrapFetchWithLogger(
fetch: FetchFunction,
logResult?: boolean
): FetchFunction {
return async (...args: any[]) => {
try {
const result = await (fetch as any)(...args);
if (logResult) {
// tslint:disable-next-line:no-console
console.log(JSON.stringify(result));
}
return result;
} catch (err) {
// tslint:disable-next-line:no-console
console.error(err);
throw err;
}
};
}
-1
View File
@@ -1,5 +1,4 @@
const path = require("path");
module.exports = {
extends: "../.babelrc.js",
plugins: [
+14
View File
@@ -0,0 +1,14 @@
/* Here we add global stylings for body and document */
:global {
body {
margin: "0";
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
}
}
.root {
}
@@ -0,0 +1,22 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import App from "./App";
it("renders correctly", () => {
const props: PropTypesOf<typeof App> = {
asset: {},
};
const wrapper = shallow(<App {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders correctly when asset is null", () => {
const props: PropTypesOf<typeof App> = {
asset: null,
};
const wrapper = shallow(<App {...props} />);
expect(wrapper).toMatchSnapshot();
});
+7 -18
View File
@@ -1,33 +1,22 @@
import * as React from "react";
import { StatelessComponent } from "react";
import { Center } from "talk-ui/components";
import { Flex } from "talk-ui/components";
import AssetListContainer from "../containers/AssetListContainer";
import PostCommentFormContainer from "../containers/PostCommentFormContainer";
import StreamContainer from "../containers/StreamContainer";
import Logo from "./Logo";
import * as styles from "./App.css";
export interface AppProps {
assets?: any | null;
asset?: {
id: string;
isClosed: boolean;
comments: any | null;
} | null;
asset: {} | null;
}
const App: StatelessComponent<AppProps> = props => {
if (props.assets) {
return <AssetListContainer assets={props.assets} />;
}
if (props.asset) {
return (
<Center>
<Logo gutterBottom />
<StreamContainer comments={props.asset.comments} />
<PostCommentFormContainer assetID={props.asset.id} />
</Center>
<Flex justifyContent="center" className={styles.root}>
<StreamContainer asset={props.asset} />
</Flex>
);
}
return <div>Asset not found </div>;
@@ -1,16 +0,0 @@
import * as React from "react";
import { StatelessComponent } from "react";
export interface AssetListProps {
assets: ReadonlyArray<{ id: string; title: string | null }>;
}
const AssetList: StatelessComponent<AssetListProps> = props => {
return (
<div>
{props.assets.map(asset => <div key={asset.id}>{asset.title}</div>)}
</div>
);
};
export default AssetList;
@@ -1,11 +0,0 @@
.root {
width: 400px;
}
.gutterBottom {
margin-bottom: calc(2px * $spacing-unit);
}
.author {
font-weight: $font-weight-medium;
}
@@ -1,32 +0,0 @@
import cn from "classnames";
import React from "react";
import { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
import * as styles from "./Comment.css";
export interface CommentProps {
className?: string;
author: {
username: string;
} | null;
body: string | null;
gutterBottom?: boolean;
}
const Comment: StatelessComponent<CommentProps> = props => {
const rootClassName = cn(styles.root, props.className, {
[styles.gutterBottom]: props.gutterBottom,
});
return (
<div className={rootClassName}>
<Typography className={styles.author} gutterBottom>
{props.author && props.author.username}
</Typography>
<Typography>{props.body}</Typography>
</div>
);
};
export default Comment;
@@ -0,0 +1,19 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Comment from "./Comment";
it("renders username and body", () => {
const props: PropTypesOf<typeof Comment> = {
id: "comment-id",
author: {
username: "Marvin",
},
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
};
const wrapper = shallow(<Comment {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,43 @@
import { Localized } from "fluent-react/compat";
import React from "react";
import { StatelessComponent } from "react";
import { Button, Popover, Typography } from "talk-ui/components";
import PermalinkPopover from "../PermalinkPopover";
import Timestamp from "./Timestamp";
import TopBar from "./TopBar";
import Username from "./Username";
export interface CommentProps {
id: string;
className?: string;
author: {
username: string;
} | null;
body: string | null;
createdAt: string;
}
const Comment: StatelessComponent<CommentProps> = props => {
return (
<div role="article">
<TopBar>
{props.author && <Username>{props.author.username}</Username>}
<Timestamp>{props.createdAt}</Timestamp>
</TopBar>
<Typography>{props.body}</Typography>
<div>
<Popover body={<PermalinkPopover commentId={props.id} />}>
{({ toggleShow, ref }) => (
<Button onClick={toggleShow} innerRef={ref} primary>
<Localized id="comments-permalink-share">
<span>Share</span>
</Localized>
</Button>
)}
</Popover>
</div>
</div>
);
};
export default Comment;
@@ -0,0 +1,3 @@
.root {
composes: timestamp from "talk-ui/shared/typography.css";
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Timestamp from "./Timestamp";
it("renders correctly", () => {
const props: PropTypesOf<typeof Timestamp> = {
children: "1995-12-17T03:24:00.000Z",
};
const wrapper = shallow(<Timestamp {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,16 @@
import React from "react";
import { StatelessComponent } from "react";
import { RelativeTime } from "talk-ui/components";
import * as styles from "./Timestamp.css";
export interface TimestampProps {
children: string;
}
const Timestamp: StatelessComponent<TimestampProps> = props => (
<RelativeTime className={styles.root} date={props.children} />
);
export default Timestamp;
@@ -0,0 +1,3 @@
.root {
margin-bottom: calc(0.5 * var(--spacing-unit));
}
@@ -0,0 +1,45 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import { PropTypesOf } from "talk-framework/types";
import { UIContext, UIContextProps } from "talk-ui/components";
import TopBar from "./TopBar";
it("renders correctly on small screens", () => {
const props: PropTypesOf<typeof TopBar> = {
children: <div>Hello World</div>,
};
const context: UIContextProps = {
mediaQueryValues: {
width: 320,
},
};
const testRenderer = TestRenderer.create(
<UIContext.Provider value={context}>
<TopBar {...props} />
</UIContext.Provider>
);
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("renders correctly on big screens", () => {
const props: PropTypesOf<typeof TopBar> = {
children: <div>Hello World</div>,
};
const context: UIContextProps = {
mediaQueryValues: {
width: 1600,
},
};
const testRenderer = TestRenderer.create(
<UIContext.Provider value={context}>
<TopBar {...props} />
</UIContext.Provider>
);
expect(testRenderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,32 @@
import cn from "classnames";
import React from "react";
import { StatelessComponent } from "react";
import { Flex, MatchMedia } from "talk-ui/components";
import * as styles from "./TopBar.css";
export interface TopBarProps {
className?: string;
children: React.ReactNode;
}
const TopBar: StatelessComponent<TopBarProps> = props => {
const rootClassName = cn(styles.root, props.className);
return (
<MatchMedia minWidth="xs">
{matches => (
<Flex
className={rootClassName}
alignItems="baseline"
direction={matches ? "row" : "column"}
itemGutter={matches ? true : "half"}
>
{props.children}
</Flex>
)}
</MatchMedia>
);
};
export default TopBar;
@@ -0,0 +1,3 @@
.root {
line-height: 1;
}
@@ -0,0 +1,45 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import { PropTypesOf } from "talk-framework/types";
import { UIContext, UIContextProps } from "talk-ui/components";
import Username from "./Username";
it("renders correctly on small screens", () => {
const props: PropTypesOf<typeof Username> = {
children: "Marvin",
};
const context: UIContextProps = {
mediaQueryValues: {
width: 320,
},
};
const testRenderer = TestRenderer.create(
<UIContext.Provider value={context}>
<Username {...props} />
</UIContext.Provider>
);
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("renders correctly on big screens", () => {
const props: PropTypesOf<typeof Username> = {
children: "Marvin",
};
const context: UIContextProps = {
mediaQueryValues: {
width: 1600,
},
};
const testRenderer = TestRenderer.create(
<UIContext.Provider value={context}>
<Username {...props} />
</UIContext.Provider>
);
expect(testRenderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,28 @@
import React from "react";
import { StatelessComponent } from "react";
import { MatchMedia, Typography } from "talk-ui/components";
import * as styles from "./Username.css";
export interface UsernameProps {
children: string;
}
const Username: StatelessComponent<UsernameProps> = props => {
return (
<MatchMedia minWidth="xs">
{matches => (
<Typography
variant={matches ? "heading2" : "heading3"}
className={styles.root}
component="span"
>
{props.children}
</Typography>
)}
</MatchMedia>
);
};
export default Username;
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders username and body 1`] = `
<div
role="article"
>
<TopBar>
<Username>
Marvin
</Username>
<Timestamp>
1995-12-17T03:24:00.000Z
</Timestamp>
</TopBar>
<withPropsOnChange(Typography)>
Woof
</withPropsOnChange(Typography)>
</div>
`;
@@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(RelativeTime)
className="Timestamp-root"
date="1995-12-17T03:24:00.000Z"
/>
`;
@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly on big screens 1`] = `
<div
className="Flex-root TopBar-root Flex-itemGutter Flex-alignBaseline Flex-directionRow"
>
<div>
Hello World
</div>
</div>
`;
exports[`renders correctly on small screens 1`] = `
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<div>
Hello World
</div>
</div>
`;
@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly on big screens 1`] = `
<span
className="Typography-root Typography-heading2 Username-root"
>
Marvin
</span>
`;
exports[`renders correctly on small screens 1`] = `
<span
className="Typography-root Typography-heading3 Username-root"
>
Marvin
</span>
`;
@@ -0,0 +1 @@
export { default, default as Comment, CommentProps } from "./Comment";
@@ -0,0 +1,8 @@
.root {
border-left: 3px solid;
padding-left: var(--spacing-unit);
}
.level0 {
border-color: var(--palette-secondary-darkest);
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Indent from "./Indent";
it("renders correctly", () => {
const props: PropTypesOf<typeof Indent> = {
children: <div>Hello World</div>,
};
const wrapper = shallow(<Indent {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,15 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import * as styles from "./Indent.css";
export interface IndentProps {
level?: number;
children: React.ReactNode;
}
const Indent: StatelessComponent<IndentProps> = props => {
return <div className={cn(styles.root, styles.level0)}>{props.children}</div>;
};
export default Indent;
@@ -0,0 +1,13 @@
.textField {
margin-right: 5px;
}
.root {
background-color: #ffffff;
border: 1px solid #c9cacb;
box-sizing: border-box;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.25);
border-radius: 1px;
padding: 6px 10px;
display: flex;
}
@@ -0,0 +1,60 @@
import { Localized } from "fluent-react/compat";
import React, { CSSProperties } from "react";
import CopyToClipboard from "react-copy-to-clipboard";
import { RefHandler } from "react-popper";
import { Button, TextField } from "talk-ui/components";
import * as styles from "./PermalinkPopover.css";
interface InnerProps {
commentId: string;
style?: CSSProperties;
innerRef?: RefHandler;
}
interface State {
copied: boolean;
}
class PermalinkPopover extends React.Component<InnerProps> {
public state: State = {
copied: false,
};
public onCopy = async () => {
await this.toggleCopied();
setTimeout(() => {
this.toggleCopied();
}, 800);
};
public toggleCopied = () => {
this.setState((state: State) => ({
copied: !state.copied,
}));
};
public render() {
const { commentId, style, innerRef } = this.props;
const { copied } = this.state;
return (
<div className={styles.root} style={style} ref={innerRef}>
<TextField defaultValue={commentId} className={styles.textField} />
<CopyToClipboard text={commentId} onCopy={this.onCopy}>
<Button primary>
{copied ? (
<Localized id="comments-permalink-copied">
<span>Copied!</span>
</Localized>
) : (
<Localized id="comments-permalink-copy">
<span>Copy</span>
</Localized>
)}
</Button>
</CopyToClipboard>
</div>
);
}
}
export default PermalinkPopover;
@@ -3,10 +3,12 @@
display: block;
height: 100px;
width: 400px;
margin-bottom: calc(2px * $spacing-unit);
width: 100%;
box-sizing: border-box;
margin-bottom: var(--spacing-unit);
}
.postButton {
float: right;
.postButtonContainer {
display: flex;
justify-content: flex-end;
}
@@ -39,11 +39,13 @@ const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
</div>
)}
</Field>
<Localized id="postCommentForm-submit">
<Button className={styles.postButton} disabled={submitting} primary>
Post
</Button>
</Localized>
<div className={styles.postButtonContainer}>
<Localized id="comments-postCommentForm-post">
<Button disabled={submitting} primary>
Post
</Button>
</Localized>
</div>
</form>
)}
</Form>
@@ -0,0 +1,42 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import sinon, { SinonSpy } from "sinon";
import { PropTypesOf } from "talk-framework/types";
import ReplyList from "./ReplyList";
it("renders correctly", () => {
const props: PropTypesOf<typeof ReplyList> = {
commentID: "comment-id",
comments: [{ id: "comment-1" }, { id: "comment-2" }],
onShowAll: noop,
hasMore: false,
disableShowAll: false,
};
const wrapper = shallow(<ReplyList {...props} />);
expect(wrapper).toMatchSnapshot();
});
describe("when there is more", () => {
const props: PropTypesOf<typeof ReplyList> = {
commentID: "comment-id",
comments: [{ id: "comment-1" }, { id: "comment-2" }],
onShowAll: sinon.spy(),
hasMore: true,
disableShowAll: false,
};
const wrapper = shallow(<ReplyList {...props} />);
it("renders a load more button", () => {
expect(wrapper).toMatchSnapshot();
});
it("calls onLoadMore", () => {
wrapper
.find("#talk-comments-replyList-showAll--comment-id")
.simulate("click");
expect((props.onShowAll as SinonSpy).calledOnce).toBe(true);
});
});
@@ -0,0 +1,50 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { Button, Flex } from "talk-ui/components";
import CommentContainer from "../containers/CommentContainer";
import Indent from "./Indent";
export interface ReplyListProps {
commentID: string;
comments: ReadonlyArray<{ id: string }>;
onShowAll: () => void;
hasMore: boolean;
disableShowAll: boolean;
}
const ReplyList: StatelessComponent<ReplyListProps> = props => {
return (
<Indent>
<Flex
direction="column"
id={`talk-comments-replyList-log--${props.commentID}`}
role="log"
itemGutter
>
{props.comments.map(comment => (
<CommentContainer key={comment.id} data={comment} />
))}
{props.hasMore && (
<Localized id="comments-replyList-showAll">
<Button
id={`talk-comments-replyList-showAll--${props.commentID}`}
aria-controls={`talk-comments-replyList-log--${props.commentID}`}
onClick={props.onShowAll}
disabled={props.disableShowAll}
secondary
invert
fullWidth
>
Show All Replies
</Button>
</Localized>
)}
</Flex>
</Indent>
);
};
export default ReplyList;
@@ -0,0 +1,4 @@
.root {
width: 100%;
max-width: 400px;
}
@@ -0,0 +1,47 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import sinon, { SinonSpy } from "sinon";
import { PropTypesOf } from "talk-framework/types";
import Stream from "./Stream";
it("renders correctly", () => {
const props: PropTypesOf<typeof Stream> = {
assetID: "asset-id",
isClosed: false,
comments: [{ id: "comment-1" }, { id: "comment-2" }],
onLoadMore: noop,
disableLoadMore: false,
hasMore: false,
};
const wrapper = shallow(<Stream {...props} />);
expect(wrapper).toMatchSnapshot();
});
describe("when there is more", () => {
const props: PropTypesOf<typeof Stream> = {
assetID: "asset-id",
isClosed: false,
comments: [{ id: "comment-1" }, { id: "comment-2" }],
onLoadMore: sinon.spy(),
disableLoadMore: false,
hasMore: true,
};
const wrapper = shallow(<Stream {...props} />);
it("renders a load more button", () => {
expect(wrapper).toMatchSnapshot();
});
it("calls onLoadMore", () => {
wrapper.find("#talk-comments-stream-loadMore").simulate("click");
expect((props.onLoadMore as SinonSpy).calledOnce).toBe(true);
});
const wrapperDisabledButton = shallow(<Stream {...props} disableLoadMore />);
it("disables load more button", () => {
expect(wrapperDisabledButton).toMatchSnapshot();
});
});
+44 -4
View File
@@ -1,18 +1,58 @@
import { Localized } from "fluent-react/compat";
import * as React from "react";
import { StatelessComponent } from "react";
import { Button, Flex } from "talk-ui/components";
import CommentContainer from "../containers/CommentContainer";
import PostCommentFormContainer from "../containers/PostCommentFormContainer";
import ReplyListContainer from "../containers/ReplyListContainer";
import Logo from "./Logo";
import * as styles from "./Stream.css";
export interface StreamProps {
assetID: string;
isClosed: boolean;
comments: ReadonlyArray<{ id: string }>;
onLoadMore: () => void;
hasMore: boolean;
disableLoadMore: boolean;
}
const Stream: StatelessComponent<StreamProps> = props => {
return (
<div>
{props.comments.map(comment => (
<CommentContainer key={comment.id} data={comment} gutterBottom />
))}
<div className={styles.root}>
<Logo gutterBottom />
<PostCommentFormContainer assetID={props.assetID} />
<Flex
direction="column"
id="talk-comments-stream-log"
role="log"
aria-live="polite"
itemGutter
>
{props.comments.map(comment => (
<Flex direction="column" key={comment.id} itemGutter>
<CommentContainer data={comment} />
<ReplyListContainer comment={comment} />
</Flex>
))}
{props.hasMore && (
<Localized id="comments-stream-loadMore">
<Button
id={"talk-comments-stream-loadMore"}
onClick={props.onLoadMore}
secondary
invert
fullWidth
disabled={props.disableLoadMore}
aria-controls="talk-comments-stream-log"
>
Load More
</Button>
</Localized>
)}
</Flex>
</div>
);
};
@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Flex
className="App-root"
justifyContent="center"
>
<Relay(StreamContainer)
asset={Object {}}
/>
</Flex>
`;
exports[`renders correctly when asset is null 1`] = `
<div>
Asset not found
</div>
`;
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="Indent-root Indent-level0"
>
<div>
Hello World
</div>
</div>
`;
@@ -0,0 +1,72 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Indent>
<Flex
direction="column"
id="talk-comments-replyList-log--comment-id"
itemGutter={true}
role="log"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
key="comment-1"
/>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
key="comment-2"
/>
</Flex>
</Indent>
`;
exports[`when there is more renders a load more button 1`] = `
<Indent>
<Flex
direction="column"
id="talk-comments-replyList-log--comment-id"
itemGutter={true}
role="log"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
key="comment-1"
/>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
key="comment-2"
/>
<Localized
id="comments-replyList-showAll"
>
<withPropsOnChange(Button)
aria-controls="talk-comments-replyList-log--comment-id"
disabled={false}
fullWidth={true}
id="talk-comments-replyList-showAll--comment-id"
invert={true}
onClick={[Function]}
secondary={true}
>
Show All Replies
</withPropsOnChange(Button)>
</Localized>
</Flex>
</Indent>
`;
@@ -0,0 +1,214 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="Stream-root"
>
<Logo
gutterBottom={true}
/>
<withContext(createMutationContainer(PostCommentFormContainer))
assetID="asset-id"
/>
<Flex
aria-live="polite"
direction="column"
id="talk-comments-stream-log"
itemGutter={true}
role="log"
>
<Flex
direction="column"
itemGutter={true}
key="comment-1"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-1",
}
}
/>
</Flex>
<Flex
direction="column"
itemGutter={true}
key="comment-2"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-2",
}
}
/>
</Flex>
</Flex>
</div>
`;
exports[`when there is more disables load more button 1`] = `
<div
className="Stream-root"
>
<Logo
gutterBottom={true}
/>
<withContext(createMutationContainer(PostCommentFormContainer))
assetID="asset-id"
/>
<Flex
aria-live="polite"
direction="column"
id="talk-comments-stream-log"
itemGutter={true}
role="log"
>
<Flex
direction="column"
itemGutter={true}
key="comment-1"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-1",
}
}
/>
</Flex>
<Flex
direction="column"
itemGutter={true}
key="comment-2"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-2",
}
}
/>
</Flex>
<Localized
id="comments-stream-loadMore"
>
<withPropsOnChange(Button)
aria-controls="talk-comments-stream-log"
disabled={true}
fullWidth={true}
id="talk-comments-stream-loadMore"
invert={true}
onClick={[Function]}
secondary={true}
>
Load More
</withPropsOnChange(Button)>
</Localized>
</Flex>
</div>
`;
exports[`when there is more renders a load more button 1`] = `
<div
className="Stream-root"
>
<Logo
gutterBottom={true}
/>
<withContext(createMutationContainer(PostCommentFormContainer))
assetID="asset-id"
/>
<Flex
aria-live="polite"
direction="column"
id="talk-comments-stream-log"
itemGutter={true}
role="log"
>
<Flex
direction="column"
itemGutter={true}
key="comment-1"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-1",
}
}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-1",
}
}
/>
</Flex>
<Flex
direction="column"
itemGutter={true}
key="comment-2"
>
<Relay(CommentContainer)
data={
Object {
"id": "comment-2",
}
}
/>
<Relay(ReplyListContainer)
comment={
Object {
"id": "comment-2",
}
}
/>
</Flex>
<Localized
id="comments-stream-loadMore"
>
<withPropsOnChange(Button)
aria-controls="talk-comments-stream-log"
disabled={false}
fullWidth={true}
id="talk-comments-stream-loadMore"
invert={true}
onClick={[Function]}
secondary={true}
>
Load More
</withPropsOnChange(Button)>
</Localized>
</Flex>
</div>
`;
@@ -0,0 +1,16 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import { AppContainer } from "./AppContainer";
it("renders correctly", () => {
const props: PropTypesOf<typeof AppContainer> = {
data: {
asset: {},
},
};
const wrapper = shallow(<AppContainer {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -11,30 +11,20 @@ interface InnerProps {
data: Data;
}
const AppContainer: StatelessComponent<InnerProps> = props => {
export const AppContainer: StatelessComponent<InnerProps> = props => {
return <App {...props.data} />;
};
const enhanced = withFragmentContainer<{ data: Data }>(
graphql`
const enhanced = withFragmentContainer<{ data: Data }>({
data: graphql`
fragment AppContainer on Query
@argumentDefinitions(
assetID: { type: "ID!" }
showAssetList: { type: "Boolean!" }
) {
assets @include(if: $showAssetList) {
...AssetListContainer_assets
}
asset(id: $assetID) @skip(if: $showAssetList) {
id
isClosed
comments {
...StreamContainer_comments
}
@argumentDefinitions(assetID: { type: "ID!" }) {
asset(id: $assetID) {
...StreamContainer_asset
}
}
`
)(AppContainer);
`,
})(AppContainer);
export type AppContainerProps = PropTypesOf<typeof enhanced>;
export default enhanced;
@@ -1,33 +0,0 @@
import React, { StatelessComponent } from "react";
import { graphql } from "react-relay";
import { withFragmentContainer } from "talk-framework/lib/relay";
import { PropTypesOf } from "talk-framework/types";
import { AssetListContainer_assets as Data } from "talk-stream/__generated__/AssetListContainer_assets.graphql";
import AssetList from "../components/AssetList";
interface InnerProps {
assets: Data;
}
const AssetListContainer: StatelessComponent<InnerProps> = props => {
const assets = props.assets.edges.map(edge => edge.node);
return <AssetList assets={assets} />;
};
const enhanced = withFragmentContainer<{ assets: Data }>(
graphql`
fragment AssetListContainer_assets on AssetsConnection {
edges {
node {
id
title
}
}
}
`
)(AssetListContainer);
export type AssetListContainerProps = PropTypesOf<typeof enhanced>;
export default enhanced;
@@ -0,0 +1,21 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import { CommentContainer } from "./CommentContainer";
it("renders username and body", () => {
const props: PropTypesOf<typeof CommentContainer> = {
data: {
author: {
username: "Marvin",
},
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
},
};
const wrapper = shallow(<CommentContainer {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -2,28 +2,39 @@ import React, { StatelessComponent } from "react";
import { graphql } from "react-relay";
import withFragmentContainer from "talk-framework/lib/relay/withFragmentContainer";
import { Omit, PropTypesOf } from "talk-framework/types";
import { PropTypesOf } from "talk-framework/types";
import { CommentContainer as Data } from "talk-stream/__generated__/CommentContainer.graphql";
import Comment, { CommentProps } from "../components/Comment";
import Comment from "../components/Comment";
type InnerProps = { data: Data } & Omit<CommentProps, keyof Data>;
interface InnerProps {
data: Data;
}
const CommentContainer: StatelessComponent<InnerProps> = props => {
// tslint:disable-next-line:no-unused-expression
graphql`
fragment CommentContainer_comment on Comment {
id
author {
username
}
body
createdAt
}
`;
export const CommentContainer: StatelessComponent<InnerProps> = props => {
const { data, ...rest } = props;
return <Comment {...rest} {...props.data} />;
};
const enhanced = withFragmentContainer<{ data: Data }>(
graphql`
const enhanced = withFragmentContainer<{ data: Data }>({
data: graphql`
fragment CommentContainer on Comment {
author {
username
}
body
...CommentContainer_comment @relay(mask: false)
}
`
)(CommentContainer);
`,
})(CommentContainer);
export type CommentContainerProps = PropTypesOf<typeof enhanced>;
export default enhanced;
@@ -25,6 +25,8 @@ class PostCommentFormContainer extends Component<InnerProps> {
if (error instanceof BadUserInputError) {
return error.invalidArgsLocalized;
}
// tslint:disable-next-line:no-console
console.error(error);
}
return undefined;
};
@@ -0,0 +1,86 @@
import { shallow, ShallowWrapper } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import ReplyList from "../components/ReplyList";
import { ReplyListContainer } from "./ReplyListContainer";
it("renders correctly", () => {
const props: PropTypesOf<typeof ReplyListContainer> = {
comment: {
id: "comment-id",
replies: {
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
},
},
relay: {
hasMore: noop,
isLoading: noop,
} as any,
};
const wrapper = shallow(<ReplyListContainer {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders correctly when replies are null", () => {
const props: PropTypesOf<typeof ReplyListContainer> = {
comment: {
id: "comment-id",
replies: null,
},
relay: {
hasMore: noop,
isLoading: noop,
} as any,
};
const wrapper = shallow(<ReplyListContainer {...props} />);
expect(wrapper).toMatchSnapshot();
});
describe("when has more replies", () => {
let finishLoading: ((error?: Error) => void) | null = null;
const props: PropTypesOf<ReplyListContainer> = {
comment: {
id: "comment-id",
replies: {
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
},
},
relay: {
hasMore: () => true,
isLoading: () => false,
loadMore: (_: any, callback: () => void) => (finishLoading = callback),
} as any,
};
let wrapper: ShallowWrapper;
beforeAll(() => (wrapper = shallow(<ReplyListContainer {...props} />)));
it("renders hasMore", () => {
expect(wrapper).toMatchSnapshot();
});
describe("when showing all", () => {
beforeAll(() => {
wrapper
.find(ReplyList)
.props()
.onShowAll();
});
it("calls relay loadMore", () => {
expect(finishLoading).not.toBeNull();
});
it("disables show all button", () => {
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
it("enable show all button after loading is done", () => {
finishLoading!();
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
});
});
@@ -0,0 +1,135 @@
import React from "react";
import { graphql, RelayPaginationProp } from "react-relay";
import { withPaginationContainer } from "talk-framework/lib/relay";
import { PropTypesOf } from "talk-framework/types";
import { ReplyListContainer_comment as Data } from "talk-stream/__generated__/ReplyListContainer_comment.graphql";
import {
COMMENT_SORT,
ReplyListContainerPaginationQueryVariables,
} from "talk-stream/__generated__/ReplyListContainerPaginationQuery.graphql";
import ReplyList from "../components/ReplyList";
export interface InnerProps {
comment: Data;
relay: RelayPaginationProp;
}
export class ReplyListContainer extends React.Component<InnerProps> {
public state = {
disableShowAll: false,
};
public render() {
if (
this.props.comment.replies === null ||
this.props.comment.replies.edges.length === 0
) {
return null;
}
const comments = this.props.comment.replies.edges.map(edge => edge.node);
return (
<ReplyList
commentID={this.props.comment.id}
comments={comments}
onShowAll={this.showAll}
hasMore={this.props.relay.hasMore()}
disableShowAll={this.state.disableShowAll}
/>
);
}
private showAll = () => {
if (!this.props.relay.hasMore() || this.props.relay.isLoading()) {
return;
}
this.setState({ disableShowAll: true });
this.props.relay.loadMore(
999999999, // Fetch All Replies
error => {
this.setState({ disableShowAll: false });
if (error) {
// tslint:disable-next-line:no-console
console.error(error);
}
}
);
};
}
// TODO: (cvle) This should be autogenerated.
interface FragmentVariables {
count: number;
cursor?: string;
orderBy: COMMENT_SORT;
}
const enhanced = withPaginationContainer<
{ comment: Data },
InnerProps,
FragmentVariables,
ReplyListContainerPaginationQueryVariables
>(
{
comment: graphql`
fragment ReplyListContainer_comment on Comment
@argumentDefinitions(
count: { type: "Int!", defaultValue: 5 }
cursor: { type: "Cursor" }
orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC }
) {
id
replies(first: $count, after: $cursor, orderBy: $orderBy)
@connection(key: "ReplyList_replies") {
edges {
node {
id
...CommentContainer
}
}
}
}
`,
},
{
direction: "forward",
getConnectionFromProps(props) {
return props.comment && props.comment.replies;
},
// This is also the default implementation of `getFragmentVariables` if it isn't provided.
getFragmentVariables(prevVars, totalCount) {
return {
...prevVars,
count: totalCount,
};
},
getVariables(props, { count, cursor }, fragmentVariables) {
return {
count,
cursor,
orderBy: fragmentVariables.orderBy,
commentID: props.comment.id,
};
},
query: graphql`
# Pagination query to be fetched upon calling 'loadMore'.
# Notice that we re-use our fragment, and the shape of this query matches our fragment spec.
query ReplyListContainerPaginationQuery(
$count: Int!
$cursor: Cursor
$orderBy: COMMENT_SORT!
$commentID: ID!
) {
comment(id: $commentID) {
...ReplyListContainer_comment
@arguments(count: $count, cursor: $cursor, orderBy: $orderBy)
}
}
`,
}
)(ReplyListContainer);
export type ReplyListContainerProps = PropTypesOf<typeof enhanced>;
export default enhanced;
@@ -0,0 +1,73 @@
import { shallow, ShallowWrapper } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Stream from "../components/Stream";
import { StreamContainer } from "./StreamContainer";
it("renders correctly", () => {
const props: PropTypesOf<StreamContainer> = {
asset: {
id: "asset-id",
isClosed: false,
comments: {
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
},
},
relay: {
hasMore: noop,
isLoading: noop,
} as any,
};
const wrapper = shallow(<StreamContainer {...props} />);
expect(wrapper).toMatchSnapshot();
});
describe("when has more comments", () => {
let finishLoading: ((error?: Error) => void) | null = null;
const props: PropTypesOf<StreamContainer> = {
asset: {
id: "asset-id",
isClosed: false,
comments: {
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
},
},
relay: {
hasMore: () => true,
isLoading: () => false,
loadMore: (_: any, callback: () => void) => (finishLoading = callback),
} as any,
};
let wrapper: ShallowWrapper;
beforeAll(() => (wrapper = shallow(<StreamContainer {...props} />)));
it("renders hasMore", () => {
expect(wrapper).toMatchSnapshot();
});
describe("when loading more", () => {
beforeAll(() => {
wrapper
.find(Stream)
.props()
.onLoadMore();
});
it("calls relay loadMore", () => {
expect(finishLoading).not.toBeNull();
});
it("disables load more button", () => {
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
it("enable load more button after loading is done", () => {
finishLoading!();
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
});
});
@@ -1,32 +1,132 @@
import React, { StatelessComponent } from "react";
import { graphql } from "react-relay";
import React from "react";
import { graphql, RelayPaginationProp } from "react-relay";
import { withFragmentContainer } from "talk-framework/lib/relay";
import { withPaginationContainer } from "talk-framework/lib/relay";
import { PropTypesOf } from "talk-framework/types";
import { StreamContainer_comments as Data } from "talk-stream/__generated__/StreamContainer_comments.graphql";
import { StreamContainer_asset as Data } from "talk-stream/__generated__/StreamContainer_asset.graphql";
import {
COMMENT_SORT,
StreamContainerPaginationQueryVariables,
} from "talk-stream/__generated__/StreamContainerPaginationQuery.graphql";
import Stream from "../components/Stream";
interface InnerProps {
comments: Data;
asset: Data;
relay: RelayPaginationProp;
}
const StreamContainer: StatelessComponent<InnerProps> = props => {
const comments = props.comments.edges.map(edge => edge.node);
return <Stream comments={comments} />;
};
export class StreamContainer extends React.Component<InnerProps> {
public state = {
disableLoadMore: false,
};
const enhanced = withFragmentContainer<{ comments: Data }>(
graphql`
fragment StreamContainer_comments on CommentsConnection {
edges {
node {
id
...CommentContainer
public render() {
const comments = this.props.asset.comments.edges.map(edge => edge.node);
return (
<Stream
assetID={this.props.asset.id}
isClosed={this.props.asset.isClosed}
comments={comments}
onLoadMore={this.loadMore}
hasMore={this.props.relay.hasMore()}
disableLoadMore={this.state.disableLoadMore}
/>
);
}
private loadMore = () => {
if (!this.props.relay.hasMore() || this.props.relay.isLoading()) {
return;
}
this.setState({ disableLoadMore: true });
this.props.relay.loadMore(
10, // Fetch the next 10 feed items
error => {
this.setState({ disableLoadMore: false });
if (error) {
// tslint:disable-next-line:no-console
console.error(error);
}
}
}
`
);
};
}
// TODO: (cvle) This should be autogenerated.
interface FragmentVariables {
count: number;
cursor?: string;
orderBy: COMMENT_SORT;
}
const enhanced = withPaginationContainer<
{ asset: Data },
InnerProps,
FragmentVariables,
StreamContainerPaginationQueryVariables
>(
{
asset: graphql`
fragment StreamContainer_asset on Asset
@argumentDefinitions(
count: { type: "Int!", defaultValue: 5 }
cursor: { type: "Cursor" }
orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_DESC }
) {
id
isClosed
comments(first: $count, after: $cursor, orderBy: $orderBy)
@connection(key: "Stream_comments") {
edges {
node {
id
...CommentContainer
...ReplyListContainer_comment
}
}
}
}
`,
},
{
direction: "forward",
getConnectionFromProps(props) {
return props.asset && props.asset.comments;
},
// This is also the default implementation of `getFragmentVariables` if it isn't provided.
getFragmentVariables(prevVars, totalCount) {
return {
...prevVars,
count: totalCount,
};
},
getVariables(props, { count, cursor }, fragmentVariables) {
return {
count,
cursor,
orderBy: fragmentVariables.orderBy,
// assetID isn't specified as an @argument for the fragment, but it should be a
// variable available for the fragment under the query root.
assetID: props.asset.id,
};
},
query: graphql`
# Pagination query to be fetched upon calling 'loadMore'.
# Notice that we re-use our fragment, and the shape of this query matches our fragment spec.
query StreamContainerPaginationQuery(
$count: Int!
$cursor: Cursor
$orderBy: COMMENT_SORT!
$assetID: ID!
) {
asset(id: $assetID) {
...StreamContainer_asset
@arguments(count: $count, cursor: $cursor, orderBy: $orderBy)
}
}
`,
}
)(StreamContainer);
export type StreamContainerProps = PropTypesOf<typeof enhanced>;
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<App
asset={Object {}}
/>
`;
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders username and body 1`] = `
<Comment
author={
Object {
"username": "Marvin",
}
}
body="Woof"
createdAt="1995-12-17T03:24:00.000Z"
/>
`;
@@ -0,0 +1,78 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<ReplyList
commentID="comment-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableShowAll={false}
onShowAll={[Function]}
/>
`;
exports[`renders correctly when replies are null 1`] = `""`;
exports[`when has more replies renders hasMore 1`] = `
<ReplyList
commentID="comment-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableShowAll={false}
hasMore={true}
onShowAll={[Function]}
/>
`;
exports[`when has more replies when showing all disables show all button 1`] = `
<ReplyList
commentID="comment-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableShowAll={true}
hasMore={true}
onShowAll={[Function]}
/>
`;
exports[`when has more replies when showing all enable show all button after loading is done 1`] = `
<ReplyList
commentID="comment-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableShowAll={false}
hasMore={true}
onShowAll={[Function]}
/>
`;
@@ -0,0 +1,80 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Stream
assetID="asset-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
isClosed={false}
onLoadMore={[Function]}
/>
`;
exports[`when has more comments renders hasMore 1`] = `
<Stream
assetID="asset-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
hasMore={true}
isClosed={false}
onLoadMore={[Function]}
/>
`;
exports[`when has more comments when loading more disables load more button 1`] = `
<Stream
assetID="asset-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={true}
hasMore={true}
isClosed={false}
onLoadMore={[Function]}
/>
`;
exports[`when has more comments when loading more enable load more button after loading is done 1`] = `
<Stream
assetID="asset-id"
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
hasMore={true}
isClosed={false}
onLoadMore={[Function]}
/>
`;
@@ -0,0 +1,31 @@
import { shallow } from "enzyme";
import React from "react";
import { render } from "./AppQuery";
it("renders app", () => {
const data = {
props: {} as any,
error: null,
};
const wrapper = shallow(React.createElement(() => render(data)));
expect(wrapper).toMatchSnapshot();
});
it("renders loading", () => {
const data = {
props: null,
error: null,
};
const wrapper = shallow(React.createElement(() => render(data)));
expect(wrapper).toMatchSnapshot();
});
it("renders error", () => {
const data = {
props: null,
error: new Error("error"),
};
const wrapper = shallow(React.createElement(() => render(data)));
expect(wrapper).toMatchSnapshot();
});
+5 -9
View File
@@ -1,10 +1,10 @@
import * as React from "react";
import { StatelessComponent } from "react";
import { ReadyState } from "react-relay";
import {
graphql,
QueryRenderer,
ReadyState,
withLocalStateContainer,
} from "talk-framework/lib/relay";
import {
@@ -15,7 +15,7 @@ import { AppQueryLocal as Local } from "talk-stream/__generated__/AppQueryLocal.
import AppContainer from "../containers/AppContainer";
const render = ({ error, props }: ReadyState<AppQueryResponse>) => {
export const render = ({ error, props }: ReadyState<AppQueryResponse>) => {
if (error) {
return <div>{error.message}</div>;
}
@@ -33,16 +33,12 @@ const AppQuery: StatelessComponent<InnerProps> = props => {
return (
<QueryRenderer<AppQueryVariables, AppQueryResponse>
query={graphql`
query AppQuery($showAssetList: Boolean!, $assetID: ID!) {
...AppContainer
@arguments(showAssetList: $showAssetList, assetID: $assetID)
query AppQuery($assetID: ID!) {
...AppContainer @arguments(assetID: $assetID)
}
`}
variables={{
// We cast `null` to any due to restrictions of the current graphql syntax.
assetID: props.local.assetID || (null as any),
// TODO: This is set to false, as server does not support querying assets yet.
showAssetList: !props.local.assetID && false,
assetID: props.local.assetID,
}}
render={render}
/>
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders app 1`] = `
<Relay(AppContainer)
data={Object {}}
/>
`;
exports[`renders error 1`] = `
<div>
error
</div>
`;
exports[`renders loading 1`] = `
<div>
Loading
</div>
`;
@@ -0,0 +1,256 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loads more comments 1`] = `
<div
className="Flex-root App-root Flex-justifyCenter"
>
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div>
<textarea
className="PostCommentForm-textarea"
name="body"
onChange={[Function]}
value=""
/>
</div>
<div
className="PostCommentForm-postButtonContainer"
>
<button
className="BaseButton-root Button-root Button-primary"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
>
Post
</button>
</div>
</form>
<div
aria-live="polite"
className="Flex-root Flex-itemGutter Flex-directionColumn"
id="talk-comments-stream-log"
role="log"
>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
</div>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Lukas
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:20:00.000Z"
title="2018-07-06T18:20:00.000Z"
>
2018-07-06T18:20:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
</div>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Isabelle
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:14:00.000Z"
title="2018-07-06T18:14:00.000Z"
>
2018-07-06T18:14:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Hey!
</p>
</div>
</div>
</div>
</div>
</div>
`;
exports[`renders comment stream 1`] = `
<div
className="Flex-root App-root Flex-justifyCenter"
>
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div>
<textarea
className="PostCommentForm-textarea"
name="body"
onChange={[Function]}
value=""
/>
</div>
<div
className="PostCommentForm-postButtonContainer"
>
<button
className="BaseButton-root Button-root Button-primary"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
>
Post
</button>
</div>
</form>
<div
aria-live="polite"
className="Flex-root Flex-itemGutter Flex-directionColumn"
id="talk-comments-stream-log"
role="log"
>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
</div>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Lukas
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:20:00.000Z"
title="2018-07-06T18:20:00.000Z"
>
2018-07-06T18:20:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
</div>
<button
aria-controls="talk-comments-stream-log"
className="BaseButton-root Button-root Button-invert Button-fullWidth Button-secondary"
disabled={false}
id="talk-comments-stream-loadMore"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
>
Load More
</button>
</div>
</div>
</div>
`;
@@ -0,0 +1,168 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders comment stream 1`] = `
<div
className="Flex-root App-root Flex-justifyCenter"
>
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div>
<textarea
className="PostCommentForm-textarea"
name="body"
onChange={[Function]}
value=""
/>
</div>
<div
className="PostCommentForm-postButtonContainer"
>
<button
className="BaseButton-root Button-root Button-primary"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
>
Post
</button>
</div>
</form>
<div
aria-live="polite"
className="Flex-root Flex-itemGutter Flex-directionColumn"
id="talk-comments-stream-log"
role="log"
>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
</div>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
I like yoghurt
</p>
</div>
<div
className="Indent-root Indent-level0"
>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
id="talk-comments-replyList-log--comment-with-replies"
role="log"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Lukas
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:20:00.000Z"
title="2018-07-06T18:20:00.000Z"
>
2018-07-06T18:20:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,108 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders comment stream 1`] = `
<div
className="Flex-root App-root Flex-justifyCenter"
>
<div
className="Stream-root"
>
<h1
className="Typography-root Typography-heading1 Typography-gutterBottom"
>
Talk NEO
</h1>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div>
<textarea
className="PostCommentForm-textarea"
name="body"
onChange={[Function]}
value=""
/>
</div>
<div
className="PostCommentForm-postButtonContainer"
>
<button
className="BaseButton-root Button-root Button-primary"
disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
>
Post
</button>
</div>
</form>
<div
aria-live="polite"
className="Flex-root Flex-itemGutter Flex-directionColumn"
id="talk-comments-stream-log"
role="log"
>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Markus
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
Joining Too
</p>
</div>
</div>
<div
className="Flex-root Flex-itemGutter Flex-directionColumn"
>
<div
role="article"
>
<div
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Username-root"
>
Lukas
</span>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:20:00.000Z"
title="2018-07-06T18:20:00.000Z"
>
2018-07-06T18:20:00.000Z
</time>
</div>
<p
className="Typography-root Typography-body1"
>
What's up?
</p>
</div>
</div>
</div>
</div>
</div>
`;

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