[next] Implement configuration panes (#2173)

* feat: moderation config

* feat: configure banned and suspect words

* chore: upgrade react and test libs to the newest version <3

* chore: upgrade typescript + some refactor

* feat: general, organization and advanced configuration panes

* fix: translation

* feat: speedup fetching markdown editor

* feat: localize markdown editor

* chore: refactor container names

* chore: rename infobox to communityGuidelines

* feat: closing comment streams duration config

* test: add feature tests for configurations

* fix: mock only console.error

* chore: upgrade node

* chore: require node >= 10

* fix: better validation and default values

* feat: Make DurationField a general purpose component and reuse for Edit Comment Timeframe

* test: add unit test for duration field

* fix: patch for bug when built in production

* chore: bump npm version to latest

* fix: adapted Dockerfile to new version of node

* refactor: harmonized seconds/milliseconds to seconds

* fix: resolve bug from merge conflict
This commit is contained in:
Kiwi
2019-02-07 01:10:51 +01:00
committed by GitHub
parent 9fa5900acc
commit 53e168ae93
228 changed files with 10582 additions and 2774 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ job_environment: &job_environment
job_defaults: &job_defaults
working_directory: ~/coralproject/talk
docker:
- image: circleci/node:8
- image: circleci/node:10
environment:
<<: *job_environment
@@ -103,7 +103,7 @@ jobs:
command: npm run build
- run:
name: Verify Bundle Size
command: npm run bundlesize
command: npx bundlesize
- save_cache:
key: v1-build-cache-{{ .Branch }}-{{ .Revision }}
paths:
+2 -2
View File
@@ -1,4 +1,4 @@
FROM node:8-alpine
FROM node:10-alpine
# Install build dependancies.
RUN apk --no-cache add git
@@ -19,7 +19,7 @@ RUN NODE_ENV=development npm install && \
npm run build && \
npm prune --production
FROM node:8-alpine
FROM node:10-alpine
# Create app directory
RUN mkdir -p /usr/src/app
+167 -623
View File
File diff suppressed because it is too large Load Diff
+19 -17
View File
@@ -11,8 +11,8 @@
"url": "git://github.com/coralproject/talk.git"
},
"engines": {
"node": ">=8.9.0",
"npm": ">=6.4.1"
"node": ">=10.0.0",
"npm": ">=6.7.0"
},
"bugs": "https://github.com/coralproject/talk/issues",
"contributors": "https://github.com/coralproject/talk/graphs/contributors",
@@ -21,7 +21,6 @@
"build": "npm-run-all generate --parallel build:*",
"build:client": "ts-node ./scripts/build.ts",
"build:server": "gulp server",
"bundlesize": "bundlesize",
"generate": "npm-run-all --parallel generate:*",
"generate:css-types": "tcm src/core/client/",
"generate:relay-stream": "ts-node ./scripts/compileRelay --src ./src/core/client/stream --schema tenant",
@@ -135,8 +134,8 @@
"@types/cors": "^2.8.4",
"@types/cross-spawn": "^6.0.0",
"@types/dotenv": "^4.0.3",
"@types/enzyme": "^3.1.11",
"@types/enzyme-adapter-react-16": "^1.0.2",
"@types/enzyme": "^3.1.15",
"@types/enzyme-adapter-react-16": "^1.0.3",
"@types/escape-string-regexp": "^1.0.0",
"@types/eventemitter2": "^4.1.0",
"@types/express": "^4.16.0",
@@ -165,15 +164,18 @@
"@types/passport-local": "^1.0.33",
"@types/passport-oauth2": "^1.4.5",
"@types/passport-strategy": "^0.2.33",
"@types/prop-types": "^15.5.8",
"@types/react": "^16.7.20",
"@types/react-copy-to-clipboard": "^4.2.5",
"@types/react-dom": "^16.0.6",
"@types/react-dom": "^16.0.11",
"@types/react-relay": "^1.3.9",
"@types/react-responsive": "^3.0.1",
"@types/react-test-renderer": "^16.0.1",
"@types/react-test-renderer": "^16.0.3",
"@types/react-transition-group": "^2.0.14",
"@types/recompose": "^0.26.5",
"@types/relay-runtime": "^1.3.6",
"@types/sane": "^2.0.0",
"@types/simplemde": "^1.11.7",
"@types/sinon": "^5.0.1",
"@types/source-map-support": "^0.4.1",
"@types/stack-utils": "^1.0.1",
@@ -198,7 +200,6 @@
"babel-plugin-use-lodash-es": "^0.2.0",
"babel-preset-react-optimize": "^1.0.1",
"bowser": "^1.9.4",
"bundlesize": "^0.17.0",
"case-sensitive-paths-webpack-plugin": "^2.1.2",
"chalk": "^2.4.1",
"chokidar": "^2.0.4",
@@ -211,16 +212,16 @@
"css-loader": "^1.0.1",
"del": "^3.0.0",
"docz": "^0.5.8",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.6.0",
"enzyme-to-json": "^3.3.4",
"enzyme": "^3.8.0",
"enzyme-adapter-react-16": "^1.7.1",
"enzyme-to-json": "^3.3.5",
"eventemitter2": "^5.0.1",
"final-form": "^4.8.1",
"flat": "^4.1.0",
"fluent": "^0.8.0",
"fluent": "^0.10.0",
"fluent-intl-polyfill": "^0.1.0",
"fluent-langneg": "^0.1.0",
"fluent-react": "^0.8.0",
"fluent-react": "^0.8.3",
"graphql-schema-typescript": "^1.2.1",
"gulp": "^4.0.0",
"gulp-babel": "^8.0.0",
@@ -256,15 +257,15 @@
"pym.js": "^1.3.2",
"querystringify": "^2.1.0",
"raw-loader": "^0.5.1",
"react": "^16.5.2",
"react": "^16.7.0",
"react-copy-to-clipboard": "^5.0.1",
"react-dev-utils": "6.0.0-next.3e165448",
"react-dom": "^16.5.2",
"react-dom": "^16.7.0",
"react-final-form": "^3.6.4",
"react-popper": "^1.3.2",
"react-relay": "^1.7.0-rc.1",
"react-responsive": "^5.0.0",
"react-test-renderer": "^16.5.2",
"react-test-renderer": "^16.7.0",
"react-timeago": "^4.1.9",
"react-transition-group": "^2.5.0",
"react-with-state-props": "^2.0.4",
@@ -274,6 +275,7 @@
"relay-local-schema": "^0.7.0",
"relay-runtime": "^1.7.0-rc.1",
"sane": "^4.0.2",
"simplemde": "^1.11.2",
"simulant": "^0.2.2",
"sinon": "^6.1.5",
"style-loader": "^0.23.1",
@@ -292,7 +294,7 @@
"typed-css-modules": "^0.3.4",
"typeface-manuale": "0.0.54",
"typeface-source-sans-pro": "0.0.54",
"typescript": "^3.0.3",
"typescript": "3.1.6",
"typescript-snapshots-plugin": "^1.2.0",
"wait-for-expect": "^1.1.0",
"webpack": "^4.27.1",
+2 -3
View File
@@ -1,3 +1,2 @@
__webpack_public_path__ = JSON.parse(
document.getElementById("config").innerText
);
__webpack_public_path__ =
JSON.parse(document.getElementById("config").innerText) || "/";
@@ -41,7 +41,7 @@ class DecisionHistoryButton extends React.Component {
<BaseButton
onClick={() => this.toggleVisibilityOncePerFrame(toggleVisibility)}
aria-controls={popoverID}
forwardRef={forwardRef}
ref={forwardRef}
className={styles.historyIcon}
data-testid="decisionHistory-toggle"
>
@@ -4,14 +4,14 @@ exports[`renders correctly 1`] = `
<div
data-testid="authBox"
>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
justifyContent="center"
>
<withPropsOnChange(HorizontalGutter)
<ForwardRef(forwardRef)
className="AuthBox-container"
size="double"
>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
justifyContent="center"
>
<div
@@ -21,21 +21,21 @@ exports[`renders correctly 1`] = `
size="lg"
/>
</div>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
<div>
<withPropsOnChange(Typography)
<ForwardRef(forwardRef)
align="center"
variant="heading3"
>
title
</withPropsOnChange(Typography)>
</ForwardRef(forwardRef)>
<withPropsOnChange(BrandName)
align="center"
size="lg"
/>
</div>
content
</withPropsOnChange(HorizontalGutter)>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</div>
`;
@@ -4,12 +4,12 @@ exports[`renders correctly 1`] = `
<Localized
id="navigation-signOutButton"
>
<withPropsOnChange(Button)
<ForwardRef(forwardRef)
className="SignOutButton-root"
id="id"
onClick={[Function]}
>
Sign Out
</withPropsOnChange(Button)>
</ForwardRef(forwardRef)>
</Localized>
`;
@@ -19,16 +19,13 @@ const mutation = graphql`
updateSettings(input: $input) {
settings {
auth {
...FacebookConfigContainer_auth
...FacebookConfigContainer_authReadOnly
...GoogleConfigContainer_auth
...GoogleConfigContainer_authReadOnly
...SSOConfigContainer_auth
...SSOConfigContainer_authReadOnly
...OIDCConfigContainer_auth
...OIDCConfigContainer_authReadOnly
...DisplayNamesConfigContainer_auth
...AuthConfigContainer_auth
}
...ModerationConfigContainer_settings
...GeneralConfigContainer_settings
...OrganizationConfigContainer_settings
...WordListConfigContainer_settings
...AdvancedConfigContainer_settings
}
clientMutationId
}
+27 -4
View File
@@ -4,9 +4,13 @@ import React from "react";
import App from "./components/App";
import AuthCheckContainer from "./containers/AuthCheckContainer";
import Community from "./routes/community/components/Community";
import ConfigureModeration from "./routes/configure/components/Moderation";
import ConfigureContainer from "./routes/configure/containers/ConfigureContainer";
import ConfigureAuthRouteContainer from "./routes/configure/sections/auth/containers/AuthRouteContainer";
import ConfigureAdvancedRouteContainer from "./routes/configure/sections/advanced/containers/AdvancedConfigRouteContainer";
import ConfigureAuthRouteContainer from "./routes/configure/sections/auth/containers/AuthConfigRouteContainer";
import ConfigureGeneralRouteContainer from "./routes/configure/sections/general/containers/GeneralConfigRouteContainer";
import ConfigureModerationRouteContainer from "./routes/configure/sections/moderation/containers/ModerationConfigRouteContainer";
import ConfigureOrganizationRouteContainer from "./routes/configure/sections/organization/containers/OrganizationRouteContainer";
import ConfigureWordListRouteContainer from "./routes/configure/sections/wordList/containers/WordListRouteContainer";
import LoginContainer from "./routes/login/containers/LoginContainer";
import ModerateContainer from "./routes/moderate/containers/ModerateContainer";
import {
@@ -40,9 +44,28 @@ export default makeRouteConfig(
<Route path="community" Component={Community} />
<Route path="stories" Component={Stories} />
<Route path="configure" Component={ConfigureContainer}>
<Redirect from="/" to="/admin/configure/moderation" />
<Route path="moderation" Component={ConfigureModeration} />
<Redirect from="/" to="/admin/configure/general" />
<Route
path="general"
{...ConfigureGeneralRouteContainer.routeConfig}
/>
<Route
path="organization"
{...ConfigureOrganizationRouteContainer.routeConfig}
/>
<Route
path="moderation"
{...ConfigureModerationRouteContainer.routeConfig}
/>
<Route
path="wordList"
{...ConfigureWordListRouteContainer.routeConfig}
/>
<Route path="auth" {...ConfigureAuthRouteContainer.routeConfig} />
<Route
path="advanced"
{...ConfigureAdvancedRouteContainer.routeConfig}
/>
</Route>
</Route>
</Route>
@@ -2,10 +2,10 @@
exports[`renders correctly 1`] = `
<MainLayout>
<withPropsOnChange(Typography)
<ForwardRef(forwardRef)
variant="heading3"
>
Community
</withPropsOnChange(Typography)>
</ForwardRef(forwardRef)>
</MainLayout>
`;
@@ -0,0 +1,12 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import Subheader from "../components/Subheader";
const ConfigurationSubHeader: StatelessComponent<{}> = () => (
<Localized id="configure-configurationSubHeader" strong={<strong />}>
<Subheader>Configuration</Subheader>
</Localized>
);
export default ConfigurationSubHeader;
@@ -30,9 +30,25 @@ const Configure: StatelessComponent<Props> = ({
<SideBar>
<HorizontalGutter size="double">
<Navigation>
<Link to="/admin/configure/moderation">Moderation</Link>
<Localized id="configure-sideBarNavigation-general">
<Link to="/admin/configure/general">General</Link>
</Localized>
<Localized id="configure-sideBarNavigation-organization">
<Link to="/admin/configure/organization">Organization</Link>
</Localized>
<Localized id="configure-sideBarNavigation-moderation">
<Link to="/admin/configure/moderation">Moderation</Link>
</Localized>
<Localized id="configure-sideBarNavigation-bannedAndSuspectWords">
<Link to="/admin/configure/wordList">
Banned and Suspect Words
</Link>
</Localized>
<Localized id="configure-sideBarNavigation-authentication">
<Link to="/admin/configure/auth">Auth</Link>
<Link to="/admin/configure/auth">Authentication</Link>
</Localized>
<Localized id="configure-sideBarNavigation-advanced">
<Link to="/admin/configure/advanced">Advanced</Link>
</Localized>
</Navigation>
</HorizontalGutter>
@@ -2,4 +2,6 @@
padding-bottom: calc(0.5 * var(--spacing-unit));
border-bottom: 1px solid var(--palette-text-primary);
margin-bottom: calc(1.5 * var(--spacing-unit));
display: block;
width: 100%;
}
@@ -1,10 +1,23 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
import { PropTypesOf } from "talk-ui/types";
import styles from "./Header.css";
const Header: StatelessComponent = ({ children }) => (
<Typography variant="heading1" className={styles.root}>
type Props = PropTypesOf<typeof Typography>;
const Header: StatelessComponent<Props> = ({
children,
className,
...rest
}) => (
<Typography
variant="heading1"
className={cn(className, styles.root)}
{...rest}
>
{children}
</Typography>
);
@@ -1,19 +0,0 @@
import React, { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
import Header from "./Header";
const Moderation: StatelessComponent = ({ children }) => (
<div>
<Header>Perspective Toxic Comment Filter</Header>
<Typography>
Using the Perspective API, the Toxic Comment filter warns users when
comments exceed the predefined toxicity threshold. Toxic comments will not
be published and are placed in the Pending Queue for review by a
moderator. If approved by a moderator, the comment will be published.
</Typography>
</div>
);
export default Moderation;
@@ -1,26 +1,18 @@
.link {
composes: sideNavigationItem from "talk-ui/shared/typography.css";
display: inline-block;
text-decoration: none;
text-transform: uppercase;
padding: calc(0.5 * var(--spacing-unit)) var(--spacing-unit)
calc(0.5 * var(--spacing-unit)) calc(var(--spacing-unit) + 2px);
margin-left: 2px;
border-left: 1px solid var(--palette-grey-lighter);
color: var(--palette-text-primary);
font-family: var(--font-family-sans-serif);
font-weight: var(--font-weight-regular);
font-size: calc(18rem / var(--rem-base));
line-height: calc(20em / 18);
letter-spacing: calc(0.2em / 18);
&:hover {
cursor: pointer;
}
}
.linkActive {
font-weight: var(--font-weight-bold);
composes: sideNavigationActive from "talk-ui/shared/typography.css";
margin-left: 0px;
border-left: calc(0.5 * var(--spacing-unit)) solid var(--palette-brand-main);
padding-left: var(--spacing-unit);
@@ -0,0 +1,61 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { parseStringBool } from "talk-framework/lib/form";
import { Validator } from "talk-framework/lib/validation";
import { RadioButton } from "talk-ui/components";
interface Props {
validate?: Validator;
name: string;
disabled: boolean;
invert?: boolean;
}
const OnOffField: StatelessComponent<Props> = ({
name,
disabled,
invert = false,
}) => (
<div>
<Field name={name} type="radio" parse={parseStringBool} value={!invert}>
{({ input }) => (
<Localized id="configure-onOffField-on">
<RadioButton
id={`${input.name}-true`}
name={input.name}
onChange={input.onChange}
onFocus={input.onFocus}
onBlur={input.onBlur}
checked={input.checked}
disabled={disabled}
value={input.value}
>
On
</RadioButton>
</Localized>
)}
</Field>
<Field name={name} type="radio" parse={parseStringBool} value={invert}>
{({ input }) => (
<Localized id="configure-onOffField-off">
<RadioButton
id={`${input.name}-fase`}
name={input.name}
onChange={input.onChange}
onFocus={input.onFocus}
onBlur={input.onBlur}
checked={input.checked}
disabled={disabled}
value={input.value}
>
Off
</RadioButton>
</Localized>
)}
</Field>
</div>
);
export default OnOffField;
@@ -0,0 +1,61 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { parseStringBool } from "talk-framework/lib/form";
import { Validator } from "talk-framework/lib/validation";
import { RadioButton } from "talk-ui/components";
interface Props {
validate?: Validator;
name: string;
disabled: boolean;
invert?: boolean;
}
const PermissionField: StatelessComponent<Props> = ({
name,
disabled,
invert = false,
}) => (
<div>
<Field name={name} type="radio" parse={parseStringBool} value={!invert}>
{({ input }) => (
<Localized id="configure-permissionField-allow">
<RadioButton
id={`${input.name}-allow`}
name={input.name}
onChange={input.onChange}
onFocus={input.onFocus}
onBlur={input.onBlur}
checked={input.checked}
disabled={disabled}
value={input.value}
>
Allow
</RadioButton>
</Localized>
)}
</Field>
<Field name={name} type="radio" parse={parseStringBool} value={invert}>
{({ input }) => (
<Localized id="configure-permissionField-dontAllow">
<RadioButton
id={`${input.name}-dontAllow`}
name={input.name}
onChange={input.onChange}
onFocus={input.onFocus}
onBlur={input.onBlur}
checked={input.checked}
disabled={disabled}
value={input.value}
>
Don't allow
</RadioButton>
</Localized>
)}
</Field>
</div>
);
export default PermissionField;
@@ -0,0 +1,5 @@
.root {
padding-bottom: calc(0.25 * var(--spacing-unit));
border-bottom: 1px solid var(--palette-divider);
margin-bottom: calc(0.75 * var(--spacing-unit));
}
@@ -3,13 +3,13 @@ import { createRenderer } from "react-test-renderer/shallow";
import { PropTypesOf } from "talk-framework/types";
import Moderation from "./Moderation";
import Subheader from "./Subheader";
it("renders correctly", () => {
const props: PropTypesOf<typeof Moderation> = {
const props: PropTypesOf<typeof Subheader> = {
children: "child",
};
const renderer = createRenderer();
renderer.render(<Moderation {...props} />);
renderer.render(<Subheader {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,12 @@
import React, { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
import styles from "./Subheader.css";
const Subheader: StatelessComponent = ({ children }) => (
<Typography variant="heading3" className={styles.root}>
{children}
</Typography>
);
export default Subheader;
@@ -0,0 +1,21 @@
import React, { StatelessComponent } from "react";
import { ValidationMessage as UIValidationMessage } from "talk-ui/components";
import { PropTypesOf } from "talk-ui/types";
import styles from "./ValidationMessage.css";
interface Props extends PropTypesOf<typeof UIValidationMessage> {
children: React.ReactNode;
}
const ValidationMessage: StatelessComponent<Props> = ({
children,
...rest
}) => (
<UIValidationMessage {...rest} className={styles.root}>
{children}
</UIValidationMessage>
);
export default ValidationMessage;
@@ -4,7 +4,7 @@ exports[`renders correctly 1`] = `
<div
className="ConfigBox-root"
>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
className="ConfigBox-title"
justifyContent="space-between"
>
@@ -18,7 +18,7 @@ exports[`renders correctly 1`] = `
topRight
</span>
</div>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
<div
className="ConfigBox-content"
>
@@ -1,10 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Typography)
<ForwardRef(forwardRef)
className="Header-root"
variant="heading1"
>
child
</withPropsOnChange(Typography)>
</ForwardRef(forwardRef)>
`;
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
className="Layout-root"
>
child
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
`;
@@ -1,12 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div>
<Header>
Perspective Toxic Comment Filter
</Header>
<withPropsOnChange(Typography)>
Using the Perspective API, the Toxic Comment filter warns users when comments exceed the predefined toxicity threshold. Toxic comments will not be published and are placed in the Pending Queue for review by a moderator. If approved by a moderator, the comment will be published.
</withPropsOnChange(Typography)>
</div>
`;
@@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<ForwardRef(forwardRef)
className="Subheader-root"
variant="heading3"
>
child
</ForwardRef(forwardRef)>
`;
@@ -102,7 +102,7 @@ class ConfigureContainer extends React.Component<Props> {
private addSubmitHook: AddSubmitHook = hook => {
this.submitHooks.push(hook);
return () => {
this.submitHooks = this.submitHooks.filter(h => h === hook);
this.submitHooks = this.submitHooks.filter(h => h !== hook);
};
};
@@ -0,0 +1,35 @@
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { HorizontalGutter } from "talk-ui/components";
import CustomCSSConfigContainer from "../containers/CustomCSSConfigContainer";
import PermittedDomainsConfigContainer from "../containers/PermittedDomainsConfigContainer";
interface Props {
disabled: boolean;
settings: PropTypesOf<typeof CustomCSSConfigContainer>["settings"] &
PropTypesOf<typeof PermittedDomainsConfigContainer>["settings"];
onInitValues: (values: any) => void;
}
const AdvancedConfig: StatelessComponent<Props> = ({
disabled,
settings,
onInitValues,
}) => (
<HorizontalGutter size="double" data-testid="configure-advancedContainer">
<CustomCSSConfigContainer
disabled={disabled}
settings={settings}
onInitValues={onInitValues}
/>
<PermittedDomainsConfigContainer
disabled={disabled}
settings={settings}
onInitValues={onInitValues}
/>
</HorizontalGutter>
);
export default AdvancedConfig;
@@ -0,0 +1,64 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import {
FormField,
HorizontalGutter,
TextField,
Typography,
ValidationMessage,
} from "talk-ui/components";
import Header from "../../../components/Header";
interface Props {
disabled: boolean;
}
const CustomCSSConfig: StatelessComponent<Props> = ({ disabled }) => (
<FormField>
<HorizontalGutter size="full">
<Localized id="configure-advanced-customCSS">
<Header container={<label htmlFor="configure-advanced-customCssUrl" />}>
Custom CSS
</Header>
</Localized>
<Localized
id="configure-advanced-customCSS-explanation"
strong={<strong />}
>
<Typography variant="detail">
URL of a CSS stylesheet that will override default Embed Stream
styles. Can be internal or external.
</Typography>
</Localized>
<Field name="customCssUrl">
{({ input, meta }) => (
<>
<TextField
id={`configure-advanced-${input.name}`}
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
fullWidth
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</HorizontalGutter>
</FormField>
);
export default CustomCSSConfig;
@@ -0,0 +1,65 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { formatStringList, parseStringList } from "talk-framework/lib/form";
import {
FormField,
HorizontalGutter,
TextField,
Typography,
ValidationMessage,
} from "talk-ui/components";
import Header from "../../../components/Header";
interface Props {
disabled: boolean;
}
const PermittedDomainsConfig: StatelessComponent<Props> = ({ disabled }) => (
<FormField>
<HorizontalGutter size="full">
<Localized id="configure-advanced-permittedDomains">
<Header container={<label htmlFor="configure-advanced-domains" />}>
Permitted Domains
</Header>
</Localized>
<Localized
id="configure-advanced-permittedDomains-explanation"
strong={<strong />}
>
<Typography variant="detail">
Domains where your Talk instance is allowed to be embedded. Typical
use is localhost, staging.yourdomain.com, yourdomain.com, etc.
</Typography>
</Localized>
<Field name="domains" parse={parseStringList} format={formatStringList}>
{({ input, meta }) => (
<>
<TextField
id={`configure-advanced-${input.name}`}
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
fullWidth
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</HorizontalGutter>
</FormField>
);
export default PermittedDomainsConfig;
@@ -0,0 +1,54 @@
import { FormApi } from "final-form";
import { RouteProps } from "found";
import { merge } from "lodash";
import React from "react";
import { graphql } from "react-relay";
import { AdvancedConfigContainer_settings as SettingsData } from "talk-admin/__generated__/AdvancedConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import AdvancedConfig from "../components/AdvancedConfig";
interface Props {
form: FormApi;
submitting: boolean;
settings: SettingsData;
}
class AdvancedConfigContainer extends React.Component<Props> {
public static routeConfig: RouteProps;
private initialValues = {};
constructor(props: Props) {
super(props);
}
public componentDidMount() {
this.props.form.initialize(this.initialValues);
}
private handleOnInitValues = (values: any) => {
this.initialValues = merge({}, this.initialValues, values);
};
public render() {
return (
<AdvancedConfig
disabled={this.props.submitting}
settings={this.props.settings}
onInitValues={this.handleOnInitValues}
/>
);
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment AdvancedConfigContainer_settings on Settings {
...CustomCSSConfigContainer_settings
...PermittedDomainsConfigContainer_settings
}
`,
})(AdvancedConfigContainer);
export default enhanced;
@@ -0,0 +1,47 @@
import { FormApi } from "final-form";
import React from "react";
import { graphql } from "react-relay";
import { AdvancedConfigRouteContainerQueryResponse } from "talk-admin/__generated__/AdvancedConfigRouteContainerQuery.graphql";
import { withRouteConfig } from "talk-framework/lib/router";
import { Delay, Spinner } from "talk-ui/components";
import AdvancedConfigContainer from "./AdvancedConfigContainer";
interface Props {
data: AdvancedConfigRouteContainerQueryResponse | null;
form: FormApi;
submitting: boolean;
}
class AdvancedConfigRouteContainer extends React.Component<Props> {
public render() {
if (!this.props.data) {
return (
<Delay>
<Spinner />
</Delay>
);
}
return (
<AdvancedConfigContainer
settings={this.props.data.settings}
form={this.props.form}
submitting={this.props.submitting}
/>
);
}
}
const enhanced = withRouteConfig({
query: graphql`
query AdvancedConfigRouteContainerQuery {
settings {
...AdvancedConfigContainer_settings
}
}
`,
cacheConfig: { force: true },
})(AdvancedConfigRouteContainer);
export default enhanced;
@@ -0,0 +1,35 @@
import React from "react";
import { graphql } from "react-relay";
import { CustomCSSConfigContainer_settings as SettingsData } from "talk-admin/__generated__/CustomCSSConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import CustomCSSConfig from "../components/CustomCSSConfig";
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled: boolean;
}
class CustomCSSConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <CustomCSSConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment CustomCSSConfigContainer_settings on Settings {
customCssUrl
}
`,
})(CustomCSSConfigContainer);
export default enhanced;
@@ -0,0 +1,35 @@
import React from "react";
import { graphql } from "react-relay";
import { PermittedDomainsConfigContainer_settings as SettingsData } from "talk-admin/__generated__/PermittedDomainsConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import PermittedDomainsConfig from "../components/PermittedDomainsConfig";
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled: boolean;
}
class PermittedDomainsConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <PermittedDomainsConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment PermittedDomainsConfigContainer_settings on Settings {
domains
}
`,
})(PermittedDomainsConfigContainer);
export default enhanced;
@@ -13,7 +13,11 @@ interface Props {
onInitValues: (values: any) => void;
}
const Auth: StatelessComponent<Props> = ({ disabled, auth, onInitValues }) => (
const AuthConfig: StatelessComponent<Props> = ({
disabled,
auth,
onInitValues,
}) => (
<HorizontalGutter size="double" data-testid="configure-authContainer">
<DisplayNamesConfigContainer
disabled={disabled}
@@ -28,4 +32,4 @@ const Auth: StatelessComponent<Props> = ({ disabled, auth, onInitValues }) => (
</HorizontalGutter>
);
export default Auth;
export default AuthConfig;
@@ -6,7 +6,7 @@ import { Field } from "react-final-form";
import { Validator } from "talk-framework/lib/validation";
import { FormField, InputLabel, TextField } from "talk-ui/components";
import ValidationMessage from "./ValidationMessage";
import ValidationMessage from "../../../components/ValidationMessage";
interface Props {
validate?: Validator;
@@ -6,7 +6,7 @@ import { Field } from "react-final-form";
import { Validator } from "talk-framework/lib/validation";
import { FormField, InputLabel, TextField } from "talk-ui/components";
import ValidationMessage from "./ValidationMessage";
import ValidationMessage from "../../../components/ValidationMessage";
interface Props {
validate?: Validator;
@@ -2,6 +2,7 @@ import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { parseStringBool } from "talk-framework/lib/form";
import {
Flex,
FormField,
@@ -16,20 +17,18 @@ interface Props {
disabled?: boolean;
}
const parseStringBool = (v: string) => v === "true";
const DisplayNamesConfig: StatelessComponent<Props> = ({ disabled }) => (
<HorizontalGutter size="oneAndAHalf">
<Localized id="configure-auth-displayNamesConfig-title">
<Header>Display Names</Header>
</Localized>
<Localized id="configure-auth-displayNamesConfig-explanationShort">
<Typography>
<Typography variant="detail">
Some AUTH integrations include a Display Name as well as a User Name.
</Typography>
</Localized>
<Localized id="configure-auth-displayNamesConfig-explanationLong">
<Typography>
<Typography variant="detail">
A User Name has to be unique (there can only be one Juan_Doe, for
example), whereas a Display Name does not. If your AUTH provider allows
for Display Names, you can enable this option. This allows for fewer
@@ -28,7 +28,8 @@ import ConfigDescription from "./ConfigDescription";
import RedirectField from "./RedirectField";
import RegistrationField from "./RegistrationField";
import TargetFilterField from "./TargetFilterField";
import ValidationMessage from "./ValidationMessage";
import ValidationMessage from "../../../components/ValidationMessage";
interface Props {
disabled?: boolean;
@@ -1,15 +0,0 @@
import React, { StatelessComponent } from "react";
import { ValidationMessage as UIValidationMessage } from "talk-ui/components";
import styles from "./ValidationMessage.css";
interface Props {
children: React.ReactNode;
}
const ValidationMessage: StatelessComponent<Props> = ({ children }) => (
<UIValidationMessage className={styles.root}>{children}</UIValidationMessage>
);
export default ValidationMessage;
@@ -5,7 +5,7 @@ import { get, merge } from "lodash";
import React from "react";
import { graphql } from "react-relay";
import { AuthContainer_auth as AuthData } from "talk-admin/__generated__/AuthContainer_auth.graphql";
import { AuthConfigContainer_auth as AuthData } from "talk-admin/__generated__/AuthConfigContainer_auth.graphql";
import { TalkContext, withContext } from "talk-framework/lib/bootstrap";
import { getMessage } from "talk-framework/lib/i18n";
@@ -16,7 +16,7 @@ import {
SubmitHook,
withSubmitHookContext,
} from "../../../submitHook";
import Auth from "../components/Auth";
import AuthConfig from "../components/AuthConfig";
interface Props {
localeBundles: TalkContext["localeBundles"];
@@ -26,7 +26,7 @@ interface Props {
auth: AuthData;
}
class AuthContainer extends React.Component<Props> {
class AuthConfigContainer extends React.Component<Props> {
public static routeConfig: RouteProps;
private initialValues = {};
private removeSubmitHook: RemoveSubmitHook;
@@ -85,7 +85,7 @@ class AuthContainer extends React.Component<Props> {
public render() {
return (
<Auth
<AuthConfig
disabled={this.props.submitting}
auth={this.props.auth}
onInitValues={this.handleOnInitValues}
@@ -96,7 +96,7 @@ class AuthContainer extends React.Component<Props> {
const enhanced = withFragmentContainer<Props>({
auth: graphql`
fragment AuthContainer_auth on Auth {
fragment AuthConfigContainer_auth on Auth {
...FacebookConfigContainer_auth
...FacebookConfigContainer_authReadOnly
...GoogleConfigContainer_auth
@@ -111,7 +111,7 @@ const enhanced = withFragmentContainer<Props>({
`,
})(
withSubmitHookContext(addSubmitHook => ({ addSubmitHook }))(
withContext(({ localeBundles }) => ({ localeBundles }))(AuthContainer)
withContext(({ localeBundles }) => ({ localeBundles }))(AuthConfigContainer)
)
);
@@ -2,14 +2,14 @@ import { FormApi } from "final-form";
import React from "react";
import { graphql } from "react-relay";
import { AuthRouteContainerQueryResponse } from "talk-admin/__generated__/AuthRouteContainerQuery.graphql";
import { AuthConfigRouteContainerQueryResponse } from "talk-admin/__generated__/AuthConfigRouteContainerQuery.graphql";
import { withRouteConfig } from "talk-framework/lib/router";
import { Delay, Spinner } from "talk-ui/components";
import AuthContainer from ".//AuthContainer";
import AuthConfigContainer from "./AuthConfigContainer";
interface Props {
data: AuthRouteContainerQueryResponse | null;
data: AuthConfigRouteContainerQueryResponse | null;
form: FormApi;
submitting?: boolean;
}
@@ -24,7 +24,7 @@ class AuthRouteContainer extends React.Component<Props> {
);
}
return (
<AuthContainer
<AuthConfigContainer
auth={this.props.data.settings.auth}
form={this.props.form}
submitting={this.props.submitting}
@@ -35,10 +35,10 @@ class AuthRouteContainer extends React.Component<Props> {
const enhanced = withRouteConfig({
query: graphql`
query AuthRouteContainerQuery {
query AuthConfigRouteContainerQuery {
settings {
auth {
...AuthContainer_auth
...AuthConfigContainer_auth
}
}
}
@@ -0,0 +1,61 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent, Suspense } from "react";
import { Field } from "react-final-form";
import {
HorizontalGutter,
Spinner,
Typography,
ValidationMessage,
} from "talk-ui/components";
import Header from "../../../components/Header";
import LazyMarkdown from "./LazyMarkdown";
interface Props {
disabled: boolean;
}
const ClosedStreamMessageConfig: StatelessComponent<Props> = ({ disabled }) => (
<HorizontalGutter size="oneAndAHalf">
<Localized id="configure-general-closedStreamMessage-title">
<Header
container={
<label htmlFor="configure-general-closedStreamMessage-content" />
}
>
Closed Stream Message
</Header>
</Localized>
<Localized
id="configure-general-closedStreamMessage-explanation"
strong={<strong />}
>
<Typography variant="detail">
Write a message to appear after a story is closed for commenting.
</Typography>
</Localized>
<Field name="closedMessage">
{({ input, meta }) => (
<>
<Suspense fallback={<Spinner />}>
<LazyMarkdown
id="configure-general-closedStreamMessage-content"
name={input.name}
onChange={input.onChange}
value={input.value}
/>
</Suspense>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</HorizontalGutter>
);
export default ClosedStreamMessageConfig;
@@ -0,0 +1,87 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { DURATION_UNIT, DurationField } from "talk-framework/components";
import {
FormField,
HorizontalGutter,
InputLabel,
Typography,
ValidationMessage,
} from "talk-ui/components";
import { Field } from "react-final-form";
import OnOffField from "talk-admin/routes/configure/components/OnOffField";
import {
composeValidators,
required,
validateWholeNumberGreaterThan,
} from "talk-framework/lib/validation";
import Header from "../../../components/Header";
interface Props {
disabled: boolean;
}
const ClosingCommentStreamsConfig: StatelessComponent<Props> = ({
disabled,
}) => (
<HorizontalGutter size="oneAndAHalf" container="fieldset">
<Localized id="configure-general-closingCommentStreams-title">
<Header container="legend">Closing Comment Streams</Header>
</Localized>
<Localized
id="configure-general-closingCommentStreams-explanation"
strong={<strong />}
>
<Typography variant="detail">
Set comment streams to close after a defined period of time after a
storys publication
</Typography>
</Localized>
<FormField container="fieldset">
<Localized id="configure-general-closingCommentStreams-closeCommentsAutomatically">
<InputLabel container="legend">Close Comments Automatically</InputLabel>
</Localized>
<OnOffField name="autoCloseStream" disabled={disabled} />
</FormField>
<FormField container="fieldset">
<Localized id="configure-general-closingCommentStreams-closeCommentsAfter">
<InputLabel container="legend">Close Comments After</InputLabel>
</Localized>
<Field
name="closedTimeout"
validate={composeValidators(
required,
validateWholeNumberGreaterThan(0)
)}
>
{({ input, meta }) => (
<>
<DurationField
units={[
DURATION_UNIT.HOURS,
DURATION_UNIT.DAYS,
DURATION_UNIT.WEEKS,
]}
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
</HorizontalGutter>
);
export default ClosingCommentStreamsConfig;
@@ -0,0 +1,3 @@
.commentEditingTextInput {
width: 120px;
}
@@ -0,0 +1,78 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { DURATION_UNIT, DurationField } from "talk-framework/components";
import {
composeValidators,
required,
validateWholeNumberGreaterThanOrEqual,
} from "talk-framework/lib/validation";
import {
FormField,
HorizontalGutter,
InputLabel,
Typography,
ValidationMessage,
} from "talk-ui/components";
import Header from "../../../components/Header";
interface Props {
disabled: boolean;
}
const CommentEditingConfig: StatelessComponent<Props> = ({ disabled }) => (
<HorizontalGutter size="oneAndAHalf">
<Localized id="configure-general-commentEditing-title">
<Header>Comment Editing</Header>
</Localized>
<Localized
id="configure-general-commentEditing-explanation"
strong={<strong />}
>
<Typography variant="detail">
Set a limit on how long commenters have to edit their comments sitewide.
Edited comments are marked as (Edited) on the comment stream and the
moderation panel.
</Typography>
</Localized>
<FormField container="fieldset">
<Localized id="configure-general-commentEditing-commentEditTimeFrame">
<InputLabel container="legend">Comment Edit Timeframe</InputLabel>
</Localized>
<Field
name="editCommentWindowLength"
validate={composeValidators(
required,
validateWholeNumberGreaterThanOrEqual(0)
)}
>
{({ input, meta }) => (
<>
<DurationField
units={[
DURATION_UNIT.SECONDS,
DURATION_UNIT.MINUTES,
DURATION_UNIT.HOURS,
]}
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
</HorizontalGutter>
);
export default CommentEditingConfig;
@@ -0,0 +1,3 @@
.commentLengthTextInput {
width: 150px;
}
@@ -0,0 +1,163 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import {
composeValidators,
createValidator,
validateWholeNumberGreaterThan,
} from "talk-framework/lib/validation";
import {
FormField,
HorizontalGutter,
InputLabel,
TextField,
Typography,
ValidationMessage,
} from "talk-ui/components";
import Header from "../../../components/Header";
import OnOffField from "../../../components/OnOffField";
import { formatEmpty, parseEmptyAsNull } from "talk-framework/lib/form";
import styles from "./CommentLengthConfig.css";
const validateMaxLongerThanMin = createValidator(
(v, values) =>
v === null ||
values.charCount.min === null ||
parseInt(v, 10) > parseInt(values.charCount.min, 10),
// tslint:disable-next-line:jsx-wrap-multiline
<Localized id="configure-general-commentLength-validateLongerThanMin">
<span>Please enter a number longer than the minimum length</span>
</Localized>
);
interface Props {
disabled: boolean;
}
const CommentLengthConfig: StatelessComponent<Props> = ({ disabled }) => (
<HorizontalGutter size="oneAndAHalf" container="fieldset">
<Localized id="configure-general-commentLength-title">
<Header container="legend">Comment Length</Header>
</Localized>
<Localized
id="configure-general-commentLength-setLimit"
strong={<strong />}
>
<Typography variant="detail">
Set a limit on the length of comments sitewide
</Typography>
</Localized>
<FormField>
<Localized id="configure-general-commentLength-limitCommentLength">
<InputLabel>Limit Comment Length</InputLabel>
</Localized>
<OnOffField name="charCount.enabled" disabled={disabled} />
</FormField>
<FormField>
<Localized id="configure-general-commentLength-minCommentLength">
<InputLabel htmlFor="configure-general-commentLength-min">
Minimum Comment Length
</InputLabel>
</Localized>
<Field
name="charCount.min"
validate={validateWholeNumberGreaterThan(0)}
parse={parseEmptyAsNull}
format={formatEmpty}
>
{({ input, meta }) => (
<>
<Localized
id="configure-general-commentLength-textField"
attrs={{ placeholder: true }}
>
<TextField
id="configure-general-commentLength-min"
className={styles.commentLengthTextInput}
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
adornment={
<Localized id="configure-general-commentLength-characters">
<Typography variant="bodyCopy">Characters</Typography>
</Localized>
}
placeholder={"No limit"}
textAlignCenter
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
<FormField>
<Localized id="configure-general-commentLength-maxCommentLength">
<InputLabel htmlFor="configure-general-commentLength-max">
Maximum Comment Length
</InputLabel>
</Localized>
<Field
name="charCount.max"
validate={composeValidators(
validateWholeNumberGreaterThan(0),
validateMaxLongerThanMin
)}
parse={parseEmptyAsNull}
format={formatEmpty}
>
{({ input, meta }) => (
<>
<Localized
id="configure-general-commentLength-textField"
attrs={{ placeholder: true }}
>
<TextField
id="configure-general-commentLength-max"
className={styles.commentLengthTextInput}
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
adornment={
<Localized id="configure-general-commentLength-characters">
<Typography variant="bodyCopy">Characters</Typography>
</Localized>
}
placeholder={"No limit"}
textAlignCenter
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
</HorizontalGutter>
);
export default CommentLengthConfig;
@@ -0,0 +1,56 @@
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { HorizontalGutter } from "talk-ui/components";
import ClosedStreamMessageConfigContainer from "../containers/ClosedStreamMessageConfigContainer";
import ClosingCommentStreamsConfigContainer from "../containers/ClosingCommentStreamsConfigContainer";
import CommentEditingConfigContainer from "../containers/CommentEditingConfigContainer";
import CommentLengthConfigContainer from "../containers/CommentLengthConfigContainer";
import GuidelinesConfigContainer from "../containers/GuidelinesConfigContainer";
interface Props {
disabled: boolean;
settings: PropTypesOf<typeof GuidelinesConfigContainer>["settings"] &
PropTypesOf<typeof CommentLengthConfigContainer>["settings"] &
PropTypesOf<typeof CommentEditingConfigContainer>["settings"] &
PropTypesOf<typeof ClosedStreamMessageConfigContainer>["settings"] &
PropTypesOf<typeof ClosingCommentStreamsConfigContainer>["settings"];
onInitValues: (values: any) => void;
}
const General: StatelessComponent<Props> = ({
disabled,
settings,
onInitValues,
}) => (
<HorizontalGutter size="double" data-testid="configure-generalContainer">
<GuidelinesConfigContainer
disabled={disabled}
settings={settings}
onInitValues={onInitValues}
/>
<CommentLengthConfigContainer
disabled={disabled}
settings={settings}
onInitValues={onInitValues}
/>
<CommentEditingConfigContainer
disabled={disabled}
settings={settings}
onInitValues={onInitValues}
/>
<ClosingCommentStreamsConfigContainer
disabled={disabled}
settings={settings}
onInitValues={onInitValues}
/>
<ClosedStreamMessageConfigContainer
disabled={disabled}
settings={settings}
onInitValues={onInitValues}
/>
</HorizontalGutter>
);
export default General;
@@ -0,0 +1,81 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent, Suspense } from "react";
import { Field } from "react-final-form";
import { ExternalLink } from "talk-framework/lib/i18n/components";
import {
FormField,
HorizontalGutter,
InputLabel,
Spinner,
Typography,
ValidationMessage,
} from "talk-ui/components";
import OnOffField from "talk-admin/routes/configure/components/OnOffField";
import Header from "../../../components/Header";
import LazyMarkdown from "./LazyMarkdown";
interface Props {
disabled: boolean;
}
const GuidelinesConfig: StatelessComponent<Props> = ({ disabled }) => (
<HorizontalGutter size="oneAndAHalf" container="fieldset">
<Localized id="configure-general-guidelines-title">
<Header container="legend">Community Guidelines Summary</Header>
</Localized>
<FormField container="fieldset">
<Localized id="configure-general-guidelines-showCommunityGuidelines">
<InputLabel container="legend">
Show Community Guidelines Summary
</InputLabel>
</Localized>
<OnOffField name="communityGuidelinesEnable" disabled={disabled} />
</FormField>
<FormField>
<Localized id="configure-general-guidelines-title">
<InputLabel htmlFor="configure-general-guidelines-content">
Community Guidelines Summary
</InputLabel>
</Localized>
<Localized
id="configure-general-guidelines-explanation"
strong={<strong />}
externalLink={<ExternalLink href="#" />}
>
<Typography variant="detail">
Write a summary of your community guidelines that will appear at the
top of each comment stream sitewide. Your summary can be formatted
using Markdown Syntax. More information on how to use Markdown can be
found here.
</Typography>
</Localized>
</FormField>
<Field name="communityGuidelines">
{({ input, meta }) => (
<>
<Suspense fallback={<Spinner />}>
<LazyMarkdown
id="configure-general-guidelines-content"
name={input.name}
onChange={input.onChange}
value={input.value}
/>
</Suspense>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</HorizontalGutter>
);
export default GuidelinesConfig;
@@ -0,0 +1,7 @@
import React from "react";
export function loadMarkdownEditor() {
return import("talk-framework/components/loadables/MarkdownEditor" /* webpackChunkName: "markdownEditor" */);
}
export default React.lazy(loadMarkdownEditor);
@@ -0,0 +1,35 @@
import React from "react";
import { graphql } from "react-relay";
import { ClosedStreamMessageConfigContainer_settings as SettingsData } from "talk-admin/__generated__/ClosedStreamMessageConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import ClosedStreamMessageConfig from "../components/ClosedStreamMessageConfig";
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled: boolean;
}
class ClosedStreamMessageConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <ClosedStreamMessageConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment ClosedStreamMessageConfigContainer_settings on Settings {
closedMessage
}
`,
})(ClosedStreamMessageConfigContainer);
export default enhanced;
@@ -0,0 +1,36 @@
import React from "react";
import { graphql } from "react-relay";
import { ClosingCommentStreamsConfigContainer_settings as SettingsData } from "talk-admin/__generated__/ClosingCommentStreamsConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import ClosingCommentStreamsConfig from "../components/ClosingCommentStreamsConfig";
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled: boolean;
}
class ClosingCommentStreamsConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <ClosingCommentStreamsConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment ClosingCommentStreamsConfigContainer_settings on Settings {
autoCloseStream
closedTimeout
}
`,
})(ClosingCommentStreamsConfigContainer);
export default enhanced;
@@ -0,0 +1,35 @@
import React from "react";
import { graphql } from "react-relay";
import { CommentEditingConfigContainer_settings as SettingsData } from "talk-admin/__generated__/CommentEditingConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import CommentEditingConfig from "../components/CommentEditingConfig";
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled: boolean;
}
class CommentEditingConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <CommentEditingConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment CommentEditingConfigContainer_settings on Settings {
editCommentWindowLength
}
`,
})(CommentEditingConfigContainer);
export default enhanced;
@@ -0,0 +1,39 @@
import React from "react";
import { graphql } from "react-relay";
import { CommentLengthConfigContainer_settings as SettingsData } from "talk-admin/__generated__/CommentLengthConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import CommentLengthConfig from "../components/CommentLengthConfig";
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled: boolean;
}
class CommentLengthConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <CommentLengthConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment CommentLengthConfigContainer_settings on Settings {
charCount {
enabled
min
max
}
}
`,
})(CommentLengthConfigContainer);
export default enhanced;
@@ -0,0 +1,57 @@
import { FormApi } from "final-form";
import { RouteProps } from "found";
import { merge } from "lodash";
import React from "react";
import { graphql } from "react-relay";
import { GeneralConfigContainer_settings as SettingsData } from "talk-admin/__generated__/GeneralConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import GeneralConfig from "../components/GeneralConfig";
interface Props {
form: FormApi;
submitting: boolean;
settings: SettingsData;
}
class GeneralConfigContainer extends React.Component<Props> {
public static routeConfig: RouteProps;
private initialValues = {};
constructor(props: Props) {
super(props);
}
public componentDidMount() {
this.props.form.initialize(this.initialValues);
}
private handleOnInitValues = (values: any) => {
this.initialValues = merge({}, this.initialValues, values);
};
public render() {
return (
<GeneralConfig
disabled={this.props.submitting}
settings={this.props.settings}
onInitValues={this.handleOnInitValues}
/>
);
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment GeneralConfigContainer_settings on Settings {
...GuidelinesConfigContainer_settings
...CommentLengthConfigContainer_settings
...CommentEditingConfigContainer_settings
...ClosedStreamMessageConfigContainer_settings
...ClosingCommentStreamsConfigContainer_settings
}
`,
})(GeneralConfigContainer);
export default enhanced;
@@ -0,0 +1,54 @@
import { FormApi } from "final-form";
import React from "react";
import { graphql } from "react-relay";
import { GeneralConfigRouteContainerQueryResponse } from "talk-admin/__generated__/GeneralConfigRouteContainerQuery.graphql";
import { withRouteConfig } from "talk-framework/lib/router";
import { Delay, Spinner } from "talk-ui/components";
import { loadMarkdownEditor } from "../components/LazyMarkdown";
import GeneralConfigContainer from "./GeneralConfigContainer";
interface Props {
data: GeneralConfigRouteContainerQueryResponse | null;
form: FormApi;
submitting: boolean;
}
class GeneralConfigRouteContainer extends React.Component<Props> {
public render() {
if (!this.props.data) {
return (
<Delay>
<Spinner />
</Delay>
);
}
return (
<GeneralConfigContainer
settings={this.props.data.settings}
form={this.props.form}
submitting={this.props.submitting}
/>
);
}
}
const enhanced = withRouteConfig({
getQuery: () => {
// Start prefetching markdown editor.
loadMarkdownEditor();
// Fetch graphql data.
return graphql`
query GeneralConfigRouteContainerQuery {
settings {
...GeneralConfigContainer_settings
}
}
`;
},
cacheConfig: { force: true },
})(GeneralConfigRouteContainer);
export default enhanced;
@@ -0,0 +1,36 @@
import React from "react";
import { graphql } from "react-relay";
import { GuidelinesConfigContainer_settings as SettingsData } from "talk-admin/__generated__/GuidelinesConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import GuidelinesConfig from "../components/GuidelinesConfig";
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled: boolean;
}
class GuidelinesConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <GuidelinesConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment GuidelinesConfigContainer_settings on Settings {
communityGuidelinesEnable
communityGuidelines
}
`,
})(GuidelinesConfigContainer);
export default enhanced;
@@ -0,0 +1,54 @@
import { Localized } from "fluent-react/compat";
import { identity } from "lodash";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { Validator } from "talk-framework/lib/validation";
import { FormField, InputLabel, TextField } from "talk-ui/components";
import ValidationMessage from "../../../components/ValidationMessage";
interface Props {
validate?: Validator;
name: string;
disabled: boolean;
}
const APIKeyField: StatelessComponent<Props> = ({
name,
disabled,
validate,
}) => (
<FormField>
<Field name={name} parse={identity} validate={validate}>
{({ input, meta }) => (
<>
<Localized id="configure-moderation-apiKey">
<InputLabel htmlFor={`configure-moderation-${input.name}`}>
API Key
</InputLabel>
</Localized>
<TextField
id={`configure-moderation-${input.name}`}
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
);
export default APIKeyField;
@@ -0,0 +1,123 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { ExternalLink } from "talk-framework/lib/i18n/components";
import {
composeValidators,
required,
validateURL,
Validator,
} from "talk-framework/lib/validation";
import {
FormField,
HorizontalGutter,
InputLabel,
TextField,
Typography,
} from "talk-ui/components";
import ConfigurationSubHeader from "../../../components/ConfigurationSubHeader";
import Header from "../../../components/Header";
import OnOffField from "../../../components/OnOffField";
import ValidationMessage from "../../../components/ValidationMessage";
import APIKeyField from "./APIKeyField";
interface Props {
disabled: boolean;
}
const AkismetConfig: StatelessComponent<Props> = ({ disabled }) => {
const validateWhenEnabled = (validator: Validator): Validator => (
v,
values
) => {
if (values.integrations.akismet.enabled) {
return validator(v, values);
}
return "";
};
return (
<HorizontalGutter size="oneAndAHalf" container="fieldset">
<Localized id="configure-moderation-akismet-title">
<Header container="legend">Akismet Spam Detection Filter</Header>
</Localized>
<Localized
id="configure-moderation-akismet-explanation"
strong={<strong />}
>
<Typography variant="detail">
Submitted comments are passed to the Akismet API for spam detection.
If a comment is determined to be spam, it will prompt the user,
indicating that the comment might be considered spam. If the user
continues after this point with the still spam-like comment, the
comment will be marked as containing spam, will not be published and
are placed in the Pending Queue for review by a moderator. If approved
by a moderator, the comment will be published.
</Typography>
</Localized>
<FormField container="fieldset">
<Localized id="configure-moderation-akismet-filter">
<InputLabel container="legend">Spam Detection Filter</InputLabel>
</Localized>
<OnOffField name="integrations.akismet.enabled" disabled={disabled} />
</FormField>
<div>
<ConfigurationSubHeader />
<Localized
id="configure-moderation-akismet-accountNote"
externalLink={<ExternalLink />}
>
<Typography variant="detail">
Note: You must add your active domain(s) in your Akismet account:
https://akismet.com/account/
</Typography>
</Localized>
</div>
<APIKeyField
name="integrations.akismet.key"
disabled={disabled}
validate={validateWhenEnabled(required)}
/>
<FormField>
<Localized id="configure-moderation-akismet-siteURL">
<InputLabel htmlFor="configure-moderation-akismet-site">
Site URL
</InputLabel>
</Localized>
<Field
name={"integrations.akismet.site"}
validate={validateWhenEnabled(
composeValidators(required, validateURL)
)}
>
{({ input, meta }) => (
<>
<TextField
id="configure-moderation-akismet-site"
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
</HorizontalGutter>
);
};
export default AkismetConfig;
@@ -0,0 +1,21 @@
import { noop } from "lodash";
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import { PropTypesOf } from "talk-framework/types";
import ModerationConfig from "./ModerationConfig";
const ModerationConfigN = removeFragmentRefs(ModerationConfig);
it("renders correctly", () => {
const props: PropTypesOf<typeof ModerationConfigN> = {
disabled: false,
settings: {},
onInitValues: noop,
};
const renderer = createRenderer();
renderer.render(<ModerationConfigN {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,35 @@
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { HorizontalGutter } from "talk-ui/components";
import AkismetConfigContainer from "../containers/AkismetConfigContainer";
import PerspectiveConfigContainer from "../containers/PerspectiveConfigContainer";
interface Props {
disabled: boolean;
settings: PropTypesOf<typeof AkismetConfigContainer>["settings"] &
PropTypesOf<typeof PerspectiveConfigContainer>["settings"];
onInitValues: (values: any) => void;
}
const ModerationConfig: StatelessComponent<Props> = ({
disabled,
settings,
onInitValues,
}) => (
<HorizontalGutter size="double" data-testid="configure-moderationContainer">
<PerspectiveConfigContainer
disabled={disabled}
settings={settings}
onInitValues={onInitValues}
/>
<AkismetConfigContainer
disabled={disabled}
settings={settings}
onInitValues={onInitValues}
/>
</HorizontalGutter>
);
export default ModerationConfig;
@@ -0,0 +1,3 @@
.thresholdTextField {
width: 60px;
}
@@ -0,0 +1,213 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { formatPercentage, parsePercentage } from "talk-framework/lib/form";
import { ExternalLink } from "talk-framework/lib/i18n/components";
import {
composeValidators,
required,
validatePercentage,
validateURL,
Validator,
} from "talk-framework/lib/validation";
import {
FormField,
HorizontalGutter,
InputDescription,
InputLabel,
TextField,
Typography,
} from "talk-ui/components";
import ConfigurationSubHeader from "../../../components/ConfigurationSubHeader";
import Header from "../../../components/Header";
import OnOffField from "../../../components/OnOffField";
import PermissionField from "../../../components/PermissionField";
import ValidationMessage from "../../../components/ValidationMessage";
import APIKeyField from "./APIKeyField";
import styles from "./PerspectiveConfig.css";
/* TODO: use a common constants for both client and server. */
const TOXICITY_DEFAULT = 80;
interface Props {
disabled: boolean;
}
const PerspectiveConfig: StatelessComponent<Props> = ({ disabled }) => {
const validateWhenEnabled = (validator: Validator): Validator => (
v,
values
) => {
if (values.integrations.perspective.enabled) {
return validator(v, values);
}
return "";
};
return (
<HorizontalGutter size="oneAndAHalf" container="fieldset">
<Localized id="configure-moderation-perspective-title">
<Header container="legend">Perspective Toxic Comment Filter</Header>
</Localized>
<Localized
id="configure-moderation-perspective-explanation"
strong={<strong />}
>
<Typography variant="detail">
Using the Perspective API, the Toxic Comment filter warns users when
comments exceed the predefined toxicity threshold. Comments with a
toxicity score above the threshold will not be published and are
placed in the Pending Queue for review by a moderator. If approved by
a moderator, the comment will be published.
</Typography>
</Localized>
<FormField container="fieldset">
<Localized id="configure-moderation-perspective-filter">
<InputLabel container="legend">Spam Detection Filter</InputLabel>
</Localized>
<OnOffField
name="integrations.perspective.enabled"
disabled={disabled}
/>
</FormField>
<FormField>
<Localized id="configure-moderation-perspective-toxicityThreshold">
<InputLabel htmlFor="configure-moderation-perspective-threshold">
Toxicity Threshold
</InputLabel>
</Localized>
<Localized
id="configure-moderation-perspective-toxicityThresholdDescription"
$default={TOXICITY_DEFAULT}
>
<InputDescription>
This value can be set a percentage between 0 and 100. This number
represents the likelihood that a comment is toxic, according to
Perspective API. Defaults to $default.
</InputDescription>
</Localized>
<Field
name="integrations.perspective.threshold"
parse={parsePercentage}
format={formatPercentage}
validate={validatePercentage(0, 1)}
>
{({ input, meta }) => (
<>
<TextField
id="configure-moderation-perspective-threshold"
className={styles.thresholdTextField}
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
adornment={<Typography variant="bodyCopy">%</Typography>}
placeholder={TOXICITY_DEFAULT.toString()}
textAlignCenter
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
<FormField container="fieldset">
<Localized id="configure-moderation-perspective-allowStoreCommentData">
<InputLabel container="legend">
Allow Google to Store Comment Data
</InputLabel>
</Localized>
<Localized id="configure-moderation-perspective-allowStoreCommentDataDescription">
<InputDescription>
Stored comments will be used for future research and community model
building purposes to improve the API over time
</InputDescription>
</Localized>
<div>
<PermissionField
name="integrations.perspective.doNotStore"
disabled={disabled}
invert
/>
</div>
</FormField>
<div>
<ConfigurationSubHeader />
<Localized
id="configure-moderation-perspective-accountNote"
externalLink={<ExternalLink />}
>
<Typography variant="detail">
For additional information on how to set up the Perspective Toxic
Comment Filter please visit:
https://github.com/conversationai/perspectiveapi/blob/master/quickstart.md
</Typography>
</Localized>
</div>
<APIKeyField
name="integrations.perspective.key"
disabled={disabled}
validate={validateWhenEnabled(required)}
/>
<FormField>
<Localized id="configure-moderation-perspective-customEndpoint">
<InputLabel htmlFor="configure-moderation-perspective-customEndpoint">
Custom Endpoint
</InputLabel>
</Localized>
<Localized
id="configure-moderation-perspective-defaultEndpoint"
$default="https://commentanalyzer.googleapis.com/v1alpha1"
>
<InputDescription>
By default the endpoint is set to $default. You may override this
here
</InputDescription>
</Localized>
<Field
name="integrations.perspective.endpoint"
validate={composeValidators(validateURL)}
>
{({ input, meta }) => (
<>
<TextField
id="configure-moderation-perspective-customEndpoint"
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</FormField>
</HorizontalGutter>
);
};
export default PerspectiveConfig;
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<ForwardRef(forwardRef)
data-testid="configure-moderationContainer"
size="double"
>
<Relay(PerspectiveConfigContainer)
disabled={false}
onInitValues={[Function]}
settings={Object {}}
/>
<Relay(AkismetConfigContainer)
disabled={false}
onInitValues={[Function]}
settings={Object {}}
/>
</ForwardRef(forwardRef)>
`;
@@ -0,0 +1,41 @@
import React from "react";
import { graphql } from "react-relay";
import { AkismetConfigContainer_settings as SettingsData } from "talk-admin/__generated__/AkismetConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import AkismetConfig from "../components/AkismetConfig";
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled: boolean;
}
class AkismetConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <AkismetConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment AkismetConfigContainer_settings on Settings {
integrations {
akismet {
enabled
key
site
}
}
}
`,
})(AkismetConfigContainer);
export default enhanced;
@@ -0,0 +1,54 @@
import { FormApi } from "final-form";
import { RouteProps } from "found";
import { merge } from "lodash";
import React from "react";
import { graphql } from "react-relay";
import { ModerationConfigContainer_settings as SettingsData } from "talk-admin/__generated__/ModerationConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import ModerationConfig from "../components/ModerationConfig";
interface Props {
form: FormApi;
submitting: boolean;
settings: SettingsData;
}
class ModerationConfigContainer extends React.Component<Props> {
public static routeConfig: RouteProps;
private initialValues = {};
constructor(props: Props) {
super(props);
}
public componentDidMount() {
this.props.form.initialize(this.initialValues);
}
private handleOnInitValues = (values: any) => {
this.initialValues = merge({}, this.initialValues, values);
};
public render() {
return (
<ModerationConfig
disabled={this.props.submitting}
settings={this.props.settings}
onInitValues={this.handleOnInitValues}
/>
);
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment ModerationConfigContainer_settings on Settings {
...AkismetConfigContainer_settings
...PerspectiveConfigContainer_settings
}
`,
})(ModerationConfigContainer);
export default enhanced;
@@ -0,0 +1,47 @@
import { FormApi } from "final-form";
import React from "react";
import { graphql } from "react-relay";
import { ModerationConfigRouteContainerQueryResponse } from "talk-admin/__generated__/ModerationConfigRouteContainerQuery.graphql";
import { withRouteConfig } from "talk-framework/lib/router";
import { Delay, Spinner } from "talk-ui/components";
import ModerationConfigContainer from "./ModerationConfigContainer";
interface Props {
data: ModerationConfigRouteContainerQueryResponse | null;
form: FormApi;
submitting: boolean;
}
class ModerationConfigRouteContainer extends React.Component<Props> {
public render() {
if (!this.props.data) {
return (
<Delay>
<Spinner />
</Delay>
);
}
return (
<ModerationConfigContainer
settings={this.props.data.settings}
form={this.props.form}
submitting={this.props.submitting}
/>
);
}
}
const enhanced = withRouteConfig({
query: graphql`
query ModerationConfigRouteContainerQuery {
settings {
...ModerationConfigContainer_settings
}
}
`,
cacheConfig: { force: true },
})(ModerationConfigRouteContainer);
export default enhanced;
@@ -0,0 +1,43 @@
import React from "react";
import { graphql } from "react-relay";
import { PerspectiveConfigContainer_settings as SettingsData } from "talk-admin/__generated__/PerspectiveConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import PerspectiveConfig from "../components/PerspectiveConfig";
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled: boolean;
}
class PerspectiveConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <PerspectiveConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment PerspectiveConfigContainer_settings on Settings {
integrations {
perspective {
enabled
endpoint
key
threshold
doNotStore
}
}
}
`,
})(PerspectiveConfigContainer);
export default enhanced;
@@ -0,0 +1,35 @@
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { HorizontalGutter } from "talk-ui/components";
import OrganizationContactEmailConfigContainer from "../containers/OrganizationContactEmailConfigContainer";
import OrganizationNameConfigContainer from "../containers/OrganizationNameConfigContainer";
interface Props {
disabled: boolean;
settings: PropTypesOf<typeof OrganizationNameConfigContainer>["settings"] &
PropTypesOf<typeof OrganizationContactEmailConfigContainer>["settings"];
onInitValues: (values: any) => void;
}
const OrganizationConfig: StatelessComponent<Props> = ({
disabled,
settings,
onInitValues,
}) => (
<HorizontalGutter size="double" data-testid="configure-organizationContainer">
<OrganizationNameConfigContainer
disabled={disabled}
settings={settings}
onInitValues={onInitValues}
/>
<OrganizationContactEmailConfigContainer
disabled={disabled}
settings={settings}
onInitValues={onInitValues}
/>
</HorizontalGutter>
);
export default OrganizationConfig;
@@ -0,0 +1,66 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { required } from "talk-framework/lib/validation";
import {
FormField,
HorizontalGutter,
TextField,
Typography,
ValidationMessage,
} from "talk-ui/components";
import Header from "../../../components/Header";
interface Props {
disabled: boolean;
}
const OrganizationNameConfig: StatelessComponent<Props> = ({ disabled }) => (
<FormField>
<HorizontalGutter size="full">
<Localized id="configure-organization-email">
<Header
container={
<label htmlFor="configure-organization-organizationContactEmail" />
}
>
Organization Email
</Header>
</Localized>
<Localized
id="configure-organization-emailExplanation"
strong={<strong />}
>
<Typography variant="detail">This E-Mail will be used</Typography>
</Localized>
<Field name="organizationContactEmail" validate={required}>
{({ input, meta }) => (
<>
<TextField
id={`configure-organization-${input.name}`}
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
fullWidth
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</HorizontalGutter>
</FormField>
);
export default OrganizationNameConfig;
@@ -0,0 +1,69 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import { required } from "talk-framework/lib/validation";
import {
FormField,
HorizontalGutter,
TextField,
Typography,
ValidationMessage,
} from "talk-ui/components";
import Header from "../../../components/Header";
interface Props {
disabled: boolean;
}
const OrganizationNameConfig: StatelessComponent<Props> = ({ disabled }) => (
<FormField>
<HorizontalGutter size="full">
<Localized id="configure-organization-name">
<Header
container={
<label htmlFor="configure-organization-organizationName" />
}
>
Organization Name
</Header>
</Localized>
<Localized
id="configure-organization-nameExplanation"
strong={<strong />}
>
<Typography variant="detail">
Your organization name will appear on emails sent by Talk to your
community and organization members
</Typography>
</Localized>
<Field name="organizationName" validate={required}>
{({ input, meta }) => (
<>
<TextField
id={`configure-organization-${input.name}`}
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
fullWidth
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
</HorizontalGutter>
</FormField>
);
export default OrganizationNameConfig;
@@ -0,0 +1,54 @@
import { FormApi } from "final-form";
import { RouteProps } from "found";
import { merge } from "lodash";
import React from "react";
import { graphql } from "react-relay";
import { OrganizationConfigContainer_settings as SettingsData } from "talk-admin/__generated__/OrganizationConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import OrganizationConfig from "../components/OrganizationConfig";
interface Props {
form: FormApi;
submitting: boolean;
settings: SettingsData;
}
class OrganizationConfigContainer extends React.Component<Props> {
public static routeConfig: RouteProps;
private initialValues = {};
constructor(props: Props) {
super(props);
}
public componentDidMount() {
this.props.form.initialize(this.initialValues);
}
private handleOnInitValues = (values: any) => {
this.initialValues = merge({}, this.initialValues, values);
};
public render() {
return (
<OrganizationConfig
disabled={this.props.submitting}
settings={this.props.settings}
onInitValues={this.handleOnInitValues}
/>
);
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment OrganizationConfigContainer_settings on Settings {
...OrganizationNameConfigContainer_settings
...OrganizationContactEmailConfigContainer_settings
}
`,
})(OrganizationConfigContainer);
export default enhanced;
@@ -0,0 +1,35 @@
import React from "react";
import { graphql } from "react-relay";
import { OrganizationContactEmailConfigContainer_settings as SettingsData } from "talk-admin/__generated__/OrganizationContactEmailConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import OrganizationContactEmailConfig from "../components/OrganizationContactEmailConfig";
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled: boolean;
}
class OrganizationContactEmailConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <OrganizationContactEmailConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment OrganizationContactEmailConfigContainer_settings on Settings {
organizationContactEmail
}
`,
})(OrganizationContactEmailConfigContainer);
export default enhanced;
@@ -0,0 +1,35 @@
import React from "react";
import { graphql } from "react-relay";
import { OrganizationNameConfigContainer_settings as SettingsData } from "talk-admin/__generated__/OrganizationNameConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import OrganizationNameConfig from "../components/OrganizationNameConfig";
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled: boolean;
}
class OrganizationNameConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <OrganizationNameConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment OrganizationNameConfigContainer_settings on Settings {
organizationName
}
`,
})(OrganizationNameConfigContainer);
export default enhanced;
@@ -0,0 +1,47 @@
import { FormApi } from "final-form";
import React from "react";
import { graphql } from "react-relay";
import { OrganizationRouteContainerQueryResponse } from "talk-admin/__generated__/OrganizationRouteContainerQuery.graphql";
import { withRouteConfig } from "talk-framework/lib/router";
import { Delay, Spinner } from "talk-ui/components";
import OrganizationConfigContainer from "./OrganizationConfigContainer";
interface Props {
data: OrganizationRouteContainerQueryResponse | null;
form: FormApi;
submitting: boolean;
}
class OrganizationRouteContainer extends React.Component<Props> {
public render() {
if (!this.props.data) {
return (
<Delay>
<Spinner />
</Delay>
);
}
return (
<OrganizationConfigContainer
settings={this.props.data.settings}
form={this.props.form}
submitting={this.props.submitting}
/>
);
}
}
const enhanced = withRouteConfig({
query: graphql`
query OrganizationRouteContainerQuery {
settings {
...OrganizationConfigContainer_settings
}
}
`,
cacheConfig: { force: true },
})(OrganizationRouteContainer);
export default enhanced;
@@ -0,0 +1,3 @@
.textArea {
margin-top: var(--spacing-unit);
}
@@ -0,0 +1,63 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { ExternalLink } from "talk-framework/lib/i18n/components";
import {
FormField,
HorizontalGutter,
InputDescription,
InputLabel,
Typography,
} from "talk-ui/components";
import Header from "../../../components/Header";
import WordListTextArea from "./WordListTextArea";
import styles from "./BannedWordListConfig.css";
interface Props {
disabled: boolean;
}
const BannedWordListConfig: StatelessComponent<Props> = ({ disabled }) => (
<HorizontalGutter size="oneAndAHalf">
<Localized id="configure-wordList-banned-bannedWordsAndPhrases">
<Header>Banned Words and Phrases</Header>
</Localized>
<Localized id="configure-wordList-banned-explanation" strong={<strong />}>
<Typography variant="detail">
Comments containing a word or phrase in the banned words list are
automatically rejected and are not published.
</Typography>
</Localized>
<FormField>
<Localized id="configure-wordList-banned-wordList">
<InputLabel htmlFor="configure-wordlist-banned">
Banned Word List
</InputLabel>
</Localized>
<Localized
id="configure-wordList-banned-wordListDetail"
strong={<strong />}
externalLink={<ExternalLink href="#" />}
>
<InputDescription>
Separate banned words or phrases with a new line. Attempting to copy
and paste a comma separated list? Learn how to convert your list to a
new line separated list.
</InputDescription>
</Localized>
<div>
<WordListTextArea
id="configure-wordlist-banned"
name={"wordList.banned"}
disabled={disabled}
className={styles.textArea}
/>
</div>
</FormField>
</HorizontalGutter>
);
export default BannedWordListConfig;
@@ -0,0 +1,3 @@
.textArea {
margin-top: var(--spacing-unit);
}
@@ -0,0 +1,64 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { ExternalLink } from "talk-framework/lib/i18n/components";
import {
FormField,
HorizontalGutter,
InputDescription,
InputLabel,
Typography,
} from "talk-ui/components";
import Header from "../../../components/Header";
import WordListTextArea from "./WordListTextArea";
import styles from "./SuspectWordListConfig.css";
interface Props {
disabled: boolean;
}
const SuspectWordListConfig: StatelessComponent<Props> = ({ disabled }) => (
<HorizontalGutter size="oneAndAHalf">
<Localized id="configure-wordList-suspect-bannedWordsAndPhrases">
<Header>Suspect Words and Phrases</Header>
</Localized>
<Localized id="configure-wordList-suspect-explanation" strong={<strong />}>
<Typography variant="detail">
Comments containing a word or phrase in the Suspect Words List are
placed into the Reported Queue for moderator review and are published
(if comments are not pre-moderated).
</Typography>
</Localized>
<FormField>
<Localized id="configure-wordList-suspect-wordList">
<InputLabel htmlFor="configure-wordlist-suspect">
Suspect Word List
</InputLabel>
</Localized>
<Localized
id="configure-wordList-suspect-wordListDetail"
strong={<strong />}
externalLink={<ExternalLink href="#" />}
>
<InputDescription>
Separate suspect words or phrases with a new line. Attempting to copy
and paste a comma separated list? Learn how to convert your list to a
new line separated list.
</InputDescription>
</Localized>
<div>
<WordListTextArea
id="configure-wordlist-suspect"
name={"wordList.suspect"}
disabled={disabled}
className={styles.textArea}
/>
</div>
</FormField>
</HorizontalGutter>
);
export default SuspectWordListConfig;
@@ -0,0 +1,35 @@
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { HorizontalGutter } from "talk-ui/components";
import BannedWordListConfigContainer from "../containers/BannedWordListConfigContainer";
import SuspectWordListConfigContainer from "../containers/SuspectWordListConfigContainer";
interface Props {
disabled: boolean;
settings: PropTypesOf<typeof SuspectWordListConfigContainer>["settings"] &
PropTypesOf<typeof BannedWordListConfigContainer>["settings"];
onInitValues: (values: any) => void;
}
const WordListConfig: StatelessComponent<Props> = ({
disabled,
settings,
onInitValues,
}) => (
<HorizontalGutter size="double" data-testid="configure-wordListContainer">
<BannedWordListConfigContainer
disabled={disabled}
settings={settings}
onInitValues={onInitValues}
/>
<SuspectWordListConfigContainer
disabled={disabled}
settings={settings}
onInitValues={onInitValues}
/>
</HorizontalGutter>
);
export default WordListConfig;
@@ -0,0 +1,6 @@
.textArea {
width: 100%;
box-sizing: border-box;
height: calc(23 * var(--spacing-unit));
padding: calc(0.5 * var(--spacing-unit));
}
@@ -0,0 +1,61 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import { Field } from "react-final-form";
import {
formatNewLineDelimitedString,
parseNewLineDelimitedString,
} from "talk-framework/lib/form";
import { Validator } from "talk-framework/lib/validation";
import ValidationMessage from "../../../components/ValidationMessage";
import styles from "./WordListTextArea.css";
interface Props {
className?: string;
validate?: Validator;
id?: string;
name: string;
disabled: boolean;
}
const WordListTextArea: StatelessComponent<Props> = ({
id,
name,
disabled,
validate,
className,
}) => (
<Field
name={name}
parse={parseNewLineDelimitedString}
format={formatNewLineDelimitedString}
validate={validate}
>
{({ input, meta }) => (
<>
<textarea
id={id}
className={cn(className, styles.textArea)}
name={input.name}
onChange={input.onChange}
value={input.value}
disabled={disabled}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
);
export default WordListTextArea;
@@ -0,0 +1,37 @@
import React from "react";
import { graphql } from "react-relay";
import { BannedWordListConfigContainer_settings as SettingsData } from "talk-admin/__generated__/BannedWordListConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import BannedWordListConfig from "../components/BannedWordListConfig";
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled: boolean;
}
class BannedWordListConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <BannedWordListConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment BannedWordListConfigContainer_settings on Settings {
wordList {
banned
}
}
`,
})(BannedWordListConfigContainer);
export default enhanced;
@@ -0,0 +1,37 @@
import React from "react";
import { graphql } from "react-relay";
import { SuspectWordListConfigContainer_settings as SettingsData } from "talk-admin/__generated__/SuspectWordListConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import SuspectWordListConfig from "../components/SuspectWordListConfig";
interface Props {
settings: SettingsData;
onInitValues: (values: SettingsData) => void;
disabled: boolean;
}
class SuspectWordListConfigContainer extends React.Component<Props> {
constructor(props: Props) {
super(props);
props.onInitValues(props.settings);
}
public render() {
const { disabled } = this.props;
return <SuspectWordListConfig disabled={disabled} />;
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment SuspectWordListConfigContainer_settings on Settings {
wordList {
suspect
}
}
`,
})(SuspectWordListConfigContainer);
export default enhanced;
@@ -0,0 +1,54 @@
import { FormApi } from "final-form";
import { RouteProps } from "found";
import { merge } from "lodash";
import React from "react";
import { graphql } from "react-relay";
import { WordListConfigContainer_settings as SettingsData } from "talk-admin/__generated__/WordListConfigContainer_settings.graphql";
import { withFragmentContainer } from "talk-framework/lib/relay";
import WordListConfig from "../components/WordListConfig";
interface Props {
form: FormApi;
submitting: boolean;
settings: SettingsData;
}
class WordListConfigContainer extends React.Component<Props> {
public static routeConfig: RouteProps;
private initialValues = {};
constructor(props: Props) {
super(props);
}
public componentDidMount() {
this.props.form.initialize(this.initialValues);
}
private handleOnInitValues = (values: any) => {
this.initialValues = merge({}, this.initialValues, values);
};
public render() {
return (
<WordListConfig
disabled={this.props.submitting}
settings={this.props.settings}
onInitValues={this.handleOnInitValues}
/>
);
}
}
const enhanced = withFragmentContainer<Props>({
settings: graphql`
fragment WordListConfigContainer_settings on Settings {
...SuspectWordListConfigContainer_settings
...BannedWordListConfigContainer_settings
}
`,
})(WordListConfigContainer);
export default enhanced;
@@ -0,0 +1,47 @@
import { FormApi } from "final-form";
import React from "react";
import { graphql } from "react-relay";
import { WordListRouteContainerQueryResponse } from "talk-admin/__generated__/WordListRouteContainerQuery.graphql";
import { withRouteConfig } from "talk-framework/lib/router";
import { Delay, Spinner } from "talk-ui/components";
import WordListConfigContainer from "./WordListConfigContainer";
interface Props {
data: WordListRouteContainerQueryResponse | null;
form: FormApi;
submitting: boolean;
}
class WordListRouteContainer extends React.Component<Props> {
public render() {
if (!this.props.data) {
return (
<Delay>
<Spinner />
</Delay>
);
}
return (
<WordListConfigContainer
settings={this.props.data.settings}
form={this.props.form}
submitting={this.props.submitting}
/>
);
}
}
const enhanced = withRouteConfig({
query: graphql`
query WordListRouteContainerQuery {
settings {
...WordListConfigContainer_settings
}
}
`,
cacheConfig: { force: true },
})(WordListRouteContainer);
export default enhanced;
@@ -4,7 +4,7 @@ exports[`renders correctly 1`] = `
<div
data-testid="completeAccountBox"
>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
justifyContent="center"
>
<div
@@ -13,21 +13,21 @@ exports[`renders correctly 1`] = `
<div
className="CompleteAccountBox-header"
>
<withPropsOnChange(Typography)
<ForwardRef(forwardRef)
align="center"
className="CompleteAccountBox-heading3"
color="textLight"
variant="heading3"
>
Finish Setting Up Your Account
</withPropsOnChange(Typography)>
<withPropsOnChange(Typography)
</ForwardRef(forwardRef)>
<ForwardRef(forwardRef)
align="center"
color="textLight"
variant="heading1"
>
title
</withPropsOnChange(Typography)>
</ForwardRef(forwardRef)>
</div>
<div
className="CompleteAccountBox-main"
@@ -35,6 +35,6 @@ exports[`renders correctly 1`] = `
content
</div>
</div>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
</div>
`;
@@ -15,9 +15,9 @@ exports[`renders correctly 1`] = `
</ListItem>
<ListItem
icon={
<withPropsOnChange(Icon)>
<ForwardRef(forwardRef)>
keyboard_arrow_right
</withPropsOnChange(Icon)>
</ForwardRef(forwardRef)>
}
>
2
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
alignItems="center"
className="HorizontalSeparator-root"
justifyContent="center"
@@ -14,5 +14,5 @@ exports[`renders correctly 1`] = `
>
Or
</div>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
`;
@@ -12,10 +12,10 @@ exports[`renders correctly 1`] = `
</Localized>
}
>
<withPropsOnChange(HorizontalGutter)
<ForwardRef(forwardRef)
size="oneAndAHalf"
>
<withPropsOnChange(HorizontalGutter) />
</withPropsOnChange(HorizontalGutter)>
<ForwardRef(forwardRef) />
</ForwardRef(forwardRef)>
</AuthBox>
`;
@@ -9,17 +9,17 @@ exports[`renders correctly 1`] = `
}
id="moderate-acceptButton"
>
<withPropsOnChange(WithMouseHover)
<ForwardRef(forwardRef)
aria-label="Accept"
className="AcceptButton-root"
>
<withPropsOnChange(Icon)
<ForwardRef(forwardRef)
className="AcceptButton-icon"
size="lg"
>
done
</withPropsOnChange(Icon)>
</withPropsOnChange(WithMouseHover)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</Localized>
`;
@@ -32,16 +32,16 @@ exports[`renders correctly inverted 1`] = `
}
id="moderate-acceptButton"
>
<withPropsOnChange(WithMouseHover)
<ForwardRef(forwardRef)
aria-label="Accept"
className="AcceptButton-root AcceptButton-invert"
>
<withPropsOnChange(Icon)
<ForwardRef(forwardRef)
className="AcceptButton-icon"
size="lg"
>
done
</withPropsOnChange(Icon)>
</withPropsOnChange(WithMouseHover)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</Localized>
`;
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Typography)
<ForwardRef(forwardRef)
className="custom CommentContent-root"
container="div"
dangerouslySetInnerHTML={
@@ -13,7 +13,7 @@ exports[`renders correctly 1`] = `
`;
exports[`renders empty words correctly 1`] = `
<withPropsOnChange(Typography)
<ForwardRef(forwardRef)
className="custom CommentContent-root"
container="div"
dangerouslySetInnerHTML={
@@ -1,26 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
alignItems="center"
>
<withPropsOnChange(Icon)
<ForwardRef(forwardRef)
className="InReplyTo-icon"
>
reply
</withPropsOnChange(Icon)>
</ForwardRef(forwardRef)>
<Localized
id="moderate-inReplyTo"
username={<Username />}
>
<withPropsOnChange(Typography)
<ForwardRef(forwardRef)
className="InReplyTo-inReplyTo"
container="span"
variant="timestamp"
>
Reply to &lt;username&gt;&lt;username&gt;
</withPropsOnChange(Typography)>
</ForwardRef(forwardRef)>
</Localized>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
`;
@@ -5,7 +5,7 @@ exports[`renders accepted correctly 1`] = `
className="ModerateCard-root"
data-testid="moderate-comment-comment-id"
>
<withPropsOnChange(Flex)>
<ForwardRef(forwardRef)>
<div
className="ModerateCard-mainContainer"
>
@@ -41,10 +41,10 @@ exports[`renders accepted correctly 1`] = `
<div
className="ModerateCard-footer"
>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
justifyContent="flex-end"
>
<withPropsOnChange(Button)
<ForwardRef(forwardRef)
anchor={true}
color="primary"
href="http://localhost/comment"
@@ -59,24 +59,24 @@ exports[`renders accepted correctly 1`] = `
</span>
</Localized>
<withPropsOnChange(Icon)>
<ForwardRef(forwardRef)>
arrow_forward
</withPropsOnChange(Icon)>
</withPropsOnChange(Button)>
</withPropsOnChange(Flex)>
<withPropsOnChange(Flex)
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
<ForwardRef(forwardRef)
itemGutter={true}
>
<Relay(MarkersContainer)
comment={Object {}}
/>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
alignItems="center"
className="ModerateCard-aside ModerateCard-asideWithoutReplyTo"
direction="column"
@@ -91,7 +91,7 @@ exports[`renders accepted correctly 1`] = `
DECISION
</div>
</Localized>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
itemGutter={true}
>
<RejectButton
@@ -103,9 +103,9 @@ exports[`renders accepted correctly 1`] = `
invert={true}
onClick={[Function]}
/>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</withPropsOnChange(Card)>
`;
@@ -114,7 +114,7 @@ exports[`renders correctly 1`] = `
className="ModerateCard-root"
data-testid="moderate-comment-comment-id"
>
<withPropsOnChange(Flex)>
<ForwardRef(forwardRef)>
<div
className="ModerateCard-mainContainer"
>
@@ -150,10 +150,10 @@ exports[`renders correctly 1`] = `
<div
className="ModerateCard-footer"
>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
justifyContent="flex-end"
>
<withPropsOnChange(Button)
<ForwardRef(forwardRef)
anchor={true}
color="primary"
href="http://localhost/comment"
@@ -168,24 +168,24 @@ exports[`renders correctly 1`] = `
</span>
</Localized>
<withPropsOnChange(Icon)>
<ForwardRef(forwardRef)>
arrow_forward
</withPropsOnChange(Icon)>
</withPropsOnChange(Button)>
</withPropsOnChange(Flex)>
<withPropsOnChange(Flex)
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
<ForwardRef(forwardRef)
itemGutter={true}
>
<Relay(MarkersContainer)
comment={Object {}}
/>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
alignItems="center"
className="ModerateCard-aside ModerateCard-asideWithoutReplyTo"
direction="column"
@@ -200,7 +200,7 @@ exports[`renders correctly 1`] = `
DECISION
</div>
</Localized>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
itemGutter={true}
>
<RejectButton
@@ -211,9 +211,9 @@ exports[`renders correctly 1`] = `
invert={false}
onClick={[Function]}
/>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</withPropsOnChange(Card)>
`;
@@ -222,7 +222,7 @@ exports[`renders dangling correctly 1`] = `
className="ModerateCard-root ModerateCard-dangling"
data-testid="moderate-comment-comment-id"
>
<withPropsOnChange(Flex)>
<ForwardRef(forwardRef)>
<div
className="ModerateCard-mainContainer"
>
@@ -258,10 +258,10 @@ exports[`renders dangling correctly 1`] = `
<div
className="ModerateCard-footer"
>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
justifyContent="flex-end"
>
<withPropsOnChange(Button)
<ForwardRef(forwardRef)
anchor={true}
color="primary"
href="http://localhost/comment"
@@ -276,24 +276,24 @@ exports[`renders dangling correctly 1`] = `
</span>
</Localized>
<withPropsOnChange(Icon)>
<ForwardRef(forwardRef)>
arrow_forward
</withPropsOnChange(Icon)>
</withPropsOnChange(Button)>
</withPropsOnChange(Flex)>
<withPropsOnChange(Flex)
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
<ForwardRef(forwardRef)
itemGutter={true}
>
<Relay(MarkersContainer)
comment={Object {}}
/>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
alignItems="center"
className="ModerateCard-aside ModerateCard-asideWithoutReplyTo"
direction="column"
@@ -308,7 +308,7 @@ exports[`renders dangling correctly 1`] = `
DECISION
</div>
</Localized>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
itemGutter={true}
>
<RejectButton
@@ -321,9 +321,9 @@ exports[`renders dangling correctly 1`] = `
invert={false}
onClick={[Function]}
/>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</withPropsOnChange(Card)>
`;
@@ -332,7 +332,7 @@ exports[`renders rejected correctly 1`] = `
className="ModerateCard-root"
data-testid="moderate-comment-comment-id"
>
<withPropsOnChange(Flex)>
<ForwardRef(forwardRef)>
<div
className="ModerateCard-mainContainer"
>
@@ -368,10 +368,10 @@ exports[`renders rejected correctly 1`] = `
<div
className="ModerateCard-footer"
>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
justifyContent="flex-end"
>
<withPropsOnChange(Button)
<ForwardRef(forwardRef)
anchor={true}
color="primary"
href="http://localhost/comment"
@@ -386,24 +386,24 @@ exports[`renders rejected correctly 1`] = `
</span>
</Localized>
<withPropsOnChange(Icon)>
<ForwardRef(forwardRef)>
arrow_forward
</withPropsOnChange(Icon)>
</withPropsOnChange(Button)>
</withPropsOnChange(Flex)>
<withPropsOnChange(Flex)
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
<ForwardRef(forwardRef)
itemGutter={true}
>
<Relay(MarkersContainer)
comment={Object {}}
/>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
alignItems="center"
className="ModerateCard-aside ModerateCard-asideWithoutReplyTo"
direction="column"
@@ -418,7 +418,7 @@ exports[`renders rejected correctly 1`] = `
DECISION
</div>
</Localized>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
itemGutter={true}
>
<RejectButton
@@ -430,9 +430,9 @@ exports[`renders rejected correctly 1`] = `
invert={false}
onClick={[Function]}
/>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</withPropsOnChange(Card)>
`;
@@ -441,7 +441,7 @@ exports[`renders reply correctly 1`] = `
className="ModerateCard-root"
data-testid="moderate-comment-comment-id"
>
<withPropsOnChange(Flex)>
<ForwardRef(forwardRef)>
<div
className="ModerateCard-mainContainer"
>
@@ -482,10 +482,10 @@ exports[`renders reply correctly 1`] = `
<div
className="ModerateCard-footer"
>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
justifyContent="flex-end"
>
<withPropsOnChange(Button)
<ForwardRef(forwardRef)
anchor={true}
color="primary"
href="http://localhost/comment"
@@ -500,24 +500,24 @@ exports[`renders reply correctly 1`] = `
</span>
</Localized>
<withPropsOnChange(Icon)>
<ForwardRef(forwardRef)>
arrow_forward
</withPropsOnChange(Icon)>
</withPropsOnChange(Button)>
</withPropsOnChange(Flex)>
<withPropsOnChange(Flex)
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
<ForwardRef(forwardRef)
itemGutter={true}
>
<Relay(MarkersContainer)
comment={Object {}}
/>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
alignItems="center"
className="ModerateCard-aside"
direction="column"
@@ -532,7 +532,7 @@ exports[`renders reply correctly 1`] = `
DECISION
</div>
</Localized>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
itemGutter={true}
>
<RejectButton
@@ -543,8 +543,8 @@ exports[`renders reply correctly 1`] = `
invert={false}
onClick={[Function]}
/>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
</withPropsOnChange(Card)>
`;
@@ -5,9 +5,9 @@ exports[`renders correctly 1`] = `
<NavigationLink
to="/admin/moderate/reported"
>
<withPropsOnChange(Icon)>
<ForwardRef(forwardRef)>
flag
</withPropsOnChange(Icon)>
</ForwardRef(forwardRef)>
<Localized
id="moderate-navigation-reported"
>
@@ -19,9 +19,9 @@ exports[`renders correctly 1`] = `
<NavigationLink
to="/admin/moderate/pending"
>
<withPropsOnChange(Icon)>
<ForwardRef(forwardRef)>
access_time
</withPropsOnChange(Icon)>
</ForwardRef(forwardRef)>
<Localized
id="moderate-navigation-pending"
>
@@ -33,9 +33,9 @@ exports[`renders correctly 1`] = `
<NavigationLink
to="/admin/moderate/unmoderated"
>
<withPropsOnChange(Icon)>
<ForwardRef(forwardRef)>
forum
</withPropsOnChange(Icon)>
</ForwardRef(forwardRef)>
<Localized
id="moderate-navigation-unmoderated"
>
@@ -47,9 +47,9 @@ exports[`renders correctly 1`] = `
<NavigationLink
to="/admin/moderate/rejected"
>
<withPropsOnChange(Icon)>
<ForwardRef(forwardRef)>
cancel
</withPropsOnChange(Icon)>
</ForwardRef(forwardRef)>
<Localized
id="moderate-navigation-rejected"
>
@@ -66,9 +66,9 @@ exports[`renders correctly with counts 1`] = `
<NavigationLink
to="/admin/moderate/reported"
>
<withPropsOnChange(Icon)>
<ForwardRef(forwardRef)>
flag
</withPropsOnChange(Icon)>
</ForwardRef(forwardRef)>
<Localized
id="moderate-navigation-reported"
>
@@ -85,9 +85,9 @@ exports[`renders correctly with counts 1`] = `
<NavigationLink
to="/admin/moderate/pending"
>
<withPropsOnChange(Icon)>
<ForwardRef(forwardRef)>
access_time
</withPropsOnChange(Icon)>
</ForwardRef(forwardRef)>
<Localized
id="moderate-navigation-pending"
>
@@ -104,9 +104,9 @@ exports[`renders correctly with counts 1`] = `
<NavigationLink
to="/admin/moderate/unmoderated"
>
<withPropsOnChange(Icon)>
<ForwardRef(forwardRef)>
forum
</withPropsOnChange(Icon)>
</ForwardRef(forwardRef)>
<Localized
id="moderate-navigation-unmoderated"
>
@@ -123,9 +123,9 @@ exports[`renders correctly with counts 1`] = `
<NavigationLink
to="/admin/moderate/rejected"
>
<withPropsOnChange(Icon)>
<ForwardRef(forwardRef)>
cancel
</withPropsOnChange(Icon)>
</ForwardRef(forwardRef)>
<Localized
id="moderate-navigation-rejected"
>
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly with load more 1`] = `
<withPropsOnChange(HorizontalGutter)
<ForwardRef(forwardRef)
className="Queue-root"
size="double"
>
@@ -11,19 +11,19 @@ exports[`renders correctly with load more 1`] = `
enter={false}
exit={true}
/>
<withPropsOnChange(Flex)
<ForwardRef(forwardRef)
justifyContent="center"
>
<withContext(WithInView)
disableLoadMore={false}
onLoadMore={[Function]}
/>
</withPropsOnChange(Flex)>
</withPropsOnChange(HorizontalGutter)>
</ForwardRef(forwardRef)>
</ForwardRef(forwardRef)>
`;
exports[`renders correctly without load more 1`] = `
<withPropsOnChange(HorizontalGutter)
<ForwardRef(forwardRef)
className="Queue-root"
size="double"
>
@@ -33,5 +33,5 @@ exports[`renders correctly without load more 1`] = `
enter={false}
exit={true}
/>
</withPropsOnChange(HorizontalGutter)>
</ForwardRef(forwardRef)>
`;

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