mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 12:15:40 +08:00
Merge branch 'next' into profile-mobile
This commit is contained in:
+37
-8
@@ -20,7 +20,12 @@ jobs:
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- restore_cache:
|
||||
key: dependency-cache-{{ checksum "package-lock.json" }}
|
||||
keys:
|
||||
# Find a cache corresponding to this specific package-lock.json
|
||||
# checksum when this file is changed, this key will fail.
|
||||
- v1-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
# Find the most recently generated cache used from any branch
|
||||
- v1-dependency-cache-
|
||||
- run:
|
||||
name: Update NPM
|
||||
command: sudo npm update -g npm
|
||||
@@ -29,11 +34,11 @@ jobs:
|
||||
command: npm audit
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: npm install
|
||||
command: npm ci
|
||||
- save_cache:
|
||||
key: dependency-cache-{{ checksum "package-lock.json" }}
|
||||
key: v1-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
paths:
|
||||
- ./node_modules
|
||||
- ~/.npm
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: node_modules
|
||||
@@ -85,20 +90,36 @@ jobs:
|
||||
at: ~/coralproject/talk
|
||||
- restore_cache:
|
||||
keys:
|
||||
- build-cache-{{ .Branch }}-{{ .Revision }}
|
||||
- build-cache-{{ .Branch }}-
|
||||
- build-cache-
|
||||
- v1-build-cache-{{ .Branch }}-{{ .Revision }}
|
||||
- v1-build-cache-{{ .Branch }}-
|
||||
- v1-build-cache-
|
||||
- run:
|
||||
name: Build
|
||||
command: npm run build
|
||||
- save_cache:
|
||||
key: build-cache-{{ .Branch }}-{{ .Revision }}
|
||||
key: v1-build-cache-{{ .Branch }}-{{ .Revision }}
|
||||
paths:
|
||||
- ./dist
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: dist
|
||||
|
||||
# release_docker will build and push the Docker image.
|
||||
release_docker:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
name: Login
|
||||
command: docker login -u $DOCKER_USER -p $DOCKER_PASS
|
||||
- run:
|
||||
name: Build
|
||||
command: docker build -t coralproject/talk:next --build-arg REVISION_HASH=${CIRCLE_SHA1} .
|
||||
- run:
|
||||
name: Push
|
||||
command: docker push coralproject/talk:next
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-and-test:
|
||||
@@ -113,3 +134,11 @@ workflows:
|
||||
- build:
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- release_docker:
|
||||
requires:
|
||||
- lint
|
||||
- unit_tests
|
||||
- build
|
||||
filters:
|
||||
branches:
|
||||
only: next
|
||||
|
||||
+1
-2
@@ -13,5 +13,4 @@ coverage
|
||||
*.DS_STORE
|
||||
|
||||
*.css.d.ts
|
||||
__generated__
|
||||
|
||||
__generated__
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
FROM node:8-alpine
|
||||
|
||||
# Install installation dependancies.
|
||||
# Install build dependancies.
|
||||
RUN apk --no-cache add git
|
||||
|
||||
# Create app directory.
|
||||
@@ -36,6 +36,7 @@ ENV NODE_ENV production
|
||||
|
||||
# Store the current git revision.
|
||||
ARG REVISION_HASH
|
||||
ENV REVISION_HASH=${REVISION_HASH}
|
||||
RUN mkdir dist/core/common/__generated__ && \
|
||||
echo "{\"revision\": \"${REVISION_HASH}\"}" > dist/core/common/__generated__/revision.json
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* This is a project wide babel configuration.
|
||||
* https://babeljs.io/docs/en/config-files#project-wide-configuration
|
||||
*
|
||||
* We use this file to apply babel configuration to packages in `node_modules`
|
||||
* for testing with jest.
|
||||
*/
|
||||
module.exports = {
|
||||
env: {
|
||||
test: {
|
||||
presets: [
|
||||
[
|
||||
"@babel/env",
|
||||
{ targets: "last 2 versions, ie 11", modules: "commonjs" },
|
||||
],
|
||||
"@babel/react",
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -16,6 +16,7 @@ module.exports = {
|
||||
testEnvironment: "node",
|
||||
testURL: "http://localhost",
|
||||
transform: {
|
||||
"^.+\\.jsx?$": "<rootDir>/node_modules/babel-jest",
|
||||
"^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest",
|
||||
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
|
||||
"^.+\\.ftl$": "<rootDir>/config/jest/contentTransform.js",
|
||||
@@ -23,7 +24,7 @@ module.exports = {
|
||||
"<rootDir>/config/jest/fileTransform.js",
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|ts|tsx)$",
|
||||
"[/\\\\]node_modules[/\\\\](?!(fluent|react-relay-network-modern)[/\\\\]).+\\.(js|jsx|mjs|ts|tsx)$",
|
||||
],
|
||||
moduleNameMapper: {
|
||||
"^talk-admin/(.*)$": "<rootDir>/src/core/client/admin/$1",
|
||||
|
||||
@@ -65,6 +65,23 @@ const config: Config = {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
compileRelayInstall: {
|
||||
paths: [
|
||||
"core/client/install/**/*.ts",
|
||||
"core/client/install/**/*.tsx",
|
||||
"core/client/install/**/*.graphql",
|
||||
"core/server/**/*.graphql",
|
||||
],
|
||||
ignore: [
|
||||
"core/**/*.d.ts",
|
||||
"core/**/*.graphql.ts",
|
||||
"**/test/**/*",
|
||||
"core/**/*.spec.*",
|
||||
],
|
||||
executor: new CommandExecutor("npm run compile:relay-install", {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
compileCSSTypes: {
|
||||
paths: ["**/*.css"],
|
||||
executor: new CommandExecutor("npm run compile:css-types", {
|
||||
@@ -94,6 +111,7 @@ const config: Config = {
|
||||
"compileCSSTypes",
|
||||
"compileRelayStream",
|
||||
"compileRelayAuth",
|
||||
"compileRelayInstall",
|
||||
"compileRelayAdmin",
|
||||
"compileSchema",
|
||||
],
|
||||
@@ -103,6 +121,7 @@ const config: Config = {
|
||||
"compileCSSTypes",
|
||||
"compileRelayStream",
|
||||
"compileRelayAuth",
|
||||
"compileRelayInstall",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
Generated
+684
-16
@@ -1563,6 +1563,15 @@
|
||||
"integrity": "sha512-LAQ1d4OPfSJ/BMbI2DuizmYrrkD9JMaTdi2hQTlI53lQ4kRQPyZQRS4CYQ7O66bnBBnP/oYdRxbk++X0xuFU6A==",
|
||||
"dev": true
|
||||
},
|
||||
"@samverschueren/stream-to-observable": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz",
|
||||
"integrity": "sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"any-observable": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"@shellscape/koa-send": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@shellscape/koa-send/-/koa-send-4.1.3.tgz",
|
||||
@@ -1675,6 +1684,15 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/bunyan-prettystream": {
|
||||
"version": "0.1.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/bunyan-prettystream/-/bunyan-prettystream-0.1.31.tgz",
|
||||
"integrity": "sha512-NE7fq2ZcX7OSMK+VhTNJkVEHlo+hm0uVXpuLeH1ifGm52Qwuo/kLD2GHo7UcEXMFu3duKver/AFo8C4TME93zw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/case-sensitive-paths-webpack-plugin": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.1.2.tgz",
|
||||
@@ -1754,6 +1772,15 @@
|
||||
"integrity": "sha512-p+gNRe4RPjpl1lTBUomFJ42P8ymArH/P93DFJ0iY873BJ4ZmogcKc6TbHgZQmtQMsy3jxcAo0HcTjidXwo8uKg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/cors": {
|
||||
"version": "2.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.4.tgz",
|
||||
"integrity": "sha512-ipZjBVsm2tF/n8qFGOuGBkUij9X9ZswVi9G3bx/6dz7POpVa6gVHcj1wsX/LVEn9MMF41fxK/PnZPPoTD1UFPw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"@types/cross-spawn": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.0.tgz",
|
||||
@@ -2905,6 +2932,12 @@
|
||||
"integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=",
|
||||
"dev": true
|
||||
},
|
||||
"any-observable": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz",
|
||||
"integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==",
|
||||
"dev": true
|
||||
},
|
||||
"any-promise": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
@@ -3587,9 +3620,9 @@
|
||||
}
|
||||
},
|
||||
"babel-jest": {
|
||||
"version": "23.2.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.2.0.tgz",
|
||||
"integrity": "sha1-FKnWo/QSLf6mBp03CFrfJqU6Tbo=",
|
||||
"version": "23.6.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-23.6.0.tgz",
|
||||
"integrity": "sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"babel-plugin-istanbul": "^4.1.6",
|
||||
@@ -5714,6 +5747,11 @@
|
||||
"safe-json-stringify": "~1"
|
||||
}
|
||||
},
|
||||
"bunyan-prettystream": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/bunyan-prettystream/-/bunyan-prettystream-0.1.3.tgz",
|
||||
"integrity": "sha1-bDtxMmb2rTIAfHtqsemYokU0nZg="
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||
@@ -6165,6 +6203,44 @@
|
||||
"restore-cursor": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"cli-truncate": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz",
|
||||
"integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"slice-ansi": "0.0.4",
|
||||
"string-width": "^1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"slice-ansi": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
|
||||
"integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
"strip-ansi": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli-width": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
|
||||
@@ -6671,6 +6747,13 @@
|
||||
"moment": "2.22.2",
|
||||
"validator": "7.2.0",
|
||||
"yargs-parser": "10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"validator": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-7.2.0.tgz",
|
||||
"integrity": "sha512-c8NGTUYeBEcUIGeMppmNVKHE7wwfm3mYbNZxV+c5mlv9fDHI7Ad3p07qfNrn/CvpdkK2k61fOLRO2sTEhgQXmg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"cookie": {
|
||||
@@ -6787,6 +6870,15 @@
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"cors": {
|
||||
"version": "2.8.4",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.4.tgz",
|
||||
"integrity": "sha1-K9OB8usgECAQXNUOpZ2mMJBpRoY=",
|
||||
"requires": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
}
|
||||
},
|
||||
"cosmiconfig": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-3.1.0.tgz",
|
||||
@@ -7608,6 +7700,12 @@
|
||||
"resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz",
|
||||
"integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.29.0.tgz",
|
||||
"integrity": "sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw==",
|
||||
"dev": true
|
||||
},
|
||||
"date-now": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
|
||||
@@ -7690,6 +7788,12 @@
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
|
||||
},
|
||||
"dedent": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
|
||||
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
|
||||
"dev": true
|
||||
},
|
||||
"deep-equal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
|
||||
@@ -9135,6 +9239,12 @@
|
||||
"integrity": "sha1-dDi3b5K0G5GfP73TUPvQdX2s3fc=",
|
||||
"dev": true
|
||||
},
|
||||
"elegant-spinner": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz",
|
||||
"integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=",
|
||||
"dev": true
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz",
|
||||
@@ -9606,6 +9716,12 @@
|
||||
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
|
||||
"dev": true
|
||||
},
|
||||
"exit-hook": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
|
||||
"integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=",
|
||||
"dev": true
|
||||
},
|
||||
"expand-brackets": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
|
||||
@@ -10158,6 +10274,12 @@
|
||||
"integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=",
|
||||
"dev": true
|
||||
},
|
||||
"find-parent-dir": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz",
|
||||
"integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=",
|
||||
"dev": true
|
||||
},
|
||||
"find-pkg": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-1.0.0.tgz",
|
||||
@@ -11092,6 +11214,12 @@
|
||||
"integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=",
|
||||
"dev": true
|
||||
},
|
||||
"get-own-enumerable-property-symbols": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-2.0.1.tgz",
|
||||
"integrity": "sha512-TtY/sbOemiMKPRUDDanGCSgBYe7Mf0vbRsWnBZ+9yghpZ1MvcpSpuZFjHdEeY/LZjZy0vdLjS77L6HosisFiug==",
|
||||
"dev": true
|
||||
},
|
||||
"get-port": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz",
|
||||
@@ -11515,14 +11643,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"graphql-playground-middleware-express": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/graphql-playground-middleware-express/-/graphql-playground-middleware-express-1.7.2.tgz",
|
||||
"integrity": "sha512-JvKsVOR/U5QguBtEvTt0ozQ49uh1C6cW8O1xR6krQpJZIxjLYqpgusLUddTiVkka6Q/A4/AXBohY85jPudxYDg==",
|
||||
"requires": {
|
||||
"graphql-playground-html": "1.6.0"
|
||||
}
|
||||
},
|
||||
"graphql-redis-subscriptions": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql-redis-subscriptions/-/graphql-redis-subscriptions-1.5.0.tgz",
|
||||
@@ -12461,6 +12581,163 @@
|
||||
"decamelize": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"husky": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/husky/-/husky-1.1.0.tgz",
|
||||
"integrity": "sha512-jnUD0PK3xGLB5Jc3f3UEwl8qOZeLd0WiWABhVyHPS5R298HOccGZJMOMBSk3gFksAa1BeK9FQYYEfPNlqkfBxg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cosmiconfig": "^5.0.6",
|
||||
"execa": "^0.9.0",
|
||||
"find-up": "^3.0.0",
|
||||
"get-stdin": "^6.0.0",
|
||||
"is-ci": "^1.2.1",
|
||||
"pkg-dir": "^3.0.0",
|
||||
"please-upgrade-node": "^3.1.1",
|
||||
"read-pkg": "^4.0.1",
|
||||
"run-node": "^1.0.0",
|
||||
"slash": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ci-info": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
|
||||
"integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==",
|
||||
"dev": true
|
||||
},
|
||||
"cosmiconfig": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.6.tgz",
|
||||
"integrity": "sha512-6DWfizHriCrFWURP1/qyhsiFvYdlJzbCzmtFWh744+KyWsJo5+kPzUZZaMRSSItoYc0pxFX7gEO7ZC1/gN/7AQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-directory": "^0.3.1",
|
||||
"js-yaml": "^3.9.0",
|
||||
"parse-json": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
|
||||
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^4.0.1",
|
||||
"shebang-command": "^1.2.0",
|
||||
"which": "^1.2.9"
|
||||
}
|
||||
},
|
||||
"execa": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-0.9.0.tgz",
|
||||
"integrity": "sha512-BbUMBiX4hqiHZUA5+JujIjNb6TyAlp2D5KLheMjMluwOuzcnylDL4AxZYLLn1n2AGB49eSWwyKvvEQoRpnAtmA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cross-spawn": "^5.0.1",
|
||||
"get-stream": "^3.0.0",
|
||||
"is-stream": "^1.1.0",
|
||||
"npm-run-path": "^2.0.0",
|
||||
"p-finally": "^1.0.0",
|
||||
"signal-exit": "^3.0.0",
|
||||
"strip-eof": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"find-up": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
|
||||
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"locate-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"get-stdin": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
|
||||
"integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==",
|
||||
"dev": true
|
||||
},
|
||||
"is-ci": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz",
|
||||
"integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ci-info": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
||||
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-locate": "^3.0.0",
|
||||
"path-exists": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz",
|
||||
"integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-try": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
|
||||
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-limit": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz",
|
||||
"integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==",
|
||||
"dev": true
|
||||
},
|
||||
"parse-json": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
|
||||
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"error-ex": "^1.3.1",
|
||||
"json-parse-better-errors": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"pkg-dir": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
|
||||
"integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"find-up": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"read-pkg": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz",
|
||||
"integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"normalize-package-data": "^2.3.2",
|
||||
"parse-json": "^4.0.0",
|
||||
"pify": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"slash": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
||||
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"hyphenate-style-name": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz",
|
||||
@@ -13129,6 +13406,15 @@
|
||||
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
|
||||
"dev": true
|
||||
},
|
||||
"is-observable": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz",
|
||||
"integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"symbol-observable": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"is-odd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz",
|
||||
@@ -13223,6 +13509,12 @@
|
||||
"has": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"is-regexp": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
|
||||
"integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=",
|
||||
"dev": true
|
||||
},
|
||||
"is-relative": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
|
||||
@@ -15864,6 +16156,318 @@
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-7.3.0.tgz",
|
||||
"integrity": "sha512-AXk40M9DAiPi7f4tdJggwuKIViUplYtVj1os1MVEteW7qOkU50EOehayCfO9TsoGK24o/EsWb41yrEgfJDDjCw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.3.1",
|
||||
"commander": "^2.14.1",
|
||||
"cosmiconfig": "^5.0.2",
|
||||
"debug": "^3.1.0",
|
||||
"dedent": "^0.7.0",
|
||||
"execa": "^0.9.0",
|
||||
"find-parent-dir": "^0.3.0",
|
||||
"is-glob": "^4.0.0",
|
||||
"is-windows": "^1.0.2",
|
||||
"jest-validate": "^23.5.0",
|
||||
"listr": "^0.14.1",
|
||||
"lodash": "^4.17.5",
|
||||
"log-symbols": "^2.2.0",
|
||||
"micromatch": "^3.1.8",
|
||||
"npm-which": "^3.0.1",
|
||||
"p-map": "^1.1.1",
|
||||
"path-is-inside": "^1.0.2",
|
||||
"pify": "^3.0.0",
|
||||
"please-upgrade-node": "^3.0.2",
|
||||
"staged-git-files": "1.1.1",
|
||||
"string-argv": "^0.0.2",
|
||||
"stringify-object": "^3.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
|
||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"cosmiconfig": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.6.tgz",
|
||||
"integrity": "sha512-6DWfizHriCrFWURP1/qyhsiFvYdlJzbCzmtFWh744+KyWsJo5+kPzUZZaMRSSItoYc0pxFX7gEO7ZC1/gN/7AQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-directory": "^0.3.1",
|
||||
"js-yaml": "^3.9.0",
|
||||
"parse-json": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
|
||||
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^4.0.1",
|
||||
"shebang-command": "^1.2.0",
|
||||
"which": "^1.2.9"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz",
|
||||
"integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"execa": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-0.9.0.tgz",
|
||||
"integrity": "sha512-BbUMBiX4hqiHZUA5+JujIjNb6TyAlp2D5KLheMjMluwOuzcnylDL4AxZYLLn1n2AGB49eSWwyKvvEQoRpnAtmA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cross-spawn": "^5.0.1",
|
||||
"get-stream": "^3.0.0",
|
||||
"is-stream": "^1.1.0",
|
||||
"npm-run-path": "^2.0.0",
|
||||
"p-finally": "^1.0.0",
|
||||
"signal-exit": "^3.0.0",
|
||||
"strip-eof": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"jest-validate": {
|
||||
"version": "23.6.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.6.0.tgz",
|
||||
"integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.0.1",
|
||||
"jest-get-type": "^22.1.0",
|
||||
"leven": "^2.1.0",
|
||||
"pretty-format": "^23.6.0"
|
||||
}
|
||||
},
|
||||
"parse-json": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
|
||||
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"error-ex": "^1.3.1",
|
||||
"json-parse-better-errors": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "23.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz",
|
||||
"integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^3.0.0",
|
||||
"ansi-styles": "^3.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"listr": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/listr/-/listr-0.14.2.tgz",
|
||||
"integrity": "sha512-vmaNJ1KlGuGWShHI35X/F8r9xxS0VTHh9GejVXwSN20fG5xpq3Jh4bJbnumoT6q5EDM/8/YP1z3YMtQbFmhuXw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@samverschueren/stream-to-observable": "^0.3.0",
|
||||
"is-observable": "^1.1.0",
|
||||
"is-promise": "^2.1.0",
|
||||
"is-stream": "^1.1.0",
|
||||
"listr-silent-renderer": "^1.1.1",
|
||||
"listr-update-renderer": "^0.4.0",
|
||||
"listr-verbose-renderer": "^0.4.0",
|
||||
"p-map": "^1.1.1",
|
||||
"rxjs": "^6.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"rxjs": {
|
||||
"version": "6.3.3",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz",
|
||||
"integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"listr-silent-renderer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz",
|
||||
"integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=",
|
||||
"dev": true
|
||||
},
|
||||
"listr-update-renderer": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz",
|
||||
"integrity": "sha1-NE2YDaLKLosUW6MFkI8yrj9MyKc=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^1.1.3",
|
||||
"cli-truncate": "^0.2.1",
|
||||
"elegant-spinner": "^1.0.1",
|
||||
"figures": "^1.7.0",
|
||||
"indent-string": "^3.0.0",
|
||||
"log-symbols": "^1.0.2",
|
||||
"log-update": "^1.0.2",
|
||||
"strip-ansi": "^3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-escapes": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz",
|
||||
"integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=",
|
||||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
||||
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^2.2.1",
|
||||
"escape-string-regexp": "^1.0.2",
|
||||
"has-ansi": "^2.0.0",
|
||||
"strip-ansi": "^3.0.0",
|
||||
"supports-color": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"cli-cursor": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz",
|
||||
"integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"restore-cursor": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"figures": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
|
||||
"integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"object-assign": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"log-symbols": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz",
|
||||
"integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"log-update": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz",
|
||||
"integrity": "sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-escapes": "^1.0.0",
|
||||
"cli-cursor": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"onetime": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
|
||||
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
|
||||
"dev": true
|
||||
},
|
||||
"restore-cursor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz",
|
||||
"integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"exit-hook": "^1.0.0",
|
||||
"onetime": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"listr-verbose-renderer": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz",
|
||||
"integrity": "sha1-ggb0z21S3cWCfl/RSYng6WWTOjU=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^1.1.3",
|
||||
"cli-cursor": "^1.0.2",
|
||||
"date-fns": "^1.27.2",
|
||||
"figures": "^1.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
||||
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^2.2.1",
|
||||
"escape-string-regexp": "^1.0.2",
|
||||
"has-ansi": "^2.0.0",
|
||||
"strip-ansi": "^3.0.0",
|
||||
"supports-color": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"cli-cursor": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz",
|
||||
"integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"restore-cursor": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"figures": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
|
||||
"integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"object-assign": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"onetime": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
|
||||
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
|
||||
"dev": true
|
||||
},
|
||||
"restore-cursor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz",
|
||||
"integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"exit-hook": "^1.0.0",
|
||||
"onetime": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"load-cfg": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/load-cfg/-/load-cfg-0.5.6.tgz",
|
||||
@@ -17322,6 +17926,15 @@
|
||||
"once": "^1.3.2"
|
||||
}
|
||||
},
|
||||
"npm-path": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/npm-path/-/npm-path-2.0.4.tgz",
|
||||
"integrity": "sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"which": "^1.2.10"
|
||||
}
|
||||
},
|
||||
"npm-run-all": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.3.tgz",
|
||||
@@ -17372,6 +17985,17 @@
|
||||
"path-key": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"npm-which": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-which/-/npm-which-3.0.1.tgz",
|
||||
"integrity": "sha1-kiXybsOihcIJyuZ8OxGmtKtxQKo=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"commander": "^2.9.0",
|
||||
"npm-path": "^2.0.2",
|
||||
"which": "^1.2.10"
|
||||
}
|
||||
},
|
||||
"nth-check": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz",
|
||||
@@ -18210,6 +18834,15 @@
|
||||
"find-up": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"please-upgrade-node": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz",
|
||||
"integrity": "sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"semver-compare": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"plugin-error": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz",
|
||||
@@ -21362,6 +21995,11 @@
|
||||
"relay-runtime": "1.7.0-rc.1"
|
||||
}
|
||||
},
|
||||
"react-relay-network-modern": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-relay-network-modern/-/react-relay-network-modern-2.4.0.tgz",
|
||||
"integrity": "sha512-LR/RhHcJclDCVEiwRhlRtf1iltSnbGSxS2rag+bAljMFJ0kOVSYUK3+NDPRbcHLRqbha1FuQXBVfHjjPE6jhMA=="
|
||||
},
|
||||
"react-responsive": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-5.0.0.tgz",
|
||||
@@ -22527,6 +23165,12 @@
|
||||
"is-promise": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"run-node": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz",
|
||||
"integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==",
|
||||
"dev": true
|
||||
},
|
||||
"run-queue": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
|
||||
@@ -22664,6 +23308,12 @@
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
|
||||
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
|
||||
},
|
||||
"semver-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
|
||||
"dev": true
|
||||
},
|
||||
"semver-diff": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz",
|
||||
@@ -23293,6 +23943,12 @@
|
||||
"integrity": "sha512-to7oADIniaYwS3MhtCa/sQhrxidCCQiF/qp4/m5iN3ipf0Y7Xlri0f6eG29r08aL7JYl8n32AF3Q5GYBZ7K8vw==",
|
||||
"dev": true
|
||||
},
|
||||
"staged-git-files": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/staged-git-files/-/staged-git-files-1.1.1.tgz",
|
||||
"integrity": "sha512-H89UNKr1rQJvI1c/PIR3kiAMBV23yvR7LItZiV74HWZwzt7f3YHuujJ9nJZlt58WlFox7XQsOahexwk7nTe69A==",
|
||||
"dev": true
|
||||
},
|
||||
"state-toggle": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.1.tgz",
|
||||
@@ -23397,6 +24053,12 @@
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
|
||||
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
|
||||
},
|
||||
"string-argv": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.0.2.tgz",
|
||||
"integrity": "sha1-2sMECGkMIfPDYwo/86BYd73L1zY=",
|
||||
"dev": true
|
||||
},
|
||||
"string-length": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz",
|
||||
@@ -23482,6 +24144,17 @@
|
||||
"is-hexadecimal": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"stringify-object": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.2.2.tgz",
|
||||
"integrity": "sha512-O696NF21oLiDy8PhpWu8AEqoZHw++QW6mUv0UvKZe8gWSdSvMXkiLufK7OmnP27Dro4GU5kb9U7JIO0mBuCRQg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"get-own-enumerable-property-symbols": "^2.0.1",
|
||||
"is-obj": "^1.0.1",
|
||||
"is-regexp": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
@@ -25305,11 +25978,6 @@
|
||||
"spdx-expression-parse": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"validator": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-7.2.0.tgz",
|
||||
"integrity": "sha512-c8NGTUYeBEcUIGeMppmNVKHE7wwfm3mYbNZxV+c5mlv9fDHI7Ad3p07qfNrn/CvpdkK2k61fOLRO2sTEhgQXmg=="
|
||||
},
|
||||
"value-equal": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz",
|
||||
|
||||
+33
-2
@@ -1,6 +1,18 @@
|
||||
{
|
||||
"name": "@coralproject/talk",
|
||||
"version": "5.0.0",
|
||||
"author": "The Coral Project",
|
||||
"homepage": "https://coralproject.net/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/coralproject/talk.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0",
|
||||
"npm": ">=6.4.1"
|
||||
},
|
||||
"bugs": "https://github.com/coralproject/talk/issues",
|
||||
"contributors": "https://github.com/coralproject/talk/graphs/contributors",
|
||||
"description": "A better commenting experience from Mozilla, The Washington Post, and The New York Times.",
|
||||
"scripts": {
|
||||
"build": "npm-run-all compile --parallel build:*",
|
||||
@@ -10,6 +22,7 @@
|
||||
"compile:css-types": "tcm src/core/client/",
|
||||
"compile:relay-stream": "ts-node ./scripts/compileRelay --src ./src/core/client/stream --schema tenant",
|
||||
"compile:relay-auth": "ts-node ./scripts/compileRelay --src ./src/core/client/auth --schema tenant",
|
||||
"compile:relay-install": "ts-node ./scripts/compileRelay --src ./src/core/client/install --schema tenant",
|
||||
"compile:relay-admin": "ts-node ./scripts/compileRelay --src ./src/core/client/admin --schema tenant",
|
||||
"compile:schema": "node ./scripts/generateSchemaTypes.js",
|
||||
"docz": "docz",
|
||||
@@ -30,7 +43,6 @@
|
||||
"tscheck:scripts": "tsc --project ./tsconfig.json --noEmit",
|
||||
"watch": "NODE_ENV=development ts-node ./scripts/watcher/bin/watcher.ts --config ./config/watcher.ts"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"akismet-api": "^4.2.0",
|
||||
@@ -38,9 +50,11 @@
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bull": "^3.4.4",
|
||||
"bunyan": "^1.8.12",
|
||||
"bunyan-prettystream": "^0.1.3",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
"consolidate": "0.14.0",
|
||||
"convict": "^4.3.1",
|
||||
"cors": "^2.8.4",
|
||||
"dataloader": "^1.4.0",
|
||||
"dotenv": "^6.0.0",
|
||||
"dotenv-expand": "^4.2.0",
|
||||
@@ -53,7 +67,7 @@
|
||||
"graphql": "^0.13.2",
|
||||
"graphql-config": "^2.0.1",
|
||||
"graphql-fields": "^1.1.0",
|
||||
"graphql-playground-middleware-express": "^1.7.2",
|
||||
"graphql-playground-html": "^1.6.0",
|
||||
"graphql-redis-subscriptions": "^1.5.0",
|
||||
"graphql-tools": "^3.0.5",
|
||||
"html-to-text": "^4.0.0",
|
||||
@@ -80,6 +94,7 @@
|
||||
"passport-strategy": "^1.0.0",
|
||||
"performance-now": "^2.1.0",
|
||||
"permit": "^0.2.4",
|
||||
"react-relay-network-modern": "^2.4.0",
|
||||
"striptags": "^3.1.1",
|
||||
"subscriptions-transport-ws": "^0.9.12",
|
||||
"tlds": "^1.203.1",
|
||||
@@ -96,6 +111,7 @@
|
||||
"@types/bcryptjs": "^2.4.1",
|
||||
"@types/bull": "^3.3.16",
|
||||
"@types/bunyan": "^1.8.4",
|
||||
"@types/bunyan-prettystream": "^0.1.31",
|
||||
"@types/case-sensitive-paths-webpack-plugin": "^2.1.2",
|
||||
"@types/cheerio": "^0.22.8",
|
||||
"@types/chokidar": "^1.7.5",
|
||||
@@ -104,6 +120,7 @@
|
||||
"@types/compression-webpack-plugin": "^0.4.2",
|
||||
"@types/consolidate": "0.0.34",
|
||||
"@types/convict": "^4.2.0",
|
||||
"@types/cors": "^2.8.4",
|
||||
"@types/cross-spawn": "^6.0.0",
|
||||
"@types/dompurify": "0.0.31",
|
||||
"@types/dotenv": "^4.0.3",
|
||||
@@ -154,6 +171,7 @@
|
||||
"@types/ws": "^5.1.2",
|
||||
"autoprefixer": "^8.6.5",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^23.6.0",
|
||||
"babel-loader": "^8.0.0-beta",
|
||||
"babel-plugin-module-resolver": "^3.1.1",
|
||||
"babel-plugin-relay": "^1.7.0-rc.1",
|
||||
@@ -189,11 +207,13 @@
|
||||
"gulp-sourcemaps": "^2.6.4",
|
||||
"gulp-typescript": "^5.0.0-alpha.3",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"husky": "^1.1.0",
|
||||
"jest": "^23.4.1",
|
||||
"jest-junit": "^5.1.0",
|
||||
"jest-localstorage-mock": "^2.2.0",
|
||||
"jest-mock-console": "^0.4.0",
|
||||
"jsdom": "^11.11.0",
|
||||
"lint-staged": "^7.3.0",
|
||||
"loader-utils": "^1.1.0",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"mini-css-extract-plugin": "^0.4.1",
|
||||
@@ -255,5 +275,16 @@
|
||||
"webpack-hot-client": "^4.1.1",
|
||||
"webpack-manifest-plugin": "^2.0.3",
|
||||
"whatwg-fetch": "^2.0.4"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,tsx}": [
|
||||
"tslint --fix",
|
||||
"git add"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
|
||||
import UglifyJsPlugin from "uglifyjs-webpack-plugin";
|
||||
import webpack, { Configuration } from "webpack";
|
||||
import ManifestPlugin from "webpack-manifest-plugin";
|
||||
import PublicURIWebpackPlugin from "./plugins/PublicURIWebpackPlugin";
|
||||
|
||||
import paths from "./paths";
|
||||
|
||||
@@ -39,6 +40,13 @@ export default function createWebpackConfig({
|
||||
|
||||
const isProduction = env.NODE_ENV === "production";
|
||||
|
||||
/**
|
||||
* ifProduction will only include the nodes if we're in production mode.
|
||||
*/
|
||||
const ifProduction = isProduction
|
||||
? <T extends {}>(...nodes: T[]) => nodes
|
||||
: <T extends {}>(...nodes: T[]) => [];
|
||||
|
||||
const htmlWebpackConfig: Options = {
|
||||
minify: isProduction && {
|
||||
removeComments: true,
|
||||
@@ -255,6 +263,22 @@ export default function createWebpackConfig({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: paths.appInstallLocalesTemplate,
|
||||
use: [
|
||||
// This is the locales loader that loads available locales
|
||||
// from a particular target.
|
||||
{
|
||||
loader: "locales-loader",
|
||||
options: {
|
||||
...localesOptions,
|
||||
// Target specifies the prefix for fluent files to be loaded.
|
||||
// ${target}-xyz.ftl and ${†arget}.ftl are loaded into the locales.
|
||||
target: "install",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// Loader for our fluent files.
|
||||
{
|
||||
test: /\.ftl$/,
|
||||
@@ -422,19 +446,29 @@ export default function createWebpackConfig({
|
||||
stream: [
|
||||
// We ship polyfills by default
|
||||
paths.appPolyfill,
|
||||
...ifProduction(paths.appPublicPath),
|
||||
...devServerEntries,
|
||||
paths.appStreamIndex,
|
||||
],
|
||||
auth: [
|
||||
// We ship polyfills by default
|
||||
paths.appPolyfill,
|
||||
...ifProduction(paths.appPublicPath),
|
||||
...devServerEntries,
|
||||
paths.appAuthIndex,
|
||||
// Remove deactivated entries.
|
||||
],
|
||||
install: [
|
||||
// We ship polyfills by default
|
||||
paths.appPolyfill,
|
||||
...ifProduction(paths.appPublicPath),
|
||||
...devServerEntries,
|
||||
paths.appInstallIndex,
|
||||
],
|
||||
admin: [
|
||||
// We ship polyfills by default
|
||||
paths.appPolyfill,
|
||||
...ifProduction(paths.appPublicPath),
|
||||
...devServerEntries,
|
||||
paths.appAdminIndex,
|
||||
],
|
||||
@@ -457,6 +491,14 @@ export default function createWebpackConfig({
|
||||
inject: "body",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
// Generates an `install.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "install.html",
|
||||
template: paths.appInstallHTML,
|
||||
chunks: ["install"],
|
||||
inject: "body",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
// Generates an `admin.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "admin.html",
|
||||
@@ -465,6 +507,15 @@ export default function createWebpackConfig({
|
||||
inject: "body",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
...ifProduction(
|
||||
// Inject the pieces we need here to resolve all the now relative url's
|
||||
// against the CDN if it's provided. It will inject the following into
|
||||
// the configuration blob on the page.
|
||||
new PublicURIWebpackPlugin(
|
||||
"{{ staticURI | dump | safe }}",
|
||||
"{{ staticURI }}"
|
||||
)
|
||||
),
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
|
||||
@@ -17,6 +17,7 @@ export default {
|
||||
appSrc: resolveSrc("."),
|
||||
appTsconfig: resolveSrc("core/client/tsconfig.json"),
|
||||
appPolyfill: resolveSrc("core/build/polyfills.js"),
|
||||
appPublicPath: resolveSrc("core/build/publicPath.js"),
|
||||
appLocales: resolveSrc("locales"),
|
||||
appThemeVariables: resolveSrc("core/client/ui/theme/variables.ts"),
|
||||
appThemeVariablesCSS: resolveSrc("core/client/ui/theme/variables.css"),
|
||||
@@ -29,6 +30,10 @@ export default {
|
||||
appAuthLocalesTemplate: resolveSrc("core/client/auth/locales.ts"),
|
||||
appAuthIndex: resolveSrc("core/client/auth/index.tsx"),
|
||||
|
||||
appInstallHTML: resolveSrc("core/client/install/index.html"),
|
||||
appInstallLocalesTemplate: resolveSrc("core/client/install/locales.ts"),
|
||||
appInstallIndex: resolveSrc("core/client/install/index.tsx"),
|
||||
|
||||
appAdminHTML: resolveSrc("core/client/admin/index.html"),
|
||||
appAdminLocalesTemplate: resolveSrc("core/client/admin/locales.ts"),
|
||||
appAdminIndex: resolveSrc("core/client/admin/index.tsx"),
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Hooks } from "html-webpack-plugin";
|
||||
import { Compiler, Plugin } from "webpack";
|
||||
|
||||
export default class PublicURIWebpackPlugin implements Plugin {
|
||||
private configTemplate: string;
|
||||
private prefixTemplate: string;
|
||||
|
||||
constructor(configTemplate: string, prefixTemplate: string) {
|
||||
this.configTemplate = configTemplate;
|
||||
this.prefixTemplate = prefixTemplate;
|
||||
}
|
||||
|
||||
private prefixAttribute(attr: string | boolean) {
|
||||
if (!attr || typeof attr !== "string" || !attr.startsWith("/")) {
|
||||
return attr;
|
||||
}
|
||||
|
||||
return this.prefixTemplate + attr;
|
||||
}
|
||||
|
||||
private prefixTag = (tag: {
|
||||
tagName: string;
|
||||
attributes: Record<string, string | boolean>;
|
||||
}) => {
|
||||
switch (tag.tagName) {
|
||||
case "link":
|
||||
tag.attributes.href = this.prefixAttribute(tag.attributes.href);
|
||||
break;
|
||||
case "script":
|
||||
tag.attributes.src = this.prefixAttribute(tag.attributes.src);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
public apply = (compiler: Compiler) => {
|
||||
compiler.hooks.compilation.tap("CDNWebpackPlugin", compilation => {
|
||||
(compilation.hooks as Hooks).htmlWebpackPluginAlterAssetTags.tapAsync(
|
||||
"CDNWebpackPlugin",
|
||||
(htmlPluginData, cb) => {
|
||||
// Prefix all the asset's url's with the template.
|
||||
htmlPluginData.head.forEach(this.prefixTag);
|
||||
htmlPluginData.body.forEach(this.prefixTag);
|
||||
|
||||
// Insert the public path reference.
|
||||
htmlPluginData.body.unshift({
|
||||
tagName: "script",
|
||||
attributes: {
|
||||
type: "application/json",
|
||||
id: "config",
|
||||
},
|
||||
innerHTML: this.configTemplate,
|
||||
voidTag: false,
|
||||
});
|
||||
|
||||
return cb(null, htmlPluginData);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
__webpack_public_path__ = JSON.parse(
|
||||
document.getElementById("config").innerText
|
||||
);
|
||||
@@ -15,7 +15,13 @@ import PymControl, {
|
||||
defaultPymControlFactory,
|
||||
PymControlFactory,
|
||||
} from "./PymControl";
|
||||
import { ensureEndSlash } from "./utils";
|
||||
import { ensureNoEndSlash } from "./utils";
|
||||
|
||||
// This is importing the url helper from the framework using a relative path
|
||||
// import because the ts paths are not configured to use the framework for this
|
||||
// target.
|
||||
// TODO: (wyattjoh) replace with import from framework when we include it in the config.
|
||||
import urls from "../framework/helpers/urls";
|
||||
|
||||
export interface StreamEmbedConfig {
|
||||
assetID?: string;
|
||||
@@ -110,7 +116,8 @@ export class StreamEmbed {
|
||||
assetURL: this.config.assetURL,
|
||||
commentID: this.config.commentID,
|
||||
});
|
||||
const url = `${ensureEndSlash(this.config.rootURL)}stream.html${
|
||||
|
||||
const url = `${ensureNoEndSlash(this.config.rootURL)}${urls.embed.stream}${
|
||||
query ? `?${query}` : ""
|
||||
}`;
|
||||
this.pymControl = this.pymControlFactory({
|
||||
|
||||
@@ -4,7 +4,7 @@ exports[`should pass correct values to pymControl 1`] = `
|
||||
Object {
|
||||
"id": "container-id",
|
||||
"title": "StreamEmbed",
|
||||
"url": "http://localhost/stream.html?assetID=asset-id&assetURL=asset-url&commentID=comment-id",
|
||||
"url": "http://localhost/embed/stream?assetID=asset-id&assetURL=asset-url&commentID=comment-id",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -12,6 +12,6 @@ exports[`should pass default values to pymControl 1`] = `
|
||||
Object {
|
||||
"id": "container-id",
|
||||
"title": "StreamEmbed",
|
||||
"url": "http://localhost/stream.html",
|
||||
"url": "http://localhost/embed/stream",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Basic integration test should render iframe 1`] = `"<iframe src=\\"http://localhost/stream.html?assetURL=http%3A%2F%2Flocalhost%2F&initialWidth=0&childId=basic-integration-test-id&parentTitle=&parentUrl=http%3A%2F%2Flocalhost%2F\\" width=\\"100%\\" scrolling=\\"no\\" marginheight=\\"0\\" frameborder=\\"0\\" title=\\"Talk Embed Stream\\" id=\\"basic-integration-test-id_iframe\\" name=\\"basic-integration-test-id_iframe\\" style=\\"width: 1px; min-width: 100%;\\"></iframe>"`;
|
||||
exports[`Basic integration test should render iframe 1`] = `"<iframe src=\\"http://localhost/embed/stream?assetURL=http%3A%2F%2Flocalhost%2F&initialWidth=0&childId=basic-integration-test-id&parentTitle=&parentUrl=http%3A%2F%2Flocalhost%2F\\" width=\\"100%\\" scrolling=\\"no\\" marginheight=\\"0\\" frameborder=\\"0\\" title=\\"Talk Embed Stream\\" id=\\"basic-integration-test-id_iframe\\" name=\\"basic-integration-test-id_iframe\\" style=\\"width: 1px; min-width: 100%;\\"></iframe>"`;
|
||||
|
||||
exports[`Basic integration test should use canonical link 1`] = `"<iframe src=\\"http://localhost/stream.html?assetURL=http%3A%2F%2Flocalhost%2Fcanonical&initialWidth=0&childId=basic-integration-test-id&parentTitle=&parentUrl=http%3A%2F%2Flocalhost%2F\\" width=\\"100%\\" scrolling=\\"no\\" marginheight=\\"0\\" frameborder=\\"0\\" title=\\"Talk Embed Stream\\" name=\\"basic-integration-test-id_iframe\\" style=\\"width: 1px; min-width: 100%;\\"></iframe>"`;
|
||||
exports[`Basic integration test should use canonical link 1`] = `"<iframe src=\\"http://localhost/embed/stream?assetURL=http%3A%2F%2Flocalhost%2Fcanonical&initialWidth=0&childId=basic-integration-test-id&parentTitle=&parentUrl=http%3A%2F%2Flocalhost%2F\\" width=\\"100%\\" scrolling=\\"no\\" marginheight=\\"0\\" frameborder=\\"0\\" title=\\"Talk Embed Stream\\" name=\\"basic-integration-test-id_iframe\\" style=\\"width: 1px; min-width: 100%;\\"></iframe>"`;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "es5"],
|
||||
"types": ["jest"],
|
||||
"types": ["jest", "node"],
|
||||
"paths": {}
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import ensureNoEndSlash from "./ensureNoEndSlash";
|
||||
|
||||
it("should remove the slash from the end", () => {
|
||||
const path = ensureNoEndSlash("/test/");
|
||||
expect(path).toBe("/test");
|
||||
});
|
||||
|
||||
it("should not change a string if there is no ending slash", () => {
|
||||
const path = ensureNoEndSlash("/test");
|
||||
expect(path).toBe("/test");
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function ensureEndSlash(p: string) {
|
||||
return p.replace(/\/$/, "");
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as buildURL } from "./buildURL";
|
||||
export { default as ensureEndSlash } from "./ensureEndSlash";
|
||||
export { default as ensureNoEndSlash } from "./ensureNoEndSlash";
|
||||
export { default as startsWith } from "./startsWith";
|
||||
export { default as prefixStorage } from "./prefixStorage";
|
||||
export { default as parseHashQuery } from "./parseHashQuery";
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as getMe } from "./getMe";
|
||||
export { default as getURLWithCommentID } from "./getURLWithCommentID";
|
||||
export { default as urls } from "./urls";
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export default (process.env.NODE_ENV !== "development"
|
||||
? {
|
||||
admin: "/admin",
|
||||
embed: {
|
||||
stream: "/embed/stream",
|
||||
auth: "/embed/auth",
|
||||
},
|
||||
}
|
||||
: {
|
||||
admin: "/admin.html",
|
||||
embed: {
|
||||
stream: "/stream.html",
|
||||
auth: "/auth.html",
|
||||
},
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { noop } from "lodash";
|
||||
import { Child as PymChild } from "pym.js";
|
||||
import React, { Component, ComponentType } from "react";
|
||||
import { Formatter } from "react-timeago";
|
||||
import { Environment, Network, RecordSource, Store } from "relay-runtime";
|
||||
import { Environment, RecordSource, Store } from "relay-runtime";
|
||||
import uuid from "uuid/v4";
|
||||
|
||||
import { getBrowserInfo } from "talk-framework/lib/browserInfo";
|
||||
@@ -21,7 +21,7 @@ import { RestClient } from "talk-framework/lib/rest";
|
||||
import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
|
||||
|
||||
import { generateBundles, LocalesData, negotiateLanguages } from "../i18n";
|
||||
import { createFetch, TokenGetter } from "../network";
|
||||
import { createNetwork, TokenGetter } from "../network";
|
||||
import { PostMessageService } from "../postMessage";
|
||||
import { TalkContext, TalkContextProvider } from "./TalkContext";
|
||||
|
||||
@@ -97,7 +97,7 @@ function createRelayEnvironment() {
|
||||
return "";
|
||||
};
|
||||
const environment = new Environment({
|
||||
network: Network.create(createFetch(tokenGetter)),
|
||||
network: createNetwork(tokenGetter),
|
||||
store: new Store(source),
|
||||
});
|
||||
return { environment, tokenGetter };
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
export interface GraphQLErrorItem {
|
||||
message: string;
|
||||
locations: Array<{
|
||||
line: number;
|
||||
column: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Graphql wraps graphql errors at the network layer.
|
||||
*/
|
||||
export default class GraphQLError extends Error {
|
||||
// Original error.
|
||||
public readonly origin: GraphQLErrorItem[];
|
||||
|
||||
constructor(origin: GraphQLErrorItem[]) {
|
||||
super(origin.map(o => o.message).join(" "));
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown.
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, GraphQLError);
|
||||
}
|
||||
this.origin = origin;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,2 @@
|
||||
export { default as NetworkError } from "./networkError";
|
||||
export { default as UnknownServerError } from "./unknownServerError";
|
||||
export { default as BadUserInputError } from "./badUserInputError";
|
||||
export { default as GraphQLError } from "./graphqlError";
|
||||
|
||||
export * from "./graphqlError";
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* NetworkError wraps errors at the network layer.
|
||||
*/
|
||||
export default class NetworkError extends Error {
|
||||
// Original error.
|
||||
public readonly origin: Error;
|
||||
|
||||
constructor(origin: Error) {
|
||||
// Pass remaining arguments (including vendor specific ones) to parent constructor.
|
||||
super(origin.message);
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown.
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, NetworkError);
|
||||
}
|
||||
this.origin = origin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FluentNumber, FluentType } from "fluent/compat";
|
||||
|
||||
import { FluentShortNumber } from "../types";
|
||||
|
||||
export default function SHORT_NUMBER([t]: [FluentType]) {
|
||||
if (!(t instanceof FluentNumber)) {
|
||||
throw new Error(`Invalid argument for SHORT_NUMBER ${t.valueOf()}`);
|
||||
}
|
||||
return new FluentShortNumber(t.valueOf());
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SHORT_NUMBER } from "./SHORT_NUMBER";
|
||||
+4
-46
@@ -1,50 +1,8 @@
|
||||
import "fluent-intl-polyfill/compat";
|
||||
import { negotiateLanguages as negotiate } from "fluent-langneg/compat";
|
||||
import { FluentBundle } from "fluent/compat";
|
||||
|
||||
export interface BundledLocales {
|
||||
[locale: string]: string;
|
||||
}
|
||||
|
||||
export interface LoadableLocales {
|
||||
[locale: string]: (() => Promise<string>);
|
||||
}
|
||||
|
||||
/**
|
||||
* This type describes the shape of the generated code from our `locales-loader`.
|
||||
* Please check `./src/loaders` and the webpack config for more information.
|
||||
*/
|
||||
export interface LocalesData {
|
||||
readonly defaultLocale: string;
|
||||
readonly fallbackLocale: string;
|
||||
readonly availableLocales: ReadonlyArray<string>;
|
||||
readonly bundled: BundledLocales;
|
||||
readonly loadables: LoadableLocales;
|
||||
}
|
||||
|
||||
/**
|
||||
* negotiateLanguages accepts `userLocales` which usually comes from
|
||||
* `navigator.languages` and the locales `data` as generated by
|
||||
* the `locales-loader` and returns an array of matching languages.
|
||||
*/
|
||||
export function negotiateLanguages(
|
||||
userLocales: ReadonlyArray<string>,
|
||||
data: LocalesData
|
||||
) {
|
||||
// Choose locale that is best for the user.
|
||||
const languages = negotiate(userLocales, data.availableLocales, {
|
||||
defaultLocale: data.defaultLocale,
|
||||
strategy: "lookup",
|
||||
});
|
||||
|
||||
if (data.fallbackLocale && languages[0] !== data.fallbackLocale) {
|
||||
// Use default locale as fallback in case we have
|
||||
// missing keys.
|
||||
languages.push(data.fallbackLocale);
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
import * as functions from "./functions";
|
||||
import { LocalesData } from "./locales";
|
||||
|
||||
// Don't warn in production.
|
||||
let decorateWarnMissing = (bundle: FluentBundle) => bundle;
|
||||
@@ -81,14 +39,14 @@ if (process.env.NODE_ENV !== "production") {
|
||||
*
|
||||
* Use it in conjunction with `negotiateLanguages`.
|
||||
*/
|
||||
export async function generateBundles(
|
||||
export default async function generateBundles(
|
||||
locales: ReadonlyArray<string>,
|
||||
data: LocalesData
|
||||
): Promise<FluentBundle[]> {
|
||||
const promises = [];
|
||||
|
||||
for (const locale of locales) {
|
||||
const bundle = new FluentBundle(locale);
|
||||
const bundle = new FluentBundle(locale, { functions });
|
||||
if (locale in data.bundled) {
|
||||
bundle.addMessages(data.bundled[locale]);
|
||||
promises.push(decorateWarnMissing(bundle));
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as generateBundles } from "./generateBundles";
|
||||
export { default as negotiateLanguages } from "./negotiateLanguages";
|
||||
export { BundledLocales, LoadableLocales, LocalesData } from "./locales";
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface BundledLocales {
|
||||
[locale: string]: string;
|
||||
}
|
||||
|
||||
export interface LoadableLocales {
|
||||
[locale: string]: (() => Promise<string>);
|
||||
}
|
||||
|
||||
/**
|
||||
* This type describes the shape of the generated code from our `locales-loader`.
|
||||
* Please check `./src/loaders` and the webpack config for more information.
|
||||
*/
|
||||
export interface LocalesData {
|
||||
readonly defaultLocale: string;
|
||||
readonly fallbackLocale: string;
|
||||
readonly availableLocales: ReadonlyArray<string>;
|
||||
readonly bundled: BundledLocales;
|
||||
readonly loadables: LoadableLocales;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { negotiateLanguages as negotiate } from "fluent-langneg/compat";
|
||||
|
||||
import { LocalesData } from "./locales";
|
||||
|
||||
/**
|
||||
* negotiateLanguages accepts `userLocales` which usually comes from
|
||||
* `navigator.languages` and the locales `data` as generated by
|
||||
* the `locales-loader` and returns an array of matching languages.
|
||||
*/
|
||||
export default function negotiateLanguages(
|
||||
userLocales: ReadonlyArray<string>,
|
||||
data: LocalesData
|
||||
) {
|
||||
// Choose locale that is best for the user.
|
||||
const languages = negotiate(userLocales, data.availableLocales, {
|
||||
defaultLocale: data.defaultLocale,
|
||||
strategy: "lookup",
|
||||
});
|
||||
|
||||
if (data.fallbackLocale && languages[0] !== data.fallbackLocale) {
|
||||
// Use default locale as fallback in case we have
|
||||
// missing keys.
|
||||
languages.push(data.fallbackLocale);
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { toPairs } from "lodash";
|
||||
import { getShortNumberCode, validateFormat } from "./FluentShortNumber";
|
||||
|
||||
describe("getShortNumberCode", () => {
|
||||
it("returns correct value", () => {
|
||||
const cases = {
|
||||
123: "100",
|
||||
4322: "1000",
|
||||
33223: "10000",
|
||||
};
|
||||
toPairs(cases).forEach(([i, o]) => {
|
||||
expect(getShortNumberCode(parseFloat(i))).toBe(o);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateFormat", () => {
|
||||
it("returns correct value", () => {
|
||||
const cases = {
|
||||
"0k": true,
|
||||
"0kilo": true,
|
||||
"0.0": false,
|
||||
"0": false,
|
||||
"0.": false,
|
||||
"0.0k": true,
|
||||
"000.0k": true,
|
||||
"000M": true,
|
||||
};
|
||||
toPairs(cases).forEach(([i, o]) => {
|
||||
expect(validateFormat(i)).toBe(o);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { FluentBundle, FluentNumber, FluentType } from "fluent/compat";
|
||||
|
||||
const formatRegExp = /^(0+|0+\.0+)[^\d\.]+$/;
|
||||
|
||||
export function validateFormat(fmt: string) {
|
||||
return formatRegExp.test(fmt);
|
||||
}
|
||||
|
||||
export function getShortNumberCode(n: number) {
|
||||
let code = "1";
|
||||
while (n >= 10) {
|
||||
n /= 10;
|
||||
code += "0";
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
function formatShortNumber(n: number, format: string, bundle: FluentBundle) {
|
||||
const lastIndexOf0 = format.lastIndexOf("0");
|
||||
const unit = format.substr(lastIndexOf0 + 1);
|
||||
const rest = format.substr(0, lastIndexOf0 + 1);
|
||||
const splitted = rest.split(".");
|
||||
const digits = splitted[0].length;
|
||||
const fractalDigits = (splitted.length > 1 && splitted[1].length) || 0;
|
||||
const threshold = Math.pow(10, digits);
|
||||
while (n > threshold) {
|
||||
n /= 10;
|
||||
}
|
||||
const formattedNumber = new FluentNumber(n, {
|
||||
maximumFractionDigits: fractalDigits,
|
||||
}).toString(bundle);
|
||||
return `${formattedNumber}${unit}`;
|
||||
}
|
||||
|
||||
export default class FluentShortNumber extends FluentNumber {
|
||||
constructor(value: any, opts?: any) {
|
||||
super(value, opts);
|
||||
}
|
||||
|
||||
public toString(bundle: FluentBundle) {
|
||||
if (this.value < 1000) {
|
||||
return super.toString(bundle);
|
||||
}
|
||||
const key = `framework-shortNumber-${getShortNumberCode(this.value)}`;
|
||||
const fmt = bundle.getMessage(key);
|
||||
|
||||
// Handle message not found.
|
||||
if (!fmt) {
|
||||
const message = `Missing translation key for ${key} for languages ${bundle.locales.toString()}`;
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn(message);
|
||||
} else {
|
||||
throw new Error(message);
|
||||
}
|
||||
return super.toString(bundle);
|
||||
}
|
||||
|
||||
// Check for invalid message.
|
||||
if (!validateFormat(fmt)) {
|
||||
const message = `Invalid Short Number Format ${fmt}`;
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn(message);
|
||||
} else {
|
||||
throw new Error(message);
|
||||
}
|
||||
return super.toString(bundle);
|
||||
}
|
||||
|
||||
return formatShortNumber(this.value, fmt, bundle);
|
||||
}
|
||||
|
||||
public match(bundle: FluentBundle, other: FluentType) {
|
||||
if (other instanceof FluentShortNumber) {
|
||||
return this.value === other.valueOf;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as FluentShortNumber } from "./FluentShortNumber";
|
||||
@@ -53,3 +53,9 @@ export const PASSWORDS_DO_NOT_MATCH = () => (
|
||||
<span>Passwords do not match. Try again.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export const INVALID_URL = () => (
|
||||
<Localized id="framework-validation-invalidURL">
|
||||
<span>Invalid URL</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
authMiddleware,
|
||||
batchMiddleware,
|
||||
cacheMiddleware,
|
||||
RelayNetworkLayer,
|
||||
retryMiddleware,
|
||||
urlMiddleware,
|
||||
} from "react-relay-network-modern/es";
|
||||
|
||||
import customErrorMiddleware from "./customErrorMiddleware";
|
||||
|
||||
export type TokenGetter = () => string;
|
||||
|
||||
const graphqlURL = "/api/tenant/graphql";
|
||||
|
||||
export default function createNetwork(tokenGetter: TokenGetter) {
|
||||
return new RelayNetworkLayer([
|
||||
customErrorMiddleware,
|
||||
cacheMiddleware({
|
||||
size: 100, // max 100 requests
|
||||
ttl: 900000, // 15 minutes
|
||||
}),
|
||||
urlMiddleware({
|
||||
url: req => Promise.resolve(graphqlURL),
|
||||
}),
|
||||
batchMiddleware({
|
||||
batchUrl: (requestMap: any) => Promise.resolve(graphqlURL),
|
||||
batchTimeout: 10,
|
||||
}),
|
||||
retryMiddleware({
|
||||
fetchTimeout: 15000,
|
||||
retryDelays: (attempt: number) => Math.pow(2, attempt + 4) * 100,
|
||||
// or simple array [3200, 6400, 12800, 25600, 51200, 102400, 204800, 409600],
|
||||
statusCodes: [500, 503, 504],
|
||||
}),
|
||||
authMiddleware({
|
||||
token: tokenGetter,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Middleware } from "react-relay-network-modern/es";
|
||||
import { BadUserInputError, UnknownServerError } from "../errors";
|
||||
|
||||
function getError(errors: Error[]): Error | null {
|
||||
if (errors.length > 1 || !(errors[0] as any).extensions) {
|
||||
// Multiple errors are GraphQL errors.
|
||||
// TODO: (cvle) Is this assumption correct?
|
||||
// No extensions == GraphQL error.
|
||||
// TODO: (cvle) harmonize with server.
|
||||
return null;
|
||||
}
|
||||
const err = errors[0];
|
||||
if ((err as any).code === "BAD_USER_INPUT") {
|
||||
return new BadUserInputError((err as any).extensions);
|
||||
}
|
||||
return new UnknownServerError(err.message, (err as any).extensions);
|
||||
}
|
||||
|
||||
const customErrorMiddleware: Middleware = next => async req => {
|
||||
const res = await next(req);
|
||||
if (req.isMutation() && res.errors) {
|
||||
// Extract custom error.
|
||||
const error = getError(res.errors);
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
export default customErrorMiddleware;
|
||||
@@ -1,72 +0,0 @@
|
||||
import { FetchFunction } from "relay-runtime";
|
||||
|
||||
import {
|
||||
BadUserInputError,
|
||||
GraphQLError,
|
||||
NetworkError,
|
||||
UnknownServerError,
|
||||
} from "../errors";
|
||||
|
||||
// Normalize errors.
|
||||
function getError(errors: Error[]): Error {
|
||||
if (errors.length > 1) {
|
||||
// Multiple errors are GraphQL errors.
|
||||
// TODO: (cvle) Is this assumption correct?
|
||||
return new GraphQLError(errors as any);
|
||||
}
|
||||
const err = errors[0] as Error;
|
||||
if ((err as any).extensions) {
|
||||
if ((err as any).code === "BAD_USER_INPUT") {
|
||||
return new BadUserInputError((err as any).extensions);
|
||||
}
|
||||
return new UnknownServerError(err.message, (err as any).extensions);
|
||||
}
|
||||
// No extensions == GraphQL error.
|
||||
// TODO: (cvle) harmonize with server.
|
||||
return new GraphQLError(errors as any);
|
||||
}
|
||||
|
||||
export type TokenGetter = () => string;
|
||||
type CreateFetch = (token?: TokenGetter) => FetchFunction;
|
||||
|
||||
/**
|
||||
* createFetch returns a simple implementation of the `FetchFunction`
|
||||
* required by Relay. It'll return a `NetworkError` on failure.
|
||||
*/
|
||||
const createFetch: CreateFetch = tokenGetter => async (
|
||||
operation,
|
||||
variables
|
||||
) => {
|
||||
const token = tokenGetter && tokenGetter();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (token) {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
try {
|
||||
const response = await fetch("/api/tenant/graphql", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
query: operation.text,
|
||||
variables,
|
||||
}),
|
||||
});
|
||||
if (response.status >= 500) {
|
||||
throw new Error(`${response.status} ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data.errors) {
|
||||
throw getError(data.errors);
|
||||
}
|
||||
return data;
|
||||
} catch (err) {
|
||||
if (err instanceof TypeError) {
|
||||
throw new NetworkError(err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export default createFetch;
|
||||
@@ -1 +1 @@
|
||||
export { default as createFetch, TokenGetter } from "./fetchQuery";
|
||||
export { default as createNetwork, TokenGetter } from "./createNetwork";
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ReactNode } from "react";
|
||||
import {
|
||||
INVALID_CHARACTERS,
|
||||
INVALID_EMAIL,
|
||||
INVALID_URL,
|
||||
PASSWORD_TOO_SHORT,
|
||||
PASSWORDS_DO_NOT_MATCH,
|
||||
USERNAME_TOO_LONG,
|
||||
@@ -56,6 +57,17 @@ export const validateUsernameCharacters = createValidator(
|
||||
INVALID_CHARACTERS()
|
||||
);
|
||||
|
||||
/**
|
||||
* validateURL is a Validator that checks that the URL only contains valid characters.
|
||||
*/
|
||||
export const validateURL = createValidator(
|
||||
v =>
|
||||
/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/.test(
|
||||
v
|
||||
),
|
||||
INVALID_URL()
|
||||
);
|
||||
|
||||
/**
|
||||
* validateUsernameMinLength is a Validator that checks that the username has a min length of characters
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as signIn, SignInInput } from "./signIn";
|
||||
export { default as signUp, SignUpInput } from "./signUp";
|
||||
export { default as signOut } from "./signOut";
|
||||
export { default as install, InstallInput } from "./install";
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { RestClient } from "../lib/rest";
|
||||
|
||||
export interface InstallInput {
|
||||
tenant: {
|
||||
organizationName: string;
|
||||
organizationContactEmail: string;
|
||||
organizationURL: string;
|
||||
domains: string[];
|
||||
};
|
||||
user: {
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function install(rest: RestClient, input: InstallInput) {
|
||||
return rest.fetch("/tenant/install", {
|
||||
method: "POST",
|
||||
body: input,
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import "fluent-intl-polyfill/compat";
|
||||
import { FluentBundle } from "fluent/compat";
|
||||
|
||||
import * as functions from "talk-framework/lib/i18n/functions";
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
@@ -34,7 +38,7 @@ function createFluentBundle(
|
||||
target: string,
|
||||
pathToLocale: string
|
||||
): FluentBundle {
|
||||
const bundle = new FluentBundle("en-US");
|
||||
const bundle = new FluentBundle("en-US", { functions });
|
||||
const files = fs.readdirSync(pathToLocale);
|
||||
const prefixes = commonPrefixes.concat(target);
|
||||
files.forEach(f => {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
export function denormalizeComment(comment: any, parents: any[] = []) {
|
||||
const replyNodes =
|
||||
(comment.replies &&
|
||||
comment.replies.edges.map((edge: any) =>
|
||||
denormalizeComment(edge, [...parents, comment])
|
||||
)) ||
|
||||
[];
|
||||
const repliesPageInfo = (comment.replies && comment.replies.pageInfo) || {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
};
|
||||
return {
|
||||
...comment,
|
||||
replies: { edges: replyNodes, pageInfo: repliesPageInfo },
|
||||
replyCount: replyNodes.length,
|
||||
parentCount: parents.length,
|
||||
parents: {
|
||||
edges: parents,
|
||||
pageInfo: { startCursor: null, hasPreviousPage: false },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function denormalizeComments(commentList: any[]) {
|
||||
return commentList.map(c => denormalizeComment(c));
|
||||
}
|
||||
|
||||
export function denormalizeAsset(asset: any) {
|
||||
const commentNodes =
|
||||
(asset.comments &&
|
||||
asset.comments.edges.map((edge: any) => denormalizeComment(edge))) ||
|
||||
[];
|
||||
const commentsPageInfo = (asset.comments && asset.comments.pageInfo) || {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
};
|
||||
return {
|
||||
...asset,
|
||||
comments: { edges: commentNodes, pageInfo: commentsPageInfo },
|
||||
commentCounts: {
|
||||
totalVisible: commentNodes.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function denormalizeAssets(assetList: any[]) {
|
||||
return assetList.map(a => denormalizeAsset(a));
|
||||
}
|
||||
@@ -9,3 +9,4 @@ export {
|
||||
NoFragmentRefs,
|
||||
} from "./removeFragmentRefs";
|
||||
export { default as createUUIDGenerator } from "./createUUIDGenerator";
|
||||
export * from "./denormalize";
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
const path = require("path");
|
||||
module.exports = {
|
||||
extends: "../.babelrc.js",
|
||||
plugins: [
|
||||
[
|
||||
"babel-plugin-relay",
|
||||
{ artifactDirectory: path.resolve(__dirname, "./__generated__") },
|
||||
],
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
/* 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;
|
||||
background-color: var(--palette-background-light);
|
||||
color: var(--palette-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.root {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--spacing-unit) var(--spacing-unit) calc(2 * var(--spacing-unit))
|
||||
var(--spacing-unit);
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const wrapper = shallow(<App />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import React, { Component } from "react";
|
||||
import * as styles from "./App.css";
|
||||
|
||||
import WizardContainer from "../containers/WizardContainer";
|
||||
import MainBar from "./MainBar";
|
||||
|
||||
class App extends Component {
|
||||
public render() {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<MainBar />
|
||||
<div className={styles.container}>
|
||||
<WizardContainer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,20 @@
|
||||
.root {
|
||||
padding: calc(4 * var(--spacing-unit)) 0;
|
||||
}
|
||||
|
||||
.headline {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.headlineMain {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.subHeadline {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.subHeadlineMain {
|
||||
font-size: 2rem;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import cn from "classnames";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import { Flex, Typography } from "talk-ui/components";
|
||||
|
||||
import * as styles from "./Header.css";
|
||||
|
||||
interface HeaderProps {
|
||||
main?: boolean;
|
||||
}
|
||||
|
||||
const Header: StatelessComponent<HeaderProps> = ({ main }) => {
|
||||
return (
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
direction="column"
|
||||
className={styles.root}
|
||||
>
|
||||
<Typography
|
||||
className={cn(styles.headline, {
|
||||
[styles.headlineMain]: main,
|
||||
})}
|
||||
>
|
||||
The Coral Project
|
||||
</Typography>
|
||||
<Localized id="install-header-title">
|
||||
<Typography
|
||||
className={cn(styles.subHeadline, {
|
||||
[styles.subHeadlineMain]: main,
|
||||
})}
|
||||
variant="heading1"
|
||||
>
|
||||
Talk Installation Wizard
|
||||
</Typography>
|
||||
</Localized>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,26 @@
|
||||
.root {
|
||||
width: 100%;
|
||||
border-bottom: 2px solid var(--palette-grey-main);
|
||||
padding: var(--spacing-unit);
|
||||
box-sizing: border-box;
|
||||
background: var(--palette-text-primary);
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--palette-text-primary) 0%,
|
||||
var(--palette-grey-main) 100%
|
||||
);
|
||||
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 10px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--palette-text-light);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
import { Typography } from "talk-ui/components";
|
||||
|
||||
import * as styles from "./MainBar.css";
|
||||
|
||||
const MainBar = () => {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.container}>
|
||||
<Typography variant="heading1" className={styles.title}>
|
||||
Talk
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainBar;
|
||||
@@ -0,0 +1,13 @@
|
||||
.root {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.stepBar {
|
||||
padding: var(--spacing-unit) 0 calc(6 * var(--spacing-unit));
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import cn from "classnames";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { Component, ReactNode } from "react";
|
||||
|
||||
import { Step, StepBar } from "talk-ui/components";
|
||||
|
||||
import { WizardProps } from "../components/Wizard";
|
||||
import Header from "./Header";
|
||||
import * as styles from "./Wizard.css";
|
||||
|
||||
export interface WizardProps {
|
||||
currentStep: number;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
class Wizard extends Component<WizardProps> {
|
||||
public render() {
|
||||
const { children, currentStep, className } = this.props;
|
||||
|
||||
const wizardChildren = React.Children.toArray(children);
|
||||
const wizardChildrenToRender = wizardChildren.filter(
|
||||
(_, i) => i === currentStep
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn(className, styles.root)}>
|
||||
<Header main={currentStep === 0} />
|
||||
{currentStep !== 0 &&
|
||||
currentStep !== wizardChildren.length - 1 && (
|
||||
<StepBar currentStep={currentStep - 1} className={styles.stepBar}>
|
||||
<Step hidden>Start</Step>
|
||||
<Step>
|
||||
<Localized id="install-createYourAccount-stepTitle">
|
||||
<span>Create Admin Account</span>
|
||||
</Localized>
|
||||
</Step>
|
||||
<Step>
|
||||
<Localized id="install-addOrganization-stepTitle">
|
||||
<span>Add Organization Details</span>
|
||||
</Localized>
|
||||
</Step>
|
||||
<Step>
|
||||
<Localized id="install-permittedDomains-stepTitle">
|
||||
<span>Add Permitted Domains</span>
|
||||
</Localized>
|
||||
</Step>
|
||||
<Step hidden>Finish</Step>
|
||||
</StepBar>
|
||||
)}
|
||||
<section className={styles.section}>{wizardChildrenToRender}</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Wizard;
|
||||
@@ -0,0 +1,14 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<div
|
||||
className="App-root"
|
||||
>
|
||||
<MainBar />
|
||||
<div
|
||||
className="App-container"
|
||||
>
|
||||
<withContext(createMutationContainer(WizardContainer)) />
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,124 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
import Wizard from "../components/Wizard";
|
||||
import { InstallMutation, withInstallMutation } from "../mutations";
|
||||
import FinalStep from "../steps/components/FinalStep";
|
||||
import InitialStep from "../steps/components/InitialStep";
|
||||
import AddOrganizationContainer from "../steps/containers/AddOrganizationContainer";
|
||||
import CreateYourAccountContainer from "../steps/containers/CreateYourAccountContainer";
|
||||
import PermittedDomainsContainer from "../steps/containers/PermittedDomainsContainer";
|
||||
|
||||
import { InstallInput } from "talk-framework/rest";
|
||||
|
||||
interface FormData {
|
||||
organizationName: string;
|
||||
organizationContactEmail: string;
|
||||
organizationURL: string;
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
domains: string[];
|
||||
}
|
||||
|
||||
interface WizardContainerState {
|
||||
step: number;
|
||||
data: FormData;
|
||||
}
|
||||
|
||||
function shapeFinalData(data: FormData): InstallInput {
|
||||
const {
|
||||
organizationName,
|
||||
organizationContactEmail,
|
||||
organizationURL,
|
||||
domains,
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
} = data;
|
||||
|
||||
return {
|
||||
tenant: {
|
||||
organizationName,
|
||||
organizationContactEmail,
|
||||
organizationURL,
|
||||
domains,
|
||||
},
|
||||
user: {
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
install: InstallMutation;
|
||||
}
|
||||
|
||||
class WizardContainer extends Component<Props, WizardContainerState> {
|
||||
public state = {
|
||||
step: 0,
|
||||
data: {
|
||||
organizationContactEmail: "",
|
||||
organizationName: "",
|
||||
organizationURL: "",
|
||||
email: "",
|
||||
username: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
domains: [],
|
||||
},
|
||||
};
|
||||
|
||||
private handleSaveData = (newData: FormData) => {
|
||||
this.setState(({ data }) => ({
|
||||
data: { ...data, ...newData },
|
||||
}));
|
||||
};
|
||||
|
||||
private handleGoToNextStep = () =>
|
||||
this.setState(({ step }) => ({
|
||||
step: step + 1,
|
||||
}));
|
||||
|
||||
private handleGoToPreviousStep = () =>
|
||||
this.setState(({ step }) => ({
|
||||
step: step - 1,
|
||||
}));
|
||||
|
||||
private handleInstall = async (newData: Partial<FormData>) => {
|
||||
return await this.props.install(
|
||||
shapeFinalData({ ...this.state.data, ...newData })
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Wizard currentStep={this.state.step}>
|
||||
<InitialStep onGoToNextStep={this.handleGoToNextStep} />
|
||||
<CreateYourAccountContainer
|
||||
data={this.state.data}
|
||||
onSaveData={this.handleSaveData}
|
||||
onGoToNextStep={this.handleGoToNextStep}
|
||||
onGoToPreviousStep={this.handleGoToPreviousStep}
|
||||
/>
|
||||
<AddOrganizationContainer
|
||||
data={this.state.data}
|
||||
onSaveData={this.handleSaveData}
|
||||
onGoToNextStep={this.handleGoToNextStep}
|
||||
onGoToPreviousStep={this.handleGoToPreviousStep}
|
||||
/>
|
||||
<PermittedDomainsContainer
|
||||
data={this.state.data}
|
||||
onGoToNextStep={this.handleGoToNextStep}
|
||||
onGoToPreviousStep={this.handleGoToPreviousStep}
|
||||
onInstall={this.handleInstall}
|
||||
/>
|
||||
<FinalStep />
|
||||
</Wizard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withInstallMutation(WizardContainer);
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Talk - Install</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import { createManaged } from "talk-framework/lib/bootstrap";
|
||||
|
||||
import App from "./components/App";
|
||||
import localesData from "./locales";
|
||||
|
||||
async function main() {
|
||||
const ManagedTalkContextProvider = await createManaged({
|
||||
localesData,
|
||||
userLocales: navigator.languages,
|
||||
});
|
||||
|
||||
const Index: StatelessComponent = () => (
|
||||
<ManagedTalkContextProvider>
|
||||
<App />
|
||||
</ManagedTalkContextProvider>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Index />, document.getElementById("app"));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* The actual content of this file is being generated by our `locales-loader`.
|
||||
* Please check `./src/loaders` and the webpack config for more information.
|
||||
*
|
||||
* This file only represents the types that gets exported.
|
||||
*/
|
||||
|
||||
import { LocalesData } from "talk-framework/lib/i18n";
|
||||
export default {} as LocalesData;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import { install, InstallInput } from "talk-framework/rest";
|
||||
|
||||
export type InstallMutation = (input: InstallInput) => Promise<void>;
|
||||
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: InstallInput,
|
||||
{ rest }: TalkContext
|
||||
) {
|
||||
try {
|
||||
await install(rest, input);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export const withInstallMutation = createMutationContainer("install", commit);
|
||||
@@ -0,0 +1 @@
|
||||
export { withInstallMutation, InstallMutation } from "./InstallMutation";
|
||||
@@ -0,0 +1,204 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import { OnSubmit } from "talk-framework/lib/form";
|
||||
import {
|
||||
composeValidators,
|
||||
required,
|
||||
validateEmail,
|
||||
validateURL,
|
||||
} from "talk-framework/lib/validation";
|
||||
import {
|
||||
CallOut,
|
||||
Flex,
|
||||
FormField,
|
||||
HorizontalGutter,
|
||||
InputDescription,
|
||||
InputLabel,
|
||||
TextField,
|
||||
Typography,
|
||||
ValidationMessage,
|
||||
} from "talk-ui/components";
|
||||
|
||||
import BackButton from "./BackButton";
|
||||
import NextButton from "./NextButton";
|
||||
|
||||
interface FormProps {
|
||||
organizationName: string;
|
||||
organizationContactEmail: string;
|
||||
organizationURL: string;
|
||||
}
|
||||
|
||||
export interface AddOrganizationForm {
|
||||
onSubmit: OnSubmit<FormProps>;
|
||||
onGoToPreviousStep: () => void;
|
||||
data: FormProps;
|
||||
}
|
||||
|
||||
const AddOrganization: StatelessComponent<AddOrganizationForm> = props => {
|
||||
return (
|
||||
<Form
|
||||
onSubmit={props.onSubmit}
|
||||
initialValues={{
|
||||
organizationName: props.data.organizationName,
|
||||
organizationContactEmail: props.data.organizationContactEmail,
|
||||
organizationURL: props.data.organizationURL,
|
||||
}}
|
||||
>
|
||||
{({ handleSubmit, submitting, submitError }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="install-addOrganization-title">
|
||||
<Typography variant="heading1" align="center">
|
||||
Add Organization
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Localized id="install-addOrganization-description">
|
||||
<Typography variant="bodyCopy" align="center">
|
||||
Please tell us the name of your organization. This will appear
|
||||
in emails when inviting new team members.
|
||||
</Typography>
|
||||
</Localized>
|
||||
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
|
||||
<Field
|
||||
name="organizationName"
|
||||
validate={composeValidators(required)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="install-addOrganization-orgName">
|
||||
<InputLabel>Organization Name</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="install-addOrganization-orgNameTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<TextField
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Organization Name"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
name="organizationContactEmail"
|
||||
validate={composeValidators(required, validateEmail)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="install-addOrganization-orgEmail">
|
||||
<InputLabel>Organization Contact Email</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="install-addOrganization-orgEmailTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<TextField
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Organization Contact Email"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
name="organizationURL"
|
||||
validate={composeValidators(required, validateURL)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="install-addOrganization-orgURL">
|
||||
<InputLabel>Organization URL</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="install-addOrganization-orgURLDescription"
|
||||
strong={<strong />}
|
||||
>
|
||||
<InputDescription>
|
||||
{/* Related: https://github.com/prettier/prettier/issues/2347 */}
|
||||
Be sure to include <strong>{"http://"}</strong> or{" "}
|
||||
<strong>{"https://"}</strong> in your URL
|
||||
</InputDescription>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="install-addOrganization-orgURLTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<TextField
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Organization URL"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
<Flex direction="row-reverse" itemGutter>
|
||||
<NextButton submitting={submitting} />
|
||||
<BackButton
|
||||
submitting={submitting}
|
||||
onGoToPreviousStep={props.onGoToPreviousStep}
|
||||
/>
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddOrganization;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import { Button } from "talk-ui/components";
|
||||
|
||||
export interface BackButtonProps {
|
||||
submitting: boolean;
|
||||
onGoToPreviousStep: () => void;
|
||||
}
|
||||
|
||||
const BackButton: StatelessComponent<BackButtonProps> = ({
|
||||
submitting,
|
||||
onGoToPreviousStep,
|
||||
}) => {
|
||||
return (
|
||||
<Localized id="install-backButton-back">
|
||||
<Button
|
||||
onClick={onGoToPreviousStep}
|
||||
variant="filled"
|
||||
color="regular"
|
||||
size="large"
|
||||
disabled={submitting}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Localized>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackButton;
|
||||
@@ -0,0 +1,235 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import { OnSubmit } from "talk-framework/lib/form";
|
||||
import {
|
||||
composeValidators,
|
||||
required,
|
||||
validateEmail,
|
||||
validateEqualPasswords,
|
||||
validatePassword,
|
||||
validateUsername,
|
||||
} from "talk-framework/lib/validation";
|
||||
import {
|
||||
CallOut,
|
||||
Flex,
|
||||
FormField,
|
||||
HorizontalGutter,
|
||||
InputDescription,
|
||||
InputLabel,
|
||||
TextField,
|
||||
Typography,
|
||||
ValidationMessage,
|
||||
} from "talk-ui/components";
|
||||
|
||||
import NextButton from "./NextButton";
|
||||
|
||||
interface FormProps {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
export interface CreateYourAccountForm {
|
||||
onSubmit: OnSubmit<FormProps>;
|
||||
onGoToPreviousStep: () => void;
|
||||
data: FormProps;
|
||||
}
|
||||
|
||||
const CreateYourAccount: StatelessComponent<CreateYourAccountForm> = props => {
|
||||
return (
|
||||
<Form
|
||||
onSubmit={props.onSubmit}
|
||||
initialValues={{
|
||||
email: props.data.email,
|
||||
username: props.data.username,
|
||||
}}
|
||||
>
|
||||
{({ handleSubmit, submitting, submitError }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="install-createYourAccount-title">
|
||||
<Typography variant="heading1" align="center">
|
||||
Create an Administrator Account
|
||||
</Typography>
|
||||
</Localized>
|
||||
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
|
||||
<Field
|
||||
name="email"
|
||||
validate={composeValidators(required, validateEmail)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="install-createYourAccount-email">
|
||||
<InputLabel>Email</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="install-createYourAccount-emailTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<TextField
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Email"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
name="username"
|
||||
validate={composeValidators(required, validateUsername)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="install-createYourAccount-username">
|
||||
<InputLabel>Username</InputLabel>
|
||||
</Localized>
|
||||
<Localized id="install-createYourAccount-usernameDescription">
|
||||
<InputDescription>
|
||||
A unique identifier displayed on your comments. You may
|
||||
use “_” and “.”
|
||||
</InputDescription>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="install-createYourAccount-usernameTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<TextField
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Username"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
name="password"
|
||||
validate={composeValidators(required, validatePassword)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="install-createYourAccount-password">
|
||||
<InputLabel>Password</InputLabel>
|
||||
</Localized>
|
||||
<Localized id="install-createYourAccount-passwordDescription">
|
||||
<InputDescription>
|
||||
Must be at least 8 characters
|
||||
</InputDescription>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="install-createYourAccount-passwordTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<TextField
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
<Field
|
||||
name="confirmPassword"
|
||||
validate={composeValidators(required, validateEqualPasswords)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="install-createYourAccount-confirmPassword">
|
||||
<InputLabel>Confirm Password</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="install-createYourAccount-confirmPasswordTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<TextField
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Confirm Password"
|
||||
type="password"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Flex direction="row-reverse">
|
||||
<NextButton submitting={submitting} />
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateYourAccount;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { urls } from "talk-framework/helpers";
|
||||
import { Button, Flex, Typography } from "talk-ui/components";
|
||||
|
||||
class FinalStep extends Component {
|
||||
public render() {
|
||||
return (
|
||||
<Flex direction="column" justifyContent="center" itemGutter="double">
|
||||
<Localized id="install-finalStep-description">
|
||||
<Typography variant="bodyCopy">
|
||||
Thanks for installing Talk! We sent an email to verify your email
|
||||
address. While you finish setting up the account, you can start
|
||||
engaging with your readers now.
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Flex direction="row" itemGutter justifyContent="center">
|
||||
<Localized id="install-finalStep-goToTheDocs">
|
||||
<Button
|
||||
anchor
|
||||
color="regular"
|
||||
variant="filled"
|
||||
href="https://docs.coralproject.net"
|
||||
target="_blank"
|
||||
>
|
||||
Go to the Docs
|
||||
</Button>
|
||||
</Localized>
|
||||
<Localized id="install-finalStep-goToAdmin">
|
||||
<Button anchor color="primary" variant="filled" href={urls.admin}>
|
||||
Go to Admin
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FinalStep;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { Button, Flex, Typography } from "talk-ui/components";
|
||||
|
||||
interface InitialStepProps {
|
||||
onGoToNextStep: () => void;
|
||||
}
|
||||
|
||||
class InitialStep extends Component<InitialStepProps> {
|
||||
public render() {
|
||||
return (
|
||||
<Flex direction="column" justifyContent="center" itemGutter="double">
|
||||
<Localized id="install-initialStep-copy">
|
||||
<Typography variant="bodyCopy">
|
||||
The remainder of the Talk installation will take about ten minutes.
|
||||
Once you complete the following three steps, you will have a free
|
||||
installation and provision Mongo and Redis.
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Flex justifyContent="center">
|
||||
<Localized id="install-initialStep-getStarted">
|
||||
<Button
|
||||
onClick={this.props.onGoToNextStep}
|
||||
color="primary"
|
||||
variant="filled"
|
||||
fullWidth={false}
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default InitialStep;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import { Button, ButtonIcon } from "talk-ui/components";
|
||||
|
||||
export interface NextButtonProps {
|
||||
submitting: boolean;
|
||||
}
|
||||
|
||||
const NextButton: StatelessComponent<NextButtonProps> = props => {
|
||||
return (
|
||||
<Button
|
||||
variant="filled"
|
||||
color="primary"
|
||||
size="large"
|
||||
type="submit"
|
||||
disabled={props.submitting}
|
||||
>
|
||||
<Localized id="install-nextButton-next">
|
||||
<span>Next</span>
|
||||
</Localized>
|
||||
<ButtonIcon>arrow_forward</ButtonIcon>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default NextButton;
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
import { OnSubmit } from "talk-framework/lib/form";
|
||||
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
Flex,
|
||||
FormField,
|
||||
HorizontalGutter,
|
||||
InputDescription,
|
||||
InputLabel,
|
||||
TextField,
|
||||
Typography,
|
||||
ValidationMessage,
|
||||
} from "talk-ui/components";
|
||||
|
||||
import BackButton from "./BackButton";
|
||||
|
||||
interface FormProps {
|
||||
domains: string;
|
||||
}
|
||||
|
||||
export interface PermittedDomainsForm {
|
||||
onSubmit: OnSubmit<FormProps>;
|
||||
onGoToPreviousStep: () => void;
|
||||
data: {
|
||||
domains: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const PermittedDomains: StatelessComponent<PermittedDomainsForm> = props => {
|
||||
return (
|
||||
<Form
|
||||
onSubmit={props.onSubmit}
|
||||
initialValues={{
|
||||
domains: props.data.domains.join(","),
|
||||
}}
|
||||
>
|
||||
{({ handleSubmit, submitting, submitError }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="install-permittedDomains-title">
|
||||
<Typography variant="heading1" align="center">
|
||||
Permitted Domains
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Localized id="install-permittedDomains-description">
|
||||
<Typography variant="bodyCopy" align="center">
|
||||
Enter the domains you would like to permit for Talk, e.g. your
|
||||
local, staging and production environments (ex. localhost:3000,
|
||||
staging.domain.com, domain.com).
|
||||
</Typography>
|
||||
</Localized>
|
||||
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
|
||||
<Field name="domains">
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="install-permittedDomains-permittedDomains">
|
||||
<InputLabel>Permitted Domains</InputLabel>
|
||||
</Localized>
|
||||
<Localized id="install-permittedDomains-permittedDomainsDescription">
|
||||
<InputDescription>
|
||||
Insert domains separated by comma
|
||||
</InputDescription>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="install-permittedDomains-permittedDomainsTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<TextField
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Domains"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Flex direction="row-reverse" itemGutter>
|
||||
<Localized id="install-permittedDomains-finishInstall">
|
||||
<Button
|
||||
variant="filled"
|
||||
color="primary"
|
||||
size="large"
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
>
|
||||
Finish Install
|
||||
</Button>
|
||||
</Localized>
|
||||
<BackButton
|
||||
submitting={submitting}
|
||||
onGoToPreviousStep={props.onGoToPreviousStep}
|
||||
/>
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermittedDomains;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-ui/types";
|
||||
|
||||
import AddOrganization, {
|
||||
AddOrganizationForm,
|
||||
} from "../components/AddOrganization";
|
||||
|
||||
interface AddOrganizationContainerProps {
|
||||
onGoToNextStep: () => void;
|
||||
onGoToPreviousStep: () => void;
|
||||
data: PropTypesOf<typeof AddOrganization>["data"];
|
||||
onSaveData: (newData: PropTypesOf<typeof AddOrganization>["data"]) => void;
|
||||
}
|
||||
|
||||
class AddOrganizationContainer extends Component<
|
||||
AddOrganizationContainerProps
|
||||
> {
|
||||
private onSubmit: AddOrganizationForm["onSubmit"] = async (input, form) => {
|
||||
try {
|
||||
this.props.onSaveData(input);
|
||||
return this.props.onGoToNextStep();
|
||||
} catch (error) {
|
||||
return { [FORM_ERROR]: error.message };
|
||||
}
|
||||
};
|
||||
public render() {
|
||||
return (
|
||||
<AddOrganization
|
||||
data={this.props.data}
|
||||
onSubmit={this.onSubmit}
|
||||
onGoToPreviousStep={this.props.onGoToPreviousStep}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AddOrganizationContainer;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-ui/types";
|
||||
|
||||
import CreateYourAccount, {
|
||||
CreateYourAccountForm,
|
||||
} from "../components/CreateYourAccount";
|
||||
|
||||
interface CreateYourAccountContainerProps {
|
||||
onGoToNextStep: () => void;
|
||||
onGoToPreviousStep: () => void;
|
||||
data: PropTypesOf<typeof CreateYourAccount>["data"];
|
||||
onSaveData: (newData: PropTypesOf<typeof CreateYourAccount>["data"]) => void;
|
||||
}
|
||||
|
||||
class CreateYourAccountContainer extends Component<
|
||||
CreateYourAccountContainerProps
|
||||
> {
|
||||
private onSubmit: CreateYourAccountForm["onSubmit"] = async (input, form) => {
|
||||
try {
|
||||
this.props.onSaveData(input);
|
||||
return this.props.onGoToNextStep();
|
||||
} catch (error) {
|
||||
return { [FORM_ERROR]: error.message };
|
||||
}
|
||||
};
|
||||
public render() {
|
||||
return (
|
||||
<CreateYourAccount
|
||||
data={this.props.data}
|
||||
onSubmit={this.onSubmit}
|
||||
onGoToPreviousStep={this.props.onGoToPreviousStep}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CreateYourAccountContainer;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-ui/types";
|
||||
|
||||
import PermittedDomains, {
|
||||
PermittedDomainsForm,
|
||||
} from "../components/PermittedDomains";
|
||||
|
||||
interface PermittedDomainsContainerProps {
|
||||
onGoToNextStep: () => void;
|
||||
onGoToPreviousStep: () => void;
|
||||
data: PropTypesOf<typeof PermittedDomains>["data"];
|
||||
onInstall: (
|
||||
newData: PropTypesOf<typeof PermittedDomains>["data"]
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
class PermittedDomainsContainer extends Component<
|
||||
PermittedDomainsContainerProps
|
||||
> {
|
||||
private onSubmit: PermittedDomainsForm["onSubmit"] = async (input, form) => {
|
||||
try {
|
||||
const domains = input.domains.split(",");
|
||||
await this.props.onInstall({ domains });
|
||||
return this.props.onGoToNextStep();
|
||||
} catch (error) {
|
||||
return { [FORM_ERROR]: error.message };
|
||||
}
|
||||
};
|
||||
public render() {
|
||||
return (
|
||||
<PermittedDomains
|
||||
data={this.props.data}
|
||||
onSubmit={this.onSubmit}
|
||||
onGoToPreviousStep={this.props.onGoToPreviousStep}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PermittedDomainsContainer;
|
||||
@@ -2,7 +2,7 @@
|
||||
:global {
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 2px 8px;
|
||||
padding: 0;
|
||||
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "talk-stream/mutations";
|
||||
import { Popup } from "talk-ui/components";
|
||||
|
||||
import { urls } from "talk-framework/helpers";
|
||||
import UserBoxAuthenticated from "../components/UserBoxAuthenticated";
|
||||
|
||||
interface InnerProps {
|
||||
@@ -57,7 +58,7 @@ export class UserBoxContainer extends Component<InnerProps> {
|
||||
return (
|
||||
<>
|
||||
<Popup
|
||||
href={`/auth.html?view=${view}`}
|
||||
href={`${urls.embed.auth}?view=${view}`}
|
||||
title="Talk Auth"
|
||||
features="menubar=0,resizable=0,width=350,height=395,top=200,left=500"
|
||||
open={open}
|
||||
|
||||
@@ -5,7 +5,7 @@ exports[`renders correctly 1`] = `
|
||||
<Popup
|
||||
features="menubar=0,resizable=0,width=350,height=395,top=200,left=500"
|
||||
focus={false}
|
||||
href="/auth.html?view=SIGN_IN"
|
||||
href="/embed/auth?view=SIGN_IN"
|
||||
onBlur={[Function]}
|
||||
onClose={[Function]}
|
||||
onFocus={[Function]}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { shallow } from "enzyme";
|
||||
import qs from "query-string";
|
||||
import React from "react";
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
|
||||
@@ -10,12 +11,20 @@ import { OnPymSetCommentID } from "./OnPymSetCommentID";
|
||||
let relayEnvironment: Environment;
|
||||
const source: RecordSource = new RecordSource();
|
||||
|
||||
const previousLocation = location.toString();
|
||||
const previousState = window.history.state;
|
||||
|
||||
beforeAll(() => {
|
||||
relayEnvironment = createRelayEnvironment({
|
||||
source,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// As history will change after the listener triggers, reset this to before.
|
||||
window.history.replaceState(previousState, document.title, previousLocation);
|
||||
});
|
||||
|
||||
it("Sets comment id", () => {
|
||||
const id = "comment1-id";
|
||||
const props = {
|
||||
@@ -29,6 +38,7 @@ it("Sets comment id", () => {
|
||||
};
|
||||
shallow(<OnPymSetCommentID {...props} />);
|
||||
expect(source.get(LOCAL_ID)!.commentID).toEqual(id);
|
||||
expect(qs.parse(location.search).commentID).toEqual(id);
|
||||
});
|
||||
|
||||
it("Sets comment id to null when empty", () => {
|
||||
@@ -44,4 +54,5 @@ it("Sets comment id to null when empty", () => {
|
||||
};
|
||||
shallow(<OnPymSetCommentID {...props} />);
|
||||
expect(source.get(LOCAL_ID)!.commentID).toEqual(null);
|
||||
expect(qs.parse(location.search).commentID).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Component } from "react";
|
||||
import { commitLocalUpdate } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { getURLWithCommentID } from "talk-framework/helpers";
|
||||
import { withContext } from "talk-framework/lib/bootstrap";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
|
||||
@@ -21,6 +22,15 @@ export class OnPymSetCommentID extends Component<Props> {
|
||||
const id = raw || null;
|
||||
if (s.get(LOCAL_ID)!.getValue("commentID") !== id) {
|
||||
s.get(LOCAL_ID)!.setValue(id, "commentID");
|
||||
|
||||
// Change iframe url, this is important
|
||||
// because it is used to cleanly initialized
|
||||
// a user session.
|
||||
window.history.replaceState(
|
||||
window.history.state,
|
||||
document.title,
|
||||
getURLWithCommentID(location.href, id || undefined)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,17 +24,19 @@ function sharedUpdater(
|
||||
store: RecordSourceSelectorProxy,
|
||||
input: CreateCommentInput
|
||||
) {
|
||||
updateAsset(store, input);
|
||||
if (input.local) {
|
||||
localUpdate(store, input);
|
||||
} else {
|
||||
update(store, input);
|
||||
}
|
||||
updateProfile(store, input);
|
||||
}
|
||||
|
||||
/**
|
||||
* update integrates new comment into the CommentConnection.
|
||||
*/
|
||||
function update(store: RecordSourceSelectorProxy, input: CreateCommentInput) {
|
||||
function updateAsset(
|
||||
store: RecordSourceSelectorProxy,
|
||||
input: CreateCommentInput
|
||||
) {
|
||||
// Updating Comment Count
|
||||
const asset = store.get(input.assetID);
|
||||
if (asset) {
|
||||
@@ -45,7 +47,12 @@ function update(store: RecordSourceSelectorProxy, input: CreateCommentInput) {
|
||||
record.setValue(currentCount + 1, "totalVisible");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* update integrates new comment into the CommentConnection.
|
||||
*/
|
||||
function update(store: RecordSourceSelectorProxy, input: CreateCommentInput) {
|
||||
// Get the payload returned from the server.
|
||||
const payload = store.getRootField("createComment")!;
|
||||
|
||||
@@ -109,6 +116,26 @@ function localUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* updateProfile integrates new comment into the profile.
|
||||
*/
|
||||
function updateProfile(
|
||||
store: RecordSourceSelectorProxy,
|
||||
input: CreateCommentInput
|
||||
) {
|
||||
// Get the payload returned from the server.
|
||||
const payload = store.getRootField("createComment")!;
|
||||
|
||||
// Get the edge of the newly created comment.
|
||||
const newEdge = payload.getLinkedRecord("edge")!;
|
||||
const newComment = newEdge.getLinkedRecord("node");
|
||||
|
||||
// TODO: update profile comments connection after we
|
||||
// integrated pagination.
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
newComment;
|
||||
}
|
||||
|
||||
const mutation = graphql`
|
||||
mutation CreateCommentMutation($input: CreateCommentInput!) {
|
||||
createComment(input: $input) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import qs from "query-string";
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
import sinon from "sinon";
|
||||
|
||||
@@ -16,10 +17,19 @@ beforeAll(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const previousLocation = location.toString();
|
||||
const previousState = window.history.state;
|
||||
|
||||
afterEach(() => {
|
||||
// As history will change after the listener triggers, reset this to before.
|
||||
window.history.replaceState(previousState, document.title, previousLocation);
|
||||
});
|
||||
|
||||
it("Sets comment id", () => {
|
||||
const id = "comment1-id";
|
||||
commit(environment, { id }, {} as any);
|
||||
expect(source.get(LOCAL_ID)!.commentID).toEqual(id);
|
||||
expect(qs.parse(location.search).commentID).toEqual(id);
|
||||
});
|
||||
|
||||
it("Should call setCommentID in pym", async () => {
|
||||
@@ -50,5 +60,6 @@ it("Should call setCommentID in pym with empty id", async () => {
|
||||
commit(environment, { id: null }, context as any);
|
||||
await timeout();
|
||||
expect(source.get(LOCAL_ID)!.commentID).toEqual(null);
|
||||
expect(qs.parse(location.search).commentID).toBeUndefined();
|
||||
context.pym.sendMessage.verify();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { getURLWithCommentID } from "talk-framework/helpers";
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
|
||||
@@ -19,6 +20,16 @@ export async function commit(
|
||||
const record = store.get(LOCAL_ID)!;
|
||||
record.setValue(input.id, "commentID");
|
||||
record.setValue("COMMENTS", "activeTab");
|
||||
|
||||
// Change iframe url, this is important
|
||||
// because it is used to cleanly initialized
|
||||
// a user session.
|
||||
window.history.replaceState(
|
||||
window.history.state,
|
||||
document.title,
|
||||
getURLWithCommentID(location.href, input.id || undefined)
|
||||
);
|
||||
|
||||
if (pym) {
|
||||
// This sets the comment id on the parent url.
|
||||
pym.sendMessage("setCommentID", input.id || "");
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
.root {
|
||||
}
|
||||
.highlight {
|
||||
background-color: var(--palette-primary-lightest);
|
||||
padding: var(--spacing-unit);
|
||||
}
|
||||
.topBar {
|
||||
margin-bottom: calc(0.5 * var(--spacing-unit));
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@ import Comment from "./Comment";
|
||||
it("renders username and body", () => {
|
||||
const props: PropTypesOf<typeof Comment> = {
|
||||
id: "comment-id",
|
||||
author: {
|
||||
username: "Marvin",
|
||||
},
|
||||
username: "Marvin",
|
||||
body: "Woof",
|
||||
createdAt: "1995-12-17T03:24:00.000Z",
|
||||
topBarRight: "topBarRight",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import cn from "classnames";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import HTMLContent from "talk-stream/components/HTMLContent";
|
||||
@@ -12,19 +13,21 @@ import Username from "./Username";
|
||||
export interface CommentProps {
|
||||
id?: string;
|
||||
className?: string;
|
||||
author: {
|
||||
username: string | null;
|
||||
} | null;
|
||||
username: string | null;
|
||||
body: string | null;
|
||||
createdAt: string;
|
||||
topBarRight?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
showEditedMarker?: boolean;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
const Comment: StatelessComponent<CommentProps> = props => {
|
||||
return (
|
||||
<div role="article" className={styles.root}>
|
||||
<div
|
||||
role="article"
|
||||
className={cn(styles.root, { [styles.highlight]: props.highlight })}
|
||||
>
|
||||
<Flex
|
||||
className={styles.topBar}
|
||||
direction="row"
|
||||
@@ -32,10 +35,7 @@ const Comment: StatelessComponent<CommentProps> = props => {
|
||||
id={props.id}
|
||||
>
|
||||
<TopBarLeft>
|
||||
{props.author &&
|
||||
props.author.username && (
|
||||
<Username>{props.author.username}</Username>
|
||||
)}
|
||||
{props.username && <Username>{props.username}</Username>}
|
||||
<Flex direction="row" alignItems="baseline" itemGutter>
|
||||
<Timestamp>{props.createdAt}</Timestamp>
|
||||
{props.showEditedMarker && <EditedMarker />}
|
||||
|
||||
@@ -8,9 +8,7 @@ import IndentedComment from "./IndentedComment";
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof IndentedComment> = {
|
||||
indentLevel: 1,
|
||||
author: {
|
||||
username: "Marvin",
|
||||
},
|
||||
username: "Marvin",
|
||||
body: "Woof",
|
||||
createdAt: "1995-12-17T03:24:00.000Z",
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import Timestamp from "talk-stream/components/Timestamp";
|
||||
import { Flex } from "talk-ui/components";
|
||||
|
||||
import TopBarLeft from "./TopBarLeft";
|
||||
import Username from "./Username";
|
||||
|
||||
export interface RootParentProps {
|
||||
id?: string;
|
||||
username: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const RootParent: StatelessComponent<RootParentProps> = props => {
|
||||
return (
|
||||
<Flex direction="row" justifyContent="space-between" id={props.id}>
|
||||
<TopBarLeft>
|
||||
{props.username && <Username>{props.username}</Username>}
|
||||
<Flex direction="row" alignItems="baseline" itemGutter>
|
||||
<Timestamp>{props.createdAt}</Timestamp>
|
||||
</Flex>
|
||||
</TopBarLeft>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootParent;
|
||||
+1
-5
@@ -6,13 +6,9 @@ exports[`renders correctly 1`] = `
|
||||
level={1}
|
||||
>
|
||||
<Comment
|
||||
author={
|
||||
Object {
|
||||
"username": "Marvin",
|
||||
}
|
||||
}
|
||||
body="Woof"
|
||||
createdAt="1995-12-17T03:24:00.000Z"
|
||||
username="Marvin"
|
||||
/>
|
||||
</Indent>
|
||||
`;
|
||||
|
||||
@@ -3,3 +3,4 @@ export { default as TopBarLeft } from "./TopBarLeft";
|
||||
export { default as Username } from "./Username";
|
||||
export { default as ButtonsBar } from "./ButtonsBar";
|
||||
export { default as ShowConversationLink } from "./ShowConversationLink";
|
||||
export { default as RootParent } from "./RootParent";
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.root {
|
||||
}
|
||||
|
||||
.loadMore {
|
||||
padding-left: var(--spacing-unit);
|
||||
padding-bottom: var(--spacing-unit);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { shallow } from "enzyme";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import { removeFragmentRefs } from "talk-framework/testHelpers";
|
||||
import ConversationThread from "./ConversationThread";
|
||||
|
||||
const ConversationThreadN = removeFragmentRefs(ConversationThread);
|
||||
|
||||
describe("with 2 remaining parent comments", () => {
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof ConversationThreadN> = {
|
||||
className: "root",
|
||||
me: {},
|
||||
asset: {},
|
||||
settings: {},
|
||||
comment: {},
|
||||
disableLoadMore: false,
|
||||
loadMore: noop,
|
||||
remaining: 2,
|
||||
parents: [],
|
||||
rootParent: {
|
||||
id: "root-parent",
|
||||
createdAt: "1995-12-17T03:24:00.000Z",
|
||||
username: "parentAuthor",
|
||||
},
|
||||
};
|
||||
const wrapper = shallow(<ConversationThreadN {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
it("renders with disabled load more", () => {
|
||||
const props: PropTypesOf<typeof ConversationThreadN> = {
|
||||
className: "root",
|
||||
me: {},
|
||||
asset: {},
|
||||
settings: {},
|
||||
comment: {},
|
||||
disableLoadMore: true,
|
||||
loadMore: noop,
|
||||
remaining: 2,
|
||||
parents: [],
|
||||
rootParent: {
|
||||
id: "root-parent",
|
||||
createdAt: "1995-12-17T03:24:00.000Z",
|
||||
username: "parentAuthor",
|
||||
},
|
||||
};
|
||||
const wrapper = shallow(<ConversationThreadN {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders with no parent comments", () => {
|
||||
const props: PropTypesOf<typeof ConversationThreadN> = {
|
||||
className: "root",
|
||||
me: {},
|
||||
asset: {},
|
||||
settings: {},
|
||||
comment: {},
|
||||
disableLoadMore: false,
|
||||
loadMore: noop,
|
||||
remaining: 0,
|
||||
parents: [],
|
||||
rootParent: null,
|
||||
};
|
||||
const wrapper = shallow(<ConversationThreadN {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import cn from "classnames";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { Button, Flex, HorizontalGutter } from "talk-ui/components";
|
||||
|
||||
import CommentContainer from "../containers/CommentContainer";
|
||||
import LocalReplyListContainer from "../containers/LocalReplyListContainer";
|
||||
import { RootParent } from "./Comment";
|
||||
import * as styles from "./ConversationThread.css";
|
||||
import Counter from "./Counter";
|
||||
import { Circle, Line } from "./Timeline";
|
||||
|
||||
export interface ConversationThreadProps {
|
||||
className?: string;
|
||||
me: PropTypesOf<typeof CommentContainer>["me"] &
|
||||
(PropTypesOf<typeof LocalReplyListContainer>["me"] | null);
|
||||
asset: PropTypesOf<typeof CommentContainer>["asset"] &
|
||||
PropTypesOf<typeof LocalReplyListContainer>["asset"];
|
||||
settings: PropTypesOf<typeof CommentContainer>["settings"] &
|
||||
PropTypesOf<typeof LocalReplyListContainer>["settings"];
|
||||
comment: PropTypesOf<typeof CommentContainer>["comment"];
|
||||
disableLoadMore: boolean;
|
||||
loadMore: () => void;
|
||||
remaining: number;
|
||||
parents: Array<
|
||||
{ id: string } & PropTypesOf<typeof CommentContainer>["comment"] &
|
||||
PropTypesOf<typeof LocalReplyListContainer>["comment"]
|
||||
>;
|
||||
rootParent: {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
username: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
const ConversationThread: StatelessComponent<
|
||||
ConversationThreadProps
|
||||
> = props => {
|
||||
if (props.remaining === 0 && props.parents.length === 0) {
|
||||
return (
|
||||
<div className={cn(props.className, styles.root)}>
|
||||
<CommentContainer
|
||||
comment={props.comment}
|
||||
asset={props.asset}
|
||||
settings={props.settings}
|
||||
me={props.me}
|
||||
highlight
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={cn(props.className, styles.root)}>
|
||||
<HorizontalGutter container={<Line dotted />}>
|
||||
{props.rootParent && (
|
||||
<Circle>
|
||||
<RootParent
|
||||
id={props.rootParent.id}
|
||||
username={props.rootParent.username}
|
||||
createdAt={props.rootParent.createdAt}
|
||||
/>
|
||||
</Circle>
|
||||
)}
|
||||
{props.remaining > 0 && (
|
||||
<Circle hollow className={styles.loadMore}>
|
||||
<Flex alignItems="center" itemGutter="half">
|
||||
<Localized
|
||||
id="comments-conversationThread-showHiddenComments"
|
||||
$count={props.remaining}
|
||||
>
|
||||
<Button
|
||||
onClick={props.loadMore}
|
||||
disabled={props.disableLoadMore}
|
||||
id="comments-conversationThread-showHiddenComments"
|
||||
variant="underlined"
|
||||
>
|
||||
Show hidden comments
|
||||
</Button>
|
||||
</Localized>
|
||||
{props.remaining > 1 && <Counter>{props.remaining}</Counter>}
|
||||
</Flex>
|
||||
</Circle>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
<HorizontalGutter container={Line}>
|
||||
{props.parents.map((parent, i) => (
|
||||
<Circle key={parent.id} hollow={!!props.remaining || i > 0}>
|
||||
<CommentContainer
|
||||
comment={parent}
|
||||
asset={props.asset}
|
||||
me={props.me}
|
||||
settings={props.settings}
|
||||
localReply
|
||||
/>
|
||||
{props.me && (
|
||||
<LocalReplyListContainer
|
||||
asset={props.asset}
|
||||
me={props.me}
|
||||
settings={props.settings}
|
||||
comment={parent}
|
||||
indentLevel={1}
|
||||
/>
|
||||
)}
|
||||
</Circle>
|
||||
))}
|
||||
<Circle end>
|
||||
<CommentContainer
|
||||
comment={props.comment}
|
||||
asset={props.asset}
|
||||
settings={props.settings}
|
||||
me={props.me}
|
||||
highlight
|
||||
/>
|
||||
</Circle>
|
||||
</HorizontalGutter>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConversationThread;
|
||||
@@ -0,0 +1,8 @@
|
||||
.root {
|
||||
composes: subTabCounter from "talk-ui/shared/typography.css";
|
||||
|
||||
background-color: var(--palette-grey-dark);
|
||||
color: var(--palette-text-light);
|
||||
border-radius: 1px;
|
||||
padding: 2px 3px 3px 3px;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import Counter from "./Counter";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const wrapper = shallow(<Counter>20</Counter>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import cn from "classnames";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import * as styles from "./Counter.css";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Counter: StatelessComponent<Props> = props => {
|
||||
return (
|
||||
<span className={cn(styles.root, props.className)}>{props.children}</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Counter;
|
||||
@@ -2,6 +2,23 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-bottom: calc(2 * var(--spacing-unit));
|
||||
.replyList {
|
||||
padding-left: calc(2 * var(--spacing-unit));
|
||||
}
|
||||
|
||||
.title1 {
|
||||
font-size: calc(14rem / var(--rem-base));
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
line-height: calc(20em / 16);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
color: var(--palette-text-primary);
|
||||
}
|
||||
.title2 {
|
||||
font-size: calc(18rem / var(--rem-base));
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
line-height: calc(20em / 16);
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
color: var(--palette-text-primary);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { shallow } from "enzyme";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import { removeFragmentRefs } from "talk-framework/testHelpers";
|
||||
import PermalinkView from "./PermalinkView";
|
||||
|
||||
const PermalinkViewN = removeFragmentRefs(PermalinkView);
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof PermalinkViewN> = {
|
||||
me: {},
|
||||
asset: {},
|
||||
settings: {},
|
||||
comment: {},
|
||||
showAllCommentsHref: "http://localhost/link",
|
||||
onShowAllComments: noop,
|
||||
};
|
||||
const wrapper = shallow(<PermalinkViewN {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders comment not found", () => {
|
||||
const props: PropTypesOf<typeof PermalinkViewN> = {
|
||||
me: {},
|
||||
asset: {},
|
||||
settings: {},
|
||||
comment: null,
|
||||
showAllCommentsHref: "http://localhost/link",
|
||||
onShowAllComments: noop,
|
||||
};
|
||||
const wrapper = shallow(<PermalinkViewN {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -2,16 +2,25 @@ import { Localized } from "fluent-react/compat";
|
||||
import React, { MouseEvent, StatelessComponent } from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { Button, Typography } from "talk-ui/components";
|
||||
import { Button, Flex, HorizontalGutter, Typography } from "talk-ui/components";
|
||||
|
||||
import CommentContainer from "../containers/CommentContainer";
|
||||
import UserBoxContainer from "../../../containers/UserBoxContainer";
|
||||
import ConversationThreadContainer from "../containers/ConversationThreadContainer";
|
||||
import ReplyListContainer from "../containers/ReplyListContainer";
|
||||
import * as styles from "./PermalinkView.css";
|
||||
|
||||
export interface PermalinkViewProps {
|
||||
me: PropTypesOf<typeof CommentContainer>["me"];
|
||||
asset: PropTypesOf<typeof CommentContainer>["asset"];
|
||||
settings: PropTypesOf<typeof CommentContainer>["settings"];
|
||||
comment: PropTypesOf<typeof CommentContainer>["comment"] | null;
|
||||
me: PropTypesOf<typeof ConversationThreadContainer>["me"] &
|
||||
PropTypesOf<typeof ReplyListContainer>["me"] &
|
||||
PropTypesOf<typeof UserBoxContainer>["me"];
|
||||
asset: PropTypesOf<typeof ConversationThreadContainer>["asset"] &
|
||||
PropTypesOf<typeof ReplyListContainer>["asset"];
|
||||
comment:
|
||||
| PropTypesOf<typeof ConversationThreadContainer>["comment"] &
|
||||
PropTypesOf<typeof ReplyListContainer>["comment"]
|
||||
| null;
|
||||
settings: PropTypesOf<typeof ConversationThreadContainer>["settings"] &
|
||||
PropTypesOf<typeof ReplyListContainer>["settings"];
|
||||
showAllCommentsHref: string | null;
|
||||
onShowAllComments: (e: MouseEvent<any>) => void;
|
||||
}
|
||||
@@ -25,38 +34,57 @@ const PermalinkView: StatelessComponent<PermalinkViewProps> = ({
|
||||
me,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
{showAllCommentsHref && (
|
||||
<Localized id="comments-permalinkView-showAllComments">
|
||||
<Button
|
||||
id="talk-comments-permalinkView-showAllComments"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={onShowAllComments}
|
||||
className={styles.button}
|
||||
href={showAllCommentsHref}
|
||||
target="_parent"
|
||||
fullWidth
|
||||
anchor
|
||||
>
|
||||
Show all Comments
|
||||
</Button>
|
||||
<HorizontalGutter className={styles.root} size="double">
|
||||
<UserBoxContainer me={me} />
|
||||
<Flex alignItems="center" justifyContent="center" direction="column">
|
||||
<Localized id="comments-permalinkView-currentViewing">
|
||||
<Typography className={styles.title1}>
|
||||
You are currently viewing a
|
||||
</Typography>
|
||||
</Localized>
|
||||
)}
|
||||
<Localized id="comments-permalinkView-singleConversation">
|
||||
<Typography className={styles.title2}>SINGLE CONVERSATION</Typography>
|
||||
</Localized>
|
||||
{showAllCommentsHref && (
|
||||
<Localized id="comments-permalinkView-viewFullDiscussion">
|
||||
<Button
|
||||
id="talk-comments-permalinkView-viewFullDiscussion"
|
||||
variant="underlined"
|
||||
color="primary"
|
||||
onClick={onShowAllComments}
|
||||
href={showAllCommentsHref}
|
||||
target="_parent"
|
||||
anchor
|
||||
>
|
||||
View Full Discussion
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
</Flex>
|
||||
{!comment && (
|
||||
<Localized id="comments-permalinkView-commentNotFound">
|
||||
<Typography>Comment not found</Typography>
|
||||
</Localized>
|
||||
)}
|
||||
{comment && (
|
||||
<CommentContainer
|
||||
settings={settings}
|
||||
me={me}
|
||||
comment={comment}
|
||||
asset={asset}
|
||||
/>
|
||||
<HorizontalGutter>
|
||||
<ConversationThreadContainer
|
||||
me={me}
|
||||
comment={comment}
|
||||
asset={asset}
|
||||
settings={settings}
|
||||
/>
|
||||
<div className={styles.replyList}>
|
||||
<ReplyListContainer
|
||||
me={me}
|
||||
comment={comment}
|
||||
asset={asset}
|
||||
settings={settings}
|
||||
/>
|
||||
</div>
|
||||
</HorizontalGutter>
|
||||
)}
|
||||
</div>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
composes: root from "talk-stream/shared/htmlContent.css";
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
background-color: var(--palette-common-white);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
composes: placeholder from "talk-ui/shared/typography.css";
|
||||
margin: var(--spacing-unit) 0 0 calc(1px + var(--spacing-unit));
|
||||
|
||||
@@ -95,6 +95,7 @@ const RTE: StatelessComponent<RTEProps> = props => {
|
||||
className={className}
|
||||
contentClassName={styles.content}
|
||||
placeholderClassName={styles.placeholder}
|
||||
toolbarClassName={styles.toolbar}
|
||||
onChange={onChange}
|
||||
value={value || defaultValue}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
.circleContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.circleSubContainer {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
left: calc(-1 * var(--spacing-unit) - var(--spacing-unit) - 1px);
|
||||
background-color: var(--palette-common-white);
|
||||
width: calc(2 * var(--spacing-unit));
|
||||
height: 23px;
|
||||
color: var(--palette-grey-main);
|
||||
}
|
||||
|
||||
.end {
|
||||
align-items: flex-start;
|
||||
padding-top: 8px;
|
||||
height: 100%;
|
||||
@media (min-width: $breakpoints-xs) {
|
||||
padding-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.circle {
|
||||
width: var(--spacing-unit);
|
||||
height: var(--spacing-unit);
|
||||
fill: currentColor;
|
||||
stroke: currentColor;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import Circle from "./Circle";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof Circle> = {
|
||||
className: "root",
|
||||
hollow: true,
|
||||
end: true,
|
||||
};
|
||||
const wrapper = shallow(<Circle {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user