mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 19:01:24 +08:00
[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:
+4
-2
@@ -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"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Generated
+2732
-1717
File diff suppressed because it is too large
Load Diff
+17
-13
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
]),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -5,9 +5,5 @@
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--spacing-unit);
|
||||
box-sizing: border-box;
|
||||
.root {
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)>
|
||||
`;
|
||||
+4
-2
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,14 @@ export {
|
||||
withSetRedirectPathMutation,
|
||||
SetRedirectPathMutation,
|
||||
} from "./SetRedirectPathMutation";
|
||||
export {
|
||||
withAcceptCommentMutation,
|
||||
AcceptCommentMutation,
|
||||
} from "./AcceptCommentMutation";
|
||||
export {
|
||||
withRejectCommentMutation,
|
||||
RejectCommentMutation,
|
||||
} from "./RejectCommentMutation";
|
||||
export {
|
||||
withUpdateSettingsMutation,
|
||||
UpdateSettingsMutation,
|
||||
|
||||
@@ -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;
|
||||
|
||||
+2
-2
@@ -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
-3
@@ -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);
|
||||
|
||||
+2
-2
@@ -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>
|
||||
`;
|
||||
+12
@@ -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>
|
||||
`;
|
||||
+2
-2
@@ -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
-2
@@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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;
|
||||
+47
@@ -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>
|
||||
`;
|
||||
+25
@@ -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",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
+26
@@ -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 <username><username>
|
||||
</withPropsOnChange(Typography)>
|
||||
</Localized>
|
||||
</withPropsOnChange(Flex)>
|
||||
`;
|
||||
+44
-6
@@ -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>
|
||||
`;
|
||||
|
||||
+550
@@ -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)>
|
||||
`;
|
||||
+138
@@ -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)>
|
||||
`;
|
||||
+11
@@ -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)>
|
||||
`;
|
||||
+47
@@ -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>
|
||||
`;
|
||||
+43
@@ -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
Reference in New Issue
Block a user