[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:
Wyatt Johnson
2019-05-09 22:54:56 +02:00
committed by Kiwi
parent 945bd7f2b0
commit df57b4eb17
487 changed files with 6794 additions and 2431 deletions
+30
View File
@@ -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",
+4
View File
@@ -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"),
+10
View File
@@ -0,0 +1,10 @@
const path = require("path");
module.exports = {
extends: "../.babelrc.js",
plugins: [
[
"babel-plugin-relay",
{ artifactDirectory: path.resolve(__dirname, "./__generated__") },
],
],
};
+5
View File
@@ -0,0 +1,5 @@
:global {
body {
margin: 0;
}
}
+38
View File
@@ -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;
+1
View File
@@ -0,0 +1 @@
export { default as CheckResetTokenFetch } from "./CheckResetTokenFetch";
+13
View File
@@ -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>
+25
View File
@@ -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();
+1
View File
@@ -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!
}
+9
View File
@@ -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";
+13
View File
@@ -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>
`;
+14
View File
@@ -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();
});
+2 -2
View File
@@ -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">
+2 -2
View File
@@ -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"
+2 -2
View File
@@ -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)} />
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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>
@@ -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,
@@ -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">
@@ -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">
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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>
@@ -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>
@@ -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";
@@ -14,7 +14,7 @@ interface Props {
sso: PropTypesOf<typeof SSOKeyFieldContainer>["sso"];
}
const SSOConfig: StatelessComponent<Props> = ({ disabled, sso }) => (
const SSOConfig: FunctionComponent<Props> = ({ disabled, sso }) => (
<ConfigBoxWithToggleField
title={
<Localized id="configure-auth-sso-loginWith">
@@ -1,5 +1,5 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import React, { FunctionComponent } from "react";
import {
Button,
@@ -20,7 +20,7 @@ interface Props {
onRegenerate?: () => void;
}
const SSOKeyField: StatelessComponent<Props> = ({
const SSOKeyField: FunctionComponent<Props> = ({
generatedKey,
keyGeneratedAt,
disabled,
@@ -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";
@@ -11,7 +11,7 @@ interface Props {
disabled: boolean;
}
const TargetFilterField: StatelessComponent<Props> = ({
const TargetFilterField: FunctionComponent<Props> = ({
name,
label,
disabled,
@@ -33,7 +33,7 @@ class AuthRouteContainer extends React.Component<Props> {
}
}
const enhanced = withRouteConfig({
const enhanced = withRouteConfig<Props>({
query: graphql`
query AuthConfigRouteContainerQuery {
settings {
@@ -1,7 +1,8 @@
import { FormApi } from "final-form";
import PropTypes from "prop-types";
import React from "react";
import { ReactContext, withReactFinalForm } from "react-final-form";
import { graphql } from "react-relay";
import { InferableComponentEnhancer } from "recompose";
import { OIDCConfigContainer_auth as AuthData } from "talk-admin/__generated__/OIDCConfigContainer_auth.graphql";
import { OIDCConfigContainer_authReadOnly as AuthReadOnlyData } from "talk-admin/__generated__/OIDCConfigContainer_authReadOnly.graphql";
@@ -20,6 +21,7 @@ interface Props {
onInitValues: (values: AuthData) => void;
disabled?: boolean;
discoverOIDCConfiguration: FetchProp<typeof DiscoverOIDCConfigurationFetch>;
reactFinalForm: FormApi;
}
interface State {
@@ -27,16 +29,12 @@ interface State {
}
class OIDCConfigContainer extends React.Component<Props, State> {
public static contextTypes = {
reactFinalForm: PropTypes.object,
};
public state = {
awaitingResponse: false,
};
private handleDiscover = async () => {
const form = this.context.reactFinalForm as FormApi;
const form = this.props.reactFinalForm;
this.setState({ awaitingResponse: true });
try {
const config = await this.props.discoverOIDCConfiguration({
@@ -75,39 +73,45 @@ class OIDCConfigContainer extends React.Component<Props, State> {
}
}
const enhanced = withFetch(DiscoverOIDCConfigurationFetch)(
withFragmentContainer<Props>({
auth: graphql`
fragment OIDCConfigContainer_auth on Auth {
integrations {
oidc {
enabled
allowRegistration
targetFilter {
admin
stream
// (cvle) Fix `withReactFinalForm` typings (v4.1.0), forgive this, we'll
// probably only use hooks in the future instead anyway ;-)
const withForm = withReactFinalForm as InferableComponentEnhancer<ReactContext>;
const enhanced = withForm(
withFetch(DiscoverOIDCConfigurationFetch)(
withFragmentContainer<Props>({
auth: graphql`
fragment OIDCConfigContainer_auth on Auth {
integrations {
oidc {
enabled
allowRegistration
targetFilter {
admin
stream
}
name
clientID
clientSecret
authorizationURL
tokenURL
jwksURI
issuer
}
name
clientID
clientSecret
authorizationURL
tokenURL
jwksURI
issuer
}
}
}
`,
authReadOnly: graphql`
fragment OIDCConfigContainer_authReadOnly on Auth {
integrations {
oidc {
callbackURL
`,
authReadOnly: graphql`
fragment OIDCConfigContainer_authReadOnly on Auth {
integrations {
oidc {
callbackURL
}
}
}
}
`,
})(OIDCConfigContainer)
`,
})(OIDCConfigContainer)
)
);
export default enhanced;
@@ -1,5 +1,5 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent, Suspense } from "react";
import React, { FunctionComponent, Suspense } from "react";
import { Field } from "react-final-form";
import { MarkdownEditor } from "talk-framework/components/loadables";
@@ -16,7 +16,7 @@ interface Props {
disabled: boolean;
}
const ClosedStreamMessageConfig: StatelessComponent<Props> = ({ disabled }) => (
const ClosedStreamMessageConfig: FunctionComponent<Props> = ({ disabled }) => (
<HorizontalGutter size="oneAndAHalf">
<Localized id="configure-general-closedStreamMessage-title">
<Header
@@ -1,5 +1,5 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import React, { FunctionComponent } from "react";
import { DURATION_UNIT, DurationField } from "talk-framework/components";
import {
@@ -25,7 +25,7 @@ interface Props {
disabled: boolean;
}
const ClosingCommentStreamsConfig: StatelessComponent<Props> = ({
const ClosingCommentStreamsConfig: FunctionComponent<Props> = ({
disabled,
}) => (
<HorizontalGutter size="oneAndAHalf" container={<FieldSet />}>
@@ -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 { DURATION_UNIT, DurationField } from "talk-framework/components";
@@ -23,7 +23,7 @@ interface Props {
disabled: boolean;
}
const CommentEditingConfig: StatelessComponent<Props> = ({ disabled }) => (
const CommentEditingConfig: FunctionComponent<Props> = ({ disabled }) => (
<HorizontalGutter size="oneAndAHalf">
<Localized id="configure-general-commentEditing-title">
<Header>Comment Editing</Header>
@@ -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 {
@@ -38,7 +38,7 @@ interface Props {
disabled: boolean;
}
const CommentLengthConfig: StatelessComponent<Props> = ({ disabled }) => (
const CommentLengthConfig: FunctionComponent<Props> = ({ disabled }) => (
<HorizontalGutter size="oneAndAHalf" container={<FieldSet />}>
<Localized id="configure-general-commentLength-title">
<Header container="legend">Comment Length</Header>
@@ -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";
@@ -21,7 +21,7 @@ interface Props {
onInitValues: (values: any) => void;
}
const General: StatelessComponent<Props> = ({
const General: FunctionComponent<Props> = ({
disabled,
settings,
onInitValues,

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