mirror of
https://github.com/wassname/talk.git
synced 2026-07-15 11:26:58 +08:00
[next] Admin Authentication (#2155)
* feat: Support sign in via E-Mail, and social logins + permission check * feat: no permission info * test: unit test * test: fix remaining tests * test: add integration tests * feat: add admin account completion * test: auth completion feature tests * fix: translation * fix: comment marker design * fix: linting issues * chore: address pr review comments * chore: add comment
This commit is contained in:
Generated
+31
-12
@@ -11378,7 +11378,7 @@
|
||||
"integrity": "sha1-ETOUSrJHeINHOZVZaIPg05z4hc8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"intl-pluralrules": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b"
|
||||
"intl-pluralrules": "github:projectfluent/IntlPluralRules#module"
|
||||
}
|
||||
},
|
||||
"fluent-langneg": {
|
||||
@@ -11672,7 +11672,8 @@
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
|
||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@@ -11693,12 +11694,14 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -11713,17 +11716,20 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@@ -11840,7 +11846,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@@ -11852,6 +11859,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -11866,6 +11874,7 @@
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@@ -11873,12 +11882,14 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz",
|
||||
"integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.1",
|
||||
"yallist": "^3.0.0"
|
||||
@@ -11897,6 +11908,7 @@
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@@ -11977,7 +11989,8 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
|
||||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
|
||||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -11989,6 +12002,7 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -12074,7 +12088,8 @@
|
||||
"safe-buffer": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
|
||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@@ -12110,6 +12125,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -12129,6 +12145,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@@ -12172,12 +12189,14 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz",
|
||||
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k="
|
||||
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
.loginContainer {
|
||||
.container {
|
||||
width: 350px;
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid var(--palette-grey-lighter);
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import AuthBox from "./AuthBox";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof AuthBox> = {
|
||||
title: "title",
|
||||
children: "content",
|
||||
};
|
||||
const renderer = createRenderer();
|
||||
|
||||
renderer.render(<AuthBox {...props} />);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import {
|
||||
BrandIcon,
|
||||
BrandName,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
Typography,
|
||||
} from "talk-ui/components";
|
||||
|
||||
import styles from "./AuthBox.css";
|
||||
|
||||
interface Props {
|
||||
title: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const AuthBox: StatelessComponent<Props> = ({ title, children }) => {
|
||||
return (
|
||||
<div data-testid="authBox">
|
||||
<Flex justifyContent="center">
|
||||
<HorizontalGutter className={styles.container} size="double">
|
||||
<Flex justifyContent="center">
|
||||
<div className={styles.brandIcon}>
|
||||
<BrandIcon size="lg" />
|
||||
</div>
|
||||
</Flex>
|
||||
<div>
|
||||
<Typography align="center" variant="heading3">
|
||||
{title}
|
||||
</Typography>
|
||||
<BrandName size="lg" align="center" />
|
||||
</div>
|
||||
{children}
|
||||
</HorizontalGutter>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthBox;
|
||||
+11
-22
@@ -1,19 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="authBox"
|
||||
>
|
||||
<withPropsOnChange(Flex)
|
||||
justifyContent="center"
|
||||
>
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
className="Login-loginContainer"
|
||||
className="AuthBox-container"
|
||||
size="double"
|
||||
>
|
||||
<withPropsOnChange(Flex)
|
||||
justifyContent="center"
|
||||
>
|
||||
<div
|
||||
className="Login-brandIcon"
|
||||
className="AuthBox-brandIcon"
|
||||
>
|
||||
<withPropsOnChange(BrandIcon)
|
||||
size="lg"
|
||||
@@ -21,31 +23,18 @@ exports[`renders correctly 1`] = `
|
||||
</div>
|
||||
</withPropsOnChange(Flex)>
|
||||
<div>
|
||||
<Localized
|
||||
id="login-login-signInTo"
|
||||
<withPropsOnChange(Typography)
|
||||
align="center"
|
||||
variant="heading3"
|
||||
>
|
||||
<withPropsOnChange(Typography)
|
||||
align="center"
|
||||
variant="heading3"
|
||||
>
|
||||
Sign in to
|
||||
</withPropsOnChange(Typography)>
|
||||
</Localized>
|
||||
title
|
||||
</withPropsOnChange(Typography)>
|
||||
<withPropsOnChange(BrandName)
|
||||
align="center"
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
<Localized
|
||||
id="login-login-enterAccountDetailsBelow"
|
||||
>
|
||||
<withPropsOnChange(Typography)
|
||||
align="center"
|
||||
>
|
||||
Enter your account details below
|
||||
</withPropsOnChange(Typography)>
|
||||
</Localized>
|
||||
<withContext(createMutationContainer(SignInContainer)) />
|
||||
content
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
</withPropsOnChange(Flex)>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
export const REDIRECT_PATH_KEY = "adminRedirectPath";
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Match, Router, withRouter } from "found";
|
||||
import React from "react";
|
||||
|
||||
import { AuthCheckContainerQueryResponse } from "talk-admin/__generated__/AuthCheckContainerQuery.graphql";
|
||||
import {
|
||||
SetRedirectPathMutation,
|
||||
withSetRedirectPathMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import RestrictedContainer from "talk-admin/views/restricted/containers/RestrictedContainer";
|
||||
import { graphql } from "talk-framework/lib/relay";
|
||||
import { withRouteConfig } from "talk-framework/lib/router";
|
||||
|
||||
interface Props {
|
||||
match: Match;
|
||||
router: Router;
|
||||
setRedirectPath: SetRedirectPathMutation;
|
||||
data: AuthCheckContainerQueryResponse | null;
|
||||
}
|
||||
|
||||
class AuthCheckContainer extends React.Component<Props> {
|
||||
private wasLoggedIn = false;
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.redirectIfNotLoggedIn();
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
if (nextProps.data && nextProps.data.me) {
|
||||
this.wasLoggedIn = true;
|
||||
}
|
||||
this.redirectIfNotLoggedIn(nextProps, this.props);
|
||||
if (nextProps.data && !nextProps.data.me) {
|
||||
this.wasLoggedIn = false;
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRedirectTo(props: Props = this.props) {
|
||||
if (!props.data || props.data.me) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private hasAccess(props: Props = this.props) {
|
||||
const {
|
||||
me,
|
||||
settings: { auth },
|
||||
} = props.data!;
|
||||
if (me) {
|
||||
if (me.role === "COMMENTER") {
|
||||
return false;
|
||||
} else if (
|
||||
!me.email ||
|
||||
!me.username ||
|
||||
(!me.profiles.some(p => p.__typename === "LocalProfile") &&
|
||||
auth.integrations.local.enabled &&
|
||||
(auth.integrations.local.targetFilter.admin ||
|
||||
auth.integrations.local.targetFilter.stream))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async redirectIfNotLoggedIn(
|
||||
props: Props = this.props,
|
||||
prevProps: Props | null = null
|
||||
) {
|
||||
if (!this.shouldRedirectTo(props)) {
|
||||
return;
|
||||
}
|
||||
// If I was previously logged in then logged out, we don't need to set the redirect path.
|
||||
if (!this.wasLoggedIn) {
|
||||
const location = props.match.location;
|
||||
await props.setRedirectPath({
|
||||
path: location.pathname + location.search + location.hash,
|
||||
});
|
||||
}
|
||||
props.router.replace("/admin/login");
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (!this.props.data || this.shouldRedirectTo()) {
|
||||
return null;
|
||||
}
|
||||
if (this.hasAccess()) {
|
||||
return this.props.children;
|
||||
}
|
||||
return <RestrictedContainer me={this.props.data.me!} />;
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withRouteConfig({
|
||||
query: graphql`
|
||||
query AuthCheckContainerQuery {
|
||||
me {
|
||||
...RestrictedContainer_me
|
||||
username
|
||||
email
|
||||
profiles {
|
||||
__typename
|
||||
}
|
||||
role
|
||||
}
|
||||
settings {
|
||||
auth {
|
||||
integrations {
|
||||
local {
|
||||
enabled
|
||||
targetFilter {
|
||||
admin
|
||||
stream
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(withRouter(withSetRedirectPathMutation(AuthCheckContainer)));
|
||||
|
||||
export default enhanced;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Match, Router, withRouter } from "found";
|
||||
import React from "react";
|
||||
|
||||
import { RedirectAppContainerLocal as Local } from "talk-admin/__generated__/RedirectAppContainerLocal.graphql";
|
||||
import { graphql, withLocalStateContainer } from "talk-framework/lib/relay";
|
||||
|
||||
interface Props {
|
||||
local: Local;
|
||||
match: Match;
|
||||
router: Router;
|
||||
}
|
||||
|
||||
class RedirectAppContainer extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.redirectIfLoggedIn();
|
||||
}
|
||||
|
||||
public componentWillReceiveProps() {
|
||||
this.redirectIfLoggedIn();
|
||||
}
|
||||
|
||||
private redirectIfLoggedIn() {
|
||||
if (this.props.local.loggedIn) {
|
||||
this.props.router.replace(this.props.local.redirectPath || "/admin");
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withRouter(
|
||||
withLocalStateContainer(
|
||||
graphql`
|
||||
fragment RedirectAppContainerLocal on Local {
|
||||
loggedIn
|
||||
redirectPath
|
||||
}
|
||||
`
|
||||
)(RedirectAppContainer)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
@@ -1,18 +1,19 @@
|
||||
import { Match, Router, withRouter } from "found";
|
||||
import React from "react";
|
||||
|
||||
import { RedirectLoginContainerLocal as Local } from "talk-admin/__generated__/RedirectLoginContainerLocal.graphql";
|
||||
import { RedirectLoginContainerQueryResponse } from "talk-admin/__generated__/RedirectLoginContainerQuery.graphql";
|
||||
import {
|
||||
SetRedirectPathMutation,
|
||||
withSetRedirectPathMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import { graphql, withLocalStateContainer } from "talk-framework/lib/relay";
|
||||
import { graphql } from "talk-framework/lib/relay";
|
||||
import { withRouteConfig } from "talk-framework/lib/router";
|
||||
|
||||
interface Props {
|
||||
local: Local;
|
||||
match: Match;
|
||||
router: Router;
|
||||
setRedirectPath: SetRedirectPathMutation;
|
||||
data: RedirectLoginContainerQueryResponse | null;
|
||||
}
|
||||
|
||||
class RedirectLoginContainer extends React.Component<Props> {
|
||||
@@ -21,35 +22,81 @@ class RedirectLoginContainer extends React.Component<Props> {
|
||||
this.redirectIfNotLoggedIn();
|
||||
}
|
||||
|
||||
public componentWillReceiveProps() {
|
||||
this.redirectIfNotLoggedIn();
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
this.redirectIfNotLoggedIn(nextProps);
|
||||
}
|
||||
|
||||
private redirectIfNotLoggedIn() {
|
||||
if (!this.props.local.loggedIn) {
|
||||
const location = this.props.match.location;
|
||||
this.props.setRedirectPath({
|
||||
private shouldRedirectTo(props: Props = this.props): string | null {
|
||||
if (!props.data) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
me,
|
||||
settings: { auth },
|
||||
} = props.data!;
|
||||
if (me) {
|
||||
if (me.role === "COMMENTER") {
|
||||
return "/admin/login";
|
||||
} else if (
|
||||
!me.email ||
|
||||
!me.username ||
|
||||
(!me.profiles.some(p => p.__typename === "LocalProfile") &&
|
||||
auth.integrations.local.enabled &&
|
||||
(auth.integrations.local.targetFilter.admin ||
|
||||
auth.integrations.local.targetFilter.stream))
|
||||
) {
|
||||
return "/admin/login";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return "/admin/login";
|
||||
}
|
||||
|
||||
private redirectIfNotLoggedIn(props: Props = this.props) {
|
||||
const redirect = this.shouldRedirectTo(props);
|
||||
if (redirect) {
|
||||
const location = props.match.location;
|
||||
props.setRedirectPath({
|
||||
path: location.pathname + location.search + location.hash,
|
||||
});
|
||||
this.props.router.replace("/admin/login");
|
||||
props.router.replace(redirect);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.shouldRedirectTo()) {
|
||||
return null;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withRouter(
|
||||
withSetRedirectPathMutation(
|
||||
withLocalStateContainer(
|
||||
graphql`
|
||||
fragment RedirectLoginContainerLocal on Local {
|
||||
loggedIn
|
||||
const enhanced = withRouteConfig({
|
||||
query: graphql`
|
||||
query RedirectLoginContainerQuery {
|
||||
me {
|
||||
username
|
||||
email
|
||||
profiles {
|
||||
__typename
|
||||
}
|
||||
`
|
||||
)(RedirectLoginContainer)
|
||||
)
|
||||
);
|
||||
role
|
||||
}
|
||||
settings {
|
||||
auth {
|
||||
integrations {
|
||||
local {
|
||||
enabled
|
||||
targetFilter {
|
||||
admin
|
||||
stream
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(withRouter(withSetRedirectPathMutation(RedirectLoginContainer)));
|
||||
|
||||
export default enhanced;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { REDIRECT_PATH_KEY } from "talk-admin/constants";
|
||||
import { getParamsFromHashAndClearIt } from "talk-framework/helpers";
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { initLocalBaseState } from "talk-framework/lib/relay";
|
||||
import { initLocalBaseState, LOCAL_ID } from "talk-framework/lib/relay";
|
||||
|
||||
/**
|
||||
* Initializes the local state, before we start the App.
|
||||
@@ -10,5 +12,24 @@ export default async function initLocalState(
|
||||
environment: Environment,
|
||||
context: TalkContext
|
||||
) {
|
||||
await initLocalBaseState(environment, context);
|
||||
const { error = null, accessToken } = getParamsFromHashAndClearIt();
|
||||
let redirectPath: string | null = null;
|
||||
if (error || accessToken) {
|
||||
// Keep redirect path as we are in the middle of an auth flow.
|
||||
redirectPath =
|
||||
(await context.localStorage.getItem(REDIRECT_PATH_KEY)) || null;
|
||||
} else {
|
||||
// Remove redirect path from local storage as we start a new auth flow.
|
||||
await context.localStorage.setItem(REDIRECT_PATH_KEY, "");
|
||||
}
|
||||
|
||||
await initLocalBaseState(environment, context, accessToken);
|
||||
|
||||
commitLocalUpdate(environment, s => {
|
||||
const localRecord = s.get(LOCAL_ID)!;
|
||||
|
||||
localRecord.setValue(redirectPath, "redirectPath");
|
||||
localRecord.setValue("SIGN_IN", "authView");
|
||||
localRecord.setValue(error, "authError");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,13 @@ type Network {
|
||||
isOffline: Boolean!
|
||||
}
|
||||
|
||||
enum View {
|
||||
SIGN_IN
|
||||
CREATE_USERNAME
|
||||
CREATE_PASSWORD
|
||||
ADD_EMAIL_ADDRESS
|
||||
}
|
||||
|
||||
type Local {
|
||||
network: Network!
|
||||
authToken: String
|
||||
@@ -10,6 +17,8 @@ type Local {
|
||||
authJTI: String
|
||||
loggedIn: Boolean!
|
||||
redirectPath: String
|
||||
authView: View
|
||||
authError: String
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
|
||||
|
||||
export type ClearAuthErrorMutation = () => Promise<void>;
|
||||
|
||||
export async function commit(environment: Environment, input: undefined) {
|
||||
return commitLocalUpdate(environment, store => {
|
||||
const record = store.get(LOCAL_ID)!;
|
||||
record.setValue(null, "authError");
|
||||
});
|
||||
}
|
||||
|
||||
export const withClearAuthErrorMutation = createMutationContainer(
|
||||
"clearAuthError",
|
||||
commit
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import { commit as setAuthToken } from "talk-framework/mutations/SetAuthTokenMutation";
|
||||
|
||||
interface CompleteAccountInput {
|
||||
authToken: string;
|
||||
}
|
||||
export type CompleteAccountMutation = (
|
||||
input: CompleteAccountInput
|
||||
) => Promise<void>;
|
||||
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: CompleteAccountInput,
|
||||
context: TalkContext
|
||||
) {
|
||||
await setAuthToken(environment, { authToken: input.authToken }, context);
|
||||
}
|
||||
|
||||
export const withCompleteAccountMutation = createMutationContainer(
|
||||
"completeAccount",
|
||||
commit
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
|
||||
import { commit } from "./SetAuthViewMutation";
|
||||
|
||||
let environment: Environment;
|
||||
const source: RecordSource = new RecordSource();
|
||||
|
||||
beforeAll(() => {
|
||||
environment = createRelayEnvironment({
|
||||
source,
|
||||
});
|
||||
});
|
||||
|
||||
it("Sets view", () => {
|
||||
const view = "SIGN_IN";
|
||||
commit(environment, { view }, {} as any);
|
||||
expect(source.get(LOCAL_ID)!.authView).toEqual(view);
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
|
||||
|
||||
export interface SetAuthViewInput {
|
||||
// TODO: replace with generated typescript types.
|
||||
view: "SIGN_IN" | "ADD_EMAIL_ADDRESS" | "CREATE_USERNAME" | "CREATE_PASSWORD";
|
||||
}
|
||||
|
||||
export type SetAuthViewMutation = (input: SetAuthViewInput) => Promise<void>;
|
||||
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: SetAuthViewInput,
|
||||
{ pym }: TalkContext
|
||||
) {
|
||||
return commitLocalUpdate(environment, store => {
|
||||
const record = store.get(LOCAL_ID)!;
|
||||
record.setValue(input.view, "authView");
|
||||
});
|
||||
}
|
||||
|
||||
export const withSetAuthViewMutation = createMutationContainer(
|
||||
"setAuthView",
|
||||
commit
|
||||
);
|
||||
@@ -0,0 +1,46 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { Omit } from "talk-framework/types";
|
||||
|
||||
import { SetEmailMutation as MutationTypes } from "talk-admin/__generated__/SetEmailMutation.graphql";
|
||||
|
||||
export type SetEmailInput = Omit<
|
||||
MutationTypes["variables"]["input"],
|
||||
"clientMutationId"
|
||||
>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation SetEmailMutation($input: SetEmailInput!) {
|
||||
setEmail(input: $input) {
|
||||
user {
|
||||
email
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: SetEmailInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const withSetEmailMutation = createMutationContainer("setEmail", commit);
|
||||
|
||||
export type SetEmailMutation = (
|
||||
input: SetEmailInput
|
||||
) => Promise<MutationTypes["response"]["setEmail"]>;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { Omit } from "talk-framework/types";
|
||||
|
||||
import { SetPasswordMutation as MutationTypes } from "talk-admin/__generated__/SetPasswordMutation.graphql";
|
||||
|
||||
export type SetPasswordInput = Omit<
|
||||
MutationTypes["variables"]["input"],
|
||||
"clientMutationId"
|
||||
>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation SetPasswordMutation($input: SetPasswordInput!) {
|
||||
setPassword(input: $input) {
|
||||
user {
|
||||
profiles {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: SetPasswordInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const withSetPasswordMutation = createMutationContainer(
|
||||
"setPassword",
|
||||
commit
|
||||
);
|
||||
|
||||
export type SetPasswordMutation = (
|
||||
input: SetPasswordInput
|
||||
) => Promise<MutationTypes["response"]["setPassword"]>;
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
|
||||
import { REDIRECT_PATH_KEY } from "talk-admin/constants";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
|
||||
import {
|
||||
createInMemoryStorage,
|
||||
createPromisifiedStorage,
|
||||
} from "talk-framework/lib/storage";
|
||||
import { commit } from "./SetRedirectPathMutation";
|
||||
|
||||
let environment: Environment;
|
||||
@@ -14,7 +19,20 @@ beforeAll(() => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Sets redirectPath", () => {
|
||||
commit(environment, { path: "/path" });
|
||||
it("Sets redirectPath", async () => {
|
||||
const storage = createInMemoryStorage();
|
||||
await commit(environment, { path: "/path" }, {
|
||||
localStorage: createPromisifiedStorage(storage),
|
||||
} as any);
|
||||
expect(source.get(LOCAL_ID)!.redirectPath).toEqual("/path");
|
||||
expect(storage.getItem(REDIRECT_PATH_KEY)).toEqual("/path");
|
||||
});
|
||||
|
||||
it("Removes redirectPath", async () => {
|
||||
const storage = createInMemoryStorage();
|
||||
await commit(environment, { path: null }, {
|
||||
localStorage: createPromisifiedStorage(storage),
|
||||
} as any);
|
||||
expect(source.get(LOCAL_ID)!.redirectPath).toEqual(null);
|
||||
expect(storage.getItem(REDIRECT_PATH_KEY)).toEqual(null);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { REDIRECT_PATH_KEY } from "talk-admin/constants";
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer, LOCAL_ID } from "talk-framework/lib/relay";
|
||||
|
||||
export interface SetRedirectPathInput {
|
||||
@@ -12,8 +14,15 @@ export type SetRedirectPathMutation = (
|
||||
|
||||
export async function commit(
|
||||
environment: Environment,
|
||||
input: SetRedirectPathInput
|
||||
input: SetRedirectPathInput,
|
||||
{ localStorage }: TalkContext
|
||||
) {
|
||||
if (!input.path) {
|
||||
await localStorage.removeItem(REDIRECT_PATH_KEY);
|
||||
} else {
|
||||
await localStorage.setItem(REDIRECT_PATH_KEY, input.path);
|
||||
}
|
||||
|
||||
return commitLocalUpdate(environment, store => {
|
||||
const record = store.get(LOCAL_ID)!;
|
||||
record.setValue(input.path, "redirectPath");
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { Omit } from "talk-framework/types";
|
||||
|
||||
import { SetUsernameMutation as MutationTypes } from "talk-admin/__generated__/SetUsernameMutation.graphql";
|
||||
|
||||
export type SetUsernameInput = Omit<
|
||||
MutationTypes["variables"]["input"],
|
||||
"clientMutationId"
|
||||
>;
|
||||
|
||||
const mutation = graphql`
|
||||
mutation SetUsernameMutation($input: SetUsernameInput!) {
|
||||
setUsername(input: $input) {
|
||||
user {
|
||||
username
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: SetUsernameInput) {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const withSetUsernameMutation = createMutationContainer(
|
||||
"setUsername",
|
||||
commit
|
||||
);
|
||||
|
||||
export type SetUsernameMutation = (
|
||||
input: SetUsernameInput
|
||||
) => Promise<MutationTypes["response"]["setUsername"]>;
|
||||
@@ -2,7 +2,6 @@ import { Environment } from "relay-runtime";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { createMutationContainer } from "talk-framework/lib/relay";
|
||||
import { commit as setAuthToken } from "talk-framework/mutations/SetAuthTokenMutation";
|
||||
import { signIn, SignInInput } from "talk-framework/rest";
|
||||
|
||||
export type SignInMutation = (input: SignInInput) => Promise<void>;
|
||||
@@ -13,7 +12,13 @@ export async function commit(
|
||||
context: TalkContext
|
||||
) {
|
||||
const result = await signIn(context.rest, input);
|
||||
setAuthToken(environment, { authToken: result.token }, context);
|
||||
|
||||
// Put the token on the hash and clean the session.
|
||||
// It'll be picked up by initLocalState.
|
||||
location.hash = `accessToken=${result.token}`;
|
||||
await context.clearSession();
|
||||
// TODO: (cvle) A better way would be if `context.clearSession` would return the new session and
|
||||
// we set the accessToken directly in there.
|
||||
}
|
||||
|
||||
export const withSignInMutation = createMutationContainer("signIn", commit);
|
||||
|
||||
@@ -20,3 +20,24 @@ export {
|
||||
withRegenerateSSOKeyMutation,
|
||||
RegenerateSSOKeyMutation,
|
||||
} from "./RegenerateSSOKeyMutation";
|
||||
export {
|
||||
withSetAuthViewMutation,
|
||||
SetAuthViewMutation,
|
||||
} from "./SetAuthViewMutation";
|
||||
export {
|
||||
withClearAuthErrorMutation,
|
||||
ClearAuthErrorMutation,
|
||||
} from "./ClearAuthErrorMutation";
|
||||
export {
|
||||
withCompleteAccountMutation,
|
||||
CompleteAccountMutation,
|
||||
} from "./CompleteAccountMutation";
|
||||
export { withSetEmailMutation, SetEmailMutation } from "./SetEmailMutation";
|
||||
export {
|
||||
withSetUsernameMutation,
|
||||
SetUsernameMutation,
|
||||
} from "./SetUsernameMutation";
|
||||
export {
|
||||
withSetPasswordMutation,
|
||||
SetPasswordMutation,
|
||||
} from "./SetPasswordMutation";
|
||||
|
||||
@@ -2,13 +2,12 @@ import { makeRouteConfig, Redirect, Route } from "found";
|
||||
import React from "react";
|
||||
|
||||
import App from "./components/App";
|
||||
import RedirectAppContainer from "./containers/RedirectAppContainer";
|
||||
import RedirectLoginContainer from "./containers/RedirectLoginContainer";
|
||||
import AuthCheckContainer from "./containers/AuthCheckContainer";
|
||||
import Community from "./routes/community/components/Community";
|
||||
import ConfigureModeration from "./routes/configure/components/Moderation";
|
||||
import ConfigureContainer from "./routes/configure/containers/ConfigureContainer";
|
||||
import ConfigureAuthContainer from "./routes/configure/sections/auth/containers/AuthContainer";
|
||||
import Login from "./routes/login/components/Login";
|
||||
import ConfigureAuthRouteContainer from "./routes/configure/sections/auth/containers/AuthRouteContainer";
|
||||
import LoginContainer from "./routes/login/containers/LoginContainer";
|
||||
import ModerateContainer from "./routes/moderate/containers/ModerateContainer";
|
||||
import {
|
||||
PendingQueueContainer,
|
||||
@@ -21,7 +20,7 @@ import Stories from "./routes/stories/components/Stories";
|
||||
|
||||
export default makeRouteConfig(
|
||||
<Route path="admin">
|
||||
<Route Component={RedirectLoginContainer}>
|
||||
<Route {...AuthCheckContainer.routeConfig}>
|
||||
<Route Component={App}>
|
||||
<Redirect from="/" to="/admin/moderate" />
|
||||
<Route
|
||||
@@ -43,12 +42,10 @@ export default makeRouteConfig(
|
||||
<Route path="configure" Component={ConfigureContainer}>
|
||||
<Redirect from="/" to="/admin/configure/moderation" />
|
||||
<Route path="moderation" Component={ConfigureModeration} />
|
||||
<Route path="auth" {...ConfigureAuthContainer.routeConfig} />
|
||||
<Route path="auth" {...ConfigureAuthRouteContainer.routeConfig} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route Component={RedirectAppContainer}>
|
||||
<Route path="login" Component={Login} />
|
||||
</Route>
|
||||
<Route path="login" {...LoginContainer.routeConfig} />
|
||||
</Route>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const Auth: StatelessComponent<Props> = ({ disabled, auth, onInitValues }) => (
|
||||
<HorizontalGutter size="double">
|
||||
<HorizontalGutter size="double" data-testid="configure-authContainer">
|
||||
<DisplayNamesConfigContainer
|
||||
disabled={disabled}
|
||||
auth={auth}
|
||||
|
||||
@@ -5,11 +5,11 @@ import { get, merge } from "lodash";
|
||||
import React from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { AuthContainerQueryResponse } from "talk-admin/__generated__/AuthContainerQuery.graphql";
|
||||
import { AuthContainer_auth as AuthData } from "talk-admin/__generated__/AuthContainer_auth.graphql";
|
||||
import { TalkContext, withContext } from "talk-framework/lib/bootstrap";
|
||||
import { getMessage } from "talk-framework/lib/i18n";
|
||||
import { Delay, Spinner } from "talk-ui/components";
|
||||
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import {
|
||||
AddSubmitHook,
|
||||
RemoveSubmitHook,
|
||||
@@ -18,14 +18,15 @@ import {
|
||||
} from "../../../submitHook";
|
||||
import Auth from "../components/Auth";
|
||||
|
||||
interface Props extends AuthContainerQueryResponse {
|
||||
interface Props {
|
||||
localeBundles: TalkContext["localeBundles"];
|
||||
form: FormApi;
|
||||
submitting?: boolean;
|
||||
addSubmitHook: AddSubmitHook;
|
||||
auth: AuthData;
|
||||
}
|
||||
|
||||
export default class AuthContainer extends React.Component<Props> {
|
||||
class AuthContainer extends React.Component<Props> {
|
||||
public static routeConfig: RouteProps;
|
||||
private initialValues = {};
|
||||
private removeSubmitHook: RemoveSubmitHook;
|
||||
@@ -86,44 +87,32 @@ export default class AuthContainer extends React.Component<Props> {
|
||||
return (
|
||||
<Auth
|
||||
disabled={this.props.submitting}
|
||||
auth={this.props.settings.auth}
|
||||
auth={this.props.auth}
|
||||
onInitValues={this.handleOnInitValues}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withSubmitHookContext(addSubmitHook => ({ addSubmitHook }))(
|
||||
withContext(({ localeBundles }) => ({ localeBundles }))(AuthContainer)
|
||||
);
|
||||
|
||||
AuthContainer.routeConfig = {
|
||||
Component: enhanced,
|
||||
query: graphql`
|
||||
query AuthContainerQuery {
|
||||
settings {
|
||||
auth {
|
||||
...FacebookConfigContainer_auth
|
||||
...FacebookConfigContainer_authReadOnly
|
||||
...GoogleConfigContainer_auth
|
||||
...GoogleConfigContainer_authReadOnly
|
||||
...SSOConfigContainer_auth
|
||||
...SSOConfigContainer_authReadOnly
|
||||
...LocalAuthConfigContainer_auth
|
||||
...DisplayNamesConfigContainer_auth
|
||||
...OIDCConfigContainer_auth
|
||||
...OIDCConfigContainer_authReadOnly
|
||||
}
|
||||
}
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
auth: graphql`
|
||||
fragment AuthContainer_auth on Auth {
|
||||
...FacebookConfigContainer_auth
|
||||
...FacebookConfigContainer_authReadOnly
|
||||
...GoogleConfigContainer_auth
|
||||
...GoogleConfigContainer_authReadOnly
|
||||
...SSOConfigContainer_auth
|
||||
...SSOConfigContainer_authReadOnly
|
||||
...LocalAuthConfigContainer_auth
|
||||
...DisplayNamesConfigContainer_auth
|
||||
...OIDCConfigContainer_auth
|
||||
...OIDCConfigContainer_authReadOnly
|
||||
}
|
||||
`,
|
||||
cacheConfig: { force: true },
|
||||
render: ({ Component, props }) =>
|
||||
props && Component ? (
|
||||
<Component {...props} />
|
||||
) : (
|
||||
<Delay>
|
||||
<Spinner />
|
||||
</Delay>
|
||||
),
|
||||
};
|
||||
})(
|
||||
withSubmitHookContext(addSubmitHook => ({ addSubmitHook }))(
|
||||
withContext(({ localeBundles }) => ({ localeBundles }))(AuthContainer)
|
||||
)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { FormApi } from "final-form";
|
||||
import React from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { AuthRouteContainerQueryResponse } from "talk-admin/__generated__/AuthRouteContainerQuery.graphql";
|
||||
import { withRouteConfig } from "talk-framework/lib/router";
|
||||
import { Delay, Spinner } from "talk-ui/components";
|
||||
|
||||
import AuthContainer from ".//AuthContainer";
|
||||
|
||||
interface Props {
|
||||
data: AuthRouteContainerQueryResponse | null;
|
||||
form: FormApi;
|
||||
submitting?: boolean;
|
||||
}
|
||||
|
||||
class AuthRouteContainer extends React.Component<Props> {
|
||||
public render() {
|
||||
if (!this.props.data) {
|
||||
return (
|
||||
<Delay>
|
||||
<Spinner />
|
||||
</Delay>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<AuthContainer
|
||||
auth={this.props.data.settings.auth}
|
||||
form={this.props.form}
|
||||
submitting={this.props.submitting}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withRouteConfig({
|
||||
query: graphql`
|
||||
query AuthRouteContainerQuery {
|
||||
settings {
|
||||
auth {
|
||||
...AuthContainer_auth
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
cacheConfig: { force: true },
|
||||
})(AuthRouteContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,23 @@
|
||||
.container {
|
||||
width: 350px;
|
||||
background-color: var(--palette-common-white);
|
||||
border: 1px solid var(--palette-divider);
|
||||
margin-top: 70px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: var(--palette-text-primary);
|
||||
padding: calc(1.5 * var(--spacing-unit));
|
||||
color: var(--palette-text-light);
|
||||
}
|
||||
|
||||
.heading3 {
|
||||
margin-bottom: calc(0.25 * var(--spacing-unit));
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: calc(2.5 * var(--spacing-unit)) calc(2 * var(--spacing-unit))
|
||||
calc(2 * var(--spacing-unit)) calc(2 * var(--spacing-unit));
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import CompleteAccountBox from "./CompleteAccountBox";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof CompleteAccountBox> = {
|
||||
title: "title",
|
||||
children: "content",
|
||||
};
|
||||
const renderer = createRenderer();
|
||||
|
||||
renderer.render(<CompleteAccountBox {...props} />);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import { Flex, Typography } from "talk-ui/components";
|
||||
|
||||
import styles from "./CompleteAccountBox.css";
|
||||
|
||||
interface Props {
|
||||
title: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const CompleteAccountBox: StatelessComponent<Props> = ({ title, children }) => {
|
||||
return (
|
||||
<div data-testid="completeAccountBox">
|
||||
<Flex justifyContent="center">
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<Typography
|
||||
align="center"
|
||||
variant="heading3"
|
||||
color="textLight"
|
||||
className={styles.heading3}
|
||||
>
|
||||
Finish Setting Up Your Account
|
||||
</Typography>
|
||||
<Typography align="center" variant="heading1" color="textLight">
|
||||
{title}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={styles.main}>{children}</div>
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompleteAccountBox;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
import {
|
||||
composeValidators,
|
||||
required,
|
||||
validateEmail,
|
||||
} from "talk-framework/lib/validation";
|
||||
import {
|
||||
FormField,
|
||||
InputLabel,
|
||||
TextField,
|
||||
ValidationMessage,
|
||||
} from "talk-ui/components";
|
||||
|
||||
interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const EmailField: StatelessComponent<Props> = props => (
|
||||
<Field name="email" validate={composeValidators(required, validateEmail)}>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="login-emailAddressLabel">
|
||||
<InputLabel htmlFor={input.name}>Email Address</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="login-emailAddressTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<TextField
|
||||
id={input.name}
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Email Address"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={props.disabled}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
|
||||
export default EmailField;
|
||||
@@ -1,44 +1,44 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import {
|
||||
BrandIcon,
|
||||
BrandName,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
Typography,
|
||||
} from "talk-ui/components";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import SignInFormContainer from "../containers/SignInFormContainer";
|
||||
import AddEmailAddressContainer from "../views/addEmailAddress/containers/AddEmailAddressContainer";
|
||||
import CreatePasswordContainer from "../views/createPassword/containers/CreatePasswordContainer";
|
||||
import CreateUsernameContainer from "../views/createUsername/containers/CreateUsernameContainer";
|
||||
import SignInContainer from "../views/signIn/containers/SignInContainer";
|
||||
|
||||
import styles from "./Login.css";
|
||||
export type View =
|
||||
| "SIGN_UP"
|
||||
| "SIGN_IN"
|
||||
| "FORGOT_PASSWORD"
|
||||
| "RESET_PASSWORD"
|
||||
| "CREATE_USERNAME"
|
||||
| "CREATE_PASSWORD"
|
||||
| "ADD_EMAIL_ADDRESS"
|
||||
| "%future added value";
|
||||
|
||||
const Login: StatelessComponent = () => (
|
||||
<div>
|
||||
<Flex justifyContent="center">
|
||||
<HorizontalGutter className={styles.loginContainer} size="double">
|
||||
<Flex justifyContent="center">
|
||||
<div className={styles.brandIcon}>
|
||||
<BrandIcon size="lg" />
|
||||
</div>
|
||||
</Flex>
|
||||
<div>
|
||||
<Localized id="login-login-signInTo">
|
||||
<Typography align="center" variant="heading3">
|
||||
Sign in to
|
||||
</Typography>
|
||||
</Localized>
|
||||
<BrandName size="lg" align="center" />
|
||||
</div>
|
||||
<Localized id="login-login-enterAccountDetailsBelow">
|
||||
<Typography align="center">
|
||||
Enter your account details below
|
||||
</Typography>
|
||||
</Localized>
|
||||
<SignInFormContainer />
|
||||
</HorizontalGutter>
|
||||
</Flex>
|
||||
</div>
|
||||
export interface AppProps {
|
||||
view: View;
|
||||
auth: PropTypesOf<typeof SignInContainer>["auth"];
|
||||
}
|
||||
|
||||
const renderView = (view: AppProps["view"], auth: AppProps["auth"]) => {
|
||||
switch (view) {
|
||||
case "SIGN_IN":
|
||||
return <SignInContainer auth={auth} />;
|
||||
case "CREATE_USERNAME":
|
||||
return <CreateUsernameContainer />;
|
||||
case "CREATE_PASSWORD":
|
||||
return <CreatePasswordContainer />;
|
||||
case "ADD_EMAIL_ADDRESS":
|
||||
return <AddEmailAddressContainer />;
|
||||
default:
|
||||
throw new Error(`Unknown view ${view}`);
|
||||
}
|
||||
};
|
||||
|
||||
const App: StatelessComponent<AppProps> = ({ view, auth }) => (
|
||||
<div>{renderView(view, auth)}</div>
|
||||
);
|
||||
|
||||
export default Login;
|
||||
export default App;
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import { OnSubmit } from "talk-framework/lib/form";
|
||||
|
||||
import { PasswordField } from "talk-framework/components";
|
||||
import {
|
||||
composeValidators,
|
||||
required,
|
||||
validateEmail,
|
||||
} from "talk-framework/lib/validation";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
FormField,
|
||||
HorizontalGutter,
|
||||
InputLabel,
|
||||
TextField,
|
||||
ValidationMessage,
|
||||
} from "talk-ui/components";
|
||||
|
||||
interface FormProps {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onSubmit: OnSubmit<FormProps>;
|
||||
}
|
||||
|
||||
const SignIn: StatelessComponent<Props> = props => (
|
||||
<Form onSubmit={props.onSubmit}>
|
||||
{({ handleSubmit, submitting, submitError }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="double">
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
|
||||
<Field
|
||||
name="email"
|
||||
validate={composeValidators(required, validateEmail)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="login-signIn-emailAddressLabel">
|
||||
<InputLabel>Email Address</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="login-signIn-emailAddressTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<TextField
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Email Address"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field name="password" validate={composeValidators(required)}>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="login-signIn-passwordLabel">
|
||||
<InputLabel>Password</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="login-signIn-passwordTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<PasswordField
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Password"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
<Localized id="login-signIn-signIn">
|
||||
<Button
|
||||
variant="filled"
|
||||
color="primary"
|
||||
size="large"
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
|
||||
export default SignIn;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<div
|
||||
data-testid="completeAccountBox"
|
||||
>
|
||||
<withPropsOnChange(Flex)
|
||||
justifyContent="center"
|
||||
>
|
||||
<div
|
||||
className="CompleteAccountBox-container"
|
||||
>
|
||||
<div
|
||||
className="CompleteAccountBox-header"
|
||||
>
|
||||
<withPropsOnChange(Typography)
|
||||
align="center"
|
||||
className="CompleteAccountBox-heading3"
|
||||
color="textLight"
|
||||
variant="heading3"
|
||||
>
|
||||
Finish Setting Up Your Account
|
||||
</withPropsOnChange(Typography)>
|
||||
<withPropsOnChange(Typography)
|
||||
align="center"
|
||||
color="textLight"
|
||||
variant="heading1"
|
||||
>
|
||||
title
|
||||
</withPropsOnChange(Typography)>
|
||||
</div>
|
||||
<div
|
||||
className="CompleteAccountBox-main"
|
||||
>
|
||||
content
|
||||
</div>
|
||||
</div>
|
||||
</withPropsOnChange(Flex)>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,141 @@
|
||||
import { withRouter, WithRouter } from "found";
|
||||
import * as React from "react";
|
||||
import { Component } from "react";
|
||||
|
||||
import { AccountCompletionContainer_auth as AuthData } from "talk-admin/__generated__/AccountCompletionContainer_auth.graphql";
|
||||
import { AccountCompletionContainer_me as UserData } from "talk-admin/__generated__/AccountCompletionContainer_me.graphql";
|
||||
import { AccountCompletionContainerLocal as Local } from "talk-admin/__generated__/AccountCompletionContainerLocal.graphql";
|
||||
import {
|
||||
CompleteAccountMutation,
|
||||
SetAuthViewMutation,
|
||||
SetRedirectPathMutation,
|
||||
withCompleteAccountMutation,
|
||||
withSetAuthViewMutation,
|
||||
withSetRedirectPathMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import {
|
||||
graphql,
|
||||
withFragmentContainer,
|
||||
withLocalStateContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
type Props = {
|
||||
completeAccount: CompleteAccountMutation;
|
||||
setAuthView: SetAuthViewMutation;
|
||||
local: Local;
|
||||
auth: AuthData;
|
||||
me: UserData | null;
|
||||
setRedirectPath: SetRedirectPathMutation;
|
||||
} & WithRouter;
|
||||
|
||||
function handleAccountCompletion(props: Props) {
|
||||
const {
|
||||
local: { authView },
|
||||
me,
|
||||
auth,
|
||||
setAuthView,
|
||||
} = props;
|
||||
if (me) {
|
||||
if (!me.email) {
|
||||
if (authView !== "ADD_EMAIL_ADDRESS") {
|
||||
setAuthView({ view: "ADD_EMAIL_ADDRESS" });
|
||||
}
|
||||
} else if (!me.username) {
|
||||
if (authView !== "CREATE_USERNAME") {
|
||||
setAuthView({ view: "CREATE_USERNAME" });
|
||||
}
|
||||
} else if (
|
||||
!me.profiles.some(p => p.__typename === "LocalProfile") &&
|
||||
auth.integrations.local.enabled &&
|
||||
(auth.integrations.local.targetFilter.admin ||
|
||||
auth.integrations.local.targetFilter.stream)
|
||||
) {
|
||||
if (authView !== "CREATE_PASSWORD") {
|
||||
setAuthView({ view: "CREATE_PASSWORD" });
|
||||
}
|
||||
} else {
|
||||
props.completeAccount({ authToken: props.local.authToken! }).then(() => {
|
||||
props.router.replace(props.local.redirectPath || "/admin");
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
interface State {
|
||||
checkedCompletionStatus: boolean;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
class AccountCompletionContainer extends Component<Props, State> {
|
||||
public state = {
|
||||
checkedCompletionStatus: false,
|
||||
completed: false,
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
const completed = handleAccountCompletion(this.props);
|
||||
this.setState({ checkedCompletionStatus: true, completed });
|
||||
}
|
||||
public componentDidUpdate() {
|
||||
if (!this.state.completed) {
|
||||
const completed = handleAccountCompletion(this.props);
|
||||
if (completed) {
|
||||
this.setState({ completed });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { children } = this.props;
|
||||
// We skip first frame to check for completion status.
|
||||
if (!this.state.checkedCompletionStatus || this.state.completed) {
|
||||
return null;
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withLocalStateContainer(
|
||||
graphql`
|
||||
fragment AccountCompletionContainerLocal on Local {
|
||||
authToken
|
||||
authView
|
||||
redirectPath
|
||||
}
|
||||
`
|
||||
)(
|
||||
withFragmentContainer<Props>({
|
||||
auth: graphql`
|
||||
fragment AccountCompletionContainer_auth on Auth {
|
||||
integrations {
|
||||
local {
|
||||
enabled
|
||||
targetFilter {
|
||||
admin
|
||||
stream
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
me: graphql`
|
||||
fragment AccountCompletionContainer_me on User {
|
||||
username
|
||||
email
|
||||
profiles {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(
|
||||
withSetAuthViewMutation(
|
||||
withSetRedirectPathMutation(
|
||||
withCompleteAccountMutation(withRouter(AccountCompletionContainer))
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,63 @@
|
||||
import { RouteProps } from "found";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { LoginContainerLocal as LocalData } from "talk-admin/__generated__/LoginContainerLocal.graphql";
|
||||
import { LoginContainerQueryResponse } from "talk-admin/__generated__/LoginContainerQuery.graphql";
|
||||
import { graphql, withLocalStateContainer } from "talk-framework/lib/relay";
|
||||
import { withRouteConfig } from "talk-framework/lib/router";
|
||||
|
||||
import Login from "../components/Login";
|
||||
import AccountCompletionContainer from "./AccountCompletionContainer";
|
||||
|
||||
interface Props {
|
||||
local: LocalData;
|
||||
data: LoginContainerQueryResponse;
|
||||
error?: Error | null;
|
||||
}
|
||||
|
||||
class LoginContainer extends Component<Props> {
|
||||
public static routeConfig: RouteProps;
|
||||
|
||||
public render() {
|
||||
if (!this.props.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccountCompletionContainer
|
||||
auth={this.props.data.settings.auth}
|
||||
me={this.props.data.me}
|
||||
>
|
||||
<Login
|
||||
auth={this.props.data.settings.auth}
|
||||
view={this.props.local.authView!}
|
||||
/>
|
||||
</AccountCompletionContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withRouteConfig<LoginContainerQueryResponse>({
|
||||
query: graphql`
|
||||
query LoginContainerQuery {
|
||||
me {
|
||||
...AccountCompletionContainer_me
|
||||
}
|
||||
settings {
|
||||
auth {
|
||||
...AccountCompletionContainer_auth
|
||||
...SignInContainer_auth
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(
|
||||
withLocalStateContainer(
|
||||
graphql`
|
||||
fragment LoginContainerLocal on Local {
|
||||
authView
|
||||
}
|
||||
`
|
||||
)(LoginContainer)
|
||||
);
|
||||
export default enhanced;
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { Form } from "react-final-form";
|
||||
|
||||
import { OnSubmit } from "talk-framework/lib/form";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
HorizontalGutter,
|
||||
Typography,
|
||||
} from "talk-ui/components";
|
||||
|
||||
import CompleteAccountBox from "../../../components/CompleteAccountBox";
|
||||
import ConfirmEmailField from "./ConfirmEmailField";
|
||||
import EmailField from "./EmailField";
|
||||
|
||||
interface FormProps {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface AddEmailAddressForm {
|
||||
onSubmit: OnSubmit<FormProps>;
|
||||
}
|
||||
|
||||
const AddEmailAddress: StatelessComponent<AddEmailAddressForm> = props => {
|
||||
return (
|
||||
<CompleteAccountBox
|
||||
title={
|
||||
<Localized id="addEmailAddress-addEmailAddressHeader">
|
||||
<span>Add Email Address</span>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<Form onSubmit={props.onSubmit}>
|
||||
{({ handleSubmit, submitting, submitError }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="oneAndAHalf">
|
||||
<Localized id="addEmailAddress-whatItIs">
|
||||
<Typography variant="bodyCopy">
|
||||
For your added security, we require users to add an email
|
||||
address to their accounts. Your email address will be used to:
|
||||
</Typography>
|
||||
</Localized>
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
<EmailField disabled={submitting} />
|
||||
<ConfirmEmailField disabled={submitting} />
|
||||
<Localized id="addEmailAddress-addEmailAddressButton">
|
||||
<Button
|
||||
variant="filled"
|
||||
color="primary"
|
||||
size="large"
|
||||
type="submit"
|
||||
fullWidth
|
||||
disabled={submitting}
|
||||
>
|
||||
Add Email Address
|
||||
</Button>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</CompleteAccountBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddEmailAddress;
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
import {
|
||||
composeValidators,
|
||||
required,
|
||||
validateEqualEmails,
|
||||
} from "talk-framework/lib/validation";
|
||||
import {
|
||||
FormField,
|
||||
InputLabel,
|
||||
TextField,
|
||||
ValidationMessage,
|
||||
} from "talk-ui/components";
|
||||
|
||||
interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const ConfirmEmailField: StatelessComponent<Props> = props => (
|
||||
<Field
|
||||
name="confirmEmail"
|
||||
validate={composeValidators(required, validateEqualEmails)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="addEmailAddress-confirmEmailAddressLabel">
|
||||
<InputLabel htmlFor={input.name}>Confirm Email Address</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="addEmailAddress-confirmEmailAddressTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<TextField
|
||||
id={input.name}
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Confirm Email Address"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={props.disabled}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
|
||||
export default ConfirmEmailField;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
import {
|
||||
composeValidators,
|
||||
required,
|
||||
validateEmail,
|
||||
} from "talk-framework/lib/validation";
|
||||
import {
|
||||
FormField,
|
||||
InputLabel,
|
||||
TextField,
|
||||
ValidationMessage,
|
||||
} from "talk-ui/components";
|
||||
|
||||
interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const EmailField: StatelessComponent<Props> = props => (
|
||||
<Field name="email" validate={composeValidators(required, validateEmail)}>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="addEmailAddress-emailAddressLabel">
|
||||
<InputLabel htmlFor={input.name}>Email Address</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="addEmailAddress-emailAddressTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<TextField
|
||||
id={input.name}
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Email Address"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={props.disabled}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
|
||||
export default EmailField;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
.root {
|
||||
list-style-type: none;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.leftCol {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { Icon } from "talk-ui/components";
|
||||
|
||||
import styles from "./ListItem.css";
|
||||
|
||||
interface Props {
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ListItem: StatelessComponent<Props> = props => (
|
||||
<li className={styles.root}>
|
||||
<div className={styles.leftCol}>{props.icon}</div>
|
||||
<div>{props.children}</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
ListItem.defaultProps = {
|
||||
icon: <Icon>keyboard_arrow_right</Icon>,
|
||||
};
|
||||
|
||||
export default ListItem;
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
.root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
& > * {
|
||||
margin: 0 0 calc(1 * var(--spacing-unit)) 0 !important;
|
||||
}
|
||||
& > *:last-child {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
|
||||
import ListItem from "./ListItem";
|
||||
import UnorderedList from "./UnorderedList";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const renderer = createRenderer();
|
||||
renderer.render(
|
||||
<UnorderedList>
|
||||
<ListItem icon={<span>-</span>}>1</ListItem>
|
||||
<ListItem>2</ListItem>
|
||||
</UnorderedList>
|
||||
);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import styles from "./UnorderedList.css";
|
||||
|
||||
const UnorderedList: StatelessComponent = props => (
|
||||
<ul className={styles.root}>{props.children}</ul>
|
||||
);
|
||||
|
||||
export default UnorderedList;
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<ul
|
||||
className="UnorderedList-root"
|
||||
>
|
||||
<ListItem
|
||||
icon={
|
||||
<span>
|
||||
-
|
||||
</span>
|
||||
}
|
||||
>
|
||||
1
|
||||
</ListItem>
|
||||
<ListItem
|
||||
icon={
|
||||
<withPropsOnChange(Icon)>
|
||||
keyboard_arrow_right
|
||||
</withPropsOnChange(Icon)>
|
||||
}
|
||||
>
|
||||
2
|
||||
</ListItem>
|
||||
</ul>
|
||||
`;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export { default as UnorderedList } from "./UnorderedList";
|
||||
export { default as ListItem } from "./ListItem";
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import {
|
||||
SetEmailMutation,
|
||||
withSetEmailMutation,
|
||||
} from "talk-admin/mutations/SetEmailMutation";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import AddEmailAddress from "../components/AddEmailAddress";
|
||||
|
||||
interface Props {
|
||||
setEmail: SetEmailMutation;
|
||||
}
|
||||
|
||||
class AddEmailAddressContainer extends Component<Props> {
|
||||
private handleSubmit: PropTypesOf<
|
||||
typeof AddEmailAddress
|
||||
>["onSubmit"] = async (input, form) => {
|
||||
try {
|
||||
await this.props.setEmail({ email: input.email });
|
||||
return form.reset();
|
||||
} catch (error) {
|
||||
return { [FORM_ERROR]: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
// tslint:disable-next-line:no-empty
|
||||
return <AddEmailAddress onSubmit={this.handleSubmit} />;
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withSetEmailMutation(AddEmailAddressContainer);
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { Form } from "react-final-form";
|
||||
|
||||
import { OnSubmit } from "talk-framework/lib/form";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
HorizontalGutter,
|
||||
Typography,
|
||||
} from "talk-ui/components";
|
||||
|
||||
import CompleteAccountBox from "../../../components/CompleteAccountBox";
|
||||
import SetPasswordField from "./SetPasswordField";
|
||||
|
||||
interface FormProps {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface CreatePasswordForm {
|
||||
onSubmit: OnSubmit<FormProps>;
|
||||
}
|
||||
|
||||
const CreatePassword: StatelessComponent<CreatePasswordForm> = props => {
|
||||
return (
|
||||
<CompleteAccountBox
|
||||
title={
|
||||
<Localized id="createPassword-createPasswordHeader">
|
||||
<span>Create Password</span>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<Form onSubmit={props.onSubmit}>
|
||||
{({ handleSubmit, submitting, submitError }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="oneAndAHalf">
|
||||
<Localized id="createPassword-whatItIs">
|
||||
<Typography variant="bodyCopy">
|
||||
To protect against unauthorized changes to your account, we
|
||||
require users to create a password.
|
||||
</Typography>
|
||||
</Localized>
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
<SetPasswordField disabled={submitting} />
|
||||
<Localized id="createPassword-createPasswordButton">
|
||||
<Button
|
||||
variant="filled"
|
||||
color="primary"
|
||||
size="large"
|
||||
type="submit"
|
||||
fullWidth
|
||||
disabled={submitting}
|
||||
>
|
||||
Create Password
|
||||
</Button>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</CompleteAccountBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePassword;
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
import { PasswordField } from "talk-framework/components";
|
||||
import {
|
||||
composeValidators,
|
||||
required,
|
||||
validatePassword,
|
||||
} from "talk-framework/lib/validation";
|
||||
import {
|
||||
FormField,
|
||||
InputDescription,
|
||||
InputLabel,
|
||||
ValidationMessage,
|
||||
} from "talk-ui/components";
|
||||
|
||||
interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const SetPasswordField: StatelessComponent<Props> = props => (
|
||||
<Field
|
||||
name="password"
|
||||
validate={composeValidators(required, validatePassword)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="createPassword-passwordLabel">
|
||||
<InputLabel htmlFor={input.name}>Password</InputLabel>
|
||||
</Localized>
|
||||
<Localized id="createPassword-passwordDescription" $minLength={8}>
|
||||
<InputDescription>
|
||||
{"Must be at least {$minLength} characters"}
|
||||
</InputDescription>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="createPassword-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={props.disabled}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
|
||||
export default SetPasswordField;
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import {
|
||||
SetPasswordMutation,
|
||||
withSetPasswordMutation,
|
||||
} from "talk-admin/mutations/SetPasswordMutation";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import CreatePassword from "../components/CreatePassword";
|
||||
|
||||
interface Props {
|
||||
setPassword: SetPasswordMutation;
|
||||
}
|
||||
|
||||
class CreatePasswordContainer extends Component<Props> {
|
||||
private handleSubmit: PropTypesOf<typeof CreatePassword>["onSubmit"] = async (
|
||||
input,
|
||||
form
|
||||
) => {
|
||||
try {
|
||||
await this.props.setPassword({ password: input.password });
|
||||
return form.reset();
|
||||
} catch (error) {
|
||||
return { [FORM_ERROR]: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
// tslint:disable-next-line:no-empty
|
||||
return <CreatePassword onSubmit={this.handleSubmit} />;
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withSetPasswordMutation(CreatePasswordContainer);
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { Form } from "react-final-form";
|
||||
|
||||
import { OnSubmit } from "talk-framework/lib/form";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
HorizontalGutter,
|
||||
Typography,
|
||||
} from "talk-ui/components";
|
||||
|
||||
import CompleteAccountBox from "../../../components/CompleteAccountBox";
|
||||
import UsernameField from "./UsernameField";
|
||||
|
||||
interface FormProps {
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface CreateUsernameForm {
|
||||
onSubmit: OnSubmit<FormProps>;
|
||||
}
|
||||
|
||||
const CreateUsername: StatelessComponent<CreateUsernameForm> = props => {
|
||||
return (
|
||||
<CompleteAccountBox
|
||||
title={
|
||||
<Localized id="createUsername-createUsernameHeader">
|
||||
<span>Create Username</span>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<Form onSubmit={props.onSubmit}>
|
||||
{({ handleSubmit, submitting, submitError }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="oneAndAHalf">
|
||||
<Localized id="createUsername-whatItIs">
|
||||
<Typography variant="bodyCopy">
|
||||
Your username is a unique identifier that will appear on all
|
||||
of your comments.
|
||||
</Typography>
|
||||
</Localized>
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
<UsernameField disabled={submitting} />
|
||||
<Localized id="createUsername-createUsernameButton">
|
||||
<Button
|
||||
variant="filled"
|
||||
color="primary"
|
||||
size="large"
|
||||
type="submit"
|
||||
fullWidth
|
||||
disabled={submitting}
|
||||
>
|
||||
Create Username
|
||||
</Button>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</CompleteAccountBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUsername;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import { Field } from "react-final-form";
|
||||
import {
|
||||
composeValidators,
|
||||
required,
|
||||
validateUsername,
|
||||
} from "talk-framework/lib/validation";
|
||||
import {
|
||||
FormField,
|
||||
InputDescription,
|
||||
InputLabel,
|
||||
TextField,
|
||||
ValidationMessage,
|
||||
} from "talk-ui/components";
|
||||
|
||||
interface Props {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const CreateUsernameField: StatelessComponent<Props> = props => (
|
||||
<Field
|
||||
name="username"
|
||||
validate={composeValidators(required, validateUsername)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="createUsername-usernameLabel">
|
||||
<InputLabel htmlFor={input.name}>Username</InputLabel>
|
||||
</Localized>
|
||||
<Localized id="createUsername-usernameDescription">
|
||||
<InputDescription>You may use “_” and “.”</InputDescription>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="createUsername-usernameTextField"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<TextField
|
||||
id={input.name}
|
||||
name={input.name}
|
||||
onChange={input.onChange}
|
||||
value={input.value}
|
||||
placeholder="Username"
|
||||
color={
|
||||
meta.touched && (meta.error || meta.submitError)
|
||||
? "error"
|
||||
: "regular"
|
||||
}
|
||||
disabled={props.disabled}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
|
||||
export default CreateUsernameField;
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import {
|
||||
SetUsernameMutation,
|
||||
withSetUsernameMutation,
|
||||
} from "talk-admin/mutations/SetUsernameMutation";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import CreateUsername from "../components/CreateUsername";
|
||||
|
||||
interface Props {
|
||||
setUsername: SetUsernameMutation;
|
||||
}
|
||||
|
||||
class CreateUsernameContainer extends Component<Props> {
|
||||
private handleSubmit: PropTypesOf<typeof CreateUsername>["onSubmit"] = async (
|
||||
input,
|
||||
form
|
||||
) => {
|
||||
try {
|
||||
await this.props.setUsername({ username: input.username });
|
||||
return form.reset();
|
||||
} catch (error) {
|
||||
return { [FORM_ERROR]: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
// tslint:disable-next-line:no-empty
|
||||
return <CreateUsername onSubmit={this.handleSubmit} />;
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withSetUsernameMutation(CreateUsernameContainer);
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,16 @@
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
.hr {
|
||||
position: absolute;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--palette-divider);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.text {
|
||||
composes: heading3 from "talk-ui/shared/typography.css";
|
||||
position: relative;
|
||||
background-color: #f5f5f5;
|
||||
padding: 0 var(--spacing-unit);
|
||||
}
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
|
||||
import OrSeparator from "./OrSeparator";
|
||||
import HorizontalSeparator from "./HorizontalSeparator";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const renderer = createRenderer();
|
||||
renderer.render(<OrSeparator />);
|
||||
renderer.render(<HorizontalSeparator>Or</HorizontalSeparator>);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { Flex } from "talk-ui/components";
|
||||
|
||||
import styles from "./HorizontalSeparator.css";
|
||||
|
||||
interface Props {
|
||||
children: string;
|
||||
}
|
||||
|
||||
const HorizontalSeparator: StatelessComponent<Props> = props => (
|
||||
<Flex className={styles.root} alignItems="center" justifyContent="center">
|
||||
<hr className={styles.hr} />
|
||||
<div className={styles.text}>{props.children}</div>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
export default HorizontalSeparator;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import HorizontalSeparator from "./HorizontalSeparator";
|
||||
|
||||
const OrSeparator: StatelessComponent = () => (
|
||||
<Localized id="login-signIn-orSeparator">
|
||||
<HorizontalSeparator>Or</HorizontalSeparator>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export default OrSeparator;
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
|
||||
import { removeFragmentRefs } from "talk-framework/testHelpers";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import SignIn from "./SignIn";
|
||||
|
||||
const SignInN = removeFragmentRefs(SignIn);
|
||||
|
||||
it("renders correctly", () => {
|
||||
const renderer = createRenderer();
|
||||
const props: PropTypesOf<typeof SignInN> = {
|
||||
error: null,
|
||||
auth: {},
|
||||
};
|
||||
renderer.render(<SignInN {...props} />);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import AuthBox from "talk-admin/components/AuthBox";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { CallOut, HorizontalGutter } from "talk-ui/components";
|
||||
|
||||
import SignInWithEmailContainer from "../containers/SignInWithEmailContainer";
|
||||
import SignInWithFacebookContainer from "../containers/SignInWithFacebookContainer";
|
||||
import SignInWithGoogleContainer from "../containers/SignInWithGoogleContainer";
|
||||
import SignInWithOIDCContainer from "../containers/SignInWithOIDCContainer";
|
||||
import OrSeparator from "./OrSeparator";
|
||||
|
||||
interface Props {
|
||||
error: string | null;
|
||||
emailEnabled?: boolean;
|
||||
facebookEnabled?: boolean;
|
||||
googleEnabled?: boolean;
|
||||
oidcEnabled?: boolean;
|
||||
auth: PropTypesOf<typeof SignInWithOIDCContainer>["auth"] &
|
||||
PropTypesOf<typeof SignInWithFacebookContainer>["auth"] &
|
||||
PropTypesOf<typeof SignInWithGoogleContainer>["auth"];
|
||||
}
|
||||
|
||||
const SignIn: StatelessComponent<Props> = ({
|
||||
emailEnabled,
|
||||
facebookEnabled,
|
||||
googleEnabled,
|
||||
oidcEnabled,
|
||||
auth,
|
||||
error,
|
||||
}) => {
|
||||
const oneClickIntegrationEnabled =
|
||||
facebookEnabled || googleEnabled || oidcEnabled;
|
||||
return (
|
||||
<AuthBox
|
||||
title={
|
||||
<Localized id="login-signInTo">
|
||||
<span>Sign in to</span>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<HorizontalGutter size="oneAndAHalf">
|
||||
{error && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{error}
|
||||
</CallOut>
|
||||
)}
|
||||
{emailEnabled && <SignInWithEmailContainer />}
|
||||
{emailEnabled && oneClickIntegrationEnabled && <OrSeparator />}
|
||||
<HorizontalGutter>
|
||||
{facebookEnabled && <SignInWithFacebookContainer auth={auth} />}
|
||||
{googleEnabled && <SignInWithGoogleContainer auth={auth} />}
|
||||
{oidcEnabled && <SignInWithOIDCContainer auth={auth} />}
|
||||
</HorizontalGutter>
|
||||
</HorizontalGutter>
|
||||
</AuthBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
import { OnSubmit } from "talk-framework/lib/form";
|
||||
|
||||
import { PasswordField } from "talk-framework/components";
|
||||
import { composeValidators, required } from "talk-framework/lib/validation";
|
||||
import {
|
||||
Button,
|
||||
ButtonIcon,
|
||||
CallOut,
|
||||
FormField,
|
||||
HorizontalGutter,
|
||||
InputLabel,
|
||||
ValidationMessage,
|
||||
} from "talk-ui/components";
|
||||
|
||||
import EmailField from "../../../components/EmailField";
|
||||
|
||||
interface FormProps {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface SignInWithEmailForm {
|
||||
onSubmit: OnSubmit<FormProps>;
|
||||
}
|
||||
|
||||
const SignInWithEmail: StatelessComponent<SignInWithEmailForm> = props => {
|
||||
return (
|
||||
<Form onSubmit={props.onSubmit}>
|
||||
{({ handleSubmit, submitting, submitError }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="full">
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
|
||||
<EmailField disabled={submitting} />
|
||||
<Field name="password" validate={composeValidators(required)}>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="login-signIn-passwordLabel">
|
||||
<InputLabel htmlFor={input.name}>Password</InputLabel>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="login-signIn-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={submitting}
|
||||
fullWidth
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<ValidationMessage fullWidth>
|
||||
{meta.error || meta.submitError}
|
||||
</ValidationMessage>
|
||||
)}
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="brand"
|
||||
size="large"
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
fullWidth
|
||||
>
|
||||
<ButtonIcon size="md">email</ButtonIcon>
|
||||
<Localized id="login-signIn-signInWithEmail">
|
||||
<span>Sign in with Email</span>
|
||||
</Localized>
|
||||
</Button>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignInWithEmail;
|
||||
+6
-10
@@ -3,20 +3,16 @@
|
||||
exports[`renders correctly 1`] = `
|
||||
<withPropsOnChange(Flex)
|
||||
alignItems="center"
|
||||
className="OrSeparator-root"
|
||||
className="HorizontalSeparator-root"
|
||||
justifyContent="center"
|
||||
>
|
||||
<hr
|
||||
className="OrSeparator-hr"
|
||||
className="HorizontalSeparator-hr"
|
||||
/>
|
||||
<Localized
|
||||
id="general-orSeparator"
|
||||
<div
|
||||
className="HorizontalSeparator-text"
|
||||
>
|
||||
<div
|
||||
className="OrSeparator-text"
|
||||
>
|
||||
Or
|
||||
</div>
|
||||
</Localized>
|
||||
Or
|
||||
</div>
|
||||
</withPropsOnChange(Flex)>
|
||||
`;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<AuthBox
|
||||
title={
|
||||
<Localized
|
||||
id="login-signInTo"
|
||||
>
|
||||
<span>
|
||||
Sign in to
|
||||
</span>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
size="oneAndAHalf"
|
||||
>
|
||||
<withPropsOnChange(HorizontalGutter) />
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
</AuthBox>
|
||||
`;
|
||||
@@ -0,0 +1,106 @@
|
||||
import { RouteProps } from "found";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { SignInContainer_auth as AuthData } from "talk-admin/__generated__/SignInContainer_auth.graphql";
|
||||
import { SignInContainerLocal as LocalData } from "talk-admin/__generated__/SignInContainerLocal.graphql";
|
||||
import {
|
||||
ClearAuthErrorMutation,
|
||||
SignInMutation,
|
||||
withClearAuthErrorMutation,
|
||||
withSignInMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import {
|
||||
graphql,
|
||||
withFragmentContainer,
|
||||
withLocalStateContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
|
||||
import SignIn from "../components/SignIn";
|
||||
|
||||
interface Props {
|
||||
local: LocalData;
|
||||
auth: AuthData;
|
||||
error?: Error | null;
|
||||
signIn: SignInMutation;
|
||||
clearAuthError: ClearAuthErrorMutation;
|
||||
}
|
||||
|
||||
class SignInContainer extends Component<Props> {
|
||||
public static routeConfig: RouteProps;
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.props.clearAuthError();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const integrations = this.props.auth.integrations;
|
||||
return (
|
||||
<SignIn
|
||||
error={this.props.local.authError}
|
||||
auth={this.props.auth}
|
||||
emailEnabled={
|
||||
integrations.local.enabled && integrations.local.targetFilter.admin
|
||||
}
|
||||
facebookEnabled={
|
||||
integrations.facebook.enabled &&
|
||||
integrations.facebook.targetFilter.admin
|
||||
}
|
||||
googleEnabled={
|
||||
integrations.google.enabled && integrations.google.targetFilter.admin
|
||||
}
|
||||
oidcEnabled={
|
||||
integrations.oidc.enabled && integrations.oidc.targetFilter.admin
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
auth: graphql`
|
||||
fragment SignInContainer_auth on Auth {
|
||||
...SignInWithOIDCContainer_auth
|
||||
...SignInWithGoogleContainer_auth
|
||||
...SignInWithFacebookContainer_auth
|
||||
integrations {
|
||||
local {
|
||||
enabled
|
||||
targetFilter {
|
||||
admin
|
||||
}
|
||||
}
|
||||
facebook {
|
||||
enabled
|
||||
targetFilter {
|
||||
admin
|
||||
}
|
||||
}
|
||||
google {
|
||||
enabled
|
||||
targetFilter {
|
||||
admin
|
||||
}
|
||||
}
|
||||
oidc {
|
||||
enabled
|
||||
targetFilter {
|
||||
admin
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(
|
||||
withClearAuthErrorMutation(
|
||||
withSignInMutation(
|
||||
withLocalStateContainer(
|
||||
graphql`
|
||||
fragment SignInContainerLocal on Local {
|
||||
authError
|
||||
}
|
||||
`
|
||||
)(SignInContainer)
|
||||
)
|
||||
)
|
||||
);
|
||||
export default enhanced;
|
||||
+8
-9
@@ -1,28 +1,27 @@
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import React, { Component } from "react";
|
||||
import { SignInMutation, withSignInMutation } from "talk-admin/mutations";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import SignInForm from "../components/SignInForm";
|
||||
import { SignInMutation, withSignInMutation } from "talk-admin/mutations";
|
||||
|
||||
import SignInWithEmail, {
|
||||
SignInWithEmailForm,
|
||||
} from "../components/SignInWithEmail";
|
||||
|
||||
interface SignInContainerProps {
|
||||
signIn: SignInMutation;
|
||||
}
|
||||
|
||||
class SignInContainer extends Component<SignInContainerProps> {
|
||||
private onSubmit: PropTypesOf<typeof SignInForm>["onSubmit"] = async (
|
||||
input,
|
||||
form
|
||||
) => {
|
||||
private onSubmit: SignInWithEmailForm["onSubmit"] = async (input, form) => {
|
||||
try {
|
||||
await this.props.signIn(input);
|
||||
await this.props.signIn({ email: input.email, password: input.password });
|
||||
return form.reset();
|
||||
} catch (error) {
|
||||
return { [FORM_ERROR]: error.message };
|
||||
}
|
||||
};
|
||||
public render() {
|
||||
return <SignInForm onSubmit={this.onSubmit} />;
|
||||
return <SignInWithEmail onSubmit={this.onSubmit} />;
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { SignInWithFacebookContainer_auth as AuthData } from "talk-admin/__generated__/SignInWithFacebookContainer_auth.graphql";
|
||||
import FacebookButton from "talk-framework/components/FacebookButton";
|
||||
import { redirectOAuth2 } from "talk-framework/helpers";
|
||||
import { graphql, withFragmentContainer } from "talk-framework/lib/relay";
|
||||
|
||||
interface Props {
|
||||
auth: AuthData;
|
||||
}
|
||||
|
||||
class SignInWithFacebookContainer extends Component<Props> {
|
||||
private handleOnClick = () => {
|
||||
redirectOAuth2(this.props.auth.integrations.facebook.redirectURL);
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Localized id="login-signInWithFacebook">
|
||||
<FacebookButton onClick={this.handleOnClick}>
|
||||
Sign in with Facebook
|
||||
</FacebookButton>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
auth: graphql`
|
||||
fragment SignInWithFacebookContainer_auth on Auth {
|
||||
integrations {
|
||||
facebook {
|
||||
redirectURL
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(SignInWithFacebookContainer);
|
||||
|
||||
export default enhanced;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { SignInWithGoogleContainer_auth as AuthData } from "talk-admin/__generated__/SignInWithGoogleContainer_auth.graphql";
|
||||
import GoogleButton from "talk-framework/components/GoogleButton";
|
||||
import { redirectOAuth2 } from "talk-framework/helpers";
|
||||
import { graphql, withFragmentContainer } from "talk-framework/lib/relay";
|
||||
|
||||
interface Props {
|
||||
auth: AuthData;
|
||||
}
|
||||
|
||||
class SignInWithGoogleContainer extends Component<Props> {
|
||||
private handleOnClick = () => {
|
||||
redirectOAuth2(this.props.auth.integrations.google.redirectURL);
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Localized id="login-signInWithGoogle">
|
||||
<GoogleButton onClick={this.handleOnClick}>
|
||||
Sign in with Google
|
||||
</GoogleButton>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
auth: graphql`
|
||||
fragment SignInWithGoogleContainer_auth on Auth {
|
||||
integrations {
|
||||
google {
|
||||
redirectURL
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(SignInWithGoogleContainer);
|
||||
|
||||
export default enhanced;
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { SignInWithOIDCContainer_auth as AuthData } from "talk-admin/__generated__/SignInWithOIDCContainer_auth.graphql";
|
||||
import OIDCButton from "talk-framework/components/OIDCButton";
|
||||
import { redirectOAuth2 } from "talk-framework/helpers";
|
||||
import { graphql, withFragmentContainer } from "talk-framework/lib/relay";
|
||||
|
||||
interface Props {
|
||||
auth: AuthData;
|
||||
}
|
||||
|
||||
class SignInWithOIDCContainer extends Component<Props> {
|
||||
private handleOnClick = () => {
|
||||
redirectOAuth2(this.props.auth.integrations.oidc.redirectURL!);
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Localized
|
||||
id="login-signInWithOIDC"
|
||||
$name={this.props.auth.integrations.oidc.name}
|
||||
>
|
||||
<OIDCButton onClick={this.handleOnClick}>Sign in with $name</OIDCButton>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
auth: graphql`
|
||||
fragment SignInWithOIDCContainer_auth on Auth {
|
||||
integrations {
|
||||
oidc {
|
||||
name
|
||||
redirectURL
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(SignInWithOIDCContainer);
|
||||
|
||||
export default enhanced;
|
||||
@@ -3,23 +3,30 @@ import React from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { ModerateContainerQueryResponse } from "talk-admin/__generated__/ModerateContainerQuery.graphql";
|
||||
import { withRouteConfig } from "talk-framework/lib/router";
|
||||
|
||||
import Moderate from "../components/Moderate";
|
||||
|
||||
type Props = ModerateContainerQueryResponse;
|
||||
interface Props {
|
||||
data: ModerateContainerQueryResponse;
|
||||
}
|
||||
|
||||
export default class ModerateContainer extends React.Component<Props> {
|
||||
class ModerateContainer extends React.Component<Props> {
|
||||
public static routeConfig: RouteProps;
|
||||
|
||||
public render() {
|
||||
if (!this.props.moderationQueues) {
|
||||
if (!this.props.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.props.data.moderationQueues) {
|
||||
return <Moderate />;
|
||||
}
|
||||
return (
|
||||
<Moderate
|
||||
unmoderatedCount={this.props.moderationQueues.unmoderated.count}
|
||||
reportedCount={this.props.moderationQueues.reported.count}
|
||||
pendingCount={this.props.moderationQueues.pending.count}
|
||||
unmoderatedCount={this.props.data.moderationQueues.unmoderated.count}
|
||||
reportedCount={this.props.data.moderationQueues.reported.count}
|
||||
pendingCount={this.props.data.moderationQueues.pending.count}
|
||||
>
|
||||
{this.props.children}
|
||||
</Moderate>
|
||||
@@ -27,8 +34,7 @@ export default class ModerateContainer extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
ModerateContainer.routeConfig = {
|
||||
Component: ModerateContainer,
|
||||
const enhanced = withRouteConfig<ModerateContainerQueryResponse>({
|
||||
query: graphql`
|
||||
query ModerateContainerQuery {
|
||||
moderationQueues {
|
||||
@@ -45,6 +51,6 @@ ModerateContainer.routeConfig = {
|
||||
}
|
||||
`,
|
||||
cacheConfig: { force: true },
|
||||
render: ({ Component, props }) =>
|
||||
Component ? <Component {...props} /> : undefined,
|
||||
};
|
||||
})(ModerateContainer);
|
||||
|
||||
export default enhanced;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,595 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`accepts valid email 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
For your added security, we require users to add an email address to their accounts.
|
||||
</p>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
onChange={[Function]}
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value="hans@test.com"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="confirmEmail"
|
||||
>
|
||||
Confirm Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorError TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="confirmEmail"
|
||||
name="confirmEmail"
|
||||
onChange={[Function]}
|
||||
placeholder="Confirm Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
This field is required.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Add Email Address
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`accepts valid email confirmation 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
For your added security, we require users to add an email address to their accounts.
|
||||
</p>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={true}
|
||||
id="email"
|
||||
name="email"
|
||||
onChange={[Function]}
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value="hans@test.com"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="confirmEmail"
|
||||
>
|
||||
Confirm Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={true}
|
||||
id="confirmEmail"
|
||||
name="confirmEmail"
|
||||
onChange={[Function]}
|
||||
placeholder="Confirm Email Address"
|
||||
type="text"
|
||||
value="hans@test.com"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth Button-disabled"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Add Email Address
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`checks for invalid email 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
For your added security, we require users to add an email address to their accounts.
|
||||
</p>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorError TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
onChange={[Function]}
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value="invalid-email"
|
||||
/>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
Please enter a valid email address.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="confirmEmail"
|
||||
>
|
||||
Confirm Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorError TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="confirmEmail"
|
||||
name="confirmEmail"
|
||||
onChange={[Function]}
|
||||
placeholder="Confirm Email Address"
|
||||
type="text"
|
||||
value="invalid-confirmation-email"
|
||||
/>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
Emails do not match. Try again.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Add Email Address
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`renders addEmailAddress view 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="completeAccountBox"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-justifyCenter"
|
||||
>
|
||||
<div
|
||||
className="CompleteAccountBox-container"
|
||||
>
|
||||
<div
|
||||
className="CompleteAccountBox-header"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading3 Typography-colorTextLight Typography-alignCenter CompleteAccountBox-heading3"
|
||||
>
|
||||
Finish Setting Up Your Account
|
||||
</h1>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextLight Typography-alignCenter"
|
||||
>
|
||||
<span>
|
||||
Add Email Address
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="CompleteAccountBox-main"
|
||||
>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
For your added security, we require users to add an email address to their accounts.
|
||||
</p>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
onChange={[Function]}
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="confirmEmail"
|
||||
>
|
||||
Confirm Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="confirmEmail"
|
||||
name="confirmEmail"
|
||||
onChange={[Function]}
|
||||
placeholder="Confirm Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Add Email Address
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`shows error when submitting empty form 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
For your added security, we require users to add an email address to their accounts.
|
||||
</p>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorError TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
onChange={[Function]}
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
This field is required.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="confirmEmail"
|
||||
>
|
||||
Confirm Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorError TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="confirmEmail"
|
||||
name="confirmEmail"
|
||||
onChange={[Function]}
|
||||
placeholder="Confirm Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
This field is required.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Add Email Address
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`shows server error 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
For your added security, we require users to add an email address to their accounts.
|
||||
</p>
|
||||
<div
|
||||
className="CallOut-root CallOut-colorError CallOut-fullWidth"
|
||||
>
|
||||
server error
|
||||
|
||||
GraphQL request (4:3)
|
||||
3: ) {
|
||||
4: setEmail(input: $input) {
|
||||
^
|
||||
5: user {
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
onChange={[Function]}
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value="hans@test.com"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="confirmEmail"
|
||||
>
|
||||
Confirm Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="confirmEmail"
|
||||
name="confirmEmail"
|
||||
onChange={[Function]}
|
||||
placeholder="Confirm Email Address"
|
||||
type="text"
|
||||
value="hans@test.com"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Add Email Address
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`successfully sets email 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
For your added security, we require users to add an email address to their accounts.
|
||||
</p>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
onChange={[Function]}
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="confirmEmail"
|
||||
>
|
||||
Confirm Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="confirmEmail"
|
||||
name="confirmEmail"
|
||||
onChange={[Function]}
|
||||
placeholder="Confirm Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Add Email Address
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
@@ -0,0 +1,458 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`checks for invalid password 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
To protect against unauthorized changes to your account,
|
||||
we require users to create a password.
|
||||
</p>
|
||||
<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-colorError PasswordField-fullWidth PasswordField-input"
|
||||
disabled={false}
|
||||
id="password"
|
||||
name="password"
|
||||
onChange={[Function]}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value="x"
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Show password"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
Password must contain at least 8 characters.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Create Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`renders createPassword view 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="completeAccountBox"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-justifyCenter"
|
||||
>
|
||||
<div
|
||||
className="CompleteAccountBox-container"
|
||||
>
|
||||
<div
|
||||
className="CompleteAccountBox-header"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading3 Typography-colorTextLight Typography-alignCenter CompleteAccountBox-heading3"
|
||||
>
|
||||
Finish Setting Up Your Account
|
||||
</h1>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextLight Typography-alignCenter"
|
||||
>
|
||||
<span>
|
||||
Create Password
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="CompleteAccountBox-main"
|
||||
>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
To protect against unauthorized changes to your account,
|
||||
we require users to create a password.
|
||||
</p>
|
||||
<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"
|
||||
disabled={false}
|
||||
id="password"
|
||||
name="password"
|
||||
onChange={[Function]}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Show password"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Create Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`shows error when submitting empty form 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
To protect against unauthorized changes to your account,
|
||||
we require users to create a password.
|
||||
</p>
|
||||
<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-colorError PasswordField-fullWidth PasswordField-input"
|
||||
disabled={false}
|
||||
id="password"
|
||||
name="password"
|
||||
onChange={[Function]}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Show password"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
This field is required.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Create Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`shows server error 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
To protect against unauthorized changes to your account,
|
||||
we require users to create a password.
|
||||
</p>
|
||||
<div
|
||||
className="CallOut-root CallOut-colorError CallOut-fullWidth"
|
||||
>
|
||||
server error
|
||||
|
||||
GraphQL request (4:3)
|
||||
3: ) {
|
||||
4: setPassword(input: $input) {
|
||||
^
|
||||
5: user {
|
||||
|
||||
</div>
|
||||
<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"
|
||||
disabled={false}
|
||||
id="password"
|
||||
name="password"
|
||||
onChange={[Function]}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value="secretpassword"
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Show password"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Create Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`successfully sets password 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
To protect against unauthorized changes to your account,
|
||||
we require users to create a password.
|
||||
</p>
|
||||
<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"
|
||||
disabled={false}
|
||||
id="password"
|
||||
name="password"
|
||||
onChange={[Function]}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Show password"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Create Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
@@ -0,0 +1,397 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`accepts valid username 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Your username is a unique identifier that will appear on all of your comments.
|
||||
</p>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="username"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<p
|
||||
className="Typography-root Typography-detail Typography-colorTextSecondary"
|
||||
>
|
||||
You may use “_” and “.” Spaces not permitted.
|
||||
</p>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={true}
|
||||
id="username"
|
||||
name="username"
|
||||
onChange={[Function]}
|
||||
placeholder="Username"
|
||||
type="text"
|
||||
value="hans"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth Button-disabled"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Create Username
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`checks for invalid username 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Your username is a unique identifier that will appear on all of your comments.
|
||||
</p>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="username"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<p
|
||||
className="Typography-root Typography-detail Typography-colorTextSecondary"
|
||||
>
|
||||
You may use “_” and “.” Spaces not permitted.
|
||||
</p>
|
||||
<input
|
||||
className="TextField-root TextField-colorError TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="username"
|
||||
name="username"
|
||||
onChange={[Function]}
|
||||
placeholder="Username"
|
||||
type="text"
|
||||
value="x"
|
||||
/>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
Username must contain at least 3 characters.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Create Username
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`renders createUsername view 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="completeAccountBox"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-justifyCenter"
|
||||
>
|
||||
<div
|
||||
className="CompleteAccountBox-container"
|
||||
>
|
||||
<div
|
||||
className="CompleteAccountBox-header"
|
||||
>
|
||||
<h1
|
||||
className="Typography-root Typography-heading3 Typography-colorTextLight Typography-alignCenter CompleteAccountBox-heading3"
|
||||
>
|
||||
Finish Setting Up Your Account
|
||||
</h1>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextLight Typography-alignCenter"
|
||||
>
|
||||
<span>
|
||||
Create Username
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="CompleteAccountBox-main"
|
||||
>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Your username is a unique identifier that will appear on all of your comments.
|
||||
</p>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="username"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<p
|
||||
className="Typography-root Typography-detail Typography-colorTextSecondary"
|
||||
>
|
||||
You may use “_” and “.” Spaces not permitted.
|
||||
</p>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="username"
|
||||
name="username"
|
||||
onChange={[Function]}
|
||||
placeholder="Username"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Create Username
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`shows error when submitting empty form 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Your username is a unique identifier that will appear on all of your comments.
|
||||
</p>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="username"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<p
|
||||
className="Typography-root Typography-detail Typography-colorTextSecondary"
|
||||
>
|
||||
You may use “_” and “.” Spaces not permitted.
|
||||
</p>
|
||||
<input
|
||||
className="TextField-root TextField-colorError TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="username"
|
||||
name="username"
|
||||
onChange={[Function]}
|
||||
placeholder="Username"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
This field is required.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Create Username
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`shows server error 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Your username is a unique identifier that will appear on all of your comments.
|
||||
</p>
|
||||
<div
|
||||
className="CallOut-root CallOut-colorError CallOut-fullWidth"
|
||||
>
|
||||
server error
|
||||
|
||||
GraphQL request (4:3)
|
||||
3: ) {
|
||||
4: setUsername(input: $input) {
|
||||
^
|
||||
5: user {
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="username"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<p
|
||||
className="Typography-root Typography-detail Typography-colorTextSecondary"
|
||||
>
|
||||
You may use “_” and “.” Spaces not permitted.
|
||||
</p>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="username"
|
||||
name="username"
|
||||
onChange={[Function]}
|
||||
placeholder="Username"
|
||||
type="text"
|
||||
value="hans"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Create Username
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`successfully sets username 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
Your username is a unique identifier that will appear on all of your comments.
|
||||
</p>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="username"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<p
|
||||
className="Typography-root Typography-detail Typography-colorTextSecondary"
|
||||
>
|
||||
You may use “_” and “.” Spaces not permitted.
|
||||
</p>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="username"
|
||||
name="username"
|
||||
onChange={[Function]}
|
||||
placeholder="Username"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorPrimary Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
Create Username
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
@@ -0,0 +1,134 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`show restricted screen for commenters 1`] = `
|
||||
<div
|
||||
data-testid="authBox"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-justifyCenter"
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root AuthBox-container HorizontalGutter-double"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-justifyCenter"
|
||||
>
|
||||
<div
|
||||
className="AuthBox-brandIcon"
|
||||
>
|
||||
<svg
|
||||
className="BrandIcon-base BrandIcon-lg"
|
||||
data-name="Layer 1"
|
||||
id="Layer_1"
|
||||
viewBox="0 0 541.77 557.72"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<clipPath
|
||||
id="clip-path"
|
||||
>
|
||||
<rect
|
||||
fill="none"
|
||||
height="570"
|
||||
width="554"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath
|
||||
id="clip-path-2"
|
||||
transform="translate(-8.12 -8.14)"
|
||||
>
|
||||
<path
|
||||
clipPath="url(#clip-path)"
|
||||
clipRule="evenodd"
|
||||
d="M61.63,350.45c-.67,10.22,1.34,21.13,6,32.21a95.36,95.36,0,0,0,27.22,36.41c26,21.16,52.14,17.65,77.43,14.24,21.22-2.85,43.17-5.8,69,5.43,33.54,14.57,59.71,45.39,66.65,78.46a99.16,99.16,0,0,1,.11,38.66H219.65a16.89,16.89,0,0,0-.71-2.44c-16.54-44.09-38.46-69.52-67.09-77.78-9.37-2.66-18-3-25.68-3.27-9.91-.37-17.75-.66-26-5.6s-14.93-13.28-19.59-24.42a17,17,0,0,0-22.12-9.07,16.69,16.69,0,0,0-9.17,21.88c7.45,17.8,18.6,31.31,33.25,40.21,15.85,9.48,30.54,10,42.35,10.48,6.55.24,12.2.45,17.55,2,15.64,4.51,29.43,20.65,41.07,48H63.07a45,45,0,0,1-44.95-45V322.78a144.51,144.51,0,0,0,43.51,27.67Zm13-31.55a109.22,109.22,0,0,1-56.48-52.22v-66a88.41,88.41,0,0,0,35.59,10.37c18.52,1.13,34.77-4.64,47-16.71,11.63-11.51,14.6-24.24,18.05-39,2.88-12.32,6.46-27.66,16.66-48.7,10.44-21.72,17.13-30,24.69-30.51,11-.79,29.3,13.46,32.09,34.75,2,14.93-3.58,25.14-10.07,37C175,161,166,177.28,172.4,198.15c6.81,22.16,25.44,31.45,40.4,38.91,15.61,7.78,25.42,13.21,30.37,26.2,4.26,11.27,4.87,27.9-.71,33.32-3.74,3.66-16.43.64-29.86-2.55-23-5.47-54.5-12.95-91.49-1.57-9.77,3-32.07,9.72-46.51,26.44ZM357.73,18.14c-15,17.37-28.76,40.23-34.85,69-4.25,19.33-13.09,59.57,13.51,88.26A71.68,71.68,0,0,0,388.11,198a61.12,61.12,0,0,0,22.65-4.23c31.63-12.61,46.84-43.4,49.25-56.36,3.24-17.51,5-35.76,6.33-51.32a11.43,11.43,0,0,1,5.38-8.61l.5-.31a12.23,12.23,0,0,1,13.33,1.63c19.82,16.56,36.73,31.82,51.69,46.63a17.07,17.07,0,0,0,2.64,2.14v93.21a16.92,16.92,0,0,0-3.87,4.69c-9.23,16.43-23.71,36.42-40.11,38.31-6.55.7-10.3-1.49-20.21-7.9S452.37,240.8,431,236c-23.2-5.22-38.56-2.06-50.9.48-10.61,2.18-17.62,3.62-28.9,0-27.43-9-42.31-36.22-51.21-52.47l-.24-.45c-19.81-36.54-18.23-71.85-16.84-103l0-.58a287.16,287.16,0,0,1,9.81-61.77Zm49.14,0h88.06a45,45,0,0,1,44.95,45V80.79c-10.08-9.22-21-18.69-32.79-28.58a45.72,45.72,0,0,0-50.93-5.38,19.1,19.1,0,0,0-2.55,1.52,46.1,46.1,0,0,0-21,34.46,1.49,1.49,0,0,0,0,.21c-1.24,14.7-2.91,31.88-5.86,47.9-.88,3.66-9.12,23.11-28.33,30.76-12.81,5.1-29.11-1.09-37.41-10-14.22-15.33-8.26-42.48-5-57.19C363.43,59.29,386.3,35,404.17,20.82a18,18,0,0,0,2.7-2.68Zm-149.42,0a319.88,319.88,0,0,0-8.67,60.21l0,.62c-1.43,32-3.38,75.91,21,120.82l.28.52c9.81,17.93,30.25,55.25,70.55,68.52,19.95,6.48,34,3.59,46.44,1,11-2.26,20.48-4.21,36.49-.61,15.43,3.48,24.69,9.46,33.63,15.25C467,290.87,478,298,493.8,298a54,54,0,0,0,5.86-.32c14.5-1.67,27.83-8.41,40.22-20.42v45.53a179.81,179.81,0,0,1-47.68.48c-17-2.18-28.9-6.21-40.39-10.1-16.13-5.46-31.37-10.63-54.46-8.79-12.34.86-49.5,3.52-68.67,32.46-21.27,32-4.79,71.47-1.27,79.05,0,.06,0,.11.07.17,11.15,23.37,28,33.16,44.25,42.63,9.57,5.57,19.46,11.33,29.45,20.4C421,497.23,435.05,523,443,555.86H342.61a132,132,0,0,0-1.29-45.63c-9.16-43.62-43.1-84-86.46-102.82-34.51-15-63.68-11.11-87.12-8-24.18,3.25-37.44,4.42-51.35-6.91-15-12.14-24-32.83-19.67-45.1,4.74-13.34,25.44-19.62,34.35-22.32,28.19-8.67,52.33-2.93,73.64,2.14,21.83,5.18,44.4,10.55,61.57-6.21,17.7-17.22,17-48.29,8.81-69.92-10-26.15-30.53-36.41-47.06-44.65-13.09-6.53-20.6-10.6-23-18.37-2-6.43.36-11.62,7-23.75,7.41-13.48,17.55-31.93,14-58-4.77-36.43-36.65-66.5-68.28-64.3-30.19,2.1-44.53,32-53.08,49.73-11.83,24.42-16.07,42.54-19.16,55.78-2.9,12.39-4.37,18.06-8.79,22.43-5.21,5.13-12.26,7.47-21,6.93-13-.8-27.57-8-37.68-18.45V63.15a45,45,0,0,1,45-45Zm282.43,449v43.76a45,45,0,0,1-44.95,45H477.46a16.09,16.09,0,0,0-.36-2.56c-4.89-22.41-12.24-42.37-22-59.73a120.79,120.79,0,0,1,39-21,119.51,119.51,0,0,1,45.77-5.45Zm0-34.82a152.6,152.6,0,0,0-56,7.12,154.85,154.85,0,0,0-48.66,25.94,153.26,153.26,0,0,0-11.29-11.5c-12.68-11.52-24.67-18.5-35.24-24.65-14.45-8.41-24-14-30.62-27.78-.87-1.9-12.66-28.42-1.23-45.62,10-15.1,33.94-16.77,42.95-17.4l.17,0c16.24-1.3,26.66,2.23,41.07,7.11,12.45,4.22,26.57,9,46.95,11.61a212.66,212.66,0,0,0,51.92.08v75.11Z"
|
||||
fill="none"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<title>
|
||||
logo mark2018
|
||||
</title>
|
||||
<g
|
||||
clipPath="url(#clip-path-2)"
|
||||
>
|
||||
<rect
|
||||
fill="#f7705f"
|
||||
height="557.72"
|
||||
width="541.77"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Typography-alignCenter"
|
||||
>
|
||||
<span>
|
||||
Currently signed in to
|
||||
</span>
|
||||
</h1>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-alignCenter BrandName-root BrandName-lg"
|
||||
>
|
||||
Talk
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-justifyCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Restricted-lockIcon Icon-lg"
|
||||
>
|
||||
lock
|
||||
</span>
|
||||
</div>
|
||||
<h1
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Typography-alignCenter Restricted-noPermission"
|
||||
>
|
||||
You do not have permission to access this page.
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary Typography-alignCenter"
|
||||
>
|
||||
You are signed in as:
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-alignCenter"
|
||||
>
|
||||
Markus
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-justifyCenter"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Sign in with a different account
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary Typography-alignCenter Restricted-contactAdmin"
|
||||
>
|
||||
If you think this is an error, please contact your administrator for assistance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,735 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`accepts correct password 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<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="email"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorError TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
onChange={[Function]}
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
This field is required.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
>
|
||||
<input
|
||||
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
|
||||
disabled={false}
|
||||
id="password"
|
||||
name="password"
|
||||
onChange={[Function]}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value="testtest"
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Show password"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorBrand Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root ButtonIcon-root Icon-md"
|
||||
>
|
||||
email
|
||||
</span>
|
||||
<span>
|
||||
Sign in with Email
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`accepts valid email 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<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="email"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
onChange={[Function]}
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value="hans@test.com"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
>
|
||||
<input
|
||||
className="PasswordField-colorError PasswordField-fullWidth PasswordField-input"
|
||||
disabled={false}
|
||||
id="password"
|
||||
name="password"
|
||||
onChange={[Function]}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Show password"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
This field is required.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorBrand Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root ButtonIcon-root Icon-md"
|
||||
>
|
||||
email
|
||||
</span>
|
||||
<span>
|
||||
Sign in with Email
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`checks for invalid email 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<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="email"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorError TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
onChange={[Function]}
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value="invalid-email"
|
||||
/>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
Please enter a valid email address.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
>
|
||||
<input
|
||||
className="PasswordField-colorError PasswordField-fullWidth PasswordField-input"
|
||||
disabled={false}
|
||||
id="password"
|
||||
name="password"
|
||||
onChange={[Function]}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Show password"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
This field is required.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorBrand Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root ButtonIcon-root Icon-md"
|
||||
>
|
||||
email
|
||||
</span>
|
||||
<span>
|
||||
Sign in with Email
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`renders sign in form 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="authBox"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-justifyCenter"
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root AuthBox-container HorizontalGutter-double"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-justifyCenter"
|
||||
>
|
||||
<div
|
||||
className="AuthBox-brandIcon"
|
||||
>
|
||||
<svg
|
||||
className="BrandIcon-base BrandIcon-lg"
|
||||
data-name="Layer 1"
|
||||
id="Layer_1"
|
||||
viewBox="0 0 541.77 557.72"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<clipPath
|
||||
id="clip-path"
|
||||
>
|
||||
<rect
|
||||
fill="none"
|
||||
height="570"
|
||||
width="554"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath
|
||||
id="clip-path-2"
|
||||
transform="translate(-8.12 -8.14)"
|
||||
>
|
||||
<path
|
||||
clipPath="url(#clip-path)"
|
||||
clipRule="evenodd"
|
||||
d="M61.63,350.45c-.67,10.22,1.34,21.13,6,32.21a95.36,95.36,0,0,0,27.22,36.41c26,21.16,52.14,17.65,77.43,14.24,21.22-2.85,43.17-5.8,69,5.43,33.54,14.57,59.71,45.39,66.65,78.46a99.16,99.16,0,0,1,.11,38.66H219.65a16.89,16.89,0,0,0-.71-2.44c-16.54-44.09-38.46-69.52-67.09-77.78-9.37-2.66-18-3-25.68-3.27-9.91-.37-17.75-.66-26-5.6s-14.93-13.28-19.59-24.42a17,17,0,0,0-22.12-9.07,16.69,16.69,0,0,0-9.17,21.88c7.45,17.8,18.6,31.31,33.25,40.21,15.85,9.48,30.54,10,42.35,10.48,6.55.24,12.2.45,17.55,2,15.64,4.51,29.43,20.65,41.07,48H63.07a45,45,0,0,1-44.95-45V322.78a144.51,144.51,0,0,0,43.51,27.67Zm13-31.55a109.22,109.22,0,0,1-56.48-52.22v-66a88.41,88.41,0,0,0,35.59,10.37c18.52,1.13,34.77-4.64,47-16.71,11.63-11.51,14.6-24.24,18.05-39,2.88-12.32,6.46-27.66,16.66-48.7,10.44-21.72,17.13-30,24.69-30.51,11-.79,29.3,13.46,32.09,34.75,2,14.93-3.58,25.14-10.07,37C175,161,166,177.28,172.4,198.15c6.81,22.16,25.44,31.45,40.4,38.91,15.61,7.78,25.42,13.21,30.37,26.2,4.26,11.27,4.87,27.9-.71,33.32-3.74,3.66-16.43.64-29.86-2.55-23-5.47-54.5-12.95-91.49-1.57-9.77,3-32.07,9.72-46.51,26.44ZM357.73,18.14c-15,17.37-28.76,40.23-34.85,69-4.25,19.33-13.09,59.57,13.51,88.26A71.68,71.68,0,0,0,388.11,198a61.12,61.12,0,0,0,22.65-4.23c31.63-12.61,46.84-43.4,49.25-56.36,3.24-17.51,5-35.76,6.33-51.32a11.43,11.43,0,0,1,5.38-8.61l.5-.31a12.23,12.23,0,0,1,13.33,1.63c19.82,16.56,36.73,31.82,51.69,46.63a17.07,17.07,0,0,0,2.64,2.14v93.21a16.92,16.92,0,0,0-3.87,4.69c-9.23,16.43-23.71,36.42-40.11,38.31-6.55.7-10.3-1.49-20.21-7.9S452.37,240.8,431,236c-23.2-5.22-38.56-2.06-50.9.48-10.61,2.18-17.62,3.62-28.9,0-27.43-9-42.31-36.22-51.21-52.47l-.24-.45c-19.81-36.54-18.23-71.85-16.84-103l0-.58a287.16,287.16,0,0,1,9.81-61.77Zm49.14,0h88.06a45,45,0,0,1,44.95,45V80.79c-10.08-9.22-21-18.69-32.79-28.58a45.72,45.72,0,0,0-50.93-5.38,19.1,19.1,0,0,0-2.55,1.52,46.1,46.1,0,0,0-21,34.46,1.49,1.49,0,0,0,0,.21c-1.24,14.7-2.91,31.88-5.86,47.9-.88,3.66-9.12,23.11-28.33,30.76-12.81,5.1-29.11-1.09-37.41-10-14.22-15.33-8.26-42.48-5-57.19C363.43,59.29,386.3,35,404.17,20.82a18,18,0,0,0,2.7-2.68Zm-149.42,0a319.88,319.88,0,0,0-8.67,60.21l0,.62c-1.43,32-3.38,75.91,21,120.82l.28.52c9.81,17.93,30.25,55.25,70.55,68.52,19.95,6.48,34,3.59,46.44,1,11-2.26,20.48-4.21,36.49-.61,15.43,3.48,24.69,9.46,33.63,15.25C467,290.87,478,298,493.8,298a54,54,0,0,0,5.86-.32c14.5-1.67,27.83-8.41,40.22-20.42v45.53a179.81,179.81,0,0,1-47.68.48c-17-2.18-28.9-6.21-40.39-10.1-16.13-5.46-31.37-10.63-54.46-8.79-12.34.86-49.5,3.52-68.67,32.46-21.27,32-4.79,71.47-1.27,79.05,0,.06,0,.11.07.17,11.15,23.37,28,33.16,44.25,42.63,9.57,5.57,19.46,11.33,29.45,20.4C421,497.23,435.05,523,443,555.86H342.61a132,132,0,0,0-1.29-45.63c-9.16-43.62-43.1-84-86.46-102.82-34.51-15-63.68-11.11-87.12-8-24.18,3.25-37.44,4.42-51.35-6.91-15-12.14-24-32.83-19.67-45.1,4.74-13.34,25.44-19.62,34.35-22.32,28.19-8.67,52.33-2.93,73.64,2.14,21.83,5.18,44.4,10.55,61.57-6.21,17.7-17.22,17-48.29,8.81-69.92-10-26.15-30.53-36.41-47.06-44.65-13.09-6.53-20.6-10.6-23-18.37-2-6.43.36-11.62,7-23.75,7.41-13.48,17.55-31.93,14-58-4.77-36.43-36.65-66.5-68.28-64.3-30.19,2.1-44.53,32-53.08,49.73-11.83,24.42-16.07,42.54-19.16,55.78-2.9,12.39-4.37,18.06-8.79,22.43-5.21,5.13-12.26,7.47-21,6.93-13-.8-27.57-8-37.68-18.45V63.15a45,45,0,0,1,45-45Zm282.43,449v43.76a45,45,0,0,1-44.95,45H477.46a16.09,16.09,0,0,0-.36-2.56c-4.89-22.41-12.24-42.37-22-59.73a120.79,120.79,0,0,1,39-21,119.51,119.51,0,0,1,45.77-5.45Zm0-34.82a152.6,152.6,0,0,0-56,7.12,154.85,154.85,0,0,0-48.66,25.94,153.26,153.26,0,0,0-11.29-11.5c-12.68-11.52-24.67-18.5-35.24-24.65-14.45-8.41-24-14-30.62-27.78-.87-1.9-12.66-28.42-1.23-45.62,10-15.1,33.94-16.77,42.95-17.4l.17,0c16.24-1.3,26.66,2.23,41.07,7.11,12.45,4.22,26.57,9,46.95,11.61a212.66,212.66,0,0,0,51.92.08v75.11Z"
|
||||
fill="none"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<title>
|
||||
logo mark2018
|
||||
</title>
|
||||
<g
|
||||
clipPath="url(#clip-path-2)"
|
||||
>
|
||||
<rect
|
||||
fill="#f7705f"
|
||||
height="557.72"
|
||||
width="541.77"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Typography-alignCenter"
|
||||
>
|
||||
<span>
|
||||
Sign in to
|
||||
</span>
|
||||
</h1>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-alignCenter BrandName-root BrandName-lg"
|
||||
>
|
||||
Talk
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
>
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<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="email"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
onChange={[Function]}
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
>
|
||||
<input
|
||||
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
|
||||
disabled={false}
|
||||
id="password"
|
||||
name="password"
|
||||
onChange={[Function]}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Show password"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorBrand Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root ButtonIcon-root Icon-md"
|
||||
>
|
||||
email
|
||||
</span>
|
||||
<span>
|
||||
Sign in with Email
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`shows error when submitting empty form 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<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="email"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorError TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
onChange={[Function]}
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
This field is required.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
>
|
||||
<input
|
||||
className="PasswordField-colorError PasswordField-fullWidth PasswordField-input"
|
||||
disabled={false}
|
||||
id="password"
|
||||
name="password"
|
||||
onChange={[Function]}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Show password"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Message-root Message-colorError Message-fullWidth"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root MessageIcon-root Icon-sm"
|
||||
>
|
||||
warning
|
||||
</span>
|
||||
<span>
|
||||
This field is required.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorBrand Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root ButtonIcon-root Icon-md"
|
||||
>
|
||||
email
|
||||
</span>
|
||||
<span>
|
||||
Sign in with Email
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`shows server error 1`] = `
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="CallOut-root CallOut-colorError CallOut-fullWidth"
|
||||
>
|
||||
Server Error
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
className="TextField-root TextField-colorRegular TextField-fullWidth"
|
||||
disabled={false}
|
||||
id="email"
|
||||
name="email"
|
||||
onChange={[Function]}
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value="hans@test.com"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root FormField-root HorizontalGutter-half"
|
||||
>
|
||||
<label
|
||||
className="Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
|
||||
htmlFor="password"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
>
|
||||
<input
|
||||
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
|
||||
disabled={false}
|
||||
id="password"
|
||||
name="password"
|
||||
onChange={[Function]}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
value="testtest"
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Show password"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorBrand Button-variantFilled Button-fullWidth"
|
||||
disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root ButtonIcon-root Icon-md"
|
||||
>
|
||||
email
|
||||
</span>
|
||||
<span>
|
||||
Sign in with Email
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`submits form successfully 1`] = `"http://localhost/admin/login#accessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIzMWIyNjU5MS00ZTlhLTQzODgtYTdmZi1lMWJkYzVkOTdjY2UifQ=="`;
|
||||
@@ -0,0 +1,127 @@
|
||||
import { get, merge } from "lodash";
|
||||
import sinon from "sinon";
|
||||
|
||||
import {
|
||||
createAuthToken,
|
||||
replaceHistoryLocation,
|
||||
wait,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import create from "../create";
|
||||
import { emptyModerationQueues, settings, users } from "../fixtures";
|
||||
|
||||
async function createTestRenderer(
|
||||
customResolver: any = {},
|
||||
options: { muteNetworkErrors?: boolean; logNetwork?: boolean } = {}
|
||||
) {
|
||||
replaceHistoryLocation("http://localhost/admin/login");
|
||||
const resolvers = {
|
||||
...customResolver,
|
||||
Query: {
|
||||
...customResolver.Query,
|
||||
moderationQueues: sinon.stub().returns(emptyModerationQueues),
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
|
||||
me: sinon
|
||||
.stub()
|
||||
.returns(
|
||||
merge(
|
||||
{ ...users[0], email: "", username: "", profiles: [] },
|
||||
get(customResolver, "Query.me")
|
||||
)
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
const { testRenderer, context } = create({
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: options.logNetwork,
|
||||
muteNetworkErrors: options.muteNetworkErrors,
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue("SIGN_IN", "authView");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(createAuthToken(), "authToken");
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
context,
|
||||
testRenderer,
|
||||
root: testRenderer.root,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders addEmailAddress view", async () => {
|
||||
const { root } = await createTestRenderer();
|
||||
await waitForElement(() => within(root).queryByText("Add Email Address"));
|
||||
});
|
||||
|
||||
it("renders createUsername view", async () => {
|
||||
const { root } = await createTestRenderer({
|
||||
Query: {
|
||||
me: {
|
||||
email: "hans@test.com",
|
||||
},
|
||||
},
|
||||
});
|
||||
await waitForElement(() => within(root).queryByText("Create Username"));
|
||||
});
|
||||
|
||||
it("renders createPassword view", async () => {
|
||||
const { root } = await createTestRenderer({
|
||||
Query: {
|
||||
me: {
|
||||
email: "hans@test.com",
|
||||
username: "hans",
|
||||
},
|
||||
},
|
||||
});
|
||||
await waitForElement(() => within(root).queryByText("Create Password"));
|
||||
});
|
||||
|
||||
it("do not render createPassword view when local auth is disabled", async () => {
|
||||
await createTestRenderer({
|
||||
Query: {
|
||||
me: {
|
||||
email: "hans@test.com",
|
||||
username: "hans",
|
||||
},
|
||||
settings: {
|
||||
auth: {
|
||||
integrations: {
|
||||
local: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await wait(() =>
|
||||
expect(window.location.toString()).toBe(
|
||||
"http://localhost/admin/moderate/reported"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it("complete account", async () => {
|
||||
await createTestRenderer({
|
||||
Query: {
|
||||
me: {
|
||||
email: "hans@test.com",
|
||||
username: "hans",
|
||||
profiles: [{ __typename: "LocalProfile" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
await wait(() =>
|
||||
expect(window.location.toString()).toBe(
|
||||
"http://localhost/admin/moderate/reported"
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
import { get, merge } from "lodash";
|
||||
import sinon from "sinon";
|
||||
|
||||
import {
|
||||
createAuthToken,
|
||||
replaceHistoryLocation,
|
||||
toJSON,
|
||||
wait,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import create from "../create";
|
||||
import { settings, users } from "../fixtures";
|
||||
|
||||
async function createTestRenderer(
|
||||
customResolver: any = {},
|
||||
options: { muteNetworkErrors?: boolean; logNetwork?: boolean } = {}
|
||||
) {
|
||||
replaceHistoryLocation("http://localhost/admin/login");
|
||||
const resolvers = {
|
||||
...customResolver,
|
||||
Query: {
|
||||
...customResolver.Query,
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
|
||||
me: sinon.stub().returns({ ...users[0], email: "" }),
|
||||
},
|
||||
};
|
||||
|
||||
const { testRenderer, context } = create({
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: options.logNetwork,
|
||||
muteNetworkErrors: options.muteNetworkErrors,
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue("ADD_EMAIL_ADDRESS", "authView");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(createAuthToken(), "authToken");
|
||||
},
|
||||
});
|
||||
const container = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("completeAccountBox")
|
||||
);
|
||||
const form = within(container).getByType("form");
|
||||
const emailAddressField = within(form).getByLabelText("Email Address");
|
||||
const confirmEmailAddressField = within(form).getByLabelText(
|
||||
"Confirm Email Address"
|
||||
);
|
||||
|
||||
return {
|
||||
context,
|
||||
testRenderer,
|
||||
form,
|
||||
root: testRenderer.root,
|
||||
emailAddressField,
|
||||
confirmEmailAddressField,
|
||||
container,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders addEmailAddress view", async () => {
|
||||
const { root } = await createTestRenderer();
|
||||
expect(toJSON(root)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows error when submitting empty form", async () => {
|
||||
const { form } = await createTestRenderer();
|
||||
form.props.onSubmit();
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("checks for invalid email", async () => {
|
||||
const {
|
||||
form,
|
||||
emailAddressField,
|
||||
confirmEmailAddressField,
|
||||
} = await createTestRenderer();
|
||||
emailAddressField.props.onChange({ target: { value: "invalid-email" } });
|
||||
confirmEmailAddressField.props.onChange({
|
||||
target: { value: "invalid-confirmation-email" },
|
||||
});
|
||||
form.props.onSubmit();
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("accepts valid email", async () => {
|
||||
const { form, emailAddressField } = await createTestRenderer();
|
||||
emailAddressField.props.onChange({ target: { value: "hans@test.com" } });
|
||||
form.props.onSubmit();
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("accepts valid email confirmation", async () => {
|
||||
const {
|
||||
form,
|
||||
emailAddressField,
|
||||
confirmEmailAddressField,
|
||||
} = await createTestRenderer();
|
||||
emailAddressField.props.onChange({ target: { value: "hans@test.com" } });
|
||||
confirmEmailAddressField.props.onChange({
|
||||
target: { value: "hans@test.com" },
|
||||
});
|
||||
form.props.onSubmit();
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows server error", async () => {
|
||||
const email = "hans@test.com";
|
||||
const setEmail = sinon.stub().callsFake((_: any, data: any) => {
|
||||
throw new Error("server error");
|
||||
});
|
||||
const {
|
||||
form,
|
||||
emailAddressField,
|
||||
confirmEmailAddressField,
|
||||
} = await createTestRenderer(
|
||||
{
|
||||
Mutation: {
|
||||
setEmail,
|
||||
},
|
||||
},
|
||||
{ muteNetworkErrors: true }
|
||||
);
|
||||
const submitButton = form.find(
|
||||
i => i.type === "button" && i.props.type === "submit"
|
||||
);
|
||||
|
||||
emailAddressField.props.onChange({ target: { value: email } });
|
||||
confirmEmailAddressField.props.onChange({
|
||||
target: { value: email },
|
||||
});
|
||||
|
||||
form.props.onSubmit();
|
||||
expect(emailAddressField.props.disabled).toBe(true);
|
||||
expect(confirmEmailAddressField.props.disabled).toBe(true);
|
||||
expect(submitButton.props.disabled).toBe(true);
|
||||
|
||||
await wait(() => expect(submitButton.props.disabled).toBe(false));
|
||||
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("successfully sets email", async () => {
|
||||
const email = "hans@test.com";
|
||||
const setEmail = sinon.stub().callsFake((_: any, data: any) => {
|
||||
expect(data.input).toEqual({
|
||||
email,
|
||||
clientMutationId: data.input.clientMutationId,
|
||||
});
|
||||
return {
|
||||
user: {
|
||||
id: "me",
|
||||
email,
|
||||
},
|
||||
clientMutationId: data.input.clientMutationId,
|
||||
};
|
||||
});
|
||||
const {
|
||||
form,
|
||||
emailAddressField,
|
||||
confirmEmailAddressField,
|
||||
} = await createTestRenderer({
|
||||
Mutation: {
|
||||
setEmail,
|
||||
},
|
||||
});
|
||||
const submitButton = form.find(
|
||||
i => i.type === "button" && i.props.type === "submit"
|
||||
);
|
||||
|
||||
emailAddressField.props.onChange({ target: { value: email } });
|
||||
confirmEmailAddressField.props.onChange({
|
||||
target: { value: email },
|
||||
});
|
||||
|
||||
form.props.onSubmit();
|
||||
expect(emailAddressField.props.disabled).toBe(true);
|
||||
expect(confirmEmailAddressField.props.disabled).toBe(true);
|
||||
expect(submitButton.props.disabled).toBe(true);
|
||||
|
||||
await wait(() => expect(submitButton.props.disabled).toBe(false));
|
||||
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
expect(setEmail.called).toBe(true);
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { get, merge } from "lodash";
|
||||
import sinon from "sinon";
|
||||
|
||||
import {
|
||||
createAuthToken,
|
||||
replaceHistoryLocation,
|
||||
toJSON,
|
||||
wait,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import create from "../create";
|
||||
import { settings, users } from "../fixtures";
|
||||
|
||||
async function createTestRenderer(
|
||||
customResolver: any = {},
|
||||
options: { muteNetworkErrors?: boolean; logNetwork?: boolean } = {}
|
||||
) {
|
||||
replaceHistoryLocation("http://localhost/admin/login");
|
||||
const resolvers = {
|
||||
...customResolver,
|
||||
Query: {
|
||||
...customResolver.Query,
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
|
||||
me: sinon.stub().returns({ ...users[0], profiles: [] }),
|
||||
},
|
||||
};
|
||||
|
||||
const { testRenderer, context } = create({
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: options.logNetwork,
|
||||
muteNetworkErrors: options.muteNetworkErrors,
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue("CREATE_PASSWORD", "authView");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(createAuthToken(), "authToken");
|
||||
},
|
||||
});
|
||||
const container = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("completeAccountBox")
|
||||
);
|
||||
const form = within(container).getByType("form");
|
||||
const passwordField = within(form).getByLabelText("Password");
|
||||
|
||||
return {
|
||||
context,
|
||||
testRenderer,
|
||||
form,
|
||||
root: testRenderer.root,
|
||||
passwordField,
|
||||
container,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders createPassword view", async () => {
|
||||
const { root } = await createTestRenderer();
|
||||
expect(toJSON(root)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows error when submitting empty form", async () => {
|
||||
const { form } = await createTestRenderer();
|
||||
form.props.onSubmit();
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("checks for invalid password", async () => {
|
||||
const { form, passwordField } = await createTestRenderer();
|
||||
passwordField.props.onChange({ target: { value: "x" } });
|
||||
form.props.onSubmit();
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows server error", async () => {
|
||||
const password = "secretpassword";
|
||||
const setPassword = sinon.stub().callsFake((_: any, data: any) => {
|
||||
throw new Error("server error");
|
||||
});
|
||||
const { form, passwordField } = await createTestRenderer(
|
||||
{
|
||||
Mutation: {
|
||||
setPassword,
|
||||
},
|
||||
},
|
||||
{ muteNetworkErrors: true }
|
||||
);
|
||||
const submitButton = form.find(
|
||||
i => i.type === "button" && i.props.type === "submit"
|
||||
);
|
||||
|
||||
passwordField.props.onChange({ target: { value: password } });
|
||||
|
||||
form.props.onSubmit();
|
||||
expect(passwordField.props.disabled).toBe(true);
|
||||
expect(submitButton.props.disabled).toBe(true);
|
||||
|
||||
await wait(() => expect(submitButton.props.disabled).toBe(false));
|
||||
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("successfully sets password", async () => {
|
||||
const password = "secretpassword";
|
||||
const setPassword = sinon.stub().callsFake((_: any, data: any) => {
|
||||
expect(data.input).toEqual({
|
||||
password,
|
||||
clientMutationId: data.input.clientMutationId,
|
||||
});
|
||||
return {
|
||||
user: {
|
||||
id: "me",
|
||||
profiles: [],
|
||||
},
|
||||
clientMutationId: data.input.clientMutationId,
|
||||
};
|
||||
});
|
||||
const { form, passwordField } = await createTestRenderer({
|
||||
Mutation: {
|
||||
setPassword,
|
||||
},
|
||||
});
|
||||
const submitButton = form.find(
|
||||
i => i.type === "button" && i.props.type === "submit"
|
||||
);
|
||||
|
||||
passwordField.props.onChange({ target: { value: password } });
|
||||
|
||||
form.props.onSubmit();
|
||||
expect(passwordField.props.disabled).toBe(true);
|
||||
expect(submitButton.props.disabled).toBe(true);
|
||||
|
||||
await wait(() => expect(submitButton.props.disabled).toBe(false));
|
||||
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
expect(setPassword.called).toBe(true);
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
import { get, merge } from "lodash";
|
||||
import sinon from "sinon";
|
||||
|
||||
import {
|
||||
createAuthToken,
|
||||
replaceHistoryLocation,
|
||||
toJSON,
|
||||
wait,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import create from "../create";
|
||||
import { settings } from "../fixtures";
|
||||
|
||||
async function createTestRenderer(
|
||||
customResolver: any = {},
|
||||
options: { muteNetworkErrors?: boolean; logNetwork?: boolean } = {}
|
||||
) {
|
||||
replaceHistoryLocation("http://localhost/admin/login");
|
||||
const resolvers = {
|
||||
...customResolver,
|
||||
Query: {
|
||||
...customResolver.Query,
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
|
||||
},
|
||||
};
|
||||
|
||||
const { testRenderer, context } = create({
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: options.logNetwork,
|
||||
muteNetworkErrors: options.muteNetworkErrors,
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue("CREATE_USERNAME", "authView");
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(createAuthToken(), "authToken");
|
||||
},
|
||||
});
|
||||
const container = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("completeAccountBox")
|
||||
);
|
||||
const form = within(container).getByType("form");
|
||||
const usernameField = within(form).getByLabelText("Username");
|
||||
|
||||
return {
|
||||
context,
|
||||
testRenderer,
|
||||
form,
|
||||
root: testRenderer.root,
|
||||
usernameField,
|
||||
container,
|
||||
};
|
||||
}
|
||||
|
||||
it("renders createUsername view", async () => {
|
||||
const { root } = await createTestRenderer();
|
||||
expect(toJSON(root)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows error when submitting empty form", async () => {
|
||||
const { form } = await createTestRenderer();
|
||||
form.props.onSubmit();
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("checks for invalid username", async () => {
|
||||
const { form, usernameField } = await createTestRenderer();
|
||||
usernameField.props.onChange({ target: { value: "x" } });
|
||||
form.props.onSubmit();
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("accepts valid username", async () => {
|
||||
const { form, usernameField } = await createTestRenderer();
|
||||
usernameField.props.onChange({ target: { value: "hans" } });
|
||||
form.props.onSubmit();
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows server error", async () => {
|
||||
const username = "hans";
|
||||
const setUsername = sinon.stub().callsFake((_: any, data: any) => {
|
||||
throw new Error("server error");
|
||||
});
|
||||
const { form, usernameField } = await createTestRenderer(
|
||||
{
|
||||
Mutation: {
|
||||
setUsername,
|
||||
},
|
||||
},
|
||||
{ muteNetworkErrors: true }
|
||||
);
|
||||
const submitButton = form.find(
|
||||
i => i.type === "button" && i.props.type === "submit"
|
||||
);
|
||||
|
||||
usernameField.props.onChange({ target: { value: username } });
|
||||
|
||||
form.props.onSubmit();
|
||||
expect(usernameField.props.disabled).toBe(true);
|
||||
expect(submitButton.props.disabled).toBe(true);
|
||||
|
||||
await wait(() => expect(submitButton.props.disabled).toBe(false));
|
||||
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("successfully sets username", async () => {
|
||||
const username = "hans";
|
||||
const setUsername = sinon.stub().callsFake((_: any, data: any) => {
|
||||
expect(data.input).toEqual({
|
||||
username,
|
||||
clientMutationId: data.input.clientMutationId,
|
||||
});
|
||||
return {
|
||||
user: {
|
||||
id: "me",
|
||||
username,
|
||||
},
|
||||
clientMutationId: data.input.clientMutationId,
|
||||
};
|
||||
});
|
||||
const { form, usernameField } = await createTestRenderer({
|
||||
Mutation: {
|
||||
setUsername,
|
||||
},
|
||||
});
|
||||
const submitButton = form.find(
|
||||
i => i.type === "button" && i.props.type === "submit"
|
||||
);
|
||||
|
||||
usernameField.props.onChange({ target: { value: username } });
|
||||
|
||||
form.props.onSubmit();
|
||||
expect(usernameField.props.disabled).toBe(true);
|
||||
expect(submitButton.props.disabled).toBe(true);
|
||||
|
||||
await wait(() => expect(submitButton.props.disabled).toBe(false));
|
||||
|
||||
expect(toJSON(form)).toMatchSnapshot();
|
||||
expect(setUsername.called).toBe(true);
|
||||
});
|
||||
+18
-9
@@ -1,19 +1,24 @@
|
||||
import sinon from "sinon";
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { replaceHistoryLocation } from "talk-framework/testHelpers";
|
||||
import {
|
||||
createAuthToken,
|
||||
replaceHistoryLocation,
|
||||
wait,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import create from "./create";
|
||||
import create from "../create";
|
||||
import {
|
||||
emptyModerationQueues,
|
||||
emptyRejectedComments,
|
||||
settings,
|
||||
} from "./fixtures";
|
||||
users,
|
||||
} from "../fixtures";
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
settings: sinon.stub().returns(settings),
|
||||
moderationQueues: sinon.stub().returns(emptyModerationQueues),
|
||||
comments: sinon.stub().returns(emptyRejectedComments),
|
||||
me: sinon.stub().returns(users[0]),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,11 +30,13 @@ it("redirect when already logged in", async () => {
|
||||
logNetwork: false,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(createAuthToken(), "authToken");
|
||||
},
|
||||
});
|
||||
await timeout();
|
||||
expect(window.location.toString()).toBe(
|
||||
"http://localhost/admin/moderate/reported"
|
||||
await wait(() =>
|
||||
expect(window.location.toString()).toBe(
|
||||
"http://localhost/admin/moderate/reported"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -41,9 +48,11 @@ it("redirect to redirectPath when already logged in", async () => {
|
||||
logNetwork: false,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(createAuthToken(), "authToken");
|
||||
localRecord.setValue("/admin/community", "redirectPath");
|
||||
},
|
||||
});
|
||||
await timeout();
|
||||
expect(window.location.toString()).toBe("http://localhost/admin/community");
|
||||
await wait(() =>
|
||||
expect(window.location.toString()).toBe("http://localhost/admin/community")
|
||||
);
|
||||
});
|
||||
+13
-12
@@ -1,17 +1,16 @@
|
||||
import { ReactTestRenderer } from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { replaceHistoryLocation } from "talk-framework/testHelpers";
|
||||
import { replaceHistoryLocation, wait } from "talk-framework/testHelpers";
|
||||
|
||||
import create from "./create";
|
||||
import create from "../create";
|
||||
import {
|
||||
emptyModerationQueues,
|
||||
emptyRejectedComments,
|
||||
settings,
|
||||
} from "./fixtures";
|
||||
} from "../fixtures";
|
||||
|
||||
function createTestRenderer(): {
|
||||
testRenderer: ReactTestRenderer;
|
||||
@@ -31,6 +30,7 @@ function createTestRenderer(): {
|
||||
logNetwork: false,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(false, "loggedIn");
|
||||
localRecord.setValue("SIGN_IN", "authView");
|
||||
},
|
||||
});
|
||||
return { testRenderer, context };
|
||||
@@ -38,12 +38,13 @@ function createTestRenderer(): {
|
||||
|
||||
it("redirect when not logged in", async () => {
|
||||
const { context } = createTestRenderer();
|
||||
await timeout();
|
||||
expect(
|
||||
context.relayEnvironment
|
||||
.getStore()
|
||||
.getSource()
|
||||
.get(LOCAL_ID)!.redirectPath
|
||||
).toBe("/admin/moderate/reported");
|
||||
expect(window.location.toString()).toBe("http://localhost/admin/login");
|
||||
await wait(() => {
|
||||
expect(
|
||||
context.relayEnvironment
|
||||
.getStore()
|
||||
.getSource()
|
||||
.get(LOCAL_ID)!.redirectPath
|
||||
).toBe("/admin/moderate/reported");
|
||||
expect(window.location.toString()).toBe("http://localhost/admin/login");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
import { ReactTestRenderer } from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import {
|
||||
createAuthToken,
|
||||
replaceHistoryLocation,
|
||||
wait,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import create from "../create";
|
||||
import {
|
||||
emptyModerationQueues,
|
||||
emptyRejectedComments,
|
||||
settings,
|
||||
users,
|
||||
} from "../fixtures";
|
||||
|
||||
function createTestRenderer(
|
||||
userDiff: any = {}
|
||||
): {
|
||||
testRenderer: ReactTestRenderer;
|
||||
context: TalkContext;
|
||||
} {
|
||||
replaceHistoryLocation("http://localhost/admin/moderate/reported");
|
||||
const resolvers = {
|
||||
Query: {
|
||||
settings: sinon.stub().returns(settings),
|
||||
moderationQueues: sinon.stub().returns(emptyModerationQueues),
|
||||
comments: sinon.stub().returns(emptyRejectedComments),
|
||||
me: sinon.stub().returns({ ...users[0], ...userDiff }),
|
||||
},
|
||||
};
|
||||
const { testRenderer, context } = create({
|
||||
resolvers,
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
localRecord.setValue(createAuthToken(), "authToken");
|
||||
localRecord.setValue("SIGN_IN", "authView");
|
||||
},
|
||||
});
|
||||
return { testRenderer, context };
|
||||
}
|
||||
|
||||
it("show restricted screen for commenters", async () => {
|
||||
const { testRenderer } = createTestRenderer({ role: "COMMENTER" });
|
||||
const authBox = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("authBox")
|
||||
);
|
||||
expect(within(authBox).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("show restricted screen when email is not set", async () => {
|
||||
const { testRenderer } = createTestRenderer({ email: "" });
|
||||
await waitForElement(() => within(testRenderer.root).getByTestID("authBox"));
|
||||
});
|
||||
|
||||
it("show restricted screen when username is not set", async () => {
|
||||
const { testRenderer } = createTestRenderer({ username: "" });
|
||||
await waitForElement(() => within(testRenderer.root).getByTestID("authBox"));
|
||||
});
|
||||
|
||||
it("show restricted screen local was not set (password)", async () => {
|
||||
const { testRenderer } = createTestRenderer({ profiles: [] });
|
||||
await waitForElement(() => within(testRenderer.root).getByTestID("authBox"));
|
||||
});
|
||||
|
||||
it("sign out when clicking on sign in as", async () => {
|
||||
const { context, testRenderer } = createTestRenderer({ role: "COMMENTER" });
|
||||
const authBox = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("authBox")
|
||||
);
|
||||
|
||||
const restMock = sinon.mock(context.rest);
|
||||
restMock
|
||||
.expects("fetch")
|
||||
.withArgs("/tenant/auth", {
|
||||
method: "DELETE",
|
||||
})
|
||||
.once()
|
||||
.returns({});
|
||||
|
||||
within(authBox)
|
||||
.getByText("Sign in with a different account")
|
||||
.props.onClick();
|
||||
|
||||
await wait(() => {
|
||||
expect(
|
||||
context.relayEnvironment
|
||||
.getStore()
|
||||
.getSource()
|
||||
.get(LOCAL_ID)!.redirectPath
|
||||
).toBe("/admin/moderate/reported");
|
||||
});
|
||||
|
||||
expect(
|
||||
context.relayEnvironment
|
||||
.getStore()
|
||||
.getSource()
|
||||
.get(LOCAL_ID)!.loggedIn
|
||||
).toBeFalsy();
|
||||
});
|
||||
+48
-44
@@ -1,21 +1,20 @@
|
||||
import { ReactTestInstance, ReactTestRenderer } from "react-test-renderer";
|
||||
import { ReactTestInstance } from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import {
|
||||
createAuthToken,
|
||||
replaceHistoryLocation,
|
||||
wait,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import create from "./create";
|
||||
import create from "../create";
|
||||
import {
|
||||
emptyModerationQueues,
|
||||
emptyRejectedComments,
|
||||
settings,
|
||||
} from "./fixtures";
|
||||
} from "../fixtures";
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
@@ -29,65 +28,71 @@ const inputPredicate = (name: string) => (n: ReactTestInstance) => {
|
||||
return n.props.name === name && n.props.onChange;
|
||||
};
|
||||
|
||||
let context: TalkContext;
|
||||
let testRenderer: ReactTestRenderer;
|
||||
let form: ReactTestInstance;
|
||||
beforeEach(async () => {
|
||||
async function createTestRenderer() {
|
||||
// deliberately setting to a different route,
|
||||
// it should be smart enough to reroute to /admin/login.
|
||||
replaceHistoryLocation("http://localhost/admin/moderate");
|
||||
|
||||
({ testRenderer, context } = create({
|
||||
const { testRenderer, context } = create({
|
||||
resolvers,
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(false, "loggedIn");
|
||||
localRecord.setValue("SIGN_IN", "authView");
|
||||
},
|
||||
}));
|
||||
await timeout();
|
||||
form = testRenderer.root.findByType("form");
|
||||
});
|
||||
});
|
||||
const form = await waitForElement(() =>
|
||||
within(testRenderer.root).getByType("form")
|
||||
);
|
||||
return { testRenderer, form, context };
|
||||
}
|
||||
|
||||
it("renders sign in form", async () => {
|
||||
const { testRenderer } = await createTestRenderer();
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows error when submitting empty form", async () => {
|
||||
const { form } = await createTestRenderer();
|
||||
form.props.onSubmit();
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
expect(within(form).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("checks for invalid email", async () => {
|
||||
form
|
||||
.find(inputPredicate("email"))
|
||||
const { form } = await createTestRenderer();
|
||||
within(form)
|
||||
.getByLabelText("Email Address")
|
||||
.props.onChange({ target: { value: "invalid-email" } });
|
||||
form.props.onSubmit();
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
expect(within(form).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("accepts valid email", async () => {
|
||||
form
|
||||
.find(inputPredicate("email"))
|
||||
const { form } = await createTestRenderer();
|
||||
within(form)
|
||||
.getByLabelText("Email Address")
|
||||
.props.onChange({ target: { value: "hans@test.com" } });
|
||||
form.props.onSubmit();
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
expect(within(form).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("accepts correct password", async () => {
|
||||
form
|
||||
.find(inputPredicate("password"))
|
||||
const { form } = await createTestRenderer();
|
||||
within(form)
|
||||
.getByLabelText("Password")
|
||||
.props.onChange({ target: { value: "testtest" } });
|
||||
form.props.onSubmit();
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
expect(within(form).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows server error", async () => {
|
||||
form
|
||||
.find(inputPredicate("email"))
|
||||
const { form, context } = await createTestRenderer();
|
||||
within(form)
|
||||
.getByLabelText("Email Address")
|
||||
.props.onChange({ target: { value: "hans@test.com" } });
|
||||
form
|
||||
.find(inputPredicate("password"))
|
||||
within(form)
|
||||
.getByLabelText("Password")
|
||||
.props.onChange({ target: { value: "testtest" } });
|
||||
|
||||
const error = new Error("Server Error");
|
||||
@@ -105,13 +110,17 @@ it("shows server error", async () => {
|
||||
.throws(error);
|
||||
|
||||
form.props.onSubmit();
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
await timeout();
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
const submitButton = within(form).getByText("Sign in with Email", {
|
||||
selector: "button",
|
||||
});
|
||||
expect(submitButton.props.disabled).toBe(true);
|
||||
await wait(() => expect(submitButton.props.disabled).toBe(false));
|
||||
expect(within(form).toJSON()).toMatchSnapshot();
|
||||
restMock.verify();
|
||||
});
|
||||
|
||||
it("submits form successfully", async () => {
|
||||
const { form, context } = await createTestRenderer();
|
||||
form
|
||||
.find(inputPredicate("email"))
|
||||
.props.onChange({ target: { value: "hans@test.com" } });
|
||||
@@ -137,17 +146,12 @@ it("submits form successfully", async () => {
|
||||
const historyMock = sinon.mock(window.history);
|
||||
|
||||
form.props.onSubmit();
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
await timeout();
|
||||
expect(testRenderer.toJSON()).toMatchSnapshot();
|
||||
const submitButton = within(form).getByText("Sign in with Email", {
|
||||
selector: "button",
|
||||
});
|
||||
expect(submitButton.props.disabled).toBe(true);
|
||||
await wait(() => expect(submitButton.props.disabled).toBe(false));
|
||||
expect(location.toString()).toMatchSnapshot();
|
||||
restMock.verify();
|
||||
await timeout();
|
||||
expect(
|
||||
context.relayEnvironment
|
||||
.getStore()
|
||||
.getSource()
|
||||
.get(LOCAL_ID)!.loggedIn
|
||||
).toBeTruthy();
|
||||
await timeout();
|
||||
historyMock.verify();
|
||||
});
|
||||
+22
-16
@@ -1,21 +1,27 @@
|
||||
import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { replaceHistoryLocation } from "talk-framework/testHelpers";
|
||||
import {
|
||||
replaceHistoryLocation,
|
||||
wait,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import create from "./create";
|
||||
import create from "../create";
|
||||
import {
|
||||
emptyModerationQueues,
|
||||
emptyRejectedComments,
|
||||
settings,
|
||||
} from "./fixtures";
|
||||
users,
|
||||
} from "../fixtures";
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
settings: sinon.stub().returns(settings),
|
||||
moderationQueues: sinon.stub().returns(emptyModerationQueues),
|
||||
comments: sinon.stub().returns(emptyRejectedComments),
|
||||
me: sinon.stub().returns(users[0]),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -40,17 +46,17 @@ it("logs out", async () => {
|
||||
.once()
|
||||
.returns({});
|
||||
|
||||
await timeout();
|
||||
testRenderer.root
|
||||
.find(i => i.props.id === "navigation-signOutButton" && i.props.onClick)
|
||||
.props.onClick();
|
||||
const signOutButton = await waitForElement(() =>
|
||||
within(testRenderer.root).getByText("Sign Out")
|
||||
);
|
||||
signOutButton.props.onClick();
|
||||
|
||||
await timeout();
|
||||
|
||||
expect(
|
||||
context.relayEnvironment
|
||||
.getStore()
|
||||
.getSource()
|
||||
.get(LOCAL_ID)!.loggedIn
|
||||
).toBeFalsy();
|
||||
await wait(() => {
|
||||
expect(
|
||||
context.relayEnvironment
|
||||
.getStore()
|
||||
.getSource()
|
||||
.get(LOCAL_ID)!.loggedIn
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -1703,6 +1703,7 @@ exports[`renders configure auth 1`] = `
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-double"
|
||||
data-testid="configure-authContainer"
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-oneAndAHalf"
|
||||
|
||||
@@ -7,10 +7,12 @@ import {
|
||||
inputPredicate,
|
||||
limitSnapshotTo,
|
||||
replaceHistoryLocation,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import create from "../create";
|
||||
import { settings } from "../fixtures";
|
||||
import { settingsWithEmptyAuth, users } from "../fixtures";
|
||||
|
||||
beforeEach(async () => {
|
||||
replaceHistoryLocation("http://localhost/admin/configure/auth");
|
||||
@@ -23,7 +25,10 @@ const createTestRenderer = async (resolver: any = {}) => {
|
||||
...resolver.Query,
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(resolver, "Query.settings"))),
|
||||
.returns(
|
||||
merge({}, settingsWithEmptyAuth, get(resolver, "Query.settings"))
|
||||
),
|
||||
me: sinon.stub().returns(users[0]),
|
||||
},
|
||||
};
|
||||
const { testRenderer } = create({
|
||||
@@ -34,24 +39,27 @@ const createTestRenderer = async (resolver: any = {}) => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
},
|
||||
});
|
||||
await timeout();
|
||||
return testRenderer;
|
||||
const configureContainer = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("configure-container")
|
||||
);
|
||||
const authContainer = await waitForElement(() =>
|
||||
within(configureContainer).getByTestID("configure-authContainer")
|
||||
);
|
||||
return { testRenderer, configureContainer, authContainer };
|
||||
};
|
||||
|
||||
it("renders configure auth", async () => {
|
||||
const testRenderer = await createTestRenderer();
|
||||
expect(
|
||||
limitSnapshotTo("configure-container", testRenderer.toJSON())
|
||||
).toMatchSnapshot();
|
||||
const { configureContainer } = await createTestRenderer();
|
||||
expect(within(configureContainer).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("regenerate sso key", async () => {
|
||||
const testRenderer = await createTestRenderer({
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
Mutation: {
|
||||
regenerateSSOKey: createSinonStub(s =>
|
||||
s.callsFake((_: any, data: any) => {
|
||||
return {
|
||||
settings: merge({}, settings, {
|
||||
settings: merge({}, settingsWithEmptyAuth, {
|
||||
auth: {
|
||||
integrations: {
|
||||
sso: {
|
||||
@@ -83,7 +91,7 @@ it("regenerate sso key", async () => {
|
||||
});
|
||||
|
||||
it("prevents admin lock out", async () => {
|
||||
const testRenderer = await createTestRenderer();
|
||||
const { testRenderer } = await createTestRenderer();
|
||||
|
||||
// Let's disable local auth.
|
||||
testRenderer.root
|
||||
@@ -99,8 +107,8 @@ it("prevents admin lock out", async () => {
|
||||
});
|
||||
|
||||
it("prevents stream lock out", async () => {
|
||||
let settingsRecord = cloneDeep(settings);
|
||||
const testRenderer = await createTestRenderer({
|
||||
let settingsRecord = cloneDeep(settingsWithEmptyAuth);
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
Mutation: {
|
||||
updateSettings: createSinonStub(s =>
|
||||
s.callsFake((_: any, data: any) => {
|
||||
@@ -159,8 +167,8 @@ it("prevents stream lock out", async () => {
|
||||
});
|
||||
|
||||
it("change settings", async () => {
|
||||
let settingsRecord = cloneDeep(settings);
|
||||
const testRenderer = await createTestRenderer({
|
||||
let settingsRecord = cloneDeep(settingsWithEmptyAuth);
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
Query: {
|
||||
discoverOIDCConfiguration: createSinonStub(s =>
|
||||
s.callsFake((_: any, data: any) => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { IResolvers } from "graphql-tools";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
import TestRenderer from "react-test-renderer";
|
||||
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
|
||||
@@ -19,6 +18,7 @@ import createNodeMock from "./createNodeMock";
|
||||
interface CreateParams {
|
||||
logNetwork?: boolean;
|
||||
resolvers?: IResolvers<any, any>;
|
||||
muteNetworkErrors?: boolean;
|
||||
initLocalState?: (
|
||||
local: RecordProxy,
|
||||
source: RecordSourceProxy,
|
||||
@@ -31,6 +31,7 @@ export default function create(params: CreateParams) {
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: params.logNetwork,
|
||||
resolvers: params.resolvers,
|
||||
muteNetworkErrors: params.muteNetworkErrors,
|
||||
initLocalState: (localRecord, source, env) => {
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, env);
|
||||
@@ -48,7 +49,7 @@ export default function create(params: CreateParams) {
|
||||
browserInfo: { ios: false },
|
||||
uuidGenerator: createUUIDGenerator(),
|
||||
eventEmitter: new EventEmitter2({ wildcard: true, maxListeners: 20 }),
|
||||
clearSession: noop,
|
||||
clearSession: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
interface CreateEnvironmentParams {
|
||||
logNetwork?: boolean;
|
||||
resolvers?: IResolvers<any, any>;
|
||||
muteNetworkErrors?: boolean;
|
||||
initLocalState?: (
|
||||
local: RecordProxy,
|
||||
source: RecordSourceProxy,
|
||||
@@ -16,6 +17,7 @@ export default function createEnvironment(params: CreateEnvironmentParams) {
|
||||
return createRelayEnvironment({
|
||||
network: {
|
||||
logNetwork: params.logNetwork,
|
||||
muteNetworkErrors: params.muteNetworkErrors,
|
||||
resolvers: params.resolvers || {},
|
||||
projectName: "tenant",
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import create from "../create";
|
||||
import { moderationActions, settings } from "../fixtures";
|
||||
import { moderationActions, settings, users } from "../fixtures";
|
||||
|
||||
beforeEach(async () => {
|
||||
replaceHistoryLocation("http://localhost/admin/configure/auth");
|
||||
@@ -62,7 +62,7 @@ const createTestRenderer = async (resolver: any = {}) => {
|
||||
Query: {
|
||||
...resolver.Query,
|
||||
me: sinon.stub().returns({
|
||||
id: "me",
|
||||
...users[0],
|
||||
commentModerationActionHistory,
|
||||
}),
|
||||
settings: sinon
|
||||
|
||||
@@ -39,6 +39,7 @@ export const settings = {
|
||||
clientID: "",
|
||||
clientSecret: "",
|
||||
callbackURL: "http://localhost/google/callback",
|
||||
redirectURL: "http://localhost/google",
|
||||
},
|
||||
facebook: {
|
||||
enabled: false,
|
||||
@@ -50,6 +51,7 @@ export const settings = {
|
||||
clientID: "",
|
||||
clientSecret: "",
|
||||
callbackURL: "http://localhost/facebook/callback",
|
||||
redirectURL: "http://localhost/facebook",
|
||||
},
|
||||
oidc: {
|
||||
enabled: false,
|
||||
@@ -58,7 +60,74 @@ export const settings = {
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
name: "OIDC",
|
||||
callbackURL: "http://localhost/oidc/callback",
|
||||
redirectURL: "http://localhost/oidc",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const settingsWithEmptyAuth = {
|
||||
...settings,
|
||||
id: "settings",
|
||||
auth: {
|
||||
displayName: {
|
||||
enabled: false,
|
||||
},
|
||||
integrations: {
|
||||
local: {
|
||||
enabled: true,
|
||||
allowRegistration: true,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
},
|
||||
sso: {
|
||||
enabled: false,
|
||||
allowRegistration: true,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
key: null,
|
||||
keyGeneratedAt: null,
|
||||
},
|
||||
google: {
|
||||
enabled: false,
|
||||
allowRegistration: true,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
clientID: "",
|
||||
clientSecret: "",
|
||||
callbackURL: "http://localhost/google/callback",
|
||||
redirectURL: "http://localhost/google",
|
||||
},
|
||||
facebook: {
|
||||
enabled: false,
|
||||
allowRegistration: true,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
clientID: "",
|
||||
clientSecret: "",
|
||||
callbackURL: "http://localhost/facebook/callback",
|
||||
redirectURL: "http://localhost/facebook",
|
||||
},
|
||||
oidc: {
|
||||
enabled: false,
|
||||
allowRegistration: false,
|
||||
targetFilter: {
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
name: "",
|
||||
callbackURL: "http://localhost/oidc/callback",
|
||||
redirectURL: "http://localhost/oidc",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -151,14 +220,23 @@ export const users = [
|
||||
{
|
||||
id: "user-0",
|
||||
username: "Markus",
|
||||
email: "markus@test.com",
|
||||
role: "ADMIN",
|
||||
profiles: [{ __typename: "LocalProfile" }],
|
||||
},
|
||||
{
|
||||
id: "user-1",
|
||||
username: "Lukas",
|
||||
email: "lukas@test.com",
|
||||
role: "MODERATOR",
|
||||
profiles: [{ __typename: "LocalProfile" }],
|
||||
},
|
||||
{
|
||||
id: "user-2",
|
||||
username: "Isabelle",
|
||||
email: "isabelle@test.com",
|
||||
role: "COMMENTER",
|
||||
profiles: [{ __typename: "LocalProfile" }],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
rejectedComments,
|
||||
reportedComments,
|
||||
settings,
|
||||
users,
|
||||
} from "../fixtures";
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -43,6 +44,7 @@ const createTestRenderer = async (resolver: any = {}) => {
|
||||
comments:
|
||||
get(resolver, "Query.comments") ||
|
||||
sinon.stub().returns(emptyRejectedComments),
|
||||
me: sinon.stub().returns(users[0]),
|
||||
},
|
||||
};
|
||||
const { testRenderer } = create({
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.lockIcon {
|
||||
color: var(--palette-grey-dark);
|
||||
}
|
||||
.noPermission {
|
||||
color: var(--palette-grey-dark);
|
||||
}
|
||||
.contactAdmin {
|
||||
color: var(--palette-grey-main);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import Restricted from "./Restricted";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof Restricted> = {
|
||||
username: "User",
|
||||
onSignInAs: noop,
|
||||
};
|
||||
const renderer = createRenderer();
|
||||
|
||||
renderer.render(<Restricted {...props} />);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import AuthBox from "talk-admin/components/AuthBox";
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
Icon,
|
||||
Typography,
|
||||
} from "talk-ui/components";
|
||||
|
||||
import styles from "./Restricted.css";
|
||||
|
||||
interface Props {
|
||||
username: string;
|
||||
onSignInAs: React.MouseEventHandler;
|
||||
}
|
||||
|
||||
const SignIn: StatelessComponent<Props> = ({ username, onSignInAs }) => {
|
||||
const Username = () => (
|
||||
<Typography variant="heading1" align="center">
|
||||
{username}
|
||||
</Typography>
|
||||
);
|
||||
return (
|
||||
<AuthBox
|
||||
title={
|
||||
<Localized id="restricted-currentlySignedInTo">
|
||||
<span>Currently signed in to</span>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<HorizontalGutter size="double">
|
||||
<div>
|
||||
<Flex justifyContent="center">
|
||||
<Icon size="lg" className={styles.lockIcon}>
|
||||
lock
|
||||
</Icon>
|
||||
</Flex>
|
||||
<Localized id="restricted-noPermissionInfo">
|
||||
<Typography
|
||||
variant="heading3"
|
||||
align="center"
|
||||
className={styles.noPermission}
|
||||
>
|
||||
You do not have permission to access this page.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
<div>
|
||||
<Localized id="restricted-signedInAs" username={<Username />}>
|
||||
<Typography variant="bodyCopy" align="center" container="div">
|
||||
{"You are signed in as: <username></username>"}
|
||||
</Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
<Flex justifyContent="center">
|
||||
<Localized id="restricted-signInWithADifferentAccount">
|
||||
<Button variant="filled" color="primary" onClick={onSignInAs}>
|
||||
Sign in with a different account
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
<Localized id="restricted-contactAdmin">
|
||||
<Typography
|
||||
variant="bodyCopy"
|
||||
align="center"
|
||||
className={styles.contactAdmin}
|
||||
>
|
||||
If you think this is an error, please contact your administrator for
|
||||
assistance.
|
||||
</Typography>
|
||||
</Localized>
|
||||
</HorizontalGutter>
|
||||
</AuthBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<AuthBox
|
||||
title={
|
||||
<Localized
|
||||
id="restricted-currentlySignedInTo"
|
||||
>
|
||||
<span>
|
||||
Currently signed in to
|
||||
</span>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
size="double"
|
||||
>
|
||||
<div>
|
||||
<withPropsOnChange(Flex)
|
||||
justifyContent="center"
|
||||
>
|
||||
<withPropsOnChange(Icon)
|
||||
className="Restricted-lockIcon"
|
||||
size="lg"
|
||||
>
|
||||
lock
|
||||
</withPropsOnChange(Icon)>
|
||||
</withPropsOnChange(Flex)>
|
||||
<Localized
|
||||
id="restricted-noPermissionInfo"
|
||||
>
|
||||
<withPropsOnChange(Typography)
|
||||
align="center"
|
||||
className="Restricted-noPermission"
|
||||
variant="heading3"
|
||||
>
|
||||
You do not have permission to access this page.
|
||||
</withPropsOnChange(Typography)>
|
||||
</Localized>
|
||||
</div>
|
||||
<div>
|
||||
<Localized
|
||||
id="restricted-signedInAs"
|
||||
username={<Username />}
|
||||
>
|
||||
<withPropsOnChange(Typography)
|
||||
align="center"
|
||||
container="div"
|
||||
variant="bodyCopy"
|
||||
>
|
||||
You are signed in as: <username></username>
|
||||
</withPropsOnChange(Typography)>
|
||||
</Localized>
|
||||
</div>
|
||||
<withPropsOnChange(Flex)
|
||||
justifyContent="center"
|
||||
>
|
||||
<Localized
|
||||
id="restricted-signInWithADifferentAccount"
|
||||
>
|
||||
<withPropsOnChange(Button)
|
||||
color="primary"
|
||||
onClick={[Function]}
|
||||
variant="filled"
|
||||
>
|
||||
Sign in with a different account
|
||||
</withPropsOnChange(Button)>
|
||||
</Localized>
|
||||
</withPropsOnChange(Flex)>
|
||||
<Localized
|
||||
id="restricted-contactAdmin"
|
||||
>
|
||||
<withPropsOnChange(Typography)
|
||||
align="center"
|
||||
className="Restricted-contactAdmin"
|
||||
variant="bodyCopy"
|
||||
>
|
||||
If you think this is an error, please contact your administrator for assistance.
|
||||
</withPropsOnChange(Typography)>
|
||||
</Localized>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
</AuthBox>
|
||||
`;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { RouteProps } from "found";
|
||||
import React, { Component } from "react";
|
||||
|
||||
import { RestrictedContainer_me as MeData } from "talk-admin/__generated__/RestrictedContainer_me.graphql";
|
||||
import {
|
||||
SetRedirectPathMutation,
|
||||
withSetRedirectPathMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import { graphql, withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { SignOutMutation, withSignOutMutation } from "talk-framework/mutations";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import Restricted from "../components/Restricted";
|
||||
|
||||
interface Props {
|
||||
me: MeData;
|
||||
error?: Error | null;
|
||||
signOut: SignOutMutation;
|
||||
setRedirectPath: SetRedirectPathMutation;
|
||||
}
|
||||
|
||||
class RestrictedContainer extends Component<Props> {
|
||||
public static routeConfig: RouteProps;
|
||||
|
||||
private handleSignInAs = async () => {
|
||||
await this.props.signOut();
|
||||
// Wait for new context to propagate.
|
||||
await timeout();
|
||||
this.props.setRedirectPath({
|
||||
path: location.pathname + location.search + location.hash,
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
if (!this.props.me) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Restricted
|
||||
username={this.props.me.username!}
|
||||
onSignInAs={this.handleSignInAs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
me: graphql`
|
||||
fragment RestrictedContainer_me on User {
|
||||
username
|
||||
}
|
||||
`,
|
||||
})(withSetRedirectPathMutation(withSignOutMutation(RestrictedContainer)));
|
||||
|
||||
export default enhanced;
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { createRenderer } from "react-test-renderer/shallow";
|
||||
|
||||
import Login from "./Login";
|
||||
import HorizontalSeparator from "./HorizontalSeparator";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const renderer = createRenderer();
|
||||
renderer.render(<Login />);
|
||||
renderer.render(<HorizontalSeparator>Or</HorizontalSeparator>);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { Flex } from "talk-ui/components";
|
||||
|
||||
import styles from "./HorizontalSeparator.css";
|
||||
|
||||
interface Props {
|
||||
children: string;
|
||||
}
|
||||
|
||||
const HorizontalSeparator: StatelessComponent<Props> = props => (
|
||||
<Flex className={styles.root} alignItems="center" justifyContent="center">
|
||||
<hr className={styles.hr} />
|
||||
<div className={styles.text}>{props.children}</div>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
export default HorizontalSeparator;
|
||||
@@ -2,18 +2,12 @@ import { Localized } from "fluent-react/compat";
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
|
||||
import { Flex } from "talk-ui/components";
|
||||
import HorizontalSeparator from "./HorizontalSeparator";
|
||||
|
||||
import styles from "./OrSeparator.css";
|
||||
|
||||
const OrSeparator: StatelessComponent = props => (
|
||||
<Flex className={styles.root} alignItems="center" justifyContent="center">
|
||||
<hr className={styles.hr} />
|
||||
|
||||
<Localized id="general-orSeparator">
|
||||
<div className={styles.text}>Or</div>
|
||||
</Localized>
|
||||
</Flex>
|
||||
const OrSeparator: StatelessComponent = () => (
|
||||
<Localized id="general-orSeparator">
|
||||
<HorizontalSeparator>Or</HorizontalSeparator>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export default OrSeparator;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<withPropsOnChange(Flex)
|
||||
alignItems="center"
|
||||
className="HorizontalSeparator-root"
|
||||
justifyContent="center"
|
||||
>
|
||||
<hr
|
||||
className="HorizontalSeparator-hr"
|
||||
/>
|
||||
<div
|
||||
className="HorizontalSeparator-text"
|
||||
>
|
||||
Or
|
||||
</div>
|
||||
</withPropsOnChange(Flex)>
|
||||
`;
|
||||
@@ -37,11 +37,15 @@ function handleAccountCompletion(props: Props) {
|
||||
if (view !== "ADD_EMAIL_ADDRESS") {
|
||||
setView({ view: "ADD_EMAIL_ADDRESS" });
|
||||
}
|
||||
} else if (!me.username) {
|
||||
return false;
|
||||
}
|
||||
if (!me.username) {
|
||||
if (view !== "CREATE_USERNAME") {
|
||||
setView({ view: "CREATE_USERNAME" });
|
||||
}
|
||||
} else if (
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!me.profiles.some(p => p.__typename === "LocalProfile") &&
|
||||
auth.integrations.local.enabled &&
|
||||
auth.integrations.local.targetFilter.stream
|
||||
@@ -49,10 +53,10 @@ function handleAccountCompletion(props: Props) {
|
||||
if (view !== "CREATE_PASSWORD") {
|
||||
setView({ view: "CREATE_PASSWORD" });
|
||||
}
|
||||
} else {
|
||||
completeAccount({ authToken: authToken! });
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
completeAccount({ authToken: authToken! });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user