mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 17:32:25 +08:00
[next] Email (#2261)
* feat: suspending, banning, now propogation * feat: added email rendering + localization support * fix: fix related to lib * refactor: moved juicer to queue task * refactor: cleanup of job processor * refactor: improved error messaging around failed email * feat: initial forgot passwor impl * fix: fixed rebase errors * feat: send back Content-Language header with requests * feat: added ban email * feat: implemented forgotten password API * fix: linting * feat: support more emails * fix: promise patches * feat: initial confirm email API * feat: added rate limiting * feat: added URL support * feat: added email docs * fix: updated docs * chore: documentation review * fix: fixed build bug * feat: implement forgot password in auth popup * test: add tests + fixes * chore: rename StatelessComponent to FunctionComponent * fix: types and test fixes * chore: upgrade deps * fix: THANK YOU TESTS FOR SAVING MY A** * chore: reorder imports * chore: remove obsolete ! * feat: implement accounts bundle * refactor: review suggestion * fix: rebase upgrade error * fix: rebase bug * feat: reset password link support * test: add tests for account password reset page * fix: remove redirect uri * fix: revert local state changes
This commit is contained in:
Vendored
+1
@@ -14,6 +14,7 @@
|
||||
"tslint.exclude": "**/node_modules/**",
|
||||
"tslint.autoFixOnSave": true,
|
||||
"tslint.jsEnable": true,
|
||||
"tslint.nodePath": "node_modules/.bin/tslint",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"postcss.validate": false
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ Preview Talk easily by running Talk via a Heroku App:
|
||||
- [Docker](#docker)
|
||||
- [Source](#source)
|
||||
- [Development](#development)
|
||||
- [Email](#email)
|
||||
- [Design Language System (UI Components)](#design-language-system-ui-components)
|
||||
- [Configuration](#configuration)
|
||||
- [License](#license)
|
||||
@@ -167,7 +168,7 @@ Then start Talk with:
|
||||
npm run watch
|
||||
```
|
||||
|
||||
When the client code has been built, navigate to http://localhost:8080/install.html
|
||||
When the client code has been built, navigate to http://localhost:8080/install
|
||||
to start the installation wizard. **Note: Ensure `localhost:8080` is used in the permitted domains list.**
|
||||
|
||||
To see the comment stream goto http://localhost:8080/.
|
||||
@@ -182,6 +183,32 @@ npm run lint
|
||||
npm run test
|
||||
```
|
||||
|
||||
#### Email
|
||||
|
||||
To test out the email sending functionality, you can run [inbucket](https://www.inbucket.org/)
|
||||
which provides a test SMTP server that can visualize emails in the browser:
|
||||
|
||||
```bash
|
||||
docker run -d --name inbucket -p 2500:2500 -p 9000:9000 inbucket/inbucket
|
||||
```
|
||||
|
||||
You can then configure the email server on Talk by updating the Tenant with:
|
||||
|
||||
```js
|
||||
{
|
||||
// ...
|
||||
"email": {
|
||||
"enabled": true,
|
||||
"smtpURI": "smtp://localhost:2500",
|
||||
"fromAddress": "community@test.com"
|
||||
},
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Restarting Talk will be needed. Navigate to http://localhost:9000, click the
|
||||
"Monitor" tab. New emails received on this screen.
|
||||
|
||||
#### Design Language System (UI Components)
|
||||
|
||||
We use [docz](https://docz.site) to document and develop our Design Language System. To start docz run:
|
||||
|
||||
@@ -27,6 +27,7 @@ module.exports = {
|
||||
"[/\\\\]node_modules[/\\\\](?!(fluent|react-relay-network-modern)[/\\\\]).+\\.(js|jsx|mjs|ts|tsx)$",
|
||||
],
|
||||
moduleNameMapper: {
|
||||
"^talk-account/(.*)$": "<rootDir>/src/core/client/account/$1",
|
||||
"^talk-admin/(.*)$": "<rootDir>/src/core/client/admin/$1",
|
||||
"^talk-auth/(.*)$": "<rootDir>/src/core/client/auth/$1",
|
||||
"^talk-ui/(.*)$": "<rootDir>/src/core/client/ui/$1",
|
||||
|
||||
@@ -31,6 +31,23 @@ const config: Config = {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
generateRelayAccount: {
|
||||
paths: [
|
||||
"core/client/account/**/*.ts",
|
||||
"core/client/account/**/*.tsx",
|
||||
"core/client/account/**/*.graphql",
|
||||
"core/server/**/*.graphql",
|
||||
],
|
||||
ignore: [
|
||||
"core/**/*.d.ts",
|
||||
"core/**/*.graphql.ts",
|
||||
"**/test/**/*",
|
||||
"core/**/*.spec.*",
|
||||
],
|
||||
executor: new CommandExecutor("npm run generate:relay-account", {
|
||||
runOnInit: true,
|
||||
}),
|
||||
},
|
||||
generateRelayAdmin: {
|
||||
paths: [
|
||||
"core/client/admin/**/*.ts",
|
||||
@@ -131,6 +148,7 @@ const config: Config = {
|
||||
"generateRelayStream",
|
||||
"generateRelayAuth",
|
||||
"generateRelayInstall",
|
||||
"generateRelayAccount",
|
||||
"generateRelayAdmin",
|
||||
"generateSchemaTypes",
|
||||
],
|
||||
|
||||
@@ -68,8 +68,12 @@ export default function({
|
||||
historyApiFallback: {
|
||||
disableDotRule: true,
|
||||
rewrites: [
|
||||
{ from: /^\/account/, to: "/account.html" },
|
||||
{ from: /^\/admin/, to: "/admin.html" },
|
||||
{ from: /^\/embed\/stream/, to: "/stream.html" },
|
||||
{ from: /^\/embed\/auth/, to: "/auth.html" },
|
||||
{ from: /^\/embed\/auth\/callback/, to: "/auth-callback.html" },
|
||||
{ from: /^\/install/, to: "/install.html" },
|
||||
],
|
||||
},
|
||||
public: allowedHost,
|
||||
|
||||
Generated
+915
-319
File diff suppressed because it is too large
Load Diff
+16
-14
@@ -56,7 +56,7 @@
|
||||
"akismet-api": "^4.2.0",
|
||||
"apollo-server-express": "^2.1.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bull": "^3.4.4",
|
||||
"bull": "^3.8.1",
|
||||
"bunyan": "^1.8.12",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
"consolidate": "0.14.0",
|
||||
@@ -72,6 +72,7 @@
|
||||
"express-static-gzip": "^0.3.2",
|
||||
"farce": "^0.2.6",
|
||||
"fluent": "^0.10.0",
|
||||
"fluent-dom": "^0.4.1",
|
||||
"found": "^0.3.21",
|
||||
"found-relay": "^0.3.1",
|
||||
"fs-extra": "^6.0.1",
|
||||
@@ -83,15 +84,16 @@
|
||||
"graphql-tools": "^3.0.5",
|
||||
"html-minifier": "^3.5.21",
|
||||
"html-to-text": "^4.0.0",
|
||||
"ioredis": "^3.2.2",
|
||||
"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",
|
||||
"linkify-it": "^2.1.0",
|
||||
"linkifyjs": "^2.1.8",
|
||||
"lodash": "^4.17.10",
|
||||
"luxon": "^1.3.1",
|
||||
"luxon": "^1.12.0",
|
||||
"metascraper-author": "^3.11.8",
|
||||
"metascraper-date": "^3.11.4",
|
||||
"metascraper-description": "^3.11.8",
|
||||
@@ -132,7 +134,7 @@
|
||||
"@coralproject/rte": "^0.10.13",
|
||||
"@intervolga/optimize-cssnano-plugin": "^1.0.6",
|
||||
"@types/bcryptjs": "^2.4.1",
|
||||
"@types/bull": "^3.3.16",
|
||||
"@types/bull": "^3.5.12",
|
||||
"@types/bunyan": "^1.8.4",
|
||||
"@types/case-sensitive-paths-webpack-plugin": "^2.1.2",
|
||||
"@types/cheerio": "^0.22.8",
|
||||
@@ -153,7 +155,7 @@
|
||||
"@types/html-minifier": "^3.5.2",
|
||||
"@types/html-to-text": "^1.4.31",
|
||||
"@types/html-webpack-plugin": "^3.2.0",
|
||||
"@types/ioredis": "^3.2.12",
|
||||
"@types/ioredis": "^4.0.10",
|
||||
"@types/jest": "^23.1.5",
|
||||
"@types/joi": "^13.0.8",
|
||||
"@types/jsdom": "^12.2.3",
|
||||
@@ -169,21 +171,21 @@
|
||||
"@types/node": "^10.5.2",
|
||||
"@types/node-fetch": "^2.3.3",
|
||||
"@types/nodemailer": "^4.6.2",
|
||||
"@types/nunjucks": "^3.0.0",
|
||||
"@types/nunjucks": "^3.1.1",
|
||||
"@types/passport": "^0.4.6",
|
||||
"@types/passport-facebook": "^2.1.8",
|
||||
"@types/passport-local": "^1.0.33",
|
||||
"@types/passport-oauth2": "^1.4.5",
|
||||
"@types/passport-strategy": "^0.2.33",
|
||||
"@types/prop-types": "^15.5.8",
|
||||
"@types/react": "^16.8.10",
|
||||
"@types/react": "^16.8.15",
|
||||
"@types/react-copy-to-clipboard": "^4.2.6",
|
||||
"@types/react-dom": "^16.8.3",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/react-relay": "^1.3.9",
|
||||
"@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/recompose": "^0.26.5",
|
||||
"@types/relay-runtime": "^1.3.6",
|
||||
"@types/sane": "^2.0.0",
|
||||
"@types/shallow-equals": "^1.0.0",
|
||||
@@ -231,7 +233,7 @@
|
||||
"enzyme-adapter-react-16": "^1.12.1",
|
||||
"enzyme-to-json": "^3.3.5",
|
||||
"eventemitter2": "^5.0.1",
|
||||
"final-form": "^4.8.1",
|
||||
"final-form": "4.11.0",
|
||||
"flat": "^4.1.0",
|
||||
"fluent-intl-polyfill": "^0.1.0",
|
||||
"fluent-langneg": "^0.1.1",
|
||||
@@ -273,15 +275,15 @@
|
||||
"pstree.remy": "^1.1.6",
|
||||
"pym.js": "^1.3.2",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react": "^16.8.4",
|
||||
"react": "^16.8.6",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-dev-utils": "^9.0.0",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-final-form": "^3.6.4",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-final-form": "4.0.2",
|
||||
"react-popper": "^1.3.2",
|
||||
"react-relay": "^1.7.0-rc.1",
|
||||
"react-responsive": "^5.0.0",
|
||||
"react-test-renderer": "^16.8.4",
|
||||
"react-test-renderer": "^16.8.6",
|
||||
"react-timeago": "^4.1.9",
|
||||
"react-transition-group": "^2.5.0",
|
||||
"react-with-state-props": "^2.0.4",
|
||||
|
||||
+1
-9
@@ -12,7 +12,7 @@ deploy_tag() {
|
||||
# v5.0.0-beta.1 will be tagged with 5.0.0-beta.1
|
||||
# v5.0.0 will be tagged with 5, 5.0, 5.0.0
|
||||
#
|
||||
if [ -n "$(echo $CIRCLE_TAG | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$')" ]
|
||||
if [[ -n "$(echo $CIRCLE_TAG | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$')" ]]
|
||||
then
|
||||
major=$(echo ${CIRCLE_TAG/#v} | cut -d. -f1)
|
||||
minor=$(echo ${CIRCLE_TAG/#v} | cut -d. -f2)
|
||||
@@ -40,14 +40,6 @@ deploy_tag() {
|
||||
echo "==> pushing $version"
|
||||
docker push coralproject/talk:$version
|
||||
done
|
||||
|
||||
# Deploy latest if we're at master, or deploy this branch if we aren't.
|
||||
if [ "$CIRCLE_BRANCH" = "master" ]
|
||||
then
|
||||
deploy_latest
|
||||
else
|
||||
deploy_branch
|
||||
fi
|
||||
}
|
||||
|
||||
deploy_latest() {
|
||||
|
||||
@@ -20,13 +20,7 @@ async function run(
|
||||
config = config.default;
|
||||
}
|
||||
|
||||
try {
|
||||
await watch(config, { only });
|
||||
} catch (err) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
await watch(config, { only });
|
||||
}
|
||||
|
||||
const cmd = program
|
||||
@@ -36,4 +30,8 @@ const cmd = program
|
||||
.description("Run watchers defined in <configFile>")
|
||||
.parse(process.argv);
|
||||
|
||||
run(cmd.args, cmd.opts());
|
||||
run(cmd.args, cmd.opts()).catch(err => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -101,6 +101,10 @@ export default async function watch(config: Config, options: Options = {}) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(chalk.cyanBright(`Start watcher "${key}"`));
|
||||
const watcherConfig = watchersConfigs[key];
|
||||
beginWatch(watcher, key, watcherConfig, rootDir);
|
||||
beginWatch(watcher, key, watcherConfig, rootDir).catch(err => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +284,22 @@ export default function createWebpackConfig(
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: paths.appAccountLocalesTemplate,
|
||||
use: [
|
||||
// This is the locales loader that loads available locales
|
||||
// from a particular target.
|
||||
{
|
||||
loader: "locales-loader",
|
||||
options: {
|
||||
...localesOptions,
|
||||
// Target specifies the prefix for fluent files to be loaded.
|
||||
// ${target}-xyz.ftl and ${†arget}.ftl are loaded into the locales.
|
||||
target: "account",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: paths.appAdminLocalesTemplate,
|
||||
use: [
|
||||
@@ -550,6 +566,13 @@ export default function createWebpackConfig(
|
||||
...devServerEntries,
|
||||
paths.appInstallIndex,
|
||||
],
|
||||
account: [
|
||||
// We ship polyfills by default
|
||||
paths.appPolyfill,
|
||||
...ifBuild(paths.appPublicPath),
|
||||
...devServerEntries,
|
||||
paths.appAccountIndex,
|
||||
],
|
||||
admin: [
|
||||
// We ship polyfills by default
|
||||
paths.appPolyfill,
|
||||
@@ -593,6 +616,13 @@ export default function createWebpackConfig(
|
||||
chunks: ["install"],
|
||||
inject: "body",
|
||||
}),
|
||||
// Generates an `account.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "account.html",
|
||||
template: paths.appAccountHTML,
|
||||
chunks: ["account"],
|
||||
inject: "body",
|
||||
}),
|
||||
// Generates an `admin.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "admin.html",
|
||||
|
||||
@@ -38,6 +38,10 @@ export default {
|
||||
appInstallLocalesTemplate: resolveSrc("core/client/install/locales.ts"),
|
||||
appInstallIndex: resolveSrc("core/client/install/index.tsx"),
|
||||
|
||||
appAccountHTML: resolveSrc("core/client/account/index.html"),
|
||||
appAccountLocalesTemplate: resolveSrc("core/client/account/locales.ts"),
|
||||
appAccountIndex: resolveSrc("core/client/account/index.tsx"),
|
||||
|
||||
appAdminHTML: resolveSrc("core/client/admin/index.html"),
|
||||
appAdminLocalesTemplate: resolveSrc("core/client/admin/locales.ts"),
|
||||
appAdminIndex: resolveSrc("core/client/admin/index.tsx"),
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
const path = require("path");
|
||||
module.exports = {
|
||||
extends: "../.babelrc.js",
|
||||
plugins: [
|
||||
[
|
||||
"babel-plugin-relay",
|
||||
{ artifactDirectory: path.resolve(__dirname, "./__generated__") },
|
||||
],
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
:global {
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { BrowserProtocol, queryMiddleware } from "farce";
|
||||
import { createFarceRouter, ElementsRenderer } from "found";
|
||||
import { Resolver } from "found-relay";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import TransitionControl from "talk-framework/testHelpers/TransitionControl";
|
||||
|
||||
import { TalkContextConsumer } from "talk-framework/lib/bootstrap/TalkContext";
|
||||
|
||||
import routeConfig from "./routeConfig";
|
||||
import NotFound from "./routes/NotFound";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
const Router = createFarceRouter({
|
||||
historyProtocol: new BrowserProtocol(),
|
||||
historyMiddlewares: [queryMiddleware],
|
||||
routeConfig,
|
||||
renderReady: ({ elements }) => (
|
||||
<>
|
||||
<ElementsRenderer elements={elements} />
|
||||
{// this enables router transition control when writing tests.
|
||||
process.env.NODE_ENV === "test" && <TransitionControl />}
|
||||
</>
|
||||
),
|
||||
renderError: ({ error }) => (
|
||||
<div>{error.status === 404 ? <NotFound /> : "Error"}</div>
|
||||
),
|
||||
});
|
||||
|
||||
const EntryContainer: FunctionComponent = () => (
|
||||
<TalkContextConsumer>
|
||||
{({ relayEnvironment }) => (
|
||||
<Router resolver={new Resolver(relayEnvironment)} />
|
||||
)}
|
||||
</TalkContextConsumer>
|
||||
);
|
||||
|
||||
export default EntryContainer;
|
||||
@@ -0,0 +1,15 @@
|
||||
.bar {
|
||||
height: calc(6 * var(--mini-unit));
|
||||
background-color: var(--palette-text-primary);
|
||||
}
|
||||
|
||||
.centered {
|
||||
max-width: calc(70 * var(--mini-unit));
|
||||
margin: 10% auto;
|
||||
padding: 0 calc(0.5 * var(--mini-unit));
|
||||
box-sizing: border-box;
|
||||
|
||||
@media (min-width: $breakpoints-xs) {
|
||||
max-width: calc(42 * var(--mini-unit));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
import styles from "./MainLayout.css";
|
||||
|
||||
const MainLayout: React.FunctionComponent = ({ children }) => (
|
||||
<div data-testid="main-layout">
|
||||
<div className={styles.bar} />
|
||||
<div className={styles.centered}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default MainLayout;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { createFetch } from "talk-framework/lib/relay";
|
||||
|
||||
const CheckResetTokenFetch = createFetch(
|
||||
"resetToken",
|
||||
async (environment: Environment, variables: { token: string }, { rest }) =>
|
||||
await rest.fetch<void>("/auth/local/forgot", {
|
||||
method: "GET",
|
||||
token: variables.token,
|
||||
})
|
||||
);
|
||||
|
||||
export default CheckResetTokenFetch;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as CheckResetTokenFetch } from "./CheckResetTokenFetch";
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Talk - Account</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { createManaged } from "talk-framework/lib/bootstrap";
|
||||
|
||||
import App from "./App";
|
||||
import { initLocalState } from "./local";
|
||||
import localesData from "./locales";
|
||||
|
||||
async function main() {
|
||||
const ManagedTalkContextProvider = await createManaged({
|
||||
initLocalState,
|
||||
localesData,
|
||||
userLocales: navigator.languages,
|
||||
});
|
||||
|
||||
const Index: FunctionComponent = () => (
|
||||
<ManagedTalkContextProvider>
|
||||
<App />
|
||||
</ManagedTalkContextProvider>
|
||||
);
|
||||
|
||||
ReactDOM.render(<Index />, document.getElementById("app"));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1 @@
|
||||
export { default as initLocalState } from "./initLocalState";
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { initLocalBaseState } from "talk-framework/lib/relay";
|
||||
|
||||
/**
|
||||
* Initializes the local state, before we start the App.
|
||||
*/
|
||||
export default async function initLocalState(
|
||||
environment: Environment,
|
||||
context: TalkContext
|
||||
) {
|
||||
await initLocalBaseState(environment, context);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
type Local {
|
||||
accessToken: String
|
||||
accessTokenExp: Int
|
||||
accessTokenJTI: String
|
||||
loggedIn: Boolean!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
local: Local!
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* The actual content of this file is being generated by our `locales-loader`.
|
||||
* Please check `./src/loaders` and the webpack config for more information.
|
||||
*
|
||||
* This file only represents the types that gets exported.
|
||||
*/
|
||||
|
||||
import { LocalesData } from "talk-framework/lib/i18n";
|
||||
export default {} as LocalesData;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { createMutation } from "talk-framework/lib/relay";
|
||||
|
||||
const ResetPasswordMutation = createMutation(
|
||||
"resetPassword",
|
||||
async (
|
||||
environment: Environment,
|
||||
variables: { token: string; password: string },
|
||||
{ rest }
|
||||
) =>
|
||||
await rest.fetch<void>("/auth/local/forgot", {
|
||||
method: "PUT",
|
||||
token: variables.token,
|
||||
body: {
|
||||
password: variables.password,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default ResetPasswordMutation;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ResetPasswordMutation } from "./ResetPasswordMutation";
|
||||
@@ -0,0 +1,13 @@
|
||||
import { makeRouteConfig, Route } from "found";
|
||||
import React from "react";
|
||||
|
||||
import MainLayout from "./components/MainLayout";
|
||||
import ResetPassword from "./routes/password/reset/Index";
|
||||
|
||||
export default makeRouteConfig(
|
||||
<Route path="account" Component={MainLayout}>
|
||||
<Route path="password">
|
||||
<Route path="reset" {...ResetPassword.routeConfig} />
|
||||
</Route>
|
||||
</Route>
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { HorizontalGutter, Typography } from "talk-ui/components";
|
||||
|
||||
const NotFound: FunctionComponent = ({ children }) => (
|
||||
<HorizontalGutter>
|
||||
<Typography variant="heading3">Not Found</Typography>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
|
||||
export default NotFound;
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
|
||||
import { withRouteConfig } from "talk-framework/lib/router";
|
||||
|
||||
import { parseHashQuery } from "talk-framework/utils";
|
||||
import ResetPasswordForm from "./ResetPasswordForm";
|
||||
import ResetTokenChecker from "./ResetTokenChecker";
|
||||
import Success from "./Success";
|
||||
|
||||
interface Props {
|
||||
token: string | undefined;
|
||||
}
|
||||
|
||||
const Index: React.FunctionComponent<Props> = ({ token }) => {
|
||||
const [suceeded, setSuceeded] = useState<boolean>(false);
|
||||
const onSuccess = useCallback(() => {
|
||||
setSuceeded(true);
|
||||
}, []);
|
||||
return (
|
||||
<ResetTokenChecker token={token}>
|
||||
{!suceeded && <ResetPasswordForm token={token!} onSuccess={onSuccess} />}
|
||||
{suceeded && <Success />}
|
||||
</ResetTokenChecker>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
render: ({ match, Component }) => (
|
||||
<Component token={parseHashQuery(match.location.hash).resetToken} />
|
||||
),
|
||||
})(Index);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,147 @@
|
||||
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 { ResetPasswordMutation } from "talk-account/mutations";
|
||||
import { InvalidRequestError } from "talk-framework/lib/errors";
|
||||
import { useMutation } from "talk-framework/lib/relay";
|
||||
import {
|
||||
composeValidators,
|
||||
required,
|
||||
validatePassword,
|
||||
} from "talk-framework/lib/validation";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
FormField,
|
||||
HorizontalGutter,
|
||||
InputDescription,
|
||||
InputLabel,
|
||||
PasswordField,
|
||||
Typography,
|
||||
ValidationMessage,
|
||||
} from "talk-ui/components";
|
||||
|
||||
interface Props {
|
||||
token: string;
|
||||
disabled?: boolean;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
password: string;
|
||||
}
|
||||
|
||||
const ResetPasswordForm: React.FunctionComponent<Props> = ({
|
||||
onSuccess,
|
||||
token,
|
||||
disabled,
|
||||
}) => {
|
||||
const resetPassword = useMutation(ResetPasswordMutation);
|
||||
const onSubmit = useCallback(
|
||||
async ({ password }: FormProps) => {
|
||||
try {
|
||||
await resetPassword({ token, password });
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidRequestError) {
|
||||
return error.invalidArgs;
|
||||
}
|
||||
return { [FORM_ERROR]: error.message };
|
||||
}
|
||||
return;
|
||||
},
|
||||
[token]
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<Form onSubmit={onSubmit}>
|
||||
{({ handleSubmit, submitting, submitError }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="double">
|
||||
<HorizontalGutter>
|
||||
<Localized id="resetPassword-resetYourPassword">
|
||||
<Typography variant="heading1">
|
||||
Reset your password
|
||||
</Typography>
|
||||
</Localized>
|
||||
<Localized id="resetPassword-pleaseEnterNewPassword">
|
||||
<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.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
<HorizontalGutter>
|
||||
<Field
|
||||
name="password"
|
||||
validate={composeValidators(required, validatePassword)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="resetPassword-passwordLabel">
|
||||
<InputLabel htmlFor={input.name}>Password</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="resetPassword-passwordDescription"
|
||||
$minLength={8}
|
||||
>
|
||||
<InputDescription>
|
||||
{"Must be at least {$minLength} characters"}
|
||||
</InputDescription>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="resetPassword-passwordTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<PasswordField
|
||||
id={input.name}
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Password"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
{meta.touched && (meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
<Localized id="resetPassword-resetPassword">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="filled"
|
||||
color="primary"
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPasswordForm;
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { CheckResetTokenFetch } from "talk-account/fetches";
|
||||
import { ERROR_CODES } from "talk-common/errors";
|
||||
import { InvalidRequestError } from "talk-framework/lib/errors";
|
||||
import { useFetch } from "talk-framework/lib/relay";
|
||||
import { Delay, Flex, Spinner } from "talk-ui/components";
|
||||
|
||||
import Sorry from "./Sorry";
|
||||
|
||||
interface Props {
|
||||
token: string | undefined;
|
||||
}
|
||||
|
||||
type TokenState =
|
||||
| "VALID"
|
||||
| "INVALID"
|
||||
| "EXPIRED"
|
||||
| "MISSING"
|
||||
| "RATE_LIMIT_EXCEEDED"
|
||||
| "UNKNOWN"
|
||||
| "UNCHECKED";
|
||||
|
||||
const ResetTokenChecker: React.FunctionComponent<Props> = ({
|
||||
token,
|
||||
children,
|
||||
}) => {
|
||||
const checkResetToken = useFetch(CheckResetTokenFetch);
|
||||
const [tokenState, setTokenState] = useState<TokenState>("UNCHECKED");
|
||||
const [reason, setReason] = useState<string>("");
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
async function setAndCheckToken() {
|
||||
try {
|
||||
await checkResetToken({ token: token! });
|
||||
setTokenState("VALID");
|
||||
} catch (e) {
|
||||
setReason(e.message);
|
||||
if (e instanceof InvalidRequestError) {
|
||||
switch (e.code) {
|
||||
case ERROR_CODES.RATE_LIMIT_EXCEEDED:
|
||||
setTokenState("RATE_LIMIT_EXCEEDED");
|
||||
return;
|
||||
case ERROR_CODES.PASSWORD_RESET_TOKEN_EXPIRED:
|
||||
setTokenState("EXPIRED");
|
||||
return;
|
||||
case ERROR_CODES.INTEGRATION_DISABLED:
|
||||
case ERROR_CODES.USER_NOT_FOUND:
|
||||
case ERROR_CODES.TOKEN_INVALID:
|
||||
setTokenState("INVALID");
|
||||
return;
|
||||
default:
|
||||
setTokenState("UNKNOWN");
|
||||
return;
|
||||
}
|
||||
}
|
||||
setTokenState("UNKNOWN");
|
||||
}
|
||||
}
|
||||
setAndCheckToken();
|
||||
} else {
|
||||
setTokenState("MISSING");
|
||||
}
|
||||
return;
|
||||
}, [token]);
|
||||
|
||||
switch (tokenState) {
|
||||
case "VALID":
|
||||
return <>{children}</>;
|
||||
case "UNCHECKED":
|
||||
return (
|
||||
<Flex justifyContent="center">
|
||||
<Delay>
|
||||
<Spinner />
|
||||
</Delay>
|
||||
</Flex>
|
||||
);
|
||||
case "MISSING":
|
||||
return (
|
||||
<Sorry
|
||||
reason={
|
||||
<Localized id="resetPassword-missingResetToken">
|
||||
<span>The Reset Token seems to be missing.</span>
|
||||
</Localized>
|
||||
}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <Sorry reason={reason} />;
|
||||
}
|
||||
};
|
||||
|
||||
export default ResetTokenChecker;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
|
||||
import { CallOut, HorizontalGutter, Typography } from "talk-ui/components";
|
||||
|
||||
interface Props {
|
||||
reason: React.ReactNode;
|
||||
}
|
||||
|
||||
const Sorry: React.FunctionComponent<Props> = ({ reason }) => {
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="resetPassword-oopsSorry">
|
||||
<Typography variant="heading1">Oops Sorry!</Typography>
|
||||
</Localized>
|
||||
<CallOut color="error" fullWidth>
|
||||
{reason}
|
||||
</CallOut>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sorry;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
|
||||
import { HorizontalGutter, Typography } from "talk-ui/components";
|
||||
|
||||
const Sorry: React.FunctionComponent = () => {
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="resetPassword-successfullyReset">
|
||||
<Typography variant="heading1">Password successfully reset</Typography>
|
||||
</Localized>
|
||||
<Localized id="resetPassword-youMayClose">
|
||||
<Typography variant="bodyCopy">
|
||||
You may now close this window and sign in to your account with your
|
||||
new password.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sorry;
|
||||
@@ -0,0 +1,133 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders form 1`] = `
|
||||
<div
|
||||
data-testid="main-layout"
|
||||
>
|
||||
<div
|
||||
className="MainLayout-bar"
|
||||
/>
|
||||
<div
|
||||
className="MainLayout-centered"
|
||||
>
|
||||
<div>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextPrimary"
|
||||
>
|
||||
Reset your password
|
||||
</h1>
|
||||
<p
|
||||
className="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.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<p
|
||||
className="Typography-root Typography-detail Typography-colorTextSecondary"
|
||||
>
|
||||
Must be at least 8 characters
|
||||
</p>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
>
|
||||
<input
|
||||
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
|
||||
id="password"
|
||||
name="password"
|
||||
onChange={[Function]}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Hide password"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Reset Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders missing reset token 1`] = `
|
||||
<div
|
||||
data-testid="main-layout"
|
||||
>
|
||||
<div
|
||||
className="MainLayout-bar"
|
||||
/>
|
||||
<div
|
||||
className="MainLayout-centered"
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextPrimary"
|
||||
>
|
||||
Oops Sorry!
|
||||
</h1>
|
||||
<div
|
||||
className="CallOut-root CallOut-colorError CallOut-fullWidth"
|
||||
>
|
||||
<span>
|
||||
The Reset Token seems to be missing.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
|
||||
import App from "talk-account/App";
|
||||
import { GQLResolver } from "talk-framework/schema";
|
||||
import {
|
||||
createTestRenderer,
|
||||
CreateTestRendererParams,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
export default function create(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
return createTestRenderer<GQLResolver>("account", <App />, params);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import sinon from "sinon";
|
||||
|
||||
import { GQLResolver } from "talk-framework/schema";
|
||||
import {
|
||||
createAccessToken,
|
||||
CreateTestRendererParams,
|
||||
replaceHistoryLocation,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import { ERROR_CODES } from "talk-common/errors";
|
||||
import { InvalidRequestError } from "talk-framework/lib/errors";
|
||||
import create from "./create";
|
||||
|
||||
const token = createAccessToken();
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context } = create();
|
||||
return {
|
||||
context,
|
||||
testRenderer,
|
||||
root: testRenderer.root,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders missing reset token", async () => {
|
||||
replaceHistoryLocation("http://localhost/account/password/reset");
|
||||
const { root } = await createTestRenderer();
|
||||
await waitForElement(() =>
|
||||
within(root).getByText("The Reset Token seems to be missing", {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
expect(within(root).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders form", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/password/reset#resetToken=${token}`
|
||||
);
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/auth/local/forgot", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once();
|
||||
|
||||
await waitForElement(() =>
|
||||
within(root).getByText("Reset your password", {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
expect(within(root).toJSON()).toMatchSnapshot();
|
||||
restMock.verify();
|
||||
});
|
||||
|
||||
it("renders error from server", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/password/reset#resetToken=${token}`
|
||||
);
|
||||
|
||||
const codes = [
|
||||
ERROR_CODES.RATE_LIMIT_EXCEEDED,
|
||||
ERROR_CODES.PASSWORD_RESET_TOKEN_EXPIRED,
|
||||
ERROR_CODES.INTEGRATION_DISABLED,
|
||||
ERROR_CODES.USER_NOT_FOUND,
|
||||
ERROR_CODES.TOKEN_INVALID,
|
||||
];
|
||||
|
||||
for (const code of codes) {
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/auth/local/forgot", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once()
|
||||
.throwsException(
|
||||
new InvalidRequestError({
|
||||
code,
|
||||
})
|
||||
);
|
||||
|
||||
await waitForElement(() =>
|
||||
within(root).getByText(code, {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
restMock.verify();
|
||||
}
|
||||
});
|
||||
|
||||
it("submits form", async () => {
|
||||
replaceHistoryLocation(
|
||||
`http://localhost/account/password/reset#resetToken=${token}`
|
||||
);
|
||||
const { root, context } = await createTestRenderer();
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/auth/local/forgot", {
|
||||
method: "GET",
|
||||
token,
|
||||
})
|
||||
.once();
|
||||
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/auth/local/forgot", {
|
||||
method: "PUT",
|
||||
token,
|
||||
body: {
|
||||
password: "testtest",
|
||||
},
|
||||
})
|
||||
.once();
|
||||
|
||||
await waitForElement(() =>
|
||||
within(root).getByText("Reset your password", {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
|
||||
const form = within(root).getByType("form");
|
||||
const textField = within(root).getByLabelText("Password");
|
||||
|
||||
// Submit an empty form.
|
||||
form.props.onSubmit();
|
||||
within(root).getByText("field is required", {
|
||||
exact: false,
|
||||
});
|
||||
|
||||
// Password too short.
|
||||
textField.props.onChange("test");
|
||||
within(root).getByText("Password must contain at least 8 characters", {
|
||||
exact: false,
|
||||
});
|
||||
|
||||
// Submit valid form.
|
||||
textField.props.onChange("testtest");
|
||||
form.props.onSubmit();
|
||||
|
||||
await waitForElement(() =>
|
||||
within(root).getByText("successfully", {
|
||||
exact: false,
|
||||
})
|
||||
);
|
||||
|
||||
restMock.verify();
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { Logo } from "talk-ui/components";
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const App: StatelessComponent<Props> = ({ children, viewer }) => (
|
||||
const App: FunctionComponent<Props> = ({ children, viewer }) => (
|
||||
<div className={styles.root}>
|
||||
<AppBar gutterBegin gutterEnd>
|
||||
<Begin itemGutter="double">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import {
|
||||
BrandIcon,
|
||||
@@ -15,7 +15,7 @@ interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const AuthBox: StatelessComponent<Props> = ({ title, children }) => {
|
||||
const AuthBox: FunctionComponent<Props> = ({ title, children }) => {
|
||||
return (
|
||||
<div data-testid="authBox">
|
||||
<Flex justifyContent="center">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { BaseButton, ClickOutside, Icon, Popover } from "talk-ui/components";
|
||||
|
||||
@@ -9,7 +9,7 @@ import styles from "./DecisionHistoryButton.css";
|
||||
|
||||
const popoverID = "decision-history-popover";
|
||||
|
||||
const DecisionHistoryButton: StatelessComponent = () => (
|
||||
const DecisionHistoryButton: FunctionComponent = () => (
|
||||
<Localized id="decisionHistory-popover" attrs={{ description: true }}>
|
||||
<Popover
|
||||
data-testid="decisionHistory-popover"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cn from "classnames";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./MainLayout.css";
|
||||
|
||||
@@ -8,7 +8,7 @@ interface Props {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const MainLayout: StatelessComponent<Props> = ({
|
||||
const MainLayout: FunctionComponent<Props> = ({
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { AppBarNavigation } from "talk-ui/components";
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
showConfigure: boolean;
|
||||
}
|
||||
|
||||
const Navigation: StatelessComponent<Props> = props => (
|
||||
const Navigation: FunctionComponent<Props> = props => (
|
||||
<AppBarNavigation>
|
||||
<Localized id="navigation-moderate">
|
||||
<NavigationLink to="/admin/moderate">Moderate</NavigationLink>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link, LocationDescriptor } from "found";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { AppBarNavigationItem } from "talk-ui/components";
|
||||
|
||||
@@ -8,7 +8,7 @@ interface Props {
|
||||
to: string | LocationDescriptor;
|
||||
}
|
||||
|
||||
const NavigationLink: StatelessComponent<Props> = props => (
|
||||
const NavigationLink: FunctionComponent<Props> = props => (
|
||||
<Link to={props.to} Component={AppBarNavigationItem} activePropName="active">
|
||||
{props.children}
|
||||
</Link>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./NotAvailable.css";
|
||||
|
||||
const NotAvailable: StatelessComponent = props => (
|
||||
const NotAvailable: FunctionComponent = props => (
|
||||
<Localized id="general-notAvailable">
|
||||
<span className={styles.root}>Not available</span>
|
||||
</Localized>
|
||||
|
||||
@@ -19,7 +19,7 @@ function createElement(
|
||||
}
|
||||
}
|
||||
|
||||
const TranslatedRole: React.StatelessComponent<Props> = props => {
|
||||
const TranslatedRole: React.FunctionComponent<Props> = props => {
|
||||
switch (props.children) {
|
||||
case GQLUSER_ROLE.COMMENTER:
|
||||
return (
|
||||
|
||||
@@ -19,7 +19,7 @@ function createElement(
|
||||
}
|
||||
}
|
||||
|
||||
const TranslatedRole: React.StatelessComponent<Props> = props => {
|
||||
const TranslatedRole: React.FunctionComponent<Props> = props => {
|
||||
switch (props.children) {
|
||||
case GQLSTORY_STATUS.OPEN:
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -18,7 +18,7 @@ interface Props {
|
||||
onSignOut: React.EventHandler<React.MouseEvent>;
|
||||
}
|
||||
|
||||
const UserMenu: StatelessComponent<Props> = props => (
|
||||
const UserMenu: FunctionComponent<Props> = props => (
|
||||
<Localized id="userMenu-popover" attrs={{ description: true }}>
|
||||
<Popover
|
||||
id="userMenu"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Typography } from "talk-ui/components";
|
||||
|
||||
import styles from "./Version.css";
|
||||
|
||||
const Version: StatelessComponent = () => {
|
||||
const Version: FunctionComponent = () => {
|
||||
return (
|
||||
<Typography className={styles.version} variant="detail">
|
||||
{process.env.TALK_VERSION ? `v${process.env.TALK_VERSION}` : "Unknown"}
|
||||
|
||||
@@ -21,7 +21,7 @@ class AppContainer extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withRouteConfig({
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
query: graphql`
|
||||
query AppContainerQuery {
|
||||
viewer {
|
||||
|
||||
@@ -94,7 +94,7 @@ class AuthCheckContainer extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withRouteConfig({
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
query: graphql`
|
||||
query AuthCheckContainerQuery {
|
||||
viewer {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BrowserProtocol, queryMiddleware } from "farce";
|
||||
import { createFarceRouter, ElementsRenderer } from "found";
|
||||
import { Resolver } from "found-relay";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import TransitionControl from "talk-framework/testHelpers/TransitionControl";
|
||||
|
||||
import { TalkContextConsumer } from "talk-framework/lib/bootstrap/TalkContext";
|
||||
@@ -25,7 +25,7 @@ const Router = createFarceRouter({
|
||||
),
|
||||
});
|
||||
|
||||
const EntryContainer: StatelessComponent = () => (
|
||||
const EntryContainer: FunctionComponent = () => (
|
||||
<TalkContextConsumer>
|
||||
{({ relayEnvironment }) => (
|
||||
<Router resolver={new Resolver(relayEnvironment)} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { createManaged } from "talk-framework/lib/bootstrap";
|
||||
|
||||
@@ -13,7 +13,7 @@ async function main() {
|
||||
userLocales: navigator.languages,
|
||||
});
|
||||
|
||||
const Index: StatelessComponent = () => (
|
||||
const Index: FunctionComponent = () => (
|
||||
<ManagedTalkContextProvider>
|
||||
<EntryContainer />
|
||||
</ManagedTalkContextProvider>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutation } from "talk-framework/lib/relay";
|
||||
import { commit as setAccessToken } from "talk-framework/mutations/SetAccessTokenMutation";
|
||||
import SetAccessTokenMutation from "talk-framework/mutations/SetAccessTokenMutation";
|
||||
|
||||
const CompleteAccountMutation = createMutation(
|
||||
"completeAccount",
|
||||
@@ -13,7 +13,7 @@ const CompleteAccountMutation = createMutation(
|
||||
},
|
||||
context: TalkContext
|
||||
) =>
|
||||
await setAccessToken(
|
||||
await SetAccessTokenMutation.commit(
|
||||
environment,
|
||||
{ accessToken: input.accessToken },
|
||||
context
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { HorizontalGutter, Typography } from "talk-ui/components";
|
||||
|
||||
const NotFound: StatelessComponent = ({ children }) => (
|
||||
const NotFound: FunctionComponent = ({ children }) => (
|
||||
<HorizontalGutter>
|
||||
<Typography variant="heading3">Not Found</Typography>
|
||||
</HorizontalGutter>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import NotAvailable from "talk-admin/components/NotAvailable";
|
||||
import {
|
||||
@@ -21,7 +21,7 @@ interface Props {
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
const BanModal: StatelessComponent<Props> = ({
|
||||
const BanModal: FunctionComponent<Props> = ({
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import MainLayout from "talk-admin/components/MainLayout";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
query: PropTypesOf<typeof UserTableContainer>["query"];
|
||||
}
|
||||
|
||||
const Community: StatelessComponent<Props> = props => (
|
||||
const Community: FunctionComponent<Props> = props => (
|
||||
<MainLayout className={styles.root} data-testid="community-container">
|
||||
<UserTableContainer query={props.query} />
|
||||
</MainLayout>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Typography } from "talk-ui/components";
|
||||
|
||||
import styles from "./EmptyMessage.css";
|
||||
|
||||
const EmptyMessage: StatelessComponent = props => (
|
||||
const EmptyMessage: FunctionComponent = props => (
|
||||
<Localized id="community-emptyMessage">
|
||||
<Typography className={styles.root} variant="bodyCopyBold" align="center">
|
||||
We could not find anyone in your community matching your criteria.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import TranslatedRole from "talk-admin/components/TranslatedRole";
|
||||
import { GQLUSER_ROLE, GQLUSER_ROLE_RL } from "talk-framework/schema";
|
||||
@@ -20,7 +20,7 @@ interface Props {
|
||||
role: GQLUSER_ROLE_RL;
|
||||
}
|
||||
|
||||
const RoleChange: StatelessComponent<Props> = props => (
|
||||
const RoleChange: FunctionComponent<Props> = props => (
|
||||
<Localized id="community-role-popover" attrs={{ description: true }}>
|
||||
<Popover
|
||||
id="community-roleChange"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cn from "classnames";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import TranslatedRole from "talk-admin/components/TranslatedRole";
|
||||
import { GQLUSER_ROLE } from "talk-framework/schema";
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
children: PropTypesOf<typeof TranslatedRole>["children"];
|
||||
}
|
||||
|
||||
const RoleText: StatelessComponent<Props> = props => (
|
||||
const RoleText: FunctionComponent<Props> = props => (
|
||||
<TranslatedRole
|
||||
container={
|
||||
<span
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import NotAvailable from "talk-admin/components/NotAvailable";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
@@ -21,7 +21,7 @@ interface Props {
|
||||
user: PropTypesOf<typeof UserStatusChangeContainer>["user"];
|
||||
}
|
||||
|
||||
const UserRow: StatelessComponent<Props> = props => (
|
||||
const UserRow: FunctionComponent<Props> = props => (
|
||||
<TableRow>
|
||||
<TableCell className={styles.usernameColumn}>
|
||||
{props.username || <NotAvailable />}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Flex, Typography } from "talk-ui/components";
|
||||
import { PropTypesOf } from "talk-ui/types";
|
||||
@@ -25,7 +25,7 @@ const render = (
|
||||
</Typography>
|
||||
);
|
||||
|
||||
const UserStatus: StatelessComponent<Props> = props => {
|
||||
const UserStatus: FunctionComponent<Props> = props => {
|
||||
if (props.banned) {
|
||||
return render(
|
||||
"error",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -22,7 +22,7 @@ interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const UserStatusChange: StatelessComponent<Props> = props => (
|
||||
const UserStatusChange: FunctionComponent<Props> = props => (
|
||||
<Localized id="community-userStatus-popover" attrs={{ description: true }}>
|
||||
<Popover
|
||||
id="community-statusChange"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
@@ -28,7 +28,7 @@ interface Props {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const UserTable: StatelessComponent<Props> = props => (
|
||||
const UserTable: FunctionComponent<Props> = props => (
|
||||
<>
|
||||
<HorizontalGutter size="double">
|
||||
<Table fullWidth>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import {
|
||||
@@ -31,7 +31,7 @@ interface Props {
|
||||
onSetSearchFilter: (search: string) => void;
|
||||
}
|
||||
|
||||
const UserTableFilter: StatelessComponent<Props> = props => (
|
||||
const UserTableFilter: FunctionComponent<Props> = props => (
|
||||
<Flex itemGutter="double">
|
||||
<FieldSet>
|
||||
<Localized id="community-filter-search">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormApi } from "final-form";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { CommunityContainerQueryResponse } from "talk-admin/__generated__/CommunityContainerQuery.graphql";
|
||||
@@ -12,11 +12,11 @@ interface Props {
|
||||
form: FormApi;
|
||||
}
|
||||
|
||||
const CommunityContainer: StatelessComponent<Props> = props => {
|
||||
const CommunityContainer: FunctionComponent<Props> = props => {
|
||||
return <Community query={props.data} />;
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig({
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
query: graphql`
|
||||
query CommunityContainerQuery {
|
||||
...UserTableContainer_query
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent, useCallback } from "react";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
|
||||
import { UpdateUserRoleMutation } from "talk-admin/mutations";
|
||||
import { useMutation } from "talk-framework/lib/relay";
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
role: GQLUSER_ROLE_RL;
|
||||
}
|
||||
|
||||
const RoleChangeContainer: StatelessComponent<Props> = props => {
|
||||
const RoleChangeContainer: FunctionComponent<Props> = props => {
|
||||
const updateUserRole = useMutation(UpdateUserRoleMutation);
|
||||
const handleOnChangeRole = useCallback(
|
||||
(role: GQLUSER_ROLE_RL) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { UserRowContainer_user as UserData } from "talk-admin/__generated__/UserRowContainer_user.graphql";
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
viewer: ViewerData;
|
||||
}
|
||||
|
||||
const UserRowContainer: StatelessComponent<Props> = props => {
|
||||
const UserRowContainer: FunctionComponent<Props> = props => {
|
||||
const { locales } = useTalkContext();
|
||||
return (
|
||||
<UserRow
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent, useCallback, useState } from "react";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
|
||||
import { UserStatusChangeContainer_user as UserData } from "talk-admin/__generated__/UserStatusChangeContainer_user.graphql";
|
||||
import { BanUserMutation, RemoveUserBanMutation } from "talk-admin/mutations";
|
||||
@@ -18,7 +18,7 @@ interface Props {
|
||||
user: UserData;
|
||||
}
|
||||
|
||||
const UserStatusChangeContainer: StatelessComponent<Props> = props => {
|
||||
const UserStatusChangeContainer: FunctionComponent<Props> = props => {
|
||||
const banUser = useMutation(BanUserMutation);
|
||||
const removeUserBan = useMutation(RemoveUserBanMutation);
|
||||
const [showBanned, setShowBanned] = useState<boolean>(false);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { UserStatusContainer_user as UserData } from "talk-admin/__generated__/UserStatusContainer_user.graphql";
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
user: UserData;
|
||||
}
|
||||
|
||||
const UserStatusContainer: StatelessComponent<Props> = props => {
|
||||
const UserStatusContainer: FunctionComponent<Props> = props => {
|
||||
return (
|
||||
<UserStatus
|
||||
banned={props.user.status.current.includes(GQLUSER_STATUS.BANNED)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent, useState } from "react";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import { UserTableContainer_query as QueryData } from "talk-admin/__generated__/UserTableContainer_query.graphql";
|
||||
@@ -20,7 +20,7 @@ interface Props {
|
||||
relay: RelayPaginationProp;
|
||||
}
|
||||
|
||||
const UserTableContainer: StatelessComponent<Props> = props => {
|
||||
const UserTableContainer: FunctionComponent<Props> = props => {
|
||||
const users = props.query
|
||||
? props.query.users.edges.map(edge => edge.node)
|
||||
: [];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Flex } from "talk-ui/components";
|
||||
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ConfigBox: StatelessComponent<Props> = ({
|
||||
const ConfigBox: FunctionComponent<Props> = ({
|
||||
id,
|
||||
title,
|
||||
topRight,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import Subheader from "../components/Subheader";
|
||||
|
||||
const ConfigurationSubHeader: StatelessComponent<{}> = () => (
|
||||
const ConfigurationSubHeader: FunctionComponent<{}> = () => (
|
||||
<Localized id="configure-configurationSubHeader" strong={<strong />}>
|
||||
<Subheader>Configuration</Subheader>
|
||||
</Localized>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FormApi, FormState } from "final-form";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Form, FormSpy } from "react-final-form";
|
||||
|
||||
import MainLayout from "talk-admin/components/MainLayout";
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
const Configure: StatelessComponent<Props> = ({
|
||||
const Configure: FunctionComponent<Props> = ({
|
||||
onSubmit,
|
||||
onChange,
|
||||
children,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cn from "classnames";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Typography } from "talk-ui/components";
|
||||
import { PropTypesOf } from "talk-ui/types";
|
||||
@@ -8,11 +8,7 @@ import styles from "./Header.css";
|
||||
|
||||
type Props = PropTypesOf<typeof Typography>;
|
||||
|
||||
const Header: StatelessComponent<Props> = ({
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
}) => (
|
||||
const Header: FunctionComponent<Props> = ({ children, className, ...rest }) => (
|
||||
<Typography
|
||||
variant="heading1"
|
||||
className={cn(className, styles.root)}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./HorizontalRule.css";
|
||||
|
||||
const HorizontalRule: StatelessComponent = ({ children }) => (
|
||||
const HorizontalRule: FunctionComponent = ({ children }) => (
|
||||
<hr className={styles.root} />
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Flex } from "talk-ui/components";
|
||||
|
||||
import styles from "./Layout.css";
|
||||
|
||||
const Layout: StatelessComponent = ({ children }) => (
|
||||
const Layout: FunctionComponent = ({ children }) => (
|
||||
<Flex className={styles.root}>{children}</Flex>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./Main.css";
|
||||
|
||||
const Main: StatelessComponent = ({ children }) => (
|
||||
const Main: FunctionComponent = ({ children }) => (
|
||||
<div className={styles.root}>{children}</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link as FoundLink, LocationDescriptor } from "found";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./Link.css";
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
to: string | LocationDescriptor;
|
||||
}
|
||||
|
||||
const Link: StatelessComponent<Props> = props => (
|
||||
const Link: FunctionComponent<Props> = props => (
|
||||
<li className={props.className}>
|
||||
<FoundLink
|
||||
to={props.to}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./Navigation.css";
|
||||
|
||||
const Navigation: StatelessComponent = ({ children }) => (
|
||||
const Navigation: FunctionComponent = ({ children }) => (
|
||||
<nav className={styles.root}>
|
||||
<ul className={styles.ul}>{children}</ul>
|
||||
</nav>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
|
||||
import { parseStringBool } from "talk-framework/lib/form";
|
||||
@@ -15,7 +15,7 @@ interface Props {
|
||||
offLabel?: React.ReactNode;
|
||||
}
|
||||
|
||||
const OnOffField: StatelessComponent<Props> = ({
|
||||
const OnOffField: FunctionComponent<Props> = ({
|
||||
name,
|
||||
disabled,
|
||||
onLabel,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
|
||||
import { parseStringBool } from "talk-framework/lib/form";
|
||||
@@ -13,7 +13,7 @@ interface Props {
|
||||
invert?: boolean;
|
||||
}
|
||||
|
||||
const PermissionField: StatelessComponent<Props> = ({
|
||||
const PermissionField: FunctionComponent<Props> = ({
|
||||
name,
|
||||
disabled,
|
||||
invert = false,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./SideBar.css";
|
||||
|
||||
const SideBar: StatelessComponent = ({ children }) => (
|
||||
const SideBar: FunctionComponent = ({ children }) => (
|
||||
<div className={styles.root}>{children}</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Typography } from "talk-ui/components";
|
||||
|
||||
import styles from "./Subheader.css";
|
||||
|
||||
const Subheader: StatelessComponent = ({ children }) => (
|
||||
const Subheader: FunctionComponent = ({ children }) => (
|
||||
<Typography variant="heading3" className={styles.root}>
|
||||
{children}
|
||||
</Typography>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { ValidationMessage as UIValidationMessage } from "talk-ui/components";
|
||||
|
||||
@@ -9,10 +9,7 @@ interface Props extends PropTypesOf<typeof UIValidationMessage> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ValidationMessage: StatelessComponent<Props> = ({
|
||||
children,
|
||||
...rest
|
||||
}) => (
|
||||
const ValidationMessage: FunctionComponent<Props> = ({ children, ...rest }) => (
|
||||
<UIValidationMessage {...rest} className={styles.root}>
|
||||
{children}
|
||||
</UIValidationMessage>
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { HorizontalGutter } from "talk-ui/components";
|
||||
@@ -13,7 +13,7 @@ interface Props {
|
||||
onInitValues: (values: any) => void;
|
||||
}
|
||||
|
||||
const AdvancedConfig: StatelessComponent<Props> = ({
|
||||
const AdvancedConfig: FunctionComponent<Props> = ({
|
||||
disabled,
|
||||
settings,
|
||||
onInitValues,
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
|
||||
import { formatEmpty, parseEmptyAsNull } from "talk-framework/lib/form";
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const CustomCSSConfig: StatelessComponent<Props> = ({ disabled }) => (
|
||||
const CustomCSSConfig: FunctionComponent<Props> = ({ disabled }) => (
|
||||
<FormField>
|
||||
<HorizontalGutter size="full">
|
||||
<Localized id="configure-advanced-customCSS">
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
|
||||
import { formatStringList, parseStringList } from "talk-framework/lib/form";
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const PermittedDomainsConfig: StatelessComponent<Props> = ({ disabled }) => (
|
||||
const PermittedDomainsConfig: FunctionComponent<Props> = ({ disabled }) => (
|
||||
<FormField>
|
||||
<HorizontalGutter size="full">
|
||||
<Localized id="configure-advanced-permittedDomains">
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ class AdvancedConfigRouteContainer extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withRouteConfig({
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
query: graphql`
|
||||
query AdvancedConfigRouteContainerQuery {
|
||||
settings {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { HorizontalGutter } from "talk-ui/components";
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
onInitValues: (values: any) => void;
|
||||
}
|
||||
|
||||
const AuthConfig: StatelessComponent<Props> = ({
|
||||
const AuthConfig: FunctionComponent<Props> = ({
|
||||
disabled,
|
||||
auth,
|
||||
onInitValues,
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { HorizontalGutter } from "talk-ui/components";
|
||||
@@ -25,7 +25,7 @@ interface Props {
|
||||
onInitValues: (values: any) => void;
|
||||
}
|
||||
|
||||
const AuthIntegrationsConfig: StatelessComponent<Props> = ({
|
||||
const AuthIntegrationsConfig: FunctionComponent<Props> = ({
|
||||
disabled,
|
||||
auth,
|
||||
onInitValues,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import { identity } from "lodash";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
|
||||
import { Validator } from "talk-framework/lib/validation";
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const ClientSecretField: StatelessComponent<Props> = ({
|
||||
const ClientSecretField: FunctionComponent<Props> = ({
|
||||
name,
|
||||
disabled,
|
||||
validate,
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import { identity } from "lodash";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
|
||||
import { Validator } from "talk-framework/lib/validation";
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const ClientSecretField: StatelessComponent<Props> = ({
|
||||
const ClientSecretField: FunctionComponent<Props> = ({
|
||||
name,
|
||||
disabled,
|
||||
validate,
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
import { parseBool } from "talk-framework/lib/form";
|
||||
|
||||
@@ -15,7 +15,7 @@ interface Props {
|
||||
children: (disabledInside: boolean) => React.ReactNode;
|
||||
}
|
||||
|
||||
const ConfigBoxWithToggleField: StatelessComponent<Props> = ({
|
||||
const ConfigBoxWithToggleField: FunctionComponent<Props> = ({
|
||||
id,
|
||||
name,
|
||||
title,
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { InputDescription } from "talk-ui/components";
|
||||
import { PropTypesOf } from "talk-ui/types";
|
||||
@@ -10,7 +10,7 @@ interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ConfigDescription: StatelessComponent<Props> = ({
|
||||
const ConfigDescription: FunctionComponent<Props> = ({
|
||||
children,
|
||||
container,
|
||||
}) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { required, Validator } from "talk-framework/lib/validation";
|
||||
import { HorizontalGutter, TextLink, Typography } from "talk-ui/components";
|
||||
@@ -33,7 +33,7 @@ const validateWhenEnabled = (validator: Validator): Validator => (
|
||||
return "";
|
||||
};
|
||||
|
||||
const FacebookConfig: StatelessComponent<Props> = ({
|
||||
const FacebookConfig: FunctionComponent<Props> = ({
|
||||
disabled,
|
||||
callbackURL,
|
||||
}) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { required, Validator } from "talk-framework/lib/validation";
|
||||
import { HorizontalGutter, TextLink, Typography } from "talk-ui/components";
|
||||
@@ -35,7 +35,7 @@ const validateWhenEnabled = (validator: Validator): Validator => (
|
||||
return "";
|
||||
};
|
||||
|
||||
const GoogleConfig: StatelessComponent<Props> = ({ disabled, callbackURL }) => (
|
||||
const GoogleConfig: FunctionComponent<Props> = ({ disabled, callbackURL }) => (
|
||||
<ConfigBoxWithToggleField
|
||||
title={
|
||||
<Localized id="configure-auth-google-loginWith">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { HorizontalGutter } from "talk-ui/components";
|
||||
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const LocalAuthConfig: StatelessComponent<Props> = ({ disabled }) => (
|
||||
const LocalAuthConfig: FunctionComponent<Props> = ({ disabled }) => (
|
||||
<ConfigBoxWithToggleField
|
||||
title={
|
||||
<Localized id="configure-auth-local-loginWith">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import { identity } from "lodash";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
|
||||
import {
|
||||
@@ -42,7 +42,7 @@ const OIDCLink = () => (
|
||||
<TextLink target="_blank">{"https://openid.net/connect/"}</TextLink>
|
||||
);
|
||||
|
||||
const OIDCConfig: StatelessComponent<Props> = ({
|
||||
const OIDCConfig: FunctionComponent<Props> = ({
|
||||
disabled,
|
||||
callbackURL,
|
||||
onDiscover,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { CopyButton } from "talk-framework/components";
|
||||
import { Flex, FormField, InputLabel, TextField } from "talk-ui/components";
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
url: string;
|
||||
}
|
||||
|
||||
const RedirectField: StatelessComponent<Props> = ({ url, description }) => (
|
||||
const RedirectField: FunctionComponent<Props> = ({ url, description }) => (
|
||||
<FormField>
|
||||
<Localized id="configure-auth-redirectURI">
|
||||
<InputLabel>Redirect URI</InputLabel>
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
|
||||
import { CheckBox, FormField, InputLabel } from "talk-ui/components";
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const RegistrationField: StatelessComponent<Props> = ({ name, disabled }) => (
|
||||
const RegistrationField: FunctionComponent<Props> = ({ name, disabled }) => (
|
||||
<FormField>
|
||||
<Localized id="configure-auth-registration">
|
||||
<InputLabel>Registration</InputLabel>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user