Compare commits

..
1 Commits
Author SHA1 Message Date
Wyatt Johnson 4dbcba2849 fix: fixed issue with directive resolver 2019-10-25 09:06:00 -06:00
2759 changed files with 72292 additions and 146649 deletions
-1
View File
@@ -1 +0,0 @@
src/docs/architecture/decisions
+13 -19
View File
@@ -1,19 +1,19 @@
# job_environment will setup the environment for any job being executed.
job_environment: &job_environment
NODE_ENV: "test"
WEBPACK_MAX_CORES: "4"
NODE_OPTIONS: "--max-old-space-size=8192"
NODE_ENV: test
WEBPACK_MAX_CORES: 4
NODE_OPTIONS: --max-old-space-size=8192
# job_defaults applies all the defaults for each job.
job_defaults: &job_defaults
working_directory: ~/coralproject/talk
resource_class: large
docker:
- image: circleci/node:12
- image: circleci/node:10
environment:
<<: *job_environment
version: 2.1
version: 2
jobs:
# npm_dependencies will install the dependencies used by all other steps.
npm_dependencies:
@@ -49,8 +49,7 @@ jobs:
- ~/.npm
- persist_to_workspace:
root: .
paths:
- node_modules
paths: node_modules
# lint will perform file linting.
lint:
@@ -66,20 +65,18 @@ jobs:
name: Lint Source Code
command: npm run lint
- run:
name: Lint Markdown
name: Lint README.md
command: |
cp README.md README.md.orig
npm run doctoc
git diff --exit-code
- run:
name: Lint Versions
command: npx @coralproject/package-version-lint
diff -q README.md README.md.orig
# unit_tests will run the unit tests.
unit_tests:
<<: *job_defaults
environment:
<<: *job_environment
CI: "true"
CI: true
JEST_JUNIT_OUTPUT: "reports/junit/js-test-results.xml"
steps:
- checkout
@@ -116,15 +113,14 @@ jobs:
no_output_timeout: 30m
- run:
name: Verify Bundle Size
command: npx bundlesize2 --enable-github-checks
command: npx bundlesize
- save_cache:
key: v1-build-cache-{{ .Branch }}-{{ .Revision }}
paths:
- ./dist
- persist_to_workspace:
root: .
paths:
- dist
paths: dist
# docker_tests will test that the docker build process completes.
docker_tests:
@@ -146,9 +142,6 @@ jobs:
<<: *job_defaults
steps:
- checkout
- run:
name: Verify release version
command: npx @coralproject/package-version-lint --expect ${CIRCLE_TAG/#v}
- setup_remote_docker
- deploy:
name: Deploy the code
@@ -175,6 +168,7 @@ filter_develop: &filter_develop
- next
workflows:
version: 2
build-test:
jobs:
- docker_tests:
+1
View File
@@ -27,3 +27,4 @@ yarn.lock
dist
*.css.d.ts
__generated__
README.md.orig
+51 -85
View File
@@ -1,11 +1,10 @@
const typescriptEslintRecommended = require("@typescript-eslint/eslint-plugin/dist/configs/eslint-recommended")
.overrides[0];
const typescriptRecommended = require("@typescript-eslint/eslint-plugin/dist/configs/recommended.js");
const typescriptRecommendedTypeChecking = require("@typescript-eslint/eslint-plugin/dist/configs/recommended-requiring-type-checking.js");
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 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"],
@@ -21,12 +20,11 @@ const typescriptOverrides = {
"@typescript-eslint/tslint",
"react",
"jsx-a11y",
"react-hooks",
],
settings: {
react: {
version: "detect",
},
}
},
rules: Object.assign(
typescriptEslintRecommended.rules,
@@ -38,45 +36,32 @@ const typescriptOverrides = {
{
"@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",
{
types: {
"{}": false,
object: false,
extendDefaults: true,
},
},
],
"@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-module-boundary-types": "off",
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
overrides: {
constructors: "off",
"overrides": {
"constructors": "off",
},
},
],
"@typescript-eslint/indent": "off",
"@typescript-eslint/interface-name-prefix": "error",
"@typescript-eslint/member-delimiter-style": "off",
"@typescript-eslint/no-empty-function": "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-unused-vars": [
"error",
{ args: "none", ignoreRestSiblings: true },
],
"@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",
@@ -92,59 +77,36 @@ const typescriptOverrides = {
"react/display-name": "error",
"react/prop-types": "off",
"react/no-unescaped-entities": "off",
"no-empty-function": "off",
// (tessalt) disabled because video elements are only used to display gifs, which have no audio
"jsx-a11y/media-has-caption": "off",
}
),
};
const jestTypeCheckingOverrides = {
files: ["test/**/*.ts", "test/**/*.tsx"],
rules: {
"@typescript-eslint/no-floating-promises": "off",
},
};
const typescriptTypeCheckingOverrides = {
let typescriptTypeCheckingOverrides = {
files: ["*.ts", "*.tsx"],
parserOptions: {
project: [
"./tsconfig.json",
"./src/tsconfig.json",
"./src/core/client/tsconfig.json",
],
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: {
rules: Object.assign(
typescriptRecommendedTypeChecking.rules,
{
"@typescript-eslint/tslint/config": ["error", {
"rules": {
"ordered-imports": {
options: {
// Legacy sorting until this is fixed: https://github.com/SoominHan/import-sorter/issues/60
"import-sources-order": "case-insensitive-legacy",
"options": {
"import-sources-order": "case-insensitive",
"module-source-path": "full",
"named-imports-order": "case-insensitive-legacy",
"named-imports-order": "case-insensitive",
},
},
},
},
],
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-return": "off",
// 28.11.19: (cvle) Disabled because behavior of regexp.exec seems different than str.match?
"@typescript-eslint/prefer-regexp-exec": "off",
"@typescript-eslint/require-await": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/unbound-method": "off", // 10.10.19: (cvle) seems to give false positive.
}),
overrides: [jestTypeCheckingOverrides],
}],
"@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 = {
@@ -153,8 +115,8 @@ const jestOverrides = {
},
files: ["test/**/*.ts", "test/**/*.tsx"],
globals: {
expectAndFail: "readonly",
fail: "readonly",
"expectAndFail": "readonly",
"fail": "readonly",
},
};
@@ -176,31 +138,35 @@ module.exports = {
"eslint:recommended",
"plugin:jsdoc/recommended",
"plugin:prettier/recommended",
"plugin:react-hooks/recommended",
],
parserOptions: {
ecmaVersion: 2018,
"ecmaVersion": 2018,
},
rules: {
"arrow-body-style": "off",
"arrow-parens": ["off", "as-needed"],
camelcase: "off",
complexity: "off",
"arrow-parens": [
"off",
"as-needed",
],
"camelcase": "off",
"complexity": "off",
"constructor-super": "error",
"spaced-comment": ["error", "always"],
curly: "error",
"curly": "error",
"dot-notation": "error",
"eol-last": "off",
eqeqeq: "error",
"eqeqeq": "error",
"guard-for-in": "error",
"jsdoc/check-param-names": "off",
"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],
"max-classes-per-file": [
"error",
1,
],
"member-ordering": "off",
"new-parens": "off",
"newline-per-chained-call": "off",
@@ -217,7 +183,7 @@ module.exports = {
"no-irregular-whitespace": "off",
"no-multiple-empty-lines": "off",
"no-new-wrappers": "error",
"no-prototype-builtins": "error",
"no-prototype-builtins": "off",
"no-shadow": "error",
"no-throw-literal": "error",
"no-undef": "off",
@@ -225,14 +191,14 @@ module.exports = {
"no-unsafe-finally": "error",
"no-unused-expressions": "error",
"no-unused-labels": "error",
"no-unused-vars": ["error", { args: "none", ignoreRestSiblings: true }],
"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",
"radix": "error",
"require-atomic-updates": "off",
"space-before-function-paren": "off",
"sort-imports": "off",
+4 -6
View File
@@ -1,17 +1,15 @@
---
name: Bug report
about: Create a bug report to help us improve Coral
about: Create a report to help us improve
labels: "bug"
---
<!--
Please note that but submitting a bug report, you agree to our Code of Conduct: http://code-of-conduct.voxmedia.com/
When submitting a bug report, provide as much detail as possible, including a screenshot or copy-paste of any related error messages, logs, or other output that might be related. Places to look for information include your browser console, server console, and network logs. The more information you can give the better.
Please provide as much detail as possible, including a screenshot or copy-paste of any related error messages, logs, or other output that might be related. Places to look for information include your browser console, server console, and network logs. The more information you can give the better.
Please help us by doing the following steps before logging an issue:
* Search for other similar issues: https://github.com/coralproject/talk/search
* Read our docs: https://docs.coralproject.net
* Search: https://github.com/coralproject/talk/search
* Read the docs: https://docs.coralproject.net
Please fill in the *entire* template below.
-->
+2 -4
View File
@@ -1,15 +1,13 @@
---
name: Feature request
about: Suggest a feature idea for Coral
about: Suggest an idea for this project
labels: "feature idea"
---
<!--
Please note that but submitting a feature request, you agree to our Code of Conduct: http://code-of-conduct.voxmedia.com/
Please help us by doing the following steps before logging an issue:
* Search to see if there are similar requests and ideas that are already logged: https://github.com/coralproject/talk/search
* Search: https://github.com/coralproject/talk/search
-->
+3 -41
View File
@@ -1,48 +1,10 @@
<!--
Thank you for submitting a pull request!
Thank you for submitting a pull request! Please note that by contributing to
Coral, you agree to our Code of Conduct: http://code-of-conduct.voxmedia.com/
Before submitting your Pull Request (or PR), please verify that:
* [ ] Your code is up-to-date with the base branch
Please verify that:
* [ ] Code is up-to-date with the base branch
* [ ] You've successfully run `npm run test` locally
Refer to CONTRIBUTING.MD for more details.
https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md
-->
## What does this PR do?
<!--
In this section, you should be describing what other Github issues or tickets
that this PR is designed to addressed.
Any related Github issue should be linked by adding its URL to this section.
-->
## What changes to the GraphQL/Database Schema does this PR introduce?
<!--
In this section, you should describe any changes to be made to the GraphQL
schema file (located https://github.com/coralproject/talk/blob/master/src/core/server/graph/schema/schema.graphql) or any
database model (located as types in the https://github.com/coralproject/talk/blob/master/src/core/server/models directory).
If no changes were added to the GraphQL/Database Schema as a part of this PR,
simply write "None".
-->
## How do I test this PR?
<!--
In this section, you should be describing any manual testing that can be used to
verify features introduced or bugs fixed in this PR.
-->
+1
View File
@@ -20,4 +20,5 @@ yarn.lock
dist
*.css.d.ts
__generated__
README.md.orig
persisted-queries.json
+1 -1
View File
@@ -1,7 +1,7 @@
{
"projects": {
"tenant": {
"schemaPath": "src/core/server/graph/schema/schema.graphql"
"schemaPath": "src/core/server/graph/tenant/schema/schema.graphql"
}
}
}
-44
View File
@@ -1,44 +0,0 @@
# .kodiak.toml
# Kodiak's configuration file should be placed at `.kodiak.toml` (repository
# root) or `.github/.kodiak.toml`.
# docs: https://kodiakhq.com/docs/config-reference
# version is the only required setting in a kodiak config.
# `1` is the only valid setting for this field.
version = 1
[merge]
# Label to enable Kodiak to merge a PR.
# By default, Kodiak will only act on PRs that have this label. You can disable
# this requirement via `merge.require_automerge_label`.
automerge_label = "🚀 merge it!" # default: "automerge"
# Kodiak will not merge a PR with any of these labels.
blocking_labels = ["don't merge"] # default: [], options: list of label names (e.g. ["wip"])
# Choose merge method for Kodiak to use.
#
# Kodiak will report a configuration error if the selected merge method is
# disabled for a repository.
#
# If you're using the "Require signed commits" GitHub Branch Protection setting
# to require commit signatures, _`"merge"` is the only compatible option_. Any
# other option will cause Kodiak to raise a configuration error.
method = "squash" # default: "merge", options: "merge", "squash", "rebase"
# Once a PR is merged, delete the branch. This option behaves like the GitHub
# repository setting "Automatically delete head branches", which automatically
# deletes head branches after pull requests are merged.
delete_branch_on_merge = true # default: false
# Don't wait for in-progress status checks on a PR to finish before updating the
# branch.
optimistic_updates = false # default: true
[merge.message]
# Strip HTML comments (`<!-- some HTML comment -->`) from merge commit body.
# This setting is useful for stripping HTML comments created by PR templates.
# This option only applies when `merge.message.body_type = "markdown"`.
strip_html_comments = true # default: false
+2 -1
View File
@@ -1 +1,2 @@
12
10
+3
View File
@@ -0,0 +1,3 @@
{
"trailingComma": "es5"
}
+9 -10
View File
@@ -12,10 +12,10 @@
},
"tslint.enable": false,
"eslint.validate": [
{ "language": "javascript", "autoFix": true },
{ "language": "typescript", "autoFix": true },
{ "language": "typescriptreact", "autoFix": true }
],
{ "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",
@@ -44,15 +44,14 @@
{
"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__"],
"search.exclude": {
"package-lock.json": true
},
"debug.node.autoAttach": "off"
"importSorter.generalConfiguration.exclude": [
"d\\.ts$",
"__generated__"
],
}
-622
View File
@@ -1,622 +0,0 @@
# Client Events Guide
This serves as a guide to events emitted by the javascript via the embed events
hook, as described below in [Viewer Events](#viewer-events).
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table of Contents
- [Viewer Events](#viewer-events)
- [Viewer Network Events](#viewer-network-events)
- [Event List](#event-list)
- [Index](#index)
- [Events](#events)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Viewer Events
_Viewer Events_ are emitted when the viewer performs certain actions.
They can be subscribed to using the `events` parameter in
`Coral.createStreamEmbed`.
```html
<script>
const CoralStreamEmbed = Coral.createStreamEmbed({
events: function(events) {
events.onAny(function(eventName, data) {
console.log(eventName, data);
});
},
});
</script>
```
Example events:
- `setMainTab {tab: "PROFILE"}`
- `showFeaturedCommentTooltip`
- `viewConversation {from: "FEATURED_COMMENTS", commentID: "c45fb5f5-03f9-49a3-a755-488c698ca0df"}`
### Viewer Network Events
_Viewer Network Events_ are events that involves a network request and thus can succeed or fail. Succeeding events will have a `.success` appended to the event name while failing events have an `.error` appended to the event name.
Moreover _Viewer Network Events_ contains the `rtt` field which indicates the time it needed from initiating the request until the _UI_ has been updated with the response data.
Example events:
```
createComment.success
{
body: "Hello world!",
storyID: "238b95ec-2b80-43f4-ab68-a6ea1f4e2584",
rtt: 307,
success: {
id: "6fecfb11-4d0f-4edc-89b7-878a9928addd"
status: "APPROVED"`
}
}
```
```
createComment.error
{
body: "Hi!",
storyID: "238b95ec-2b80-43f4-ab68-a6ea1f4e2584",
rtt: 229,
error: {
code: "COMMENT_BODY_TOO_SHORT"
message: "Comment body must have at least 10 characters."
}
}
```
## Event List
<!-- START docs:events -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN npm run docs:events -->
### Index
- <a href="#approveComment">approveComment</a>
- <a href="#banUser">banUser</a>
- <a href="#cancelAccountDeletion">cancelAccountDeletion</a>
- <a href="#changeEmail">changeEmail</a>
- <a href="#changePassword">changePassword</a>
- <a href="#changeUsername">changeUsername</a>
- <a href="#closeStory">closeStory</a>
- <a href="#copyPermalink">copyPermalink</a>
- <a href="#createComment">createComment</a>
- <a href="#createCommentFocus">createCommentFocus</a>
- <a href="#createCommentReaction">createCommentReaction</a>
- <a href="#createCommentReply">createCommentReply</a>
- <a href="#editComment">editComment</a>
- <a href="#featureComment">featureComment</a>
- <a href="#gotoModeration">gotoModeration</a>
- <a href="#ignoreUser">ignoreUser</a>
- <a href="#loadMoreAllComments">loadMoreAllComments</a>
- <a href="#loadMoreFeaturedComments">loadMoreFeaturedComments</a>
- <a href="#loadMoreHistoryComments">loadMoreHistoryComments</a>
- <a href="#loginPrompt">loginPrompt</a>
- <a href="#openSortMenu">openSortMenu</a>
- <a href="#openStory">openStory</a>
- <a href="#rejectComment">rejectComment</a>
- <a href="#removeCommentReaction">removeCommentReaction</a>
- <a href="#removeUserIgnore">removeUserIgnore</a>
- <a href="#replyCommentFocus">replyCommentFocus</a>
- <a href="#reportComment">reportComment</a>
- <a href="#requestAccountDeletion">requestAccountDeletion</a>
- <a href="#requestDownloadCommentHistory">requestDownloadCommentHistory</a>
- <a href="#resendEmailVerification">resendEmailVerification</a>
- <a href="#setCommentsOrderBy">setCommentsOrderBy</a>
- <a href="#setCommentsTab">setCommentsTab</a>
- <a href="#setMainTab">setMainTab</a>
- <a href="#setProfileTab">setProfileTab</a>
- <a href="#showAbsoluteTimestamp">showAbsoluteTimestamp</a>
- <a href="#showAllReplies">showAllReplies</a>
- <a href="#showAuthPopup">showAuthPopup</a>
- <a href="#showEditEmailDialog">showEditEmailDialog</a>
- <a href="#showEditForm">showEditForm</a>
- <a href="#showEditPasswordDialog">showEditPasswordDialog</a>
- <a href="#showEditUsernameDialog">showEditUsernameDialog</a>
- <a href="#showFeaturedCommentTooltip">showFeaturedCommentTooltip</a>
- <a href="#showIgnoreUserdDialog">showIgnoreUserdDialog</a>
- <a href="#showModerationPopover">showModerationPopover</a>
- <a href="#showMoreOfConversation">showMoreOfConversation</a>
- <a href="#showMoreReplies">showMoreReplies</a>
- <a href="#showReplyForm">showReplyForm</a>
- <a href="#showReportPopover">showReportPopover</a>
- <a href="#showSharePopover">showSharePopover</a>
- <a href="#showUserPopover">showUserPopover</a>
- <a href="#signOut">signOut</a>
- <a href="#signedIn">signedIn</a>
- <a href="#unfeatureComment">unfeatureComment</a>
- <a href="#updateNotificationSettings">updateNotificationSettings</a>
- <a href="#updateStorySettings">updateStorySettings</a>
- <a href="#updateUserMediaSettings">updateUserMediaSettings</a>
- <a href="#viewConversation">viewConversation</a>
- <a href="#viewFullDiscussion">viewFullDiscussion</a>
- <a href="#viewNewComments">viewNewComments</a>
### Events
- <a id="approveComment">**approveComment.success**, **approveComment.error**</a>: This event is emitted when the viewer approves a comment.
```ts
{
commentID: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="banUser">**banUser.success**, **banUser.error**</a>: This event is emitted when the viewer bans a user.
```ts
{
userID: string;
commentID: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="cancelAccountDeletion">**cancelAccountDeletion.success**, **cancelAccountDeletion.error**</a>: This event is emitted when the viewer cancels the account deletion.
```ts
{
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="changeEmail">**changeEmail.success**, **changeEmail.error**</a>: This event is emitted when the viewer changes its email.
```ts
{
oldEmail: string;
newEmail: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="changePassword">**changePassword.success**, **changePassword.error**</a>: This event is emitted when the viewer changes its password.
```ts
{
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="changeUsername">**changeUsername.success**, **changeUsername.error**</a>: This event is emitted when the viewer changes its username.
```ts
{
oldUsername: string;
newUsername: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="closeStory">**closeStory.success**, **closeStory.error**</a>: This event is emitted when the viewer closes the story.
```ts
{
storyID: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="copyPermalink">**copyPermalink**</a>: This event is emitted when the viewer copies the permalink with the button.
```ts
{
commentID: string;
}
```
- <a id="createComment">**createComment.success**, **createComment.error**</a>: This event is emitted when a top level comment is created.
```ts
{
storyID: string;
body: string;
success: {
id: string;
status: COMMENT_STATUS;
};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="createCommentFocus">**createCommentFocus**</a>: This event is emitted when the viewer focus on the RTE to create a comment.
- <a id="createCommentReaction">**createCommentReaction.success**, **createCommentReaction.error**</a>: This event is emitted when the viewer reacts to a comment.
```ts
{
commentID: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="createCommentReply">**createCommentReply.success**, **createCommentReply.error**</a>: This event is emitted when a comment reply is created.
```ts
{
body: string;
parentID: string;
success: {
id: string;
status: COMMENT_STATUS;
};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="editComment">**editComment.success**, **editComment.error**</a>: This event is emitted when the viewer edits a comment.
```ts
{
body: string;
commentID: string;
success: {
status: COMMENT_STATUS;
};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="featureComment">**featureComment.success**, **featureComment.error**</a>: This event is emitted when the viewer features a comment.
```ts
{
commentID: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="gotoModeration">**gotoModeration**</a>: This event is emitted when the viewer goes to moderation.
```ts
{
commentID: string;
}
```
- <a id="ignoreUser">**ignoreUser.success**, **ignoreUser.error**</a>: This event is emitted when the viewer ignores a user.
```ts
{
userID: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="loadMoreAllComments">**loadMoreAllComments.success**, **loadMoreAllComments.error**</a>: This event is emitted when the viewer loads more top level comments into the comment stream.
```ts
{
storyID: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="loadMoreFeaturedComments">**loadMoreFeaturedComments.success**, **loadMoreFeaturedComments.error**</a>: This event is emitted when the viewer loads more featured comments.
```ts
{
storyID: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="loadMoreHistoryComments">**loadMoreHistoryComments.success**, **loadMoreHistoryComments.error**</a>: This event is emitted when the viewer loads more top level comments into the history comment stream.
```ts
{
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="loginPrompt">**loginPrompt**</a>: This event is emitted when the viewer does an action that will prompt a login dialog.
- <a id="openSortMenu">**openSortMenu**</a>: This event is emitted when the viewer clicks on the sort menu.
- <a id="openStory">**openStory.success**, **openStory.error**</a>: This event is emitted when the viewer opens the story.
```ts
{
storyID: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="rejectComment">**rejectComment.success**, **rejectComment.error**</a>: This event is emitted when the viewer rejects a comment.
```ts
{
commentID: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="removeCommentReaction">**removeCommentReaction.success**, **removeCommentReaction.error**</a>: This event is emitted when the viewer removes its reaction from a comment.
```ts
{
commentID: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="removeUserIgnore">**removeUserIgnore.success**, **removeUserIgnore.error**</a>: This event is emitted when the viewer remove a user from its ignored users list.
```ts
{
userID: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="replyCommentFocus">**replyCommentFocus**</a>: This event is emitted when the viewer focus on the RTE to reply to a comment.
- <a id="reportComment">**reportComment.success**, **reportComment.error**</a>: This event is emitted when the viewer reports a comment.
```ts
{
reason: string;
commentID: string;
additionalDetails?: string | undefined;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="requestAccountDeletion">**requestAccountDeletion.success**, **requestAccountDeletion.error**</a>: This event is emitted when the viewer requests to delete its account.
```ts
{
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="requestDownloadCommentHistory">**requestDownloadCommentHistory.success**, **requestDownloadCommentHistory.error**</a>: This event is emitted when the viewer requests to download its comment history.
```ts
{
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="resendEmailVerification">**resendEmailVerification.success**, **resendEmailVerification.error**</a>: This event is emitted when the viewer request another email verification email.
```ts
{
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="setCommentsOrderBy">**setCommentsOrderBy**</a>: This event is emitted when the viewer changes the sort order of the comments.
```ts
{
orderBy: string;
}
```
- <a id="setCommentsTab">**setCommentsTab**</a>: This event is emitted when the viewer changes the tab of the comments tab bar.
```ts
{
tab: string;
}
```
- <a id="setMainTab">**setMainTab**</a>: This event is emitted when the viewer changes the tab of the main tab bar.
```ts
{
tab: string;
}
```
- <a id="setProfileTab">**setProfileTab**</a>: This event is emitted when the viewer changes the tab of the profile tab bar.
```ts
{
tab: string;
}
```
- <a id="showAbsoluteTimestamp">**showAbsoluteTimestamp**</a>: This event is emitted when the viewer clicks on the relative timestamp to show the absolute time.
- <a id="showAllReplies">**showAllReplies.success**, **showAllReplies.error**</a>: This event is emitted when the viewer reveals all replies of a comment.
```ts
{
commentID: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="showAuthPopup">**showAuthPopup**</a>: This event is emitted when the viewer requests the auth popup.
```ts
{
view: string;
}
```
- <a id="showEditEmailDialog">**showEditEmailDialog**</a>: This event is emitted when the viewer opens the edit email dialog.
- <a id="showEditForm">**showEditForm**</a>: This event is emitted when the viewer opens the edit form.
```ts
{
commentID: string;
}
```
- <a id="showEditPasswordDialog">**showEditPasswordDialog**</a>: This event is emitted when the viewer opens the edit password dialog.
- <a id="showEditUsernameDialog">**showEditUsernameDialog**</a>: This event is emitted when the viewer opens the edit username dialog.
- <a id="showFeaturedCommentTooltip">**showFeaturedCommentTooltip**</a>: This event is emitted when the viewer clicks to show the featured comment tooltip.
- <a id="showIgnoreUserdDialog">**showIgnoreUserdDialog**</a>: This event is emitted when the viewer opens the ignore user dialog.
- <a id="showModerationPopover">**showModerationPopover**</a>: This event is emitted when the viewer opens the moderation popover.
```ts
{
commentID: string;
}
```
- <a id="showMoreOfConversation">**showMoreOfConversation.success**, **showMoreOfConversation.error**</a>: This event is emitted when the viewer reveals more of the parent conversation thread.
```ts
{
commentID: string | null;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="showMoreReplies">**showMoreReplies**</a>: This event is emitted when the viewer reveals new live replies.
```ts
{
commentID: string;
count: number;
}
```
- <a id="showReplyForm">**showReplyForm**</a>: This event is emitted when the viewer opens the reply form.
```ts
{
commentID: string;
}
```
- <a id="showReportPopover">**showReportPopover**</a>: This event is emitted when the viewer opens the report popover.
```ts
{
commentID: string;
}
```
- <a id="showSharePopover">**showSharePopover**</a>: This event is emitted when the viewer opens the share popover.
```ts
{
commentID: string;
}
```
- <a id="showUserPopover">**showUserPopover**</a>: This event is emitted when the viewer clicks on a username which shows the user popover.
```ts
{
userID: string;
}
```
- <a id="signOut">**signOut.success**, **signOut.error**</a>: This event is emitted when the viewer signs out.
```ts
{
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="signedIn">**signedIn**</a>: This event is emitted when the viewer signed in (not applicable for SSO).
- <a id="unfeatureComment">**unfeatureComment.success**, **unfeatureComment.error**</a>: This event is emitted when the viewer unfeatures a comment.
```ts
{
commentID: string;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="updateNotificationSettings">**updateNotificationSettings.success**, **updateNotificationSettings.error**</a>: This event is emitted when the viewer updates its notification settings.
```ts
{
onReply?: boolean | null | undefined;
onFeatured?: boolean | null | undefined;
onStaffReplies?: boolean | null | undefined;
onModeration?: boolean | null | undefined;
digestFrequency?: "NONE" | "DAILY" | "HOURLY" | null | undefined;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="updateStorySettings">**updateStorySettings.success**, **updateStorySettings.error**</a>: This event is emitted when the viewer updates the story settings.
```ts
{
storyID: string;
live?: {
enabled?: boolean | null | undefined;
} | null | undefined;
moderation?: "POST" | "PRE" | null | undefined;
premodLinksEnable?: boolean | null | undefined;
messageBox?: {
enabled?: boolean | null | undefined;
icon?: string | null | undefined;
content?: string | null | undefined;
} | null | undefined;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="updateUserMediaSettings">**updateUserMediaSettings.success**, **updateUserMediaSettings.error**</a>:
```ts
{
unfurlEmbeds?: boolean | null | undefined;
success: {};
error: {
message: string;
code?: string | undefined;
};
}
```
- <a id="viewConversation">**viewConversation**</a>: This event is emitted when the viewer changes to the single conversation view.
```ts
{
from: "FEATURED_COMMENTS" | "COMMENT_STREAM" | "COMMENT_HISTORY";
commentID: string;
}
```
- <a id="viewFullDiscussion">**viewFullDiscussion**</a>: This event is emitted when the viewer exits the single conversation.
```ts
{
commentID: string | null;
}
```
- <a id="viewNewComments">**viewNewComments**</a>: This event is emitted when the viewer reveals new live comments.
```ts
{
storyID: string;
count: number;
}
```
<!-- END docs:events -->
+12 -507
View File
@@ -5,60 +5,20 @@ Welcome! We are very excited that you are interested in contributing to Coral.
This document is a companion to help you approach contributing. If it does not
do so, please [let us know how we can improve it](https://github.com/coralproject/talk/issues)!
By contributing to this project you agree to the [Code of Conduct](CODE_OF_CONDUCT.md).
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table of Contents
- [What should I Contribute?](#what-should-i-contribute)
- [Writing Code](#writing-code)
- [When should I create an issue?](#when-should-i-create-an-issue)
- [What should I include in my issue?](#what-should-i-include-in-my-issue)
- [When should I create a pull request?](#when-should-i-create-a-pull-request)
- [What should I include in my pull request?](#what-should-i-include-in-my-pull-request)
- [Reviewing pull requests](#reviewing-pull-requests)
- [Ensure contributions are linted and tested](#ensure-contributions-are-linted-and-tested)
- [Review the feature/fixes](#review-the-featurefixes)
- [Review architectural decisions](#review-architectural-decisions)
- [Verify localizations](#verify-localizations)
- [Localization](#localization)
- [Documentation](#documentation)
- [Design Principles](#design-principles)
- [GraphQL](#graphql)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
By contributing to this project you agree to the
[Code of Conduct](CODE_OF_CONDUCT.md).
## What should I Contribute?
There are at least three different ways to contribute to Coral:
There are at least three ways to contribute to Coral:
- [Writing Code](#writing-code)
- [Reviewing pull requests](#reviewing-pull-requests)
- [Localization](#localization)
- [Documentation](#documentation)
Typically these take the form of creating a Pull Request for Coral, and
submitting it to be reviewed by a member of our team and the greater Coral
community.
Working on your first Pull Request? You can learn how from this free video
series:
[How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github)
If you decide to fix an issue, please be sure to check the comment thread in
case somebody is already working on a fix. If nobody is working on it at the
moment, please leave a comment stating that you intend to work on it so other
people dont accidentally duplicate your effort.
If somebody claims an issue but doesnt follow up for more than two weeks, its
fine to take it over but you should still leave a comment.
- Writing Code
- Providing Translations
## Writing Code
Conversation surrounding contributions begins when you can create an issue
describing your issue or suggestion.
Conversation surrounding contributions begins in
[issues](https://github.com/coralproject/talk/issues).
### When should I create an issue?
@@ -66,7 +26,9 @@ File an issue as soon as you have an idea of something you'd like to contribute.
We would love to hear what you're thinking and help refine the idea to make it
into the Coral ecosystem.
### What should I include in my issue?
Please file issues if you would like to contribute to Coral.
### What should I include?
Coral has adopted an iterative, agile development philosophy. All contributions
that make it into the Coral repository should start with a user story in this
@@ -76,8 +38,8 @@ form:
This exercise does two things:
- Allows us to ground our technical choices in a clear, simple product need.
- Expresses that product need in a way that doesn't imply a specific technical
- allows us to ground our technical choices in a clear, simple product need.
- expresses that product need in a way that doesn't imply a specific technical
solution allowing for debate as to the best way to solve the problem.
Please feel free to provide as much detail as possible when filing the issue but
@@ -87,72 +49,6 @@ technical or design solutions.
If you have a specific technical or design solution in mind, please submit it as
the first comment on the thread.
### When should I create a pull request?
File a pull request if you've created an issue in our [issues](https://github.com/coralproject/talk/issues)
page and have heard back from a member or contributor to Coral. This allows our
team to review the proposed changes prior to time being spent if the team
already has the feature/fix in the road map.
### What should I include in my pull request?
When you create a pull request, the template will describe the required
components needed for it to be reviewed by a member of the Coral team. You
should end up filling out:
- What does this PR (pull request) do?
- How do I test this PR?
You should describe what Github issue or ticket that the PR is associated with
to assist the review process. If this PR is resolving a particular bug, a
testing strategy should be described in the testing section. If this PR is
contributing a new feature, a description should describe a scenario to test or
verify the new functionality.
## Reviewing pull requests
Reviewing pull requests in Coral is generally completed by the core Coral team
that is composed of developers employed by Vox Media Inc, but external reviews
or suggestions are also welcomed.
Our review process generally follows a few core principles:
### Ensure contributions are linted and tested
It is the job of CI linting and tests to notify of style issues within the
codebase. If it is not possible for style issues to be encapsulated as a
linting rule, it shouldn't be concretely enforced during the review process.
This can ensure that code reviews contain more meaningful feedback tied to the
contribution rather than nit-picking on stylistic choices.
Reviewers must ensure that linting and tests pass in CI and locally prior to a
review taking place. You can do this by running `npm run generate` followed by
`npm run lint` and `npm run test`.
### Review the feature/fixes
Any new features added to Coral should be reviewed for bugs through a manual
verification process to ensure that they function on your machine. If possible
you should review any automated tests that were added (or not added) related to
the feature.
While the Coral team is not strict on test driven development (or TDD), any
contributions that include tests are greatly appreciated, and preferred over
those that do not.
### Review architectural decisions
Any substantial changes made to the codebase should be reviewed to ensure
that they conform to the current way code/services are laid out.
Architecture Decision Records (or [ADR](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions)) are now being used to describe architectural decisions and can be found in the `src/docs/architecture/decisions` directory.
### Verify localizations
While we don't have any automated tools at the time of writing that will
verify this in CI, any strings being added that are presented to the end user
should be wrapped in localization components to support other languages.
## Localization
We use the [fluent](http://projectfluent.org/) library and store our
@@ -168,394 +64,3 @@ where the string is used.
Once a language has enough coverage, it should be added to
`src/core/common/helpers/i18n/locales.ts`.
The [Perspective API](https://github.com/conversationai/perspectiveapi/blob/master/2-api/methods.md#analyzecomment-request)
also supports comments in specific languages. When the language is supported in
Coral and supported by the Perspective API, the language should be added to the
language map in `src/core/server/services/comments/pipeline/phases/toxic.ts`.
To assist with the translation process, we have a script that is based on the
work by @cristiandean in https://github.com/coralproject/talk/pull/2949 that
will detect missing, new, or changed translation keys for the specified
language. You can use this with:
```sh
# usage: ./scripts/i18n/validate.ts <locale>
./scripts/i18n/validate.ts pt-BR
```
## Documentation
Documentation that is publicly shown on
[docs.coralproject.net](https://docs.coralproject.net/coral/) is stored under
the `release/4` branch as it contains information for versions 4.0 onwards of
Coral.
To contribute new docs, you can either click the "Suggest Edits" in the top
right of each page, or you can edit directly via source. We suggest that for
individual fixes or contributions to the documentation.
If you want to contribute via source files, you can follow the procedure
outlined below:
1. Clone the Talk repository via `git clone https://github.com/coralproject/talk.git`
2. Switch to the `release/4` branch via `git checkout release/4`
3. Follow the procedure outlined on that branch's [CONTRIBUTING.md](https://github.com/coralproject/talk/blob/release/4/CONTRIBUTING.md#contributing-documentation) file for contributing documentation changes
4. Create a pull request to merge your changes back into the `release/4` branch
## Design Principles
### GraphQL
Coral relies heavily on [GraphQL](https://graphql.org) as the query language for
the API and the runtime on the server that powers resolving data from data
sources. This heavily influences a lot of the decisions around how we create and
consume it's API internally and how we expose it to others to interact with.
There are many GraphQL types in our [`schema.graphql`](https://github.com/coralproject/talk/blob/master/src/core/server/graph/schema/schema.graphql)
that define the way we handle data in our API. We'll try to outline a few of
them here with examples to help you understand their uses.
#### Types
Similar to defining an interface or a _struct_ definition, GraphQL has flexible
types that can be used to define data types that are used for querying data from
the API. This retrieval can happen directly via a query, or after executing an
action using a mutation and querying its response result.
An example of these types is the `Comment` and its nested `CommentRevision`
type:
```graphql
"""
Comment is a comment left by a User on an Story or another Comment as a reply.
"""
type Comment {
"""
id is the identifier of the Comment.
"""
id: ID!
"""
body is the content of the Comment, and is an alias to the body of the
`revision.body`.
"""
body: String
"""
revision is the current revision of the Comment's body.
"""
revision: CommentRevision
"""
revisionHistory stores the previous CommentRevision's, with the most recent
edit last.
"""
revisionHistory: [CommentRevision!]!
@auth(
roles: [MODERATOR, ADMIN]
userIDField: "author_id"
permit: [SUSPENDED, BANNED, PENDING_DELETION]
)
"""
status represents the Comment's current status.
"""
status: COMMENT_STATUS!
}
```
Notice how the `Comment` type can nest more custom defined types. A `Comment`
can have a current `CommentRevision` named `revision`. It also has a list of its
historical `revisionHistory`:
```graphql
type CommentRevision {
"""
id is the identifier of the CommentRevision.
"""
id: ID!
"""
comment is the reference to the original Comment associated with the current
Comment.
"""
comment: Comment!
"""
actionCounts stores the counts of all the actions for the CommentRevision
specifically.
"""
actionCounts: ActionCounts! @auth(roles: [MODERATOR, ADMIN])
"""
body is the content of the CommentRevision. If null, it indicates that the
body text was deleted.
"""
body: String
}
```
Another thing to note, see how `CommentRevision` is not only referenced by
`revision` and `revisionHistory` on the `Comment` type. The `CommentRevision`
also references back to its parent `Comment` via the `comment: Comment!`
property. This is how defined types interact between each other in the GraphQL
schema.
Our naming scheme is upper camel case (also known as Pascal Case) for these
types:
- Start with a capital letter
- Following characters are lower case
- Every new word in the type name begins with a new capital letter
- Acronyms are always capitalized (with the only exception being the
`clientMutationId: String!` field in mutation input/payload types)
Some of the properties have an `!` beside their type (i.e `id: ID!`) which
indicates that this property is required and is non-nullable. GraphQL will
validate the input request for these properties and ensure they are provided
during the GraphQL request.
You can learn more about GraphQL types in their documentation:
[Learn GraphQL: Schemas and Types](https://graphql.org/learn/schema/)
#### Enumeration Types
In the previous example with the `Comment` type. We also had a property called
`status` which was of type `COMMENT_STATUS`.
This is another kind of defined type, an [Enumeration Type](https://graphql.org/learn/schema/#enumeration-types),
also called _enums_.
```graphql
enum COMMENT_STATUS {
NONE
APPROVED
REJECTED
PREMOD
SYSTEM_WITHHELD
}
```
Like all enumeration types this definition enumerates out typed, named values
that are reusable for state elsewhere on other types.
Our naming scheme for enumeration types and values in those types as:
- All capital letters
- Spaces delimited with underscores
This is because they are treated as shared constant values across the schema.
Rather than storing strings or numbers to capture selected state, we prefer
using enumeration types because they are much more stricter in terms of value.
You can learn more about GraphQL Enumeration types in their documentation:
[Learn GraphQL: Enumeration Types](https://graphql.org/learn/schema/#enumeration-types)
#### Mutation Types
Mutations are a request to GraphQL to initiate an action which will result in a response. As such,they're broken up into an `Input` and `Payload` pair that matches a mutation's request and response pair.
An example is the `CreateCommentInput` and `CreateCommentPayload`:
```graphql
"""
CreateCommentInput provides the input for the createComment Mutation.
"""
input CreateCommentInput {
"""
storyID is the ID of the Story where we are creating a comment on.
"""
storyID: ID!
"""
nudge when true will instead return an error related to recoverable moderation
faults such as a toxic comment or spam comment to provide user feedback to
nudge the user to correct the comment.
"""
nudge: Boolean = false
"""
body is the Comment body, the content of the Comment.
"""
body: String!
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
}
"""
CreateCommentPayload contains the created Comment after the createComment
mutation.
"""
type CreateCommentPayload {
"""
edge is the possibly created comment edge.
"""
edge: CommentEdge!
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
}
```
The `CreateCommentInput` type contains some parameters that allow us to execute
the mutation. These are:
- `storyID` - the story we are submitting the comment to.
- `body` - the body of our comment.
- `nudge` - whether we should return validation prompts to the user about
improper comment language.
- `clientMutationId` - the identifier used by Relay to identify this mutation,
our front-end state cache to process the mutation request. This is the only
place in the code-base that we do not capitalize the acronym when written in
camel-case, this is unfortunately due to legacy reasons from within Relay.
The returned response for a mutation is a what we call a Payload, in this case
it's `CreateCommentPayload`. This usually has a response that is the full data
type of whatever was modified by the earlier called mutation action. The
properties on this type are:
- `edge` - we return the full comment edge that was created by the previous
input. This is done so that the mutation request can query whatever it needs
to from the returned input to update state on the client.
- `clientMutationId` - an identifier used by Relay (our front-end client state
cache) to process the mutation request.
#### Access Permissions On Types
Sometimes, you only want specific users to be allowed to view certain
information. For instance, we have roles that are defined in our schema so we
can filter who can have access to what.
These roles are used with directives on our schema that GraphQL then enforces
when trying to resolve requests a user makes. If the user has the specified role
associated with their signed-in account, they are given access to the requested
data.
An example of this is the `revisionHistory` on the `Comment` type:
```graphql
fragment on Comment {
"""
revisionHistory stores the previous CommentRevision's, with the most recent
edit last.
"""
revisionHistory: [CommentRevision!]!
@auth(
roles: [MODERATOR, ADMIN]
userIDField: "author_id"
permit: [SUSPENDED, BANNED, PENDING_DELETION]
)
}
```
Here we see the `@auth` directive. It has documentation describing the various
parameters allowed located in the [`schema.graphql`](https://github.com/coralproject/talk/blob/master/src/core/server/graph/schema/schema.graphql)
file, we'll discuss below what this particular set of parameters can be read as:
- The roles that are allowed to access this information are `MODERATOR` and
`ADMIN` as defined by the `roles` argument.
- We let the directive know that the author of the comment is `author_id` from
the `Comment` by defining the `userIDField`. It's a rule of thumb in Coral if
the Author created the document, they have permission to view it. A Comment
for example is authored by a user, with the underlying field associated with
the id of that author living on the `author_id` field. You can see how this
is related if you look at the resolver for the `Comment` type to see that it
is based off of the `Comment` interface from `src/core/server/models/comment`.
- We permit returning comments when the author has the following conditions
associated with their account: `SUSPENDED`, `BANNED`, or `PENDING_DELETION`.
These directives can be simpler, for example the `metadata` property on the
`CommentRevision`:
```graphql
fragment on CommentRevision {
"""
metadata stores details on a CommentRevision.
"""
metadata: CommentRevisionMetadata! @auth(roles: [ADMIN, MODERATOR])
}
```
Here we see an auth directive with only roles defined. This is sufficient to
make sure that the metadata property is only accessible to `ADMIN` and
`MODERATOR` user roles.
Note: Wondering how the user roles are defined? They're simply an enumeration
type that is also defined in the schema.
```graphql
enum USER_ROLE {
COMMENTER
STAFF
MODERATOR
ADMIN
}
```
#### Arrays of Items
Sometimes you don't want a singular property, your property is instead a
collection of items.
The `revisionHistory` from the `Comment` is again useful as an example:
```graphql
fragment on Comment {
"""
revisionHistory stores the previous CommentRevision's, with the most recent
edit last.
"""
revisionHistory: [CommentRevision!]!
}
```
The interior type `CommentRevision` is required using the `!` and the outer
array is also required using `!`.
We do this for a couple of reasons:
- This ensures that we do not return null/undefined values within the array.
- Why would we return a null when we can just return nothing for null values?
- We want the array to always be defined, if empty, we return and empty array (i.e. `[]`).
- This can be handled nicely in our resolvers. We simply check if the retrieved values is null or undefined and simply return an empty array in its stead.
These little tweaks aren't necessary, but they ease the use of our API by making
the results for arrays predictable and strongly typed.
#### Documenting
As you may have noticed, there is quite a bit of documentation in the schema examples listed here.
We typically follow these two rules in commenting our GraphQL types:
- Always comment the property within a type describing its purpose on its parent type
i.e. `createdAt` on our `Setting` type:
```graphql
fragment on Setting {
"""
createdAt is the time that the Settings was created at.
"""
createdAt: Time! @auth(roles: [ADMIN])
}
```
- Always comment the purpose of each type
i.e. the `Comment` type:
```graphql
"""
Comment is a comment left by a User on an Story or another Comment as a reply.
"""
type Comment { }
```
-272
View File
@@ -1,272 +0,0 @@
# CSS Variables
Coral defines a set of CSS Variables that you can use to broadly customize the
look and feel of your comment stream. For example you can easily redefine the colors
or fonts that are being used.
To change the CSS Variables, use the following example in your custom CSS file:
```
:root {
--font-family-primary: 'Verdana';
--round-corners: 0px;
}
```
The following list contains the CSS Variables that can be customized.
**Info**: *Before 6.3.0 Coral uses a different set of CSS Variables. If you are still using
the old CSS Variables, please upgrade. Currently we have a compatibility layer to bridge the old CSS Variables to the new ones. This mechanism will be removed in the future.*
<!-- START docs:css-variables -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN npm run docs:css-variables -->
### Index
- <a href="#variables">Variables</a>
- <a href="#palette">palette</a>
- <a href="#palette-primary">palette-primary</a>
- <a href="#palette-background">palette-background</a>
- <a href="#palette-text">palette-text</a>
- <a href="#palette-grey">palette-grey</a>
- <a href="#palette-error">palette-error</a>
- <a href="#palette-success">palette-success</a>
- <a href="#palette-warning">palette-warning</a>
- <a href="#font-family">font-family</a>
- <a href="#font-weight">font-weight</a>
- <a href="#font-weight-primary">font-weight-primary</a>
- <a href="#font-weight-secondary">font-weight-secondary</a>
- <a href="#font-size">font-size</a>
- <a href="#shadow">shadow</a>
- <a href="#spacing">spacing</a>
- <a href="#mini-unit">mini-unit</a>
### Variables
Use this to remove round corners or make them more round.
`--round-corners: 3px;`
### palette
`--palette-divider: rgba(0, 0, 0, 0.12);`
#### palette-primary
Color palette that is used as the primary color.
`--palette-primary-100: #EBF6FF; /* Before 6.3.0: --palette-primary-lightest */`
`--palette-primary-200: #B7DCFF; /* Before 6.3.0: --palette-primary-lighter */`
`--palette-primary-300: #61B3FF; /* Before 6.3.0: --palette-primary-light */`
`--palette-primary-400: #2897FF; /* Before 6.3.0: --palette-primary-main */`
`--palette-primary-500: #0070D9; /* Before 6.3.0: --palette-primary-main */`
`--palette-primary-600: #0062BE; /* Before 6.3.0: --palette-primary-main */`
`--palette-primary-700: #005AAE; /* Before 6.3.0: --palette-primary-main */`
`--palette-primary-800: #004688; /* Before 6.3.0: --palette-primary-dark */`
`--palette-primary-900: #00386D; /* Before 6.3.0: --palette-primary-darkest */`
#### palette-background
Color palette that is used for background colors.
`--palette-background-body: #FFFFFF;`
`--palette-background-popover: #FFFFFF;`
`--palette-background-tooltip: #65696B;`
`--palette-background-input: #FFFFFF;`
`--palette-background-input-disabled: #EFEFEF;`
#### palette-text
Color palette that is used for text.
`--palette-text-000: #FFFFFF; /* Before 6.3.0: --palette-text-light */`
`--palette-text-100: #65696B; /* Before 6.3.0: --palette-text-secondary */`
`--palette-text-500: #353F44; /* Before 6.3.0: --palette-text-primary */`
`--palette-text-900: #14171A; /* Before 6.3.0: --palette-text-dark */`
`--palette-text-placeholder: #9FA4A6; /* Before 6.3.0: --palette-grey-lighter */`
`--palette-text-input-disabled: #9FA4A6; /* Before 6.3.0: --palette-grey-lighter */`
#### palette-grey
Color palette that is used for grey shades.
`--palette-grey-100: #F4F7F7; /* Before 6.3.0: --palette-grey-lightest */`
`--palette-grey-200: #EAEFF0; /* Before 6.3.0: --palette-grey-lightest */`
`--palette-grey-300: #CBD1D2; /* Before 6.3.0: --palette-grey-lighter */`
`--palette-grey-400: #9FA4A6; /* Before 6.3.0: --palette-grey-lighter */`
`--palette-grey-500: #65696B; /* Before 6.3.0: --palette-grey-main */`
`--palette-grey-600: #49545C; /* Before 6.3.0: --palette-grey-dark */`
`--palette-grey-700: #32404D; /* Before 6.3.0: --palette-grey-darkest */`
`--palette-grey-800: #202E3E; /* Before 6.3.0: --palette-grey-darkest */`
`--palette-grey-900: #132033; /* Before 6.3.0: --palette-grey-darkest */`
#### palette-error
Color palette that is used for indicating something is error red.
`--palette-error-100: #FCE5D9; /* Before 6.3.0: --palette-error-lightest */`
`--palette-error-200: #FAC6B4; /* Before 6.3.0: --palette-error-lighter */`
`--palette-error-300: #F29D8B; /* Before 6.3.0: --palette-error-lighter */`
`--palette-error-400: #E5766C; /* Before 6.3.0: --palette-error-light */`
`--palette-error-500: #D53F3F; /* Before 6.3.0: --palette-error-main */`
`--palette-error-600: #B72E39; /* Before 6.3.0: --palette-error-main */`
`--palette-error-700: #991F34; /* Before 6.3.0: --palette-error-dark */`
`--palette-error-800: #7B142E; /* Before 6.3.0: --palette-error-darkest */`
`--palette-error-900: #660C2B; /* Before 6.3.0: --palette-error-darkest */`
#### palette-success
Color palette that is used for indicating something is success green.
`--palette-success-100: #D8F9D5; /* Before 6.3.0: --palette-success-lightest */`
`--palette-success-200: #ADF3AD; /* Before 6.3.0: --palette-success-lighter */`
`--palette-success-300: #7CDB85; /* Before 6.3.0: --palette-success-lighter */`
`--palette-success-400: #54B767; /* Before 6.3.0: --palette-success-light */`
`--palette-success-500: #268742; /* Before 6.3.0: --palette-success-main */`
`--palette-success-600: #1B743D; /* Before 6.3.0: --palette-success-main */`
`--palette-success-700: #136138; /* Before 6.3.0: --palette-success-dark */`
`--palette-success-800: #0C4E32; /* Before 6.3.0: --palette-success-darkest */`
`--palette-success-900: #07402E; /* Before 6.3.0: --palette-success-darkest */`
#### palette-warning
Color palette that is used for indicating a warning and is usually yellow.
`--palette-warning-100: #FFFACC; /* Before 6.3.0: --palette-warning-main */`
`--palette-warning-500: #FFE91F; /* Before 6.3.0: --palette-warning-main */`
### font-family
Different font families currently in use.
`--font-family-primary: "Open Sans"; /* Before 6.3.0: --font-family-sans-serif */`
`--font-family-secondary: "Nunito"; /* Before 6.3.0: --font-family-serif */`
### font-weight
Different font weights with matching values for the fonts.
#### font-weight-primary
`--font-weight-primary-bold: 700; /* Before 6.3.0: --font-weight-bold */`
`--font-weight-primary-semi-bold: 600; /* Before 6.3.0: --font-weight-medium */`
`--font-weight-primary-regular: 300; /* Before 6.3.0: --font-weight-light */`
#### font-weight-secondary
`--font-weight-secondary-bold: 700; /* Before 6.3.0: --font-weight-bold */`
`--font-weight-secondary-regular: 300; /* Before 6.3.0: --font-weight-light */`
### font-size
`--font-size-1: 0.75rem;`
`--font-size-2: 0.875rem;`
`--font-size-3: 1rem;`
`--font-size-4: 1.125rem;`
`--font-size-5: 1.25rem;`
`--font-size-6: 1.5rem;`
`--font-size-7: 1.75rem;`
`--font-size-8: 2rem;`
`--font-size-9: 2.25rem;`
`--font-size-icon-xl: 2.25rem;`
`--font-size-icon-lg: 1.5rem;`
`--font-size-icon-md: 1.125rem;`
`--font-size-icon-sm: 0.875rem;`
`--font-size-icon-xs: 0.75rem;`
### shadow
Different shadows that are currently used in Coral.
`--shadow-popover: 1px 0px 4px rgba(0, 0, 0, 0.25); /* Before 6.3.0: --elevation-main */`
### spacing
Different spacing units currenty used in Coral.
`--spacing-1: 4px;`
`--spacing-2: 8px;`
`--spacing-3: 12px;`
`--spacing-4: 16px;`
`--spacing-5: 24px;`
`--spacing-6: 32px;`
`--spacing-7: 44px;`
`--spacing-8: 60px;`
`--spacing-9: 84px;`
### mini-unit
Grid units for smaller and larger screens.
`--mini-unit-small: 4;`
`--mini-unit-large: 8;`
<!-- END docs:css-variables -->
+8 -3
View File
@@ -1,4 +1,4 @@
FROM node:12-alpine
FROM node:10-alpine
# Install build dependancies.
RUN apk --no-cache add git python
@@ -7,6 +7,9 @@ RUN apk --no-cache add git python
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# Setup the environment for production.
ENV NODE_ENV production
# Bundle application source.
COPY . /usr/src/app
@@ -16,13 +19,15 @@ RUN mkdir -p dist/core/common/__generated__ && \
echo "{\"revision\": \"${REVISION_HASH}\"}" > dist/core/common/__generated__/revision.json
# Install build static assets and clear caches.
RUN npm ci && \
RUN NODE_ENV=development npm install && \
npm run generate && \
npm run build && \
npm prune --production
# Setup the environment
ENV NODE_ENV production
ENV PATH /usr/src/app/bin:$PATH
ENV PORT 5000
EXPOSE 5000
ENV NODE_ENV production
CMD ["npm", "run", "start"]
-240
View File
@@ -1,240 +0,0 @@
# External Moderation Phases Guide
This document is in reference to external moderation phases emitted by Coral.
You can configure external moderation phases on your installation of Coral by
visiting `/admin/configure/moderation/phases`.
Once you've configured a external moderation phase in Coral, you will start to
receive moderation requests in the form of a
[External Moderation Requests](#external-moderation-request) at the provided
callback URL. These will be in the form of `POST` requests with a `JSON`
payload.
When a comment is created or edited, it will be processed by moderation phases in
a predefined order. Any external moderation phase is run last, and only if all
other moderation phases before it do not return a status. The current set of
moderation phases is listed in order [here](https://github.com/coralproject/talk/blob/master/src/core/server/services/comments/pipeline/phases/index.ts).
Once you have received a moderation request, you must respond within the
provided timeout else the phase will be skipped and it will continue. It is
strongly recommended to [verify the request signature](#request-signing).
The external moderation phase must respond with one of the following:
1. Do not moderate the comment, and return a 204 without a body.
2. Perform a moderation action and return a 200 with a [External Moderation Response](#external-moderation-response)
as a `JSON` encoded body containing the operations you want to perform on the
comment.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table of Contents
- [Request Signing](#request-signing)
- [Schema](#schema)
- [External Moderation Request](#external-moderation-request)
- [External Moderation Response](#external-moderation-response)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Request Signing
Requests sent by Coral for external moderation phases use the same process as
those used by webhooks. Refer to the [webhooks documentation](WEBHOOKS.md#webhook-signing)
for instructions on how to verify signatures sent by Coral.
## Schema
### External Moderation Request
```ts
interface ExternalModerationRequest {
/**
* action refers to the specific operation being performed. If `NEW`, this
* is referring to a new comment being created. If `EDIT`, then this refers to
* an operation involving an edit operation on an existing Comment.
*/
action: "NEW" | "EDIT";
/**
* comment refers to the actual Comment data for the Comment being
* created/edited.
*/
comment: {
/**
* body refers to the actual body text of the Comment being created/edited.
*/
body: string;
/**
* parentID is the identifier for the parent comment (if this Comment is a
* reply, null otherwise).
*/
parentID: string | null;
};
/**
* author refers to the User that is creating/editing the Comment.
*/
author: {
/**
* id is the identifier for this User.
*/
id: string;
/**
* role refers to the role of this User.
*/
role: "COMMENTER" | "STAFF" | "MODERATOR" | "ADMIN";
};
/**
* story refers to the Story being commented on.
*/
story: {
/**
* id is the identifier for this Story.
*/
id: string;
/**
* url is the URL for this Story.
*/
url: string;
};
/**
* site refers to the Site that the story being commented on belongs to.
*/
site: {
/**
* id is the identifier for this Site.
*/
id: string;
};
/**
* tenantID is the identifer of the Tenant that this Comment is being
* created/edited on.
*/
tenantID: string;
/**
* tenantDomain is the domain that is associated with this Tenant that this
* Comment is being created/edited on.
*/
tenantDomain: string;
}
```
#### Example
New comment on a story:
```json
{
"action": "NEW",
"comment": {
"body": "Here's a comment!",
"parentID": null
},
"author": {
"id": "baf4e943-3594-4fcc-b2ba-3e8de7a76352",
"role": "COMMENTER"
},
"story": {
"id": "245b3856-b0a0-4d2f-a6bb-58c71f18d6a6",
"url": "http://localhost:1313/posts/a-story-url/"
},
"site": {
"id": "a4bede88-2d2c-4424-bc18-4322a9e285a6"
},
"tenantID": "19ba5794-7eeb-4d46-a81b-c00c61672501",
"tenantDomain": "localhost"
}
```
New reply on a comment on a story:
```json
{
"action": "NEW",
"comment": {
"body": "Here's a reply!",
"parentID": "d79b787f-f406-49a0-a179-72e3652e54be"
},
"author": {
"id": "baf4e943-3594-4fcc-b2ba-3e8de7a76352",
"role": "COMMENTER"
},
"story": {
"id": "245b3856-b0a0-4d2f-a6bb-58c71f18d6a6",
"url": "http://localhost:1313/posts/a-story-url/"
},
"site": {
"id": "a4bede88-2d2c-4424-bc18-4322a9e285a6"
},
"tenantID": "19ba5794-7eeb-4d46-a81b-c00c61672501",
"tenantDomain": "localhost"
}
```
### External Moderation Response
```ts
interface ExternalModerationResponse {
/**
* actions is an optional list of any flags to be added to this Comment.
*/
actions?: Array<{
actionType: "FLAG";
reason: "COMMENT_DETECTED_TOXIC" | "COMMENT_DETECTED_SPAM";
}>;
/**
* tags are any listed tags that should be added to the comment.
*/
tags?: Array<"FEATURED" | "STAFF">;
/**
* status when provided decides and terminates the moderation process by
* setting the status of the comment.
*/
status?: "NONE" | "APPROVED" | "REJECTED" | "PREMOD" | "SYSTEM_WITHHELD";
}
```
#### Examples
Add a flag to a comment and do not set a status:
```json
{
"actions": [{ "actionType": "FLAG", "reason": "COMMENT_DETECTED_TOXIC" }]
}
```
Reject a comment:
```json
{
"status": "REJECTED"
}
```
Feature a comment and do not set a status:
```json
{
"tags": ["FEATURED"]
}
```
Approve a comment and mark it as featured:
```json
{
"status": "APPROVED",
"tags": ["FEATURED"]
}
```
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 2020 Vox Media, Inc
Copyright 2019 Vox Media, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+26 -37
View File
@@ -1,52 +1,41 @@
<p align="center">
<a href="https://coralproject.net" target="_blank"><img width="250" src="https://docs.coralproject.net/coral/images/coralproject_by_voxmedia.svg" alt="Coral by Vox Media" /></a>
</p>
# Coral ![CircleCI](https://img.shields.io/circleci/project/github/coralproject/talk/next.svg)
<p align="center">
A better commenting experience from <a href="https://product.voxmedia.com/" target="_blank">Vox Media</a>.
</p>
Online comments are broken. Our open-source commenting platform, Coral, rethinks
how moderation, comment display, and conversation function, creating the
opportunity for safer, smarter discussions around your work.
[Read more about Coral here](https://coralproject.net/talk).
<p align="center">
<a href="https://circleci.com/gh/coralproject/talk" target="_blank"><img src="https://img.shields.io/circleci/build/gh/coralproject/talk?style=flat-square" alt="CircleCI" /></a>
<a href="https://hub.docker.com/r/coralproject/talk" target="_blank"><img src="https://img.shields.io/docker/v/coralproject/talk?label=docker%20hub&sort=semver&style=flat-square" alt="Docker Image Version" /></a>
<a href="https://hub.docker.com/r/coralproject/talk" target="_blank"><img src="https://img.shields.io/docker/image-size/coralproject/talk?label=docker%20image%20size&sort=semver&style=flat-square" alt="Docker Image Size" /></a>
<a href="https://twitter.com/coralproject" target="_blank"><img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/coralproject?style=flat-square"></a>
</p>
Built with <3 by The Coral Project, a part of [Vox Media](https://product.voxmedia.com/).
## Description
Preview Coral easily by running Coral via a Heroku App:
Online comments are broken. Our open-source commenting platform,
[Coral](https://coralproject.net), rethinks how moderation, comment display, and
conversation function, creating the opportunity for safer, smarter discussions
around your work.
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/coralproject/talk)
We offer hosting and support packages for Coral, as well as exclusive,
customer-only features. [Contact us](https://coralproject.net/pricing/) for more
information or [sign up for a webinar](https://coralproject.net).
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table of Contents
- [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 -->
## Documentation
If you're new to Coral, the [Coral documentation](https://docs.coralproject.net/)
is a great place to start running and developing with Coral.
You can get started with Coral using our [Documentation](https://docs.coralproject.net/talk/).
Youve installed Coral, and youre 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
[Community Guides](https://guides.coralproject.net/start-here/) to learn more.
## Pre-Launch Guide
## Support
Youve installed Talk on your server, and youre 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/).
We can help you set up Coral, migrate your comments from another system,
integrate your registration platform, pair with your programmers, and help you
with bespoke installs. To learn more, [contact us](https://coralproject.net/pricing/).
## More Resources
## Contributing
Coral is a Apache-2.0 licensed open-source project built with <3 by the Coral
team, a part of [Vox Media](https://product.voxmedia.com/).
If you are interested in contributing to Coral, check out our [Contributor's Guide](CONTRIBUTING.md).
- [Our Blog](https://coralproject.net/blog)
- [Community Guides for Journalism](https://guides.coralproject.net/)
- [More About Us](https://coralproject.net/)
## License
Coral is [Apache-2.0 licensed](LICENSE).
Coral is released under the [Apache License, v2.0](/LICENSE).
-220
View File
@@ -1,220 +0,0 @@
# Webhooks Guide
This document is in reference to webhooks emitted by Coral. You can configure
webhooks on your installation of Coral by visiting `/admin/configure/webhooks`.
Once you've configured a webhook endpoint in Coral, you will receive updates
from Coral when those events occur. These will be in the form of `POST` requests
with a `JSON` payload consisting of the schema represented below.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table of Contents
- [Webhook Signing](#webhook-signing)
- [How to verify the signature(s)](#how-to-verify-the-signatures)
- [Schema](#schema)
- [Events Listing](#events-listing)
- [Events](#events)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Webhook Signing
Each webhook sent by Coral is signed by your webhook endpoint signing secret.
The signature method closely resembles the signing method used by Stripe for
their `v1` signing method. The `X-Coral-Signature` header contains one or more
signatures prefixed by `sha256=`.
If you receive a signature containing multiple signatures, it is typically when
you have rolled the signing secret from the administrative panel, and chosen to
keep the previous secret active for a duration of time.
### How to verify the signature(s)
```js
// Set your signing secret here from the administration panel.
const SIGNING_SECRET = "< YOUR SIGNING SECRET HERE >";
// We're using crypto to verify the signatures.
const crypto = require("crypto");
// We're using express to receive webhooks here.
const app = require("express")();
// Use the body-parser to get the raw body as a buffer so we can use it with the
// hashing functions.
const parser = require("body-parser");
function extractEvent(body, sig) {
// Step 1: Extract signatures from the header.
const signatures = sig
// Split the header by `,` to get a list of elements.
.split(",")
// Split each element by `=` to get a prefix and value pair.
.map(element => element.split("="))
// Grab all the elements with the prefix of `sha256`.
.filter(([prefix]) => prefix === "sha256")
// Grab the value from the prefix and value pair.
.map(([, value]) => value);
// Step 2: Prepare the `signed_payload`.
const signed_payload = body;
// Step 3: Calculate the expected signature.
const expected = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(signed_payload)
.digest()
.toString("hex");
// Step 4: Compare signatures.
if (
// For each of the signatures on the request...
!signatures.some(signature =>
// Compare the expected signature to the signature on in the header. If at
// least one of the match, we should continue to process the event.
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
)
) {
throw new Error("Invalid signature");
}
// Parse the JSON for the event.
return JSON.parse(body.toString());
}
app.post("/webhook", parser.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-coral-signature"];
let event;
try {
// Parse the JSON for the event.
event = extractEvent(req.body, sig);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event.
switch (event.type) {
case "STORY_CREATED":
const data = event.data;
console.log(
`A Story with ID ${data.storyID} and URL ${data.storyURL} was created!`
);
break;
// ... handle other event types.
default:
// Unexpected event type
return response.status(400).end();
}
// Return a response to acknowledge receipt of the event
res.json({ received: true });
});
app.listen(4242, () => console.log("Running on port 4242"));
```
The procedure of how to verify the signatures follows.
#### **Step 1**: Extract signatures from the header
Split the header using `,` as the separator, to get a list of elements. Then
split each of these elements using `=` as the separator, to get a prefix and
value pair. The value for the prefix `sha256` corresponds to the signature(s).
#### **Step 2**: Prepare the `signed_payload` string
You can do this by taking the string contents of the body (before parsing or the
request body).
#### **Step 3**: Calculate the expected signature
Compute an HMAC signature using the SHA256 hash function. You can use the
webhook endpoint's signing secret as the key, and the above calculated
`signed_payload` as the message.
#### **Step 4**: Compare signatures
Compare the signature(s) in the header to the expected signature. To protect
against timing attacks, ensure you use a constant-time string comparison
function when comparing signatures.
## Schema
```ts
{
/**
* id is the identifier for this event, each event
* will have a unique id.
*/
id: string;
/**
* type is the name of this event, this indicates
* what is stored in the following `data` property.
* Refer to the `Events List` below to see what the
* type is for each event.
*/
type: string;
/**
* data is the object representing this particular
* event. Each type of event has a different shape
* to the data property. Refer to the `Events List`
* below to see what the data looks like for each
* event.
*/
data: object;
/**
* createdAt is the ISO 8601 representation of the
* date when this event was created.
*/
createdAt: string;
/**
* tenantID is the ID of the Tenant that this event originated at.
*/
tenantID: string;
/**
* tenantDomain is the domain that is associated with this Tenant that this event originated at.
*/
tenantDomain: string;
}
```
## Events Listing
- [`STORY_CREATED`](#story-created-event)
## Events
- <a id="story-created-event">**STORY_CREATED**</a>
```ts
{
id: string;
type: "STORY_CREATED";
data: {
/**
* storyID is the ID of the newly created Story.
*/
storyID: string;
/**
* storyURL is the URL of the newly created Story.
*/
storyURL: string;
/**
* siteID is the Site that the newly created Story was created on.
*/
siteID: string;
}
createdAt: string;
}
```
+42
View File
@@ -0,0 +1,42 @@
{
"name": "Coral",
"description": "A better commenting experience from Vox Media.",
"env": {
"REWRITE_ENV": {
"description": "Used to rewrite the environment variables set by Heroku.",
"value": "REDIS_URI:REDIS_URL,MONGODB_URI:MONGO_URI"
},
"SIGNING_SECRET": {
"description": "The shared secret to use to sign JSON Web Tokens (JWT) with the selected signing algorithm.",
"generator": "secret"
},
"CONCURRENCY": {
"description": "The number of worker nodes to spawn to handle traffic.",
"value": "1"
},
"LOCALE": {
"description": "Specify the default locale to use for all requests without a locale specified",
"value": "en-US"
},
"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": [
{
"plan": "mongolab:sandbox",
"as": "MONGO"
},
{
"plan": "rediscloud:30",
"as": "REDIS"
}
],
"success_url": "/install",
"website": "https://github.com/coralproject/talk"
}
+18 -10
View File
@@ -1,20 +1,28 @@
/**
* This is a project wide babel configuration.
* https://babeljs.io/docs/en/config-files#project-wide-configuration
*
* We use this file to apply babel configuration to packages in `node_modules`
*/
// Note: If Webpack env is set, we are building for the client.
const plugins =
const lodashOptimizations =
process.env.WEBPACK === "true" ? ["use-lodash-es", "lodash"] : [];
const environment =
process.env.WEBPACK === "true"
? { modules: false }
: { targets: { node: "current" }, modules: "commonjs" };
module.exports = {
babelrcRoots: ["./src/core/client/*"],
plugins,
presets: [["@babel/env", environment]],
env: {
production: {
plugins: [...lodashOptimizations],
},
development: {
plugins: [...lodashOptimizations],
},
test: {
presets: [
["@babel/env", { targets: { node: "current" } }],
"@babel/react",
],
plugins: ["dynamic-import-node"],
},
},
};
+6 -4
View File
@@ -7,7 +7,7 @@ module.exports = {
collectCoverageFrom: ["**/*.{js,jsx,mjs,ts,tsx}"],
coveragePathIgnorePatterns: ["/node_modules/"],
setupFiles: [
"<rootDir>/src/core/client/test/polyfills.ts",
"<rootDir>/src/core/build/polyfills.js",
"<rootDir>/src/core/client/test/setup.ts",
],
setupFilesAfterEnv: ["<rootDir>/src/core/client/test/setupTestFramework.ts"],
@@ -40,9 +40,11 @@ module.exports = {
snapshotSerializers: ["enzyme-to-json/serializer"],
globals: {
"ts-jest": {
babelConfig: true,
tsConfig: path.resolve(__dirname, "../../src/core/client/tsconfig.json"),
useBabelrc: true,
tsConfigFile: path.resolve(
__dirname,
"../../src/core/client/tsconfig.json"
),
},
},
preset: "ts-jest/presets/js-with-babel",
};
+2 -4
View File
@@ -6,7 +6,6 @@ module.exports = {
roots: ["<rootDir>/src"],
collectCoverageFrom: ["**/*.{js,jsx,mjs,ts,tsx}"],
coveragePathIgnorePatterns: ["/node_modules/"],
setupFilesAfterEnv: ["<rootDir>/src/core/server/test/setupTestFramework.ts"],
testMatch: ["**/*.spec.{js,jsx,mjs,ts,tsx}"],
testPathIgnorePatterns: ["/node_modules/", "/client/"],
testEnvironment: "node",
@@ -21,9 +20,8 @@ module.exports = {
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
globals: {
"ts-jest": {
babelConfig: true,
tsConfig: path.resolve(__dirname, "../../src/tsconfig.json"),
useBabelrc: true,
tsConfigFile: path.resolve(__dirname, "../../src/tsconfig.json"),
},
},
preset: "ts-jest",
};
+101 -4
View File
@@ -1,10 +1,107 @@
// Apply all the configuration provided in the .env file.
require("dotenv").config();
module.exports = {
title: "Coral 5",
src: "./src",
const path = require("path");
const fs = require("fs");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
const extensions = [".ts", ".tsx", ".js"];
// TODO: There is some weird issue with including paths.ts here
const postCSSConfigPath = "./src/core/build/postcss.config";
const isProduction = process.NODE_ENV === "production";
const appDirectory = fs.realpathSync(process.cwd());
const styleLoader = {
loader: require.resolve("style-loader"),
options: {
sourceMap: true,
hmr: true,
},
};
export default {
title: "Coral 5.0",
source: "./src",
typescript: true,
host: process.env.HOST || "0.0.0.0",
port: parseInt(process.env.DOCZ_PORT, 10) || 3030,
files: "**/*.mdx",
codeSandbox: false, // Too large to create code sandboxes..
modifyBundlerConfig: config => {
config.entry.app.push(
`${appDirectory}/src/core/client/ui/theme/variables.css.ts`
);
config.module.rules.push({
test: /\.css\.ts$/,
use: [
styleLoader,
{
loader: require.resolve("css-loader"),
options: {
modules: true,
importLoaders: 2,
localIdentName: "[name]-[local]-[hash:base64:5]",
sourceMap: true,
},
},
{
loader: require.resolve("postcss-loader"),
options: {
config: {
path: postCSSConfigPath,
},
parser: "postcss-js",
},
},
{
loader: require.resolve("babel-loader"),
options: {
configFile: false,
babelrc: false,
presets: [
"@babel/typescript",
[
"@babel/env",
{ targets: { node: "10.0.0" }, modules: "commonjs" },
],
],
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
},
},
],
});
config.module.rules.push({
test: /\.css$/,
use: [
styleLoader,
{
loader: require.resolve("css-loader"),
options: {
modules: true,
importLoaders: 1,
localIdentName: "[name]-[local]-[hash:base64:5]",
sourceMap: true,
},
},
{
loader: require.resolve("postcss-loader"),
options: {
config: {
path: postCSSConfigPath,
},
},
},
],
});
config.resolve.plugins = [
new TsconfigPathsPlugin({
extensions,
// TODO: There is some weird issue with including paths.ts here
configFile: "./src/core/client/tsconfig.json",
}),
];
// fs.writeFileSync(path.resolve(__dirname, "tmp"), stringify(config, null, 2));
return config;
},
};
-216
View File
@@ -1,216 +0,0 @@
const fs = require("fs");
const path = require("path");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
/** Path to postCSSConfig */
const postCSSConfigPath = path.resolve(
__dirname,
"../src/core/build/postcss.config"
);
const rootDir = path.resolve(__dirname, "../");
const srcDir = path.resolve(rootDir, "./src");
const appTsconfig = path.resolve(rootDir, "./src/core/client/tsconfig.json");
const CSS_PATTERN = /\.css$/;
const MODULE_CSS_PATTERN = /\.module\.css$/;
// Define `RegExp.toJSON` so that we can stringify RegExp.
Object.defineProperty(RegExp.prototype, "toJSON", {
value: RegExp.prototype.toString,
});
const isCssRules = (rule) =>
rule.test &&
(rule.test.toString() === CSS_PATTERN.toString() ||
rule.test.toString() === MODULE_CSS_PATTERN.toString());
const findCssRules = (config) =>
config.module.rules.find(
(rule) => Array.isArray(rule.oneOf) && rule.oneOf.every(isCssRules)
);
exports.onCreateWebpackConfig = ({
stage,
rules,
loaders,
plugins,
actions,
getConfig,
}) => {
// Get webpack config.
const config = getConfig();
if (stage === "develop") {
config.entry.commons.push(
// Add our stream css variables file.
`${srcDir}/core/client/ui/theme/stream.css.ts`
);
}
/*
TODO: (cvle) couldn't get build to work...
if (stage === "build-javascript") {
config.entry.app = [
config.entry.app,
`${appDir}/core/client/ui/theme/stream.css.ts`,
];
}
*/
// Find the gatsby CSS rules.
const cssRules = findCssRules(config);
// Exclude them from our src dir because they are incomaptible with our
// CSS rules.
cssRules.exclude = srcDir;
// Add .tx .tsx to modules
config.resolve.extensions.push(".ts", ".tsx");
actions.replaceWebpackConfig(config);
// Write out webpack config to .docz folder.
fs.writeFileSync(
path.resolve(__dirname, "webpack-" + stage),
JSON.stringify(config, {}, 2)
);
// Turn on sourceMap during develop.
const sourceMap = stage.startsWith("develop");
// CSS loaders to prepend.
const prependCSSLoaders = [];
if (stage === "develop") {
prependCSSLoaders.push(loaders.style());
}
/*
TODO: (cvle) couldn't get build to work...
if (stage === "build-javascript") {
moreLoaders.push(loaders.style());
}
*/
actions.setWebpackConfig({
resolve: {
plugins: [
// Resolve our custom paths.
new TsconfigPathsPlugin({
extensions: [".ts", ".tsx", ".js"],
configFile: path.resolve(rootDir, "./src/core/client/tsconfig.json"),
}),
],
},
module: {
rules: [
{
include: srcDir,
oneOf: [
{
test: /\.css\.ts$/,
use: [
...prependCSSLoaders,
{
loader: require.resolve("css-loader"),
options: {
modules: {
localIdentName: "[name]-[local]-[hash:base64:5]",
},
importLoaders: 2,
sourceMap,
},
},
{
loader: require.resolve("postcss-loader"),
options: {
config: {
path: postCSSConfigPath,
},
parser: "postcss-js",
},
},
{
loader: require.resolve("babel-loader"),
options: {
configFile: false,
babelrc: false,
presets: [
"@babel/typescript",
[
"@babel/env",
{ targets: { node: "current" }, modules: "commonjs" },
],
],
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
},
},
],
},
{
test: /\.css$/,
use: [
...prependCSSLoaders,
{
loader: require.resolve("css-loader"),
options: {
modules: {
localIdentName: "[name]-[local]-[hash:base64:5]",
},
importLoaders: 1,
sourceMap,
},
},
{
loader: require.resolve("postcss-loader"),
options: {
config: {
path: postCSSConfigPath,
},
},
},
],
},
{
test: /\.tsx?$/,
use: [
{
loader: require.resolve("babel-loader"),
options: {
root: rootDir,
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
},
},
{
loader: require.resolve("ts-loader"),
options: {
configFile: appTsconfig,
compilerOptions: {
target: "es2015",
module: "esnext",
jsx: "preserve",
noEmit: false,
},
transpileOnly: true,
// Overwrites the behavior of `include` and `exclude` to only
// include files that are actually being imported and which
// are necessary to compile the bundle.
onlyCompileBundledFiles: true,
},
},
],
},
],
},
],
},
});
// Write out processed webpack config to .docz folder.
fs.writeFileSync(
path.resolve(__dirname, "webpack-" + stage + "-processed"),
JSON.stringify(getConfig(), {}, 2)
);
};
+16544 -35368
View File
File diff suppressed because it is too large Load Diff
+263 -336
View File
@@ -1,10 +1,9 @@
{
"name": "@coralproject/talk",
"version": "6.3.2",
"version": "5.2.2",
"author": "The Coral Project",
"homepage": "https://coralproject.net/",
"sideEffects": [
"*.css.ts",
"*.css"
],
"repository": {
@@ -12,24 +11,21 @@
"url": "git://github.com/coralproject/talk.git"
},
"engines": {
"node": "^12",
"npm": "^6"
"node": ">=10.0.0",
"npm": ">=6.9.0"
},
"bugs": "https://github.com/coralproject/talk/issues",
"contributors": [
"https://github.com/coralproject/talk/graphs/contributors"
],
"description": "A better commenting experience from Vox Media.",
"description": "A better commenting experience from Mozilla, The Washington Post, and The New York Times.",
"scripts": {
"clean": "gulp clean",
"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",
"docs:events": "ts-node ./scripts/generateEventDocs.ts ./src/core/client/stream/events.ts ./CLIENT_EVENTS.md",
"docs:css-variables": "ts-node ./scripts/generateCSSVariablesDocs.ts ./src/core/client/ui/theme/streamVariables.ts ./CSS_VARIABLES.md",
"doctoc": "doctoc --maxlevel=3 --title '## Table of Contents' CLIENT_EVENTS.md CONTRIBUTING.md WEBHOOKS.md EXTERNAL_MODERATION_PHASES.md",
"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",
"generate:css-types": "tcm src/core/client/",
@@ -43,15 +39,15 @@
"generate:schema": "node ./scripts/generateSchemaTypes.js",
"docz": "docz",
"start": "NODE_ENV=production node dist/index.js",
"start:development": "NODE_ENV=development TS_NODE_PROJECT=./src/tsconfig.json ts-node-dev --inspect --transpile-only --no-notify -r tsconfig-paths/register ./src/index.ts",
"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": "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/schema/schema.graphql",
"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 --trace-warnings scripts/test.js --env=jsdom",
"test": "node scripts/test.js --env=jsdom",
"tscheck": "npm-run-all --parallel tscheck:*",
"tscheck:server": "tsc --project ./src/tsconfig.json --noEmit",
"tscheck:client": "tsc --project ./src/core/client/tsconfig.json --noEmit",
@@ -61,361 +57,316 @@
"license": "Apache-2.0",
"dependencies": {
"@coralproject/bunyan-prettystream": "^0.1.4",
"@emotion/core": "^10.0.28",
"@fluent/bundle": "^0.15.0",
"@fluent/dom": "^0.6.0",
"@hapi/joi": "^17.1.1",
"@metascraper/helpers": "^5.11.6",
"@rudderstack/rudder-sdk-node": "0.0.2",
"abort-controller": "^3.0.0",
"akismet-api": "^5.0.0",
"apollo-server-core": "^2.14.4",
"apollo-server-express": "^2.14.2",
"archiver": "^3.1.1",
"akismet-api": "^4.2.0",
"apollo-server-express": "^2.8.1",
"archiver": "^3.0.3",
"basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3",
"bull": "^3.13.0",
"bull": "^3.8.1",
"bunyan": "^1.8.12",
"bytes": "^3.1.0",
"cheerio": "^1.0.0-rc.3",
"cheerio": "^1.0.0-rc.2",
"consolidate": "0.14.0",
"content-security-policy-builder": "^2.1.0",
"convict": "^5.2.0",
"content-security-policy-builder": "^2.0.0",
"convict": "^4.3.1",
"cookie": "^0.4.0",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"cron": "^1.8.2",
"csv-stringify": "^5.3.6",
"dataloader": "1.4.0",
"dompurify": "^2.0.8",
"dotenv": "^8.2.0",
"emailjs": "^3.2.0",
"cookie-parser": "^1.4.4",
"cors": "^2.8.4",
"cron": "^1.7.1",
"csv-stringify": "^5.3.0",
"dataloader": "^1.4.0",
"dompurify": "^1.0.8",
"dotenv": "^6.0.0",
"dotenv-expand": "^4.2.0",
"env-rewrite": "^1.0.2",
"express": "^4.17.1",
"express-enforces-ssl": "^1.1.0",
"express-static-gzip": "^2.0.6",
"fs-extra": "^8.1.0",
"graphql": "^14.6.0",
"graphql-config": "^2.2.2",
"graphql-extensions": "^0.11.0",
"graphql-fields": "2.0.3",
"graphql-playground-html": "1.6.13",
"graphql-redis-subscriptions": "^2.2.1",
"express": "^4.16.3",
"express-static-gzip": "^0.3.2",
"fluent": "^0.10.0",
"fluent-dom": "^0.4.1",
"fs-extra": "^6.0.1",
"graphql": "^0.13.2",
"graphql-config": "^2.0.1",
"graphql-extensions": "^0.2.1",
"graphql-fields": "^1.1.0",
"graphql-playground-html": "^1.6.0",
"graphql-redis-subscriptions": "^2.1.0",
"graphql-subscriptions": "^1.1.0",
"graphql-tools": "^4.0.7",
"helmet": "^3.22.0",
"html-minifier": "^4.0.0",
"html-to-text": "^5.1.1",
"ioredis": "^4.16.1",
"jsdom": "^16.2.2",
"jsonwebtoken": "^8.5.1",
"juice": "^6.0.0",
"jwks-rsa": "^1.7.0",
"linkifyjs": "^2.1.9",
"lodash": "^4.17.19",
"long-settimeout": "^1.0.1",
"graphql-tools": "^3.0.5",
"html-minifier": "^3.5.21",
"html-to-text": "^4.0.0",
"ioredis": "^4.9.0",
"joi": "^13.4.0",
"jsdom": "^15.0.0",
"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",
"luxon": "^1.22.2",
"metascraper-author": "^5.11.6",
"metascraper-description": "^5.11.6",
"metascraper-image": "^5.11.6",
"metascraper-title": "^5.11.6",
"mongodb": "^3.5.9",
"luxon": "^1.12.0",
"metascraper-author": "^3.11.8",
"metascraper-date": "^3.11.4",
"metascraper-description": "^3.11.8",
"metascraper-image": "^3.11.8",
"metascraper-title": "^3.11.8",
"mongodb": "^3.2.7",
"mongodb-core": "^3.2.7",
"ms": "^2.1.2",
"node-fetch": "^2.6.0",
"nunjucks": "^3.2.1",
"ms": "^2.1.1",
"node-fetch": "^2.2.0",
"nodemailer": "^4.6.7",
"nunjucks": "^3.1.3",
"on-finished": "^2.3.0",
"passport": "^0.4.1",
"passport-facebook": "^3.0.0",
"passport-google-oauth2": "^0.2.0",
"passport": "^0.4.0",
"passport-facebook": "^2.1.1",
"passport-google-oauth2": "^0.1.6",
"passport-local": "^1.0.0",
"passport-oauth2": "^1.5.0",
"passport-oauth2": "^1.4.0",
"passport-strategy": "^1.0.0",
"performance-now": "^2.1.0",
"permit": "^0.2.4",
"prom-client": "^12.0.0",
"proxy-agent": "^3.1.1",
"querystringify": "^2.1.1",
"source-map-support": "^0.5.16",
"stack-utils": "^2.0.1",
"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",
"throng": "^4.0.0",
"tsscmp": "^1.0.6",
"url-regex": "^5.0.0",
"uuid": "^7.0.3",
"verror": "^1.10.0",
"xregexp": "^4.3.0"
"uuid": "^3.3.3",
"verror": "^1.10.0"
},
"devDependencies": {
"@babel/core": "^7.10.3",
"@babel/preset-env": "^7.10.3",
"@babel/preset-react": "^7.10.1",
"@babel/preset-typescript": "^7.10.1",
"@babel/runtime-corejs3": "^7.10.3",
"@babel/core": "^7.4.5",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-modules-commonjs": "^7.4.4",
"@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.4.5",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.3.3",
"@coralproject/npm-run-all": "^4.1.5",
"@coralproject/rte": "^1.1.1",
"@fluent/react": "^0.11.1",
"@coralproject/rte": "^0.10.15",
"@intervolga/optimize-cssnano-plugin": "^1.0.6",
"@types/archiver": "^3.1.0",
"@types/basic-auth": "^1.1.3",
"@types/bcryptjs": "^2.4.2",
"@types/bull": "^3.12.1",
"@types/bunyan": "^1.8.6",
"@types/bytes": "^3.1.0",
"@types/case-sensitive-paths-webpack-plugin": "^2.1.4",
"@types/cheerio": "^0.22.17",
"@types/classnames": "^2.2.10",
"@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",
"@types/bunyan": "^1.8.4",
"@types/case-sensitive-paths-webpack-plugin": "^2.1.2",
"@types/cheerio": "^0.22.8",
"@types/classnames": "^2.2.7",
"@types/commander": "^2.12.2",
"@types/common-tags": "^1.8.0",
"@types/compression-webpack-plugin": "^2.0.1",
"@types/consolidate": "0.14.0",
"@types/convict": "^5.2.1",
"@types/compression-webpack-plugin": "^2.0.0",
"@types/consolidate": "0.0.34",
"@types/convict": "^4.2.0",
"@types/cookie": "^0.3.3",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.6",
"@types/cron": "^1.7.2",
"@types/cross-spawn": "^6.0.1",
"@types/dompurify": "^2.0.1",
"@types/enzyme": "^3.10.5",
"@types/enzyme-adapter-react-16": "^1.0.6",
"@types/cookie-parser": "^1.4.1",
"@types/cors": "^2.8.4",
"@types/cron": "^1.7.1",
"@types/cross-spawn": "^6.0.0",
"@types/dotenv": "^4.0.3",
"@types/enzyme": "^3.1.15",
"@types/enzyme-adapter-react-16": "^1.0.3",
"@types/eventemitter2": "^4.1.0",
"@types/express": "^4.17.4",
"@types/express-enforces-ssl": "^1.1.1",
"@types/express-serve-static-core": "^4.17.3",
"@types/flat": "^5.0.0",
"@types/fs-extra": "^8.1.0",
"@types/hapi__joi": "^16.0.12",
"@types/helmet": "^0.0.45",
"@types/html-minifier": "^3.5.3",
"@types/html-minifier-terser": "^5.0.0",
"@types/express": "^4.16.0",
"@types/flat": "0.0.28",
"@types/fs-extra": "^5.0.4",
"@types/graphql": "^0.13.3",
"@types/html-minifier": "^3.5.2",
"@types/html-to-text": "^1.4.31",
"@types/html-webpack-plugin": "^3.2.3",
"@types/ioredis": "^4.14.9",
"@types/jest": "^26.0.3",
"@types/jest-axe": "^3.2.2",
"@types/jsdom": "^16.2.0",
"@types/jsonwebtoken": "^8.3.8",
"@types/linkifyjs": "^2.1.3",
"@types/lodash": "^4.14.149",
"@types/html-webpack-plugin": "^3.2.0",
"@types/ioredis": "^4.0.10",
"@types/jest": "^24.0.13",
"@types/joi": "^13.0.8",
"@types/jsdom": "^12.2.3",
"@types/jsonwebtoken": "^7.2.7",
"@types/linkifyjs": "^2.1.1",
"@types/lodash": "^4.14.118",
"@types/lru-cache": "^5.1.0",
"@types/luxon": "^1.22.0",
"@types/marked": "^0.7.3",
"@types/mini-css-extract-plugin": "^0.9.1",
"@types/mongodb": "3.1.22",
"@types/ms": "^0.7.31",
"@types/node": "^12.12.34",
"@types/node-fetch": "^2.5.5",
"@types/nunjucks": "^3.1.3",
"@types/luxon": "^1.12.0",
"@types/marked": "^0.6.0",
"@types/mini-css-extract-plugin": "^0.2.0",
"@types/mongodb": "^3.1.22",
"@types/ms": "^0.7.30",
"@types/node": "^10.5.2",
"@types/node-fetch": "^2.3.3",
"@types/nodemailer": "^4.6.2",
"@types/nunjucks": "^3.1.1",
"@types/object-diff": "0.0.0",
"@types/on-finished": "^2.3.1",
"@types/passport": "^1.0.3",
"@types/passport-facebook": "^2.1.9",
"@types/passport": "^0.4.6",
"@types/passport-facebook": "^2.1.8",
"@types/passport-local": "^1.0.33",
"@types/passport-oauth2": "^1.4.8",
"@types/passport-strategy": "^0.2.35",
"@types/passport-oauth2": "^1.4.5",
"@types/passport-strategy": "^0.2.33",
"@types/permit": "^0.2.1",
"@types/prettier": "^1.19.1",
"@types/react": "^16.9.31",
"@types/react-axe": "^3.1.0",
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-dom": "^16.9.6",
"@types/react-helmet": "^5.0.15",
"@types/react-relay": "^7.0.3",
"@types/react-responsive": "^8.0.2",
"@types/react-test-renderer": "^16.9.2",
"@types/react-transition-group": "^4.2.4",
"@types/recharts": "^1.8.9",
"@types/recompose": "^0.30.7",
"@types/relay-runtime": "^8.0.7",
"@types/prop-types": "^15.5.8",
"@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",
"@types/react-transition-group": "^2.0.14",
"@types/recompose": "^0.26.5",
"@types/relay-runtime": "^1.3.12",
"@types/sane": "^2.0.0",
"@types/shallow-equals": "^1.0.0",
"@types/simplemde": "^1.11.7",
"@types/sinon": "^7.5.2",
"@types/source-map-support": "^0.5.1",
"@types/stack-trace": "0.0.29",
"@types/sinon": "^7.0.11",
"@types/source-map-support": "^0.5.0",
"@types/stack-utils": "^1.0.1",
"@types/uuid": "^7.0.2",
"@types/throng": "^4.0.2",
"@types/uuid": "^3.4.4",
"@types/verror": "^1.10.3",
"@types/vinyl": "^2.0.4",
"@types/webpack": "^4.41.17",
"@types/webpack-assets-manifest": "^3.0.1",
"@types/webpack-bundle-analyzer": "^3.8.0",
"@types/webpack-dev-server": "^3.11.0",
"@types/ws": "^7.2.3",
"@types/xregexp": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^3.4.0",
"@typescript-eslint/eslint-plugin-tslint": "^3.4.0",
"@typescript-eslint/parser": "^3.4.0",
"autoprefixer": "^9.7.5",
"@types/vinyl": "^2.0.2",
"@types/webpack": "^4.4.31",
"@types/webpack-assets-manifest": "^3.0.0",
"@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",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^26.1.0",
"babel-loader": "^8.1.0",
"babel-jest": "^24.8.0",
"babel-loader": "^8.0.5",
"babel-plugin-dynamic-import-node": "^2.2.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-module-resolver": "^4.0.0",
"babel-plugin-relay": "^9.1.0",
"babel-plugin-module-resolver": "^3.2.0",
"babel-plugin-relay": "^4.0.0",
"babel-plugin-use-lodash-es": "^0.2.0",
"bowser": "^2.9.0",
"case-sensitive-paths-webpack-plugin": "^2.3.0",
"chalk": "^3.0.0",
"chokidar": "^3.3.1",
"babel-preset-react-optimize": "^1.0.1",
"bowser": "^1.9.4",
"case-sensitive-paths-webpack-plugin": "^2.2.0",
"chalk": "^2.4.2",
"chokidar": "^3.0.0",
"classnames": "^2.2.6",
"commander": "^5.0.0",
"comment-json": "^3.0.2",
"commander": "^2.20.0",
"comment-json": "^1.1.3",
"common-tags": "^1.8.0",
"compression-webpack-plugin": "^3.1.0",
"core-js": "^3.6.4",
"cross-spawn": "^7.0.1",
"css-loader": "^3.4.2",
"css-vars-ponyfill": "^2.2.1",
"del": "^5.1.0",
"compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^5.0.3",
"cross-spawn": "^6.0.5",
"css-loader": "^1.0.1",
"del": "^4.1.1",
"doctoc": "^1.4.0",
"docz": "^v2.3.0-alpha.14",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "^3.5.0",
"eslint": "^7.3.1",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-jsdoc": "^28.5.1",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^4.0.8",
"eventemitter2": "^6.3.1",
"farce": "^0.2.8",
"final-form": "4.18.6",
"final-form-arrays": "^3.0.2",
"flat": "^5.0.0",
"docz": "^0.13.7",
"docz-theme-default": "^0.13.7",
"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",
"fork-ts-checker-webpack-plugin": "^4.1.2",
"found": "^0.4.10",
"found-relay": "^0.7.0",
"graphql-schema-linter": "^0.4.0",
"graphql-schema-typescript": "^1.3.2",
"fluent-langneg": "^0.1.1",
"fluent-react": "^0.8.4",
"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",
"gulp-babel": "^8.0.0",
"gulp-cli": "^2.3.0",
"gulp-cli": "^2.2.0",
"gulp-sourcemaps": "^2.6.5",
"gulp-typescript": "^6.0.0-alpha.1",
"html-webpack-plugin": "^4.0.4",
"husky": "^4.2.3",
"intersection-observer": "^0.7.0",
"intl": "^1.2.5",
"jest": "^26.1.0",
"jest-axe": "^3.4.0",
"jest-junit": "^11.0.1",
"jest-localstorage-mock": "^2.4.2",
"jest-mock-console": "^1.0.1",
"keymaster": "^1.6.2",
"lint-staged": "^10.2.11",
"loader-utils": "^2.0.0",
"lodash-es": "^4.17.15",
"marked": "^0.8.2",
"gulp-typescript": "^5.0.1",
"html-webpack-plugin": "^4.0.0-beta.5",
"husky": "^2.2.0",
"intersection-observer": "^0.6.0",
"jest": "^24.8.0",
"jest-junit": "^6.4.0",
"jest-localstorage-mock": "^2.4.0",
"jest-mock-console": "^1.0.0",
"lint-staged": "^8.1.6",
"loader-utils": "^1.2.3",
"lodash-es": "^4.17.14",
"marked": "^0.7.0",
"material-design-icons": "^3.0.1",
"mini-css-extract-plugin": "^0.9.0",
"postcss-advanced-variables": "^3.0.1",
"postcss-calc-function": "^1.1.0",
"postcss-flexbugs-fixes": "^4.2.0",
"postcss-font-magician": "^2.3.1",
"postcss-import": "^12.0.1",
"postcss-js": "^2.0.3",
"mini-css-extract-plugin": "^0.6.0",
"object-diff": "0.0.4",
"postcss-advanced-variables": "^3.0.0",
"postcss-css-variables": "^0.11.0",
"postcss-flexbugs-fixes": "^4.1.0",
"postcss-font-magician": "^2.2.1",
"postcss-import": "^11.1.0",
"postcss-js": "^2.0.1",
"postcss-loader": "^3.0.0",
"postcss-mixins": "^6.2.3",
"postcss-nested": "^4.2.1",
"postcss-mixins": "^6.2.1",
"postcss-nested": "^4.1.1",
"postcss-prepend-imports": "^1.0.1",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.0.2",
"proxy-polyfill": "^0.3.1",
"pstree.remy": "^1.1.7",
"postcss-preset-env": "^6.5.0",
"prettier": "^1.18.2",
"prop-types": "^15.6.2",
"pstree.remy": "^1.1.6",
"pym.js": "^1.3.2",
"raw-loader": "^4.0.0",
"react": "^16.13.1",
"react-axe": "^3.4.1",
"react-copy-to-clipboard": "^5.0.2",
"react-dev-utils": "^10.2.1",
"react-dom": "^16.13.1",
"react-error-overlay": "^6.0.7",
"react-final-form": "6.3.0",
"react-final-form-arrays": "^3.1.1",
"react-helmet": "^5.2.1",
"react-popper": "^1.3.7",
"react-relay": "^9.0.0",
"react-relay-network-modern": "^4.6.1",
"react-responsive": "^8.0.3",
"react-test-renderer": "^16.13.1",
"react-timeago": "^4.4.0",
"react-transition-group": "^4.3.0",
"recharts": "^1.8.5",
"recompose": "^0.30.0",
"regenerator-runtime": "^0.13.5",
"relay-compiler": "^9.0.0",
"relay-compiler-language-typescript": "^12.0.3",
"raw-loader": "^0.5.1",
"react": "^16.9.0-alpha.0",
"react-copy-to-clipboard": "^5.0.1",
"react-dev-utils": "^9.0.0",
"react-dom": "^16.9.0-alpha.0",
"react-error-overlay": "^5.1.6",
"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",
"react-transition-group": "^2.9.0",
"react-with-state-props": "^2.0.4",
"recompose": "0.27.1",
"relay-compiler": "^4.0.0",
"relay-compiler-language-typescript": "^4.1.0",
"relay-local-schema": "^0.8.0",
"relay-runtime": "^9.0.0",
"relay-runtime": "^4.0.0",
"resize-observer-polyfill": "^1.5.1",
"sane": "^4.1.0",
"shallow-equals": "^1.0.0",
"simplemde": "^1.11.2",
"simulant": "^0.2.2",
"sinon": "^9.0.1",
"sockjs-client": "^1.4.0",
"stack-trace": "^0.0.10",
"strip-ansi": "^6.0.0",
"style-loader": "^1.1.3",
"sinon": "^7.3.2",
"sockjs-client": "^1.3.0",
"strip-ansi": "^5.2.0",
"style-loader": "^0.23.1",
"subscriptions-transport-ws": "^0.9.16",
"terser-webpack-plugin": "^2.3.5",
"thread-loader": "^2.1.3",
"terser-webpack-plugin": "^1.2.3",
"thread-loader": "^2.1.2",
"timekeeper": "^2.2.0",
"ts-jest": "26.1.1",
"ts-loader": "^7.0.5",
"ts-node": "^8.10.2",
"ts-node-dev": "1.0.0-pre.44",
"tsconfig-paths": "^3.9.0",
"ts-jest": "<23.10.0",
"ts-loader": "^6.0.0",
"ts-node": "^8.1.0",
"ts-node-dev": "^1.0.0-pre.37",
"tsconfig-paths": "^3.8.0",
"tsconfig-paths-webpack-plugin": "^3.2.0",
"tslint": "^6.1.2",
"typed-css-modules": "^0.6.4",
"typeface-manuale": "^1.1.4",
"typeface-nunito": "^1.1.3",
"typeface-open-sans": "^0.0.75",
"typeface-source-sans-pro": "^1.1.5",
"typescript": "^3.9.5",
"typescript-snapshots-plugin": "^1.7.0",
"wait-for-expect": "^1.3.0",
"webpack": "^4.43.0",
"tslint": "^5.20.0",
"typed-css-modules": "^0.4.2",
"typeface-manuale": "^0.0.71",
"typeface-source-sans-pro": "^0.0.54",
"typescript": "3.3.4000",
"typescript-snapshots-plugin": "^1.6.0",
"wait-for-expect": "^1.1.1",
"webpack": "^4.30.0",
"webpack-assets-manifest": "^3.1.1",
"webpack-bundle-analyzer": "^3.8.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0",
"whatwg-fetch": "^3.4.0"
},
"dependencies-pins-documentation": {
"ts-node-dev@1.0.0-pre.44": [
"Newer versions unbearibly slow"
],
"ts-jest@25.4.0": [
"Newer versions require every definition file to be included",
"in tsconfig.json in the `files` field. This is cumbersome and",
"here is a discussion of it: https://github.com/kulshekhar/ts-jest/issues/1604"
],
"wait-for-expect@1.x.x": [
"Newer versions breaks the use of jest fake timers"
],
"consolidate@14.0.0": [
"Newer versions has problems with using nunjucks.",
"Update when the following issue is resolved:",
"https://github.com/tj/consolidate.js/issues/244"
],
"final-form@4.18.6": [
"Newer versions has problems with resetting forms.",
"Update when the following issue is resolved:",
"https://github.com/final-form/final-form/issues/317"
],
"react-final-form@6.3.0": [
"Newer versions has problems with radio and checkbox fields.",
"Update when the following issue is resolved:",
"https://github.com/final-form/react-final-form/issues/683"
],
"graphql-playground-html@1.6.13": [
"A template was broken in a release.",
"Update when the following issue is resolved:",
"https://github.com/prisma-labs/graphql-playground/pull/1238"
]
"webpack-bundle-analyzer": "^3.3.2",
"webpack-cli": "^3.3.2",
"webpack-dev-server": "3.2.1",
"whatwg-fetch": "^3.0.0"
},
"husky": {
"hooks": {
@@ -426,43 +377,19 @@
"*.{j,t}s{,x}": [
"eslint"
],
"src/core/server/graph/schema/schema.graphql": [
"src/core/server/graph/tenant/schema/schema.graphql": [
"graphql-schema-linter"
],
"{src/core/client/stream/events.ts,scripts/generateEventDocs.ts,CLIENT_EVENTS.md}": [
"npm run docs:events -- --verify"
],
"{src/core/client/ui/theme/sharedVariables.ts,src/core/client/ui/theme/streamVariables.ts,scripts/generateCSSVariablesDocs.ts,CSS_VARIABLES.md}": [
"npm run docs:css-variables -- --verify"
],
"{CLIENT_EVENTS,CONTRIBUTING,WEBHOOKS,EXTERNAL_MODERATION_PHASES}.md": [
"npm run doctoc"
]
},
"bundlesize": [
{
"path": "./dist/static/assets/js/embed.js",
"maxSize": "15 kB"
},
{
"path": "./dist/static/assets/js/count.js",
"maxSize": "2 kB"
}
],
"graphql-schema-linter": {
"rules": [
"types-are-capitalized"
]
},
"browsers": [
">1%",
"last 4 versions",
"IE 11",
"iOS >= 9",
"Android >= 4.4.4",
"Edge >= 17",
"Firefox >= 68",
"Chrome >= 49",
"not dead"
]
}
}
+11 -13
View File
@@ -27,14 +27,14 @@ const forkTsCheckerWebpackPlugin = require("react-dev-utils/ForkTsCheckerWebpack
const isInteractive = false;
function prepareUrls(protocol, host, port) {
const formatUrl = (hostname) =>
const formatUrl = hostname =>
url.format({
protocol,
hostname,
port,
pathname: "/",
});
const prettyPrintUrl = (hostname) =>
const prettyPrintUrl = hostname =>
url.format({
protocol,
hostname,
@@ -144,12 +144,12 @@ function createCompiler({
const tsMessagesPromises = [];
if (useTypeScript) {
compiler.compilers.forEach((singleCompiler) => {
compiler.compilers.forEach(singleCompiler => {
let tsMessagesPromise;
let tsMessagesResolver;
singleCompiler.hooks.beforeCompile.tap("beforeCompile", () => {
tsMessagesPromise = new Promise((resolve) => {
tsMessagesResolver = (msgs) => resolve(msgs);
tsMessagesPromise = new Promise(resolve => {
tsMessagesResolver = msgs => resolve(msgs);
});
tsMessagesPromises.push(tsMessagesPromise);
});
@@ -170,15 +170,13 @@ function createCompiler({
(diagnostics, lints) => {
console.log("RECEIVED");
const allMsgs = [...diagnostics, ...lints];
const format = (message) =>
const format = message =>
`${message.file}\n${typescriptFormatter(message, true)}`;
tsMessagesResolver({
errors: allMsgs
.filter((msg) => msg.severity === "error")
.map(format),
errors: allMsgs.filter(msg => msg.severity === "error").map(format),
warnings: allMsgs
.filter((msg) => msg.severity === "warning")
.filter(msg => msg.severity === "warning")
.map(format),
});
}
@@ -188,7 +186,7 @@ function createCompiler({
// "done" event fires when Webpack has finished recompiling the bundle.
// Whether or not you have warnings or errors, you will get this event.
compiler.hooks.done.tap("done", async (stats) => {
compiler.hooks.done.tap("done", async stats => {
if (isInteractive) {
clearConsole();
}
@@ -205,7 +203,7 @@ function createCompiler({
...config.stats,
};
if (Array.isArray(config)) {
statOptions.children = config.map((c) => c.stats || {});
statOptions.children = config.map(c => c.stats || {});
}
const statsData = stats.toJson(statOptions);
@@ -222,7 +220,7 @@ function createCompiler({
const results = await Promise.all(tsMessagesPromises);
clearTimeout(delayedMsg);
results.forEach((msgs) => {
results.forEach(msgs => {
statsData.errors.push(...msgs.errors);
statsData.warnings.push(...msgs.warnings);
+2 -4
View File
@@ -31,7 +31,7 @@ const isProduction = process.env.NODE_ENV === "production";
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on("unhandledRejection", (err) => {
process.on("unhandledRejection", err => {
throw err;
});
@@ -117,9 +117,7 @@ function build(previousFileSizes: any) {
return reject(err);
}
const messages = formatWebpackMessages(
stats.toJson({
children: webpackConfig.map((c) => c.stats || {}) as any,
})
stats.toJson({ children: webpackConfig.map(c => c.stats || {}) as any })
);
if (messages.errors.length) {
// Only keep the first error. Others are often indicative
+1 -4
View File
@@ -52,9 +52,6 @@ const args = [
"--language",
"typescript",
"--no-watchman",
"--customScalars.Time=String",
"--customScalars.Cursor=unknown",
"--customScalars.Locale=string",
"--src",
program.src,
"--artifactDirectory",
@@ -77,7 +74,7 @@ if (persist) {
if (fs.existsSync(persist)) {
// Create the new filename.
const name = path.basename(program.src);
const generated = "./src/core/server/graph/persisted/__generated__";
const generated = "./src/core/server/graph/common/persisted/__generated__";
// Create the generated directory if it doesn't exist.
fs.ensureDirSync(generated);
-306
View File
@@ -1,306 +0,0 @@
import { stripIndent } from "common-tags";
import fs from "fs";
import { kebabCase, trim } from "lodash";
import path from "path";
import ts from "typescript";
import colors from "../src/core/client/ui/theme/colors";
/** We collect inforamtion from the AST and put it into DocEntry */
interface DocEntry {
/** Name of property */
key: string;
/** Documentation if available */
docs?: string;
/** Value if it is a leaf */
value?: string;
/** Children if it is a node */
children?: DocEntry[];
}
/**
* We use this regexp to find a previous block that we
* are going to update in the readme file.
*/
const BLOCK_REGEXP = /<!-- START docs:css-variables -->(.|\n)*<!-- END docs:css-variables -->/gm;
/** Sort doc entries will sort childrenless entries first */
function sortDocEntries(data: DocEntry[]) {
data.forEach((d) => {
if (d.children) {
d.children = sortDocEntries(d.children);
}
});
return data.sort((a, b) => {
if (a.children && !b.children) {
return 1;
}
if (b.children && !a.children) {
return -1;
}
return 0;
});
}
/** Generate documentation for all classes in a set of .ts files */
function gatherEntries(
fileName: string,
options: ts.CompilerOptions
): DocEntry[] {
// Build a program using the set of root file names in fileNames
const program = ts.createProgram([fileName], options);
// Get the checker, we will use it to find more about classes
const checker = program.getTypeChecker();
const data: DocEntry[] = [];
const currentSourceFile = program.getSourceFile(fileName)!;
ts.forEachChild(currentSourceFile, visit);
return sortDocEntries(data);
/** visit nodes finding css variables */
function visit(node: ts.Node) {
if (ts.isVariableStatement(node)) {
// TODO (cvle) - Currently the variable name is hardcoded. We might want to change that.
if (!node.getFullText().includes("streamVariables")) {
return;
}
const firstChild = node.declarationList.declarations[0];
if (ts.isVariableDeclaration(firstChild)) {
const symbol = checker.getSymbolAtLocation(firstChild.name);
if (symbol) {
const type = checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration
);
type.getProperties().forEach((property) => {
serializePropertySymbol(property);
});
}
}
}
}
/** This will evaluate `symbol` and addthe doc entries */
function serializePropertySymbol(symbol: ts.Symbol, parent?: DocEntry) {
const entry: DocEntry = { key: symbol.name };
const pt = checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration
);
if (pt.symbol?.name === "__object") {
pt.getProperties().forEach((p2) => {
serializePropertySymbol(p2, entry);
});
} else {
entry.value = symbol.valueDeclaration
// Last child contains value.
.getChildAt(symbol.valueDeclaration.getChildCount() - 1)!
.getFullText();
}
entry.docs = ts.displayPartsToString(
symbol.getDocumentationComment(checker)
);
if (parent) {
if (parent.children) {
parent.children.push(entry);
} else {
parent.children = [entry];
}
} else {
data.push(entry);
}
}
}
/**
* Finds any references to `object` by `variableName` and replaces the text
* with the actual value.
*
* E.g. `replaceObjectVariablesInText("colors.teal100", colors, "colors") === "#E2FAF7"`;
*/
function replaceObjectVariablesInText(
text: string,
object: any,
variableName: string
) {
let result = text;
Object.keys(object).forEach((c: string) => {
if (typeof object[c] === "object") {
result = replaceObjectVariablesInText(
result,
object[c],
`${variableName}.${c}`
);
} else {
result = result.replace(`${variableName}.${c}`, object[c]);
}
});
return result;
}
/**
* transforms value to be used in the documentation.
*/
function transformValue(value: string) {
let compat = "";
value = trim(value);
// Detect compat value.
if (value.startsWith("compat")) {
const result = /compat\((.*), *"(.*)"\)/.exec(value);
if (!result) {
throw new Error("Unrecognized compat format");
}
value = result[1];
compat = result[2];
}
// If it's a raw string, evaluate it to get rid of the initial quotes.
if (value[0] === '"' || value[0] === "'") {
// eslint-disable-next-line no-eval
value = eval(value);
}
// Replace all references to colors.
value = replaceObjectVariablesInText(value, colors, "colors");
if (compat) {
// add compat information.
return `${value}; /* Before 6.3.0: --${compat} */`;
}
return `${value};`;
}
function prefixLines(text: string, prefix: string) {
return text.split("\n").join(`\n${prefix}`);
}
function entries2Summary(
entries: DocEntry[],
keyprefix = "",
nestprefix = ""
): string {
let doc = "";
entries.forEach((entry) => {
if (!entry.children) {
return;
}
const header = kebabCase(keyprefix + entry.key);
doc += `${nestprefix}- <a href="#${header}">${header}</a>\n`;
if (entry.children) {
doc += entries2Summary(
entry.children,
keyprefix + entry.key + "-",
nestprefix + " "
);
}
});
return doc;
}
function entries2Doc(entries: DocEntry[], header = "###", prefix = ""): string {
let doc = "";
entries.forEach((entry) => {
if (entry.children) {
doc += `\n${header} ${kebabCase(prefix + entry.key)}\n`;
if (entry.docs) {
doc += `\n${entry.docs}\n`;
}
doc += entries2Doc(
entry.children,
header + "#",
`${prefix}${entry.key}-`
);
} else {
if (entry.docs) {
doc += `\n${entry.docs}\n`;
}
doc += `\n\`--${kebabCase(prefix + entry.key)}: ${transformValue(
entry.value!
)}\`\n`;
}
});
return doc;
}
/**
* Append or update previous documention in markdownFile.
*
* @param markdownFile The markdown file we want to inject the docs too.
* @param entries data as returned by gatherEntries.
*/
function emitDocs(markdownFile: string, entries: DocEntry[], verify = false) {
const previousContent = fs.existsSync(markdownFile)
? fs.readFileSync(markdownFile).toString()
: "";
const summary = entries2Summary(entries, "", " ");
const list = entries2Doc(entries);
const output = stripIndent`
<!-- START docs:css-variables -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN npm run docs:css-variables -->
### Index
- <a href="#variables">Variables</a>
${prefixLines(summary, " ")}
### Variables
${prefixLines(list, " ")}
<!-- END docs:css-variables -->
`;
let newContent;
// Find previous block.
if (BLOCK_REGEXP.test(previousContent)) {
newContent = previousContent.replace(BLOCK_REGEXP, output);
} else {
newContent = previousContent + "\n" + output;
}
if (previousContent === newContent) {
// eslint-disable-next-line no-console
console.log(`${markdownFile} is up to date`);
return;
}
if (verify) {
// eslint-disable-next-line no-console
console.error(
`${markdownFile} is outdated, please run \`npm run docs:css-variables\``
);
process.exit(1);
}
fs.writeFileSync(markdownFile, newContent);
// eslint-disable-next-line no-console
console.log(`Successfully injected documentation into ${markdownFile}`);
}
function main() {
if (process.argv.length < 4) {
throw new Error("Must provide path to css variables and a markdown file.");
}
const variableFile = process.argv[2];
const markdownFile = process.argv[3];
// Find tsconfig file.
const configFile = ts.findConfigFile(variableFile, fs.existsSync);
if (!configFile) {
throw new Error("tsconfig file not found");
}
const configText = fs.readFileSync(configFile).toString();
const result = ts.parseConfigFileTextToJson(configFile, configText);
if (result.error) {
throw result.error;
}
// Parse the JSON raw data into actual consumable compiler options.
const config = ts.parseJsonConfigFileContent(
result.config,
ts.sys,
path.dirname(configFile)
);
const entries = gatherEntries(variableFile, config.options);
emitDocs(markdownFile, entries, process.argv[4] === "--verify");
}
main();
-277
View File
@@ -1,277 +0,0 @@
/* eslint-disable no-bitwise */
import { codeBlock, stripIndent } from "common-tags";
import fs from "fs";
import path from "path";
import ts from "typescript";
interface DocEntry {
name: string;
docs?: string;
type: "ViewerNetworkEvent" | "ViewerEvent";
text?: string;
}
/**
* We use this regexp to find a previous block that we
* are going to update in the readme file.
*/
const BLOCK_REGEXP = /<!-- START docs:events -->(.|\n)*<!-- END docs:events -->/gm;
/** Build flags that affects AST generation */
const buildFlags =
// Do not truncate output.
ts.NodeBuilderFlags.NoTruncation |
// Use multiline object literals format.
ts.NodeBuilderFlags.MultilineObjectLiterals;
/** Generate documentation for all classes in a set of .ts files */
function gatherEntries(
fileNames: string[],
options: ts.CompilerOptions
): DocEntry[] {
// Build a program using the set of root file names in fileNames
const program = ts.createProgram(fileNames, options);
const printer = ts.createPrinter({
noEmitHelpers: true,
omitTrailingSemicolon: true,
removeComments: false,
});
// Get the checker, we will use it to find more about classes
const checker = program.getTypeChecker();
const data: DocEntry[] = [];
/** Hold a pointer to the sourcefile we are currently processing. */
let currentSourceFile: ts.SourceFile;
// Visit every sourceFile in the program
for (const sourceFile of program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile) {
currentSourceFile = sourceFile;
// Walk the tree to search for classes
ts.forEachChild(sourceFile, visit);
}
}
const sorted = data.sort((a, b) => {
if (a.name > b.name) {
return 1;
}
if (b.name > a.name) {
return -1;
}
return 0;
});
return sorted;
/** visit nodes finding exported events */
function visit(node: ts.Node) {
// Only consider exported nodes
if (!isNodeExported(node)) {
return;
}
if (ts.isVariableStatement(node)) {
if (
!node.getFullText().includes("createViewerNetworkEvent") &&
!node.getFullText().includes("createViewerEvent")
) {
return;
}
const firstChild = node.declarationList.declarations[0];
if (ts.isVariableDeclaration(firstChild)) {
const symbol = checker.getSymbolAtLocation(firstChild.name);
if (symbol) {
serializeEventSymbol(symbol);
}
}
}
}
function serializeEventSymbol(symbol: ts.Symbol) {
const type = checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration
);
const typeNode = checker.typeToTypeNode(type, undefined, buildFlags)!;
const typeName = symbol.getName();
const entry: DocEntry = {
name: typeName,
docs: ts.displayPartsToString(symbol.getDocumentationComment(checker)),
type: type.getSymbol()!.getName() as DocEntry["type"],
};
typeNode.forEachChild((ch) => {
if (ts.isTypeLiteralNode(ch)) {
const text = printer.printNode(
ts.EmitHint.Unspecified,
ch,
currentSourceFile
);
if (text !== "{}") {
entry.text = text;
}
/*
Go through each parameter.
ch.members.forEach(m => {
if (ts.isPropertySignature(m)) {
if (ts.isIdentifier(m.name)) {
data.parameters[m.name.text] = printer.printNode(
ts.EmitHint.Unspecified,
m.type!,
currentSourceFile
);
}
}
});
*/
}
});
data.push(entry);
}
/** True if this is visible outside this file, false otherwise */
function isNodeExported(node: ts.Node): boolean {
return (
// eslint-disable-next-line no-bitwise, @typescript-eslint/no-unnecessary-type-assertion
(ts.getCombinedModifierFlags(node as ts.Declaration) &
ts.ModifierFlags.Export) !==
0 ||
(!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
);
}
}
function prefixLines(text: string, prefix: string) {
return text.split("\n").join(`\n${prefix}`);
}
function getEventName(typeName: string) {
return (
typeName[0].toLocaleLowerCase() +
typeName.slice(1, typeName.length - "Event".length)
);
}
/**
* Removes "%future added value" from text. This is a placeholder type
* added by Relay to help with future proofness.
*/
function removeFutureAddedValue(text: string) {
return text
.replace(': "%future added value" | ', ": ")
.replace(' | "%future added value"', "");
}
/**
* Append or update previous documention in markdownFile.
*
* @param markdownFile The markdown file we want to inject the docs too.
* @param entries data as returned by gatherEntries.
*/
function emitDocs(markdownFile: string, entries: DocEntry[], verify = false) {
const previousContent = fs.existsSync(markdownFile)
? fs.readFileSync(markdownFile).toString()
: "";
const summary = stripIndent`
- ${entries
.map(
(e) => `<a href="#${getEventName(e.name)}">${getEventName(e.name)}</a>`
)
.join("\n - ")}
`;
const list = entries
.map(
(e) =>
codeBlock`
- ${
e.type === "ViewerEvent"
? `<a id="${getEventName(e.name)}">**${getEventName(e.name)}**</a>`
: `<a id="${getEventName(e.name)}">**${getEventName(
e.name
)}.success**, **${getEventName(e.name)}.error**</a>`
}: ${e.docs ? e.docs.replace("\n", " ") : ""}
${
e.text
? codeBlock`
\`\`\`ts
${removeFutureAddedValue(e.text)}
\`\`\`
`
: ""
}
`
)
.join("\n");
const output = stripIndent`
<!-- START docs:events -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN npm run docs:events -->
### Index
${prefixLines(summary, " ")}
### Events
${prefixLines(list, " ")}
<!-- END docs:events -->
`;
let newContent;
// Find previous block.
if (BLOCK_REGEXP.test(previousContent)) {
newContent = previousContent.replace(BLOCK_REGEXP, output);
} else {
newContent = previousContent + "\n" + output;
}
if (previousContent === newContent) {
// eslint-disable-next-line no-console
console.log(`${markdownFile} is up to date`);
return;
}
if (verify) {
// eslint-disable-next-line no-console
console.error(
`${markdownFile} is outdated, please run \`npm run docs:events\``
);
process.exit(1);
return;
}
fs.writeFileSync(markdownFile, newContent);
// eslint-disable-next-line no-console
console.log(`Successfully injected documentation into ${markdownFile}`);
}
function main() {
if (process.argv.length < 4) {
throw new Error("Must provide path to events and a markdown file.");
}
const eventFile = process.argv[2];
const markdownFile = process.argv[3];
// Find tsconfig file.
const configFile = ts.findConfigFile(eventFile, fs.existsSync);
if (!configFile) {
throw new Error("tsconfig file not found");
}
const configText = fs.readFileSync(configFile).toString();
const result = ts.parseConfigFileTextToJson(configFile, configText);
if (result.error) {
throw result.error;
}
// Parse the JSON raw data into actual consumable compiler options.
const config = ts.parseJsonConfigFileContent(
result.config,
ts.sys,
path.dirname(configFile)
);
const entries = gatherEntries([eventFile], config.options);
emitDocs(markdownFile, entries, process.argv[4] === "--verify");
}
main();
+6 -6
View File
@@ -12,12 +12,12 @@ async function main() {
name: "tenant",
fileName: path.join(
__dirname,
"../src/core/server/graph/schema/__generated__/types.ts"
"../src/core/server/graph/tenant/schema/__generated__/types.ts"
),
config: {
contextType: "GraphContext",
contextType: "TenantContext",
importStatements: [
'import GraphContext from "coral-server/graph/context";',
'import TenantContext from "coral-server/graph/tenant/context";',
'import { Cursor } from "coral-server/models/helpers";',
],
customScalarType: { Cursor: "Cursor", Time: "Date" },
@@ -47,7 +47,7 @@ async function main() {
}
// Create the types for this file.
const types = await generateTSTypesAsString(schema, file.fileName, {
const types = await generateTSTypesAsString(schema, {
tabSpaces: 2,
typePrefix: "GQL",
strictNulls: false,
@@ -66,13 +66,13 @@ if (require.main === module) {
// Only run the main module on file load if this is the main module (we're
// executing this file directly).
main()
.then((files) => {
.then(files => {
for (const { fileName } of files) {
// eslint-disable-next-line no-console
console.log(`Generated ${fileName}`);
}
})
.catch((err) => {
.catch(err => {
// eslint-disable-next-line no-console
console.error(err);
});
-153
View File
@@ -1,153 +0,0 @@
#!/usr/bin/env ./node_modules/.bin/tsnd
//
// Based on the script written by @cristiandean
//
// Source: https://gist.github.com/cristiandean/8196bc2b965f9acf9ad8f9e4530011c1
//
import { FluentResource } from "@fluent/bundle";
import fs from "fs";
import path from "path";
const PROJECT_ROOT = path.resolve(__dirname, "..", "..");
const LOCALE_DIRECTORIES = [
path.join(PROJECT_ROOT, "src/locales"),
path.join(PROJECT_ROOT, "src/core/server/locales"),
];
const wrap = (color: string) => (msg: string) => {
if (!msg) {
return;
}
// eslint-disable-next-line no-console
console.log(`${color}%s\x1b[0m`, msg);
};
const log = {
red: wrap("\x1b[31m"),
green: wrap("\x1b[32m"),
plain: wrap("\x1b[0m"),
};
interface LocaleFileListing {
locale: string;
files: LocaleFile[];
}
interface LocaleFile {
fileName: string;
directory: string;
filePath: string;
keys: Set<string>;
}
function loadLocale(locale: string) {
const listing: LocaleFileListing = { locale, files: [] };
for (const directory of LOCALE_DIRECTORIES) {
// Resolve the directory with the language code, this should be a folder
// containing language files for the specified context.
const localeDirectory = path.join(directory, locale);
if (!fs.existsSync(localeDirectory)) {
continue;
}
// The folder exists, list all files in this directory and begin loading
// them.
const localeFiles = fs.readdirSync(localeDirectory);
for (const fileName of localeFiles) {
const filePath = path.join(localeDirectory, fileName);
// Load the file.
const source = fs.readFileSync(filePath, "utf-8").toString();
// Create the resource based on the file.
const resource = new FluentResource(source);
// Iterate over the ids to create the set of ID's.
const keys = new Set<string>();
for (const entry of resource.body) {
if (entry.id.startsWith("-")) {
// Identifiers starting with a `-` define terms, these are not used
// for this as we're only comparing messages.
continue;
}
keys.add(entry.id);
}
// Add the new file.
listing.files.push({ fileName, directory, filePath, keys });
}
}
return listing;
}
function diffSet(a: Set<string>, b: Set<string>): Set<string> {
const n = new Set(a);
for (const e of b) {
n.delete(e);
}
return n;
}
function prefixDiffSet(prefix: string, a: Set<string>, b: Set<string>) {
return [...diffSet(a, b).values()]
.map((value) => `${prefix}${value}`)
.join("\n");
}
function diff(from: LocaleFileListing, to: LocaleFileListing) {
for (const f of from.files) {
// Log the header information for this file.
log.plain(`* From: ${f.filePath}`);
// Find the associated "to" file.
const t = to.files.find(
(file) => file.fileName === f.fileName && file.directory === f.directory
);
if (!t) {
log.red(
`* To: ${path.join(f.directory, to.locale, f.fileName)} (missing)`
);
log.plain("-".repeat(60));
continue;
}
log.plain(`* To: ${t.filePath}`);
log.green(prefixDiffSet(" + ", f.keys, t.keys));
log.red(prefixDiffSet(" - ", t.keys, f.keys));
log.plain("-".repeat(60));
}
}
function main() {
try {
if (process.argv.length < 3) {
throw new Error("usage: ./scripts/i18n/missingTranslations.ts <locale>");
}
// Load the target translation.
const to = loadLocale(process.argv[2]);
// Load the base language (US English).
const from = loadLocale("en-US");
// Report the difference in key existence.
diff(from, to);
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
process.exit(1);
}
}
main();
+1 -2
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env ts-node
import dotenv from "dotenv";
// Apply all the configuration provided in the .env file if it isn't already in
@@ -27,7 +26,7 @@ process.env.NODE_ENV = "development";
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on("unhandledRejection", (err) => {
process.on("unhandledRejection", err => {
throw err;
});
+1 -1
View File
@@ -17,7 +17,7 @@ process.env.NODE_ENV = "test";
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on("unhandledRejection", (err) => {
process.on("unhandledRejection", err => {
throw err;
});
+3 -5
View File
@@ -1,6 +1,5 @@
import chokidar from "chokidar";
import path from "path";
import { Watcher, WatchOptions } from "./types";
function prependRootDir(
@@ -24,10 +23,9 @@ export default class ChokidarWatcher implements Watcher {
let firstError: Error | null = null;
// If this is set, a pending promise is waiting for the next result.
let pending: {
resolve: (result: string) => void;
reject: (error: Error) => void;
} | null = null;
let pending:
| ({ resolve: (result: string) => void; reject: (error: Error) => void })
| null = null;
// Only start client if we have something to watch.
if (paths.length) {
+1 -1
View File
@@ -76,7 +76,7 @@ export default class CommandExecutor implements Executor {
});
}
public execute(filePath: string): void {
public execute(filePath: string) {
this.spawnProcessPotentiallyDebounced();
}
}
+2 -2
View File
@@ -99,14 +99,14 @@ export default class LongRunningExecutor implements Executor {
}
// This is called before exiting.
public async onCleanup(): Promise<void> {
public async onCleanup() {
this.restartDebounced.cancel();
if (this.isRunning) {
await this.kill();
}
}
public execute(filePath: string): void {
public execute(filePath: string) {
if (this.isRunning) {
this.restartDebounced();
return;
+1 -1
View File
@@ -48,7 +48,7 @@ export default class SaneWatcher implements Watcher {
const queue: string[] = [];
// If this is set, a pending promise is waiting for the next result.
let pending: { resolve: (result: string) => void } | null = null;
let pending: ({ resolve: (result: string) => void }) | null = null;
// Only start client if we have something to watch.
if (paths.length) {
+1 -1
View File
@@ -30,7 +30,7 @@ const cmd = program
.description("Run watchers defined in <configFile>")
.parse(process.argv);
run(cmd.args, cmd.opts()).catch((err) => {
run(cmd.args, cmd.opts()).catch(err => {
// eslint-disable-next-line no-console
console.error(err);
process.exit(1);
+14 -4
View File
@@ -1,4 +1,4 @@
import Joi from "@hapi/joi";
import Joi from "joi";
export interface WatchOptions {
ignore?: ReadonlyArray<string>;
@@ -44,13 +44,23 @@ export const configSchema = Joi.object({
watchers: Joi.object().pattern(
/.*/,
Joi.object({
paths: Joi.array().items(Joi.string()).unique(),
ignore: Joi.array().items(Joi.string()).unique().optional(),
paths: Joi.array()
.items(Joi.string())
.unique(),
ignore: Joi.array()
.items(Joi.string())
.unique()
.optional(),
executor: Joi.object(),
})
),
defaultSet: Joi.string().optional(),
sets: Joi.object()
.pattern(/.*/, Joi.array().items(Joi.string()).unique())
.pattern(
/.*/,
Joi.array()
.items(Joi.string())
.unique()
)
.optional(),
}).with("defaultSet", "sets");
+7 -10
View File
@@ -1,5 +1,5 @@
import Joi from "@hapi/joi";
import chalk from "chalk";
import Joi from "joi";
import { pickBy } from "lodash";
import SaneWatcher from "./SaneWatcher";
@@ -28,7 +28,7 @@ async function beginWatch(
}
function setupCleanup(watcher: Watcher, config: Config) {
["SIGINT", "SIGTERM"].forEach((signal) =>
["SIGINT", "SIGTERM"].forEach(signal =>
process.once(signal as any, async () => {
const cleanups = [];
if (watcher.onCleanup) {
@@ -50,7 +50,7 @@ function resolveSets(
value: ReadonlyArray<string>
) {
const resolved: string[] = [];
value.forEach((v) => {
value.forEach(v => {
if (v in sets) {
resolved.push(...sets[v]);
return;
@@ -66,12 +66,12 @@ function filterOnly(
sets?: Record<string, ReadonlyArray<string>>
): Config["watchers"] {
const resolved = sets ? resolveSets(sets, only) : only;
const unknown = resolved.filter((r) => !(r in watchers));
const unknown = resolved.filter(r => !(r in watchers));
if (unknown.length) {
throw new Error(`Watcher Configuration or Set for ${unknown} not found`);
}
return pickBy(watchers, (value, key) => {
if (!resolved.includes(key)) {
if (resolved.indexOf(key) === -1) {
// eslint-disable-next-line no-console
console.log(chalk.grey(`Disabled watcher "${key}"`));
return false;
@@ -80,10 +80,7 @@ function filterOnly(
}) as Config["watchers"];
}
export default async function watch(
config: Config,
options: Options = {}
): Promise<void> {
export default async function watch(config: Config, options: Options = {}) {
Joi.assert(config, configSchema);
const watcher: Watcher = config.backend || new SaneWatcher();
const rootDir = config.rootDir || process.cwd();
@@ -104,7 +101,7 @@ export default async function watch(
// eslint-disable-next-line no-console
console.log(chalk.cyanBright(`Start watcher "${key}"`));
const watcherConfig = watchersConfigs[key];
beginWatch(watcher, key, watcherConfig, rootDir).catch((err) => {
beginWatch(watcher, key, watcherConfig, rootDir).catch(err => {
// eslint-disable-next-line no-console
console.error(err);
process.exit(1);
+3 -4
View File
@@ -24,8 +24,7 @@ var url = require("url");
var launchEditorEndpoint = require("react-dev-utils/launchEditorEndpoint");
var formatWebpackMessages = require("react-dev-utils/formatWebpackMessages");
var ErrorOverlay = require("react-error-overlay");
var lodash = require("lodash");
var debounce = lodash.debounce;
var { debounce } = require("lodash");
ErrorOverlay.setEditorHandler(function editorHandler(errorLocation) {
// Keep this sync with errorOverlayMiddleware.js
@@ -125,7 +124,7 @@ function handleWarnings(warnings) {
// TODO: remove this workaround when we can upgrade to WebpackDevServer >= v3.3.0,
// which includes proper `warningsFilter` support.
warnings = warnings.filter(function(w){ return !/export .* was not found in/.test(w)});
warnings = warnings.filter(w => !/export .* was not found in/.test(w));
function printWarnings() {
// Print warnings to the console.
@@ -199,7 +198,7 @@ function handleAvailableHash(hash) {
mostRecentCompilationHash = hash;
}
var debouncedReload = debounce(function() {
const debouncedReload = debounce(() => {
window.location.reload();
}, 1000);
+1 -3
View File
@@ -1,5 +1,4 @@
import convict from "convict";
import os from "os";
import { LOCALES } from "../common/helpers/i18n/locales";
@@ -68,7 +67,7 @@ const config = convict({
maxCores: {
doc: "Set maximum of available cores",
format: "nat",
default: os.cpus().length,
default: require("os").cpus().length,
env: "WEBPACK_MAX_CORES",
arg: "maxCores",
},
@@ -78,7 +77,6 @@ export type Config = typeof config;
export const createClientEnv = (c: Config) => ({
NODE_ENV: c.get("env"),
WEBPACK: "true",
});
// Setup the base configuration.
+39 -16
View File
@@ -7,6 +7,7 @@ import HtmlWebpackPlugin from "html-webpack-plugin";
import { identity } from "lodash";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import path from "path";
import typescriptFormatter from "react-dev-utils/typescriptFormatter";
import WatchMissingNodeModulesPlugin from "react-dev-utils/WatchMissingNodeModulesPlugin";
import TerserPlugin from "terser-webpack-plugin";
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
@@ -17,7 +18,8 @@ import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
// TODO: import form coral-common/version, for some reason this fails currently.
// Try again when we have a chance to upgrade typescript.
import { version } from "../common/version";
import { Config, createClientEnv } from "./config";
import { Config } from "./config";
import { createClientEnv } from "./config";
import paths from "./paths";
/**
@@ -81,6 +83,10 @@ export default function createWebpackConfig(
const styleLoader = {
loader: require.resolve("style-loader"),
options: {
sourceMap: !disableSourcemaps,
hmr: watch,
},
};
const localesOptions = {
@@ -109,10 +115,10 @@ export default function createWebpackConfig(
...ifBuild(
new MiniCssExtractPlugin({
filename: isProduction
? "assets/css/[name].[contenthash].css"
? "assets/css/[name].[hash].css"
: "assets/css/[name].css",
chunkFilename: isProduction
? "assets/css/[id].[contenthash].css"
? "assets/css/[id].[hash].css"
: "assets/css/[id].css",
}),
isProduction &&
@@ -217,10 +223,10 @@ export default function createWebpackConfig(
// Generated JS file names (with nested folders).
// There will be one main bundle, and one file per asynchronous chunk.
filename: isProduction
? "assets/js/[name].[contenthash].js"
? "assets/js/[name].[chunkhash].js"
: "assets/js/[name].js",
chunkFilename: isProduction
? "assets/js/[name].[contenthash].chunk.js"
? "assets/js/[name].[chunkhash].chunk.js"
: "assets/js/[name].chunk.js",
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath,
@@ -351,7 +357,7 @@ export default function createWebpackConfig(
options: {
limit: 10000,
name: isProduction
? "assets/media/[name].[contenthash].[ext]"
? "assets/media/[name].[hash].[ext]"
: "assets/media/[name].[ext]",
},
},
@@ -362,10 +368,9 @@ export default function createWebpackConfig(
{
loader: require.resolve("css-loader"),
options: {
modules: {
localIdentName: "[name]-[local]-[contenthash]",
},
modules: true,
importLoaders: 2,
localIdentName: "[name]-[local]-[contenthash]",
sourceMap: !disableSourcemaps,
},
},
@@ -387,7 +392,7 @@ export default function createWebpackConfig(
"@babel/typescript",
[
"@babel/env",
{ targets: { node: "current" }, modules: "commonjs" },
{ targets: { node: "10.0.0" }, modules: "commonjs" },
],
],
// This is a feature of `babel-loader` for webpack (not Babel itself).
@@ -445,11 +450,19 @@ export default function createWebpackConfig(
{
test: /\.js$/,
include: /node_modules\//,
exclude: /node_modules\/(@babel|babel|core-js|regenerator-runtime)/,
use: [
{
loader: require.resolve("babel-loader"),
options: {
// This will ensure that all packages in node_modules that
// import lodash do so in a way that supports tree shaking.
plugins: ["lodash"],
presets: [
[
"@babel/env",
{ targets: "last 2 versions", modules: false },
],
],
cacheDirectory: true,
},
},
@@ -467,10 +480,9 @@ export default function createWebpackConfig(
{
loader: require.resolve("css-loader"),
options: {
modules: {
localIdentName: "[name]-[local]-[contenthash]",
},
modules: true,
importLoaders: 1,
localIdentName: "[name]-[local]-[hash:base64:5]",
sourceMap: !disableSourcemaps,
},
},
@@ -498,8 +510,13 @@ export default function createWebpackConfig(
exclude: [/\.(js|ts|tsx)$/, /\.html$/, /\.json$/],
loader: require.resolve("file-loader"),
options: {
// Because the resources loaded via CSS can sometimes be loaded
// directly from a CSS file, this will ensure that they are
// relative to those referencing files.
publicPath: (loaderPublicPath: string) =>
"../../" + loaderPublicPath,
name: isProduction
? "assets/media/[name].[contenthash].[ext]"
? "assets/media/[name].[hash:8].[ext]"
: "assets/media/[name].[ext]",
},
},
@@ -522,6 +539,11 @@ export default function createWebpackConfig(
useTypescriptIncrementalApi: false,
checkSyntacticErrors: true,
tsconfig: paths.appTsconfig,
watch: paths.appSrc,
// TODO: (cvle) ForkTsCheckerWebpackPlugin are currently not working, so we resort to default reporting.
silent: false,
// The formatter is normally invoked directly in WebpackDevServerUtils during development
formatter: typescriptFormatter,
})
),
// Makes some environment variables available to the JS code, for example:
@@ -690,7 +712,8 @@ export default function createWebpackConfig(
sideEffects: true,
},
entry: [
// Polyfills are in the index.ts file.
/* Use minimal amount of polyfills (for IE) */
"intersection-observer", // also for Safari
paths.appEmbedIndex,
],
output: {
+12 -12
View File
@@ -40,7 +40,7 @@ function getFiles(target, pathToLocale, context) {
const files = fs.readdirSync(pathToLocale);
files.forEach((f) => {
files.forEach(f => {
if (commonFiles.includes(f)) {
common.push(f);
return;
@@ -62,12 +62,12 @@ function generateTarget(target, context) {
locales,
bundled,
} = context;
const getLocalePath = (locale) => path.join(pathToLocales, locale);
const getLocaleFiles = memoize((locale) =>
const getLocalePath = locale => path.join(pathToLocales, locale);
const getLocaleFiles = memoize(locale =>
getFiles(target, getLocalePath(locale), context)
);
const loadables = locales.filter((locale) => !bundled.includes(locale));
const loadables = locales.filter(locale => !bundled.includes(locale));
return `
var ret = {
@@ -81,22 +81,22 @@ function generateTarget(target, context) {
// Bundled locales are directly available in the main bundle.
${bundled
.map(
(locale) => `
locale => `
{
var suffixes = ${JSON.stringify(getLocaleFiles(locale).suffixes)};
var contents = [];
${getLocaleFiles(locale)
.common.map(
(file) => `
file => `
contents.push(require(${JSON.stringify(
path.join(getLocalePath(locale), file).replace(/\\/g, "/")
)}).default);
)}));
`
)
.join("\n")}
contents = contents.concat(suffixes.map(function(suffix) { return require("${path
.join(getLocalePath(locale), target)
.replace(/\\/g, "/")}" + suffix).default; }));
.replace(/\\/g, "/")}" + suffix); }));
ret.bundled[${JSON.stringify(locale)}] = contents.join("\\n");
}
`
@@ -106,13 +106,13 @@ function generateTarget(target, context) {
// Loadables are in a separate bundle, that can be easily loaded.
${loadables
.map(
(locale) => `
locale => `
ret.loadables[${JSON.stringify(locale)}] = function() {
var suffixes = ${JSON.stringify(getLocaleFiles(locale).suffixes)};
var promises = [];
${getLocaleFiles(locale)
.common.map(
(file) => `
file => `
promises.push(
import(
/* webpackChunkName: ${JSON.stringify(
@@ -147,7 +147,7 @@ function generateTarget(target, context) {
`;
}
module.exports = function (source) {
module.exports = function(source) {
const options = Object.assign(
{},
DEFAULT_QUERY_VALUES,
@@ -165,7 +165,7 @@ module.exports = function (source) {
let locales = fs.readdirSync(pathToLocales);
if (availableLocales) {
availableLocales.forEach((locale) => {
availableLocales.forEach(locale => {
if (!locales.includes(locale)) {
throw new Error(`locale ${fallbackLocale} not available`);
}
+2 -4
View File
@@ -16,13 +16,11 @@ export default {
appLoaders: resolveSrc("core/build/loaders"),
appSrc: resolveSrc("."),
appTsconfig: resolveSrc("core/client/tsconfig.json"),
appPolyfill: resolveSrc("core/build/polyfills.ts"),
appPolyfill: resolveSrc("core/build/polyfills.js"),
appPublicPath: resolveSrc("core/build/publicPath.js"),
appLocales: resolveSrc("locales"),
appThemeVariables: resolveSrc("core/client/ui/theme/variables.ts"),
appSassLikeVariables: resolveSrc("core/client/ui/theme/sassLikeVariables.ts"),
appThemeStreamCSS: resolveSrc("core/client/ui/theme/stream.css"),
appThemeAdminCSS: resolveSrc("core/client/ui/theme/admin.css"),
appThemeVariablesCSS: resolveSrc("core/client/ui/theme/variables.css"),
appThemeMixinsCSS: resolveSrc("core/client/ui/theme/mixins.css"),
appStreamHTML: resolveSrc("core/client/stream/index.html"),
+2
View File
@@ -0,0 +1,2 @@
require("@babel/polyfill");
require("intersection-observer");
-6
View File
@@ -1,6 +0,0 @@
import polyfillNodeListForEach from "../client/framework/helpers/polyfillNodeListForEach";
import "core-js/stable";
import "regenerator-runtime/runtime";
polyfillNodeListForEach();
+17 -18
View File
@@ -3,10 +3,10 @@ require("ts-node/register");
const kebabCase = require("lodash/kebabCase");
const mapKeys = require("lodash/mapKeys");
const mapValues = require("lodash/mapValues");
const pickBy = require("lodash/pickBy");
const flat = require("flat");
const paths = require("./paths").default;
const autoprefixer = require("autoprefixer");
const postcssCalcFunction = require("postcss-calc-function").default;
const postcssFontMagician = require("postcss-font-magician");
const postcssFlexbugsFixes = require("postcss-flexbugs-fixes");
const postcssPresetEnv = require("postcss-preset-env");
@@ -16,28 +16,23 @@ const postcssMixins = require("postcss-mixins");
const postcssPrependImports = require("postcss-prepend-imports");
const postcssAdvancedVariables = require("postcss-advanced-variables");
delete require.cache[paths.appSassLikeVariables];
const sassLikeVariables = require(paths.appSassLikeVariables).default;
const kebabs = mapKeys(
mapValues(flat(sassLikeVariables, { delimiter: "-" }), (v) => v.toString()),
delete require.cache[paths.appThemeVariables];
const variables = require(paths.appThemeVariables).default;
const flatKebabVariables = mapKeys(
mapValues(flat(variables, { delimiter: "-" }), v => v.toString()),
(_, k) => kebabCase(k)
);
// Generate sass-style variables to inject into css
const postCssVariables = mapValues(kebabs, (value, key) => {
// These are sass style variables used in media queries.
const mediaQueryVariables = mapValues(
pickBy(flatKebabVariables, (v, k) => k.startsWith("breakpoints-")),
// Add unit to breakpoints.
// Add 1 to support mobile first approach where we start
// with the smallest screen and gradually add styling for the
// next bigger screen. This is realized using `min-width` without
// ever using `max-width`.
if (key.toString().startsWith("breakpoints-")) {
return `${Number.parseInt(value, 10) + 1}px`;
}
// Default return the raw value
return value;
});
v => `${Number.parseInt(v, 10) + 1}px`
);
module.exports = {
// Necessary for external CSS imports to work
@@ -56,9 +51,7 @@ module.exports = {
// Support nesting.
postcssNested(),
// Sass style variables to be used in media queries.
postcssAdvancedVariables({ variables: postCssVariables }),
// Reduce some calc()
postcssCalcFunction(),
postcssAdvancedVariables({ variables: mediaQueryVariables }),
// Provides a modern CSS environment.
postcssPresetEnv(),
// Does all the font handling logic.
@@ -67,6 +60,12 @@ module.exports = {
postcssFlexbugsFixes,
// Vendor prefixing.
autoprefixer({
browsers: [
">1%",
"last 4 versions",
"Firefox ESR",
"not ie < 9", // React doesn't support IE8 anyway
],
flexbox: "no-2009",
}),
],
+17
View File
@@ -1,3 +1,20 @@
const lodashOptimizations = ["use-lodash-es", "lodash"];
module.exports = {
presets: ["@babel/react"],
plugins: ["@babel/syntax-dynamic-import"],
env: {
production: {
presets: [["@babel/env", { targets: "last 2 versions", modules: false }]],
plugins: [...lodashOptimizations],
},
development: {
presets: [["@babel/env", { targets: "last 2 versions", modules: false }]],
plugins: [...lodashOptimizations],
},
test: {
presets: [["@babel/env", { targets: { node: "current" } }]],
plugins: ["@babel/transform-modules-commonjs"],
},
},
};
-3
View File
@@ -2,7 +2,4 @@
body {
margin: 0;
}
input::-ms-clear, input::-ms-reveal {
display: none;
}
}
+2 -2
View File
@@ -1,10 +1,10 @@
.bar {
height: calc(6 * var(--mini-unit));
background-color: var(--palette-text-500);
background-color: var(--palette-text-primary);
}
.centered {
margin: 10% auto;
padding: 0 calc(0.5 * var(--mini-unit));
box-sizing: border-box;
}
}
@@ -1,6 +1,6 @@
import React, { FunctionComponent } from "react";
import { Delay, Flex, Spinner } from "coral-ui/components/v2";
import { Delay, Flex, Spinner } from "coral-ui/components";
const Loading: FunctionComponent = () => (
<Flex justifyContent="center">
+1 -1
View File
@@ -4,7 +4,7 @@
<title>Coral - Account</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
</head>
<body>
+2 -7
View File
@@ -1,8 +1,6 @@
import React, { FunctionComponent } from "react";
import ReactDOM from "react-dom";
import injectConditionalPolyfills from "coral-framework/helpers/injectConditionalPolyfills";
import potentiallyInjectAxe from "coral-framework/helpers/potentiallyInjectAxe";
import { createManaged } from "coral-framework/lib/bootstrap";
import App from "./App";
@@ -10,12 +8,9 @@ import { initLocalState } from "./local";
import localesData from "./locales";
// Import css variables.
import "coral-ui/theme/stream.css";
import "coral-ui/theme/variables.css";
async function main() {
await injectConditionalPolyfills();
// Potentially inject react-axe for runtime a11y checks.
await potentiallyInjectAxe();
const ManagedCoralContextProvider = await createManaged({
initLocalState,
localesData,
@@ -30,4 +25,4 @@ async function main() {
ReactDOM.render(<Index />, document.getElementById("app"));
}
void main();
main();
@@ -1,6 +1,5 @@
import { Environment } from "relay-runtime";
import { AuthState } from "coral-framework/lib/auth";
import { CoralContext } from "coral-framework/lib/bootstrap";
import { initLocalBaseState } from "coral-framework/lib/relay";
@@ -9,8 +8,7 @@ import { initLocalBaseState } from "coral-framework/lib/relay";
*/
export default async function initLocalState(
environment: Environment,
context: CoralContext,
auth?: AuthState
context: CoralContext
) {
initLocalBaseState(environment, context, auth);
await initLocalBaseState(environment, context);
}
@@ -1,11 +0,0 @@
.content {
font-family: var(--font-family-secondary);
font-style: normal;
font-weight: var(--font-weight-secondary-bold);
font-size: var(--font-size-6);
line-height: 1.17;
color: var(--palette-text-500);
text-align: center;
}
+3 -8
View File
@@ -1,15 +1,10 @@
import { Localized } from "@fluent/react/compat";
import React, { FunctionComponent } from "react";
import { HorizontalGutter } from "coral-ui/components/v2";
import styles from "./NotFound.css";
import { HorizontalGutter, Typography } from "coral-ui/components";
const NotFound: FunctionComponent = () => (
<HorizontalGutter container="main">
<Localized id="notFound">
<div className={styles.content}>Not Found</div>
</Localized>
<HorizontalGutter>
<Typography variant="heading3">Not Found</Typography>
</HorizontalGutter>
);
@@ -1,13 +1,12 @@
.title {
font-family: var(--font-family-secondary);
font-family: var(--font-family-serif);
font-weight: 600;
font-style: normal;
font-weight: var(--font-weight-secondary-bold);
font-size: var(--font-size-6);
line-height: 1.17;
font-size: calc(48rem / var(--rem-base));
line-height: calc(42em / 48);
text-align: center;
color: var(--palette-text-500);
padding-left: var(--spacing-2);
color: var(--palette-common-black);
}
.content {
@@ -19,15 +18,14 @@
}
.sectionText {
font-family: var(--font-family-primary);
font-family: var(--font-family-sans-serif);
font-weight: normal;
font-style: normal;
font-weight: var(--font-weight-primary-regular);
font-size: var(--font-size-3);
line-height: 1.25;
font-size: calc(24rem / var(--rem-base));
line-height: calc(34em / 24);
letter-spacing: 0.3px;
color: var(--palette-text-500);
margin-bottom: var(--spacing-3);
color: var(--palette-text-primary);
}
.list {
@@ -36,7 +34,7 @@
}
.list li {
margin-bottom: 0.75rem;
margin-bottom: calc(16em / 24);
}
.listContent {
@@ -45,6 +43,6 @@
.bullet {
display: inline-block;
min-width: 16px;
min-width: 24px;
margin-right: var(--spacing-2);
}
}
@@ -1,8 +1,8 @@
import { Localized } from "@fluent/react/compat";
import cn from "classnames";
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import { Flex, HorizontalGutter, Icon } from "coral-ui/components/v2";
import { Flex, HorizontalGutter, Icon, Typography } from "coral-ui/components";
import styles from "./DownloadDescription.css";
@@ -10,72 +10,84 @@ const DownloadDescription: FunctionComponent = () => {
return (
<HorizontalGutter size="double">
<Localized id="download-landingPage-title">
<h1 className={styles.title}>Download your comment history</h1>
<div className={styles.title}>Download Your Comment History</div>
</Localized>
<div className={styles.content}>
<div className={styles.section}>
<Localized id="download-landingPage-description">
<div className={styles.sectionText}>
<Typography variant="bodyCopy" className={styles.sectionText}>
Your comment history will be downloaded into a .zip file. After
your comment history is unzipped you will have a comma separated
value (or .csv) file that you can easily import into your favorite
spreadsheet application.
</div>
</Typography>
</Localized>
</div>
<div className={styles.section}>
<Localized id="download-landingPage-contentsDescription">
<div className={styles.sectionText}>
<Typography variant="bodyCopy" className={styles.sectionText}>
For each of your comments the following information is included:
</div>
</Typography>
</Localized>
<ul className={styles.list}>
<li>
<Flex alignItems="flex-start">
<Icon size="md" className={styles.bullet}>
<Flex alignItems="center">
<Icon size="lg" className={styles.bullet}>
check
</Icon>
<Localized id="download-landingPage-contentsDate">
<div className={cn(styles.sectionText, styles.listContent)}>
<Typography
variant="bodyCopy"
className={cn(styles.sectionText, styles.listContent)}
>
When you wrote the comment
</div>
</Typography>
</Localized>
</Flex>
</li>
<li>
<Flex alignItems="flex-start">
<Icon size="md" className={styles.bullet}>
<Flex alignItems="center">
<Icon size="lg" className={styles.bullet}>
check
</Icon>
<Localized id="download-landingPage-contentsUrl">
<div className={cn(styles.sectionText, styles.listContent)}>
<Typography
variant="bodyCopy"
className={cn(styles.sectionText, styles.listContent)}
>
The permalink URL for the comment
</div>
</Typography>
</Localized>
</Flex>
</li>
<li>
<Flex alignItems="flex-start">
<Icon size="md" className={styles.bullet}>
<Flex alignItems="center">
<Icon size="lg" className={styles.bullet}>
check
</Icon>
<Localized id="download-landingPage-contentsText">
<div className={cn(styles.sectionText, styles.listContent)}>
<Typography
variant="bodyCopy"
className={cn(styles.sectionText, styles.listContent)}
>
The comment text
</div>
</Typography>
</Localized>
</Flex>
</li>
<li>
<Flex alignItems="flex-start">
<Icon size="md" className={styles.bullet}>
<Flex alignItems="center">
<Icon size="lg" className={styles.bullet}>
check
</Icon>
<Localized id="download-landingPage-contentsStoryUrl">
<div className={cn(styles.sectionText, styles.listContent)}>
<Typography
variant="bodyCopy"
className={cn(styles.sectionText, styles.listContent)}
>
The URL on the article or story where the comment appears
</div>
</Typography>
</Localized>
</Flex>
</li>
@@ -1,3 +1,8 @@
.form {
text-align: center;
}
.downloadButton {
font-size: calc(18rem / var(--rem-base));
line-height: calc(23em / 18);
}
@@ -1,8 +1,7 @@
import { Localized } from "@fluent/react/compat";
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent, useCallback, useState } from "react";
import { HorizontalGutter } from "coral-ui/components/v2";
import { Button } from "coral-ui/components/v3";
import { Button, HorizontalGutter } from "coral-ui/components";
import styles from "./DownloadForm.css";
@@ -12,9 +11,8 @@ interface Props {
const DownloadForm: FunctionComponent<Props> = ({ token }) => {
const [submitted, setSubmitted] = useState(false);
const onSubmit = useCallback(() => {
const onClick = useCallback(() => {
setSubmitted(true);
return true;
}, [setSubmitted]);
return (
@@ -23,19 +21,18 @@ const DownloadForm: FunctionComponent<Props> = ({ token }) => {
className={styles.form}
method="post"
action="/api/account/download"
onSubmit={onSubmit}
>
<input name="token" type="hidden" value={token} />
<Localized id="download-landingPage-download">
<Localized id="download-landingPage-downloadComments ">
<Button
type="submit"
variant="filled"
color="primary"
paddingSize="medium"
disabled={submitted}
upperCase
onClick={onClick}
className={styles.downloadButton}
>
Download
Download My Comment History
</Button>
</Localized>
</form>
@@ -6,11 +6,11 @@
.root {
display: inline-block;
max-width: calc(70 * var(--spacing-2));
max-width: calc(70 * var(--mini-unit));
@media (min-width: $breakpoints-xs) {
max-width: calc(60 * var(--spacing-2));
max-width: calc(90 * var(--mini-unit));
}
text-align: left;
}
}
@@ -6,7 +6,7 @@ import { useToken } from "coral-framework/hooks";
import { createFetch } from "coral-framework/lib/relay";
import { withRouteConfig } from "coral-framework/lib/router";
import { parseHashQuery } from "coral-framework/utils";
import { HorizontalGutter } from "coral-ui/components/v2";
import { HorizontalGutter } from "coral-ui/components";
import DownloadDescription from "./DownloadDescription";
import DownloadForm from "./DownloadForm";
@@ -32,35 +32,35 @@ const DownloadRoute: FunctionComponent<Props> = ({ token }) => {
if (state === "UNCHECKED") {
return (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<HorizontalGutter size="double">
<Loading />
</HorizontalGutter>
</div>
</main>
</div>
);
}
if (state !== "VALID" || error) {
return (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<HorizontalGutter size="double">
<DownloadDescription />
<Sorry />
</HorizontalGutter>
</div>
</main>
</div>
);
}
return (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<DownloadDescription />
<DownloadForm token={token!} />
</div>
</main>
</div>
);
};
@@ -0,0 +1,7 @@
.icon {
margin-right: var(--spacing-2);
}
.callout {
justify-content: left;
}
@@ -1,21 +1,22 @@
import { Localized } from "@fluent/react/compat";
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import { Icon } from "coral-ui/components/v2";
import { CallOut } from "coral-ui/components/v3";
import { CallOut, Flex, Icon } from "coral-ui/components";
import styles from "./Sorry.css";
const Sorry: FunctionComponent = () => {
return (
<CallOut
color="error"
icon={<Icon>error</Icon>}
titleWeight="semiBold"
title={
<CallOut color="error" fullWidth className={styles.callout}>
<Flex>
<Icon size="md" className={styles.icon}>
error
</Icon>
<Localized id="download-landingPage-sorry">
Your download link is invalid.
</Localized>
}
/>
</Flex>
</CallOut>
);
};
@@ -1,52 +0,0 @@
.title {
font-family: var(--font-family-secondary);
font-style: normal;
font-weight: var(--font-weight-secondary-bold);
font-size: var(--font-size-6);
line-height: 1.17;
color: var(--palette-text-500);
margin-bottom: var(--spacing-2);
}
.description {
font-family: var(--font-family-primary);
font-style: normal;
font-weight: var(--font-weight-primary-regular);
font-size: var(--font-size-3);
line-height: 1.25;
color: var(--palette-text-500);
margin-bottom: var(--spacing-3);
}
.label {
font-family: var(--font-family-primary);
font-style: normal;
font-weight: var(--font-weight-primary-bold);
font-size: var(--font-size-2);
line-height: 1.14;
color: var(--palette-text-500);
margin-bottom: var(--spacing-1);
}
.labelDescription {
font-family: var(--font-family-primary);
font-style: normal;
font-weight: var(--font-weight-primary-regular);
font-size: var(--font-size-2);
line-height: 1.285;
color: var(--palette-text-100);
margin-bottom: var(--spacing-2);
}
.submit {
margin-top: var(--spacing-5);
margin-bottom: var(--spacing-1);
}
@@ -1,16 +1,14 @@
import { Localized } from "@fluent/react/compat";
import { FORM_ERROR } from "final-form";
import { Localized } from "fluent-react/compat";
import React, { useCallback } from "react";
import { Form } from "react-final-form";
import { InvalidRequestError } from "coral-framework/lib/errors";
import { useMutation } from "coral-framework/lib/relay";
import { Button } from "coral-ui/components/v3";
import { Button, HorizontalGutter, Typography } from "coral-ui/components";
import ConfirmMutation from "./ConfirmMutation";
import styles from "./Confirm.css";
interface Props {
token: string;
disabled?: boolean;
@@ -35,18 +33,18 @@ const ConfirmForm: React.FunctionComponent<Props> = ({ onSuccess, token }) => {
<Form onSubmit={onSubmit}>
{({ handleSubmit, submitting }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<div>
<div>
<Localized id="confirmEmail-confirmYourEmailAddress">
<div className={styles.title}>Confirm your email address</div>
<HorizontalGutter size="double">
<HorizontalGutter>
<Localized id="confirmEmail-emailConfirmation">
<Typography variant="heading1">Email Confirmation</Typography>
</Localized>
<Localized id="confirmEmail-pleaseClickToConfirm">
<div className={styles.description}>
<Typography variant="bodyCopy">
Click below to confirm your email address.
</div>
</Typography>
</Localized>
</div>
<div>
</HorizontalGutter>
<HorizontalGutter>
<Localized id="confirmEmail-confirmEmail">
<Button
type="submit"
@@ -54,14 +52,12 @@ const ConfirmForm: React.FunctionComponent<Props> = ({ onSuccess, token }) => {
color="primary"
disabled={submitting}
fullWidth
upperCase
className={styles.submit}
>
Confirm email
</Button>
</Localized>
</div>
</div>
</HorizontalGutter>
</HorizontalGutter>
</form>
)}
</Form>
@@ -35,36 +35,36 @@ const ConfirmRoute: React.FunctionComponent<Props> = ({ token }) => {
if (state === "UNCHECKED") {
return (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<Loading />
</div>
</main>
</div>
);
}
if (state !== "VALID" || error) {
return (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<Sorry reason={error} />
</div>
</main>
</div>
);
}
return !finished ? (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<ConfirmForm token={token!} onSuccess={onSuccess} />
</div>
</main>
</div>
) : (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<Success />
</div>
</main>
</div>
);
};
@@ -1,9 +1,7 @@
import { Localized } from "@fluent/react/compat";
import { Localized } from "fluent-react/compat";
import React from "react";
import { CallOut } from "coral-ui/components/v3";
import styles from "./Confirm.css";
import { CallOut, HorizontalGutter, Typography } from "coral-ui/components";
interface Props {
reason: React.ReactNode;
@@ -11,27 +9,23 @@ interface Props {
const Sorry: React.FunctionComponent<Props> = ({ reason }) => {
return (
<div>
<HorizontalGutter size="double">
<Localized id="confirmEmail-oopsSorry">
<div className={styles.title}>Oops Sorry!</div>
<Typography variant="heading1">Oops Sorry!</Typography>
</Localized>
<CallOut
color="error"
titleWeight="semiBold"
title={
reason ? (
reason
) : (
<Localized id="account-tokenNotFound">
<span data-testid="invalid-link">
The specified link is invalid, check to see if it was copied
correctly.
</span>
</Localized>
)
}
/>
</div>
<CallOut color="error" fullWidth>
{reason ? (
reason
) : (
<Localized id="account-tokenNotFound">
<span data-testid="invalid-link">
The specified link is invalid, check to see if it was copied
correctly.
</span>
</Localized>
)}
</CallOut>
</HorizontalGutter>
);
};
@@ -1,18 +1,20 @@
import { Localized } from "@fluent/react/compat";
import { Localized } from "fluent-react/compat";
import React from "react";
import styles from "./Confirm.css";
import { HorizontalGutter, Typography } from "coral-ui/components";
const Success: React.FunctionComponent = () => {
return (
<div>
<HorizontalGutter size="double">
<Localized id="confirmEmail-successfullyConfirmed">
<div className={styles.title}>Email successfully confirmed</div>
<Typography variant="heading1">Email successfully confirmed</Typography>
</Localized>
<Localized id="confirmEmail-youMayClose">
<div className={styles.description}>You may now close this window.</div>
<Typography variant="bodyCopy">
You may now close this window.
</Typography>
</Localized>
</div>
</HorizontalGutter>
);
};
@@ -1,9 +1,7 @@
import { Localized } from "@fluent/react/compat";
import { Localized } from "fluent-react/compat";
import React from "react";
import { CallOut } from "coral-ui/components/v3";
import styles from "./Unsubscribe.css";
import { CallOut, HorizontalGutter, Typography } from "coral-ui/components";
interface Props {
reason: React.ReactNode;
@@ -11,27 +9,23 @@ interface Props {
const Sorry: React.FunctionComponent<Props> = ({ reason }) => {
return (
<div>
<HorizontalGutter size="double">
<Localized id="unsubscribe-oopsSorry">
<div className={styles.title}>Oops Sorry!</div>
<Typography variant="heading1">Oops Sorry!</Typography>
</Localized>
<CallOut
color="error"
titleWeight="semiBold"
title={
reason ? (
reason
) : (
<Localized id="account-tokenNotFound">
<span data-testid="invalid-link">
The specified link is invalid, check to see if it was copied
correctly.
</span>
</Localized>
)
}
/>
</div>
<CallOut color="error" fullWidth>
{reason ? (
reason
) : (
<Localized id="account-tokenNotFound">
<span data-testid="invalid-link">
The specified link is invalid, check to see if it was copied
correctly.
</span>
</Localized>
)}
</CallOut>
</HorizontalGutter>
);
};
@@ -1,20 +1,17 @@
import { Localized } from "@fluent/react/compat";
import { Localized } from "fluent-react/compat";
import React from "react";
import styles from "./Unsubscribe.css";
import { HorizontalGutter, Typography } from "coral-ui/components";
const Success: React.FunctionComponent = () => {
return (
<div data-testid="success">
<Localized id="unsubscribe-unsubscribedSuccessfully">
<div className={styles.title}>
Unsubscribed successfully from email notifications
</div>
<HorizontalGutter data-testid="success" size="double">
<Localized id="unsubscribe-successfullyUnsubscribed">
<Typography variant="heading1">
You are now unsubscribed from all notifications
</Typography>
</Localized>
<Localized id="unsubscribe-youMayNowClose">
<div className={styles.description}>You may now close this window</div>
</Localized>
</div>
</HorizontalGutter>
);
};
@@ -1,52 +0,0 @@
.title {
font-family: var(--font-family-secondary);
font-style: normal;
font-weight: var(--font-weight-secondary-bold);
font-size: var(--font-size-6);
line-height: 1.17;
color: var(--palette-text-500);
margin-bottom: var(--spacing-2);
}
.description {
font-family: var(--font-family-primary);
font-style: normal;
font-weight: var(--font-weight-primary-regular);
font-size: var(--font-size-3);
line-height: 1.25;
color: var(--palette-text-500);
margin-bottom: var(--spacing-3);
}
.label {
font-family: var(--font-family-primary);
font-style: normal;
font-weight: var(--font-weight-primary-bold);
font-size: var(--font-size-2);
line-height: 1.14;
color: var(--palette-text-500);
margin-bottom: var(--spacing-1);
}
.labelDescription {
font-family: var(--font-family-primary);
font-style: normal;
font-weight: var(--font-weight-primary-regular);
font-size: var(--font-size-2);
line-height: 1.285;
color: var(--palette-text-100);
margin-bottom: var(--spacing-2);
}
.submit {
margin-top: var(--spacing-5);
margin-bottom: var(--spacing-1);
}
@@ -1,16 +1,19 @@
import { Localized } from "@fluent/react/compat";
import { FORM_ERROR } from "final-form";
import { Localized } from "fluent-react/compat";
import React, { useCallback } from "react";
import { Form } from "react-final-form";
import { InvalidRequestError } from "coral-framework/lib/errors";
import { useMutation } from "coral-framework/lib/relay";
import { Button, CallOut } from "coral-ui/components/v3";
import {
Button,
CallOut,
HorizontalGutter,
Typography,
} from "coral-ui/components";
import UnsubscribeNotificationsMutation from "./UnsubscribeNotificationsMutation";
import styles from "./Unsubscribe.css";
interface Props {
token: string;
disabled?: boolean;
@@ -39,34 +42,30 @@ const UnsubscribeForm: React.FunctionComponent<Props> = ({
<Form onSubmit={onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form onSubmit={handleSubmit}>
<div>
<Localized id="unsubscribe-unsubscribeFromEmails">
<div className={styles.title}>
Unsubscribe from email notifications
</div>
</Localized>
<HorizontalGutter>
<Localized id="unsubscribe-clickToConfirm">
<div className={styles.description}>
<Typography variant="heading1">
Click below to confirm that you want to unsubscribe from all
notifications.
</div>
</Typography>
</Localized>
{submitError && <CallOut color="error" title={submitError} />}
<Localized id="unsubscribe-submit-unsubscribe">
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<Localized id="unsubscribe-confirm">
<Button
type="submit"
variant="filled"
paddingSize="medium"
color="primary"
disabled={submitting}
upperCase
fullWidth
className={styles.submit}
>
Unsubscribe
Confirm
</Button>
</Localized>
</div>
</HorizontalGutter>
</form>
)}
</Form>
@@ -35,36 +35,36 @@ const UnsubscribeRoute: React.FunctionComponent<Props> = ({ token }) => {
if (state === "UNCHECKED") {
return (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<Loading />
</div>
</main>
</div>
);
}
if (state !== "VALID" || error) {
return (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<Sorry reason={error} />
</div>
</main>
</div>
);
}
return !finished ? (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<UnsubscribeForm token={token!} onSuccess={onSuccess} />
</div>
</main>
</div>
) : (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<Success />
</div>
</main>
</div>
);
};
@@ -1,52 +0,0 @@
.title {
font-family: var(--font-family-secondary);
font-style: normal;
font-weight: var(--font-weight-secondary-bold);
font-size: var(--font-size-6);
line-height: 1.17;
color: var(--palette-text-500);
margin-bottom: var(--spacing-2);
}
.description {
font-family: var(--font-family-primary);
font-style: normal;
font-weight: var(--font-weight-primary-regular);
font-size: var(--font-size-3);
line-height: 1.25;
color: var(--palette-text-500);
margin-bottom: var(--spacing-3);
}
.label {
font-family: var(--font-family-primary);
font-style: normal;
font-weight: var(--font-weight-primary-bold);
font-size: var(--font-size-2);
line-height: 1.14;
color: var(--palette-text-500);
margin-bottom: var(--spacing-1);
}
.labelDescription {
font-family: var(--font-family-primary);
font-style: normal;
font-weight: var(--font-weight-primary-regular);
font-size: var(--font-size-2);
line-height: 1.285;
color: var(--palette-text-100);
margin-bottom: var(--spacing-2);
}
.submit {
margin-top: var(--spacing-5);
margin-bottom: var(--spacing-1);
}
@@ -1,23 +1,29 @@
import { Localized } from "@fluent/react/compat";
import { FORM_ERROR } from "final-form";
import { Localized } from "fluent-react/compat";
import React, { useCallback } from "react";
import { Field, Form } from "react-final-form";
import { InvalidRequestError } from "coral-framework/lib/errors";
import { colorFromMeta } from "coral-framework/lib/form";
import { colorFromMeta, ValidationMessage } from "coral-framework/lib/form";
import { useMutation } from "coral-framework/lib/relay";
import {
composeValidators,
required,
validatePassword,
} from "coral-framework/lib/validation";
import { FormField, PasswordField } from "coral-ui/components/v2";
import { Button, CallOut, ValidationMessage } from "coral-ui/components/v3";
import {
Button,
CallOut,
FormField,
HorizontalGutter,
InputDescription,
InputLabel,
PasswordField,
Typography,
} from "coral-ui/components";
import ResetPasswordMutation from "./ResetPasswordMutation";
import styles from "./Reset.css";
interface Props {
token: string;
disabled?: boolean;
@@ -54,20 +60,22 @@ const ResetPasswordForm: React.FunctionComponent<Props> = ({
<Form onSubmit={onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<div>
<div>
<HorizontalGutter size="double">
<HorizontalGutter>
<Localized id="resetPassword-resetYourPassword">
<div className={styles.title}>Reset your password</div>
<Typography variant="heading1">
Reset your password
</Typography>
</Localized>
<Localized id="resetPassword-pleaseEnterNewPassword">
<div className={styles.description}>
<Typography variant="bodyCopy">
Please enter a new password to use to sign in to your
account. Make sure it is unique and be sure to keep it
secure.
</div>
</Typography>
</Localized>
</div>
<div>
</HorizontalGutter>
<HorizontalGutter>
<Field
name="password"
validate={composeValidators(required, validatePassword)}
@@ -75,17 +83,15 @@ const ResetPasswordForm: React.FunctionComponent<Props> = ({
{({ input, meta }) => (
<FormField>
<Localized id="resetPassword-passwordLabel">
<label className={styles.label} htmlFor={input.name}>
Password
</label>
<InputLabel htmlFor={input.name}>Password</InputLabel>
</Localized>
<Localized
id="resetPassword-passwordDescription"
$minLength={8}
>
<div className={styles.labelDescription}>
<InputDescription>
{"Must be at least {$minLength} characters"}
</div>
</InputDescription>
</Localized>
<Localized
id="resetPassword-passwordTextField"
@@ -101,27 +107,28 @@ const ResetPasswordForm: React.FunctionComponent<Props> = ({
{...input}
/>
</Localized>
<ValidationMessage meta={meta} />
<ValidationMessage meta={meta} fullWidth />
</FormField>
)}
</Field>
{submitError && <CallOut color="error" title={submitError} />}
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<Localized id="resetPassword-resetPassword">
<Button
type="submit"
variant="filled"
color="primary"
paddingSize="medium"
disabled={submitting}
upperCase
fullWidth
className={styles.submit}
>
Reset Password
</Button>
</Localized>
</div>
</div>
</HorizontalGutter>
</HorizontalGutter>
</form>
)}
</Form>
@@ -35,36 +35,36 @@ const ResetRoute: React.FunctionComponent<Props> = ({ token }) => {
if (state === "UNCHECKED") {
return (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<Loading />
</div>
</main>
</div>
);
}
if (state !== "VALID" || error) {
return (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<Sorry reason={error} />
</div>
</main>
</div>
);
}
return !finished ? (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<ResetPasswordForm token={token!} onSuccess={onSuccess} />
</div>
</main>
</div>
) : (
<main className={styles.container}>
<div className={styles.container}>
<div className={styles.root}>
<Success />
</div>
</main>
</div>
);
};
@@ -1,9 +1,7 @@
import { Localized } from "@fluent/react/compat";
import { Localized } from "fluent-react/compat";
import React from "react";
import { CallOut } from "coral-ui/components/v3";
import styles from "./Reset.css";
import { CallOut, HorizontalGutter, Typography } from "coral-ui/components";
interface Props {
reason: React.ReactNode;
@@ -11,27 +9,23 @@ interface Props {
const Sorry: React.FunctionComponent<Props> = ({ reason }) => {
return (
<div>
<HorizontalGutter size="double">
<Localized id="resetPassword-oopsSorry">
<div className={styles.title}>Oops Sorry!</div>
<Typography variant="heading1">Oops Sorry!</Typography>
</Localized>
<CallOut
color="error"
titleWeight="semiBold"
title={
reason ? (
reason
) : (
<Localized id="account-tokenNotFound">
<span data-testid="invalid-link">
The specified link is invalid, check to see if it was copied
correctly.
</span>
</Localized>
)
}
/>
</div>
<CallOut color="error" fullWidth>
{reason ? (
reason
) : (
<Localized id="account-tokenNotFound">
<span data-testid="invalid-link">
The specified link is invalid, check to see if it was copied
correctly.
</span>
</Localized>
)}
</CallOut>
</HorizontalGutter>
);
};
@@ -1,21 +1,21 @@
import { Localized } from "@fluent/react/compat";
import { Localized } from "fluent-react/compat";
import React from "react";
import styles from "./Reset.css";
import { HorizontalGutter, Typography } from "coral-ui/components";
const Success: React.FunctionComponent = () => {
return (
<div>
<HorizontalGutter size="double">
<Localized id="resetPassword-successfullyReset">
<div className={styles.title}>Password successfully reset</div>
<Typography variant="heading1">Password successfully reset</Typography>
</Localized>
<Localized id="resetPassword-youMayClose">
<div className={styles.description}>
<Typography variant="bodyCopy">
You may now close this window and sign in to your account with your
new password.
</div>
</Typography>
</Localized>
</div>
</HorizontalGutter>
);
};
@@ -10,7 +10,7 @@ exports[`renders form 1`] = `
<div
className="MainLayout-centered"
>
<main
<div
className="ConfirmRoute-container"
>
<div
@@ -20,22 +20,29 @@ exports[`renders form 1`] = `
autoComplete="off"
onSubmit={[Function]}
>
<div>
<div>
<div
className="Confirm-title"
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
>
Confirm your email address
</div>
<div
className="Confirm-description"
Email Confirmation
</h1>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Click below to confirm your email address.
</div>
</p>
</div>
<div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<button
className="BaseButton-root Button-base Button-filled Button-fontSizeSmall Button-textAlignCenter Button-fontFamilyPrimary Button-fontWeightPrimaryBold Button-paddingSizeSmall Button-colorPrimary Button-upperCase Button-fullWidth Confirm-submit"
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth"
data-color="primary"
data-variant="filled"
disabled={false}
onBlur={[Function]}
@@ -51,7 +58,7 @@ exports[`renders form 1`] = `
</div>
</form>
</div>
</main>
</div>
</div>
</div>
`;
@@ -66,45 +73,34 @@ exports[`renders missing confirm token 1`] = `
<div
className="MainLayout-centered"
>
<main
<div
className="ConfirmRoute-container"
>
<div
className="ConfirmRoute-root"
>
<div>
<div
className="Confirm-title"
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
>
<h1
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
>
Oops Sorry!
</div>
</h1>
<div
className="CallOut-root CallOut-error CallOut-leftBorder"
className="CallOut-root CallOut-colorError CallOut-fullWidth"
>
<div
className="CallOut-container"
>
<div
className="CallOut-content"
<div>
<span
data-testid="invalid-link"
>
<div
className="CallOut-title CallOut-titleSemiBold"
>
<span
data-testid="invalid-link"
>
The specified link is invalid, check to see if it was copied correctly.
</span>
</div>
<div
className="CallOut-body"
/>
</div>
The specified link is invalid, check to see if it was copied correctly.
</span>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</div>
`;
@@ -10,7 +10,7 @@ exports[`renders form 1`] = `
<div
className="MainLayout-centered"
>
<main
<div
className="ResetRoute-container"
>
<div
@@ -21,35 +21,41 @@ exports[`renders form 1`] = `
autoComplete="off"
onSubmit={[Function]}
>
<div>
<div>
<div
className="Reset-title"
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
>
Reset your password
</div>
<div
className="Reset-description"
</h1>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Please enter a new password to use to sign in to your account.
Make sure it is unique and be sure to keep it secure.
</div>
</p>
</div>
<div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Box-root HorizontalGutter-root FormField-root HorizontalGutter-spacing-2"
className="Box-root HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Reset-label"
className="Box-root Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
htmlFor="password"
>
Password
</label>
<div
className="Reset-labelDescription"
<p
className="Box-root Typography-root Typography-fieldDescription Typography-colorTextSecondary"
>
Must be at least 8 characters
</div>
</p>
<div
className="PasswordField-fullWidth PasswordField-root"
>
@@ -61,7 +67,6 @@ Make sure it is unique and be sure to keep it secure.
autoComplete="new-password"
autoCorrect="off"
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
data-testid="password-field"
disabled={false}
id="password"
name="password"
@@ -92,7 +97,8 @@ Make sure it is unique and be sure to keep it secure.
</div>
</div>
<button
className="BaseButton-root Button-base Button-filled Button-fontSizeSmall Button-textAlignCenter Button-fontFamilyPrimary Button-fontWeightPrimaryBold Button-paddingSizeMedium Button-colorPrimary Button-upperCase Button-fullWidth Reset-submit"
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth"
data-color="primary"
data-variant="filled"
disabled={false}
onBlur={[Function]}
@@ -109,7 +115,7 @@ Make sure it is unique and be sure to keep it secure.
</form>
</div>
</div>
</main>
</div>
</div>
</div>
`;
@@ -124,45 +130,34 @@ exports[`renders missing reset token 1`] = `
<div
className="MainLayout-centered"
>
<main
<div
className="ResetRoute-container"
>
<div
className="ResetRoute-root"
>
<div>
<div
className="Reset-title"
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
>
<h1
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
>
Oops Sorry!
</div>
</h1>
<div
className="CallOut-root CallOut-error CallOut-leftBorder"
className="CallOut-root CallOut-colorError CallOut-fullWidth"
>
<div
className="CallOut-container"
>
<div
className="CallOut-content"
<div>
<span
data-testid="invalid-link"
>
<div
className="CallOut-title CallOut-titleSemiBold"
>
<span
data-testid="invalid-link"
>
The specified link is invalid, check to see if it was copied correctly.
</span>
</div>
<div
className="CallOut-body"
/>
</div>
The specified link is invalid, check to see if it was copied correctly.
</span>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</div>
`;
@@ -10,7 +10,7 @@ exports[`renders form 1`] = `
<div
className="MainLayout-centered"
>
<main
<div
className="UnsubscribeRoute-container"
>
<div
@@ -22,19 +22,17 @@ exports[`renders form 1`] = `
<form
onSubmit={[Function]}
>
<div>
<div
className="Unsubscribe-title"
>
Unsubscribe from email notifications
</div>
<div
className="Unsubscribe-description"
<div
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
>
Click below to confirm that you want to unsubscribe from all notifications.
</div>
</h1>
<button
className="BaseButton-root Button-base Button-filled Button-fontSizeSmall Button-textAlignCenter Button-fontFamilyPrimary Button-fontWeightPrimaryBold Button-paddingSizeMedium Button-colorPrimary Button-upperCase Button-fullWidth Unsubscribe-submit"
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth"
data-color="primary"
data-variant="filled"
disabled={false}
onBlur={[Function]}
@@ -44,13 +42,13 @@ exports[`renders form 1`] = `
onTouchEnd={[Function]}
type="submit"
>
Unsubscribe
Confirm
</button>
</div>
</form>
</div>
</div>
</main>
</div>
</div>
</div>
`;
@@ -65,45 +63,34 @@ exports[`renders missing confirm token 1`] = `
<div
className="MainLayout-centered"
>
<main
<div
className="UnsubscribeRoute-container"
>
<div
className="UnsubscribeRoute-root"
>
<div>
<div
className="Unsubscribe-title"
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
>
<h1
className="Box-root Typography-root Typography-heading1 Typography-colorTextPrimary"
>
Oops Sorry!
</div>
</h1>
<div
className="CallOut-root CallOut-error CallOut-leftBorder"
className="CallOut-root CallOut-colorError CallOut-fullWidth"
>
<div
className="CallOut-container"
>
<div
className="CallOut-content"
<div>
<span
data-testid="invalid-link"
>
<div
className="CallOut-title CallOut-titleSemiBold"
>
<span
data-testid="invalid-link"
>
The specified link is invalid, check to see if it was copied correctly.
</span>
</div>
<div
className="CallOut-body"
/>
</div>
The specified link is invalid, check to see if it was copied correctly.
</span>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</div>
`;
@@ -31,9 +31,7 @@ it("renders missing confirm token", async () => {
replaceHistoryLocation("http://localhost/account/email/confirm");
const { root } = await createTestRenderer();
await waitForElement(() => within(root).getByTestID("invalid-link"));
expect(within(root).toJSON()).toMatchSnapshot();
expect(await within(root).axe()).toHaveNoViolations();
});
it("renders form", async () => {
@@ -53,11 +51,12 @@ it("renders form", async () => {
await act(async () => {
await waitForElement(() =>
within(root).getByText("Confirm your email address")
within(root).getByText("Email Confirmation", {
exact: false,
})
);
});
expect(within(root).toJSON()).toMatchSnapshot();
expect(await within(root).axe()).toHaveNoViolations();
restMock.verify();
});
@@ -99,7 +98,6 @@ it("renders error from server", async () => {
);
});
restMock.verify();
expect(await within(root).axe()).toHaveNoViolations();
}
});
@@ -128,7 +126,9 @@ it("submits form", async () => {
await act(async () => {
await waitForElement(() =>
within(root).getByText("Confirm your email address")
within(root).getByText("Email Confirmation", {
exact: false,
})
);
});
const form = within(root).getByType("form");
@@ -32,7 +32,6 @@ it("renders missing reset token", async () => {
const { root } = await createTestRenderer();
await waitForElement(() => within(root).getByTestID("invalid-link"));
expect(within(root).toJSON()).toMatchSnapshot();
expect(await within(root).axe()).toHaveNoViolations();
});
it("renders form", async () => {
@@ -58,7 +57,6 @@ it("renders form", async () => {
);
});
expect(within(root).toJSON()).toMatchSnapshot();
expect(await within(root).axe()).toHaveNoViolations();
restMock.verify();
});
@@ -100,7 +98,6 @@ it("renders error from server", async () => {
);
});
restMock.verify();
expect(await within(root).axe()).toHaveNoViolations();
}
});
@@ -32,7 +32,6 @@ it("renders missing confirm token", async () => {
const { root } = await createTestRenderer();
await waitForElement(() => within(root).getByTestID("invalid-link"));
expect(within(root).toJSON()).toMatchSnapshot();
expect(await within(root).axe()).toHaveNoViolations();
});
it("renders form", async () => {
@@ -54,7 +53,6 @@ it("renders form", async () => {
await waitForElement(() => within(root).getByTestID("unsubscribe-form"));
});
expect(within(root).toJSON()).toMatchSnapshot();
expect(await within(root).axe()).toHaveNoViolations();
restMock.verify();
});
@@ -95,7 +93,6 @@ it("renders error from server", async () => {
);
});
restMock.verify();
expect(await within(root).axe()).toHaveNoViolations();
}
});
@@ -1,6 +1,8 @@
import { Localized } from "@fluent/react/compat";
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";
@@ -8,7 +10,6 @@ import Footer from "./Footer";
import GoToCommentLink from "./GoToCommentLink";
import Info from "./Info";
import Timestamp from "./Timestamp";
import Username from "./Username";
interface Props {
href: string;
@@ -17,7 +18,11 @@ interface Props {
onGotoComment?: React.EventHandler<React.MouseEvent>;
}
const ApprovedComment: FunctionComponent<Props> = (props) => (
const Username: FunctionComponent<{ username: string }> = ({ username }) => (
<strong>{username}</strong>
);
const ApprovedComment: FunctionComponent<Props> = props => (
<DecisionItem icon={<ApprovedIcon />}>
<Localized
id="decisionHistory-approvedCommentBy"
@@ -26,7 +31,9 @@ const ApprovedComment: FunctionComponent<Props> = (props) => (
<Info>{"Approved comment by <Username></Username>"}</Info>
</Localized>
<Footer>
<Timestamp>{props.date}</Timestamp>
<Typography variant="timestamp">
<Timestamp>{props.date}</Timestamp>
</Typography>
<DotDivider />
<GoToCommentLink href={props.href} onClick={props.onGotoComment} />
</Footer>
@@ -1,3 +1,3 @@
.root {
color: var(--palette-success-500);
color: var(--palette-success-main);
}
@@ -1,13 +1,11 @@
import React, { FunctionComponent } from "react";
import { Icon } from "coral-ui/components/v2";
import { Icon } from "coral-ui/components";
import styles from "./ApprovedIcon.css";
const ApprovedIcon: FunctionComponent = () => (
<Icon size="md" className={styles.root}>
check_circled
</Icon>
<Icon className={styles.root}>check</Icon>
);
export default ApprovedIcon;
@@ -19,13 +19,13 @@ interface Props {
onClosePopover: () => void;
}
const DecisionHistory: FunctionComponent<Props> = (props) => (
const DecisionHistory: FunctionComponent<Props> = props => (
<div data-testid="decisionHistory-container">
<Title />
<Main>
<DecisionList>
{props.actions.length === 0 && <Empty />}
{props.actions.map((action) => (
{props.actions.map(action => (
<DecisionHistoryItemContainer
key={action.id}
action={action}
@@ -3,6 +3,6 @@
}
.historyIcon {
color: var(--palette-text-100);
margin-right: 10px;
color: var(--palette-text-secondary);
margin-right: calc(1.5 * var(--mini-unit));
}
@@ -1,12 +1,7 @@
import { Localized } from "@fluent/react/compat";
import { Localized } from "fluent-react/compat";
import React, { FunctionComponent } from "react";
import {
BaseButton,
ClickOutside,
Icon,
Popover,
} from "coral-ui/components/v2";
import { BaseButton, ClickOutside, Icon, Popover } from "coral-ui/components";
import DecisionHistoryQuery from "./DecisionHistoryQuery";
@@ -23,7 +23,7 @@ export class DecisionHistoryContainer extends React.Component<
public render() {
const actions = this.props.viewer.commentModerationActionHistory.edges.map(
(edge) => edge.node
edge => edge.node
);
return (
<DecisionHistory
@@ -43,7 +43,7 @@ export class DecisionHistoryContainer extends React.Component<
this.setState({ disableLoadMore: true });
this.props.relay.loadMore(
10, // Fetch the next 10 feed items
(error) => {
error => {
this.setState({ disableLoadMore: false });
if (error) {
// eslint-disable-next-line no-console
@@ -1,6 +1,6 @@
import React, { FunctionComponent } from "react";
import { Delay, Flex, Spinner } from "coral-ui/components/v2";
import { Delay, Flex, Spinner } from "coral-ui/components";
import Main from "./Main";
import Title from "./Title";
@@ -1,7 +1,6 @@
import React, { Component } from "react";
import { graphql } from "react-relay";
import { QueryRenderer } from "coral-framework/lib/relay";
import { graphql, QueryRenderer } from "coral-framework/lib/relay";
import { DecisionHistoryQuery as QueryTypes } from "coral-admin/__generated__/DecisionHistoryQuery.graphql";

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