Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dbcba2849 | ||
|
|
ac96e5e312 | ||
|
|
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 |
@@ -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",
|
||||
},
|
||||
};
|
||||
@@ -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__"
|
||||
],
|
||||
}
|
||||
|
||||
@@ -15,640 +15,26 @@ Preview Coral easily by running Coral via a Heroku App:
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table of Contents
|
||||
|
||||
- [Requirements](#requirements)
|
||||
- [Running](#running)
|
||||
- [Docker](#docker)
|
||||
- [Source](#source)
|
||||
- [Embed On Your Site](#embed-on-your-site)
|
||||
- [Single Sign On](#single-sign-on)
|
||||
- [External Integrations](#external-integrations)
|
||||
- [Login Prompts](#login-prompts)
|
||||
- [Development](#development)
|
||||
- [Email](#email)
|
||||
- [Design Language System (UI Components)](#design-language-system-ui-components)
|
||||
- [GraphQL API](#graphql-api)
|
||||
- [Making your first request](#making-your-first-request)
|
||||
- [Understanding the response](#understanding-the-response)
|
||||
- [Authorizing a request](#authorizing-a-request)
|
||||
- [Bearer Token](#bearer-token)
|
||||
- [Cookie](#cookie)
|
||||
- [Persisted Queries](#persisted-queries)
|
||||
- [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!
|
||||
|
||||
### 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.
|
||||
- `user.badges` (_optional_) - array of strings to be displayed as badges beside
|
||||
username inside Coral, visible to other users and moderators. For example, to indicate
|
||||
a user's subscription status.
|
||||
- `user.role` (_optional_) - one of "COMMENTER", "STAFF", "MODERATOR", "ADMIN". Will create/update
|
||||
Coral user with this role.
|
||||
|
||||
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();
|
||||
```
|
||||
|
||||
#### External Integrations
|
||||
|
||||
You can integrate directly with the Coral GraphQL API in order to facilitate
|
||||
account updates for your users when using Coral SSO. The relevant mutations are
|
||||
as follows:
|
||||
|
||||
- `updateUserUsername` lets you update a given user with a new username using
|
||||
an admin token.
|
||||
- `updateUserEmail` lets you update a given user with a new email address
|
||||
using an admin token.
|
||||
- `deleteUser` lets you delete a given account using an admin token.
|
||||
Note that even with an admin token, you may not delete yourself via
|
||||
this method, and instead must use the `requestAccountDeletion`
|
||||
mutation instead. This differs from the `requestAccountDeletion` as
|
||||
it does the operation immediately instead of scheduling it as
|
||||
`requestAccountDeletion` does.
|
||||
- `requestUserCommentsDownload` lets you retrieve a given account's comments download. This mutation will provide you with a `archiveURL` that can be used to download a ZIP file containing the user's comment export.
|
||||
|
||||
If you're unsure on how to call GraphQL API's, refer to the section here on [Making your first GraphQL request](#making-your-first-request).
|
||||
|
||||
#### Login Prompts
|
||||
|
||||
In order to handle login prompts (e.g. a user clicks on the sign in button) you can listen to the `loginPrompt` event.
|
||||
|
||||
```js
|
||||
var embed = Coral.createStreamEmbed({
|
||||
// Don't forget to include the parameters from the
|
||||
// "Embed On Your Site" section.
|
||||
events: function(events) {
|
||||
events.on("loginPrompt", function() {
|
||||
// Redirect user to a login page.
|
||||
location.href = "http://example.com/login";
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
We recommend installing [watchman](https://facebook.github.io/watchman/docs/install.html) for better watch
|
||||
performance.
|
||||
|
||||
```bash
|
||||
# On macOS, you can run the following with Homebrew.
|
||||
brew update
|
||||
brew install watchman
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
#### 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/.
|
||||
|
||||
## GraphQL API
|
||||
|
||||
Our API is generally served via GraphQL at `/api/graphql` on your Coral installation. If you're running Coral locally, this would be https://localhost:8080/api/graphql.
|
||||
|
||||
You can enable the GraphiQL interface at https://localhost:3000/graphiql (Note the port number here is not 8080, this is because this route is directly served by the server, and not the webpack development server) when running in development to access a GraphQL playground to use with documentation provided in the sidebar on what edges are available to you. You can do this by setting `ENABLE_GRAPHIQL=true`. **(🚨 Note 🚨) we do not recommend using this in production environments as it disables many safety features used by the application**.
|
||||
|
||||
### Making your first request
|
||||
|
||||
To learn a bit about how to interact with Coral, we'll query for comments on a
|
||||
page of Coral.
|
||||
|
||||
The GraphQL endpoint we have can be used with any HTTP client available, but our
|
||||
examples below will use the common `curl` tool:
|
||||
|
||||
```sh
|
||||
curl --request POST \
|
||||
--url "http://localhost:8080/api/graphql" \
|
||||
--header "content-type: application/json" \
|
||||
--data '{"query":"query GetComments($url: String!) {story(url: $url) { id metadata { title } url comments { nodes { id body author { id username } } } } }","variables":{"url":"http://localhost:8080/"},"operationName":"GetComments"}'
|
||||
```
|
||||
|
||||
When you unpack that, it's really quite simple. We're executing a `POST` request
|
||||
to the `/api/graphql` route of the local Talk server with the GraphQL
|
||||
request we want to make. It's composed of the `query`, `variables`, and
|
||||
`operationName`.
|
||||
|
||||
```graphql
|
||||
query GetComments($url: String!) {
|
||||
story(url: $url) {
|
||||
metadata {
|
||||
title
|
||||
}
|
||||
url
|
||||
comments {
|
||||
nodes {
|
||||
body
|
||||
author {
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We are grabbing the asset with the specified `$url`, and grabbing it's title and the comments under it.
|
||||
|
||||
We can then also specify our variables to the query being executed (in this
|
||||
case, the url for the page where we have comments on our local install of Coral):
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "http://localhost:8080/"
|
||||
}
|
||||
```
|
||||
|
||||
It's also sometimes common to have multiple queries within a query, which is
|
||||
where the `operationName` comes into play, where we simply specify the named
|
||||
query that we want to execute (in this case, `GetComments`).
|
||||
|
||||
To get a deeper understanding of GraphQL queries, read up on
|
||||
[GraphQL Queries and Mutations](http://graphql.org/learn/queries/).
|
||||
|
||||
### Understanding the response
|
||||
|
||||
Once you completed the above GraphQL query with `curl`, you'll get a response
|
||||
sort of like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"story": {
|
||||
"metadata": {
|
||||
"title": "Coral 5.0 – Embed Stream"
|
||||
},
|
||||
"url": "http://localhost:8080/",
|
||||
"comments": {
|
||||
"nodes": [
|
||||
{
|
||||
"body": "First comment!",
|
||||
"author": {
|
||||
"username": "wyatt.johnson"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All of the parameters you requested should be available under the `data`
|
||||
property. Any errors that you get would appear in a `errors` array at the top
|
||||
level, like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"message": "The specified story URL does not exist in the permitted domains list.",
|
||||
"locations": [
|
||||
{
|
||||
"line": 2,
|
||||
"column": 3
|
||||
}
|
||||
],
|
||||
"path": ["story"],
|
||||
"extensions": {
|
||||
"code": "STORY_URL_NOT_PERMITTED",
|
||||
"id": "e255e860-d3ab-11e9-acdf-b9e9700f06fa",
|
||||
"type": "INVALID_REQUEST_ERROR",
|
||||
"message": "The specified story URL does not exist in the permitted domains list."
|
||||
}
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"story": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You should know that any property that is marked with a `!` is considered
|
||||
required, and non-nullable, which means you can always guarantee on it being
|
||||
there in your request if there were no errors.
|
||||
|
||||
### Authorizing a request
|
||||
|
||||
Some queries you may notice seem to return an error of
|
||||
`USER_NOT_ENTITLED`. It's likely the case that you are making a request to a
|
||||
route that requires authorization. You can perform authorization a few ways in
|
||||
Talk.
|
||||
|
||||
Essentially, you need to get access to a JWT token that you can use to authorize
|
||||
your requests.
|
||||
|
||||
```sh
|
||||
curl --request POST \
|
||||
--url http://localhost:3000/api/auth/local \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{ "email": "${EMAIL}", "password": "${PASSWORD}"}'
|
||||
```
|
||||
|
||||
Which returns a response similar to:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "${TOKEN}"
|
||||
}
|
||||
```
|
||||
|
||||
Where `${EMAIL}` is the email address of an admin user, and `${PASSWORD}` is the password for that admin user. This will generate a short term token (valid for 90 days). To generate a long lived access token (or Personal Access Token), you have to exchange the token generated above to create a new long lived token:
|
||||
|
||||
```sh
|
||||
curl --request POST \
|
||||
--url "http://localhost:3000/api/graphql" \
|
||||
--header 'authorization: Bearer ${TOKEN}' \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{"query":"mutation CreateAccessToken { createToken(input: { clientMutationId: \"\", name: \"My PAT\" }) { signedToken }}","operationName":"CreateAccessToken"}'
|
||||
```
|
||||
|
||||
Which returns a response similar to:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"createToken": {
|
||||
"signedToken": "${TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Where `${TOKEN}` in the request being from the previous set of login steps and returning a new `signedToken` as `${TOKEN}` which can be used instead of the previous `${TOKEN}` value in the below examples.
|
||||
|
||||
Once you have your access token, you can substitute it as `${TOKEN}` in your
|
||||
`curl` request as follows:
|
||||
|
||||
#### Bearer Token
|
||||
|
||||
```sh
|
||||
curl --request POST \
|
||||
--url "http://localhost:8080/api/graphql" \
|
||||
--header "content-type: application/json" \
|
||||
--header "Authorization: Bearer ${TOKEN}" \
|
||||
--data '{"query":"query GetComments($url: String!) {story(url: $url) { id metadata { title } url comments { nodes { id body author { id username } } } } }","variables":{"url":"http://localhost:8080/"},"operationName":"GetComments"}'
|
||||
```
|
||||
|
||||
#### Cookie
|
||||
|
||||
```sh
|
||||
curl --request POST \
|
||||
--url "http://localhost:8080/api/graphql" \
|
||||
--header "content-type: application/json" \
|
||||
--cookie "authorization=${TOKEN}"
|
||||
--data '{"query":"query GetComments($url: String!) {story(url: $url) { id metadata { title } url comments { nodes { id body author { id username } } } } }","variables":{"url":"http://localhost:8080/"},"operationName":"GetComments"}'
|
||||
```
|
||||
|
||||
### Persisted Queries
|
||||
|
||||
You might see an error like `RAW_QUERY_NOT_AUTHORIZED`. This means that you attempted to use either no token, or a token from a user without admin privileges. In Coral, we whitelist the GraphQL mutations and queries that are performed by the applications for security reasons meaning that only admin users can make arbitrary queries against the GraphQL API.
|
||||
|
||||
## 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 interface at `/graphiql`. **(🚨 Note 🚨) we do not recommend using this in production environments as it disables many safety features used by the application**. (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`)
|
||||
- `TRUST_PROXY` - When provided, it configures the "trust proxy" settings for Express (See https://expressjs.com/en/guide/behind-proxies.html)
|
||||
- [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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coralproject/talk",
|
||||
"version": "5.1.0",
|
||||
"version": "5.2.2",
|
||||
"author": "The Coral Project",
|
||||
"homepage": "https://coralproject.net/",
|
||||
"sideEffects": [
|
||||
@@ -20,10 +20,11 @@
|
||||
],
|
||||
"description": "A better commenting experience from Mozilla, The Washington Post, and The New York Times.",
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production npm-run-all generate-persist --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 generate:css-types generate:schema generate:relay",
|
||||
"generate-persist": "npm-run-all generate:css-types generate:schema generate:relay-persist",
|
||||
@@ -41,9 +42,9 @@
|
||||
"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",
|
||||
@@ -98,6 +99,7 @@
|
||||
"jsonwebtoken": "^8.3.0",
|
||||
"juice": "^5.2.0",
|
||||
"jwks-rsa": "^1.3.0",
|
||||
"keymaster": "^1.6.2",
|
||||
"linkifyjs": "^2.1.8",
|
||||
"lodash": "^4.17.15",
|
||||
"lru-cache": "^5.1.1",
|
||||
@@ -125,6 +127,7 @@
|
||||
"prom-client": "^11.3.0",
|
||||
"proxy-agent": "^3.1.0",
|
||||
"querystringify": "^2.1.0",
|
||||
"react-helmet": "^5.2.1",
|
||||
"source-map-support": "^0.5.12",
|
||||
"stack-utils": "^1.0.2",
|
||||
"striptags": "^3.1.1",
|
||||
@@ -135,12 +138,6 @@
|
||||
"verror": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"farce": "^0.2.6",
|
||||
"common-tags": "^1.8.0",
|
||||
"found": "^0.4.0-alpha.17",
|
||||
"found-relay": "^0.4.0-alpha.8",
|
||||
"react-relay-network-modern": "^4.0.4",
|
||||
"@types/archiver": "^3.0.0",
|
||||
"@babel/core": "^7.4.5",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.4.4",
|
||||
@@ -152,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",
|
||||
@@ -209,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",
|
||||
@@ -230,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",
|
||||
@@ -249,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",
|
||||
@@ -260,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",
|
||||
@@ -299,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",
|
||||
@@ -312,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",
|
||||
@@ -341,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",
|
||||
@@ -360,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";
|
||||
|
||||
@@ -19,29 +19,29 @@ program
|
||||
.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`
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
*/
|
||||
|
||||
import { LocalesData } from "coral-framework/lib/i18n";
|
||||
|
||||
export default {} as LocalesData;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -69,9 +69,11 @@ const UnsubscribeRoute: React.FunctionComponent<Props> = ({ token }) => {
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
render: ({ match, Component }) => (
|
||||
<Component token={parseHashQuery(match.location.hash).unsubscribeToken} />
|
||||
),
|
||||
render: function UnsubscribeRouteRender({ match, Component }) {
|
||||
return (
|
||||
<Component token={parseHashQuery(match.location.hash).unsubscribeToken} />
|
||||
);
|
||||
},
|
||||
})(UnsubscribeRoute);
|
||||
|
||||
export default enhanced;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -81,6 +81,7 @@ 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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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 |
@@ -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"],
|
||||
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { BaseButton } from "coral-ui/components";
|
||||
|
||||
import styles from "./FlagDetailsEntry.css";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -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;
|
||||
@@ -30,7 +32,7 @@ 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,
|
||||
@@ -46,7 +48,7 @@ const markers: Array<
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_BANNED_WORD && (
|
||||
<Localized id="moderate-marker-bannedWord" key={keyCounter++}>
|
||||
<Marker color="error">Banned Word</Marker>
|
||||
<Marker color="error">Banned word</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
@@ -55,7 +57,7 @@ const markers: Array<
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SUSPECT_WORD && (
|
||||
<Localized id="moderate-marker-suspectWord" key={keyCounter++}>
|
||||
<Marker color="error" variant="filled">
|
||||
Suspect Word
|
||||
Suspect word
|
||||
</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
@@ -64,7 +66,7 @@ const markers: Array<
|
||||
(c.revision &&
|
||||
c.revision.actionCounts.flag.reasons.COMMENT_DETECTED_SPAM && (
|
||||
<Localized id="moderate-marker-spamDetected" key={keyCounter++}>
|
||||
<Marker color="error">Spam Detected</Marker>
|
||||
<Marker color="error">Spam detected</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
@@ -76,11 +78,19 @@ const markers: Array<
|
||||
</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>
|
||||
<Marker color="error">Recent history</Marker>
|
||||
</Localized>
|
||||
)) ||
|
||||
null,
|
||||
@@ -123,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}
|
||||
@@ -141,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 {
|
||||
@@ -155,6 +170,7 @@ const enhanced = withFragmentContainer<MarkersContainerProps>({
|
||||
COMMENT_DETECTED_SUSPECT_WORD
|
||||
COMMENT_REPORTED_OFFENSIVE
|
||||
COMMENT_REPORTED_SPAM
|
||||
COMMENT_DETECTED_REPEAT_POST
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,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));
|
||||
@@ -89,6 +89,10 @@
|
||||
transition: background 100ms, box-shadow 100ms;
|
||||
}
|
||||
|
||||
.root:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dangling {
|
||||
background-color: var(--palette-grey-lightest);
|
||||
box-shadow: none;
|
||||
@@ -159,4 +163,18 @@
|
||||
|
||||
.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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
@@ -14,6 +22,7 @@ import {
|
||||
} from "coral-ui/components";
|
||||
|
||||
import ApproveButton from "./ApproveButton";
|
||||
import CommentAuthorContainer from "./CommentAuthorContainer";
|
||||
import CommentContent from "./CommentContent";
|
||||
import FeatureButton from "./FeatureButton";
|
||||
import InReplyTo from "./InReplyTo";
|
||||
@@ -32,7 +41,8 @@ interface Props {
|
||||
id: string;
|
||||
username: string | null;
|
||||
} | null;
|
||||
comment: PropTypesOf<typeof MarkersContainer>["comment"];
|
||||
comment: PropTypesOf<typeof MarkersContainer>["comment"] &
|
||||
PropTypesOf<typeof CommentAuthorContainer>["comment"];
|
||||
settings: PropTypesOf<typeof MarkersContainer>["settings"];
|
||||
status: "approved" | "rejected" | "undecided";
|
||||
featured: boolean;
|
||||
@@ -48,8 +58,10 @@ interface Props {
|
||||
onReject: () => void;
|
||||
onFeature: () => 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
|
||||
@@ -57,6 +69,10 @@ interface Props {
|
||||
*/
|
||||
dangling?: boolean;
|
||||
deleted?: boolean;
|
||||
edited: boolean;
|
||||
selectPrev?: () => void;
|
||||
selectNext?: () => void;
|
||||
onBan: () => void;
|
||||
}
|
||||
|
||||
const ModerateCard: FunctionComponent<Props> = ({
|
||||
@@ -82,10 +98,52 @@ 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>
|
||||
@@ -110,9 +168,14 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
styles.root,
|
||||
{ [styles.borderless]: mini },
|
||||
{ [styles.dangling]: dangling },
|
||||
{ [styles.deleted]: deleted }
|
||||
{ [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}>
|
||||
@@ -130,7 +193,15 @@ const ModerateCard: FunctionComponent<Props> = ({
|
||||
<Username>{username}</Username>
|
||||
</BaseButton>
|
||||
)}
|
||||
<Timestamp className={styles.timestamp}>{createdAt}</Timestamp>
|
||||
<CommentAuthorContainer comment={comment} />
|
||||
<Timestamp>{createdAt}</Timestamp>
|
||||
{edited && (
|
||||
<Localized id="moderate-comment-edited">
|
||||
<Typography variant="timestamp" className={styles.edited}>
|
||||
(edited)
|
||||
</Typography>
|
||||
</Localized>
|
||||
)}
|
||||
<FeatureButton
|
||||
featured={featured}
|
||||
onClick={onFeature}
|
||||
|
||||
@@ -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,30 +83,43 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
unfeatureComment,
|
||||
mini,
|
||||
hideUsername,
|
||||
selected,
|
||||
selectPrev,
|
||||
selectNext,
|
||||
onUsernameClicked: usernameClicked,
|
||||
onSetSelected: setSelected,
|
||||
banUser,
|
||||
loadNext,
|
||||
}) => {
|
||||
const handleApprove = useCallback(() => {
|
||||
const [showBanModal, setShowBanModal] = useState(false);
|
||||
const handleApprove = useCallback(async () => {
|
||||
if (!comment.revision) {
|
||||
return;
|
||||
}
|
||||
|
||||
approveComment({
|
||||
await approveComment({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
storyID: match.params.storyID,
|
||||
});
|
||||
if (loadNext) {
|
||||
loadNext();
|
||||
}
|
||||
}, [approveComment, comment, match]);
|
||||
|
||||
const handleReject = useCallback(() => {
|
||||
const handleReject = useCallback(async () => {
|
||||
if (!comment.revision) {
|
||||
return;
|
||||
}
|
||||
|
||||
rejectComment({
|
||||
await rejectComment({
|
||||
commentID: comment.id,
|
||||
commentRevisionID: comment.revision.id,
|
||||
storyID: match.params.storyID,
|
||||
});
|
||||
if (loadNext) {
|
||||
loadNext();
|
||||
}
|
||||
}, [rejectComment, comment, match]);
|
||||
|
||||
const handleFeature = useCallback(() => {
|
||||
@@ -146,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)}>
|
||||
@@ -171,6 +225,10 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
onReject={handleReject}
|
||||
onFeature={onFeature}
|
||||
onUsernameClick={onUsernameClicked}
|
||||
selected={selected}
|
||||
selectPrev={selectPrev}
|
||||
selectNext={selectNext}
|
||||
onBan={openBanModal}
|
||||
moderatedBy={
|
||||
<ModeratedByContainer
|
||||
onUsernameClicked={onUsernameClicked}
|
||||
@@ -178,6 +236,7 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
comment={comment}
|
||||
/>
|
||||
}
|
||||
onFocusOrClick={onFocusOrClick}
|
||||
showStory={showStoryInfo}
|
||||
storyTitle={
|
||||
(comment.story.metadata && comment.story.metadata.title) || (
|
||||
@@ -189,8 +248,19 @@ const ModerateCardContainer: FunctionComponent<Props> = ({
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -202,6 +272,9 @@ const enhanced = withFragmentContainer<Props>({
|
||||
author {
|
||||
id
|
||||
username
|
||||
status {
|
||||
current
|
||||
}
|
||||
}
|
||||
statusLiveUpdated
|
||||
createdAt
|
||||
@@ -213,6 +286,9 @@ const enhanced = withFragmentContainer<Props>({
|
||||
revision {
|
||||
id
|
||||
}
|
||||
editing {
|
||||
edited
|
||||
}
|
||||
parent {
|
||||
author {
|
||||
id
|
||||
@@ -230,6 +306,7 @@ const enhanced = withFragmentContainer<Props>({
|
||||
deleted
|
||||
...MarkersContainer_comment
|
||||
...ModeratedByContainer_comment
|
||||
...CommentAuthorContainer_comment
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
@@ -248,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;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { Flex, HorizontalGutter, Icon, Tab, TabBar } from "coral-ui/components";
|
||||
|
||||
import { ModerateCardDetailsContainer_comment as CommentData } from "coral-admin/__generated__/ModerateCardDetailsContainer_comment.graphql";
|
||||
import { ModerateCardDetailsContainer_settings as SettingsData } from "coral-admin/__generated__/ModerateCardDetailsContainer_settings.graphql";
|
||||
|
||||
import CommentRevisionContainer from "./CommentRevisionContainer";
|
||||
import FlagDetailsContainer from "./FlagDetailsContainer";
|
||||
|
||||
import styles from "./ModerateCardDetailsContainer.css";
|
||||
|
||||
interface Props {
|
||||
comment: CommentData;
|
||||
settings: SettingsData;
|
||||
onUsernameClick: (id?: string) => void;
|
||||
hasDetails: boolean;
|
||||
hasRevisions: boolean;
|
||||
}
|
||||
|
||||
const ModerateCardDetailsContainer: FunctionComponent<Props> = ({
|
||||
comment,
|
||||
onUsernameClick,
|
||||
settings,
|
||||
hasDetails,
|
||||
hasRevisions,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<"DETAILS" | "HISTORY">(
|
||||
hasDetails ? "DETAILS" : "HISTORY"
|
||||
);
|
||||
|
||||
return (
|
||||
<HorizontalGutter>
|
||||
<TabBar
|
||||
variant="secondary"
|
||||
activeTab={activeTab}
|
||||
onTabClick={id => setActiveTab(id as "DETAILS" | "HISTORY")}
|
||||
>
|
||||
{hasDetails && (
|
||||
<Tab tabID="DETAILS" classes={styles}>
|
||||
<Flex alignItems="center" itemGutter>
|
||||
<Icon size="md">list</Icon>
|
||||
<Localized id="moderateCardDetails-tab-details">
|
||||
<span>Details</span>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</Tab>
|
||||
)}
|
||||
{hasRevisions && (
|
||||
<Tab tabID="HISTORY" classes={styles}>
|
||||
<Flex alignItems="center" itemGutter>
|
||||
<Icon>edit</Icon>
|
||||
<Localized id="moderateCardDetails-tab-edits">
|
||||
<span>Edit history</span>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</Tab>
|
||||
)}
|
||||
</TabBar>
|
||||
{activeTab === "DETAILS" && (
|
||||
<FlagDetailsContainer
|
||||
comment={comment}
|
||||
settings={settings}
|
||||
onUsernameClick={onUsernameClick}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "HISTORY" && (
|
||||
<CommentRevisionContainer comment={comment} settings={settings} />
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
comment: graphql`
|
||||
fragment ModerateCardDetailsContainer_comment on Comment {
|
||||
...FlagDetailsContainer_comment
|
||||
...CommentRevisionContainer_comment
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ModerateCardDetailsContainer_settings on Settings {
|
||||
...FlagDetailsContainer_settings
|
||||
...CommentRevisionContainer_settings
|
||||
}
|
||||
`,
|
||||
})(ModerateCardDetailsContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -2,11 +2,12 @@ import { Localized } from "fluent-react/compat";
|
||||
import React, { useCallback } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { ModeratedByContainer_comment } from "coral-admin/__generated__/ModeratedByContainer_comment.graphql";
|
||||
import { ModeratedByContainer_viewer } from "coral-admin/__generated__/ModeratedByContainer_viewer.graphql";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { BaseButton } from "coral-ui/components";
|
||||
|
||||
import { ModeratedByContainer_comment } from "coral-admin/__generated__/ModeratedByContainer_comment.graphql";
|
||||
import { ModeratedByContainer_viewer } from "coral-admin/__generated__/ModeratedByContainer_viewer.graphql";
|
||||
|
||||
import styles from "./ModeratedByContainer.css";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
|
||||
import RejectButton from "./RejectButton";
|
||||
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
|
||||
import RejectButton from "./RejectButton";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof RejectButton> = {
|
||||
invert: false,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLTAG } from "coral-framework/schema";
|
||||
|
||||
import { UnfeatureCommentMutation } from "coral-stream/__generated__/UnfeatureCommentMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
exports[`renders all markers 1`] = `
|
||||
<Markers
|
||||
details={
|
||||
<Relay(FlagDetailsContainer)
|
||||
<Relay(ModerateCardDetailsContainer)
|
||||
comment={
|
||||
Object {
|
||||
"editing": Object {
|
||||
"edited": false,
|
||||
},
|
||||
"revision": Object {
|
||||
"actionCounts": Object {
|
||||
"flag": Object {
|
||||
@@ -13,6 +16,7 @@ exports[`renders all markers 1`] = `
|
||||
"COMMENT_DETECTED_BANNED_WORD": 1,
|
||||
"COMMENT_DETECTED_LINKS": 1,
|
||||
"COMMENT_DETECTED_RECENT_HISTORY": 1,
|
||||
"COMMENT_DETECTED_REPEAT_POST": 1,
|
||||
"COMMENT_DETECTED_SPAM": 1,
|
||||
"COMMENT_DETECTED_SUSPECT_WORD": 1,
|
||||
"COMMENT_DETECTED_TOXIC": 1,
|
||||
@@ -30,6 +34,8 @@ exports[`renders all markers 1`] = `
|
||||
"status": "PREMOD",
|
||||
}
|
||||
}
|
||||
hasDetails={true}
|
||||
hasRevisions={false}
|
||||
onUsernameClick={[Function]}
|
||||
settings={
|
||||
Object {
|
||||
@@ -49,7 +55,7 @@ exports[`renders all markers 1`] = `
|
||||
<withPropsOnChange(Marker)
|
||||
color="primary"
|
||||
>
|
||||
Pre-Mod
|
||||
Pre-mod
|
||||
</withPropsOnChange(Marker)>
|
||||
</Localized>
|
||||
<Localized
|
||||
@@ -67,7 +73,7 @@ exports[`renders all markers 1`] = `
|
||||
<withPropsOnChange(Marker)
|
||||
color="error"
|
||||
>
|
||||
Banned Word
|
||||
Banned word
|
||||
</withPropsOnChange(Marker)>
|
||||
</Localized>
|
||||
<Localized
|
||||
@@ -77,7 +83,7 @@ exports[`renders all markers 1`] = `
|
||||
color="error"
|
||||
variant="filled"
|
||||
>
|
||||
Suspect Word
|
||||
Suspect word
|
||||
</withPropsOnChange(Marker)>
|
||||
</Localized>
|
||||
<Localized
|
||||
@@ -86,7 +92,7 @@ exports[`renders all markers 1`] = `
|
||||
<withPropsOnChange(Marker)
|
||||
color="error"
|
||||
>
|
||||
Spam Detected
|
||||
Spam detected
|
||||
</withPropsOnChange(Marker)>
|
||||
</Localized>
|
||||
<Localized
|
||||
@@ -98,13 +104,22 @@ exports[`renders all markers 1`] = `
|
||||
Toxic
|
||||
</withPropsOnChange(Marker)>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="moderate-marker-repeatPost"
|
||||
>
|
||||
<withPropsOnChange(Marker)
|
||||
color="error"
|
||||
>
|
||||
Repeat comment
|
||||
</withPropsOnChange(Marker)>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="moderate-marker-recentHistory"
|
||||
>
|
||||
<withPropsOnChange(Marker)
|
||||
color="error"
|
||||
>
|
||||
Recent History
|
||||
Recent history
|
||||
</withPropsOnChange(Marker)>
|
||||
</Localized>
|
||||
<withPropsOnChange(Marker)
|
||||
@@ -143,9 +158,12 @@ exports[`renders all markers 1`] = `
|
||||
exports[`renders some markers 1`] = `
|
||||
<Markers
|
||||
details={
|
||||
<Relay(FlagDetailsContainer)
|
||||
<Relay(ModerateCardDetailsContainer)
|
||||
comment={
|
||||
Object {
|
||||
"editing": Object {
|
||||
"edited": false,
|
||||
},
|
||||
"revision": Object {
|
||||
"actionCounts": Object {
|
||||
"flag": Object {
|
||||
@@ -153,6 +171,7 @@ exports[`renders some markers 1`] = `
|
||||
"COMMENT_DETECTED_BANNED_WORD": 1,
|
||||
"COMMENT_DETECTED_LINKS": 0,
|
||||
"COMMENT_DETECTED_RECENT_HISTORY": 1,
|
||||
"COMMENT_DETECTED_REPEAT_POST": 0,
|
||||
"COMMENT_DETECTED_SPAM": 0,
|
||||
"COMMENT_DETECTED_SUSPECT_WORD": 0,
|
||||
"COMMENT_DETECTED_TOXIC": 1,
|
||||
@@ -170,6 +189,8 @@ exports[`renders some markers 1`] = `
|
||||
"status": "PREMOD",
|
||||
}
|
||||
}
|
||||
hasDetails={true}
|
||||
hasRevisions={false}
|
||||
onUsernameClick={[Function]}
|
||||
settings={
|
||||
Object {
|
||||
@@ -189,7 +210,7 @@ exports[`renders some markers 1`] = `
|
||||
<withPropsOnChange(Marker)
|
||||
color="primary"
|
||||
>
|
||||
Pre-Mod
|
||||
Pre-mod
|
||||
</withPropsOnChange(Marker)>
|
||||
</Localized>
|
||||
<Localized
|
||||
@@ -198,7 +219,7 @@ exports[`renders some markers 1`] = `
|
||||
<withPropsOnChange(Marker)
|
||||
color="error"
|
||||
>
|
||||
Banned Word
|
||||
Banned word
|
||||
</withPropsOnChange(Marker)>
|
||||
</Localized>
|
||||
<Localized
|
||||
@@ -216,7 +237,7 @@ exports[`renders some markers 1`] = `
|
||||
<withPropsOnChange(Marker)
|
||||
color="error"
|
||||
>
|
||||
Recent History
|
||||
Recent history
|
||||
</withPropsOnChange(Marker)>
|
||||
</Localized>
|
||||
<withPropsOnChange(Marker)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders approved correctly 1`] = `
|
||||
<withPropsOnChange(Card)
|
||||
<ForwardRef(forwardRef)
|
||||
className="ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-id"
|
||||
id="moderate-comment-comment-id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
@@ -23,8 +26,10 @@ exports[`renders approved correctly 1`] = `
|
||||
Theon
|
||||
</Username>
|
||||
</ForwardRef(forwardRef)>
|
||||
<Relay(CommentAuthorContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
<Timestamp
|
||||
className="ModerateCard-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-11-29T16:01:51.897Z
|
||||
@@ -112,13 +117,16 @@ exports[`renders approved correctly 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</withPropsOnChange(Card)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<withPropsOnChange(Card)
|
||||
<ForwardRef(forwardRef)
|
||||
className="ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-id"
|
||||
id="moderate-comment-comment-id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
@@ -138,8 +146,10 @@ exports[`renders correctly 1`] = `
|
||||
Theon
|
||||
</Username>
|
||||
</ForwardRef(forwardRef)>
|
||||
<Relay(CommentAuthorContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
<Timestamp
|
||||
className="ModerateCard-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-11-29T16:01:51.897Z
|
||||
@@ -227,13 +237,16 @@ exports[`renders correctly 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</withPropsOnChange(Card)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`renders dangling correctly 1`] = `
|
||||
<withPropsOnChange(Card)
|
||||
<ForwardRef(forwardRef)
|
||||
className="ModerateCard-root ModerateCard-dangling"
|
||||
data-testid="moderate-comment-comment-id"
|
||||
id="moderate-comment-comment-id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
@@ -253,8 +266,10 @@ exports[`renders dangling correctly 1`] = `
|
||||
Theon
|
||||
</Username>
|
||||
</ForwardRef(forwardRef)>
|
||||
<Relay(CommentAuthorContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
<Timestamp
|
||||
className="ModerateCard-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-11-29T16:01:51.897Z
|
||||
@@ -342,13 +357,16 @@ exports[`renders dangling correctly 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</withPropsOnChange(Card)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`renders rejected correctly 1`] = `
|
||||
<withPropsOnChange(Card)
|
||||
<ForwardRef(forwardRef)
|
||||
className="ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-id"
|
||||
id="moderate-comment-comment-id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
@@ -368,8 +386,10 @@ exports[`renders rejected correctly 1`] = `
|
||||
Theon
|
||||
</Username>
|
||||
</ForwardRef(forwardRef)>
|
||||
<Relay(CommentAuthorContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
<Timestamp
|
||||
className="ModerateCard-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-11-29T16:01:51.897Z
|
||||
@@ -457,13 +477,16 @@ exports[`renders rejected correctly 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</withPropsOnChange(Card)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`renders reply correctly 1`] = `
|
||||
<withPropsOnChange(Card)
|
||||
<ForwardRef(forwardRef)
|
||||
className="ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-id"
|
||||
id="moderate-comment-comment-id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
@@ -483,8 +506,10 @@ exports[`renders reply correctly 1`] = `
|
||||
Theon
|
||||
</Username>
|
||||
</ForwardRef(forwardRef)>
|
||||
<Relay(CommentAuthorContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
<Timestamp
|
||||
className="ModerateCard-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-11-29T16:01:51.897Z
|
||||
@@ -582,13 +607,16 @@ exports[`renders reply correctly 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</withPropsOnChange(Card)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`renders story info 1`] = `
|
||||
<withPropsOnChange(Card)
|
||||
<ForwardRef(forwardRef)
|
||||
className="ModerateCard-root"
|
||||
data-testid="moderate-comment-comment-id"
|
||||
id="moderate-comment-comment-id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
@@ -608,8 +636,10 @@ exports[`renders story info 1`] = `
|
||||
Theon
|
||||
</Username>
|
||||
</ForwardRef(forwardRef)>
|
||||
<Relay(CommentAuthorContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
<Timestamp
|
||||
className="ModerateCard-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-11-29T16:01:51.897Z
|
||||
@@ -726,13 +756,16 @@ exports[`renders story info 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</withPropsOnChange(Card)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
exports[`renders tombstoned when comment is deleted 1`] = `
|
||||
<withPropsOnChange(Card)
|
||||
<ForwardRef(forwardRef)
|
||||
className="ModerateCard-root ModerateCard-deleted"
|
||||
data-testid="moderate-comment-comment-id"
|
||||
id="moderate-comment-comment-id"
|
||||
onClick={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ForwardRef(forwardRef)>
|
||||
<div
|
||||
@@ -752,8 +785,10 @@ exports[`renders tombstoned when comment is deleted 1`] = `
|
||||
Theon
|
||||
</Username>
|
||||
</ForwardRef(forwardRef)>
|
||||
<Relay(CommentAuthorContainer)
|
||||
comment={Object {}}
|
||||
/>
|
||||
<Timestamp
|
||||
className="ModerateCard-timestamp"
|
||||
toggleAbsolute={true}
|
||||
>
|
||||
2018-11-29T16:01:51.897Z
|
||||
@@ -847,5 +882,5 @@ exports[`renders tombstoned when comment is deleted 1`] = `
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</ForwardRef(forwardRef)>
|
||||
</withPropsOnChange(Card)>
|
||||
</ForwardRef(forwardRef)>
|
||||
`;
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import BanAction, { BanActionProps } from "./BanAction";
|
||||
import PremodAction, { PremodActionProps } from "./PremodAction";
|
||||
import SuspensionAction, { SuspensionActionProps } from "./SuspensionAction";
|
||||
import UsernameChangeAction, {
|
||||
UsernameChangeActionProps,
|
||||
} from "./UsernameChangeAction";
|
||||
|
||||
export interface HistoryActionProps {
|
||||
kind: "username" | "suspension" | "ban";
|
||||
action: UsernameChangeActionProps | SuspensionActionProps | BanActionProps;
|
||||
kind: "username" | "suspension" | "ban" | "premod";
|
||||
action:
|
||||
| UsernameChangeActionProps
|
||||
| SuspensionActionProps
|
||||
| BanActionProps
|
||||
| PremodActionProps;
|
||||
}
|
||||
|
||||
const AccountHistoryAction: FunctionComponent<HistoryActionProps> = ({
|
||||
@@ -17,11 +22,15 @@ const AccountHistoryAction: FunctionComponent<HistoryActionProps> = ({
|
||||
}) => {
|
||||
switch (kind) {
|
||||
case "username":
|
||||
return <UsernameChangeAction {...action as UsernameChangeActionProps} />;
|
||||
return (
|
||||
<UsernameChangeAction {...(action as UsernameChangeActionProps)} />
|
||||
);
|
||||
case "suspension":
|
||||
return <SuspensionAction {...action as SuspensionActionProps} />;
|
||||
return <SuspensionAction {...(action as SuspensionActionProps)} />;
|
||||
case "ban":
|
||||
return <BanAction {...action as BanActionProps} />;
|
||||
return <BanAction {...(action as BanActionProps)} />;
|
||||
case "premod":
|
||||
return <PremodAction {...(action as PremodActionProps)} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { pick } from "lodash";
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { CreateModeratorNoteMutation as MutationTypes } from "coral-admin/__generated__/CreateModeratorNoteMutation.graphql";
|
||||
import { getViewer } from "coral-framework/helpers";
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
lookup,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLUser } from "coral-framework/schema";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const CreateModeratorNoteMutation = createMutation(
|
||||
"createModeratorNote",
|
||||
(
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes>,
|
||||
{ uuidGenerator }: CoralContext
|
||||
) => {
|
||||
const viewer = getViewer(environment)!;
|
||||
const notes =
|
||||
lookup<GQLUser>(environment, input.userID)!.moderatorNotes.map(note => {
|
||||
const createdBy = pick(note.createdBy, ["username", "id"]);
|
||||
return {
|
||||
...pick(note, ["id", "body", "createdAt"]),
|
||||
createdBy,
|
||||
};
|
||||
}) || [];
|
||||
const now = new Date();
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation CreateModeratorNoteMutation(
|
||||
$input: CreateModeratorNoteInput!
|
||||
) {
|
||||
createModeratorNote(input: $input) {
|
||||
user {
|
||||
moderatorNotes {
|
||||
id
|
||||
body
|
||||
createdBy {
|
||||
username
|
||||
id
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
createModeratorNote: {
|
||||
user: {
|
||||
id: input.userID,
|
||||
moderatorNotes: [
|
||||
{
|
||||
id: uuidGenerator(),
|
||||
body: input.body,
|
||||
createdAt: now.toISOString(),
|
||||
createdBy: {
|
||||
username: viewer.username,
|
||||
id: viewer.id,
|
||||
} as any,
|
||||
},
|
||||
...notes,
|
||||
],
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default CreateModeratorNoteMutation;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { pick } from "lodash";
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { DeleteModeratorNoteMutation as MutationTypes } from "coral-admin/__generated__/DeleteModeratorNoteMutation.graphql";
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
lookup,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { GQLUser } from "coral-framework/schema";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const DeleteModeratorNoteMutation = createMutation(
|
||||
"deleteModeratorNote",
|
||||
(
|
||||
environment: Environment,
|
||||
input: MutationInput<MutationTypes>,
|
||||
{ uuidGenerator }: CoralContext
|
||||
) => {
|
||||
const notes =
|
||||
lookup<GQLUser>(environment, input.userID)!.moderatorNotes.map(note => {
|
||||
const createdBy = pick(note.createdBy, ["username", "id"]);
|
||||
return {
|
||||
...pick(note, ["id", "body", "createdAt"]),
|
||||
createdBy,
|
||||
};
|
||||
}) || [];
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation DeleteModeratorNoteMutation(
|
||||
$input: DeleteModeratorNoteInput!
|
||||
) {
|
||||
deleteModeratorNote(input: $input) {
|
||||
user {
|
||||
moderatorNotes {
|
||||
id
|
||||
body
|
||||
createdBy {
|
||||
username
|
||||
id
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
deleteModeratorNote: {
|
||||
user: {
|
||||
id: input.userID,
|
||||
moderatorNotes: notes.filter(note => note.id !== input.id),
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default DeleteModeratorNoteMutation;
|
||||
@@ -0,0 +1,37 @@
|
||||
.root {
|
||||
|
||||
}
|
||||
|
||||
.body {
|
||||
background-color: #f2f2f2;
|
||||
border-radius: 4px;
|
||||
padding: var(--spacing-4);
|
||||
}
|
||||
|
||||
.bodyType {
|
||||
color: var(--palette-text-dark);
|
||||
}
|
||||
|
||||
.leftBy {
|
||||
padding-left: var(--spacing-4);
|
||||
padding-right: var(--spacing-1);
|
||||
position: relative;
|
||||
color: var(--palette-grey-main);
|
||||
}
|
||||
|
||||
.leftBy:before {
|
||||
content: "";
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: var(--palette-grey-main);
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
left: var(--spacing-1);
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-family: var(--font-family-serif);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--palette-grey-main);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
|
||||
import { Button, Flex, Icon, Timestamp, Typography } from "coral-ui/components";
|
||||
|
||||
import styles from "./ModeratorNote.css";
|
||||
|
||||
interface Props {
|
||||
body: string;
|
||||
moderator: string | null;
|
||||
createdAt: string;
|
||||
onDelete: ((id: string) => Promise<any>) | null;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ModeratorNote: FunctionComponent<Props> = ({
|
||||
moderator,
|
||||
createdAt,
|
||||
body,
|
||||
onDelete,
|
||||
id,
|
||||
}) => {
|
||||
const deleteNote = useCallback(() => {
|
||||
if (onDelete) {
|
||||
onDelete(id);
|
||||
}
|
||||
}, [id]);
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.body}>
|
||||
<Typography variant="bodyCopy" className={styles.bodyType}>
|
||||
{body}
|
||||
</Typography>
|
||||
</div>
|
||||
<Flex justifyContent="space-between">
|
||||
<Flex alignItems="center">
|
||||
<Timestamp>{createdAt}</Timestamp>
|
||||
{moderator && (
|
||||
<>
|
||||
<Localized id="moderatorNote-left-by">
|
||||
<Typography variant="timestamp" className={styles.leftBy}>
|
||||
Left by:
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Typography className={styles.username} variant="timestamp">
|
||||
{moderator}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
{onDelete && (
|
||||
<Localized id="moderatorNote-delete">
|
||||
<Button size="small" color="primary" onClick={deleteNote}>
|
||||
<Icon>delete</Icon>
|
||||
<span>Delete</span>
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModeratorNote;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
export interface PremodActionProps {
|
||||
action: "created" | "removed";
|
||||
}
|
||||
|
||||
const PremodAction: FunctionComponent<PremodActionProps> = ({ action }) =>
|
||||
action === "created" ? (
|
||||
<Localized id="moderate-user-drawer-account-history-premod-set">
|
||||
<span>Set always premoderate</span>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized id="moderate-user-drawer-account-history-premod-removed">
|
||||
<span>Removed always premoderate</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export default PremodAction;
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { GQLCOMMENT_STATUS } from "coral-framework/schema";
|
||||
|
||||
import { RecentHistoryContainer_settings } from "coral-admin/__generated__/RecentHistoryContainer_settings.graphql";
|
||||
import { RecentHistoryContainer_user } from "coral-admin/__generated__/RecentHistoryContainer_user.graphql";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { GQLCOMMENT_STATUS } from "coral-framework/schema";
|
||||
|
||||
import RecentHistory from "./RecentHistory";
|
||||
|
||||
const PUBLISHED_STATUSES = [GQLCOMMENT_STATUS.NONE, GQLCOMMENT_STATUS.APPROVED];
|
||||
|
||||
@@ -57,4 +57,12 @@
|
||||
.scrollable {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.redDot {
|
||||
background-color: var(--palette-error-main);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin-left: 2px;
|
||||
}
|
||||
@@ -2,21 +2,34 @@ import cn from "classnames";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
|
||||
import { Icon, Tab, TabBar, TabContent, TabPane } from "coral-ui/components";
|
||||
import {
|
||||
Flex,
|
||||
Icon,
|
||||
Tab,
|
||||
TabBar,
|
||||
TabContent,
|
||||
TabPane,
|
||||
} from "coral-ui/components";
|
||||
|
||||
import UserDrawerAccountHistoryQuery from "./UserDrawerAccountHistoryQuery";
|
||||
import UserDrawerNotesQuery from "./UserDrawerNotesQuery";
|
||||
import UserHistoryDrawerAllCommentsQuery from "./UserHistoryDrawerAllCommentsQuery";
|
||||
import UserHistoryDrawerRejectedCommentsQuery from "./UserHistoryDrawerRejectedCommentsQuery";
|
||||
|
||||
import styles from "./Tabs.css";
|
||||
|
||||
type UserTabs = "ALL_COMMENTS" | "REJECTED_COMMENTS" | "ACCOUNT_HISTORY";
|
||||
type UserTabs =
|
||||
| "ALL_COMMENTS"
|
||||
| "REJECTED_COMMENTS"
|
||||
| "ACCOUNT_HISTORY"
|
||||
| "NOTES";
|
||||
|
||||
interface Props {
|
||||
userID: string;
|
||||
notesCount: number;
|
||||
}
|
||||
|
||||
const UserHistoryTabs: FunctionComponent<Props> = ({ userID }) => {
|
||||
const UserHistoryTabs: FunctionComponent<Props> = ({ userID, notesCount }) => {
|
||||
const [currentTab, setCurrentTab] = useState<UserTabs>("ALL_COMMENTS");
|
||||
|
||||
const onTabChanged = useCallback(
|
||||
@@ -62,6 +75,23 @@ const UserHistoryTabs: FunctionComponent<Props> = ({ userID }) => {
|
||||
</Localized>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab tabID="NOTES" onTabClick={onTabChanged}>
|
||||
<div
|
||||
className={cn(styles.tab, {
|
||||
[styles.activeTab]: currentTab === "NOTES",
|
||||
})}
|
||||
>
|
||||
<Icon size="sm" className={styles.tabIcon}>
|
||||
subject
|
||||
</Icon>
|
||||
<Flex>
|
||||
<Localized id="moderate-user-drawer-tab-notes">
|
||||
<span>Notes</span>
|
||||
</Localized>
|
||||
{notesCount > 0 && <div className={styles.redDot} />}
|
||||
</Flex>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab tabID="ACCOUNT_HISTORY" onTabClick={onTabChanged}>
|
||||
<div
|
||||
className={cn(styles.tab, {
|
||||
@@ -99,6 +129,13 @@ const UserHistoryTabs: FunctionComponent<Props> = ({ userID }) => {
|
||||
</div>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tabID="NOTES">
|
||||
<div className={styles.container}>
|
||||
<div className={styles.scrollable}>
|
||||
<UserDrawerNotesQuery userID={userID} />
|
||||
</div>
|
||||
</div>
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { UserBadgesContainer_user as UserData } from "coral-admin/__generated__/UserBadgesContainer_user.graphql";
|
||||
import withFragmentContainer from "coral-framework/lib/relay/withFragmentContainer";
|
||||
|
||||
import CLASSES from "coral-stream/classes";
|
||||
import { Tag } from "coral-ui/components";
|
||||
|
||||
import { UserBadgesContainer_user as UserData } from "coral-admin/__generated__/UserBadgesContainer_user.graphql";
|
||||
|
||||
interface Props {
|
||||
user: UserData;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
|
||||
import { UserDrawerAccountHistory_user } from "coral-admin/__generated__/UserDrawerAccountHistory_user.graphql";
|
||||
|
||||
import { useCoralContext } from "coral-framework/lib/bootstrap";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
@@ -16,6 +14,8 @@ import {
|
||||
TableRow,
|
||||
} from "coral-ui/components";
|
||||
|
||||
import { UserDrawerAccountHistory_user } from "coral-admin/__generated__/UserDrawerAccountHistory_user.graphql";
|
||||
|
||||
import AccountHistoryAction, {
|
||||
HistoryActionProps,
|
||||
} from "./AccountHistoryAction";
|
||||
@@ -107,6 +107,18 @@ const UserDrawerAccountHistory: FunctionComponent<Props> = ({ user }) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Merge in all the premod history items.
|
||||
user.status.premod.history.forEach(record => {
|
||||
history.push({
|
||||
kind: "premod",
|
||||
action: {
|
||||
action: record.active ? "created" : "removed",
|
||||
},
|
||||
date: new Date(record.createdAt),
|
||||
takenBy: record.createdBy ? record.createdBy.username : system,
|
||||
});
|
||||
});
|
||||
|
||||
user.status.username.history.forEach((record, i) => {
|
||||
history.push({
|
||||
kind: "username",
|
||||
@@ -190,6 +202,15 @@ const enhanced = withFragmentContainer<any>({
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
premod {
|
||||
history {
|
||||
active
|
||||
createdBy {
|
||||
username
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
suspension {
|
||||
history {
|
||||
active
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { graphql, QueryRenderer } from "coral-framework/lib/relay";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { ReadyState } from "react-relay";
|
||||
|
||||
import { UserDrawerAccountHistoryQuery as QueryTypes } from "coral-admin/__generated__/UserDrawerAccountHistoryQuery.graphql";
|
||||
|
||||
import { graphql, QueryRenderer } from "coral-framework/lib/relay";
|
||||
import { CallOut, Spinner } from "coral-ui/components";
|
||||
|
||||
import { UserDrawerAccountHistoryQuery as QueryTypes } from "coral-admin/__generated__/UserDrawerAccountHistoryQuery.graphql";
|
||||
|
||||
import UserDrawerAccountHistory from "./UserDrawerAccountHistory";
|
||||
|
||||
import styles from "./UserDrawerAccountHistoryQuery.css";
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
.root {
|
||||
|
||||
}
|
||||
|
||||
.textArea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: calc(12 * var(--mini-unit));
|
||||
border-width: 1px;
|
||||
border-color: var(--palette-grey-main);
|
||||
padding: calc(0.5 * var(--mini-unit));
|
||||
border-radius: 2px;
|
||||
margin-bottom: var(--spacing-2);
|
||||
padding: var(--spacing-3);
|
||||
font-weight: var(--font-weight-regular);
|
||||
font-family: var(--font-family-sans-serif);
|
||||
font-size: calc(16rem / var(--rem-base));
|
||||
line-height: 1;
|
||||
letter-spacing: calc(0.2em / 16);
|
||||
color: var(--palette-text-primary);
|
||||
}
|
||||
|
||||
.textArea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form {
|
||||
padding: var(--spacing-2) 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
|
||||
import { UserDrawerNotesContainer_user as UserData } from "coral-admin/__generated__/UserDrawerNotesContainer_user.graphql";
|
||||
import { UserDrawerNotesContainer_viewer as ViewerData } from "coral-admin/__generated__/UserDrawerNotesContainer_viewer.graphql";
|
||||
import {
|
||||
graphql,
|
||||
useMutation,
|
||||
withFragmentContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { required } from "coral-framework/lib/validation";
|
||||
import { Button, Flex, HorizontalGutter } from "coral-ui/components";
|
||||
import { FormApi } from "final-form";
|
||||
import { Field, Form } from "react-final-form";
|
||||
import CreateModeratorNoteMutation from "./CreateModeratorNoteMutation";
|
||||
import DeleteModeratorNoteMutation from "./DeleteModeratorNoteMutation";
|
||||
import ModeratorNote from "./ModeratorNote";
|
||||
|
||||
import styles from "./UserDrawerNotesContainer.css";
|
||||
|
||||
interface Props {
|
||||
user: UserData;
|
||||
viewer: ViewerData | null;
|
||||
}
|
||||
|
||||
const UserDrawerNotesContainer: FunctionComponent<Props> = ({
|
||||
user,
|
||||
viewer,
|
||||
}) => {
|
||||
const createNote = useMutation(CreateModeratorNoteMutation);
|
||||
const deleteNote = useMutation(DeleteModeratorNoteMutation);
|
||||
const onDelete = useCallback(
|
||||
(id: string) => {
|
||||
return deleteNote({
|
||||
id,
|
||||
userID: user.id,
|
||||
});
|
||||
},
|
||||
[user]
|
||||
);
|
||||
const onSubmit = useCallback(
|
||||
async ({ body }, form: FormApi) => {
|
||||
await createNote({
|
||||
userID: user.id,
|
||||
body,
|
||||
});
|
||||
form.reset();
|
||||
},
|
||||
[user]
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<Form onSubmit={onSubmit}>
|
||||
{({ handleSubmit, submitError, invalid, submitting, ...formProps }) => (
|
||||
<form
|
||||
className={styles.form}
|
||||
onSubmit={handleSubmit}
|
||||
data-testid="userdrawer-notes-form"
|
||||
>
|
||||
<Localized id="moderate-user-drawer-notes-field">
|
||||
<Field
|
||||
className={styles.textArea}
|
||||
id="suspendModal-message"
|
||||
component="textarea"
|
||||
name="body"
|
||||
validate={required}
|
||||
placeholder="Leave a note..."
|
||||
/>
|
||||
</Localized>
|
||||
<Flex justifyContent="flex-end">
|
||||
<Localized id="moderate-user-drawer-notes-button">
|
||||
<Button variant="filled" color="primary" type="submit">
|
||||
Add note
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
<HorizontalGutter size="double">
|
||||
{user.moderatorNotes &&
|
||||
user.moderatorNotes
|
||||
.concat()
|
||||
.reverse()
|
||||
.map(
|
||||
note =>
|
||||
note && (
|
||||
<ModeratorNote
|
||||
key={note.id}
|
||||
id={note.id}
|
||||
body={note.body}
|
||||
moderator={note.createdBy.username}
|
||||
createdAt={note.createdAt}
|
||||
onDelete={
|
||||
viewer && viewer.id === note.createdBy.id
|
||||
? onDelete
|
||||
: null
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
user: graphql`
|
||||
fragment UserDrawerNotesContainer_user on User {
|
||||
id
|
||||
moderatorNotes {
|
||||
id
|
||||
body
|
||||
createdAt
|
||||
createdBy {
|
||||
username
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
viewer: graphql`
|
||||
fragment UserDrawerNotesContainer_viewer on User {
|
||||
id
|
||||
}
|
||||
`,
|
||||
})(UserDrawerNotesContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,14 @@
|
||||
.root {
|
||||
|
||||
}
|
||||
|
||||
.spinner {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.callout {
|
||||
width: 100%;
|
||||
font-family: var(--font-family-sans-serif);
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { graphql, QueryRenderer } from "coral-framework/lib/relay";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { ReadyState } from "react-relay";
|
||||
|
||||
import { UserDrawerNotesQuery as QueryTypes } from "coral-admin/__generated__/UserDrawerNotesQuery.graphql";
|
||||
|
||||
import { CallOut, Spinner } from "coral-ui/components";
|
||||
|
||||
import UserDrawerNotesContainer from "./UserDrawerNotesContainer";
|
||||
|
||||
import styles from "./UserDrawerNotesQuery.css";
|
||||
|
||||
interface Props {
|
||||
userID: string;
|
||||
}
|
||||
|
||||
const UserDrawerNotesQuery: FunctionComponent<Props> = ({ userID }) => {
|
||||
return (
|
||||
<QueryRenderer<QueryTypes>
|
||||
query={graphql`
|
||||
query UserDrawerNotesQuery($userID: ID!) {
|
||||
user(id: $userID) {
|
||||
...UserDrawerNotesContainer_user
|
||||
}
|
||||
viewer {
|
||||
...UserDrawerNotesContainer_viewer
|
||||
}
|
||||
}
|
||||
`}
|
||||
variables={{ userID }}
|
||||
cacheConfig={{ force: true }}
|
||||
render={({ error, props }: ReadyState<QueryTypes["response"]>) => {
|
||||
if (error) {
|
||||
return (
|
||||
<div className={styles.callout}>
|
||||
<CallOut>{error.message}</CallOut>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!props) {
|
||||
return (
|
||||
<div className={styles.spinner}>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!props.user) {
|
||||
return (
|
||||
<div className={styles.callout}>
|
||||
<CallOut>
|
||||
<Localized id="moderate-user-drawer-user-not-found ">
|
||||
User not found.
|
||||
</Localized>
|
||||
</CallOut>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<UserDrawerNotesContainer user={props.user} viewer={props.viewer} />
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserDrawerNotesQuery;
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import { ModerateCardContainer } from "coral-admin/components/ModerateCard";
|
||||
import {
|
||||
useLoadMore,
|
||||
withPaginationContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
import { Button, CallOut, Typography } from "coral-ui/components";
|
||||
|
||||
import { UserHistoryDrawerAllComments_settings } from "coral-admin/__generated__/UserHistoryDrawerAllComments_settings.graphql";
|
||||
import { UserHistoryDrawerAllComments_user } from "coral-admin/__generated__/UserHistoryDrawerAllComments_user.graphql";
|
||||
import { UserHistoryDrawerAllComments_viewer } from "coral-admin/__generated__/UserHistoryDrawerAllComments_viewer.graphql";
|
||||
import { UserHistoryDrawerAllCommentsPaginationQueryVariables } from "coral-admin/__generated__/UserHistoryDrawerAllCommentsPaginationQuery.graphql";
|
||||
|
||||
import { ModerateCardContainer } from "coral-admin/components/ModerateCard";
|
||||
import { Button, CallOut, Typography } from "coral-ui/components";
|
||||
|
||||
import styles from "./UserHistoryDrawerAllComments.css";
|
||||
|
||||
interface Props {
|
||||
|
||||