mirror of
https://github.com/wassname/talk.git
synced 2026-07-13 17:45:56 +08:00
Merge branch 'next' into reply
This commit is contained in:
@@ -13,6 +13,7 @@ client/coral-framework/graphql/introspection.json
|
||||
.idea/
|
||||
*.swp
|
||||
*.DS_STORE
|
||||
.prettierrc.json
|
||||
|
||||
coverage/
|
||||
test/e2e/reports/
|
||||
@@ -52,3 +53,4 @@ plugins/*
|
||||
!plugins/talk-plugin-slack-notifications
|
||||
|
||||
**/node_modules/*
|
||||
yarn-error.log
|
||||
|
||||
+7
-1
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"exec": "npm-run-all --parallel generate-introspection start:development",
|
||||
"verbose": true,
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
|
||||
"ext": "js,json,graphql,yml"
|
||||
"ext": "js,json,graphql,yml",
|
||||
"watch": [
|
||||
".",
|
||||
"bin/cli",
|
||||
"bin/cli-serve"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 app = express();
|
||||
|
||||
@@ -41,6 +42,22 @@ if (process.env.NODE_ENV !== 'test') {
|
||||
app.use(morgan('dev'));
|
||||
}
|
||||
|
||||
if (ENABLE_TRACING && APOLLO_ENGINE_KEY) {
|
||||
const {Engine} = require('apollo-engine');
|
||||
|
||||
const engine = new Engine({
|
||||
engineConfig: {
|
||||
apiKey: APOLLO_ENGINE_KEY
|
||||
},
|
||||
graphqlPort: PORT,
|
||||
endpoint: `${MOUNT_PATH}api/v1/graph/ql`,
|
||||
});
|
||||
|
||||
engine.start();
|
||||
|
||||
app.use(engine.expressMiddleware());
|
||||
}
|
||||
|
||||
// Trust the first proxy in front of us, this will enable us to trust the fact
|
||||
// that SSL was terminated correctly.
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
require('./util');
|
||||
const program = require('commander');
|
||||
const {head, map} = require('lodash');
|
||||
const Matcher = require('did-you-mean');
|
||||
|
||||
@@ -18,40 +19,37 @@ 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.
|
||||
const cmds = map(program.commands, '_name');
|
||||
const cmd = head(program.args);
|
||||
if (!cmds.includes(cmd)) {
|
||||
const m = new Matcher(cmds);
|
||||
const similarCMDs = m.list(cmd);
|
||||
const commands = map(program.commands, '_name');
|
||||
const command = head(program.args);
|
||||
if (!commands.includes(command)) {
|
||||
const m = new Matcher(commands);
|
||||
const similarCommands = m.list(command);
|
||||
|
||||
console.error(`cli '${cmd}' is not a talk cli command. See 'cli --help'.`);
|
||||
if (similarCMDs.length > 0) {
|
||||
const sc = similarCMDs.map(({value}) => `\t${value}\n`).join('');
|
||||
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('');
|
||||
console.error(`\nThe most similar commands are\n${sc}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* When this provess exists, check to see if we have a running command, if we do
|
||||
* check to see if it is still running. If it is, then kill it with a SIGINT
|
||||
* signal. This is for the use case where we want to kill the process that is
|
||||
* labled with the PID written out by the parent process.
|
||||
*/
|
||||
process.once('exit', () => {
|
||||
if (
|
||||
// /**
|
||||
// * When this process exists, check to see if we have a running command, if we do
|
||||
// * check to see if it is still running. If it is, then kill it with a SIGINT
|
||||
// * signal. This is for the use case where we want to kill the process that is
|
||||
// * labeled with the PID written out by the parent process.
|
||||
// */
|
||||
// process.once('exit', () => {
|
||||
// if (
|
||||
|
||||
// program.runningCommand &&
|
||||
program.runningCommand.killed === false &&
|
||||
program.runningCommand.exitCode === null
|
||||
) {
|
||||
program.runningCommand.kill('SIGINT');
|
||||
}
|
||||
});
|
||||
// // program.runningCommand &&
|
||||
// program.runningCommand.killed === false &&
|
||||
// program.runningCommand.exitCode === null
|
||||
// ) {
|
||||
// program.runningCommand.kill('SIGINT');
|
||||
// }
|
||||
// });
|
||||
|
||||
+2
-2
@@ -4,7 +4,8 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const parseDuration = require('ms');
|
||||
const Table = require('cli-table');
|
||||
const AssetModel = require('../models/asset');
|
||||
@@ -12,7 +13,6 @@ const CommentModel = require('../models/comment');
|
||||
const AssetsService = require('../services/assets');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const scraper = require('../services/scraper');
|
||||
const util = require('./util');
|
||||
const inquirer = require('inquirer');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
|
||||
+2
-2
@@ -4,10 +4,10 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const scraper = require('../services/scraper');
|
||||
const mailer = require('../services/mailer');
|
||||
const util = require('./util');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const kue = require('../services/kue');
|
||||
|
||||
|
||||
+1
-1
@@ -4,8 +4,8 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const MigrationService = require('../services/migration');
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@
|
||||
// Interface heavily inspired by the yarn package manager:
|
||||
// https://yarnpkg.com/
|
||||
|
||||
const program = require('./commander');
|
||||
require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
|
||||
// Make things colorful!
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const serve = require('../serve');
|
||||
|
||||
//==============================================================================
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const SettingsService = require('../services/settings');
|
||||
const util = require('./util');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
+2
-2
@@ -4,7 +4,8 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const SettingModel = require('../models/setting');
|
||||
@@ -12,7 +13,6 @@ const MODERATION_OPTIONS = require('../models/enum/moderation_options');
|
||||
const SettingsService = require('../services/settings');
|
||||
const SetupService = require('../services/setup');
|
||||
const UsersService = require('../services/users');
|
||||
const util = require('./util');
|
||||
const errors = require('../errors');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
|
||||
+2
-2
@@ -4,10 +4,10 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const TokensService = require('../services/tokens');
|
||||
const util = require('./util');
|
||||
const Table = require('cli-table');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
|
||||
+182
-412
@@ -4,133 +4,36 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
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'));
|
||||
|
||||
const schema = require('../graph/schema');
|
||||
const Context = require('../graph/context');
|
||||
const UsersService = require('../services/users');
|
||||
const UserModel = require('../models/user');
|
||||
const CommentModel = require('../models/comment');
|
||||
const ActionModel = require('../models/action');
|
||||
const USER_ROLES = require('../models/enum/user_roles');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const Table = require('cli-table');
|
||||
const databaseVerifications = require('./verifications/database');
|
||||
|
||||
const validateRequired = (msg = 'Field is required', len = 1) => (input) => {
|
||||
if (input && input.length >= len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return msg;
|
||||
};
|
||||
|
||||
// Regeister the shutdown criteria.
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
function getUserCreateAnswers(options) {
|
||||
if (options.flag_mode) {
|
||||
|
||||
let user = {
|
||||
email: options.email,
|
||||
password: options.password,
|
||||
confirmPassword: options.password,
|
||||
username: options.name,
|
||||
roles: []
|
||||
};
|
||||
|
||||
if (options.role && USER_ROLES.indexOf(options.role) > -1) {
|
||||
user.roles = [options.role];
|
||||
}
|
||||
|
||||
return Promise.resolve(user);
|
||||
}
|
||||
|
||||
return inquirer.prompt([
|
||||
{
|
||||
name: 'email',
|
||||
message: 'Email',
|
||||
format: 'email',
|
||||
validate: validateRequired('Email is required')
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: (password) => {
|
||||
return UsersService
|
||||
.isValidPassword(password)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
filter: (confirmPassword) => {
|
||||
return UsersService
|
||||
.isValidPassword(confirmPassword)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
message: 'Username',
|
||||
filter: (username) => {
|
||||
return UsersService
|
||||
.isValidUsername(username)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'roles',
|
||||
message: 'User Role',
|
||||
type: 'checkbox',
|
||||
choices: USER_ROLES
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts for input and registers a user based on those.
|
||||
*/
|
||||
async function createUser(options) {
|
||||
try {
|
||||
const answers = await getUserCreateAnswers(options);
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
throw new Error('Passwords do not match');
|
||||
}
|
||||
|
||||
const user = await UsersService.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim());
|
||||
console.log(`Created user ${user.id}.`);
|
||||
|
||||
if (answers.roles.length > 0) {
|
||||
return Promise.all(answers.roles.map((role) => {
|
||||
return UsersService
|
||||
.addRoleToUser(user.id, role)
|
||||
.then(() => {
|
||||
console.log(`Added the role ${role} to User ${user.id}.`);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a user.
|
||||
* Deletes a user and cleans up their associated verifications.
|
||||
*/
|
||||
async function deleteUser(userID) {
|
||||
|
||||
@@ -142,23 +45,50 @@ async function deleteUser(userID) {
|
||||
throw new Error(`user with id ${userID} not found`);
|
||||
}
|
||||
|
||||
printUserAsTable(user);
|
||||
|
||||
console.warn(stripIndent`
|
||||
|
||||
This will delete the above user.
|
||||
|
||||
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({
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Continue',
|
||||
default: false,
|
||||
});
|
||||
if (!confirm) {
|
||||
return util.shutdown();
|
||||
}
|
||||
|
||||
console.warn('Removing user\'s actions');
|
||||
|
||||
// Remove all the user's actions.
|
||||
await ActionModel
|
||||
.where({user_id: user.id})
|
||||
.setOptions({multi: true})
|
||||
.remove();
|
||||
|
||||
console.warn('Removing user\'s comments');
|
||||
|
||||
// Remove all the user's comments.
|
||||
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});
|
||||
}
|
||||
|
||||
console.warn('Removing the user');
|
||||
|
||||
// Remove the user.
|
||||
await user.remove();
|
||||
|
||||
@@ -169,146 +99,90 @@ async function deleteUser(userID) {
|
||||
}
|
||||
}
|
||||
|
||||
function printUserAsTable(user) {
|
||||
let table = new Table({});
|
||||
|
||||
table.push(
|
||||
{'ID': user.id.gray},
|
||||
{'Username': user.username},
|
||||
{'Emails': user.profiles.filter(({provider}) => provider === 'local').map(({id})=> id)
|
||||
.join(', ')},
|
||||
{'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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the password for a user.
|
||||
* Searches for users based on their username and email address.
|
||||
*/
|
||||
function passwd(userID) {
|
||||
inquirer.prompt([
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
validate: validateRequired('Password is required')
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
validate: validateRequired('Confirm Password is required')
|
||||
async function searchUsers() {
|
||||
const ctx = Context.forSystem();
|
||||
const searchQuery = `
|
||||
query SearchUsers($value: String) {
|
||||
users(query: {value: $value}) {
|
||||
nodes {
|
||||
id
|
||||
username
|
||||
role
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
.then((answers) => {
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
throw new Error('Password mismatch');
|
||||
}
|
||||
`;
|
||||
|
||||
return UsersService.changePassword(userID, answers.password);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Password changed.');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user from the options array.
|
||||
*/
|
||||
function updateUser(userID, options) {
|
||||
const updates = [];
|
||||
|
||||
if (options.email && typeof options.email === 'string' && options.email.length > 0) {
|
||||
let q = UserModel.update({
|
||||
'id': userID,
|
||||
'profiles.provider': 'local'
|
||||
}, {
|
||||
$set: {
|
||||
'profiles.$.id': options.email
|
||||
}
|
||||
});
|
||||
|
||||
updates.push(q);
|
||||
}
|
||||
|
||||
if (options.name && typeof options.name === 'string' && options.name.length > 0) {
|
||||
let q = UserModel.update({
|
||||
'id': userID
|
||||
}, {
|
||||
$set: {
|
||||
username: options.name
|
||||
}
|
||||
});
|
||||
|
||||
updates.push(q);
|
||||
}
|
||||
|
||||
Promise
|
||||
.all(updates.map((q) => q.exec()))
|
||||
.then(() => {
|
||||
console.log(`User ${userID} updated.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all the users registered in the database.
|
||||
*/
|
||||
function listUsers() {
|
||||
UsersService
|
||||
.all()
|
||||
.then((users) => {
|
||||
let table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Username',
|
||||
'Profiles',
|
||||
'Roles',
|
||||
'Status',
|
||||
'State'
|
||||
]
|
||||
});
|
||||
|
||||
users.forEach((user) => {
|
||||
let state = user.disabled ? 'Disabled' : 'Enabled';
|
||||
const profile = user.profiles.find(({provider}) => provider === 'local');
|
||||
if (profile && profile.metadata && profile.metadata.confirmed_at) {
|
||||
state += ', Verified';
|
||||
} else {
|
||||
state += ', Unverified';
|
||||
try {
|
||||
const answers = await inquirer.prompt({
|
||||
type: 'autocomplete',
|
||||
name: 'userID',
|
||||
message: 'Search for a user',
|
||||
source: async (answers, value) => {
|
||||
if (value === null) {
|
||||
value = '';
|
||||
}
|
||||
|
||||
table.push([
|
||||
user.id,
|
||||
user.username,
|
||||
user.profiles.map((p) => p.provider).join(', '),
|
||||
user.roles.join(', '),
|
||||
user.status,
|
||||
state
|
||||
]);
|
||||
});
|
||||
const {data, errors} = await graphql(schema, searchQuery, {}, ctx, {
|
||||
value,
|
||||
});
|
||||
if (errors && errors.length > 0) {
|
||||
throw errors[0];
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
if (data.users === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two users using the specified ID's.
|
||||
* @param {String} dstUserID id of the user to which is the target of the merge
|
||||
* @param {String} srcUserID id of the user to which is the source of the merge
|
||||
*/
|
||||
function mergeUsers(dstUserID, srcUserID) {
|
||||
UsersService
|
||||
.mergeUsers(dstUserID, srcUserID)
|
||||
.then(() => {
|
||||
console.log(`User ${srcUserID} was merged into user ${dstUserID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
return data.users.nodes.map((user) => {
|
||||
const emails = user.profiles
|
||||
.filter(({provider}) => provider === 'local')
|
||||
.map(({id})=> id)
|
||||
.join(', ');
|
||||
|
||||
return {
|
||||
name: `${user.username} (${emails}) ${user.id.gray} - ${user.role.gray}`,
|
||||
value: user.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const {userID} = answers;
|
||||
const user = await UserModel.findOne({id: userID});
|
||||
|
||||
printUserAsTable(user);
|
||||
util.shutdown(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,127 +190,75 @@ function mergeUsers(dstUserID, srcUserID) {
|
||||
* @param {String} userUD id of the user to add the role to
|
||||
* @param {String} role the role to add
|
||||
*/
|
||||
function addRole(userID, role) {
|
||||
async function setUserRole(userID) {
|
||||
try {
|
||||
const {role} = await inquirer.prompt([
|
||||
{
|
||||
name: 'role',
|
||||
message: 'User Role',
|
||||
type: 'list',
|
||||
choices: USER_ROLES
|
||||
}
|
||||
]);
|
||||
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
console.error(`Role '${role}' is not supported. Supported roles are ${USER_ROLES.join(', ')}.`);
|
||||
await UsersService.setRole(userID, role);
|
||||
|
||||
console.log(`Set User ${userID} to the ${role} role.`);
|
||||
util.shutdown();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
UsersService
|
||||
.addRoleToUser(userID, role)
|
||||
.then(() => {
|
||||
console.log(`Added the ${role} role to User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a role from a user
|
||||
* @param {String} userUD id of the user to remove the role from
|
||||
* @param {String} role the role to remove
|
||||
*/
|
||||
function removeRole(userID, role) {
|
||||
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
console.error(`Role '${role}' is not supported. Supported roles are ${USER_ROLES.join(', ')}.`);
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
UsersService
|
||||
.removeRoleFromUser(userID, role)
|
||||
.then(() => {
|
||||
console.log(`Removed the ${role} role from User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ban a user
|
||||
* @param {String} userID id of the user to ban
|
||||
*/
|
||||
function ban(userID) {
|
||||
UsersService
|
||||
.setStatus(userID, 'BANNED')
|
||||
.then(() => {
|
||||
console.log(`Banned the User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unban a user
|
||||
* @param {String} userUD id of the user to remove the role from
|
||||
*/
|
||||
function unban(userID) {
|
||||
UsersService
|
||||
.setStatus(userID, 'ACTIVE')
|
||||
.then(() => {
|
||||
console.log(`Unban the User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a given user.
|
||||
* @param {String} userID the ID of a user to disable
|
||||
*/
|
||||
function disableUser(userID) {
|
||||
UsersService
|
||||
.disableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was disabled.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enabled a given user.
|
||||
* @param {String} userID the ID of a user to enable
|
||||
*/
|
||||
function enableUser(userID) {
|
||||
UsersService
|
||||
.enableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was enabled.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies an email address for a user.
|
||||
*
|
||||
* @param userID the user's id
|
||||
* @param email the user's email address to be verified
|
||||
* @param email the user's email address to be verified, otherwise verifies the
|
||||
* first email if there is one, if there are multiple, you get a
|
||||
* prompt.
|
||||
*/
|
||||
async function verify(userID, email) {
|
||||
async function verifyUserEmail(userID, email) {
|
||||
try {
|
||||
|
||||
// Get the user.
|
||||
const user = await UserModel.findOne({id: userID});
|
||||
if (!user) {
|
||||
throw new Error(`user with ID ${userID} cannot be found`);
|
||||
}
|
||||
|
||||
// Get all the user's email addresses.
|
||||
const emails = user.profiles.filter(({provider}) => provider === 'local').map(({id}) => id);
|
||||
if (!emails || emails.length === 0) {
|
||||
throw new Error('user did not have any email addresses');
|
||||
}
|
||||
|
||||
if (!email && emails.length === 1) {
|
||||
|
||||
// The email wasn't passed, and there is only one option.
|
||||
email = emails[0];
|
||||
} 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
|
||||
}
|
||||
]);
|
||||
|
||||
email = answers.email;
|
||||
}
|
||||
|
||||
// Verify the email.
|
||||
await UsersService.confirmEmail(userID, email);
|
||||
console.log(`User ${userID} had their email ${email} verified.`);
|
||||
util.shutdown();
|
||||
@@ -450,77 +272,25 @@ async function verify(userID, email) {
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('create')
|
||||
.option('--email [email]', 'Email to use')
|
||||
.option('--password [password]', 'Password to use')
|
||||
.option('--name [name]', 'Name to use')
|
||||
.option('--role [role]', 'Role to add')
|
||||
.option('-f, --flag_mode', 'Source from flags instead of prompting')
|
||||
.description('create a new user')
|
||||
.action(createUser);
|
||||
|
||||
program
|
||||
.command('delete <userID>')
|
||||
.description('delete a user')
|
||||
.action(deleteUser);
|
||||
|
||||
program
|
||||
.command('passwd <userID>')
|
||||
.description('change a password for a user')
|
||||
.action(passwd);
|
||||
.command('search')
|
||||
.description('searches for a user based on their stored username and email')
|
||||
.action(searchUsers);
|
||||
|
||||
program
|
||||
.command('update <userID>')
|
||||
.option('--email [email]', 'Email to use')
|
||||
.option('--name [name]', 'Name to use')
|
||||
.description('update a user')
|
||||
.action(updateUser);
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description('list all the users in the database')
|
||||
.action(listUsers);
|
||||
|
||||
program
|
||||
.command('merge <dstUserID> <srcUserID>')
|
||||
.description('merge srcUser into the dstUser')
|
||||
.action(mergeUsers);
|
||||
|
||||
program
|
||||
.command('addrole <userID> <role>')
|
||||
.description('adds a role to a given user')
|
||||
.action(addRole);
|
||||
|
||||
program
|
||||
.command('removerole <userID> <role>')
|
||||
.description('removes a role from a given user')
|
||||
.action(removeRole);
|
||||
|
||||
program
|
||||
.command('ban <userID>')
|
||||
.description('ban a given user')
|
||||
.action(ban);
|
||||
|
||||
program
|
||||
.command('uban <userID>')
|
||||
.description('unban a given user')
|
||||
.action(unban);
|
||||
|
||||
program
|
||||
.command('disable <userID>')
|
||||
.description('disable a given user from logging in')
|
||||
.action(disableUser);
|
||||
|
||||
program
|
||||
.command('enable <userID>')
|
||||
.description('enable a given user from logging in')
|
||||
.action(enableUser);
|
||||
.command('set-role <userID> <role>')
|
||||
.description('sets the role on a user')
|
||||
.action(setUserRole);
|
||||
|
||||
program
|
||||
.command('verify <userID> <email>')
|
||||
.description('verifies the given user\'s email address')
|
||||
.action(verify);
|
||||
.action(verifyUserEmail);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
|
||||
+2
-2
@@ -4,9 +4,9 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const databaseVerifications = require('./verifications/database');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
const pkg = require('../package.json');
|
||||
const dotenv = require('dotenv');
|
||||
const fs = require('fs');
|
||||
const program = require('commander');
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
const parseArgs = require('minimist')(process.argv.slice(2), {
|
||||
alias: {
|
||||
'c': 'config'
|
||||
},
|
||||
string: [
|
||||
'config',
|
||||
'pid'
|
||||
],
|
||||
default: {
|
||||
'config': null,
|
||||
'pid': null
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* If the config flag is present, then we have to load the configuration from
|
||||
* the file specified. We will then load those values into the environment.
|
||||
*/
|
||||
if (parseArgs.config) {
|
||||
let envConfig = dotenv.parse(fs.readFileSync(parseArgs.config, {encoding: 'utf8'}));
|
||||
|
||||
Object.keys(envConfig).forEach((k) => {
|
||||
process.env[k] = envConfig[k];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If the pid flag is present, then we have to create a pid file at the location
|
||||
* specified.
|
||||
*/
|
||||
if (parseArgs.pid) {
|
||||
const util = require('./util');
|
||||
|
||||
console.log('Wrote PID');
|
||||
|
||||
util.pid(parseArgs.pid);
|
||||
}
|
||||
|
||||
module.exports = program
|
||||
.version(pkg.version)
|
||||
.option('-c, --config [path]', 'Specify the configuration file to load')
|
||||
.option('--pid [path]', 'Specify a path to output the current PID to');
|
||||
+11
-38
@@ -1,5 +1,8 @@
|
||||
|
||||
// Setup the environment.
|
||||
require('../services/env');
|
||||
|
||||
const debug = require('debug')('talk:util');
|
||||
const fs = require('fs');
|
||||
|
||||
const util = module.exports = {};
|
||||
|
||||
@@ -49,46 +52,16 @@ util.onshutdown = (jobs) => {
|
||||
util.toshutdown = util.toshutdown.concat(jobs);
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a PID file to be maintained for the lifespan of the process.
|
||||
* @param {String} path path to the PID file to create
|
||||
*/
|
||||
util.pid = (path) => {
|
||||
if (!/\//.test(path)) {
|
||||
if (!/\.pid/.test(path)) {
|
||||
path += '.pid';
|
||||
}
|
||||
path = `/tmp/${path}`;
|
||||
}
|
||||
|
||||
const pid = `${process.pid.toString()}\n`;
|
||||
|
||||
fs.writeFile(path, pid, (err) => {
|
||||
if (err) {
|
||||
console.error(`Can't write PID file: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Add the cleanup for the fs onto the shutdown.
|
||||
util.onshutdown([
|
||||
() => new Promise((resolve, reject) => {
|
||||
|
||||
// Remove the pid file.
|
||||
fs.unlink(path, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
})
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
// 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.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) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
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 debug = require('debug')('talk:cli:verify');
|
||||
|
||||
const MODELS = [
|
||||
UserModel,
|
||||
CommentModel,
|
||||
];
|
||||
|
||||
async function processBatch(Model, documents) {
|
||||
|
||||
// Get an array of all the document id's.
|
||||
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'));
|
||||
|
||||
// Iterate over the documents.
|
||||
for (let i = 0; i < documents.length; i++) {
|
||||
const document = documents[i];
|
||||
const actionSummaries = totalActionSummaries[i];
|
||||
|
||||
let ops = [];
|
||||
|
||||
for (const actionSummary of actionSummaries) {
|
||||
if (actionSummary.group_id === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// And we generate the group id.
|
||||
const ACTION_TYPE = actionSummary.action_type.toLowerCase();
|
||||
const GROUP_ID = actionSummary.group_id.toLowerCase();
|
||||
|
||||
if (GROUP_ID.length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// And we add a new batch operation if the action summary is associated
|
||||
// with a group.
|
||||
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) {
|
||||
|
||||
// Batch updates for those changes.
|
||||
ops.push({
|
||||
[`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Group all the action summaries together from all the different group
|
||||
// ids.
|
||||
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();
|
||||
|
||||
if (!(ACTION_TYPE in acc)) {
|
||||
acc[ACTION_TYPE] = 0;
|
||||
}
|
||||
|
||||
acc[ACTION_TYPE] += actionSummary.count;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If this comment has action summaries that should be updated, then
|
||||
// perform an update!
|
||||
if (ops.length > 0) {
|
||||
operations.push({
|
||||
updateOne: {
|
||||
filter: {
|
||||
id: document.id
|
||||
},
|
||||
update: {
|
||||
$set: Object.assign({}, ...ops),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
module.exports = async ({fix, batch}) => {
|
||||
for (const Model of MODELS) {
|
||||
const cursor = Model
|
||||
.collection
|
||||
.find({})
|
||||
.project({
|
||||
id: 1,
|
||||
action_counts: 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();
|
||||
|
||||
// Push the document into the documents array.
|
||||
documents.push(document);
|
||||
|
||||
// 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);
|
||||
|
||||
// Push the batch operations into the model operations.
|
||||
operations.push(...batchOperations);
|
||||
|
||||
// Clear this batch contents.
|
||||
documents = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Check to see if there are any documents left over.
|
||||
if (documents.length > 0) {
|
||||
|
||||
// Process this batch.
|
||||
let batchOperations = await processBatch(Model, documents);
|
||||
|
||||
// Push the batch operations into the model operations.
|
||||
operations.push(...batchOperations);
|
||||
}
|
||||
|
||||
const OPERATIONS_LENGTH = operations.length;
|
||||
|
||||
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}...`);
|
||||
|
||||
while (operations.length) {
|
||||
let result = await Model.collection.bulkWrite(operations.splice(0, batch));
|
||||
|
||||
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}.`);
|
||||
} else {
|
||||
console.warn('Skipping fixing, --fix was not enabled, pass --fix to fix these errors');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+1
-64
@@ -1,7 +1,5 @@
|
||||
const CommentModel = require('../../../models/comment');
|
||||
const ActionsService = require('../../../services/actions');
|
||||
const {arrayJoinBy, singleJoinBy} = require('../../../graph/loaders/util');
|
||||
const sc = require('snake-case');
|
||||
const {singleJoinBy} = require('../../../graph/loaders/util');
|
||||
const debug = require('debug')('talk:cli:verify');
|
||||
|
||||
const getBatch = async (limit, offset) => CommentModel
|
||||
@@ -55,15 +53,9 @@ module.exports = async ({fix, limit, batch}) => {
|
||||
.then(singleJoinBy(commentIDs, '_id'))
|
||||
.then((results) => results.map((result) => result ? result.count : 0));
|
||||
|
||||
// Get their action summaries.
|
||||
let allActionSummaries = await ActionsService
|
||||
.getActionSummaries(commentIDs)
|
||||
.then(arrayJoinBy(commentIDs, 'item_id'));
|
||||
|
||||
// Loop over the comments, with their action summaries.
|
||||
for (let i = 0; i < comments.length; i++) {
|
||||
let comment = comments[i];
|
||||
let actionSummaries = allActionSummaries[i];
|
||||
let replyCount = allReplyCounts[i];
|
||||
|
||||
// And check to see if the action summaries we just computed match what is
|
||||
@@ -77,61 +69,6 @@ module.exports = async ({fix, limit, batch}) => {
|
||||
});
|
||||
}
|
||||
|
||||
// First we process all the group id's.
|
||||
for (let actionSummary of actionSummaries) {
|
||||
if (actionSummary.group_id === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// And we generate the group id.
|
||||
const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase());
|
||||
const GROUP_ID = sc(actionSummary.group_id.toLowerCase());
|
||||
|
||||
if (GROUP_ID.length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// And we add a new batch operation if the action summary is associated
|
||||
// with a group.
|
||||
const ACTION_COUNT_FIELD = `${ACTION_TYPE}_${GROUP_ID}`;
|
||||
|
||||
// Check that the action summaries match the cached counts.
|
||||
if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== actionSummary.count) {
|
||||
|
||||
// Batch updates for those changes.
|
||||
commentOperations.push({
|
||||
[`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Group all the action summaries together from all the different group
|
||||
// ids.
|
||||
let groupedActionSummaries = actionSummaries.reduce((acc, actionSummary) => {
|
||||
const ACTION_TYPE = sc(actionSummary.action_type.toLowerCase());
|
||||
|
||||
if (!(ACTION_TYPE in acc)) {
|
||||
acc[ACTION_TYPE] = 0;
|
||||
}
|
||||
|
||||
acc[ACTION_TYPE] += actionSummary.count;
|
||||
|
||||
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 (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== count) {
|
||||
|
||||
// Batch updates for those changes.
|
||||
commentOperations.push({
|
||||
[`action_counts.${ACTION_COUNT_FIELD}`]: count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If this comment has action summaries that should be updated, then
|
||||
// perform an update!
|
||||
if (commentOperations.length > 0) {
|
||||
@@ -6,7 +6,8 @@
|
||||
//
|
||||
// async ({fix = false, batch = 1000}) => {}
|
||||
//
|
||||
// where their options are derrived.
|
||||
// where their options are derived.
|
||||
module.exports = [
|
||||
require('./comments'),
|
||||
require('./comment_replies'),
|
||||
require('./action_counts'),
|
||||
];
|
||||
|
||||
+5
-2
@@ -7,6 +7,8 @@ machine:
|
||||
environment:
|
||||
PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
|
||||
NODE_ENV: "test"
|
||||
MOCHA_FILE: "${CIRCLE_TEST_REPORTS}/junit/test-results.xml"
|
||||
MOCHA_REPORTER: "mocha-junit-reporter"
|
||||
pre:
|
||||
# TODO: use the following to add in support for MongoDB 3.4.
|
||||
# # Upgrade the database version to 3.4.
|
||||
@@ -47,10 +49,11 @@ database:
|
||||
test:
|
||||
override:
|
||||
# Run the tests using the junit reporter.
|
||||
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test
|
||||
- yarn test
|
||||
# Run the end to end tests
|
||||
- yarn e2e:ci
|
||||
# Check dependancies using nsp.
|
||||
- nsp check
|
||||
- yarn e2e-ci
|
||||
|
||||
deployment:
|
||||
release:
|
||||
|
||||
@@ -4,4 +4,3 @@ export const showBanUserDialog = ({userId, username, commentId, commentStatus})
|
||||
({type: SHOW_BAN_USER_DIALOG, userId, username, commentId, commentStatus});
|
||||
|
||||
export const hideBanUserDialog = () => ({type: HIDE_BAN_USER_DIALOG});
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
SORT_UPDATE,
|
||||
SET_PAGE,
|
||||
SET_SEARCH_VALUE,
|
||||
SET_ROLE,
|
||||
SET_COMMENTER_STATUS,
|
||||
SHOW_BANUSER_DIALOG,
|
||||
HIDE_BANUSER_DIALOG,
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
@@ -56,20 +54,6 @@ export const setSearchValue = (value) => ({
|
||||
value,
|
||||
});
|
||||
|
||||
export const setRole = (id, role) => (dispatch, _, {rest}) => {
|
||||
return rest(`/users/${id}/role`, {method: 'POST', body: {role}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_ROLE, id, role});
|
||||
});
|
||||
};
|
||||
|
||||
export const setCommenterStatus = (id, status) => (dispatch, _, {rest}) => {
|
||||
return rest(`/users/${id}/status`, {method: 'POST', body: {status}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_COMMENTER_STATUS, id, status});
|
||||
});
|
||||
};
|
||||
|
||||
// Ban User Dialog
|
||||
export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user});
|
||||
export const hideBanUserDialog = () => ({type: HIDE_BANUSER_DIALOG});
|
||||
|
||||
@@ -3,12 +3,12 @@ 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 changeUserDetailStatuses = (tab) => {
|
||||
export const changeTab = (tab) => {
|
||||
let statuses = null;
|
||||
if (tab === 'rejected') {
|
||||
statuses = ['REJECTED'];
|
||||
}
|
||||
return {type: actions.CHANGE_USER_DETAIL_STATUSES, tab, statuses};
|
||||
return {type: actions.CHANGE_TAB_USER_DETAIL, tab, statuses};
|
||||
};
|
||||
|
||||
export const clearUserDetailSelections = () => ({type: actions.CLEAR_USER_DETAIL_SELECTIONS});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
.table {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table, .headerRow, .row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.headerRowItem, .item {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
|
||||
&:nth-child(2) {
|
||||
flex: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.headerRowItem {
|
||||
color: #595959;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.headerRow, .row {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.action {
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {murmur3} from 'murmurhash-js';
|
||||
import styles from './AccountHistory.css';
|
||||
import cn from 'classnames';
|
||||
import flatten from 'lodash/flatten';
|
||||
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');
|
||||
};
|
||||
|
||||
const buildActionResponse = (typename, status) => {
|
||||
const actionResponses = {
|
||||
'UsernameStatusHistory' : `Username Status: ${status}`,
|
||||
'BannedStatusHistory': status ? 'User banned' : 'Ban removed',
|
||||
'SuspensionStatusHistory': status ? 'Account Suspended' : 'Suspension removed'
|
||||
};
|
||||
|
||||
return actionResponses[typename];
|
||||
};
|
||||
|
||||
class AccountHistory extends React.Component {
|
||||
render() {
|
||||
const {userState} = this.props;
|
||||
const userHistory = buildUserHistory(userState);
|
||||
return (
|
||||
<div>
|
||||
<div className={cn(styles.table, 'talk-admin-account-history')}>
|
||||
<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((h) => (
|
||||
<div className={cn(styles.row, 'talk-admin-account-history-row')} key={`${h.__typename}_${murmur3(h.created_at)}`}>
|
||||
<div className={cn(styles.item, 'talk-admin-account-history-row-date')}>
|
||||
{moment(new Date(h.created_at)).format('MMM DD, YYYY')}
|
||||
</div>
|
||||
<div className={cn(styles.item, styles.action, 'talk-admin-account-history-row-status')}>
|
||||
{buildActionResponse(h.__typename, h.status)}
|
||||
</div>
|
||||
<div className={cn(styles.item, 'talk-admin-account-history-row-assigned-by')}>
|
||||
{h.assigned_by ? h.assigned_by.username : 'SYSTEM'}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AccountHistory.propTypes = {
|
||||
history: PropTypes.array,
|
||||
userState: PropTypes.object,
|
||||
};
|
||||
|
||||
export default AccountHistory;
|
||||
@@ -8,9 +8,6 @@
|
||||
color: black;
|
||||
> :global(.mdl-menu__container) {
|
||||
margin-left: 10px;
|
||||
> :global(.mdl-menu__outline) {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +15,13 @@
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #616161;
|
||||
border-color: #616161;
|
||||
}
|
||||
|
||||
.arrowIcon {
|
||||
margin-left: 6px;
|
||||
margin-right: 0;
|
||||
vertical-align: middle;
|
||||
vertical-align: middle;
|
||||
margin-right: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -33,8 +31,10 @@
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
background-color: #2a2a2a;
|
||||
color: white;
|
||||
color: #2a2a2a;
|
||||
background-color: white;
|
||||
font-size: 0.95em;
|
||||
|
||||
&:first-child {
|
||||
margin-bottom: 1px;
|
||||
border-radius: 2px 2px 0px 0px;
|
||||
@@ -43,7 +43,8 @@
|
||||
border-radius: 0px 0px 2px 2px;
|
||||
}
|
||||
&:hover, &:active, &:focus {
|
||||
background-color: #767676;
|
||||
background-color: #e2e2e2;
|
||||
border-color: #616161;
|
||||
}
|
||||
&[disabled], &[disabled]:hover, &[disabled]:focus, &[disabled]:active {
|
||||
background-color: #262626;
|
||||
|
||||
@@ -32,18 +32,18 @@ class ActionsMenu extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {className = ''} = this.props;
|
||||
const {className = '', buttonClassNames = '', label = ''} = this.props;
|
||||
return (
|
||||
<div className={cn(styles.root, className)} onBlur={this.syncOpenState} >
|
||||
<Button
|
||||
cStyle='actions'
|
||||
className={cn(styles.button, {[styles.buttonOpen]: this.state.open})}
|
||||
className={cn(styles.button, {[styles.buttonOpen]: this.state.open}, buttonClassNames)}
|
||||
disabled={false}
|
||||
id={this.id}
|
||||
onClick={this.syncOpenState}
|
||||
icon={this.props.icon}
|
||||
raised>
|
||||
{t('modqueue.actions')}
|
||||
{label ? label : t('modqueue.actions')}
|
||||
<Icon
|
||||
name={this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
|
||||
className={styles.arrowIcon}
|
||||
@@ -61,6 +61,8 @@ ActionsMenu.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
buttonClassNames: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
|
||||
@@ -1,153 +1,153 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 500px;
|
||||
width: 400px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 184px;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: black;
|
||||
font-size: 1.76em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
.header {
|
||||
color: black;
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: black;
|
||||
font-size: 1.4em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
.subheader {
|
||||
color: black;
|
||||
font-size: 1.3em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.textField {
|
||||
margin-top: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.textField label {
|
||||
font-size: 1.08em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
font-size: 1.08em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.textField input {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 10px 6px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 10px 6px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer span {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.socialConnections {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.signInButton {
|
||||
margin-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #6b6b6b;
|
||||
color: #6b6b6b;
|
||||
}
|
||||
|
||||
input.error{
|
||||
border: solid 2px #f44336;
|
||||
border: solid 2px #f44336;
|
||||
}
|
||||
|
||||
.errorMsg, .hint {
|
||||
color: grey;
|
||||
font-weight: 600;
|
||||
padding: 3px 0 16px;
|
||||
color: grey;
|
||||
font-weight: 600;
|
||||
padding: 3px 0 16px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.alert--success {
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
}
|
||||
|
||||
.alert--error {
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
}
|
||||
|
||||
.userBox a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.attention {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: #B71C1C;
|
||||
color: #FFEBEE;
|
||||
font-weight: bolder;
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 9px;
|
||||
line-height: 7px;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: #B71C1C;
|
||||
color: #FFEBEE;
|
||||
font-weight: bolder;
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 9px;
|
||||
line-height: 7px;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
border: 1px solid green;
|
||||
background-color: lightgreen;
|
||||
padding: 10px;
|
||||
border: 1px solid green;
|
||||
background-color: lightgreen;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.passwordRequestFailure {
|
||||
border: 1px solid orange;
|
||||
background-color: 1px solid coral;
|
||||
padding: 10px;
|
||||
border: 1px solid orange;
|
||||
background-color: 1px solid coral;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
@@ -160,6 +160,20 @@ input.error{
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin: 20px;
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.legend {
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.messageInput {
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -7,39 +7,132 @@ import styles from './BanUserDialog.css';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const BanUserDialog = ({open, onCancel, onPerform, username, info}) => (
|
||||
<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>
|
||||
<div className={styles.header}>
|
||||
<h2>{t('bandialog.ban_user')}</h2>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h3>{t('bandialog.are_you_sure', username)}</h3>
|
||||
<i>{info}</i>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
className={cn(styles.cancel, 'talk-ban-user-dialog-button-cancel')}
|
||||
cStyle="cancel"
|
||||
onClick={onCancel}
|
||||
raised >
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(styles.ban, 'talk-ban-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={onPerform}
|
||||
raised >
|
||||
{t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
const initialState = {step: 0, message: ''};
|
||||
|
||||
class BanUserDialog extends React.Component {
|
||||
|
||||
state = initialState;
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
if (this.props.open && !next.open) {
|
||||
this.setState(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
handleMessageChange = (e) => {
|
||||
const {value: message} = e;
|
||||
this.setState({message});
|
||||
}
|
||||
|
||||
goToStep1 = () => {
|
||||
this.setState({
|
||||
step: 1,
|
||||
message: t(
|
||||
'bandialog.email_message_ban',
|
||||
this.props.username,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
renderStep0() {
|
||||
const {
|
||||
onCancel,
|
||||
username,
|
||||
info,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<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>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-cancel')}
|
||||
cStyle="white"
|
||||
onClick={onCancel}
|
||||
raised >
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={this.goToStep1}
|
||||
raised >
|
||||
{t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderStep1() {
|
||||
const {
|
||||
onCancel,
|
||||
onPerform,
|
||||
} = this.props;
|
||||
const {message} = this.state;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<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>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={styles.messageInput}
|
||||
value={message}
|
||||
onChange={this.handleMessageChange}
|
||||
/>
|
||||
</fieldset>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-cancel')}
|
||||
cStyle="white"
|
||||
onClick={onCancel}
|
||||
raised >
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={onPerform}
|
||||
raised >
|
||||
{t('bandialog.send')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
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>
|
||||
{step === 0 && this.renderStep0()}
|
||||
{step === 1 && this.renderStep1()}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BanUserDialog.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
|
||||
@@ -17,7 +17,7 @@ function getUserFlaggedType(actions) {
|
||||
.some((action) =>
|
||||
action.__typename === 'FlagAction' &&
|
||||
action.user &&
|
||||
action.user.roles.some((role) => staffRoles.includes(role))
|
||||
staffRoles.includes(action.user.role)
|
||||
) ? 'Staff' : 'User';
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ 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
|
||||
@@ -16,6 +16,7 @@ const LoadMore = ({loadMore, showLoadMore, className, ...rest}) =>
|
||||
</div>;
|
||||
|
||||
LoadMore.propTypes = {
|
||||
className: PropTypes.string,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
showLoadMore: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15);
|
||||
z-index: 10;
|
||||
z-index: 4;
|
||||
|
||||
.ctaHeader {
|
||||
font-size: 16px;
|
||||
|
||||
@@ -62,7 +62,7 @@ class SuspendUserDialog extends React.Component {
|
||||
const {onCancel, username} = this.props;
|
||||
const {duration} = this.state;
|
||||
return (
|
||||
<section>
|
||||
<section className="talk-admin-suspend-user-dialog-step-0">
|
||||
<h1 className={styles.header}>
|
||||
{t('suspenduser.title_suspend')}
|
||||
</h1>
|
||||
@@ -97,10 +97,10 @@ class SuspendUserDialog extends React.Component {
|
||||
}
|
||||
|
||||
renderStep1() {
|
||||
const {onCancel, username} = this.props;
|
||||
const {message} = this.state;
|
||||
const {onCancel, username} = this.props;
|
||||
return (
|
||||
<section>
|
||||
<section className="talk-admin-suspend-user-dialog-step-1">
|
||||
<h1 className={styles.header}>
|
||||
{t('suspenduser.title_notify')}
|
||||
</h1>
|
||||
|
||||
@@ -94,73 +94,52 @@
|
||||
width: calc(100% - 90px);
|
||||
}
|
||||
|
||||
.commentStatuses {
|
||||
.tabBar {
|
||||
padding: 0 0 0 10px;
|
||||
margin: 0;
|
||||
align-self: center;
|
||||
list-style: none;
|
||||
box-sizing: border-box;
|
||||
li {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
padding: 0 10px;
|
||||
}
|
||||
border: none;
|
||||
}
|
||||
|
||||
.active {
|
||||
.tab {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.tabButton {
|
||||
border: none;
|
||||
background: white;
|
||||
border-radius: 0;
|
||||
font-size: 1em;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tabButtonActive {
|
||||
font-weight: bold;
|
||||
border-bottom: 3px solid #F36451;
|
||||
}
|
||||
|
||||
.bulkActionGroup {
|
||||
height: 52px;
|
||||
background-color: #efefef;
|
||||
padding: 0 0 0 10px;
|
||||
display: flex;
|
||||
i {
|
||||
margin-right: 0;
|
||||
}
|
||||
.bulkAction {
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
transform: scale(.7);
|
||||
min-width: 0;
|
||||
}
|
||||
.bulkAction:last-child {
|
||||
margin-left: -10px;
|
||||
}
|
||||
.username {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.selectedCommentsInfo {
|
||||
align-self: center;
|
||||
font-weight: 500;
|
||||
margin-left: 15px;
|
||||
.actionsMenu {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.loadMore>button {
|
||||
background-color: #696969;
|
||||
&:hover {
|
||||
background-color: #404040;
|
||||
color: white;
|
||||
}
|
||||
.actionsMenuSuspended {
|
||||
background-color: #F29336;
|
||||
border-color: #F29336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toggleAll {
|
||||
padding: 0 10px 0 0;
|
||||
align-self: center;
|
||||
.actionsMenuBanned {
|
||||
background-color: #E45241;
|
||||
border-color: #E45241;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.commentList {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.bulkActionHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 52px;
|
||||
&.selected {
|
||||
background-color: #efefef;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,21 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import Comment from '../containers/UserDetailComment';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import {getErrorMessages} from 'coral-framework/utils';
|
||||
import styles from './UserDetail.css';
|
||||
import {Icon, Drawer, Spinner} from 'coral-ui';
|
||||
import AccountHistory from './AccountHistory';
|
||||
import {Slot} from 'coral-framework/components';
|
||||
import UserDetailCommentList from '../components/UserDetailCommentList';
|
||||
import {getReliability, isSuspended, isBanned} from 'coral-framework/utils/user';
|
||||
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import LoadMore from '../components/LoadMore';
|
||||
import cn from 'classnames';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import {getReliability} from 'coral-framework/utils/user';
|
||||
import ApproveButton from './ApproveButton';
|
||||
import RejectButton from './RejectButton';
|
||||
import {getErrorMessages} from 'coral-framework/utils';
|
||||
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';
|
||||
|
||||
export default class UserDetail extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
hideUserDetail: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
changeStatus: PropTypes.func.isRequired,
|
||||
toggleSelect: PropTypes.func.isRequired,
|
||||
bulkAccept: PropTypes.func.isRequired,
|
||||
bulkReject: PropTypes.func.isRequired,
|
||||
toggleSelectAll: PropTypes.func.isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
data: PropTypes.shape({
|
||||
refetch: PropTypes.func.isRequired,
|
||||
}),
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
loadMore: PropTypes.any.isRequired,
|
||||
notify: PropTypes.func.isRequired
|
||||
}
|
||||
class UserDetail extends React.Component {
|
||||
|
||||
rejectThenReload = async (info) => {
|
||||
try {
|
||||
@@ -82,13 +61,19 @@ export default class UserDetail extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
showAll = () => {
|
||||
this.props.changeStatus('all');
|
||||
changeTab = (tab) => {
|
||||
this.props.changeTab(tab);
|
||||
}
|
||||
|
||||
showRejected = () => {
|
||||
this.props.changeStatus('rejected');
|
||||
}
|
||||
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,
|
||||
});
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
@@ -100,15 +85,27 @@ export default class UserDetail extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
getActionMenuLabel() {
|
||||
const {root: {user}} = this.props;
|
||||
|
||||
if (isBanned(user)) {
|
||||
return 'Banned';
|
||||
} else if (isSuspended(user)) {
|
||||
return 'Suspended';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
renderLoaded() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
root: {
|
||||
me,
|
||||
user,
|
||||
totalComments,
|
||||
rejectedComments,
|
||||
comments: {nodes, hasNextPage}
|
||||
},
|
||||
activeTab,
|
||||
selectedCommentIds,
|
||||
@@ -116,20 +113,61 @@ export default class UserDetail extends React.Component {
|
||||
hideUserDetail,
|
||||
viewUserDetail,
|
||||
loadMore,
|
||||
toggleSelectAll
|
||||
toggleSelectAll,
|
||||
unbanUser,
|
||||
unsuspendUser,
|
||||
modal,
|
||||
} = this.props;
|
||||
|
||||
// if totalComments is 0, you're dividing by zero
|
||||
let rejectedPercent = (rejectedComments / totalComments) * 100;
|
||||
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
|
||||
|
||||
// if totalComments is 0, you're dividing by zero, which is naughty
|
||||
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
|
||||
rejectedPercent = 0;
|
||||
}
|
||||
|
||||
const banned = isBanned(user);
|
||||
const suspended = isSuspended(user);
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={hideUserDetail}>
|
||||
<Drawer onClose={hideUserDetail}>
|
||||
<h3>{user.username}</h3>
|
||||
<ClickOutside onClickOutside={modal ? null : hideUserDetail}>
|
||||
<Drawer className="talk-admin-user-detail-drawer" onClose={hideUserDetail}>
|
||||
<h3 className={cn(styles.username, 'talk-admin-user-detail-username')}>
|
||||
{user.username}
|
||||
</h3>
|
||||
|
||||
{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>}
|
||||
|
||||
</ActionsMenu>
|
||||
}
|
||||
|
||||
{(banned || suspended) && <UserInfoTooltip user={user} banned={banned} suspended={suspended} />}
|
||||
|
||||
<div>
|
||||
<ul className={styles.userDetailList}>
|
||||
@@ -173,65 +211,72 @@ export default class UserDetail extends React.Component {
|
||||
data={this.props.data}
|
||||
queryData={{root, user}}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
<div className={(selectedCommentIds.length > 0) ? cn(styles.bulkActionHeader, styles.selected) : styles.bulkActionHeader}>
|
||||
{
|
||||
selectedCommentIds.length === 0
|
||||
? (
|
||||
<ul className={styles.commentStatuses}>
|
||||
<li className={activeTab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
|
||||
<li className={activeTab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
|
||||
</ul>
|
||||
)
|
||||
: (
|
||||
<div className={styles.bulkActionGroup}>
|
||||
<ApproveButton
|
||||
onClick={this.bulkAcceptThenReload}
|
||||
minimal
|
||||
/>
|
||||
<RejectButton
|
||||
onClick={this.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>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.commentList}>
|
||||
{
|
||||
nodes.map((comment) => {
|
||||
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
|
||||
return <Comment
|
||||
key={comment.id}
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
comment={comment}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
selected={selected}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<LoadMore
|
||||
className={styles.loadMore}
|
||||
loadMore={loadMore}
|
||||
showLoadMore={hasNextPage}
|
||||
/>
|
||||
|
||||
<TabBar
|
||||
onTabClick={this.changeTab}
|
||||
activeTab={activeTab}
|
||||
className={cn(styles.tabBar, 'talk-admin-user-detail-tab-bar')}
|
||||
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')} >
|
||||
All
|
||||
</Tab>
|
||||
<Tab
|
||||
tabId={'rejected'}
|
||||
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')}>
|
||||
Account History
|
||||
</Tab>
|
||||
</TabBar>
|
||||
|
||||
<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}
|
||||
data={data}
|
||||
loadMore={loadMore}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
selectedCommentIds={selectedCommentIds}
|
||||
toggleSelectAll={toggleSelectAll}
|
||||
bulkAcceptThenReload={this.bulkAcceptThenReload}
|
||||
bulkRejectThenReload={this.bulkRejectThenReload}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tabId={'rejected'} className={'talk-admin-user-detail-rejected-tab-pane'}>
|
||||
<UserDetailCommentList
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
loadMore={loadMore}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
selectedCommentIds={selectedCommentIds}
|
||||
toggleSelectAll={toggleSelectAll}
|
||||
bulkAcceptThenReload={this.bulkAcceptThenReload}
|
||||
bulkRejectThenReload={this.bulkRejectThenReload}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tabId={'history'} className={'talk-admin-user-detail-history-tab-pane'}>
|
||||
<AccountHistory
|
||||
userState={user.state}
|
||||
/>
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</Drawer>
|
||||
</ClickOutside>
|
||||
);
|
||||
@@ -244,3 +289,32 @@ export default class UserDetail extends React.Component {
|
||||
return this.renderLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
UserDetail.propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
hideUserDetail: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
changeTab: PropTypes.func.isRequired,
|
||||
toggleSelect: PropTypes.func.isRequired,
|
||||
bulkAccept: PropTypes.func.isRequired,
|
||||
bulkReject: PropTypes.func.isRequired,
|
||||
toggleSelectAll: PropTypes.func.isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
data: PropTypes.shape({
|
||||
refetch: PropTypes.func.isRequired,
|
||||
}),
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
loadMore: PropTypes.any.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
modal: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default UserDetail;
|
||||
|
||||
@@ -117,6 +117,8 @@ class UserDetailComment extends React.Component {
|
||||
}
|
||||
|
||||
UserDetailComment.propTypes = {
|
||||
selected: PropTypes.bool,
|
||||
data: PropTypes.object,
|
||||
user: PropTypes.object.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
.commentList {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.loadMore > button {
|
||||
background-color: #696969;
|
||||
&:hover {
|
||||
background-color: #404040;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.bulkActionHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 52px;
|
||||
justify-content: flex-end;
|
||||
|
||||
&.selected {
|
||||
background-color: #efefef;
|
||||
}
|
||||
}
|
||||
|
||||
.bulkActionGroup {
|
||||
height: 52px;
|
||||
background-color: #efefef;
|
||||
padding: 0 0 0 10px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
i {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.bulkAction {
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
transform: scale(.7);
|
||||
min-width: 0;
|
||||
}
|
||||
.bulkAction:last-child {
|
||||
margin-left: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.selectedCommentsInfo {
|
||||
align-self: center;
|
||||
font-weight: 500;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.toggleAll {
|
||||
padding-right: 14px;
|
||||
align-self: center;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './UserDetailCommentList.css';
|
||||
import LoadMore from '../components/LoadMore';
|
||||
import Comment from '../containers/UserDetailComment';
|
||||
import RejectButton from './RejectButton';
|
||||
import ApproveButton from './ApproveButton';
|
||||
|
||||
const UserDetailCommentList = (props) => {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
root: {
|
||||
user,
|
||||
comments: {
|
||||
nodes,
|
||||
hasNextPage
|
||||
}
|
||||
},
|
||||
acceptComment,
|
||||
rejectComment,
|
||||
selectedCommentIds,
|
||||
toggleSelect,
|
||||
viewUserDetail,
|
||||
loadMore,
|
||||
toggleSelectAll,
|
||||
bulkAcceptThenReload,
|
||||
bulkRejectThenReload,
|
||||
} = 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}>
|
||||
{selectedCommentIds.length > 0 && (
|
||||
<div className={styles.bulkActionGroup}>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
nodes.map((comment) => {
|
||||
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
|
||||
return <Comment
|
||||
key={comment.id}
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
comment={comment}
|
||||
acceptComment={acceptComment}
|
||||
rejectComment={rejectComment}
|
||||
selected={selected}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>;
|
||||
})
|
||||
}
|
||||
<LoadMore
|
||||
className={styles.loadMore}
|
||||
loadMore={loadMore}
|
||||
showLoadMore={hasNextPage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
UserDetailCommentList.propTypes = {
|
||||
root: PropTypes.object.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
loadMore: PropTypes.any.isRequired,
|
||||
toggleSelect: PropTypes.func.isRequired,
|
||||
toggleSelectAll: PropTypes.func.isRequired,
|
||||
bulkAcceptThenReload: PropTypes.func.isRequired,
|
||||
bulkRejectThenReload: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default UserDetailCommentList;
|
||||
@@ -0,0 +1,69 @@
|
||||
.userInfo {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
color: #616161;
|
||||
-ms-user-select:none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout:none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color:rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu {
|
||||
background-color: white;
|
||||
border: solid 1px #999;
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
-webkit-box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
|
||||
z-index: 10;
|
||||
top: 32px;
|
||||
right: 0px;
|
||||
width: 140px;
|
||||
text-align: left;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.menu::before{
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: #999;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: -20px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.menu::after{
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: white;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: -19px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.descriptionList {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.strongItem {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.descriptionItem {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
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};
|
||||
|
||||
class UserInfoTooltip extends React.Component {
|
||||
state = initialState;
|
||||
|
||||
toogleMenu = () => {
|
||||
this.setState({menuVisible: !this.state.menuVisible});
|
||||
}
|
||||
|
||||
hideMenu = () => {
|
||||
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;
|
||||
|
||||
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')}>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
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>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserInfoTooltip.propTypes = {
|
||||
user: PropTypes.object,
|
||||
banned: PropTypes.bool,
|
||||
suspended: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default UserInfoTooltip;
|
||||
@@ -6,8 +6,6 @@ export const FETCH_USERS_FAILURE = `${prefix}_FETCH_USERS_FAILURE`;
|
||||
|
||||
export const SORT_UPDATE = `${prefix}_SORT_UPDATE`;
|
||||
export const SET_PAGE = `${prefix}_SET_PAGE`;
|
||||
export const SET_ROLE = `${prefix}_SET_ROLE`;
|
||||
export const SET_COMMENTER_STATUS = `${prefix}_SET_COMMENTER_STATUS`;
|
||||
|
||||
export const FETCH_FLAGGED_COMMENTERS_REQUEST = `${prefix}_FETCH_FLAGGED_COMMENTERS_REQUEST`;
|
||||
export const FETCH_FLAGGED_COMMENTERS_SUCCESS = `${prefix}_FETCH_FLAGGED_COMMENTERS_SUCCESS`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL';
|
||||
export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL';
|
||||
export const CHANGE_USER_DETAIL_STATUSES = 'CHANGE_USER_DETAIL_STATUSES';
|
||||
export const CHANGE_TAB_USER_DETAIL = 'CHANGE_TAB_USER_DETAIL';
|
||||
export const SELECT_USER_DETAIL_COMMENT = 'SELECT_USER_DETAIL_COMMENT';
|
||||
export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT';
|
||||
export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import BanUserDialog from '../components/BanUserDialog';
|
||||
import {hideBanUserDialog} from '../actions/banUserDialog';
|
||||
import {withSetUserStatus, withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
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';
|
||||
@@ -12,9 +13,9 @@ import {notify} from 'coral-framework/actions/notification';
|
||||
class BanUserDialogContainer extends Component {
|
||||
|
||||
banUser = async () => {
|
||||
const {userId, commentId, commentStatus, setUserStatus, setCommentStatus, hideBanUserDialog, notify} = this.props;
|
||||
const {userId, commentId, commentStatus, banUser, setCommentStatus, hideBanUserDialog, notify} = this.props;
|
||||
try {
|
||||
await setUserStatus({userId, status: 'BANNED'});
|
||||
await banUser({id: userId, message: ''});
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({commentId, status: 'REJECTED'});
|
||||
@@ -46,6 +47,14 @@ class BanUserDialogContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
BanUserDialogContainer.propTypes = {
|
||||
banUser: PropTypes.func.isRequired,
|
||||
hideBanUserDialog: PropTypes.func,
|
||||
open: PropTypes.bool,
|
||||
username: PropTypes.string,
|
||||
commentStatus: PropTypes.string,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
|
||||
open,
|
||||
userId,
|
||||
@@ -62,7 +71,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
|
||||
export default compose(
|
||||
withSetUserStatus,
|
||||
withBanUser,
|
||||
withSetCommentStatus,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
|
||||
@@ -25,7 +25,7 @@ export default withFragments({
|
||||
}
|
||||
user {
|
||||
id
|
||||
roles
|
||||
role
|
||||
}
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
|
||||
@@ -14,7 +14,11 @@ export default withQuery(gql`
|
||||
})
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
state: {
|
||||
status: {
|
||||
username: [SET, CHANGED]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
`, {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
@@ -8,15 +9,17 @@ import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
import {
|
||||
viewUserDetail,
|
||||
hideUserDetail,
|
||||
changeUserDetailStatuses,
|
||||
changeTab,
|
||||
clearUserDetailSelections,
|
||||
toggleSelectCommentInUserDetail,
|
||||
toggleSelectAllCommentInUserDetail
|
||||
} from 'coral-admin/src/actions/userDetail';
|
||||
import {withSetCommentStatus} 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';
|
||||
|
||||
const commentConnectionFragment = gql`
|
||||
fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection {
|
||||
@@ -114,7 +117,7 @@ class UserDetailContainer extends React.Component {
|
||||
return <UserDetail
|
||||
bulkReject={this.bulkReject}
|
||||
bulkAccept={this.bulkAccept}
|
||||
changeStatus={this.props.changeUserDetailStatuses}
|
||||
changeTab={this.props.changeTab}
|
||||
toggleSelect={this.props.toggleSelectCommentInUserDetail}
|
||||
toggleSelectAll={this.props.toggleSelectAllCommentInUserDetail}
|
||||
acceptComment={this.acceptComment}
|
||||
@@ -125,6 +128,19 @@ class UserDetailContainer extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
UserDetailContainer.propTypes = {
|
||||
changeTab: PropTypes.func,
|
||||
toggleSelectCommentInUserDetail: PropTypes.func,
|
||||
toggleSelectAllCommentInUserDetail: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
setCommentStatus: PropTypes.func,
|
||||
clearUserDetailSelections: PropTypes.func,
|
||||
selectedCommentIds: PropTypes.array,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
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}) {
|
||||
@@ -147,8 +163,45 @@ export const withUserDetailQuery = withQuery(gql`
|
||||
reliable {
|
||||
flagger
|
||||
}
|
||||
state {
|
||||
status {
|
||||
suspension {
|
||||
until
|
||||
history {
|
||||
until
|
||||
created_at
|
||||
assigned_by {
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
banned {
|
||||
status
|
||||
history {
|
||||
status
|
||||
assigned_by {
|
||||
username
|
||||
}
|
||||
created_at
|
||||
}
|
||||
}
|
||||
username {
|
||||
status
|
||||
history {
|
||||
status
|
||||
assigned_by {
|
||||
username
|
||||
}
|
||||
created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'user')}
|
||||
}
|
||||
me {
|
||||
id
|
||||
}
|
||||
totalComments: commentCount(query: {author_id: $author_id, statuses: []})
|
||||
rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]})
|
||||
comments: comments(query: {
|
||||
@@ -177,11 +230,14 @@ const mapStateToProps = (state) => ({
|
||||
selectedCommentIds: state.userDetail.selectedCommentIds,
|
||||
statuses: state.userDetail.statuses,
|
||||
activeTab: state.userDetail.activeTab,
|
||||
modal: state.ui.modal
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
changeUserDetailStatuses,
|
||||
showBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
changeTab,
|
||||
clearUserDetailSelections,
|
||||
toggleSelectCommentInUserDetail,
|
||||
viewUserDetail,
|
||||
@@ -195,4 +251,6 @@ export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withUserDetailQuery,
|
||||
withSetCommentStatus,
|
||||
withUnbanUser,
|
||||
withUnsuspendUser,
|
||||
)(UserDetailContainer);
|
||||
|
||||
@@ -1,35 +1,154 @@
|
||||
import update from 'immutability-helper';
|
||||
import {mapLeaves} from 'coral-framework/utils';
|
||||
import {gql} from 'react-apollo';
|
||||
|
||||
const userStatusFragment = gql`
|
||||
fragment Talk_UpdateUserStatus on User {
|
||||
state {
|
||||
status {
|
||||
banned {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const userRoleFragment = gql`
|
||||
fragment Talk_UpdateUserRole on User {
|
||||
role
|
||||
}`;
|
||||
|
||||
export default {
|
||||
mutations: {
|
||||
SetUserStatus: ({variables: {status, userId}}) => ({
|
||||
SetUserRole: ({variables: {id, role}}) => ({
|
||||
update: (proxy) => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = proxy.readFragment({fragment: userRoleFragment, id: fragmentId});
|
||||
|
||||
const updated = update(data, {
|
||||
role: {
|
||||
$set: role
|
||||
}
|
||||
});
|
||||
|
||||
proxy.writeFragment({fragment: userRoleFragment, id: fragmentId, data: updated});
|
||||
},
|
||||
}),
|
||||
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}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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 : {
|
||||
status: {
|
||||
suspension: {
|
||||
until: {$set: null}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
proxy.writeFragment({fragment: userStatusFragment, id: fragmentId, data: updated});
|
||||
},
|
||||
}),
|
||||
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) => {
|
||||
if (status !== 'APPROVED') {
|
||||
if (!status) {
|
||||
return prev;
|
||||
}
|
||||
const updated = update(prev, {
|
||||
users: {
|
||||
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
|
||||
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== id)},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
}),
|
||||
RejectUsername: ({variables: {input: {id: userId}}}) => ({
|
||||
ApproveUsername: ({variables: {id}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_Community: (prev) => {
|
||||
const updated = update(prev, {
|
||||
users: {
|
||||
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
|
||||
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) => {
|
||||
@@ -42,4 +161,3 @@ export default {
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ import {
|
||||
SORT_UPDATE,
|
||||
SET_PAGE,
|
||||
SET_SEARCH_VALUE,
|
||||
SET_ROLE,
|
||||
SET_COMMENTER_STATUS,
|
||||
SHOW_BANUSER_DIALOG,
|
||||
HIDE_BANUSER_DIALOG,
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
@@ -60,27 +58,6 @@ export default function community (state = initialState, action) {
|
||||
...state,
|
||||
pagePeople: action.page,
|
||||
};
|
||||
case SET_ROLE : {
|
||||
const commenters = state.users;
|
||||
const idx = commenters.findIndex((el) => el.id === action.id);
|
||||
|
||||
commenters[idx].roles[0] = action.role;
|
||||
return {
|
||||
...state,
|
||||
users: commenters.map((id) => id),
|
||||
};
|
||||
}
|
||||
case SET_COMMENTER_STATUS: {
|
||||
const commenters = state.users;
|
||||
const idx = commenters.findIndex((el) => el.id === action.id);
|
||||
|
||||
commenters[idx].status = action.status;
|
||||
return {
|
||||
...state,
|
||||
users: commenters.map((id) => id),
|
||||
};
|
||||
|
||||
}
|
||||
case SORT_UPDATE :
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -8,6 +8,7 @@ import config from './config';
|
||||
import banUserDialog from './banUserDialog';
|
||||
import suspendUserDialog from './suspendUserDialog';
|
||||
import userDetail from './userDetail';
|
||||
import ui from './ui';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
@@ -20,4 +21,5 @@ export default {
|
||||
moderation,
|
||||
install,
|
||||
config,
|
||||
ui,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export default function banUserDialog(state = initialState, action) {
|
||||
...state,
|
||||
selectedCommentIds: [],
|
||||
};
|
||||
case actions.CHANGE_USER_DETAIL_STATUSES:
|
||||
case actions.CHANGE_TAB_USER_DETAIL:
|
||||
return {
|
||||
...state,
|
||||
activeTab: action.tab,
|
||||
|
||||
@@ -12,7 +12,11 @@ class Community extends Component {
|
||||
const activeTab = route.path === ':id' ? 'flagged' : route.path;
|
||||
|
||||
if (activeTab === 'people') {
|
||||
return <People community={community} />;
|
||||
return <People
|
||||
community={community}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,8 +13,6 @@ class FlaggedAccounts extends React.Component {
|
||||
const {
|
||||
users,
|
||||
loadMore,
|
||||
showBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
showRejectUsernameDialog,
|
||||
approveUser,
|
||||
me,
|
||||
@@ -48,8 +46,6 @@ class FlaggedAccounts extends React.Component {
|
||||
<FlaggedUser
|
||||
user={user}
|
||||
key={user.id}
|
||||
showBanUserDialog={showBanUserDialog}
|
||||
showSuspendUserDialog={showSuspendUserDialog}
|
||||
showRejectUsernameDialog={showRejectUsernameDialog}
|
||||
approveUser={approveUser}
|
||||
me={me}
|
||||
@@ -74,8 +70,6 @@ class FlaggedAccounts extends React.Component {
|
||||
FlaggedAccounts.propTypes = {
|
||||
users: PropTypes.object,
|
||||
loadMore: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showRejectUsernameDialog: PropTypes.func,
|
||||
approveUser: PropTypes.func,
|
||||
me: PropTypes.object,
|
||||
|
||||
@@ -4,8 +4,6 @@ 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 ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
|
||||
@@ -19,18 +17,10 @@ const shortReasons = {
|
||||
|
||||
class User extends React.Component {
|
||||
|
||||
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
|
||||
userId: this.props.user.id,
|
||||
username: this.props.user.username,
|
||||
});
|
||||
|
||||
showBanUserDialog = () => this.props.showBanUserDialog({
|
||||
userId: this.props.user.id,
|
||||
username: this.props.user.username,
|
||||
});
|
||||
|
||||
viewAuthorDetail = () => this.props.viewUserDetail(this.props.user.id);
|
||||
|
||||
showRejectUsernameDialog = () => this.props.showRejectUsernameDialog({id: this.props.user.id});
|
||||
|
||||
approveUser = () => this.props.approveUser({
|
||||
userId: this.props.user.id,
|
||||
});
|
||||
@@ -40,7 +30,6 @@ class User extends React.Component {
|
||||
user,
|
||||
viewUserDetail,
|
||||
selected,
|
||||
me,
|
||||
className,
|
||||
} = this.props;
|
||||
|
||||
@@ -55,20 +44,6 @@ class User extends React.Component {
|
||||
className={styles.button}>
|
||||
{user.username}
|
||||
</button>
|
||||
{me.id !== user.id &&
|
||||
<ActionsMenu icon="not_interested">
|
||||
<ActionsMenuItem
|
||||
disabled={user.status === 'BANNED'}
|
||||
onClick={this.showSuspenUserDialog}>
|
||||
Suspend User
|
||||
</ActionsMenuItem>
|
||||
<ActionsMenuItem
|
||||
disabled={user.status === 'BANNED'}
|
||||
onClick={this.showBanUserDialog}>
|
||||
Ban User
|
||||
</ActionsMenuItem>
|
||||
</ActionsMenu>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn('talk-admin-community-flagged-user-body', styles.body)}>
|
||||
@@ -136,8 +111,6 @@ class User extends React.Component {
|
||||
}
|
||||
|
||||
User.propTypes = {
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
viewUserDetail: PropTypes.func,
|
||||
showRejectUsernameDialog: PropTypes.func,
|
||||
approveUser: PropTypes.func,
|
||||
|
||||
+67
-4
@@ -1,3 +1,45 @@
|
||||
.container {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
padding-bottom: 200px;
|
||||
}
|
||||
|
||||
.leftColumn {
|
||||
padding: 42px 56px;
|
||||
width: 234px;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
width: 100%;
|
||||
padding: 9px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
background: white;
|
||||
box-sizing: border-box;
|
||||
height: 40px;
|
||||
|
||||
i {
|
||||
color: #A1A1A1
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
padding: 0 2px 0 15px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
width: calc(100% - 300px);
|
||||
padding: 34px 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dataTable {
|
||||
width: 100%;
|
||||
border-left: none;
|
||||
@@ -8,10 +50,6 @@ th.header {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
th.header:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
th.header:nth-child(2), th.header:nth-child(3) {
|
||||
width: 100px;
|
||||
}
|
||||
@@ -67,3 +105,28 @@ th.header:nth-child(2), th.header:nth-child(3) {
|
||||
}
|
||||
}
|
||||
|
||||
.actionsMenu {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.actionsMenu .actionsMenuButton {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
font-size: 0.98em;
|
||||
}
|
||||
|
||||
.actionsMenuSuspended {
|
||||
background-color: #F29336;
|
||||
border-color: #F29336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.actionsMenuBanned {
|
||||
background-color: #E45241;
|
||||
border-color: #E45241;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.loadMore {
|
||||
margin-top: 24px;
|
||||
}
|
||||
@@ -1,75 +1,191 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import styles from './styles.css';
|
||||
import Table from './Table';
|
||||
import {Icon} from 'coral-ui';
|
||||
import styles from './People.css';
|
||||
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 moment from 'moment';
|
||||
|
||||
const People = (props) => {
|
||||
const {
|
||||
users = [],
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
onHeaderClickHandler,
|
||||
onPageChange,
|
||||
totalPages,
|
||||
page,
|
||||
setRole,
|
||||
setCommenterStatus,
|
||||
viewUserDetail,
|
||||
} = props;
|
||||
const headers = [
|
||||
{
|
||||
title: t('community.username_and_email'),
|
||||
field: 'username'
|
||||
},
|
||||
{
|
||||
title: t('community.account_creation_date'),
|
||||
field: 'created_at'
|
||||
},
|
||||
{
|
||||
title: t('community.status'),
|
||||
field: 'status'
|
||||
},
|
||||
{
|
||||
title: t('community.newsroom_role'),
|
||||
field: 'role'
|
||||
}
|
||||
];
|
||||
|
||||
const hasResults = !!users.length;
|
||||
class People extends React.Component {
|
||||
|
||||
return (
|
||||
<div className={cn(styles.container, 'talk-admin-community-people-container')}>
|
||||
<div className={styles.leftColumn}>
|
||||
<div className={styles.searchBox}>
|
||||
<Icon name='search' className={styles.searchIcon}/>
|
||||
<input
|
||||
id="commenters-search"
|
||||
type="text"
|
||||
className={styles.searchBoxInput}
|
||||
value={searchValue}
|
||||
onChange={onSearchChange}
|
||||
placeholder={t('streams.search')}
|
||||
/>
|
||||
getActionMenuLabel = (user) => {
|
||||
if (isBanned(user)) {
|
||||
return 'Banned';
|
||||
} else if (isSuspended(user)) {
|
||||
return 'Suspended';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
unsuspendUser = (input) => {
|
||||
this.props.unsuspendUser(input);
|
||||
}
|
||||
|
||||
unbanUser = (input) => {
|
||||
this.props.unbanUser(input);
|
||||
}
|
||||
|
||||
showBanUserDialog = (input) => {
|
||||
this.props.showBanUserDialog(input);
|
||||
}
|
||||
|
||||
showSuspendUserDialog = (input) => {
|
||||
this.props.showSuspendUserDialog(input);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
onSearchChange,
|
||||
users = [],
|
||||
setUserRole,
|
||||
viewUserDetail,
|
||||
loadMore,
|
||||
} = this.props;
|
||||
|
||||
const hasResults = !!users.nodes.length;
|
||||
|
||||
return (
|
||||
<div className={cn(styles.container, 'talk-admin-community-people-container')}>
|
||||
<div className={styles.leftColumn}>
|
||||
<div className={styles.searchBox}>
|
||||
<Icon name='search' className={styles.searchIcon}/>
|
||||
<input
|
||||
id="commenters-search"
|
||||
type="text"
|
||||
className={styles.searchBoxInput}
|
||||
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>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<LoadMore
|
||||
className={styles.loadMore}
|
||||
loadMore={loadMore}
|
||||
showLoadMore={users.hasNextPage}
|
||||
/>
|
||||
</div>
|
||||
: <EmptyCard>{t('community.no_results')}</EmptyCard>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
{
|
||||
hasResults
|
||||
? <Table
|
||||
users={users}
|
||||
setRole={setRole}
|
||||
viewUserDetail={viewUserDetail}
|
||||
setCommenterStatus={setCommenterStatus}
|
||||
onHeaderClickHandler={onHeaderClickHandler}
|
||||
pageCount={totalPages}
|
||||
onPageChange={onPageChange}
|
||||
page={page}
|
||||
/>
|
||||
: <EmptyCard>{t('community.no_results')}</EmptyCard>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
People.propTypes = {
|
||||
onHeaderClickHandler: PropTypes.func,
|
||||
users: PropTypes.array,
|
||||
page: PropTypes.number.isRequired,
|
||||
searchValue: PropTypes.string,
|
||||
onSearchChange: PropTypes.func,
|
||||
totalPages: PropTypes.number,
|
||||
onPageChange: PropTypes.func,
|
||||
setCommenterStatus: PropTypes.func.isRequired,
|
||||
setRole: PropTypes.func.isRequired,
|
||||
users: PropTypes.object.isRequired,
|
||||
onSearchChange: PropTypes.func.isRequired,
|
||||
setUserRole: PropTypes.func.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default People;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.modalButtons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 50px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.emailInput {
|
||||
|
||||
@@ -45,7 +45,7 @@ class RejectUsernameDialog extends Component {
|
||||
const next = () => this.setState({stage: stage + 1});
|
||||
const suspend = async () => {
|
||||
try {
|
||||
await rejectUsername({id: user.id, message: this.state.email});
|
||||
await rejectUsername(user.id);
|
||||
this.props.handleClose();
|
||||
} catch (err) {
|
||||
|
||||
@@ -70,7 +70,7 @@ class RejectUsernameDialog extends Component {
|
||||
const {stage} = this.state;
|
||||
|
||||
return <Dialog
|
||||
className={cn(styles.suspendDialog, 'talk-reject-username-dialog')}
|
||||
className={cn(styles.suspendDialog, 'talk-admin-reject-username-dialog')}
|
||||
id="rejectUsernameDialog"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
@@ -79,28 +79,31 @@ class RejectUsernameDialog extends Component {
|
||||
<div className={styles.title}>
|
||||
{t(stages[stage].title, t('reject_username.username'))}
|
||||
</div>
|
||||
<div className={styles.container}>
|
||||
<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 &&
|
||||
<div className={styles.writeContainer}>
|
||||
<div className={styles.emailMessage}>{t('reject_username.write_message')}</div>
|
||||
<div className={styles.emailContainer}>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={cn(styles.emailInput, 'talk-reject-username-dialog-suspension-message')}
|
||||
className={cn(styles.emailInput, 'talk-admin-reject-username-dialog-suspension-message')}
|
||||
value={this.state.email}
|
||||
onChange={this.onEmailChange}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className={cn(styles.modalButtons, 'talk-reject-username-dialog-buttons')}>
|
||||
} */}
|
||||
<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-reject-username-dialog-button', `talk-reject-username-dialog-button-${key}`)}
|
||||
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>
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './Table.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Paginate, Dropdown, Option} from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
|
||||
const headers = [
|
||||
{
|
||||
title: t('community.username_and_email'),
|
||||
field: 'username'
|
||||
},
|
||||
{
|
||||
title: t('community.account_creation_date'),
|
||||
field: 'created_at'
|
||||
},
|
||||
{
|
||||
title: t('community.status'),
|
||||
field: 'status'
|
||||
},
|
||||
{
|
||||
title: t('community.newsroom_role'),
|
||||
field: 'role'
|
||||
}
|
||||
];
|
||||
|
||||
const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUserDetail, pageCount, page, onPageChange}) => (
|
||||
<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"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
{header.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((row, i)=> (
|
||||
<tr key={i} className="talk-admin-community-people-row">
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<button onClick={() => {viewUserDetail(row.id);}} className={cn(styles.username, styles.button)}>{row.username}</button>
|
||||
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.created_at}
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
containerClassName="talk-admin-community-people-dd-status"
|
||||
value={row.status}
|
||||
placeholder={t('community.status')}
|
||||
onChange={(status) => setCommenterStatus(row.id, status)}>
|
||||
<Option value={'ACTIVE'} label={t('community.active')} />
|
||||
<Option value={'BANNED'} label={t('community.banned')} />
|
||||
</Dropdown>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
containerClassName="talk-admin-community-people-dd-role"
|
||||
value={row.roles[0] || ''}
|
||||
placeholder={t('community.role')}
|
||||
onChange={(role) => setRole(row.id, role)}>
|
||||
<Option value={''} label={t('community.none')} />
|
||||
<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>
|
||||
<Paginate
|
||||
pageCount={pageCount}
|
||||
page={page - 1}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Table.propTypes = {
|
||||
users: PropTypes.array,
|
||||
onHeaderClickHandler: PropTypes.func.isRequired,
|
||||
setRole: PropTypes.func.isRequired,
|
||||
setCommenterStatus: PropTypes.func.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
pageCount: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Table;
|
||||
@@ -240,3 +240,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
** This stylesheet is deprecated. DO NOT ADD CODE.
|
||||
**/
|
||||
|
||||
@@ -3,9 +3,10 @@ import {bindActionCreators} from 'redux';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
|
||||
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 Community from '../components/Community';
|
||||
|
||||
@@ -20,17 +21,25 @@ const mapDispatchToProps = (dispatch) =>
|
||||
|
||||
const withData = withQuery(gql`
|
||||
query TalkAdmin_Community {
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
})
|
||||
flaggedUsernamesCount: userCount(
|
||||
query:{
|
||||
action_type: FLAG,
|
||||
state: {
|
||||
status: {
|
||||
username: [SET, CHANGED]
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
...${getDefinitionName(FlaggedAccounts.fragments.root)}
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
...${getDefinitionName(People.fragments.root)}
|
||||
me {
|
||||
...${getDefinitionName(FlaggedUser.fragments.me)}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
${People.fragments.root}
|
||||
${FlaggedAccounts.fragments.root}
|
||||
${FlaggedUser.fragments.root}
|
||||
${FlaggedUser.fragments.me}
|
||||
@@ -42,7 +51,6 @@ const withData = withQuery(gql`
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSetUserStatus,
|
||||
withRejectUsername,
|
||||
withData
|
||||
)(Community);
|
||||
|
||||
@@ -5,10 +5,7 @@ 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 {withSetUserStatus} from 'coral-framework/graphql/mutations';
|
||||
import {showBanUserDialog} from 'actions/banUserDialog';
|
||||
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
|
||||
import {withApproveUsername} from 'coral-framework/graphql/mutations';
|
||||
import {showRejectUsernameDialog} from '../../../actions/community';
|
||||
import {viewUserDetail} from '../../../actions/userDetail';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
@@ -24,11 +21,8 @@ class FlaggedAccountsContainer extends Component {
|
||||
super(props);
|
||||
}
|
||||
|
||||
approveUser = ({userId}) => {
|
||||
return this.props.setUserStatus({
|
||||
userId,
|
||||
status: 'APPROVED'
|
||||
});
|
||||
approveUser = ({userId: id}) => {
|
||||
return this.props.approveUsername(id);
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
@@ -36,16 +30,16 @@ class FlaggedAccountsContainer extends Component {
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables: {
|
||||
limit: 5,
|
||||
cursor: this.props.root.users.endCursor,
|
||||
cursor: this.props.root.flaggedUsers.endCursor,
|
||||
},
|
||||
updateQuery: (previous, {fetchMoreResult:{users}}) => {
|
||||
updateQuery: (previous, {fetchMoreResult:{flaggedUsers}}) => {
|
||||
const updated = update(previous, {
|
||||
users: {
|
||||
flaggedUsers: {
|
||||
nodes: {
|
||||
$apply: (nodes) => appendNewNodes(nodes, users.nodes),
|
||||
$apply: (nodes) => appendNewNodes(nodes, flaggedUsers.nodes),
|
||||
},
|
||||
hasNextPage: {$set: users.hasNextPage},
|
||||
endCursor: {$set: users.endCursor},
|
||||
hasNextPage: {$set: flaggedUsers.hasNextPage},
|
||||
endCursor: {$set: flaggedUsers.endCursor},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
@@ -61,17 +55,16 @@ class FlaggedAccountsContainer extends Component {
|
||||
if (this.props.data.loading) {
|
||||
return <div><Spinner/></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FlaggedAccounts
|
||||
showBanUserDialog={this.props.showBanUserDialog}
|
||||
showSuspendUserDialog={this.props.showSuspendUserDialog}
|
||||
showRejectUsernameDialog={this.props.showRejectUsernameDialog}
|
||||
viewUserDetail={this.props.viewUserDetail}
|
||||
approveUser={this.approveUser}
|
||||
loadMore={this.loadMore}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
users={this.props.root.users}
|
||||
users={this.props.root.flaggedUsers}
|
||||
me={this.props.root.me}
|
||||
/>
|
||||
);
|
||||
@@ -79,18 +72,25 @@ class FlaggedAccountsContainer extends Component {
|
||||
}
|
||||
|
||||
FlaggedAccountsContainer.propTypes = {
|
||||
showBanUserDialog: PropTypes.func,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showRejectUsernameDialog: PropTypes.func,
|
||||
viewUserDetail: PropTypes.func,
|
||||
setUserStatus: PropTypes.func,
|
||||
approveUsername: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object
|
||||
root: PropTypes.object,
|
||||
};
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query TalkAdmin_LoadMoreFlaggedAccounts($limit: Int, $cursor: Cursor) {
|
||||
users(query:{action_type: FLAG, statuses: [PENDING], limit: $limit, cursor: $cursor}){
|
||||
flaggedUsers: users(query:{
|
||||
action_type: FLAG,
|
||||
state: {
|
||||
status: {
|
||||
username: [SET, CHANGED]
|
||||
}
|
||||
},
|
||||
limit: $limit,
|
||||
cursor: $cursor
|
||||
}){
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
@@ -104,19 +104,25 @@ const LOAD_MORE_QUERY = gql`
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
showBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
showRejectUsernameDialog,
|
||||
viewUserDetail,
|
||||
}, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withSetUserStatus,
|
||||
withApproveUsername,
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdminCommunity_FlaggedAccounts_root on RootQuery {
|
||||
users(query:{action_type: FLAG, statuses: [PENDING], limit: 10}){
|
||||
flaggedUsers: users(query:{
|
||||
action_type: FLAG,
|
||||
state: {
|
||||
status: {
|
||||
username: [SET, CHANGED]
|
||||
}
|
||||
}
|
||||
limit: 10
|
||||
}){
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
|
||||
@@ -17,8 +17,20 @@ export default withFragments({
|
||||
fragment TalkAdminCommunity_FlaggedUser_user on User {
|
||||
id
|
||||
username
|
||||
status
|
||||
roles
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
banned {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
role
|
||||
actions{
|
||||
id
|
||||
created_at
|
||||
|
||||
@@ -1,104 +1,230 @@
|
||||
import React from 'react';
|
||||
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 {
|
||||
fetchUsers,
|
||||
updateSorting,
|
||||
setPage,
|
||||
hideRejectUsernameDialog,
|
||||
setCommenterStatus,
|
||||
setRole,
|
||||
setSearchValue,
|
||||
} from '../../../actions/community';
|
||||
import {appendNewNodes} from 'plugin-api/beta/client/utils';
|
||||
import update from 'immutability-helper';
|
||||
import {Spinner} from 'coral-ui';
|
||||
|
||||
class PeopleContainer extends React.Component {
|
||||
timer=null;
|
||||
timer = null;
|
||||
|
||||
fetchUsers = (query = {}) => {
|
||||
const {community} = this.props;
|
||||
state = {
|
||||
searchValue: ''
|
||||
};
|
||||
|
||||
this.props.fetchUsers({
|
||||
value: community.searchValue,
|
||||
field: community.fieldPeople,
|
||||
asc: community.ascPeople,
|
||||
...query
|
||||
onSearchChange = (e) => {
|
||||
const {value} = e.target;
|
||||
this.setState({searchValue: value}, () => {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => {
|
||||
this.search(value);
|
||||
}, 350);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.fetchUsers();
|
||||
search = async (value) => {
|
||||
return this.props.data.fetchMore({
|
||||
query: SEARCH_QUERY,
|
||||
variables: {
|
||||
value,
|
||||
limit: 5,
|
||||
},
|
||||
updateQuery: (previous, {fetchMoreResult:{users}}) => {
|
||||
const updated = update(previous, {
|
||||
users: {
|
||||
nodes: {
|
||||
$set: users.nodes,
|
||||
},
|
||||
hasNextPage: {$set: users.hasNextPage},
|
||||
endCursor: {$set: users.endCursor},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
setUserRole = async (id, role) => {
|
||||
await this.props.setUserRole(id, role);
|
||||
}
|
||||
|
||||
onKeyDownHandler = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.fetchUsers();
|
||||
}
|
||||
}
|
||||
|
||||
onSearchChange = (e) => {
|
||||
const value = e.target.value;
|
||||
|
||||
this.props.setSearchValue(value);
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => {
|
||||
this.fetchUsers({value});
|
||||
}, 350);
|
||||
}
|
||||
|
||||
onHeaderClickHandler = (sort) => {
|
||||
this.props.updateSorting(sort);
|
||||
this.fetchUsers();
|
||||
}
|
||||
|
||||
onPageChange = ({selected}) => {
|
||||
const page = selected + 1;
|
||||
this.props.setPage(page);
|
||||
this.fetchUsers({page});
|
||||
}
|
||||
loadMore = () => {
|
||||
return this.props.data.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables: {
|
||||
value: this.state.searchValue,
|
||||
limit: 5,
|
||||
cursor: this.props.root.users.endCursor,
|
||||
},
|
||||
updateQuery: (previous, {fetchMoreResult:{users}}) => {
|
||||
const updated = update(previous, {
|
||||
users: {
|
||||
nodes: {
|
||||
$apply: (nodes) => appendNewNodes(nodes, users.nodes),
|
||||
},
|
||||
hasNextPage: {$set: users.hasNextPage},
|
||||
endCursor: {$set: users.endCursor},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (this.props.data.loading) {
|
||||
return <div><Spinner/></div>;
|
||||
}
|
||||
|
||||
return <People
|
||||
users={this.props.community.users}
|
||||
searchValue={this.props.community.searchValue}
|
||||
onSearchChange={this.onSearchChange}
|
||||
onHeaderClickHandler={this.onHeaderClickHandler}
|
||||
onPageChange={this.onPageChange}
|
||||
totalPages={this.props.community.totalPagesPeople}
|
||||
setCommenterStatus={this.props.setCommenterStatus}
|
||||
setRole={this.props.setRole}
|
||||
page={this.props.community.pagePeople}
|
||||
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}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
PeopleContainer.propTypes = {
|
||||
setPage: PropTypes.func,
|
||||
fetchUsers: PropTypes.func,
|
||||
updateSorting: PropTypes.func,
|
||||
setRole: PropTypes.func.isRequired,
|
||||
setCommenterStatus: PropTypes.func.isRequired,
|
||||
setSearchValue: PropTypes.func.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
community: PropTypes.object,
|
||||
setUserRole: PropTypes.func.isRequired,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
setPage,
|
||||
fetchUsers,
|
||||
updateSorting,
|
||||
hideRejectUsernameDialog,
|
||||
setCommenterStatus,
|
||||
setRole,
|
||||
viewUserDetail,
|
||||
setSearchValue,
|
||||
showSuspendUserDialog,
|
||||
showBanUserDialog,
|
||||
}, dispatch);
|
||||
|
||||
export default connect(null, mapDispatchToProps)(PeopleContainer);
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query TalkAdminCommunity_People_LoadMoreUsers($limit: Int, $cursor: Cursor, $value: String) {
|
||||
users(query: {
|
||||
value: $value,
|
||||
limit: $limit,
|
||||
cursor: $cursor
|
||||
}){
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
__typename
|
||||
id
|
||||
username
|
||||
role
|
||||
created_at
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
state {
|
||||
status {
|
||||
banned {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const SEARCH_QUERY = gql`
|
||||
query TalkAdminCommunity_People_SearchUsers($value: String, $limit: Int) {
|
||||
users(query: {
|
||||
value: $value,
|
||||
limit: $limit,
|
||||
}){
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
__typename
|
||||
id
|
||||
username
|
||||
role
|
||||
created_at
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
state {
|
||||
status {
|
||||
banned {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withSetUserRole,
|
||||
withUnsuspendUser,
|
||||
withUnbanUser,
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdminCommunity_People_root on RootQuery {
|
||||
users(query: {}){
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
__typename
|
||||
id
|
||||
username
|
||||
role
|
||||
created_at
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
state {
|
||||
status {
|
||||
banned {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
)(PeopleContainer);
|
||||
|
||||
@@ -5,6 +5,7 @@ import styles from './EmbedLink.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {BASE_URL} from 'coral-framework/constants/url';
|
||||
import ConfigureCard from 'coral-framework/components/ConfigureCard';
|
||||
import {stripIndent} from 'common-tags';
|
||||
|
||||
class EmbedLink extends Component {
|
||||
|
||||
@@ -23,17 +24,16 @@ class EmbedLink extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const coralJsUrl = join(BASE_URL, '/embed.js');
|
||||
const nonce = String(Math.random()).slice(2);
|
||||
const streamElementId = `coral_talk_${nonce}`;
|
||||
const embedText = `
|
||||
<div id="${streamElementId}"></div>
|
||||
<script src="${coralJsUrl}" async onload="
|
||||
Coral.Talk.render(document.getElementById('${streamElementId}'), {
|
||||
talk: '${BASE_URL}'
|
||||
});
|
||||
"></script>
|
||||
`.trim();
|
||||
const coralJsUrl = join(BASE_URL, '/static/embed.js');
|
||||
const embedText = stripIndent`
|
||||
<div id="coral_talk_stream"></div>
|
||||
<script src="${coralJsUrl}" async onload="
|
||||
Coral.Talk.render(document.getElementById('coral_talk_stream'), {
|
||||
talk: '${BASE_URL}'
|
||||
});
|
||||
"></script>
|
||||
`;
|
||||
|
||||
return (
|
||||
<ConfigureCard title={'Embed Comment Stream'}>
|
||||
<p>{t('configure.copy_and_paste')}</p>
|
||||
|
||||
@@ -8,8 +8,6 @@ import styles from './Comment.css';
|
||||
import CommentLabels from 'coral-admin/src/components/CommentLabels';
|
||||
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
@@ -19,10 +17,9 @@ import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
|
||||
class Comment extends React.Component {
|
||||
|
||||
ref = null;
|
||||
|
||||
handleRef = (ref) => this.ref = ref;
|
||||
handleRef = (ref) => (this.ref = ref);
|
||||
|
||||
handleFocusOrClick = () => {
|
||||
if (!this.props.selected) {
|
||||
@@ -30,40 +27,20 @@ class Comment extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
showSuspendUserDialog = () => {
|
||||
const {comment, showSuspendUserDialog} = this.props;
|
||||
return showSuspendUserDialog({
|
||||
userId: comment.user.id,
|
||||
username: comment.user.username,
|
||||
commentId: comment.id,
|
||||
commentStatus: comment.status,
|
||||
});
|
||||
};
|
||||
|
||||
showBanUserDialog = () => {
|
||||
const {comment, showBanUserDialog} = this.props;
|
||||
return showBanUserDialog({
|
||||
userId: comment.user.id,
|
||||
username: comment.user.username,
|
||||
commentId: comment.id,
|
||||
commentStatus: comment.status,
|
||||
});
|
||||
};
|
||||
|
||||
viewUserDetail = () => {
|
||||
const {viewUserDetail, comment} = this.props;
|
||||
return viewUserDetail(comment.user.id);
|
||||
};
|
||||
|
||||
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});
|
||||
|
||||
componentDidUpdate(prev) {
|
||||
if (!prev.selected && this.props.selected) {
|
||||
@@ -79,7 +56,6 @@ class Comment extends React.Component {
|
||||
data,
|
||||
root,
|
||||
root: {settings},
|
||||
currentUserId,
|
||||
currentAsset,
|
||||
clearHeightCache,
|
||||
dangling,
|
||||
@@ -91,7 +67,14 @@ class Comment extends React.Component {
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
className={cn(className, 'mdl-card', selectionStateCSS, styles.root, {[styles.selected]: selected, [styles.dangling]: dangling}, 'talk-admin-moderate-comment')}
|
||||
className={cn(
|
||||
className,
|
||||
'mdl-card',
|
||||
selectionStateCSS,
|
||||
styles.root,
|
||||
{[styles.selected]: selected, [styles.dangling]: dangling},
|
||||
'talk-admin-moderate-comment'
|
||||
)}
|
||||
id={`comment_${comment.id}`}
|
||||
onClick={this.handleFocusOrClick}
|
||||
ref={this.handleRef}
|
||||
@@ -100,38 +83,28 @@ class Comment extends React.Component {
|
||||
<div className={styles.container}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
{
|
||||
(
|
||||
<span className={styles.username} onClick={this.viewUserDetail}>
|
||||
{comment.user.username}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
<span
|
||||
className={cn(
|
||||
styles.username,
|
||||
'talk-admin-moderate-comment-username'
|
||||
)}
|
||||
onClick={this.viewUserDetail}
|
||||
>
|
||||
{comment.user.username}
|
||||
</span>
|
||||
|
||||
<span className={styles.created}>
|
||||
{timeago(comment.created_at)}
|
||||
</span>
|
||||
{
|
||||
(comment.editing && comment.editing.edited)
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
}
|
||||
{currentUserId !== comment.user.id &&
|
||||
<ActionsMenu icon="not_interested" className="talk-admin-moderate-comment-actions-menu">
|
||||
<ActionsMenuItem
|
||||
disabled={comment.user.status === 'BANNED'}
|
||||
onClick={this.showSuspendUserDialog}>
|
||||
Suspend User</ActionsMenuItem>
|
||||
<ActionsMenuItem
|
||||
disabled={comment.user.status === 'BANNED'}
|
||||
onClick={this.showBanUserDialog}>
|
||||
Ban User
|
||||
</ActionsMenuItem>
|
||||
</ActionsMenu>
|
||||
}
|
||||
{comment.editing && comment.editing.edited ? (
|
||||
<span>
|
||||
<span className={styles.editedMarker}>
|
||||
({t('comment.edited')})
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
<div className={styles.adminCommentInfoBar}>
|
||||
<CommentLabels
|
||||
comment={comment}
|
||||
/>
|
||||
<CommentLabels comment={comment} />
|
||||
<Slot
|
||||
fill="adminCommentInfoBar"
|
||||
data={data}
|
||||
@@ -144,8 +117,11 @@ class Comment extends React.Component {
|
||||
|
||||
<div className={styles.moderateArticle}>
|
||||
Story: {comment.asset.title}
|
||||
{!currentAsset &&
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
|
||||
{!currentAsset && (
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>
|
||||
{t('modqueue.moderate')}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<CommentAnimatedEdit body={comment.body}>
|
||||
<div className={styles.itemBody}>
|
||||
@@ -154,8 +130,7 @@ class Comment extends React.Component {
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
bannedWords={settings.wordlist.banned}
|
||||
body={comment.body}
|
||||
/>
|
||||
{' '}
|
||||
/>{' '}
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
@@ -216,8 +191,6 @@ Comment.propTypes = {
|
||||
className: PropTypes.string,
|
||||
dangling: PropTypes.bool,
|
||||
currentAsset: PropTypes.object,
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
clearHeightCache: PropTypes.func,
|
||||
comment: PropTypes.shape({
|
||||
@@ -229,12 +202,12 @@ Comment.propTypes = {
|
||||
created_at: PropTypes.string.isRequired,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
status: PropTypes.string
|
||||
status: PropTypes.string,
|
||||
}).isRequired,
|
||||
asset: PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
id: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
data: PropTypes.object.isRequired,
|
||||
|
||||
@@ -12,7 +12,6 @@ import Slot from 'coral-framework/components/Slot';
|
||||
import ViewOptions from './ViewOptions';
|
||||
|
||||
class Moderation extends Component {
|
||||
|
||||
state = {
|
||||
isLoadingMore: false,
|
||||
};
|
||||
@@ -27,13 +26,14 @@ class Moderation extends Component {
|
||||
key('t', () => this.nextQueue());
|
||||
key('f', () => this.moderate(false));
|
||||
key('d', () => this.moderate(true));
|
||||
this.getMenuItems()
|
||||
.forEach((menuItem, idx) => key(`${idx + 1}`, () => this.selectQueue(menuItem)));
|
||||
this.getMenuItems().forEach((menuItem, idx) =>
|
||||
key(`${idx + 1}`, () => this.selectQueue(menuItem))
|
||||
);
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.props.toggleModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
nextQueue = () => {
|
||||
const activeTab = this.props.activeTab;
|
||||
@@ -41,38 +41,45 @@ class Moderation extends Component {
|
||||
const menuItems = this.getMenuItems();
|
||||
|
||||
const activeTabIndex = menuItems.findIndex((item) => item === activeTab);
|
||||
const nextQueueIndex = (activeTabIndex === menuItems.length - 1) ? 0 : activeTabIndex + 1;
|
||||
const nextQueueIndex =
|
||||
activeTabIndex === menuItems.length - 1 ? 0 : activeTabIndex + 1;
|
||||
|
||||
this.selectQueue(menuItems[nextQueueIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
selectQueue = (key) => {
|
||||
const assetId = this.props.data.variables.asset_id;
|
||||
this.props.router.push(this.props.getModPath(key, assetId));
|
||||
}
|
||||
};
|
||||
|
||||
getMenuItems = () => Object.keys(this.props.queueConfig);
|
||||
|
||||
closeSearch = () => {
|
||||
const {toggleStorySearch} = this.props;
|
||||
toggleStorySearch(false);
|
||||
}
|
||||
};
|
||||
|
||||
openSearch = () => {
|
||||
this.props.toggleStorySearch(true);
|
||||
}
|
||||
};
|
||||
|
||||
getActiveTabCount = (props = this.props) => {
|
||||
return props.root[`${props.activeTab}Count`];
|
||||
}
|
||||
};
|
||||
|
||||
moderate = (accept) => {
|
||||
const {acceptComment, rejectComment, moderation: {selectedCommentId}} = this.props;
|
||||
const {
|
||||
acceptComment,
|
||||
rejectComment,
|
||||
moderation: {selectedCommentId},
|
||||
} = this.props;
|
||||
|
||||
// Accept or reject only if there's a selected comment
|
||||
if(selectedCommentId != null){
|
||||
if (selectedCommentId != null) {
|
||||
const comments = this.getComments();
|
||||
const commentIdx = comments.findIndex((comment) => comment.id === selectedCommentId);
|
||||
const commentIdx = comments.findIndex(
|
||||
(comment) => comment.id === selectedCommentId
|
||||
);
|
||||
const comment = comments[commentIdx];
|
||||
|
||||
if (accept) {
|
||||
@@ -81,12 +88,12 @@ class Moderation extends Component {
|
||||
comment.status !== 'REJECTED' && rejectComment({commentId: comment.id});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getComments = (props = this.props) => {
|
||||
const {root, activeTab} = props;
|
||||
return root[activeTab].nodes;
|
||||
}
|
||||
};
|
||||
|
||||
loadMore = async () => {
|
||||
if (!this.state.isLoadingMore) {
|
||||
@@ -95,14 +102,13 @@ class Moderation extends Component {
|
||||
const result = await this.props.loadMore(this.props.activeTab);
|
||||
this.setState({isLoadingMore: false});
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
this.setState({isLoadingMore: false});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
key.unbind('s');
|
||||
@@ -112,12 +118,21 @@ class Moderation extends Component {
|
||||
key.unbind('t');
|
||||
key.unbind('f');
|
||||
key.unbind('d');
|
||||
this.getMenuItems()
|
||||
.forEach((menuItem, idx) => key.unbind(`${idx + 1}`));
|
||||
this.getMenuItems().forEach((menuItem, idx) => key.unbind(`${idx + 1}`));
|
||||
}
|
||||
|
||||
render () {
|
||||
const {root, data, moderation, viewUserDetail, activeTab, getModPath, queueConfig, handleCommentChange, ...props} = this.props;
|
||||
render() {
|
||||
const {
|
||||
root,
|
||||
data,
|
||||
moderation,
|
||||
viewUserDetail,
|
||||
activeTab,
|
||||
getModPath,
|
||||
queueConfig,
|
||||
handleCommentChange,
|
||||
...props
|
||||
} = this.props;
|
||||
const {asset} = root;
|
||||
const assetId = asset && asset.id;
|
||||
|
||||
@@ -128,7 +143,7 @@ class Moderation extends Component {
|
||||
key: queue,
|
||||
name: queueConfig[queue].name,
|
||||
icon: queueConfig[queue].icon,
|
||||
count: root[`${queue}Count`]
|
||||
count: root[`${queue}Count`],
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -145,7 +160,9 @@ class Moderation extends Component {
|
||||
items={menuItems}
|
||||
activeTab={activeTab}
|
||||
/>
|
||||
<div className={cn(styles.container, 'talk-admin-moderation-container')}>
|
||||
<div
|
||||
className={cn(styles.container, 'talk-admin-moderation-container')}
|
||||
>
|
||||
<ViewOptions
|
||||
selectSort={this.props.setSortOrder}
|
||||
sort={this.props.moderation.sortOrder}
|
||||
@@ -159,9 +176,7 @@ class Moderation extends Component {
|
||||
hasNextPage={comments.hasNextPage}
|
||||
activeTab={activeTab}
|
||||
singleView={moderation.singleView}
|
||||
selectedCommentId={moderation.selectedCommentId}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
selectedCommentId={this.state.selectedCommentId}
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
loadMore={this.loadMore}
|
||||
@@ -192,7 +207,7 @@ class Moderation extends Component {
|
||||
queryData={{root, asset}}
|
||||
activeTab={activeTab}
|
||||
handleCommentChange={handleCommentChange}
|
||||
fill='adminModeration'
|
||||
fill="adminModeration"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -213,8 +228,6 @@ Moderation.propTypes = {
|
||||
commentBelongToQueue: PropTypes.func.isRequired,
|
||||
handleCommentChange: PropTypes.func.isRequired,
|
||||
setSortOrder: PropTypes.func.isRequired,
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
|
||||
@@ -7,7 +7,12 @@ import EmptyCard from '../../../components/EmptyCard';
|
||||
import AutoLoadMore from './AutoLoadMore';
|
||||
import ViewMore from './ViewMore';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {WindowScroller, CellMeasurer, CellMeasurerCache, List} from 'react-virtualized';
|
||||
import {
|
||||
WindowScroller,
|
||||
CellMeasurer,
|
||||
CellMeasurerCache,
|
||||
List,
|
||||
} from 'react-virtualized';
|
||||
import throttle from 'lodash/throttle';
|
||||
import key from 'keymaster';
|
||||
|
||||
@@ -49,7 +54,6 @@ function invalidateCursor(invalidated, state, props) {
|
||||
// getVisibileComments returns a list containing comments
|
||||
// which comes after the `idCursor`.
|
||||
function getVisibleComments(comments, idCursor) {
|
||||
|
||||
if (!comments) {
|
||||
return [];
|
||||
}
|
||||
@@ -97,8 +101,7 @@ class ModerationQueue extends React.Component {
|
||||
const view = this.state.view;
|
||||
if (index < view.length) {
|
||||
return view[index].id;
|
||||
}
|
||||
else if (index === view.length) {
|
||||
} else if (index === view.length) {
|
||||
return 'loadMore';
|
||||
}
|
||||
throw new Error(`unknown index ${index}`);
|
||||
@@ -133,7 +136,10 @@ class ModerationQueue extends React.Component {
|
||||
const {comments: nextComments} = next;
|
||||
|
||||
// New comments where added and our cursor list is incomplete.
|
||||
if (this.state.idCursors.length < 2 && nextComments.length > this.state.idCursors.length) {
|
||||
if (
|
||||
this.state.idCursors.length < 2 &&
|
||||
nextComments.length > this.state.idCursors.length
|
||||
) {
|
||||
this.setState(resetCursors(this.state, next));
|
||||
return;
|
||||
}
|
||||
@@ -142,17 +148,24 @@ class ModerationQueue extends React.Component {
|
||||
|
||||
// Comments have been removed.
|
||||
if (
|
||||
prevComments && nextComments &&
|
||||
prevComments &&
|
||||
nextComments &&
|
||||
nextComments.length < prevComments.length
|
||||
) {
|
||||
|
||||
// Invalidate first cursor if referenced comment was removed.
|
||||
if (this.state.idCursors[0] && !hasComment(nextComments, this.state.idCursors[0])) {
|
||||
if (
|
||||
this.state.idCursors[0] &&
|
||||
!hasComment(nextComments, this.state.idCursors[0])
|
||||
) {
|
||||
idCursors = invalidateCursor(0, this.state, next);
|
||||
}
|
||||
|
||||
// Invalidate second cursor if referenced comment was removed.
|
||||
if (this.state.idCursors[1] && !hasComment(nextComments, this.state.idCursors[1])) {
|
||||
if (
|
||||
this.state.idCursors[1] &&
|
||||
!hasComment(nextComments, this.state.idCursors[1])
|
||||
) {
|
||||
idCursors = invalidateCursor(1, this.state, next);
|
||||
}
|
||||
|
||||
@@ -165,10 +178,12 @@ class ModerationQueue extends React.Component {
|
||||
let nextSelectedCommentId = null;
|
||||
|
||||
// Determine a comment to select.
|
||||
const prevIndex = view.findIndex((comment) => comment.id === this.props.selectedCommentId);
|
||||
const prevIndex = view.findIndex(
|
||||
(comment) => comment.id === this.props.selectedCommentId
|
||||
);
|
||||
if (prevIndex !== view.length - 1) {
|
||||
nextSelectedCommentId = view[prevIndex + 1].id;
|
||||
} else if(prevIndex > 0) {
|
||||
} else if (prevIndex > 0) {
|
||||
nextSelectedCommentId = view[prevIndex - 1].id;
|
||||
}
|
||||
this.props.selectCommentId(nextSelectedCommentId);
|
||||
@@ -182,12 +197,14 @@ class ModerationQueue extends React.Component {
|
||||
|
||||
// TODO: removing or adding a comment from the list seems to render incorrect, is this a bug?
|
||||
// Find first changed comment and perform a reflow.
|
||||
const index = this.state.view.findIndex((comment, i) => !nextView[i] || nextView[i].id !== comment.id);
|
||||
const index = this.state.view.findIndex(
|
||||
(comment, i) => !nextView[i] || nextView[i].id !== comment.id
|
||||
);
|
||||
this.reflowList(index);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prev) {
|
||||
componentDidUpdate(prev) {
|
||||
const {commentCount, selectedCommentId} = this.props;
|
||||
|
||||
const switchedToMultiMode = prev.singleView && !this.props.singleView;
|
||||
@@ -213,13 +230,18 @@ class ModerationQueue extends React.Component {
|
||||
|
||||
// Returns comment counts without dangling comments.
|
||||
getCommentCountWithoutDagling(props = this.props) {
|
||||
return props.comments.filter((comment) => props.commentBelongToQueue(props.activeTab, comment)).length;
|
||||
return props.comments.filter((comment) =>
|
||||
props.commentBelongToQueue(props.activeTab, comment)
|
||||
).length;
|
||||
}
|
||||
|
||||
async selectDown() {
|
||||
const view = this.state.view;
|
||||
const index = view.findIndex(({id}) => id === this.props.selectedCommentId);
|
||||
if (index === view.length - 1 && this.getCommentCountWithoutDagling() !== this.props.commentCount) {
|
||||
if (
|
||||
index === view.length - 1 &&
|
||||
this.getCommentCountWithoutDagling() !== this.props.commentCount
|
||||
) {
|
||||
await this.props.loadMore();
|
||||
this.selectDown();
|
||||
return;
|
||||
@@ -268,17 +290,16 @@ class ModerationQueue extends React.Component {
|
||||
if (index >= 0) {
|
||||
cache.clear(index);
|
||||
this.listRef && this.listRef.recomputeRowHeights(index);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
cache.clearAll();
|
||||
this.listRef && this.listRef.recomputeRowHeights();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
rowRenderer = ({
|
||||
index, // Index of row within collection
|
||||
index, // Index of row within collection
|
||||
parent,
|
||||
style // Style object to be applied to row (to position it)
|
||||
style, // Style object to be applied to row (to position it)
|
||||
}) => {
|
||||
const view = this.state.view;
|
||||
const rowCount = view.length + 1;
|
||||
@@ -291,43 +312,41 @@ class ModerationQueue extends React.Component {
|
||||
if (index === rowCount - 1) {
|
||||
key = 'end-of-comment-list';
|
||||
child = (
|
||||
<div
|
||||
style={style}
|
||||
id={'end-of-comment-list'}
|
||||
>
|
||||
{this.props.hasNextPage && <AutoLoadMore
|
||||
loadMore={this.props.loadMore}
|
||||
loading={this.props.isLoadingMore}
|
||||
/>}
|
||||
<div style={style} id={'end-of-comment-list'}>
|
||||
{this.props.hasNextPage && (
|
||||
<AutoLoadMore
|
||||
loadMore={this.props.loadMore}
|
||||
loading={this.props.isLoadingMore}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const comment = view[index];
|
||||
|
||||
// Use callback cache so not to change the identity of these arrow functions.
|
||||
// Otherwise shallow compare will fail to optimize.
|
||||
if (!this.callbackCaches.clearHeightCache[index]) {
|
||||
this.callbackCaches.clearHeightCache[index] = () => this.reflowList(index);
|
||||
this.callbackCaches.clearHeightCache[index] = () =>
|
||||
this.reflowList(index);
|
||||
}
|
||||
if (!this.callbackCaches.selectCommentId[comment.id]) {
|
||||
this.callbackCaches.selectCommentId[comment.id] = () => this.props.selectCommentId(comment.id);
|
||||
this.callbackCaches.selectCommentId[comment.id] = () =>
|
||||
this.props.selectCommentId(comment.id);
|
||||
}
|
||||
|
||||
key = comment.id;
|
||||
child = (
|
||||
<div
|
||||
style={style}
|
||||
>
|
||||
<div style={style}>
|
||||
<Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
comment={comment}
|
||||
dangling={!this.props.commentBelongToQueue(this.props.activeTab, comment)}
|
||||
dangling={
|
||||
!this.props.commentBelongToQueue(this.props.activeTab, comment)
|
||||
}
|
||||
selected={comment.id === this.props.selectedCommentId}
|
||||
viewUserDetail={this.props.viewUserDetail}
|
||||
showBanUserDialog={this.props.showBanUserDialog}
|
||||
showSuspendUserDialog={this.props.showSuspendUserDialog}
|
||||
acceptComment={this.props.acceptComment}
|
||||
rejectComment={this.props.rejectComment}
|
||||
currentAsset={this.props.currentAsset}
|
||||
@@ -352,7 +371,7 @@ class ModerationQueue extends React.Component {
|
||||
);
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {
|
||||
comments,
|
||||
selectedCommentId,
|
||||
@@ -370,7 +389,9 @@ class ModerationQueue extends React.Component {
|
||||
}
|
||||
|
||||
if (singleView) {
|
||||
const index = comments.findIndex((comment) => comment.id === selectedCommentId);
|
||||
const index = comments.findIndex(
|
||||
(comment) => comment.id === selectedCommentId
|
||||
);
|
||||
const comment = comments[index];
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
@@ -381,8 +402,6 @@ class ModerationQueue extends React.Component {
|
||||
comment={comment}
|
||||
selected={true}
|
||||
viewUserDetail={viewUserDetail}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
currentAsset={props.currentAsset}
|
||||
@@ -428,18 +447,16 @@ class ModerationQueue extends React.Component {
|
||||
}
|
||||
|
||||
ModerationQueue.propTypes = {
|
||||
selectCommentId: PropTypes.func.isRequired,
|
||||
selectedCommentId: PropTypes.string,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
currentAsset: PropTypes.object,
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
selectCommentId: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
commentBelongToQueue: PropTypes.func.isRequired,
|
||||
cleanUpQueue: PropTypes.func.isRequired,
|
||||
commentCount: PropTypes.number.isRequired,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
selectedCommentId: PropTypes.string,
|
||||
singleView: PropTypes.bool,
|
||||
isLoadingMore: PropTypes.bool,
|
||||
hasNextPage: PropTypes.bool,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
height: 144px;
|
||||
min-height: auto;
|
||||
margin-top: 16px;
|
||||
z-index: 10;
|
||||
z-index: 4;
|
||||
|
||||
@media (--tablet) {
|
||||
width: 650px;
|
||||
|
||||
@@ -37,7 +37,6 @@ export default withFragments({
|
||||
user {
|
||||
id
|
||||
username
|
||||
status
|
||||
}
|
||||
asset {
|
||||
id
|
||||
|
||||
@@ -11,10 +11,12 @@ import NotFoundAsset from '../components/NotFoundAsset';
|
||||
import {isPremod, getModPath} from '../../../utils';
|
||||
|
||||
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
import {handleCommentChange, commentBelongToQueue, cleanUpQueue} from '../graphql';
|
||||
import {
|
||||
handleCommentChange,
|
||||
commentBelongToQueue,
|
||||
cleanUpQueue,
|
||||
} from '../graphql';
|
||||
|
||||
import {showBanUserDialog} from 'actions/banUserDialog';
|
||||
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
|
||||
import {viewUserDetail} from '../../../actions/userDetail';
|
||||
import {
|
||||
toggleModal,
|
||||
@@ -67,13 +69,15 @@ class ModerationContainer extends Component {
|
||||
};
|
||||
|
||||
get activeTab() {
|
||||
|
||||
const {root: {asset, settings}} = this.props;
|
||||
const id = getAssetId(this.props);
|
||||
const tab = getTab(this.props);
|
||||
|
||||
// Grab premod from asset or from settings if it's defined.
|
||||
const setting = id && asset && asset.settings ? asset.settings.moderation : settings.moderation;
|
||||
const setting =
|
||||
id && asset && asset.settings
|
||||
? asset.settings.moderation
|
||||
: settings.moderation;
|
||||
|
||||
const queue = isPremod(setting) ? 'premod' : 'new';
|
||||
const activeTab = tab ? tab : queue;
|
||||
@@ -86,60 +90,101 @@ class ModerationContainer extends Component {
|
||||
{
|
||||
document: COMMENT_ADDED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentAdded: comment}}}) => {
|
||||
updateQuery: (
|
||||
prev,
|
||||
{subscriptionData: {data: {commentAdded: comment}}}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_ACCEPTED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body));
|
||||
updateQuery: (
|
||||
prev,
|
||||
{subscriptionData: {data: {commentAccepted: comment}}}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_accepted',
|
||||
user.username,
|
||||
prepareNotificationText(comment.body)
|
||||
);
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_REJECTED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body));
|
||||
updateQuery: (
|
||||
prev,
|
||||
{subscriptionData: {data: {commentRejected: comment}}}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_rejected',
|
||||
user.username,
|
||||
prepareNotificationText(comment.body)
|
||||
);
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_RESET_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentReset: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_reset', user.username, prepareNotificationText(comment.body));
|
||||
updateQuery: (
|
||||
prev,
|
||||
{subscriptionData: {data: {commentReset: comment}}}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_reset',
|
||||
user.username,
|
||||
prepareNotificationText(comment.body)
|
||||
);
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_EDITED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => {
|
||||
updateQuery: (
|
||||
prev,
|
||||
{subscriptionData: {data: {commentEdited: comment}}}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_FLAGGED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => {
|
||||
updateQuery: (
|
||||
prev,
|
||||
{subscriptionData: {data: {commentFlagged: comment}}}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
this.subscriptions = parameters.map((param) => this.props.data.subscribeToMoreThrottled(param));
|
||||
this.subscriptions = parameters.map((param) =>
|
||||
this.props.data.subscribeToMoreThrottled(param)
|
||||
);
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
@@ -164,7 +209,9 @@ class ModerationContainer extends Component {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
||||
// Resubscribe when we change between assets.
|
||||
if(this.props.data.variables.asset_id !== nextProps.data.variables.asset_id) {
|
||||
if (
|
||||
this.props.data.variables.asset_id !== nextProps.data.variables.asset_id
|
||||
) {
|
||||
this.resubscribe(nextProps.data.variables);
|
||||
}
|
||||
}
|
||||
@@ -172,22 +219,27 @@ class ModerationContainer extends Component {
|
||||
cleanUpQueue = (queue) => {
|
||||
if (!this.props.data.loading) {
|
||||
this.props.data.updateQuery((query) => {
|
||||
return cleanUpQueue(query, queue, this.props.moderation.sortOrder, this.props.queueConfig);
|
||||
return cleanUpQueue(
|
||||
query,
|
||||
queue,
|
||||
this.props.moderation.sortOrder,
|
||||
this.props.queueConfig
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
acceptComment = ({commentId}) => {
|
||||
return this.props.setCommentStatus({commentId, status: 'ACCEPTED'});
|
||||
}
|
||||
};
|
||||
|
||||
rejectComment = ({commentId}) => {
|
||||
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
|
||||
}
|
||||
};
|
||||
|
||||
commentBelongToQueue = (queue, comment) => {
|
||||
return commentBelongToQueue(queue, comment, this.props.queueConfig);
|
||||
}
|
||||
};
|
||||
|
||||
loadMore = (tab) => {
|
||||
const variables = {
|
||||
@@ -202,7 +254,7 @@ class ModerationContainer extends Component {
|
||||
return this.props.data.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables,
|
||||
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
|
||||
updateQuery: (prev, {fetchMoreResult: {comments}}) => {
|
||||
return update(prev, {
|
||||
[tab]: {
|
||||
nodes: {$push: comments.nodes},
|
||||
@@ -210,11 +262,11 @@ class ModerationContainer extends Component {
|
||||
endCursor: {$set: comments.endCursor},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {root, root: {asset, settings}, data} = this.props;
|
||||
const assetId = getAssetId(this.props);
|
||||
|
||||
@@ -230,14 +282,15 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
if(data.loading) {
|
||||
if (data.loading) {
|
||||
|
||||
// loading.
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const premodEnabled = assetId ? isPremod(asset.settings.moderation) :
|
||||
isPremod(settings.moderation);
|
||||
const premodEnabled = assetId
|
||||
? isPremod(asset.settings.moderation)
|
||||
: isPremod(settings.moderation);
|
||||
|
||||
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
|
||||
|
||||
@@ -249,19 +302,21 @@ class ModerationContainer extends Component {
|
||||
delete currentQueueConfig.premod;
|
||||
}
|
||||
|
||||
return <Moderation
|
||||
{...this.props}
|
||||
getModPath={getModPath}
|
||||
loadMore={this.loadMore}
|
||||
acceptComment={this.acceptComment}
|
||||
rejectComment={this.rejectComment}
|
||||
activeTab={this.activeTab}
|
||||
queueConfig={currentQueueConfig}
|
||||
handleCommentChange={this.handleCommentChange}
|
||||
selectedCommentId={this.props.selectedCommentId}
|
||||
commentBelongToQueue={this.commentBelongToQueue}
|
||||
cleanUpQueue={this.cleanUpQueue}
|
||||
/>;
|
||||
return (
|
||||
<Moderation
|
||||
{...this.props}
|
||||
getModPath={getModPath}
|
||||
loadMore={this.loadMore}
|
||||
acceptComment={this.acceptComment}
|
||||
rejectComment={this.rejectComment}
|
||||
activeTab={this.activeTab}
|
||||
queueConfig={currentQueueConfig}
|
||||
handleCommentChange={this.handleCommentChange}
|
||||
selectedCommentId={this.props.selectedCommentId}
|
||||
commentBelongToQueue={this.commentBelongToQueue}
|
||||
cleanUpQueue={this.cleanUpQueue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
const COMMENT_ADDED_SUBSCRIPTION = gql`
|
||||
@@ -368,28 +423,57 @@ const commentConnectionFragment = gql`
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
const withModQueueQuery = withQuery(
|
||||
({queueConfig}) => gql`
|
||||
query CoralAdmin_Moderation($asset_id: ID, $sortOrder: SORT_ORDER, $allAssets: Boolean!, $nullStatuses: [COMMENT_STATUS!]) {
|
||||
${Object.keys(queueConfig).map((queue) => `
|
||||
${Object.keys(queueConfig).map(
|
||||
(queue) => `
|
||||
${queue}: comments(query: {
|
||||
statuses: ${queueConfig[queue].statuses ? `[${queueConfig[queue].statuses.join(', ')}],` : '$nullStatuses'}
|
||||
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
|
||||
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
|
||||
statuses: ${
|
||||
queueConfig[queue].statuses
|
||||
? `[${queueConfig[queue].statuses.join(', ')}],`
|
||||
: '$nullStatuses'
|
||||
}
|
||||
${
|
||||
queueConfig[queue].tags
|
||||
? `tags: ["${queueConfig[queue].tags.join('", "')}"],`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
queueConfig[queue].action_type
|
||||
? `action_type: ${queueConfig[queue].action_type}`
|
||||
: ''
|
||||
}
|
||||
asset_id: $asset_id,
|
||||
sortOrder: $sortOrder,
|
||||
limit: 20,
|
||||
}) {
|
||||
...CoralAdmin_Moderation_CommentConnection
|
||||
}
|
||||
`)}
|
||||
${Object.keys(queueConfig).map((queue) => `
|
||||
`
|
||||
)}
|
||||
${Object.keys(queueConfig).map(
|
||||
(queue) => `
|
||||
${queue}Count: commentCount(query: {
|
||||
statuses: ${queueConfig[queue].statuses ? `[${queueConfig[queue].statuses.join(', ')}],` : '$nullStatuses'}
|
||||
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
|
||||
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
|
||||
statuses: ${
|
||||
queueConfig[queue].statuses
|
||||
? `[${queueConfig[queue].statuses.join(', ')}],`
|
||||
: '$nullStatuses'
|
||||
}
|
||||
${
|
||||
queueConfig[queue].tags
|
||||
? `tags: ["${queueConfig[queue].tags.join('", "')}"],`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
queueConfig[queue].action_type
|
||||
? `action_type: ${queueConfig[queue].action_type}`
|
||||
: ''
|
||||
}
|
||||
asset_id: $asset_id,
|
||||
})
|
||||
`)}
|
||||
`
|
||||
)}
|
||||
asset(id: $asset_id) @skip(if: $allAssets) {
|
||||
id
|
||||
title
|
||||
@@ -409,20 +493,22 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
}
|
||||
${Comment.fragments.root}
|
||||
${commentConnectionFragment}
|
||||
`, {
|
||||
options: (props) => {
|
||||
const id = getAssetId(props);
|
||||
return {
|
||||
variables: {
|
||||
asset_id: id,
|
||||
sortOrder: props.moderation.sortOrder,
|
||||
allAssets: id === null,
|
||||
nullStatuses: null,
|
||||
},
|
||||
fetchPolicy: 'network-only'
|
||||
};
|
||||
},
|
||||
});
|
||||
`,
|
||||
{
|
||||
options: (props) => {
|
||||
const id = getAssetId(props);
|
||||
return {
|
||||
variables: {
|
||||
asset_id: id,
|
||||
sortOrder: props.moderation.sortOrder,
|
||||
allAssets: id === null,
|
||||
nullStatuses: null,
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
moderation: state.moderation,
|
||||
@@ -430,25 +516,26 @@ const mapStateToProps = (state) => ({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
toggleModal,
|
||||
singleView,
|
||||
showBanUserDialog,
|
||||
hideShortcutsNote,
|
||||
toggleStorySearch,
|
||||
showSuspendUserDialog,
|
||||
viewUserDetail,
|
||||
setSortOrder,
|
||||
storySearchChange,
|
||||
clearState,
|
||||
notify,
|
||||
selectCommentId,
|
||||
}, dispatch),
|
||||
...bindActionCreators(
|
||||
{
|
||||
toggleModal,
|
||||
singleView,
|
||||
hideShortcutsNote,
|
||||
toggleStorySearch,
|
||||
viewUserDetail,
|
||||
setSortOrder,
|
||||
storySearchChange,
|
||||
clearState,
|
||||
notify,
|
||||
selectCommentId,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
withQueueConfig(baseQueueConfig),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSetCommentStatus,
|
||||
withModQueueQuery,
|
||||
withModQueueQuery
|
||||
)(ModerationContainer);
|
||||
|
||||
@@ -11,4 +11,3 @@ export const isPremod = (mod) => mod === 'PRE';
|
||||
|
||||
export const getModPath = (type = 'all', assetId) =>
|
||||
assetId ? `/admin/moderate/${type}/${assetId}` : `/admin/moderate/${type}`;
|
||||
|
||||
|
||||
@@ -2,8 +2,13 @@ import jwtDecode from 'jwt-decode';
|
||||
import bowser from 'bowser';
|
||||
import * as actions from '../constants/auth';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import get from 'lodash/get';
|
||||
|
||||
export const updateStatus = (status) => ({
|
||||
type: actions.UPDATE_STATUS,
|
||||
status,
|
||||
});
|
||||
|
||||
export const showSignInDialog = () => ({
|
||||
type: actions.SHOW_SIGNIN_DIALOG,
|
||||
@@ -35,45 +40,19 @@ export const blurSignInDialog = () => ({
|
||||
type: actions.BLUR_SIGNIN_DIALOG,
|
||||
});
|
||||
|
||||
export const createUsernameRequest = () => ({
|
||||
type: actions.CREATE_USERNAME_REQUEST
|
||||
});
|
||||
export const showCreateUsernameDialog = () => ({
|
||||
type: actions.SHOW_CREATEUSERNAME_DIALOG
|
||||
});
|
||||
|
||||
export const hideCreateUsernameDialog = () => ({
|
||||
type: actions.HIDE_CREATEUSERNAME_DIALOG
|
||||
});
|
||||
|
||||
const createUsernameSuccess = () => ({
|
||||
type: actions.CREATE_USERNAME_SUCCESS
|
||||
});
|
||||
|
||||
const createUsernameFailure = (error) => ({
|
||||
type: actions.CREATE_USERNAME_FAILURE,
|
||||
error
|
||||
});
|
||||
|
||||
export const updateUsername = ({username}) => ({
|
||||
export const updateUsername = (username) => ({
|
||||
type: actions.UPDATE_USERNAME,
|
||||
username
|
||||
});
|
||||
|
||||
export const createUsername = (userId, formData) => (dispatch, _, {rest}) => {
|
||||
dispatch(createUsernameRequest());
|
||||
rest('/account/username', {method: 'PUT', body: formData})
|
||||
.then(() => {
|
||||
dispatch(createUsernameSuccess());
|
||||
dispatch(hideCreateUsernameDialog());
|
||||
dispatch(updateUsername(formData));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(createUsernameFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
export const changeView = (view) => (dispatch) => {
|
||||
dispatch({
|
||||
type: actions.CHANGE_VIEW,
|
||||
@@ -304,6 +283,8 @@ const checkLoginSuccess = (user, isAdmin) => ({
|
||||
isAdmin
|
||||
});
|
||||
|
||||
const ErrNotLoggedIn = new Error('Not logged in');
|
||||
|
||||
export const checkLogin = () => (dispatch, _, {rest, client, pym, storage}) => {
|
||||
dispatch(checkLoginRequest());
|
||||
rest('/auth')
|
||||
@@ -312,7 +293,7 @@ export const checkLogin = () => (dispatch, _, {rest, client, pym, storage}) => {
|
||||
if (storage) {
|
||||
storage.removeItem('token');
|
||||
}
|
||||
throw new Error('Not logged in');
|
||||
throw ErrNotLoggedIn;
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
@@ -321,13 +302,15 @@ export const checkLogin = () => (dispatch, _, {rest, client, pym, storage}) => {
|
||||
dispatch(checkLoginSuccess(result.user));
|
||||
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
|
||||
|
||||
// Display create username dialog if necessary.
|
||||
if (result.user.canEditName && result.user.status !== 'BANNED') {
|
||||
// This is for login via social. Usernames should be set.
|
||||
if (get(result.user, 'status.username.status') === 'UNSET' && !get(result.user, 'status.banned.status')) {
|
||||
dispatch(showCreateUsernameDialog());
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
if (error !== ErrNotLoggedIn) {
|
||||
console.error(error);
|
||||
}
|
||||
if (error.status && error.status === 401 && storage) {
|
||||
|
||||
// Unauthorized.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import React, {Component} from 'react';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBox';
|
||||
|
||||
class BannedAccount extends Component {
|
||||
render () {
|
||||
return (
|
||||
<RestrictedMessageBox>
|
||||
<span>
|
||||
<b>{t('framework.banned_account_header')}</b><br/> {t('framework.banned_account_body')}
|
||||
</span>
|
||||
</RestrictedMessageBox>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BannedAccount;
|
||||
@@ -54,3 +54,4 @@ export const SET_REQUIRE_EMAIL_VERIFICATION = 'SET_REQUIRE_EMAIL_VERIFICATION';
|
||||
export const SET_REDIRECT_URI = 'SET_REDIRECT_URI';
|
||||
|
||||
export const RESET_SIGNIN_DIALOG = 'RESET_SIGNIN_DIALOG';
|
||||
export const UPDATE_STATUS = 'UPDATE_STATUS';
|
||||
|
||||
@@ -21,7 +21,14 @@ import t from 'coral-framework/services/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import {setActiveTab} from '../actions/embed';
|
||||
|
||||
const {logout, checkLogin, focusSignInDialog, blurSignInDialog, hideSignInDialog} = authActions;
|
||||
const {
|
||||
logout,
|
||||
checkLogin,
|
||||
focusSignInDialog,
|
||||
blurSignInDialog,
|
||||
hideSignInDialog,
|
||||
updateStatus,
|
||||
} = authActions;
|
||||
const {fetchAssetSuccess} = assetActions;
|
||||
|
||||
class EmbedContainer extends React.Component {
|
||||
@@ -35,20 +42,23 @@ class EmbedContainer extends React.Component {
|
||||
if (props.auth.loggedIn) {
|
||||
const newSubscriptions = [{
|
||||
document: USER_BANNED_SUBSCRIPTION,
|
||||
updateQuery: () => {
|
||||
updateQuery: (_, {subscriptionData: {data: {userBanned: {state}}}}) => {
|
||||
notify('info', t('your_account_has_been_banned'));
|
||||
props.updateStatus(state.status);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: USER_SUSPENDED_SUBSCRIPTION,
|
||||
updateQuery: () => {
|
||||
updateQuery: (_, {subscriptionData: {data: {userSuspended: {state}}}}) => {
|
||||
notify('info', t('your_account_has_been_suspended'));
|
||||
props.updateStatus(state.status);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: USERNAME_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: () => {
|
||||
updateQuery: (_, {subscriptionData: {data: {usernameRejected: {state}}}}) => {
|
||||
notify('info', t('your_username_has_been_rejected'));
|
||||
props.updateStatus(state.status);
|
||||
},
|
||||
}];
|
||||
|
||||
@@ -116,10 +126,18 @@ const USER_BANNED_SUBSCRIPTION = gql`
|
||||
subscription UserBanned($user_id: ID!) {
|
||||
userBanned(user_id: $user_id){
|
||||
id
|
||||
status
|
||||
canEditName
|
||||
suspension {
|
||||
until
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
banned {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,10 +147,18 @@ const USER_SUSPENDED_SUBSCRIPTION = gql`
|
||||
subscription UserSuspended($user_id: ID!) {
|
||||
userSuspended(user_id: $user_id){
|
||||
id
|
||||
status
|
||||
canEditName
|
||||
suspension {
|
||||
until
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
banned {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,10 +168,18 @@ const USERNAME_REJECTED_SUBSCRIPTION = gql`
|
||||
subscription UsernameRejected($user_id: ID!) {
|
||||
usernameRejected(user_id: $user_id){
|
||||
id
|
||||
status
|
||||
canEditName
|
||||
suspension {
|
||||
until
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
banned {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,7 +204,19 @@ const EMBED_QUERY = gql`
|
||||
) {
|
||||
me {
|
||||
id
|
||||
status
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
banned {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
asset(id: $assetId, url: $assetUrl) {
|
||||
...${getDefinitionName(Configure.fragments.asset)}
|
||||
@@ -224,6 +270,7 @@ const mapDispatchToProps = (dispatch) =>
|
||||
focusSignInDialog,
|
||||
blurSignInDialog,
|
||||
hideSignInDialog,
|
||||
updateStatus,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import update from 'immutability-helper';
|
||||
import uuid from 'uuid/v4';
|
||||
import {insertCommentIntoEmbedQuery, removeCommentFromEmbedQuery} from './utils';
|
||||
import {
|
||||
insertCommentIntoEmbedQuery,
|
||||
removeCommentFromEmbedQuery,
|
||||
} from './utils';
|
||||
import {mapLeaves} from 'coral-framework/utils';
|
||||
|
||||
export default {
|
||||
@@ -45,7 +48,7 @@ export default {
|
||||
}
|
||||
}
|
||||
`,
|
||||
CreateDontAgreeResponse : gql`
|
||||
CreateDontAgreeResponse: gql`
|
||||
fragment CoralEmbedStream_CreateDontAgreeResponse on CreateDontAgreeResponse {
|
||||
dontagree {
|
||||
id
|
||||
@@ -143,13 +146,13 @@ export default {
|
||||
tag: {
|
||||
name: tag,
|
||||
created_at: new Date().toISOString(),
|
||||
__typename: 'Tag'
|
||||
__typename: 'Tag',
|
||||
},
|
||||
assigned_by: {
|
||||
id: auth.user.id,
|
||||
__typename: 'User'
|
||||
__typename: 'User',
|
||||
},
|
||||
__typename: 'TagLink'
|
||||
__typename: 'TagLink',
|
||||
})),
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
@@ -159,9 +162,9 @@ export default {
|
||||
tag: {
|
||||
name: tag,
|
||||
created_at: new Date().toISOString(),
|
||||
__typename: 'Tag'
|
||||
__typename: 'Tag',
|
||||
},
|
||||
__typename: 'TagLink'
|
||||
__typename: 'TagLink',
|
||||
})),
|
||||
status: 'NONE',
|
||||
replyCount: 0,
|
||||
@@ -171,9 +174,7 @@ export default {
|
||||
title: '',
|
||||
url: '',
|
||||
},
|
||||
parent: parent_id
|
||||
? {__typename: 'Comment', id: parent_id}
|
||||
: null,
|
||||
parent: parent_id ? {__typename: 'Comment', id: parent_id} : null,
|
||||
replies: {
|
||||
__typename: 'CommentConnection',
|
||||
nodes: [],
|
||||
@@ -188,13 +189,17 @@ export default {
|
||||
},
|
||||
status_history: [],
|
||||
id: `pending-${uuid()}`,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (prev, {mutationResult: {data: {createComment: {comment}}}}) => {
|
||||
CoralEmbedStream_Embed: (
|
||||
prev,
|
||||
{mutationResult: {data: {createComment: {comment}}}}
|
||||
) => {
|
||||
if (
|
||||
prev.me.roles.indexOf('ADMIN') === -1 && prev.asset.settings.moderation === 'PRE' ||
|
||||
(prev.me.role !== 'ADMIN' &&
|
||||
prev.asset.settings.moderation === 'PRE') ||
|
||||
comment.status === 'PREMOD' ||
|
||||
comment.status === 'REJECTED' ||
|
||||
comment.status === 'SYSTEM_WITHHELD'
|
||||
@@ -203,7 +208,10 @@ export default {
|
||||
}
|
||||
return insertCommentIntoEmbedQuery(prev, comment);
|
||||
},
|
||||
CoralEmbedStream_Profile: (prev, {mutationResult: {data: {createComment: {comment}}}}) => {
|
||||
CoralEmbedStream_Profile: (
|
||||
prev,
|
||||
{mutationResult: {data: {createComment: {comment}}}}
|
||||
) => {
|
||||
return update(prev, {
|
||||
me: {
|
||||
comments: {
|
||||
@@ -212,19 +220,24 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
},
|
||||
}),
|
||||
EditComment: () => ({
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (prev, {mutationResult: {data: {editComment: {comment}}}}) => {
|
||||
if (!['PREMOD', 'REJECTED', 'SYSTEM_WITHHELD'].includes(comment.status)) {
|
||||
CoralEmbedStream_Embed: (
|
||||
prev,
|
||||
{mutationResult: {data: {editComment: {comment}}}}
|
||||
) => {
|
||||
if (
|
||||
!['PREMOD', 'REJECTED', 'SYSTEM_WITHHELD'].includes(comment.status)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return removeCommentFromEmbedQuery(prev, comment.id);
|
||||
},
|
||||
},
|
||||
}),
|
||||
UpdateAssetSettings: ({variables: {input}}) => ({
|
||||
UpdateAssetSettings: ({variables: {input}}) => ({
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (prev) => {
|
||||
const updated = update(prev, {
|
||||
@@ -233,9 +246,8 @@ export default {
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as actions from '../constants/auth';
|
||||
import pym from 'coral-framework/services/pym';
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
@@ -198,6 +199,7 @@ export default function auth (state = initialState, action) {
|
||||
user: {
|
||||
...state.user,
|
||||
username: action.username,
|
||||
lowercaseUsername: action.username.toLowerCase(),
|
||||
}
|
||||
};
|
||||
case actions.VERIFY_EMAIL_FAILURE:
|
||||
@@ -227,35 +229,15 @@ export default function auth (state = initialState, action) {
|
||||
...state,
|
||||
redirectUri: action.uri,
|
||||
};
|
||||
case 'APOLLO_SUBSCRIPTION_RESULT':
|
||||
if (action.operationName === 'UserBanned' && state.getIn(['user', 'id']) === action.variables.user_id) {
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
...action.result.data.userBanned,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (action.operationName === 'UserSuspended' && state.getIn(['user', 'id']) === action.variables.user_id) {
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
...action.result.data.userSuspended,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (action.operationName === 'UsernameRejected' && state.getIn(['user', 'id']) === action.variables.user_id) {
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
...action.result.data.usernameRejected,
|
||||
},
|
||||
};
|
||||
}
|
||||
return state;
|
||||
case actions.UPDATE_STATUS: {
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
status: merge({}, state.user.status, action.status),
|
||||
},
|
||||
};
|
||||
}
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -15,23 +15,6 @@ export default function stream(state = initialState, action) {
|
||||
activeTab: action.tab,
|
||||
previousTab: state.activeTab,
|
||||
};
|
||||
case 'APOLLO_QUERY_INIT':
|
||||
if (action.queryString.indexOf('query CoralEmbedStream_Embed(') >= 0) {
|
||||
return {
|
||||
...state,
|
||||
refetching: action.isRefetch ? true : state.refetching,
|
||||
refetchRequestId: action.isRefetch ? action.requestId : state.refetchRequestId,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
case 'APOLLO_QUERY_RESULT':
|
||||
if (action.operationName === 'CoralEmbedStream_Embed') {
|
||||
return {
|
||||
...state,
|
||||
refetching: action.requestId === state.refetchRequestId ? false : state.refetching,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
.editNameInput {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px #d8d8d8;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-top: 10px;
|
||||
color: #B71C1C;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './ChangeUsername.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBox';
|
||||
import {forEachError} from 'plugin-api/beta/client/utils';
|
||||
|
||||
class ChangeUsername extends Component {
|
||||
|
||||
static propTypes = {
|
||||
changeUsername: PropTypes.func.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
username: '',
|
||||
alert: '',
|
||||
}
|
||||
|
||||
onSubmitClick = (e) => {
|
||||
const {changeUsername, user} = this.props;
|
||||
const {username} = this.state;
|
||||
e.preventDefault();
|
||||
|
||||
if (username === this.props.user.username) {
|
||||
this.setState({alert: t('error.SAME_USERNAME_PROVIDED')});
|
||||
}
|
||||
else if (validate.username(username)) {
|
||||
changeUsername(user.id, username)
|
||||
.then(() => location.reload())
|
||||
.catch((error) => {
|
||||
let errorMsg = '';
|
||||
forEachError(error, ({msg}) => errorMsg = errorMsg ? `${errorMsg}, ${msg}` : msg);
|
||||
this.setState({alert: errorMsg});
|
||||
});
|
||||
} else {
|
||||
this.setState({alert: t('framework.edit_name.error')});
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {username, alert} = this.state;
|
||||
|
||||
return <RestrictedMessageBox>
|
||||
<div className="talk-change-username">
|
||||
<span>
|
||||
{t('framework.edit_name.msg')}
|
||||
</span>
|
||||
<div className={styles.alert}>
|
||||
{alert}
|
||||
</div>
|
||||
<label
|
||||
htmlFor='username'
|
||||
className="screen-reader-text">
|
||||
{t('framework.edit_name.label')}
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
className={cn(styles.editNameInput, 'talk-change-username-username-input')}
|
||||
value={username}
|
||||
placeholder={t('framework.edit_name.label')}
|
||||
onChange={(e) => this.setState({username: e.target.value})}
|
||||
/><br/>
|
||||
<Button
|
||||
className="talk-change-username-submit-button"
|
||||
onClick={this.onSubmitClick} >
|
||||
{t('framework.edit_name.button')}
|
||||
</Button>
|
||||
</div>
|
||||
</RestrictedMessageBox>;
|
||||
}
|
||||
}
|
||||
|
||||
export default ChangeUsername;
|
||||
@@ -2,7 +2,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {StreamError} from './StreamError';
|
||||
import Comment from '../containers/Comment';
|
||||
import SuspendedAccount from './SuspendedAccount';
|
||||
import BannedAccount from '../../../components/BannedAccount';
|
||||
import ChangeUsername from '../containers/ChangeUsername';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import InfoBox from 'talk-plugin-infobox/InfoBox';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
@@ -15,6 +16,7 @@ import QuestionBox from '../../../components/QuestionBox';
|
||||
import {isCommentActive} from 'coral-framework/utils';
|
||||
import {Tab, TabCount, TabPane} from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import {getTopLevelParent, attachCommentToParent} from '../../../graphql/utils';
|
||||
import AllCommentsPane from './AllCommentsPane';
|
||||
@@ -225,19 +227,20 @@ class Stream extends React.Component {
|
||||
notify,
|
||||
updateItem,
|
||||
auth: {loggedIn, user},
|
||||
editName,
|
||||
} = this.props;
|
||||
const {keepCommentBox} = this.state;
|
||||
const open = !asset.isClosed;
|
||||
|
||||
const banned = user && user.status === 'BANNED';
|
||||
const banned = get(user, 'status.banned.status');
|
||||
const suspensionUntil = get(user, 'status.suspension.until');
|
||||
const rejectedUsername = get(user, 'status.username.status') === 'REJECTED';
|
||||
|
||||
const temporarilySuspended =
|
||||
user &&
|
||||
user.suspension.until &&
|
||||
new Date(user.suspension.until) > new Date();
|
||||
suspensionUntil &&
|
||||
new Date(suspensionUntil) > new Date();
|
||||
|
||||
const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox);
|
||||
const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !rejectedUsername && !highlightedComment) || keepCommentBox);
|
||||
const slotProps = {data};
|
||||
const slotQueryData = {root, asset};
|
||||
|
||||
@@ -270,15 +273,11 @@ class Stream extends React.Component {
|
||||
{t(
|
||||
'stream.temporarily_suspended',
|
||||
root.settings.organizationName,
|
||||
timeago(user.suspension.until)
|
||||
timeago(suspensionUntil)
|
||||
)}
|
||||
</RestrictedMessageBox>}
|
||||
{banned &&
|
||||
<SuspendedAccount
|
||||
canEditName={user && user.canEditName}
|
||||
editName={editName}
|
||||
currentUsername={user.username}
|
||||
/>}
|
||||
{!banned && rejectedUsername && <ChangeUsername user={user} />}
|
||||
{banned && <BannedAccount />}
|
||||
{showCommentBox &&
|
||||
<CommentBox
|
||||
notify={notify}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import {compose} from 'react-apollo';
|
||||
import {withChangeUsername} from 'coral-framework/graphql/mutations';
|
||||
import ChangeUsername from '../components/ChangeUsername';
|
||||
|
||||
export default compose(
|
||||
withChangeUsername,
|
||||
)(ChangeUsername);
|
||||
@@ -2,15 +2,25 @@ import React from 'react';
|
||||
import {gql, compose} from 'react-apollo';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {ADDTL_COMMENTS_ON_LOAD_MORE, THREADING_LEVEL} from '../../../constants/stream';
|
||||
import {
|
||||
withPostComment, withPostFlag, withPostDontAgree,
|
||||
withDeleteAction, withEditComment
|
||||
ADDTL_COMMENTS_ON_LOAD_MORE,
|
||||
THREADING_LEVEL,
|
||||
} from '../../../constants/stream';
|
||||
import {
|
||||
withPostComment,
|
||||
withPostFlag,
|
||||
withPostDontAgree,
|
||||
withDeleteAction,
|
||||
withEditComment,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
|
||||
import * as authActions from 'coral-embed-stream/src/actions/auth';
|
||||
import * as notificationActions from 'coral-framework/actions/notification';
|
||||
import {setActiveReplyBox, setActiveTab, viewAllComments} from '../../../actions/stream';
|
||||
import {
|
||||
setActiveReplyBox,
|
||||
setActiveTab,
|
||||
viewAllComments,
|
||||
} from '../../../actions/stream';
|
||||
import Stream from '../components/Stream';
|
||||
import Comment from './Comment';
|
||||
import {withFragments, withEmit} from 'coral-framework/hocs';
|
||||
@@ -43,7 +53,10 @@ class StreamContainer extends React.Component {
|
||||
|
||||
// Ignore mutations from me.
|
||||
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
|
||||
if (this.props.auth.user && commentEdited.user.id === this.props.auth.user.id) {
|
||||
if (
|
||||
this.props.auth.user &&
|
||||
commentEdited.user.id === this.props.auth.user.id
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
@@ -52,7 +65,11 @@ class StreamContainer extends React.Component {
|
||||
return prev;
|
||||
}
|
||||
|
||||
if (['PREMOD', 'REJECTED', 'SYSTEM_WITHHELD'].includes(commentEdited.status)) {
|
||||
if (
|
||||
['PREMOD', 'REJECTED', 'SYSTEM_WITHHELD'].includes(
|
||||
commentEdited.status
|
||||
)
|
||||
) {
|
||||
return removeCommentFromEmbedQuery(prev, commentEdited.id);
|
||||
}
|
||||
},
|
||||
@@ -69,14 +86,20 @@ class StreamContainer extends React.Component {
|
||||
|
||||
// Ignore mutations from me.
|
||||
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
|
||||
if (this.props.auth.user && commentAdded.user.id === this.props.auth.user.id) {
|
||||
if (
|
||||
this.props.auth.user &&
|
||||
commentAdded.user.id === this.props.auth.user.id
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// Exit if author is ignored.
|
||||
if (
|
||||
this.props.root.me &&
|
||||
this.props.root.me.ignoredUsers.some(({id}) => id === commentAdded.user.id)) {
|
||||
this.props.root.me.ignoredUsers.some(
|
||||
({id}) => id === commentAdded.user.id
|
||||
)
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
@@ -121,11 +144,11 @@ class StreamContainer extends React.Component {
|
||||
sortOrder: 'ASC',
|
||||
excludeIgnored: this.props.data.variables.excludeIgnored,
|
||||
},
|
||||
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
|
||||
updateQuery: (prev, {fetchMoreResult: {comments}}) => {
|
||||
return insertFetchedCommentsIntoEmbedQuery(prev, comments, parent_id);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadMoreComments = () => {
|
||||
return this.props.data.fetchMore({
|
||||
@@ -139,7 +162,7 @@ class StreamContainer extends React.Component {
|
||||
sortBy: this.props.data.variables.sortBy,
|
||||
excludeIgnored: this.props.data.variables.excludeIgnored,
|
||||
},
|
||||
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
|
||||
updateQuery: (prev, {fetchMoreResult: {comments}}) => {
|
||||
return insertFetchedCommentsIntoEmbedQuery(prev, comments);
|
||||
},
|
||||
});
|
||||
@@ -168,7 +191,10 @@ class StreamContainer extends React.Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.sortOrder !== nextProps.sortOrder || this.props.sortBy !== nextProps.sortBy) {
|
||||
if (
|
||||
this.props.sortOrder !== nextProps.sortOrder ||
|
||||
this.props.sortBy !== nextProps.sortBy
|
||||
) {
|
||||
nextProps.data.refetch();
|
||||
}
|
||||
}
|
||||
@@ -178,23 +204,26 @@ class StreamContainer extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.asset
|
||||
|| this.props.asset.comment === undefined
|
||||
&& !this.props.asset.comments
|
||||
if (
|
||||
!this.props.asset ||
|
||||
(this.props.asset.comment === undefined && !this.props.asset.comments)
|
||||
) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const streamLoading = this.props.refetching || this.props.data.loading;
|
||||
// @TODO: Detect refetch when we have apollo 2.0.
|
||||
const streamLoading = this.props.data.loading;
|
||||
|
||||
return <Stream
|
||||
{...this.props}
|
||||
loadMore={this.loadMore}
|
||||
loadMoreComments={this.loadMoreComments}
|
||||
loadNewReplies={this.loadNewReplies}
|
||||
userIsDegraged={this.userIsDegraged()}
|
||||
loading={streamLoading}
|
||||
/>;
|
||||
return (
|
||||
<Stream
|
||||
{...this.props}
|
||||
loadMore={this.loadMore}
|
||||
loadMoreComments={this.loadMoreComments}
|
||||
loadNewReplies={this.loadNewReplies}
|
||||
userIsDegraged={this.userIsDegraged()}
|
||||
loading={streamLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,8 +240,8 @@ const commentFragment = gql`
|
||||
`;
|
||||
|
||||
const COMMENTS_ADDED_SUBSCRIPTION = gql`
|
||||
subscription CommentAdded($assetId: ID!, $excludeIgnored: Boolean){
|
||||
commentAdded(asset_id: $assetId){
|
||||
subscription CommentAdded($assetId: ID!, $excludeIgnored: Boolean) {
|
||||
commentAdded(asset_id: $assetId) {
|
||||
parent {
|
||||
id
|
||||
}
|
||||
@@ -223,8 +252,8 @@ const COMMENTS_ADDED_SUBSCRIPTION = gql`
|
||||
`;
|
||||
|
||||
const COMMENTS_EDITED_SUBSCRIPTION = gql`
|
||||
subscription CommentEdited($assetId: ID!){
|
||||
commentEdited(asset_id: $assetId){
|
||||
subscription CommentEdited($assetId: ID!) {
|
||||
commentEdited(asset_id: $assetId) {
|
||||
id
|
||||
body
|
||||
status
|
||||
@@ -281,11 +310,23 @@ const fragments = {
|
||||
root: gql`
|
||||
fragment CoralEmbedStream_Stream_root on RootQuery {
|
||||
me {
|
||||
status
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
banned {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
ignoredUsers {
|
||||
id
|
||||
}
|
||||
roles
|
||||
role
|
||||
}
|
||||
settings {
|
||||
organizationName
|
||||
@@ -299,12 +340,15 @@ const fragments = {
|
||||
fragment CoralEmbedStream_Stream_asset on Asset {
|
||||
comment(id: $commentId) @include(if: $hasComment) {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
${nest(`
|
||||
${nest(
|
||||
`
|
||||
parent {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
...nest
|
||||
}
|
||||
`, THREADING_LEVEL)}
|
||||
`,
|
||||
THREADING_LEVEL
|
||||
)}
|
||||
}
|
||||
id
|
||||
title
|
||||
@@ -344,7 +388,6 @@ const fragments = {
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth,
|
||||
refetching: state.embed.refetching,
|
||||
activeReplyBox: state.stream.activeReplyBox,
|
||||
commentId: state.stream.commentId,
|
||||
assetId: state.stream.assetId,
|
||||
@@ -360,14 +403,17 @@ const mapStateToProps = (state) => ({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
showSignInDialog,
|
||||
notify,
|
||||
setActiveReplyBox,
|
||||
editName,
|
||||
viewAllComments,
|
||||
setActiveStreamTab: setActiveTab,
|
||||
}, dispatch);
|
||||
bindActionCreators(
|
||||
{
|
||||
showSignInDialog,
|
||||
notify,
|
||||
setActiveReplyBox,
|
||||
editName,
|
||||
viewAllComments,
|
||||
setActiveStreamTab: setActiveTab,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default compose(
|
||||
withFragments(fragments),
|
||||
@@ -377,5 +423,5 @@ export default compose(
|
||||
withPostFlag,
|
||||
withPostDontAgree,
|
||||
withDeleteAction,
|
||||
withEditComment,
|
||||
withEditComment
|
||||
)(StreamContainer);
|
||||
|
||||
@@ -4,7 +4,8 @@ import {findDOMNode} from 'react-dom';
|
||||
|
||||
export default class ClickOutside extends Component {
|
||||
static propTypes = {
|
||||
onClickOutside: PropTypes.func.isRequired
|
||||
onClickOutside: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -16,7 +17,7 @@ export default class ClickOutside extends Component {
|
||||
handleClick = (e) => {
|
||||
const {onClickOutside} = this.props;
|
||||
if (!e || !this.domNode.contains(e.target)) {
|
||||
onClickOutside(e);
|
||||
onClickOutside && onClickOutside(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,11 +3,17 @@ import {createDefaultResponseFragments} from '../utils';
|
||||
// fragments defined here are automatically registered.
|
||||
export default {
|
||||
...createDefaultResponseFragments(
|
||||
'SetUserRoleResponse',
|
||||
'ChangeUsernameResponse',
|
||||
'SetUsernameResponse',
|
||||
'BanUsersResponse',
|
||||
'UnbanUserResponse',
|
||||
'SetUserSuspensionStatusResponse',
|
||||
'SetCommentStatusResponse',
|
||||
'SetUsernameStatusResponse',
|
||||
'UnsuspendUserResponse',
|
||||
'SuspendUserResponse',
|
||||
'RejectUsernameResponse',
|
||||
'CreateCommentResponse',
|
||||
'SetUserStatusResponse',
|
||||
'CreateFlagResponse',
|
||||
'EditCommentResponse',
|
||||
'PostFlagResponse',
|
||||
|
||||
@@ -177,39 +177,176 @@ export const withSuspendUser = withMutation(
|
||||
})
|
||||
});
|
||||
|
||||
export const withRejectUsername = withMutation(
|
||||
export const withUnsuspendUser = withMutation(
|
||||
gql`
|
||||
mutation RejectUsername($input: RejectUsernameInput!) {
|
||||
rejectUsername(input: $input) {
|
||||
...RejectUsernameResponse
|
||||
mutation UnsuspendUser($input: UnsuspendUserInput!) {
|
||||
unsuspendUser(input: $input) {
|
||||
...UnsuspendUserResponse
|
||||
}
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
rejectUsername: (input) => {
|
||||
unsuspendUser: (input) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
}),
|
||||
});
|
||||
|
||||
export const withSetUserStatus = withMutation(
|
||||
export const withApproveUsername = withMutation(
|
||||
gql`
|
||||
mutation SetUserStatus($userId: ID!, $status: USER_STATUS!) {
|
||||
setUserStatus(id: $userId, status: $status) {
|
||||
...SetUserStatusResponse
|
||||
mutation ApproveUsername($id: ID!) {
|
||||
approveUsername(id: $id) {
|
||||
...SetUsernameStatusResponse
|
||||
}
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
setUserStatus: ({userId, status}) => {
|
||||
approveUsername: (id) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
userId,
|
||||
status
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
export const withRejectUsername = withMutation(
|
||||
gql`
|
||||
mutation RejectUsername($id: ID!) {
|
||||
rejectUsername(id: $id) {
|
||||
...SetUsernameStatusResponse
|
||||
}
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
rejectUsername: (id) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const SetUsernameFragment = gql`
|
||||
fragment Talk_SetUsername on User {
|
||||
username
|
||||
}`;
|
||||
|
||||
export const withChangeUsername = withMutation(
|
||||
gql`
|
||||
mutation ChangeUsername($id: ID!, $username: String!) {
|
||||
changeUsername(id: $id, username: $username) {
|
||||
...ChangeUsernameResponse
|
||||
}
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
changeUsername: (id, username) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
id,
|
||||
username,
|
||||
},
|
||||
update: (proxy) => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = {
|
||||
__typename: 'User',
|
||||
username,
|
||||
};
|
||||
proxy.writeFragment({fragment: SetUsernameFragment, id: fragmentId, data});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
export const withSetUsername = withMutation(
|
||||
gql`
|
||||
mutation SetUsername($id: ID!, $username: String!) {
|
||||
setUsername(id: $id, username: $username) {
|
||||
...SetUsernameResponse
|
||||
}
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
setUsername: (id, username) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
id,
|
||||
username,
|
||||
},
|
||||
update: (proxy) => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = {
|
||||
__typename: 'User',
|
||||
username,
|
||||
};
|
||||
proxy.writeFragment({fragment: SetUsernameFragment, id: fragmentId, data});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
export const withBanUser = withMutation(
|
||||
gql`
|
||||
mutation BanUser($input: BanUserInput!) {
|
||||
banUser(input: $input) {
|
||||
...BanUsersResponse
|
||||
}
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
banUser: (input) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
export const withUnbanUser = withMutation(
|
||||
gql`
|
||||
mutation UnbanUser($input: UnbanUserInput!) {
|
||||
unbanUser(input: $input) {
|
||||
...UnbanUserResponse
|
||||
}
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
unbanUser: (input) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
export const withSetUserRole = withMutation(
|
||||
gql`
|
||||
mutation SetUserRole($id: ID!, $role: USER_ROLES!) {
|
||||
setUserRole(id: $id, role: $role) {
|
||||
...SetUserRoleResponse
|
||||
}
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
setUserRole: (id, role) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
id,
|
||||
role,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -228,7 +365,7 @@ export const withPostComment = withMutation(
|
||||
postComment: (input) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
input
|
||||
input,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ import has from 'lodash/has';
|
||||
import get from 'lodash/get';
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
import moment from 'moment';
|
||||
import 'moment/locale/da';
|
||||
import 'moment/locale/es';
|
||||
import 'moment/locale/fr';
|
||||
import 'moment/locale/pt-br';
|
||||
|
||||
import daTA from 'timeago.js/locales/da';
|
||||
import esTA from 'timeago.js/locales/es';
|
||||
import frTA from 'timeago.js/locales/fr';
|
||||
@@ -40,6 +46,9 @@ function init() {
|
||||
const locale = getLocale();
|
||||
setLocale(locale);
|
||||
|
||||
// Setting moment
|
||||
moment.locale(locale);
|
||||
|
||||
// Extract language key.
|
||||
lang = locale.split('-')[0];
|
||||
|
||||
|
||||
@@ -1,40 +1,63 @@
|
||||
import intersection from 'lodash/intersection';
|
||||
import get from 'lodash/get';
|
||||
|
||||
// =========================================================================
|
||||
// BASIC PERMISSIONS
|
||||
// =========================================================================
|
||||
|
||||
const basicPerms = {
|
||||
INTERACT_WITH_COMMUNITY: (user) => {
|
||||
const banned = get(user, 'status.banned.status');
|
||||
const suspensionUntil = get(user, 'status.suspension.until');
|
||||
const suspended = suspensionUntil && new Date(suspensionUntil) > new Date();
|
||||
|
||||
return !banned && !suspended;
|
||||
},
|
||||
EDIT_NAME: (user) => {
|
||||
const usernameStatus = user.status.username.status;
|
||||
return usernameStatus === 'UNSET' || usernameStatus === 'REJECTED';
|
||||
},
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// PERMISSIONS BY ROLE
|
||||
// =========================================================================
|
||||
|
||||
const basicRoles = {
|
||||
HAS_STAFF_TAG: ['ADMIN', 'MODERATOR', 'STAFF']
|
||||
HAS_STAFF_TAG: ['ADMIN', 'MODERATOR', 'STAFF'],
|
||||
};
|
||||
|
||||
const queryRoles = {
|
||||
UPDATE_CONFIG: ['ADMIN'],
|
||||
ACCESS_ADMIN: ['ADMIN', 'MODERATOR'],
|
||||
VIEW_USER_EMAILS: ['ADMIN']
|
||||
VIEW_USER_EMAILS: ['ADMIN'],
|
||||
};
|
||||
|
||||
const mutationRoles = {
|
||||
CHANGE_ROLES: ['ADMIN'],
|
||||
MODERATE_COMMENTS: ['ADMIN', 'MODERATOR']
|
||||
MODERATE_COMMENTS: ['ADMIN', 'MODERATOR'],
|
||||
};
|
||||
|
||||
const roles = {...basicRoles, ...queryRoles, ...mutationRoles};
|
||||
|
||||
export const can = (user, ...perms) => {
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const banned = user.status === 'BANNED';
|
||||
const suspended = user.suspension.until && new Date(user.suspension.until) > new Date();
|
||||
|
||||
return perms.every((perm) => {
|
||||
if (perm === 'INTERACT_WITH_COMMUNITY') {
|
||||
return !banned && !suspended;
|
||||
|
||||
// Basic Permissions
|
||||
const permAction = basicPerms[perm];
|
||||
if (typeof permAction !== 'undefined') {
|
||||
return permAction(user);
|
||||
}
|
||||
|
||||
// Permissions by Role
|
||||
const role = roles[perm];
|
||||
if (typeof role === 'undefined') {
|
||||
throw new Error(`${perm} is not a valid role`);
|
||||
throw new Error(`${perm} is not a valid role or permission`);
|
||||
}
|
||||
|
||||
return intersection(role, user.roles).length > 0;
|
||||
return role.includes(user.role);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import get from 'lodash/get';
|
||||
|
||||
/**
|
||||
* getReliability
|
||||
* retrieves reliability value as string
|
||||
@@ -12,3 +14,22 @@ export const getReliability = (reliabilityValue) => {
|
||||
return 'unreliable';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* isSuspended
|
||||
* retrieves boolean based on the user suspension status
|
||||
*/
|
||||
|
||||
export const isSuspended = (user) => {
|
||||
const suspensionUntil = get(user, 'state.status.suspension.until');
|
||||
return user && suspensionUntil && new Date(suspensionUntil) > new Date();
|
||||
};
|
||||
|
||||
/**
|
||||
* isBanned
|
||||
* retrieves boolean based on the user ban status
|
||||
*/
|
||||
|
||||
export const isBanned = (user) => {
|
||||
return get(user, 'state.status.banned.status');
|
||||
};
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
.bio textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
min-height: 100px;
|
||||
margin: 10px 0;
|
||||
border: solid 1px #d8d8d8;
|
||||
}
|
||||
|
||||
.bio h1 {
|
||||
font-size: 16px;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.bio p {
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './Bio.css';
|
||||
import {Button} from '../../coral-ui';
|
||||
|
||||
export default ({bio, handleSave, handleInput, handleCancel}) => (
|
||||
<div className={styles.bio}>
|
||||
<h1>Bio</h1>
|
||||
<p>Tell the community about yourself</p>
|
||||
<form>
|
||||
<textarea value={bio} onChange={handleInput} />
|
||||
<div className={styles.actions}>
|
||||
<Button cStyle='cancel' type="button" onClick={handleCancel} raised>Cancel</Button>
|
||||
<Button cStyle='success' type="submit" onClick={handleSave}>Save Changes</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import Bio from '../components/Bio';
|
||||
|
||||
export default class BioContainer extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
bio: props.bio
|
||||
};
|
||||
|
||||
this.handleSave = this.handleSave.bind(this);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.handleCancel = this.handleCancel.bind(this);
|
||||
}
|
||||
|
||||
handleInput(e) {
|
||||
this.setState({
|
||||
bio: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
handleSave (e) {
|
||||
e.preventDefault();
|
||||
const {userData, saveBio} = this.props;
|
||||
const {bio} = this.state;
|
||||
saveBio(userData.id, {bio});
|
||||
}
|
||||
|
||||
handleCancel () {
|
||||
this.setState({
|
||||
bio: this.props.bio
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Bio
|
||||
bio={this.state.bio}
|
||||
userData={this.props.userData}
|
||||
handleSave={this.handleSave}
|
||||
handleInput={this.handleInput}
|
||||
handleCancel={this.handleCancel}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
@@ -150,6 +150,7 @@
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #616161;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
composes: buttonReset from 'coral-framework/styles/reset.css';
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -34,7 +35,6 @@
|
||||
top: 60px;
|
||||
box-shadow: -1px 3px 4px 0px rgba(0,0,0,0.15);
|
||||
text-align: center;
|
||||
padding-top: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Drawer.css';
|
||||
|
||||
const Drawer = ({children, onClose}) => {
|
||||
const Drawer = ({children, onClose, className = ''}) => {
|
||||
return (
|
||||
<div className={styles.drawer}>
|
||||
<div className={styles.closeButton} onClick={onClose}>×</div>
|
||||
<div className={cn(styles.drawer, className)}>
|
||||
<button className={cn(styles.closeButton, [className, 'close-button'].join('-'))} onClick={onClose}>×</button>
|
||||
<div className={styles.content}>
|
||||
{children}
|
||||
</div>
|
||||
@@ -15,7 +16,9 @@ const Drawer = ({children, onClose}) => {
|
||||
|
||||
Drawer.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired
|
||||
onClose: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Drawer;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
.dropdown {
|
||||
position: relative;
|
||||
width: 150px;
|
||||
height: 36px;
|
||||
height: 34px;
|
||||
background: #2c2c2c;
|
||||
box-sizing: border-box;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
|
||||
line-height: 1.4;
|
||||
font-size: 13px;
|
||||
font-size: 0.98em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
padding: 10px 15px;
|
||||
padding: 8px 15px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
right: 15px;
|
||||
font-size: 1.2rem;
|
||||
vertical-align: middle;
|
||||
top: 13px;
|
||||
top: 11px;
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@ class Tab extends React.Component {
|
||||
getRootClassName({active, className, sub, classNames = {}} = this.props) {
|
||||
return cn(
|
||||
'talk-tab',
|
||||
className,
|
||||
{
|
||||
[classNames.root || styles.root]: !sub,
|
||||
[classNames.rootSub || styles.rootSub]: sub,
|
||||
[classNames.rootActive || styles.rootActive]: active && !sub,
|
||||
[classNames.rootSubActive || styles.rootSubActive]: active && sub,
|
||||
'talk-tab-active': active,
|
||||
}
|
||||
},
|
||||
className,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ class TabBar extends React.Component {
|
||||
getRootClassName({className, classNames = {}, sub} = this.props) {
|
||||
return cn(
|
||||
'talk-tab-bar',
|
||||
className,
|
||||
{
|
||||
[classNames.root || styles.root]: !sub,
|
||||
[classNames.rootSub || styles.rootSub]: sub,
|
||||
}
|
||||
},
|
||||
className,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {PopupMenu, Button} from 'coral-ui';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import cn from 'classnames';
|
||||
import styles from './styles.css';
|
||||
import * as REASONS from '../helpers/flagReasons';
|
||||
|
||||
import {getErrorMessages} from 'coral-framework/utils';
|
||||
|
||||
@@ -90,10 +91,9 @@ export default class FlagButton extends Component {
|
||||
let action = {
|
||||
item_id,
|
||||
item_type: itemType,
|
||||
reason: null,
|
||||
message
|
||||
};
|
||||
if (reason === 'COMMENT_NOAGREE') {
|
||||
if (reason === REASONS.comment.noagree) {
|
||||
postDontAgree(action)
|
||||
.then(({data}) => {
|
||||
if (itemType === 'COMMENTS') {
|
||||
@@ -122,7 +122,7 @@ export default class FlagButton extends Component {
|
||||
onPopupOptionClick = (sets) => (e) => {
|
||||
|
||||
// If flagging a user, indicate that this is referencing the username rather than the bio
|
||||
if(sets === 'itemType' && e.target.value === 'users') {
|
||||
if (sets === 'itemType' && e.target.value === 'users') {
|
||||
this.setState({field: 'username'});
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user