[next] Moderate (#2118)

* fix: load .env before building / watching

* feat: Implement AppBar, Brand, and SubBar

* feat: add card ui component

* feat: add modqueue components

* feat: implement modqueue

* feat: add translations

* test: add unit tests

* feat: single comment view

* test: feature / integration tests for modqueue

* test: fix remaining tests

* feature: support TextMatchOptions

* fix: remove body count marker

* fix: remove accidently added package

* feat: testHelper toJSON

* chore: cleanup + comments

* chore: better types

* test: fix test

* chore: refactor decision history test

* chore: tiny fix

* fix: adjust to recent server changes

* fix: marking suspect and banned words

* feat: added moderation queue edge to accept/reject comment payloads

- Simplified moderationQueue returns
- Simplified resolvers

* feat: update counts

* feat: added id's to moderation queue and settings

* fix+test: test count changes, apply fix

* chore: adapt to server change, and remove custom mutation handlers

* fix: use common utils

* fix: purify fix, babel fix

* fix: workaround css treeshake issue and upgrade css plugins

* fix: fixed snapshot

* fix: support empty word lists

* feat: separate client config
This commit is contained in:
Kiwi
2018-12-18 19:00:39 +01:00
committed by Wyatt Johnson
parent 90e67ca479
commit 1fc49f8e50
316 changed files with 11587 additions and 3005 deletions
+4 -2
View File
@@ -3,9 +3,10 @@
* https://babeljs.io/docs/en/config-files#project-wide-configuration
*
* We use this file to apply babel configuration to packages in `node_modules`
* for testing with jest.
*/
const lodashOptimizations = ["use-lodash-es", "lodash"];
const lodashOptimizations =
process.env.WEBPACK === "true" ? ["use-lodash-es", "lodash"] : [];
module.exports = {
env: {
@@ -23,6 +24,7 @@ module.exports = {
],
"@babel/react",
],
plugins: ["dynamic-import-node"],
},
},
};
+2732 -1717
View File
File diff suppressed because it is too large Load Diff
+17 -13
View File
@@ -80,7 +80,7 @@
"joi": "^13.4.0",
"jsonwebtoken": "^8.3.0",
"jwks-rsa": "^1.3.0",
"linkify-it": "^2.0.3",
"linkify-it": "^2.1.0",
"linkifyjs": "^2.1.7",
"lodash": "^4.17.10",
"luxon": "^1.3.1",
@@ -116,6 +116,7 @@
"@babel/preset-env": "^7.2.0",
"@babel/preset-react": "^7.0.0",
"@coralproject/rte": "^0.10.13",
"@intervolga/optimize-cssnano-plugin": "^1.0.6",
"@types/bcryptjs": "^2.4.1",
"@types/bull": "^3.3.16",
"@types/bunyan": "^1.8.4",
@@ -144,7 +145,7 @@
"@types/joi": "^13.0.8",
"@types/jsdom": "^11.12.0",
"@types/jsonwebtoken": "^7.2.7",
"@types/linkify-it": "^2.0.3",
"@types/linkify-it": "^2.0.4",
"@types/linkifyjs": "^2.1.0",
"@types/lodash": "^4.14.118",
"@types/lodash-webpack-plugin": "^0.11.3",
@@ -166,6 +167,7 @@
"@types/react-relay": "^1.3.9",
"@types/react-responsive": "^3.0.1",
"@types/react-test-renderer": "^16.0.1",
"@types/react-transition-group": "^2.0.14",
"@types/recompose": "^0.26.5",
"@types/relay-runtime": "^1.3.6",
"@types/sane": "^2.0.0",
@@ -182,6 +184,7 @@
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^23.6.0",
"babel-loader": "^8.0.4",
"babel-plugin-dynamic-import-node": "^2.2.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-module-resolver": "^3.1.1",
"babel-plugin-relay": "^1.7.0",
@@ -198,7 +201,7 @@
"compression-webpack-plugin": "^1.1.11",
"copy-webpack-plugin": "^4.5.1",
"cross-spawn": "^6.0.5",
"css-loader": "^0.28.11",
"css-loader": "^1.0.1",
"del": "^3.0.0",
"docz": "^0.5.8",
"enzyme": "^3.7.0",
@@ -230,17 +233,17 @@
"lodash-es": "^4.17.11",
"lodash-webpack-plugin": "^0.11.5",
"material-design-icons": "^3.0.1",
"mini-css-extract-plugin": "^0.4.1",
"mini-css-extract-plugin": "^0.5.0",
"npm-run-all": "^4.1.3",
"postcss-advanced-variables": "^2.3.3",
"postcss-css-variables": "^0.9.0",
"postcss-flexbugs-fixes": "^3.3.1",
"postcss-advanced-variables": "^3.0.0",
"postcss-css-variables": "^0.11.0",
"postcss-flexbugs-fixes": "^4.1.0",
"postcss-font-magician": "^2.2.1",
"postcss-import": "^11.1.0",
"postcss-loader": "^2.1.6",
"postcss-nested": "^3.0.0",
"postcss-loader": "^3.0.0",
"postcss-nested": "^4.1.1",
"postcss-prepend-imports": "^1.0.1",
"postcss-preset-env": "^5.2.1",
"postcss-preset-env": "^6.5.0",
"prettier": "^1.13.7",
"prop-types": "^15.6.2",
"pstree.remy": "^1.1.0",
@@ -257,6 +260,7 @@
"react-responsive": "^5.0.0",
"react-test-renderer": "^16.5.2",
"react-timeago": "^4.1.9",
"react-transition-group": "^2.5.0",
"react-with-state-props": "^2.0.4",
"recompose": "^0.27.1",
"relay-compiler": "^1.7.0-rc.1",
@@ -266,7 +270,7 @@
"sane": "^4.0.2",
"simulant": "^0.2.2",
"sinon": "^6.1.5",
"style-loader": "^0.21.0",
"style-loader": "^0.23.1",
"terser-webpack-plugin": "^1.1.0",
"timekeeper": "^2.1.2",
"ts-jest": "^23.0.0",
@@ -275,9 +279,9 @@
"tsconfig-paths": "^3.4.2",
"tsconfig-paths-webpack-plugin": "^3.1.4",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.14.0",
"tslint-config-prettier": "^1.17.0",
"tslint-loader": "^3.6.0",
"tslint-plugin-prettier": "^1.3.0",
"tslint-plugin-prettier": "^2.0.1",
"tslint-react": "^3.6.0",
"typed-css-modules": "^0.3.4",
"typeface-manuale": "0.0.54",
+8 -4
View File
@@ -1,5 +1,11 @@
#!/usr/bin/env ts-node
import dotenv from "dotenv";
// Apply all the configuration provided in the .env file if it isn't already in
// the environment.
dotenv.config();
import chalk from "chalk";
import fs from "fs-extra";
import FileSizeReporter from "react-dev-utils/FileSizeReporter";
@@ -8,8 +14,8 @@ import printBuildError from "react-dev-utils/printBuildError";
import webpack from "webpack";
import paths from "../config/paths";
import config from "../src/core/build/config";
import createWebpackConfig from "../src/core/build/createWebpackConfig";
import config, { createClientEnv } from "../src/core/common/config";
// tslint:disable: no-console
@@ -97,9 +103,7 @@ measureFileSizesBeforeBuild(paths.appDistStatic)
// Create the production build and print the deployment instructions.
function build(previousFileSizes: any) {
console.log("Creating an optimized production build...");
const webpackConfig = createWebpackConfig({
env: createClientEnv(config),
});
const webpackConfig = createWebpackConfig(config);
const compiler = webpack(webpackConfig);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
+7 -4
View File
@@ -1,4 +1,9 @@
#!/usr/bin/env ts-node
import dotenv from "dotenv";
// Apply all the configuration provided in the .env file if it isn't already in
// the environment.
dotenv.config();
import chalk from "chalk";
import {
@@ -10,8 +15,8 @@ import webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";
import createDevServerConfig from "../config/webpackDevServer.config";
import config from "../src/core/build/config";
import createWebpackConfig from "../src/core/build/createWebpackConfig";
import config, { createClientEnv } from "../src/core/common/config";
// tslint:disable: no-console
@@ -58,9 +63,7 @@ choosePort(HOST, PORT)
const protocol = "http";
const appName = "Talk";
const urls = prepareUrls(protocol, HOST, port);
const webpackConfig = createWebpackConfig({
env: createClientEnv(config),
});
const webpackConfig = createWebpackConfig(config);
// Create a webpack compiler that is configured with custom messages.
const compiler = createCompiler(webpack, webpackConfig, appName, urls);
// Serve webpack assets generated by the compiler over a web sever.
+54
View File
@@ -0,0 +1,54 @@
import convict from "convict";
const config = convict({
env: {
doc: "The application environment.",
format: ["production", "development", "test"],
default: "development",
env: "NODE_ENV",
},
port: {
doc: "The port the server is bound to",
format: "port",
default: 3000,
env: "PORT",
arg: "port",
},
generateReport: {
doc: "Generate a report using webpack-bundle-analyzer",
format: Boolean,
default: false,
env: "WEBPACK_REPORT",
arg: "generateReport",
},
disableSourcemaps: {
doc: "Disable sourcemaps generation",
format: Boolean,
default: false,
env: "WEBPACK_DISABLE_SOURCEMAPS",
arg: "disableSourceMaps",
},
disableMinimize: {
doc: "Disable minimization in production",
format: Boolean,
default: false,
env: "WEBPACK_DISABLE_MINIMIZE",
arg: "disableMinimize",
},
enableTreeShake: {
doc: "Enabled tree shaking in development",
format: Boolean,
default: false,
env: "WEBPACK_TREESHAKE",
arg: "enableTreeShake",
},
});
export type Config = typeof config;
export const createClientEnv = (c: Config) => ({
NODE_ENV: c.get("env"),
});
// Setup the base configuration.
export default config;
+53 -46
View File
@@ -1,3 +1,4 @@
import OptimizeCssnanoPlugin from "@intervolga/optimize-cssnano-plugin";
import CaseSensitivePathsPlugin from "case-sensitive-paths-webpack-plugin";
import CompressionPlugin from "compression-webpack-plugin";
import HtmlWebpackPlugin, { Options } from "html-webpack-plugin";
@@ -13,6 +14,8 @@ import webpack, { Configuration, Plugin } from "webpack";
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
import ManifestPlugin from "webpack-manifest-plugin";
import { Config } from "./config";
import { createClientEnv } from "./config";
import paths from "./paths";
import PublicURIWebpackPlugin from "./plugins/PublicURIWebpackPlugin";
@@ -25,33 +28,34 @@ import PublicURIWebpackPlugin from "./plugins/PublicURIWebpackPlugin";
const filterPlugins = (plugins: Array<Plugin | null>): Plugin[] =>
plugins.filter(identity) as Plugin[];
interface CreateWebpackConfig {
publicPath?: string;
publicURL?: string;
env?: Record<string, string>;
disableSourcemaps?: boolean;
interface CreateWebpackOptions {
appendPlugins?: any[];
}
export default function createWebpackConfig({
publicPath = "/",
publicURL = "",
env = process.env as Record<string, string>,
appendPlugins = [],
disableSourcemaps,
}: CreateWebpackConfig = {}): Configuration[] {
const publicPath = "/";
export default function createWebpackConfig(
config: Config,
{ appendPlugins = [] }: CreateWebpackOptions = {}
): Configuration[] {
const env = createClientEnv(config);
const disableSourcemaps = config.get("disableSourcemaps");
const generateReport = config.get("generateReport");
const isProduction = env.NODE_ENV === "production";
const minimize = isProduction && !config.get("disableMinimize");
const treeShake = config.get("enableTreeShake");
const envStringified = {
"process.env": Object.keys(env).reduce<Record<string, string>>(
(result, key) => {
result[key] = JSON.stringify(env[key]);
result[key] = JSON.stringify((env as any)[key]);
return result;
},
{}
),
};
const isProduction = env.NODE_ENV === "production";
/**
* ifProduction will only include the nodes if we're in production mode.
*/
@@ -60,7 +64,7 @@ export default function createWebpackConfig({
: <T extends {}>(...nodes: T[]) => [];
const htmlWebpackConfig: Options = {
minify: isProduction && {
minify: minimize && {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
@@ -109,6 +113,19 @@ export default function createWebpackConfig({
filename: "assets/css/[name].[hash].css",
chunkFilename: "assets/css/[id].[hash].css",
}),
new OptimizeCssnanoPlugin({
sourceMap: true,
cssnanoOptions: {
preset: [
"default",
{
discardComments: {
removeAll: true,
},
},
],
},
}),
// Pre-compress all the assets as they will be served as is.
new CompressionPlugin({}),
]
@@ -128,9 +145,6 @@ export default function createWebpackConfig({
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
];
// If the WEBPACK_STATS environment variable is specified, output the stats!
const includeStats = Boolean(process.env.WEBPACK_STATS);
const baseConfig: Configuration = {
// Set webpack mode.
mode: isProduction ? "production" : "development",
@@ -138,20 +152,15 @@ export default function createWebpackConfig({
concatenateModules: isProduction,
providedExports: true,
usedExports: true,
sideEffects: true,
// We also minimize during development but only
// limit it to dead code and unused code elemination
// to be sure nothing breaks later in the production build.
//
// If modules are written in a side-effects free way this
// should not happen. We strive to write side-effects free
// modules but no one is perfect ;-)
minimize: true,
// We can't use side effects because it disturbs css order
// https://github.com/webpack/webpack/issues/7094.
sideEffects: false,
minimize: minimize || treeShake,
minimizer: [
// Minify the code.
new TerserPlugin({
terserOptions: {
compress: isProduction
compress: minimize
? {}
: {
defaults: false,
@@ -160,9 +169,9 @@ export default function createWebpackConfig({
side_effects: true,
unused: true,
},
mangle: isProduction && {},
mangle: minimize && {},
output: {
comments: !isProduction,
comments: !minimize,
// Turned on because emoji and regex is not minified properly using default
// https://github.com/facebookincubator/create-react-app/issues/2488
ascii_only: true,
@@ -397,7 +406,6 @@ export default function createWebpackConfig({
modules: true,
importLoaders: 1,
localIdentName: "[name]-[local]-[hash:base64:5]",
minimize: isProduction,
sourceMap: isProduction && !disableSourcemaps,
},
},
@@ -438,6 +446,13 @@ export default function createWebpackConfig({
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'development') { ... }. See `./env.js`.
new webpack.DefinePlugin(envStringified),
// If stats are enabled, output them!
generateReport
? new BundleAnalyzerPlugin({
analyzerMode: "static",
reportFilename: "report-assets.html",
})
: null,
...additionalPlugins,
...appendPlugins,
],
@@ -563,18 +578,17 @@ export default function createWebpackConfig({
new ManifestPlugin({
fileName: "asset-manifest.json",
}),
// If stats are enabled, output them!
includeStats
? new BundleAnalyzerPlugin({
analyzerMode: "static",
reportFilename: "report-assets.html",
})
: null,
]),
},
/* Webpack config for our embed */
{
...baseConfig,
optimization: {
...baseConfig.optimization,
// We can turn on sideEffects here as we don't use
// css here and don't run into: https://github.com/webpack/webpack/issues/7094
sideEffects: true,
},
entry: [
/* Use minimal amount of polyfills (for IE) */
"intersection-observer", // also for Safari
@@ -624,13 +638,6 @@ export default function createWebpackConfig({
new ManifestPlugin({
fileName: "embed-manifest.json",
}),
// If stats are enabled, output them!
includeStats
? new BundleAnalyzerPlugin({
analyzerMode: "static",
reportFilename: "report-embed.html",
})
: null,
]),
},
];
+1 -5
View File
@@ -5,9 +5,5 @@
}
}
.container {
max-width: 1280px;
margin: 0 auto;
padding: 0 var(--spacing-unit);
box-sizing: border-box;
.root {
}
+19 -6
View File
@@ -1,15 +1,28 @@
import React, { StatelessComponent } from "react";
import styles from "./App.css";
import AppBar from "./AppBar";
import { Logo } from "talk-ui/components";
import { AppBar, Begin, Divider, End } from "talk-ui/components/AppBar";
import SignOutButtonContainer from "../containers/SignOutButtonContainer";
import DecisionHistoryButton from "./DecisionHistoryButton";
import Navigation from "./Navigation";
import styles from "./App.css";
const App: StatelessComponent = ({ children }) => (
<div>
<AppBar>
<Navigation />
<div className={styles.root}>
<AppBar gutterBegin gutterEnd>
<Begin itemGutter="double">
<Logo />
<Navigation />
</Begin>
<End>
<DecisionHistoryButton />
<Divider />
<SignOutButtonContainer id="navigation-signOutButton" />
</End>
</AppBar>
<div className={styles.container}>{children}</div>
{children}
</div>
);
@@ -1,16 +0,0 @@
.root {
background-color: #f5f5f5;
border-bottom: 1px solid var(--palette-grey-lighter);
}
.container {
max-width: 1280px;
margin: 0 auto;
padding: 0 var(--spacing-unit) 0 calc(2 * var(--spacing-unit));
height: calc(6 * var(--spacing-unit));
box-sizing: border-box;
}
.logo {
margin-right: calc(2.5 * var(--spacing-unit));
}
@@ -1,17 +0,0 @@
import React, { StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import styles from "./AppBar.css";
import Logo from "./Logo";
const AppBar: StatelessComponent = ({ children }) => (
<div className={styles.root}>
<Flex className={styles.container}>
<Logo className={styles.logo} />
{children}
</Flex>
</div>
);
export default AppBar;
@@ -4,6 +4,7 @@ import { oncePerFrame } from "talk-common/utils";
import { BaseButton, ClickOutside, Icon, Popover } from "talk-ui/components";
import DecisionHistoryQuery from "../views/decisionHistory/queries/DecisionHistoryQuery";
import styles from "./DecisionHistoryButton.css";
class DecisionHistoryButton extends React.Component {
@@ -24,17 +25,17 @@ class DecisionHistoryButton extends React.Component {
placement="bottom-end"
description="A dialog showing a permalink to the comment"
classes={{ popover: styles.popover }}
body={({ toggleVisibility }) => (
<ClickOutside
onClickOutside={() =>
this.toggleVisibilityOncePerFrame(toggleVisibility)
}
>
<div>
<DecisionHistoryQuery />
</div>
</ClickOutside>
)}
body={({ toggleVisibility }) => {
const hide = () =>
this.toggleVisibilityOncePerFrame(toggleVisibility);
return (
<ClickOutside onClickOutside={hide}>
<div>
<DecisionHistoryQuery onClosePopover={hide} />
</div>
</ClickOutside>
);
}}
>
{({ toggleVisibility, forwardRef, visible }) => (
<BaseButton
-21
View File
@@ -1,21 +0,0 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import BrandIcon from "./BrandIcon";
import BrandName from "./BrandName";
import styles from "./Logo.css";
interface Props {
className?: string;
}
const Logo: StatelessComponent<Props> = props => (
<Flex alignItems="center" className={cn(styles.root, props.className)}>
<BrandIcon className={styles.icon} />
<BrandName />
</Flex>
);
export default Logo;
@@ -0,0 +1,6 @@
.root {
max-width: 1280px;
margin: 0 auto;
padding: 0 var(--spacing-unit);
box-sizing: border-box;
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import MainLayout from "./MainLayout";
it("renders correctly", () => {
const props: PropTypesOf<typeof MainLayout> = {
children: "content",
};
const wrapper = shallow(<MainLayout {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,21 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import styles from "./MainLayout.css";
interface Props {
className?: string;
children?: React.ReactNode;
}
const MainLayout: StatelessComponent<Props> = ({
children,
className,
...rest
}) => (
<div {...rest} className={cn(styles.root, className)}>
{children}
</div>
);
export default MainLayout;
@@ -1,33 +0,0 @@
.root {
height: 100%;
width: 100%;
}
.link {
color: var(--palette-text-secondary);
font-family: var(--font-family-sans-serif);
font-weight: var(--font-weight-bold);
font-size: calc(18rem / var(--rem-base));
line-height: calc(20em / 18);
letter-spacing: calc(-0.1em / 18);
height: 100%;
display: inline-flex;
align-items: center;
padding: 0 calc(1.5 * var(--spacing-unit));
text-transform: uppercase;
text-decoration: none;
&:hover {
cursor: pointer;
}
}
.active {
background-color: var(--palette-brand);
text-decoration: none;
color: var(--palette-text-light);
}
.historyIcon {
color: var(--palette-text-secondary);
padding: var(--spacing-unit) var(--spacing-unit);
}
+18 -26
View File
@@ -1,36 +1,28 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import { AppBarNavigation } from "talk-ui/components";
import SignOutButtonContainer from "../containers/SignOutButtonContainer";
import DecisionHistoryButton from "./DecisionHistoryButton";
import styles from "./Navigation.css";
import NavigationDivider from "./NavigationDivider";
import NavigationLink from "./NavigationLink";
/* TODO:
<Localized id="navigation-community">
<NavigationLink to="/admin/community">Community</NavigationLink>
</Localized>
<Localized id="navigation-stories">
<NavigationLink to="/admin/stories">Stories</NavigationLink>
</Localized>
*/
const Navigation: StatelessComponent = () => (
<Flex className={styles.root} justifyContent="space-between">
<Flex alignItems="center">
<Localized id="navigation-moderate">
<NavigationLink to="/admin/moderate">Moderate</NavigationLink>
</Localized>
<Localized id="navigation-community">
<NavigationLink to="/admin/community">Community</NavigationLink>
</Localized>
<Localized id="navigation-stories">
<NavigationLink to="/admin/stories">Stories</NavigationLink>
</Localized>
<Localized id="navigation-configure">
<NavigationLink to="/admin/configure">Configure</NavigationLink>
</Localized>
</Flex>
<Flex alignItems="center">
<DecisionHistoryButton />
<NavigationDivider />
<SignOutButtonContainer id="navigation-signOutButton" />
</Flex>
</Flex>
<AppBarNavigation>
<Localized id="navigation-moderate">
<NavigationLink to="/admin/moderate">Moderate</NavigationLink>
</Localized>
<Localized id="navigation-configure">
<NavigationLink to="/admin/configure">Configure</NavigationLink>
</Localized>
</AppBarNavigation>
);
export default Navigation;
@@ -1,4 +0,0 @@
.root {
height: 75%;
border-right: 1px solid var(--palette-divider);
}
@@ -1,9 +0,0 @@
import React, { StatelessComponent } from "react";
import styles from "./NavigationDivider.css";
const NavigationDivider: StatelessComponent<{}> = () => (
<div className={styles.root} />
);
export default NavigationDivider;
@@ -1,23 +0,0 @@
.root {
color: var(--palette-text-secondary);
font-family: var(--font-family-sans-serif);
font-weight: var(--font-weight-medium);
font-size: calc(18rem / var(--rem-base));
line-height: calc(20em / 18);
letter-spacing: calc(-0.1em / 18);
height: 100%;
display: inline-flex;
align-items: center;
padding: 0 calc(1.5 * var(--spacing-unit));
text-transform: uppercase;
text-decoration: none;
&:hover {
cursor: pointer;
}
}
.active {
background-color: var(--palette-brand);
text-decoration: none;
color: var(--palette-text-light);
}
@@ -1,7 +1,7 @@
import { Link, LocationDescriptor } from "found";
import React, { StatelessComponent } from "react";
import styles from "./NavigationLink.css";
import { AppBarNavigationItem } from "talk-ui/components";
interface Props {
children: React.ReactNode;
@@ -9,7 +9,7 @@ interface Props {
}
const NavigationLink: StatelessComponent<Props> = props => (
<Link to={props.to} className={styles.root} activeClassName={styles.active}>
<Link to={props.to} Component={AppBarNavigationItem} activePropName="active">
{props.children}
</Link>
);
@@ -1,14 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div>
<AppBar>
<Navigation />
</AppBar>
<div
className="App-container"
<div
className="App-root"
>
<withPropsOnChange(AppBar)
gutterBegin={true}
gutterEnd={true}
>
child
</div>
<withPropsOnChange(Begin)
itemGutter="double"
>
<withPropsOnChange(Logo) />
<Navigation />
</withPropsOnChange(Begin)>
<withPropsOnChange(End)>
<DecisionHistoryButton />
<withPropsOnChange(Divider) />
<withContext(createMutationContainer(RedirectAppContainer))
id="navigation-signOutButton"
/>
</withPropsOnChange(End)>
</withPropsOnChange(AppBar)>
child
</div>
`;
@@ -1,16 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="AppBar-root"
>
<withPropsOnChange(Flex)
className="AppBar-container"
>
<Logo
className="AppBar-logo"
/>
child
</withPropsOnChange(Flex)>
</div>
`;
@@ -1,46 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<svg
className="BrandIcon-base custom BrandIcon-lg"
data-name="Layer 1"
id="Layer_1"
viewBox="0 0 541.77 557.72"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<clipPath
id="clip-path"
>
<rect
fill="none"
height="570"
width="554"
/>
</clipPath>
<clipPath
id="clip-path-2"
transform="translate(-8.12 -8.14)"
>
<path
clipPath="url(#clip-path)"
clipRule="evenodd"
d="M61.63,350.45c-.67,10.22,1.34,21.13,6,32.21a95.36,95.36,0,0,0,27.22,36.41c26,21.16,52.14,17.65,77.43,14.24,21.22-2.85,43.17-5.8,69,5.43,33.54,14.57,59.71,45.39,66.65,78.46a99.16,99.16,0,0,1,.11,38.66H219.65a16.89,16.89,0,0,0-.71-2.44c-16.54-44.09-38.46-69.52-67.09-77.78-9.37-2.66-18-3-25.68-3.27-9.91-.37-17.75-.66-26-5.6s-14.93-13.28-19.59-24.42a17,17,0,0,0-22.12-9.07,16.69,16.69,0,0,0-9.17,21.88c7.45,17.8,18.6,31.31,33.25,40.21,15.85,9.48,30.54,10,42.35,10.48,6.55.24,12.2.45,17.55,2,15.64,4.51,29.43,20.65,41.07,48H63.07a45,45,0,0,1-44.95-45V322.78a144.51,144.51,0,0,0,43.51,27.67Zm13-31.55a109.22,109.22,0,0,1-56.48-52.22v-66a88.41,88.41,0,0,0,35.59,10.37c18.52,1.13,34.77-4.64,47-16.71,11.63-11.51,14.6-24.24,18.05-39,2.88-12.32,6.46-27.66,16.66-48.7,10.44-21.72,17.13-30,24.69-30.51,11-.79,29.3,13.46,32.09,34.75,2,14.93-3.58,25.14-10.07,37C175,161,166,177.28,172.4,198.15c6.81,22.16,25.44,31.45,40.4,38.91,15.61,7.78,25.42,13.21,30.37,26.2,4.26,11.27,4.87,27.9-.71,33.32-3.74,3.66-16.43.64-29.86-2.55-23-5.47-54.5-12.95-91.49-1.57-9.77,3-32.07,9.72-46.51,26.44ZM357.73,18.14c-15,17.37-28.76,40.23-34.85,69-4.25,19.33-13.09,59.57,13.51,88.26A71.68,71.68,0,0,0,388.11,198a61.12,61.12,0,0,0,22.65-4.23c31.63-12.61,46.84-43.4,49.25-56.36,3.24-17.51,5-35.76,6.33-51.32a11.43,11.43,0,0,1,5.38-8.61l.5-.31a12.23,12.23,0,0,1,13.33,1.63c19.82,16.56,36.73,31.82,51.69,46.63a17.07,17.07,0,0,0,2.64,2.14v93.21a16.92,16.92,0,0,0-3.87,4.69c-9.23,16.43-23.71,36.42-40.11,38.31-6.55.7-10.3-1.49-20.21-7.9S452.37,240.8,431,236c-23.2-5.22-38.56-2.06-50.9.48-10.61,2.18-17.62,3.62-28.9,0-27.43-9-42.31-36.22-51.21-52.47l-.24-.45c-19.81-36.54-18.23-71.85-16.84-103l0-.58a287.16,287.16,0,0,1,9.81-61.77Zm49.14,0h88.06a45,45,0,0,1,44.95,45V80.79c-10.08-9.22-21-18.69-32.79-28.58a45.72,45.72,0,0,0-50.93-5.38,19.1,19.1,0,0,0-2.55,1.52,46.1,46.1,0,0,0-21,34.46,1.49,1.49,0,0,0,0,.21c-1.24,14.7-2.91,31.88-5.86,47.9-.88,3.66-9.12,23.11-28.33,30.76-12.81,5.1-29.11-1.09-37.41-10-14.22-15.33-8.26-42.48-5-57.19C363.43,59.29,386.3,35,404.17,20.82a18,18,0,0,0,2.7-2.68Zm-149.42,0a319.88,319.88,0,0,0-8.67,60.21l0,.62c-1.43,32-3.38,75.91,21,120.82l.28.52c9.81,17.93,30.25,55.25,70.55,68.52,19.95,6.48,34,3.59,46.44,1,11-2.26,20.48-4.21,36.49-.61,15.43,3.48,24.69,9.46,33.63,15.25C467,290.87,478,298,493.8,298a54,54,0,0,0,5.86-.32c14.5-1.67,27.83-8.41,40.22-20.42v45.53a179.81,179.81,0,0,1-47.68.48c-17-2.18-28.9-6.21-40.39-10.1-16.13-5.46-31.37-10.63-54.46-8.79-12.34.86-49.5,3.52-68.67,32.46-21.27,32-4.79,71.47-1.27,79.05,0,.06,0,.11.07.17,11.15,23.37,28,33.16,44.25,42.63,9.57,5.57,19.46,11.33,29.45,20.4C421,497.23,435.05,523,443,555.86H342.61a132,132,0,0,0-1.29-45.63c-9.16-43.62-43.1-84-86.46-102.82-34.51-15-63.68-11.11-87.12-8-24.18,3.25-37.44,4.42-51.35-6.91-15-12.14-24-32.83-19.67-45.1,4.74-13.34,25.44-19.62,34.35-22.32,28.19-8.67,52.33-2.93,73.64,2.14,21.83,5.18,44.4,10.55,61.57-6.21,17.7-17.22,17-48.29,8.81-69.92-10-26.15-30.53-36.41-47.06-44.65-13.09-6.53-20.6-10.6-23-18.37-2-6.43.36-11.62,7-23.75,7.41-13.48,17.55-31.93,14-58-4.77-36.43-36.65-66.5-68.28-64.3-30.19,2.1-44.53,32-53.08,49.73-11.83,24.42-16.07,42.54-19.16,55.78-2.9,12.39-4.37,18.06-8.79,22.43-5.21,5.13-12.26,7.47-21,6.93-13-.8-27.57-8-37.68-18.45V63.15a45,45,0,0,1,45-45Zm282.43,449v43.76a45,45,0,0,1-44.95,45H477.46a16.09,16.09,0,0,0-.36-2.56c-4.89-22.41-12.24-42.37-22-59.73a120.79,120.79,0,0,1,39-21,119.51,119.51,0,0,1,45.77-5.45Zm0-34.82a152.6,152.6,0,0,0-56,7.12,154.85,154.85,0,0,0-48.66,25.94,153.26,153.26,0,0,0-11.29-11.5c-12.68-11.52-24.67-18.5-35.24-24.65-14.45-8.41-24-14-30.62-27.78-.87-1.9-12.66-28.42-1.23-45.62,10-15.1,33.94-16.77,42.95-17.4l.17,0c16.24-1.3,26.66,2.23,41.07,7.11,12.45,4.22,26.57,9,46.95,11.61a212.66,212.66,0,0,0,51.92.08v75.11Z"
fill="none"
/>
</clipPath>
</defs>
<title>
logo mark2018
</title>
<g
clipPath="url(#clip-path-2)"
>
<rect
fill="#f7705f"
height="557.72"
width="541.77"
/>
</g>
</svg>
`;
@@ -1,15 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Localized
id="general-brandName"
>
<withPropsOnChange(Typography)
align="center"
className="custom BrandName-root BrandName-lg"
variant="heading1"
>
Talk
</withPropsOnChange(Typography)>
</Localized>
`;
@@ -1,17 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Flex)
alignItems="center"
className="Logo-root custom"
>
<BrandIcon
className="Logo-icon"
size="md"
/>
<BrandName
align="center"
size="md"
/>
</withPropsOnChange(Flex)>
`;
@@ -2,6 +2,8 @@
exports[`renders correctly 1`] = `
<div
className="NavigationDivider-root"
/>
className="MainLayout-root"
>
content
</div>
`;
@@ -1,58 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Flex)
className="Navigation-root"
justifyContent="space-between"
>
<withPropsOnChange(Flex)
alignItems="center"
<withPropsOnChange(Navigation)>
<Localized
id="navigation-moderate"
>
<Localized
id="navigation-moderate"
<NavigationLink
to="/admin/moderate"
>
<NavigationLink
to="/admin/moderate"
>
Moderate
</NavigationLink>
</Localized>
<Localized
id="navigation-community"
>
<NavigationLink
to="/admin/community"
>
Community
</NavigationLink>
</Localized>
<Localized
id="navigation-stories"
>
<NavigationLink
to="/admin/stories"
>
Stories
</NavigationLink>
</Localized>
<Localized
id="navigation-configure"
>
<NavigationLink
to="/admin/configure"
>
Configure
</NavigationLink>
</Localized>
</withPropsOnChange(Flex)>
<withPropsOnChange(Flex)
alignItems="center"
Moderate
</NavigationLink>
</Localized>
<Localized
id="navigation-configure"
>
<DecisionHistoryButton />
<NavigationDivider />
<withContext(createMutationContainer(RedirectAppContainer))
id="navigation-signOutButton"
/>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
<NavigationLink
to="/admin/configure"
>
Configure
</NavigationLink>
</Localized>
</withPropsOnChange(Navigation)>
`;
@@ -2,8 +2,8 @@
exports[`renders correctly 1`] = `
<Link
activeClassName="NavigationLink-active"
className="NavigationLink-root"
Component={[Function]}
activePropName="active"
to="/moderate"
>
link
@@ -0,0 +1,23 @@
import { ConnectionHandler, RecordSourceSelectorProxy } from "relay-runtime";
type Queue = "reported" | "pending" | "unmoderated" | "rejected";
export default function getQueueConnection(
queue: Queue,
store: RecordSourceSelectorProxy
) {
const root = store.getRoot();
if (queue === "rejected") {
return ConnectionHandler.getConnection(root, "RejectedQueue_comments", {
filter: { status: "REJECTED" },
});
}
const queuesRecord = root.getLinkedRecord("moderationQueues")!;
if (!queuesRecord) {
return null;
}
return ConnectionHandler.getConnection(
queuesRecord.getLinkedRecord(queue),
"Queue_comments"
);
}
+1
View File
@@ -0,0 +1 @@
export { default as getQueueConnection } from "./getQueueConnection";
@@ -0,0 +1,82 @@
import { graphql } from "react-relay";
import { ConnectionHandler, Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { AcceptCommentMutation as MutationTypes } from "talk-admin/__generated__/AcceptCommentMutation.graphql";
import { getQueueConnection } from "talk-admin/helpers";
export type AcceptCommentInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
const mutation = graphql`
mutation AcceptCommentMutation($input: AcceptCommentInput!) {
acceptComment(input: $input) {
comment {
id
status
}
moderationQueues {
unmoderated {
count
}
reported {
count
}
pending {
count
}
}
clientMutationId
}
}
`;
let clientMutationId = 0;
function commit(environment: Environment, input: AcceptCommentInput) {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
input: {
...input,
clientMutationId: clientMutationId.toString(),
},
},
optimisticResponse: {
acceptComment: {
comment: {
id: input.commentID,
status: "ACCEPTED",
},
clientMutationId: (clientMutationId++).toString(),
},
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
updater: store => {
const connections = [
getQueueConnection("reported", store),
getQueueConnection("pending", store),
getQueueConnection("unmoderated", store),
getQueueConnection("rejected", store),
].filter(c => c);
connections.forEach(con =>
ConnectionHandler.deleteNode(con, input.commentID)
);
},
});
}
export const withAcceptCommentMutation = createMutationContainer(
"acceptComment",
commit
);
export type AcceptCommentMutation = (
input: AcceptCommentInput
) => Promise<MutationTypes["response"]["acceptComment"]>;
@@ -43,21 +43,6 @@ function commit(
clientMutationId: (clientMutationId++).toString(),
},
},
updater: store => {
const record = store
.getRootField("createOIDCAuthIntegration")!
.getLinkedRecord("settings")!
.getLinkedRecord("auth")!
.getLinkedRecord("integrations");
if (record) {
store
.getRoot()
.getLinkedRecord("settings")!
.getLinkedRecord("auth")!
.getLinkedRecord("integrations")!
.copyFieldsFrom(record);
}
},
});
}
@@ -36,23 +36,6 @@ function commit(environment: Environment) {
clientMutationId: (clientMutationId++).toString(),
},
},
updater: store => {
const record = store
.getRootField("regenerateSSOKey")!
.getLinkedRecord("settings")!
.getLinkedRecord("auth")!
.getLinkedRecord("integrations")!
.getLinkedRecord("sso");
if (record) {
store
.getRoot()
.getLinkedRecord("settings")!
.getLinkedRecord("auth")!
.getLinkedRecord("integrations")!
.getLinkedRecord("sso")!
.copyFieldsFrom(record);
}
},
});
}
@@ -0,0 +1,81 @@
import { graphql } from "react-relay";
import { ConnectionHandler, Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { RejectCommentMutation as MutationTypes } from "talk-admin/__generated__/RejectCommentMutation.graphql";
import { getQueueConnection } from "talk-admin/helpers";
export type RejectCommentInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
const mutation = graphql`
mutation RejectCommentMutation($input: RejectCommentInput!) {
rejectComment(input: $input) {
comment {
id
status
}
moderationQueues {
unmoderated {
count
}
reported {
count
}
pending {
count
}
}
clientMutationId
}
}
`;
let clientMutationId = 0;
function commit(environment: Environment, input: RejectCommentInput) {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
input: {
...input,
clientMutationId: clientMutationId.toString(),
},
},
optimisticResponse: {
rejectComment: {
comment: {
id: input.commentID,
status: "REJECTED",
},
clientMutationId: (clientMutationId++).toString(),
},
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
updater: store => {
const connections = [
getQueueConnection("reported", store),
getQueueConnection("pending", store),
getQueueConnection("unmoderated", store),
].filter(c => c);
connections.forEach(con =>
ConnectionHandler.deleteNode(con, input.commentID)
);
},
});
}
export const withRejectCommentMutation = createMutationContainer(
"rejectComment",
commit
);
export type RejectCommentMutation = (
input: RejectCommentInput
) => Promise<MutationTypes["response"]["rejectComment"]>;
@@ -46,17 +46,6 @@ function commit(environment: Environment, input: UpdateSettingsInput) {
clientMutationId: (clientMutationId++).toString(),
},
},
updater: store => {
const record = store
.getRootField("updateSettings")!
.getLinkedRecord("settings");
if (record) {
store
.getRoot()
.getLinkedRecord("settings")!
.copyFieldsFrom(record);
}
},
});
}
+8
View File
@@ -3,6 +3,14 @@ export {
withSetRedirectPathMutation,
SetRedirectPathMutation,
} from "./SetRedirectPathMutation";
export {
withAcceptCommentMutation,
AcceptCommentMutation,
} from "./AcceptCommentMutation";
export {
withRejectCommentMutation,
RejectCommentMutation,
} from "./RejectCommentMutation";
export {
withUpdateSettingsMutation,
UpdateSettingsMutation,
+25 -5
View File
@@ -5,11 +5,18 @@ import App from "./components/App";
import RedirectAppContainer from "./containers/RedirectAppContainer";
import RedirectLoginContainer from "./containers/RedirectLoginContainer";
import Community from "./routes/community/components/Community";
import ConfigureMisc from "./routes/configure/components/Misc";
import ConfigureModeration from "./routes/configure/components/Moderation";
import ConfigureContainer from "./routes/configure/containers/ConfigureContainer";
import ConfigureAuthContainer from "./routes/configure/sections/auth/containers/AuthContainer";
import Login from "./routes/login/components/Login";
import Moderate from "./routes/moderate/components/Moderate";
import ModerateContainer from "./routes/moderate/containers/ModerateContainer";
import {
PendingQueueContainer,
ReportedQueueContainer,
UnmoderatedQueueContainer,
} from "./routes/moderate/containers/QueueContainer";
import RejectedQueueContainer from "./routes/moderate/containers/RejectedQueueContainer";
import SingleModerateContainer from "./routes/moderate/containers/SingleModerateContainer";
import Stories from "./routes/stories/components/Stories";
export default makeRouteConfig(
@@ -17,13 +24,26 @@ export default makeRouteConfig(
<Route Component={RedirectLoginContainer}>
<Route Component={App}>
<Redirect from="/" to="/admin/moderate" />
<Route path="moderate" Component={Moderate} />
<Route
path="moderate/comment/:commentID"
{...SingleModerateContainer.routeConfig}
/>
<Route path="moderate" {...ModerateContainer.routeConfig}>
<Redirect from="/" to="/admin/moderate/reported" />
<Route path="reported" {...ReportedQueueContainer.routeConfig} />
<Route path="pending" {...PendingQueueContainer.routeConfig} />
<Route
path="unmoderated"
{...UnmoderatedQueueContainer.routeConfig}
/>
<Route path="rejected" {...RejectedQueueContainer.routeConfig} />
</Route>
<Route path="community" Component={Community} />
<Route path="stories" Component={Stories} />
<Route path="configure" Component={ConfigureContainer}>
<Redirect from="/" to="/admin/configure/auth" />
<Redirect from="/" to="/admin/configure/moderation" />
<Route path="moderation" Component={ConfigureModeration} />
<Route path="auth" {...ConfigureAuthContainer.routeConfig} />
<Route path="misc" Component={ConfigureMisc} />
</Route>
</Route>
</Route>
@@ -1,10 +1,12 @@
import React, { StatelessComponent } from "react";
import { HorizontalGutter, Typography } from "talk-ui/components";
import MainLayout from "talk-admin/components/MainLayout";
import { Typography } from "talk-ui/components";
const Community: StatelessComponent = () => (
<HorizontalGutter>
<MainLayout>
<Typography variant="heading3">Community</Typography>
</HorizontalGutter>
</MainLayout>
);
export default Community;
@@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(HorizontalGutter)>
<MainLayout>
<withPropsOnChange(Typography)
variant="heading3"
>
Community
</withPropsOnChange(Typography)>
</withPropsOnChange(HorizontalGutter)>
</MainLayout>
`;
@@ -3,7 +3,9 @@ import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Form, FormSpy } from "react-final-form";
import MainLayout from "talk-admin/components/MainLayout";
import { Button, CallOut, HorizontalGutter } from "talk-ui/components";
import Layout from "./Layout";
import Main from "./Main";
import { Link, Navigation } from "./Navigation";
@@ -19,7 +21,7 @@ const Configure: StatelessComponent<Props> = ({
onChange,
children,
}) => (
<div data-test="configure-container">
<MainLayout data-test="configure-container">
<Form onSubmit={onSave}>
{({ handleSubmit, submitting, pristine, form, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit} id="configure-form">
@@ -28,10 +30,10 @@ const Configure: StatelessComponent<Props> = ({
<SideBar>
<HorizontalGutter size="double">
<Navigation>
<Link to="/admin/configure/moderation">Moderation</Link>
<Localized id="configure-sideBarNavigation-authentication">
<Link to="/admin/configure/auth">Auth</Link>
</Localized>
<Link to="/admin/configure/misc">Misc</Link>
</Navigation>
</HorizontalGutter>
<HorizontalGutter size="double">
@@ -67,7 +69,7 @@ const Configure: StatelessComponent<Props> = ({
</form>
)}
</Form>
</div>
</MainLayout>
);
export default Configure;
@@ -1,14 +0,0 @@
import React, { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
import Header from "./Header";
const Misc: StatelessComponent = ({ children }) => (
<div>
<Header>Misc Integrations</Header>
<Typography>Other stuff</Typography>
</div>
);
export default Misc;
@@ -3,12 +3,12 @@ import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Misc from "./Misc";
import Moderation from "./Moderation";
it("renders correctly", () => {
const props: PropTypesOf<typeof Misc> = {
const props: PropTypesOf<typeof Moderation> = {
children: "child",
};
const wrapper = shallow(<Misc {...props} />);
const wrapper = shallow(<Moderation {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,19 @@
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;
@@ -9,10 +9,10 @@
color: var(--palette-text-primary);
font-family: var(--font-family-sans-serif);
font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-regular);
font-size: calc(18rem / var(--rem-base));
line-height: calc(20em / 18);
letter-spacing: calc(-0.1em / 18);
letter-spacing: calc(0.2em / 18);
&:hover {
cursor: pointer;
@@ -20,6 +20,7 @@
}
.linkActive {
font-weight: var(--font-weight-bold);
margin-left: 0px;
border-left: calc(0.5 * var(--spacing-unit)) solid var(--palette-brand);
padding-left: var(--spacing-unit);
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
<MainLayout
data-test="configure-container"
>
<ReactFinalForm
@@ -9,5 +9,5 @@ exports[`renders correctly 1`] = `
>
<Component />
</ReactFinalForm>
</div>
</MainLayout>
`;
@@ -1,12 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div>
<Header>
Misc Integrations
</Header>
<withPropsOnChange(Typography)>
Other stuff
</withPropsOnChange(Typography)>
</div>
`;
@@ -0,0 +1,12 @@
// 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>
`;
@@ -3,13 +3,13 @@ import React, { StatelessComponent } from "react";
import { InputDescription } from "talk-ui/components";
import { PropTypesOf } from "talk-ui/types";
import styles from "./ConfigDescription.css";
interface Props {
container?: PropTypesOf<typeof InputDescription>["container"];
children: React.ReactNode;
}
import styles from "./ConfigDescription.css";
const ConfigDescription: StatelessComponent<Props> = ({
children,
container,
@@ -26,12 +26,13 @@ import ClientIDField from "./ClientIDField";
import ClientSecretField from "./ClientSecretField";
import ConfigBoxWithToggleField from "./ConfigBoxWithToggleField";
import ConfigDescription from "./ConfigDescription";
import styles from "./OIDCConfig.css";
import RedirectField from "./RedirectField";
import RegistrationField from "./RegistrationField";
import TargetFilterField from "./TargetFilterField";
import ValidationMessage from "./ValidationMessage";
import styles from "./OIDCConfig.css";
interface Props {
index: number;
disabled?: boolean;
@@ -2,12 +2,12 @@ import React, { StatelessComponent } from "react";
import { ValidationMessage as UIValidationMessage } from "talk-ui/components";
import styles from "./ValidationMessage.css";
interface Props {
children: React.ReactNode;
}
import styles from "./ValidationMessage.css";
const ValidationMessage: StatelessComponent<Props> = ({ children }) => (
<UIValidationMessage className={styles.root}>{children}</UIValidationMessage>
);
@@ -79,7 +79,7 @@ export default class AuthContainer extends React.Component<Props> {
};
private handleOnInitValues = (values: any) => {
this.initialValues = merge(this.initialValues, values);
this.initialValues = merge({}, this.initialValues, values);
};
public render() {
@@ -1,17 +1,20 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import AppBar from "talk-admin/components/AppBar";
import BrandName from "talk-admin/components/BrandName";
import { Flex, HorizontalGutter, Typography } from "talk-ui/components";
import {
BrandIcon,
BrandName,
Flex,
HorizontalGutter,
Typography,
} from "talk-ui/components";
import BrandIcon from "talk-admin/components/BrandIcon";
import SignInFormContainer from "../containers/SignInFormContainer";
import styles from "./Login.css";
const Login: StatelessComponent = () => (
<div>
<AppBar />
<Flex justifyContent="center">
<HorizontalGutter className={styles.loginContainer} size="double">
<Flex justifyContent="center">
@@ -2,7 +2,6 @@
exports[`renders correctly 1`] = `
<div>
<AppBar />
<withPropsOnChange(Flex)
justifyContent="center"
>
@@ -16,7 +15,7 @@ exports[`renders correctly 1`] = `
<div
className="Login-brandIcon"
>
<BrandIcon
<withPropsOnChange(BrandIcon)
size="lg"
/>
</div>
@@ -32,7 +31,7 @@ exports[`renders correctly 1`] = `
Sign in to
</withPropsOnChange(Typography)>
</Localized>
<BrandName
<withPropsOnChange(BrandName)
align="center"
size="lg"
/>
@@ -0,0 +1,25 @@
.root {
border: 1px solid var(--palette-success-main);
box-sizing: border-box;
border-radius: 2px;
width: 65px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
color: var(--palette-success-main);
&:active {
background-color: var(--palette-success-main);
color: var(--palette-common-white);
}
}
.invert {
background-color: var(--palette-success-main);
color: var(--palette-common-white);
}
.icon {
font-weight: var(--font-weight-bold);
color: inherit;
}
@@ -0,0 +1,22 @@
import { shallow } from "enzyme";
import React from "react";
import AcceptButton from "./AcceptButton";
import { PropTypesOf } from "talk-framework/types";
it("renders correctly", () => {
const props: PropTypesOf<typeof AcceptButton> = {
invert: false,
};
const wrapper = shallow(<AcceptButton {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders correctly inverted", () => {
const props: PropTypesOf<typeof AcceptButton> = {
invert: true,
};
const wrapper = shallow(<AcceptButton {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,34 @@
import cn from "classnames";
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { BaseButton, Icon } from "talk-ui/components";
import styles from "./AcceptButton.css";
interface Props extends PropTypesOf<typeof BaseButton> {
invert?: boolean;
}
const AcceptButton: StatelessComponent<Props> = ({
invert,
className,
...rest
}) => (
<Localized id="moderate-acceptButton" attrs={{ "aria-label": true }}>
<BaseButton
{...rest}
className={cn(className, styles.root, {
[styles.invert]: invert,
})}
aria-label="Accept"
>
<Icon size="lg" className={styles.icon}>
done
</Icon>
</BaseButton>
</Localized>
);
export default AcceptButton;
@@ -0,0 +1,11 @@
.root {
mark {
background-color: var(--palette-highlight);
padding: 0 2px;
}
a {
color: var(--palette-primary-dark);
background-color: var(--palette-highlight);
padding: 0 2px;
}
}
@@ -0,0 +1,28 @@
import { shallow } from "enzyme";
import React from "react";
import CommentContent from "./CommentContent";
import { PropTypesOf } from "talk-framework/types";
it("renders correctly", () => {
const props: PropTypesOf<typeof CommentContent> = {
suspectWords: ["idiot", "damn"],
bannedWords: ["fuck", "fucking"],
className: "custom",
children: "Hello <strong>idiot</strong>, you fucking bastard",
};
const wrapper = shallow(<CommentContent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders empty words correctly", () => {
const props: PropTypesOf<typeof CommentContent> = {
suspectWords: [],
bannedWords: [],
className: "custom",
children: "Hello <strong>idiot</strong>, you fucking bastard",
};
const wrapper = shallow(<CommentContent {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,142 @@
import cn from "classnames";
import { memoize } from "lodash";
import React, { StatelessComponent } from "react";
import { createPurify } from "talk-common/utils/purify";
import { Typography } from "talk-ui/components";
import styles from "./CommentContent.css";
/**
* Create a purify instance that will be used to handle HTML content.
*/
const purify = createPurify(window, false);
interface Props {
className?: string;
children: string;
suspectWords: ReadonlyArray<string>;
bannedWords: ReadonlyArray<string>;
}
function escapeHTML(unsafe: string) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function escapeRegExp(str: string) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
// generate a regulare expression that catches the `phrases`.
function generateRegExp(phrases: ReadonlyArray<string>) {
const inner = phrases
.map(phrase =>
phrase
.split(/\s+/)
.map(word => escapeRegExp(word))
.join('[\\s"?!.]+')
)
.join("|");
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
try {
return new RegExp(pattern, "iu");
} catch (_err) {
// IE does not support unicode support, so we'll create one without.
return new RegExp(pattern, "i");
}
}
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
function getPhrasesRegexp(
suspectWords: ReadonlyArray<string>,
bannedWords: ReadonlyArray<string>
) {
return generateRegExp([...suspectWords, ...bannedWords]);
}
// Memoized version as arguments rarely change.
const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
// markPhrasesHTML looks for `supsectWords` and `bannedWords` inside `text` and highlights them by returning
// a HTML string.
function markPhrasesHTML(
text: string,
suspectWords: ReadonlyArray<string>,
bannedWords: ReadonlyArray<string>
) {
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
const tokens = text.split(regexp);
if (tokens.length === 1) {
return text;
}
return tokens
.map(
(token, i) =>
// Using our Regexp patterns it returns tokens arranged this way
// [STRING_WITH_NO_MATCH, NEW_WORD_DELIMITER, MATCHED_WORD, ...].
// This pattern repeats throughout. Next line will mark MATCHED_WORD
// and escape all tokens.
i % 3 === 2 ? `<mark>${escapeHTML(token)}</mark>` : escapeHTML(token)
)
.join("");
}
// markHTMLNode manipulates the node by looking for #text nodes and adding markers
// for `supsectWords` and `bannedWords`.
function markHTMLNode(
parentNode: Node,
suspectWords: ReadonlyArray<string>,
bannedWords: ReadonlyArray<string>
) {
parentNode.childNodes.forEach(node => {
if (node.nodeName === "#text") {
const newContent = markPhrasesHTML(
node.textContent!,
suspectWords,
bannedWords
);
if (newContent !== node.textContent) {
const newNode = document.createElement("span");
newNode.innerHTML = newContent;
parentNode.replaceChild(newNode, node);
}
} else {
markHTMLNode(node, suspectWords, bannedWords);
}
});
}
const CommentContent: StatelessComponent<Props> = ({
suspectWords,
bannedWords,
className,
children,
}) => {
// We create a Shadow DOM Tree with the HTML body content and
// use it as a parser.
const node = document.createElement("div");
node.innerHTML = purify.sanitize(children);
if (suspectWords.length || bannedWords.length) {
// Then we traverse it recursively and manipulate it to highlight suspect words
// and banned words.
markHTMLNode(node, suspectWords, bannedWords);
}
// Finally we render the content of the Shadow DOM Tree
return (
<Typography
className={cn(className, styles.root)}
dangerouslySetInnerHTML={{ __html: node.innerHTML }}
container="div"
/>
);
};
export default CommentContent;
@@ -0,0 +1,9 @@
.icon {
color: var(--palette-grey-main);
}
.inReplyTo {
color: var(--palette-grey-main);
}
.username {
color: var(--palette-grey-main);
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import InReplyTo from "./InReplyTo";
it("renders correctly", () => {
const props: PropTypesOf<typeof InReplyTo> = {
children: "Username",
};
const wrapper = shallow(<InReplyTo {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,35 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Flex, Icon, Typography } from "talk-ui/components";
import styles from "./InReplyTo.css";
interface Props {
children: string;
}
const InReplyTo: StatelessComponent<Props> = ({ children }) => {
const Username = () => (
<Typography variant="heading5" container="span" className={styles.username}>
{children}
</Typography>
);
return (
<Flex alignItems="center">
<Icon className={styles.icon}>reply</Icon>{" "}
<Localized id="moderate-inReplyTo" username={<Username />}>
<Typography
variant="timestamp"
container="span"
className={styles.inReplyTo}
>
{"Reply to <username><username>"}
</Typography>
</Localized>
</Flex>
);
};
export default InReplyTo;
@@ -0,0 +1,11 @@
import React, { StatelessComponent } from "react";
import { Flex, Spinner } from "talk-ui/components";
const LoadingQueue: StatelessComponent = () => (
<Flex justifyContent="center" data-test="loading-moderate-container">
<Spinner />
</Flex>
);
export default LoadingQueue;
@@ -0,0 +1,15 @@
.background {
position: fixed;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
background-color: var(--palette-background-light);
}
.main {
margin: calc(2 * var(--spacing-unit)) 0 calc(4 * var(--spacing-unit)) 0;
display: flex;
justify-content: center;
}
@@ -3,7 +3,18 @@ import React from "react";
import Moderate from "./Moderate";
import { PropTypesOf } from "talk-framework/types";
it("renders correctly", () => {
const wrapper = shallow(<Moderate />);
expect(wrapper).toMatchSnapshot();
});
it("renders correctly with counts", () => {
const props: PropTypesOf<typeof Moderate> = {
unmoderatedCount: 3,
reportedCount: 4,
pendingCount: 0,
};
const wrapper = shallow(<Moderate {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -1,10 +1,38 @@
import React, { StatelessComponent } from "react";
import { HorizontalGutter, Typography } from "talk-ui/components";
const Moderate: StatelessComponent = ({ children }) => (
<HorizontalGutter>
<Typography variant="heading3">Moderate</Typography>
</HorizontalGutter>
import MainLayout from "talk-admin/components/MainLayout";
import { SubBar } from "talk-ui/components/SubBar";
import Navigation from "./Navigation";
import styles from "./Moderate.css";
interface Props {
unmoderatedCount?: number;
reportedCount?: number;
pendingCount?: number;
children?: React.ReactNode;
}
const Moderate: StatelessComponent<Props> = ({
unmoderatedCount,
reportedCount,
pendingCount,
children,
}) => (
<div data-test="moderate-container">
<SubBar data-test="moderate-subBar-container">
<Navigation
unmoderatedCount={unmoderatedCount}
reportedCount={reportedCount}
pendingCount={pendingCount}
/>
</SubBar>
<div className={styles.background} />
<MainLayout data-test="moderate-main-container">
<main className={styles.main}>{children}</main>
</MainLayout>
</div>
);
export default Moderate;
@@ -0,0 +1,55 @@
.topBar {
margin-bottom: var(--spacing-unit);
}
.username {
margin-right: var(--spacing-unit);
}
.footer {
margin-top: calc(2 * var(--spacing-unit));
}
.content {
min-height: calc(4.5 * var(--spacing-unit));
}
.mainContainer {
flex-grow: 1;
}
.aside {
padding-top: 25px;
flex-shrink: 0;
flex-grow: 0;
box-sizing: border-box;
}
.asideWithoutReplyTo {
padding-top: 10px;
}
.decision {
font-size: calc(14rem / var(--rem-base));
font-weight: var(--font-weight-medium);
font-family: var(--font-family-sans-serif);
line-height: calc(16em / 14);
letter-spacing: calc(0.2em / 14);
color: var(--palette-text-primary);
text-transform: uppercase;
}
.separator {
flex-shrink: 0;
flex-grow: 0;
border-right: 1px solid var(--palette-divider);
margin: 0 calc(2 * var(--spacing-unit));
}
.root {
transition: background 100ms;
}
.dangling {
background-color: var(--palette-grey-lightest);
}
@@ -0,0 +1,69 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import { PropTypesOf } from "talk-framework/types";
import ModerateCard from "./ModerateCard";
const ModerateCardN = removeFragmentRefs(ModerateCard);
const baseProps: PropTypesOf<typeof ModerateCardN> = {
id: "comment-id",
username: "Theon",
createdAt: "2018-11-29T16:01:51.897Z",
body: "content",
inReplyTo: null,
comment: {},
status: "undecided",
viewContextHref: "http://localhost/comment",
suspectWords: ["idiot"],
bannedWords: ["fuck"],
onAccept: noop,
onReject: noop,
};
it("renders correctly", () => {
const props: PropTypesOf<typeof ModerateCardN> = {
...baseProps,
};
const wrapper = shallow(<ModerateCardN {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders reply correctly", () => {
const props: PropTypesOf<typeof ModerateCardN> = {
...baseProps,
inReplyTo: "Julian",
};
const wrapper = shallow(<ModerateCardN {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders accepted correctly", () => {
const props: PropTypesOf<typeof ModerateCardN> = {
...baseProps,
status: "accepted",
};
const wrapper = shallow(<ModerateCardN {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders rejected correctly", () => {
const props: PropTypesOf<typeof ModerateCardN> = {
...baseProps,
status: "rejected",
};
const wrapper = shallow(<ModerateCardN {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders dangling correctly", () => {
const props: PropTypesOf<typeof ModerateCardN> = {
...baseProps,
dangling: true,
};
const wrapper = shallow(<ModerateCardN {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,127 @@
import cn from "classnames";
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { Button, Card, Flex, Icon } from "talk-ui/components";
import MarkersContainer from "../containers/MarkersContainer";
import AcceptButton from "./AcceptButton";
import CommentContent from "./CommentContent";
import InReplyTo from "./InReplyTo";
import RejectButton from "./RejectButton";
import Timestamp from "./Timestamp";
import Username from "./Username";
import styles from "./ModerateCard.css";
interface Props {
id: string;
username: string;
createdAt: string;
body: string;
inReplyTo: string | null;
comment: PropTypesOf<typeof MarkersContainer>["comment"];
status: "accepted" | "rejected" | "undecided";
viewContextHref: string;
suspectWords: ReadonlyArray<string>;
bannedWords: ReadonlyArray<string>;
onAccept: () => void;
onReject: () => void;
/**
* If set to true, it means this comment is about to be removed
* from the queue. This will trigger some styling changes to
* reflect that
*/
dangling?: boolean;
}
const ModerateCard: StatelessComponent<Props> = ({
id,
username,
createdAt,
body,
inReplyTo,
comment,
viewContextHref,
status,
suspectWords,
bannedWords,
onAccept,
onReject,
dangling,
}) => (
<Card
className={cn(styles.root, { [styles.dangling]: dangling })}
data-test={`moderate-comment-${id}`}
>
<Flex>
<div className={styles.mainContainer}>
<div className={styles.topBar}>
<div>
<Username className={styles.username}>{username}</Username>
<Timestamp>{createdAt}</Timestamp>
</div>
{inReplyTo && (
<div>
<InReplyTo>{inReplyTo}</InReplyTo>
</div>
)}
</div>
<CommentContent
suspectWords={suspectWords}
bannedWords={bannedWords}
className={styles.content}
>
{body}
</CommentContent>
<div className={styles.footer}>
<Flex justifyContent="flex-end">
<Button
variant="underlined"
color="primary"
anchor
href={viewContextHref}
target="_blank"
>
<Localized id="moderate-viewContext">
<span>View Context</span>
</Localized>{" "}
<Icon>arrow_forward</Icon>
</Button>
</Flex>
<Flex itemGutter>
<MarkersContainer comment={comment} />
</Flex>
</div>
</div>
<div className={styles.separator} />
<Flex
className={cn(styles.aside, {
[styles.asideWithoutReplyTo]: !inReplyTo,
})}
alignItems="center"
direction="column"
itemGutter
>
<Localized id="moderate-decision">
<div className={styles.decision}>DECISION</div>
</Localized>
<Flex itemGutter>
<RejectButton
onClick={onReject}
invert={status === "rejected"}
disabled={status === "rejected" || dangling}
/>
<AcceptButton
onClick={onAccept}
invert={status === "accepted"}
disabled={status === "accepted" || dangling}
/>
</Flex>
</Flex>
</Flex>
</Card>
);
export default ModerateCard;
@@ -0,0 +1,20 @@
import { shallow } from "enzyme";
import React from "react";
import Navigation from "./Navigation";
import { PropTypesOf } from "talk-framework/types";
it("renders correctly", () => {
const wrapper = shallow(<Navigation />);
expect(wrapper).toMatchSnapshot();
});
it("renders correctly with counts", () => {
const props: PropTypesOf<typeof Navigation> = {
unmoderatedCount: 3,
reportedCount: 4,
pendingCount: 0,
};
const wrapper = shallow(<Navigation {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,63 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { Counter, Icon, SubBarNavigation } from "talk-ui/components";
import NavigationLink from "./NavigationLink";
interface Props {
unmoderatedCount?: number;
reportedCount?: number;
pendingCount?: number;
children?: React.ReactNode;
}
const Navigation: StatelessComponent<Props> = ({
unmoderatedCount,
reportedCount,
pendingCount,
}) => (
<SubBarNavigation>
<NavigationLink to="/admin/moderate/reported">
<Icon>flag</Icon>
<Localized id="moderate-navigation-reported">
<span>Reported</span>
</Localized>
{reportedCount !== undefined && (
<Counter data-test="moderate-navigation-reported-count">
{reportedCount}
</Counter>
)}
</NavigationLink>
<NavigationLink to="/admin/moderate/pending">
<Icon>access_time</Icon>
<Localized id="moderate-navigation-pending">
<span>Pending</span>
</Localized>
{pendingCount !== undefined && (
<Counter data-test="moderate-navigation-pending-count">
{pendingCount}
</Counter>
)}
</NavigationLink>
<NavigationLink to="/admin/moderate/unmoderated">
<Icon>forum</Icon>
<Localized id="moderate-navigation-unmoderated">
<span>Unmoderated</span>
</Localized>
{unmoderatedCount !== undefined && (
<Counter data-test="moderate-navigation-unmoderated-count">
{unmoderatedCount}
</Counter>
)}
</NavigationLink>
<NavigationLink to="/admin/moderate/rejected">
<Icon>cancel</Icon>
<Localized id="moderate-navigation-rejected">
<span>Rejected</span>
</Localized>
</NavigationLink>
</SubBarNavigation>
);
export default Navigation;
@@ -0,0 +1,15 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import NavigationLink from "./NavigationLink";
it("renders correctly", () => {
const props: PropTypesOf<typeof NavigationLink> = {
to: "/moderate",
children: "link",
};
const wrapper = shallow(<NavigationLink {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,17 @@
import { Link, LocationDescriptor } from "found";
import React, { StatelessComponent } from "react";
import { SubBarNavigationItem } from "talk-ui/components";
interface Props {
children: React.ReactNode;
to: string | LocationDescriptor;
}
const NavigationLink: StatelessComponent<Props> = props => (
<Link to={props.to} Component={SubBarNavigationItem} activePropName="active">
{props.children}
</Link>
);
export default NavigationLink;
@@ -0,0 +1,16 @@
.root {
width: calc(75 * var(--spacing-unit));
}
.exitTransition {
opacity: 1;
transition: 300ms opacity;
}
.exitTransitionActive {
opacity: 0;
}
.exitTransitionDone {
opacity: 0;
}
@@ -0,0 +1,36 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import { PropTypesOf } from "talk-framework/types";
import Queue from "./Queue";
const QueueN = removeFragmentRefs(Queue);
it("renders correctly with load more", () => {
const props: PropTypesOf<typeof QueueN> = {
comments: [],
settings: {},
onLoadMore: noop,
hasMore: true,
disableLoadMore: false,
danglingLogic: () => true,
};
const wrapper = shallow(<QueueN {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders correctly without load more", () => {
const props: PropTypesOf<typeof QueueN> = {
comments: [],
settings: {},
onLoadMore: noop,
hasMore: false,
disableLoadMore: false,
danglingLogic: () => true,
};
const wrapper = shallow(<QueueN {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,62 @@
import React, { StatelessComponent } from "react";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import { Flex, HorizontalGutter } from "talk-ui/components";
import { PropTypesOf } from "talk-ui/types";
import AutoLoadMoreContainer from "../containers/AutoLoadMoreContainer";
import ModerateCardContainer from "../containers/ModerateCardContainer";
import styles from "./Queue.css";
interface Props {
comments: Array<
{ id: string } & PropTypesOf<typeof ModerateCardContainer>["comment"]
>;
settings: PropTypesOf<typeof ModerateCardContainer>["settings"];
onLoadMore: () => void;
hasMore: boolean;
disableLoadMore: boolean;
danglingLogic: PropTypesOf<typeof ModerateCardContainer>["danglingLogic"];
}
const Queue: StatelessComponent<Props> = ({
settings,
comments,
hasMore,
disableLoadMore,
onLoadMore,
danglingLogic,
}) => (
<HorizontalGutter className={styles.root} size="double">
<TransitionGroup component={null} appear={false} enter={false} exit>
{comments.map(c => (
<CSSTransition
key={c.id}
timeout={400}
classNames={{
exit: styles.exitTransition,
exitActive: styles.exitTransitionActive,
exitDone: styles.exitTransitionDone,
}}
>
<ModerateCardContainer
settings={settings}
comment={c}
danglingLogic={danglingLogic}
/>
</CSSTransition>
))}
</TransitionGroup>
{hasMore && (
<Flex justifyContent="center">
<AutoLoadMoreContainer
disableLoadMore={disableLoadMore}
onLoadMore={onLoadMore}
/>
</Flex>
)}
</HorizontalGutter>
);
export default Queue;
@@ -0,0 +1,24 @@
.root {
border: 1px solid var(--palette-error-main);
box-sizing: border-box;
border-radius: 2px;
width: 65px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
color: var(--palette-error-main);
&:active {
background-color: var(--palette-error-main);
color: var(--palette-common-white);
}
}
.invert {
background-color: var(--palette-error-main);
color: var(--palette-common-white);
}
.icon {
color: inherit;
}
@@ -0,0 +1,22 @@
import { shallow } from "enzyme";
import React from "react";
import RejectButton from "./RejectButton";
import { PropTypesOf } from "talk-framework/types";
it("renders correctly", () => {
const props: PropTypesOf<typeof RejectButton> = {
invert: false,
};
const wrapper = shallow(<RejectButton {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders correctly inverted", () => {
const props: PropTypesOf<typeof RejectButton> = {
invert: true,
};
const wrapper = shallow(<RejectButton {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,34 @@
import cn from "classnames";
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import { PropTypesOf } from "talk-framework/types";
import { BaseButton, Icon } from "talk-ui/components";
import styles from "./RejectButton.css";
interface Props extends PropTypesOf<typeof BaseButton> {
invert?: boolean;
}
const RejectButton: StatelessComponent<Props> = ({
invert,
className,
...rest
}) => (
<Localized id="moderate-rejectButton" attrs={{ "aria-label": true }}>
<BaseButton
{...rest}
className={cn(className, styles.root, {
[styles.invert]: invert,
})}
aria-label="Reject"
>
<Icon size="lg" className={styles.icon}>
close
</Icon>
</BaseButton>
</Localized>
);
export default RejectButton;
@@ -0,0 +1,47 @@
.background {
position: fixed;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
background-color: var(--palette-background-light);
}
.main {
margin: calc(2 * var(--spacing-unit)) 0 calc(4 * var(--spacing-unit)) 0;
display: flex;
justify-content: center;
}
.subBar {
height: calc(3 * var(--spacing-unit));
background-color: var(--palette-primary-dark);
margin-top: -1px;
}
.subBarBegin {
position: absolute;
left: 0;
font-size: calc(12rem / var(--rem-base));
font-weight: var(--font-weight-medium);
font-family: var(--font-family-sans-serif);
line-height: calc(14em / 12);
letter-spacing: calc(0.2em / 12);
color: var(--palette-common-white);
text-transform: uppercase;
text-decoration: none;
}
.subBarTitle {
font-size: calc(14rem / var(--rem-base));
font-weight: var(--font-weight-medium);
font-family: var(--font-family-sans-serif);
line-height: calc(16em / 14);
letter-spacing: calc(0.2em / 14);
color: var(--palette-common-white);
text-transform: uppercase;
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import SingleModerate from "./SingleModerate";
it("renders correctly", () => {
const props: PropTypesOf<typeof SingleModerate> = {
children: "singe comment queue",
};
const wrapper = shallow(<SingleModerate {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,33 @@
import { Localized } from "fluent-react/compat";
import { Link } from "found";
import React, { StatelessComponent } from "react";
import MainLayout from "talk-admin/components/MainLayout";
import { SubBar } from "talk-ui/components";
import styles from "./SingleModerate.css";
interface Props {
children?: React.ReactNode;
}
const Moderate: StatelessComponent<Props> = ({ children }) => (
<div data-test="single-moderate-container">
<SubBar className={styles.subBar} gutterBegin gutterEnd>
<Localized id="moderate-single-goToModerationQueues">
<Link className={styles.subBarBegin} to="/admin/moderate/">
Go to moderation queues
</Link>
</Localized>
<Localized id="moderate-single-singleCommentView">
<div className={styles.subBarTitle}>Single Comment View</div>
</Localized>
</SubBar>
<div className={styles.background} />
<MainLayout>
<main className={styles.main}>{children}</main>
</MainLayout>
</div>
);
export default Moderate;
@@ -0,0 +1,4 @@
.root {
composes: timestamp from "talk-ui/shared/typography.css";
color: var(--palette-grey-lighter);
}
@@ -0,0 +1,14 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import Timestamp from "./Timestamp";
it("renders correctly", () => {
const props: PropTypesOf<typeof Timestamp> = {
children: "1995-12-17T03:24:00.000Z",
};
const wrapper = shallow(<Timestamp {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,16 @@
import React from "react";
import { StatelessComponent } from "react";
import { RelativeTime } from "talk-ui/components";
import styles from "./Timestamp.css";
export interface TimestampProps {
children: string;
}
const Timestamp: StatelessComponent<TimestampProps> = props => (
<RelativeTime className={styles.root} date={props.children} />
);
export default Timestamp;
@@ -0,0 +1,4 @@
.root {
line-height: 1;
color: var(--palette-grey-dark);
}
@@ -0,0 +1,15 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import { PropTypesOf } from "talk-framework/types";
import Username from "./Username";
it("renders correctly", () => {
const props: PropTypesOf<typeof Username> = {
children: "Marvin",
};
const testRenderer = TestRenderer.create(<Username {...props} />);
expect(testRenderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,26 @@
import cn from "classnames";
import React from "react";
import { StatelessComponent } from "react";
import { Typography } from "talk-ui/components";
import styles from "./Username.css";
export interface UsernameProps {
className?: string;
children: string;
}
const Username: StatelessComponent<UsernameProps> = props => {
return (
<Typography
variant="heading2"
className={cn(props.className, styles.root)}
container="span"
>
{props.children}
</Typography>
);
};
export default Username;
@@ -0,0 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Localized
attrs={
Object {
"aria-label": true,
}
}
id="moderate-acceptButton"
>
<withPropsOnChange(WithMouseHover)
aria-label="Accept"
className="AcceptButton-root"
>
<withPropsOnChange(Icon)
className="AcceptButton-icon"
size="lg"
>
done
</withPropsOnChange(Icon)>
</withPropsOnChange(WithMouseHover)>
</Localized>
`;
exports[`renders correctly inverted 1`] = `
<Localized
attrs={
Object {
"aria-label": true,
}
}
id="moderate-acceptButton"
>
<withPropsOnChange(WithMouseHover)
aria-label="Accept"
className="AcceptButton-root AcceptButton-invert"
>
<withPropsOnChange(Icon)
className="AcceptButton-icon"
size="lg"
>
done
</withPropsOnChange(Icon)>
</withPropsOnChange(WithMouseHover)>
</Localized>
`;
@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Typography)
className="custom CommentContent-root"
container="div"
dangerouslySetInnerHTML={
Object {
"__html": "Hello <strong><span><mark>idiot</mark></span></strong><span>, you <mark>fucking</mark> bastard</span>",
}
}
/>
`;
exports[`renders empty words correctly 1`] = `
<withPropsOnChange(Typography)
className="custom CommentContent-root"
container="div"
dangerouslySetInnerHTML={
Object {
"__html": "Hello <strong>idiot</strong>, you fucking bastard",
}
}
/>
`;
@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Flex)
alignItems="center"
>
<withPropsOnChange(Icon)
className="InReplyTo-icon"
>
reply
</withPropsOnChange(Icon)>
<Localized
id="moderate-inReplyTo"
username={<Username />}
>
<withPropsOnChange(Typography)
className="InReplyTo-inReplyTo"
container="span"
variant="timestamp"
>
Reply to &lt;username&gt;&lt;username&gt;
</withPropsOnChange(Typography)>
</Localized>
</withPropsOnChange(Flex)>
`;
@@ -1,11 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(HorizontalGutter)>
<withPropsOnChange(Typography)
variant="heading3"
<div
data-test="moderate-container"
>
<withPropsOnChange(SubBar)
data-test="moderate-subBar-container"
>
Moderate
</withPropsOnChange(Typography)>
</withPropsOnChange(HorizontalGutter)>
<Navigation />
</withPropsOnChange(SubBar)>
<div
className="Moderate-background"
/>
<MainLayout
data-test="moderate-main-container"
>
<main
className="Moderate-main"
/>
</MainLayout>
</div>
`;
exports[`renders correctly with counts 1`] = `
<div
data-test="moderate-container"
>
<withPropsOnChange(SubBar)
data-test="moderate-subBar-container"
>
<Navigation
pendingCount={0}
reportedCount={4}
unmoderatedCount={3}
/>
</withPropsOnChange(SubBar)>
<div
className="Moderate-background"
/>
<MainLayout
data-test="moderate-main-container"
>
<main
className="Moderate-main"
/>
</MainLayout>
</div>
`;
@@ -0,0 +1,550 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders accepted correctly 1`] = `
<withPropsOnChange(Card)
className="ModerateCard-root"
data-test="moderate-comment-comment-id"
>
<withPropsOnChange(Flex)>
<div
className="ModerateCard-mainContainer"
>
<div
className="ModerateCard-topBar"
>
<div>
<Username
className="ModerateCard-username"
>
Theon
</Username>
<Timestamp>
2018-11-29T16:01:51.897Z
</Timestamp>
</div>
</div>
<CommentContent
bannedWords={
Array [
"fuck",
]
}
className="ModerateCard-content"
suspectWords={
Array [
"idiot",
]
}
>
content
</CommentContent>
<div
className="ModerateCard-footer"
>
<withPropsOnChange(Flex)
justifyContent="flex-end"
>
<withPropsOnChange(Button)
anchor={true}
color="primary"
href="http://localhost/comment"
target="_blank"
variant="underlined"
>
<Localized
id="moderate-viewContext"
>
<span>
View Context
</span>
</Localized>
<withPropsOnChange(Icon)>
arrow_forward
</withPropsOnChange(Icon)>
</withPropsOnChange(Button)>
</withPropsOnChange(Flex)>
<withPropsOnChange(Flex)
itemGutter={true}
>
<Relay(MarkersContainer)
comment={Object {}}
/>
</withPropsOnChange(Flex)>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<withPropsOnChange(Flex)
alignItems="center"
className="ModerateCard-aside ModerateCard-asideWithoutReplyTo"
direction="column"
itemGutter={true}
>
<Localized
id="moderate-decision"
>
<div
className="ModerateCard-decision"
>
DECISION
</div>
</Localized>
<withPropsOnChange(Flex)
itemGutter={true}
>
<RejectButton
invert={false}
onClick={[Function]}
/>
<AcceptButton
disabled={true}
invert={true}
onClick={[Function]}
/>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Card)>
`;
exports[`renders correctly 1`] = `
<withPropsOnChange(Card)
className="ModerateCard-root"
data-test="moderate-comment-comment-id"
>
<withPropsOnChange(Flex)>
<div
className="ModerateCard-mainContainer"
>
<div
className="ModerateCard-topBar"
>
<div>
<Username
className="ModerateCard-username"
>
Theon
</Username>
<Timestamp>
2018-11-29T16:01:51.897Z
</Timestamp>
</div>
</div>
<CommentContent
bannedWords={
Array [
"fuck",
]
}
className="ModerateCard-content"
suspectWords={
Array [
"idiot",
]
}
>
content
</CommentContent>
<div
className="ModerateCard-footer"
>
<withPropsOnChange(Flex)
justifyContent="flex-end"
>
<withPropsOnChange(Button)
anchor={true}
color="primary"
href="http://localhost/comment"
target="_blank"
variant="underlined"
>
<Localized
id="moderate-viewContext"
>
<span>
View Context
</span>
</Localized>
<withPropsOnChange(Icon)>
arrow_forward
</withPropsOnChange(Icon)>
</withPropsOnChange(Button)>
</withPropsOnChange(Flex)>
<withPropsOnChange(Flex)
itemGutter={true}
>
<Relay(MarkersContainer)
comment={Object {}}
/>
</withPropsOnChange(Flex)>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<withPropsOnChange(Flex)
alignItems="center"
className="ModerateCard-aside ModerateCard-asideWithoutReplyTo"
direction="column"
itemGutter={true}
>
<Localized
id="moderate-decision"
>
<div
className="ModerateCard-decision"
>
DECISION
</div>
</Localized>
<withPropsOnChange(Flex)
itemGutter={true}
>
<RejectButton
invert={false}
onClick={[Function]}
/>
<AcceptButton
invert={false}
onClick={[Function]}
/>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Card)>
`;
exports[`renders dangling correctly 1`] = `
<withPropsOnChange(Card)
className="ModerateCard-root ModerateCard-dangling"
data-test="moderate-comment-comment-id"
>
<withPropsOnChange(Flex)>
<div
className="ModerateCard-mainContainer"
>
<div
className="ModerateCard-topBar"
>
<div>
<Username
className="ModerateCard-username"
>
Theon
</Username>
<Timestamp>
2018-11-29T16:01:51.897Z
</Timestamp>
</div>
</div>
<CommentContent
bannedWords={
Array [
"fuck",
]
}
className="ModerateCard-content"
suspectWords={
Array [
"idiot",
]
}
>
content
</CommentContent>
<div
className="ModerateCard-footer"
>
<withPropsOnChange(Flex)
justifyContent="flex-end"
>
<withPropsOnChange(Button)
anchor={true}
color="primary"
href="http://localhost/comment"
target="_blank"
variant="underlined"
>
<Localized
id="moderate-viewContext"
>
<span>
View Context
</span>
</Localized>
<withPropsOnChange(Icon)>
arrow_forward
</withPropsOnChange(Icon)>
</withPropsOnChange(Button)>
</withPropsOnChange(Flex)>
<withPropsOnChange(Flex)
itemGutter={true}
>
<Relay(MarkersContainer)
comment={Object {}}
/>
</withPropsOnChange(Flex)>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<withPropsOnChange(Flex)
alignItems="center"
className="ModerateCard-aside ModerateCard-asideWithoutReplyTo"
direction="column"
itemGutter={true}
>
<Localized
id="moderate-decision"
>
<div
className="ModerateCard-decision"
>
DECISION
</div>
</Localized>
<withPropsOnChange(Flex)
itemGutter={true}
>
<RejectButton
disabled={true}
invert={false}
onClick={[Function]}
/>
<AcceptButton
disabled={true}
invert={false}
onClick={[Function]}
/>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Card)>
`;
exports[`renders rejected correctly 1`] = `
<withPropsOnChange(Card)
className="ModerateCard-root"
data-test="moderate-comment-comment-id"
>
<withPropsOnChange(Flex)>
<div
className="ModerateCard-mainContainer"
>
<div
className="ModerateCard-topBar"
>
<div>
<Username
className="ModerateCard-username"
>
Theon
</Username>
<Timestamp>
2018-11-29T16:01:51.897Z
</Timestamp>
</div>
</div>
<CommentContent
bannedWords={
Array [
"fuck",
]
}
className="ModerateCard-content"
suspectWords={
Array [
"idiot",
]
}
>
content
</CommentContent>
<div
className="ModerateCard-footer"
>
<withPropsOnChange(Flex)
justifyContent="flex-end"
>
<withPropsOnChange(Button)
anchor={true}
color="primary"
href="http://localhost/comment"
target="_blank"
variant="underlined"
>
<Localized
id="moderate-viewContext"
>
<span>
View Context
</span>
</Localized>
<withPropsOnChange(Icon)>
arrow_forward
</withPropsOnChange(Icon)>
</withPropsOnChange(Button)>
</withPropsOnChange(Flex)>
<withPropsOnChange(Flex)
itemGutter={true}
>
<Relay(MarkersContainer)
comment={Object {}}
/>
</withPropsOnChange(Flex)>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<withPropsOnChange(Flex)
alignItems="center"
className="ModerateCard-aside ModerateCard-asideWithoutReplyTo"
direction="column"
itemGutter={true}
>
<Localized
id="moderate-decision"
>
<div
className="ModerateCard-decision"
>
DECISION
</div>
</Localized>
<withPropsOnChange(Flex)
itemGutter={true}
>
<RejectButton
disabled={true}
invert={true}
onClick={[Function]}
/>
<AcceptButton
invert={false}
onClick={[Function]}
/>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Card)>
`;
exports[`renders reply correctly 1`] = `
<withPropsOnChange(Card)
className="ModerateCard-root"
data-test="moderate-comment-comment-id"
>
<withPropsOnChange(Flex)>
<div
className="ModerateCard-mainContainer"
>
<div
className="ModerateCard-topBar"
>
<div>
<Username
className="ModerateCard-username"
>
Theon
</Username>
<Timestamp>
2018-11-29T16:01:51.897Z
</Timestamp>
</div>
<div>
<InReplyTo>
Julian
</InReplyTo>
</div>
</div>
<CommentContent
bannedWords={
Array [
"fuck",
]
}
className="ModerateCard-content"
suspectWords={
Array [
"idiot",
]
}
>
content
</CommentContent>
<div
className="ModerateCard-footer"
>
<withPropsOnChange(Flex)
justifyContent="flex-end"
>
<withPropsOnChange(Button)
anchor={true}
color="primary"
href="http://localhost/comment"
target="_blank"
variant="underlined"
>
<Localized
id="moderate-viewContext"
>
<span>
View Context
</span>
</Localized>
<withPropsOnChange(Icon)>
arrow_forward
</withPropsOnChange(Icon)>
</withPropsOnChange(Button)>
</withPropsOnChange(Flex)>
<withPropsOnChange(Flex)
itemGutter={true}
>
<Relay(MarkersContainer)
comment={Object {}}
/>
</withPropsOnChange(Flex)>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<withPropsOnChange(Flex)
alignItems="center"
className="ModerateCard-aside"
direction="column"
itemGutter={true}
>
<Localized
id="moderate-decision"
>
<div
className="ModerateCard-decision"
>
DECISION
</div>
</Localized>
<withPropsOnChange(Flex)
itemGutter={true}
>
<RejectButton
invert={false}
onClick={[Function]}
/>
<AcceptButton
invert={false}
onClick={[Function]}
/>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Flex)>
</withPropsOnChange(Card)>
`;
@@ -0,0 +1,138 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<withPropsOnChange(Navigation)>
<NavigationLink
to="/admin/moderate/reported"
>
<withPropsOnChange(Icon)>
flag
</withPropsOnChange(Icon)>
<Localized
id="moderate-navigation-reported"
>
<span>
Reported
</span>
</Localized>
</NavigationLink>
<NavigationLink
to="/admin/moderate/pending"
>
<withPropsOnChange(Icon)>
access_time
</withPropsOnChange(Icon)>
<Localized
id="moderate-navigation-pending"
>
<span>
Pending
</span>
</Localized>
</NavigationLink>
<NavigationLink
to="/admin/moderate/unmoderated"
>
<withPropsOnChange(Icon)>
forum
</withPropsOnChange(Icon)>
<Localized
id="moderate-navigation-unmoderated"
>
<span>
Unmoderated
</span>
</Localized>
</NavigationLink>
<NavigationLink
to="/admin/moderate/rejected"
>
<withPropsOnChange(Icon)>
cancel
</withPropsOnChange(Icon)>
<Localized
id="moderate-navigation-rejected"
>
<span>
Rejected
</span>
</Localized>
</NavigationLink>
</withPropsOnChange(Navigation)>
`;
exports[`renders correctly with counts 1`] = `
<withPropsOnChange(Navigation)>
<NavigationLink
to="/admin/moderate/reported"
>
<withPropsOnChange(Icon)>
flag
</withPropsOnChange(Icon)>
<Localized
id="moderate-navigation-reported"
>
<span>
Reported
</span>
</Localized>
<withPropsOnChange(Counter)
data-test="moderate-navigation-reported-count"
>
4
</withPropsOnChange(Counter)>
</NavigationLink>
<NavigationLink
to="/admin/moderate/pending"
>
<withPropsOnChange(Icon)>
access_time
</withPropsOnChange(Icon)>
<Localized
id="moderate-navigation-pending"
>
<span>
Pending
</span>
</Localized>
<withPropsOnChange(Counter)
data-test="moderate-navigation-pending-count"
>
0
</withPropsOnChange(Counter)>
</NavigationLink>
<NavigationLink
to="/admin/moderate/unmoderated"
>
<withPropsOnChange(Icon)>
forum
</withPropsOnChange(Icon)>
<Localized
id="moderate-navigation-unmoderated"
>
<span>
Unmoderated
</span>
</Localized>
<withPropsOnChange(Counter)
data-test="moderate-navigation-unmoderated-count"
>
3
</withPropsOnChange(Counter)>
</NavigationLink>
<NavigationLink
to="/admin/moderate/rejected"
>
<withPropsOnChange(Icon)>
cancel
</withPropsOnChange(Icon)>
<Localized
id="moderate-navigation-rejected"
>
<span>
Rejected
</span>
</Localized>
</NavigationLink>
</withPropsOnChange(Navigation)>
`;
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Link
Component={[Function]}
activePropName="active"
to="/moderate"
>
link
</Link>
`;
@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly with load more 1`] = `
<withPropsOnChange(HorizontalGutter)
className="Queue-root"
size="double"
>
<TransitionGroup
appear={false}
component={null}
enter={false}
exit={true}
/>
<withPropsOnChange(Flex)
justifyContent="center"
>
<withContext(WithInView)
disableLoadMore={false}
onLoadMore={[Function]}
/>
</withPropsOnChange(Flex)>
</withPropsOnChange(HorizontalGutter)>
`;
exports[`renders correctly without load more 1`] = `
<withPropsOnChange(HorizontalGutter)
className="Queue-root"
size="double"
>
<TransitionGroup
appear={false}
component={null}
enter={false}
exit={true}
/>
</withPropsOnChange(HorizontalGutter)>
`;
@@ -0,0 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Localized
attrs={
Object {
"aria-label": true,
}
}
id="moderate-rejectButton"
>
<withPropsOnChange(WithMouseHover)
aria-label="Reject"
className="RejectButton-root"
>
<withPropsOnChange(Icon)
className="RejectButton-icon"
size="lg"
>
close
</withPropsOnChange(Icon)>
</withPropsOnChange(WithMouseHover)>
</Localized>
`;
exports[`renders correctly inverted 1`] = `
<Localized
attrs={
Object {
"aria-label": true,
}
}
id="moderate-rejectButton"
>
<withPropsOnChange(WithMouseHover)
aria-label="Reject"
className="RejectButton-root RejectButton-invert"
>
<withPropsOnChange(Icon)
className="RejectButton-icon"
size="lg"
>
close
</withPropsOnChange(Icon)>
</withPropsOnChange(WithMouseHover)>
</Localized>
`;
@@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
data-test="single-moderate-container"
>
<withPropsOnChange(SubBar)
className="SingleModerate-subBar"
gutterBegin={true}
gutterEnd={true}
>
<Localized
id="moderate-single-goToModerationQueues"
>
<Link
className="SingleModerate-subBarBegin"
to="/admin/moderate/"
>
Go to moderation queues
</Link>
</Localized>
<Localized
id="moderate-single-singleCommentView"
>
<div
className="SingleModerate-subBarTitle"
>
Single Comment View
</div>
</Localized>
</withPropsOnChange(SubBar)>
<div
className="SingleModerate-background"
/>
<MainLayout>
<main
className="SingleModerate-main"
>
singe comment queue
</main>
</MainLayout>
</div>
`;

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