mirror of
https://github.com/wassname/talk.git
synced 2026-07-09 18:04:44 +08:00
feat: added support for --only flag
Merge pull request #1731 from coralproject/next-watcher-flags Watcher --only Use JSDocs comments (#1727) Merge branch 'next' into prevent-compile-loop-relay Merge pull request #1726 from coralproject/prevent-compile-loop-relay [next] Adapt relay watch config [next] Remove nodemon (#1725) * Remove old nodemon configs * Remove nodemon [next] Jest implementation for React Components (#1733) * Make jest testing work with custom path and css modules * Add first test * feat: added unit tests to ci * fix: updated package-lock.json * Update cssTransform.js * Update cssTransform.js * Fix test in ci Adapt files.exclude (#1736) Permalink ui Adding Copy to clipboard functionality WIP clean request wip progress progress work in progress wip ui functionality Translations :/ wip Merge branch 'permalink' of github.com:coralproject/talk into permalink * 'permalink' of github.com:coralproject/talk: (42 commits) [next] Support server side jest testing (#1747) Update snapshots Add comments Remove precss Move react-responsive to dev deps Remove comment Mobile first approach Support standard css variables, dynamically set spacing-unit Add docs Fully implement Flex and MatchMedia Responsive Components <3 fix: linting fix: adjusted pageInfo Remove obsoloe snapshot Move jsdom to dev deps Mark comments as always returning a value Add comment Fix unit tests Translate, concept for translation and id strings Add aria props ... Adding Attachment and Popover Component merge conflicts progress Any Working Support for refs Ready
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
Vendored
+3
-5
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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
@@ -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",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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}`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"exec": "npm-run-all compile:relay-stream",
|
||||
"ext": "ts,tsx,graphql",
|
||||
"watch": [
|
||||
"./src/core/client/stream",
|
||||
"./src/core/**/*.graphql"
|
||||
]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"exec": "npm run start:development",
|
||||
"ext": "ts,graphql",
|
||||
"watch": [
|
||||
"./src"
|
||||
],
|
||||
"ignore": [
|
||||
"./src/client"
|
||||
]
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
|
||||
@@ -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%",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"module": "es2015"
|
||||
}
|
||||
}
|
||||
+23
-2
@@ -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"],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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]",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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}`,
|
||||
},
|
||||
|
||||
@@ -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$/,
|
||||
|
||||
Generated
+3883
-1539
File diff suppressed because it is too large
Load Diff
+62
-34
@@ -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
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,8 @@ module.exports = {
|
||||
production: {
|
||||
plugins: [],
|
||||
},
|
||||
test: {
|
||||
plugins: ["@babel/transform-modules-commonjs"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { LocalizationProvider } from "fluent-react/compat";
|
||||
import { MessageContext } from "fluent/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { Formatter } from "react-timeago";
|
||||
import { Environment } from "relay-runtime";
|
||||
import { UIContext } from "talk-ui/components";
|
||||
|
||||
export interface TalkContext {
|
||||
// relayEnvironment for our relay framework.
|
||||
@@ -9,6 +11,9 @@ export interface TalkContext {
|
||||
|
||||
// localMessages for our i18n framework.
|
||||
localeMessages: MessageContext[];
|
||||
|
||||
// formatter for timeago.
|
||||
timeagoFormatter?: Formatter;
|
||||
}
|
||||
|
||||
const { Provider, Consumer } = React.createContext<TalkContext>({} as any);
|
||||
@@ -27,7 +32,9 @@ export const TalkContextProvider: StatelessComponent<{
|
||||
}> = ({ value, children }) => (
|
||||
<Provider value={value}>
|
||||
<LocalizationProvider messages={value.localeMessages}>
|
||||
{children}
|
||||
<UIContext.Provider value={{ timeagoFormatter: value.timeagoFormatter }}>
|
||||
{children}
|
||||
</UIContext.Provider>
|
||||
</LocalizationProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
+23
@@ -1,4 +1,7 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
import { Formatter } from "react-timeago";
|
||||
import { Environment, Network, RecordSource, Store } from "relay-runtime";
|
||||
|
||||
import { generateMessages, LocalesData, negotiateLanguages } from "../i18n";
|
||||
@@ -16,6 +19,25 @@ interface CreateContextArguments {
|
||||
init?: ((context: TalkContext) => void | Promise<void>);
|
||||
}
|
||||
|
||||
/**
|
||||
* timeagoFormatter integrates timeago into our translation
|
||||
* framework. It gets injected into the UIContext.
|
||||
*/
|
||||
export const timeagoFormatter: Formatter = (value, unit, suffix) => {
|
||||
// We use 'in' instead of 'from now' for language consistency
|
||||
const ourSuffix = suffix === "from now" ? "in" : suffix;
|
||||
return (
|
||||
<Localized
|
||||
id="framework-timeago"
|
||||
$value={value}
|
||||
$unit={unit}
|
||||
$suffix={ourSuffix}
|
||||
>
|
||||
<span>now</span>
|
||||
</Localized>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* `createContext` manages the dependencies of our framework
|
||||
* and returns a `TalkContext` that can be passed to the
|
||||
@@ -46,6 +68,7 @@ export default async function createContext({
|
||||
const context = {
|
||||
relayEnvironment,
|
||||
localeMessages,
|
||||
timeagoFormatter,
|
||||
};
|
||||
|
||||
// Run custom initializations.
|
||||
@@ -1,5 +1,9 @@
|
||||
import * as React from "react";
|
||||
import { hoistStatics, InferableComponentEnhancer } from "recompose";
|
||||
import {
|
||||
hoistStatics,
|
||||
InferableComponentEnhancer,
|
||||
wrapDisplayName,
|
||||
} from "recompose";
|
||||
|
||||
import { TalkContext, TalkContextConsumer } from "./TalkContext";
|
||||
|
||||
@@ -12,11 +16,17 @@ function withContext<T>(
|
||||
propsCallback: (context: TalkContext) => T
|
||||
): InferableComponentEnhancer<T> {
|
||||
return hoistStatics<T>(
|
||||
<U extends T>(WrappedComponent: React.ComponentType<U>) => (props: any) => (
|
||||
<TalkContextConsumer>
|
||||
{context => <WrappedComponent {...props} {...propsCallback(context)} />}
|
||||
</TalkContextConsumer>
|
||||
)
|
||||
<U extends T>(WrappedComponent: React.ComponentType<U>) => {
|
||||
const Component: React.StatelessComponent<any> = props => (
|
||||
<TalkContextConsumer>
|
||||
{context => (
|
||||
<WrappedComponent {...props} {...propsCallback(context)} />
|
||||
)}
|
||||
</TalkContextConsumer>
|
||||
);
|
||||
Component.displayName = wrapDisplayName(WrappedComponent, "withContext");
|
||||
return Component;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export const VALIDATION_REQUIRED = () => (
|
||||
);
|
||||
|
||||
export const VALIDATION_TOO_SHORT = () => (
|
||||
<Localized id="framework-validation-too-short">
|
||||
<Localized id="framework-validation-tooShort">
|
||||
<span>This field is too short.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import React, { Component } from "react";
|
||||
import { QueryRenderer } from "react-relay";
|
||||
import { CacheConfig, GraphQLTaggedNode, RerunParam } from "relay-runtime";
|
||||
import {
|
||||
QueryRenderer,
|
||||
QueryRendererProps as QueryRendererPropsOrig,
|
||||
} from "react-relay";
|
||||
|
||||
import { Omit } from "talk-framework/types";
|
||||
|
||||
import { TalkContextConsumer } from "../bootstrap/TalkContext";
|
||||
|
||||
// Taken from relay types and added Generic support for Variables and Response
|
||||
export interface QueryRendererProps<V, R> {
|
||||
cacheConfig?: CacheConfig;
|
||||
query?: GraphQLTaggedNode | null;
|
||||
render(readyState: ReadyState<R>): React.ReactElement<any> | undefined | null;
|
||||
variables: V;
|
||||
rerunParamExperimental?: RerunParam;
|
||||
}
|
||||
|
||||
// Taken from relay types and added Generic support for Variables and Response
|
||||
export interface ReadyState<R> {
|
||||
error: Error | undefined | null;
|
||||
props: R | undefined | null;
|
||||
retry?(): void;
|
||||
}
|
||||
// Omit environment as we are passing this from the context.
|
||||
export type QueryRendererProps<V, R> = Omit<
|
||||
QueryRendererPropsOrig<V, R>,
|
||||
"environment"
|
||||
>;
|
||||
|
||||
/**
|
||||
* TalkQueryRenderer is a wrappper around Relay's `QueryRenderer`.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import * as React from "react";
|
||||
import { compose, hoistStatics, InferableComponentEnhancer } from "recompose";
|
||||
import {
|
||||
compose,
|
||||
hoistStatics,
|
||||
InferableComponentEnhancer,
|
||||
wrapDisplayName,
|
||||
} from "recompose";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { withContext } from "../bootstrap";
|
||||
@@ -19,6 +24,11 @@ function createMutationContainer<T extends string, I, R>(
|
||||
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
|
||||
hoistStatics((WrappedComponent: React.ComponentType<any>) => {
|
||||
class CreateMutationContainer extends React.Component<any> {
|
||||
public static displayName = wrapDisplayName(
|
||||
WrappedComponent,
|
||||
"createMutationContainer"
|
||||
);
|
||||
|
||||
private commit = (input: I) => {
|
||||
return commit(this.props.relayEnvironment, input);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ export { default as QueryRenderer } from "./QueryRenderer";
|
||||
export * from "./QueryRenderer";
|
||||
export { default as createMutationContainer } from "./createMutationContainer";
|
||||
export { default as createAndRetain } from "./createAndRetain";
|
||||
export { default as wrapFetchWithLogger } from "./wrapFetchWithLogger";
|
||||
export {
|
||||
commitMutationPromise,
|
||||
commitMutationPromiseNormalized,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { InferableComponentEnhancerWithProps } from "recompose";
|
||||
* from Relay.
|
||||
*/
|
||||
export default <T>(
|
||||
fragmentSpec: GraphQLTaggedNode
|
||||
fragmentSpec: { [P in keyof T]: GraphQLTaggedNode }
|
||||
): InferableComponentEnhancerWithProps<T, { [P in keyof T]: any }> => (
|
||||
component: React.ComponentType<any>
|
||||
) => createFragmentContainer(component, fragmentSpec) as any;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import * as React from "react";
|
||||
import { compose, hoistStatics, InferableComponentEnhancer } from "recompose";
|
||||
import { CSelector, CSnapshot, Environment } from "relay-runtime";
|
||||
import {
|
||||
CSelector,
|
||||
CSnapshot,
|
||||
Environment,
|
||||
GraphQLTaggedNode,
|
||||
} from "relay-runtime";
|
||||
|
||||
import { withContext } from "../bootstrap";
|
||||
|
||||
@@ -25,7 +30,7 @@ export const LOCAL_ID = "client:root.local";
|
||||
* must have the `LOCAL_ID`.
|
||||
*/
|
||||
function withLocalStateContainer<T>(
|
||||
fragmentSpec: any
|
||||
fragmentSpec: GraphQLTaggedNode
|
||||
): InferableComponentEnhancer<{ local: T }> {
|
||||
return compose(
|
||||
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
|
||||
@@ -33,7 +38,7 @@ function withLocalStateContainer<T>(
|
||||
class LocalStateContainer extends React.Component<Props, any> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const fragment = fragmentSpec.data().default;
|
||||
const fragment = (fragmentSpec as any).data().default;
|
||||
if (fragment.kind !== "Fragment") {
|
||||
throw new Error("Expected fragment");
|
||||
}
|
||||
|
||||
@@ -10,9 +10,13 @@ import { InferableComponentEnhancerWithProps } from "recompose";
|
||||
* withPaginationContainer is a curried version of `createPaginationContainers`
|
||||
* from Relay.
|
||||
*/
|
||||
export default <T, InnerProps>(
|
||||
fragmentSpec: GraphQLTaggedNode,
|
||||
connectionConfig: ConnectionConfig<InnerProps>
|
||||
export default <T, InnerProps, FragmentVariables, QueryVariables>(
|
||||
fragmentSpec: { [P in keyof T]: GraphQLTaggedNode },
|
||||
connectionConfig: ConnectionConfig<
|
||||
InnerProps,
|
||||
FragmentVariables,
|
||||
QueryVariables
|
||||
>
|
||||
): InferableComponentEnhancerWithProps<
|
||||
T & { relay: RelayPaginationProp },
|
||||
{ [P in keyof T]: any }
|
||||
|
||||
@@ -10,7 +10,7 @@ import { InferableComponentEnhancerWithProps } from "recompose";
|
||||
* from Relay.
|
||||
*/
|
||||
export default <T>(
|
||||
fragmentSpec: GraphQLTaggedNode,
|
||||
fragmentSpec: { [P in keyof T]: GraphQLTaggedNode },
|
||||
refetchQuery: GraphQLTaggedNode
|
||||
): InferableComponentEnhancerWithProps<
|
||||
T & { relay: RelayRefetchProp },
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { FetchFunction } from "relay-runtime";
|
||||
|
||||
/**
|
||||
* Decorates the fetch function with error logging.
|
||||
* Intended for testing purposes.
|
||||
*/
|
||||
export default function wrapFetchWithLogger(
|
||||
fetch: FetchFunction,
|
||||
logResult?: boolean
|
||||
): FetchFunction {
|
||||
return async (...args: any[]) => {
|
||||
try {
|
||||
const result = await (fetch as any)(...args);
|
||||
if (logResult) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
extends: "../.babelrc.js",
|
||||
plugins: [
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Here we add global stylings for body and document */
|
||||
:global {
|
||||
body {
|
||||
margin: "0";
|
||||
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Firefox. */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
.root {
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof App> = {
|
||||
asset: {},
|
||||
};
|
||||
const wrapper = shallow(<App {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders correctly when asset is null", () => {
|
||||
const props: PropTypesOf<typeof App> = {
|
||||
asset: null,
|
||||
};
|
||||
const wrapper = shallow(<App {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -1,33 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { Center } from "talk-ui/components";
|
||||
import { Flex } from "talk-ui/components";
|
||||
|
||||
import AssetListContainer from "../containers/AssetListContainer";
|
||||
import PostCommentFormContainer from "../containers/PostCommentFormContainer";
|
||||
import StreamContainer from "../containers/StreamContainer";
|
||||
import Logo from "./Logo";
|
||||
|
||||
import * as styles from "./App.css";
|
||||
|
||||
export interface AppProps {
|
||||
assets?: any | null;
|
||||
asset?: {
|
||||
id: string;
|
||||
isClosed: boolean;
|
||||
comments: any | null;
|
||||
} | null;
|
||||
asset: {} | null;
|
||||
}
|
||||
|
||||
const App: StatelessComponent<AppProps> = props => {
|
||||
if (props.assets) {
|
||||
return <AssetListContainer assets={props.assets} />;
|
||||
}
|
||||
if (props.asset) {
|
||||
return (
|
||||
<Center>
|
||||
<Logo gutterBottom />
|
||||
<StreamContainer comments={props.asset.comments} />
|
||||
<PostCommentFormContainer assetID={props.asset.id} />
|
||||
</Center>
|
||||
<Flex justifyContent="center" className={styles.root}>
|
||||
<StreamContainer asset={props.asset} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
return <div>Asset not found </div>;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
export interface AssetListProps {
|
||||
assets: ReadonlyArray<{ id: string; title: string | null }>;
|
||||
}
|
||||
|
||||
const AssetList: StatelessComponent<AssetListProps> = props => {
|
||||
return (
|
||||
<div>
|
||||
{props.assets.map(asset => <div key={asset.id}>{asset.title}</div>)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetList;
|
||||
@@ -1,11 +0,0 @@
|
||||
.root {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.gutterBottom {
|
||||
margin-bottom: calc(2px * $spacing-unit);
|
||||
}
|
||||
|
||||
.author {
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import cn from "classnames";
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { Typography } from "talk-ui/components";
|
||||
|
||||
import * as styles from "./Comment.css";
|
||||
|
||||
export interface CommentProps {
|
||||
className?: string;
|
||||
author: {
|
||||
username: string;
|
||||
} | null;
|
||||
body: string | null;
|
||||
gutterBottom?: boolean;
|
||||
}
|
||||
|
||||
const Comment: StatelessComponent<CommentProps> = props => {
|
||||
const rootClassName = cn(styles.root, props.className, {
|
||||
[styles.gutterBottom]: props.gutterBottom,
|
||||
});
|
||||
return (
|
||||
<div className={rootClassName}>
|
||||
<Typography className={styles.author} gutterBottom>
|
||||
{props.author && props.author.username}
|
||||
</Typography>
|
||||
<Typography>{props.body}</Typography>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Comment;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import Comment from "./Comment";
|
||||
|
||||
it("renders username and body", () => {
|
||||
const props: PropTypesOf<typeof Comment> = {
|
||||
id: "comment-id",
|
||||
author: {
|
||||
username: "Marvin",
|
||||
},
|
||||
body: "Woof",
|
||||
createdAt: "1995-12-17T03:24:00.000Z",
|
||||
};
|
||||
const wrapper = shallow(<Comment {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import { Button, Popover, Typography } from "talk-ui/components";
|
||||
import PermalinkPopover from "../PermalinkPopover";
|
||||
import Timestamp from "./Timestamp";
|
||||
import TopBar from "./TopBar";
|
||||
import Username from "./Username";
|
||||
|
||||
export interface CommentProps {
|
||||
id: string;
|
||||
className?: string;
|
||||
author: {
|
||||
username: string;
|
||||
} | null;
|
||||
body: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const Comment: StatelessComponent<CommentProps> = props => {
|
||||
return (
|
||||
<div role="article">
|
||||
<TopBar>
|
||||
{props.author && <Username>{props.author.username}</Username>}
|
||||
<Timestamp>{props.createdAt}</Timestamp>
|
||||
</TopBar>
|
||||
<Typography>{props.body}</Typography>
|
||||
<div>
|
||||
<Popover body={<PermalinkPopover commentId={props.id} />}>
|
||||
{({ toggleShow, ref }) => (
|
||||
<Button onClick={toggleShow} innerRef={ref} primary>
|
||||
<Localized id="comments-permalink-share">
|
||||
<span>Share</span>
|
||||
</Localized>
|
||||
</Button>
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Comment;
|
||||
@@ -0,0 +1,3 @@
|
||||
.root {
|
||||
composes: timestamp from "talk-ui/shared/typography.css";
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import Timestamp from "./Timestamp";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof Timestamp> = {
|
||||
children: "1995-12-17T03:24:00.000Z",
|
||||
};
|
||||
const wrapper = shallow(<Timestamp {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { RelativeTime } from "talk-ui/components";
|
||||
|
||||
import * as styles from "./Timestamp.css";
|
||||
|
||||
export interface TimestampProps {
|
||||
children: string;
|
||||
}
|
||||
|
||||
const Timestamp: StatelessComponent<TimestampProps> = props => (
|
||||
<RelativeTime className={styles.root} date={props.children} />
|
||||
);
|
||||
|
||||
export default Timestamp;
|
||||
@@ -0,0 +1,3 @@
|
||||
.root {
|
||||
margin-bottom: calc(0.5 * var(--spacing-unit));
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import TestRenderer from "react-test-renderer";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { UIContext, UIContextProps } from "talk-ui/components";
|
||||
|
||||
import TopBar from "./TopBar";
|
||||
|
||||
it("renders correctly on small screens", () => {
|
||||
const props: PropTypesOf<typeof TopBar> = {
|
||||
children: <div>Hello World</div>,
|
||||
};
|
||||
|
||||
const context: UIContextProps = {
|
||||
mediaQueryValues: {
|
||||
width: 320,
|
||||
},
|
||||
};
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<UIContext.Provider value={context}>
|
||||
<TopBar {...props} />
|
||||
</UIContext.Provider>
|
||||
);
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders correctly on big screens", () => {
|
||||
const props: PropTypesOf<typeof TopBar> = {
|
||||
children: <div>Hello World</div>,
|
||||
};
|
||||
|
||||
const context: UIContextProps = {
|
||||
mediaQueryValues: {
|
||||
width: 1600,
|
||||
},
|
||||
};
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<UIContext.Provider value={context}>
|
||||
<TopBar {...props} />
|
||||
</UIContext.Provider>
|
||||
);
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import cn from "classnames";
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { Flex, MatchMedia } from "talk-ui/components";
|
||||
|
||||
import * as styles from "./TopBar.css";
|
||||
|
||||
export interface TopBarProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const TopBar: StatelessComponent<TopBarProps> = props => {
|
||||
const rootClassName = cn(styles.root, props.className);
|
||||
return (
|
||||
<MatchMedia minWidth="xs">
|
||||
{matches => (
|
||||
<Flex
|
||||
className={rootClassName}
|
||||
alignItems="baseline"
|
||||
direction={matches ? "row" : "column"}
|
||||
itemGutter={matches ? true : "half"}
|
||||
>
|
||||
{props.children}
|
||||
</Flex>
|
||||
)}
|
||||
</MatchMedia>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopBar;
|
||||
@@ -0,0 +1,3 @@
|
||||
.root {
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import TestRenderer from "react-test-renderer";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { UIContext, UIContextProps } from "talk-ui/components";
|
||||
|
||||
import Username from "./Username";
|
||||
|
||||
it("renders correctly on small screens", () => {
|
||||
const props: PropTypesOf<typeof Username> = {
|
||||
children: "Marvin",
|
||||
};
|
||||
|
||||
const context: UIContextProps = {
|
||||
mediaQueryValues: {
|
||||
width: 320,
|
||||
},
|
||||
};
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<UIContext.Provider value={context}>
|
||||
<Username {...props} />
|
||||
</UIContext.Provider>
|
||||
);
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders correctly on big screens", () => {
|
||||
const props: PropTypesOf<typeof Username> = {
|
||||
children: "Marvin",
|
||||
};
|
||||
|
||||
const context: UIContextProps = {
|
||||
mediaQueryValues: {
|
||||
width: 1600,
|
||||
},
|
||||
};
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<UIContext.Provider value={context}>
|
||||
<Username {...props} />
|
||||
</UIContext.Provider>
|
||||
);
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { MatchMedia, Typography } from "talk-ui/components";
|
||||
|
||||
import * as styles from "./Username.css";
|
||||
|
||||
export interface UsernameProps {
|
||||
children: string;
|
||||
}
|
||||
|
||||
const Username: StatelessComponent<UsernameProps> = props => {
|
||||
return (
|
||||
<MatchMedia minWidth="xs">
|
||||
{matches => (
|
||||
<Typography
|
||||
variant={matches ? "heading2" : "heading3"}
|
||||
className={styles.root}
|
||||
component="span"
|
||||
>
|
||||
{props.children}
|
||||
</Typography>
|
||||
)}
|
||||
</MatchMedia>
|
||||
);
|
||||
};
|
||||
|
||||
export default Username;
|
||||
@@ -0,0 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders username and body 1`] = `
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<TopBar>
|
||||
<Username>
|
||||
Marvin
|
||||
</Username>
|
||||
<Timestamp>
|
||||
1995-12-17T03:24:00.000Z
|
||||
</Timestamp>
|
||||
</TopBar>
|
||||
<withPropsOnChange(Typography)>
|
||||
Woof
|
||||
</withPropsOnChange(Typography)>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,8 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<withPropsOnChange(RelativeTime)
|
||||
className="Timestamp-root"
|
||||
date="1995-12-17T03:24:00.000Z"
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly on big screens 1`] = `
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-itemGutter Flex-alignBaseline Flex-directionRow"
|
||||
>
|
||||
<div>
|
||||
Hello World
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders correctly on small screens 1`] = `
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<div>
|
||||
Hello World
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,17 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly on big screens 1`] = `
|
||||
<span
|
||||
className="Typography-root Typography-heading2 Username-root"
|
||||
>
|
||||
Marvin
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`renders correctly on small screens 1`] = `
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Username-root"
|
||||
>
|
||||
Marvin
|
||||
</span>
|
||||
`;
|
||||
@@ -0,0 +1 @@
|
||||
export { default, default as Comment, CommentProps } from "./Comment";
|
||||
@@ -0,0 +1,8 @@
|
||||
.root {
|
||||
border-left: 3px solid;
|
||||
padding-left: var(--spacing-unit);
|
||||
}
|
||||
|
||||
.level0 {
|
||||
border-color: var(--palette-secondary-darkest);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import Indent from "./Indent";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof Indent> = {
|
||||
children: <div>Hello World</div>,
|
||||
};
|
||||
const wrapper = shallow(<Indent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import cn from "classnames";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import * as styles from "./Indent.css";
|
||||
|
||||
export interface IndentProps {
|
||||
level?: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Indent: StatelessComponent<IndentProps> = props => {
|
||||
return <div className={cn(styles.root, styles.level0)}>{props.children}</div>;
|
||||
};
|
||||
|
||||
export default Indent;
|
||||
@@ -0,0 +1,13 @@
|
||||
.textField {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.root {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #c9cacb;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 1px;
|
||||
padding: 6px 10px;
|
||||
display: flex;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { CSSProperties } from "react";
|
||||
import CopyToClipboard from "react-copy-to-clipboard";
|
||||
import { RefHandler } from "react-popper";
|
||||
import { Button, TextField } from "talk-ui/components";
|
||||
import * as styles from "./PermalinkPopover.css";
|
||||
|
||||
interface InnerProps {
|
||||
commentId: string;
|
||||
style?: CSSProperties;
|
||||
innerRef?: RefHandler;
|
||||
}
|
||||
|
||||
interface State {
|
||||
copied: boolean;
|
||||
}
|
||||
|
||||
class PermalinkPopover extends React.Component<InnerProps> {
|
||||
public state: State = {
|
||||
copied: false,
|
||||
};
|
||||
|
||||
public onCopy = async () => {
|
||||
await this.toggleCopied();
|
||||
setTimeout(() => {
|
||||
this.toggleCopied();
|
||||
}, 800);
|
||||
};
|
||||
|
||||
public toggleCopied = () => {
|
||||
this.setState((state: State) => ({
|
||||
copied: !state.copied,
|
||||
}));
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { commentId, style, innerRef } = this.props;
|
||||
const { copied } = this.state;
|
||||
return (
|
||||
<div className={styles.root} style={style} ref={innerRef}>
|
||||
<TextField defaultValue={commentId} className={styles.textField} />
|
||||
<CopyToClipboard text={commentId} onCopy={this.onCopy}>
|
||||
<Button primary>
|
||||
{copied ? (
|
||||
<Localized id="comments-permalink-copied">
|
||||
<span>Copied!</span>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized id="comments-permalink-copy">
|
||||
<span>Copy</span>
|
||||
</Localized>
|
||||
)}
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PermalinkPopover;
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
display: block;
|
||||
height: 100px;
|
||||
width: 400px;
|
||||
margin-bottom: calc(2px * $spacing-unit);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: var(--spacing-unit);
|
||||
}
|
||||
|
||||
.postButton {
|
||||
float: right;
|
||||
.postButtonContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@@ -39,11 +39,13 @@ const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<Localized id="postCommentForm-submit">
|
||||
<Button className={styles.postButton} disabled={submitting} primary>
|
||||
Post
|
||||
</Button>
|
||||
</Localized>
|
||||
<div className={styles.postButtonContainer}>
|
||||
<Localized id="comments-postCommentForm-post">
|
||||
<Button disabled={submitting} primary>
|
||||
Post
|
||||
</Button>
|
||||
</Localized>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { shallow } from "enzyme";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
import sinon, { SinonSpy } from "sinon";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import ReplyList from "./ReplyList";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof ReplyList> = {
|
||||
commentID: "comment-id",
|
||||
comments: [{ id: "comment-1" }, { id: "comment-2" }],
|
||||
onShowAll: noop,
|
||||
hasMore: false,
|
||||
disableShowAll: false,
|
||||
};
|
||||
const wrapper = shallow(<ReplyList {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when there is more", () => {
|
||||
const props: PropTypesOf<typeof ReplyList> = {
|
||||
commentID: "comment-id",
|
||||
comments: [{ id: "comment-1" }, { id: "comment-2" }],
|
||||
onShowAll: sinon.spy(),
|
||||
hasMore: true,
|
||||
disableShowAll: false,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<ReplyList {...props} />);
|
||||
it("renders a load more button", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls onLoadMore", () => {
|
||||
wrapper
|
||||
.find("#talk-comments-replyList-showAll--comment-id")
|
||||
.simulate("click");
|
||||
expect((props.onShowAll as SinonSpy).calledOnce).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { Button, Flex } from "talk-ui/components";
|
||||
|
||||
import CommentContainer from "../containers/CommentContainer";
|
||||
import Indent from "./Indent";
|
||||
|
||||
export interface ReplyListProps {
|
||||
commentID: string;
|
||||
comments: ReadonlyArray<{ id: string }>;
|
||||
onShowAll: () => void;
|
||||
hasMore: boolean;
|
||||
disableShowAll: boolean;
|
||||
}
|
||||
|
||||
const ReplyList: StatelessComponent<ReplyListProps> = props => {
|
||||
return (
|
||||
<Indent>
|
||||
<Flex
|
||||
direction="column"
|
||||
id={`talk-comments-replyList-log--${props.commentID}`}
|
||||
role="log"
|
||||
itemGutter
|
||||
>
|
||||
{props.comments.map(comment => (
|
||||
<CommentContainer key={comment.id} data={comment} />
|
||||
))}
|
||||
{props.hasMore && (
|
||||
<Localized id="comments-replyList-showAll">
|
||||
<Button
|
||||
id={`talk-comments-replyList-showAll--${props.commentID}`}
|
||||
aria-controls={`talk-comments-replyList-log--${props.commentID}`}
|
||||
onClick={props.onShowAll}
|
||||
disabled={props.disableShowAll}
|
||||
secondary
|
||||
invert
|
||||
fullWidth
|
||||
>
|
||||
Show All Replies
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
</Flex>
|
||||
</Indent>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReplyList;
|
||||
@@ -0,0 +1,4 @@
|
||||
.root {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { shallow } from "enzyme";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
import sinon, { SinonSpy } from "sinon";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import Stream from "./Stream";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof Stream> = {
|
||||
assetID: "asset-id",
|
||||
isClosed: false,
|
||||
comments: [{ id: "comment-1" }, { id: "comment-2" }],
|
||||
onLoadMore: noop,
|
||||
disableLoadMore: false,
|
||||
hasMore: false,
|
||||
};
|
||||
const wrapper = shallow(<Stream {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when there is more", () => {
|
||||
const props: PropTypesOf<typeof Stream> = {
|
||||
assetID: "asset-id",
|
||||
isClosed: false,
|
||||
comments: [{ id: "comment-1" }, { id: "comment-2" }],
|
||||
onLoadMore: sinon.spy(),
|
||||
disableLoadMore: false,
|
||||
hasMore: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<Stream {...props} />);
|
||||
it("renders a load more button", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls onLoadMore", () => {
|
||||
wrapper.find("#talk-comments-stream-loadMore").simulate("click");
|
||||
expect((props.onLoadMore as SinonSpy).calledOnce).toBe(true);
|
||||
});
|
||||
|
||||
const wrapperDisabledButton = shallow(<Stream {...props} disableLoadMore />);
|
||||
it("disables load more button", () => {
|
||||
expect(wrapperDisabledButton).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,58 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { Button, Flex } from "talk-ui/components";
|
||||
|
||||
import CommentContainer from "../containers/CommentContainer";
|
||||
import PostCommentFormContainer from "../containers/PostCommentFormContainer";
|
||||
import ReplyListContainer from "../containers/ReplyListContainer";
|
||||
import Logo from "./Logo";
|
||||
import * as styles from "./Stream.css";
|
||||
|
||||
export interface StreamProps {
|
||||
assetID: string;
|
||||
isClosed: boolean;
|
||||
comments: ReadonlyArray<{ id: string }>;
|
||||
onLoadMore: () => void;
|
||||
hasMore: boolean;
|
||||
disableLoadMore: boolean;
|
||||
}
|
||||
|
||||
const Stream: StatelessComponent<StreamProps> = props => {
|
||||
return (
|
||||
<div>
|
||||
{props.comments.map(comment => (
|
||||
<CommentContainer key={comment.id} data={comment} gutterBottom />
|
||||
))}
|
||||
<div className={styles.root}>
|
||||
<Logo gutterBottom />
|
||||
<PostCommentFormContainer assetID={props.assetID} />
|
||||
<Flex
|
||||
direction="column"
|
||||
id="talk-comments-stream-log"
|
||||
role="log"
|
||||
aria-live="polite"
|
||||
itemGutter
|
||||
>
|
||||
{props.comments.map(comment => (
|
||||
<Flex direction="column" key={comment.id} itemGutter>
|
||||
<CommentContainer data={comment} />
|
||||
<ReplyListContainer comment={comment} />
|
||||
</Flex>
|
||||
))}
|
||||
{props.hasMore && (
|
||||
<Localized id="comments-stream-loadMore">
|
||||
<Button
|
||||
id={"talk-comments-stream-loadMore"}
|
||||
onClick={props.onLoadMore}
|
||||
secondary
|
||||
invert
|
||||
fullWidth
|
||||
disabled={props.disableLoadMore}
|
||||
aria-controls="talk-comments-stream-log"
|
||||
>
|
||||
Load More
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<Flex
|
||||
className="App-root"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Relay(StreamContainer)
|
||||
asset={Object {}}
|
||||
/>
|
||||
</Flex>
|
||||
`;
|
||||
|
||||
exports[`renders correctly when asset is null 1`] = `
|
||||
<div>
|
||||
Asset not found
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<div
|
||||
className="Indent-root Indent-level0"
|
||||
>
|
||||
<div>
|
||||
Hello World
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,72 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<Indent>
|
||||
<Flex
|
||||
direction="column"
|
||||
id="talk-comments-replyList-log--comment-id"
|
||||
itemGutter={true}
|
||||
role="log"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
key="comment-1"
|
||||
/>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
key="comment-2"
|
||||
/>
|
||||
</Flex>
|
||||
</Indent>
|
||||
`;
|
||||
|
||||
exports[`when there is more renders a load more button 1`] = `
|
||||
<Indent>
|
||||
<Flex
|
||||
direction="column"
|
||||
id="talk-comments-replyList-log--comment-id"
|
||||
itemGutter={true}
|
||||
role="log"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
key="comment-1"
|
||||
/>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
key="comment-2"
|
||||
/>
|
||||
<Localized
|
||||
id="comments-replyList-showAll"
|
||||
>
|
||||
<withPropsOnChange(Button)
|
||||
aria-controls="talk-comments-replyList-log--comment-id"
|
||||
disabled={false}
|
||||
fullWidth={true}
|
||||
id="talk-comments-replyList-showAll--comment-id"
|
||||
invert={true}
|
||||
onClick={[Function]}
|
||||
secondary={true}
|
||||
>
|
||||
Show All Replies
|
||||
</withPropsOnChange(Button)>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</Indent>
|
||||
`;
|
||||
@@ -0,0 +1,214 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<Logo
|
||||
gutterBottom={true}
|
||||
/>
|
||||
<withContext(createMutationContainer(PostCommentFormContainer))
|
||||
assetID="asset-id"
|
||||
/>
|
||||
<Flex
|
||||
aria-live="polite"
|
||||
direction="column"
|
||||
id="talk-comments-stream-log"
|
||||
itemGutter={true}
|
||||
role="log"
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
itemGutter={true}
|
||||
key="comment-1"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
direction="column"
|
||||
itemGutter={true}
|
||||
key="comment-2"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`when there is more disables load more button 1`] = `
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<Logo
|
||||
gutterBottom={true}
|
||||
/>
|
||||
<withContext(createMutationContainer(PostCommentFormContainer))
|
||||
assetID="asset-id"
|
||||
/>
|
||||
<Flex
|
||||
aria-live="polite"
|
||||
direction="column"
|
||||
id="talk-comments-stream-log"
|
||||
itemGutter={true}
|
||||
role="log"
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
itemGutter={true}
|
||||
key="comment-1"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
direction="column"
|
||||
itemGutter={true}
|
||||
key="comment-2"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<Localized
|
||||
id="comments-stream-loadMore"
|
||||
>
|
||||
<withPropsOnChange(Button)
|
||||
aria-controls="talk-comments-stream-log"
|
||||
disabled={true}
|
||||
fullWidth={true}
|
||||
id="talk-comments-stream-loadMore"
|
||||
invert={true}
|
||||
onClick={[Function]}
|
||||
secondary={true}
|
||||
>
|
||||
Load More
|
||||
</withPropsOnChange(Button)>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`when there is more renders a load more button 1`] = `
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<Logo
|
||||
gutterBottom={true}
|
||||
/>
|
||||
<withContext(createMutationContainer(PostCommentFormContainer))
|
||||
assetID="asset-id"
|
||||
/>
|
||||
<Flex
|
||||
aria-live="polite"
|
||||
direction="column"
|
||||
id="talk-comments-stream-log"
|
||||
itemGutter={true}
|
||||
role="log"
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
itemGutter={true}
|
||||
key="comment-1"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
direction="column"
|
||||
itemGutter={true}
|
||||
key="comment-2"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<Localized
|
||||
id="comments-stream-loadMore"
|
||||
>
|
||||
<withPropsOnChange(Button)
|
||||
aria-controls="talk-comments-stream-log"
|
||||
disabled={false}
|
||||
fullWidth={true}
|
||||
id="talk-comments-stream-loadMore"
|
||||
invert={true}
|
||||
onClick={[Function]}
|
||||
secondary={true}
|
||||
>
|
||||
Load More
|
||||
</withPropsOnChange(Button)>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import { AppContainer } from "./AppContainer";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof AppContainer> = {
|
||||
data: {
|
||||
asset: {},
|
||||
},
|
||||
};
|
||||
const wrapper = shallow(<AppContainer {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -11,30 +11,20 @@ interface InnerProps {
|
||||
data: Data;
|
||||
}
|
||||
|
||||
const AppContainer: StatelessComponent<InnerProps> = props => {
|
||||
export const AppContainer: StatelessComponent<InnerProps> = props => {
|
||||
return <App {...props.data} />;
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<{ data: Data }>(
|
||||
graphql`
|
||||
const enhanced = withFragmentContainer<{ data: Data }>({
|
||||
data: graphql`
|
||||
fragment AppContainer on Query
|
||||
@argumentDefinitions(
|
||||
assetID: { type: "ID!" }
|
||||
showAssetList: { type: "Boolean!" }
|
||||
) {
|
||||
assets @include(if: $showAssetList) {
|
||||
...AssetListContainer_assets
|
||||
}
|
||||
asset(id: $assetID) @skip(if: $showAssetList) {
|
||||
id
|
||||
isClosed
|
||||
comments {
|
||||
...StreamContainer_comments
|
||||
}
|
||||
@argumentDefinitions(assetID: { type: "ID!" }) {
|
||||
asset(id: $assetID) {
|
||||
...StreamContainer_asset
|
||||
}
|
||||
}
|
||||
`
|
||||
)(AppContainer);
|
||||
`,
|
||||
})(AppContainer);
|
||||
|
||||
export type AppContainerProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { AssetListContainer_assets as Data } from "talk-stream/__generated__/AssetListContainer_assets.graphql";
|
||||
|
||||
import AssetList from "../components/AssetList";
|
||||
|
||||
interface InnerProps {
|
||||
assets: Data;
|
||||
}
|
||||
|
||||
const AssetListContainer: StatelessComponent<InnerProps> = props => {
|
||||
const assets = props.assets.edges.map(edge => edge.node);
|
||||
return <AssetList assets={assets} />;
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<{ assets: Data }>(
|
||||
graphql`
|
||||
fragment AssetListContainer_assets on AssetsConnection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
)(AssetListContainer);
|
||||
|
||||
export type AssetListContainerProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import { CommentContainer } from "./CommentContainer";
|
||||
|
||||
it("renders username and body", () => {
|
||||
const props: PropTypesOf<typeof CommentContainer> = {
|
||||
data: {
|
||||
author: {
|
||||
username: "Marvin",
|
||||
},
|
||||
body: "Woof",
|
||||
createdAt: "1995-12-17T03:24:00.000Z",
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = shallow(<CommentContainer {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -2,28 +2,39 @@ import React, { StatelessComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import withFragmentContainer from "talk-framework/lib/relay/withFragmentContainer";
|
||||
import { Omit, PropTypesOf } from "talk-framework/types";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { CommentContainer as Data } from "talk-stream/__generated__/CommentContainer.graphql";
|
||||
|
||||
import Comment, { CommentProps } from "../components/Comment";
|
||||
import Comment from "../components/Comment";
|
||||
|
||||
type InnerProps = { data: Data } & Omit<CommentProps, keyof Data>;
|
||||
interface InnerProps {
|
||||
data: Data;
|
||||
}
|
||||
|
||||
const CommentContainer: StatelessComponent<InnerProps> = props => {
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
graphql`
|
||||
fragment CommentContainer_comment on Comment {
|
||||
id
|
||||
author {
|
||||
username
|
||||
}
|
||||
body
|
||||
createdAt
|
||||
}
|
||||
`;
|
||||
|
||||
export const CommentContainer: StatelessComponent<InnerProps> = props => {
|
||||
const { data, ...rest } = props;
|
||||
return <Comment {...rest} {...props.data} />;
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<{ data: Data }>(
|
||||
graphql`
|
||||
const enhanced = withFragmentContainer<{ data: Data }>({
|
||||
data: graphql`
|
||||
fragment CommentContainer on Comment {
|
||||
author {
|
||||
username
|
||||
}
|
||||
body
|
||||
...CommentContainer_comment @relay(mask: false)
|
||||
}
|
||||
`
|
||||
)(CommentContainer);
|
||||
`,
|
||||
})(CommentContainer);
|
||||
|
||||
export type CommentContainerProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
|
||||
@@ -25,6 +25,8 @@ class PostCommentFormContainer extends Component<InnerProps> {
|
||||
if (error instanceof BadUserInputError) {
|
||||
return error.invalidArgsLocalized;
|
||||
}
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(error);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import ReplyList from "../components/ReplyList";
|
||||
import { ReplyListContainer } from "./ReplyListContainer";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof ReplyListContainer> = {
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
replies: {
|
||||
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
|
||||
},
|
||||
},
|
||||
relay: {
|
||||
hasMore: noop,
|
||||
isLoading: noop,
|
||||
} as any,
|
||||
};
|
||||
const wrapper = shallow(<ReplyListContainer {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders correctly when replies are null", () => {
|
||||
const props: PropTypesOf<typeof ReplyListContainer> = {
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
replies: null,
|
||||
},
|
||||
relay: {
|
||||
hasMore: noop,
|
||||
isLoading: noop,
|
||||
} as any,
|
||||
};
|
||||
const wrapper = shallow(<ReplyListContainer {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when has more replies", () => {
|
||||
let finishLoading: ((error?: Error) => void) | null = null;
|
||||
const props: PropTypesOf<ReplyListContainer> = {
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
replies: {
|
||||
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
|
||||
},
|
||||
},
|
||||
relay: {
|
||||
hasMore: () => true,
|
||||
isLoading: () => false,
|
||||
loadMore: (_: any, callback: () => void) => (finishLoading = callback),
|
||||
} as any,
|
||||
};
|
||||
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
beforeAll(() => (wrapper = shallow(<ReplyListContainer {...props} />)));
|
||||
|
||||
it("renders hasMore", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when showing all", () => {
|
||||
beforeAll(() => {
|
||||
wrapper
|
||||
.find(ReplyList)
|
||||
.props()
|
||||
.onShowAll();
|
||||
});
|
||||
it("calls relay loadMore", () => {
|
||||
expect(finishLoading).not.toBeNull();
|
||||
});
|
||||
it("disables show all button", () => {
|
||||
wrapper.update();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it("enable show all button after loading is done", () => {
|
||||
finishLoading!();
|
||||
wrapper.update();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
import React from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import { withPaginationContainer } from "talk-framework/lib/relay";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { ReplyListContainer_comment as Data } from "talk-stream/__generated__/ReplyListContainer_comment.graphql";
|
||||
import {
|
||||
COMMENT_SORT,
|
||||
ReplyListContainerPaginationQueryVariables,
|
||||
} from "talk-stream/__generated__/ReplyListContainerPaginationQuery.graphql";
|
||||
|
||||
import ReplyList from "../components/ReplyList";
|
||||
|
||||
export interface InnerProps {
|
||||
comment: Data;
|
||||
relay: RelayPaginationProp;
|
||||
}
|
||||
|
||||
export class ReplyListContainer extends React.Component<InnerProps> {
|
||||
public state = {
|
||||
disableShowAll: false,
|
||||
};
|
||||
|
||||
public render() {
|
||||
if (
|
||||
this.props.comment.replies === null ||
|
||||
this.props.comment.replies.edges.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const comments = this.props.comment.replies.edges.map(edge => edge.node);
|
||||
return (
|
||||
<ReplyList
|
||||
commentID={this.props.comment.id}
|
||||
comments={comments}
|
||||
onShowAll={this.showAll}
|
||||
hasMore={this.props.relay.hasMore()}
|
||||
disableShowAll={this.state.disableShowAll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private showAll = () => {
|
||||
if (!this.props.relay.hasMore() || this.props.relay.isLoading()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ disableShowAll: true });
|
||||
this.props.relay.loadMore(
|
||||
999999999, // Fetch All Replies
|
||||
error => {
|
||||
this.setState({ disableShowAll: false });
|
||||
if (error) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: (cvle) This should be autogenerated.
|
||||
interface FragmentVariables {
|
||||
count: number;
|
||||
cursor?: string;
|
||||
orderBy: COMMENT_SORT;
|
||||
}
|
||||
|
||||
const enhanced = withPaginationContainer<
|
||||
{ comment: Data },
|
||||
InnerProps,
|
||||
FragmentVariables,
|
||||
ReplyListContainerPaginationQueryVariables
|
||||
>(
|
||||
{
|
||||
comment: graphql`
|
||||
fragment ReplyListContainer_comment on Comment
|
||||
@argumentDefinitions(
|
||||
count: { type: "Int!", defaultValue: 5 }
|
||||
cursor: { type: "Cursor" }
|
||||
orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_ASC }
|
||||
) {
|
||||
id
|
||||
replies(first: $count, after: $cursor, orderBy: $orderBy)
|
||||
@connection(key: "ReplyList_replies") {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...CommentContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
direction: "forward",
|
||||
getConnectionFromProps(props) {
|
||||
return props.comment && props.comment.replies;
|
||||
},
|
||||
// This is also the default implementation of `getFragmentVariables` if it isn't provided.
|
||||
getFragmentVariables(prevVars, totalCount) {
|
||||
return {
|
||||
...prevVars,
|
||||
count: totalCount,
|
||||
};
|
||||
},
|
||||
getVariables(props, { count, cursor }, fragmentVariables) {
|
||||
return {
|
||||
count,
|
||||
cursor,
|
||||
orderBy: fragmentVariables.orderBy,
|
||||
commentID: props.comment.id,
|
||||
};
|
||||
},
|
||||
query: graphql`
|
||||
# Pagination query to be fetched upon calling 'loadMore'.
|
||||
# Notice that we re-use our fragment, and the shape of this query matches our fragment spec.
|
||||
query ReplyListContainerPaginationQuery(
|
||||
$count: Int!
|
||||
$cursor: Cursor
|
||||
$orderBy: COMMENT_SORT!
|
||||
$commentID: ID!
|
||||
) {
|
||||
comment(id: $commentID) {
|
||||
...ReplyListContainer_comment
|
||||
@arguments(count: $count, cursor: $cursor, orderBy: $orderBy)
|
||||
}
|
||||
}
|
||||
`,
|
||||
}
|
||||
)(ReplyListContainer);
|
||||
|
||||
export type ReplyListContainerProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,73 @@
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import Stream from "../components/Stream";
|
||||
import { StreamContainer } from "./StreamContainer";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<StreamContainer> = {
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
isClosed: false,
|
||||
comments: {
|
||||
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
|
||||
},
|
||||
},
|
||||
relay: {
|
||||
hasMore: noop,
|
||||
isLoading: noop,
|
||||
} as any,
|
||||
};
|
||||
const wrapper = shallow(<StreamContainer {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when has more comments", () => {
|
||||
let finishLoading: ((error?: Error) => void) | null = null;
|
||||
const props: PropTypesOf<StreamContainer> = {
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
isClosed: false,
|
||||
comments: {
|
||||
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
|
||||
},
|
||||
},
|
||||
relay: {
|
||||
hasMore: () => true,
|
||||
isLoading: () => false,
|
||||
loadMore: (_: any, callback: () => void) => (finishLoading = callback),
|
||||
} as any,
|
||||
};
|
||||
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
beforeAll(() => (wrapper = shallow(<StreamContainer {...props} />)));
|
||||
|
||||
it("renders hasMore", () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when loading more", () => {
|
||||
beforeAll(() => {
|
||||
wrapper
|
||||
.find(Stream)
|
||||
.props()
|
||||
.onLoadMore();
|
||||
});
|
||||
it("calls relay loadMore", () => {
|
||||
expect(finishLoading).not.toBeNull();
|
||||
});
|
||||
it("disables load more button", () => {
|
||||
wrapper.update();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it("enable load more button after loading is done", () => {
|
||||
finishLoading!();
|
||||
wrapper.update();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,32 +1,132 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
import React from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { withPaginationContainer } from "talk-framework/lib/relay";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { StreamContainer_comments as Data } from "talk-stream/__generated__/StreamContainer_comments.graphql";
|
||||
import { StreamContainer_asset as Data } from "talk-stream/__generated__/StreamContainer_asset.graphql";
|
||||
import {
|
||||
COMMENT_SORT,
|
||||
StreamContainerPaginationQueryVariables,
|
||||
} from "talk-stream/__generated__/StreamContainerPaginationQuery.graphql";
|
||||
|
||||
import Stream from "../components/Stream";
|
||||
|
||||
interface InnerProps {
|
||||
comments: Data;
|
||||
asset: Data;
|
||||
relay: RelayPaginationProp;
|
||||
}
|
||||
|
||||
const StreamContainer: StatelessComponent<InnerProps> = props => {
|
||||
const comments = props.comments.edges.map(edge => edge.node);
|
||||
return <Stream comments={comments} />;
|
||||
};
|
||||
export class StreamContainer extends React.Component<InnerProps> {
|
||||
public state = {
|
||||
disableLoadMore: false,
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<{ comments: Data }>(
|
||||
graphql`
|
||||
fragment StreamContainer_comments on CommentsConnection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...CommentContainer
|
||||
public render() {
|
||||
const comments = this.props.asset.comments.edges.map(edge => edge.node);
|
||||
return (
|
||||
<Stream
|
||||
assetID={this.props.asset.id}
|
||||
isClosed={this.props.asset.isClosed}
|
||||
comments={comments}
|
||||
onLoadMore={this.loadMore}
|
||||
hasMore={this.props.relay.hasMore()}
|
||||
disableLoadMore={this.state.disableLoadMore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private loadMore = () => {
|
||||
if (!this.props.relay.hasMore() || this.props.relay.isLoading()) {
|
||||
return;
|
||||
}
|
||||
this.setState({ disableLoadMore: true });
|
||||
this.props.relay.loadMore(
|
||||
10, // Fetch the next 10 feed items
|
||||
error => {
|
||||
this.setState({ disableLoadMore: false });
|
||||
if (error) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: (cvle) This should be autogenerated.
|
||||
interface FragmentVariables {
|
||||
count: number;
|
||||
cursor?: string;
|
||||
orderBy: COMMENT_SORT;
|
||||
}
|
||||
|
||||
const enhanced = withPaginationContainer<
|
||||
{ asset: Data },
|
||||
InnerProps,
|
||||
FragmentVariables,
|
||||
StreamContainerPaginationQueryVariables
|
||||
>(
|
||||
{
|
||||
asset: graphql`
|
||||
fragment StreamContainer_asset on Asset
|
||||
@argumentDefinitions(
|
||||
count: { type: "Int!", defaultValue: 5 }
|
||||
cursor: { type: "Cursor" }
|
||||
orderBy: { type: "COMMENT_SORT!", defaultValue: CREATED_AT_DESC }
|
||||
) {
|
||||
id
|
||||
isClosed
|
||||
comments(first: $count, after: $cursor, orderBy: $orderBy)
|
||||
@connection(key: "Stream_comments") {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...CommentContainer
|
||||
...ReplyListContainer_comment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
direction: "forward",
|
||||
getConnectionFromProps(props) {
|
||||
return props.asset && props.asset.comments;
|
||||
},
|
||||
// This is also the default implementation of `getFragmentVariables` if it isn't provided.
|
||||
getFragmentVariables(prevVars, totalCount) {
|
||||
return {
|
||||
...prevVars,
|
||||
count: totalCount,
|
||||
};
|
||||
},
|
||||
getVariables(props, { count, cursor }, fragmentVariables) {
|
||||
return {
|
||||
count,
|
||||
cursor,
|
||||
orderBy: fragmentVariables.orderBy,
|
||||
// assetID isn't specified as an @argument for the fragment, but it should be a
|
||||
// variable available for the fragment under the query root.
|
||||
assetID: props.asset.id,
|
||||
};
|
||||
},
|
||||
query: graphql`
|
||||
# Pagination query to be fetched upon calling 'loadMore'.
|
||||
# Notice that we re-use our fragment, and the shape of this query matches our fragment spec.
|
||||
query StreamContainerPaginationQuery(
|
||||
$count: Int!
|
||||
$cursor: Cursor
|
||||
$orderBy: COMMENT_SORT!
|
||||
$assetID: ID!
|
||||
) {
|
||||
asset(id: $assetID) {
|
||||
...StreamContainer_asset
|
||||
@arguments(count: $count, cursor: $cursor, orderBy: $orderBy)
|
||||
}
|
||||
}
|
||||
`,
|
||||
}
|
||||
)(StreamContainer);
|
||||
|
||||
export type StreamContainerProps = PropTypesOf<typeof enhanced>;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<App
|
||||
asset={Object {}}
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders username and body 1`] = `
|
||||
<Comment
|
||||
author={
|
||||
Object {
|
||||
"username": "Marvin",
|
||||
}
|
||||
}
|
||||
body="Woof"
|
||||
createdAt="1995-12-17T03:24:00.000Z"
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,78 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<ReplyList
|
||||
commentID="comment-id"
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableShowAll={false}
|
||||
onShowAll={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`renders correctly when replies are null 1`] = `""`;
|
||||
|
||||
exports[`when has more replies renders hasMore 1`] = `
|
||||
<ReplyList
|
||||
commentID="comment-id"
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableShowAll={false}
|
||||
hasMore={true}
|
||||
onShowAll={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`when has more replies when showing all disables show all button 1`] = `
|
||||
<ReplyList
|
||||
commentID="comment-id"
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableShowAll={true}
|
||||
hasMore={true}
|
||||
onShowAll={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`when has more replies when showing all enable show all button after loading is done 1`] = `
|
||||
<ReplyList
|
||||
commentID="comment-id"
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableShowAll={false}
|
||||
hasMore={true}
|
||||
onShowAll={[Function]}
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,80 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<Stream
|
||||
assetID="asset-id"
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableLoadMore={false}
|
||||
isClosed={false}
|
||||
onLoadMore={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`when has more comments renders hasMore 1`] = `
|
||||
<Stream
|
||||
assetID="asset-id"
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableLoadMore={false}
|
||||
hasMore={true}
|
||||
isClosed={false}
|
||||
onLoadMore={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`when has more comments when loading more disables load more button 1`] = `
|
||||
<Stream
|
||||
assetID="asset-id"
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableLoadMore={true}
|
||||
hasMore={true}
|
||||
isClosed={false}
|
||||
onLoadMore={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`when has more comments when loading more enable load more button after loading is done 1`] = `
|
||||
<Stream
|
||||
assetID="asset-id"
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableLoadMore={false}
|
||||
hasMore={true}
|
||||
isClosed={false}
|
||||
onLoadMore={[Function]}
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { render } from "./AppQuery";
|
||||
|
||||
it("renders app", () => {
|
||||
const data = {
|
||||
props: {} as any,
|
||||
error: null,
|
||||
};
|
||||
const wrapper = shallow(React.createElement(() => render(data)));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders loading", () => {
|
||||
const data = {
|
||||
props: null,
|
||||
error: null,
|
||||
};
|
||||
const wrapper = shallow(React.createElement(() => render(data)));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders error", () => {
|
||||
const data = {
|
||||
props: null,
|
||||
error: new Error("error"),
|
||||
};
|
||||
const wrapper = shallow(React.createElement(() => render(data)));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import { ReadyState } from "react-relay";
|
||||
|
||||
import {
|
||||
graphql,
|
||||
QueryRenderer,
|
||||
ReadyState,
|
||||
withLocalStateContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import {
|
||||
@@ -15,7 +15,7 @@ import { AppQueryLocal as Local } from "talk-stream/__generated__/AppQueryLocal.
|
||||
|
||||
import AppContainer from "../containers/AppContainer";
|
||||
|
||||
const render = ({ error, props }: ReadyState<AppQueryResponse>) => {
|
||||
export const render = ({ error, props }: ReadyState<AppQueryResponse>) => {
|
||||
if (error) {
|
||||
return <div>{error.message}</div>;
|
||||
}
|
||||
@@ -33,16 +33,12 @@ const AppQuery: StatelessComponent<InnerProps> = props => {
|
||||
return (
|
||||
<QueryRenderer<AppQueryVariables, AppQueryResponse>
|
||||
query={graphql`
|
||||
query AppQuery($showAssetList: Boolean!, $assetID: ID!) {
|
||||
...AppContainer
|
||||
@arguments(showAssetList: $showAssetList, assetID: $assetID)
|
||||
query AppQuery($assetID: ID!) {
|
||||
...AppContainer @arguments(assetID: $assetID)
|
||||
}
|
||||
`}
|
||||
variables={{
|
||||
// We cast `null` to any due to restrictions of the current graphql syntax.
|
||||
assetID: props.local.assetID || (null as any),
|
||||
// TODO: This is set to false, as server does not support querying assets yet.
|
||||
showAssetList: !props.local.assetID && false,
|
||||
assetID: props.local.assetID,
|
||||
}}
|
||||
render={render}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders app 1`] = `
|
||||
<Relay(AppContainer)
|
||||
data={Object {}}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`renders error 1`] = `
|
||||
<div>
|
||||
error
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders loading 1`] = `
|
||||
<div>
|
||||
Loading
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,256 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`loads more comments 1`] = `
|
||||
<div
|
||||
className="Flex-root App-root Flex-justifyCenter"
|
||||
>
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-gutterBottom"
|
||||
>
|
||||
Talk NEO
|
||||
</h1>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div>
|
||||
<textarea
|
||||
className="PostCommentForm-textarea"
|
||||
name="body"
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="PostCommentForm-postButtonContainer"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-primary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
>
|
||||
Post
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Flex-root Flex-itemGutter Flex-directionColumn"
|
||||
id="talk-comments-stream-log"
|
||||
role="log"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-itemGutter Flex-directionColumn"
|
||||
>
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Username-root"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-body1"
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Flex-root Flex-itemGutter Flex-directionColumn"
|
||||
>
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Username-root"
|
||||
>
|
||||
Lukas
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-body1"
|
||||
>
|
||||
What's up?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Flex-root Flex-itemGutter Flex-directionColumn"
|
||||
>
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Username-root"
|
||||
>
|
||||
Isabelle
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:14:00.000Z"
|
||||
title="2018-07-06T18:14:00.000Z"
|
||||
>
|
||||
2018-07-06T18:14:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-body1"
|
||||
>
|
||||
Hey!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders comment stream 1`] = `
|
||||
<div
|
||||
className="Flex-root App-root Flex-justifyCenter"
|
||||
>
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-gutterBottom"
|
||||
>
|
||||
Talk NEO
|
||||
</h1>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div>
|
||||
<textarea
|
||||
className="PostCommentForm-textarea"
|
||||
name="body"
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="PostCommentForm-postButtonContainer"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-primary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
>
|
||||
Post
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Flex-root Flex-itemGutter Flex-directionColumn"
|
||||
id="talk-comments-stream-log"
|
||||
role="log"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-itemGutter Flex-directionColumn"
|
||||
>
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Username-root"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-body1"
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Flex-root Flex-itemGutter Flex-directionColumn"
|
||||
>
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Username-root"
|
||||
>
|
||||
Lukas
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-body1"
|
||||
>
|
||||
What's up?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="talk-comments-stream-log"
|
||||
className="BaseButton-root Button-root Button-invert Button-fullWidth Button-secondary"
|
||||
disabled={false}
|
||||
id="talk-comments-stream-loadMore"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
>
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,168 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders comment stream 1`] = `
|
||||
<div
|
||||
className="Flex-root App-root Flex-justifyCenter"
|
||||
>
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-gutterBottom"
|
||||
>
|
||||
Talk NEO
|
||||
</h1>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div>
|
||||
<textarea
|
||||
className="PostCommentForm-textarea"
|
||||
name="body"
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="PostCommentForm-postButtonContainer"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-primary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
>
|
||||
Post
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Flex-root Flex-itemGutter Flex-directionColumn"
|
||||
id="talk-comments-stream-log"
|
||||
role="log"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-itemGutter Flex-directionColumn"
|
||||
>
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Username-root"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-body1"
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Flex-root Flex-itemGutter Flex-directionColumn"
|
||||
>
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Username-root"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-body1"
|
||||
>
|
||||
I like yoghurt
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="Indent-root Indent-level0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-itemGutter Flex-directionColumn"
|
||||
id="talk-comments-replyList-log--comment-with-replies"
|
||||
role="log"
|
||||
>
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Username-root"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-body1"
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Username-root"
|
||||
>
|
||||
Lukas
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-body1"
|
||||
>
|
||||
What's up?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,108 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders comment stream 1`] = `
|
||||
<div
|
||||
className="Flex-root App-root Flex-justifyCenter"
|
||||
>
|
||||
<div
|
||||
className="Stream-root"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-gutterBottom"
|
||||
>
|
||||
Talk NEO
|
||||
</h1>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div>
|
||||
<textarea
|
||||
className="PostCommentForm-textarea"
|
||||
name="body"
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="PostCommentForm-postButtonContainer"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-primary"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
>
|
||||
Post
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="Flex-root Flex-itemGutter Flex-directionColumn"
|
||||
id="talk-comments-stream-log"
|
||||
role="log"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-itemGutter Flex-directionColumn"
|
||||
>
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Username-root"
|
||||
>
|
||||
Markus
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:24:00.000Z"
|
||||
title="2018-07-06T18:24:00.000Z"
|
||||
>
|
||||
2018-07-06T18:24:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-body1"
|
||||
>
|
||||
Joining Too
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Flex-root Flex-itemGutter Flex-directionColumn"
|
||||
>
|
||||
<div
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Username-root"
|
||||
>
|
||||
Lukas
|
||||
</span>
|
||||
<time
|
||||
className="Timestamp-root RelativeTime-root"
|
||||
dateTime="2018-07-06T18:20:00.000Z"
|
||||
title="2018-07-06T18:20:00.000Z"
|
||||
>
|
||||
2018-07-06T18:20:00.000Z
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-body1"
|
||||
>
|
||||
What's up?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user