Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e91da1c9a3 | ||
|
|
e816f8e019 | ||
|
|
121469ade5 | ||
|
|
1d6e9e38b0 | ||
|
|
626d46d813 | ||
|
|
24915be712 | ||
|
|
044cbd39f1 | ||
|
|
cd6a1db85b | ||
|
|
8bee5ae1f3 | ||
|
|
153c910de7 | ||
|
|
990a70d6f9 | ||
|
|
9aa39bf842 | ||
|
|
d169d3dd51 | ||
|
|
b38a4f117b | ||
|
|
ea71230b90 | ||
|
|
2dac5f29c1 | ||
|
|
9df473178b | ||
|
|
1f7fdb0d00 | ||
|
|
3bfcc509d2 | ||
|
|
b0e0ba6633 | ||
|
|
0fbfdac846 | ||
|
|
8d08382aea | ||
|
|
8409dbb4ea | ||
|
|
bae88f476e | ||
|
|
918ba10867 | ||
|
|
e7ab0b27f9 | ||
|
|
191687335b | ||
|
|
33487be0ce | ||
|
|
e56793f2fb | ||
|
|
6fe4646755 | ||
|
|
dd45f46b19 | ||
|
|
4a1492e88d | ||
|
|
3a4eae87ad | ||
|
|
fa3d442b36 | ||
|
|
6edb411175 | ||
|
|
6dbc724f31 | ||
|
|
d0df6bc849 | ||
|
|
808b355a27 | ||
|
|
53fa5f43e5 | ||
|
|
c045f52daa | ||
|
|
b3b26bd9f3 | ||
|
|
0af2fec31b | ||
|
|
139c1eb01f | ||
|
|
941a385d53 | ||
|
|
e61e62a238 | ||
|
|
a5c3e94751 | ||
|
|
5c1eb57d3e | ||
|
|
7b5b260a18 | ||
|
|
1c24060f94 | ||
|
|
4e4986fc03 | ||
|
|
37e39ff8c5 | ||
|
|
fe2d78f1f7 | ||
|
|
43844bca18 | ||
|
|
6fb772944c | ||
|
|
b3064c89de | ||
|
|
e4bfbbc1f8 | ||
|
|
25c7f89b31 | ||
|
|
921461008e | ||
|
|
64f102e6d4 | ||
|
|
741739bc16 | ||
|
|
2cc32d4332 | ||
|
|
867f0993cc | ||
|
|
24a27ca958 | ||
|
|
d03b180a30 | ||
|
|
72ef65577b | ||
|
|
8ef366f132 | ||
|
|
3114727410 | ||
|
|
a89dc4675a | ||
|
|
2559d1c0e8 | ||
|
|
ea3c1e3b58 | ||
|
|
457644c45b | ||
|
|
1b9a72c2f0 | ||
|
|
47834a82bb | ||
|
|
a8976a2141 | ||
|
|
718bd2758f | ||
|
|
721b2e7d67 | ||
|
|
f9e1bf144e | ||
|
|
e86e9d2c68 | ||
|
|
d031df38f6 | ||
|
|
8d368b03ec | ||
|
|
3e64aa1193 | ||
|
|
f84c3e4a60 | ||
|
|
36225e443b | ||
|
|
ed8be75774 | ||
|
|
f8cf4e94d3 | ||
|
|
1a69f48087 | ||
|
|
e0ab799c84 | ||
|
|
aa8e1e2d7a | ||
|
|
d2af78d3d7 | ||
|
|
9bd38db61a | ||
|
|
ac203607ae | ||
|
|
e49905c5ad | ||
|
|
15d41f1ae8 | ||
|
|
60a1f43d51 | ||
|
|
3c9ebcf1a1 | ||
|
|
e35978096f | ||
|
|
04c56b3fb5 | ||
|
|
5bf4f22931 | ||
|
|
1550377f39 | ||
|
|
efea0e8e1c | ||
|
|
0fad1070a6 | ||
|
|
ac1ed45e4b | ||
|
|
a696ef9be5 | ||
|
|
aa32fcaea6 | ||
|
|
fcf3640adc | ||
|
|
46f0d08cf7 | ||
|
|
fc039f8186 | ||
|
|
7e7ce9fbd0 | ||
|
|
0e37a474fa | ||
|
|
ffecd5e981 | ||
|
|
07df7ac182 | ||
|
|
2edabf44c7 | ||
|
|
1c75b7659d | ||
|
|
6d803b0179 | ||
|
|
f3e615a610 | ||
|
|
b9f9e9b2d4 | ||
|
|
7809cd3d68 | ||
|
|
add5224338 | ||
|
|
8dae0feb52 | ||
|
|
ee93267d97 | ||
|
|
0f02b05689 | ||
|
|
3baf4e96f5 | ||
|
|
8d91bd7dd1 | ||
|
|
d400c1518c | ||
|
|
d9ad1b2ee4 | ||
|
|
035ee73edd | ||
|
|
43b6a2cdcd | ||
|
|
635e740fc0 | ||
|
|
c37ee12e57 | ||
|
|
e76fddd13c |
@@ -0,0 +1,3 @@
|
||||
*.d.ts
|
||||
*.graphql.ts
|
||||
**/__generated__/**
|
||||
@@ -0,0 +1,208 @@
|
||||
const typescriptEslintRecommended = require('@typescript-eslint/eslint-plugin/dist/configs/eslint-recommended').default.overrides[0];
|
||||
const typescriptRecommended = require('@typescript-eslint/eslint-plugin/dist/configs/recommended.json');
|
||||
const typescriptRecommendedTypeChecking = require('@typescript-eslint/eslint-plugin/dist/configs/recommended-requiring-type-checking.json');
|
||||
const typescriptEslintPrettier = require('eslint-config-prettier/@typescript-eslint');
|
||||
const react = require('eslint-plugin-react').configs.recommended;
|
||||
const jsxA11y = require('eslint-plugin-jsx-a11y').configs.recommended;
|
||||
const reactPrettier = require('eslint-config-prettier/react');
|
||||
|
||||
const typescriptOverrides = {
|
||||
files: ["*.ts", "*.tsx"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
"@typescript-eslint",
|
||||
"@typescript-eslint/tslint",
|
||||
"react",
|
||||
"jsx-a11y",
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
}
|
||||
},
|
||||
rules: Object.assign(
|
||||
typescriptEslintRecommended.rules,
|
||||
typescriptRecommended.rules,
|
||||
typescriptEslintPrettier.rules,
|
||||
react.rules,
|
||||
jsxA11y.rules,
|
||||
reactPrettier.rules,
|
||||
{
|
||||
"@typescript-eslint/adjacent-overload-signatures": "error",
|
||||
// TODO: (cvle) change `readonly` param to `array-simple` when upgraded typescript.
|
||||
"@typescript-eslint/array-type": ["error", { "default": "array-simple", "readonly": "generic"}],
|
||||
"@typescript-eslint/ban-types": "error",
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/consistent-type-assertions": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": "error",
|
||||
"@typescript-eslint/class-name-casing": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": [
|
||||
"error",
|
||||
{
|
||||
"overrides": {
|
||||
"constructors": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/indent": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "error",
|
||||
"@typescript-eslint/member-delimiter-style": "off",
|
||||
"@typescript-eslint/no-empty-function": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-parameter-properties": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", {"args": "none", "ignoreRestSiblings": true}],
|
||||
"@typescript-eslint/no-use-before-define": "off", // TODO: (cvle) Should be on?
|
||||
"@typescript-eslint/no-use-before-declare": "off",
|
||||
"@typescript-eslint/no-var-requires": "error",
|
||||
"@typescript-eslint/prefer-for-of": "error",
|
||||
"@typescript-eslint/prefer-function-type": "error",
|
||||
"@typescript-eslint/prefer-namespace-keyword": "error",
|
||||
"@typescript-eslint/triple-slash-reference": "error",
|
||||
"@typescript-eslint/type-annotation-spacing": "off",
|
||||
"@typescript-eslint/unified-signatures": "error",
|
||||
// (cvle) disabled, because the way we use labels in our code cause to many
|
||||
// false positives.
|
||||
"jsx-a11y/label-has-associated-control": "off",
|
||||
"react/display-name": "error",
|
||||
"react/prop-types": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
let typescriptTypeCheckingOverrides = {
|
||||
files: ["*.ts", "*.tsx"],
|
||||
parserOptions: {
|
||||
project: ["tsconfig.json", "./src/tsconfig.json", "./src/core/client/tsconfig.json"],
|
||||
// TODO: (cvle) this is a workaround, see: https://github.com/typescript-eslint/typescript-eslint/issues/1091.
|
||||
createDefaultProgram: true,
|
||||
},
|
||||
rules: Object.assign(
|
||||
typescriptRecommendedTypeChecking.rules,
|
||||
{
|
||||
"@typescript-eslint/tslint/config": ["error", {
|
||||
"rules": {
|
||||
"ordered-imports": {
|
||||
"options": {
|
||||
"import-sources-order": "case-insensitive",
|
||||
"module-source-path": "full",
|
||||
"named-imports-order": "case-insensitive",
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
"@typescript-eslint/require-await": "off",
|
||||
"@typescript-eslint/no-misused-promises": "off",
|
||||
"@typescript-eslint/unbound-method": "off", // 10.10.19: (cvle) seems to give false positive.
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
const jestOverrides = {
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
files: ["test/**/*.ts", "test/**/*.tsx"],
|
||||
globals: {
|
||||
"expectAndFail": "readonly",
|
||||
"fail": "readonly",
|
||||
},
|
||||
};
|
||||
|
||||
// Setup the overrides.
|
||||
const overrides = [jestOverrides, typescriptOverrides];
|
||||
// Skip type information to make it faster!
|
||||
if (process.env.FAST_LINT !== "true") {
|
||||
overrides.push(typescriptTypeCheckingOverrides);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
overrides,
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:jsdoc/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
parserOptions: {
|
||||
"ecmaVersion": 2018,
|
||||
},
|
||||
rules: {
|
||||
"arrow-body-style": "off",
|
||||
"arrow-parens": [
|
||||
"off",
|
||||
"as-needed",
|
||||
],
|
||||
"camelcase": "off",
|
||||
"complexity": "off",
|
||||
"constructor-super": "error",
|
||||
"spaced-comment": ["error", "always"],
|
||||
"curly": "error",
|
||||
"dot-notation": "error",
|
||||
"eol-last": "off",
|
||||
"eqeqeq": "error",
|
||||
"guard-for-in": "error",
|
||||
"jsdoc/require-jsdoc": "off",
|
||||
"jsdoc/require-returns": "off",
|
||||
"jsdoc/require-param": "off",
|
||||
"jsdoc/require-param-type": "off",
|
||||
"jsdoc/require-returns-type": "off",
|
||||
"linebreak-style": "off",
|
||||
"max-classes-per-file": [
|
||||
"error",
|
||||
1,
|
||||
],
|
||||
"member-ordering": "off",
|
||||
"new-parens": "off",
|
||||
"newline-per-chained-call": "off",
|
||||
"no-bitwise": "error",
|
||||
"no-caller": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-console": "error",
|
||||
"no-debugger": "error",
|
||||
"no-empty": "error",
|
||||
"no-eval": "error",
|
||||
"no-extra-semi": "off",
|
||||
"no-fallthrough": "error",
|
||||
"no-invalid-this": "off",
|
||||
"no-irregular-whitespace": "off",
|
||||
"no-multiple-empty-lines": "off",
|
||||
"no-new-wrappers": "error",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-shadow": "error",
|
||||
"no-throw-literal": "error",
|
||||
"no-undef": "off",
|
||||
"no-undef-init": "error",
|
||||
"no-unsafe-finally": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"no-unused-labels": "error",
|
||||
"no-unused-vars": ["error", {"args": "none", "ignoreRestSiblings": true}],
|
||||
"no-var": "error",
|
||||
"object-shorthand": "error",
|
||||
"one-var": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
"prefer-const": "error",
|
||||
"quote-props": "off",
|
||||
"radix": "error",
|
||||
"require-atomic-updates": "off",
|
||||
"space-before-function-paren": "off",
|
||||
"sort-imports": "off",
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "off",
|
||||
},
|
||||
};
|
||||
@@ -21,3 +21,4 @@ dist
|
||||
*.css.d.ts
|
||||
__generated__
|
||||
README.md.orig
|
||||
persisted-queries.json
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"kumar-harsh.graphql-for-vscode",
|
||||
"editorconfig.editorconfig",
|
||||
"ms-azuretools.vscode-cosmosdb"
|
||||
"ms-azuretools.vscode-cosmosdb",
|
||||
"mike-co.import-sorter"
|
||||
]
|
||||
}
|
||||
@@ -10,12 +10,48 @@
|
||||
"**/.DS_Store": true,
|
||||
".vs": true
|
||||
},
|
||||
"tslint.exclude": "**/node_modules/**",
|
||||
"tslint.jsEnable": true,
|
||||
"tslint.enable": false,
|
||||
"eslint.validate": [
|
||||
{ "language": "javascript", "autoFix": true },
|
||||
{ "language": "typescript", "autoFix": true },
|
||||
{ "language": "typescriptreact", "autoFix": true }
|
||||
],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"postcss.validate": false,
|
||||
"javascript.preferences.importModuleSpecifier": "non-relative",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.tslint": true
|
||||
}
|
||||
"importSorter.importStringConfiguration.quoteMark": "double",
|
||||
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.count": 80,
|
||||
"importSorter.sortConfiguration.customOrderingRules.rules": [
|
||||
{
|
||||
"type": "importMember",
|
||||
"regex": "^$",
|
||||
"orderLevel": 70,
|
||||
"disableSort": true
|
||||
},
|
||||
{
|
||||
"regex": "__generated__",
|
||||
"orderLevel": 40
|
||||
},
|
||||
{
|
||||
"regex": "^coral-",
|
||||
"orderLevel": 30
|
||||
},
|
||||
{
|
||||
"regex": "\\.css$",
|
||||
"orderLevel": 60,
|
||||
"disableSort": true
|
||||
},
|
||||
{
|
||||
"regex": "^[.]",
|
||||
"orderLevel": 50
|
||||
},
|
||||
],
|
||||
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.type": "newLineEachExpressionAfterCountLimitExceptIfOnlyOne",
|
||||
"importSorter.importStringConfiguration.trailingComma": "multiLine",
|
||||
"importSorter.importStringConfiguration.tabSize": 2,
|
||||
"eslint.enable": true,
|
||||
"importSorter.generalConfiguration.exclude": [
|
||||
"d\\.ts$",
|
||||
"__generated__"
|
||||
],
|
||||
}
|
||||
|
||||
@@ -9,385 +9,32 @@ Built with <3 by The Coral Project, a part of [Vox Media](https://product.voxmed
|
||||
|
||||
Preview Coral easily by running Coral via a Heroku App:
|
||||
|
||||
[](https://heroku.com/deploy?template=https://github.com/coralproject/talk/tree/next)
|
||||
[](https://heroku.com/deploy?template=https://github.com/coralproject/talk)
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table of Contents
|
||||
|
||||
- [Requirements](#requirements)
|
||||
- [Running](#running)
|
||||
- [Docker](#docker)
|
||||
- [Source](#source)
|
||||
- [Development](#development)
|
||||
- [Embed On Your Site](#embed-on-your-site)
|
||||
- [Single Sign On](#single-sign-on)
|
||||
- [Email](#email)
|
||||
- [Design Language System (UI Components)](#design-language-system-ui-components)
|
||||
- [Configuration](#configuration)
|
||||
- [Documentation](#documentation)
|
||||
- [Pre-Launch Guide](#pre-launch-guide)
|
||||
- [More Resources](#more-resources)
|
||||
- [License](#license)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Requirements
|
||||
## Documentation
|
||||
|
||||
- MongoDB >=3.6
|
||||
- Redis >=3.2
|
||||
- NodeJS >=10
|
||||
- NPM >=6.7
|
||||
You can get started with Coral using our [Documentation](https://docs.coralproject.net/talk/).
|
||||
|
||||
## Running
|
||||
## Pre-Launch Guide
|
||||
|
||||
You can install Coral using Docker or via Source. We recommend Docker, as it
|
||||
provides the easiest deployment solution going forward, as all the dependencies
|
||||
are baked and shipped with the provided
|
||||
[coralproject/talk:next](https://hub.docker.com/r/coralproject/talk) image.
|
||||
When v5 releases to master, you'll be able to select it using
|
||||
`coralproject/talk:5`.
|
||||
You’ve installed Talk on your server, and you’re preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://coralproject.net/blog/youve-installed-talk-now-what/).
|
||||
|
||||
### Docker
|
||||
## More Resources
|
||||
|
||||
The easiest way to get started with Coral is through our published Docker image
|
||||
and provided example `docker-compose.yml` file. The following assumes that you
|
||||
have Docker and Docker Compose installed on your local machine:
|
||||
|
||||
- Install Docker: https://docs.docker.com/install/
|
||||
- Install Docker Compose: https://docs.docker.com/compose/install/ (this is typically included in the Docker Desktop editions already)
|
||||
|
||||
```bash
|
||||
# Create directories to persist the data in MongoDB and Redis.
|
||||
mkdir -p data/{mongo,redis}
|
||||
|
||||
# Create the `docker-compose.yml` file to get started.
|
||||
cat > docker-compose.yml <<EOF
|
||||
version: "2"
|
||||
services:
|
||||
talk:
|
||||
image: coralproject/talk:next
|
||||
restart: always
|
||||
ports:
|
||||
- "127.0.0.1:3000:5000"
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
environment:
|
||||
- MONGODB_URI=mongodb://mongo:27017/coral
|
||||
- REDIS_URI=redis://redis:6379
|
||||
- SIGNING_SECRET=<replace me with something secret>
|
||||
mongo:
|
||||
image: mongo:3.6
|
||||
volumes:
|
||||
- ./data/mongo:/data/db
|
||||
redis:
|
||||
image: redis:3.2
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
EOF
|
||||
|
||||
# Start up Coral using Docker.
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Then head on over to http://localhost:3000 to install Coral!
|
||||
|
||||
### Source
|
||||
|
||||
Coral requires NodeJS >=10, we recommend using `nvm` to help manage node
|
||||
versions: https://github.com/creationix/nvm.
|
||||
|
||||
```bash
|
||||
# Clone and cd into the Coral directory.
|
||||
git clone https://github.com/coralproject/talk.git
|
||||
cd talk
|
||||
|
||||
# Install dependencies.
|
||||
npm install
|
||||
|
||||
# Build the application dependencies.
|
||||
# This might take a while.
|
||||
npm run build
|
||||
```
|
||||
|
||||
This should output all the compiled application code to `./dist`.
|
||||
|
||||
Running Coral with default settings assumes that you have:
|
||||
|
||||
- MongoDB >=3.6 running on `127.0.0.1:27017`
|
||||
- Redis >=3.2 running on `127.0.0.1:6379`
|
||||
|
||||
If you don't already have these databases running, you can execute the following
|
||||
assuming you have Docker installed on your local machine:
|
||||
|
||||
```bash
|
||||
docker run -d -p 27017:27017 --restart always --name mongo mongo:3.6
|
||||
docker run -d -p 6379:6379 --restart always --name redis redis:3.2
|
||||
```
|
||||
|
||||
Then start Coral with:
|
||||
|
||||
```bash
|
||||
# Start the server in production mode.
|
||||
npm run start
|
||||
```
|
||||
|
||||
Then head on over to http://localhost:3000 to install Coral!
|
||||
|
||||
### Development
|
||||
|
||||
Running Coral for development is very similar to installing Coral via Source as
|
||||
described above.
|
||||
|
||||
Coral requires NodeJS >=10, we recommend using `nvm` to help manage node
|
||||
versions: https://github.com/creationix/nvm.
|
||||
|
||||
```bash
|
||||
# Clone and cd into the Coral directory.
|
||||
git clone https://github.com/coralproject/talk.git
|
||||
cd talk
|
||||
|
||||
# Install dependencies.
|
||||
npm install
|
||||
```
|
||||
|
||||
Running Coral with default settings assumes that you have:
|
||||
|
||||
- MongoDB >=3.6 running on `127.0.0.1:27017`
|
||||
- Redis >=3.2 running on `127.0.0.1:6379`
|
||||
|
||||
If you don't already have these databases running, you can execute the following
|
||||
assuming you have Docker installed on your local machine:
|
||||
|
||||
```bash
|
||||
docker run -d -p 27017:27017 --restart always --name mongo mongo:3.6
|
||||
docker run -d -p 6379:6379 --restart always --name redis redis:3.2
|
||||
```
|
||||
|
||||
Then start Coral with:
|
||||
|
||||
```bash
|
||||
# Run the server in development mode in order to facilitate auto-restarting and
|
||||
# rebuilding when file changes are detected. This might take a while to fully run.
|
||||
npm run watch
|
||||
```
|
||||
|
||||
When the client code has been built, navigate to http://localhost:8080/install
|
||||
to start the installation wizard. **Note: Ensure `localhost:8080` is used in the permitted domains list.**
|
||||
|
||||
To see the comment stream goto http://localhost:8080/.
|
||||
|
||||
To run linting and tests use the following commands:
|
||||
|
||||
```bash
|
||||
# Run the linters.
|
||||
npm run lint
|
||||
|
||||
# Run our unit and integration tests.
|
||||
npm run test
|
||||
```
|
||||
|
||||
#### Embed On Your Site
|
||||
|
||||
With Coral setup and running locally you can test embeding the comment stream with this sample embed script:
|
||||
|
||||
```
|
||||
<div id="coral_thread"></div>
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var talk = document.createElement('script'); talk.type = 'text/javascript'; talk.async = true;
|
||||
var url = '{{ CORAL_DOMAIN_NAME }}';
|
||||
talk.src = '//' + url + '/assets/js/embed.js';
|
||||
talk.onload = function() {
|
||||
Coral.createStreamEmbed({
|
||||
id: "coral_thread",
|
||||
autoRender: true,
|
||||
rootURL: '//' + url,
|
||||
});
|
||||
};
|
||||
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(talk);
|
||||
})();
|
||||
</script>
|
||||
```
|
||||
|
||||
> **NOTE:** Replace the value of `{{ CORAL_DOMAIN_NAME }}` with the location of your running instance of Coral.
|
||||
|
||||
#### Single Sign On
|
||||
|
||||
In order to allow seamless connection to an existing authentication system,
|
||||
Coral utilizes the industry standard [JWT Token](https://jwt.io/) to connect. To
|
||||
learn more about how to create a JWT token, see [this introduction](https://jwt.io/introduction/).
|
||||
|
||||
1. Visit: `https://{{ CORAL_DOMAIN_NAME }}/admin/configure/auth`
|
||||
2. Scroll to the `Login with Single Sign On` section
|
||||
3. Enable the Single Sign On Authentication Integration
|
||||
4. Enable `Allow Registration`
|
||||
5. Copy the string in the `Key` box
|
||||
6. Click Save
|
||||
|
||||
> **NOTE:** Replace the value of `{{ CORAL_DOMAIN_NAME }}` with the location of your running instance of Coral.
|
||||
|
||||
You will then have to generate a JWT with the following claims:
|
||||
|
||||
- `jti` (_optional_) - A unique ID for this particular JWT token. We recommend
|
||||
using a [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier)
|
||||
for this value. Without this parameter, the logout functionality inside the
|
||||
embed stream will not work and you will need to call logout on the embed
|
||||
itself.
|
||||
- `exp` (_optional_) - When the given SSO token should expire. This is
|
||||
specified as a unix time stamp in seconds. Once the token has expired, a new
|
||||
token should be generated and passed into Coral. Without this parameter, the
|
||||
logout functionality inside the embed stream will not work and you will need
|
||||
to call logout on the embed itself.
|
||||
- `iat` (_optional_) - When the given SSO token was issued. This is required to
|
||||
utilize the automatic user detail update system. If this time is newer than
|
||||
the time we received the last update, the contents of the token will be used
|
||||
to update the user.
|
||||
- `user.id` (**required**) - the ID of the user from your authentication system.
|
||||
This is required to connect the user in your system to allow a seamless
|
||||
connection to Coral.
|
||||
- `user.email` (**required**) - the email address of the user from your
|
||||
authentication system. This is required to facilitate notification email's
|
||||
about status changes on a user account such as bans or suspensions.
|
||||
- `user.username` (**required**) - the username that should be used when being
|
||||
presented inside Coral to moderators and other users.
|
||||
|
||||
An example of the claims for this token would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"jti": "151c19fc-ad15-4f80-a49c-09f137789fbb",
|
||||
"exp": 1572172094,
|
||||
"iat": 1562172094,
|
||||
"user": {
|
||||
"id": "628bdc61-6616-4add-bfec-dd79156715d4",
|
||||
"email": "bob@example.com",
|
||||
"username": "bob"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With the claims provided, you can sign them with the `Key` obtained from the
|
||||
Coral administration panel in the previous steps with a `HS256` algorithm. This
|
||||
token can be provided in the above mentioned embed code by adding it to the
|
||||
`createStreamEmbed` function:
|
||||
|
||||
```js
|
||||
Coral.createStreamEmbed({
|
||||
// Don't forget to include the parameters from the
|
||||
// "Embed On Your Site" section.
|
||||
accessToken: "{{ SSO_TOKEN }}",
|
||||
});
|
||||
```
|
||||
|
||||
Or by calling the `login/logout` method on the embed object:
|
||||
|
||||
```js
|
||||
var embed = Coral.createStreamEmbed({
|
||||
// Don't forget to include the parameters from the
|
||||
// "Embed On Your Site" section.
|
||||
});
|
||||
|
||||
// Login the current embed with the generated SSO token.
|
||||
embed.login("{{ SSO_TOKEN }}");
|
||||
|
||||
// Logout the user.
|
||||
embed.logout();
|
||||
```
|
||||
|
||||
#### Email
|
||||
|
||||
To test out the email sending functionality, you can run [inbucket](https://www.inbucket.org/)
|
||||
which provides a test SMTP server that can visualize emails in the browser:
|
||||
|
||||
```bash
|
||||
docker run -d --name inbucket --restart always -p 2500:2500 -p 9000:9000 inbucket/inbucket
|
||||
```
|
||||
|
||||
You can then configure the email server on Coral
|
||||
by setting the email settings in
|
||||
`Configure -> Email` in the admin:
|
||||
|
||||
| Field | Value |
|
||||
| -------------- | -------------------- |
|
||||
| From Address | `community@test.com` |
|
||||
| Secure | `No` |
|
||||
| Host | `localhost` |
|
||||
| Port | `2500` |
|
||||
| Authentication | `No` |
|
||||
|
||||
Navigate to http://localhost:9000, click the "Monitor" tab. New emails received
|
||||
on this screen.
|
||||
|
||||
#### Design Language System (UI Components)
|
||||
|
||||
We use [docz](https://docz.site) to document and develop our Design Language System. To start docz run:
|
||||
|
||||
```bash
|
||||
# Make sure CSS types are generated.
|
||||
# This is not required when `npm run watch` is already running.
|
||||
npm run generate:css-types
|
||||
|
||||
# Run docz in development.
|
||||
npm run docz -- dev
|
||||
```
|
||||
|
||||
After compilation has finished you can access docz at http://localhost:3030/.
|
||||
|
||||
## Configuration
|
||||
|
||||
The following environment variables can be set to configure the Coral Server. You
|
||||
can expose them in your shell via `export NODE_ENV=development` or by placing
|
||||
the variables in a `.env` file in the root of the project in a simple
|
||||
`NODE_ENV=development` format delimited by newlines.
|
||||
|
||||
- `NODE_ENV` - Can be one of `production` or `development`. All production
|
||||
deployments should use `production`. Defaults to `production` when ran with
|
||||
`npm run start` and `development` when run with `npm run start:development`.
|
||||
- `PORT` - The port to listen for HTTP and Websocket requests. (Default `3000`)
|
||||
- `MONGODB_URI` - The MongoDB database URI to connect to.
|
||||
(Default `mongodb://127.0.0.1:27017/coral`)
|
||||
- `REDIS_URI` - The Redis database URI to connect to.
|
||||
(Default `redis://127.0.0.1:6379`)
|
||||
- `REDIS_OPTIONS` - A JSON string with optional configuration options to be used
|
||||
when connecting to Redis as specified in the [ioredis](https://github.com/luin/ioredis/blob/1dac50a63753c2afc969315cfe38faf0edc50bc5/API.md#new_Redis_new) documentation.
|
||||
(Default: `{}`)
|
||||
- `SIGNING_SECRET` - The shared secret to use to sign JSON Web Tokens (JWT) with
|
||||
the selected signing algorithm. 🚨 **Don't forget to set this variable!** 🚨
|
||||
(Default: `keyboard cat`)
|
||||
- `SIGNING_ALGORITHM` - The signing algorithm to use for signing JWT's.
|
||||
(Default `HS256`).
|
||||
- `LOGGING_LEVEL` - The logging level that can be set to one of `fatal`,
|
||||
`error`, `warn`, `info`, `debug`, or `trace`. (Default `info`)
|
||||
- `STATIC_URI` - The URI that static assets can be accessed from. This URI can
|
||||
be to a proxy that uses this Coral server on `PORT` as the upstream. Disabled
|
||||
by default.
|
||||
- `DISABLE_TENANT_CACHING` - When `true`, all tenants will be loaded from the
|
||||
database when needed rather than keeping a in-memory copy in sync via
|
||||
published events on Redis. (Default `false`)
|
||||
- `DISABLE_MONGODB_AUTOINDEXING` - When `true`, Coral will not perform indexing
|
||||
operations when it starts up. This can be desired when you've already
|
||||
installed Coral on the target MongoDB, but want to improve start performance.
|
||||
**You should not use this parameter unless you know what you're doing! Upgrades
|
||||
may introduce additional indexes that the application relies on.**
|
||||
(Default `false`)
|
||||
- `LOCALE` - Specify the default locale to use for all requests without a locale
|
||||
specified. (Default `en-US`)
|
||||
- `ENABLE_GRAPHIQL` - When `true`, it will enable the `/graphiql` even in
|
||||
production, use with care. (Default `false`)
|
||||
- `CONCURRENCY` - The number of worker nodes to spawn to handle web traffic,
|
||||
this should be tied to the number of CPU's available. (Default
|
||||
`os.cpus().length`)
|
||||
- `DEV_PORT` - The port where the Webpack Development server is running on.
|
||||
(Default `8080`)
|
||||
- `METRICS_USERNAME` - The username for _Basic Authentication_ at the `/metrics` and `/cluster_metrics`
|
||||
endpoint.
|
||||
- `METRICS_PASSWORD` - The password for _Basic Authentication_ at the `/metrics` and `/cluster_metrics`
|
||||
endpoint.
|
||||
- `CLUSTER_METRICS_PORT` - If `CONCURRENCY` is more than `1`, the metrics are provided at this port under `/cluster_metrics`. (Default `3001`)
|
||||
- `DISABLE_LIVE_UPDATES` - When `true`, disables subscriptions for the comment
|
||||
stream for all stories across all tenants (Default `false`)
|
||||
- `WEBSOCKET_KEEP_ALIVE_TIMEOUT` - A duration in a parsable format (e.g. `30 seconds`
|
||||
, `1 minute`) that should be used to send keep alive messages through the
|
||||
websocket to keep the socket alive (Default `30 seconds`)
|
||||
- [Our Blog](https://coralproject.net/blog)
|
||||
- [Community Guides for Journalism](https://guides.coralproject.net/)
|
||||
- [More About Us](https://coralproject.net/)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
"ENABLE_GRAPHIQL": {
|
||||
"description": "When true, this will enable the GraphiQL routes",
|
||||
"value": "false"
|
||||
},
|
||||
"TRUST_PROXY": {
|
||||
"description": "When set to 1, it instructs Coral to trust up to one proxy, this is needed for authentication support",
|
||||
"value": "1"
|
||||
}
|
||||
},
|
||||
"addons": [
|
||||
|
||||
@@ -29,6 +29,7 @@ module.exports = {
|
||||
"^coral-account/(.*)$": "<rootDir>/src/core/client/account/$1",
|
||||
"^coral-admin/(.*)$": "<rootDir>/src/core/client/admin/$1",
|
||||
"^coral-auth/(.*)$": "<rootDir>/src/core/client/auth/$1",
|
||||
"^coral-count/(.*)$": "<rootDir>/src/core/client/count/$1",
|
||||
"^coral-ui/(.*)$": "<rootDir>/src/core/client/ui/$1",
|
||||
"^coral-stream/(.*)$": "<rootDir>/src/core/client/stream/$1",
|
||||
"^coral-framework/(.*)$": "<rootDir>/src/core/client/framework/$1",
|
||||
|
||||
@@ -27,7 +27,7 @@ const config: Config = {
|
||||
"**/test/**/*",
|
||||
"core/**/*.spec.*",
|
||||
],
|
||||
executor: new CommandExecutor("npm run --silent generate:relay-stream", {
|
||||
executor: new CommandExecutor("npm run --silent generate:relay:stream", {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
@@ -44,7 +44,7 @@ const config: Config = {
|
||||
"**/test/**/*",
|
||||
"core/**/*.spec.*",
|
||||
],
|
||||
executor: new CommandExecutor("npm run generate:relay-account", {
|
||||
executor: new CommandExecutor("npm run generate:relay:account", {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
@@ -61,7 +61,7 @@ const config: Config = {
|
||||
"**/test/**/*",
|
||||
"core/**/*.spec.*",
|
||||
],
|
||||
executor: new CommandExecutor("npm run --silent generate:relay-admin", {
|
||||
executor: new CommandExecutor("npm run --silent generate:relay:admin", {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
@@ -78,7 +78,7 @@ const config: Config = {
|
||||
"**/test/**/*",
|
||||
"core/**/*.spec.*",
|
||||
],
|
||||
executor: new CommandExecutor("npm run --silent generate:relay-auth", {
|
||||
executor: new CommandExecutor("npm run --silent generate:relay:auth", {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
@@ -95,7 +95,7 @@ const config: Config = {
|
||||
"**/test/**/*",
|
||||
"core/**/*.spec.*",
|
||||
],
|
||||
executor: new CommandExecutor("npm run --silent generate:relay-install", {
|
||||
executor: new CommandExecutor("npm run --silent generate:relay:install", {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function({
|
||||
{ from: /^\/account/, to: "/account.html" },
|
||||
{ from: /^\/admin/, to: "/admin.html" },
|
||||
{ from: /^\/embed\/stream/, to: "/stream.html" },
|
||||
{ from: /^\/embed\/auth/, to: "/auth.html" },
|
||||
{ from: /^\/embed\/auth$/, to: "/auth.html" },
|
||||
{ from: /^\/embed\/auth\/callback/, to: "/auth-callback.html" },
|
||||
{ from: /^\/install/, to: "/install.html" },
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coralproject/talk",
|
||||
"version": "5.0.0-beta.6",
|
||||
"version": "5.2.0",
|
||||
"author": "The Coral Project",
|
||||
"homepage": "https://coralproject.net/",
|
||||
"sideEffects": [
|
||||
@@ -20,27 +20,31 @@
|
||||
],
|
||||
"description": "A better commenting experience from Mozilla, The Washington Post, and The New York Times.",
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production npm-run-all generate --parallel lint:client build:client build:server",
|
||||
"build:development": "NODE_ENV=development npm-run-all generate --parallel lint:client build:client build:server",
|
||||
"build": "NODE_ENV=production npm-run-all generate-persist --parallel build:client build:server",
|
||||
"build:development": "NODE_ENV=development npm-run-all generate --parallel build:client build:server",
|
||||
"build:client": "ts-node --transpile-only ./scripts/build.ts",
|
||||
"build:server": "gulp server",
|
||||
"migration:create": "ts-node --transpile-only ./scripts/migration/create.ts",
|
||||
"doctoc": "doctoc --title='## Table of Contents' --github README.md",
|
||||
"generate": "npm-run-all --parallel generate:*",
|
||||
"generate": "npm-run-all generate:css-types generate:schema generate:relay",
|
||||
"generate-persist": "npm-run-all generate:css-types generate:schema generate:relay-persist",
|
||||
"generate:css-types": "tcm src/core/client/",
|
||||
"generate:relay-stream": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/stream --schema tenant",
|
||||
"generate:relay-account": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/account --schema tenant",
|
||||
"generate:relay-auth": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/auth --schema tenant",
|
||||
"generate:relay-install": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/install --schema tenant",
|
||||
"generate:relay-admin": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/admin --schema tenant",
|
||||
"generate:relay": "npm-run-all --parallel generate:relay:*",
|
||||
"generate:relay-persist": "npm-run-all --parallel 'generate:relay:* -- --persist'",
|
||||
"generate:relay:stream": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/stream --schema tenant",
|
||||
"generate:relay:account": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/account --schema tenant",
|
||||
"generate:relay:auth": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/auth --schema tenant",
|
||||
"generate:relay:install": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/install --schema tenant",
|
||||
"generate:relay:admin": "ts-node --transpile-only ./scripts/compileRelay --src ./src/core/client/admin --schema tenant",
|
||||
"generate:schema": "node ./scripts/generateSchemaTypes.js",
|
||||
"docz": "docz",
|
||||
"start": "NODE_ENV=production node dist/index.js",
|
||||
"start:development": "NODE_ENV=development CONCURRENCY=${CONCURRENCY:-1} TS_NODE_PROJECT=./src/tsconfig.json ts-node-dev --inspect --transpile-only --no-notify -r tsconfig-paths/register ./src/index.ts",
|
||||
"start:webpackDevServer": "ts-node --transpile-only ./scripts/start.ts",
|
||||
"lint": "npm-run-all --parallel lint:* tscheck:*",
|
||||
"lint:server": "tslint --project ./src/tsconfig.json",
|
||||
"lint:client": "tslint --project ./src/core/client/tsconfig.json",
|
||||
"lint:scripts": "tslint --project ./tsconfig.json",
|
||||
"lint:server": "eslint 'src/**/*.{js,ts,tsx}' --ignore-pattern 'src/core/client/**'",
|
||||
"lint:client": "eslint 'src/core/client/**/*.{js,ts,tsx}'",
|
||||
"lint:scripts": "eslint 'scripts/**/*.{js,ts,tsx}'",
|
||||
"lint:graphql": "graphql-schema-linter src/core/server/graph/tenant/schema/schema.graphql",
|
||||
"lint-fix": "npm run lint:server -- --fix && npm run lint:client -- --fix && npm run lint:scripts -- --fix",
|
||||
"test": "node scripts/test.js --env=jsdom",
|
||||
@@ -53,22 +57,21 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@coralproject/bunyan-prettystream": "^0.1.4",
|
||||
"@types/archiver": "^3.0.0",
|
||||
"akismet-api": "^4.2.0",
|
||||
"apollo-server-express": "^2.1.0",
|
||||
"apollo-server-express": "^2.8.1",
|
||||
"archiver": "^3.0.3",
|
||||
"basic-auth": "^2.0.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bull": "^3.8.1",
|
||||
"bunyan": "^1.8.12",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
"common-tags": "^1.8.0",
|
||||
"consolidate": "0.14.0",
|
||||
"content-security-policy-builder": "^2.0.0",
|
||||
"convict": "^4.3.1",
|
||||
"cookie": "^0.4.0",
|
||||
"cookie-parser": "^1.4.4",
|
||||
"cors": "^2.8.4",
|
||||
"cron": "^1.7.1",
|
||||
"csv-stringify": "^5.3.0",
|
||||
"dataloader": "^1.4.0",
|
||||
"dompurify": "^1.0.8",
|
||||
@@ -77,11 +80,8 @@
|
||||
"env-rewrite": "^1.0.2",
|
||||
"express": "^4.16.3",
|
||||
"express-static-gzip": "^0.3.2",
|
||||
"farce": "^0.2.6",
|
||||
"fluent": "^0.10.0",
|
||||
"fluent-dom": "^0.4.1",
|
||||
"found": "^0.4.0-alpha.17",
|
||||
"found-relay": "^0.4.0-alpha.8",
|
||||
"fs-extra": "^6.0.1",
|
||||
"graphql": "^0.13.2",
|
||||
"graphql-config": "^2.0.1",
|
||||
@@ -99,9 +99,10 @@
|
||||
"jsonwebtoken": "^8.3.0",
|
||||
"juice": "^5.2.0",
|
||||
"jwks-rsa": "^1.3.0",
|
||||
"linkify-it": "^2.1.0",
|
||||
"keymaster": "^1.6.2",
|
||||
"linkifyjs": "^2.1.8",
|
||||
"lodash": "^4.17.10",
|
||||
"lodash": "^4.17.15",
|
||||
"lru-cache": "^5.1.1",
|
||||
"luxon": "^1.12.0",
|
||||
"metascraper-author": "^3.11.8",
|
||||
"metascraper-date": "^3.11.4",
|
||||
@@ -126,15 +127,14 @@
|
||||
"prom-client": "^11.3.0",
|
||||
"proxy-agent": "^3.1.0",
|
||||
"querystringify": "^2.1.0",
|
||||
"react-relay-network-modern": "^3.0.4",
|
||||
"react-helmet": "^5.2.1",
|
||||
"source-map-support": "^0.5.12",
|
||||
"stack-utils": "^1.0.2",
|
||||
"striptags": "^3.1.1",
|
||||
"throng": "^4.0.0",
|
||||
"tlds": "^1.203.1",
|
||||
"tsscmp": "^1.0.6",
|
||||
"url-regex": "^5.0.0",
|
||||
"uuid": "^3.3.2",
|
||||
"uuid": "^3.3.3",
|
||||
"verror": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -149,6 +149,7 @@
|
||||
"@coralproject/rte": "^0.10.15",
|
||||
"@intervolga/optimize-cssnano-plugin": "^1.0.6",
|
||||
"@types/agent-base": "^4.2.0",
|
||||
"@types/archiver": "^3.0.0",
|
||||
"@types/basic-auth": "^1.1.2",
|
||||
"@types/bcryptjs": "^2.4.1",
|
||||
"@types/bull": "^3.5.12",
|
||||
@@ -164,6 +165,7 @@
|
||||
"@types/cookie": "^0.3.3",
|
||||
"@types/cookie-parser": "^1.4.1",
|
||||
"@types/cors": "^2.8.4",
|
||||
"@types/cron": "^1.7.1",
|
||||
"@types/cross-spawn": "^6.0.0",
|
||||
"@types/dotenv": "^4.0.3",
|
||||
"@types/enzyme": "^3.1.15",
|
||||
@@ -181,9 +183,9 @@
|
||||
"@types/joi": "^13.0.8",
|
||||
"@types/jsdom": "^12.2.3",
|
||||
"@types/jsonwebtoken": "^7.2.7",
|
||||
"@types/linkify-it": "^2.1.0",
|
||||
"@types/linkifyjs": "^2.1.1",
|
||||
"@types/lodash": "^4.14.118",
|
||||
"@types/lru-cache": "^5.1.0",
|
||||
"@types/luxon": "^1.12.0",
|
||||
"@types/marked": "^0.6.0",
|
||||
"@types/mini-css-extract-plugin": "^0.2.0",
|
||||
@@ -193,6 +195,7 @@
|
||||
"@types/node-fetch": "^2.3.3",
|
||||
"@types/nodemailer": "^4.6.2",
|
||||
"@types/nunjucks": "^3.1.1",
|
||||
"@types/object-diff": "0.0.0",
|
||||
"@types/on-finished": "^2.3.1",
|
||||
"@types/passport": "^0.4.6",
|
||||
"@types/passport-facebook": "^2.1.8",
|
||||
@@ -204,6 +207,7 @@
|
||||
"@types/react": "^16.8.15",
|
||||
"@types/react-copy-to-clipboard": "^4.2.6",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/react-helmet": "^5.0.10",
|
||||
"@types/react-relay": "^1.3.14",
|
||||
"@types/react-responsive": "^3.0.1",
|
||||
"@types/react-test-renderer": "^16.8.1",
|
||||
@@ -217,7 +221,6 @@
|
||||
"@types/source-map-support": "^0.5.0",
|
||||
"@types/stack-utils": "^1.0.1",
|
||||
"@types/throng": "^4.0.2",
|
||||
"@types/tlds": "^1.199.0",
|
||||
"@types/uuid": "^3.4.4",
|
||||
"@types/verror": "^1.10.3",
|
||||
"@types/vinyl": "^2.0.2",
|
||||
@@ -226,6 +229,9 @@
|
||||
"@types/webpack-bundle-analyzer": "^2.13.1",
|
||||
"@types/webpack-dev-server": "^3.1.5",
|
||||
"@types/ws": "^5.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "2.3.3",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "2.3.3",
|
||||
"@typescript-eslint/parser": "2.3.3",
|
||||
"acorn": "^6.1.1",
|
||||
"ansi-styles": "^3.2.0",
|
||||
"autoprefixer": "^9.5.1",
|
||||
@@ -245,6 +251,7 @@
|
||||
"classnames": "^2.2.6",
|
||||
"commander": "^2.20.0",
|
||||
"comment-json": "^1.1.3",
|
||||
"common-tags": "^1.8.0",
|
||||
"compression-webpack-plugin": "^2.0.0",
|
||||
"copy-webpack-plugin": "^5.0.3",
|
||||
"cross-spawn": "^6.0.5",
|
||||
@@ -256,13 +263,22 @@
|
||||
"enzyme": "^3.9.0",
|
||||
"enzyme-adapter-react-16": "^1.12.1",
|
||||
"enzyme-to-json": "^3.3.5",
|
||||
"eslint": "^6.5.1",
|
||||
"eslint-config-prettier": "^6.3.0",
|
||||
"eslint-plugin-jsdoc": "^15.9.7",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-react": "^7.15.1",
|
||||
"eventemitter2": "^5.0.1",
|
||||
"farce": "^0.2.6",
|
||||
"final-form": "4.11.0",
|
||||
"flat": "^4.1.0",
|
||||
"fluent-intl-polyfill": "^0.1.0",
|
||||
"fluent-langneg": "^0.1.1",
|
||||
"fluent-react": "^0.8.4",
|
||||
"fork-ts-checker-webpack-plugin": "^1.3.0",
|
||||
"fork-ts-checker-webpack-plugin": "^1.5.0",
|
||||
"found": "^0.4.0-alpha.17",
|
||||
"found-relay": "^0.4.0-alpha.8",
|
||||
"graphql-schema-linter": "^0.2.0",
|
||||
"graphql-schema-typescript": "^1.2.9",
|
||||
"gulp": "^4.0.2",
|
||||
@@ -279,10 +295,11 @@
|
||||
"jest-mock-console": "^1.0.0",
|
||||
"lint-staged": "^8.1.6",
|
||||
"loader-utils": "^1.2.3",
|
||||
"lodash-es": "^4.17.11",
|
||||
"marked": "^0.6.0",
|
||||
"lodash-es": "^4.17.14",
|
||||
"marked": "^0.7.0",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"mini-css-extract-plugin": "^0.6.0",
|
||||
"object-diff": "0.0.4",
|
||||
"postcss-advanced-variables": "^3.0.0",
|
||||
"postcss-css-variables": "^0.11.0",
|
||||
"postcss-flexbugs-fixes": "^4.1.0",
|
||||
@@ -294,7 +311,7 @@
|
||||
"postcss-nested": "^4.1.1",
|
||||
"postcss-prepend-imports": "^1.0.1",
|
||||
"postcss-preset-env": "^6.5.0",
|
||||
"prettier": "^1.17.0",
|
||||
"prettier": "^1.18.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"pstree.remy": "^1.1.6",
|
||||
"pym.js": "^1.3.2",
|
||||
@@ -307,6 +324,7 @@
|
||||
"react-final-form": "4.0.2",
|
||||
"react-popper": "^1.3.2",
|
||||
"react-relay": "^4.0.0",
|
||||
"react-relay-network-modern": "^4.0.4",
|
||||
"react-responsive": "^7.0.0",
|
||||
"react-test-renderer": "^16.9.0-alpha.0",
|
||||
"react-timeago": "^4.1.9",
|
||||
@@ -336,10 +354,7 @@
|
||||
"ts-node-dev": "^1.0.0-pre.37",
|
||||
"tsconfig-paths": "^3.8.0",
|
||||
"tsconfig-paths-webpack-plugin": "^3.2.0",
|
||||
"tslint": "^5.16.0",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"tslint-plugin-prettier": "^2.0.1",
|
||||
"tslint-react": "^4.0.0",
|
||||
"tslint": "^5.20.0",
|
||||
"typed-css-modules": "^0.4.2",
|
||||
"typeface-manuale": "^0.0.71",
|
||||
"typeface-source-sans-pro": "^0.0.54",
|
||||
@@ -355,12 +370,12 @@
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
"pre-commit": "FAST_LINT=true lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{j,t}s{,x}": [
|
||||
"tslint"
|
||||
"eslint"
|
||||
],
|
||||
"src/core/server/graph/tenant/schema/schema.graphql": [
|
||||
"graphql-schema-linter"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
// tslint:disable:no-console
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const address = require("address");
|
||||
const url = require("url");
|
||||
@@ -21,7 +21,6 @@ const clearConsole = require("react-dev-utils/clearConsole");
|
||||
const formatWebpackMessages = require("react-dev-utils/formatWebpackMessages");
|
||||
const typescriptFormatter = require("react-dev-utils/typescriptFormatter");
|
||||
const forkTsCheckerWebpackPlugin = require("react-dev-utils/ForkTsCheckerWebpackPlugin");
|
||||
const Stats = require("webpack/lib/Stats");
|
||||
|
||||
// (cvle): Changed to false as we are sharing the tty with other processes.
|
||||
// const isInteractive = process.stdout.isTTY && false;
|
||||
@@ -142,7 +141,7 @@ function createCompiler({
|
||||
});
|
||||
|
||||
let isFirstCompile = true;
|
||||
let tsMessagesPromises = [];
|
||||
const tsMessagesPromises = [];
|
||||
|
||||
if (useTypeScript) {
|
||||
compiler.compilers.forEach(singleCompiler => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import paths from "../config/paths";
|
||||
import config from "../src/core/build/config";
|
||||
import createWebpackConfig from "../src/core/build/createWebpackConfig";
|
||||
|
||||
// tslint:disable: no-console
|
||||
/* eslint-disable no-console */
|
||||
|
||||
process.env.WEBPACK = "true";
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || "production";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import program from "commander";
|
||||
import spawn from "cross-spawn";
|
||||
import fs from "fs";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
|
||||
const config = JSON.parse(
|
||||
@@ -14,33 +14,34 @@ program
|
||||
.usage("--src ./src/core/client/stream --schema tenant")
|
||||
.option("--src <folder>", "Find gql recursively in this folder")
|
||||
.option("--schema <schema>", "Identifier of schema")
|
||||
.option("--persist", "Use persisted queries")
|
||||
.description("Compile relay gql data")
|
||||
.parse(process.argv);
|
||||
|
||||
if (!program.schema) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Schema identifier not provided");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!program.src) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Src not provided");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!config.projects) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Missing projects key in .graphqconfig");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!config.projects[program.schema]) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Project ${program.schema} not found in .graphqconfig`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!config.projects[program.schema].schemaPath) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`SchemaPath for project ${program.schema} not found in .graphqconfig`
|
||||
);
|
||||
@@ -57,8 +58,30 @@ const args = [
|
||||
`${program.src}/__generated__`,
|
||||
"--schema",
|
||||
config.projects[program.schema].schemaPath,
|
||||
// "--persist-output",
|
||||
// `${program.src}/persisted-queries.json`,
|
||||
];
|
||||
|
||||
// Set the persisted query path.
|
||||
const persist = program.persist
|
||||
? `${program.src}/persisted-queries.json`
|
||||
: null;
|
||||
if (persist) {
|
||||
args.push("--persist-output", persist);
|
||||
}
|
||||
|
||||
spawn.sync("relay-compiler", args, { stdio: "inherit" });
|
||||
|
||||
if (persist) {
|
||||
if (fs.existsSync(persist)) {
|
||||
// Create the new filename.
|
||||
const name = path.basename(program.src);
|
||||
const generated = "./src/core/server/graph/common/persisted/__generated__";
|
||||
|
||||
// Create the generated directory if it doesn't exist.
|
||||
fs.ensureDirSync(generated);
|
||||
|
||||
// Copy the file over to the destination directory.
|
||||
fs.copySync(persist, `${generated}/${name}.json`, {
|
||||
overwrite: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const { Linter, Configuration } = require("tslint");
|
||||
const { generateTSTypesAsString } = require("graphql-schema-typescript");
|
||||
const { getGraphQLConfig } = require("graphql-config");
|
||||
const path = require("path");
|
||||
@@ -69,12 +68,12 @@ if (require.main === module) {
|
||||
main()
|
||||
.then(files => {
|
||||
for (const { fileName } of files) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Generated ${fileName}`);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env ts-node
|
||||
|
||||
/**
|
||||
* This script can be invoked via:
|
||||
*
|
||||
* npm run migration:create <migration name>
|
||||
*
|
||||
* To create new database migrations.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import fs from "fs-extra";
|
||||
import lodash from "lodash";
|
||||
import path from "path";
|
||||
|
||||
const templateFilePath = path.resolve(
|
||||
path.join(
|
||||
__dirname,
|
||||
"../../src/core/server/services/migrate/migration_sample.ts"
|
||||
)
|
||||
);
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
if (argv.length !== 1) {
|
||||
console.error("usage: npm run migration:create <migration name>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get the name of the new migration.
|
||||
const name = lodash.snakeCase(argv[0]);
|
||||
|
||||
// Get the version of the new migration.
|
||||
const version = Date.now();
|
||||
|
||||
// Get the filePath of the new migration.
|
||||
const filePath = path.resolve(
|
||||
path.join(
|
||||
__dirname,
|
||||
`../../src/core/server/services/migrate/migrations/${version}_${name}.ts`
|
||||
)
|
||||
);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
console.error(`migration already exists at: ${filePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Write the template out to the file.
|
||||
fs.copyFileSync(templateFilePath, filePath);
|
||||
|
||||
console.log(`created new migration at: ${filePath}`);
|
||||
@@ -14,7 +14,7 @@ import createDevServerConfig from "../config/webpackDevServer.config";
|
||||
import config from "../src/core/build/config";
|
||||
import createWebpackConfig from "../src/core/build/createWebpackConfig";
|
||||
|
||||
// tslint:disable: no-console
|
||||
/* eslint-disable no-console */
|
||||
|
||||
// Enforce environment to be development.
|
||||
config.validate().set("env", "development");
|
||||
|
||||
@@ -24,7 +24,7 @@ process.on("unhandledRejection", err => {
|
||||
const paths = require("../config/paths.ts").default;
|
||||
|
||||
const jest = require("jest");
|
||||
let argv = process.argv.slice(2);
|
||||
const argv = process.argv.slice(2);
|
||||
argv.push("--config", paths.appJestConfig);
|
||||
|
||||
// Watch unless on CI or in coverage mode
|
||||
|
||||
@@ -21,8 +21,8 @@ export default class CommandExecutor implements Executor {
|
||||
private args?: ReadonlyArray<string>;
|
||||
private spawnMultiple: boolean;
|
||||
private runOnInit: boolean;
|
||||
private isRunning: boolean = false;
|
||||
private shouldRespawn: boolean = false;
|
||||
private isRunning = false;
|
||||
private shouldRespawn = false;
|
||||
private spawnProcessDebounced?: (() => void) & Cancelable;
|
||||
|
||||
constructor(cmd: string, opts: CommandExecutorOptions = {}) {
|
||||
@@ -67,7 +67,7 @@ export default class CommandExecutor implements Executor {
|
||||
child.on("close", (code: number) => {
|
||||
this.isRunning = false;
|
||||
if (code !== 0 && code !== null) {
|
||||
// tslint:disable-next-line: no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.red(`Command exited with ${code}`));
|
||||
}
|
||||
if (this.shouldRespawn) {
|
||||
|
||||
@@ -17,8 +17,8 @@ export default class LongRunningExecutor implements Executor {
|
||||
private cmd: string;
|
||||
private args?: ReadonlyArray<string>;
|
||||
private process: ChildProcess | null = null;
|
||||
private isRunning: boolean = false;
|
||||
private shouldRestart: boolean = false;
|
||||
private isRunning = false;
|
||||
private shouldRestart = false;
|
||||
private restartDebounced: (() => void) & Cancelable;
|
||||
|
||||
constructor(cmd: string, opts: LongRunningExecutorOptions = {}) {
|
||||
@@ -37,11 +37,11 @@ export default class LongRunningExecutor implements Executor {
|
||||
shell: !this.args,
|
||||
});
|
||||
|
||||
this.process!.on("exit", (code: number) => {
|
||||
this.process.on("exit", (code: number) => {
|
||||
this.isRunning = false;
|
||||
|
||||
if (code !== 0 && code !== null) {
|
||||
// tslint:disable-next-line: no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.red(`Command exited with ${code}`));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ function canUseWatchman(): boolean {
|
||||
try {
|
||||
execSync("watchman --version", { stdio: ["ignore"] });
|
||||
return true;
|
||||
// tslint:disable-next-line:no-empty
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
return false;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export default class SaneWatcher implements Watcher {
|
||||
// Autodetect watchman.
|
||||
if (this.watchman === undefined && canUseWatchman()) {
|
||||
this.watchman = true;
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.grey(`Watchman detected`));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ async function run(
|
||||
throw new Error("Config file not specified");
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
let config: any = require(path.resolve(configFile));
|
||||
if (config.__esModule) {
|
||||
config = config.default;
|
||||
@@ -31,7 +31,7 @@ const cmd = program
|
||||
.parse(process.argv);
|
||||
|
||||
run(cmd.args, cmd.opts()).catch(err => {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ async function beginWatch(
|
||||
await executor.onInit();
|
||||
}
|
||||
for await (const filePath of watcher.watch(rootDir, paths, { ignore })) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.cyanBright(`Execute "${key}"`));
|
||||
executor.execute(filePath);
|
||||
}
|
||||
@@ -72,7 +72,7 @@ function filterOnly(
|
||||
}
|
||||
return pickBy(watchers, (value, key) => {
|
||||
if (resolved.indexOf(key) === -1) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.grey(`Disabled watcher "${key}"`));
|
||||
return false;
|
||||
}
|
||||
@@ -98,11 +98,11 @@ export default async function watch(config: Config, options: Options = {}) {
|
||||
}
|
||||
|
||||
for (const key of Object.keys(watchersConfigs)) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.cyanBright(`Start watcher "${key}"`));
|
||||
const watcherConfig = watchersConfigs[key];
|
||||
beginWatch(watcher, key, watcherConfig, rootDir).catch(err => {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// tslint:disable:no-console
|
||||
/* eslint-disable */
|
||||
|
||||
// This alternative WebpackDevServer combines the functionality of:
|
||||
// https://github.com/webpack/webpack-dev-server/blob/webpack-1/client/index.js
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import convict from "convict";
|
||||
|
||||
import { LOCALES } from "../common/helpers/i18n/locales";
|
||||
|
||||
const config = convict({
|
||||
env: {
|
||||
doc: "The application environment.",
|
||||
@@ -21,6 +23,12 @@ const config = convict({
|
||||
env: "DEV_PORT",
|
||||
arg: "dev-port",
|
||||
},
|
||||
defaultLocale: {
|
||||
doc: "Specify the default locale to use",
|
||||
format: LOCALES,
|
||||
default: "en-US",
|
||||
env: "LOCALE",
|
||||
},
|
||||
generateReport: {
|
||||
doc: "Generate a report using webpack-bundle-analyzer",
|
||||
format: Boolean,
|
||||
|
||||
@@ -92,23 +92,23 @@ export default function createWebpackConfig(
|
||||
const localesOptions = {
|
||||
pathToLocales: paths.appLocales,
|
||||
|
||||
// Default locale if non could be negotiated.
|
||||
defaultLocale: "en-US",
|
||||
// Default locale if none was specified.
|
||||
defaultLocale: config.get("defaultLocale"),
|
||||
|
||||
// Fallback locale if a translation was not found.
|
||||
// If not set, will use the text that is already
|
||||
// in the code base.
|
||||
fallbackLocale: "en-US",
|
||||
fallbackLocale: config.get("defaultLocale"),
|
||||
|
||||
// Common fluent files are always included in the locale bundles.
|
||||
commonFiles: ["framework.ftl", "common.ftl", "ui.ftl"],
|
||||
|
||||
// Locales that come with the main bundle. Others are loaded on demand.
|
||||
bundled: ["en-US"],
|
||||
bundled: [config.get("defaultLocale")],
|
||||
|
||||
// All available locales can be loadable on demand.
|
||||
// To restrict available locales set:
|
||||
// availableLocales: ["en-US"],
|
||||
// availableLocales: [config.get("defaultLocale")],
|
||||
};
|
||||
|
||||
const additionalPlugins = [
|
||||
@@ -223,10 +223,10 @@ export default function createWebpackConfig(
|
||||
// Generated JS file names (with nested folders).
|
||||
// There will be one main bundle, and one file per asynchronous chunk.
|
||||
filename: isProduction
|
||||
? "assets/js/[name].[chunkhash:8].js"
|
||||
? "assets/js/[name].[chunkhash].js"
|
||||
: "assets/js/[name].js",
|
||||
chunkFilename: isProduction
|
||||
? "assets/js/[name].[chunkhash:8].chunk.js"
|
||||
? "assets/js/[name].[chunkhash].chunk.js"
|
||||
: "assets/js/[name].chunk.js",
|
||||
// We inferred the "public path" (such as / or /my-project) from homepage.
|
||||
publicPath,
|
||||
@@ -357,7 +357,7 @@ export default function createWebpackConfig(
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: isProduction
|
||||
? "assets/media/[name].[hash:8].[ext]"
|
||||
? "assets/media/[name].[hash].[ext]"
|
||||
: "assets/media/[name].[ext]",
|
||||
},
|
||||
},
|
||||
@@ -370,7 +370,7 @@ export default function createWebpackConfig(
|
||||
options: {
|
||||
modules: true,
|
||||
importLoaders: 2,
|
||||
localIdentName: "[name]-[local]-[hash:base64:5]",
|
||||
localIdentName: "[name]-[local]-[contenthash]",
|
||||
sourceMap: !disableSourcemaps,
|
||||
},
|
||||
},
|
||||
@@ -530,9 +530,9 @@ export default function createWebpackConfig(
|
||||
// TODO: (cvle) this should work in build too but for some reasons it terminates the build afterwards.
|
||||
// Preventing from running post build steps.
|
||||
...ifWatch(
|
||||
// We run tslint in a separate process to have a quicker build.
|
||||
// We run eslint in a separate process to have a quicker build.
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
tslint: true,
|
||||
eslint: true,
|
||||
typescript: require.resolve("typescript"),
|
||||
async: true,
|
||||
// TODO: (cvle) For some reason if incremental build is turned on it does not find lint errors during initial build.
|
||||
@@ -752,5 +752,44 @@ export default function createWebpackConfig(
|
||||
),
|
||||
]),
|
||||
},
|
||||
/* Webpack config for count */
|
||||
{
|
||||
...baseConfig,
|
||||
optimization: {
|
||||
...baseConfig.optimization,
|
||||
// Ensure that we never split the count into chunks.
|
||||
splitChunks: {
|
||||
chunks: "async",
|
||||
},
|
||||
// We can turn on sideEffects here as we don't use
|
||||
// css here and don't run into: https://github.com/webpack/webpack/issues/7094
|
||||
sideEffects: true,
|
||||
},
|
||||
entry: [paths.appCountIndex],
|
||||
output: {
|
||||
...baseConfig.output,
|
||||
// don't hash the count, cache-busting must be completed by the requester
|
||||
// as this lives in a static template on the embed site.
|
||||
filename: "assets/js/count.js",
|
||||
},
|
||||
plugins: filterPlugins([
|
||||
...baseConfig.plugins!,
|
||||
...ifWatch(
|
||||
// Generates an `embed.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "count.html",
|
||||
template: paths.appCountHTML,
|
||||
inject: "body",
|
||||
})
|
||||
),
|
||||
...ifBuild(
|
||||
new WebpackAssetsManifest({
|
||||
output: "count-asset-manifest.json",
|
||||
entrypoints: true,
|
||||
integrity: true,
|
||||
})
|
||||
),
|
||||
]),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ function generateTarget(target, context) {
|
||||
defaultLocale,
|
||||
fallbackLocale,
|
||||
pathToLocales,
|
||||
resourcePath,
|
||||
locales,
|
||||
bundled,
|
||||
} = context;
|
||||
|
||||
@@ -34,6 +34,9 @@ export default {
|
||||
appAuthCallbackHTML: resolveSrc("core/client/auth-callback/index.html"),
|
||||
appAuthCallbackIndex: resolveSrc("core/client/auth-callback/index.ts"),
|
||||
|
||||
appCountHTML: resolveSrc("core/client/count/index.html"),
|
||||
appCountIndex: resolveSrc("core/client/count/index.ts"),
|
||||
|
||||
appInstallHTML: resolveSrc("core/client/install/index.html"),
|
||||
appInstallLocalesTemplate: resolveSrc("core/client/install/locales.ts"),
|
||||
appInstallIndex: resolveSrc("core/client/install/index.tsx"),
|
||||
|
||||
@@ -23,12 +23,6 @@ const flatKebabVariables = mapKeys(
|
||||
(_, k) => kebabCase(k)
|
||||
);
|
||||
|
||||
// These are the default css standard variables.
|
||||
const cssVariables = pickBy(
|
||||
flatKebabVariables,
|
||||
(v, k) => !k.startsWith("breakpoints-")
|
||||
);
|
||||
|
||||
// These are sass style variables used in media queries.
|
||||
const mediaQueryVariables = mapValues(
|
||||
pickBy(flatKebabVariables, (v, k) => k.startsWith("breakpoints-")),
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
__webpack_public_path__ =
|
||||
JSON.parse(document.getElementById("config").innerText) || "/";
|
||||
JSON.parse(document.getElementById("config").innerText).staticURI || "/";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import TransitionControl from "coral-framework/testHelpers/TransitionControl";
|
||||
import { BrowserProtocol, queryMiddleware } from "farce";
|
||||
import { createFarceRouter, ElementsRenderer } from "found";
|
||||
import { Resolver } from "found-relay";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { CoralContextConsumer } from "coral-framework/lib/bootstrap/CoralContext";
|
||||
import TransitionControl from "coral-framework/testHelpers/TransitionControl";
|
||||
|
||||
import routeConfig from "../routeConfig";
|
||||
import NotFound from "../routes/NotFound";
|
||||
@@ -16,16 +16,17 @@ const Router = createFarceRouter({
|
||||
historyProtocol: new BrowserProtocol(),
|
||||
historyMiddlewares: [queryMiddleware],
|
||||
routeConfig,
|
||||
renderReady: ({ elements }) => (
|
||||
<>
|
||||
<ElementsRenderer elements={elements} />
|
||||
{// this enables router transition control when writing tests.
|
||||
process.env.NODE_ENV === "test" && <TransitionControl />}
|
||||
</>
|
||||
),
|
||||
renderError: ({ error }) => (
|
||||
<div>{error.status === 404 ? <NotFound /> : "Error"}</div>
|
||||
),
|
||||
renderReady: function FarceRouterReady({ elements }) {
|
||||
return (
|
||||
<>
|
||||
<ElementsRenderer elements={elements} />
|
||||
{process.env.NODE_ENV === "test" && <TransitionControl />}
|
||||
</>
|
||||
);
|
||||
},
|
||||
renderError: function FarceRouterError({ error }) {
|
||||
return <div>{error.status === 404 ? <NotFound /> : "Error"}</div>;
|
||||
},
|
||||
});
|
||||
|
||||
const EntryContainer: FunctionComponent = () => (
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createManaged } from "coral-framework/lib/bootstrap";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import { createManaged } from "coral-framework/lib/bootstrap";
|
||||
|
||||
import App from "./App";
|
||||
import { initLocalState } from "./local";
|
||||
import localesData from "./locales";
|
||||
@@ -13,7 +14,6 @@ async function main() {
|
||||
const ManagedCoralContextProvider = await createManaged({
|
||||
initLocalState,
|
||||
localesData,
|
||||
userLocales: navigator.languages,
|
||||
});
|
||||
|
||||
const Index: FunctionComponent = () => (
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
*/
|
||||
|
||||
import { LocalesData } from "coral-framework/lib/i18n";
|
||||
|
||||
export default {} as LocalesData;
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from "react";
|
||||
|
||||
import DownloadRoute from "./routes/download/Download";
|
||||
import ConfirmRoute from "./routes/email/Confirm";
|
||||
import UnsubscribeRoute from "./routes/notifications/Unsubscribe";
|
||||
import ResetRoute from "./routes/password/Reset";
|
||||
|
||||
export default makeRouteConfig(
|
||||
@@ -14,5 +15,8 @@ export default makeRouteConfig(
|
||||
<Route path="confirm" {...ConfirmRoute.routeConfig} />
|
||||
</Route>
|
||||
<Route path="download" {...DownloadRoute.routeConfig} />
|
||||
<Route path="notifications">
|
||||
<Route path="unsubscribe" {...UnsubscribeRoute.routeConfig} />
|
||||
</Route>
|
||||
</Route>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HorizontalGutter, Typography } from "coral-ui/components";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { HorizontalGutter, Typography } from "coral-ui/components";
|
||||
|
||||
const NotFound: FunctionComponent = () => (
|
||||
<HorizontalGutter>
|
||||
<Typography variant="heading3">Not Found</Typography>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
|
||||
import { Button, HorizontalGutter } from "coral-ui/components";
|
||||
|
||||
@@ -10,6 +10,11 @@ interface Props {
|
||||
}
|
||||
|
||||
const DownloadForm: FunctionComponent<Props> = ({ token }) => {
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const onClick = useCallback(() => {
|
||||
setSubmitted(true);
|
||||
}, [setSubmitted]);
|
||||
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<form
|
||||
@@ -23,7 +28,8 @@ const DownloadForm: FunctionComponent<Props> = ({ token }) => {
|
||||
type="submit"
|
||||
variant="filled"
|
||||
color="primary"
|
||||
fullWidth={false}
|
||||
disabled={submitted}
|
||||
onClick={onClick}
|
||||
className={styles.downloadButton}
|
||||
>
|
||||
Download My Comment History
|
||||
|
||||
@@ -17,7 +17,7 @@ import styles from "./DownloadRoute.css";
|
||||
const fetcher = createFetch(
|
||||
"downloadToken",
|
||||
async (environment: Environment, variables: { token: string }, { rest }) =>
|
||||
await rest.fetch<void>("/account/downloadcheck", {
|
||||
await rest.fetch<void>("/account/download", {
|
||||
method: "GET",
|
||||
token: variables.token,
|
||||
})
|
||||
@@ -65,9 +65,11 @@ const DownloadRoute: FunctionComponent<Props> = ({ token }) => {
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
render: ({ match, Component }) => (
|
||||
<Component token={parseHashQuery(match.location.hash).downloadToken} />
|
||||
),
|
||||
render: function DownloadRouteRender({ match, Component }) {
|
||||
return (
|
||||
<Component token={parseHashQuery(match.location.hash).downloadToken} />
|
||||
);
|
||||
},
|
||||
})(DownloadRoute);
|
||||
|
||||
export default enhanced;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { CallOut, Flex, Icon } from "coral-ui/components";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
|
||||
import styles from "./Sorry.css";
|
||||
|
||||
|
||||
@@ -69,9 +69,11 @@ const ConfirmRoute: React.FunctionComponent<Props> = ({ token }) => {
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
render: ({ match, Component }) => (
|
||||
<Component token={parseHashQuery(match.location.hash).confirmToken} />
|
||||
),
|
||||
render: function ConfirmRouteRender({ match, Component }) {
|
||||
return (
|
||||
<Component token={parseHashQuery(match.location.hash).confirmToken} />
|
||||
);
|
||||
},
|
||||
})(ConfirmRoute);
|
||||
|
||||
export default enhanced;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
|
||||
import { CallOut, HorizontalGutter, Typography } from "coral-ui/components";
|
||||
|
||||
interface Props {
|
||||
reason: React.ReactNode;
|
||||
}
|
||||
|
||||
const Sorry: React.FunctionComponent<Props> = ({ reason }) => {
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="unsubscribe-oopsSorry">
|
||||
<Typography variant="heading1">Oops Sorry!</Typography>
|
||||
</Localized>
|
||||
<CallOut color="error" fullWidth>
|
||||
{reason ? (
|
||||
reason
|
||||
) : (
|
||||
<Localized id="account-tokenNotFound">
|
||||
<span data-testid="invalid-link">
|
||||
The specified link is invalid, check to see if it was copied
|
||||
correctly.
|
||||
</span>
|
||||
</Localized>
|
||||
)}
|
||||
</CallOut>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sorry;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
|
||||
import { HorizontalGutter, Typography } from "coral-ui/components";
|
||||
|
||||
const Success: React.FunctionComponent = () => {
|
||||
return (
|
||||
<HorizontalGutter data-testid="success" size="double">
|
||||
<Localized id="unsubscribe-successfullyUnsubscribed">
|
||||
<Typography variant="heading1">
|
||||
You are now unsubscribed from all notifications
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Success;
|
||||
@@ -0,0 +1,76 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { useCallback } from "react";
|
||||
import { Form } from "react-final-form";
|
||||
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { useMutation } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
HorizontalGutter,
|
||||
Typography,
|
||||
} from "coral-ui/components";
|
||||
|
||||
import UnsubscribeNotificationsMutation from "./UnsubscribeNotificationsMutation";
|
||||
|
||||
interface Props {
|
||||
token: string;
|
||||
disabled?: boolean;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const UnsubscribeForm: React.FunctionComponent<Props> = ({
|
||||
onSuccess,
|
||||
token,
|
||||
}) => {
|
||||
const unsubscribe = useMutation(UnsubscribeNotificationsMutation);
|
||||
const onSubmit = useCallback(async () => {
|
||||
try {
|
||||
await unsubscribe({ token });
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidRequestError) {
|
||||
return error.invalidArgs;
|
||||
}
|
||||
return { [FORM_ERROR]: error.message };
|
||||
}
|
||||
return;
|
||||
}, [token]);
|
||||
return (
|
||||
<div data-testid="unsubscribe-form">
|
||||
<Form onSubmit={onSubmit}>
|
||||
{({ handleSubmit, submitting, submitError }) => (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<HorizontalGutter>
|
||||
<Localized id="unsubscribe-clickToConfirm">
|
||||
<Typography variant="heading1">
|
||||
Click below to confirm that you want to unsubscribe from all
|
||||
notifications.
|
||||
</Typography>
|
||||
</Localized>
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
<Localized id="unsubscribe-confirm">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="filled"
|
||||
color="primary"
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnsubscribeForm;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { createMutation } from "coral-framework/lib/relay";
|
||||
|
||||
const UnsubscribeNotificationsMutation = createMutation(
|
||||
"unsubscribeNotifications",
|
||||
async (environment: Environment, variables: { token: string }, { rest }) =>
|
||||
await rest.fetch<void>("/account/notifications/unsubscribe", {
|
||||
method: "DELETE",
|
||||
token: variables.token,
|
||||
})
|
||||
);
|
||||
|
||||
export default UnsubscribeNotificationsMutation;
|
||||
@@ -0,0 +1,16 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.root {
|
||||
display: inline-block;
|
||||
|
||||
max-width: calc(70 * var(--mini-unit));
|
||||
|
||||
@media (min-width: $breakpoints-xs) {
|
||||
max-width: calc(42 * var(--mini-unit));
|
||||
}
|
||||
|
||||
text-align: left;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import Loading from "coral-account/components/Loading";
|
||||
import { useToken } from "coral-framework/hooks";
|
||||
import { createFetch } from "coral-framework/lib/relay";
|
||||
import { withRouteConfig } from "coral-framework/lib/router";
|
||||
import { parseHashQuery } from "coral-framework/utils";
|
||||
|
||||
import Sorry from "./Sorry";
|
||||
import Success from "./Success";
|
||||
import UnsubscribeForm from "./UnsubscribeForm";
|
||||
|
||||
import styles from "./UnsubscribeRoute.css";
|
||||
|
||||
const fetcher = createFetch(
|
||||
"unsubscribeToken",
|
||||
async (environment: Environment, variables: { token: string }, { rest }) =>
|
||||
await rest.fetch<void>("/account/notifications/unsubscribe", {
|
||||
method: "GET",
|
||||
token: variables.token,
|
||||
})
|
||||
);
|
||||
|
||||
interface Props {
|
||||
token: string | undefined;
|
||||
}
|
||||
|
||||
const UnsubscribeRoute: React.FunctionComponent<Props> = ({ token }) => {
|
||||
const [finished, setFinished] = useState(false);
|
||||
const onSuccess = useCallback(() => {
|
||||
setFinished(true);
|
||||
}, []);
|
||||
const [state, error] = useToken(fetcher, token);
|
||||
|
||||
if (state === "UNCHECKED") {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<Loading />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state !== "VALID" || error) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<Sorry reason={error} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return !finished ? (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<UnsubscribeForm token={token!} onSuccess={onSuccess} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.root}>
|
||||
<Success />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
render: function UnsubscribeRouteRender({ match, Component }) {
|
||||
return (
|
||||
<Component token={parseHashQuery(match.location.hash).unsubscribeToken} />
|
||||
);
|
||||
},
|
||||
})(UnsubscribeRoute);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1 @@
|
||||
export { default, default as UnsubscribeRoute } from "./UnsubscribeRoute";
|
||||
@@ -69,9 +69,9 @@ const ResetRoute: React.FunctionComponent<Props> = ({ token }) => {
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
render: ({ match, Component }) => (
|
||||
<Component token={parseHashQuery(match.location.hash).resetToken} />
|
||||
),
|
||||
render: function ResetRouteRender({ match, Component }) {
|
||||
return <Component token={parseHashQuery(match.location.hash).resetToken} />;
|
||||
},
|
||||
})(ResetRoute);
|
||||
|
||||
export default enhanced;
|
||||
|
||||
@@ -42,6 +42,8 @@ exports[`renders form 1`] = `
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
data-color="primary"
|
||||
data-variant="filled"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
|
||||
@@ -52,7 +52,7 @@ Make sure it is unique and be sure to keep it secure.
|
||||
Password
|
||||
</label>
|
||||
<p
|
||||
className="Box-root Typography-root Typography-detail Typography-colorTextSecondary"
|
||||
className="Box-root Typography-root Typography-fieldDescription Typography-colorTextSecondary"
|
||||
>
|
||||
Must be at least 8 characters
|
||||
</p>
|
||||
@@ -81,22 +81,25 @@ Make sure it is unique and be sure to keep it secure.
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Hide password"
|
||||
>
|
||||
<span
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
data-color="primary"
|
||||
data-variant="filled"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders form 1`] = `
|
||||
<div
|
||||
data-testid="main-layout"
|
||||
>
|
||||
<div
|
||||
className="MainLayout-bar"
|
||||
/>
|
||||
<div
|
||||
className="MainLayout-centered"
|
||||
>
|
||||
<div
|
||||
className="UnsubscribeRoute-container"
|
||||
>
|
||||
<div
|
||||
className="UnsubscribeRoute-root"
|
||||
>
|
||||
<div
|
||||
data-testid="unsubscribe-form"
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<h1
|
||||
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
|
||||
>
|
||||
Click below to confirm that you want to unsubscribe from all notifications.
|
||||
</h1>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
data-color="primary"
|
||||
data-variant="filled"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders missing confirm token 1`] = `
|
||||
<div
|
||||
data-testid="main-layout"
|
||||
>
|
||||
<div
|
||||
className="MainLayout-bar"
|
||||
/>
|
||||
<div
|
||||
className="MainLayout-centered"
|
||||
>
|
||||
<div
|
||||
className="UnsubscribeRoute-container"
|
||||
>
|
||||
<div
|
||||
className="UnsubscribeRoute-root"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<h1
|
||||
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
|
||||
>
|
||||
Oops Sorry!
|
||||
</h1>
|
||||
<div
|
||||
className="CallOut-root CallOut-colorError CallOut-fullWidth"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
data-testid="invalid-link"
|
||||
>
|
||||
The specified link is invalid, check to see if it was copied correctly.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,5 +1,7 @@
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ERROR_CODES } from "coral-common/errors";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
act,
|
||||
@@ -10,8 +12,6 @@ import {
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import { ERROR_CODES } from "coral-common/errors";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import create from "./create";
|
||||
|
||||
const token = createAccessToken();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ERROR_CODES } from "coral-common/errors";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
act,
|
||||
@@ -10,8 +12,6 @@ import {
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import { ERROR_CODES } from "coral-common/errors";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import create from "./create";
|
||||
|
||||
const token = createAccessToken();
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ERROR_CODES } from "coral-common/errors";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
act,
|
||||
createAccessToken,
|
||||
CreateTestRendererParams,
|
||||
replaceHistoryLocation,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import create from "./create";
|
||||
|
||||
const token = createAccessToken();
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context } = create();
|
||||
return {
|
||||
context,
|
||||
testRenderer,
|
||||
root: testRenderer.root,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders missing confirm token", async () => {
|
||||
replaceHistoryLocation("http://localhost/account/notifications/unsubscribe");
|
||||
const { root } = await createTestRenderer();
|
||||
await waitForElement(() => within(root).getByTestID("invalid-link"));
|
||||
expect(within(root).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders form", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/notifications/unsubscribe#unsubscribeToken=${token}`
|
||||
);
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/account/notifications/unsubscribe", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once();
|
||||
|
||||
await act(async () => {
|
||||
await waitForElement(() => within(root).getByTestID("unsubscribe-form"));
|
||||
});
|
||||
expect(within(root).toJSON()).toMatchSnapshot();
|
||||
restMock.verify();
|
||||
});
|
||||
|
||||
it("renders error from server", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/notifications/unsubscribe#unsubscribeToken=${token}`
|
||||
);
|
||||
|
||||
const codes = [
|
||||
ERROR_CODES.RATE_LIMIT_EXCEEDED,
|
||||
ERROR_CODES.INTEGRATION_DISABLED,
|
||||
ERROR_CODES.USER_NOT_FOUND,
|
||||
ERROR_CODES.TOKEN_INVALID,
|
||||
];
|
||||
|
||||
for (const code of codes) {
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/account/notifications/unsubscribe", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once()
|
||||
.throwsException(
|
||||
new InvalidRequestError({
|
||||
code,
|
||||
})
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await waitForElement(() =>
|
||||
within(root).getByText(code, {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
restMock.verify();
|
||||
}
|
||||
});
|
||||
|
||||
it("submits form", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/notifications/unsubscribe#unsubscribeToken=${token}`
|
||||
);
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/account/notifications/unsubscribe", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once();
|
||||
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/account/notifications/unsubscribe", {
|
||||
method: "DELETE",
|
||||
token,
|
||||
})
|
||||
.once();
|
||||
|
||||
await act(async () => {
|
||||
await waitForElement(() => within(root).getByTestID("unsubscribe-form"));
|
||||
});
|
||||
const form = within(root).getByType("form");
|
||||
|
||||
// Submit valid form.
|
||||
await act(async () => {
|
||||
form.props.onSubmit();
|
||||
await waitForElement(() => within(root).getByTestID("success"));
|
||||
});
|
||||
|
||||
restMock.verify();
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import TransitionControl from "coral-framework/testHelpers/TransitionControl";
|
||||
import { BrowserProtocol, queryMiddleware } from "farce";
|
||||
import { createFarceRouter, ElementsRenderer } from "found";
|
||||
import { Resolver } from "found-relay";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { CoralContextConsumer } from "coral-framework/lib/bootstrap/CoralContext";
|
||||
import TransitionControl from "coral-framework/testHelpers/TransitionControl";
|
||||
|
||||
import routeConfig from "../routeConfig";
|
||||
import NotFound from "../routes/NotFound";
|
||||
@@ -13,16 +13,17 @@ const Router = createFarceRouter({
|
||||
historyProtocol: new BrowserProtocol(),
|
||||
historyMiddlewares: [queryMiddleware],
|
||||
routeConfig,
|
||||
renderReady: ({ elements }) => (
|
||||
<>
|
||||
<ElementsRenderer elements={elements} />
|
||||
{// this enables router transition control when writing tests.
|
||||
process.env.NODE_ENV === "test" && <TransitionControl />}
|
||||
</>
|
||||
),
|
||||
renderError: ({ error }) => (
|
||||
<div>{error.status === 404 ? <NotFound /> : "Error"}</div>
|
||||
),
|
||||
renderReady: function FarceRouterReady({ elements }) {
|
||||
return (
|
||||
<>
|
||||
<ElementsRenderer elements={elements} />
|
||||
{process.env.NODE_ENV === "test" && <TransitionControl />}
|
||||
</>
|
||||
);
|
||||
},
|
||||
renderError: function FarceRouterError({ error }) {
|
||||
return <div>{error.status === 404 ? <NotFound /> : "Error"}</div>;
|
||||
},
|
||||
});
|
||||
|
||||
const App: FunctionComponent = () => (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Typography } from "coral-ui/components";
|
||||
|
||||
import ApprovedIcon from "./ApprovedIcon";
|
||||
import DecisionItem from "./DecisionItem";
|
||||
import DotDivider from "./DotDivider";
|
||||
@@ -9,8 +11,6 @@ import GoToCommentLink from "./GoToCommentLink";
|
||||
import Info from "./Info";
|
||||
import Timestamp from "./Timestamp";
|
||||
|
||||
import { Typography } from "coral-ui/components";
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
username: string;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import { withPaginationContainer } from "coral-framework/lib/relay";
|
||||
|
||||
import { DecisionHistoryContainer_viewer as ViewerData } from "coral-admin/__generated__/DecisionHistoryContainer_viewer.graphql";
|
||||
import { DecisionHistoryContainerPaginationQueryVariables } from "coral-admin/__generated__/DecisionHistoryContainerPaginationQuery.graphql";
|
||||
import { withPaginationContainer } from "coral-framework/lib/relay";
|
||||
|
||||
import DecisionHistory from "./DecisionHistory";
|
||||
|
||||
@@ -45,7 +46,7 @@ export class DecisionHistoryContainer extends React.Component<
|
||||
error => {
|
||||
this.setState({ disableLoadMore: false });
|
||||
if (error) {
|
||||
// tslint:disable-next-line:no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { DecisionHistoryItemContainer_action as ActionData } from "coral-admin/__generated__/DecisionHistoryItemContainer_action.graphql";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
|
||||
import { DecisionHistoryItemContainer_action as ActionData } from "coral-admin/__generated__/DecisionHistoryItemContainer_action.graphql";
|
||||
|
||||
import ApprovedComment from "./ApprovedComment";
|
||||
import RejectedComment from "./RejectedComment";
|
||||
|
||||
@@ -16,9 +17,7 @@ class DecisionHistoryItemContainer extends React.Component<
|
||||
DecisionHistoryItemContainerProps
|
||||
> {
|
||||
public render() {
|
||||
const href = `/admin/moderate/comment/${
|
||||
this.props.action.revision.comment.id
|
||||
}`;
|
||||
const href = `/admin/moderate/comment/${this.props.action.revision.comment.id}`;
|
||||
const username =
|
||||
(this.props.action.revision.comment.author &&
|
||||
this.props.action.revision.comment.author.username) ||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { graphql, QueryRenderer } from "coral-framework/lib/relay";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { graphql, QueryRenderer } from "coral-framework/lib/relay";
|
||||
|
||||
import { DecisionHistoryQuery as QueryTypes } from "coral-admin/__generated__/DecisionHistoryQuery.graphql";
|
||||
|
||||
import DecisionHistoryContainer from "./DecisionHistoryContainer";
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Typography } from "coral-ui/components";
|
||||
|
||||
import DecisionItem from "./DecisionItem";
|
||||
import DotDivider from "./DotDivider";
|
||||
import Footer from "./Footer";
|
||||
@@ -9,8 +11,6 @@ import Info from "./Info";
|
||||
import RejectedIcon from "./RejectedIcon";
|
||||
import Timestamp from "./Timestamp";
|
||||
|
||||
import { Typography } from "coral-ui/components";
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
username: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { removeFragmentRefs } from "coral-framework/testHelpers";
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
|
||||
import { removeFragmentRefs } from "coral-framework/testHelpers";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
|
||||
import Main from "./Main";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import { Logo } from "coral-ui/components";
|
||||
import { LogoHorizontal } from "coral-ui/components";
|
||||
import { AppBar, Begin, Divider, End } from "coral-ui/components/AppBar";
|
||||
|
||||
import { DecisionHistoryButton } from "./DecisionHistory";
|
||||
@@ -22,8 +22,7 @@ const Main: FunctionComponent<Props> = ({ children, viewer }) => (
|
||||
<AppBar gutterBegin gutterEnd>
|
||||
<Begin itemGutter="double">
|
||||
<div className={styles.logoContainer}>
|
||||
<Logo />
|
||||
<Version />
|
||||
<LogoHorizontal />
|
||||
</div>
|
||||
<NavigationContainer viewer={viewer} />
|
||||
</Begin>
|
||||
@@ -34,6 +33,7 @@ const Main: FunctionComponent<Props> = ({ children, viewer }) => (
|
||||
</End>
|
||||
</AppBar>
|
||||
{children}
|
||||
<Version />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
|
||||
import { MainRouteQueryResponse } from "coral-admin/__generated__/MainRouteQuery.graphql";
|
||||
|
||||
import { graphql } from "coral-framework/lib/relay";
|
||||
import { withRouteConfig } from "coral-framework/lib/router";
|
||||
|
||||
import { MainRouteQueryResponse } from "coral-admin/__generated__/MainRouteQuery.graphql";
|
||||
|
||||
import Main from "./Main";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
import { NavigationContainer_viewer as ViewerData } from "coral-admin/__generated__/NavigationContainer_viewer.graphql";
|
||||
import { Ability, can } from "coral-admin/permissions";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
@@ -8,6 +7,8 @@ import {
|
||||
withSignOutMutation,
|
||||
} from "coral-framework/mutations";
|
||||
|
||||
import { NavigationContainer_viewer as ViewerData } from "coral-admin/__generated__/NavigationContainer_viewer.graphql";
|
||||
|
||||
import Navigation from "./Navigation";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
import { UserMenuContainer_viewer as ViewerData } from "coral-admin/__generated__/UserMenuContainer_viewer.graphql";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
SignOutMutation,
|
||||
withSignOutMutation,
|
||||
} from "coral-framework/mutations";
|
||||
|
||||
import { UserMenuContainer_viewer as ViewerData } from "coral-admin/__generated__/UserMenuContainer_viewer.graphql";
|
||||
|
||||
import UserMenu from "./UserMenu";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.version {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 23px;
|
||||
font-size: calc(10rem / var(--rem-base));
|
||||
color: var(--palette-text-secondary);
|
||||
text-align: center;
|
||||
padding-bottom: var(--spacing-3);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@ exports[`renders correctly 1`] = `
|
||||
<div
|
||||
className="Main-logoContainer"
|
||||
>
|
||||
<withPropsOnChange(Logo) />
|
||||
<Version />
|
||||
<withPropsOnChange(LogoHorizontal) />
|
||||
</div>
|
||||
<withContext(createMutationContainer(Relay(NavigationContainer)))
|
||||
viewer={Object {}}
|
||||
@@ -30,5 +29,6 @@ exports[`renders correctly 1`] = `
|
||||
</withPropsOnChange(End)>
|
||||
</withPropsOnChange(AppBar)>
|
||||
child
|
||||
<Version />
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
import favicon120 from "./assets/favicon120.png";
|
||||
import favicon128 from "./assets/favicon128.png";
|
||||
import favicon152 from "./assets/favicon152.png";
|
||||
import favicon180 from "./assets/favicon180.png";
|
||||
import favicon196 from "./assets/favicon196.png";
|
||||
import favicon228 from "./assets/favicon228.png";
|
||||
import favicon32 from "./assets/favicon32.png";
|
||||
import favicon57 from "./assets/favicon57.png";
|
||||
import favicon76 from "./assets/favicon76.png";
|
||||
import favicon96 from "./assets/favicon96.png";
|
||||
|
||||
const Head: FunctionComponent = () => (
|
||||
<Helmet>
|
||||
<link rel="icon" href={favicon32} sizes="32x32" />
|
||||
<link rel="icon" href={favicon57} sizes="57x57" />
|
||||
<link rel="icon" href={favicon76} sizes="76x76" />
|
||||
<link rel="icon" href={favicon96} sizes="96x96" />
|
||||
<link rel="icon" href={favicon128} sizes="128x128" />
|
||||
<link rel="icon" href={favicon228} sizes="228x228" />
|
||||
<link rel="shortcut icon" sizes="196x196" href={favicon196} />
|
||||
<link rel="apple-touch-icon" href={favicon120} sizes="120x120" />
|
||||
<link rel="apple-touch-icon" href={favicon152} sizes="152x152" />
|
||||
<link rel="apple-touch-icon" href={favicon180} sizes="180x180" />
|
||||
</Helmet>
|
||||
);
|
||||
|
||||
export default Head;
|
||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -1,7 +1,7 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import {
|
||||
BrandIcon,
|
||||
BrandMark,
|
||||
BrandName,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
@@ -22,7 +22,7 @@ const AuthBox: FunctionComponent<Props> = ({ title, children }) => {
|
||||
<HorizontalGutter className={styles.container} size="double">
|
||||
<Flex justifyContent="center">
|
||||
<div className={styles.brandIcon}>
|
||||
<BrandIcon size="lg" />
|
||||
<BrandMark size="lg" />
|
||||
</div>
|
||||
</Flex>
|
||||
<div>
|
||||
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
}
|
||||
|
||||
class AutoLoadMoresContainer extends React.Component<Props> {
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
public UNSAFE_componentWillReceiveProps(nextProps: Props) {
|
||||
if (nextProps.inView && !nextProps.disableLoadMore) {
|
||||
nextProps.onLoadMore();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
|
||||
import ApproveButton from "./ApproveButton";
|
||||
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
|
||||
import ApproveButton from "./ApproveButton";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof ApproveButton> = {
|
||||
invert: false,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { BanCommentUserMutation as MutationTypes } from "coral-admin/__generated__/BanCommentUserMutation.graphql";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
const clientMutationId = 0;
|
||||
|
||||
const BanCommentUserMutation = createMutation(
|
||||
"banUser",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) => {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation BanCommentUserMutation($input: BanUserInput!) {
|
||||
banUser(input: $input) {
|
||||
user {
|
||||
id
|
||||
status {
|
||||
current
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
updater: store => {
|
||||
const user = store.get(input.userID);
|
||||
if (user) {
|
||||
const comments = user.getLinkedRecords("comments");
|
||||
if (comments) {
|
||||
comments.forEach(comment => {
|
||||
if (comment) {
|
||||
comment.setLinkedRecord(user, "author");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default BanCommentUserMutation;
|
||||
@@ -0,0 +1,3 @@
|
||||
.authorStatus {
|
||||
padding-right: var(--spacing-2);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { CommentAuthorContainer_comment as CommentData } from "coral-admin/__generated__/CommentAuthorContainer_comment.graphql";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { Tag } from "coral-ui/components";
|
||||
|
||||
import styles from "./CommentAuthorContainer.css";
|
||||
|
||||
interface Props {
|
||||
comment: CommentData;
|
||||
}
|
||||
|
||||
const CommentAuthorContainer: FunctionComponent<Props> = ({ comment }) => {
|
||||
if (!comment.author || !comment.author.status.ban.active) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Localized id="commentAuthor-status-banned">
|
||||
<div className={styles.authorStatus}>
|
||||
<Tag color="error">BANNED</Tag>
|
||||
</div>
|
||||
</Localized>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
comment: graphql`
|
||||
fragment CommentAuthorContainer_comment on Comment {
|
||||
author {
|
||||
id
|
||||
username
|
||||
status {
|
||||
ban {
|
||||
active
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(CommentAuthorContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
|
||||
import CommentContent from "./CommentContent";
|
||||
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
|
||||
import CommentContent from "./CommentContent";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof CommentContent> = {
|
||||
suspectWords: ["worse"],
|
||||
|
||||
@@ -14,7 +14,7 @@ const purify = createPurify(window, false);
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
children: string;
|
||||
children: string | React.ReactElement;
|
||||
suspectWords: ReadonlyArray<string>;
|
||||
bannedWords: ReadonlyArray<string>;
|
||||
}
|
||||
@@ -117,25 +117,29 @@ const CommentContent: FunctionComponent<Props> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
// We create a Shadow DOM Tree with the HTML body content and
|
||||
// use it as a parser.
|
||||
const node = document.createElement("div");
|
||||
node.innerHTML = purify.sanitize(children);
|
||||
if (typeof children === "string") {
|
||||
// We create a Shadow DOM Tree with the HTML body content and
|
||||
// use it as a parser.
|
||||
const node = document.createElement("div");
|
||||
node.innerHTML = purify.sanitize(children);
|
||||
|
||||
if (suspectWords.length || bannedWords.length) {
|
||||
// Then we traverse it recursively and manipulate it to highlight suspect words
|
||||
// and banned words.
|
||||
markHTMLNode(node, suspectWords, bannedWords);
|
||||
if (suspectWords.length || bannedWords.length) {
|
||||
// Then we traverse it recursively and manipulate it to highlight suspect words
|
||||
// and banned words.
|
||||
markHTMLNode(node, suspectWords, bannedWords);
|
||||
}
|
||||
|
||||
// Finally we render the content of the Shadow DOM Tree
|
||||
return (
|
||||
<Typography
|
||||
className={cn(className, styles.root)}
|
||||
dangerouslySetInnerHTML={{ __html: node.innerHTML }}
|
||||
container="div"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Finally we render the content of the Shadow DOM Tree
|
||||
return (
|
||||
<Typography
|
||||
className={cn(className, styles.root)}
|
||||
dangerouslySetInnerHTML={{ __html: node.innerHTML }}
|
||||
container="div"
|
||||
/>
|
||||
);
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default CommentContent;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { HorizontalGutter, Timestamp } from "coral-ui/components";
|
||||
|
||||
import { CommentRevisionContainer_comment as CommentData } from "coral-admin/__generated__/CommentRevisionContainer_comment.graphql";
|
||||
import { CommentRevisionContainer_settings as SettingsData } from "coral-admin/__generated__/CommentRevisionContainer_settings.graphql";
|
||||
|
||||
import CommentContent from "./CommentContent";
|
||||
|
||||
interface Props {
|
||||
comment: CommentData;
|
||||
settings: SettingsData;
|
||||
}
|
||||
|
||||
const CommentRevisionContainer: FunctionComponent<Props> = ({
|
||||
settings,
|
||||
comment,
|
||||
}) => {
|
||||
return (
|
||||
<HorizontalGutter>
|
||||
{comment.revisionHistory
|
||||
.concat()
|
||||
.reverse()
|
||||
.filter(c =>
|
||||
comment && comment.revision && comment.revision.id
|
||||
? comment.revision.id !== c.id
|
||||
: true
|
||||
)
|
||||
.map(c => (
|
||||
<div key={c.id}>
|
||||
<Timestamp>{c.createdAt}</Timestamp>
|
||||
<CommentContent
|
||||
suspectWords={settings.wordList.suspect}
|
||||
bannedWords={settings.wordList.banned}
|
||||
>
|
||||
{c.body ? c.body : ""}
|
||||
</CommentContent>
|
||||
</div>
|
||||
))}
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
comment: graphql`
|
||||
fragment CommentRevisionContainer_comment on Comment {
|
||||
revision {
|
||||
id
|
||||
}
|
||||
revisionHistory {
|
||||
id
|
||||
body
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment CommentRevisionContainer_settings on Settings {
|
||||
wordList {
|
||||
banned
|
||||
suspect
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(CommentRevisionContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -9,10 +9,12 @@ import styles from "./FeatureButton.css";
|
||||
|
||||
interface Props extends PropTypesOf<typeof BaseButton> {
|
||||
featured: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
const FeatureButton: FunctionComponent<Props> = ({
|
||||
featured,
|
||||
enabled = true,
|
||||
className,
|
||||
...rest
|
||||
}) => (
|
||||
@@ -21,6 +23,7 @@ const FeatureButton: FunctionComponent<Props> = ({
|
||||
className={cn(className, styles.root, {
|
||||
[styles.invert]: featured,
|
||||
})}
|
||||
disabled={!enabled}
|
||||
>
|
||||
{featured ? (
|
||||
<Localized id="moderate-comment-featuredText">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { ConnectionHandler, Environment } from "relay-runtime";
|
||||
|
||||
import { FeatureCommentMutation } from "coral-admin/__generated__/FeatureCommentMutation.graphql";
|
||||
import { getQueueConnection } from "coral-admin/helpers";
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
@@ -11,6 +10,8 @@ import {
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLCOMMENT_STATUS, GQLTAG } from "coral-framework/schema";
|
||||
|
||||
import { FeatureCommentMutation } from "coral-admin/__generated__/FeatureCommentMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const FeatureCommentMutation = createMutation(
|
||||
|
||||
@@ -2,14 +2,15 @@ import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { FlagDetailsContainer_comment } from "coral-admin/__generated__/FlagDetailsContainer_comment.graphql";
|
||||
import { FlagDetailsContainer_settings } from "coral-admin/__generated__/FlagDetailsContainer_settings.graphql";
|
||||
import NotAvailable from "coral-admin/components/NotAvailable";
|
||||
import { TOXICITY_THRESHOLD_DEFAULT } from "coral-common/constants";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { GQLCOMMENT_FLAG_REASON } from "coral-framework/schema";
|
||||
import { HorizontalGutter } from "coral-ui/components";
|
||||
|
||||
import { FlagDetailsContainer_comment } from "coral-admin/__generated__/FlagDetailsContainer_comment.graphql";
|
||||
import { FlagDetailsContainer_settings } from "coral-admin/__generated__/FlagDetailsContainer_settings.graphql";
|
||||
|
||||
import FlagDetailsCategory from "./FlagDetailsCategory";
|
||||
import FlagDetailsEntry from "./FlagDetailsEntry";
|
||||
import ToxicityLabel from "./ToxicityLabel";
|
||||
@@ -21,13 +22,13 @@ interface Props {
|
||||
}
|
||||
|
||||
const FlagDetailsContainer: FunctionComponent<Props> = ({
|
||||
comment: {
|
||||
revision: { metadata },
|
||||
flags: { nodes },
|
||||
},
|
||||
comment,
|
||||
onUsernameClick,
|
||||
settings,
|
||||
}) => {
|
||||
const metadata = comment.revision ? comment.revision.metadata : null;
|
||||
const nodes = comment.flags.nodes;
|
||||
|
||||
const offensive = nodes.filter(
|
||||
({ reason }) => reason === GQLCOMMENT_FLAG_REASON.COMMENT_REPORTED_OFFENSIVE
|
||||
);
|
||||
@@ -37,7 +38,7 @@ const FlagDetailsContainer: FunctionComponent<Props> = ({
|
||||
|
||||
return (
|
||||
<HorizontalGutter size="oneAndAHalf">
|
||||
{metadata.perspective && (
|
||||
{metadata && metadata.perspective && (
|
||||
<FlagDetailsCategory
|
||||
category={
|
||||
<Localized id="moderate-flagDetails-toxicityScore">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { BaseButton } from "coral-ui/components";
|
||||
|
||||
import styles from "./FlagDetailsEntry.css";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -7,3 +7,7 @@
|
||||
.username {
|
||||
color: var(--palette-grey-main);
|
||||
}
|
||||
|
||||
.usernameButton {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
}
|
||||
|
||||
.detailsDivider {
|
||||
border: 1px solid var(--palette-grey-lightest);
|
||||
border-color: var(--palette-grey-lightest);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ it("renders all markers", () => {
|
||||
const props: PropTypesOf<typeof MarkersContainerN> = {
|
||||
comment: {
|
||||
status: "PREMOD",
|
||||
editing: {
|
||||
edited: false,
|
||||
},
|
||||
revision: {
|
||||
actionCounts: {
|
||||
flag: {
|
||||
@@ -25,6 +28,7 @@ it("renders all markers", () => {
|
||||
COMMENT_DETECTED_SUSPECT_WORD: 1,
|
||||
COMMENT_REPORTED_OFFENSIVE: 2,
|
||||
COMMENT_REPORTED_SPAM: 3,
|
||||
COMMENT_DETECTED_REPEAT_POST: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -53,6 +57,9 @@ it("renders some markers", () => {
|
||||
const props: PropTypesOf<typeof MarkersContainerN> = {
|
||||
comment: {
|
||||
status: "PREMOD",
|
||||
editing: {
|
||||
edited: false,
|
||||
},
|
||||
revision: {
|
||||
actionCounts: {
|
||||
flag: {
|
||||
@@ -65,6 +72,7 @@ it("renders some markers", () => {
|
||||
COMMENT_DETECTED_SUSPECT_WORD: 0,
|
||||
COMMENT_REPORTED_OFFENSIVE: 2,
|
||||
COMMENT_REPORTED_SPAM: 0,
|
||||
COMMENT_DETECTED_REPEAT_POST: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,12 +2,14 @@ import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { MarkersContainer_comment } from "coral-admin/__generated__/MarkersContainer_comment.graphql";
|
||||
import { MarkersContainer_settings } from "coral-admin/__generated__/MarkersContainer_settings.graphql";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { Marker, MarkerCount } from "coral-ui/components";
|
||||
import FlagDetailsContainer from "./FlagDetailsContainer";
|
||||
|
||||
import { MarkersContainer_comment } from "coral-admin/__generated__/MarkersContainer_comment.graphql";
|
||||
import { MarkersContainer_settings } from "coral-admin/__generated__/MarkersContainer_settings.graphql";
|
||||
|
||||
import Markers from "./Markers";
|
||||
import ModerateCardDetailsContainer from "./ModerateCardDetailsContainer";
|
||||
|
||||
interface MarkersContainerProps {
|
||||
comment: MarkersContainer_comment;
|
||||
@@ -16,11 +18,11 @@ interface MarkersContainerProps {
|
||||
}
|
||||
|
||||
function hasDetails(c: MarkersContainer_comment) {
|
||||
return (
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE +
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM >
|
||||
0 || c.revision.metadata.perspective
|
||||
);
|
||||
return c.revision
|
||||
? c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE +
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM >
|
||||
0 || c.revision.metadata.perspective
|
||||
: false;
|
||||
}
|
||||
|
||||
let keyCounter = 0;
|
||||
@@ -30,77 +32,93 @@ const markers: Array<
|
||||
c =>
|
||||
(c.status === "PREMOD" && (
|
||||
<Localized id="moderate-marker-preMod" key={keyCounter++}>
|
||||
<Marker color="primary">Pre-Mod</Marker>
|
||||
<Marker color="primary">Pre-mod</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_LINKS && (
|
||||
<Localized id="moderate-marker-link" key={keyCounter++}>
|
||||
<Marker color="primary">Link</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_LINKS && (
|
||||
<Localized id="moderate-marker-link" key={keyCounter++}>
|
||||
<Marker color="primary">Link</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_BANNED_WORD && (
|
||||
<Localized id="moderate-marker-bannedWord" key={keyCounter++}>
|
||||
<Marker color="error">Banned Word</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_BANNED_WORD && (
|
||||
<Localized id="moderate-marker-bannedWord" key={keyCounter++}>
|
||||
<Marker color="error">Banned word</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SUSPECT_WORD && (
|
||||
<Localized id="moderate-marker-suspectWord" key={keyCounter++}>
|
||||
<Marker color="error" variant="filled">
|
||||
Suspect Word
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SUSPECT_WORD && (
|
||||
<Localized id="moderate-marker-suspectWord" key={keyCounter++}>
|
||||
<Marker color="error" variant="filled">
|
||||
Suspect word
|
||||
</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SPAM && (
|
||||
<Localized id="moderate-marker-spamDetected" key={keyCounter++}>
|
||||
<Marker color="error">Spam detected</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_TOXIC && (
|
||||
<Localized id="moderate-marker-toxic" key={keyCounter++}>
|
||||
<Marker color="error">Toxic</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_REPEAT_POST && (
|
||||
<Localized id="moderate-marker-repeatPost" key={keyCounter++}>
|
||||
<Marker color="error">Repeat comment</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_RECENT_HISTORY && (
|
||||
<Localized id="moderate-marker-recentHistory" key={keyCounter++}>
|
||||
<Marker color="error">Recent history</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE && (
|
||||
<Marker key={keyCounter++} color="error">
|
||||
<Localized id="moderate-marker-offensive">
|
||||
<span>Offensive</span>
|
||||
</Localized>{" "}
|
||||
<MarkerCount>
|
||||
{c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE}
|
||||
</MarkerCount>
|
||||
</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SPAM && (
|
||||
<Localized id="moderate-marker-spam" key={keyCounter++}>
|
||||
<Marker color="error">Spam</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_TOXIC && (
|
||||
<Localized id="moderate-marker-toxic" key={keyCounter++}>
|
||||
<Marker color="error">Toxic</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_RECENT_HISTORY && (
|
||||
<Localized id="moderate-marker-recentHistory" key={keyCounter++}>
|
||||
<Marker color="error">Recent History</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE && (
|
||||
<Marker key={keyCounter++} color="error">
|
||||
<Localized id="moderate-marker-offensive">
|
||||
<span>Offensive</span>
|
||||
</Localized>{" "}
|
||||
<MarkerCount>
|
||||
{c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_OFFENSIVE}
|
||||
</MarkerCount>
|
||||
</Marker>
|
||||
)) ||
|
||||
null,
|
||||
c =>
|
||||
(c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM && (
|
||||
<Marker key={keyCounter++} color="error">
|
||||
<Localized id="moderate-marker-spam">
|
||||
<span>Spam</span>
|
||||
</Localized>{" "}
|
||||
<MarkerCount>
|
||||
{c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM}
|
||||
</MarkerCount>
|
||||
</Marker>
|
||||
)) ||
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM && (
|
||||
<Marker key={keyCounter++} color="error">
|
||||
<Localized id="moderate-marker-spam">
|
||||
<span>Spam</span>
|
||||
</Localized>{" "}
|
||||
<MarkerCount>
|
||||
{c.revision.actionCounts.flag.reasons.COMMENT_REPORTED_SPAM}
|
||||
</MarkerCount>
|
||||
</Marker>
|
||||
)) ||
|
||||
null,
|
||||
];
|
||||
|
||||
@@ -115,8 +133,10 @@ export class MarkersContainer extends React.Component<MarkersContainerProps> {
|
||||
return (
|
||||
<Markers
|
||||
details={
|
||||
doesHaveDetails ? (
|
||||
<FlagDetailsContainer
|
||||
doesHaveDetails || this.props.comment.editing.edited ? (
|
||||
<ModerateCardDetailsContainer
|
||||
hasDetails={!!doesHaveDetails}
|
||||
hasRevisions={this.props.comment.editing.edited}
|
||||
onUsernameClick={this.props.onUsernameClick}
|
||||
comment={this.props.comment}
|
||||
settings={this.props.settings}
|
||||
@@ -133,8 +153,11 @@ export class MarkersContainer extends React.Component<MarkersContainerProps> {
|
||||
const enhanced = withFragmentContainer<MarkersContainerProps>({
|
||||
comment: graphql`
|
||||
fragment MarkersContainer_comment on Comment {
|
||||
...FlagDetailsContainer_comment
|
||||
...ModerateCardDetailsContainer_comment
|
||||
status
|
||||
editing {
|
||||
edited
|
||||
}
|
||||
revision {
|
||||
actionCounts {
|
||||
flag {
|
||||
@@ -147,6 +170,7 @@ const enhanced = withFragmentContainer<MarkersContainerProps>({
|
||||
COMMENT_DETECTED_SUSPECT_WORD
|
||||
COMMENT_REPORTED_OFFENSIVE
|
||||
COMMENT_REPORTED_SPAM
|
||||
COMMENT_DETECTED_REPEAT_POST
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,7 +184,7 @@ const enhanced = withFragmentContainer<MarkersContainerProps>({
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment MarkersContainer_settings on Settings {
|
||||
...FlagDetailsContainer_settings
|
||||
...ModerateCardDetailsContainer_settings
|
||||
}
|
||||
`,
|
||||
})(MarkersContainer);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-right: var(--mini-unit);
|
||||
margin-right: var(--spacing-1);
|
||||
padding: var(--spacing-1);
|
||||
margin-left: calc(-1 * var(--spacing-1));
|
||||
line-height: calc(16rem / var(--rem-base));
|
||||
@@ -58,8 +58,8 @@
|
||||
}
|
||||
|
||||
.asideMiniWithReplyTo {
|
||||
/*
|
||||
We have reply to field, so we align below the fattest
|
||||
/*
|
||||
We have reply to field, so we align below the fattest
|
||||
element plus the reply to field.
|
||||
*/
|
||||
padding-top: calc(2 * (14rem / var(--rem-base)) + 5px + var(--spacing-4));
|
||||
@@ -89,6 +89,10 @@
|
||||
transition: background 100ms, box-shadow 100ms;
|
||||
}
|
||||
|
||||
.root:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dangling {
|
||||
background-color: var(--palette-grey-lightest);
|
||||
box-shadow: none;
|
||||
@@ -151,4 +155,26 @@
|
||||
.miniButton {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.deleted {
|
||||
background: var(--palette-grey-lightest);
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: var(--palette-grey-lighter);
|
||||
}
|
||||
|
||||
.edited {
|
||||
color: var(--palette-grey-lighter);
|
||||
padding-left: var(--spacing-2)
|
||||
}
|
||||
|
||||
.selected {
|
||||
box-shadow: 1px 4px 15px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.authorStatus {
|
||||
padding-right: var(--spacing-2);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,10 @@ const baseProps: PropTypesOf<typeof ModerateCardN> = {
|
||||
username: "Theon",
|
||||
createdAt: "2018-11-29T16:01:51.897Z",
|
||||
body: "content",
|
||||
edited: false,
|
||||
inReplyTo: null,
|
||||
comment: {},
|
||||
settings: {},
|
||||
comment: {},
|
||||
status: "undecided",
|
||||
featured: false,
|
||||
viewContextHref: "http://localhost/comment",
|
||||
@@ -25,7 +26,9 @@ const baseProps: PropTypesOf<typeof ModerateCardN> = {
|
||||
onApprove: noop,
|
||||
onReject: noop,
|
||||
onFeature: noop,
|
||||
onBan: noop,
|
||||
onUsernameClick: noop,
|
||||
onFocusOrClick: noop,
|
||||
showStory: false,
|
||||
moderatedBy: null,
|
||||
};
|
||||
@@ -42,7 +45,10 @@ it("renders correctly", () => {
|
||||
it("renders reply correctly", () => {
|
||||
const props: PropTypesOf<typeof ModerateCardN> = {
|
||||
...baseProps,
|
||||
inReplyTo: "Julian",
|
||||
inReplyTo: {
|
||||
username: "Julian",
|
||||
id: "1234",
|
||||
},
|
||||
};
|
||||
const renderer = createRenderer();
|
||||
renderer.render(<ModerateCardN {...props} />);
|
||||
@@ -91,3 +97,13 @@ it("renders story info", () => {
|
||||
renderer.render(<ModerateCardN {...props} />);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders tombstoned when comment is deleted", () => {
|
||||
const props: PropTypesOf<typeof ModerateCardN> = {
|
||||
...baseProps,
|
||||
deleted: true,
|
||||
};
|
||||
const renderer = createRenderer();
|
||||
renderer.render(<ModerateCardN {...props} />);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import cn from "classnames";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import key from "keymaster";
|
||||
import { noop } from "lodash";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
|
||||
import { HOTKEYS } from "coral-admin/constants";
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import {
|
||||
BaseButton,
|
||||
@@ -9,15 +17,17 @@ import {
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
TextLink,
|
||||
Timestamp,
|
||||
Typography,
|
||||
} from "coral-ui/components";
|
||||
|
||||
import ApproveButton from "./ApproveButton";
|
||||
import CommentAuthorContainer from "./CommentAuthorContainer";
|
||||
import CommentContent from "./CommentContent";
|
||||
import FeatureButton from "./FeatureButton";
|
||||
import InReplyTo from "./InReplyTo";
|
||||
import MarkersContainer from "./MarkersContainer";
|
||||
import RejectButton from "./RejectButton";
|
||||
import Timestamp from "./Timestamp";
|
||||
import Username from "./Username";
|
||||
|
||||
import styles from "./ModerateCard.css";
|
||||
@@ -27,8 +37,12 @@ interface Props {
|
||||
username: string;
|
||||
createdAt: string;
|
||||
body: string;
|
||||
inReplyTo: string | null;
|
||||
comment: PropTypesOf<typeof MarkersContainer>["comment"];
|
||||
inReplyTo?: {
|
||||
id: string;
|
||||
username: string | null;
|
||||
} | null;
|
||||
comment: PropTypesOf<typeof MarkersContainer>["comment"] &
|
||||
PropTypesOf<typeof CommentAuthorContainer>["comment"];
|
||||
settings: PropTypesOf<typeof MarkersContainer>["settings"];
|
||||
status: "approved" | "rejected" | "undecided";
|
||||
featured: boolean;
|
||||
@@ -43,15 +57,22 @@ interface Props {
|
||||
onApprove: () => void;
|
||||
onReject: () => void;
|
||||
onFeature: () => void;
|
||||
onUsernameClick: () => void;
|
||||
onUsernameClick: (id?: string) => void;
|
||||
onFocusOrClick: () => void;
|
||||
mini?: boolean;
|
||||
hideUsername?: boolean;
|
||||
selected?: boolean;
|
||||
/**
|
||||
* If set to true, it means this comment is about to be removed
|
||||
* from the queue. This will trigger some styling changes to
|
||||
* reflect that
|
||||
*/
|
||||
dangling?: boolean;
|
||||
deleted?: boolean;
|
||||
edited: boolean;
|
||||
selectPrev?: () => void;
|
||||
selectNext?: () => void;
|
||||
onBan: () => void;
|
||||
}
|
||||
|
||||
const ModerateCard: FunctionComponent<Props> = ({
|
||||
@@ -77,20 +98,84 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
storyHref,
|
||||
onModerateStory,
|
||||
moderatedBy,
|
||||
selected,
|
||||
onFocusOrClick,
|
||||
mini = false,
|
||||
hideUsername = false,
|
||||
deleted = false,
|
||||
edited,
|
||||
selectNext,
|
||||
selectPrev,
|
||||
onBan,
|
||||
}) => {
|
||||
const div = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (selected) {
|
||||
if (selectNext) {
|
||||
key(HOTKEYS.NEXT, id, selectNext);
|
||||
}
|
||||
if (selectPrev) {
|
||||
key(HOTKEYS.PREV, id, selectPrev);
|
||||
}
|
||||
if (onBan) {
|
||||
key(HOTKEYS.BAN, id, onBan);
|
||||
}
|
||||
key(HOTKEYS.APPROVE, id, onApprove);
|
||||
key(HOTKEYS.REJECT, id, onReject);
|
||||
|
||||
// The the scope such that only events attached to the ${id} scope will
|
||||
// be honored.
|
||||
key.setScope(id);
|
||||
|
||||
return () => {
|
||||
// Remove all events that are set in the ${id} scope.
|
||||
key.deleteScope(id);
|
||||
};
|
||||
} else {
|
||||
// Remove all events that were set in the ${id} scope.
|
||||
key.deleteScope(id);
|
||||
}
|
||||
|
||||
return noop;
|
||||
}, [selected, id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected && div && div.current) {
|
||||
div.current.focus();
|
||||
}
|
||||
}, [selected]);
|
||||
const commentBody = deleted ? (
|
||||
<Localized id="moderate-comment-deleted-body">
|
||||
<Typography>
|
||||
This comment is no longer available. The commenter has deleted their
|
||||
account.
|
||||
</Typography>
|
||||
</Localized>
|
||||
) : (
|
||||
body
|
||||
);
|
||||
const commentAuthorClick = useCallback(() => {
|
||||
onUsernameClick();
|
||||
}, [onUsernameClick]);
|
||||
const commentParentAuthorClick = useCallback(() => {
|
||||
if (inReplyTo) {
|
||||
onUsernameClick(inReplyTo.id);
|
||||
}
|
||||
}, [onUsernameClick, inReplyTo]);
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
styles.root,
|
||||
{ [styles.borderless]: mini },
|
||||
{ [styles.dangling]: dangling }
|
||||
{ [styles.dangling]: dangling },
|
||||
{ [styles.deleted]: deleted },
|
||||
{ [styles.selected]: selected }
|
||||
)}
|
||||
ref={div}
|
||||
tabIndex={0}
|
||||
data-testid={`moderate-comment-${id}`}
|
||||
id={`moderate-comment-${id}`}
|
||||
onClick={onFocusOrClick}
|
||||
>
|
||||
<Flex>
|
||||
<div className={styles.mainContainer}>
|
||||
@@ -108,12 +193,29 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
<Username>{username}</Username>
|
||||
</BaseButton>
|
||||
)}
|
||||
<CommentAuthorContainer comment={comment} />
|
||||
<Timestamp>{createdAt}</Timestamp>
|
||||
<FeatureButton featured={featured} onClick={onFeature} />
|
||||
{edited && (
|
||||
<Localized id="moderate-comment-edited">
|
||||
<Typography variant="timestamp" className={styles.edited}>
|
||||
(edited)
|
||||
</Typography>
|
||||
</Localized>
|
||||
)}
|
||||
<FeatureButton
|
||||
featured={featured}
|
||||
onClick={onFeature}
|
||||
enabled={!deleted}
|
||||
/>
|
||||
</Flex>
|
||||
{inReplyTo && (
|
||||
{inReplyTo && inReplyTo.username && (
|
||||
<div>
|
||||
<InReplyTo>{inReplyTo}</InReplyTo>
|
||||
<BaseButton
|
||||
onClick={commentParentAuthorClick}
|
||||
className={styles.username}
|
||||
>
|
||||
<InReplyTo>{inReplyTo.username}</InReplyTo>
|
||||
</BaseButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -122,7 +224,7 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
bannedWords={bannedWords}
|
||||
className={styles.content}
|
||||
>
|
||||
{body}
|
||||
{commentBody}
|
||||
</CommentContent>
|
||||
<div className={styles.footer}>
|
||||
<HorizontalGutter>
|
||||
@@ -170,7 +272,8 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
className={cn(styles.aside, {
|
||||
[styles.asideWithoutReplyTo]: !inReplyTo,
|
||||
[styles.asideMini]: mini && !inReplyTo,
|
||||
[styles.asideMiniWithReplyTo]: mini && inReplyTo,
|
||||
[styles.asideMiniWithReplyTo]:
|
||||
mini && inReplyTo && inReplyTo.username,
|
||||
})}
|
||||
alignItems="center"
|
||||
direction="column"
|
||||
@@ -185,13 +288,13 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
<RejectButton
|
||||
onClick={onReject}
|
||||
invert={status === "rejected"}
|
||||
disabled={status === "rejected" || dangling}
|
||||
disabled={status === "rejected" || dangling || deleted}
|
||||
className={cn({ [styles.miniButton]: mini })}
|
||||
/>
|
||||
<ApproveButton
|
||||
onClick={onApprove}
|
||||
invert={status === "approved"}
|
||||
disabled={status === "approved" || dangling}
|
||||
disabled={status === "approved" || dangling || deleted}
|
||||
className={cn({ [styles.miniButton]: mini })}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
import { Match, Router, withRouter } from "found";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import NotAvailable from "coral-admin/components/NotAvailable";
|
||||
import BanModal from "coral-admin/components/UserStatus/BanModal";
|
||||
import { getModerationLink } from "coral-admin/helpers";
|
||||
import {
|
||||
ApproveCommentMutation,
|
||||
RejectCommentMutation,
|
||||
} from "coral-admin/mutations";
|
||||
import FadeInTransition from "coral-framework/components/FadeInTransition";
|
||||
import {
|
||||
MutationProp,
|
||||
withFragmentContainer,
|
||||
withMutation,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLUSER_STATUS } from "coral-framework/schema";
|
||||
import { GQLTAG } from "coral-framework/schema";
|
||||
|
||||
import {
|
||||
COMMENT_STATUS,
|
||||
ModerateCardContainer_comment,
|
||||
} from "coral-admin/__generated__/ModerateCardContainer_comment.graphql";
|
||||
import { ModerateCardContainer_settings } from "coral-admin/__generated__/ModerateCardContainer_settings.graphql";
|
||||
import { ModerateCardContainer_viewer } from "coral-admin/__generated__/ModerateCardContainer_viewer.graphql";
|
||||
import NotAvailable from "coral-admin/components/NotAvailable";
|
||||
import { getModerationLink } from "coral-admin/helpers";
|
||||
import { ApproveCommentMutation } from "coral-admin/mutations";
|
||||
import { RejectCommentMutation } from "coral-admin/mutations";
|
||||
import FadeInTransition from "coral-framework/components/FadeInTransition";
|
||||
import {
|
||||
MutationProp,
|
||||
withFragmentContainer,
|
||||
withMutation,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLTAG } from "coral-framework/schema";
|
||||
|
||||
import BanCommentUserMutation from "./BanCommentUserMutation";
|
||||
import FeatureCommentMutation from "./FeatureCommentMutation";
|
||||
import ModerateCard from "./ModerateCard";
|
||||
import ModeratedByContainer from "./ModeratedByContainer";
|
||||
@@ -33,6 +39,7 @@ interface Props {
|
||||
rejectComment: MutationProp<typeof RejectCommentMutation>;
|
||||
featureComment: MutationProp<typeof FeatureCommentMutation>;
|
||||
unfeatureComment: MutationProp<typeof UnfeatureCommentMutation>;
|
||||
banUser: MutationProp<typeof BanCommentUserMutation>;
|
||||
danglingLogic: (status: COMMENT_STATUS) => boolean;
|
||||
match: Match;
|
||||
router: Router;
|
||||
@@ -40,6 +47,11 @@ interface Props {
|
||||
mini?: boolean;
|
||||
hideUsername?: boolean;
|
||||
onUsernameClicked?: (userID: string) => void;
|
||||
onSetSelected?: () => void;
|
||||
selected?: boolean;
|
||||
selectPrev?: () => void;
|
||||
selectNext?: () => void;
|
||||
loadNext?: (() => void) | null;
|
||||
}
|
||||
|
||||
function getStatus(comment: ModerateCardContainer_comment) {
|
||||
@@ -71,25 +83,50 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
unfeatureComment,
|
||||
mini,
|
||||
hideUsername,
|
||||
selected,
|
||||
selectPrev,
|
||||
selectNext,
|
||||
onUsernameClicked: usernameClicked,
|
||||
onSetSelected: setSelected,
|
||||
banUser,
|
||||
loadNext,
|
||||
}) => {
|
||||
const handleApprove = useCallback(() => {
|
||||
approveComment({
|
||||
const [showBanModal, setShowBanModal] = useState(false);
|
||||
const handleApprove = useCallback(async () => {
|
||||
if (!comment.revision) {
|
||||
return;
|
||||
}
|
||||
|
||||
await approveComment({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
storyID: match.params.storyID,
|
||||
});
|
||||
if (loadNext) {
|
||||
loadNext();
|
||||
}
|
||||
}, [approveComment, comment, match]);
|
||||
|
||||
const handleReject = useCallback(() => {
|
||||
rejectComment({
|
||||
const handleReject = useCallback(async () => {
|
||||
if (!comment.revision) {
|
||||
return;
|
||||
}
|
||||
|
||||
await rejectComment({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
storyID: match.params.storyID,
|
||||
});
|
||||
if (loadNext) {
|
||||
loadNext();
|
||||
}
|
||||
}, [rejectComment, comment, match]);
|
||||
|
||||
const handleFeature = useCallback(() => {
|
||||
if (!comment.revision) {
|
||||
return;
|
||||
}
|
||||
|
||||
featureComment({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
@@ -134,6 +171,35 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
[router, comment]
|
||||
);
|
||||
|
||||
const onFocusOrClick = useCallback(() => {
|
||||
if (setSelected) {
|
||||
setSelected();
|
||||
}
|
||||
}, [selected, comment]);
|
||||
|
||||
const handleBanModalClose = useCallback(() => {
|
||||
setShowBanModal(false);
|
||||
}, []);
|
||||
|
||||
const openBanModal = useCallback(() => {
|
||||
if (
|
||||
!comment.author ||
|
||||
comment.author.status.current.includes(GQLUSER_STATUS.BANNED)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setShowBanModal(true);
|
||||
}, [comment]);
|
||||
|
||||
const handleBanConfirm = useCallback(
|
||||
async (message: string) => {
|
||||
if (comment.author) {
|
||||
await banUser({ userID: comment.author.id, message });
|
||||
}
|
||||
setShowBanModal(false);
|
||||
},
|
||||
[comment]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<FadeInTransition active={Boolean(comment.enteredLive)}>
|
||||
@@ -146,7 +212,7 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
}
|
||||
createdAt={comment.createdAt}
|
||||
body={comment.body!}
|
||||
inReplyTo={comment.parent && comment.parent.author!.username!}
|
||||
inReplyTo={comment.parent && comment.parent.author}
|
||||
comment={comment}
|
||||
settings={settings}
|
||||
dangling={danglingLogic(comment.status)}
|
||||
@@ -159,9 +225,18 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
onReject={handleReject}
|
||||
onFeature={onFeature}
|
||||
onUsernameClick={onUsernameClicked}
|
||||
selected={selected}
|
||||
selectPrev={selectPrev}
|
||||
selectNext={selectNext}
|
||||
onBan={openBanModal}
|
||||
moderatedBy={
|
||||
<ModeratedByContainer viewer={viewer} comment={comment} />
|
||||
<ModeratedByContainer
|
||||
onUsernameClicked={onUsernameClicked}
|
||||
viewer={viewer}
|
||||
comment={comment}
|
||||
/>
|
||||
}
|
||||
onFocusOrClick={onFocusOrClick}
|
||||
showStory={showStoryInfo}
|
||||
storyTitle={
|
||||
(comment.story.metadata && comment.story.metadata.title) || (
|
||||
@@ -172,8 +247,20 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
onModerateStory={handleModerateStory}
|
||||
mini={mini}
|
||||
hideUsername={hideUsername}
|
||||
deleted={comment.deleted ? comment.deleted : false}
|
||||
edited={comment.editing.edited}
|
||||
/>
|
||||
</FadeInTransition>
|
||||
<BanModal
|
||||
username={
|
||||
comment.author && comment.author.username
|
||||
? comment.author.username
|
||||
: ""
|
||||
}
|
||||
open={showBanModal}
|
||||
onClose={handleBanModalClose}
|
||||
onConfirm={handleBanConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -185,6 +272,9 @@ const enhanced = withFragmentContainer<Props>({
|
||||
author {
|
||||
id
|
||||
username
|
||||
status {
|
||||
current
|
||||
}
|
||||
}
|
||||
statusLiveUpdated
|
||||
createdAt
|
||||
@@ -196,8 +286,12 @@ const enhanced = withFragmentContainer<Props>({
|
||||
revision {
|
||||
id
|
||||
}
|
||||
editing {
|
||||
edited
|
||||
}
|
||||
parent {
|
||||
author {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
@@ -209,8 +303,10 @@ const enhanced = withFragmentContainer<Props>({
|
||||
}
|
||||
permalink
|
||||
enteredLive
|
||||
deleted
|
||||
...MarkersContainer_comment
|
||||
...ModeratedByContainer_comment
|
||||
...CommentAuthorContainer_comment
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
@@ -229,10 +325,12 @@ const enhanced = withFragmentContainer<Props>({
|
||||
`,
|
||||
})(
|
||||
withRouter(
|
||||
withMutation(ApproveCommentMutation)(
|
||||
withMutation(RejectCommentMutation)(
|
||||
withMutation(FeatureCommentMutation)(
|
||||
withMutation(UnfeatureCommentMutation)(ModerateCardContainer)
|
||||
withMutation(BanCommentUserMutation)(
|
||||
withMutation(ApproveCommentMutation)(
|
||||
withMutation(RejectCommentMutation)(
|
||||
withMutation(FeatureCommentMutation)(
|
||||
withMutation(UnfeatureCommentMutation)(ModerateCardContainer)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.button {
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
}
|
||||