replaced eslint:recommended with prettier

This commit is contained in:
Wyatt Johnson
2018-01-11 20:00:34 -07:00
parent d56c19016a
commit 0abc2ca243
649 changed files with 16235 additions and 13008 deletions
+13 -9
View File
@@ -6,11 +6,11 @@ const merge = require('lodash/merge');
const helmet = require('helmet');
const plugins = require('./services/plugins');
const compression = require('compression');
const {HELMET_CONFIGURATION} = require('./config');
const {MOUNT_PATH} = require('./url');
const { HELMET_CONFIGURATION } = require('./config');
const { MOUNT_PATH } = require('./url');
const routes = require('./routes');
const debug = require('debug')('talk:app');
const {ENABLE_TRACING, APOLLO_ENGINE_KEY, PORT} = require('./config');
const { ENABLE_TRACING, APOLLO_ENGINE_KEY, PORT } = require('./config');
const app = express();
@@ -26,7 +26,7 @@ app.use((req, res, next) => {
//==============================================================================
// Inject server route plugins.
plugins.get('server', 'app').forEach(({plugin, app: callback}) => {
plugins.get('server', 'app').forEach(({ plugin, app: callback }) => {
debug(`added plugin '${plugin.name}'`);
// Pass the app to the plugin to mount it's routes.
@@ -43,11 +43,11 @@ if (process.env.NODE_ENV !== 'test') {
}
if (ENABLE_TRACING && APOLLO_ENGINE_KEY) {
const {Engine} = require('apollo-engine');
const { Engine } = require('apollo-engine');
const engine = new Engine({
engineConfig: {
apiKey: APOLLO_ENGINE_KEY
apiKey: APOLLO_ENGINE_KEY,
},
graphqlPort: PORT,
endpoint: `${MOUNT_PATH}api/v1/graph/ql`,
@@ -64,9 +64,13 @@ app.set('trust proxy', 1);
// Enable a suite of security good practices through helmet. We disable
// frameguard to allow crossdomain injection of the embed.
app.use(helmet(merge(HELMET_CONFIGURATION, {
frameguard: false,
})));
app.use(
helmet(
merge(HELMET_CONFIGURATION, {
frameguard: false,
})
)
);
// Compress the responses if appropriate.
app.use(compression());
+9 -4
View File
@@ -6,7 +6,7 @@
require('./util');
const program = require('commander');
const {head, map} = require('lodash');
const { head, map } = require('lodash');
const Matcher = require('did-you-mean');
program
@@ -19,7 +19,10 @@ program
.command('users', 'work with the application auth')
.command('migration', 'provides utilities for migrating the database')
.command('verify', 'provides utilities for performing data verification')
.command('plugins', 'provides utilities for interacting with the plugin system')
.command(
'plugins',
'provides utilities for interacting with the plugin system'
)
.parse(process.argv);
// If the command wasn't found, output help.
@@ -29,9 +32,11 @@ if (!commands.includes(command)) {
const m = new Matcher(commands);
const similarCommands = m.list(command);
console.error(`cli '${command}' is not a talk cli command. See 'cli --help'.`);
console.error(
`cli '${command}' is not a talk cli command. See 'cli --help'.`
);
if (similarCommands.length > 0) {
const sc = similarCommands.map(({value}) => `\t${value}\n`).join('');
const sc = similarCommands.map(({ value }) => `\t${value}\n`).join('');
console.error(`\nThe most similar commands are\n${sc}`);
}
process.exit(1);
+20 -24
View File
@@ -16,30 +16,24 @@ const scraper = require('../services/scraper');
const inquirer = require('inquirer');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
/**
* Lists all the assets registered in the database.
*/
async function listAssets() {
try {
let assets = await AssetModel.find({}).sort({'created_at': 1});
let assets = await AssetModel.find({}).sort({ created_at: 1 });
let table = new Table({
head: [
'ID',
'Title',
'URL'
]
head: ['ID', 'Title', 'URL'],
});
assets.forEach((asset) => {
assets.forEach(asset => {
table.push([
asset.id,
asset.title ? asset.title : '',
asset.url ? asset.url : ''
asset.url ? asset.url : '',
]);
});
@@ -61,13 +55,13 @@ async function refreshAssets(ageString) {
$or: [
{
scraped: {
$lte: age
}
$lte: age,
},
},
{
scraped: null
}
]
scraped: null,
},
],
});
// Queue all the assets for scraping.
@@ -95,7 +89,6 @@ async function updateURL(assetID, assetURL) {
async function merge(srcID, dstID) {
try {
// Grab the assets...
let [srcAsset, dstAsset] = await AssetsService.findByIDs([srcID, dstID]);
if (!srcAsset || !dstAsset) {
@@ -103,21 +96,22 @@ async function merge(srcID, dstID) {
}
// Count the affected resources...
let srcCommentCount = await CommentModel.find({asset_id: srcID}).count();
let srcCommentCount = await CommentModel.find({ asset_id: srcID }).count();
console.log(`Now going to update ${srcCommentCount} comments and delete the source Asset[${srcID}].`);
console.log(
`Now going to update ${srcCommentCount} comments and delete the source Asset[${srcID}].`
);
let {confirm} = await inquirer.prompt([
let { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Proceed with merge',
default: false
}
default: false,
},
]);
if (confirm) {
// Perform the merge!
await AssetsService.merge(srcID, dstID);
} else {
@@ -152,7 +146,9 @@ program
program
.command('merge <srcID> <dstID>')
.description('merges two assets together by moving comments from src to dst and deleting the src asset')
.description(
'merges two assets together by moving comments from src to dst and deleting the src asset'
)
.action(merge);
program.parse(process.argv);
+12 -20
View File
@@ -11,20 +11,15 @@ const mailer = require('../services/mailer');
const mongoose = require('../services/mongoose');
const kue = require('../services/kue');
util.onshutdown([
() => mongoose.disconnect(),
]);
util.onshutdown([() => mongoose.disconnect()]);
/**
* Starts the job processor.
*/
function processJobs() {
// The scraper only needs to shutdown when the scraper has actually been
// started.
util.onshutdown([
() => kue.Task.shutdown()
]);
util.onshutdown([() => kue.Task.shutdown()]);
// Start the scraper processor.
scraper.process();
@@ -39,13 +34,15 @@ function processJobs() {
* @return {Promise}
*/
function removeJob(job) {
return new Promise((resolve, reject) => job.remove((err) => {
if (err) {
return reject(err);
}
return new Promise((resolve, reject) =>
job.remove(err => {
if (err) {
return reject(err);
}
return resolve(job);
}));
return resolve(job);
})
);
}
/**
@@ -82,17 +79,13 @@ async function getJobBatch(n, includeStuck) {
* Cleans up the jobs that are in the queue.
*/
async function cleanupJobs(options) {
// The scraper only needs to shutdown when the scraper has actually been
// started.
util.onshutdown([
() => kue.Task.shutdown()
]);
util.onshutdown([() => kue.Task.shutdown()]);
const n = 100;
try {
// Connect to redis by establishing a queue.
kue.Task.connect();
@@ -100,9 +93,8 @@ async function cleanupJobs(options) {
let jobs = await getJobBatch(n, options.stuck);
while (jobs.length > 0) {
// Remove all the jobs.
await Promise.all(jobs.map((job) => removeJob(job)));
await Promise.all(jobs.map(job => removeJob(job)));
jobCount += jobs.length;
+11 -15
View File
@@ -11,13 +11,10 @@ const mongoose = require('../services/mongoose');
const MigrationService = require('../services/migration');
// Register shutdown hooks.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
async function createMigration(name) {
try {
// Create the migration.
await MigrationService.create(name);
@@ -29,20 +26,20 @@ async function createMigration(name) {
}
async function runMigrations() {
try {
let {backedUp} = await inquirer.prompt([
let { backedUp } = await inquirer.prompt([
{
type: 'confirm',
name: 'backedUp',
message: 'Did you perform a database backup',
default: false
}
default: false,
},
]);
if (!backedUp) {
throw new Error('Please backup your databases prior to migrations occuring');
throw new Error(
'Please backup your databases prior to migrations occuring'
);
}
// Get the migrations to run.
@@ -50,21 +47,20 @@ async function runMigrations() {
console.log('Now going to run the following migrations:\n');
for (let {filename} of migrations) {
for (let { filename } of migrations) {
console.log(`\tmigrations/${filename}`);
}
let {confirm} = await inquirer.prompt([
let { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Proceed with migrations',
default: false
}
default: false,
},
]);
if (confirm) {
// Run the migrations.
await MigrationService.run(migrations);
} else {
+117 -77
View File
@@ -21,11 +21,11 @@ const path = require('path');
const spawn = require('cross-spawn');
const semver = require('semver');
const resolve = require('resolve');
const {plugins, iteratePlugins, isInternal} = require('../plugins');
const { plugins, iteratePlugins, isInternal } = require('../plugins');
function existsInNodeModules(name) {
try {
resolve.sync(name, {basedir: dir});
resolve.sync(name, { basedir: dir });
return true;
} catch (e) {
@@ -39,13 +39,13 @@ function versionMatch(name, version) {
resolve.sync(name, {
basedir: dir,
packageFilter: (pkg) => {
packageFilter: pkg => {
if (pkg && pkg.version && semver.satisfies(pkg.version, version)) {
matched = true;
}
return pkg;
}
},
});
return matched;
@@ -56,7 +56,7 @@ function versionMatch(name, version) {
const EXTERNAL = /^\w[a-z\-0-9.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
function reconcilePackages({quiet = false, upgradeRemote = false}) {
function reconcilePackages({ quiet = false, upgradeRemote = false }) {
const fetchable = [];
const local = [];
const upgradable = [];
@@ -74,10 +74,11 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
let section = iteratePlugins(plugins[i]);
for (let j in section) {
let {name, version} = section[j];
let { name, version } = section[j];
let namespaced = name.charAt(0) === '@';
let dep = name.split('/')
let dep = name
.split('/')
.slice(0, namespaced ? 2 : 1)
.join('/');
@@ -91,7 +92,7 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
console.log(` l ${name}`);
}
local.push({name, version});
local.push({ name, version });
continue;
}
@@ -99,9 +100,8 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
if (!quiet) {
console.log(` m ${name}`);
}
fetchable.push({name, version});
fetchable.push({ name, version });
} else if (!versionMatch(dep, version)) {
// A plugin was found, yet the current version does not match the
// current version installed. We should warn if upgradeRemote is
// not enabled that it is currently not supported.
@@ -115,14 +115,14 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
console.log(` oe ${name} (package upgrade may be required)`);
upgradable.push({name, version});
upgradable.push({ name, version });
} else {
if (!quiet) {
console.log(` e ${name}`);
}
if (upgradeRemote) {
upgradable.push({name, version});
upgradable.push({ name, version });
}
}
}
@@ -132,33 +132,44 @@ function reconcilePackages({quiet = false, upgradeRemote = false}) {
console.log();
}
return {local, fetchable, upgradable};
return { local, fetchable, upgradable };
}
async function reconcileRemotePlugins({skipLocal, dryRun, upgradeRemote}) {
console.log(`\n[${skipLocal ? '1/2' : '2/3'}] ${emoji.get('mag')} Reconciling plugins...`.yellow);
const {fetchable, upgradable} = reconcilePackages({upgradeRemote});
async function reconcileRemotePlugins({ skipLocal, dryRun, upgradeRemote }) {
console.log(
`\n[${skipLocal ? '1/2' : '2/3'}] ${emoji.get(
'mag'
)} Reconciling plugins...`.yellow
);
const { fetchable, upgradable } = reconcilePackages({ upgradeRemote });
console.log(`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`.yellow);
console.log(
`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`
.yellow
);
if (fetchable.length > 0) {
console.log(`$ yarn add --ignore-scripts ${fetchable.map(({name, version}) => `${name}@${version}`.cyan)}`);
console.log(
`$ yarn add --ignore-scripts ${fetchable.map(
({ name, version }) => `${name}@${version}`.cyan
)}`
);
if (!dryRun) {
let args = [
'add',
'--ignore-scripts',
...fetchable.map(({name, version}) => `${name}@${version}`)
...fetchable.map(({ name, version }) => `${name}@${version}`),
];
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit']
stdio: ['ignore', 'pipe', 'inherit'],
});
if (output.status) {
throw new Error('Could not install external plugins, errors occured during install');
throw new Error(
'Could not install external plugins, errors occured during install'
);
}
console.log(output.stdout.toString());
@@ -166,36 +177,45 @@ async function reconcileRemotePlugins({skipLocal, dryRun, upgradeRemote}) {
}
if (upgradable.length > 0) {
console.log(`$ yarn upgrade ${upgradable.map(({name, version}) => `${name}@${version}`.cyan)}`);
console.log(
`$ yarn upgrade ${upgradable.map(
({ name, version }) => `${name}@${version}`.cyan
)}`
);
if (!dryRun) {
let args = [
'upgrade',
...upgradable.map(({name, version}) => `${name}@${version}`)
...upgradable.map(({ name, version }) => `${name}@${version}`),
];
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit']
stdio: ['ignore', 'pipe', 'inherit'],
});
if (output.status) {
throw new Error('Could not install external plugins, errors occured during install');
throw new Error(
'Could not install external plugins, errors occured during install'
);
}
console.log(output.stdout.toString());
}
}
return {upgradable, fetchable};
return { upgradable, fetchable };
}
async function reconcileLocalPlugins({skipRemote, dryRun}) {
console.log(`\n[${skipRemote ? '1/1' : '1/3'}] ${emoji.get('pick')} Installing local plugin dependencies...\n`.yellow);
const {local} = reconcilePackages({quiet: true});
async function reconcileLocalPlugins({ skipRemote, dryRun }) {
console.log(
`\n[${skipRemote ? '1/1' : '1/3'}] ${emoji.get(
'pick'
)} Installing local plugin dependencies...\n`.yellow
);
const { local } = reconcilePackages({ quiet: true });
for (let i in local) {
let {name} = local[i];
let { name } = local[i];
if (!fs.existsSync(path.join(dir, 'plugins', name, 'package.json'))) {
continue;
@@ -210,11 +230,13 @@ async function reconcileLocalPlugins({skipRemote, dryRun}) {
let output = spawn.sync('yarn', args, {
stdio: ['ignore', 'pipe', 'inherit'],
cwd: wd
cwd: wd,
});
if (output.status) {
throw new Error('Could not install local plugin dependencies, errors occured during install');
throw new Error(
'Could not install local plugin dependencies, errors occured during install'
);
}
console.log(output.stdout.toString());
@@ -225,7 +247,12 @@ async function reconcileLocalPlugins({skipRemote, dryRun}) {
// This traverses the local plugins and installs any dependencies listed there,
// this only is really needed for plugins that are installed via docker because
// core plugins will have their dependencies already included in core.
async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote}) {
async function reconcilePluginDeps({
skipLocal,
skipRemote,
dryRun,
upgradeRemote,
}) {
let startTime = new Date();
// We don't need to do anything if we skip everything....
@@ -235,14 +262,19 @@ async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote
// Traverse local plugins and install dependencies if enabled.
if (!skipLocal) {
await reconcileLocalPlugins({skipRemote, dryRun});
await reconcileLocalPlugins({ skipRemote, dryRun });
}
// Locate any external plugins and install them.
if (!skipRemote) {
let results = [];
try {
results = await reconcileRemotePlugins({skipLocal, skipRemote, dryRun, upgradeRemote});
results = await reconcileRemotePlugins({
skipLocal,
skipRemote,
dryRun,
upgradeRemote,
});
} catch (e) {
throw e;
}
@@ -262,7 +294,9 @@ async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote
} else if (results.fetchable.length === 0) {
message = `Upgraded ${results.upgradable.length} new plugins.`;
} else {
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${results.upgradable.length} plugins.`;
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
results.upgradable.length
} plugins.`;
}
console.log(`\n${status} ${message}`);
@@ -279,8 +313,7 @@ async function createSeedPlugin() {
function pluginNameExists(pluginName) {
const pluginNames = fs.readdirSync(pluginsDir);
return !!pluginNames
.filter((pn) => pn === pluginName).length;
return !!pluginNames.filter(pn => pn === pluginName).length;
}
let answers = await inquirer.prompt([
@@ -288,8 +321,7 @@ async function createSeedPlugin() {
type: 'input',
name: 'pluginName',
message: 'Plugin Name:',
validate: (input) => {
validate: input => {
if (pluginNameExists(input)) {
return 'Please, choose another name. That name already exists';
}
@@ -299,23 +331,23 @@ async function createSeedPlugin() {
}
return 'Plugin Name is required.';
}
},
},
{
type: 'confirm',
name: 'server',
message: 'Is this plugin extending the server capabilities?'
message: 'Is this plugin extending the server capabilities?',
},
{
type: 'confirm',
name: 'client',
message: 'Is this plugin extending the client capabilities?'
message: 'Is this plugin extending the client capabilities?',
},
{
type: 'confirm',
name: 'addPluginsJson',
message: 'Should we add it to the plugins.json?'
}
message: 'Should we add it to the plugins.json?',
},
]);
//==============================================================================
@@ -326,41 +358,41 @@ async function createSeedPlugin() {
const newPluginPath = path.join(pluginsDir, answers.pluginName);
if (fs.existsSync(seedPlugin)) {
if (answers.server && answers.client) {
// This is a server-side and client-side plugin!, let's copy the template
fs.copySync(seedPlugin, newPluginPath);
} else {
} else {
fs.copySync(seedPlugin, newPluginPath, {
filter: p => {
// Allowing plugin folder and files with no subfolders
const rootRx = /plugin$|plugin\/[^/]*(\.).{2,3}/gim;
if (
rootRx.test(p) &&
(fs.lstatSync(p).isDirectory() || fs.lstatSync(p).isFile())
) {
return true;
}
fs.copySync(seedPlugin, newPluginPath, {filter: (p) => {
// If it's a client-side plugin, copying client folder
if (answers.client) {
return /client/.test(p);
}
// Allowing plugin folder and files with no subfolders
const rootRx = /plugin$|plugin\/[^/]*(\.).{2,3}/igm;
if (rootRx.test(p) && (fs.lstatSync(p).isDirectory() || fs.lstatSync(p).isFile())) {
return true;
}
// If it's a client-side plugin, copying client folder
if (answers.client) {
return /client/.test(p);
}
// If it's a server-side plugin, copying server folder
if (answers.server) {
return /server/.test(p);
}
}});
// If it's a server-side plugin, copying server folder
if (answers.server) {
return /server/.test(p);
}
},
});
}
// Let's add this to the plugins.json
if (answers.addPluginsJson) {
const pluginsJson = path.resolve(__dirname, '..', 'plugins.json');
fs.readJson(pluginsJson)
.then((j) => {
fs
.readJson(pluginsJson)
.then(j => {
// This is a client-side plugin, let's push this.
if (answers.client) {
j.client.push(answers.pluginName);
@@ -377,14 +409,17 @@ async function createSeedPlugin() {
fs.writeFileSync(pluginsJson, output);
}
})
.catch((err) => {
.catch(err => {
console.error(err);
});
}
console.log(`✨ Yay! Plugin created! Find your plugin: ${answers.pluginName} in the ./plugins folder`);
console.log(
`✨ Yay! Plugin created! Find your plugin: ${
answers.pluginName
} in the ./plugins folder`
);
}
}
//==============================================================================
@@ -403,9 +438,14 @@ program
program
.command('reconcile')
.description('reconciles local plugin dependencies and downloads external plugins')
.description(
'reconciles local plugin dependencies and downloads external plugins'
)
.option('-u, --upgrade-remote', 'upgrades remote dependencies')
.option('-d, --dry-run', 'does not actually change anything on the filesystem acts only as a simulation')
.option(
'-d, --dry-run',
'does not actually change anything on the filesystem acts only as a simulation'
)
.option('--skip-local', 'skips the local dependancy reconciliation')
.option('--skip-remote', 'skips the remote plugin reconciliation')
.action(reconcilePluginDeps);
+5 -3
View File
@@ -10,12 +10,14 @@ const serve = require('../serve');
program
.option('-j, --jobs', 'enable job processing on this thread')
.option('-w, --websockets', 'enable the websocket (subscriptions) handler on this thread')
.option(
'-w, --websockets',
'enable the websocket (subscriptions) handler on this thread'
)
.parse(process.argv);
// Start serving.
serve({jobs: program.jobs, websockets: program.websockets}).catch((err) => {
serve({ jobs: program.jobs, websockets: program.websockets }).catch(err => {
console.error(err);
util.shutdown(1);
});
+3 -3
View File
@@ -16,12 +16,12 @@ async function changeOrgName() {
try {
let settings = await SettingsService.retrieve();
let {organizationName} = await inquirer.prompt([
let { organizationName } = await inquirer.prompt([
{
name: 'organizationName',
message: 'Organization Name',
default: settings.organizationName
}
default: settings.organizationName,
},
]);
if (settings.organizationName !== organizationName) {
+39 -47
View File
@@ -16,9 +16,7 @@ const UsersService = require('../services/users');
const errors = require('../errors');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
//==============================================================================
// Setting up the program command line arguments.
@@ -34,19 +32,15 @@ program
//==============================================================================
const performSetup = async () => {
// Get the current settings, we are expecing an error here.
try {
// Try to get the settings.
await SettingsService.retrieve();
// We should NOT have gotten a settings object, this means that the
// application is already setup. Error out here.
throw errors.ErrSettingsInit;
} catch (e) {
// If the error is `not init`, then we're good, otherwise, it's something
// else.
if (e !== errors.ErrSettingsNotInit) {
@@ -66,7 +60,9 @@ const performSetup = async () => {
// Create the base settings model.
let settings = new SettingModel();
console.log('\nWe\'ll ask you some questions in order to setup your installation of Talk.\n');
console.log(
"\nWe'll ask you some questions in order to setup your installation of Talk.\n"
);
let answers = await inquirer.prompt([
{
@@ -74,31 +70,31 @@ const performSetup = async () => {
name: 'organizationName',
message: 'Organization Name',
default: settings.organizationName,
validate: (input) => {
validate: input => {
if (input && input.length > 0) {
return true;
}
return 'Organization Name is required.';
}
},
},
{
type: 'list',
choices: MODERATION_OPTIONS,
name: 'moderation',
default: settings.moderation,
message: 'Select a moderation mode'
message: 'Select a moderation mode',
},
{
type: 'confirm',
name: 'requireEmailConfirmation',
default: settings.requireEmailConfirmation,
message: 'Should emails always be confirmed'
message: 'Should emails always be confirmed',
},
]);
// Update the settings that were changed.
Object.keys(answers).forEach((key) => {
Object.keys(answers).forEach(key => {
if (answers[key] !== undefined) {
settings[key] = answers[key];
}
@@ -109,97 +105,93 @@ const performSetup = async () => {
type: 'confirm',
name: 'inputWhitelistedDomains',
default: true,
message: 'Would you like to specify a whitelisted domain'
message: 'Would you like to specify a whitelisted domain',
},
{
type: 'input',
name: 'whitelistedDomain',
message: 'Whitelisted Domain',
when: ({inputWhitelistedDomains}) => inputWhitelistedDomains,
validate: (input) => {
when: ({ inputWhitelistedDomains }) => inputWhitelistedDomains,
validate: input => {
if (input && input.length > 0) {
return true;
}
return 'Whitelisted Domain cannot be empty.';
}
}
},
},
]);
if (answers.inputWhitelistedDomains) {
settings.domains.whitelist = [answers.whitelistedDomain];
}
console.log('\nWe\'ll ask you some questions about your first admin user.\n');
console.log("\nWe'll ask you some questions about your first admin user.\n");
let user = await inquirer.prompt([
{
type: 'input',
name: 'username',
message: 'Username',
filter: (username) => {
return UsersService
.isValidUsername(username, false)
.catch((err) => {
throw err.message;
});
}
filter: username => {
return UsersService.isValidUsername(username, false).catch(err => {
throw err.message;
});
},
},
{
name: 'email',
message: 'Email',
format: 'email',
validate: (value) => {
validate: value => {
if (value && value.length >= 3) {
return true;
}
return 'Email is required';
}
},
},
{
name: 'password',
message: 'Password',
type: 'password',
filter: (password) => {
return UsersService
.isValidPassword(password)
.catch((err) => {
throw err.message;
});
}
filter: password => {
return UsersService.isValidPassword(password).catch(err => {
throw err.message;
});
},
},
{
name: 'confirmPassword',
message: 'Confirm Password',
type: 'password',
filter: (confirmPassword, {password}) => {
filter: (confirmPassword, { password }) => {
if (password !== confirmPassword) {
return Promise.reject(new Error('Passwords do not match'));
}
return UsersService
.isValidPassword(confirmPassword)
.catch((err) => {
throw err.message;
});
}
return UsersService.isValidPassword(confirmPassword).catch(err => {
throw err.message;
});
},
},
]);
let {user: newUser} = await SetupService.setup({
let { user: newUser } = await SetupService.setup({
settings: settings.toObject(),
user: {
email: user.email,
username: user.username,
password: user.password
}
password: user.password,
},
});
console.log('Settings created.');
console.log(`User ${newUser.id} created.`);
console.log('\nTalk is now installed!');
console.log('\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.');
console.log(
'\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.'
);
};
// Start tthe setup process.
@@ -207,7 +199,7 @@ performSetup()
.then(() => {
util.shutdown();
})
.catch((e) => {
.catch(e => {
console.error(e);
util.shutdown(1);
});
+5 -17
View File
@@ -11,28 +11,18 @@ const TokensService = require('../services/tokens');
const Table = require('cli-table');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
async function listTokens(userID) {
try {
let tokens = await TokensService.list(userID);
let table = new Table({
head: [
'ID',
'Name',
'Status'
]
head: ['ID', 'Name', 'Status'],
});
tokens.forEach((token) => {
table.push([
token.id,
token.name,
token.active ? 'Active' : 'Revoked'
]);
tokens.forEach(token => {
table.push([token.id, token.name, token.active ? 'Active' : 'Revoked']);
});
console.log(table.toString());
@@ -46,7 +36,6 @@ async function listTokens(userID) {
async function revokeToken(tokenID) {
try {
await TokensService.revoke(null, tokenID);
console.log(`Revoked Token[${tokenID}]`);
@@ -60,8 +49,7 @@ async function revokeToken(tokenID) {
async function createToken(userID, tokenName) {
try {
let {pat: {id}, jwt} = await TokensService.create(userID, tokenName);
let { pat: { id }, jwt } = await TokensService.create(userID, tokenName);
console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`);
+45 -47
View File
@@ -7,15 +7,18 @@
const util = require('./util');
const program = require('commander');
const inquirer = require('inquirer');
const {graphql} = require('graphql');
const {stripIndent} = require('common-tags');
const { graphql } = require('graphql');
const { stripIndent } = require('common-tags');
const Table = require('cli-table');
// Make things colorful!
require('colors');
// Register the autocomplete plugin.
inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt'));
inquirer.registerPrompt(
'autocomplete',
require('inquirer-autocomplete-prompt')
);
const schema = require('../graph/schema');
const Context = require('../graph/context');
@@ -28,19 +31,15 @@ const mongoose = require('../services/mongoose');
const databaseVerifications = require('./verifications/database');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
/**
* Deletes a user and cleans up their associated verifications.
*/
async function deleteUser(userID) {
try {
// Find the user we're removing.
const user = await UserModel.findOne({id: userID});
const user = await UserModel.findOne({ id: userID });
if (!user) {
throw new Error(`user with id ${userID} not found`);
}
@@ -54,7 +53,7 @@ async function deleteUser(userID) {
This might take a long time if there is a lot of data, please confirm that
you want to continue.
`);
const {confirm} = await inquirer.prompt({
const { confirm } = await inquirer.prompt({
type: 'confirm',
name: 'confirm',
message: 'Continue',
@@ -64,27 +63,25 @@ async function deleteUser(userID) {
return util.shutdown();
}
console.warn('Removing user\'s actions');
console.warn("Removing user's actions");
// Remove all the user's actions.
await ActionModel
.where({user_id: user.id})
.setOptions({multi: true})
await ActionModel.where({ user_id: user.id })
.setOptions({ multi: true })
.remove();
console.warn('Removing user\'s comments');
console.warn("Removing user's comments");
// Remove all the user's comments.
await CommentModel
.where({author_id: user.id})
.setOptions({multi: true})
await CommentModel.where({ author_id: user.id })
.setOptions({ multi: true })
.remove();
console.warn('Updating the database indexes');
// Update the counts that might have changed.
for (const verification of databaseVerifications) {
await verification({fix: true, limit: Infinity, batch: 1000});
await verification({ fix: true, limit: Infinity, batch: 1000 });
}
console.warn('Removing the user');
@@ -103,15 +100,19 @@ function printUserAsTable(user) {
let table = new Table({});
table.push(
{'ID': user.id.gray},
{'Username': user.username},
{'Emails': user.emails},
{'Tags': user.tags ? user.tags.map(({tag: {name}}) => name) : ''},
{'Role': user.role},
{'Verified': user.hasVerifiedEmail},
{'Username': user.status.username.status},
{'Banned': user.banned},
{'Suspension': user.suspended ? `Until ${user.status.suspension.until}` : false},
{ ID: user.id.gray },
{ Username: user.username },
{ Emails: user.emails },
{ Tags: user.tags ? user.tags.map(({ tag: { name } }) => name) : '' },
{ Role: user.role },
{ Verified: user.hasVerifiedEmail },
{ Username: user.status.username.status },
{ Banned: user.banned },
{
Suspension: user.suspended
? `Until ${user.status.suspension.until}`
: false,
}
);
console.log(table.toString());
@@ -148,7 +149,7 @@ async function searchUsers() {
value = '';
}
const {data, errors} = await graphql(schema, searchQuery, {}, ctx, {
const { data, errors } = await graphql(schema, searchQuery, {}, ctx, {
value,
});
if (errors && errors.length > 0) {
@@ -159,18 +160,20 @@ async function searchUsers() {
return [];
}
return data.users.nodes.map((user) => {
return data.users.nodes.map(user => {
const emails = user.emails.join(', ');
return {
name: `${user.username} (${emails}) ${user.id.gray} - ${user.role.gray}`,
name: `${user.username} (${emails}) ${user.id.gray} - ${
user.role.gray
}`,
value: user.id,
};
});
}
},
});
const {userID} = answers;
const user = await UserModel.findOne({id: userID});
const { userID } = answers;
const user = await UserModel.findOne({ id: userID });
printUserAsTable(user);
util.shutdown(0);
@@ -187,13 +190,13 @@ async function searchUsers() {
*/
async function setUserRole(userID) {
try {
const {role} = await inquirer.prompt([
const { role } = await inquirer.prompt([
{
name: 'role',
message: 'User Role',
type: 'list',
choices: USER_ROLES
}
choices: USER_ROLES,
},
]);
await UsersService.setRole(userID, role);
@@ -204,7 +207,6 @@ async function setUserRole(userID) {
console.error(err);
util.shutdown(1);
}
}
/**
@@ -217,9 +219,8 @@ async function setUserRole(userID) {
*/
async function verifyUserEmail(userID, email) {
try {
// Get the user.
const user = await UserModel.findOne({id: userID});
const user = await UserModel.findOne({ id: userID });
if (!user) {
throw new Error(`user with ID ${userID} cannot be found`);
}
@@ -231,23 +232,20 @@ async function verifyUserEmail(userID, email) {
}
if (!email && emails.length === 1) {
// The email wasn't passed, and there is only one option.
email = emails[0];
} else if (!emails.includes(email)){
} else if (!emails.includes(email)) {
// The email passed doesn't belong to this user.
throw new Error(`user does not have the email ${email}`);
} else if (emails.length > 1) {
// The email wasn't passed, and there is more than one choice.
const answers = await inquirer.prompt([
{
name: 'email',
message: 'Select Email to Verify',
type: 'list',
choices: emails
}
choices: emails,
},
]);
email = answers.email;
@@ -284,7 +282,7 @@ program
program
.command('verify <userID> <email>')
.description('verifies the given user\'s email address')
.description("verifies the given user's email address")
.action(verifyUserEmail);
program.parse(process.argv);
+17 -8
View File
@@ -10,17 +10,18 @@ const mongoose = require('../services/mongoose');
const databaseVerifications = require('./verifications/database');
// Register the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
util.onshutdown([() => mongoose.disconnect()]);
async function database({fix = false, limit = Infinity, batch = 1000}) {
async function database({ fix = false, limit = Infinity, batch = 1000 }) {
try {
for (const verification of databaseVerifications) {
await verification({fix, limit, batch});
await verification({ fix, limit, batch });
}
} catch (err) {
console.error(`Failed to process all the ${databaseVerifications.length} verifications`, err);
console.error(
`Failed to process all the ${databaseVerifications.length} verifications`,
err
);
util.shutdown(1);
return;
}
@@ -36,8 +37,16 @@ program
.command('db')
.description('verifies the database integrity')
.option('-f, --fix', 'fix the problems found with database inconsistencies')
.option('-l, --limit [size]', 'limit the amount of documents to process in a single pass, this will ensure only a maximum number of batch operations are issued [default: inf]', parseInt)
.option('-b, --batch [size]', 'batch size to process verifications and repairs of documents [default: 1000]', parseInt)
.option(
'-l, --limit [size]',
'limit the amount of documents to process in a single pass, this will ensure only a maximum number of batch operations are issued [default: inf]',
parseInt
)
.option(
'-b, --batch [size]',
'batch size to process verifications and repairs of documents [default: 1000]',
parseInt
)
.action(database);
program.parse(process.argv);
@@ -1,12 +1,12 @@
import React from 'react';
import {CoralLogo} from 'plugin-api/beta/client/components/ui';
import { CoralLogo } from 'plugin-api/beta/client/components/ui';
import styles from './MyPluginComponent.css';
class MyPluginComponent extends React.Component {
render() {
return (
<div className={styles.myPluginContainer}>
<CoralLogo className={styles.logo}/>
<CoralLogo className={styles.logo} />
<div className={styles.description}>
<h3>Plugin created by Talk CLI</h3>
+2 -3
View File
@@ -1,4 +1,3 @@
/**
This is a client index example file and it could look like this:
@@ -21,6 +20,6 @@ import MyPluginComponent from './components/MyPluginComponent';
export default {
slots: {
stream: [MyPluginComponent]
}
stream: [MyPluginComponent],
},
};
+11 -11
View File
@@ -1,10 +1,9 @@
// Setup the environment.
require('../services/env');
const debug = require('debug')('talk:util');
const util = module.exports = {};
const util = (module.exports = {});
/**
* Stores an array of functions that should be executed in the event that the
@@ -18,20 +17,22 @@ util.toshutdown = [];
* @param {Number} [defaultCode=0] default return code upon sucesfull shutdown.
*/
util.shutdown = (defaultCode = 0, signal = null) => {
if (signal) {
debug(`Reached ${signal} signal`);
}
debug(`${util.toshutdown.length} jobs now being called`);
Promise
.all(util.toshutdown.map((func) => func ? func(signal) : null).filter((func) => func))
Promise.all(
util.toshutdown
.map(func => (func ? func(signal) : null))
.filter(func => func)
)
.then(() => {
debug('Shutdown complete, now exiting');
process.exit(defaultCode);
})
.catch((err) => {
.catch(err => {
console.error(err);
process.exit(1);
@@ -44,8 +45,7 @@ util.shutdown = (defaultCode = 0, signal = null) => {
* @param {Array} jobs Array of promise capable shutdown functions that are
* executed.
*/
util.onshutdown = (jobs) => {
util.onshutdown = jobs => {
debug(`${jobs.length} jobs registered to be called during shutdown`);
// Add the new jobs to shutdown to the object reference.
@@ -55,13 +55,13 @@ util.onshutdown = (jobs) => {
// Attach to the SIGTERM + SIGINT handles to ensure a clean shutdown in the
// event that we have an external event. SIGUSR2 is called when the app is asked
// to be 'killed', same procedure here.
process.on('SIGTERM', () => util.shutdown(0, 'SIGTERM'));
process.on('SIGINT', () => util.shutdown(0, 'SIGINT'));
process.on('SIGTERM', () => util.shutdown(0, 'SIGTERM'));
process.on('SIGINT', () => util.shutdown(0, 'SIGINT'));
process.once('SIGUSR2', () => util.shutdown(0, 'SIGUSR2'));
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', (err) => {
process.on('unhandledRejection', err => {
throw err;
});
+55 -41
View File
@@ -1,28 +1,24 @@
const UserModel = require('../../../models/user');
const CommentModel = require('../../../models/comment');
const ActionsService = require('../../../services/actions');
const {arrayJoinBy} = require('../../../graph/loaders/util');
const {get} = require('lodash');
const { arrayJoinBy } = require('../../../graph/loaders/util');
const { get } = require('lodash');
const debug = require('debug')('talk:cli:verify');
const MODELS = [
UserModel,
CommentModel,
];
const MODELS = [UserModel, CommentModel];
async function processBatch(Model, documents) {
// Get an array of all the document id's.
const documentIDs = documents.map(({id}) => id);
const documentIDs = documents.map(({ id }) => id);
// Store all the operations on this batch in this array that we'll return
// later.
const operations = [];
// Get the action summaries for this batch.
const totalActionSummaries = await ActionsService
.getActionSummaries(documentIDs)
.then(arrayJoinBy(documentIDs, 'item_id'));
const totalActionSummaries = await ActionsService.getActionSummaries(
documentIDs
).then(arrayJoinBy(documentIDs, 'item_id'));
// Iterate over the documents.
for (let i = 0; i < documents.length; i++) {
@@ -49,8 +45,10 @@ async function processBatch(Model, documents) {
const ACTION_COUNT_FIELD = `${ACTION_TYPE}_${GROUP_ID}`;
// Check that the action summaries match the cached counts.
if (get(document, ['action_counts', ACTION_COUNT_FIELD]) !== actionSummary.count) {
if (
get(document, ['action_counts', ACTION_COUNT_FIELD]) !==
actionSummary.count
) {
// Batch updates for those changes.
ops.push({
[`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count,
@@ -60,27 +58,28 @@ async function processBatch(Model, documents) {
// Group all the action summaries together from all the different group
// ids.
const groupedActionSummaries = actionSummaries.reduce((acc, actionSummary) => {
const groupedActionSummaries = actionSummaries.reduce(
(acc, actionSummary) => {
// action_type is already snake cased (as it would have had to be when it
// was inserted in the database).
const ACTION_TYPE = actionSummary.action_type.toLowerCase();
// action_type is already snake cased (as it would have had to be when it
// was inserted in the database).
const ACTION_TYPE = actionSummary.action_type.toLowerCase();
if (!(ACTION_TYPE in acc)) {
acc[ACTION_TYPE] = 0;
}
if (!(ACTION_TYPE in acc)) {
acc[ACTION_TYPE] = 0;
}
acc[ACTION_TYPE] += actionSummary.count;
acc[ACTION_TYPE] += actionSummary.count;
return acc;
}, {});
return acc;
},
{}
);
for (const ACTION_COUNT_FIELD of Object.keys(groupedActionSummaries)) {
const count = groupedActionSummaries[ACTION_COUNT_FIELD];
// Check that the action summaries match the cached counts.
if (get(document, ['action_counts', ACTION_COUNT_FIELD]) !== count) {
// Batch updates for those changes.
ops.push({
[`action_counts.${ACTION_COUNT_FIELD}`]: count,
@@ -94,7 +93,7 @@ async function processBatch(Model, documents) {
operations.push({
updateOne: {
filter: {
id: document.id
id: document.id,
},
update: {
$set: Object.assign({}, ...ops),
@@ -107,23 +106,21 @@ async function processBatch(Model, documents) {
return operations;
}
module.exports = async ({fix, batch}) => {
module.exports = async ({ fix, batch }) => {
for (const Model of MODELS) {
const cursor = Model
.collection
const cursor = Model.collection
.find({})
.project({
id: 1,
action_counts: 1
action_counts: 1,
})
.sort({created_at: 1});
.sort({ created_at: 1 });
let operations = [];
let documents = [];
// While there are documents to process.
while (await cursor.hasNext()) {
// Load the document.
const document = await cursor.next();
@@ -133,7 +130,6 @@ module.exports = async ({fix, batch}) => {
// Check to see if the length of the documents array requires us to
// process it.
if (documents.length > batch) {
// Process this batch.
let batchOperations = await processBatch(Model, documents);
@@ -147,7 +143,6 @@ module.exports = async ({fix, batch}) => {
// Check to see if there are any documents left over.
if (documents.length > 0) {
// Process this batch.
let batchOperations = await processBatch(Model, documents);
@@ -157,24 +152,43 @@ module.exports = async ({fix, batch}) => {
const OPERATIONS_LENGTH = operations.length;
console.log(`action_counts.js: ${OPERATIONS_LENGTH} ${Model.collection.name} need their action counts fixed.`);
console.log(
`action_counts.js: ${OPERATIONS_LENGTH} ${
Model.collection.name
} need their action counts fixed.`
);
// If fix was enabled, execute the batch writes.
if (OPERATIONS_LENGTH > 0) {
if (fix) {
debug(`action_counts.js: fixing ${OPERATIONS_LENGTH} ${Model.collection.name}...`);
debug(
`action_counts.js: fixing ${OPERATIONS_LENGTH} ${
Model.collection.name
}...`
);
while (operations.length) {
let result = await Model.collection.bulkWrite(operations.splice(0, batch));
let result = await Model.collection.bulkWrite(
operations.splice(0, batch)
);
debug(`action_counts.js: fixed batch of ${result.modifiedCount} ${Model.collection.name}.`);
debug(
`action_counts.js: fixed batch of ${result.modifiedCount} ${
Model.collection.name
}.`
);
}
console.log(`action_counts.js: applied all ${OPERATIONS_LENGTH} fixes to ${Model.collection.name}.`);
console.log(
`action_counts.js: applied all ${OPERATIONS_LENGTH} fixes to ${
Model.collection.name
}.`
);
} else {
console.warn('Skipping fixing, --fix was not enabled, pass --fix to fix these errors');
console.warn(
'Skipping fixing, --fix was not enabled, pass --fix to fix these errors'
);
}
}
}
};
+46 -36
View File
@@ -1,15 +1,15 @@
const CommentModel = require('../../../models/comment');
const {singleJoinBy} = require('../../../graph/loaders/util');
const { singleJoinBy } = require('../../../graph/loaders/util');
const debug = require('debug')('talk:cli:verify');
const getBatch = async (limit, offset) => CommentModel
.find({})
.select({'id': 1, 'action_counts': 1, 'reply_count': 1})
.limit(limit)
.skip(offset)
.sort('created_at');
const getBatch = async (limit, offset) =>
CommentModel.find({})
.select({ id: 1, action_counts: 1, reply_count: 1 })
.limit(limit)
.skip(offset)
.sort('created_at');
module.exports = async ({fix, limit, batch}) => {
module.exports = async ({ fix, limit, batch }) => {
let operations = [];
// Count how many comments there are to process.
@@ -23,35 +23,33 @@ module.exports = async ({fix, limit, batch}) => {
// Keep processing documents until there are is none left.
while (offset < totalCount) {
// Get a batch of comments.
comments = await getBatch(batch, offset);
commentIDs = comments.map(({id}) => id);
commentIDs = comments.map(({ id }) => id);
// Get their reply counts.
let allReplyCounts = await CommentModel
.aggregate([
{
$match: {
parent_id: {
$in: commentIDs,
},
status: {
$in: ['NONE', 'ACCEPTED']
}
}
let allReplyCounts = await CommentModel.aggregate([
{
$match: {
parent_id: {
$in: commentIDs,
},
status: {
$in: ['NONE', 'ACCEPTED'],
},
},
{
$group: {
_id: '$parent_id',
count: {
$sum: 1
}
}
}
])
},
{
$group: {
_id: '$parent_id',
count: {
$sum: 1,
},
},
},
])
.then(singleJoinBy(commentIDs, '_id'))
.then((results) => results.map((result) => result ? result.count : 0));
.then(results => results.map(result => (result ? result.count : 0)));
// Loop over the comments, with their action summaries.
for (let i = 0; i < comments.length; i++) {
@@ -75,7 +73,7 @@ module.exports = async ({fix, limit, batch}) => {
operations.push({
updateOne: {
filter: {
id: comment.id
id: comment.id,
},
update: {
$set: Object.assign({}, ...commentOperations),
@@ -88,10 +86,17 @@ module.exports = async ({fix, limit, batch}) => {
debug(`Processed batch of ${comments.length} comments.`);
if (operations.length >= limit) {
debug(`Queued operations are ${operations.length}, reached limit of ${limit}, not processing any more.`);
debug(
`Queued operations are ${
operations.length
}, reached limit of ${limit}, not processing any more.`
);
if (operations.length > limit) {
debug(`${operations.length - limit} operations have been truncated to enforce the limit`);
debug(
`${operations.length -
limit} operations have been truncated to enforce the limit`
);
}
break;
@@ -103,7 +108,10 @@ module.exports = async ({fix, limit, batch}) => {
const OPERATIONS_LENGTH = operations.length;
if (limit < Infinity && offset + comments.length < totalCount) {
console.log(`Processed ${offset + comments.length}/${totalCount} comments because we reached the update limit of ${limit}.`);
console.log(
`Processed ${offset +
comments.length}/${totalCount} comments because we reached the update limit of ${limit}.`
);
} else {
console.log(`Processed all ${totalCount} comments.`);
}
@@ -124,7 +132,9 @@ module.exports = async ({fix, limit, batch}) => {
console.log(`Applied all ${OPERATIONS_LENGTH} fixes.`);
} else {
console.warn('Skipping fixing, --fix was not enabled, pass --fix to fix these errors');
console.warn(
'Skipping fixing, --fix was not enabled, pass --fix to fix these errors'
);
}
}
};
+1 -4
View File
@@ -7,7 +7,4 @@
// async ({fix = false, batch = 1000}) => {}
//
// where their options are derived.
module.exports = [
require('./comment_replies'),
require('./action_counts'),
];
module.exports = [require('./comment_replies'), require('./action_counts')];
+17 -17
View File
@@ -1,44 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
import { Router, Route, IndexRedirect, IndexRoute } from 'react-router';
import Configure from 'routes/Configure';
import Install from 'routes/Install';
import Stories from 'routes/Stories';
import Community from 'routes/Community/containers/Community';
import {ModerationLayout, Moderation} from 'routes/Moderation';
import { ModerationLayout, Moderation } from 'routes/Moderation';
import Layout from 'containers/Layout';
const routes = (
<div>
<Route exact path="/admin/install" component={Install}/>
<Route path='/admin' component={Layout}>
<IndexRedirect to='/admin/moderate' />
<Route path='configure' component={Configure} />
<Route path='stories' component={Stories} />
<Route exact path="/admin/install" component={Install} />
<Route path="/admin" component={Layout}>
<IndexRedirect to="/admin/moderate" />
<Route path="configure" component={Configure} />
<Route path="stories" component={Stories} />
{/* Community Routes */}
<Route path='community'>
<Route path='flagged' components={Community}>
<Route path=':id' components={Community} />
<Route path="community">
<Route path="flagged" components={Community}>
<Route path=":id" components={Community} />
</Route>
<Route path='people' components={Community}>
<Route path=':id' components={Community} />
<Route path="people" components={Community}>
<Route path=":id" components={Community} />
</Route>
<IndexRedirect to='flagged' />
<IndexRedirect to="flagged" />
</Route>
{/* Moderation Routes */}
<Route path='moderate' component={ModerationLayout}>
<Route path="moderate" component={ModerationLayout}>
<IndexRoute components={Moderation} />
<Route path=':tabOrId' components={Moderation} />
<Route path=":tabOrId" components={Moderation} />
<Route path=':tab' components={Moderation}>
<Route path=':id' components={Moderation} />
<Route path=":tab" components={Moderation}>
<Route path=":id" components={Moderation} />
</Route>
</Route>
</Route>
+43 -34
View File
@@ -7,26 +7,29 @@ import jwtDecode from 'jwt-decode';
// SIGN IN
//==============================================================================
export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client, storage}) => {
dispatch({type: actions.LOGIN_REQUEST});
export const handleLogin = (email, password, recaptchaResponse) => (
dispatch,
_,
{ rest, client, storage }
) => {
dispatch({ type: actions.LOGIN_REQUEST });
const params = {
method: 'POST',
body: {
email,
password
}
password,
},
};
if (recaptchaResponse) {
params.headers = {
'X-Recaptcha-Response': recaptchaResponse
'X-Recaptcha-Response': recaptchaResponse,
};
}
return rest('/auth/local', params)
.then(({user, token}) => {
.then(({ user, token }) => {
if (!user) {
if (!bowser.safari && !bowser.ios && storage) {
storage.removeItem('token');
@@ -38,19 +41,19 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _,
client.resetWebsocket();
dispatch(checkLoginSuccess(user));
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
if (error.translation_key === 'NOT_AUTHORIZED') {
// invalid credentials
dispatch({
type: actions.LOGIN_FAILURE,
message: t('error.email_password')
message: t('error.email_password'),
});
}
else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
} else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
dispatch({
type: actions.LOGIN_MAXIMUM_EXCEEDED,
message: t(`error.${error.translation_key}`),
@@ -69,27 +72,32 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _,
//==============================================================================
const forgotPasswordRequest = () => ({
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
type: actions.FETCH_FORGOT_PASSWORD_REQUEST,
});
const forgotPasswordSuccess = () => ({
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS,
});
const forgotPasswordFailure = (error) => ({
const forgotPasswordFailure = error => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
error,
});
export const requestPasswordReset = (email) => (dispatch, _, {rest}) => {
export const requestPasswordReset = email => (dispatch, _, { rest }) => {
dispatch(forgotPasswordRequest(email));
const redirectUri = location.href;
return rest('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
return rest('/account/password/reset', {
method: 'POST',
body: { email, loc: redirectUri },
})
.then(() => dispatch(forgotPasswordSuccess()))
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(forgotPasswordFailure(errorMessage));
});
};
@@ -99,24 +107,24 @@ export const requestPasswordReset = (email) => (dispatch, _, {rest}) => {
//==============================================================================
const checkLoginRequest = () => ({
type: actions.CHECK_LOGIN_REQUEST
type: actions.CHECK_LOGIN_REQUEST,
});
const checkLoginSuccess = (user, isAdmin) => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
isAdmin
isAdmin,
});
const checkLoginFailure = (error) => ({
const checkLoginFailure = error => ({
type: actions.CHECK_LOGIN_FAILURE,
error
error,
});
export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
export const checkLogin = () => (dispatch, _, { rest, client, storage }) => {
dispatch(checkLoginRequest());
return rest('/auth')
.then(({user}) => {
.then(({ user }) => {
if (!user) {
if (!bowser.safari && !bowser.ios && storage) {
storage.removeItem('token');
@@ -127,9 +135,11 @@ export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
client.resetWebsocket();
dispatch(checkLoginSuccess(user));
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(checkLoginFailure(errorMessage));
});
};
@@ -138,8 +148,8 @@ export const checkLogin = () => (dispatch, _, {rest, client, storage}) => {
// LOGOUT
//==============================================================================
export const logout = () => (dispatch, _, {rest, client, storage}) => {
return rest('/auth', {method: 'DELETE'}).then(() => {
export const logout = () => (dispatch, _, { rest, client, storage }) => {
return rest('/auth', { method: 'DELETE' }).then(() => {
if (storage) {
storage.removeItem('token');
}
@@ -147,7 +157,7 @@ export const logout = () => (dispatch, _, {rest, client, storage}) => {
// Reset the websocket.
client.resetWebsocket();
dispatch({type: actions.LOGOUT});
dispatch({ type: actions.LOGOUT });
});
};
@@ -155,11 +165,10 @@ export const logout = () => (dispatch, _, {rest, client, storage}) => {
// AUTH TOKEN
//==============================================================================
export const handleAuthToken = (token) => (dispatch, _, {storage}) => {
export const handleAuthToken = token => (dispatch, _, { storage }) => {
if (storage) {
storage.setItem('exp', jwtDecode(token).exp);
storage.setItem('token', token);
}
dispatch({type: 'HANDLE_AUTH_TOKEN'});
dispatch({ type: 'HANDLE_AUTH_TOKEN' });
};
@@ -1,6 +1,19 @@
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
import {
SHOW_BAN_USER_DIALOG,
HIDE_BAN_USER_DIALOG,
} from '../constants/banUserDialog';
export const showBanUserDialog = ({userId, username, commentId, commentStatus}) =>
({type: SHOW_BAN_USER_DIALOG, userId, username, commentId, commentStatus});
export const showBanUserDialog = ({
userId,
username,
commentId,
commentStatus,
}) => ({
type: SHOW_BAN_USER_DIALOG,
userId,
username,
commentId,
commentStatus,
});
export const hideBanUserDialog = () => ({type: HIDE_BAN_USER_DIALOG});
export const hideBanUserDialog = () => ({ type: HIDE_BAN_USER_DIALOG });
+23 -16
View File
@@ -10,54 +10,61 @@ import {
SHOW_BANUSER_DIALOG,
HIDE_BANUSER_DIALOG,
SHOW_REJECT_USERNAME_DIALOG,
HIDE_REJECT_USERNAME_DIALOG
HIDE_REJECT_USERNAME_DIALOG,
} from '../constants/community';
import t from 'coral-framework/services/i18n';
export const fetchUsers = (query = {}) => (dispatch, _, {rest}) => {
export const fetchUsers = (query = {}) => (dispatch, _, { rest }) => {
dispatch(requestFetchUsers());
rest(`/users?${queryString.stringify(query)}`)
.then(({result, page, count, limit, totalPages}) =>{
.then(({ result, page, count, limit, totalPages }) => {
dispatch({
type: FETCH_USERS_SUCCESS,
users: result,
page,
count,
limit,
totalPages
totalPages,
});
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: FETCH_USERS_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: FETCH_USERS_FAILURE, error: errorMessage });
});
};
const requestFetchUsers = () => ({
type: FETCH_USERS_REQUEST
type: FETCH_USERS_REQUEST,
});
export const updateSorting = (sort) => ({
export const updateSorting = sort => ({
type: SORT_UPDATE,
sort
sort,
});
export const setPage = (page) => ({
export const setPage = page => ({
type: SET_PAGE,
page,
});
export const setSearchValue = (value) => ({
export const setSearchValue = value => ({
type: SET_SEARCH_VALUE,
value,
});
// Ban User Dialog
export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user});
export const hideBanUserDialog = () => ({type: HIDE_BANUSER_DIALOG});
export const showBanUserDialog = user => ({ type: SHOW_BANUSER_DIALOG, user });
export const hideBanUserDialog = () => ({ type: HIDE_BANUSER_DIALOG });
// Reject Username Dialog
export const showRejectUsernameDialog = (user) => ({type: SHOW_REJECT_USERNAME_DIALOG, user});
export const hideRejectUsernameDialog = () => ({type: HIDE_REJECT_USERNAME_DIALOG});
export const showRejectUsernameDialog = user => ({
type: SHOW_REJECT_USERNAME_DIALOG,
user,
});
export const hideRejectUsernameDialog = () => ({
type: HIDE_REJECT_USERNAME_DIALOG,
});
+2 -2
View File
@@ -1,7 +1,7 @@
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
export const fetchConfig = () => (dispatch) => {
export const fetchConfig = () => dispatch => {
let json = document.getElementById('data');
let data = JSON.parse(json.textContent);
dispatch({type: CONFIG_UPDATED, data});
dispatch({ type: CONFIG_UPDATED, data });
};
+5 -5
View File
@@ -1,13 +1,13 @@
import * as actions from 'constants/configure';
export const updatePending = ({updater, errorUpdater}) => {
return {type: actions.UPDATE_PENDING, updater, errorUpdater};
export const updatePending = ({ updater, errorUpdater }) => {
return { type: actions.UPDATE_PENDING, updater, errorUpdater };
};
export const clearPending = () => {
return {type: actions.CLEAR_PENDING};
return { type: actions.CLEAR_PENDING };
};
export const setActiveSection = (section) => {
return {type: actions.SET_ACTIVE_SECTION, section};
export const setActiveSection = section => {
return { type: actions.SET_ACTIVE_SECTION, section };
};
+77 -60
View File
@@ -3,17 +3,17 @@ import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
import t from 'coral-framework/services/i18n';
export const nextStep = () => ({type: actions.NEXT_STEP});
export const previousStep = () => ({type: actions.PREVIOUS_STEP});
export const goToStep = (step) => ({type: actions.GO_TO_STEP, step});
export const nextStep = () => ({ type: actions.NEXT_STEP });
export const previousStep = () => ({ type: actions.PREVIOUS_STEP });
export const goToStep = step => ({ type: actions.GO_TO_STEP, step });
const installRequest = () => ({type: actions.INSTALL_REQUEST});
const installSuccess = () => ({type: actions.INSTALL_SUCCESS});
const installFailure = (error) => ({type: actions.INSTALL_FAILURE, error});
const installRequest = () => ({ type: actions.INSTALL_REQUEST });
const installSuccess = () => ({ type: actions.INSTALL_SUCCESS });
const installFailure = error => ({ type: actions.INSTALL_FAILURE, error });
const addError = (name, error) => ({type: actions.ADD_ERROR, name, error});
const hasError = (error) => ({type: actions.HAS_ERROR, error});
const clearErrors = () => ({type: actions.CLEAR_ERRORS});
const addError = (name, error) => ({ type: actions.ADD_ERROR, name, error });
const hasError = error => ({ type: actions.HAS_ERROR, error });
const clearErrors = () => ({ type: actions.CLEAR_ERRORS });
const validation = (formData, dispatch, next) => {
if (!(formData != null)) {
@@ -21,24 +21,21 @@ const validation = (formData, dispatch, next) => {
return;
}
const validKeys = Object.keys(formData)
.filter((name) => name !== 'domains');
const validKeys = Object.keys(formData).filter(name => name !== 'domains');
// Required Validation
const empty = validKeys
.filter((name) => {
const cond = !formData[name].length;
const empty = validKeys.filter(name => {
const cond = !formData[name].length;
if (cond) {
if (cond) {
// Adding Error
dispatch(addError(name, 'This field is required.'));
} else {
dispatch(addError(name, ''));
}
// Adding Error
dispatch(addError(name, 'This field is required.'));
} else {
dispatch(addError(name, ''));
}
return cond;
});
return cond;
});
if (empty.length) {
dispatch(hasError());
@@ -46,19 +43,17 @@ const validation = (formData, dispatch, next) => {
}
// RegExp Validation
const validation = validKeys
.filter((name) => {
const cond = !validate[name](formData[name]);
if (cond) {
const validation = validKeys.filter(name => {
const cond = !validate[name](formData[name]);
if (cond) {
// Adding Error
dispatch(addError(name, errorMsj[name]));
} else {
dispatch(addError(name, ''));
}
dispatch(addError(name, errorMsj[name]));
} else {
dispatch(addError(name, ''));
}
return cond;
});
return cond;
});
if (validation.length) {
dispatch(hasError());
@@ -67,20 +62,21 @@ const validation = (formData, dispatch, next) => {
// Confirm Validation
const prefixLength = 'confirm'.length;
const confirm = validKeys
.filter((name) => {
if (!name.startsWith('confirm')) {
return false;
}
const confirm = validKeys.filter(name => {
if (!name.startsWith('confirm')) {
return false;
}
// Check that 'confirmX' equals 'X'.
const other = name.substr(prefixLength, 1).toLowerCase() + name.substr(prefixLength + 1);
const cond = formData[other] !== formData[name];
if (cond) {
dispatch(addError(name, errorMsj[name]));
}
return cond;
});
// Check that 'confirmX' equals 'X'.
const other =
name.substr(prefixLength, 1).toLowerCase() +
name.substr(prefixLength + 1);
const cond = formData[other] !== formData[name];
if (cond) {
dispatch(addError(name, errorMsj[name]));
}
return cond;
});
if (confirm.length) {
dispatch(hasError());
@@ -105,41 +101,62 @@ export const submitUser = () => (dispatch, getState) => {
});
};
export const finishInstall = () => (dispatch, getState, {rest}) => {
export const finishInstall = () => (dispatch, getState, { rest }) => {
const data = getState().install.data;
dispatch(installRequest());
return rest('/setup', {method: 'POST', body: data})
return rest('/setup', { method: 'POST', body: data })
.then(() => {
dispatch(installSuccess());
dispatch(nextStep());
})
.catch((error) => {
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(installFailure(errorMessage));
});
};
export const updateSettingsFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_SETTINGS, name, value});
export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_USER, name, value});
export const updatePermittedDomains = (value) => ({type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS, value});
export const updateSettingsFormData = (name, value) => ({
type: actions.UPDATE_FORMDATA_SETTINGS,
name,
value,
});
export const updateUserFormData = (name, value) => ({
type: actions.UPDATE_FORMDATA_USER,
name,
value,
});
export const updatePermittedDomains = value => ({
type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS,
value,
});
const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error});
const checkInstallRequest = () => ({ type: actions.CHECK_INSTALL_REQUEST });
const checkInstallSuccess = installed => ({
type: actions.CHECK_INSTALL_SUCCESS,
installed,
});
const checkInstallFailure = error => ({
type: actions.CHECK_INSTALL_FAILURE,
error,
});
export const checkInstall = (next) => async (dispatch, _, {rest}) => {
export const checkInstall = next => async (dispatch, _, { rest }) => {
dispatch(checkInstallRequest());
try {
const {installed} = await rest('/setup');
const { installed } = await rest('/setup');
dispatch(checkInstallSuccess(installed));
if (installed) {
next();
}
} catch (error) {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch(checkInstallFailure(errorMessage));
}
};
+12 -13
View File
@@ -1,41 +1,40 @@
import * as actions from 'constants/moderation';
export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open});
export const singleView = () => ({type: actions.SINGLE_VIEW});
export const toggleModal = open => ({ type: actions.TOGGLE_MODAL, open });
export const singleView = () => ({ type: actions.SINGLE_VIEW });
// hide shortcuts note
export const hideShortcutsNote = () => (dispatch, _, {storage}) => {
export const hideShortcutsNote = () => (dispatch, _, { storage }) => {
try {
if (storage) {
storage.setItem('coral:shortcutsNote', 'hide');
}
} catch (e) {
// above will fail in Safari private mode
}
dispatch({type: actions.HIDE_SHORTCUTS_NOTE});
dispatch({ type: actions.HIDE_SHORTCUTS_NOTE });
};
export const setSortOrder = (order) => ({
export const setSortOrder = order => ({
type: actions.SET_SORT_ORDER,
order
order,
});
export const toggleStorySearch = (active) => ({
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH
export const toggleStorySearch = active => ({
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH,
});
export const storySearchChange = (value) => ({
export const storySearchChange = value => ({
type: actions.STORY_SEARCH_CHANGE_VALUE,
value
value,
});
export const clearState = () => ({
type: actions.MODERATION_CLEAR_STATE
type: actions.MODERATION_CLEAR_STATE,
});
export const selectCommentId = (id) => ({
export const selectCommentId = id => ({
type: actions.MODERATION_SELECT_COMMENT,
id,
});
+27 -22
View File
@@ -10,7 +10,7 @@ import {
UPDATE_ASSET_STATE_REQUEST,
UPDATE_ASSET_STATE_SUCCESS,
UPDATE_ASSET_STATE_FAILURE,
UPDATE_ASSETS
UPDATE_ASSETS,
} from '../constants/stories';
import t from 'coral-framework/services/i18n';
@@ -21,53 +21,58 @@ import t from 'coral-framework/services/i18n';
// Fetch a page of assets
// Get comments to fill each of the three lists on the mod queue
export const fetchAssets = (query = {}) => (dispatch, _, {rest}) => {
dispatch({type: FETCH_ASSETS_REQUEST});
export const fetchAssets = (query = {}) => (dispatch, _, { rest }) => {
dispatch({ type: FETCH_ASSETS_REQUEST });
return rest(`/assets?${queryString.stringify(query)}`)
.then(({result, page, count, limit, totalPages}) =>
dispatch({type: FETCH_ASSETS_SUCCESS,
.then(({ result, page, count, limit, totalPages }) =>
dispatch({
type: FETCH_ASSETS_SUCCESS,
assets: result,
page,
count,
limit,
totalPages,
}))
.catch((error) => {
})
)
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: FETCH_ASSETS_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: FETCH_ASSETS_FAILURE, error: errorMessage });
});
};
// Update an asset state
// Get comments to fill each of the three lists on the mod queue
export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => {
dispatch({type: UPDATE_ASSET_STATE_REQUEST, id, closedAt});
return rest(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
.then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
.catch((error) => {
export const updateAssetState = (id, closedAt) => (dispatch, _, { rest }) => {
dispatch({ type: UPDATE_ASSET_STATE_REQUEST, id, closedAt });
return rest(`/assets/${id}/status`, { method: 'PUT', body: { closedAt } })
.then(() => dispatch({ type: UPDATE_ASSET_STATE_SUCCESS }))
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage });
});
};
export const updateAssets = (assets) => (dispatch) => {
dispatch({type: UPDATE_ASSETS, assets});
export const updateAssets = assets => dispatch => {
dispatch({ type: UPDATE_ASSETS, assets });
};
export const setPage = (page) => ({
export const setPage = page => ({
type: SET_PAGE,
page,
});
export const setSearchValue = (value) => ({
export const setSearchValue = value => ({
type: SET_SEARCH_VALUE,
value,
});
export const setCriteria = (criteria) => ({
export const setCriteria = criteria => ({
type: SET_CRITERIA,
criteria,
});
@@ -1,7 +1,19 @@
import {SHOW_SUSPEND_USER_DIALOG, HIDE_SUSPEND_USER_DIALOG} from '../constants/suspendUserDialog.js';
import {
SHOW_SUSPEND_USER_DIALOG,
HIDE_SUSPEND_USER_DIALOG,
} from '../constants/suspendUserDialog.js';
export const showSuspendUserDialog = ({userId, username, commentId, commentStatus}) =>
({type: SHOW_SUSPEND_USER_DIALOG, userId, username, commentId, commentStatus});
export const hideSuspendUserDialog = () => ({type: HIDE_SUSPEND_USER_DIALOG});
export const showSuspendUserDialog = ({
userId,
username,
commentId,
commentStatus,
}) => ({
type: SHOW_SUSPEND_USER_DIALOG,
userId,
username,
commentId,
commentStatus,
});
export const hideSuspendUserDialog = () => ({ type: HIDE_SUSPEND_USER_DIALOG });
+18 -9
View File
@@ -1,28 +1,37 @@
import * as actions from 'constants/userDetail';
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
export const viewUserDetail = userId => ({
type: actions.VIEW_USER_DETAIL,
userId,
});
export const hideUserDetail = () => ({ type: actions.HIDE_USER_DETAIL });
export const changeTab = (tab) => {
export const changeTab = tab => {
let statuses = null;
if (tab === 'rejected') {
statuses = ['REJECTED'];
}
return {type: actions.CHANGE_TAB_USER_DETAIL, tab, statuses};
return { type: actions.CHANGE_TAB_USER_DETAIL, tab, statuses };
};
export const clearUserDetailSelections = () => ({type: actions.CLEAR_USER_DETAIL_SELECTIONS});
export const clearUserDetailSelections = () => ({
type: actions.CLEAR_USER_DETAIL_SELECTIONS,
});
export const toggleSelectCommentInUserDetail = (id, active) => {
return {
type: active ? actions.SELECT_USER_DETAIL_COMMENT : actions.UNSELECT_USER_DETAIL_COMMENT,
id
type: active
? actions.SELECT_USER_DETAIL_COMMENT
: actions.UNSELECT_USER_DETAIL_COMMENT,
id,
};
};
export const toggleSelectAllCommentInUserDetail = (ids, active) => {
return {
type: active ? actions.SELECT_ALL_USER_DETAIL_COMMENT : actions.CLEAR_USER_DETAIL_SELECTIONS,
ids
type: active
? actions.SELECT_ALL_USER_DETAIL_COMMENT
: actions.CLEAR_USER_DETAIL_SELECTIONS,
ids,
};
};
+39 -21
View File
@@ -6,38 +6,56 @@ import t from 'coral-framework/services/i18n';
*/
// change status of a user
export const userStatusUpdate = (status, userId, commentId) => {
return (dispatch, _, {rest}) => {
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
return rest(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
.then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
.catch((error) => {
return (dispatch, _, { rest }) => {
dispatch({ type: userTypes.UPDATE_STATUS_REQUEST });
return rest(`/users/${userId}/status`, {
method: 'POST',
body: { status: status, comment_id: commentId },
})
.then(res => dispatch({ type: userTypes.UPDATE_STATUS_SUCCESS, res }))
.catch(error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error: errorMessage});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({
type: userTypes.UPDATE_STATUS_FAILURE,
error: errorMessage,
});
});
};
};
// change status of a user
export const sendNotificationEmail = (userId, subject, body) => {
return (dispatch, _, {rest}) => {
return rest(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
.catch((error) => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: userTypes.USER_EMAIL_FAILURE, error: errorMessage});
});
return (dispatch, _, { rest }) => {
return rest(`/users/${userId}/email`, {
method: 'POST',
body: { subject, body },
}).catch(error => {
console.error(error);
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({ type: userTypes.USER_EMAIL_FAILURE, error: errorMessage });
});
};
};
// let a user edit their username
export const enableUsernameEdit = (userId) => {
return (dispatch, _, {rest}) => {
return rest(`/users/${userId}/username-enable`, {method: 'POST'})
.catch((error) => {
export const enableUsernameEdit = userId => {
return (dispatch, _, { rest }) => {
return rest(`/users/${userId}/username-enable`, { method: 'POST' }).catch(
error => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error: errorMessage});
});
const errorMessage = error.translation_key
? t(`error.${error.translation_key}`)
: error.toString();
dispatch({
type: userTypes.USERNAME_ENABLE_FAILURE,
error: errorMessage,
});
}
);
};
};
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {murmur3} from 'murmurhash-js';
import { murmur3 } from 'murmurhash-js';
import styles from './AccountHistory.css';
import cn from 'classnames';
import flatten from 'lodash/flatten';
@@ -8,21 +8,27 @@ import orderBy from 'lodash/orderBy';
import moment from 'moment';
const buildUserHistory = (userState = {}) => {
return orderBy(flatten(Object.keys(userState.status)
.filter((k) => k !== '__typename')
.map((k) => userState.status[k].history)), 'created_at', 'desc');
return orderBy(
flatten(
Object.keys(userState.status)
.filter(k => k !== '__typename')
.map(k => userState.status[k].history)
),
'created_at',
'desc'
);
};
const buildActionResponse = (typename, until, status) => {
switch (typename) {
case 'UsernameStatusHistory':
return `Username ${status}`;
case 'BannedStatusHistory':
return status ? 'User banned' : 'Ban removed';
case 'SuspensionStatusHistory':
return until ? 'Account Suspended' : 'Suspension removed' ;
default:
return '-';
case 'UsernameStatusHistory':
return `Username ${status}`;
case 'BannedStatusHistory':
return status ? 'User banned' : 'Ban removed';
case 'SuspensionStatusHistory':
return until ? 'Account Suspended' : 'Suspension removed';
default:
return '-';
}
};
@@ -35,31 +41,55 @@ const getModerationValue = (userId, assignedBy = {}) => {
class AccountHistory extends React.Component {
render() {
const {user} = this.props;
const { user } = this.props;
const userHistory = buildUserHistory(user.state);
return (
<div>
<div className={cn(styles.table, 'talk-admin-account-history')}>
<div className={cn(styles.headerRow, 'talk-admin-account-history-header-row')}>
<div
className={cn(
styles.headerRow,
'talk-admin-account-history-header-row'
)}
>
<div className={styles.headerRowItem}>Date</div>
<div className={styles.headerRowItem}>Action</div>
<div className={styles.headerRowItem}>Moderation</div>
</div>
{
userHistory.map(({__typename, created_at, assigned_by, until, status}) => (
<div className={cn(styles.row, 'talk-admin-account-history-row')} key={`${__typename}_${murmur3(created_at)}`}>
<div className={cn(styles.item, 'talk-admin-account-history-row-date')}>
{userHistory.map(
({ __typename, created_at, assigned_by, until, status }) => (
<div
className={cn(styles.row, 'talk-admin-account-history-row')}
key={`${__typename}_${murmur3(created_at)}`}
>
<div
className={cn(
styles.item,
'talk-admin-account-history-row-date'
)}
>
{moment(new Date(created_at)).format('MMM DD, YYYY')}
</div>
<div className={cn(styles.item, styles.action, 'talk-admin-account-history-row-status')}>
<div
className={cn(
styles.item,
styles.action,
'talk-admin-account-history-row-status'
)}
>
{buildActionResponse(__typename, until, status)}
</div>
<div className={cn(styles.item, 'talk-admin-account-history-row-assigned-by')}>
<div
className={cn(
styles.item,
'talk-admin-account-history-row-assigned-by'
)}
>
{getModerationValue(user.id, assigned_by)}
</div>
</div>
))
}
)
)}
</div>
</div>
);
@@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button, Icon} from 'coral-ui';
import {Menu} from 'react-mdl';
import { Button, Icon } from 'coral-ui';
import { Menu } from 'react-mdl';
import cn from 'classnames';
import {findDOMNode} from 'react-dom';
import { findDOMNode } from 'react-dom';
import styles from './ActionsMenu.css';
import t from 'coral-framework/services/i18n';
@@ -13,36 +13,41 @@ let count = 0;
class ActionsMenu extends React.Component {
id = `actions-dropdown-${count++}`;
menu = null;
state = {open: false};
state = { open: false };
timeout = null;
componentWillUnmount() {
clearTimeout(this.timeout);
}
handleRef = (ref) => {
handleRef = ref => {
this.menu = ref ? findDOMNode(ref).parentNode : null;
}
};
syncOpenState = () => {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.setState({open: this.menu.className.indexOf('is-visible') >= 0});
this.setState({ open: this.menu.className.indexOf('is-visible') >= 0 });
}, 150);
};
render() {
const {className = '', buttonClassNames = '', label = ''} = this.props;
const { className = '', buttonClassNames = '', label = '' } = this.props;
return (
<div className={cn(styles.root, className)} onBlur={this.syncOpenState} >
<div className={cn(styles.root, className)} onBlur={this.syncOpenState}>
<Button
cStyle='actions'
className={cn(styles.button, {[styles.buttonOpen]: this.state.open}, buttonClassNames)}
cStyle="actions"
className={cn(
styles.button,
{ [styles.buttonOpen]: this.state.open },
buttonClassNames
)}
disabled={false}
id={this.id}
onClick={this.syncOpenState}
icon={this.props.icon}
raised>
raised
>
{label ? label : t('modqueue.actions')}
<Icon
name={this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
@@ -1,13 +1,18 @@
import React from 'react';
import cn from 'classnames';
import {MenuItem} from 'react-mdl';
import { MenuItem } from 'react-mdl';
import PropTypes from 'prop-types';
import styles from './ActionsMenu.css';
import camelCase from 'lodash/camelCase';
const ActionsMenuItem = (props) =>
<MenuItem className={cn(styles.menuItem, props.className, 'action-menu-item')} {...props} id={camelCase(props.children)}/>;
const ActionsMenuItem = props => (
<MenuItem
className={cn(styles.menuItem, props.className, 'action-menu-item')}
{...props}
id={camelCase(props.children)}
/>
);
ActionsMenuItem.propTypes = {
className: PropTypes.string,
children: PropTypes.string,
+80 -54
View File
@@ -2,102 +2,128 @@ import React from 'react';
import PropTypes from 'prop-types';
import Layout from 'coral-admin/src/components/ui/Layout';
import styles from './NotFound.css';
import {Button, TextField, Alert, Success} from 'coral-ui';
import { Button, TextField, Alert, Success } from 'coral-ui';
import Recaptcha from 'react-recaptcha';
import cn from 'classnames';
class AdminLogin extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {email: '', password: '', requestPassword: false};
this.state = { email: '', password: '', requestPassword: false };
}
handleSignIn = (e) => {
handleSignIn = e => {
e.preventDefault();
this.props.handleLogin(this.state.email, this.state.password);
}
};
onRecaptchaLoad = () => {
// do something?
}
};
onRecaptchaVerify = (recaptchaResponse) => {
this.props.handleLogin(this.state.email, this.state.password, recaptchaResponse);
}
onRecaptchaVerify = recaptchaResponse => {
this.props.handleLogin(
this.state.email,
this.state.password,
recaptchaResponse
);
};
handleRequestPassword = (e) => {
handleRequestPassword = e => {
e.preventDefault();
this.props.requestPasswordReset(this.state.email);
}
};
render () {
const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props;
render() {
const { errorMessage, loginMaxExceeded, recaptchaPublic } = this.props;
const signInForm = (
<form className="talk-admin-login-sign-in" onSubmit={this.handleSignIn}>
{errorMessage && <Alert>{errorMessage}</Alert>}
<TextField
id="email"
label='Email Address'
label="Email Address"
value={this.state.email}
onChange={(e) => this.setState({email: e.target.value})} />
onChange={e => this.setState({ email: e.target.value })}
/>
<TextField
id="password"
label='Password'
label="Password"
value={this.state.password}
onChange={(e) => this.setState({password: e.target.value})}
type='password' />
<div style={{height: 10}}></div>
onChange={e => this.setState({ password: e.target.value })}
type="password"
/>
<div style={{ height: 10 }} />
<Button
className="talk-admin-login-sign-in-button"
type='submit'
cStyle='black'
type="submit"
cStyle="black"
full
onClick={this.handleSignIn}>Sign In</Button>
onClick={this.handleSignIn}
>
Sign In
</Button>
<p className={styles.forgotPasswordCTA}>
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={(e) => {
e.preventDefault();
this.setState({requestPassword: true});
}}>Request a new one.</a>
Forgot your password?{' '}
<a
href="#"
className={styles.forgotPasswordLink}
onClick={e => {
e.preventDefault();
this.setState({ requestPassword: true });
}}
>
Request a new one.
</a>
</p>
{
loginMaxExceeded &&
{loginMaxExceeded && (
<Recaptcha
sitekey={recaptchaPublic}
render='explicit'
theme='dark'
render="explicit"
theme="dark"
onloadCallback={this.onRecaptchaLoad}
verifyCallback={this.onRecaptchaVerify} />
}
verifyCallback={this.onRecaptchaVerify}
/>
)}
</form>
);
const requestPasswordForm = (
this.props.passwordRequestSuccess
? <p className={styles.passwordRequestSuccess} onClick={() => {
const requestPasswordForm = this.props.passwordRequestSuccess ? (
<p
className={styles.passwordRequestSuccess}
onClick={() => {
location.href = location.href;
}}>
{this.props.passwordRequestSuccess} <a className={styles.signInLink} href="#">Sign in</a>
<Success />
</p>
: <form onSubmit={this.handleRequestPassword}>
<TextField
label='Email Address'
value={this.state.email}
onChange={(e) => this.setState({email: e.target.value})} />
<Button
type='submit'
cStyle='black'
full
onClick={this.handleRequestPassword}>Reset Password</Button>
</form>
}}
>
{this.props.passwordRequestSuccess}{' '}
<a className={styles.signInLink} href="#">
Sign in
</a>
<Success />
</p>
) : (
<form onSubmit={this.handleRequestPassword}>
<TextField
label="Email Address"
value={this.state.email}
onChange={e => this.setState({ email: e.target.value })}
/>
<Button
type="submit"
cStyle="black"
full
onClick={this.handleRequestPassword}
>
Reset Password
</Button>
</form>
);
return (
<Layout fixedDrawer restricted={true}>
<div className={cn(styles.loginLayout, 'talk-admin-login')}>
<h1 className={styles.loginHeader}>Team sign in</h1>
<p className={styles.loginCTA}>Sign in to interact with your community.</p>
{ this.state.requestPassword ? requestPasswordForm : signInForm }
<p className={styles.loginCTA}>
Sign in to interact with your community.
</p>
{this.state.requestPassword ? requestPasswordForm : signInForm}
</div>
</Layout>
);
+1 -1
View File
@@ -5,7 +5,7 @@ import 'material-design-lite';
import AppRouter from '../AppRouter';
export default class App extends React.Component {
render () {
render() {
return (
<div>
<ToastContainer />
@@ -3,15 +3,19 @@ import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './ApproveButton.css';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
const ApproveButton = ({active, minimal, onClick, className}) => {
const ApproveButton = ({ active, minimal, onClick, className }) => {
const text = active ? t('modqueue.approved') : t('modqueue.approve');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
className={cn(
styles.root,
{ [styles.minimal]: minimal, [styles.active]: active },
className
)}
onClick={onClick}
>
<Icon name={'done'} className={styles.icon} />
@@ -28,4 +32,3 @@ ApproveButton.propTypes = {
};
export default ApproveButton;
@@ -1,16 +1,15 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import { Dialog } from 'coral-ui';
import styles from './BanUserDialog.css';
import Button from 'coral-ui/components/Button';
import t from 'coral-framework/services/i18n';
const initialState = {step: 0, message: ''};
const initialState = { step: 0, message: '' };
class BanUserDialog extends React.Component {
state = initialState;
componentWillReceiveProps(next) {
@@ -18,53 +17,44 @@ class BanUserDialog extends React.Component {
this.setState(initialState);
}
}
handleMessageChange = (e) => {
const {value: message} = e;
this.setState({message});
}
handleMessageChange = e => {
const { value: message } = e;
this.setState({ message });
};
goToStep1 = () => {
this.setState({
step: 1,
message: t(
'bandialog.email_message_ban',
this.props.username,
),
message: t('bandialog.email_message_ban', this.props.username),
});
}
};
renderStep0() {
const {
onCancel,
username,
info,
} = this.props;
const { onCancel, username, info } = this.props;
return (
<section>
<h2 className={styles.header}>
{t('bandialog.ban_user')}
</h2>
<h2 className={styles.header}>{t('bandialog.ban_user')}</h2>
<h3 className={styles.subheader}>
{t('bandialog.are_you_sure', username)}
</h3>
<p className={styles.description}>
{info}
</p>
<p className={styles.description}>{info}</p>
<div className={styles.buttons}>
<Button
className={cn('talk-ban-user-dialog-button-cancel')}
cStyle="white"
onClick={onCancel}
raised >
raised
>
{t('bandialog.cancel')}
</Button>
<Button
<Button
className={cn('talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={this.goToStep1}
raised >
raised
>
{t('bandialog.yes_ban_user')}
</Button>
</div>
@@ -73,22 +63,19 @@ class BanUserDialog extends React.Component {
}
renderStep1() {
const {
onCancel,
onPerform,
} = this.props;
const {message} = this.state;
const { onCancel, onPerform } = this.props;
const { message } = this.state;
return (
<section>
<h2 className={styles.header}>
{t('bandialog.notify_ban_headline')}
</h2>
<h2 className={styles.header}>{t('bandialog.notify_ban_headline')}</h2>
<p className={styles.description}>
{t('bandialog.notify_ban_description')}
</p>
<fieldset>
<legend className={styles.legend}>{t('bandialog.write_a_message')}</legend>
<legend className={styles.legend}>
{t('bandialog.write_a_message')}
</legend>
<textarea
rows={5}
className={styles.messageInput}
@@ -101,14 +88,16 @@ class BanUserDialog extends React.Component {
className={cn('talk-ban-user-dialog-button-cancel')}
cStyle="white"
onClick={onCancel}
raised >
raised
>
{t('bandialog.cancel')}
</Button>
<Button
<Button
className={cn('talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={onPerform}
raised >
raised
>
{t('bandialog.send')}
</Button>
</div>
@@ -117,16 +106,19 @@ class BanUserDialog extends React.Component {
}
render() {
const {step} = this.state;
const {open, onCancel} = this.props;
const { step } = this.state;
const { open, onCancel } = this.props;
return (
<Dialog
className={cn(styles.dialog, 'talk-ban-user-dialog')}
id="banUserDialog"
open={open}
onCancel={onCancel}
title={t('bandialog.ban_user')} >
<span className={styles.close} onClick={onCancel}>×</span>
title={t('bandialog.ban_user')}
>
<span className={styles.close} onClick={onCancel}>
×
</span>
{step === 0 && this.renderStep0()}
{step === 1 && this.renderStep1()}
</Dialog>
@@ -1,15 +1,11 @@
import React from 'react';
import {Button} from 'coral-ui';
import { Button } from 'coral-ui';
import t from 'coral-framework/services/i18n';
import {withCopyToClipboard} from 'coral-framework/hocs';
import { withCopyToClipboard } from 'coral-framework/hocs';
class ButtonCopyToClipboard extends React.Component {
render () {
return (
<Button {...this.props} >
{t('common.copy')}
</Button>
);
render() {
return <Button {...this.props}>{t('common.copy')}</Button>;
}
}
@@ -1,10 +1,10 @@
import React from 'react';
import {murmur3} from 'murmurhash-js';
import {CSSTransitionGroup} from 'react-transition-group';
import { murmur3 } from 'murmurhash-js';
import { CSSTransitionGroup } from 'react-transition-group';
import styles from './CommentAnimatedEdit.css';
import PropTypes from 'prop-types';
const CommentBodyHighlighter = ({children, body}) => {
const CommentBodyHighlighter = ({ children, body }) => {
return (
<CSSTransitionGroup
component={'div'}
@@ -20,7 +20,9 @@ const CommentBodyHighlighter = ({children, body}) => {
transitionEnterTimeout={3600}
transitionLeaveTimeout={2800}
>
{React.cloneElement(React.Children.only(children), {key: murmur3(body)})}
{React.cloneElement(React.Children.only(children), {
key: murmur3(body),
})}
</CSSTransitionGroup>
);
};
@@ -1,5 +1,5 @@
import React from 'react';
import {matchLinks} from '../utils';
import { matchLinks } from '../utils';
import memoize from 'lodash/memoize';
function escapeRegExp(string) {
@@ -9,18 +9,18 @@ function escapeRegExp(string) {
// generate a regulare expression that catches the `phrases`.
function generateRegExp(phrases) {
const inner = phrases
.map((phrase) =>
phrase.split(/\s+/)
.map((word) => escapeRegExp(word))
.map(phrase =>
phrase
.split(/\s+/)
.map(word => escapeRegExp(word))
.join('[\\s"?!.]+')
).join('|');
)
.join('|');
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
try {
return new RegExp(pattern, 'iu');
}
catch (_err) {
} catch (_err) {
// IE does not support unicode support, so we'll create one without.
return new RegExp(pattern, 'i');
}
@@ -39,10 +39,9 @@ const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
const tokens = body.split(regexp);
return tokens.map((token, i) =>
i % 3 === 2
? <mark key={`${keyPrefix}_${i}`}>{token}</mark>
: token
return tokens.map(
(token, i) =>
i % 3 === 2 ? <mark key={`${keyPrefix}_${i}`}>{token}</mark> : token
);
}
@@ -53,34 +52,26 @@ function markLinks(body) {
const content = [];
let index = 0;
if (matches) {
matches
.forEach((match, i) => {
content.push(body.substring(index, match.index));
content.push(<mark key={i}>{match.text}</mark>);
index = match.lastIndex;
});
matches.forEach((match, i) => {
content.push(body.substring(index, match.index));
content.push(<mark key={i}>{match.text}</mark>);
index = match.lastIndex;
});
}
content.push(body.substring(index));
return content;
}
export default ({suspectWords, bannedWords, body, ...rest}) => {
export default ({ suspectWords, bannedWords, body, ...rest }) => {
// First highlight links.
const content = markLinks(body)
.map((element, index) => {
const content = markLinks(body).map((element, index) => {
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
// Keep highlighted links.
if (typeof element !== 'string') {
return element;
}
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
return (
<div {...rest}>
{content}
</div>
);
// Highlight suspect and banned phrase inside this part of text.
return markPhrases(element, suspectWords, bannedWords, index);
});
return <div {...rest}>{content}</div>;
};
@@ -1,4 +1,4 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from './CommentDetails.css';
import t from 'coral-framework/services/i18n';
@@ -7,26 +7,26 @@ import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
class CommentDetails extends Component {
state = {
showDetail: false
showDetail: false,
};
constructor () {
constructor() {
super();
this.state = {
showDetail: false
showDetail: false,
};
}
toggleDetail = () => {
this.setState((state) => ({
showDetail: !state.showDetail
this.setState(state => ({
showDetail: !state.showDetail,
}));
this.props.clearHeightCache && this.props.clearHeightCache();
}
};
render() {
const {data, root, comment, clearHeightCache} = this.props;
const {showDetail} = this.state;
const { data, root, comment, clearHeightCache } = this.props;
const { showDetail } = this.state;
const queryData = {
root,
comment,
@@ -49,12 +49,14 @@ class CommentDetails extends Component {
queryData={queryData}
more={showDetail}
/>
{showDetail && <Slot
fill="adminCommentMoreDetails"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>}
{showDetail && (
<Slot
fill="adminCommentMoreDetails"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>
)}
</div>
);
}
@@ -9,37 +9,67 @@ import styles from './CommentLabels.css';
const staffRoles = ['ADMIN', 'STAFF', 'MODERATOR'];
function isUserFlagged(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.user);
return actions.some(
action => action.__typename === 'FlagAction' && action.user
);
}
function getUserFlaggedType(actions) {
return actions
.some((action) =>
return actions.some(
action =>
action.__typename === 'FlagAction' &&
action.user &&
staffRoles.includes(action.user.role)
) ? 'Staff' : 'User';
)
? 'Staff'
: 'User';
}
function hasSuspectedWords(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'SUSPECT_WORD');
return actions.some(
action =>
action.__typename === 'FlagAction' && action.reason === 'SUSPECT_WORD'
);
}
function hasHistoryFlag(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'TRUST');
return actions.some(
action => action.__typename === 'FlagAction' && action.reason === 'TRUST'
);
}
const CommentLabels = ({comment, comment: {className, status, actions, hasParent}}) => {
const CommentLabels = ({
comment,
comment: { className, status, actions, hasParent },
}) => {
return (
<div className={cn(className, styles.root)}>
<div className={styles.coreLabels}>
{hasParent && <Label iconName="reply" className={styles.replyLabel}>reply</Label>}
{status === 'PREMOD' && <Label iconName="query_builder" className={styles.premodLabel}>Pre-Mod</Label>}
{isUserFlagged(actions) && <FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>}
{hasSuspectedWords(actions) && <FlagLabel iconName="sms_failed">Suspect</FlagLabel>}
{hasHistoryFlag(actions) && <FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>}
{hasParent && (
<Label iconName="reply" className={styles.replyLabel}>
reply
</Label>
)}
{status === 'PREMOD' && (
<Label iconName="query_builder" className={styles.premodLabel}>
Pre-Mod
</Label>
)}
{isUserFlagged(actions) && (
<FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>
)}
{hasSuspectedWords(actions) && (
<FlagLabel iconName="sms_failed">Suspect</FlagLabel>
)}
{hasHistoryFlag(actions) && (
<FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>
)}
</div>
<Slot className={styles.slot} fill="adminCommentLabels" queryData={{comment}} />
<Slot
className={styles.slot}
fill="adminCommentLabels"
queryData={{ comment }}
/>
</div>
);
};
@@ -4,7 +4,7 @@ import styles from './CountBadge.css';
import t from 'coral-framework/services/i18n';
const CountBadge = ({count}) => {
const CountBadge = ({ count }) => {
let number = count;
// shorten large counts to abbreviations
@@ -16,13 +16,11 @@ const CountBadge = ({count}) => {
number = `${(number / 1e3).toFixed(1)}${t('modqueue.thousand')}`;
}
return (
<span className={styles.count}>{number}</span>
);
return <span className={styles.count}>{number}</span>;
};
CountBadge.propTypes = {
count: PropTypes.number.isRequired
count: PropTypes.number.isRequired,
};
export default CountBadge;
+30 -30
View File
@@ -1,61 +1,61 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Navigation, Drawer} from 'react-mdl';
import {IndexLink, Link} from 'react-router';
import { Navigation, Drawer } from 'react-mdl';
import { IndexLink, Link } from 'react-router';
import styles from './Drawer.css';
import t from 'coral-framework/services/i18n';
import {can} from 'coral-framework/services/perms';
import { can } from 'coral-framework/services/perms';
import cn from 'classnames';
const CoralDrawer = ({handleLogout, auth = {}}) => (
const CoralDrawer = ({ handleLogout, auth = {} }) => (
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
{ auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
<div>
<Navigation className={styles.nav}>
{
can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
</IndexLink>
)
}
{can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}
>
{t('configure.moderate')}
</IndexLink>
)}
<Link
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}>
activeClassName={styles.active}
>
{t('configure.stories')}
</Link>
<Link
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}>
activeClassName={styles.active}
>
{t('configure.community')}
</Link>
{
can(auth.user, 'UPDATE_CONFIG') &&
(
<Link
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}>
{t('configure.configure')}
</Link>
)
}
{can(auth.user, 'UPDATE_CONFIG') && (
<Link
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}
>
{t('configure.configure')}
</Link>
)}
<a onClick={handleLogout}>Sign Out</a>
<span>{`v${process.env.VERSION}`}</span>
</Navigation>
</div> : null }
</div>
) : null}
</Drawer>
);
CoralDrawer.propTypes = {
handleLogout: PropTypes.func.isRequired,
restricted: PropTypes.bool, // hide app elements from a logged out user
auth: PropTypes.object
auth: PropTypes.object,
};
export default CoralDrawer;
@@ -1,15 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Card} from 'coral-ui';
import { Card } from 'coral-ui';
const EmptyCard = (props) => (
<Card style={{textAlign: 'center', maxWidth: 400, margin: '0 auto'}}>
const EmptyCard = props => (
<Card style={{ textAlign: 'center', maxWidth: 400, margin: '0 auto' }}>
{props.children}
</Card>
);
EmptyCard.propTypes = {
children: PropTypes.node.isRequired
children: PropTypes.node.isRequired,
};
export default EmptyCard;
@@ -1,11 +1,11 @@
import React from 'react';
import {Layout} from 'react-mdl';
import { Layout } from 'react-mdl';
import styles from './FullLoading.css';
import {CoralLogo} from 'coral-ui';
import { CoralLogo } from 'coral-ui';
export const FullLoading = () => (
<Layout fixedDrawer>
<div className={styles.layout} >
<div className={styles.layout}>
<h1>Loading</h1>
<CoralLogo />
</div>
@@ -1,7 +1,7 @@
import React from 'react';
import {matchLinks} from '../utils';
import { matchLinks } from '../utils';
export default ({text, children}) => {
export default ({ text, children }) => {
const hasLinks = !!matchLinks(text);
if (!hasLinks) {
@@ -1,24 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button} from 'coral-ui';
import { Button } from 'coral-ui';
import styles from './LoadMore.css';
import cn from 'classnames';
const LoadMore = ({loadMore, showLoadMore, className = '', ...rest}) =>
const LoadMore = ({ loadMore, showLoadMore, className = '', ...rest }) => (
<div {...rest} className={cn(className, styles.loadMoreContainer)}>
{
showLoadMore && <Button
className={styles.loadMore}
onClick={loadMore}>
{showLoadMore && (
<Button className={styles.loadMore} onClick={loadMore}>
Load More
</Button>
}
</div>;
)}
</div>
);
LoadMore.propTypes = {
className: PropTypes.string,
loadMore: PropTypes.func.isRequired,
showLoadMore: PropTypes.bool.isRequired
showLoadMore: PropTypes.bool.isRequired,
};
export default LoadMore;
+5 -3
View File
@@ -1,11 +1,13 @@
import React from 'react';
import {Button, Icon} from 'react-mdl';
import { Button, Icon } from 'react-mdl';
import styles from './Modal.css';
export default ({open, children, onClose}) => (
export default ({ open, children, onClose }) => (
<div className={`${styles.container} ${!open ? styles.hide : ''}`}>
<div className={styles.inner}>
<Button className={styles.close} onClick={onClose}><Icon name='close' /></Button>
<Button className={styles.close} onClick={onClose}>
<Icon name="close" />
</Button>
{children}
</div>
</div>
@@ -5,52 +5,75 @@ import styles from './ModerationKeysModal.css';
import t from 'coral-framework/services/i18n';
export default class ModerationKeysModal extends React.Component {
static propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
hideShortcutsNote: PropTypes.func.isRequired,
shortcutsNoteVisible: PropTypes.string.isRequired,
queueCount: PropTypes.number.isRequired
}
queueCount: PropTypes.number.isRequired,
};
buildShortcuts = () => {
return [
{
title: 'modqueue.navigation',
shortcuts: {
'j': 'modqueue.next_comment',
'k': 'modqueue.prev_comment',
j: 'modqueue.next_comment',
k: 'modqueue.prev_comment',
'ctrl+f': 'modqueue.toggle_search',
't': 'modqueue.next_queue',
t: 'modqueue.next_queue',
[`1...${this.props.queueCount}`]: 'modqueue.jump_to_queue',
's': 'modqueue.singleview',
'?': 'modqueue.thismenu'
}
s: 'modqueue.singleview',
'?': 'modqueue.thismenu',
},
},
{
title: 'modqueue.actions',
shortcuts: {
'd': 'modqueue.approve',
'f': 'modqueue.reject'
}
}
d: 'modqueue.approve',
f: 'modqueue.reject',
},
},
];
}
};
render () {
const {open, onClose, hideShortcutsNote, shortcutsNoteVisible} = this.props;
render() {
const {
open,
onClose,
hideShortcutsNote,
shortcutsNoteVisible,
} = this.props;
return (
<div>
<div className={styles.callToAction} style={{display: shortcutsNoteVisible === 'show' ? 'block' : 'none'}}>
<div onClick={hideShortcutsNote} className={styles.closeButton}>×</div>
<div
className={styles.callToAction}
style={{
display: shortcutsNoteVisible === 'show' ? 'block' : 'none',
}}
>
<div onClick={hideShortcutsNote} className={styles.closeButton}>
×
</div>
<p className={styles.ctaHeader}>{t('modqueue.mod_faster')}</p>
<p><strong>{t('modqueue.try_these')}:</strong></p>
<p>
<strong>{t('modqueue.try_these')}:</strong>
</p>
<ul>
<li><span>{t('modqueue.approve')}</span> <span className={styles.smallKey}>d</span></li>
<li><span>{t('modqueue.reject')}</span> <span className={styles.smallKey}>f</span></li>
<li>
<span>{t('modqueue.approve')}</span>{' '}
<span className={styles.smallKey}>d</span>
</li>
<li>
<span>{t('modqueue.reject')}</span>{' '}
<span className={styles.smallKey}>f</span>
</li>
</ul>
<p><span>{t('modqueue.view_more_shortcuts')}</span> <span className={styles.smallKey}>{t('modqueue.shift_key')}</span> + <span className={styles.smallKey}>/</span></p>
<p>
<span>{t('modqueue.view_more_shortcuts')}</span>{' '}
<span className={styles.smallKey}>{t('modqueue.shift_key')}</span> +{' '}
<span className={styles.smallKey}>/</span>
</p>
</div>
<Modal open={open} onClose={onClose}>
<h3>{t('modqueue.shortcuts')}</h3>
@@ -63,9 +86,11 @@ export default class ModerationKeysModal extends React.Component {
</tr>
</thead>
<tbody>
{Object.keys(shortcut.shortcuts).map((key) => (
{Object.keys(shortcut.shortcuts).map(key => (
<tr key={`${key}tr`}>
<td className={styles.shortcut}><span className={styles.key}>{key}</span></td>
<td className={styles.shortcut}>
<span className={styles.key}>{key}</span>
</td>
<td>{t(shortcut.shortcuts[key])}</td>
</tr>
))}
@@ -1,13 +1,16 @@
import React from 'react';
import {Layout} from 'react-mdl';
import { Layout } from 'react-mdl';
import styles from './NotFound.css';
export const NotFound = () => (
<Layout fixedDrawer>
<div className={styles.layout} >
<div className={styles.layout}>
<h1>Page Not Found</h1>
<p>The communicorn feels your pain.</p>
<img src="https://coralproject.net/images/communicorn.jpg" alt="Communicorn"/>
<img
src="https://coralproject.net/images/communicorn.jpg"
alt="Communicorn"
/>
</div>
</Layout>
);
@@ -3,15 +3,19 @@ import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './RejectButton.css';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
const RejectButton = ({active, minimal, onClick, className}) => {
const RejectButton = ({ active, minimal, onClick, className }) => {
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
className={cn(
styles.root,
{ [styles.minimal]: minimal, [styles.active]: active },
className
)}
onClick={onClick}
>
<Icon name={'close'} className={styles.icon} />
@@ -28,4 +32,3 @@ RejectButton.propTypes = {
};
export default RejectButton;
@@ -1,25 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import {RadioGroup, Radio} from 'react-mdl';
import { Dialog } from 'coral-ui';
import { RadioGroup, Radio } from 'react-mdl';
import styles from './SuspendUserDialog.css';
import cn from 'classnames';
import Button from 'coral-ui/components/Button';
import t, {timeago} from 'coral-framework/services/i18n';
import {dateAdd} from 'coral-framework/utils';
import t, { timeago } from 'coral-framework/services/i18n';
import { dateAdd } from 'coral-framework/utils';
const initialState = {step: 0, duration: '3'};
const initialState = { step: 0, duration: '3' };
function durationsToDate(hours) {
// Add 1 minute more to help `timeago.js` to display the correct duration.
return dateAdd(new Date(), 'minute', hours * 60 + 1);
}
class SuspendUserDialog extends React.Component {
state = initialState;
componentWillReceiveProps(next) {
@@ -28,13 +26,13 @@ class SuspendUserDialog extends React.Component {
}
}
handleDurationChange = (event) => {
this.setState({duration: event.target.value});
}
handleDurationChange = event => {
this.setState({ duration: event.target.value });
};
handleMessageChange = (event) => {
this.setState({message: event.target.value});
}
handleMessageChange = event => {
this.setState({ message: event.target.value });
};
goToStep1 = () => {
this.setState({
@@ -43,13 +41,12 @@ class SuspendUserDialog extends React.Component {
'suspenduser.email_message_suspend',
this.props.username,
this.props.organizationName,
timeago(durationsToDate(this.state.duration)),
timeago(durationsToDate(this.state.duration))
),
});
}
};
handlePerform = () => {
this.props.onPerform({
message: this.state.message,
@@ -59,36 +56,49 @@ class SuspendUserDialog extends React.Component {
};
renderStep0() {
const {onCancel, username} = this.props;
const {duration} = this.state;
const { onCancel, username } = this.props;
const { duration } = this.state;
return (
<section className="talk-admin-suspend-user-dialog-step-0">
<h1 className={styles.header}>
{t('suspenduser.title_suspend')}
</h1>
<h1 className={styles.header}>{t('suspenduser.title_suspend')}</h1>
<p className={styles.description}>
{t('suspenduser.description_suspend', username)}
</p>
<fieldset>
<legend className={styles.legend}>{t('suspenduser.select_duration')}</legend>
<legend className={styles.legend}>
{t('suspenduser.select_duration')}
</legend>
<RadioGroup
name='status filter'
name="status filter"
value={duration}
childContainer='div'
childContainer="div"
onChange={this.handleDurationChange}
className={styles.radioGroup}
>
<Radio value='1'>{t('suspenduser.one_hour')}</Radio>
<Radio value='3'>{t('suspenduser.hours', 3)}</Radio>
<Radio value='24'>{t('suspenduser.hours', 24)}</Radio>
<Radio value='168'>{t('suspenduser.days', 7)}</Radio>
<Radio value="1">{t('suspenduser.one_hour')}</Radio>
<Radio value="3">{t('suspenduser.hours', 3)}</Radio>
<Radio value="24">{t('suspenduser.hours', 24)}</Radio>
<Radio value="168">{t('suspenduser.days', 7)}</Radio>
</RadioGroup>
</fieldset>
<div className={styles.buttons}>
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
<Button
cStyle="white"
className={styles.cancel}
onClick={onCancel}
raised
>
{t('suspenduser.cancel')}
</Button>
<Button cStyle="black" className={cn(styles.perform, 'talk-admin-suspend-user-dialog-confirm')} onClick={this.goToStep1} raised>
<Button
cStyle="black"
className={cn(
styles.perform,
'talk-admin-suspend-user-dialog-confirm'
)}
onClick={this.goToStep1}
raised
>
{t('suspenduser.suspend_user')}
</Button>
</div>
@@ -97,31 +107,40 @@ class SuspendUserDialog extends React.Component {
}
renderStep1() {
const {message} = this.state;
const {onCancel, username} = this.props;
const { message } = this.state;
const { onCancel, username } = this.props;
return (
<section className="talk-admin-suspend-user-dialog-step-1">
<h1 className={styles.header}>
{t('suspenduser.title_notify')}
</h1>
<h1 className={styles.header}>{t('suspenduser.title_notify')}</h1>
<p className={styles.description}>
{t('suspenduser.description_notify', username)}
</p>
<fieldset>
<legend className={styles.legend}>{t('suspenduser.write_message')}</legend>
<legend className={styles.legend}>
{t('suspenduser.write_message')}
</legend>
<textarea
rows={5}
className={styles.messageInput}
value={message}
onChange={this.handleMessageChange} />
onChange={this.handleMessageChange}
/>
</fieldset>
<div className={styles.buttons}>
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
<Button
cStyle="white"
className={styles.cancel}
onClick={onCancel}
raised
>
{t('suspenduser.cancel')}
</Button>
<Button
cStyle="black"
className={cn(styles.perform, 'talk-admin-suspend-user-dialog-send')}
className={cn(
styles.perform,
'talk-admin-suspend-user-dialog-send'
)}
onClick={this.handlePerform}
disabled={this.state.message.length === 0}
raised
@@ -134,8 +153,8 @@ class SuspendUserDialog extends React.Component {
}
render() {
const {open, onCancel} = this.props;
const {step} = this.state;
const { open, onCancel } = this.props;
const { step } = this.state;
return (
<Dialog
className={cn(styles.dialog, 'talk-admin-suspend-user-dialog')}
@@ -143,7 +162,13 @@ class SuspendUserDialog extends React.Component {
open={open}
>
<div className={styles.close}>
<button aria-label="Close" onClick={onCancel} className={styles.closeButton}>×</button>
<button
aria-label="Close"
onClick={onCancel}
className={styles.closeButton}
>
×
</button>
</div>
{step === 0 && this.renderStep0()}
{step === 1 && this.renderStep1()}
@@ -5,8 +5,9 @@ import AutosizeInput from 'react-input-autosize';
import PropTypes from 'prop-types';
import cn from 'classnames';
const autosizingRenderInput = ({onChange, value, addTag: _, ...other}) =>
<AutosizeInput type='text' onChange={onChange} value={value} {...other} />;
const autosizingRenderInput = ({ onChange, value, addTag: _, ...other }) => (
<AutosizeInput type="text" onChange={onChange} value={value} {...other} />
);
autosizingRenderInput.propTypes = {
onChange: PropTypes.func,
@@ -14,17 +15,16 @@ autosizingRenderInput.propTypes = {
addTag: PropTypes.func,
};
const TagsInputComponent = ({className = '', ...props}) => {
const TagsInputComponent = ({ className = '', ...props }) => {
return (
<TagsInput
addOnBlur={true}
addOnPaste={true}
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
pasteSplit={data => data.split(',').map(d => d.trim())}
className={cn(styles.root, 'tags-input', className)}
focusedClassName={styles.rootFocus}
renderInput={autosizingRenderInput}
{...props}
tagProps={{
className: styles.tag,
classNameRemove: styles.tagRemove,
@@ -1,6 +1,6 @@
import './ToastContainer.css';
import {defaultProps} from 'recompose';
import {ToastContainer} from 'react-toastify';
import { defaultProps } from 'recompose';
import { ToastContainer } from 'react-toastify';
export default defaultProps({
autoClose: 5000,
+140 -76
View File
@@ -2,78 +2,87 @@ import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import capitalize from 'lodash/capitalize';
import {getErrorMessages} from 'coral-framework/utils';
import { getErrorMessages } from 'coral-framework/utils';
import styles from './UserDetail.css';
import AccountHistory from './AccountHistory';
import {Slot} from 'coral-framework/components';
import { Slot } from 'coral-framework/components';
import UserDetailCommentList from '../components/UserDetailCommentList';
import {getReliability, isSuspended, isBanned} from 'coral-framework/utils/user';
import {
getReliability,
isSuspended,
isBanned,
} from 'coral-framework/utils/user';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import ClickOutside from 'coral-framework/components/ClickOutside';
import {Icon, Drawer, Spinner, TabBar, Tab, TabContent, TabPane} from 'coral-ui';
import {
Icon,
Drawer,
Spinner,
TabBar,
Tab,
TabContent,
TabPane,
} from 'coral-ui';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import UserInfoTooltip from './UserInfoTooltip';
class UserDetail extends React.Component {
rejectThenReload = async (info) => {
rejectThenReload = async info => {
try {
await this.props.rejectComment(info);
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
acceptThenReload = async (info) => {
acceptThenReload = async info => {
try {
await this.props.acceptComment(info);
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
bulkAcceptThenReload = async () => {
try {
await this.props.bulkAccept();
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
bulkRejectThenReload = async () => {
try {
await this.props.bulkReject();
this.props.data.refetch();
} catch (err) {
console.error(err);
this.props.notify('error', getErrorMessages(err));
}
}
};
changeTab = (tab) => {
changeTab = tab => {
this.props.changeTab(tab);
}
};
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
showSuspenUserDialog = () =>
this.props.showSuspendUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
showBanUserDialog = () => this.props.showBanUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
showBanUserDialog = () =>
this.props.showBanUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
renderLoading() {
return (
@@ -86,7 +95,7 @@ class UserDetail extends React.Component {
}
getActionMenuLabel() {
const {root: {user}} = this.props;
const { root: { user } } = this.props;
if (isBanned(user)) {
return 'Banned';
@@ -101,12 +110,7 @@ class UserDetail extends React.Component {
const {
data,
root,
root: {
me,
user,
totalComments,
rejectedComments,
},
root: { me, user, totalComments, rejectedComments },
activeTab,
selectedCommentIds,
toggleSelect,
@@ -120,7 +124,7 @@ class UserDetail extends React.Component {
} = this.props;
// if totalComments is 0, you're dividing by zero
let rejectedPercent = (rejectedComments / totalComments) * 100;
let rejectedPercent = rejectedComments / totalComments * 100;
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
rejectedPercent = 0;
@@ -131,43 +135,67 @@ class UserDetail extends React.Component {
return (
<ClickOutside onClickOutside={modal ? null : hideUserDetail}>
<Drawer className="talk-admin-user-detail-drawer" onClose={hideUserDetail}>
<h3 className={cn(styles.username, 'talk-admin-user-detail-username')}>
<Drawer
className="talk-admin-user-detail-drawer"
onClose={hideUserDetail}
>
<h3
className={cn(styles.username, 'talk-admin-user-detail-username')}
>
{user.username}
</h3>
{user.id &&
{user.id && (
<ActionsMenu
icon="person"
className={cn(styles.actionsMenu, 'talk-admin-user-detail-actions-menu')}
buttonClassNames={cn({
[styles.actionsMenuSuspended]: suspended,
[styles.actionsMenuBanned]: banned,
}, 'talk-admin-user-detail-actions-button')}
label={this.getActionMenuLabel()}>
{suspended ? <ActionsMenuItem
onClick={() => unsuspendUser({id: user.id})}>
Remove Suspension
</ActionsMenuItem> : <ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showSuspenUserDialog}>
Suspend User
</ActionsMenuItem>}
{banned ? <ActionsMenuItem
onClick={() => unbanUser({id: user.id})}>
Remove Ban
</ActionsMenuItem> : <ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showBanUserDialog}>
Ban User
</ActionsMenuItem>}
className={cn(
styles.actionsMenu,
'talk-admin-user-detail-actions-menu'
)}
buttonClassNames={cn(
{
[styles.actionsMenuSuspended]: suspended,
[styles.actionsMenuBanned]: banned,
},
'talk-admin-user-detail-actions-button'
)}
label={this.getActionMenuLabel()}
>
{suspended ? (
<ActionsMenuItem onClick={() => unsuspendUser({ id: user.id })}>
Remove Suspension
</ActionsMenuItem>
) : (
<ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showSuspenUserDialog}
>
Suspend User
</ActionsMenuItem>
)}
{banned ? (
<ActionsMenuItem onClick={() => unbanUser({ id: user.id })}>
Remove Ban
</ActionsMenuItem>
) : (
<ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showBanUserDialog}
>
Ban User
</ActionsMenuItem>
)}
</ActionsMenu>
}
)}
{(banned || suspended) && <UserInfoTooltip user={user} banned={banned} suspended={suspended} />}
{(banned || suspended) && (
<UserInfoTooltip
user={user}
banned={banned}
suspended={suspended}
/>
)}
<div>
<ul className={styles.userDetailList}>
@@ -177,13 +205,18 @@ class UserDetail extends React.Component {
{new Date(user.created_at).toLocaleString()}
</li>
{user.profiles.map(({id}) =>
{user.profiles.map(({ id }) => (
<li key={id}>
<Icon name="email" />
<span className={styles.userDetailItem}>Email:</span>
{id} <ButtonCopyToClipboard className={styles.copyButton} icon="content_copy" copyText={id} />
{id}{' '}
<ButtonCopyToClipboard
className={styles.copyButton}
icon="content_copy"
copyText={id}
/>
</li>
)}
))}
</ul>
<ul className={styles.stats}>
@@ -199,7 +232,12 @@ class UserDetail extends React.Component {
</li>
<li className={styles.stat}>
<span className={styles.statItem}>Reports</span>
<span className={cn(styles.statReportResult, styles[getReliability(user.reliable.flagger)])}>
<span
className={cn(
styles.statReportResult,
styles[getReliability(user.reliable.flagger)]
)}
>
{capitalize(getReliability(user.reliable.flagger))}
</span>
</li>
@@ -209,7 +247,7 @@ class UserDetail extends React.Component {
<Slot
fill="userProfile"
data={this.props.data}
queryData={{root, user}}
queryData={{ root, user }}
/>
<hr />
@@ -218,28 +256,48 @@ class UserDetail extends React.Component {
onTabClick={this.changeTab}
activeTab={activeTab}
className={cn(styles.tabBar, 'talk-admin-user-detail-tab-bar')}
aria-controls='talk-admin-user-detail-content'
aria-controls="talk-admin-user-detail-content"
tabClassNames={{
button: styles.tabButton,
buttonActive: styles.tabButtonActive,
}} >
}}
>
<Tab
tabId={'all'}
className={cn(styles.tab, styles.button, 'talk-admin-user-detail-all-tab')} >
className={cn(
styles.tab,
styles.button,
'talk-admin-user-detail-all-tab'
)}
>
All
</Tab>
<Tab
tabId={'rejected'}
className={cn(styles.tab, 'talk-admin-user-detail-rejected-tab')} >
className={cn(styles.tab, 'talk-admin-user-detail-rejected-tab')}
>
Rejected
</Tab>
<Tab tabId={'history'} className={cn(styles.tab, styles.button, 'talk-admin-user-detail-history-tab')}>
<Tab
tabId={'history'}
className={cn(
styles.tab,
styles.button,
'talk-admin-user-detail-history-tab'
)}
>
Account History
</Tab>
</TabBar>
<TabContent activeTab={activeTab} className='talk-admin-user-detail-content'>
<TabPane tabId={'all'} className={'talk-admin-user-detail-all-tab-pane'}>
<TabContent
activeTab={activeTab}
className="talk-admin-user-detail-content"
>
<TabPane
tabId={'all'}
className={'talk-admin-user-detail-all-tab-pane'}
>
<UserDetailCommentList
user={user}
root={root}
@@ -255,7 +313,10 @@ class UserDetail extends React.Component {
bulkRejectThenReload={this.bulkRejectThenReload}
/>
</TabPane>
<TabPane tabId={'rejected'} className={'talk-admin-user-detail-rejected-tab-pane'}>
<TabPane
tabId={'rejected'}
className={'talk-admin-user-detail-rejected-tab-pane'}
>
<UserDetailCommentList
user={user}
root={root}
@@ -271,7 +332,10 @@ class UserDetail extends React.Component {
bulkRejectThenReload={this.bulkRejectThenReload}
/>
</TabPane>
<TabPane tabId={'history'} className={'talk-admin-user-detail-history-tab-pane'}>
<TabPane
tabId={'history'}
className={'talk-admin-user-detail-history-tab-pane'}
>
<AccountHistory user={user} />
</TabPane>
</TabContent>
@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Link} from 'react-router';
import { Link } from 'react-router';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import CommentDetails from './CommentDetails';
import styles from './UserDetailComment.css';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
@@ -13,19 +13,18 @@ import CommentLabels from '../containers/CommentLabels';
import ApproveButton from './ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import t, {timeago} from 'coral-framework/services/i18n';
import t, { timeago } from 'coral-framework/services/i18n';
class UserDetailComment extends React.Component {
approve = () =>
this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({ commentId: this.props.comment.id });
approve = () => (this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({commentId: this.props.comment.id})
);
reject = () => (this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({commentId: this.props.comment.id})
);
reject = () =>
this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({ commentId: this.props.comment.id });
render() {
const {
@@ -34,30 +33,35 @@ class UserDetailComment extends React.Component {
toggleSelect,
className,
data,
root: {settings: {wordlist: {banned, suspect}}},
root: { settings: { wordlist: { banned, suspect } } },
} = this.props;
return (
<li
tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
className={cn(className, styles.root, {
[styles.rootSelected]: selected,
})}
>
<div className={styles.container}>
<div className={styles.header}>
<input
className={styles.bulkSelectInput}
type='checkbox'
type="checkbox"
value={comment.id}
checked={selected}
onChange={(e) => toggleSelect(e.target.value, e.target.checked)} />
onChange={e => toggleSelect(e.target.value, e.target.checked)}
/>
<span className={styles.created}>
{timeago(comment.created_at)}
</span>
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
{comment.editing && comment.editing.edited ? (
<span>
&nbsp;<span className={styles.editedMarker}>
({t('comment.edited')})
</span>
</span>
) : null}
<div className={styles.labels}>
<CommentLabels comment={comment} />
@@ -65,7 +69,11 @@ class UserDetailComment extends React.Component {
</div>
<div className={styles.story}>
Story: {comment.asset.title}
{<Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
{
<Link to={`/admin/moderate/${comment.asset.id}`}>
{t('modqueue.moderate')}
</Link>
}
</div>
<CommentAnimatedEdit body={comment.body}>
<div className={styles.bodyContainer}>
@@ -74,8 +82,7 @@ class UserDetailComment extends React.Component {
suspectWords={suspect}
bannedWords={banned}
body={comment.body}
/>
{' '}
/>{' '}
<a
className={styles.external}
href={`${comment.asset.url}?commentId=${comment.id}`}
@@ -106,11 +113,7 @@ class UserDetailComment extends React.Component {
</div>
</CommentAnimatedEdit>
</div>
<CommentDetails
data={data}
root={root}
comment={comment}
/>
<CommentDetails data={data} root={root} comment={comment} />
</li>
);
}
@@ -142,7 +145,7 @@ UserDetailComment.propTypes = {
asset: PropTypes.shape({
title: PropTypes.string,
url: PropTypes.string,
id: PropTypes.string
id: PropTypes.string,
}),
}),
};
@@ -7,17 +7,11 @@ import Comment from '../containers/UserDetailComment';
import RejectButton from './RejectButton';
import ApproveButton from './ApproveButton';
const UserDetailCommentList = (props) => {
const UserDetailCommentList = props => {
const {
data,
root,
root: {
user,
comments: {
nodes,
hasNextPage
}
},
root: { user, comments: { nodes, hasNextPage } },
acceptComment,
rejectComment,
selectedCommentIds,
@@ -30,39 +24,49 @@ const UserDetailCommentList = (props) => {
} = props;
return (
<div className={cn(styles.commentList, 'talk-admin-user-detail-comment-list')}>
<div className={(selectedCommentIds.length > 0) ? cn(styles.bulkActionHeader, styles.selected) : styles.bulkActionHeader}>
<div
className={cn(styles.commentList, 'talk-admin-user-detail-comment-list')}
>
<div
className={
selectedCommentIds.length > 0
? cn(styles.bulkActionHeader, styles.selected)
: styles.bulkActionHeader
}
>
{selectedCommentIds.length > 0 && (
<div className={styles.bulkActionGroup}>
<ApproveButton
onClick={bulkAcceptThenReload}
minimal
/>
<RejectButton
onClick={bulkRejectThenReload}
minimal
/>
<span className={styles.selectedCommentsInfo}> {selectedCommentIds.length} comments selected</span>
<ApproveButton onClick={bulkAcceptThenReload} minimal />
<RejectButton onClick={bulkRejectThenReload} minimal />
<span className={styles.selectedCommentsInfo}>
{' '}
{selectedCommentIds.length} comments selected
</span>
</div>
)}
<div className={styles.toggleAll}>
<input
type='checkbox'
id='toogleAll'
checked={selectedCommentIds.length > 0 && selectedCommentIds.length === nodes.length}
onChange={(e) => {
toggleSelectAll(nodes.map((comment) => comment.id), e.target.checked);
}} />
<label htmlFor='toogleAll'>Select all</label>
type="checkbox"
id="toogleAll"
checked={
selectedCommentIds.length > 0 &&
selectedCommentIds.length === nodes.length
}
onChange={e => {
toggleSelectAll(
nodes.map(comment => comment.id),
e.target.checked
);
}}
/>
<label htmlFor="toogleAll">Select all</label>
</div>
</div>
{
nodes.map((comment) => {
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return <Comment
{nodes.map(comment => {
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return (
<Comment
key={comment.id}
user={user}
root={root}
@@ -73,9 +77,9 @@ const UserDetailCommentList = (props) => {
selected={selected}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
/>;
})
}
/>
);
})}
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
@@ -1,83 +1,165 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import {Icon} from 'coral-ui';
import { Icon } from 'coral-ui';
import styles from './UserInfoTooltip.css';
import ClickOutside from 'coral-framework/components/ClickOutside';
import moment from 'moment';
const initialState = {menuVisible: false};
const initialState = { menuVisible: false };
class UserInfoTooltip extends React.Component {
state = initialState;
toogleMenu = () => {
this.setState({menuVisible: !this.state.menuVisible});
}
this.setState({ menuVisible: !this.state.menuVisible });
};
hideMenu = () => {
this.setState({menuVisible: false});
}
this.setState({ menuVisible: false });
};
getLastHistoryItem = (user, status = 'banned') => {
const userHistory = user.state.status[status].history;
return userHistory[userHistory.length - 1];
}
};
render() {
const {menuVisible} = this.state;
const {user, banned, suspended} = this.props;
const { menuVisible } = this.state;
const { user, banned, suspended } = this.props;
return (
<ClickOutside onClickOutside={this.hideMenu}>
<div className={cn(styles.userInfo, 'talk-admin-user-info-tooltip')}>
<span onClick={this.toogleMenu} className={cn(styles.icon, 'talk-admin-user-info-tooltip-icon')}>
<span
onClick={this.toogleMenu}
className={cn(styles.icon, 'talk-admin-user-info-tooltip-icon')}
>
<Icon name="info_outline" />
</span>
{menuVisible && (
<div className={cn(styles.menu, 'talk-admin-user-info-tooltip-menu')}>
{
banned && (
<div className={cn(styles.description, 'talk-admin-user-info-tooltip-description-banned')}>
<ul className={cn(styles.descriptionList, 'talk-admin-user-info-tooltip-description-list')}>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>Banned On</strong>
<span>{moment(new Date(this.getLastHistoryItem(user, 'banned').created_at)).format('MMMM Do YYYY, h:mm:ss a')}</span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>By</strong>
<span>{this.getLastHistoryItem(user, 'banned').assigned_by.username}</span>
</li>
</ul>
</div>
)
}
<div
className={cn(styles.menu, 'talk-admin-user-info-tooltip-menu')}
>
{banned && (
<div
className={cn(
styles.description,
'talk-admin-user-info-tooltip-description-banned'
)}
>
<ul
className={cn(
styles.descriptionList,
'talk-admin-user-info-tooltip-description-list'
)}
>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Banned On</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(user, 'banned').created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>By</strong>
<span>
{
this.getLastHistoryItem(user, 'banned').assigned_by
.username
}
</span>
</li>
</ul>
</div>
)}
{
suspended && (
<div className={cn(styles.description, 'talk-admin-user-info-tooltip-description-suspended')}>
<ul className={cn(styles.descriptionList, 'talk-admin-user-info-tooltip-description-list')}>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>Suspension</strong>
<span></span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>By</strong>
<span>{this.getLastHistoryItem(user, 'suspension').assigned_by.username}</span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>Start</strong>
<span>{moment(new Date(this.getLastHistoryItem(user, 'suspension').created_at)).format('MMMM Do YYYY, h:mm:ss a')}</span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>End</strong>
<span>{moment(new Date(this.getLastHistoryItem(user, 'suspension').until)).format('MMMM Do YYYY, h:mm:ss a')}</span>
</li>
</ul>
</div>
)
}
{suspended && (
<div
className={cn(
styles.description,
'talk-admin-user-info-tooltip-description-suspended'
)}
>
<ul
className={cn(
styles.descriptionList,
'talk-admin-user-info-tooltip-description-list'
)}
>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Suspension</strong>
<span />
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>By</strong>
<span>
{
this.getLastHistoryItem(user, 'suspension')
.assigned_by.username
}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>Start</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(
user,
'suspension'
).created_at
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
<li
className={cn(
styles.descriptionItem,
'talk-admin-user-info-tooltip-description-item'
)}
>
<strong className={styles.strongItem}>End</strong>
<span>
{moment(
new Date(
this.getLastHistoryItem(user, 'suspension').until
)
).format('MMMM Do YYYY, h:mm:ss a')}
</span>
</li>
</ul>
</div>
)}
</div>
)}
</div>
+76 -62
View File
@@ -1,98 +1,112 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
import {Link, IndexLink} from 'react-router';
import { Navigation, Header, IconButton, MenuItem, Menu } from 'react-mdl';
import { Link, IndexLink } from 'react-router';
import styles from './Header.css';
import t from 'coral-framework/services/i18n';
import {Logo} from './Logo';
import {can} from 'coral-framework/services/perms';
import { Logo } from './Logo';
import { can } from 'coral-framework/services/perms';
import Indicator from './Indicator';
const CoralHeader = ({
handleLogout,
showShortcuts = () => {},
auth,
root
root,
}) => {
return (
<div className={styles.headerWrapper}>
<Header className={styles.header}>
<Logo className={styles.logo} />
<div>
{
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
<Navigation className={styles.nav}>
{
can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
id='moderateNav'
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
{(root.premodCount !== 0 || root.reportedCount !== 0) && <Indicator />}
</IndexLink>
)
}
<Link
id='storiesNav'
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}>
{t('configure.stories')}
</Link>
<div>
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
<Navigation className={styles.nav}>
{can(auth.user, 'MODERATE_COMMENTS') && (
<IndexLink
id="moderateNav"
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}
>
{t('configure.moderate')}
{(root.premodCount !== 0 || root.reportedCount !== 0) && (
<Indicator />
)}
</IndexLink>
)}
<Link
id="storiesNav"
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}
>
{t('configure.stories')}
</Link>
<Link
id='communityNav'
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}>
{t('configure.community')}
{root.flaggedUsernamesCount !== 0 && <Indicator />}
</Link>
<Link
id="communityNav"
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}
>
{t('configure.community')}
{root.flaggedUsernamesCount !== 0 && <Indicator />}
</Link>
{
can(auth.user, 'UPDATE_CONFIG') && (
<Link
id='configureNav'
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}>
{t('configure.configure')}
</Link>
)
}
</Navigation>
:
null
}
{can(auth.user, 'UPDATE_CONFIG') && (
<Link
id="configureNav"
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}
>
{t('configure.configure')}
</Link>
)}
</Navigation>
) : null}
<div className={styles.rightPanel}>
<ul>
<li className={cn(styles.settings, 'talk-admin-header-settings')}>
<div>
<IconButton name="settings" id="menu-settings" className="talk-admin-header-settings-button"/>
<IconButton
name="settings"
id="menu-settings"
className="talk-admin-header-settings-button"
/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
<MenuItem onClick={() => showShortcuts(true)}>
{t('configure.shortcuts')}
</MenuItem>
<MenuItem>
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
View latest version
<a
href="https://github.com/coralproject/talk/releases"
target="_blank"
rel="noopener noreferrer"
>
View latest version
</a>
</MenuItem>
<MenuItem>
<a href="https://support.coralproject.net" target="_blank" rel="noopener noreferrer">
Report a bug or give feedback
<a
href="https://support.coralproject.net"
target="_blank"
rel="noopener noreferrer"
>
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem onClick={handleLogout} className="talk-admin-header-sign-out">
<MenuItem
onClick={handleLogout}
className="talk-admin-header-sign-out"
>
{t('configure.sign_out')}
</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
<li>{`v${process.env.VERSION}`}</li>
</ul>
</div>
</div>
@@ -105,7 +119,7 @@ CoralHeader.propTypes = {
auth: PropTypes.object,
showShortcuts: PropTypes.func,
handleLogout: PropTypes.func.isRequired,
root: PropTypes.object.isRequired
root: PropTypes.object.isRequired,
};
export default CoralHeader;
@@ -1,7 +1,6 @@
import React from 'react';
import styles from './Indicator.css';
const Indicator = () =>
<span className={styles.indicator}></span>;
const Indicator = () => <span className={styles.indicator} />;
export default Indicator;
+4 -10
View File
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Layout as LayoutMDL} from 'react-mdl';
import { Layout as LayoutMDL } from 'react-mdl';
import Header from '../../containers/Header';
import Drawer from '../Drawer';
import styles from './Layout.css';
@@ -18,14 +18,8 @@ const Layout = ({
showShortcuts={toggleShortcutModal}
auth={auth}
/>
<Drawer
handleLogout={handleLogout}
restricted={restricted}
auth={auth}
/>
<div className={styles.layout}>
{children}
</div>
<Drawer handleLogout={handleLogout} restricted={restricted} auth={auth} />
<div className={styles.layout}>{children}</div>
</LayoutMDL>
);
@@ -34,7 +28,7 @@ Layout.propTypes = {
auth: PropTypes.object,
handleLogout: PropTypes.func,
toggleShortcutModal: PropTypes.func,
restricted: PropTypes.bool // hide elements from a user that's logged out
restricted: PropTypes.bool, // hide elements from a user that's logged out
};
export default Layout;
+3 -3
View File
@@ -1,9 +1,9 @@
import React from 'react';
import styles from './Logo.css';
import {CoralLogo} from 'coral-ui';
import { CoralLogo } from 'coral-ui';
import PropTypes from 'prop-types';
export const Logo = ({className = ''}) => (
export const Logo = ({ className = '' }) => (
<div className={`${styles.logo} ${className}`}>
<h1>
<CoralLogo className={styles.base} />
@@ -13,5 +13,5 @@ export const Logo = ({className = ''}) => (
);
Logo.propTypes = {
className: PropTypes.string
className: PropTypes.string,
};
+2 -1
View File
@@ -1 +1,2 @@
export const ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS = 'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS';
export const ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS =
'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS';
+4 -2
View File
@@ -2,8 +2,10 @@ export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
export const SHOW_SUSPENDUSER_DIALOG = 'SHOW_SUSPENDUSER_DIALOG';
export const HIDE_SUSPENDUSER_DIALOG = 'HIDE_SUSPENDUSER_DIALOG';
export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST = 'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST';
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST =
'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST';
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS =
'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS';
export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED';
export const COMMENT_STREAM_FETCH_SUCCESS = 'COMMENT_STREAM_FETCH_SUCCESS';
+2 -1
View File
@@ -10,7 +10,8 @@ export const INSTALL_SUCCESS = 'INSTALL_SUCCESS';
export const INSTALL_FAILURE = 'INSTALL_FAILURE';
export const UPDATE_FORMDATA_USER = 'UPDATE_FORMDATA_USER';
export const UPDATE_FORMDATA_SETTINGS = 'UPDATE_FORMDATA_SETTINGS';
export const UPDATE_PERMITTED_DOMAINS_SETTINGS = 'UPDATE_PERMITTED_DOMAINS_SETTINGS';
export const UPDATE_PERMITTED_DOMAINS_SETTINGS =
'UPDATE_PERMITTED_DOMAINS_SETTINGS';
export const CHECK_INSTALL_REQUEST = 'CHECK_INSTALL_REQUEST';
export const CHECK_INSTALL_SUCCESS = 'CHECK_INSTALL_SUCCESS';
@@ -1,30 +1,39 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import BanUserDialog from '../components/BanUserDialog';
import {hideBanUserDialog} from '../actions/banUserDialog';
import {withBanUser, withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {compose} from 'react-apollo';
import { hideBanUserDialog } from '../actions/banUserDialog';
import {
withBanUser,
withSetCommentStatus,
} from 'coral-framework/graphql/mutations';
import { compose } from 'react-apollo';
import t from 'coral-framework/services/i18n';
import {getErrorMessages} from 'coral-framework/utils';
import {notify} from 'coral-framework/actions/notification';
import { getErrorMessages } from 'coral-framework/utils';
import { notify } from 'coral-framework/actions/notification';
class BanUserDialogContainer extends Component {
banUser = async () => {
const {userId, commentId, commentStatus, banUser, setCommentStatus, hideBanUserDialog, notify} = this.props;
const {
userId,
commentId,
commentStatus,
banUser,
setCommentStatus,
hideBanUserDialog,
notify,
} = this.props;
try {
await banUser({id: userId, message: ''});
await banUser({ id: userId, message: '' });
hideBanUserDialog();
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({commentId, status: 'REJECTED'});
await setCommentStatus({ commentId, status: 'REJECTED' });
}
}
catch(err) {
} catch (err) {
notify('error', getErrorMessages(err));
}
}
};
getInfo() {
let note = t('bandialog.note_ban_user');
@@ -55,7 +64,9 @@ BanUserDialogContainer.propTypes = {
commentStatus: PropTypes.string,
};
const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
const mapStateToProps = ({
banUserDialog: { open, userId, username, commentId, commentStatus },
}) => ({
open,
userId,
username,
@@ -63,18 +74,18 @@ const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, com
commentStatus,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
hideBanUserDialog,
notify,
}, dispatch),
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
hideBanUserDialog,
notify,
},
dispatch
),
});
export default compose(
withBanUser,
withSetCommentStatus,
connect(
mapStateToProps,
mapDispatchToProps,
),
connect(mapStateToProps, mapDispatchToProps)
)(BanUserDialogContainer);
@@ -1,12 +1,9 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import CommentDetails from '../components/CommentDetails';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import withFragments from 'coral-framework/hocs/withFragments';
const slots = [
'adminCommentDetailArea',
'adminCommentMoreDetails',
];
const slots = ['adminCommentDetailArea', 'adminCommentMoreDetails'];
export default withFragments({
root: gql`
@@ -20,5 +17,5 @@ export default withFragments({
__typename
${getSlotFragmentSpreads(slots, 'comment')}
}
`
`,
})(CommentDetails);
@@ -1,11 +1,9 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import CommentLabels from '../components/CommentLabels';
import withFragments from 'coral-framework/hocs/withFragments';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
const slots = [
'adminCommentLabels',
];
const slots = ['adminCommentLabels'];
export default withFragments({
root: gql`
@@ -30,5 +28,5 @@ export default withFragments({
}
${getSlotFragmentSpreads(slots, 'comment')}
}
`
`,
})(CommentLabels);
+21 -23
View File
@@ -1,28 +1,26 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import Header from '../components/ui/Header';
export default withQuery(gql`
query TalkAdmin_Header {
__typename
premodCount: commentCount(query: {
statuses: [PREMOD]
})
reportedCount: commentCount(query: {
statuses: [NONE, PREMOD, SYSTEM_WITHHELD],
action_type: FLAG
})
flaggedUsernamesCount: userCount(query: {
action_type: FLAG,
state: {
status: {
username: [SET, CHANGED]
export default withQuery(
gql`
query TalkAdmin_Header {
__typename
premodCount: commentCount(query: { statuses: [PREMOD] })
reportedCount: commentCount(
query: { statuses: [NONE, PREMOD, SYSTEM_WITHHELD], action_type: FLAG }
)
flaggedUsernamesCount: userCount(
query: {
action_type: FLAG
state: { status: { username: [SET, CHANGED] } }
}
}
})
}
`, {
options: {
pollInterval: 10000
)
}
})(Header);
`,
{
options: {
pollInterval: 10000,
},
}
)(Header);
+35 -24
View File
@@ -1,35 +1,39 @@
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Layout from '../components/ui/Layout';
import {fetchConfig} from '../actions/config';
import { fetchConfig } from '../actions/config';
import AdminLogin from '../components/AdminLogin';
import {FullLoading} from '../components/FullLoading';
import { FullLoading } from '../components/FullLoading';
import BanUserDialog from './BanUserDialog';
import SuspendUserDialog from './SuspendUserDialog';
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
import {checkLogin, handleLogin, requestPasswordReset, logout} from '../actions/auth';
import {can} from 'coral-framework/services/perms';
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
import {
checkLogin,
handleLogin,
requestPasswordReset,
logout,
} from '../actions/auth';
import { can } from 'coral-framework/services/perms';
import UserDetail from 'coral-admin/src/containers/UserDetail';
import PropTypes from 'prop-types';
class LayoutContainer extends React.Component {
componentWillMount() {
const {checkLogin, fetchConfig} = this.props;
const { checkLogin, fetchConfig } = this.props;
checkLogin();
fetchConfig();
}
render() {
const {
user,
loggedIn,
loadingUser,
loginError,
loginMaxExceeded,
passwordRequestSuccess
passwordRequestSuccess,
} = this.props.auth;
const {
@@ -61,7 +65,8 @@ class LayoutContainer extends React.Component {
<Layout
handleLogout={logout}
toggleShortcutModal={toggleShortcutModal}
auth={this.props.auth} >
auth={this.props.auth}
>
<BanUserDialog />
<SuspendUserDialog />
<UserDetail />
@@ -71,7 +76,10 @@ class LayoutContainer extends React.Component {
} else if (loggedIn) {
return (
<Layout {...this.props}>
<p>This page is for team use only. Please contact an administrator if you want to join this team.</p>
<p>
This page is for team use only. Please contact an administrator if
you want to join this team.
</p>
</Layout>
);
}
@@ -89,22 +97,25 @@ LayoutContainer.propTypes = {
toggleShortcutModal: PropTypes.func,
TALK_RECAPTCHA_PUBLIC: PropTypes.string,
checkLogin: PropTypes.func,
fetchConfig: PropTypes.func
fetchConfig: PropTypes.func,
};
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
auth: state.auth,
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
checkLogin,
fetchConfig,
handleLogin,
requestPasswordReset,
toggleShortcutModal,
logout
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
checkLogin,
fetchConfig,
handleLogin,
requestPasswordReset,
toggleShortcutModal,
logout,
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
@@ -1,33 +1,43 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import SuspendUserDialog from '../components/SuspendUserDialog';
import {hideSuspendUserDialog} from '../actions/suspendUserDialog';
import {withSetCommentStatus, withSuspendUser} from 'coral-framework/graphql/mutations';
import {compose, gql} from 'react-apollo';
import t, {timeago} from 'coral-framework/services/i18n';
import { hideSuspendUserDialog } from '../actions/suspendUserDialog';
import {
withSetCommentStatus,
withSuspendUser,
} from 'coral-framework/graphql/mutations';
import { compose, gql } from 'react-apollo';
import t, { timeago } from 'coral-framework/services/i18n';
import withQuery from 'coral-framework/hocs/withQuery';
import {getErrorMessages} from 'coral-framework/utils';
import { getErrorMessages } from 'coral-framework/utils';
import get from 'lodash/get';
import {notify} from 'coral-framework/actions/notification';
import { notify } from 'coral-framework/actions/notification';
class SuspendUserDialogContainer extends Component {
suspendUser = async ({message, until}) => {
const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser, notify} = this.props;
suspendUser = async ({ message, until }) => {
const {
userId,
username,
commentStatus,
commentId,
hideSuspendUserDialog,
setCommentStatus,
suspendUser,
notify,
} = this.props;
hideSuspendUserDialog();
try {
await suspendUser({id: userId, message, until});
await suspendUser({ id: userId, message, until });
notify(
'success',
t('suspenduser.notify_suspend_until', username, timeago(until)),
t('suspenduser.notify_suspend_until', username, timeago(until))
);
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({commentId, status: 'REJECTED'});
await setCommentStatus({ commentId, status: 'REJECTED' });
}
}
catch(err) {
} catch (err) {
notify('error', getErrorMessages(err));
}
};
@@ -53,14 +63,16 @@ SuspendUserDialogContainer.propTypes = {
const withOrganizationName = withQuery(gql`
query CoralAdmin_SuspendUserDialog {
__typename
settings {
organizationName
__typename
settings {
organizationName
}
}
`);
const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
const mapStateToProps = ({
suspendUserDialog: { open, userId, username, commentId, commentStatus },
}) => ({
open,
userId,
username,
@@ -68,19 +80,19 @@ const mapStateToProps = ({suspendUserDialog: {open, userId, username, commentId,
commentStatus,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
hideSuspendUserDialog,
notify,
}, dispatch),
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
hideSuspendUserDialog,
notify,
},
dispatch
),
});
export default compose(
connect(
mapStateToProps,
mapDispatchToProps,
),
connect(mapStateToProps, mapDispatchToProps),
withSuspendUser,
withSetCommentStatus,
withOrganizationName,
withOrganizationName
)(SuspendUserDialogContainer);
+105 -78
View File
@@ -1,25 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import {compose, gql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { compose, gql } from 'react-apollo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import UserDetail from '../components/UserDetail';
import withQuery from 'coral-framework/hocs/withQuery';
import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
import {
getDefinitionName,
getSlotFragmentSpreads,
} from 'coral-framework/utils';
import {
viewUserDetail,
hideUserDetail,
changeTab,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
toggleSelectAllCommentInUserDetail
toggleSelectAllCommentInUserDetail,
} from 'coral-admin/src/actions/userDetail';
import {withSetCommentStatus, withUnbanUser, withUnsuspendUser} from 'coral-framework/graphql/mutations';
import {
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser,
} from 'coral-framework/graphql/mutations';
import UserDetailComment from './UserDetailComment';
import update from 'immutability-helper';
import {notify} from 'coral-framework/actions/notification';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import { notify } from 'coral-framework/actions/notification';
import { showBanUserDialog } from 'actions/banUserDialog';
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
const commentConnectionFragment = gql`
fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection {
@@ -33,38 +40,36 @@ const commentConnectionFragment = gql`
${UserDetailComment.fragments.comment}
`;
const slots = [
'userProfile',
];
const slots = ['userProfile'];
class UserDetailContainer extends React.Component {
isLoadingMore = false;
// status can be 'ACCEPTED' or 'REJECTED'
bulkSetCommentStatus = async (status) => {
const changes = this.props.selectedCommentIds.map((commentId) => {
return this.props.setCommentStatus({commentId, status});
bulkSetCommentStatus = async status => {
const changes = this.props.selectedCommentIds.map(commentId => {
return this.props.setCommentStatus({ commentId, status });
});
await Promise.all(changes);
this.props.clearUserDetailSelections(); // un-select everything
}
};
bulkReject = () => {
return this.bulkSetCommentStatus('REJECTED');
}
};
bulkAccept = () => {
return this.bulkSetCommentStatus('ACCEPTED');
}
};
acceptComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'ACCEPTED'});
}
acceptComment = ({ commentId }) => {
return this.props.setCommentStatus({ commentId, status: 'ACCEPTED' });
};
rejectComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
}
rejectComment = ({ commentId }) => {
return this.props.setCommentStatus({ commentId, status: 'REJECTED' });
};
loadMore = () => {
if (this.isLoadingMore) {
@@ -78,24 +83,25 @@ class UserDetailContainer extends React.Component {
author_id: this.props.data.variables.author_id,
statuses: this.props.data.variables.statuses,
};
this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables,
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
return update(prev, {
comments: {
nodes: {$push: comments.nodes},
hasNextPage: {$set: comments.hasNextPage},
startCursor: {$set: comments.startCursor},
endCursor: {$set: comments.endCursor},
},
});
}
})
this.props.data
.fetchMore({
query: LOAD_MORE_QUERY,
variables,
updateQuery: (prev, { fetchMoreResult: { comments } }) => {
return update(prev, {
comments: {
nodes: { $push: comments.nodes },
hasNextPage: { $set: comments.hasNextPage },
startCursor: { $set: comments.startCursor },
endCursor: { $set: comments.endCursor },
},
});
},
})
.then(() => {
this.isLoadingMore = false;
})
.catch((err) => {
.catch(err => {
this.isLoadingMore = false;
throw err;
});
@@ -107,24 +113,27 @@ class UserDetailContainer extends React.Component {
}
}
render () {
render() {
if (!this.props.userId) {
return null;
}
const loading = this.props.data.loading;
return <UserDetail
bulkReject={this.bulkReject}
bulkAccept={this.bulkAccept}
changeTab={this.props.changeTab}
toggleSelect={this.props.toggleSelectCommentInUserDetail}
toggleSelectAll={this.props.toggleSelectAllCommentInUserDetail}
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
loading={loading}
loadMore={this.loadMore}
{...this.props} />;
return (
<UserDetail
bulkReject={this.bulkReject}
bulkAccept={this.bulkAccept}
changeTab={this.props.changeTab}
toggleSelect={this.props.toggleSelectCommentInUserDetail}
toggleSelectAll={this.props.toggleSelectAllCommentInUserDetail}
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
loading={loading}
loadMore={this.loadMore}
{...this.props}
/>
);
}
}
@@ -142,15 +151,28 @@ UserDetailContainer.propTypes = {
};
const LOAD_MORE_QUERY = gql`
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) {
query CoralAdmin_Moderation_LoadMore(
$limit: Int = 10
$cursor: Cursor
$author_id: ID!
$statuses: [COMMENT_STATUS!]
) {
comments(
query: {
limit: $limit
cursor: $cursor
author_id: $author_id
statuses: $statuses
}
) {
...CoralAdmin_UserDetail_CommentConnection
}
}
${commentConnectionFragment}
`;
export const withUserDetailQuery = withQuery(gql`
export const withUserDetailQuery = withQuery(
gql`
query CoralAdmin_UserDetail($author_id: ID!, $statuses: [COMMENT_STATUS!]) {
user(id: $author_id) {
id
@@ -218,36 +240,41 @@ export const withUserDetailQuery = withQuery(gql`
}
${UserDetailComment.fragments.root}
${commentConnectionFragment}
`, {
options: ({userId, statuses}) => {
return {
variables: {author_id: userId, statuses},
fetchPolicy: 'network-only',
};
},
skip: (ownProps) => !ownProps.userId,
});
`,
{
options: ({ userId, statuses }) => {
return {
variables: { author_id: userId, statuses },
fetchPolicy: 'network-only',
};
},
skip: ownProps => !ownProps.userId,
}
);
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
userId: state.userDetail.userId,
selectedCommentIds: state.userDetail.selectedCommentIds,
statuses: state.userDetail.statuses,
activeTab: state.userDetail.activeTab,
modal: state.ui.modal
modal: state.ui.modal,
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
showBanUserDialog,
showSuspendUserDialog,
changeTab,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
viewUserDetail,
hideUserDetail,
toggleSelectAllCommentInUserDetail,
notify
}, dispatch)
const mapDispatchToProps = dispatch => ({
...bindActionCreators(
{
showBanUserDialog,
showSuspendUserDialog,
changeTab,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
viewUserDetail,
hideUserDetail,
toggleSelectAllCommentInUserDetail,
notify,
},
dispatch
),
});
export default compose(
@@ -255,5 +282,5 @@ export default compose(
withUserDetailQuery,
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser,
withUnsuspendUser
)(UserDetailContainer);
@@ -1,7 +1,7 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import UserDetailComment from '../components/UserDetailComment';
import withFragments from 'coral-framework/hocs/withFragments';
import {getDefinitionName} from 'coral-framework/utils';
import { getDefinitionName } from 'coral-framework/utils';
import CommentLabels from './CommentLabels';
import CommentDetails from './CommentDetails';
@@ -43,5 +43,5 @@ export default withFragments({
}
${CommentLabels.fragments.comment}
${CommentDetails.fragments.comment}
`
`,
})(UserDetailComment);
+136 -97
View File
@@ -1,6 +1,6 @@
import update from 'immutability-helper';
import {mapLeaves} from 'coral-framework/utils';
import {gql} from 'react-apollo';
import { mapLeaves } from 'coral-framework/utils';
import { gql } from 'react-apollo';
const userStatusFragment = gql`
fragment Talk_UpdateUserStatus on User {
@@ -14,150 +14,189 @@ const userStatusFragment = gql`
}
}
}
}`;
}
`;
const userRoleFragment = gql`
fragment Talk_UpdateUserRole on User {
role
}`;
}
`;
export default {
mutations: {
SetUserRole: ({variables: {id, role}}) => ({
update: (proxy) => {
SetUserRole: ({ variables: { id, role } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userRoleFragment, id: fragmentId});
const data = proxy.readFragment({
fragment: userRoleFragment,
id: fragmentId,
});
const updated = update(data, {
role: {
$set: role
}
$set: role,
},
});
proxy.writeFragment({fragment: userRoleFragment, id: fragmentId, data: updated});
proxy.writeFragment({
fragment: userRoleFragment,
id: fragmentId,
data: updated,
});
},
}),
SuspendUser: ({variables: {input: {id, until}}}) => ({
update: (proxy) => {
SuspendUser: ({ variables: { input: { id, until } } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userStatusFragment, id: fragmentId});
const updated = update(data, {
state : {
status: {
suspension: {
until: {$set: until}
}
}
}
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
proxy.writeFragment({fragment: userStatusFragment, id: fragmentId, data: updated});
}
}),
UnsuspendUser: ({variables: {input: {id}}}) => ({
update: (proxy) => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userStatusFragment, id: fragmentId});
const updated = update(data, {
state : {
state: {
status: {
suspension: {
until: {$set: null}
}
}
}
until: { $set: until },
},
},
},
});
proxy.writeFragment({fragment: userStatusFragment, id: fragmentId, data: updated});
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
BanUser: ({variables: {input: {id}}}) => ({
update: (proxy) => {
UnsuspendUser: ({ variables: { input: { id } } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userStatusFragment, id: fragmentId});
const updated = update(data, {
state : {
status: {
banned: {
status: {$set: true}
}
}
}
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
proxy.writeFragment({fragment: userStatusFragment, id: fragmentId, data: updated});
}
}),
UnbanUser: ({variables: {input: {id}}}) => ({
update: (proxy) => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userStatusFragment, id: fragmentId});
const updated = update(data, {
state : {
state: {
status: {
banned: {
status: {$set: false}
}
}
}
suspension: {
until: { $set: null },
},
},
},
});
proxy.writeFragment({fragment: userStatusFragment, id: fragmentId, data: updated});
}
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
SetUserBanStatus: ({variables: {status, id}}) => ({
BanUser: ({ variables: { input: { id } } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
const updated = update(data, {
state: {
status: {
banned: {
status: { $set: true },
},
},
},
});
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
UnbanUser: ({ variables: { input: { id } } }) => ({
update: proxy => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({
fragment: userStatusFragment,
id: fragmentId,
});
const updated = update(data, {
state: {
status: {
banned: {
status: { $set: false },
},
},
},
});
proxy.writeFragment({
fragment: userStatusFragment,
id: fragmentId,
data: updated,
});
},
}),
SetUserBanStatus: ({ variables: { status, id } }) => ({
updateQueries: {
TalkAdmin_Community: (prev) => {
TalkAdmin_Community: prev => {
if (!status) {
return prev;
}
const updated = update(prev, {
users: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== id)},
nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
},
});
return updated;
}
}
}),
ApproveUsername: ({variables: {id}}) => ({
updateQueries: {
TalkAdmin_Community: (prev) => {
const updated = update(prev, {
flaggedUsers: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== id)},
},
});
return updated;
}
}
}),
RejectUsername: ({variables: {id: userId}}) => ({
updateQueries: {
TalkAdmin_Community: (prev) => {
const updated = update(prev, {
flaggedUsers: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
},
});
return updated;
}
},
},
}),
UpdateSettings: ({variables: {input}}) => ({
ApproveUsername: ({ variables: { id } }) => ({
updateQueries: {
TalkAdmin_Configure: (prev) => {
TalkAdmin_Community: prev => {
const updated = update(prev, {
settings: mapLeaves(input, (leaf) => ({$set: leaf})),
flaggedUsers: {
nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
},
});
return updated;
}
}
},
},
}),
RejectUsername: ({ variables: { id: userId } }) => ({
updateQueries: {
TalkAdmin_Community: prev => {
const updated = update(prev, {
flaggedUsers: {
nodes: {
$apply: nodes => nodes.filter(node => node.id !== userId),
},
},
});
return updated;
},
},
}),
UpdateSettings: ({ variables: { input } }) => ({
updateQueries: {
TalkAdmin_Configure: prev => {
const updated = update(prev, {
settings: mapLeaves(input, leaf => ({ $set: leaf })),
});
return updated;
},
},
}),
},
};
+15 -9
View File
@@ -1,20 +1,20 @@
import React from 'react';
import {render} from 'react-dom';
import { render } from 'react-dom';
import smoothscroll from 'smoothscroll-polyfill';
import TalkProvider from 'coral-framework/components/TalkProvider';
import {createContext} from 'coral-framework/services/bootstrap';
import { createContext } from 'coral-framework/services/bootstrap';
import reducers from './reducers';
import App from './components/App';
import 'react-mdl/extra/material.js';
import graphqlExtension from './graphql';
import pluginsConfig from 'pluginsConfig';
import {toast} from 'react-toastify';
import {createNotificationService} from './services/notification';
import {hideShortcutsNote} from './actions/moderation';
import { toast } from 'react-toastify';
import { createNotificationService } from './services/notification';
import { hideShortcutsNote } from './actions/moderation';
smoothscroll.polyfill();
function init({store, storage}) {
function init({ store, storage }) {
if (storage && storage.getItem('coral:shortcutsNote') === 'hide') {
store.dispatch(hideShortcutsNote());
}
@@ -22,13 +22,19 @@ function init({store, storage}) {
async function main() {
const notification = createNotificationService(toast);
const context = await createContext({reducers, graphqlExtension, pluginsConfig, notification, init});
const context = await createContext({
reducers,
graphqlExtension,
pluginsConfig,
notification,
init,
});
render(
<TalkProvider {...context}>
<App />
</TalkProvider>
, document.querySelector('#root')
</TalkProvider>,
document.querySelector('#root')
);
}
+53 -52
View File
@@ -5,60 +5,61 @@ const initialState = {
user: null,
loginError: null,
loginMaxExceeded: false,
passwordRequestSuccess: null
passwordRequestSuccess: null,
};
export default function auth (state = initialState, action) {
export default function auth(state = initialState, action) {
switch (action.type) {
case actions.CHECK_LOGIN_REQUEST:
return {
...state,
loadingUser: true,
};
case actions.CHECK_LOGIN_FAILURE:
return {
...state,
loggedIn: false,
loadingUser: false,
user: null,
};
case actions.CHECK_LOGIN_SUCCESS:
return {
...state,
loggedIn: true,
loadingUser: false,
user: action.user,
};
case actions.LOGOUT:
return initialState;
case actions.LOGIN_SUCCESS:
return {
...state,
loginMaxExceeded: false,
loginError: null,
};
case actions.LOGIN_FAILURE:
return {
...state,
loginError: action.message,
};
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
return {
...state,
passwordRequestSuccess: null,
};
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
return {
...state,
passwordRequestSuccess: 'If you have a registered account, a password reset link was sent to that email.',
};
case actions.LOGIN_MAXIMUM_EXCEEDED:
return {
...state,
loginMaxExceeded: true,
loginError: action.message,
};
default :
return state;
case actions.CHECK_LOGIN_REQUEST:
return {
...state,
loadingUser: true,
};
case actions.CHECK_LOGIN_FAILURE:
return {
...state,
loggedIn: false,
loadingUser: false,
user: null,
};
case actions.CHECK_LOGIN_SUCCESS:
return {
...state,
loggedIn: true,
loadingUser: false,
user: action.user,
};
case actions.LOGOUT:
return initialState;
case actions.LOGIN_SUCCESS:
return {
...state,
loginMaxExceeded: false,
loginError: null,
};
case actions.LOGIN_FAILURE:
return {
...state,
loginError: action.message,
};
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
return {
...state,
passwordRequestSuccess: null,
};
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
return {
...state,
passwordRequestSuccess:
'If you have a registered account, a password reset link was sent to that email.',
};
case actions.LOGIN_MAXIMUM_EXCEEDED:
return {
...state,
loginMaxExceeded: true,
loginError: action.message,
};
default:
return state;
}
}
@@ -1,4 +1,7 @@
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
import {
SHOW_BAN_USER_DIALOG,
HIDE_BAN_USER_DIALOG,
} from '../constants/banUserDialog';
const initialState = {
open: false,
@@ -10,21 +13,21 @@ const initialState = {
export default function banUserDialog(state = initialState, action) {
switch (action.type) {
case SHOW_BAN_USER_DIALOG:
return {
...state,
open: true,
userId: action.userId,
username: action.username,
commentId: action.commentId,
commentStatus: action.commentStatus,
};
case HIDE_BAN_USER_DIALOG:
return {
...state,
open: false,
};
default:
return state;
case SHOW_BAN_USER_DIALOG:
return {
...state,
open: true,
userId: action.userId,
username: action.username,
commentId: action.commentId,
commentStatus: action.commentStatus,
};
case HIDE_BAN_USER_DIALOG:
return {
...state,
open: false,
};
default:
return state;
}
}
+67 -67
View File
@@ -8,7 +8,7 @@ import {
SHOW_BANUSER_DIALOG,
HIDE_BANUSER_DIALOG,
SHOW_REJECT_USERNAME_DIALOG,
HIDE_REJECT_USERNAME_DIALOG
HIDE_REJECT_USERNAME_DIALOG,
} from '../constants/community';
const initialState = {
@@ -23,75 +23,75 @@ const initialState = {
pagePeople: 0,
user: {},
banDialog: false,
rejectUsernameDialog: false
rejectUsernameDialog: false,
};
export default function community (state = initialState, action) {
export default function community(state = initialState, action) {
switch (action.type) {
case FETCH_USERS_REQUEST :
return {
...state,
isFetchingPeople: true,
};
case FETCH_USERS_FAILURE :
return {
...state,
isFetchingPeople: false,
errorPeople: action.error,
};
case FETCH_USERS_SUCCESS : {
case FETCH_USERS_REQUEST:
return {
...state,
isFetchingPeople: true,
};
case FETCH_USERS_FAILURE:
return {
...state,
isFetchingPeople: false,
errorPeople: action.error,
};
case FETCH_USERS_SUCCESS: {
const {users, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line
return {
...state,
isFetchingPeople: false,
errorPeople: '',
pagePeople: page,
countPeople: count,
limitPeople: limit,
totalPagesPeople: totalPages,
...rest,
users, // Sets to normal array
};
}
case SET_PAGE:
return {
...state,
pagePeople: action.page,
};
case SORT_UPDATE :
return {
...state,
fieldPeople: action.sort.field,
ascPeople: !state.ascPeople,
};
case HIDE_BANUSER_DIALOG:
return {
...state,
banDialog: false,
};
case SHOW_BANUSER_DIALOG:
return {
...state,
user: action.user,
banDialog: true,
};
case HIDE_REJECT_USERNAME_DIALOG:
return {
...state,
rejectUsernameDialog: false,
};
case SHOW_REJECT_USERNAME_DIALOG:
return {
...state,
user: action.user,
rejectUsernameDialog: true
};
case SET_SEARCH_VALUE:
return {
...state,
searchValue: action.value,
};
default :
return state;
return {
...state,
isFetchingPeople: false,
errorPeople: '',
pagePeople: page,
countPeople: count,
limitPeople: limit,
totalPagesPeople: totalPages,
...rest,
users, // Sets to normal array
};
}
case SET_PAGE:
return {
...state,
pagePeople: action.page,
};
case SORT_UPDATE:
return {
...state,
fieldPeople: action.sort.field,
ascPeople: !state.ascPeople,
};
case HIDE_BANUSER_DIALOG:
return {
...state,
banDialog: false,
};
case SHOW_BANUSER_DIALOG:
return {
...state,
user: action.user,
banDialog: true,
};
case HIDE_REJECT_USERNAME_DIALOG:
return {
...state,
rejectUsernameDialog: false,
};
case SHOW_REJECT_USERNAME_DIALOG:
return {
...state,
user: action.user,
rejectUsernameDialog: true,
};
case SET_SEARCH_VALUE:
return {
...state,
searchValue: action.value,
};
default:
return state;
}
}
+9 -9
View File
@@ -1,17 +1,17 @@
import * as actions from '../actions/config';
const initialState = {
data: {}
data: {},
};
export default function config (state = initialState, action) {
export default function config(state = initialState, action) {
switch (action.type) {
case actions.CONFIG_UPDATED:
return {
...state,
data: action.data,
};
default:
return state;
case actions.CONFIG_UPDATED:
return {
...state,
data: action.data,
};
default:
return state;
}
}
+31 -28
View File
@@ -11,37 +11,40 @@ const initialState = {
export default function configure(state = initialState, action) {
switch (action.type) {
case actions.UPDATE_PENDING: {
let next = state;
if (action.updater) {
case actions.UPDATE_PENDING: {
let next = state;
if (action.updater) {
next = update(next, {
pending: action.updater,
});
}
if (action.errorUpdater) {
next = update(next, {
errors: action.errorUpdater,
});
}
const noErrors = Object.keys(next.errors).reduce(
(res, error) => res && !next.errors[error],
true
);
const canSave = !isEmpty(next.pending) && noErrors;
next = update(next, {
pending: action.updater,
canSave: { $set: canSave },
});
}
if (action.errorUpdater) {
next = update(next, {
errors: action.errorUpdater,
});
}
const noErrors = Object.keys(next.errors).reduce((res, error) => res && !next.errors[error], true);
const canSave = !isEmpty(next.pending) && noErrors;
next = update(next, {
canSave: {$set: canSave},
});
return next;
}
case actions.CLEAR_PENDING:
return {
...state,
pending: {},
canSave: false,
};
case actions.SET_ACTIVE_SECTION:
return {
...state,
activeSection: action.section,
};
return next;
}
case actions.CLEAR_PENDING:
return {
...state,
pending: {},
canSave: false,
};
case actions.SET_ACTIVE_SECTION:
return {
...state,
activeSection: action.section,
};
}
return state;
}
+99 -97
View File
@@ -8,127 +8,129 @@ const initialState = {
organizationName: '',
domains: {
whitelist: [],
}
},
},
user: {
username: '',
email: '',
password: '',
confirmPassword: ''
}
confirmPassword: '',
},
},
errors: {
organizationName: '',
username: '',
email: '',
password: '',
confirmPassword: ''
confirmPassword: '',
},
showErrors: false,
hasError: false,
error: null,
step: 0,
navItems: [{
text: '1. Add Organization Name',
step: 1
},
{
text: '2. Create your account',
step: 2
},
{
text: '3. Domain Whitelist',
step: 3
}],
navItems: [
{
text: '1. Add Organization Name',
step: 1,
},
{
text: '2. Create your account',
step: 2,
},
{
text: '3. Domain Whitelist',
step: 3,
},
],
installRequest: null,
installRequestError: null,
alreadyInstalled: false
alreadyInstalled: false,
};
export default function install (state = initialState, action) {
export default function install(state = initialState, action) {
switch (action.type) {
case actions.NEXT_STEP:
return {
...state,
step: state.step + 1,
};
case actions.PREVIOUS_STEP:
return {
...state,
step: state.step - 1,
};
case actions.GO_TO_STEP:
return {
...state,
step: action.step,
};
case actions.UPDATE_PERMITTED_DOMAINS_SETTINGS:
return update(state, {
data: {
settings: {
domains: {
whitelist: {$set: action.value},
case actions.NEXT_STEP:
return {
...state,
step: state.step + 1,
};
case actions.PREVIOUS_STEP:
return {
...state,
step: state.step - 1,
};
case actions.GO_TO_STEP:
return {
...state,
step: action.step,
};
case actions.UPDATE_PERMITTED_DOMAINS_SETTINGS:
return update(state, {
data: {
settings: {
domains: {
whitelist: { $set: action.value },
},
},
},
},
});
case actions.UPDATE_FORMDATA_SETTINGS:
return update(state, {
data: {
settings: {
[action.name]: {$set: action.value},
});
case actions.UPDATE_FORMDATA_SETTINGS:
return update(state, {
data: {
settings: {
[action.name]: { $set: action.value },
},
},
},
});
case actions.UPDATE_FORMDATA_USER:
return update(state, {
data: {
user: {
[action.name]: {$set: action.value},
});
case actions.UPDATE_FORMDATA_USER:
return update(state, {
data: {
user: {
[action.name]: { $set: action.value },
},
},
},
});
case actions.HAS_ERROR:
return {
...state,
hasError: true,
showErrors: true,
};
case actions.ADD_ERROR:
return update(state, {
errors: {
[action.name]: {$set: action.error},
},
});
case actions.CLEAR_ERRORS:
return {
...state,
errors: {},
};
case actions.INSTALL_REQUEST:
return {
...state,
isLoading: true,
};
case actions.INSTALL_SUCCESS:
return {
...state,
isLoading: false,
installRequest: 'SUCCESS',
};
case actions.INSTALL_FAILURE:
return {
...state,
isLoading: false,
installRequest: 'FAILURE',
installRequestError: action.error
};
case actions.CHECK_INSTALL_SUCCESS:
return {
...state,
alreadyInstalled: action.installed,
};
default :
return state;
});
case actions.HAS_ERROR:
return {
...state,
hasError: true,
showErrors: true,
};
case actions.ADD_ERROR:
return update(state, {
errors: {
[action.name]: { $set: action.error },
},
});
case actions.CLEAR_ERRORS:
return {
...state,
errors: {},
};
case actions.INSTALL_REQUEST:
return {
...state,
isLoading: true,
};
case actions.INSTALL_SUCCESS:
return {
...state,
isLoading: false,
installRequest: 'SUCCESS',
};
case actions.INSTALL_FAILURE:
return {
...state,
isLoading: false,
installRequest: 'FAILURE',
installRequestError: action.error,
};
case actions.CHECK_INSTALL_SUCCESS:
return {
...state,
alreadyInstalled: action.installed,
};
default:
return state;
}
}
+48 -48
View File
@@ -10,54 +10,54 @@ const initialState = {
selectedCommentId: '',
};
export default function moderation (state = initialState, action) {
export default function moderation(state = initialState, action) {
switch (action.type) {
case actions.MODERATION_CLEAR_STATE:
return {
...initialState,
shortcutsNoteVisible: state.shortcutsNoteVisible,
};
case actions.TOGGLE_MODAL:
return {
...state,
modalOpen: action.open,
};
case actions.SINGLE_VIEW:
return {
...state,
singleView: !state.singleView,
};
case actions.HIDE_SHORTCUTS_NOTE:
return {
...state,
shortcutsNoteVisible: 'hide',
};
case actions.SHOW_STORY_SEARCH:
return {
...state,
storySearchVisible: true,
};
case actions.HIDE_STORY_SEARCH:
return {
...state,
storySearchVisible: false,
};
case actions.STORY_SEARCH_CHANGE_VALUE:
return {
...state,
storySearchString: action.value,
};
case actions.SET_SORT_ORDER:
return {
...state,
sortOrder: action.order,
};
case actions.MODERATION_SELECT_COMMENT:
return {
...state,
selectedCommentId: action.id,
};
default:
return state;
case actions.MODERATION_CLEAR_STATE:
return {
...initialState,
shortcutsNoteVisible: state.shortcutsNoteVisible,
};
case actions.TOGGLE_MODAL:
return {
...state,
modalOpen: action.open,
};
case actions.SINGLE_VIEW:
return {
...state,
singleView: !state.singleView,
};
case actions.HIDE_SHORTCUTS_NOTE:
return {
...state,
shortcutsNoteVisible: 'hide',
};
case actions.SHOW_STORY_SEARCH:
return {
...state,
storySearchVisible: true,
};
case actions.HIDE_STORY_SEARCH:
return {
...state,
storySearchVisible: false,
};
case actions.STORY_SEARCH_CHANGE_VALUE:
return {
...state,
storySearchString: action.value,
};
case actions.SET_SORT_ORDER:
return {
...state,
sortOrder: action.order,
};
case actions.MODERATION_SELECT_COMMENT:
return {
...state,
selectedCommentId: action.id,
};
default:
return state;
}
}
+51 -52
View File
@@ -5,7 +5,7 @@ const initialState = {
assets: {
byId: {},
ids: [],
assets: []
assets: [],
},
searchValue: '',
criteria: {
@@ -14,60 +14,59 @@ const initialState = {
},
};
export default function assets (state = initialState, action) {
export default function assets(state = initialState, action) {
switch (action.type) {
case actions.FETCH_ASSETS_SUCCESS: {
const assets = action.assets.reduce((prev, curr) => {
prev[curr.id] = curr;
return prev;
}, {});
case actions.FETCH_ASSETS_SUCCESS: {
const assets = action.assets.reduce((prev, curr) => {
prev[curr.id] = curr;
return prev;
}, {});
return update(state, {
assets: {
totalPages: {$set: action.totalPages},
page: {$set: action.page},
byId: {$set: assets},
count: {$set: action.count},
ids: {$set: Object.keys(assets)},
},
});
}
case actions.UPDATE_ASSET_STATE_REQUEST:
return update(state, {
assets: {
byId: {
[action.id]: {
closedAt: {$set: action.closedAt},
return update(state, {
assets: {
totalPages: { $set: action.totalPages },
page: { $set: action.page },
byId: { $set: assets },
count: { $set: action.count },
ids: { $set: Object.keys(assets) },
},
});
}
case actions.UPDATE_ASSET_STATE_REQUEST:
return update(state, {
assets: {
byId: {
[action.id]: {
closedAt: { $set: action.closedAt },
},
},
},
},
});
case actions.UPDATE_ASSETS:
return update(state, {
assets: {
assets: {$set: action.assets},
},
});
case actions.SET_PAGE:
return {
...state,
page: action.page,
};
case actions.SET_SEARCH_VALUE:
return {
...state,
searchValue: action.value,
};
case actions.SET_CRITERIA:
return {
...state,
criteria: {
...state.criteria,
...action.criteria,
},
};
default:
return state;
});
case actions.UPDATE_ASSETS:
return update(state, {
assets: {
assets: { $set: action.assets },
},
});
case actions.SET_PAGE:
return {
...state,
page: action.page,
};
case actions.SET_SEARCH_VALUE:
return {
...state,
searchValue: action.value,
};
case actions.SET_CRITERIA:
return {
...state,
criteria: {
...state.criteria,
...action.criteria,
},
};
default:
return state;
}
}
@@ -1,4 +1,7 @@
import {SHOW_SUSPEND_USER_DIALOG, HIDE_SUSPEND_USER_DIALOG} from '../constants/suspendUserDialog';
import {
SHOW_SUSPEND_USER_DIALOG,
HIDE_SUSPEND_USER_DIALOG,
} from '../constants/suspendUserDialog';
const initialState = {
open: false,
@@ -10,21 +13,21 @@ const initialState = {
export default function suspendUserDialog(state = initialState, action) {
switch (action.type) {
case SHOW_SUSPEND_USER_DIALOG:
return {
...state,
open: true,
userId: action.userId,
username: action.username,
commentId: action.commentId,
commentStatus: action.commentStatus,
};
case HIDE_SUSPEND_USER_DIALOG:
return {
...state,
open: false,
};
default:
return state;
case SHOW_SUSPEND_USER_DIALOG:
return {
...state,
open: true,
userId: action.userId,
username: action.username,
commentId: action.commentId,
commentStatus: action.commentStatus,
};
case HIDE_SUSPEND_USER_DIALOG:
return {
...state,
open: false,
};
default:
return state;
}
}
+32 -26
View File
@@ -1,33 +1,39 @@
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
import {SHOW_SUSPEND_USER_DIALOG, HIDE_SUSPEND_USER_DIALOG} from '../constants/suspendUserDialog';
import {
SHOW_BAN_USER_DIALOG,
HIDE_BAN_USER_DIALOG,
} from '../constants/banUserDialog';
import {
SHOW_SUSPEND_USER_DIALOG,
HIDE_SUSPEND_USER_DIALOG,
} from '../constants/suspendUserDialog';
const initialState = {
modal: false
modal: false,
};
export default function config (state = initialState, action) {
export default function config(state = initialState, action) {
switch (action.type) {
case SHOW_BAN_USER_DIALOG:
return {
...state,
modal: true,
};
case SHOW_SUSPEND_USER_DIALOG:
return {
...state,
modal: true,
};
case HIDE_BAN_USER_DIALOG:
return {
...state,
modal: false,
};
case HIDE_SUSPEND_USER_DIALOG:
return {
...state,
modal: false,
};
default:
return state;
case SHOW_BAN_USER_DIALOG:
return {
...state,
modal: true,
};
case SHOW_SUSPEND_USER_DIALOG:
return {
...state,
modal: true,
};
case HIDE_BAN_USER_DIALOG:
return {
...state,
modal: false,
};
case HIDE_SUSPEND_USER_DIALOG:
return {
...state,
modal: false,
};
default:
return state;
}
}
+41 -39
View File
@@ -9,44 +9,46 @@ const initialState = {
export default function banUserDialog(state = initialState, action) {
switch (action.type) {
case actions.VIEW_USER_DETAIL:
return {
...state,
userId: action.userId,
};
case actions.HIDE_USER_DETAIL:
return {
...state,
userId: null,
selectedCommentIds: [],
};
case actions.CLEAR_USER_DETAIL_SELECTIONS:
return {
...state,
selectedCommentIds: [],
};
case actions.CHANGE_TAB_USER_DETAIL:
return {
...state,
activeTab: action.tab,
statuses: action.statuses,
};
case actions.SELECT_USER_DETAIL_COMMENT:
return {
...state,
selectedCommentIds: [...state.selectedCommentIds, action.id],
};
case actions.UNSELECT_USER_DETAIL_COMMENT:
return {
...state,
selectedCommentIds: state.selectedCommentIds.filter((id) => id !== action.id),
};
case actions.SELECT_ALL_USER_DETAIL_COMMENT:
return {
...state,
selectedCommentIds: action.ids
};
default:
return state;
case actions.VIEW_USER_DETAIL:
return {
...state,
userId: action.userId,
};
case actions.HIDE_USER_DETAIL:
return {
...state,
userId: null,
selectedCommentIds: [],
};
case actions.CLEAR_USER_DETAIL_SELECTIONS:
return {
...state,
selectedCommentIds: [],
};
case actions.CHANGE_TAB_USER_DETAIL:
return {
...state,
activeTab: action.tab,
statuses: action.statuses,
};
case actions.SELECT_USER_DETAIL_COMMENT:
return {
...state,
selectedCommentIds: [...state.selectedCommentIds, action.id],
};
case actions.UNSELECT_USER_DETAIL_COMMENT:
return {
...state,
selectedCommentIds: state.selectedCommentIds.filter(
id => id !== action.id
),
};
case actions.SELECT_ALL_USER_DETAIL_COMMENT:
return {
...state,
selectedCommentIds: action.ids,
};
default:
return state;
}
}
@@ -1,4 +1,4 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from './Community.css';
import People from '../containers/People';
@@ -8,23 +8,22 @@ import FlaggedAccounts from '../containers/FlaggedAccounts';
class Community extends Component {
renderTab() {
const {route, community, ...props} = this.props;
const { route, community, ...props } = this.props;
const activeTab = route.path === ':id' ? 'flagged' : route.path;
if (activeTab === 'people') {
return <People
community={community}
data={this.props.data}
root={this.props.root}
/>;
return (
<People
community={community}
data={this.props.data}
root={this.props.root}
/>
);
}
return (
<div>
<FlaggedAccounts
data={this.props.data}
root={this.props.root}
/>
<FlaggedAccounts data={this.props.data} root={this.props.root} />
<RejectUsernameDialog
user={community.user}
open={community.rejectUsernameDialog}
@@ -36,14 +35,12 @@ class Community extends Component {
}
render() {
const {root: {flaggedUsernamesCount}} = this.props;
const { root: { flaggedUsernamesCount } } = this.props;
return (
<div className="talk-admin-community">
<CommunityMenu flaggedUsernamesCount={flaggedUsernamesCount} />
<div className={styles.container}>
{this.renderTab()}
</div>
<div className={styles.container}>{this.renderTab()}</div>
</div>
);
}
@@ -1,23 +1,33 @@
import React from 'react';
import styles from './CommunityMenu.css';
import t from 'coral-framework/services/i18n';
import {Link} from 'react-router';
import { Link } from 'react-router';
import PropTypes from 'prop-types';
import CountBadge from '../../../components/CountBadge';
const CommunityMenu = ({flaggedUsernamesCount = 0}) => {
const CommunityMenu = ({ flaggedUsernamesCount = 0 }) => {
const flaggedPath = '/admin/community/flagged';
const peoplePath = '/admin/community/people';
return (
<div className='mdl-tabs'>
<div className="mdl-tabs">
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<div>
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab} talk-admin-nav-flagged-accounts`} activeClassName={styles.active}>
<Link
to={flaggedPath}
className={`mdl-tabs__tab ${
styles.tab
} talk-admin-nav-flagged-accounts`}
activeClassName={styles.active}
>
{t('community.flaggedaccounts')}
<CountBadge count={flaggedUsernamesCount} />
</Link>
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab} talk-admin-nav-people`} activeClassName={styles.active}>
<Link
to={peoplePath}
className={`mdl-tabs__tab ${styles.tab} talk-admin-nav-people`}
activeClassName={styles.active}
>
{t('community.people')}
</Link>
</div>
@@ -5,7 +5,7 @@ import t from 'coral-framework/services/i18n';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
import LoadMore from '../../../components/LoadMore';
import FlaggedUser from '../containers/FlaggedUser';
import {CSSTransitionGroup} from 'react-transition-group';
import { CSSTransitionGroup } from 'react-transition-group';
import styles from './FlaggedAccounts.css';
class FlaggedAccounts extends React.Component {
@@ -22,45 +22,45 @@ class FlaggedAccounts extends React.Component {
const hasResults = users.nodes && !!users.nodes.length;
return (
<div className={cn('talk-adnin-community-flagged-accounts', styles.container)}>
<div
className={cn(
'talk-adnin-community-flagged-accounts',
styles.container
)}
>
<div className={styles.mainFlaggedContent}>
{
hasResults
? <CSSTransitionGroup
component={'ul'}
className={styles.list}
transitionName={{
enter: styles.userEnter,
enterActive: styles.userEnterActive,
leave: styles.userLeave,
leaveActive: styles.userLeaveActive,
}}
transitionEnter={true}
transitionLeave={true}
transitionEnterTimeout={1000}
transitionLeaveTimeout={1000}
>
{
users.nodes.map((user) => {
return (
<FlaggedUser
user={user}
key={user.id}
showRejectUsernameDialog={showRejectUsernameDialog}
approveUser={approveUser}
me={me}
viewUserDetail={viewUserDetail}
/>
);
})
}
</CSSTransitionGroup>
: <EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
}
<LoadMore
loadMore={loadMore}
showLoadMore={users.hasNextPage}
/>
{hasResults ? (
<CSSTransitionGroup
component={'ul'}
className={styles.list}
transitionName={{
enter: styles.userEnter,
enterActive: styles.userEnterActive,
leave: styles.userLeave,
leaveActive: styles.userLeaveActive,
}}
transitionEnter={true}
transitionLeave={true}
transitionEnterTimeout={1000}
transitionLeaveTimeout={1000}
>
{users.nodes.map(user => {
return (
<FlaggedUser
user={user}
key={user.id}
showRejectUsernameDialog={showRejectUsernameDialog}
approveUser={approveUser}
me={me}
viewUserDetail={viewUserDetail}
/>
);
})}
</CSSTransitionGroup>
) : (
<EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
)}
<LoadMore loadMore={loadMore} showLoadMore={users.hasNextPage} />
</div>
</div>
);
@@ -3,7 +3,7 @@ import styles from './FlaggedUser.css';
import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import {username} from 'talk-plugin-flags/helpers/flagReasons';
import { username } from 'talk-plugin-flags/helpers/flagReasons';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
@@ -16,79 +16,94 @@ const shortReasons = {
};
class User extends React.Component {
viewAuthorDetail = () => this.props.viewUserDetail(this.props.user.id);
showRejectUsernameDialog = () => this.props.showRejectUsernameDialog({id: this.props.user.id});
showRejectUsernameDialog = () =>
this.props.showRejectUsernameDialog({ id: this.props.user.id });
approveUser = () => this.props.approveUser({
userId: this.props.user.id,
});
approveUser = () =>
this.props.approveUser({
userId: this.props.user.id,
});
render() {
const {
user,
viewUserDetail,
selected,
className,
} = this.props;
const { user, viewUserDetail, selected, className } = this.props;
return (
<li tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})} >
<div className={cn('talk-admin-community-flagged-user', styles.container)}>
<div className={cn('talk-admin-community-flagged-user-header', styles.header)}>
<li
tabIndex={0}
className={cn(className, styles.root, {
[styles.rootSelected]: selected,
})}
>
<div
className={cn('talk-admin-community-flagged-user', styles.container)}
>
<div
className={cn(
'talk-admin-community-flagged-user-header',
styles.header
)}
>
<div className={styles.author}>
<button
onClick={this.viewAuthorDetail}
className={styles.button}>
<button onClick={this.viewAuthorDetail} className={styles.button}>
{user.username}
</button>
</div>
</div>
<div className={cn('talk-admin-community-flagged-user-body', styles.body)}>
<div
className={cn(
'talk-admin-community-flagged-user-body',
styles.body
)}
>
<div className={styles.flagged}>
<div className={styles.flaggedByCount}>
<i className={cn('material-icons', styles.flagIcon)}>flag</i>
<span className={styles.flaggedByLabel}>
{t('community.flags')}({ user.actions.length })
{t('community.flags')}({user.actions.length})
</span>:
{ user.action_summaries.map(
(action, i) => {
return <span className={styles.flaggedBy} key={i}>
{user.action_summaries.map((action, i) => {
return (
<span className={styles.flaggedBy} key={i}>
{shortReasons[action.reason]} ({action.count})
</span>;
}
)}
</span>
);
})}
</div>
<div className={styles.flaggedReasons}>
{ user.action_summaries.map(
(action_sum, i) => {
return <div key={i}>
{user.action_summaries.map((action_sum, i) => {
return (
<div key={i}>
<span className={styles.flaggedByLabel}>
{shortReasons[action_sum.reason]} ({action_sum.count})
</span>
{user.actions.map(
// find the action by action_sum.reason
(action, j) => {
if (action.reason === action_sum.reason) {
return <p className={styles.flaggedByReason} key={j}>
{action.user &&
<button onClick={() => {viewUserDetail(action.user.id);}} className={styles.button}>
{action.user.username}
</button>
}
: {action.message ? action.message : 'n/a'}
</p>;
return (
<p className={styles.flaggedByReason} key={j}>
{action.user && (
<button
onClick={() => {
viewUserDetail(action.user.id);
}}
className={styles.button}
>
{action.user.username}
</button>
)}
: {action.message ? action.message : 'n/a'}
</p>
);
}
return null;
}
)}
</div>;
}
)}
</div>
);
})}
</div>
</div>
<div className={styles.sideActions}>
@@ -1,38 +1,37 @@
import React from 'react';
import cn from 'classnames';
import styles from './People.css';
import {Icon, Dropdown, Option} from 'coral-ui';
import { Icon, Dropdown, Option } from 'coral-ui';
import EmptyCard from '../../../components/EmptyCard';
import t from 'coral-framework/services/i18n';
import LoadMore from '../../../components/LoadMore';
import PropTypes from 'prop-types';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import {isSuspended, isBanned} from 'coral-framework/utils/user';
import { isSuspended, isBanned } from 'coral-framework/utils/user';
import moment from 'moment';
const headers = [
{
title: t('community.username_and_email'),
field: 'username'
field: 'username',
},
{
title: t('community.account_creation_date'),
field: 'created_at'
field: 'created_at',
},
{
title: t('community.status'),
field: 'status'
field: 'status',
},
{
title: t('community.newsroom_role'),
field: 'role'
}
field: 'role',
},
];
class People extends React.Component {
getActionMenuLabel = (user) => {
getActionMenuLabel = user => {
if (isBanned(user)) {
return 'Banned';
} else if (isSuspended(user)) {
@@ -41,23 +40,23 @@ class People extends React.Component {
return '';
};
unsuspendUser = (input) => {
unsuspendUser = input => {
this.props.unsuspendUser(input);
}
};
unbanUser = (input) => {
unbanUser = input => {
this.props.unbanUser(input);
}
};
showBanUserDialog = (input) => {
showBanUserDialog = input => {
this.props.showBanUserDialog(input);
}
};
showSuspendUserDialog = (input) => {
showSuspendUserDialog = input => {
this.props.showSuspendUserDialog(input);
}
};
render () {
render() {
const {
onSearchChange,
users = [],
@@ -69,107 +68,169 @@ class People extends React.Component {
const hasResults = !!users.nodes.length;
return (
<div className={cn(styles.container, 'talk-admin-community-people-container')}>
<div
className={cn(
styles.container,
'talk-admin-community-people-container'
)}
>
<div className={styles.leftColumn}>
<div className={styles.searchBox}>
<Icon name='search' className={styles.searchIcon}/>
<Icon name="search" className={styles.searchIcon} />
<input
id="commenters-search"
type="text"
className={styles.searchBoxInput}
defaultValue=''
defaultValue=""
onChange={onSearchChange}
placeholder={t('streams.search')}
/>
</div>
</div>
<div className={styles.mainContent}>
{
hasResults
? <div>
<div>
<table className={`mdl-data-table ${styles.dataTable}`}>
<thead>
<tr>
{headers.map((header, i) =>(
<th
key={i}
className={cn('mdl-data-table__cell--non-numeric', styles.header)}
scope="col" >
{header.title}
</th>
))}
</tr>
</thead>
<tbody>
{users.nodes.map((user)=> (
<tr key={user.id} className="talk-admin-community-people-row">
<td className="mdl-data-table__cell--non-numeric">
<button onClick={() => {viewUserDetail(user.id);}} className={cn(styles.username, styles.button)}>{user.username}</button>
<span className={styles.email}>{user.profiles.map(({id}) => id)}</span>
</td>
<td className="mdl-data-table__cell--non-numeric">
{moment(new Date(user.created_at)).format('MMMM Do YYYY, h:mm:ss a')}
</td>
<td className="mdl-data-table__cell--non-numeric">
<ActionsMenu
icon="person"
className={cn(styles.actionsMenu, 'talk-admin-community-people-dd-status')}
buttonClassNames={cn(styles.actionsMenuButton, {
[styles.actionsMenuSuspended]: isSuspended(user),
[styles.actionsMenuBanned]: isBanned(user),
}, 'talk-admin-user-detail-actions-button')}
label={this.getActionMenuLabel(user)} >
{isSuspended(user) ? <ActionsMenuItem
onClick={() => this.unsuspendUser({id: user.id})}>
Remove Suspension
</ActionsMenuItem> : <ActionsMenuItem
onClick={() => this.showSuspendUserDialog({
userId: user.id,
username: user.username,
})}>
Suspend User
</ActionsMenuItem>}
{isBanned(user) ? <ActionsMenuItem
onClick={() => this.unbanUser({id: user.id})}>
Remove Ban
</ActionsMenuItem> : <ActionsMenuItem
onClick={() => this.showBanUserDialog({
userId: user.id,
username: user.username,
})}>
Ban User
</ActionsMenuItem>}
</ActionsMenu>
</td>
<td className="mdl-data-table__cell--non-numeric">
<Dropdown
containerClassName="talk-admin-community-people-dd-role"
value={user.role}
placeholder={t('community.role')}
onChange={(role) => setUserRole(user.id, role)}>
<Option value={'COMMENTER'} label={t('community.commenter')} />
<Option value={'STAFF'} label={t('community.staff')} />
<Option value={'MODERATOR'} label={t('community.moderator')} />
<Option value={'ADMIN'} label={t('community.admin')} />
</Dropdown>
</td>
</tr>
{hasResults ? (
<div>
<div>
<table className={`mdl-data-table ${styles.dataTable}`}>
<thead>
<tr>
{headers.map((header, i) => (
<th
key={i}
className={cn(
'mdl-data-table__cell--non-numeric',
styles.header
)}
scope="col"
>
{header.title}
</th>
))}
</tbody>
</table>
</div>
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
showLoadMore={users.hasNextPage}
/>
</tr>
</thead>
<tbody>
{users.nodes.map(user => (
<tr
key={user.id}
className="talk-admin-community-people-row"
>
<td className="mdl-data-table__cell--non-numeric">
<button
onClick={() => {
viewUserDetail(user.id);
}}
className={cn(styles.username, styles.button)}
>
{user.username}
</button>
<span className={styles.email}>
{user.profiles.map(({ id }) => id)}
</span>
</td>
<td className="mdl-data-table__cell--non-numeric">
{moment(new Date(user.created_at)).format(
'MMMM Do YYYY, h:mm:ss a'
)}
</td>
<td className="mdl-data-table__cell--non-numeric">
<ActionsMenu
icon="person"
className={cn(
styles.actionsMenu,
'talk-admin-community-people-dd-status'
)}
buttonClassNames={cn(
styles.actionsMenuButton,
{
[styles.actionsMenuSuspended]: isSuspended(
user
),
[styles.actionsMenuBanned]: isBanned(user),
},
'talk-admin-user-detail-actions-button'
)}
label={this.getActionMenuLabel(user)}
>
{isSuspended(user) ? (
<ActionsMenuItem
onClick={() =>
this.unsuspendUser({ id: user.id })
}
>
Remove Suspension
</ActionsMenuItem>
) : (
<ActionsMenuItem
onClick={() =>
this.showSuspendUserDialog({
userId: user.id,
username: user.username,
})
}
>
Suspend User
</ActionsMenuItem>
)}
{isBanned(user) ? (
<ActionsMenuItem
onClick={() => this.unbanUser({ id: user.id })}
>
Remove Ban
</ActionsMenuItem>
) : (
<ActionsMenuItem
onClick={() =>
this.showBanUserDialog({
userId: user.id,
username: user.username,
})
}
>
Ban User
</ActionsMenuItem>
)}
</ActionsMenu>
</td>
<td className="mdl-data-table__cell--non-numeric">
<Dropdown
containerClassName="talk-admin-community-people-dd-role"
value={user.role}
placeholder={t('community.role')}
onChange={role => setUserRole(user.id, role)}
>
<Option
value={'COMMENTER'}
label={t('community.commenter')}
/>
<Option
value={'STAFF'}
label={t('community.staff')}
/>
<Option
value={'MODERATOR'}
label={t('community.moderator')}
/>
<Option
value={'ADMIN'}
label={t('community.admin')}
/>
</Dropdown>
</td>
</tr>
))}
</tbody>
</table>
</div>
: <EmptyCard>{t('community.no_results')}</EmptyCard>
}
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
showLoadMore={users.hasNextPage}
/>
</div>
) : (
<EmptyCard>{t('community.no_results')}</EmptyCard>
)}
</div>
</div>
);
@@ -1,7 +1,7 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import {Dialog, Button} from 'coral-ui';
import { Dialog, Button } from 'coral-ui';
import styles from './RejectUsernameDialog.css';
import t from 'coral-framework/services/i18n';
@@ -11,26 +11,28 @@ const stages = [
title: 'reject_username.title_reject',
description: 'reject_username.description_reject',
options: {
'j': 'reject_username.no_cancel',
'k': 'reject_username.yes_suspend'
}
j: 'reject_username.no_cancel',
k: 'reject_username.yes_suspend',
},
},
{
title: 'reject_username.title_notify',
description: 'reject_username.description_notify',
options: {
'j': 'bandialog.cancel',
'k': 'reject_username.send'
}
}
j: 'bandialog.cancel',
k: 'reject_username.send',
},
},
];
class RejectUsernameDialog extends Component {
state = {email: '', stage: 0}
class RejectUsernameDialog extends Component {
state = { email: '', stage: 0 };
componentDidMount() {
this.setState({email: t('reject_username.email_message_reject'), about: t('reject_username.username')});
this.setState({
email: t('reject_username.email_message_reject'),
about: t('reject_username.username'),
});
}
/*
@@ -38,53 +40,59 @@ class RejectUsernameDialog extends Component {
* handles the possible actions for that dialog.
*/
onActionClick = (stage, menuOption) => () => {
const {rejectUsername, user} = this.props;
const {stage} = this.state;
const { rejectUsername, user } = this.props;
const { stage } = this.state;
const cancel = this.props.handleClose;
const next = () => this.setState({stage: stage + 1});
const next = () => this.setState({ stage: stage + 1 });
const suspend = async () => {
try {
await rejectUsername(user.id);
this.props.handleClose();
} catch (err) {
// TODO: handle error.
console.error(err);
}
};
const suspendModalActions = [
[ cancel, next ],
[ cancel, suspend ]
];
const suspendModalActions = [[cancel, next], [cancel, suspend]];
return suspendModalActions[stage][menuOption]();
}
};
onEmailChange = (e) => {
this.setState({email: e.target.value});
}
onEmailChange = e => {
this.setState({ email: e.target.value });
};
render () {
const {open, handleClose} = this.props;
const {stage} = this.state;
render() {
const { open, handleClose } = this.props;
const { stage } = this.state;
return <Dialog
className={cn(styles.suspendDialog, 'talk-admin-reject-username-dialog')}
id="rejectUsernameDialog"
open={open}
onClose={handleClose}
onCancel={handleClose}
title={t('reject_username.suspend_user')}>
<div className={styles.title}>
{t(stages[stage].title, t('reject_username.username'))}
</div>
<div className={cn(styles.container, `talk-admin-reject-username-dialog-step-${stage}`)}>
<div className={styles.description}>
{t(stages[stage].description, t('reject_username.username'))}
return (
<Dialog
className={cn(
styles.suspendDialog,
'talk-admin-reject-username-dialog'
)}
id="rejectUsernameDialog"
open={open}
onClose={handleClose}
onCancel={handleClose}
title={t('reject_username.suspend_user')}
>
<div className={styles.title}>
{t(stages[stage].title, t('reject_username.username'))}
</div>
<div
className={cn(
styles.container,
`talk-admin-reject-username-dialog-step-${stage}`
)}
>
<div className={styles.description}>
{t(stages[stage].description, t('reject_username.username'))}
</div>
{/* {
{/* {
// Suspension Message: This functionality it's not entirely done on the BE - It will be released soon.
stage === 1 &&
@@ -99,18 +107,28 @@ class RejectUsernameDialog extends Component {
</div>
</div>
} */}
<div className={cn(styles.modalButtons, 'talk-admin-reject-username-dialog-buttons')}>
{Object.keys(stages[stage].options).map((key, i) => (
<Button
key={i}
className={cn('talk-admin-username-dialog-button', `talk-admin-reject-username-dialog-button-${key}`)}
onClick={this.onActionClick(stage, i)} >
{t(stages[stage].options[key], t('reject_username.username'))}
</Button>
))}
<div
className={cn(
styles.modalButtons,
'talk-admin-reject-username-dialog-buttons'
)}
>
{Object.keys(stages[stage].options).map((key, i) => (
<Button
key={i}
className={cn(
'talk-admin-username-dialog-button',
`talk-admin-reject-username-dialog-button-${key}`
)}
onClick={this.onActionClick(stage, i)}
>
{t(stages[stage].options[key], t('reject_username.username'))}
</Button>
))}
</div>
</div>
</div>
</Dialog>;
</Dialog>
);
}
}
@@ -1,25 +1,29 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import {getDefinitionName} from 'coral-framework/utils';
import {withRejectUsername} from 'coral-framework/graphql/mutations';
import { getDefinitionName } from 'coral-framework/utils';
import { withRejectUsername } from 'coral-framework/graphql/mutations';
import FlaggedAccounts from '../containers/FlaggedAccounts';
import FlaggedUser from '../containers/FlaggedUser';
import People from '../containers/People';
import {hideRejectUsernameDialog} from '../../../actions/community';
import { hideRejectUsernameDialog } from '../../../actions/community';
import Community from '../components/Community';
const mapStateToProps = (state) => ({
const mapStateToProps = state => ({
community: state.community,
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
hideRejectUsernameDialog,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
hideRejectUsernameDialog,
},
dispatch
);
const withData = withQuery(gql`
const withData = withQuery(
gql`
query TalkAdmin_Community {
flaggedUsernamesCount: userCount(
query:{
@@ -43,11 +47,13 @@ const withData = withQuery(gql`
${FlaggedAccounts.fragments.root}
${FlaggedUser.fragments.root}
${FlaggedUser.fragments.me}
`, {
options: {
fetchPolicy: 'network-only',
},
});
`,
{
options: {
fetchPolicy: 'network-only',
},
}
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
@@ -1,29 +1,28 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import {withFragments} from 'plugin-api/beta/client/hocs';
import {Spinner} from 'coral-ui';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import { withFragments } from 'plugin-api/beta/client/hocs';
import { Spinner } from 'coral-ui';
import PropTypes from 'prop-types';
import {withApproveUsername} from 'coral-framework/graphql/mutations';
import {showRejectUsernameDialog} from '../../../actions/community';
import {viewUserDetail} from '../../../actions/userDetail';
import {getDefinitionName} from 'coral-framework/utils';
import {appendNewNodes} from 'plugin-api/beta/client/utils';
import { withApproveUsername } from 'coral-framework/graphql/mutations';
import { showRejectUsernameDialog } from '../../../actions/community';
import { viewUserDetail } from '../../../actions/userDetail';
import { getDefinitionName } from 'coral-framework/utils';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import FlaggedAccounts from '../components/FlaggedAccounts';
import FlaggedUser from '../containers/FlaggedUser';
class FlaggedAccountsContainer extends Component {
constructor(props) {
super(props);
}
approveUser = ({userId: id}) => {
approveUser = ({ userId: id }) => {
return this.props.approveUsername(id);
}
};
loadMore = () => {
return this.props.data.fetchMore({
@@ -32,14 +31,14 @@ class FlaggedAccountsContainer extends Component {
limit: 5,
cursor: this.props.root.flaggedUsers.endCursor,
},
updateQuery: (previous, {fetchMoreResult:{flaggedUsers}}) => {
updateQuery: (previous, { fetchMoreResult: { flaggedUsers } }) => {
const updated = update(previous, {
flaggedUsers: {
nodes: {
$apply: (nodes) => appendNewNodes(nodes, flaggedUsers.nodes),
$apply: nodes => appendNewNodes(nodes, flaggedUsers.nodes),
},
hasNextPage: {$set: flaggedUsers.hasNextPage},
endCursor: {$set: flaggedUsers.endCursor},
hasNextPage: { $set: flaggedUsers.hasNextPage },
endCursor: { $set: flaggedUsers.endCursor },
},
});
return updated;
@@ -53,7 +52,11 @@ class FlaggedAccountsContainer extends Component {
}
if (this.props.data.loading) {
return <div><Spinner/></div>;
return (
<div>
<Spinner />
</div>
);
}
return (
@@ -102,11 +105,14 @@ const LOAD_MORE_QUERY = gql`
${FlaggedUser.fragments.user}
`;
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
showRejectUsernameDialog,
viewUserDetail,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
showRejectUsernameDialog,
viewUserDetail,
},
dispatch
);
export default compose(
connect(null, mapDispatchToProps),
@@ -136,5 +142,5 @@ export default compose(
}
${FlaggedUser.fragments.user}
`,
}),
})
)(FlaggedAccountsContainer);
@@ -1,18 +1,18 @@
import {gql} from 'react-apollo';
import { gql } from 'react-apollo';
import FlaggedUser from '../components/FlaggedUser';
import {withFragments} from 'plugin-api/beta/client/hocs';
import { withFragments } from 'plugin-api/beta/client/hocs';
export default withFragments({
root: gql`
fragment TalkAdminCommunity_FlaggedUser_root on RootQuery {
__typename
}
`,
`,
me: gql`
fragment TalkAdminCommunity_FlaggedUser_me on User {
id
}
`,
`,
user: gql`
fragment TalkAdminCommunity_FlaggedUser_user on User {
id
@@ -31,7 +31,7 @@ export default withFragments({
}
}
role
actions{
actions {
id
created_at
... on FlagAction {
@@ -50,5 +50,5 @@ export default withFragments({
}
}
}
`
`,
})(FlaggedUser);
@@ -1,50 +1,54 @@
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { compose, gql } from 'react-apollo';
import People from '../components/People';
import PropTypes from 'prop-types';
import {withFragments} from 'plugin-api/beta/client/hocs';
import {withUnbanUser, withUnsuspendUser, withSetUserRole} from 'coral-framework/graphql/mutations';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {viewUserDetail} from '../../../actions/userDetail';
import {appendNewNodes} from 'plugin-api/beta/client/utils';
import { withFragments } from 'plugin-api/beta/client/hocs';
import {
withUnbanUser,
withUnsuspendUser,
withSetUserRole,
} from 'coral-framework/graphql/mutations';
import { showBanUserDialog } from 'actions/banUserDialog';
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
import { viewUserDetail } from '../../../actions/userDetail';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import {Spinner} from 'coral-ui';
import { Spinner } from 'coral-ui';
class PeopleContainer extends React.Component {
timer = null;
state = {
searchValue: ''
searchValue: '',
};
onSearchChange = (e) => {
const {value} = e.target;
this.setState({searchValue: value}, () => {
onSearchChange = e => {
const { value } = e.target;
this.setState({ searchValue: value }, () => {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.search(value);
}, 350);
});
}
};
search = async (value) => {
search = async value => {
return this.props.data.fetchMore({
query: SEARCH_QUERY,
variables: {
value,
limit: 5,
},
updateQuery: (previous, {fetchMoreResult:{users}}) => {
updateQuery: (previous, { fetchMoreResult: { users } }) => {
const updated = update(previous, {
users: {
nodes: {
$set: users.nodes,
},
hasNextPage: {$set: users.hasNextPage},
endCursor: {$set: users.endCursor},
hasNextPage: { $set: users.hasNextPage },
endCursor: { $set: users.endCursor },
},
});
return updated;
@@ -54,7 +58,7 @@ class PeopleContainer extends React.Component {
setUserRole = async (id, role) => {
await this.props.setUserRole(id, role);
}
};
loadMore = () => {
return this.props.data.fetchMore({
@@ -64,14 +68,14 @@ class PeopleContainer extends React.Component {
limit: 5,
cursor: this.props.root.users.endCursor,
},
updateQuery: (previous, {fetchMoreResult:{users}}) => {
updateQuery: (previous, { fetchMoreResult: { users } }) => {
const updated = update(previous, {
users: {
nodes: {
$apply: (nodes) => appendNewNodes(nodes, users.nodes),
$apply: nodes => appendNewNodes(nodes, users.nodes),
},
hasNextPage: {$set: users.hasNextPage},
endCursor: {$set: users.endCursor},
hasNextPage: { $set: users.hasNextPage },
endCursor: { $set: users.endCursor },
},
});
return updated;
@@ -80,28 +84,33 @@ class PeopleContainer extends React.Component {
};
render() {
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
}
if (this.props.data.loading) {
return <div><Spinner/></div>;
return (
<div>
<Spinner />
</div>
);
}
return <People
onSearchChange={this.onSearchChange}
viewUserDetail={this.props.viewUserDetail}
setUserRole={this.setUserRole}
showSuspendUserDialog={this.props.showSuspendUserDialog}
showBanUserDialog={this.props.showBanUserDialog}
unbanUser={this.props.unbanUser}
unsuspendUser={this.props.unsuspendUser}
data={this.props.data}
root={this.props.root}
users={this.props.root.users}
loadMore={this.loadMore}
/>;
return (
<People
onSearchChange={this.onSearchChange}
viewUserDetail={this.props.viewUserDetail}
setUserRole={this.setUserRole}
showSuspendUserDialog={this.props.showSuspendUserDialog}
showBanUserDialog={this.props.showBanUserDialog}
unbanUser={this.props.unbanUser}
unsuspendUser={this.props.unsuspendUser}
data={this.props.data}
root={this.props.root}
users={this.props.root.users}
loadMore={this.loadMore}
/>
);
}
}
@@ -117,20 +126,23 @@ PeopleContainer.propTypes = {
root: PropTypes.object,
};
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
viewUserDetail,
showSuspendUserDialog,
showBanUserDialog,
}, dispatch);
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
viewUserDetail,
showSuspendUserDialog,
showBanUserDialog,
},
dispatch
);
const LOAD_MORE_QUERY = gql`
query TalkAdminCommunity_People_LoadMoreUsers($limit: Int, $cursor: Cursor, $value: String) {
users(query: {
value: $value,
limit: $limit,
cursor: $cursor
}){
query TalkAdminCommunity_People_LoadMoreUsers(
$limit: Int
$cursor: Cursor
$value: String
) {
users(query: { value: $value, limit: $limit, cursor: $cursor }) {
hasNextPage
endCursor
nodes {
@@ -160,10 +172,7 @@ const LOAD_MORE_QUERY = gql`
const SEARCH_QUERY = gql`
query TalkAdminCommunity_People_SearchUsers($value: String, $limit: Int) {
users(query: {
value: $value,
limit: $limit,
}){
users(query: { value: $value, limit: $limit }) {
hasNextPage
endCursor
nodes {
@@ -199,7 +208,7 @@ export default compose(
withFragments({
root: gql`
fragment TalkAdminCommunity_People_root on RootQuery {
users(query: {}){
users(query: {}) {
hasNextPage
endCursor
nodes {
@@ -226,5 +235,5 @@ export default compose(
}
}
`,
}),
})
)(PeopleContainer);
@@ -1,74 +1,77 @@
import React, {Component} from 'react';
import React, { Component } from 'react';
import {Button, List, Item} from 'coral-ui';
import { Button, List, Item } from 'coral-ui';
import styles from './Configure.css';
import StreamSettings from '../containers/StreamSettings';
import ModerationSettings from '../containers/ModerationSettings';
import TechSettings from '../containers/TechSettings';
import t from 'coral-framework/services/i18n';
import {can} from 'coral-framework/services/perms';
import { can } from 'coral-framework/services/perms';
import PropTypes from 'prop-types';
export default class Configure extends Component {
getSectionComponent(section) {
switch(section){
case 'stream':
return StreamSettings;
case 'moderation':
return ModerationSettings;
case 'tech':
return TechSettings;
switch (section) {
case 'stream':
return StreamSettings;
case 'moderation':
return ModerationSettings;
case 'tech':
return TechSettings;
}
throw new Error(`Unknown section ${section}`);
}
render () {
const {auth: {user}, canSave, savePending, setActiveSection, activeSection} = this.props;
render() {
const {
auth: { user },
canSave,
savePending,
setActiveSection,
activeSection,
} = this.props;
const SectionComponent = this.getSectionComponent(activeSection);
if (!can(user, 'UPDATE_CONFIG')) {
return <p>You must be an administrator to access config settings. Please find the nearest Admin and ask them to level you up!</p>;
return (
<p>
You must be an administrator to access config settings. Please find
the nearest Admin and ask them to level you up!
</p>
);
}
return (
<div className={styles.container}>
<div className={styles.leftColumn}>
<List onChange={setActiveSection} activeItem={activeSection}>
<Item itemId='stream' icon='speaker_notes'>
<Item itemId="stream" icon="speaker_notes">
{t('configure.stream_settings')}
</Item>
<Item itemId='moderation' icon='thumbs_up_down'>
<Item itemId="moderation" icon="thumbs_up_down">
{t('configure.moderation_settings')}
</Item>
<Item itemId='tech' icon='code'>
<Item itemId="tech" icon="code">
{t('configure.tech_settings')}
</Item>
</List>
<div className={styles.saveBox}>
{
canSave ?
<Button
raised
onClick={savePending}
className={styles.changedSave}
icon='check'
full
>
{t('configure.save_changes')}
</Button>
:
<Button
raised
disabled
icon='check'
full
>
{t('configure.save_changes')}
</Button>
}
{canSave ? (
<Button
raised
onClick={savePending}
className={styles.changedSave}
icon="check"
full
>
{t('configure.save_changes')}
</Button>
) : (
<Button raised disabled icon="check" full>
{t('configure.save_changes')}
</Button>
)}
</div>
</div>
<div className={styles.mainContent}>
<SectionComponent
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import styles from './ConfigurePage.css';
const ConfigurePage = ({title, children, ...rest}) => (
const ConfigurePage = ({ title, children, ...rest }) => (
<div {...rest}>
<h3 className={styles.title}>{title}</h3>
{children}
@@ -4,14 +4,14 @@ import TagsInput from 'coral-admin/src/components/TagsInput';
import t from 'coral-framework/services/i18n';
import ConfigureCard from 'coral-framework/components/ConfigureCard';
const Domainlist = ({domains, onChangeDomainlist}) => {
const Domainlist = ({ domains, onChangeDomainlist }) => {
return (
<ConfigureCard title={t('configure.domain_list_title')}>
<p>{t('configure.domain_list_text')}</p>
<TagsInput
value={domains}
inputProps={{placeholder: 'URL'}}
onChange={(tags) => onChangeDomainlist('whitelist', tags)}
inputProps={{ placeholder: 'URL' }}
onChange={tags => onChangeDomainlist('whitelist', tags)}
/>
</ConfigureCard>
);

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