mirror of
https://github.com/wassname/talk.git
synced 2026-07-13 17:45:56 +08:00
Merge branch 'reply' of github.com:coralproject/talk into reply
* 'reply' of github.com:coralproject/talk: (193 commits) Add some margin to loadmore typo updated tests updated tests Fully implement SetUsername removed snakecase fix to optional required param Use correct mutation and show errors refactored mailer service with lodash! Fix live status updates Move ChangeUsername to the appropiate tab Fix change username not displaying errors Remove defunct refetch detection workaround Rename UnBan and UnSuspend hardened configuration user cli patches don't apply configuration from dotfiles during testing applied rename to graph mutation edges reintroduced the errors.ErrSameUsernameProvided fixed wrong query logic ...
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