diff --git a/src/core/build/createWebpackConfig.ts b/src/core/build/createWebpackConfig.ts index cb0ebe804..847d65772 100644 --- a/src/core/build/createWebpackConfig.ts +++ b/src/core/build/createWebpackConfig.ts @@ -288,7 +288,7 @@ export default function createWebpackConfig({ // smaller than specified limit in bytes as data URLs to avoid requests. // A missing `test` is equivalent to a match. { - test: [/\.gif$/, /\.jpe?g$/, /\.png$/], + test: [/\.gif$/, /\.jpe?g$/, /\.png$/, /\.svg$/], loader: require.resolve("url-loader"), options: { limit: 10000, diff --git a/src/core/client/admin/components/App.css b/src/core/client/admin/components/App.css index 47d40f6b0..d24beb684 100644 --- a/src/core/client/admin/components/App.css +++ b/src/core/client/admin/components/App.css @@ -1,4 +1,13 @@ -.root { - width: 1080px; +:global { + body { + padding: 0; + margin: 0; + } +} + +.container { + max-width: 1280px; margin: 0 auto; + padding: 0 var(--spacing-unit); + box-sizing: border-box; } diff --git a/src/core/client/admin/components/App.spec.tsx b/src/core/client/admin/components/App.spec.tsx new file mode 100644 index 000000000..1a1c6f411 --- /dev/null +++ b/src/core/client/admin/components/App.spec.tsx @@ -0,0 +1,14 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import App from "./App"; + +it("renders correctly", () => { + const props: PropTypesOf = { + children: "child", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/components/App.tsx b/src/core/client/admin/components/App.tsx index e35ebc9af..0f9184263 100644 --- a/src/core/client/admin/components/App.tsx +++ b/src/core/client/admin/components/App.tsx @@ -1,13 +1,16 @@ import React, { StatelessComponent } from "react"; -import { HorizontalGutter } from "talk-ui/components"; + import styles from "./App.css"; +import AppBar from "./AppBar"; import Navigation from "./Navigation"; const App: StatelessComponent = ({ children }) => ( - - - {children} - +
+ + + +
{children}
+
); export default App; diff --git a/src/core/client/admin/components/AppBar.css b/src/core/client/admin/components/AppBar.css new file mode 100644 index 000000000..effb9c43b --- /dev/null +++ b/src/core/client/admin/components/AppBar.css @@ -0,0 +1,16 @@ +.root { + background-color: #f5f5f5; + border-bottom: 1px solid var(--palette-grey-lighter); +} + +.container { + max-width: 1280px; + margin: 0 auto; + padding: 0 calc(2 * var(--spacing-unit)); + height: calc(6 * var(--spacing-unit)); + box-sizing: border-box; +} + +.logo { + margin-right: calc(2.5 * var(--spacing-unit)); +} diff --git a/src/core/client/admin/components/AppBar.spec.tsx b/src/core/client/admin/components/AppBar.spec.tsx new file mode 100644 index 000000000..1853ac189 --- /dev/null +++ b/src/core/client/admin/components/AppBar.spec.tsx @@ -0,0 +1,14 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import AppBar from "./AppBar"; + +it("renders correctly", () => { + const props: PropTypesOf = { + children: "child", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/components/AppBar.tsx b/src/core/client/admin/components/AppBar.tsx new file mode 100644 index 000000000..af22aa902 --- /dev/null +++ b/src/core/client/admin/components/AppBar.tsx @@ -0,0 +1,17 @@ +import React, { StatelessComponent } from "react"; + +import { Flex } from "talk-ui/components"; + +import styles from "./AppBar.css"; +import Logo from "./Logo"; + +const AppBar: StatelessComponent = ({ children }) => ( +
+ + + {children} + +
+); + +export default AppBar; diff --git a/src/core/client/admin/components/BrandIcon.css b/src/core/client/admin/components/BrandIcon.css new file mode 100644 index 000000000..32aa5aca0 --- /dev/null +++ b/src/core/client/admin/components/BrandIcon.css @@ -0,0 +1,12 @@ +.base { +} + +.md { + height: 35px; + width: 36px; +} + +.lg { + height: 58px; + width: 59px; +} diff --git a/src/core/client/admin/components/BrandIcon.spec.tsx b/src/core/client/admin/components/BrandIcon.spec.tsx new file mode 100644 index 000000000..26993b470 --- /dev/null +++ b/src/core/client/admin/components/BrandIcon.spec.tsx @@ -0,0 +1,15 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import BrandIcon from "./BrandIcon"; + +it("renders correctly", () => { + const props: PropTypesOf = { + className: "custom", + size: "lg", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/components/BrandIcon.tsx b/src/core/client/admin/components/BrandIcon.tsx new file mode 100644 index 000000000..511354514 --- /dev/null +++ b/src/core/client/admin/components/BrandIcon.tsx @@ -0,0 +1,45 @@ +import cn from "classnames"; +import React, { StatelessComponent } from "react"; + +import styles from "./BrandIcon.css"; + +interface Props { + className?: string; + size?: "md" | "lg"; +} + +const BrandIcon: StatelessComponent = props => ( + + + + + + + + + + logo mark2018 + + + + +); +BrandIcon.defaultProps = { + size: "md", +}; + +export default BrandIcon; diff --git a/src/core/client/admin/components/BrandName.css b/src/core/client/admin/components/BrandName.css new file mode 100644 index 000000000..d02986f0c --- /dev/null +++ b/src/core/client/admin/components/BrandName.css @@ -0,0 +1,15 @@ +.root { + font-weight: var(--font-weight-bold); + font-family: var(--font-family-sans-serif); + color: var(--palette-brand); + margin: 0; +} + +.md { + font-size: calc(36rem / var(--rem-base)); + line-height: calc(38em / 36); +} +.lg { + font-size: calc(48rem / var(--rem-base)); + line-height: 1; +} diff --git a/src/core/client/admin/components/BrandName.spec.tsx b/src/core/client/admin/components/BrandName.spec.tsx new file mode 100644 index 000000000..3f38c5b3e --- /dev/null +++ b/src/core/client/admin/components/BrandName.spec.tsx @@ -0,0 +1,16 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import BrandName from "./BrandName"; + +it("renders correctly", () => { + const props: PropTypesOf = { + align: "center", + className: "custom", + size: "lg", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/components/BrandName.tsx b/src/core/client/admin/components/BrandName.tsx new file mode 100644 index 000000000..56cb03dbc --- /dev/null +++ b/src/core/client/admin/components/BrandName.tsx @@ -0,0 +1,36 @@ +import cn from "classnames"; +import { Localized } from "fluent-react/compat"; +import React, { StatelessComponent } from "react"; + +import { PropTypesOf } from "talk-framework/types"; +import { Typography } from "talk-ui/components"; + +import styles from "./BrandName.css"; + +interface Props { + align?: PropTypesOf["align"]; + className?: string; + size?: "md" | "lg"; +} + +const BrandName: StatelessComponent = props => ( + + + Talk + + +); + +BrandName.defaultProps = { + size: "md", + align: "center", +}; + +export default BrandName; diff --git a/src/core/client/admin/components/Logo.css b/src/core/client/admin/components/Logo.css new file mode 100644 index 000000000..18643cc2a --- /dev/null +++ b/src/core/client/admin/components/Logo.css @@ -0,0 +1,6 @@ +.root { +} + +.icon { + margin-right: var(--spacing-unit); +} diff --git a/src/core/client/admin/components/Logo.spec.tsx b/src/core/client/admin/components/Logo.spec.tsx new file mode 100644 index 000000000..74fec6784 --- /dev/null +++ b/src/core/client/admin/components/Logo.spec.tsx @@ -0,0 +1,14 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import Logo from "./Logo"; + +it("renders correctly", () => { + const props: PropTypesOf = { + className: "custom", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/components/Logo.tsx b/src/core/client/admin/components/Logo.tsx new file mode 100644 index 000000000..09033496d --- /dev/null +++ b/src/core/client/admin/components/Logo.tsx @@ -0,0 +1,21 @@ +import cn from "classnames"; +import React, { StatelessComponent } from "react"; + +import { Flex } from "talk-ui/components"; + +import BrandIcon from "./BrandIcon"; +import BrandName from "./BrandName"; +import styles from "./Logo.css"; + +interface Props { + className?: string; +} + +const Logo: StatelessComponent = props => ( + + + + +); + +export default Logo; diff --git a/src/core/client/admin/components/Navigation.css b/src/core/client/admin/components/Navigation.css index c41c8523c..fd02200e2 100644 --- a/src/core/client/admin/components/Navigation.css +++ b/src/core/client/admin/components/Navigation.css @@ -1,7 +1,28 @@ +.root { + height: 100%; + width: 100%; +} + .link { + color: var(--palette-text-secondary); + font-family: var(--font-family-sans-serif); + font-weight: var(--font-weight-bold); + font-size: calc(18rem / var(--rem-base)); + line-height: calc(20em / 18); + letter-spacing: calc(-0.1em / 18); + height: 100%; + display: inline-flex; + align-items: center; + padding: 0 calc(1.5 * var(--spacing-unit)); + text-transform: uppercase; text-decoration: none; + &:hover { + cursor: pointer; + } } .active { - border-bottom: 3px solid var(--palette-primary-main); + background-color: var(--palette-brand); + text-decoration: none; + color: var(--palette-text-light); } diff --git a/src/core/client/admin/components/Navigation.spec.tsx b/src/core/client/admin/components/Navigation.spec.tsx new file mode 100644 index 000000000..49038dc6d --- /dev/null +++ b/src/core/client/admin/components/Navigation.spec.tsx @@ -0,0 +1,9 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import Navigation from "./Navigation"; + +it("renders correctly", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/components/Navigation.tsx b/src/core/client/admin/components/Navigation.tsx index f440ac418..18112c2af 100644 --- a/src/core/client/admin/components/Navigation.tsx +++ b/src/core/client/admin/components/Navigation.tsx @@ -1,39 +1,31 @@ -import { Link } from "found"; +import { Localized } from "fluent-react/compat"; import React, { StatelessComponent } from "react"; -import { Button, Flex, Typography } from "talk-ui/components"; + +import { Flex } from "talk-ui/components"; + +import SignOutButtonContainer from "../containers/SignOutButtonContainer"; import styles from "./Navigation.css"; +import NavigationLink from "./NavigationLink"; const Navigation: StatelessComponent = () => ( - - Talk - - - - - - - - - - - - + + + + Moderate + + + Community + + + Stories + + + Configure + + + + + ); diff --git a/src/core/client/admin/components/NavigationLink.css b/src/core/client/admin/components/NavigationLink.css new file mode 100644 index 000000000..80cd64d7d --- /dev/null +++ b/src/core/client/admin/components/NavigationLink.css @@ -0,0 +1,23 @@ +.root { + color: var(--palette-text-secondary); + font-family: var(--font-family-sans-serif); + font-weight: var(--font-weight-bold); + font-size: calc(18rem / var(--rem-base)); + line-height: calc(20em / 18); + letter-spacing: calc(-0.1em / 18); + height: 100%; + display: inline-flex; + align-items: center; + padding: 0 calc(1.5 * var(--spacing-unit)); + text-transform: uppercase; + text-decoration: none; + &:hover { + cursor: pointer; + } +} + +.active { + background-color: var(--palette-brand); + text-decoration: none; + color: var(--palette-text-light); +} diff --git a/src/core/client/admin/components/NavigationLink.spec.tsx b/src/core/client/admin/components/NavigationLink.spec.tsx new file mode 100644 index 000000000..285457c89 --- /dev/null +++ b/src/core/client/admin/components/NavigationLink.spec.tsx @@ -0,0 +1,15 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import NavigationLink from "./NavigationLink"; + +it("renders correctly", () => { + const props: PropTypesOf = { + to: "/moderate", + children: "link", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/components/NavigationLink.tsx b/src/core/client/admin/components/NavigationLink.tsx new file mode 100644 index 000000000..e91cb1d5c --- /dev/null +++ b/src/core/client/admin/components/NavigationLink.tsx @@ -0,0 +1,17 @@ +import { Link, LocationDescriptor } from "found"; +import React, { StatelessComponent } from "react"; + +import styles from "./NavigationLink.css"; + +interface Props { + children: React.ReactNode; + to: string | LocationDescriptor; +} + +const NavigationLink: StatelessComponent = props => ( + + {props.children} + +); + +export default NavigationLink; diff --git a/src/core/client/admin/components/SignOutButton.spec.tsx b/src/core/client/admin/components/SignOutButton.spec.tsx new file mode 100644 index 000000000..d5275c83a --- /dev/null +++ b/src/core/client/admin/components/SignOutButton.spec.tsx @@ -0,0 +1,16 @@ +import { shallow } from "enzyme"; +import { noop } from "lodash"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import SignOutButton from "./SignOutButton"; + +it("renders correctly", () => { + const props: PropTypesOf = { + id: "id", + onClick: noop, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/components/SignOutButton.tsx b/src/core/client/admin/components/SignOutButton.tsx new file mode 100644 index 000000000..c4f0bd8c9 --- /dev/null +++ b/src/core/client/admin/components/SignOutButton.tsx @@ -0,0 +1,19 @@ +import { Localized } from "fluent-react/compat"; +import React, { StatelessComponent } from "react"; + +import { Button } from "talk-ui/components"; + +interface Props { + id?: string; + onClick: React.EventHandler; +} + +const SignOutButton: StatelessComponent = props => ( + + + +); + +export default SignOutButton; diff --git a/src/core/client/admin/components/__snapshots__/App.spec.tsx.snap b/src/core/client/admin/components/__snapshots__/App.spec.tsx.snap new file mode 100644 index 000000000..49f6a0fc5 --- /dev/null +++ b/src/core/client/admin/components/__snapshots__/App.spec.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+ + + +
+ child +
+
+`; diff --git a/src/core/client/admin/components/__snapshots__/AppBar.spec.tsx.snap b/src/core/client/admin/components/__snapshots__/AppBar.spec.tsx.snap new file mode 100644 index 000000000..1c6eaf2d5 --- /dev/null +++ b/src/core/client/admin/components/__snapshots__/AppBar.spec.tsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+ + + child + +
+`; diff --git a/src/core/client/admin/components/__snapshots__/BrandIcon.spec.tsx.snap b/src/core/client/admin/components/__snapshots__/BrandIcon.spec.tsx.snap new file mode 100644 index 000000000..c4c3eaabd --- /dev/null +++ b/src/core/client/admin/components/__snapshots__/BrandIcon.spec.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + + + + + + + + + + logo mark2018 + + + + + +`; diff --git a/src/core/client/admin/components/__snapshots__/BrandName.spec.tsx.snap b/src/core/client/admin/components/__snapshots__/BrandName.spec.tsx.snap new file mode 100644 index 000000000..12472cf0d --- /dev/null +++ b/src/core/client/admin/components/__snapshots__/BrandName.spec.tsx.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + + Talk + + +`; diff --git a/src/core/client/admin/components/__snapshots__/Logo.spec.tsx.snap b/src/core/client/admin/components/__snapshots__/Logo.spec.tsx.snap new file mode 100644 index 000000000..0cef7c39a --- /dev/null +++ b/src/core/client/admin/components/__snapshots__/Logo.spec.tsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + + + +`; diff --git a/src/core/client/admin/components/__snapshots__/Navigation.spec.tsx.snap b/src/core/client/admin/components/__snapshots__/Navigation.spec.tsx.snap new file mode 100644 index 000000000..1d44e5052 --- /dev/null +++ b/src/core/client/admin/components/__snapshots__/Navigation.spec.tsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + + + + Moderate + + + + + Community + + + + + Stories + + + + + Configure + + + + + + + +`; diff --git a/src/core/client/admin/components/__snapshots__/NavigationLink.spec.tsx.snap b/src/core/client/admin/components/__snapshots__/NavigationLink.spec.tsx.snap new file mode 100644 index 000000000..5a9a17e26 --- /dev/null +++ b/src/core/client/admin/components/__snapshots__/NavigationLink.spec.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + link + +`; diff --git a/src/core/client/admin/components/__snapshots__/SignOutButton.spec.tsx.snap b/src/core/client/admin/components/__snapshots__/SignOutButton.spec.tsx.snap new file mode 100644 index 000000000..a44d3d67c --- /dev/null +++ b/src/core/client/admin/components/__snapshots__/SignOutButton.spec.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + + Sign Out + + +`; diff --git a/src/core/client/admin/containers/AppContainer.tsx b/src/core/client/admin/containers/AppContainer.tsx new file mode 100644 index 000000000..20b819fb4 --- /dev/null +++ b/src/core/client/admin/containers/AppContainer.tsx @@ -0,0 +1,29 @@ +import { BrowserProtocol, queryMiddleware } from "farce"; +import { createFarceRouter, createRender } from "found"; +import { Resolver } from "found-relay"; + +import React, { StatelessComponent } from "react"; +import { TalkContextConsumer } from "talk-framework/lib/bootstrap/TalkContext"; +import routeConfig from "../routeConfig"; +import NotFound from "../routes/NotFound"; + +const Router = createFarceRouter({ + historyProtocol: new BrowserProtocol(), + historyMiddlewares: [queryMiddleware], + routeConfig, + render: createRender({ + renderError: ({ error }) => ( +
{error.status === 404 ? : "Error"}
+ ), + }), +}); + +const AppContainer: StatelessComponent = () => ( + + {({ relayEnvironment }) => ( + + )} + +); + +export default AppContainer; diff --git a/src/core/client/admin/containers/RedirectAppContainer.tsx b/src/core/client/admin/containers/RedirectAppContainer.tsx new file mode 100644 index 000000000..71e3af2d5 --- /dev/null +++ b/src/core/client/admin/containers/RedirectAppContainer.tsx @@ -0,0 +1,45 @@ +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 { + 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; diff --git a/src/core/client/admin/containers/RedirectLoginContainer.tsx b/src/core/client/admin/containers/RedirectLoginContainer.tsx new file mode 100644 index 000000000..3df2da920 --- /dev/null +++ b/src/core/client/admin/containers/RedirectLoginContainer.tsx @@ -0,0 +1,55 @@ +import { Match, Router, withRouter } from "found"; +import React from "react"; + +import { RedirectLoginContainerLocal as Local } from "talk-admin/__generated__/RedirectLoginContainerLocal.graphql"; +import { + SetRedirectPathMutation, + withSetRedirectPathMutation, +} from "talk-admin/mutations"; +import { graphql, withLocalStateContainer } from "talk-framework/lib/relay"; + +interface Props { + local: Local; + match: Match; + router: Router; + setRedirectPath: SetRedirectPathMutation; +} + +class RedirectLoginContainer extends React.Component { + constructor(props: Props) { + super(props); + this.redirectIfNotLoggedIn(); + } + + public componentWillReceiveProps() { + this.redirectIfNotLoggedIn(); + } + + private redirectIfNotLoggedIn() { + if (!this.props.local.loggedIn) { + const location = this.props.match.location; + this.props.setRedirectPath({ + path: location.pathname + location.search + location.hash, + }); + this.props.router.replace("/admin/login"); + } + } + + public render() { + return this.props.children; + } +} + +const enhanced = withRouter( + withSetRedirectPathMutation( + withLocalStateContainer( + graphql` + fragment RedirectLoginContainerLocal on Local { + loggedIn + } + ` + )(RedirectLoginContainer) + ) +); + +export default enhanced; diff --git a/src/core/client/admin/containers/SignOutButtonContainer.tsx b/src/core/client/admin/containers/SignOutButtonContainer.tsx new file mode 100644 index 000000000..226421ef0 --- /dev/null +++ b/src/core/client/admin/containers/SignOutButtonContainer.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +import { SignOutMutation, withSignOutMutation } from "talk-framework/mutations"; +import SignOutButton from "../components/SignOutButton"; + +interface Props { + signOut: SignOutMutation; + id?: string; +} + +class RedirectAppContainer extends React.Component { + private handleClick = () => this.props.signOut(); + public render() { + return ; + } +} + +const enhanced = withSignOutMutation(RedirectAppContainer); + +export default enhanced; diff --git a/src/core/client/admin/index.tsx b/src/core/client/admin/index.tsx index 55f3e7a4f..b2e1f1772 100644 --- a/src/core/client/admin/index.tsx +++ b/src/core/client/admin/index.tsx @@ -1,14 +1,10 @@ -import { BrowserProtocol, queryMiddleware } from "farce"; -import { createFarceRouter, createRender } from "found"; -import { Resolver } from "found-relay"; - import React, { StatelessComponent } from "react"; import ReactDOM from "react-dom"; import { createManaged } from "talk-framework/lib/bootstrap"; -import { TalkContextConsumer } from "talk-framework/lib/bootstrap/TalkContext"; + +import AppContainer from "./containers/AppContainer"; import { initLocalState } from "./local"; import localesData from "./locales"; -import routeConfig from "./routeConfig"; async function main() { const ManagedTalkContextProvider = await createManaged({ @@ -17,20 +13,9 @@ async function main() { userLocales: navigator.languages, }); - const Router = createFarceRouter({ - historyProtocol: new BrowserProtocol(), - historyMiddlewares: [queryMiddleware], - routeConfig, - render: createRender({}), - }); - const Index: StatelessComponent = () => ( - - {({ relayEnvironment }) => ( - - )} - + ); diff --git a/src/core/client/admin/local/initLocalState.ts b/src/core/client/admin/local/initLocalState.ts index c6a974d70..50f8d3741 100644 --- a/src/core/client/admin/local/initLocalState.ts +++ b/src/core/client/admin/local/initLocalState.ts @@ -1,21 +1,14 @@ -import { commitLocalUpdate, Environment } from "relay-runtime"; +import { Environment } from "relay-runtime"; -import { - createAndRetain, - LOCAL_ID, - LOCAL_TYPE, -} from "talk-framework/lib/relay"; +import { TalkContext } from "talk-framework/lib/bootstrap"; +import { initLocalBaseState } from "talk-framework/lib/relay"; /** * Initializes the local state, before we start the App. */ -export default async function initLocalState(environment: Environment) { - commitLocalUpdate(environment, s => { - const root = s.getRoot(); - - // Create the Local Record which is the Root for the client states. - const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE); - - root.setLinkedRecord(localRecord, "local"); - }); +export default async function initLocalState( + environment: Environment, + context: TalkContext +) { + await initLocalBaseState(environment, context); } diff --git a/src/core/client/admin/local/local.graphql b/src/core/client/admin/local/local.graphql index 57ef889ca..54ac8b637 100644 --- a/src/core/client/admin/local/local.graphql +++ b/src/core/client/admin/local/local.graphql @@ -1,5 +1,15 @@ +# Extend graph with local types +type Network { + isOffline: Boolean! +} + type Local { + network: Network! authToken: String + authExp: Int + authJTI: String + loggedIn: Boolean! + redirectPath: String } extend type Query { diff --git a/src/core/client/admin/mutations/SetRedirectPathMutation.spec.ts b/src/core/client/admin/mutations/SetRedirectPathMutation.spec.ts new file mode 100644 index 000000000..1993ac7fe --- /dev/null +++ b/src/core/client/admin/mutations/SetRedirectPathMutation.spec.ts @@ -0,0 +1,20 @@ +import { Environment, RecordSource } from "relay-runtime"; + +import { LOCAL_ID } from "talk-framework/lib/relay"; +import { createRelayEnvironment } from "talk-framework/testHelpers"; + +import { commit } from "./SetRedirectPathMutation"; + +let environment: Environment; +const source: RecordSource = new RecordSource(); + +beforeAll(() => { + environment = createRelayEnvironment({ + source, + }); +}); + +it("Sets redirectPath", () => { + commit(environment, { path: "/path" }); + expect(source.get(LOCAL_ID)!.redirectPath).toEqual("/path"); +}); diff --git a/src/core/client/admin/mutations/SetRedirectPathMutation.ts b/src/core/client/admin/mutations/SetRedirectPathMutation.ts new file mode 100644 index 000000000..2fe29d1f1 --- /dev/null +++ b/src/core/client/admin/mutations/SetRedirectPathMutation.ts @@ -0,0 +1,26 @@ +import { commitLocalUpdate, Environment } from "relay-runtime"; + +import { createMutationContainer, LOCAL_ID } from "talk-framework/lib/relay"; + +export interface SetRedirectPathInput { + path: string | null; +} + +export type SetRedirectPathMutation = ( + input: SetRedirectPathInput +) => Promise; + +export async function commit( + environment: Environment, + input: SetRedirectPathInput +) { + return commitLocalUpdate(environment, store => { + const record = store.get(LOCAL_ID)!; + record.setValue(input.path, "redirectPath"); + }); +} + +export const withSetRedirectPathMutation = createMutationContainer( + "setRedirectPath", + commit +); diff --git a/src/core/client/admin/mutations/SignInMutation.ts b/src/core/client/admin/mutations/SignInMutation.ts new file mode 100644 index 000000000..74f2e1f0b --- /dev/null +++ b/src/core/client/admin/mutations/SignInMutation.ts @@ -0,0 +1,19 @@ +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; + +export async function commit( + environment: Environment, + input: SignInInput, + context: TalkContext +) { + const result = await signIn(context.rest, input); + setAuthToken(environment, { authToken: result.token }, context); +} + +export const withSignInMutation = createMutationContainer("signIn", commit); diff --git a/src/core/client/admin/mutations/index.ts b/src/core/client/admin/mutations/index.ts new file mode 100644 index 000000000..245732316 --- /dev/null +++ b/src/core/client/admin/mutations/index.ts @@ -0,0 +1,5 @@ +export { withSignInMutation, SignInMutation } from "./SignInMutation"; +export { + withSetRedirectPathMutation, + SetRedirectPathMutation, +} from "./SetRedirectPathMutation"; diff --git a/src/core/client/admin/routeConfig.tsx b/src/core/client/admin/routeConfig.tsx index 30af0dfef..c724f8e80 100644 --- a/src/core/client/admin/routeConfig.tsx +++ b/src/core/client/admin/routeConfig.tsx @@ -1,17 +1,28 @@ -import { makeRouteConfig, Route } from "found"; +import { makeRouteConfig, Redirect, Route } from "found"; import React from "react"; import App from "./components/App"; -import Community from "./components/Community"; -import Configure from "./components/Configure"; -import Moderate from "./components/Moderate"; -import Stories from "./components/Stories"; +import RedirectAppContainer from "./containers/RedirectAppContainer"; +import RedirectLoginContainer from "./containers/RedirectLoginContainer"; +import Community from "./routes/community/components/Community"; +import Configure from "./routes/configure/components/Configure"; +import Login from "./routes/login/components/Login"; +import Moderate from "./routes/moderate/components/Moderate"; +import Stories from "./routes/stories/components/Stories"; export default makeRouteConfig( - - - - - + + + + + + + + + + + + + ); diff --git a/src/core/client/admin/routes/NotFound.tsx b/src/core/client/admin/routes/NotFound.tsx new file mode 100644 index 000000000..551d5880d --- /dev/null +++ b/src/core/client/admin/routes/NotFound.tsx @@ -0,0 +1,10 @@ +import React, { StatelessComponent } from "react"; +import { HorizontalGutter, Typography } from "talk-ui/components"; + +const NotFound: StatelessComponent = ({ children }) => ( + + Not Found + +); + +export default NotFound; diff --git a/src/core/client/admin/routes/community/components/Community.spec.tsx b/src/core/client/admin/routes/community/components/Community.spec.tsx new file mode 100644 index 000000000..9b0a2c23c --- /dev/null +++ b/src/core/client/admin/routes/community/components/Community.spec.tsx @@ -0,0 +1,9 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import Community from "./Community"; + +it("renders correctly", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/components/Community.tsx b/src/core/client/admin/routes/community/components/Community.tsx similarity index 81% rename from src/core/client/admin/components/Community.tsx rename to src/core/client/admin/routes/community/components/Community.tsx index 73dfddf69..b0f09ee17 100644 --- a/src/core/client/admin/components/Community.tsx +++ b/src/core/client/admin/routes/community/components/Community.tsx @@ -1,7 +1,7 @@ import React, { StatelessComponent } from "react"; import { HorizontalGutter, Typography } from "talk-ui/components"; -const Community: StatelessComponent = ({ children }) => ( +const Community: StatelessComponent = () => ( Community diff --git a/src/core/client/admin/routes/community/components/__snapshots__/Community.spec.tsx.snap b/src/core/client/admin/routes/community/components/__snapshots__/Community.spec.tsx.snap new file mode 100644 index 000000000..cf6dd0489 --- /dev/null +++ b/src/core/client/admin/routes/community/components/__snapshots__/Community.spec.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + + Community + + +`; diff --git a/src/core/client/admin/routes/configure/components/Configure.spec.tsx b/src/core/client/admin/routes/configure/components/Configure.spec.tsx new file mode 100644 index 000000000..e0e20a5a4 --- /dev/null +++ b/src/core/client/admin/routes/configure/components/Configure.spec.tsx @@ -0,0 +1,9 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import Configure from "./Configure"; + +it("renders correctly", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/components/Configure.tsx b/src/core/client/admin/routes/configure/components/Configure.tsx similarity index 100% rename from src/core/client/admin/components/Configure.tsx rename to src/core/client/admin/routes/configure/components/Configure.tsx diff --git a/src/core/client/admin/routes/configure/components/__snapshots__/Configure.spec.tsx.snap b/src/core/client/admin/routes/configure/components/__snapshots__/Configure.spec.tsx.snap new file mode 100644 index 000000000..eaf351718 --- /dev/null +++ b/src/core/client/admin/routes/configure/components/__snapshots__/Configure.spec.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + + Configure + + +`; diff --git a/src/core/client/admin/routes/login/components/Login.css b/src/core/client/admin/routes/login/components/Login.css new file mode 100644 index 000000000..4e127bea3 --- /dev/null +++ b/src/core/client/admin/routes/login/components/Login.css @@ -0,0 +1,17 @@ +.loginContainer { + width: 350px; + background-color: #f5f5f5; + border: 1px solid var(--palette-grey-lighter); + margin-top: 70px; + padding: 0 calc(3 * var(--spacing-unit)) calc(4 * var(--spacing-unit)) + calc(3 * var(--spacing-unit)); + box-sizing: border-box; +} + +.brandIcon { + display: inline-flex; + background: var(--palette-common-white); + padding: calc(0.5 * var(--spacing-unit)); + border: 1px solid var(--palette-grey-lighter); + margin-top: -42px; +} diff --git a/src/core/client/admin/routes/login/components/Login.spec.tsx b/src/core/client/admin/routes/login/components/Login.spec.tsx new file mode 100644 index 000000000..4ea379113 --- /dev/null +++ b/src/core/client/admin/routes/login/components/Login.spec.tsx @@ -0,0 +1,9 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import Login from "./Login"; + +it("renders correctly", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/routes/login/components/Login.tsx b/src/core/client/admin/routes/login/components/Login.tsx new file mode 100644 index 000000000..145c22ee7 --- /dev/null +++ b/src/core/client/admin/routes/login/components/Login.tsx @@ -0,0 +1,41 @@ +import { Localized } from "fluent-react/compat"; +import React, { StatelessComponent } from "react"; + +import AppBar from "talk-admin/components/AppBar"; +import BrandName from "talk-admin/components/BrandName"; +import { Flex, HorizontalGutter, Typography } from "talk-ui/components"; + +import BrandIcon from "talk-admin/components/BrandIcon"; +import SignInFormContainer from "../containers/SignInFormContainer"; +import styles from "./Login.css"; + +const Login: StatelessComponent = () => ( +
+ + + + +
+ +
+
+
+ + + Sign in to + + + +
+ + + Enter your account details below + + + +
+
+
+); + +export default Login; diff --git a/src/core/client/admin/routes/login/components/SignInForm.tsx b/src/core/client/admin/routes/login/components/SignInForm.tsx new file mode 100644 index 000000000..131097899 --- /dev/null +++ b/src/core/client/admin/routes/login/components/SignInForm.tsx @@ -0,0 +1,132 @@ +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 { + 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; +} + +const SignIn: StatelessComponent = props => ( +
+ {({ handleSubmit, submitting, submitError }) => ( + + + {submitError && ( + + {submitError} + + )} + + + {({ input, meta }) => ( + + + Email Address + + + + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + )} + + + + {({ input, meta }) => ( + + + Password + + + + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + )} + + + + + +
+ )} + +); + +export default SignIn; diff --git a/src/core/client/admin/routes/login/components/__snapshots__/Login.spec.tsx.snap b/src/core/client/admin/routes/login/components/__snapshots__/Login.spec.tsx.snap new file mode 100644 index 000000000..8a2af4fd0 --- /dev/null +++ b/src/core/client/admin/routes/login/components/__snapshots__/Login.spec.tsx.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+ + + + +
+ +
+
+
+ + + Sign in to + + + +
+ + + Enter your account details below + + + +
+
+
+`; diff --git a/src/core/client/admin/routes/login/containers/SignInFormContainer.tsx b/src/core/client/admin/routes/login/containers/SignInFormContainer.tsx new file mode 100644 index 000000000..f3e0e086d --- /dev/null +++ b/src/core/client/admin/routes/login/containers/SignInFormContainer.tsx @@ -0,0 +1,30 @@ +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"; + +interface SignInContainerProps { + signIn: SignInMutation; +} + +class SignInContainer extends Component { + private onSubmit: PropTypesOf["onSubmit"] = async ( + input, + form + ) => { + try { + await this.props.signIn(input); + return form.reset(); + } catch (error) { + return { [FORM_ERROR]: error.message }; + } + }; + public render() { + return ; + } +} + +const enhanced = withSignInMutation(SignInContainer); +export default enhanced; diff --git a/src/core/client/admin/routes/moderate/components/Moderate.spec.tsx b/src/core/client/admin/routes/moderate/components/Moderate.spec.tsx new file mode 100644 index 000000000..fa8dfb722 --- /dev/null +++ b/src/core/client/admin/routes/moderate/components/Moderate.spec.tsx @@ -0,0 +1,9 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import Moderate from "./Moderate"; + +it("renders correctly", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/components/Moderate.tsx b/src/core/client/admin/routes/moderate/components/Moderate.tsx similarity index 100% rename from src/core/client/admin/components/Moderate.tsx rename to src/core/client/admin/routes/moderate/components/Moderate.tsx diff --git a/src/core/client/admin/routes/moderate/components/__snapshots__/Moderate.spec.tsx.snap b/src/core/client/admin/routes/moderate/components/__snapshots__/Moderate.spec.tsx.snap new file mode 100644 index 000000000..dd278bb5e --- /dev/null +++ b/src/core/client/admin/routes/moderate/components/__snapshots__/Moderate.spec.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + + Moderate + + +`; diff --git a/src/core/client/admin/routes/stories/components/Stories.spec.tsx b/src/core/client/admin/routes/stories/components/Stories.spec.tsx new file mode 100644 index 000000000..b17720743 --- /dev/null +++ b/src/core/client/admin/routes/stories/components/Stories.spec.tsx @@ -0,0 +1,9 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import Stories from "./Stories"; + +it("renders correctly", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/admin/components/Stories.tsx b/src/core/client/admin/routes/stories/components/Stories.tsx similarity index 100% rename from src/core/client/admin/components/Stories.tsx rename to src/core/client/admin/routes/stories/components/Stories.tsx diff --git a/src/core/client/admin/routes/stories/components/__snapshots__/Stories.spec.tsx.snap b/src/core/client/admin/routes/stories/components/__snapshots__/Stories.spec.tsx.snap new file mode 100644 index 000000000..676b2430b --- /dev/null +++ b/src/core/client/admin/routes/stories/components/__snapshots__/Stories.spec.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + + Stories + + +`; diff --git a/src/core/client/admin/test/__snapshots__/login.spec.tsx.snap b/src/core/client/admin/test/__snapshots__/login.spec.tsx.snap new file mode 100644 index 000000000..daa441ee9 --- /dev/null +++ b/src/core/client/admin/test/__snapshots__/login.spec.tsx.snap @@ -0,0 +1,1857 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`accepts correct password 1`] = ` +
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +

+ Talk +

+
+
+
+
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +
+
+
+

+ Sign in to +

+

+ Talk +

+
+

+ Enter your account details below +

+
+
+
+ + +
+ + + This field is required. + +
+
+
+ + +
+ +
+
+
+
+
+`; + +exports[`accepts valid email 1`] = ` +
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +

+ Talk +

+
+
+
+
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +
+
+
+

+ Sign in to +

+

+ Talk +

+
+

+ Enter your account details below +

+
+
+
+ + +
+
+ + +
+ + + This field is required. + +
+
+ +
+
+
+
+
+`; + +exports[`checks for invalid email 1`] = ` +
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +

+ Talk +

+
+
+
+
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +
+
+
+

+ Sign in to +

+

+ Talk +

+
+

+ Enter your account details below +

+
+
+
+ + +
+ + + Please enter a valid email address. + +
+
+
+ + +
+ + + This field is required. + +
+
+ +
+
+
+
+
+`; + +exports[`renders sign in form 1`] = ` +
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +

+ Talk +

+
+
+
+
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +
+
+
+

+ Sign in to +

+

+ Talk +

+
+

+ Enter your account details below +

+
+
+
+ + +
+
+ + +
+ +
+
+
+
+
+`; + +exports[`shows error when submitting empty form 1`] = ` +
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +

+ Talk +

+
+
+
+
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +
+
+
+

+ Sign in to +

+

+ Talk +

+
+

+ Enter your account details below +

+
+
+
+ + +
+ + + This field is required. + +
+
+
+ + +
+ + + This field is required. + +
+
+ +
+
+
+
+
+`; + +exports[`shows server error 1`] = ` +
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +

+ Talk +

+
+
+
+
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +
+
+
+

+ Sign in to +

+

+ Talk +

+
+

+ Enter your account details below +

+
+
+
+ + +
+
+ + +
+ +
+
+
+
+
+`; + +exports[`shows server error 2`] = ` +
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +

+ Talk +

+
+
+
+
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +
+
+
+

+ Sign in to +

+

+ Talk +

+
+

+ Enter your account details below +

+
+
+
+ Server Error +
+
+ + +
+
+ + +
+ +
+
+
+
+
+`; + +exports[`submits form successfully 1`] = ` +
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +

+ Talk +

+
+
+
+
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +
+
+
+

+ Sign in to +

+

+ Talk +

+
+

+ Enter your account details below +

+
+
+
+ + +
+
+ + +
+ +
+
+
+
+
+`; + +exports[`submits form successfully 2`] = ` +
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +

+ Talk +

+
+
+
+
+
+
+
+ + + + + + + + + + + logo mark2018 + + + + + +
+
+
+

+ Sign in to +

+

+ Talk +

+
+

+ Enter your account details below +

+
+
+
+ + +
+
+ + +
+ +
+
+
+
+
+`; diff --git a/src/core/client/admin/test/create.tsx b/src/core/client/admin/test/create.tsx new file mode 100644 index 000000000..f752fe233 --- /dev/null +++ b/src/core/client/admin/test/create.tsx @@ -0,0 +1,60 @@ +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"; + +import AppContainer from "talk-admin/containers/AppContainer"; +import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap"; +import { PostMessageService } from "talk-framework/lib/postMessage"; +import { RestClient } from "talk-framework/lib/rest"; +import { createPromisifiedStorage } from "talk-framework/lib/storage"; +import { createUUIDGenerator } from "talk-framework/testHelpers"; + +import createEnvironment from "./createEnvironment"; +import createFluentBundle from "./createFluentBundle"; + +interface CreateParams { + logNetwork?: boolean; + resolvers?: IResolvers; + initLocalState?: ( + local: RecordProxy, + source: RecordSourceProxy, + environment: Environment + ) => void; +} + +export default function create(params: CreateParams) { + const environment = createEnvironment({ + // Set this to true, to see graphql responses. + logNetwork: params.logNetwork, + resolvers: params.resolvers, + initLocalState: (localRecord, source, env) => { + if (params.initLocalState) { + params.initLocalState(localRecord, source, env); + } + }, + }); + + const context: TalkContext = { + relayEnvironment: environment, + localeBundles: [createFluentBundle()], + localStorage: createPromisifiedStorage(), + sessionStorage: createPromisifiedStorage(), + rest: new RestClient("http://localhost/api"), + postMessage: new PostMessageService(), + browserInfo: { ios: false }, + uuidGenerator: createUUIDGenerator(), + eventEmitter: new EventEmitter2({ wildcard: true, maxListeners: 20 }), + clearSession: noop, + }; + + const testRenderer = TestRenderer.create( + + + + ); + + return { context, testRenderer }; +} diff --git a/src/core/client/admin/test/createEnvironment.ts b/src/core/client/admin/test/createEnvironment.ts new file mode 100644 index 000000000..700b354c9 --- /dev/null +++ b/src/core/client/admin/test/createEnvironment.ts @@ -0,0 +1,28 @@ +import { IResolvers } from "graphql-tools"; +import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime"; +import { createRelayEnvironment } from "talk-framework/testHelpers"; + +interface CreateEnvironmentParams { + logNetwork?: boolean; + resolvers?: IResolvers; + initLocalState?: ( + local: RecordProxy, + source: RecordSourceProxy, + environment: Environment + ) => void; +} + +export default function createEnvironment(params: CreateEnvironmentParams) { + return createRelayEnvironment({ + network: { + logNetwork: params.logNetwork, + resolvers: params.resolvers || {}, + projectName: "tenant", + }, + initLocalState: (localRecord, source, environment) => { + if (params.initLocalState) { + params.initLocalState(localRecord, source, environment); + } + }, + }); +} diff --git a/src/core/client/admin/test/createFluentBundle.ts b/src/core/client/admin/test/createFluentBundle.ts new file mode 100644 index 000000000..0b883ad53 --- /dev/null +++ b/src/core/client/admin/test/createFluentBundle.ts @@ -0,0 +1,10 @@ +import path from "path"; + +import { createFluentBundle } from "talk-framework/testHelpers"; + +export default function create() { + return createFluentBundle( + "admin", + path.resolve(__dirname, "../../../../locales/en-US") + ); +} diff --git a/src/core/client/admin/test/login.spec.tsx b/src/core/client/admin/test/login.spec.tsx new file mode 100644 index 000000000..9e49bd7a2 --- /dev/null +++ b/src/core/client/admin/test/login.spec.tsx @@ -0,0 +1,146 @@ +import { ReactTestInstance, 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 create from "./create"; + +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 () => { + // deliberately setting to a different route, + // it should be smart enough to reroute to /admin/login. + replaceHistoryLocation("http://localhost/admin/moderate"); + + ({ testRenderer, context } = create({ + // Set this to true, to see graphql responses. + logNetwork: false, + initLocalState: localRecord => { + localRecord.setValue(false, "loggedIn"); + }, + })); + await timeout(); + form = testRenderer.root.findByType("form"); +}); + +it("renders sign in form", async () => { + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("shows error when submitting empty form", async () => { + form.props.onSubmit(); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("checks for invalid email", async () => { + form + .find(inputPredicate("email")) + .props.onChange({ target: { value: "invalid-email" } }); + form.props.onSubmit(); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("accepts valid email", async () => { + form + .find(inputPredicate("email")) + .props.onChange({ target: { value: "hans@test.com" } }); + form.props.onSubmit(); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("accepts correct password", async () => { + form + .find(inputPredicate("password")) + .props.onChange({ target: { value: "testtest" } }); + form.props.onSubmit(); + expect(testRenderer.toJSON()).toMatchSnapshot(); +}); + +it("shows server error", async () => { + form + .find(inputPredicate("email")) + .props.onChange({ target: { value: "hans@test.com" } }); + form + .find(inputPredicate("password")) + .props.onChange({ target: { value: "testtest" } }); + + const error = new Error("Server Error"); + const restMock = sinon.mock(context.rest); + restMock + .expects("fetch") + .withArgs("/tenant/auth/local", { + method: "POST", + body: { + email: "hans@test.com", + password: "testtest", + }, + }) + .once() + .throws(error); + + form.props.onSubmit(); + expect(testRenderer.toJSON()).toMatchSnapshot(); + await timeout(); + expect(testRenderer.toJSON()).toMatchSnapshot(); + restMock.verify(); +}); + +it("submits form successfully", async () => { + form + .find(inputPredicate("email")) + .props.onChange({ target: { value: "hans@test.com" } }); + form + .find(inputPredicate("password")) + .props.onChange({ target: { value: "testtest" } }); + + const authToken = `${btoa( + JSON.stringify({ + alg: "HS256", + typ: "JWT", + }) + )}.${btoa( + JSON.stringify({ + exp: 1540503165, + jti: "31b26591-4e9a-4388-a7ff-e1bdc5d97cce", + }) + )}`; + + const restMock = sinon.mock(context.rest); + restMock + .expects("fetch") + .withArgs("/tenant/auth/local", { + method: "POST", + body: { + email: "hans@test.com", + password: "testtest", + }, + }) + .once() + .returns({ token: authToken }); + + const historyMock = sinon.mock(window.history); + + form.props.onSubmit(); + expect(testRenderer.toJSON()).toMatchSnapshot(); + await timeout(); + expect(testRenderer.toJSON()).toMatchSnapshot(); + restMock.verify(); + await timeout(); + expect( + context.relayEnvironment + .getStore() + .getSource() + .get(LOCAL_ID)!.loggedIn + ).toBeTruthy(); + await timeout(); + historyMock.verify(); +}); diff --git a/src/core/client/admin/test/redirectLoggedIn.spec.tsx b/src/core/client/admin/test/redirectLoggedIn.spec.tsx new file mode 100644 index 000000000..ef8e345e9 --- /dev/null +++ b/src/core/client/admin/test/redirectLoggedIn.spec.tsx @@ -0,0 +1,31 @@ +import { timeout } from "talk-common/utils"; +import { replaceHistoryLocation } from "talk-framework/testHelpers"; + +import create from "./create"; + +it("redirect when already logged in", async () => { + replaceHistoryLocation("http://localhost/admin/login"); + create({ + // Set this to true, to see graphql responses. + logNetwork: false, + initLocalState: localRecord => { + localRecord.setValue(true, "loggedIn"); + }, + }); + await timeout(); + expect(window.location.toString()).toBe("http://localhost/admin/moderate"); +}); + +it("redirect to redirectPath when already logged in", async () => { + replaceHistoryLocation("http://localhost/admin/login"); + create({ + // Set this to true, to see graphql responses. + logNetwork: false, + initLocalState: localRecord => { + localRecord.setValue(true, "loggedIn"); + localRecord.setValue("/admin/configure", "redirectPath"); + }, + }); + await timeout(); + expect(window.location.toString()).toBe("http://localhost/admin/configure"); +}); diff --git a/src/core/client/admin/test/redirectLoggedOut.spec.tsx b/src/core/client/admin/test/redirectLoggedOut.spec.tsx new file mode 100644 index 000000000..a46b4b1c2 --- /dev/null +++ b/src/core/client/admin/test/redirectLoggedOut.spec.tsx @@ -0,0 +1,36 @@ +import { ReactTestRenderer } from "react-test-renderer"; + +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 create from "./create"; + +function createTestRenderer(): { + testRenderer: ReactTestRenderer; + context: TalkContext; +} { + replaceHistoryLocation("http://localhost/admin/moderate"); + + const { testRenderer, context } = create({ + // Set this to true, to see graphql responses. + logNetwork: false, + initLocalState: localRecord => { + localRecord.setValue(false, "loggedIn"); + }, + }); + return { testRenderer, context }; +} + +it("redirect when not logged in", async () => { + const { context } = createTestRenderer(); + await timeout(); + expect( + context.relayEnvironment + .getStore() + .getSource() + .get(LOCAL_ID)!.redirectPath + ).toBe("/admin/moderate"); + expect(window.location.toString()).toBe("http://localhost/admin/login"); +}); diff --git a/src/core/client/admin/test/signOut.spec.tsx b/src/core/client/admin/test/signOut.spec.tsx new file mode 100644 index 000000000..70dc54405 --- /dev/null +++ b/src/core/client/admin/test/signOut.spec.tsx @@ -0,0 +1,42 @@ +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 create from "./create"; + +it("logs out", async () => { + replaceHistoryLocation("http://localhost/admin/moderate"); + + const { testRenderer, context } = create({ + // Set this to true, to see graphql responses. + logNetwork: false, + initLocalState: localRecord => { + localRecord.setValue(true, "loggedIn"); + }, + }); + + const restMock = sinon.mock(context.rest); + restMock + .expects("fetch") + .withArgs("/tenant/auth", { + method: "DELETE", + }) + .once() + .returns({}); + + await timeout(); + testRenderer.root + .find(i => i.props.id === "navigation-signOutButton" && i.props.onClick) + .props.onClick(); + + await timeout(); + + expect( + context.relayEnvironment + .getStore() + .getSource() + .get(LOCAL_ID)!.loggedIn + ).toBeFalsy(); +}); diff --git a/src/core/client/auth/local/initLocalState.spec.ts b/src/core/client/auth/local/initLocalState.spec.ts index bcf88006f..7c8975d40 100644 --- a/src/core/client/auth/local/initLocalState.spec.ts +++ b/src/core/client/auth/local/initLocalState.spec.ts @@ -1,7 +1,10 @@ import { Environment, RecordSource } from "relay-runtime"; import { LOCAL_ID } from "talk-framework/lib/relay"; -import { createRelayEnvironment } from "talk-framework/testHelpers"; +import { + createRelayEnvironment, + replaceHistoryLocation, +} from "talk-framework/testHelpers"; import initLocalState from "./initLocalState"; @@ -23,14 +26,10 @@ it("init local state", () => { it("set view from query", () => { const view = "SIGN_UP"; - const previousLocation = location.toString(); - const previousState = window.history.state; - window.history.replaceState( - previousState, - document.title, + const restoreHistoryLocation = replaceHistoryLocation( `http://localhost/?view=${view}` ); initLocalState(environment); expect(source.get(LOCAL_ID)!.view).toBe(view); - window.history.replaceState(previousState, document.title, previousLocation); + restoreHistoryLocation(); }); diff --git a/src/core/client/framework/lib/jwt.ts b/src/core/client/framework/lib/jwt.ts new file mode 100644 index 000000000..379d8815d --- /dev/null +++ b/src/core/client/framework/lib/jwt.ts @@ -0,0 +1,30 @@ +export interface JWT { + header: { + alg: string; + typ: string; + }; + payload: { + iat?: number; + exp?: number; + iss?: string; + sub?: string; + jti?: string; + }; + expired: boolean; +} + +export function parseJWT(token: string, skewTolerance = 300): JWT { + const [headerBase64, payloadBase64] = token.split("."); + if (!headerBase64 && !payloadBase64) { + throw new Error("invalid jwt token"); + } + const header = JSON.parse(atob(headerBase64)); + const payload = JSON.parse(atob(payloadBase64)); + return { + header, + payload, + get expired() { + return Date.now() - skewTolerance < payload.exp; + }, + }; +} diff --git a/src/core/client/framework/lib/relay/index.ts b/src/core/client/framework/lib/relay/index.ts index 31bae3d63..998d9af3b 100644 --- a/src/core/client/framework/lib/relay/index.ts +++ b/src/core/client/framework/lib/relay/index.ts @@ -16,3 +16,4 @@ export { graphql } from "react-relay"; export { default as commitLocalUpdatePromisified, } from "./commitLocalUpdatePromisified"; +export { initLocalBaseState, setAuthTokenInLocalState } from "./localState"; diff --git a/src/core/client/framework/lib/relay/localState.ts b/src/core/client/framework/lib/relay/localState.ts new file mode 100644 index 000000000..4bc4ea309 --- /dev/null +++ b/src/core/client/framework/lib/relay/localState.ts @@ -0,0 +1,68 @@ +import { + commitLocalUpdate, + Environment, + RecordSourceProxy, +} from "relay-runtime"; + +import { TalkContext } from "talk-framework/lib/bootstrap"; +import { parseJWT } from "talk-framework/lib/jwt"; +import { createAndRetain } from "talk-framework/lib/relay"; + +/** + * The Root Record of Client-Side Schema Extension must be of this type. + */ +export const LOCAL_TYPE = "Local"; + +/** + * The Root Record of Client-Side Schema Extension must have this id. + */ +export const LOCAL_ID = "client:root.local"; + +export const NETWORK_TYPE = "Network"; +export const NETWORK_ID = "client:root.local.network"; + +export function setAuthTokenInLocalState( + authToken: string | null, + source: RecordSourceProxy +) { + const localRecord = source.get(LOCAL_ID)!; + localRecord.setValue(authToken || "", "authToken"); + if (authToken) { + const { payload, expired } = parseJWT(authToken); + localRecord.setValue(payload.exp, "authExp"); + localRecord.setValue(payload.jti, "authJTI"); + localRecord.setValue(!expired, "loggedIn"); + } else { + localRecord.setValue(null, "authExp"); + localRecord.setValue(null, "authJTI"); + localRecord.setValue(false, "loggedIn"); + } +} + +export async function initLocalBaseState( + environment: Environment, + { localStorage }: TalkContext +) { + const authToken = await localStorage!.getItem("authToken"); + + commitLocalUpdate(environment, s => { + const root = s.getRoot(); + + // Create the Local Record which is the Root for the client states. + const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE); + root.setLinkedRecord(localRecord, "local"); + + // Set auth token + setAuthTokenInLocalState(authToken, s); + + // Create network Record + const networkRecord = createAndRetain( + environment, + s, + NETWORK_ID, + NETWORK_TYPE + ); + networkRecord.setValue(false, "isOffline"); + localRecord.setLinkedRecord(networkRecord, "network"); + }); +} diff --git a/src/core/client/framework/mutations/SetAuthTokenMutation.spec.ts b/src/core/client/framework/mutations/SetAuthTokenMutation.spec.ts index 158dd0854..d0f5e2b3f 100644 --- a/src/core/client/framework/mutations/SetAuthTokenMutation.spec.ts +++ b/src/core/client/framework/mutations/SetAuthTokenMutation.spec.ts @@ -17,13 +17,24 @@ beforeAll(() => { }); }); +const authToken = `${btoa( + JSON.stringify({ + alg: "HS256", + typ: "JWT", + }) +)}.${btoa( + JSON.stringify({ + exp: 1540503165, + jti: "31b26591-4e9a-4388-a7ff-e1bdc5d97cce", + }) +)}`; + it("Sets auth token to localStorage", async () => { const clearSessionStub = sinon.stub(); const context: Partial = { localStorage: createPromisifiedStorage(), clearSession: clearSessionStub, }; - const authToken = "auth token"; await commit(environment, { authToken }, context as any); expect(source.get(LOCAL_ID)!.authToken).toEqual(authToken); await expect(context.localStorage!.getItem("authToken")).resolves.toEqual( @@ -38,7 +49,7 @@ it("Removes auth token from localStorage", async () => { localStorage: createPromisifiedStorage(), clearSession: clearSessionStub, }; - localStorage.setItem("authToken", "tmp"); + localStorage.setItem("authToken", authToken); await commit(environment, { authToken: null }, context as any); await expect(context.localStorage!.getItem("authToken")).resolves.toBeNull(); expect(clearSessionStub.calledOnce).toBe(true); diff --git a/src/core/client/framework/mutations/SetAuthTokenMutation.ts b/src/core/client/framework/mutations/SetAuthTokenMutation.ts index f0da6df92..120188ac4 100644 --- a/src/core/client/framework/mutations/SetAuthTokenMutation.ts +++ b/src/core/client/framework/mutations/SetAuthTokenMutation.ts @@ -4,8 +4,8 @@ import { TalkContext } from "talk-framework/lib/bootstrap"; import { commitLocalUpdatePromisified, createMutationContainer, + setAuthTokenInLocalState, } from "talk-framework/lib/relay"; -import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer"; export interface SetAuthTokenInput { authToken: string | null; @@ -19,8 +19,7 @@ export async function commit( { localStorage, clearSession }: TalkContext ) { return await commitLocalUpdatePromisified(environment, async store => { - const record = store.get(LOCAL_ID)!; - record.setValue(input.authToken, "authToken"); + setAuthTokenInLocalState(input.authToken, store); if (input.authToken) { await localStorage.setItem("authToken", input.authToken); } else { diff --git a/src/core/client/stream/mutations/SetNetworkStatusMutation.spec.ts b/src/core/client/framework/mutations/SetNetworkStatusMutation.spec.ts similarity index 91% rename from src/core/client/stream/mutations/SetNetworkStatusMutation.spec.ts rename to src/core/client/framework/mutations/SetNetworkStatusMutation.spec.ts index 2ca97bea9..fe3bcbde9 100644 --- a/src/core/client/stream/mutations/SetNetworkStatusMutation.spec.ts +++ b/src/core/client/framework/mutations/SetNetworkStatusMutation.spec.ts @@ -2,7 +2,7 @@ import { Environment, RecordSource } from "relay-runtime"; import { createRelayEnvironment } from "talk-framework/testHelpers"; -import { NETWORK_ID, NETWORK_TYPE } from "../local"; +import { NETWORK_ID, NETWORK_TYPE } from "../lib/relay/localState"; import { commit } from "./SetNetworkStatusMutation"; let environment: Environment; diff --git a/src/core/client/stream/mutations/SetNetworkStatusMutation.ts b/src/core/client/framework/mutations/SetNetworkStatusMutation.ts similarity index 92% rename from src/core/client/stream/mutations/SetNetworkStatusMutation.ts rename to src/core/client/framework/mutations/SetNetworkStatusMutation.ts index 5f2fa6486..a1e1969e3 100644 --- a/src/core/client/stream/mutations/SetNetworkStatusMutation.ts +++ b/src/core/client/framework/mutations/SetNetworkStatusMutation.ts @@ -2,7 +2,7 @@ import { commitLocalUpdate, Environment } from "relay-runtime"; import { createMutationContainer } from "talk-framework/lib/relay"; -import { NETWORK_ID } from "../local"; +import { NETWORK_ID } from "../lib/relay/localState"; export interface SetNetworkStatusInput { isOffline: boolean; diff --git a/src/core/client/framework/mutations/index.ts b/src/core/client/framework/mutations/index.ts index b128402e6..5536b91b6 100644 --- a/src/core/client/framework/mutations/index.ts +++ b/src/core/client/framework/mutations/index.ts @@ -4,3 +4,8 @@ export { SetAuthTokenInput, } from "./SetAuthTokenMutation"; export { withSignOutMutation, SignOutMutation } from "./SignOutMutation"; +export { + withSetNetworkStatusMutation, + SetNetworkStatusMutation, + SetNetworkStatusInput, +} from "./SetNetworkStatusMutation"; diff --git a/src/core/client/framework/testHelpers/index.ts b/src/core/client/framework/testHelpers/index.ts index 9dec944d2..9547b9c49 100644 --- a/src/core/client/framework/testHelpers/index.ts +++ b/src/core/client/framework/testHelpers/index.ts @@ -10,3 +10,4 @@ export { } from "./removeFragmentRefs"; export { default as createUUIDGenerator } from "./createUUIDGenerator"; export * from "./denormalize"; +export { default as replaceHistoryLocation } from "./replaceHistoryLocation"; diff --git a/src/core/client/framework/testHelpers/replaceHistoryLocation.ts b/src/core/client/framework/testHelpers/replaceHistoryLocation.ts new file mode 100644 index 000000000..d353f7d6e --- /dev/null +++ b/src/core/client/framework/testHelpers/replaceHistoryLocation.ts @@ -0,0 +1,15 @@ +type RestoreHistoryFunction = () => void; + +export default function replaceHistoryLocation( + location: string +): RestoreHistoryFunction { + const previousState = window.history.state; + const previousLocation = location.toString(); + window.history.replaceState(previousState, document.title, location); + return () => + window.history.replaceState( + previousState, + document.title, + previousLocation + ); +} diff --git a/src/core/client/stream/components/App.spec.tsx b/src/core/client/stream/components/App.spec.tsx index 01c0bec0b..942a67b78 100644 --- a/src/core/client/stream/components/App.spec.tsx +++ b/src/core/client/stream/components/App.spec.tsx @@ -1,15 +1,13 @@ import { shallow } from "enzyme"; -import noop from "lodash"; import React from "react"; import { PropTypesOf } from "talk-framework/types"; import App from "./App"; -it("renders comments", () => { +it("renders correctly", () => { const props: PropTypesOf = { activeTab: "COMMENTS", - onTabClick: noop, }; const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); diff --git a/src/core/client/stream/components/App.tsx b/src/core/client/stream/components/App.tsx index 3b6ccac08..e1b6916f2 100644 --- a/src/core/client/stream/components/App.tsx +++ b/src/core/client/stream/components/App.tsx @@ -1,17 +1,8 @@ -import { Localized } from "fluent-react/compat"; import * as React from "react"; import { StatelessComponent } from "react"; -import { - HorizontalGutter, - Tab, - TabBar, - TabContent, - TabPane, -} from "talk-ui/components"; +import TabBarQuery from "talk-stream/queries/TabBarQuery"; +import { HorizontalGutter, TabContent, TabPane } from "talk-ui/components"; -import { PropTypesOf } from "talk-ui/types"; -import CommentsCountQuery from "../queries/CommentsCountQuery"; -import IfLoggedInQuery from "../queries/IfLoggedInQuery"; import CommentsPaneContainer from "../tabs/comments/containers/CommentsPaneContainer"; import ProfileQuery from "../tabs/profile/queries/ProfileQuery"; import * as styles from "./App.css"; @@ -20,28 +11,12 @@ type TabValue = "COMMENTS" | "PROFILE" | "%future added value"; export interface AppProps { activeTab: TabValue; - onTabClick: (tab: TabValue) => void; } -const CommentsTab: StatelessComponent> = props => ( - -); - -const MyProfileTab: StatelessComponent> = props => ( - - - My Profile - - -); - const App: StatelessComponent = props => { return ( - - - - + diff --git a/src/core/client/stream/components/TabBar.tsx b/src/core/client/stream/components/TabBar.tsx new file mode 100644 index 000000000..4400b5bd4 --- /dev/null +++ b/src/core/client/stream/components/TabBar.tsx @@ -0,0 +1,37 @@ +import { Localized } from "fluent-react/compat"; +import * as React from "react"; +import { StatelessComponent } from "react"; +import { Tab, TabBar } from "talk-ui/components"; + +type TabValue = "COMMENTS" | "PROFILE" | "%future added value"; + +export interface Props { + activeTab: TabValue; + onTabClick: (tab: TabValue) => void; + commentCount: number; + showProfileTab: boolean; +} + +const AppTabBar: StatelessComponent = props => { + return ( + + + + {"{$commentCount} Comments"} + + + {props.showProfileTab && ( + + + My Profile + + + )} + + ); +}; + +export default AppTabBar; diff --git a/src/core/client/stream/components/__snapshots__/App.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/App.spec.tsx.snap index f99caa827..8962ad068 100644 --- a/src/core/client/stream/components/__snapshots__/App.spec.tsx.snap +++ b/src/core/client/stream/components/__snapshots__/App.spec.tsx.snap @@ -1,20 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders comments 1`] = ` +exports[`renders correctly 1`] = ` - - - - + { - private handleSetActiveTab = (tab: SetActiveTabInput["tab"]) => { - this.props.setActiveTab({ tab }); - }; - public render() { const { local: { activeTab }, } = this.props; - return ; + return ; } } -const enhanced = withSetActiveTabMutation( - withLocalStateContainer( - graphql` - fragment AppContainerLocal on Local { - activeTab - } - ` - )(AppContainer) -); +const enhanced = withLocalStateContainer( + graphql` + fragment AppContainerLocal on Local { + activeTab + } + ` +)(AppContainer); export default enhanced; diff --git a/src/core/client/stream/containers/CommentsCountContainer.tsx b/src/core/client/stream/containers/CommentsCountContainer.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/core/client/stream/containers/TabBarContainer.tsx b/src/core/client/stream/containers/TabBarContainer.tsx new file mode 100644 index 000000000..9dfb2f377 --- /dev/null +++ b/src/core/client/stream/containers/TabBarContainer.tsx @@ -0,0 +1,70 @@ +import * as React from "react"; +import { Component } from "react"; + +import { + graphql, + withFragmentContainer, + withLocalStateContainer, +} from "talk-framework/lib/relay"; + +import { TabBarContainer_story as StoryData } from "talk-stream/__generated__/TabBarContainer_story.graphql"; +import { TabBarContainerLocal as Local } from "talk-stream/__generated__/TabBarContainerLocal.graphql"; +import { + SetActiveTabInput, + SetActiveTabMutation, + withSetActiveTabMutation, +} from "talk-stream/mutations"; + +import TabBar from "../components/TabBar"; + +interface InnerProps { + story: StoryData | null; + local: Local; + setActiveTab: SetActiveTabMutation; +} + +export class TabBarContainer extends Component { + private handleSetActiveTab = (tab: SetActiveTabInput["tab"]) => { + this.props.setActiveTab({ tab }); + }; + + public render() { + const { + local: { loggedIn, activeTab }, + story, + } = this.props; + + const commentCount = (story && story.commentCounts.totalVisible) || -1; + return ( + + ); + } +} + +const enhanced = withSetActiveTabMutation( + withLocalStateContainer( + graphql` + fragment TabBarContainerLocal on Local { + loggedIn + activeTab + } + ` + )( + withFragmentContainer({ + story: graphql` + fragment TabBarContainer_story on Story { + commentCounts { + totalVisible + } + } + `, + })(TabBarContainer) + ) +); + +export default enhanced; diff --git a/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap b/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap index b7fe1473c..073f8648a 100644 --- a/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap +++ b/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap @@ -13,6 +13,9 @@ exports[`init local state 1`] = ` \\"__id\\": \\"client:root.local\\", \\"__typename\\": \\"Local\\", \\"authToken\\": \\"\\", + \\"authExp\\": null, + \\"authJTI\\": null, + \\"loggedIn\\": false, \\"network\\": { \\"__ref\\": \\"client:root.local.network\\" }, diff --git a/src/core/client/stream/local/constants.ts b/src/core/client/stream/local/constants.ts index d4f4bd473..44f5be973 100644 --- a/src/core/client/stream/local/constants.ts +++ b/src/core/client/stream/local/constants.ts @@ -1,9 +1,5 @@ /** * This files contains the various types and ids of the local schema. */ - -export const NETWORK_TYPE = "Network"; -export const NETWORK_ID = "client:root.local.network"; - export const AUTH_POPUP_TYPE = "AuthPopup"; export const AUTH_POPUP_ID = "client:root.local.authPopup"; diff --git a/src/core/client/stream/local/initLocalState.spec.ts b/src/core/client/stream/local/initLocalState.spec.ts index 710c0caa1..cee7eee7e 100644 --- a/src/core/client/stream/local/initLocalState.spec.ts +++ b/src/core/client/stream/local/initLocalState.spec.ts @@ -4,7 +4,10 @@ import { timeout } from "talk-common/utils"; import { TalkContext } from "talk-framework/lib/bootstrap"; import { LOCAL_ID } from "talk-framework/lib/relay"; import { createPromisifiedStorage } from "talk-framework/lib/storage"; -import { createRelayEnvironment } from "talk-framework/testHelpers"; +import { + createRelayEnvironment, + replaceHistoryLocation, +} from "talk-framework/testHelpers"; import initLocalState from "./initLocalState"; @@ -33,16 +36,12 @@ it("set storyID from query", async () => { localStorage: createPromisifiedStorage(), }; const storyID = "story-id"; - const previousLocation = location.toString(); - const previousState = window.history.state; - window.history.replaceState( - previousState, - document.title, + const restoreHistoryLocation = replaceHistoryLocation( `http://localhost/?storyID=${storyID}` ); await initLocalState(environment, context as any); expect(source.get(LOCAL_ID)!.storyID).toBe(storyID); - window.history.replaceState(previousState, document.title, previousLocation); + restoreHistoryLocation(); }); it("set commentID from query", async () => { @@ -50,23 +49,29 @@ it("set commentID from query", async () => { localStorage: createPromisifiedStorage(), }; const commentID = "comment-id"; - const previousLocation = location.toString(); - const previousState = window.history.state; - window.history.replaceState( - previousState, - document.title, + const restoreHistoryLocation = replaceHistoryLocation( `http://localhost/?commentID=${commentID}` ); await initLocalState(environment, context as any); expect(source.get(LOCAL_ID)!.commentID).toBe(commentID); - window.history.replaceState(previousState, document.title, previousLocation); + restoreHistoryLocation(); }); it("set authToken from localStorage", async () => { const context: Partial = { localStorage: createPromisifiedStorage(), }; - const authToken = "auth-token"; + const authToken = `${btoa( + JSON.stringify({ + alg: "HS256", + typ: "JWT", + }) + )}.${btoa( + JSON.stringify({ + exp: 1540503165, + jti: "31b26591-4e9a-4388-a7ff-e1bdc5d97cce", + }) + )}`; context.localStorage!.setItem("authToken", authToken); await initLocalState(environment, context as any); expect(source.get(LOCAL_ID)!.authToken).toBe(authToken); diff --git a/src/core/client/stream/local/initLocalState.ts b/src/core/client/stream/local/initLocalState.ts index 6febe0e6a..67cf06e6f 100644 --- a/src/core/client/stream/local/initLocalState.ts +++ b/src/core/client/stream/local/initLocalState.ts @@ -2,38 +2,21 @@ import qs from "query-string"; import { commitLocalUpdate, Environment } from "relay-runtime"; import { TalkContext } from "talk-framework/lib/bootstrap"; -import { - createAndRetain, - LOCAL_ID, - LOCAL_TYPE, -} from "talk-framework/lib/relay"; +import { createAndRetain, initLocalBaseState } from "talk-framework/lib/relay"; -import { - AUTH_POPUP_ID, - AUTH_POPUP_TYPE, - NETWORK_ID, - NETWORK_TYPE, -} from "./constants"; +import { AUTH_POPUP_ID, AUTH_POPUP_TYPE } from "./constants"; /** * Initializes the local state, before we start the App. */ export default async function initLocalState( environment: Environment, - { localStorage }: TalkContext + context: TalkContext ) { - const authToken = await localStorage!.getItem("authToken"); - + await initLocalBaseState(environment, context); commitLocalUpdate(environment, s => { - // TODO: (cvle) move local, auth token and network initialization to framework. const root = s.getRoot(); - - // Create the Local Record which is the Root for the client states. - const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE); - root.setLinkedRecord(localRecord, "local"); - - // Set auth token - localRecord.setValue(authToken || "", "authToken"); + const localRecord = root.getLinkedRecord("local")!; // Parse query params const query = qs.parse(location.search); @@ -50,16 +33,6 @@ export default async function initLocalState( localRecord.setValue(query.commentID, "commentID"); } - // Create network Record - const networkRecord = createAndRetain( - environment, - s, - NETWORK_ID, - NETWORK_TYPE - ); - networkRecord.setValue(false, "isOffline"); - localRecord.setLinkedRecord(networkRecord, "network"); - // Create authPopup Record const authPopupRecord = createAndRetain( environment, diff --git a/src/core/client/stream/local/local.graphql b/src/core/client/stream/local/local.graphql index 1408f33fc..4b254b40a 100644 --- a/src/core/client/stream/local/local.graphql +++ b/src/core/client/stream/local/local.graphql @@ -29,12 +29,15 @@ extend type Comment { type Local { network: Network! + authToken: String + authExp: Int + authJTI: String + loggedIn: Boolean! + activeTab: Tab! + authPopup: AuthPopup! storyID: String storyURL: String commentID: String - authPopup: AuthPopup! - authToken: String - activeTab: Tab! } extend type Query { diff --git a/src/core/client/stream/mutations/index.ts b/src/core/client/stream/mutations/index.ts index ef2364995..648ae90cc 100644 --- a/src/core/client/stream/mutations/index.ts +++ b/src/core/client/stream/mutations/index.ts @@ -8,11 +8,6 @@ export { EditCommentMutation, EditCommentInput, } from "./EditCommentMutation"; -export { - withSetNetworkStatusMutation, - SetNetworkStatusMutation, - SetNetworkStatusInput, -} from "./SetNetworkStatusMutation"; export { withSetCommentIDMutation, SetCommentIDMutation, diff --git a/src/core/client/stream/queries/CommentsCountQuery.tsx b/src/core/client/stream/queries/CommentsCountQuery.tsx deleted file mode 100644 index 063826255..000000000 --- a/src/core/client/stream/queries/CommentsCountQuery.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { Component } from "react"; -import { - graphql, - QueryRenderer, - withLocalStateContainer, -} from "talk-framework/lib/relay"; -import { CommentsCountQuery as QueryTypes } from "talk-stream/__generated__/CommentsCountQuery.graphql"; -import { CommentsCountQueryLocal as Local } from "talk-stream/__generated__/CommentsCountQueryLocal.graphql"; -import { Spinner } from "talk-ui/components"; -import { Tab } from "talk-ui/components"; -import { PropTypesOf } from "talk-ui/types"; -import CommentCountTab from "../components/CommentCountTab"; - -interface InnerProps extends PropTypesOf { - local: Local; -} - -class CommentsCountQuery extends Component { - public render() { - const { storyID, storyURL } = this.props.local; - const { local: _, ...rest } = this.props; - return ( - - query={graphql` - query CommentsCountQuery($storyID: ID, $storyURL: String) { - story(id: $storyID, url: $storyURL) { - commentCounts { - totalVisible - } - } - } - `} - variables={{ - storyID, - storyURL, - }} - render={({ error, props }) => { - if (error) { - return
{error.message}
; - } - - if (props && props.story) { - return ( - - ); - } - - return ; - }} - /> - ); - } -} - -const enhanced = withLocalStateContainer( - graphql` - fragment CommentsCountQueryLocal on Local { - storyID - storyURL - } - ` -)(CommentsCountQuery); - -export default enhanced; diff --git a/src/core/client/stream/queries/IfLoggedInQuery.tsx b/src/core/client/stream/queries/IfLoggedInQuery.tsx deleted file mode 100644 index 19687f631..000000000 --- a/src/core/client/stream/queries/IfLoggedInQuery.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { Component } from "react"; - -import { graphql, QueryRenderer } from "talk-framework/lib/relay"; -import { IfLoggedInQuery as QueryTypes } from "talk-stream/__generated__/IfLoggedInQuery.graphql"; - -class IfLoggedInContainer extends Component { - public render() { - return ( - - query={graphql` - query IfLoggedInQuery { - me { - id - } - } - `} - render={({ error, props }) => { - if (error) { - return
{error.message}
; - } - - if (props && props.me) { - return <>{this.props.children}; - } - - return null; - }} - variables={{}} - /> - ); - } -} - -export default IfLoggedInContainer; diff --git a/src/core/client/stream/queries/TabBarQuery.tsx b/src/core/client/stream/queries/TabBarQuery.tsx new file mode 100644 index 000000000..bd1064988 --- /dev/null +++ b/src/core/client/stream/queries/TabBarQuery.tsx @@ -0,0 +1,54 @@ +import React, { Component } from "react"; +import { + graphql, + QueryRenderer, + withLocalStateContainer, +} from "talk-framework/lib/relay"; + +import { TabBarQuery as QueryTypes } from "talk-stream/__generated__/TabBarQuery.graphql"; +import { TabBarQueryLocal as Local } from "talk-stream/__generated__/TabBarQueryLocal.graphql"; + +import TabBarContainer from "../containers/TabBarContainer"; + +interface InnerProps { + local: Local; +} + +class TabBarQuery extends Component { + public render() { + const { storyID, storyURL } = this.props.local; + return ( + + query={graphql` + query TabBarQuery($storyID: ID, $storyURL: String) { + story(id: $storyID, url: $storyURL) { + ...TabBarContainer_story + } + } + `} + variables={{ + storyID, + storyURL, + }} + render={({ error, props }) => { + if (error) { + return
{error.message}
; + } + + return ; + }} + /> + ); + } +} + +const enhanced = withLocalStateContainer( + graphql` + fragment TabBarQueryLocal on Local { + storyID + storyURL + } + ` +)(TabBarQuery); + +export default enhanced; diff --git a/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap index 8d606219b..06aefabe2 100644 --- a/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/editComment.spec.tsx.snap @@ -27,7 +27,9 @@ exports[`cancel edit: edit canceled 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments +
  • - My Profile + + My Profile +
  • @@ -540,7 +544,9 @@ exports[`edit a comment: edit form 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments +
  • - My Profile + + My Profile +
  • @@ -1076,7 +1084,9 @@ exports[`edit a comment: optimistic response 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments +
  • - My Profile + + My Profile +
  • @@ -1612,7 +1624,9 @@ exports[`edit a comment: render stream 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments +
  • - My Profile + + My Profile +
  • @@ -2125,7 +2141,9 @@ exports[`edit a comment: server response 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments +
  • - My Profile + + My Profile +
  • @@ -2647,7 +2667,9 @@ exports[`shows expiry message: edit form closed 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments +
  • - My Profile + + My Profile +
  • @@ -3160,7 +3184,9 @@ exports[`shows expiry message: edit time expired 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments +
  • - My Profile + + My Profile +
  • diff --git a/src/core/client/stream/test/comments/__snapshots__/loadMore.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/loadMore.spec.tsx.snap index 6cbdf0852..66c898c8a 100644 --- a/src/core/client/stream/test/comments/__snapshots__/loadMore.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/loadMore.spec.tsx.snap @@ -27,7 +27,9 @@ exports[`loads more comments 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments + @@ -589,7 +591,9 @@ exports[`renders comment stream 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments + diff --git a/src/core/client/stream/test/comments/__snapshots__/permalinkView.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/permalinkView.spec.tsx.snap index ee62b5cc6..304440f0f 100644 --- a/src/core/client/stream/test/comments/__snapshots__/permalinkView.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/permalinkView.spec.tsx.snap @@ -27,7 +27,9 @@ exports[`renders permalink view 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments + @@ -834,7 +836,9 @@ exports[`show all comments 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments + diff --git a/src/core/client/stream/test/comments/__snapshots__/permalinkViewCommentNotFound.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/permalinkViewCommentNotFound.spec.tsx.snap index bde03fd75..0cd2d17f6 100644 --- a/src/core/client/stream/test/comments/__snapshots__/permalinkViewCommentNotFound.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/permalinkViewCommentNotFound.spec.tsx.snap @@ -27,7 +27,9 @@ exports[`renders permalink view with unknown comment 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments + @@ -137,7 +139,9 @@ exports[`show all comments 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments + diff --git a/src/core/client/stream/test/comments/__snapshots__/permalinkViewLoadMoreParents.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/permalinkViewLoadMoreParents.spec.tsx.snap index fcd6cb2a1..7eacde3f6 100644 --- a/src/core/client/stream/test/comments/__snapshots__/permalinkViewLoadMoreParents.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/permalinkViewLoadMoreParents.spec.tsx.snap @@ -27,7 +27,9 @@ exports[`renders permalink view 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments + @@ -381,7 +383,9 @@ exports[`views pervious comments 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments + diff --git a/src/core/client/stream/test/comments/__snapshots__/permalinkViewStoryNotFound.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/permalinkViewStoryNotFound.spec.tsx.snap index 251c501e0..55249b6f1 100644 --- a/src/core/client/stream/test/comments/__snapshots__/permalinkViewStoryNotFound.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/permalinkViewStoryNotFound.spec.tsx.snap @@ -8,23 +8,30 @@ exports[`renders permalink view with unknown story 1`] = ` className="TabBar-root TabBar-primary" role="tablist" > - - - + +
    - ⁨3⁩ ⁨Comments⁩ + + ⁨3⁩ Comments +
  • - My Profile + + My Profile +
  • @@ -660,7 +664,9 @@ exports[`post a comment: server response 1`] = ` role="tab" type="button" > - ⁨3⁩ ⁨Comments⁩ + + ⁨3⁩ Comments +
  • - My Profile + + My Profile +
  • @@ -1283,7 +1291,9 @@ exports[`renders comment stream 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments +
  • - My Profile + + My Profile +
  • diff --git a/src/core/client/stream/test/comments/__snapshots__/postLocalReply.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/postLocalReply.spec.tsx.snap index 27b9ee6a5..778e60021 100644 --- a/src/core/client/stream/test/comments/__snapshots__/postLocalReply.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/postLocalReply.spec.tsx.snap @@ -27,7 +27,9 @@ exports[`post a reply: open reply form 1`] = ` role="tab" type="button" > - ⁨1⁩ ⁨Comment⁩ + + ⁨1⁩ Comment +
  • - My Profile + + My Profile +
  • @@ -1224,7 +1228,9 @@ exports[`post a reply: optimistic response 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments +
  • - My Profile + + My Profile +
  • @@ -2547,7 +2555,9 @@ exports[`post a reply: server response 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments +
  • - My Profile + + My Profile +
  • @@ -3710,7 +3722,9 @@ exports[`renders comment stream 1`] = ` role="tab" type="button" > - ⁨1⁩ ⁨Comment⁩ + + ⁨1⁩ Comment +
  • - My Profile + + My Profile +
  • diff --git a/src/core/client/stream/test/comments/__snapshots__/postReply.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/postReply.spec.tsx.snap index f4341ca95..8114a0987 100644 --- a/src/core/client/stream/test/comments/__snapshots__/postReply.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/postReply.spec.tsx.snap @@ -27,7 +27,9 @@ exports[`post a reply: open reply form 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments +
  • - My Profile + + My Profile +
  • @@ -674,7 +678,9 @@ exports[`post a reply: optimistic response 1`] = ` role="tab" type="button" > - ⁨3⁩ ⁨Comments⁩ + + ⁨3⁩ Comments +
  • - My Profile + + My Profile +
  • @@ -1463,7 +1471,9 @@ exports[`post a reply: server response 1`] = ` role="tab" type="button" > - ⁨3⁩ ⁨Comments⁩ + + ⁨3⁩ Comments +
  • - My Profile + + My Profile +
  • @@ -2092,7 +2104,9 @@ exports[`renders comment stream 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments +
  • - My Profile + + My Profile +
  • diff --git a/src/core/client/stream/test/comments/__snapshots__/renderReplies.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/renderReplies.spec.tsx.snap index d93f13749..962e4d499 100644 --- a/src/core/client/stream/test/comments/__snapshots__/renderReplies.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/renderReplies.spec.tsx.snap @@ -27,7 +27,9 @@ exports[`renders comment stream 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments + diff --git a/src/core/client/stream/test/comments/__snapshots__/renderStream.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/renderStream.spec.tsx.snap index fa4efd937..05250ec3f 100644 --- a/src/core/client/stream/test/comments/__snapshots__/renderStream.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/renderStream.spec.tsx.snap @@ -27,7 +27,9 @@ exports[`renders comment stream 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments + diff --git a/src/core/client/stream/test/comments/__snapshots__/showAllReplies.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/showAllReplies.spec.tsx.snap index 531e75b6f..0dfe5f46e 100644 --- a/src/core/client/stream/test/comments/__snapshots__/showAllReplies.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/showAllReplies.spec.tsx.snap @@ -27,7 +27,9 @@ exports[`renders comment stream 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments + @@ -493,7 +495,9 @@ exports[`show all replies 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ + + ⁨2⁩ Comments + diff --git a/src/core/client/stream/test/comments/__snapshots__/showConversation.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/showConversation.spec.tsx.snap index a32c35843..d79429976 100644 --- a/src/core/client/stream/test/comments/__snapshots__/showConversation.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/showConversation.spec.tsx.snap @@ -27,7 +27,9 @@ exports[`renders comment stream 1`] = ` role="tab" type="button" > - ⁨1⁩ ⁨Comment⁩ + + ⁨1⁩ Comment + @@ -1013,7 +1015,9 @@ exports[`shows conversation 1`] = ` role="tab" type="button" > - ⁨1⁩ ⁨Comment⁩ + + ⁨1⁩ Comment + diff --git a/src/core/client/stream/test/comments/create.ts b/src/core/client/stream/test/comments/create.ts index 090a3e3f1..d7995a51e 100644 --- a/src/core/client/stream/test/comments/create.ts +++ b/src/core/client/stream/test/comments/create.ts @@ -6,6 +6,7 @@ export default function create(params: CreateParams) { initLocalState: (localRecord, source, environment) => { if (params.initLocalState) { localRecord.setValue("COMMENTS", "activeTab"); + localRecord.setValue(false, "loggedIn"); params.initLocalState(localRecord, source, environment); } }, diff --git a/src/core/client/stream/test/comments/editComment.spec.tsx b/src/core/client/stream/test/comments/editComment.spec.tsx index 0e0d918b4..1c75b4f22 100644 --- a/src/core/client/stream/test/comments/editComment.spec.tsx +++ b/src/core/client/stream/test/comments/editComment.spec.tsx @@ -53,6 +53,7 @@ function createTestRenderer() { resolvers, initLocalState: localRecord => { localRecord.setValue(stories[0].id, "storyID"); + localRecord.setValue(true, "loggedIn"); }, }); return testRenderer; diff --git a/src/core/client/stream/test/comments/postComment.spec.tsx b/src/core/client/stream/test/comments/postComment.spec.tsx index e2994ef53..790c344b2 100644 --- a/src/core/client/stream/test/comments/postComment.spec.tsx +++ b/src/core/client/stream/test/comments/postComment.spec.tsx @@ -57,6 +57,7 @@ beforeEach(() => { resolvers, initLocalState: localRecord => { localRecord.setValue(stories[0].id, "storyID"); + localRecord.setValue(true, "loggedIn"); }, })); }); diff --git a/src/core/client/stream/test/comments/postLocalReply.spec.tsx b/src/core/client/stream/test/comments/postLocalReply.spec.tsx index 1bf80315c..e56f23d16 100644 --- a/src/core/client/stream/test/comments/postLocalReply.spec.tsx +++ b/src/core/client/stream/test/comments/postLocalReply.spec.tsx @@ -62,6 +62,7 @@ beforeEach(() => { resolvers, initLocalState: localRecord => { localRecord.setValue(storyWithDeepestReplies.id, "storyID"); + localRecord.setValue(true, "loggedIn"); }, })); }); diff --git a/src/core/client/stream/test/comments/postReply.spec.tsx b/src/core/client/stream/test/comments/postReply.spec.tsx index 62f3d8465..17baf2a1c 100644 --- a/src/core/client/stream/test/comments/postReply.spec.tsx +++ b/src/core/client/stream/test/comments/postReply.spec.tsx @@ -57,6 +57,7 @@ beforeEach(() => { resolvers, initLocalState: localRecord => { localRecord.setValue(stories[0].id, "storyID"); + localRecord.setValue(true, "loggedIn"); }, })); }); diff --git a/src/core/client/stream/test/profile/__snapshots__/loadMore.spec.tsx.snap b/src/core/client/stream/test/profile/__snapshots__/loadMore.spec.tsx.snap index 392adb7fa..868221212 100644 --- a/src/core/client/stream/test/profile/__snapshots__/loadMore.spec.tsx.snap +++ b/src/core/client/stream/test/profile/__snapshots__/loadMore.spec.tsx.snap @@ -27,29 +27,9 @@ exports[`loads more comments 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ - - - @@ -307,29 +287,9 @@ exports[`renders comment stream 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ - - - diff --git a/src/core/client/stream/test/profile/__snapshots__/renderProfile.spec.tsx.snap b/src/core/client/stream/test/profile/__snapshots__/renderProfile.spec.tsx.snap index 32c692d8e..f65586d21 100644 --- a/src/core/client/stream/test/profile/__snapshots__/renderProfile.spec.tsx.snap +++ b/src/core/client/stream/test/profile/__snapshots__/renderProfile.spec.tsx.snap @@ -27,29 +27,9 @@ exports[`renders profile 1`] = ` role="tab" type="button" > - ⁨2⁩ ⁨Comments⁩ - - - diff --git a/src/core/client/ui/theme/variables.ts b/src/core/client/ui/theme/variables.ts index a98a55ba2..b8ba80e38 100644 --- a/src/core/client/ui/theme/variables.ts +++ b/src/core/client/ui/theme/variables.ts @@ -61,6 +61,7 @@ const variables = { }, /* Divider */ divider: "rgba(0, 0, 0, 0.12)", + brand: "#F77160", }, /* gitter and spacing */ spacingUnitSmall: 5, @@ -74,6 +75,7 @@ const variables = { fontWeightLight: 300, fontWeightRegular: 400, fontWeightMedium: 600, + fontWeightBold: 700, /* Breakpoints */ breakpoints: { xs: 320, diff --git a/src/locales/en-US/admin.ftl b/src/locales/en-US/admin.ftl new file mode 100644 index 000000000..2867cd485 --- /dev/null +++ b/src/locales/en-US/admin.ftl @@ -0,0 +1,27 @@ +### Localization for Admin + +## General + +general-brandName = { -product-name } + +## Navigation +navigation-moderate = moderate +navigation-community = community +navigation-stories = stories +navigation-configure = configure +navigation-signOutButton = Sign Out + +## Login + +login-login-signInTo = Sign in to +login-login-enterAccountDetailsBelow = Enter your account details below + +login-signIn-emailAddressLabel = Email Address +login-signIn-emailAddressTextField = + .placeholder = Email Address + +login-signIn-passwordLabel = Password +login-signIn-passwordTextField = + .placeholder = Password + +login-signIn-signIn = Sign in diff --git a/src/locales/en-US/common.ftl b/src/locales/en-US/common.ftl index d7ce40012..07aefc95b 100644 --- a/src/locales/en-US/common.ftl +++ b/src/locales/en-US/common.ftl @@ -1 +1,2 @@ -brand-name = The Coral Project +-product-name = Talk diff --git a/src/locales/en-US/stream.ftl b/src/locales/en-US/stream.ftl index 2dd6c4ea3..30b16047d 100644 --- a/src/locales/en-US/stream.ftl +++ b/src/locales/en-US/stream.ftl @@ -12,12 +12,12 @@ general-userBoxAuthenticated-signedInAs = general-userBoxAuthenticated-notYou = Not you? -general-app-commentsTab = { SHORT_NUMBER($commentCount) } { $commentCount -> - [0] Comments - [1] Comment - *[other] Comments +general-tabBar-commentsTab = { $commentCount -> + [-1] Comments + [1] { SHORT_NUMBER($commentCount) } Comment + *[other] { SHORT_NUMBER($commentCount) } Comments } -general-app-myProfileTab = My Profile +general-tabBar-myProfileTab = My Profile ## Comments Tab diff --git a/src/types/images.d.ts b/src/types/images.d.ts new file mode 100644 index 000000000..89e5fcf1d --- /dev/null +++ b/src/types/images.d.ts @@ -0,0 +1,2 @@ +// Allowing loading svg files. +declare module "*.svg";