mirror of
https://github.com/wassname/talk.git
synced 2026-08-13 12:40:11 +08:00
Merge branch 'master' into better-change-detection
This commit is contained in:
@@ -18,7 +18,6 @@ program
|
||||
.command('token', 'work with the access tokens')
|
||||
.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'
|
||||
@@ -41,20 +40,3 @@ if (!commands.includes(command)) {
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 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');
|
||||
// }
|
||||
// });
|
||||
|
||||
+59
-27
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
const util = require('./util');
|
||||
const _ = require('lodash');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
@@ -25,46 +26,60 @@ async function createMigration(name) {
|
||||
}
|
||||
}
|
||||
|
||||
async function runMigrations() {
|
||||
async function runMigrations(options) {
|
||||
const { yes, queryBatchSize, updateBatchSize } = options;
|
||||
try {
|
||||
let { backedUp } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'backedUp',
|
||||
message: 'Did you perform a database backup',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
if (!yes) {
|
||||
const { backedUp } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'backedUp',
|
||||
message: 'Did you perform a database backup',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!backedUp) {
|
||||
throw new Error(
|
||||
'Please backup your databases prior to migrations occuring'
|
||||
);
|
||||
if (!backedUp) {
|
||||
throw new Error(
|
||||
'Please backup your databases prior to migrations occuring'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the migrations to run.
|
||||
let migrations = await MigrationService.listPending();
|
||||
const migrations = await MigrationService.listPending();
|
||||
|
||||
console.log('Now going to run the following migrations:\n');
|
||||
|
||||
for (let { filename } of migrations) {
|
||||
for (const { filename } of migrations) {
|
||||
console.log(`\tmigrations/${filename}`);
|
||||
}
|
||||
|
||||
let { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Proceed with migrations',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
if (!yes) {
|
||||
const { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Proceed with migrations',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (confirm) {
|
||||
// Run the migrations.
|
||||
await MigrationService.run(migrations);
|
||||
if (confirm) {
|
||||
// Run the migrations.
|
||||
await MigrationService.run(migrations, {
|
||||
queryBatchSize,
|
||||
updateBatchSize,
|
||||
});
|
||||
} else {
|
||||
console.warn('Skipping migrations');
|
||||
}
|
||||
} else {
|
||||
console.warn('Skipping migrations');
|
||||
// Run the migrations.
|
||||
await MigrationService.run(migrations, {
|
||||
queryBatchSize,
|
||||
updateBatchSize,
|
||||
});
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
@@ -83,8 +98,25 @@ program
|
||||
.description('creates a new migration')
|
||||
.action(createMigration);
|
||||
|
||||
// Bypasses issue that defaults + coercion doesn't work well together.
|
||||
// Ref: https://github.com/tj/commander.js/issues/400#issuecomment-310860869
|
||||
const parse10 = _.ary(_.partialRight(parseInt, 10), 1);
|
||||
|
||||
program
|
||||
.command('run')
|
||||
.option(
|
||||
'-q, --query-batch-size <n>',
|
||||
'change the size of queried documents that are batched at a time',
|
||||
parse10,
|
||||
10000
|
||||
)
|
||||
.option(
|
||||
'-u, --update-batch-size <n>',
|
||||
'change the size of documents that are batched before the update is sent',
|
||||
parse10,
|
||||
20000
|
||||
)
|
||||
.option('-y, --yes', 'will answer yes to all questions')
|
||||
.description('runs all pending migrations')
|
||||
.action(runMigrations);
|
||||
|
||||
|
||||
+44
-42
@@ -235,7 +235,7 @@ async function reconcileLocalPlugins({ skipRemote, dryRun }) {
|
||||
|
||||
if (output.status) {
|
||||
throw new Error(
|
||||
'Could not install local plugin dependencies, errors occured during install'
|
||||
'Could not install local plugin dependencies, errors occurred during install'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -253,59 +253,61 @@ async function reconcilePluginDeps({
|
||||
dryRun,
|
||||
upgradeRemote,
|
||||
}) {
|
||||
let startTime = new Date();
|
||||
try {
|
||||
let startTime = new Date();
|
||||
|
||||
// We don't need to do anything if we skip everything....
|
||||
if (skipLocal && skipRemote) {
|
||||
return;
|
||||
}
|
||||
// We don't need to do anything if we skip everything....
|
||||
if (skipLocal && skipRemote) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Traverse local plugins and install dependencies if enabled.
|
||||
if (!skipLocal) {
|
||||
await reconcileLocalPlugins({ skipRemote, dryRun });
|
||||
}
|
||||
// Traverse local plugins and install dependencies if enabled.
|
||||
if (!skipLocal) {
|
||||
await reconcileLocalPlugins({ skipRemote, dryRun });
|
||||
}
|
||||
|
||||
// Locate any external plugins and install them.
|
||||
if (!skipRemote) {
|
||||
let results = [];
|
||||
try {
|
||||
results = await reconcileRemotePlugins({
|
||||
// Locate any external plugins and install them.
|
||||
if (!skipRemote) {
|
||||
const results = await reconcileRemotePlugins({
|
||||
skipLocal,
|
||||
skipRemote,
|
||||
dryRun,
|
||||
upgradeRemote,
|
||||
});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
|
||||
let status;
|
||||
if (dryRun) {
|
||||
status = '[dry-run] success'.green;
|
||||
} else {
|
||||
status = 'success'.green;
|
||||
}
|
||||
|
||||
let message;
|
||||
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
|
||||
message = 'Already up-to-date.';
|
||||
} else if (results.upgradable.length === 0) {
|
||||
message = `Fetched ${results.fetchable.length} new plugins.`;
|
||||
} else if (results.fetchable.length === 0) {
|
||||
message = `Upgraded ${results.upgradable.length} new plugins.`;
|
||||
} else {
|
||||
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
|
||||
results.upgradable.length
|
||||
} plugins.`;
|
||||
}
|
||||
|
||||
console.log(`\n${status} ${message}`);
|
||||
}
|
||||
|
||||
let status;
|
||||
if (dryRun) {
|
||||
status = '[dry-run] success'.green;
|
||||
} else {
|
||||
status = 'success'.green;
|
||||
}
|
||||
let endTime = new Date();
|
||||
|
||||
let message;
|
||||
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
|
||||
message = 'Already up-to-date.';
|
||||
} else if (results.upgradable.length === 0) {
|
||||
message = `Fetched ${results.fetchable.length} new plugins.`;
|
||||
} else if (results.fetchable.length === 0) {
|
||||
message = `Upgraded ${results.upgradable.length} new plugins.`;
|
||||
} else {
|
||||
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
|
||||
results.upgradable.length
|
||||
} plugins.`;
|
||||
}
|
||||
|
||||
console.log(`\n${status} ${message}`);
|
||||
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(
|
||||
2
|
||||
);
|
||||
console.log(`✨ Done in ${totalTime}s.`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let endTime = new Date();
|
||||
|
||||
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(2);
|
||||
console.log(`✨ Done in ${totalTime}s.`);
|
||||
}
|
||||
|
||||
async function createSeedPlugin() {
|
||||
|
||||
+8
-1
@@ -13,6 +13,7 @@ const MODERATION_OPTIONS = require('../models/enum/moderation_options');
|
||||
const SettingsService = require('../services/settings');
|
||||
const SetupService = require('../services/setup');
|
||||
const UsersService = require('../services/users');
|
||||
const MigrationService = require('../services/migration');
|
||||
const errors = require('../errors');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
@@ -51,6 +52,12 @@ const performSetup = async () => {
|
||||
if (program.defaults) {
|
||||
await SettingsService.init();
|
||||
|
||||
// Get the migrations to run.
|
||||
let migrations = await MigrationService.listPending();
|
||||
|
||||
// Perform all migrations.
|
||||
await MigrationService.run(migrations);
|
||||
|
||||
console.log('Settings created.');
|
||||
console.log('\nTalk is now installed!');
|
||||
|
||||
@@ -194,7 +201,7 @@ const performSetup = async () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Start tthe setup process.
|
||||
// Start the setup process.
|
||||
performSetup()
|
||||
.then(() => {
|
||||
util.shutdown();
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const databaseVerifications = require('./verifications/database');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
async function database({ fix = false, limit = Infinity, batch = 1000 }) {
|
||||
try {
|
||||
for (const verification of databaseVerifications) {
|
||||
await verification({ fix, limit, batch });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Failed to process all the ${databaseVerifications.length} verifications`,
|
||||
err
|
||||
);
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('db')
|
||||
.description('verifies the database integrity')
|
||||
.option('-f, --fix', 'fix the problems found with database inconsistencies')
|
||||
.option(
|
||||
'-l, --limit [size]',
|
||||
'limit the amount of documents to process in a single pass, this will ensure only a maximum number of batch operations are issued [default: inf]',
|
||||
parseInt
|
||||
)
|
||||
.option(
|
||||
'-b, --batch [size]',
|
||||
'batch size to process verifications and repairs of documents [default: 1000]',
|
||||
parseInt
|
||||
)
|
||||
.action(database);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
+2
-1
@@ -63,5 +63,6 @@ process.once('SIGUSR2', () => util.shutdown(0, 'SIGUSR2'));
|
||||
// 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;
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
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,140 +0,0 @@
|
||||
const CommentModel = require('../../../models/comment');
|
||||
const { singleJoinBy } = require('../../../graph/loaders/util');
|
||||
const debug = require('debug')('talk:cli:verify');
|
||||
|
||||
const getBatch = async (limit, offset) =>
|
||||
CommentModel.find({})
|
||||
.select({ id: 1, action_counts: 1, reply_count: 1 })
|
||||
.limit(limit)
|
||||
.skip(offset)
|
||||
.sort('created_at');
|
||||
|
||||
module.exports = async ({ fix, limit, batch }) => {
|
||||
let operations = [];
|
||||
|
||||
// Count how many comments there are to process.
|
||||
const totalCount = await CommentModel.count();
|
||||
|
||||
let offset = 0;
|
||||
let comments = [];
|
||||
let commentIDs = [];
|
||||
|
||||
console.log(`Processing ${totalCount} comments in batches of ${limit}...`);
|
||||
|
||||
// Keep processing documents until there are is none left.
|
||||
while (offset < totalCount) {
|
||||
// Get a batch of comments.
|
||||
comments = await getBatch(batch, offset);
|
||||
commentIDs = comments.map(({ id }) => id);
|
||||
|
||||
// Get their reply counts.
|
||||
let allReplyCounts = await CommentModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
parent_id: {
|
||||
$in: commentIDs,
|
||||
},
|
||||
status: {
|
||||
$in: ['NONE', 'ACCEPTED'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$parent_id',
|
||||
count: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
.then(singleJoinBy(commentIDs, '_id'))
|
||||
.then(results => results.map(result => (result ? result.count : 0)));
|
||||
|
||||
// Loop over the comments, with their action summaries.
|
||||
for (let i = 0; i < comments.length; i++) {
|
||||
let comment = comments[i];
|
||||
let replyCount = allReplyCounts[i];
|
||||
|
||||
// And check to see if the action summaries we just computed match what is
|
||||
// currently set for the comments.
|
||||
let commentOperations = [];
|
||||
|
||||
// If the reply count needs to be updated, then update it!
|
||||
if (comment.reply_count !== replyCount) {
|
||||
commentOperations.push({
|
||||
reply_count: replyCount,
|
||||
});
|
||||
}
|
||||
|
||||
// If this comment has action summaries that should be updated, then
|
||||
// perform an update!
|
||||
if (commentOperations.length > 0) {
|
||||
operations.push({
|
||||
updateOne: {
|
||||
filter: {
|
||||
id: comment.id,
|
||||
},
|
||||
update: {
|
||||
$set: Object.assign({}, ...commentOperations),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
debug(`Processed batch of ${comments.length} comments.`);
|
||||
|
||||
if (operations.length >= limit) {
|
||||
debug(
|
||||
`Queued operations are ${
|
||||
operations.length
|
||||
}, reached limit of ${limit}, not processing any more.`
|
||||
);
|
||||
|
||||
if (operations.length > limit) {
|
||||
debug(
|
||||
`${operations.length -
|
||||
limit} operations have been truncated to enforce the limit`
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
offset += batch;
|
||||
}
|
||||
|
||||
const OPERATIONS_LENGTH = operations.length;
|
||||
|
||||
if (limit < Infinity && offset + comments.length < totalCount) {
|
||||
console.log(
|
||||
`Processed ${offset +
|
||||
comments.length}/${totalCount} comments because we reached the update limit of ${limit}.`
|
||||
);
|
||||
} else {
|
||||
console.log(`Processed all ${totalCount} comments.`);
|
||||
}
|
||||
|
||||
console.log(`${OPERATIONS_LENGTH} documents need fixing.`);
|
||||
|
||||
// If fix was enabled, execute the batch writes.
|
||||
if (OPERATIONS_LENGTH > 0) {
|
||||
if (fix) {
|
||||
debug(`Fixing ${OPERATIONS_LENGTH} documents...`);
|
||||
|
||||
while (operations.length) {
|
||||
let batchOperations = operations.splice(0, batch);
|
||||
let result = await CommentModel.collection.bulkWrite(batchOperations);
|
||||
|
||||
debug(`Fixed batch of ${result.modifiedCount} documents.`);
|
||||
}
|
||||
|
||||
console.log(`Applied all ${OPERATIONS_LENGTH} fixes.`);
|
||||
} else {
|
||||
console.warn(
|
||||
'Skipping fixing, --fix was not enabled, pass --fix to fix these errors'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
// This will import all the verifications that should be run by the:
|
||||
//
|
||||
// cli verify database
|
||||
//
|
||||
// command. They exist in the form:
|
||||
//
|
||||
// async ({fix = false, batch = 1000}) => {}
|
||||
//
|
||||
// where their options are derived.
|
||||
module.exports = [require('./comment_replies'), require('./action_counts')];
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
HIDE_BANUSER_DIALOG,
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG,
|
||||
SET_INDICATOR_TRACK,
|
||||
} from '../constants/community';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -68,3 +69,9 @@ export const showRejectUsernameDialog = user => ({
|
||||
export const hideRejectUsernameDialog = () => ({
|
||||
type: HIDE_REJECT_USERNAME_DIALOG,
|
||||
});
|
||||
|
||||
// Enable or disable the activity indicator subscriptions.
|
||||
export const setIndicatorTrack = track => ({
|
||||
type: SET_INDICATOR_TRACK,
|
||||
track,
|
||||
});
|
||||
|
||||
@@ -31,10 +31,16 @@ export const storySearchChange = value => ({
|
||||
});
|
||||
|
||||
export const clearState = () => ({
|
||||
type: actions.MODERATION_CLEAR_STATE,
|
||||
type: actions.CLEAR_STATE,
|
||||
});
|
||||
|
||||
export const selectCommentId = id => ({
|
||||
type: actions.MODERATION_SELECT_COMMENT,
|
||||
type: actions.SELECT_COMMENT,
|
||||
id,
|
||||
});
|
||||
|
||||
// Enable or disable the activity indicator subscriptions.
|
||||
export const setIndicatorTrack = track => ({
|
||||
type: actions.SET_INDICATOR_TRACK,
|
||||
track,
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ const CoralHeader = ({
|
||||
showShortcuts = () => {},
|
||||
auth,
|
||||
root,
|
||||
data,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.headerWrapper}>
|
||||
@@ -31,7 +32,7 @@ const CoralHeader = ({
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.moderate')}
|
||||
<ModerationIndicator root={root} />
|
||||
<ModerationIndicator root={root} data={data} />
|
||||
</IndexLink>
|
||||
)}
|
||||
<Link
|
||||
@@ -50,7 +51,7 @@ const CoralHeader = ({
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.community')}
|
||||
<CommunityIndicator root={root} />
|
||||
<CommunityIndicator root={root} data={data} />
|
||||
</Link>
|
||||
|
||||
{can(auth.user, 'UPDATE_CONFIG') && (
|
||||
@@ -119,6 +120,7 @@ CoralHeader.propTypes = {
|
||||
showShortcuts: PropTypes.func,
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default CoralHeader;
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
import styles from './UserDetail.css';
|
||||
import AccountHistory from './AccountHistory';
|
||||
import { Slot } from 'coral-framework/components';
|
||||
@@ -29,43 +28,23 @@ import UserInfoTooltip from './UserInfoTooltip';
|
||||
|
||||
class UserDetail extends React.Component {
|
||||
rejectThenReload = async info => {
|
||||
try {
|
||||
await this.props.rejectComment(info);
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
await this.props.rejectComment(info);
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
acceptThenReload = async info => {
|
||||
try {
|
||||
await this.props.acceptComment(info);
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
await this.props.acceptComment(info);
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
bulkAcceptThenReload = async () => {
|
||||
try {
|
||||
await this.props.bulkAccept();
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
await this.props.bulkAccept();
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
bulkRejectThenReload = async () => {
|
||||
try {
|
||||
await this.props.bulkReject();
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
await this.props.bulkReject();
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
changeTab = tab => {
|
||||
@@ -94,6 +73,16 @@ class UserDetail extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderError() {
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.props.hideUserDetail}>
|
||||
<Drawer onClose={this.props.hideUserDetail}>
|
||||
<div>{this.props.data.error.message}</div>
|
||||
</Drawer>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
|
||||
getActionMenuLabel() {
|
||||
const { root: { user } } = this.props;
|
||||
|
||||
@@ -345,6 +334,10 @@ class UserDetail extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
return this.renderError();
|
||||
}
|
||||
|
||||
if (this.props.loading) {
|
||||
return this.renderLoading();
|
||||
}
|
||||
@@ -371,7 +364,6 @@ UserDetail.propTypes = {
|
||||
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,
|
||||
|
||||
@@ -18,3 +18,5 @@ export const SHOW_REJECT_USERNAME_DIALOG = `${prefix}_SHOW_REJECT_USERNAME_DIALO
|
||||
export const HIDE_REJECT_USERNAME_DIALOG = `${prefix}_HIDE_REJECT_USERNAME_DIALOG`;
|
||||
|
||||
export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`;
|
||||
|
||||
export const SET_INDICATOR_TRACK = `${prefix}_SET_INDICATOR_TRACK`;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
export const TOGGLE_MODAL = 'TOGGLE_MODAL';
|
||||
export const SINGLE_VIEW = 'SINGLE_VIEW';
|
||||
export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE';
|
||||
export const SET_SORT_ORDER = 'MODERATION_SET_SORT_ORDER';
|
||||
export const SHOW_STORY_SEARCH = 'SHOW_STORY_SEARCH';
|
||||
export const HIDE_STORY_SEARCH = 'HIDE_STORY_SEARCH';
|
||||
export const STORY_SEARCH_CHANGE_VALUE = 'STORY_SEARCH_CHANGE_VALUE';
|
||||
export const MODERATION_CLEAR_STATE = 'MODERATION_CLEAR_STATE';
|
||||
export const MODERATION_SELECT_COMMENT = 'MODERATION_SELECT_COMMENT';
|
||||
const prefix = `MODERATION`;
|
||||
|
||||
export const TOGGLE_MODAL = `${prefix}_TOGGLE_MODAL`;
|
||||
export const SINGLE_VIEW = `${prefix}_SINGLE_VIEW`;
|
||||
export const HIDE_SHORTCUTS_NOTE = `${prefix}_HIDE_SHORTCUTS_NOTE`;
|
||||
export const SET_SORT_ORDER = `${prefix}_SET_SORT_ORDER`;
|
||||
export const SHOW_STORY_SEARCH = `${prefix}_SHOW_STORY_SEARCH`;
|
||||
export const HIDE_STORY_SEARCH = `${prefix}_HIDE_STORY_SEARCH`;
|
||||
export const STORY_SEARCH_CHANGE_VALUE = `${prefix}_STORY_SEARCH_CHANGE_VALUE`;
|
||||
export const CLEAR_STATE = `${prefix}_CLEAR_STATE`;
|
||||
export const SELECT_COMMENT = `${prefix}_SELECT_COMMENT`;
|
||||
export const SET_INDICATOR_TRACK = `${prefix}_SET_INDICATOR_TRACK`;
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
import { compose } from 'react-apollo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
|
||||
class BanUserDialogContainer extends Component {
|
||||
banUser = async () => {
|
||||
@@ -22,16 +20,11 @@ class BanUserDialogContainer extends Component {
|
||||
banUser,
|
||||
setCommentStatus,
|
||||
hideBanUserDialog,
|
||||
notify,
|
||||
} = this.props;
|
||||
try {
|
||||
await banUser({ id: userId, message: '' });
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({ commentId, status: 'REJECTED' });
|
||||
}
|
||||
} catch (err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
await banUser({ id: userId, message: '' });
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({ commentId, status: 'REJECTED' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,14 +71,13 @@ const mapDispatchToProps = dispatch => ({
|
||||
...bindActionCreators(
|
||||
{
|
||||
hideBanUserDialog,
|
||||
notify,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withBanUser,
|
||||
withSetCommentStatus,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
withSetCommentStatus
|
||||
)(BanUserDialogContainer);
|
||||
|
||||
@@ -25,6 +25,7 @@ export default withFragments({
|
||||
id
|
||||
role
|
||||
}
|
||||
created_at
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getDefinitionName } from 'coral-framework/utils';
|
||||
|
||||
export default withQuery(
|
||||
gql`
|
||||
query TalkAdmin_Header {
|
||||
query TalkAdmin_Header($nullID: ID) {
|
||||
...${getDefinitionName(ModerationIndicator.fragments.root)}
|
||||
...${getDefinitionName(CommunityIndicator.fragments.root)}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export default withQuery(
|
||||
`,
|
||||
{
|
||||
options: {
|
||||
pollInterval: 10000,
|
||||
variables: { nullID: null },
|
||||
},
|
||||
}
|
||||
)(Header);
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
import get from 'lodash/get';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
|
||||
@@ -28,17 +27,13 @@ class SuspendUserDialogContainer extends Component {
|
||||
notify,
|
||||
} = this.props;
|
||||
hideSuspendUserDialog();
|
||||
try {
|
||||
await suspendUser({ id: userId, message, until });
|
||||
notify(
|
||||
'success',
|
||||
t('suspenduser.notify_suspend_until', username, timeago(until))
|
||||
);
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({ commentId, status: 'REJECTED' });
|
||||
}
|
||||
} catch (err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
await suspendUser({ id: userId, message, until });
|
||||
notify(
|
||||
'success',
|
||||
t('suspenduser.notify_suspend_until', username, timeago(until))
|
||||
);
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({ commentId, status: 'REJECTED' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
} 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';
|
||||
|
||||
@@ -130,6 +129,7 @@ class UserDetailContainer extends React.Component {
|
||||
acceptComment={this.acceptComment}
|
||||
rejectComment={this.rejectComment}
|
||||
loading={loading}
|
||||
error={this.props.data && this.props.data.error}
|
||||
loadMore={this.loadMore}
|
||||
{...this.props}
|
||||
/>
|
||||
@@ -271,7 +271,6 @@ const mapDispatchToProps = dispatch => ({
|
||||
viewUserDetail,
|
||||
hideUserDetail,
|
||||
toggleSelectAllCommentInUserDetail,
|
||||
notify,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
|
||||
@@ -173,13 +173,17 @@ export default {
|
||||
},
|
||||
updateQueries: {
|
||||
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
|
||||
const decrement = {
|
||||
flaggedUsernamesCount: { $apply: count => count - 1 },
|
||||
};
|
||||
|
||||
// Remove from list after the mutation was "really" completed.
|
||||
if (get(mutationResult, 'data.approveUsername.isOptimistic')) {
|
||||
return prev;
|
||||
return update(prev, decrement);
|
||||
}
|
||||
|
||||
const updated = update(prev, {
|
||||
flaggedUsernamesCount: { $apply: count => count - 1 },
|
||||
...decrement,
|
||||
flaggedUsers: {
|
||||
nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
|
||||
},
|
||||
@@ -227,13 +231,17 @@ export default {
|
||||
},
|
||||
updateQueries: {
|
||||
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
|
||||
const decrement = {
|
||||
flaggedUsernamesCount: { $apply: count => count - 1 },
|
||||
};
|
||||
|
||||
// Remove from list after the mutation was "really" completed.
|
||||
if (get(mutationResult, 'data.rejectUsername.isOptimistic')) {
|
||||
return prev;
|
||||
return update(prev, decrement);
|
||||
}
|
||||
|
||||
const updated = update(prev, {
|
||||
flaggedUsernamesCount: { $apply: count => count - 1 },
|
||||
...decrement,
|
||||
flaggedUsers: {
|
||||
nodes: {
|
||||
$apply: nodes => nodes.filter(node => node.id !== id),
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
HIDE_BANUSER_DIALOG,
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG,
|
||||
SET_INDICATOR_TRACK,
|
||||
} from '../constants/community';
|
||||
|
||||
const initialState = {
|
||||
@@ -24,6 +25,10 @@ const initialState = {
|
||||
user: {},
|
||||
banDialog: false,
|
||||
rejectUsernameDialog: false,
|
||||
// If true the activity indicator will track flagged account changes
|
||||
// in order to determine the current queue count. Set this to false
|
||||
// if the queue count is determined by other means.
|
||||
indicatorTrack: true,
|
||||
};
|
||||
|
||||
export default function community(state = initialState, action) {
|
||||
@@ -91,6 +96,11 @@ export default function community(state = initialState, action) {
|
||||
...state,
|
||||
searchValue: action.value,
|
||||
};
|
||||
case SET_INDICATOR_TRACK:
|
||||
return {
|
||||
...state,
|
||||
indicatorTrack: action.track,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -8,14 +8,19 @@ const initialState = {
|
||||
shortcutsNoteVisible: 'show',
|
||||
sortOrder: 'DESC',
|
||||
selectedCommentId: '',
|
||||
// If true the activity indicator will turn on subscriptions
|
||||
// in order to determine queue counts. Set this to false
|
||||
// if the queue count is determined by other means.
|
||||
indicatorTrack: true,
|
||||
};
|
||||
|
||||
export default function moderation(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.MODERATION_CLEAR_STATE:
|
||||
case actions.CLEAR_STATE:
|
||||
return {
|
||||
...initialState,
|
||||
shortcutsNoteVisible: state.shortcutsNoteVisible,
|
||||
indicatorTrack: state.indicatorTrack,
|
||||
};
|
||||
case actions.TOGGLE_MODAL:
|
||||
return {
|
||||
@@ -52,11 +57,16 @@ export default function moderation(state = initialState, action) {
|
||||
...state,
|
||||
sortOrder: action.order,
|
||||
};
|
||||
case actions.MODERATION_SELECT_COMMENT:
|
||||
case actions.SELECT_COMMENT:
|
||||
return {
|
||||
...state,
|
||||
selectedCommentId: action.id,
|
||||
};
|
||||
case actions.SET_INDICATOR_TRACK:
|
||||
return {
|
||||
...state,
|
||||
indicatorTrack: action.track,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
transition: background 200ms, box-shadow 200ms, margin-bottom 200ms;
|
||||
padding: 10px 0 0;
|
||||
padding: 10px 0 10px;
|
||||
min-height: 220px;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -6,16 +6,18 @@ import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import { Spinner } from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withApproveUsername } from 'coral-framework/graphql/mutations';
|
||||
import { showRejectUsernameDialog } from '../../../actions/community';
|
||||
import {
|
||||
showRejectUsernameDialog,
|
||||
setIndicatorTrack,
|
||||
} from '../../../actions/community';
|
||||
import { viewUserDetail } from '../../../actions/userDetail';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
import { appendNewNodes } from 'plugin-api/beta/client/utils';
|
||||
import update from 'immutability-helper';
|
||||
import { handleFlaggedUsernameChange } from '../graphql';
|
||||
import { handleFlaggedAccountsChange } from '../graphql';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { isFlaggedUserDangling } from '../utils';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { notifyOnMutationError } from 'coral-framework/hocs';
|
||||
|
||||
import FlaggedAccounts from '../components/FlaggedAccounts';
|
||||
import FlaggedUser from '../containers/FlaggedUser';
|
||||
@@ -32,10 +34,6 @@ function whoFlagged(user) {
|
||||
class FlaggedAccountsContainer extends Component {
|
||||
subscriptions = [];
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
getCountWithoutDangling() {
|
||||
return this.props.root.flaggedUsers.nodes.filter(
|
||||
node => !isFlaggedUserDangling(node)
|
||||
@@ -50,7 +48,7 @@ class FlaggedAccountsContainer extends Component {
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameFlagged: user } } }
|
||||
) => {
|
||||
return handleFlaggedUsernameChange(prev, user, () => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
'flagged_usernames.notify_flagged',
|
||||
whoFlagged(user),
|
||||
@@ -66,7 +64,7 @@ class FlaggedAccountsContainer extends Component {
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameApproved: user } } }
|
||||
) => {
|
||||
return handleFlaggedUsernameChange(prev, user, () => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
'flagged_usernames.notify_approved',
|
||||
whoChangedTheStatus(user.state.status.username),
|
||||
@@ -82,7 +80,7 @@ class FlaggedAccountsContainer extends Component {
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameRejected: user } } }
|
||||
) => {
|
||||
return handleFlaggedUsernameChange(prev, user, () => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
'flagged_usernames.notify_rejected',
|
||||
whoChangedTheStatus(user.state.status.username),
|
||||
@@ -102,7 +100,7 @@ class FlaggedAccountsContainer extends Component {
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleFlaggedUsernameChange(prev, user, () => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
'flagged_usernames.notify_changed',
|
||||
previousUsername,
|
||||
@@ -125,10 +123,14 @@ class FlaggedAccountsContainer extends Component {
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
// Stop activity indicator tracking, as we'll handle it here.
|
||||
this.props.setIndicatorTrack(false);
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Restart activity indicator tracking.
|
||||
this.props.setIndicatorTrack(true);
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
@@ -197,6 +199,7 @@ FlaggedAccountsContainer.propTypes = {
|
||||
approveUsername: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
setIndicatorTrack: PropTypes.func,
|
||||
};
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
@@ -288,6 +291,7 @@ const mapDispatchToProps = dispatch =>
|
||||
showRejectUsernameDialog,
|
||||
viewUserDetail,
|
||||
notify,
|
||||
setIndicatorTrack,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
@@ -295,7 +299,6 @@ const mapDispatchToProps = dispatch =>
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withApproveUsername,
|
||||
notifyOnMutationError(['approveUsername']),
|
||||
withQuery(
|
||||
gql`
|
||||
query TalkAdmin_Community_FlaggedAccounts {
|
||||
|
||||
@@ -1,14 +1,152 @@
|
||||
import React, { Component } from 'react';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import Indicator from '../../../components/Indicator';
|
||||
import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import { branch, renderNothing } from 'recompose';
|
||||
import { handleIndicatorChange } from '../graphql';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const hideIfNoData = hasNoData => branch(hasNoData, renderNothing);
|
||||
class IndicatorContainer extends Component {
|
||||
subscriptions = [];
|
||||
|
||||
subscribeToUpdates() {
|
||||
const parameters = [
|
||||
{
|
||||
document: USERNAME_FLAGGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameFlagged: user } } }
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: USERNAME_APPROVED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameApproved: user } } }
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: USERNAME_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameRejected: user } } }
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: USERNAME_CHANGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameChanged: { user } } } }
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
this.subscriptions = parameters.map(param =>
|
||||
this.props.data.subscribeToMore(param)
|
||||
);
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
this.subscriptions.forEach(unsubscribe => unsubscribe());
|
||||
this.subscriptions = [];
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (this.props.track) {
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.track && nextProps.track) {
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
if (this.props.track && !nextProps.track) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.root || !this.props.root.flaggedUsernamesCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Indicator />;
|
||||
}
|
||||
}
|
||||
|
||||
IndicatorContainer.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
track: PropTypes.bool,
|
||||
};
|
||||
|
||||
const fields = `
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const USERNAME_FLAGGED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_CommunityIndicator_UsernameFlagged {
|
||||
usernameFlagged {
|
||||
${fields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const USERNAME_APPROVED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ComunityIndicator_UsernameApproved {
|
||||
usernameApproved {
|
||||
${fields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const USERNAME_REJECTED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_CommunityIndicator_UsernameRejected {
|
||||
usernameRejected {
|
||||
${fields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const USERNAME_CHANGED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ComunityIndicator_UsernameChanged {
|
||||
usernameChanged {
|
||||
previousUsername
|
||||
user {
|
||||
${fields}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
track: state.community.indicatorTrack,
|
||||
});
|
||||
|
||||
const enhance = compose(
|
||||
connect(mapStateToProps),
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdmin_Community_Indicator_root on RootQuery {
|
||||
fragment TalkAdmin_CommunityIndicator_root on RootQuery {
|
||||
flaggedUsernamesCount: userCount(
|
||||
query: {
|
||||
action_type: FLAG
|
||||
@@ -17,8 +155,7 @@ const enhance = compose(
|
||||
)
|
||||
}
|
||||
`,
|
||||
}),
|
||||
hideIfNoData(props => !props.root.flaggedUsernamesCount)
|
||||
})
|
||||
);
|
||||
|
||||
export default enhance(Indicator);
|
||||
export default enhance(IndicatorContainer);
|
||||
|
||||
@@ -4,8 +4,6 @@ import { hideRejectUsernameDialog } from '../../../actions/community';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose } from 'react-apollo';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { notifyOnMutationError } from 'coral-framework/hocs';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
user: state.community.user,
|
||||
@@ -16,13 +14,11 @@ const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
handleClose: hideRejectUsernameDialog,
|
||||
notify,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withRejectUsername,
|
||||
notifyOnMutationError(['rejectUsername'])
|
||||
withRejectUsername
|
||||
)(RejectUsernameDialog);
|
||||
|
||||
@@ -49,13 +49,13 @@ function decrementFlaggedUserCount(root) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Assimilate flagged user changes into current store.
|
||||
* Assimilate flagged acount changes into current store.
|
||||
* @param {Object} root current state of the store
|
||||
* @param {Object} user user that was changed
|
||||
* @param {function} notify callback to show notification
|
||||
* @return {Object} next state of the store
|
||||
*/
|
||||
export function handleFlaggedUsernameChange(root, user, notify) {
|
||||
export function handleFlaggedAccountsChange(root, user, notify) {
|
||||
if (user.state.status.username.status !== 'SET') {
|
||||
// Check if change came from current user, if so ignore it.
|
||||
const lastChange =
|
||||
@@ -87,7 +87,7 @@ export function handleFlaggedUsernameChange(root, user, notify) {
|
||||
break;
|
||||
case 'APPROVED':
|
||||
case 'REJECTED':
|
||||
return root;
|
||||
return decrementFlaggedUserCount(root);
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -105,3 +105,21 @@ export function handleFlaggedUsernameChange(root, user, notify) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track indicator status
|
||||
* @param {Object} root current state of the store
|
||||
* @param {Object} user user that was changed
|
||||
* @return {Object} next state of the store
|
||||
*/
|
||||
export function handleIndicatorChange(root, user) {
|
||||
switch (user.state.status.username.status) {
|
||||
case 'SET':
|
||||
case 'CHANGED':
|
||||
return incrementFlaggedUserCount(root);
|
||||
case 'APPROVED':
|
||||
case 'REJECTED':
|
||||
return decrementFlaggedUserCount(root);
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@ import { bindActionCreators } from 'redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { withQuery, withMergedSettings } from 'coral-framework/hocs';
|
||||
import { Spinner } from 'coral-ui';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withUpdateSettings } from 'coral-framework/graphql/mutations';
|
||||
import { getErrorMessages, getDefinitionName } from 'coral-framework/utils';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
import StreamSettings from './StreamSettings';
|
||||
import TechSettings from './TechSettings';
|
||||
import ModerationSettings from './ModerationSettings';
|
||||
@@ -16,22 +15,21 @@ import Configure from '../components/Configure';
|
||||
|
||||
class ConfigureContainer extends Component {
|
||||
savePending = async () => {
|
||||
try {
|
||||
await this.props.updateSettings(this.props.pending);
|
||||
this.props.clearPending();
|
||||
} catch (err) {
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
await this.props.updateSettings(this.props.pending);
|
||||
this.props.clearPending();
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (this.props.data.loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Configure
|
||||
notify={this.props.notify}
|
||||
auth={this.props.auth}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
@@ -67,6 +65,7 @@ const withConfigureQuery = withQuery(
|
||||
{
|
||||
options: () => ({
|
||||
variables: {},
|
||||
fetchPolicy: 'network-only',
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -81,7 +80,6 @@ const mapStateToProps = state => ({
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
notify,
|
||||
clearPending,
|
||||
setActiveSection,
|
||||
},
|
||||
@@ -89,9 +87,9 @@ const mapDispatchToProps = dispatch =>
|
||||
);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withUpdateSettings,
|
||||
withConfigureQuery,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withMergedSettings('root.settings', 'pending', 'mergedSettings')
|
||||
)(ConfigureContainer);
|
||||
|
||||
@@ -99,7 +97,6 @@ ConfigureContainer.propTypes = {
|
||||
updateSettings: PropTypes.func.isRequired,
|
||||
clearPending: PropTypes.func.isRequired,
|
||||
setActiveSection: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
|
||||
@@ -1,25 +1,207 @@
|
||||
import React, { Component } from 'react';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import Indicator from '../../../components/Indicator';
|
||||
import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import { branch, renderNothing } from 'recompose';
|
||||
import { handleIndicatorChange, subscriptionFields } from '../graphql';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import withQueueConfig from '../hoc/withQueueConfig';
|
||||
import baseQueueConfig from '../queueConfig';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
const hideIfNoData = hasNoData => branch(hasNoData, renderNothing);
|
||||
class IndicatorContainer extends Component {
|
||||
subscriptions = [];
|
||||
|
||||
handleCommentChange = (root, comment) => {
|
||||
return handleIndicatorChange(root, comment, this.props.queueConfig);
|
||||
};
|
||||
|
||||
subscribeToUpdates() {
|
||||
const parameters = [
|
||||
{
|
||||
document: COMMENT_ADDED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAdded: comment } } }
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_FLAGGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentFlagged: comment } } }
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_EDITED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentEdited: comment } } }
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_ACCEPTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAccepted: comment } } }
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentRejected: comment } } }
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_RESET_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentReset: comment } } }
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
this.subscriptions = parameters.map(param =>
|
||||
this.props.data.subscribeToMore(param)
|
||||
);
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
this.subscriptions.forEach(unsubscribe => unsubscribe());
|
||||
this.subscriptions = [];
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (this.props.track) {
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.track && nextProps.track) {
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
if (this.props.track && !nextProps.track) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (
|
||||
!this.props.root ||
|
||||
(!this.props.root.premodCount && !this.props.root.reportedCount)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Indicator />
|
||||
<Slot
|
||||
data={this.props.data}
|
||||
handleCommentChange={this.handleCommentChange}
|
||||
fill="adminModerationIndicator"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndicatorContainer.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
track: PropTypes.bool,
|
||||
queueConfig: PropTypes.object,
|
||||
};
|
||||
|
||||
const COMMENT_ADDED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ModerationIndicator_CommentAdded {
|
||||
commentAdded(statuses: null) {
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const COMMENT_FLAGGED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ModerationIndicator_CommentFlagged {
|
||||
commentFlagged {
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const COMMENT_EDITED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ModerationIndicator_CommentEdited {
|
||||
commentEdited {
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const COMMENT_ACCEPTED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ModerationIndicator_CommentAccepted {
|
||||
commentAccepted {
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const COMMENT_REJECTED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ModerationIndicator_CommentRejected {
|
||||
commentRejected {
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const COMMENT_RESET_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ModerationIndicator_CommentReset {
|
||||
commentReset {
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
track: state.moderation.indicatorTrack,
|
||||
});
|
||||
|
||||
const enhance = compose(
|
||||
connect(mapStateToProps),
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdmin_Moderation_Indicator_root on RootQuery {
|
||||
premodCount: commentCount(query: { statuses: [PREMOD] })
|
||||
premodCount: commentCount(
|
||||
query: { statuses: [PREMOD], asset_id: $nullID }
|
||||
)
|
||||
reportedCount: commentCount(
|
||||
query: {
|
||||
statuses: [NONE, PREMOD, SYSTEM_WITHHELD]
|
||||
action_type: FLAG
|
||||
asset_id: $nullID
|
||||
}
|
||||
)
|
||||
}
|
||||
`,
|
||||
}),
|
||||
hideIfNoData(props => !props.root.premodCount && !props.root.reportedCount)
|
||||
withQueueConfig(baseQueueConfig)
|
||||
);
|
||||
|
||||
export default enhance(Indicator);
|
||||
export default enhance(IndicatorContainer);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
handleCommentChange,
|
||||
commentBelongToQueue,
|
||||
cleanUpQueue,
|
||||
subscriptionFields,
|
||||
} from '../graphql';
|
||||
|
||||
import { viewUserDetail } from '../../../actions/userDetail';
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
storySearchChange,
|
||||
clearState,
|
||||
selectCommentId,
|
||||
setIndicatorTrack,
|
||||
} from 'actions/moderation';
|
||||
import withQueueConfig from '../hoc/withQueueConfig';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
@@ -198,21 +200,42 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (!this.props.data.variables.asset_id) {
|
||||
// Stop activity indicator tracking, as we'll handle it here.
|
||||
this.props.setIndicatorTrack(false);
|
||||
}
|
||||
this.props.clearState();
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (!this.props.data.variables.asset_id) {
|
||||
// Restart activity indicator tracking.
|
||||
this.props.setIndicatorTrack(true);
|
||||
}
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const currentAssetId = this.props.data.variables.asset_id;
|
||||
const nextAssetId = nextProps.data.variables.asset_id;
|
||||
|
||||
// Resubscribe when we change between assets.
|
||||
if (
|
||||
this.props.data.variables.asset_id !== nextProps.data.variables.asset_id
|
||||
) {
|
||||
if (currentAssetId !== nextAssetId) {
|
||||
this.resubscribe(nextProps.data.variables);
|
||||
}
|
||||
|
||||
// We are only subscribing to a specific asset_id, so activity indicator
|
||||
// needs to do its own tracking.
|
||||
if (!currentAssetId && nextAssetId) {
|
||||
this.props.setIndicatorTrack(true);
|
||||
}
|
||||
|
||||
// We are subscribing to all comment changes, and as such there is no
|
||||
// need for the activity indicator to do the same.
|
||||
if (currentAssetId && !nextAssetId) {
|
||||
this.props.setIndicatorTrack(false);
|
||||
}
|
||||
}
|
||||
|
||||
cleanUpQueue = queue => {
|
||||
@@ -269,10 +292,6 @@ class ModerationContainer extends Component {
|
||||
const { root, root: { asset, settings }, data } = this.props;
|
||||
const assetId = getAssetId(this.props);
|
||||
|
||||
if (data.error) {
|
||||
return <div>Error</div>;
|
||||
}
|
||||
|
||||
if (assetId) {
|
||||
if (asset === null) {
|
||||
// Not found.
|
||||
@@ -280,6 +299,10 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
return <div>{data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (data.loading && data.networkStatus !== 3) {
|
||||
// loading.
|
||||
return <Spinner />;
|
||||
@@ -316,10 +339,12 @@ class ModerationContainer extends Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const COMMENT_ADDED_SUBSCRIPTION = gql`
|
||||
subscription CommentAdded($asset_id: ID){
|
||||
commentAdded(asset_id: $asset_id, statuses: null){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
@@ -329,6 +354,7 @@ const COMMENT_EDITED_SUBSCRIPTION = gql`
|
||||
subscription CommentEdited($asset_id: ID){
|
||||
commentEdited(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
@@ -338,6 +364,7 @@ const COMMENT_FLAGGED_SUBSCRIPTION = gql`
|
||||
subscription CommentFlagged($asset_id: ID){
|
||||
commentFlagged(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
@@ -347,14 +374,7 @@ const COMMENT_ACCEPTED_SUBSCRIPTION = gql`
|
||||
subscription CommentAccepted($asset_id: ID){
|
||||
commentAccepted(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
status_history {
|
||||
type
|
||||
created_at
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
@@ -364,14 +384,7 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql`
|
||||
subscription CommentRejected($asset_id: ID){
|
||||
commentRejected(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
status_history {
|
||||
type
|
||||
created_at
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
@@ -381,14 +394,7 @@ const COMMENT_RESET_SUBSCRIPTION = gql`
|
||||
subscription CommentReset($asset_id: ID){
|
||||
commentReset(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
status_history {
|
||||
type
|
||||
created_at
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
@@ -525,6 +531,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
clearState,
|
||||
notify,
|
||||
selectCommentId,
|
||||
setIndicatorTrack,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
|
||||
@@ -91,6 +91,76 @@ function getCommentQueues(comment, queueConfig) {
|
||||
return queues;
|
||||
}
|
||||
|
||||
function getOlderDate(a, b) {
|
||||
if (a) {
|
||||
a = new Date(a);
|
||||
}
|
||||
if (b) {
|
||||
b = new Date(b);
|
||||
}
|
||||
|
||||
if (!b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
if (!a) {
|
||||
return b;
|
||||
}
|
||||
return a < b ? b : a;
|
||||
}
|
||||
|
||||
function determineLatestChange(comment) {
|
||||
let lc = null;
|
||||
|
||||
comment.status_history.forEach(item => {
|
||||
lc = getOlderDate(lc, item.created_at);
|
||||
});
|
||||
|
||||
comment.actions.forEach(item => {
|
||||
lc = getOlderDate(lc, item.created_at);
|
||||
});
|
||||
|
||||
return lc;
|
||||
}
|
||||
|
||||
function reconstructPreviousCommentState(comment) {
|
||||
const history = comment.status_history;
|
||||
const actions = comment.actions;
|
||||
const lastChangeDate = determineLatestChange(comment);
|
||||
const previousComment = {
|
||||
...comment,
|
||||
status_history: history.filter(
|
||||
item => new Date(item.created_at) < lastChangeDate
|
||||
),
|
||||
actions: actions.filter(item => new Date(item.created_at) < lastChangeDate),
|
||||
};
|
||||
|
||||
// Comment did not exist previously.
|
||||
if (!previousComment.status_history.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
previousComment.status =
|
||||
previousComment.status_history[
|
||||
previousComment.status_history.length - 1
|
||||
].type;
|
||||
|
||||
return previousComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* getPreviousCommentQueues determines queues that this comment previously belonged to.
|
||||
*/
|
||||
function getPreviousCommentQueues(comment, queueConfig) {
|
||||
const previousCommentState = reconstructPreviousCommentState(comment);
|
||||
|
||||
if (!previousCommentState) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getCommentQueues(previousCommentState, queueConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether or not the comment belongs to the queue.
|
||||
*/
|
||||
@@ -203,17 +273,7 @@ export function handleCommentChange(
|
||||
let next = root;
|
||||
|
||||
// Queues that this comment previously belonged to.
|
||||
const prevQueues =
|
||||
comment.status_history.length <= 1
|
||||
? []
|
||||
: getCommentQueues(
|
||||
{
|
||||
...comment,
|
||||
status:
|
||||
comment.status_history[comment.status_history.length - 2].type,
|
||||
},
|
||||
queueConfig
|
||||
);
|
||||
const prevQueues = getPreviousCommentQueues(comment, queueConfig);
|
||||
|
||||
// Queues that this comment needs to be placed.
|
||||
const nextQueues = getCommentQueues(comment, queueConfig);
|
||||
@@ -291,3 +351,49 @@ export function handleCommentChange(
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
const indicatorQueues = ['premod', 'reported'];
|
||||
|
||||
/**
|
||||
* Track indicator status
|
||||
* @param {Object} root current state of the store
|
||||
* @param {Object} comment comment that was changed
|
||||
* @return {Object} next state of the store
|
||||
*/
|
||||
export function handleIndicatorChange(root, comment, queueConfig) {
|
||||
let next = root;
|
||||
|
||||
// Queues that this comment previously belonged to.
|
||||
const prevQueues = getPreviousCommentQueues(comment, queueConfig);
|
||||
|
||||
// Queues that this comment needs to be placed.
|
||||
const nextQueues = getCommentQueues(comment, queueConfig);
|
||||
|
||||
for (const queue of indicatorQueues) {
|
||||
if (prevQueues.indexOf(queue) === -1 && nextQueues.indexOf(queue) >= 0) {
|
||||
next = increaseCommentCount(next, queue);
|
||||
}
|
||||
if (prevQueues.indexOf(queue) >= 0 && nextQueues.indexOf(queue) === -1) {
|
||||
next = decreaseCommentCount(next, queue);
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
export const subscriptionFields = `
|
||||
status
|
||||
actions {
|
||||
__typename
|
||||
created_at
|
||||
}
|
||||
status_history {
|
||||
type
|
||||
assigned_by {
|
||||
id
|
||||
}
|
||||
created_at
|
||||
}
|
||||
updated_at
|
||||
created_at
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Get the auth element and parse it as JSON by decoding it.
|
||||
const auth = document.getElementById('auth');
|
||||
const doc = document.implementation.createHTMLDocument('');
|
||||
doc.body.innerHTML = auth.innerText;
|
||||
|
||||
// Set the item in localStorage.
|
||||
localStorage.setItem('auth', doc.body.textContent);
|
||||
|
||||
// Close the window.
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 50);
|
||||
});
|
||||
@@ -57,7 +57,7 @@ class Settings extends React.Component {
|
||||
<Configuration
|
||||
checked={premodLinksEnable}
|
||||
title={t('configure.enable_premod_links')}
|
||||
description={t('configure.enable_premod_description')}
|
||||
description={t('configure.enable_premod_links_description')}
|
||||
onCheckbox={onTogglePremodLinks}
|
||||
/>
|
||||
<Configuration
|
||||
|
||||
@@ -9,6 +9,10 @@ import { getDefinitionName } from 'coral-framework/utils';
|
||||
|
||||
class ConfigureContainer extends React.Component {
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Configure
|
||||
data={this.props.data}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import React from 'react';
|
||||
import { gql, compose } from 'react-apollo';
|
||||
import { withFragments, withMergedSettings } from 'coral-framework/hocs';
|
||||
import {
|
||||
getErrorMessages,
|
||||
getSlotFragmentSpreads,
|
||||
} from 'coral-framework/utils';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
import Settings from '../components/Settings.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withUpdateAssetSettings } from 'coral-framework/graphql/mutations';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { clearPending, updatePending } from '../../../actions/configure';
|
||||
|
||||
const slots = ['streamSettings'];
|
||||
@@ -50,15 +46,11 @@ class SettingsContainer extends React.Component {
|
||||
};
|
||||
|
||||
savePending = async () => {
|
||||
try {
|
||||
await this.props.updateAssetSettings(
|
||||
this.props.asset.id,
|
||||
this.props.pending
|
||||
);
|
||||
this.props.clearPending();
|
||||
} catch (err) {
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
await this.props.updateAssetSettings(
|
||||
this.props.asset.id,
|
||||
this.props.pending
|
||||
);
|
||||
this.props.clearPending();
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -98,7 +90,6 @@ SettingsContainer.propTypes = {
|
||||
mergedSettings: PropTypes.object.isRequired,
|
||||
updateAssetSettings: PropTypes.func.isRequired,
|
||||
clearPending: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
updatePending: PropTypes.func.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
};
|
||||
@@ -135,7 +126,6 @@ const mapStateToProps = state => ({
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
notify,
|
||||
clearPending,
|
||||
updatePending,
|
||||
},
|
||||
@@ -143,9 +133,9 @@ const mapDispatchToProps = dispatch =>
|
||||
);
|
||||
|
||||
const enhance = compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSettingsFragments,
|
||||
withUpdateAssetSettings,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withMergedSettings('asset.settings', 'pending', 'mergedSettings')
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
||||
import LoadMore from './LoadMore';
|
||||
import NewCount from './NewCount';
|
||||
import { TransitionGroup } from 'react-transition-group';
|
||||
import { forEachError } from 'coral-framework/utils';
|
||||
import Comment from '../containers/Comment';
|
||||
import NoComments from './NoComments';
|
||||
|
||||
@@ -91,11 +90,8 @@ class AllCommentsPane extends React.Component {
|
||||
.then(() => {
|
||||
this.setState({ loadingState: 'success' });
|
||||
})
|
||||
.catch(error => {
|
||||
.catch(() => {
|
||||
this.setState({ loadingState: 'error' });
|
||||
forEachError(error, ({ msg }) => {
|
||||
this.props.notify('error', msg);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import { EditableCommentContent } from './EditableCommentContent';
|
||||
import {
|
||||
getActionSummary,
|
||||
iPerformedThisAction,
|
||||
forEachError,
|
||||
isCommentActive,
|
||||
getShallowChanges,
|
||||
} from 'coral-framework/utils';
|
||||
@@ -261,11 +260,8 @@ export default class Comment extends React.Component {
|
||||
loadingState: 'success',
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
.catch(() => {
|
||||
this.setState({ loadingState: 'error' });
|
||||
forEachError(error, ({ msg }) => {
|
||||
this.props.notify('error', msg);
|
||||
});
|
||||
});
|
||||
emit('ui.Comment.showMoreReplies', { id });
|
||||
return;
|
||||
|
||||
@@ -6,7 +6,6 @@ import styles from './Comment.css';
|
||||
import { CountdownSeconds } from './CountdownSeconds';
|
||||
import { getEditableUntilDate } from './util';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import { forEachError } from 'coral-framework/utils';
|
||||
|
||||
import { Icon } from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -80,7 +79,7 @@ export class EditableCommentContent extends React.Component {
|
||||
|
||||
this.setState({ loadingState: 'loading' });
|
||||
|
||||
const { editComment, notify, stopEditing } = this.props;
|
||||
const { editComment, stopEditing } = this.props;
|
||||
if (typeof editComment !== 'function') {
|
||||
return;
|
||||
}
|
||||
@@ -95,7 +94,6 @@ export class EditableCommentContent extends React.Component {
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({ loadingState: 'error' });
|
||||
forEachError(error, ({ msg }) => notify('error', msg));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StreamError } from './StreamError';
|
||||
import StreamError from './StreamError';
|
||||
import Comment from '../containers/Comment';
|
||||
import BannedAccount from '../../../components/BannedAccount';
|
||||
import ChangeUsername from '../containers/ChangeUsername';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import styles from './StreamError.css';
|
||||
|
||||
export const StreamError = ({ children }) => (
|
||||
export default ({ children }) => (
|
||||
<div className={styles.streamError}>{children}</div>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
insertFetchedCommentsIntoEmbedQuery,
|
||||
nest,
|
||||
} from '../../../graphql/utils';
|
||||
import StreamError from '../components/StreamError';
|
||||
|
||||
const { showSignInDialog, editName } = authActions;
|
||||
const { notify } = notificationActions;
|
||||
@@ -208,6 +209,10 @@ class StreamContainer extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
return <StreamError>{this.props.data.error.message}</StreamError>;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.props.asset ||
|
||||
(this.props.asset.comment === undefined && !this.props.asset.comments)
|
||||
@@ -424,7 +429,8 @@ export default compose(
|
||||
withEmit,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withPostComment,
|
||||
withPostFlag,
|
||||
// `talk-plugin-flags` has a custom error handling logic.
|
||||
withPostFlag({ notifyOnError: false }),
|
||||
withPostDontAgree,
|
||||
withDeleteAction,
|
||||
withEditComment
|
||||
|
||||
@@ -6,4 +6,3 @@ export { default as withEmit } from './withEmit';
|
||||
export { default as excludeIf } from './excludeIf';
|
||||
export { default as connect } from './connect';
|
||||
export { default as withMergedSettings } from './withMergedSettings';
|
||||
export { default as notifyOnMutationError } from './notifyOnMutationError';
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose } from 'react-apollo';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { forEachError } from 'coral-framework/utils';
|
||||
import { withProps } from 'recompose';
|
||||
|
||||
const notifyOnMutationError = keys =>
|
||||
compose(
|
||||
connect(null, dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
notify,
|
||||
},
|
||||
dispatch
|
||||
)
|
||||
),
|
||||
withProps(ownProps =>
|
||||
keys.reduce((props, key) => {
|
||||
props[key] = async (...args) => {
|
||||
try {
|
||||
return await ownProps[key](...args);
|
||||
} catch (e) {
|
||||
forEachError(e, ({ msg }) => {
|
||||
ownProps.notify('error', msg);
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
return props;
|
||||
}, {})
|
||||
)
|
||||
);
|
||||
|
||||
export default notifyOnMutationError;
|
||||
@@ -4,11 +4,16 @@ import merge from 'lodash/merge';
|
||||
import uniq from 'lodash/uniq';
|
||||
import flatten from 'lodash/flatten';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { getDefinitionName, getResponseErrors } from '../utils';
|
||||
import {
|
||||
getDefinitionName,
|
||||
getResponseErrors,
|
||||
getErrorMessages,
|
||||
} from '../utils';
|
||||
import PropTypes from 'prop-types';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import union from 'lodash/union';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
|
||||
class ResponseErrors extends Error {
|
||||
constructor(errors) {
|
||||
@@ -27,11 +32,7 @@ class ResponseError {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a HOC with the same signature as `graphql`, that will
|
||||
* apply mutation options registered in the graphRegistry.
|
||||
*/
|
||||
export default (document, config = {}) =>
|
||||
const createHOC = (document, config, { notifyOnError = true }) =>
|
||||
hoistStatics(WrappedComponent => {
|
||||
config = {
|
||||
...config,
|
||||
@@ -46,10 +47,18 @@ export default (document, config = {}) =>
|
||||
graphql: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
notify: PropTypes.func,
|
||||
};
|
||||
|
||||
get graphqlRegistry() {
|
||||
return this.context.graphql.registry;
|
||||
}
|
||||
|
||||
notifyErrors(messages) {
|
||||
this.context.store.dispatch(notify('error', messages));
|
||||
}
|
||||
|
||||
resolveDocument(documentOrCallback) {
|
||||
return this.context.graphql.resolveDocument(
|
||||
documentOrCallback,
|
||||
@@ -165,6 +174,11 @@ export default (document, config = {}) =>
|
||||
variables,
|
||||
error,
|
||||
});
|
||||
|
||||
// Show errors as notifications.
|
||||
if (notifyOnError) {
|
||||
this.notifyErrors(getErrorMessages(error));
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
@@ -213,3 +227,18 @@ export default (document, config = {}) =>
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Exports a HOC with the same signature as `graphql`, that will
|
||||
* apply mutation options registered in the graphRegistry.
|
||||
*
|
||||
* The returned HOC accepts a settings object with the following properties:
|
||||
* notifyOnError: show a notification to the user when an error occured.
|
||||
* Defaults to true.
|
||||
*/
|
||||
export default (document, config = {}) => settingsOrComponent => {
|
||||
if (typeof settingsOrComponent === 'function') {
|
||||
return createHOC(document, config, {})(settingsOrComponent);
|
||||
}
|
||||
return createHOC(document, config, settingsOrComponent);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@ import PropTypes from 'prop-types';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import { getOperationName } from 'apollo-client/queries/getFromAST';
|
||||
import throttle from 'lodash/throttle';
|
||||
import get from 'lodash/get';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
|
||||
const withSkipOnErrors = reducer => (prev, action, ...rest) => {
|
||||
if (
|
||||
@@ -36,21 +38,23 @@ function networkStatusToString(networkStatus) {
|
||||
return 'ready';
|
||||
case 8:
|
||||
return 'error';
|
||||
default:
|
||||
throw new Error(`Unknown network status ${networkStatus}`);
|
||||
}
|
||||
throw new Error(`Unknown network status ${networkStatus}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a HOC with the same signature as `graphql`, that will
|
||||
* apply query options registered in the graphRegistry.
|
||||
*/
|
||||
export default (document, config = {}) =>
|
||||
const createHOC = (document, config, { notifyOnError = true }) =>
|
||||
hoistStatics(WrappedComponent => {
|
||||
return class WithQuery extends React.Component {
|
||||
static contextTypes = {
|
||||
eventEmitter: PropTypes.object,
|
||||
graphql: PropTypes.object,
|
||||
client: PropTypes.object,
|
||||
store: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
notify: PropTypes.func,
|
||||
};
|
||||
|
||||
// Lazily resolve fragments from graphRegistry to support circular dependencies.
|
||||
@@ -166,10 +170,24 @@ export default (document, config = {}) =>
|
||||
return () => this.client.networkInterface.unsubscribe(id);
|
||||
};
|
||||
|
||||
notifyErrors(messages) {
|
||||
this.context.store.dispatch(notify('error', messages));
|
||||
}
|
||||
|
||||
nextData(data) {
|
||||
this.apolloData = data;
|
||||
this.emitWhenNeeded(data);
|
||||
|
||||
if (
|
||||
get(data, 'error.message') &&
|
||||
get(this, 'data.error.message') !== get(data, 'error.message')
|
||||
) {
|
||||
// Show errors as notifications.
|
||||
if (notifyOnError) {
|
||||
this.notifyErrors(data.error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// If data was previously set, we update it in a immutable way.
|
||||
if (this.data) {
|
||||
if (this.data.loading && !data.loading) {
|
||||
@@ -319,3 +337,18 @@ export default (document, config = {}) =>
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Exports a HOC with the same signature as `graphql`, that will
|
||||
* apply query options registered in the graphRegistry.
|
||||
*
|
||||
* The returned HOC accepts a settings object with the following properties:
|
||||
* notifyOnError: show a notification to the user when an error occured.
|
||||
* Defaults to true.
|
||||
*/
|
||||
export default (document, config = {}) => settingsOrComponent => {
|
||||
if (typeof settingsOrComponent === 'function') {
|
||||
return createHOC(document, config, {})(settingsOrComponent);
|
||||
}
|
||||
return createHOC(document, config, settingsOrComponent);
|
||||
};
|
||||
|
||||
@@ -65,6 +65,10 @@ class ProfileContainer extends Component {
|
||||
const { me } = this.props.root;
|
||||
const loading = this.props.data.loading;
|
||||
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (!auth.loggedIn) {
|
||||
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import { forEachError } from 'coral-framework/utils';
|
||||
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -93,9 +92,8 @@ class CommentBox extends React.Component {
|
||||
commentPostedHandler();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch(() => {
|
||||
this.setState({ loadingState: 'error' });
|
||||
forEachError(err, ({ msg }) => notify('error', msg));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -60,9 +60,10 @@ export default class FlagButton extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
onPopupContinue = () => {
|
||||
onPopupContinue = async () => {
|
||||
const { postFlag, postDontAgree, id, author_id } = this.props;
|
||||
const { itemType, reason, step, message } = this.state;
|
||||
let failed = false;
|
||||
|
||||
switch (step) {
|
||||
case 0:
|
||||
@@ -75,13 +76,9 @@ export default class FlagButton extends Component {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Proceed to the next step or close the menu if we've reached the end
|
||||
if (step + 1 >= this.props.getPopupMenu.length) {
|
||||
this.closeMenu();
|
||||
} else {
|
||||
this.setState({ step: step + 1 });
|
||||
case this.props.getPopupMenu.length:
|
||||
this.closeMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
// If itemType and reason are both set, post the action
|
||||
@@ -96,43 +93,46 @@ export default class FlagButton extends Component {
|
||||
break;
|
||||
}
|
||||
|
||||
if (itemType === 'COMMENTS') {
|
||||
this.setState({ localPost: 'temp' });
|
||||
}
|
||||
|
||||
let action = {
|
||||
item_id,
|
||||
item_type: itemType,
|
||||
message,
|
||||
};
|
||||
|
||||
if (reason === REASONS.comment.noagree) {
|
||||
postDontAgree(action)
|
||||
.then(({ data }) => {
|
||||
if (itemType === 'COMMENTS') {
|
||||
this.setState({ localPost: data.createDontAgree.dontagree.id });
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
console.error(err);
|
||||
});
|
||||
} else {
|
||||
postFlag({ ...action, reason })
|
||||
.then(({ data }) => {
|
||||
if (itemType === 'COMMENTS') {
|
||||
this.setState({ localPost: data.createFlag.flag.id });
|
||||
}
|
||||
})
|
||||
.catch(errors => {
|
||||
forEachError(errors, ({ error, msg }) => {
|
||||
if (error.translation_key === 'ALREADY_EXISTS') {
|
||||
msg = t('already_flagged_username');
|
||||
}
|
||||
this.props.notify('error', msg);
|
||||
const result = await postDontAgree(action);
|
||||
try {
|
||||
if (itemType === 'COMMENTS') {
|
||||
this.setState({
|
||||
localPost: result.data.createDontAgree.dontagree.id,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
console.error(err);
|
||||
failed = true;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const result = await postFlag({ ...action, reason });
|
||||
if (itemType === 'COMMENTS') {
|
||||
this.setState({ localPost: result.data.createFlag.flag.id });
|
||||
}
|
||||
} catch (errors) {
|
||||
forEachError(errors, ({ error, msg }) => {
|
||||
if (error.translation_key === 'ALREADY_EXISTS') {
|
||||
msg = t('already_flagged_username');
|
||||
}
|
||||
this.props.notify('error', msg);
|
||||
});
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!failed) {
|
||||
this.setState({ step: step + 1 });
|
||||
}
|
||||
};
|
||||
|
||||
onPopupOptionClick = sets => e => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Comment from './Comment';
|
||||
import LoadMore from './LoadMore';
|
||||
import { forEachError } from 'plugin-api/beta/client/utils';
|
||||
|
||||
class CommentHistory extends React.Component {
|
||||
state = {
|
||||
@@ -16,11 +15,8 @@ class CommentHistory extends React.Component {
|
||||
.then(() => {
|
||||
this.setState({ loadingState: 'success' });
|
||||
})
|
||||
.catch(error => {
|
||||
.catch(() => {
|
||||
this.setState({ loadingState: 'error' });
|
||||
forEachError(error, ({ msg }) => {
|
||||
this.props.notify('error', msg);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -55,7 +51,6 @@ class CommentHistory extends React.Component {
|
||||
CommentHistory.propTypes = {
|
||||
comments: PropTypes.object.isRequired,
|
||||
loadMore: PropTypes.func,
|
||||
notify: PropTypes.func,
|
||||
link: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
|
||||
@@ -40,28 +40,6 @@ documents rather than performing a nice table alter. If the process crashes
|
||||
during the migration, simply re-run it. The migration operations are designed
|
||||
to act atomically, and be idempotent to documents already updated.
|
||||
|
||||
## Database Verifications
|
||||
|
||||
In `v3.*`, we introduced the concept of "verifying the database". Some of our
|
||||
operations update cached values that live along side the original document to
|
||||
improve performance. Running the cli command for verifying the database's cache
|
||||
ensures that all the cached values are up to date.
|
||||
|
||||
Running the following will start the database verification process:
|
||||
|
||||
```bash
|
||||
./bin/cli verify db --fix
|
||||
```
|
||||
You can notice the `--fix` option, without it, the tool should instead perform
|
||||
a dry run of the operations it intends to perform.
|
||||
{: .code-aside}
|
||||
|
||||
This process, like the migration process, should take some time to complete on
|
||||
large databases.
|
||||
|
||||
Once you have updated your databases, that's all you have to do! Talk should now
|
||||
function even better and faster with all the new features we poured into v4.0.0!
|
||||
|
||||
## Template Change
|
||||
|
||||
In `v4.0.0`, we introduced extensive support for compressing our javascript
|
||||
|
||||
@@ -24,6 +24,7 @@ const Limit = require('../services/limit');
|
||||
const Mailer = require('../services/mailer');
|
||||
const Metadata = require('../services/metadata');
|
||||
const Migration = require('../services/migration');
|
||||
const Moderation = require('../services/moderation');
|
||||
const Mongoose = require('../services/mongoose');
|
||||
const Passport = require('../services/passport');
|
||||
const Plugins = require('../services/plugins');
|
||||
@@ -62,6 +63,7 @@ const connectors = {
|
||||
Mailer,
|
||||
Metadata,
|
||||
Migration,
|
||||
Moderation,
|
||||
Mongoose,
|
||||
Passport,
|
||||
Plugins,
|
||||
|
||||
+13
-269
@@ -1,13 +1,11 @@
|
||||
const errors = require('../../errors');
|
||||
const ActionModel = require('../../models/action');
|
||||
const AssetsService = require('../../services/assets');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const TagsService = require('../../services/tags');
|
||||
const CommentsService = require('../../services/comments');
|
||||
const KarmaService = require('../../services/karma');
|
||||
const merge = require('lodash/merge');
|
||||
const linkify = require('linkify-it')().tlds(require('tlds'));
|
||||
const Wordlist = require('../../services/wordlist');
|
||||
|
||||
const {
|
||||
CREATE_COMMENT,
|
||||
SET_COMMENT_STATUS,
|
||||
@@ -15,10 +13,6 @@ const {
|
||||
EDIT_COMMENT,
|
||||
} = require('../../perms/constants');
|
||||
const debug = require('debug')('talk:graph:mutators:comment');
|
||||
const {
|
||||
DISABLE_AUTOFLAG_SUSPECT_WORDS,
|
||||
IGNORE_FLAGS_AGAINST_STAFF,
|
||||
} = require('../../config');
|
||||
|
||||
const resolveTagsForComment = async (
|
||||
{ user, loaders: { Tags } },
|
||||
@@ -188,279 +182,27 @@ const createComment = async (
|
||||
return comment;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the comment object and outputs wordlist results.
|
||||
* @param {Object} context graphql context
|
||||
* @param {String} body body of a comment
|
||||
* @param {String} [asset_id] id of asset comment is posted on
|
||||
* @return {Object} resolves to the wordlist results
|
||||
*/
|
||||
const filterNewComment = async (context, { body, asset_id }) => {
|
||||
// Load the settings.
|
||||
const [settings, asset] = await Promise.all([
|
||||
context.loaders.Settings.load(),
|
||||
context.loaders.Assets.getByID.load(asset_id),
|
||||
]);
|
||||
|
||||
// Create a new instance of the Wordlist.
|
||||
const wl = new Wordlist();
|
||||
|
||||
// Load the wordlist.
|
||||
wl.upsert(settings.wordlist);
|
||||
|
||||
// Load the wordlist and filter the comment content.
|
||||
return [
|
||||
// Scan the word.
|
||||
wl.scan('body', body),
|
||||
|
||||
// Return the asset's settings.
|
||||
await AssetsService.rectifySettings(asset, settings),
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* moderationPhases is an array of phases carried out in order until a status is
|
||||
* returned.
|
||||
*/
|
||||
const moderationPhases = [
|
||||
// This phase checks to see if the comment is long enough.
|
||||
(context, comment) => {
|
||||
// Check to see if the body is too short, if it is, then complain about it!
|
||||
if (comment.body.length < 2) {
|
||||
throw errors.ErrCommentTooShort;
|
||||
}
|
||||
},
|
||||
|
||||
// This phase checks to see if the asset being processed is closed or not.
|
||||
(context, comment, { asset }) => {
|
||||
// Check to see if the asset has closed commenting...
|
||||
if (asset.isClosed) {
|
||||
throw new errors.ErrAssetCommentingClosed(asset.closedMessage);
|
||||
}
|
||||
},
|
||||
|
||||
// This phase checks the comment against the wordlist.
|
||||
(context, comment, { wordlist }) => {
|
||||
// Decide the status based on whether or not the current asset/settings
|
||||
// has pre-mod enabled or not. If the comment was rejected based on the
|
||||
// wordlist, then reject it, otherwise if the moderation setting is
|
||||
// premod, set it to `premod`.
|
||||
if (wordlist.banned) {
|
||||
// Add the flag related to Trust to the comment.
|
||||
return {
|
||||
status: 'REJECTED',
|
||||
actions: [
|
||||
{
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'BANNED_WORD',
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// If the comment has a suspect word or a link, we need to add a
|
||||
// flag to it to indicate that it needs to be looked at.
|
||||
// Otherwise just return the new comment.
|
||||
|
||||
// If the wordlist has matched the suspect word filter and we haven't disabled
|
||||
// auto-flagging suspect words, then we should flag the comment!
|
||||
if (wordlist.suspect && !DISABLE_AUTOFLAG_SUSPECT_WORDS) {
|
||||
// TODO: this is kind of fragile, we should refactor this to resolve
|
||||
// all these const's that we're using like 'COMMENTS', 'FLAG' to be
|
||||
// defined in a checkable schema.
|
||||
return {
|
||||
actions: [
|
||||
{
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'SUSPECT_WORD',
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// This phase checks to see if the comment's length exceeds maximum.
|
||||
(context, comment, { assetSettings: { charCountEnable, charCount } }) => {
|
||||
// Reject if the comment is too long
|
||||
if (charCountEnable && comment.body.length > charCount) {
|
||||
// Add the flag related to Trust to the comment.
|
||||
return {
|
||||
status: 'REJECTED',
|
||||
actions: [
|
||||
{
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'BODY_COUNT',
|
||||
metadata: {
|
||||
count: comment.body.length,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// If a given user is a staff member, always approve their comment.
|
||||
context => {
|
||||
if (IGNORE_FLAGS_AGAINST_STAFF && context.user && context.user.isStaff()) {
|
||||
return {
|
||||
status: 'ACCEPTED',
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// This phase checks the comment if it has any links in it if the check is
|
||||
// enabled.
|
||||
(context, comment, { assetSettings: { premodLinksEnable } }) => {
|
||||
if (premodLinksEnable && linkify.test(comment.body)) {
|
||||
// Add the flag related to Trust to the comment.
|
||||
return {
|
||||
status: 'SYSTEM_WITHHELD',
|
||||
actions: [
|
||||
{
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'LINKS',
|
||||
metadata: {
|
||||
links: comment.body,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// This phase checks to see if the user making the comment is allowed to do so
|
||||
// considering their reliability (Trust) status.
|
||||
context => {
|
||||
if (context.user && context.user.metadata) {
|
||||
// If the user is not a reliable commenter (passed the unreliability
|
||||
// threshold by having too many rejected comments) then we can change the
|
||||
// status of the comment to `SYSTEM_WITHHELD`, therefore pushing the user's
|
||||
// comments away from the public eye until a moderator can manage them. This of
|
||||
// course can only be applied if the comment's current status is `NONE`,
|
||||
// we don't want to interfere if the comment was rejected.
|
||||
if (
|
||||
KarmaService.isReliable('comment', context.user.metadata.trust) ===
|
||||
false
|
||||
) {
|
||||
// Add the flag related to Trust to the comment.
|
||||
return {
|
||||
status: 'SYSTEM_WITHHELD',
|
||||
actions: [
|
||||
{
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'TRUST',
|
||||
metadata: {
|
||||
trust: context.user.metadata.trust,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// This phase checks to see if the comment was already prescribed a status.
|
||||
(context, comment) => {
|
||||
// If the status was already defined, don't redefine it. It's only defined
|
||||
// when specific external conditions exist, we don't want to override that.
|
||||
if (comment.status && comment.status.length > 0) {
|
||||
return {
|
||||
status: comment.status,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// This phase checks to see if the settings have premod enabled, if they do,
|
||||
// the comment is premod, otherwise, it's just none.
|
||||
(context, comment, { assetSettings: { moderation } }) => {
|
||||
// If the settings say that we're in premod mode, then the comment is in
|
||||
// premod status.
|
||||
if (moderation === 'PRE') {
|
||||
return {
|
||||
status: 'PREMOD',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'NONE',
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* This resolves a given comment's status and actions.
|
||||
* @param {Object} context graphql context
|
||||
* @param {String} body body of the comment
|
||||
* @param {String} [asset_id] asset for the comment
|
||||
* @param {Object} [wordlist={}] the results of the wordlist scan
|
||||
* @return {Promise} resolves to the comment's status and actions
|
||||
*/
|
||||
const resolveCommentModeration = async (context, comment) => {
|
||||
// First we filter the comment contents to ensure that we note any validation
|
||||
// issues.
|
||||
let [wordlist, settings] = await filterNewComment(context, comment);
|
||||
|
||||
// Get the asset from the loader.
|
||||
const asset = await context.loaders.Assets.getByID.load(comment.asset_id);
|
||||
if (!asset) {
|
||||
// And leave now if this asset wasn't found.
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
// Combine the asset and the settings to get the asset settings.
|
||||
const assetSettings = await AssetsService.rectifySettings(asset, settings);
|
||||
|
||||
let actions = comment.actions || [];
|
||||
|
||||
// Loop over all the moderation phases and see if we've resolved the status.
|
||||
for (const phase of moderationPhases) {
|
||||
const result = await phase(context, comment, {
|
||||
asset,
|
||||
assetSettings,
|
||||
settings,
|
||||
wordlist,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
if (result.actions) {
|
||||
actions.push(...result.actions);
|
||||
}
|
||||
|
||||
// If this result contained a status, then we've finished resolving
|
||||
// phases!
|
||||
if (result.status) {
|
||||
return { status: result.status, actions };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* createPublicComment is designed to create a comment from a public source. It
|
||||
* validates the comment, and performs some automated moderator actions based on
|
||||
* the settings.
|
||||
* @param {Object} context the graphql context
|
||||
* @param {Object} ctx the graphql context
|
||||
* @param {Object} commentInput the new comment to be created
|
||||
* @return {Promise} resolves to a new comment
|
||||
*/
|
||||
const createPublicComment = async (context, comment) => {
|
||||
const createPublicComment = async (ctx, comment) => {
|
||||
const { connectors: { services: { Moderation } } } = ctx;
|
||||
|
||||
// We then take the wordlist and the comment into consideration when
|
||||
// considering what status to assign the new comment, and resolve the new
|
||||
// status to set the comment to.
|
||||
let { actions, status } = await resolveCommentModeration(context, comment);
|
||||
let { actions, status } = await Moderation.process(ctx, comment);
|
||||
|
||||
// Assign status to comment.
|
||||
comment.status = status;
|
||||
|
||||
// Then we actually create the comment with the new status.
|
||||
const result = await createComment(context, comment);
|
||||
const result = await createComment(ctx, comment);
|
||||
|
||||
// Create all the actions that were determined during the moderation check
|
||||
// phase.
|
||||
@@ -522,18 +264,20 @@ const setStatus = async ({ user, loaders: { Comments } }, { id, status }) => {
|
||||
* @param {Object} edit describes how to edit the comment
|
||||
* @param {String} edit.body the new Comment body
|
||||
*/
|
||||
const edit = async (context, { id, asset_id, edit: { body } }) => {
|
||||
const edit = async (ctx, { id, asset_id, edit: { body } }) => {
|
||||
const { connectors: { services: { Moderation } } } = ctx;
|
||||
|
||||
// Build up the new comment we're setting. We need to check this with
|
||||
// moderation now.
|
||||
let comment = { id, asset_id, body };
|
||||
|
||||
// Determine the new status of the comment.
|
||||
const { actions, status } = await resolveCommentModeration(context, comment);
|
||||
const { actions, status } = await Moderation.process(ctx, comment);
|
||||
|
||||
// Execute the edit.
|
||||
comment = await CommentsService.edit({
|
||||
id,
|
||||
author_id: context.user.id,
|
||||
author_id: ctx.user.id,
|
||||
body,
|
||||
status,
|
||||
});
|
||||
@@ -543,7 +287,7 @@ const edit = async (context, { id, asset_id, edit: { body } }) => {
|
||||
await createActions(comment.id, actions);
|
||||
|
||||
// Publish the edited comment via the subscription.
|
||||
context.pubsub.publish('commentEdited', comment);
|
||||
ctx.pubsub.publish('commentEdited', comment);
|
||||
|
||||
return comment;
|
||||
};
|
||||
|
||||
@@ -50,8 +50,12 @@ const decorateWithPermissionCheck = (typeResolver, protect) => {
|
||||
*/
|
||||
const decorateUserField = (typeResolver, field) => {
|
||||
// The default resolver for the user decorator is loading the user by id.
|
||||
let fieldResolver = (obj, args, ctx) =>
|
||||
ctx.loaders.Users.getByID.load(obj[field]);
|
||||
let fieldResolver = (obj, args, ctx) => {
|
||||
if (!obj[field]) {
|
||||
return null;
|
||||
}
|
||||
return ctx.loaders.Users.getByID.load(obj[field]);
|
||||
};
|
||||
|
||||
// The resolver can be overridden however. This decorator will simply wrap the
|
||||
// field with a permission check.
|
||||
|
||||
@@ -502,6 +502,9 @@ type Comment {
|
||||
# The time when the comment was created
|
||||
created_at: Date!
|
||||
|
||||
# The time when the comment was updated.
|
||||
updated_at: Date
|
||||
|
||||
# describes how the comment can be edited
|
||||
editing: EditInfo
|
||||
|
||||
|
||||
@@ -1,65 +1,38 @@
|
||||
const CommentModel = require('../models/comment');
|
||||
|
||||
module.exports = {
|
||||
async up() {
|
||||
// Find all comments that have tags.
|
||||
let comments = await CommentModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
tags: {
|
||||
$exists: true,
|
||||
$ne: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
id: true,
|
||||
tags: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
// OLD
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// name: 'OFF_TOPIC',
|
||||
// assigned_by: '',
|
||||
// created_at: new Date()
|
||||
// }
|
||||
// ]
|
||||
|
||||
// If no comments were found, nothing needs to be done!
|
||||
if (comments.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updates = [];
|
||||
|
||||
// Loop over the comments retrieved, updating the tag structure.
|
||||
for (let { id, tags } of comments) {
|
||||
// OLD
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// name: 'OFF_TOPIC',
|
||||
// assigned_by: '',
|
||||
// created_at: new Date()
|
||||
// }
|
||||
// ]
|
||||
|
||||
// NEW
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// tag: {
|
||||
// name: 'OFF_TOPIC',
|
||||
// permissions: {
|
||||
// public: true,
|
||||
// self: false,
|
||||
// roles: []
|
||||
// },
|
||||
// models: ['COMMENTS'],
|
||||
// created_at: new Date()
|
||||
// },
|
||||
// assigned_by: '',
|
||||
// created_at: new Date()
|
||||
// }
|
||||
// ]
|
||||
|
||||
// Remap the tag structure.
|
||||
tags = tags.map(({ name, assigned_by, created_at }) => ({
|
||||
// NEW
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// tag: {
|
||||
// name: 'OFF_TOPIC',
|
||||
// permissions: {
|
||||
// public: true,
|
||||
// self: false,
|
||||
// roles: []
|
||||
// },
|
||||
// models: ['COMMENTS'],
|
||||
// created_at: new Date()
|
||||
// },
|
||||
// assigned_by: '',
|
||||
// created_at: new Date()
|
||||
// }
|
||||
// ]
|
||||
const transformTags = ({ id, tags }) => ({
|
||||
query: { id },
|
||||
update: {
|
||||
$set: {
|
||||
tags: tags.map(({ name, assigned_by, created_at }) => ({
|
||||
tag: {
|
||||
name,
|
||||
permissions: {
|
||||
@@ -72,22 +45,34 @@ module.exports = {
|
||||
},
|
||||
assigned_by,
|
||||
created_at,
|
||||
}));
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
updates.push({ query: { id }, update: { $set: { tags } } });
|
||||
}
|
||||
module.exports = {
|
||||
async up({ transformSingleWithCursor }) {
|
||||
// Find all comments that have tags.
|
||||
const cursor = CommentModel.collection.aggregate(
|
||||
[
|
||||
{
|
||||
$match: {
|
||||
tags: {
|
||||
$exists: true,
|
||||
$ne: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
id: true,
|
||||
tags: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
{ allowDiskUse: true }
|
||||
);
|
||||
|
||||
if (updates.length > 0) {
|
||||
// Create a new batch operation.
|
||||
let batch = CommentModel.collection.initializeUnorderedBulkOp();
|
||||
|
||||
for (const { query, update } of updates) {
|
||||
// Execute the batch operation.
|
||||
batch.find(query).updateOne(update);
|
||||
}
|
||||
|
||||
// Execute the batch update operation.
|
||||
await batch.execute();
|
||||
}
|
||||
await transformSingleWithCursor(cursor, transformTags, CommentModel);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ const mapping = {
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
async up() {
|
||||
async up({ processManyUpdates }) {
|
||||
const updates = [];
|
||||
for (const item_type in mapping) {
|
||||
const mappings = mapping[item_type];
|
||||
@@ -44,15 +44,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
if (updates.length > 0) {
|
||||
// Setup the batch operation.
|
||||
const batch = ActionModel.collection.initializeUnorderedBulkOp();
|
||||
|
||||
for (const { query, update } of updates) {
|
||||
batch.find(query).update(update);
|
||||
}
|
||||
|
||||
// Execute the batch update operation.
|
||||
await batch.execute();
|
||||
await processManyUpdates(ActionModel, updates);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,60 +1,132 @@
|
||||
const UserModel = require('../models/user');
|
||||
const merge = require('lodash/merge');
|
||||
|
||||
const getUserBatch = async () => {
|
||||
let query = {
|
||||
status: {
|
||||
$in: ['ACTIVE', 'BANNED', 'PENDING', 'APPROVED'],
|
||||
const transformUser = user => {
|
||||
const created_at = Date.now();
|
||||
|
||||
const { id, status, canEditName, suspension, disabled } = user;
|
||||
|
||||
let update = {
|
||||
$unset: {
|
||||
canEditName: '',
|
||||
suspension: '',
|
||||
disabled: '',
|
||||
},
|
||||
$set: {
|
||||
status: {
|
||||
// The username status is specific to each case.
|
||||
username: {
|
||||
history: [],
|
||||
},
|
||||
|
||||
// The user is not banned by default.
|
||||
banned: {
|
||||
status: false,
|
||||
history: [],
|
||||
},
|
||||
|
||||
// The user is not suspended by default.
|
||||
suspension: {
|
||||
until: null,
|
||||
history: [],
|
||||
},
|
||||
},
|
||||
updated_at: created_at,
|
||||
},
|
||||
};
|
||||
|
||||
// Find all the users that need migrating.
|
||||
return UserModel.collection.find(query);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
async up() {
|
||||
const created_at = Date.now();
|
||||
|
||||
// Get the first batch of users.
|
||||
let cursor = await getUserBatch();
|
||||
|
||||
const updates = [];
|
||||
while (await cursor.hasNext()) {
|
||||
const user = await cursor.next();
|
||||
|
||||
const { id, status, canEditName, suspension, disabled } = user;
|
||||
|
||||
let update = {
|
||||
$unset: {
|
||||
canEditName: '',
|
||||
suspension: '',
|
||||
disabled: '',
|
||||
if (disabled) {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
banned: {
|
||||
status: true,
|
||||
history: [
|
||||
{
|
||||
status: true,
|
||||
created_at,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
$set: {
|
||||
status: {
|
||||
// The username status is specific to each case.
|
||||
username: {
|
||||
history: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// The user is not banned by default.
|
||||
banned: {
|
||||
status: false,
|
||||
history: [],
|
||||
},
|
||||
// If the user has an "until" property of their suspension, then we need
|
||||
// to reflect that in the new status object.
|
||||
if (suspension && suspension.until !== null) {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
suspension: {
|
||||
until: suspension.until,
|
||||
history: [
|
||||
{
|
||||
until: suspension.until,
|
||||
created_at,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// The user is not suspended by default.
|
||||
suspension: {
|
||||
until: null,
|
||||
history: [],
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
if (canEditName) {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
status: 'UNSET',
|
||||
history: [
|
||||
{
|
||||
status: 'UNSET',
|
||||
created_at,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
updated_at: created_at,
|
||||
},
|
||||
};
|
||||
|
||||
if (disabled) {
|
||||
});
|
||||
} else {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
status: 'SET',
|
||||
history: [
|
||||
{
|
||||
status: 'SET',
|
||||
created_at,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'BANNED':
|
||||
if (canEditName) {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
status: 'REJECTED',
|
||||
history: [
|
||||
{
|
||||
status: 'REJECTED',
|
||||
created_at,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
@@ -67,22 +139,11 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// If the user has an "until" property of their suspension, then we need
|
||||
// to reflect that in the new status object.
|
||||
if (suspension && suspension.until !== null) {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
suspension: {
|
||||
until: suspension.until,
|
||||
username: {
|
||||
status: 'SET',
|
||||
history: [
|
||||
{
|
||||
until: suspension.until,
|
||||
status: 'SET',
|
||||
created_at,
|
||||
},
|
||||
],
|
||||
@@ -91,138 +152,57 @@ module.exports = {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
if (canEditName) {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
status: 'UNSET',
|
||||
history: [
|
||||
{
|
||||
status: 'UNSET',
|
||||
created_at,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
status: 'SET',
|
||||
history: [
|
||||
{
|
||||
status: 'SET',
|
||||
created_at,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'BANNED':
|
||||
if (canEditName) {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
status: 'REJECTED',
|
||||
history: [
|
||||
{
|
||||
status: 'REJECTED',
|
||||
created_at,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
banned: {
|
||||
status: true,
|
||||
history: [
|
||||
{
|
||||
status: true,
|
||||
created_at,
|
||||
},
|
||||
],
|
||||
},
|
||||
username: {
|
||||
status: 'SET',
|
||||
history: [
|
||||
{
|
||||
status: 'SET',
|
||||
created_at,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'PENDING':
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
break;
|
||||
case 'PENDING':
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
status: 'CHANGED',
|
||||
history: [
|
||||
{
|
||||
status: 'CHANGED',
|
||||
history: [
|
||||
{
|
||||
status: 'CHANGED',
|
||||
created_at,
|
||||
},
|
||||
],
|
||||
created_at,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
break;
|
||||
case 'APPROVED':
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
case 'APPROVED':
|
||||
update = merge(update, {
|
||||
$set: {
|
||||
status: {
|
||||
username: {
|
||||
status: 'APPROVED',
|
||||
history: [
|
||||
{
|
||||
status: 'APPROVED',
|
||||
history: [
|
||||
{
|
||||
status: 'APPROVED',
|
||||
created_at,
|
||||
},
|
||||
],
|
||||
created_at,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`${status} is an invalid status`);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`${status} is an invalid status`);
|
||||
}
|
||||
|
||||
updates.push({ query: { id }, update });
|
||||
}
|
||||
return { query: { id }, update };
|
||||
};
|
||||
|
||||
if (updates.length > 0) {
|
||||
// Create a new batch operation.
|
||||
let bulk = UserModel.collection.initializeUnorderedBulkOp();
|
||||
module.exports = {
|
||||
async up({ transformSingleWithCursor }) {
|
||||
// Get the first batch of users.
|
||||
const cursor = UserModel.collection.find({
|
||||
status: {
|
||||
$in: ['ACTIVE', 'BANNED', 'PENDING', 'APPROVED'],
|
||||
},
|
||||
});
|
||||
|
||||
for (const { query, update } of updates) {
|
||||
bulk.find(query).updateOne(update);
|
||||
}
|
||||
|
||||
// Execute the bulk update operation.
|
||||
await bulk.execute();
|
||||
}
|
||||
await transformSingleWithCursor(cursor, transformUser, UserModel);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,18 +13,16 @@ const findNewRole = roles => {
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
async up() {
|
||||
const cursor = await UserModel.collection.find({
|
||||
async up({ transformSingleWithCursor }) {
|
||||
const cursor = UserModel.collection.find({
|
||||
roles: {
|
||||
$exists: true,
|
||||
},
|
||||
});
|
||||
|
||||
const updates = [];
|
||||
while (await cursor.hasNext()) {
|
||||
const user = await cursor.next();
|
||||
|
||||
updates.push({
|
||||
await transformSingleWithCursor(
|
||||
cursor,
|
||||
user => ({
|
||||
query: {
|
||||
id: user.id,
|
||||
},
|
||||
@@ -38,19 +36,8 @@ module.exports = {
|
||||
roles: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (updates.length > 0) {
|
||||
// Create a new batch operation.
|
||||
const bulk = UserModel.collection.initializeUnorderedBulkOp();
|
||||
|
||||
for (const { query, update } of updates) {
|
||||
bulk.find(query).updateOne(update);
|
||||
}
|
||||
|
||||
// Execute the bulk update operation.
|
||||
await bulk.execute();
|
||||
}
|
||||
}),
|
||||
UserModel
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
const ActionModel = require('../models/action');
|
||||
const UserModel = require('../models/user');
|
||||
const CommentModel = require('../models/comment');
|
||||
|
||||
module.exports = {
|
||||
async up({ transformSingleWithCursor }) {
|
||||
const models = [
|
||||
{ Model: CommentModel, item_type: 'COMMENTS' },
|
||||
{ Model: UserModel, item_type: 'USERS' },
|
||||
];
|
||||
for (const { Model, item_type } of models) {
|
||||
let cursor = ActionModel.collection.aggregate(
|
||||
[
|
||||
{
|
||||
$match: {
|
||||
group_id: { $ne: null },
|
||||
item_type,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
// group unique documents by these properties, we are leveraging the
|
||||
// fact that each uuid is completely unique.
|
||||
_id: {
|
||||
item_id: '$item_id',
|
||||
action_type: '$action_type',
|
||||
group_id: '$group_id',
|
||||
},
|
||||
|
||||
// and sum up all actions matching the above grouping criteria
|
||||
count: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
// suppress the _id field
|
||||
_id: false,
|
||||
|
||||
// map the fields from the _id grouping down a level
|
||||
item_id: '$_id.item_id',
|
||||
action_type: { $toLower: '$_id.action_type' },
|
||||
group_id: { $toLower: '$_id.group_id' },
|
||||
|
||||
// map the field directly
|
||||
count: '$count',
|
||||
},
|
||||
},
|
||||
],
|
||||
{ allowDiskUse: true }
|
||||
);
|
||||
|
||||
// Transform those documents.
|
||||
await transformSingleWithCursor(
|
||||
cursor,
|
||||
({ item_id, action_type, group_id, count }) => ({
|
||||
query: { id: item_id },
|
||||
update: {
|
||||
$set: {
|
||||
[`action_counts.${action_type}_${group_id}`]: count,
|
||||
},
|
||||
},
|
||||
}),
|
||||
Model
|
||||
);
|
||||
|
||||
// Secondly, we'll collect the group group id's (all the actions for a
|
||||
// specific action type) to update counts of.
|
||||
cursor = ActionModel.collection.aggregate(
|
||||
[
|
||||
{
|
||||
$match: {
|
||||
item_type,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
// group unique documents by these properties, we are leveraging the
|
||||
// fact that each uuid is completely unique.
|
||||
_id: {
|
||||
item_id: '$item_id',
|
||||
action_type: '$action_type',
|
||||
},
|
||||
|
||||
// and sum up all actions matching the above grouping criteria
|
||||
count: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
// suppress the _id field
|
||||
_id: false,
|
||||
|
||||
// map the fields from the _id grouping down a level
|
||||
item_id: '$_id.item_id',
|
||||
action_type: { $toLower: '$_id.action_type' },
|
||||
|
||||
// map the field directly
|
||||
count: '$count',
|
||||
},
|
||||
},
|
||||
],
|
||||
{ allowDiskUse: true }
|
||||
);
|
||||
|
||||
// Transform those documents.
|
||||
await transformSingleWithCursor(
|
||||
cursor,
|
||||
({ item_id, action_type, count }) => ({
|
||||
query: { id: item_id },
|
||||
update: {
|
||||
$set: {
|
||||
[`action_counts.${action_type}`]: count,
|
||||
},
|
||||
},
|
||||
}),
|
||||
Model
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
const CommentModel = require('../models/comment');
|
||||
|
||||
const transformComments = ({ _id: parent_id, reply_count }) => ({
|
||||
query: { id: parent_id, reply_count: { $ne: reply_count } },
|
||||
update: { $set: { reply_count } },
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
async up({ transformSingleWithCursor }) {
|
||||
const cursor = CommentModel.collection.aggregate(
|
||||
[
|
||||
{
|
||||
$match: {
|
||||
parent_id: { $ne: null },
|
||||
status: { $in: ['NONE', 'ACCEPTED'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$parent_id',
|
||||
reply_count: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
{ allowDiskUse: true }
|
||||
);
|
||||
|
||||
// Transform those documents.
|
||||
await transformSingleWithCursor(cursor, transformComments, CommentModel);
|
||||
},
|
||||
};
|
||||
+8
-4
@@ -2,6 +2,7 @@ const mongoose = require('../services/mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
const uuid = require('uuid');
|
||||
const TagLinkSchema = require('./schema/tag_link');
|
||||
const get = require('lodash/get');
|
||||
|
||||
const AssetSchema = new Schema(
|
||||
{
|
||||
@@ -45,8 +46,8 @@ const AssetSchema = new Schema(
|
||||
// the base settings from the base Settings object. This is to be accessed
|
||||
// always after running `rectifySettings` against it.
|
||||
settings: {
|
||||
type: Schema.Types.Mixed,
|
||||
default: {},
|
||||
type: Object,
|
||||
},
|
||||
|
||||
// Tags are added by the self or by administrators.
|
||||
@@ -85,9 +86,12 @@ AssetSchema.index(
|
||||
* Returns true if the asset is closed, false else.
|
||||
*/
|
||||
AssetSchema.virtual('isClosed').get(function() {
|
||||
return Boolean(
|
||||
this.closedAt && this.closedAt.getTime() <= new Date().getTime()
|
||||
);
|
||||
const closedAt = get(this, 'closedAt', null);
|
||||
if (closedAt === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return closedAt.getTime() <= new Date().getTime();
|
||||
});
|
||||
|
||||
const Asset = mongoose.model('Asset', AssetSchema);
|
||||
|
||||
+9
-21
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"talk": {
|
||||
"migration": {
|
||||
"minVersion": 1511801783
|
||||
"minVersion": 1516920160
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
@@ -211,7 +211,7 @@
|
||||
"mocha-junit-reporter": "^1.12.1",
|
||||
"nightwatch": "^0.9.16",
|
||||
"nodemon": "^1.11.0",
|
||||
"pre-git": "^3.16.0",
|
||||
"pre-commit": "^1.2.2",
|
||||
"selenium-standalone": "^6.11.0",
|
||||
"sinon": "^3.2.1",
|
||||
"sinon-chai": "^2.13.0",
|
||||
@@ -220,24 +220,12 @@
|
||||
"engines": {
|
||||
"node": "^8"
|
||||
},
|
||||
"config": {
|
||||
"pre-git": {
|
||||
"pre-commit": [
|
||||
"yarn lint",
|
||||
"yarn test:client",
|
||||
"yarn test:server"
|
||||
],
|
||||
"pre-push": [
|
||||
"yarn lint",
|
||||
"yarn test:client",
|
||||
"yarn test:server"
|
||||
],
|
||||
"post-commit": [],
|
||||
"post-checkout": [],
|
||||
"post-merge": []
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"analyzeCommits": "simple-commit-message"
|
||||
"pre-commit": {
|
||||
"silent": false,
|
||||
"run": [
|
||||
"lint",
|
||||
"test:client",
|
||||
"test:server"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,7 @@ import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import withMutation from 'coral-framework/hocs/withMutation';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { capitalize } from 'coral-framework/helpers/strings';
|
||||
import {
|
||||
getMyActionSummary,
|
||||
getTotalActionCount,
|
||||
getErrorMessages,
|
||||
} from 'coral-framework/utils';
|
||||
import { getMyActionSummary, getTotalActionCount } from 'coral-framework/utils';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { getDefinitionName } from '../utils';
|
||||
@@ -282,7 +278,6 @@ export default (reaction, options = {}) =>
|
||||
})
|
||||
.catch(err => {
|
||||
this.duringMutation = false;
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
@@ -307,7 +302,6 @@ export default (reaction, options = {}) =>
|
||||
})
|
||||
.catch(err => {
|
||||
this.duringMutation = false;
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { getDisplayName } from 'coral-framework/helpers/hoc';
|
||||
import { capitalize } from 'coral-framework/helpers/strings';
|
||||
import { withAddTag, withRemoveTag } from 'coral-framework/graphql/mutations';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { getErrorMessages, isTagged } from 'coral-framework/utils';
|
||||
import { isTagged } from 'coral-framework/utils';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
import { getDefinitionName } from '../utils';
|
||||
|
||||
@@ -38,7 +36,7 @@ export default (tag, options = {}) =>
|
||||
loading = false;
|
||||
|
||||
postTag = () => {
|
||||
const { comment, asset, notify } = this.props;
|
||||
const { comment, asset } = this.props;
|
||||
|
||||
if (this.loading) {
|
||||
return;
|
||||
@@ -59,13 +57,12 @@ export default (tag, options = {}) =>
|
||||
})
|
||||
.catch(err => {
|
||||
this.loading = false;
|
||||
notify('error', getErrorMessages(err));
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
deleteTag = () => {
|
||||
const { comment, asset, notify } = this.props;
|
||||
const { comment, asset } = this.props;
|
||||
|
||||
if (this.loading) {
|
||||
return;
|
||||
@@ -84,7 +81,6 @@ export default (tag, options = {}) =>
|
||||
})
|
||||
.catch(err => {
|
||||
this.loading = false;
|
||||
notify('error', getErrorMessages(err));
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
@@ -114,9 +110,6 @@ export default (tag, options = {}) =>
|
||||
user: state.auth.user,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ notify }, dispatch);
|
||||
|
||||
const enhance = compose(
|
||||
withFragments({
|
||||
...fragments,
|
||||
@@ -146,7 +139,7 @@ export default (tag, options = {}) =>
|
||||
}),
|
||||
withAddTag,
|
||||
withRemoveTag,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
connect(mapStateToProps, null)
|
||||
);
|
||||
|
||||
WithTags.displayName = `WithTags(${getDisplayName(WrappedComponent)})`;
|
||||
|
||||
@@ -64,7 +64,6 @@ export default class ModTag extends React.Component {
|
||||
ModTag.propTypes = {
|
||||
alreadyTagged: PropTypes.bool,
|
||||
deleteTag: PropTypes.func,
|
||||
notify: PropTypes.func,
|
||||
openFeaturedDialog: PropTypes.func,
|
||||
comment: PropTypes.object,
|
||||
asset: PropTypes.object,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import Comment from '../containers/Comment';
|
||||
import LoadMore from './LoadMore';
|
||||
import { getErrorMessages } from 'plugin-api/beta/client/utils';
|
||||
|
||||
class TabPane extends React.Component {
|
||||
state = {
|
||||
@@ -15,9 +14,8 @@ class TabPane extends React.Component {
|
||||
.then(() => {
|
||||
this.setState({ loadingState: 'success' });
|
||||
})
|
||||
.catch(error => {
|
||||
.catch(() => {
|
||||
this.setState({ loadingState: 'error' });
|
||||
this.props.notify('error', getErrorMessages(error));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ const mapDispatchToProps = dispatch =>
|
||||
);
|
||||
|
||||
const enhance = compose(
|
||||
withTags('featured'),
|
||||
connect(null, mapDispatchToProps)
|
||||
connect(null, mapDispatchToProps),
|
||||
withTags('featured')
|
||||
);
|
||||
|
||||
export default enhance(ModActionButton);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { gql } from 'react-apollo';
|
||||
import { subscriptionFields } from 'coral-admin/src/routes/Moderation/graphql';
|
||||
|
||||
class ModIndicatorSubscription extends React.Component {
|
||||
subscriptions = null;
|
||||
|
||||
componentWillMount() {
|
||||
const configs = [
|
||||
{
|
||||
document: COMMENT_FEATURED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentFeatured: { comment } } } }
|
||||
) => {
|
||||
return this.props.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_UNFEATURED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentUnfeatured: { comment } } } }
|
||||
) => {
|
||||
return this.props.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
];
|
||||
this.subscriptions = configs.map(config =>
|
||||
this.props.data.subscribeToMore(config)
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscriptions.forEach(unsubscribe => unsubscribe());
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const COMMENT_FEATURED_SUBSCRIPTION = gql`
|
||||
subscription TalkFeaturedComments_Indicator_CommentFeatured {
|
||||
commentFeatured {
|
||||
comment {
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const COMMENT_UNFEATURED_SUBSCRIPTION = gql`
|
||||
subscription TalkFeaturedComments_Indicator_CommentUnfeatured {
|
||||
commentUnfeatured {
|
||||
comment {
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default ModIndicatorSubscription;
|
||||
@@ -5,6 +5,7 @@ import Comment from 'coral-admin/src/routes/Moderation/containers/Comment';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
import truncate from 'lodash/truncate';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { subscriptionFields } from 'coral-admin/src/routes/Moderation/graphql';
|
||||
|
||||
function prepareNotificationText(text) {
|
||||
return truncate(text, { length: 50 }).replace('\n', ' ');
|
||||
@@ -79,14 +80,7 @@ const COMMENT_FEATURED_SUBSCRIPTION = gql`
|
||||
commentFeatured(asset_id: $assetId) {
|
||||
comment {
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
status_history {
|
||||
type
|
||||
created_at
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
${subscriptionFields}
|
||||
}
|
||||
user {
|
||||
id
|
||||
@@ -102,6 +96,7 @@ const COMMENT_UNFEATURED_SUBSCRIPTION = gql`
|
||||
commentUnfeatured(asset_id: $assetId){
|
||||
comment {
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
${subscriptionFields}
|
||||
}
|
||||
user {
|
||||
id
|
||||
|
||||
@@ -3,12 +3,10 @@ import { withTags, connect } from 'plugin-api/beta/client/hocs';
|
||||
import { gql, compose } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { openFeaturedDialog } from '../actions';
|
||||
import { notify } from 'plugin-api/beta/client/actions/notification';
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
notify,
|
||||
openFeaturedDialog,
|
||||
},
|
||||
dispatch
|
||||
@@ -24,8 +22,8 @@ const fragments = {
|
||||
`,
|
||||
};
|
||||
const enhance = compose(
|
||||
withTags('featured', { fragments }),
|
||||
connect(null, mapDispatchToProps)
|
||||
connect(null, mapDispatchToProps),
|
||||
withTags('featured', { fragments })
|
||||
);
|
||||
|
||||
export default enhance(ModTag);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { compose, gql } from 'react-apollo';
|
||||
import TabPane from '../components/TabPane';
|
||||
import { withFragments, connect } from 'plugin-api/beta/client/hocs';
|
||||
import Comment from '../containers/Comment';
|
||||
import { notify } from 'plugin-api/beta/client/actions/notification';
|
||||
import { viewComment } from 'coral-embed-stream/src/actions/stream';
|
||||
import {
|
||||
appendNewNodes,
|
||||
@@ -81,7 +80,6 @@ const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
viewComment,
|
||||
notify,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import update from 'immutability-helper';
|
||||
import ModTag from './containers/ModTag';
|
||||
import ModActionButton from './containers/ModActionButton';
|
||||
import ModSubscription from './containers/ModSubscription';
|
||||
import ModIndicatorSubscription from './containers/ModIndicatorSubscription';
|
||||
import FeaturedDialog from './containers/FeaturedDialog';
|
||||
import { gql } from 'react-apollo';
|
||||
import reducer from './reducer';
|
||||
@@ -23,6 +24,7 @@ export default {
|
||||
moderationActions: [ModActionButton],
|
||||
adminModeration: [ModSubscription, FeaturedDialog],
|
||||
adminCommentInfoBar: [ModTag],
|
||||
adminModerationIndicator: [ModIndicatorSubscription],
|
||||
},
|
||||
mutations: {
|
||||
IgnoreUser: ({ variables }) => ({
|
||||
|
||||
@@ -36,7 +36,7 @@ module.exports = {
|
||||
commentFeatured: (options, args) => ({
|
||||
commentFeatured: {
|
||||
filter: ({ comment }, { user }) => {
|
||||
if (args.asset_id === null) {
|
||||
if (!args.asset_id) {
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
}
|
||||
return comment.asset_id === args.asset_id;
|
||||
@@ -46,7 +46,7 @@ module.exports = {
|
||||
commentUnfeatured: (options, args) => ({
|
||||
commentUnfeatured: {
|
||||
filter: ({ comment }, { user }) => {
|
||||
if (args.asset_id === null) {
|
||||
if (!args.asset_id) {
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
}
|
||||
return comment.asset_id === args.asset_id;
|
||||
|
||||
@@ -10,21 +10,16 @@ import { bindActionCreators } from 'redux';
|
||||
import { closeMenu } from 'plugins/talk-plugin-author-menu/client/actions';
|
||||
import { notify } from 'plugin-api/beta/client/actions/notification';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import { getErrorMessages } from 'plugin-api/beta/client/utils';
|
||||
|
||||
class IgnoreUserConfirmationContainer extends React.Component {
|
||||
ignoreUser = () => {
|
||||
const { ignoreUser, notify, comment, closeMenu } = this.props;
|
||||
ignoreUser(comment.user.id)
|
||||
.then(() => {
|
||||
notify(
|
||||
'success',
|
||||
t('talk-plugin-ignore-user.notify_success', comment.user.username)
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
notify('error', getErrorMessages(err));
|
||||
});
|
||||
ignoreUser(comment.user.id).then(() => {
|
||||
notify(
|
||||
'success',
|
||||
t('talk-plugin-ignore-user.notify_success', comment.user.username)
|
||||
);
|
||||
});
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
import React from 'react';
|
||||
import { compose } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { getErrorMessages } from 'plugin-api/beta/client/utils';
|
||||
import { notify } from 'plugin-api/beta/client/actions/notification';
|
||||
import ApproveCommentAction from '../components/ApproveCommentAction';
|
||||
import { connect, withSetCommentStatus } from 'plugin-api/beta/client/hocs';
|
||||
import { withSetCommentStatus } from 'plugin-api/beta/client/hocs';
|
||||
|
||||
class ApproveCommentActionContainer extends React.Component {
|
||||
approveComment = async () => {
|
||||
const { setCommentStatus, comment, hideMenu, notify } = this.props;
|
||||
const { setCommentStatus, comment, hideMenu } = this.props;
|
||||
|
||||
try {
|
||||
await setCommentStatus({
|
||||
commentId: comment.id,
|
||||
status: 'ACCEPTED',
|
||||
});
|
||||
} catch (err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
}
|
||||
await setCommentStatus({
|
||||
commentId: comment.id,
|
||||
status: 'ACCEPTED',
|
||||
});
|
||||
|
||||
hideMenu();
|
||||
};
|
||||
@@ -32,17 +25,6 @@ class ApproveCommentActionContainer extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
notify,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
const enhance = compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withSetCommentStatus
|
||||
);
|
||||
const enhance = compose(withSetCommentStatus);
|
||||
|
||||
export default enhance(ApproveCommentActionContainer);
|
||||
|
||||
@@ -3,19 +3,16 @@ import PropTypes from 'prop-types';
|
||||
import { compose } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { closeBanDialog, closeMenu } from '../actions';
|
||||
import { notify } from 'plugin-api/beta/client/actions/notification';
|
||||
import {
|
||||
connect,
|
||||
withSetCommentStatus,
|
||||
withBanUser,
|
||||
} from 'plugin-api/beta/client/hocs';
|
||||
import { getErrorMessages } from 'plugin-api/beta/client/utils';
|
||||
import BanUserDialog from '../components/BanUserDialog';
|
||||
|
||||
class BanUserDialogContainer extends React.Component {
|
||||
banUser = async () => {
|
||||
const {
|
||||
notify,
|
||||
authorId,
|
||||
commentId,
|
||||
commentStatus,
|
||||
@@ -25,23 +22,19 @@ class BanUserDialogContainer extends React.Component {
|
||||
banUser,
|
||||
} = this.props;
|
||||
|
||||
try {
|
||||
await banUser({
|
||||
id: authorId,
|
||||
message: '',
|
||||
await banUser({
|
||||
id: authorId,
|
||||
message: '',
|
||||
});
|
||||
|
||||
closeMenu();
|
||||
closeBanDialog();
|
||||
|
||||
if (commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({
|
||||
commentId: commentId,
|
||||
status: 'REJECTED',
|
||||
});
|
||||
|
||||
closeMenu();
|
||||
closeBanDialog();
|
||||
|
||||
if (commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({
|
||||
commentId: commentId,
|
||||
status: 'REJECTED',
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,7 +63,6 @@ const mapStateToProps = ({ talkPluginModerationActions: state }) => ({
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
notify,
|
||||
closeBanDialog,
|
||||
closeMenu,
|
||||
},
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
import React from 'react';
|
||||
import { compose } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { getErrorMessages } from 'plugin-api/beta/client/utils';
|
||||
import { notify } from 'plugin-api/beta/client/actions/notification';
|
||||
import RejectCommentAction from '../components/RejectCommentAction';
|
||||
import { connect, withSetCommentStatus } from 'plugin-api/beta/client/hocs';
|
||||
import { withSetCommentStatus } from 'plugin-api/beta/client/hocs';
|
||||
|
||||
class RejectCommentActionContainer extends React.Component {
|
||||
rejectComment = async () => {
|
||||
const { setCommentStatus, comment, hideMenu, notify } = this.props;
|
||||
const { setCommentStatus, comment, hideMenu } = this.props;
|
||||
|
||||
try {
|
||||
await setCommentStatus({
|
||||
commentId: comment.id,
|
||||
status: 'REJECTED',
|
||||
});
|
||||
} catch (err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
}
|
||||
await setCommentStatus({
|
||||
commentId: comment.id,
|
||||
status: 'REJECTED',
|
||||
});
|
||||
|
||||
hideMenu();
|
||||
};
|
||||
@@ -27,17 +20,6 @@ class RejectCommentActionContainer extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
notify,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
const enhance = compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withSetCommentStatus
|
||||
);
|
||||
const enhance = compose(withSetCommentStatus);
|
||||
|
||||
export default enhance(RejectCommentActionContainer);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console').text(err.message).addClass('active');
|
||||
} catch (err) {
|
||||
$('.error-console').text(error).addClass('active');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function(event) {
|
||||
localStorage.setItem('auth', document.getElementById('auth').innerText);
|
||||
setTimeout(function() { window.close(); }, 50);
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
const debug = require('debug')('talk:services:migration');
|
||||
|
||||
/**
|
||||
* processUpdates processes batches of updates on the given model.
|
||||
*
|
||||
* @param {Object} model mongoose model that should perform the operations on
|
||||
* @param {Array<Object>} updates array of updates to execute
|
||||
*/
|
||||
const processUpdates = async (model, updates) => {
|
||||
// Create a new batch operation.
|
||||
const bulk = model.collection.initializeUnorderedBulkOp();
|
||||
|
||||
for (const { query, update } of updates) {
|
||||
bulk.find(query).updateOne(update);
|
||||
}
|
||||
|
||||
// Execute the bulk update operation.
|
||||
await bulk.execute();
|
||||
};
|
||||
|
||||
const debugProcessStatistics = (count, totalCount) => {
|
||||
if (totalCount > 0) {
|
||||
debug(
|
||||
`processed ${(count / totalCount * 100).toFixed(
|
||||
2
|
||||
)}% (${count}/${totalCount}) updates`
|
||||
);
|
||||
} else {
|
||||
debug(`processed ${count} updates`);
|
||||
}
|
||||
};
|
||||
|
||||
const transformSingleWithCursor = ({
|
||||
queryBatchSize,
|
||||
updateBatchSize,
|
||||
}) => async (query, process, Model) => {
|
||||
debug('starting transform');
|
||||
|
||||
// We'll manage the updates that we store inside this object.
|
||||
let updates = [];
|
||||
|
||||
// Count the elements in the transformation.
|
||||
let totalCount = 0;
|
||||
try {
|
||||
totalCount = await query.count();
|
||||
} catch (err) {}
|
||||
|
||||
// First we'll collect all the individual actions with specific group id's.
|
||||
const cursor = await query.batchSize(queryBatchSize);
|
||||
|
||||
let count = 0;
|
||||
while (await cursor.hasNext()) {
|
||||
const result = await cursor.next();
|
||||
|
||||
const transformed = await process(result);
|
||||
if (transformed) {
|
||||
updates.push(transformed);
|
||||
}
|
||||
|
||||
if (updates.length > updateBatchSize) {
|
||||
// Process the updates.
|
||||
await processUpdates(Model, updates);
|
||||
count += updates.length;
|
||||
debugProcessStatistics(count, totalCount);
|
||||
|
||||
// Clear the updates array.
|
||||
updates = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (updates.length > 0) {
|
||||
// Process the updates.
|
||||
await processUpdates(Model, updates);
|
||||
count += updates.length;
|
||||
debugProcessStatistics(count, totalCount);
|
||||
|
||||
// Clear the updates array.
|
||||
updates = [];
|
||||
}
|
||||
|
||||
debug('finished transform');
|
||||
};
|
||||
|
||||
/**
|
||||
* processManyUpdates processes batches of updates on many models with the given
|
||||
* model.
|
||||
*
|
||||
* @param {Object} model mongoose model that should perform the operations on
|
||||
* @param {Array<Object>} updates array of updates to execute
|
||||
*/
|
||||
const processManyUpdates = async (model, updates) => {
|
||||
// Create a new batch operation.
|
||||
const bulk = model.collection.initializeUnorderedBulkOp();
|
||||
|
||||
for (const { query, update } of updates) {
|
||||
bulk.find(query).update(update);
|
||||
}
|
||||
|
||||
// Execute the bulk update operation.
|
||||
await bulk.execute();
|
||||
};
|
||||
|
||||
module.exports = ctx => ({
|
||||
processManyUpdates,
|
||||
processUpdates,
|
||||
transformSingleWithCursor: transformSingleWithCursor(ctx),
|
||||
});
|
||||
@@ -1,17 +1,20 @@
|
||||
const MigrationModel = require('../models/migration');
|
||||
const MigrationModel = require('../../models/migration');
|
||||
const fs = require('fs');
|
||||
const ms = require('ms');
|
||||
const path = require('path');
|
||||
const Joi = require('joi');
|
||||
const debug = require('debug')('talk:services:migration');
|
||||
const sc = require('snake-case');
|
||||
const { talk: { migration: { minVersion } } } = require('../package.json');
|
||||
const helpers = require('./helpers');
|
||||
const { stripIndent } = require('common-tags');
|
||||
const { talk: { migration: { minVersion } } } = require('../../package.json');
|
||||
|
||||
const migrationTemplate = `module.exports = {
|
||||
async up() {
|
||||
|
||||
}
|
||||
};
|
||||
const migrationTemplate = stripIndent`
|
||||
module.exports = {
|
||||
async up({ queryBatchSize, updateBatchSize }) {
|
||||
|
||||
}
|
||||
};
|
||||
`;
|
||||
|
||||
class MigrationService {
|
||||
@@ -44,7 +47,7 @@ class MigrationService {
|
||||
static async listPending() {
|
||||
// Get all the migration files.
|
||||
let migrationFiles = fs.readdirSync(
|
||||
path.join(__dirname, '..', 'migrations')
|
||||
path.join(__dirname, '..', '..', 'migrations')
|
||||
);
|
||||
|
||||
// Ensure that all migrations follow this format.
|
||||
@@ -61,6 +64,7 @@ class MigrationService {
|
||||
|
||||
// Parse the migrations from the file listing.
|
||||
let migrations = migrationFiles
|
||||
.filter(filename => versionRe.test(filename))
|
||||
.map(filename => {
|
||||
// Parse the version from the filename.
|
||||
let matches = filename.match(versionRe);
|
||||
@@ -75,7 +79,7 @@ class MigrationService {
|
||||
}
|
||||
|
||||
// Read the migration from the filesystem.
|
||||
let migration = require(`../migrations/${filename}`);
|
||||
let migration = require(`../../migrations/${filename}`);
|
||||
Joi.assert(
|
||||
migration,
|
||||
migrationSchema,
|
||||
@@ -109,17 +113,26 @@ class MigrationService {
|
||||
*
|
||||
* @param {Array} migrations a list of migrations returned by `listPending`
|
||||
*/
|
||||
static async run(migrations) {
|
||||
static async run(
|
||||
migrations,
|
||||
{ queryBatchSize = 10000, updateBatchSize = 20000 } = {}
|
||||
) {
|
||||
if (migrations.length === 0) {
|
||||
console.log('No migrations to run!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the context helpers.
|
||||
const ctx = helpers({ queryBatchSize, updateBatchSize });
|
||||
|
||||
for (let { filename, version, migration } of migrations) {
|
||||
try {
|
||||
const startTime = new Date();
|
||||
console.log(`Starting migration ${filename}`);
|
||||
await migration.up();
|
||||
console.log(`Finished migration ${filename}`);
|
||||
await migration.up(ctx);
|
||||
const endTime = new Date();
|
||||
const totalTime = endTime.getTime() - startTime.getTime();
|
||||
console.log(`Finished migration ${filename} in ${ms(totalTime)}`);
|
||||
} catch (e) {
|
||||
console.error(`Migration ${filename} failed`);
|
||||
throw e;
|
||||
@@ -0,0 +1,130 @@
|
||||
const errors = require('../../errors');
|
||||
const get = require('lodash/get');
|
||||
|
||||
// Load in the phases to use.
|
||||
const {
|
||||
wordlist,
|
||||
commentLength,
|
||||
assetClosed,
|
||||
karma,
|
||||
staff,
|
||||
links,
|
||||
premod,
|
||||
} = require('./phases');
|
||||
|
||||
// This phase checks to see if the comment was already prescribed a status. This
|
||||
// essentially provides a hook for plugins to inject their own comments.
|
||||
const applyPreexisting = (ctx, comment) => {
|
||||
const status = get(comment, 'status');
|
||||
|
||||
// If the status was already defined, don't redefine it. It's only defined
|
||||
// when specific external conditions exist, we don't want to override that.
|
||||
if (status) {
|
||||
return {
|
||||
status,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Applies the defaulted status.
|
||||
const applyStatus = status => () => ({ status });
|
||||
|
||||
/**
|
||||
* phases is an array of moderation phases carried out in order until a status is
|
||||
* returned.
|
||||
*/
|
||||
const phases = [
|
||||
commentLength,
|
||||
assetClosed,
|
||||
wordlist,
|
||||
staff,
|
||||
links,
|
||||
karma,
|
||||
applyPreexisting,
|
||||
premod,
|
||||
applyStatus('NONE'),
|
||||
];
|
||||
|
||||
/**
|
||||
* compose will create a moderation pipeline for which is executable with the
|
||||
* passed actions.
|
||||
*
|
||||
* @param {Array} phases the set of moderation phases to pass the comment and
|
||||
* their options through.
|
||||
*/
|
||||
const compose = phases => async (ctx, comment, options) => {
|
||||
const actions = get(comment, 'actions', []);
|
||||
|
||||
// Loop over all the moderation phases and see if we've resolved the status.
|
||||
for (const phase of phases) {
|
||||
const result = await phase(ctx, comment, options);
|
||||
if (result) {
|
||||
if (result.actions) {
|
||||
actions.push(...result.actions);
|
||||
}
|
||||
|
||||
// If this result contained a status, then we've finished resolving
|
||||
// phases!
|
||||
if (result.status) {
|
||||
return { status: result.status, actions };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* fetchOptions will generate the options used by the moderation service to
|
||||
* determine the end status.
|
||||
*
|
||||
* @param {Object} ctx graph context
|
||||
* @param {Object} comment comment object to use
|
||||
*/
|
||||
const fetchOptions = async (ctx, comment) => {
|
||||
const {
|
||||
connectors: { services: { Assets: AssetsService } },
|
||||
loaders: { Settings, Assets },
|
||||
} = ctx;
|
||||
|
||||
// Load the settings.
|
||||
const settings = await Settings.load();
|
||||
|
||||
// Pull the asset id out of the comment.
|
||||
const assetID = get(comment, 'asset_id', null);
|
||||
if (assetID === null) {
|
||||
// And leave now if this asset wasn't found.
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
// Load the asset.
|
||||
const asset = await Assets.getByID.load(assetID);
|
||||
if (!asset) {
|
||||
// And leave now if this asset wasn't found.
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
// Combine the asset and the settings to get the asset settings.
|
||||
asset.settings = await AssetsService.rectifySettings(asset, settings);
|
||||
|
||||
// Create the options that will be consumed by the phases.
|
||||
return {
|
||||
asset,
|
||||
settings,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* process the comment and return moderation details.
|
||||
*
|
||||
* @param {Object} ctx graphql context
|
||||
* @param {Object} comment comment to perform the moderation phases on
|
||||
*/
|
||||
const process = async (ctx, comment) => {
|
||||
// Fetch the options to use for the moderation phases.
|
||||
const options = await fetchOptions(ctx, comment);
|
||||
|
||||
// Compose a moderation pipeline from the moderation phases and execute it on
|
||||
// the comment.
|
||||
return compose(phases)(ctx, comment, options);
|
||||
};
|
||||
|
||||
module.exports.process = process;
|
||||
@@ -0,0 +1,9 @@
|
||||
const { ErrAssetCommentingClosed } = require('../../../errors');
|
||||
|
||||
// This phase checks to see if the asset being processed is closed or not.
|
||||
module.exports = (ctx, comment, { asset }) => {
|
||||
// Check to see if the asset has closed commenting...
|
||||
if (asset.isClosed) {
|
||||
throw new ErrAssetCommentingClosed(asset.closedMessage);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
const { ErrCommentTooShort } = require('../../../errors');
|
||||
|
||||
// This phase checks to see if the comment is long enough.
|
||||
module.exports = (
|
||||
ctx,
|
||||
comment,
|
||||
{ asset: { settings: { charCountEnable, charCount } } }
|
||||
) => {
|
||||
// Check to see if the body is too short, if it is, then complain about it!
|
||||
if (comment.body.length < 2) {
|
||||
throw ErrCommentTooShort;
|
||||
}
|
||||
|
||||
// Reject if the comment is too long
|
||||
if (charCountEnable && comment.body.length > charCount) {
|
||||
// Add the flag related to Trust to the comment.
|
||||
return {
|
||||
status: 'REJECTED',
|
||||
actions: [
|
||||
{
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'BODY_COUNT',
|
||||
metadata: {
|
||||
count: comment.body.length,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports.wordlist = require('./wordlist');
|
||||
module.exports.commentLength = require('./commentLength');
|
||||
module.exports.assetClosed = require('./assetClosed');
|
||||
module.exports.karma = require('./karma');
|
||||
module.exports.staff = require('./staff');
|
||||
module.exports.links = require('./links');
|
||||
module.exports.premod = require('./premod');
|
||||
@@ -0,0 +1,33 @@
|
||||
const get = require('lodash/get');
|
||||
|
||||
// This phase checks to see if the user making the comment is allowed to do so
|
||||
// considering their reliability (Trust) status.
|
||||
module.exports = ctx => {
|
||||
const { connectors: { services: { Karma } } } = ctx;
|
||||
const trust = get(ctx, 'user.metadata.trust', null);
|
||||
|
||||
if (trust !== null) {
|
||||
// If the user is not a reliable commenter (passed the unreliability
|
||||
// threshold by having too many rejected comments) then we can change the
|
||||
// status of the comment to `SYSTEM_WITHHELD`, therefore pushing the user's
|
||||
// comments away from the public eye until a moderator can manage them. This of
|
||||
// course can only be applied if the comment's current status is `NONE`,
|
||||
// we don't want to interfere if the comment was rejected.
|
||||
if (Karma.isReliable('comment', trust) === false) {
|
||||
// Add the flag related to Trust to the comment.
|
||||
return {
|
||||
status: 'SYSTEM_WITHHELD',
|
||||
actions: [
|
||||
{
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'TRUST',
|
||||
metadata: {
|
||||
trust,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
const linkify = require('linkify-it')().tlds(require('tlds'));
|
||||
|
||||
// This phase checks the comment if it has any links in it if the check is
|
||||
// enabled.
|
||||
module.exports = (
|
||||
ctx,
|
||||
comment,
|
||||
{ asset: { settings: { premodLinksEnable } } }
|
||||
) => {
|
||||
if (premodLinksEnable && linkify.test(comment.body)) {
|
||||
// Add the flag related to Trust to the comment.
|
||||
return {
|
||||
status: 'SYSTEM_WITHHELD',
|
||||
actions: [
|
||||
{
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'LINKS',
|
||||
metadata: {
|
||||
links: comment.body,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
// This phase checks to see if the settings have premod enabled, if they do,
|
||||
// the comment is premod, otherwise, it's just none.
|
||||
module.exports = (ctx, comment, { asset: { settings: { moderation } } }) => {
|
||||
// If the settings say that we're in premod mode, then the comment is in
|
||||
// premod status.
|
||||
if (moderation === 'PRE') {
|
||||
return {
|
||||
status: 'PREMOD',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'NONE',
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
const { IGNORE_FLAGS_AGAINST_STAFF } = require('../../../config');
|
||||
|
||||
// If a given user is a staff member, always approve their comment.
|
||||
module.exports = ctx => {
|
||||
if (IGNORE_FLAGS_AGAINST_STAFF && ctx.user && ctx.user.isStaff()) {
|
||||
return {
|
||||
status: 'ACCEPTED',
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
const { DISABLE_AUTOFLAG_SUSPECT_WORDS } = require('../../../config');
|
||||
|
||||
// This phase checks the comment against the wordlist.
|
||||
module.exports = async (ctx, comment, { settings }) => {
|
||||
const { connectors: { services: { Wordlist } } } = ctx;
|
||||
|
||||
// Create a new instance of the Wordlist.
|
||||
const wl = new Wordlist();
|
||||
|
||||
// Load the wordlist.
|
||||
wl.upsert(settings.wordlist);
|
||||
|
||||
// Scan the comment body for wordlist violations.
|
||||
const { banned = null, suspect = null } = wl.scan('body', comment.body);
|
||||
|
||||
// Decide the status based on whether or not the current asset/settings
|
||||
// has pre-mod enabled or not. If the comment was rejected based on the
|
||||
// wordlist, then reject it, otherwise if the moderation setting is
|
||||
// premod, set it to `premod`.
|
||||
if (banned) {
|
||||
// Add the flag related to Trust to the comment.
|
||||
return {
|
||||
status: 'REJECTED',
|
||||
actions: [
|
||||
{
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'BANNED_WORD',
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// If the comment has a suspect word or a link, we need to add a
|
||||
// flag to it to indicate that it needs to be looked at.
|
||||
// Otherwise just return the new comment.
|
||||
|
||||
// If the wordlist has matched the suspect word filter and we haven't disabled
|
||||
// auto-flagging suspect words, then we should flag the comment!
|
||||
if (suspect && !DISABLE_AUTOFLAG_SUSPECT_WORDS) {
|
||||
// TODO: this is kind of fragile, we should refactor this to resolve
|
||||
// all these const's that we're using like 'COMMENTS', 'FLAG' to be
|
||||
// defined in a checkable schema.
|
||||
return {
|
||||
actions: [
|
||||
{
|
||||
action_type: 'FLAG',
|
||||
user_id: null,
|
||||
group_id: 'SUSPECT_WORD',
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -49,6 +49,9 @@ describe('graph.mutations.createComment', () => {
|
||||
|
||||
return graphql(schema, query, {}, context).then(
|
||||
({ data, errors }) => {
|
||||
if (errors) {
|
||||
console.error(errors);
|
||||
}
|
||||
expect(errors).to.be.undefined;
|
||||
if (error) {
|
||||
expect(data.createComment).to.have.property('comment').null;
|
||||
@@ -98,7 +101,9 @@ describe('graph.mutations.createComment', () => {
|
||||
async () => {
|
||||
const context = new Context({ user });
|
||||
const { data, errors } = await graphql(schema, query, {}, context);
|
||||
|
||||
if (errors) {
|
||||
console.error(errors);
|
||||
}
|
||||
expect(errors).to.be.undefined;
|
||||
if (error) {
|
||||
expect(data.createComment).to.have.property('comment').null;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
const migration = require('../../../migrations/1510174676_user_status');
|
||||
const UserModel = require('../../../models/user');
|
||||
const helpers = require('../../../services/migration/helpers');
|
||||
|
||||
const chai = require('chai');
|
||||
chai.use(require('chai-datetime'));
|
||||
const { expect } = chai;
|
||||
|
||||
const performMigration = () =>
|
||||
migration.up(helpers({ queryBatchSize: 100, updateBatchSize: 100 }));
|
||||
|
||||
describe('migration.1510174676_user_status', () => {
|
||||
describe('active user', () => {
|
||||
beforeEach(async () => {
|
||||
@@ -24,7 +28,7 @@ describe('migration.1510174676_user_status', () => {
|
||||
expect(user).to.have.property('canEditName', false);
|
||||
|
||||
// Perform the migration.
|
||||
await migration.up();
|
||||
await performMigration();
|
||||
|
||||
user = await UserModel.collection.findOne({ id: '123' });
|
||||
|
||||
@@ -54,7 +58,7 @@ describe('migration.1510174676_user_status', () => {
|
||||
expect(user).to.have.property('canEditName', true);
|
||||
|
||||
// Perform the migration.
|
||||
await migration.up();
|
||||
await performMigration();
|
||||
|
||||
user = await UserModel.collection.findOne({ id: '123' });
|
||||
|
||||
@@ -85,7 +89,7 @@ describe('migration.1510174676_user_status', () => {
|
||||
expect(user.canEditName).to.equal(true);
|
||||
|
||||
// Perform the migration.
|
||||
await migration.up();
|
||||
await performMigration();
|
||||
|
||||
user = await UserModel.collection.findOne({ id: '123' });
|
||||
|
||||
@@ -117,7 +121,7 @@ describe('migration.1510174676_user_status', () => {
|
||||
expect(user.canEditName).to.equal(false);
|
||||
|
||||
// Perform the migration.
|
||||
await migration.up();
|
||||
await performMigration();
|
||||
|
||||
user = await UserModel.collection.findOne({ id: '123' });
|
||||
|
||||
@@ -153,7 +157,7 @@ describe('migration.1510174676_user_status', () => {
|
||||
const until = user.suspension.until;
|
||||
|
||||
// Perform the migration.
|
||||
await migration.up();
|
||||
await performMigration();
|
||||
|
||||
user = await UserModel.collection.findOne({ id: '123' });
|
||||
|
||||
@@ -187,7 +191,7 @@ describe('migration.1510174676_user_status', () => {
|
||||
expect(user.status).to.equal('BANNED');
|
||||
|
||||
// Perform the migration.
|
||||
await migration.up();
|
||||
await performMigration();
|
||||
|
||||
user = await UserModel.collection.findOne({ id: '123' });
|
||||
|
||||
|
||||
@@ -20,9 +20,17 @@
|
||||
</form>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script src="/public/javascripts/admin.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console').text(err.message).addClass('active');
|
||||
} catch (err) {
|
||||
$('.error-console').text(error).addClass('active');
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
@@ -30,9 +30,17 @@
|
||||
</form>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script src="/public/javascripts/admin.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console').text(err.message).addClass('active');
|
||||
} catch (err) {
|
||||
$('.error-console').text(error).addClass('active');
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit (e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
<html>
|
||||
<body>
|
||||
<script type="application/json" id="auth"><%- encodeJSONForHTML(auth) %></script>
|
||||
<script type="text/javascript" src="<%= STATIC_URL %>public/javascripts/auth-callback.js"></script>
|
||||
<script type="text/javascript" src="<%= STATIC_URL %>static/coral-auth-callback/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+26
-8
@@ -19,7 +19,11 @@ const targetPlugins = manager.section('targets').plugins;
|
||||
|
||||
debug(`Using ${pluginsPath} as the plugin configuration path`);
|
||||
|
||||
const buildTargets = ['coral-admin', 'coral-docs'];
|
||||
const buildTargets = [
|
||||
'coral-admin',
|
||||
'coral-docs',
|
||||
{ name: 'coral-auth-callback', disablePolyfill: true },
|
||||
];
|
||||
|
||||
const buildEmbeds = ['stream'];
|
||||
|
||||
@@ -156,9 +160,14 @@ const config = {
|
||||
modules: [
|
||||
path.resolve(__dirname, 'plugins'),
|
||||
path.resolve(__dirname, 'client'),
|
||||
...buildTargets.map(target =>
|
||||
path.join(__dirname, 'client', target, 'src')
|
||||
),
|
||||
...buildTargets.map(target => {
|
||||
if (typeof target !== 'string') {
|
||||
target = target.name;
|
||||
}
|
||||
|
||||
return path.join(__dirname, 'client', target, 'src');
|
||||
}),
|
||||
|
||||
...buildEmbeds.map(embed =>
|
||||
path.join(__dirname, 'client', `coral-embed-${embed}`, 'src')
|
||||
),
|
||||
@@ -276,10 +285,19 @@ module.exports = [
|
||||
// All framework targets/embeds/plugins.
|
||||
applyConfig([
|
||||
// Load in all the targets.
|
||||
...buildTargets.map(target => ({
|
||||
name: `${target}/bundle`,
|
||||
path: path.join(__dirname, 'client/', target, '/src/index'),
|
||||
})),
|
||||
...buildTargets.map(target => {
|
||||
let disablePolyfill = false;
|
||||
if (typeof target !== 'string') {
|
||||
disablePolyfill = target.disablePolyfill;
|
||||
target = target.name;
|
||||
}
|
||||
|
||||
return {
|
||||
name: `${target}/bundle`,
|
||||
path: path.join(__dirname, 'client/', target, '/src/index'),
|
||||
disablePolyfill,
|
||||
};
|
||||
}),
|
||||
|
||||
// Load in all the embeds.
|
||||
...buildEmbeds.map(embed => ({
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@coralproject/eslint-config-talk@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@coralproject/eslint-config-talk/-/eslint-config-talk-0.1.0.tgz#3ddc5f6fb4362a1cd05a5fea56cdb3095afc8cc3"
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@coralproject/eslint-config-talk/-/eslint-config-talk-0.1.1.tgz#71991b4937a3ffe657128d7f1170da4b5fb75c9e"
|
||||
dependencies:
|
||||
babel-eslint "^8.0.1"
|
||||
eslint-config-prettier "^2.9.0"
|
||||
@@ -212,14 +212,6 @@ alphanum-sort@^1.0.1, alphanum-sort@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
|
||||
|
||||
always-error@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/always-error/-/always-error-1.0.0.tgz#95c84042cfa86f38c86ca6c2cc42c0a0103441b2"
|
||||
|
||||
am-i-a-dependency@1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/am-i-a-dependency/-/am-i-a-dependency-1.1.2.tgz#f9d3422304d6f642f821e4c407565035f6167f1f"
|
||||
|
||||
amdefine@>=0.0.4:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
|
||||
@@ -230,10 +222,6 @@ ansi-align@^2.0.0:
|
||||
dependencies:
|
||||
string-width "^2.0.0"
|
||||
|
||||
ansi-escapes@^1.1.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
|
||||
|
||||
ansi-escapes@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b"
|
||||
@@ -242,10 +230,6 @@ ansi-escapes@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92"
|
||||
|
||||
ansi-regex@^1.0.0, ansi-regex@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-1.1.1.tgz#41c847194646375e6a1a5d10c3ca054ef9fc980d"
|
||||
|
||||
ansi-regex@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
|
||||
@@ -447,7 +431,7 @@ arrify@^1.0.0, arrify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
|
||||
|
||||
asap@^2.0.0, asap@~2.0.3:
|
||||
asap@~2.0.3:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
||||
|
||||
@@ -1249,15 +1233,11 @@ block-stream@*:
|
||||
dependencies:
|
||||
inherits "~2.0.0"
|
||||
|
||||
bluebird@2.9.24:
|
||||
version "2.9.24"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.9.24.tgz#14a2e75f0548323dc35aa440d92007ca154e967c"
|
||||
|
||||
bluebird@3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
|
||||
|
||||
bluebird@3.5.1, bluebird@^3.0.6, bluebird@^3.3.4, bluebird@^3.4.6, bluebird@^3.5.0:
|
||||
bluebird@^3.0.6, bluebird@^3.3.4, bluebird@^3.4.6, bluebird@^3.5.0:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
|
||||
|
||||
@@ -1629,14 +1609,6 @@ chain-function@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc"
|
||||
|
||||
chalk@2.3.0, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
|
||||
dependencies:
|
||||
ansi-styles "^3.1.0"
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^4.0.0"
|
||||
|
||||
chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
|
||||
@@ -1647,6 +1619,14 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
|
||||
strip-ansi "^3.0.0"
|
||||
supports-color "^2.0.0"
|
||||
|
||||
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
|
||||
dependencies:
|
||||
ansi-styles "^3.1.0"
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^4.0.0"
|
||||
|
||||
change-emitter@^0.1.2:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515"
|
||||
@@ -1665,32 +1645,10 @@ charenc@~0.0.1:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
|
||||
|
||||
chdir-promise@0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/chdir-promise/-/chdir-promise-0.4.1.tgz#1888bb33719699c9fb72138c07556503c4913e85"
|
||||
dependencies:
|
||||
check-more-types "2.24.0"
|
||||
debug "2.6.8"
|
||||
lazy-ass "1.6.0"
|
||||
q "1.5.0"
|
||||
spots "0.5.0"
|
||||
|
||||
check-error@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
|
||||
|
||||
check-more-types@2.23.0:
|
||||
version "2.23.0"
|
||||
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.23.0.tgz#6226264d30b1095aa1c0a5b874edbdd5d2d0a66f"
|
||||
|
||||
check-more-types@2.24.0:
|
||||
version "2.24.0"
|
||||
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
|
||||
|
||||
check-more-types@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.3.0.tgz#b8397c69dc92a3e645f18932c045b09c74419ec4"
|
||||
|
||||
cheerio@^0.20.0:
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.20.0.tgz#5c710f2bab95653272842ba01c6ea61b3545ec35"
|
||||
@@ -1779,28 +1737,18 @@ cli-boxes@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
|
||||
|
||||
cli-cursor@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
|
||||
dependencies:
|
||||
restore-cursor "^1.0.1"
|
||||
|
||||
cli-cursor@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
|
||||
dependencies:
|
||||
restore-cursor "^2.0.0"
|
||||
|
||||
cli-table@0.3.1, cli-table@^0.3.1:
|
||||
cli-table@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
|
||||
dependencies:
|
||||
colors "1.0.3"
|
||||
|
||||
cli-width@^1.0.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-1.1.1.tgz#a4d293ef67ebb7b88d4a4d42c0ccf00c4d1e366d"
|
||||
|
||||
cli-width@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
|
||||
@@ -1911,14 +1859,10 @@ colors@1.0.3, colors@1.0.x:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
|
||||
|
||||
colors@1.1.2, colors@^1.1.2, colors@~1.1.2:
|
||||
colors@^1.1.2, colors@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
|
||||
|
||||
colors@~0.6.0-1:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc"
|
||||
|
||||
combined-stream@^1.0.5, combined-stream@~1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
|
||||
@@ -1931,10 +1875,6 @@ combined-stream@~0.0.4:
|
||||
dependencies:
|
||||
delayed-stream "0.0.5"
|
||||
|
||||
commander@2.11.0:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
|
||||
|
||||
commander@2.8.x:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
|
||||
@@ -1951,10 +1891,6 @@ commander@^2.11.0, commander@^2.9.0:
|
||||
version "2.12.2"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555"
|
||||
|
||||
commander@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.1.0.tgz#d121bbae860d9992a3d517ba96f56588e47c6781"
|
||||
|
||||
commander@~2.13.0:
|
||||
version "2.13.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
|
||||
@@ -2072,19 +2008,6 @@ content-type@~1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||
|
||||
conventional-commit-message@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/conventional-commit-message/-/conventional-commit-message-1.1.0.tgz#ece8c661a168e983692e1d5a14875acb59510f6a"
|
||||
dependencies:
|
||||
check-more-types "2.3.0"
|
||||
cz-conventional-changelog "1.1.5"
|
||||
lazy-ass "1.3.0"
|
||||
word-wrap "1.1.0"
|
||||
|
||||
conventional-commit-types@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/conventional-commit-types/-/conventional-commit-types-2.2.0.tgz#5db95739d6c212acbe7b6f656a11b940baa68946"
|
||||
|
||||
convert-source-map@^1.4.0, convert-source-map@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
|
||||
@@ -2374,26 +2297,6 @@ cyclist@~0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
|
||||
|
||||
cz-conventional-changelog@1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/cz-conventional-changelog/-/cz-conventional-changelog-1.1.5.tgz#0a4d1550c4e2fb6a3aed8f6cd858c21760e119b8"
|
||||
dependencies:
|
||||
word-wrap "^1.0.3"
|
||||
|
||||
cz-conventional-changelog@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cz-conventional-changelog/-/cz-conventional-changelog-2.1.0.tgz#2f4bc7390e3244e4df293e6ba351e4c740a7c764"
|
||||
dependencies:
|
||||
conventional-commit-types "^2.0.0"
|
||||
lodash.map "^4.5.1"
|
||||
longest "^1.0.1"
|
||||
right-pad "^1.0.1"
|
||||
word-wrap "^1.0.3"
|
||||
|
||||
d3-helpers@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-helpers/-/d3-helpers-0.3.0.tgz#4b31dce4a2121a77336384574d893fbed5fb293d"
|
||||
|
||||
d@1:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
|
||||
@@ -2604,7 +2507,13 @@ dns-prefetch-control@0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz#60ddb457774e178f1f9415f0cabb0e85b0b300b2"
|
||||
|
||||
doctrine@^2.0.0, doctrine@^2.0.2:
|
||||
doctrine@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
doctrine@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.2.tgz#68f96ce8efc56cc42651f1faadb4f175273b0075"
|
||||
dependencies:
|
||||
@@ -2843,7 +2752,7 @@ error-ex@^1.2.0, error-ex@^1.3.1:
|
||||
dependencies:
|
||||
is-arrayish "^0.2.1"
|
||||
|
||||
es-abstract@^1.4.3, es-abstract@^1.7.0:
|
||||
es-abstract@^1.4.3:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.9.0.tgz#690829a07cae36b222e7fd9b75c0d0573eb25227"
|
||||
dependencies:
|
||||
@@ -2853,7 +2762,7 @@ es-abstract@^1.4.3, es-abstract@^1.7.0:
|
||||
is-callable "^1.1.3"
|
||||
is-regex "^1.0.4"
|
||||
|
||||
es-abstract@^1.6.1:
|
||||
es-abstract@^1.6.1, es-abstract@^1.7.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864"
|
||||
dependencies:
|
||||
@@ -2970,8 +2879,8 @@ eslint-config-prettier@^2.9.0:
|
||||
get-stdin "^5.0.1"
|
||||
|
||||
eslint-plugin-jest@^21.6.1:
|
||||
version "21.6.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-21.6.1.tgz#adca015bbdb8d23b210438ff9e1cee1dd9ec35df"
|
||||
version "21.7.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-21.7.0.tgz#651f1c6ce999af3ac59ab8bf8a376d742fd0fc23"
|
||||
|
||||
eslint-plugin-mocha@^4.11.0:
|
||||
version "4.11.0"
|
||||
@@ -2980,8 +2889,8 @@ eslint-plugin-mocha@^4.11.0:
|
||||
ramda "^0.24.1"
|
||||
|
||||
eslint-plugin-prettier@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-2.4.0.tgz#85cab0775c6d5e3344ef01e78d960f166fb93aae"
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-2.5.0.tgz#39a91dd7528eaf19cd42c0ee3f2c1f684606a05f"
|
||||
dependencies:
|
||||
fast-diff "^1.1.1"
|
||||
jest-docblock "^21.0.0"
|
||||
@@ -3156,10 +3065,6 @@ execa@^0.7.0:
|
||||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
exit-hook@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
|
||||
|
||||
expand-brackets@^0.1.4:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
|
||||
@@ -3317,13 +3222,6 @@ fd-slicer@~1.0.1:
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
figures@^1.3.5:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
|
||||
dependencies:
|
||||
escape-string-regexp "^1.0.5"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
figures@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
|
||||
@@ -3400,16 +3298,6 @@ find-cache-dir@^1.0.0:
|
||||
make-dir "^1.0.0"
|
||||
pkg-dir "^2.0.0"
|
||||
|
||||
find-parent-dir@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54"
|
||||
|
||||
find-up@2.1.0, find-up@^2.0.0, find-up@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
|
||||
dependencies:
|
||||
locate-path "^2.0.0"
|
||||
|
||||
find-up@^1.0.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
|
||||
@@ -3417,12 +3305,11 @@ find-up@^1.0.0:
|
||||
path-exists "^2.0.0"
|
||||
pinkie-promise "^2.0.0"
|
||||
|
||||
findup@0.1.5:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/findup/-/findup-0.1.5.tgz#8ad929a3393bac627957a7e5de4623b06b0e2ceb"
|
||||
find-up@^2.0.0, find-up@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
|
||||
dependencies:
|
||||
colors "~0.6.0-1"
|
||||
commander "~2.1.0"
|
||||
locate-path "^2.0.0"
|
||||
|
||||
flat-cache@^1.2.1:
|
||||
version "1.3.0"
|
||||
@@ -3683,57 +3570,6 @@ getpass@^0.1.1:
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
ggit@1.23.1:
|
||||
version "1.23.1"
|
||||
resolved "https://registry.yarnpkg.com/ggit/-/ggit-1.23.1.tgz#e513c2f222a6249a46e4d0df354b8604b5baeedb"
|
||||
dependencies:
|
||||
always-error "1.0.0"
|
||||
bluebird "3.5.0"
|
||||
chdir-promise "0.4.1"
|
||||
check-more-types "2.24.0"
|
||||
cli-table "0.3.1"
|
||||
colors "1.1.2"
|
||||
commander "2.11.0"
|
||||
d3-helpers "0.3.0"
|
||||
debug "2.6.8"
|
||||
find-up "2.1.0"
|
||||
glob "7.1.2"
|
||||
lazy-ass "1.6.0"
|
||||
lodash "3.10.1"
|
||||
moment "2.18.1"
|
||||
optimist "0.6.1"
|
||||
pluralize "6.0.0"
|
||||
q "2.0.3"
|
||||
quote "0.4.0"
|
||||
ramda "0.24.1"
|
||||
semver "5.4.1"
|
||||
|
||||
ggit@2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/ggit/-/ggit-2.4.0.tgz#b99d981f3ede2a3a8a8e4bbff578bc277d400588"
|
||||
dependencies:
|
||||
always-error "1.0.0"
|
||||
bluebird "3.5.1"
|
||||
chdir-promise "0.4.1"
|
||||
check-more-types "2.24.0"
|
||||
cli-table "0.3.1"
|
||||
colors "1.1.2"
|
||||
commander "2.11.0"
|
||||
d3-helpers "0.3.0"
|
||||
debug "3.1.0"
|
||||
find-up "2.1.0"
|
||||
glob "7.1.2"
|
||||
lazy-ass "1.6.0"
|
||||
lodash "4.17.4"
|
||||
moment "2.19.1"
|
||||
moment-timezone "0.5.13"
|
||||
optimist "0.6.1"
|
||||
pluralize "7.0.0"
|
||||
q "2.0.3"
|
||||
quote "0.4.0"
|
||||
ramda "0.25.0"
|
||||
semver "5.4.1"
|
||||
|
||||
git-up@^2.0.0:
|
||||
version "2.0.9"
|
||||
resolved "https://registry.yarnpkg.com/git-up/-/git-up-2.0.9.tgz#219bfd27c82daeead8495beb386dc18eae63636d"
|
||||
@@ -3797,17 +3633,6 @@ glob@7.1.1:
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^5.0.3:
|
||||
version "5.0.15"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
|
||||
@@ -3818,6 +3643,17 @@ glob@^5.0.3:
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
global-dirs@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"
|
||||
@@ -4204,10 +4040,6 @@ hpkp@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/hpkp/-/hpkp-2.0.0.tgz#10e142264e76215a5d30c44ec43de64dee6d1672"
|
||||
|
||||
hr@0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/hr/-/hr-0.1.3.tgz#d9aa30f5929dabfd0b65ba395938a3e184dbcafe"
|
||||
|
||||
hsts@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.1.0.tgz#cbd6c918a2385fee1dd5680bfb2b3a194c0121cc"
|
||||
@@ -4418,44 +4250,6 @@ inquirer-autocomplete-prompt@^0.12.1:
|
||||
inquirer "3.2.0"
|
||||
run-async "^2.3.0"
|
||||
|
||||
inquirer-confirm@0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/inquirer-confirm/-/inquirer-confirm-0.2.2.tgz#6f406d037bf9d9e455ef0f953929f357fe9a8848"
|
||||
dependencies:
|
||||
bluebird "2.9.24"
|
||||
inquirer "0.8.2"
|
||||
|
||||
inquirer@0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
|
||||
dependencies:
|
||||
ansi-escapes "^1.1.0"
|
||||
ansi-regex "^2.0.0"
|
||||
chalk "^1.0.0"
|
||||
cli-cursor "^1.0.1"
|
||||
cli-width "^2.0.0"
|
||||
figures "^1.3.5"
|
||||
lodash "^4.3.0"
|
||||
readline2 "^1.0.1"
|
||||
run-async "^0.1.0"
|
||||
rx-lite "^3.1.2"
|
||||
string-width "^1.0.1"
|
||||
strip-ansi "^3.0.0"
|
||||
through "^2.3.6"
|
||||
|
||||
inquirer@0.8.2:
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.8.2.tgz#41586548e1c5d9b3f81df7325034baacab6f58ab"
|
||||
dependencies:
|
||||
ansi-regex "^1.1.1"
|
||||
chalk "^1.0.0"
|
||||
cli-width "^1.0.1"
|
||||
figures "^1.3.5"
|
||||
lodash "^3.3.1"
|
||||
readline2 "^0.1.1"
|
||||
rx "^2.4.3"
|
||||
through "^2.3.6"
|
||||
|
||||
inquirer@3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.2.0.tgz#45b44c2160c729d7578c54060b3eed94487bb42b"
|
||||
@@ -4475,7 +4269,7 @@ inquirer@3.2.0:
|
||||
strip-ansi "^4.0.0"
|
||||
through "^2.3.6"
|
||||
|
||||
inquirer@3.3.0, inquirer@^3.0.6, inquirer@^3.2.2:
|
||||
inquirer@^3.0.6, inquirer@^3.2.2:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
|
||||
dependencies:
|
||||
@@ -5417,31 +5211,12 @@ kue@0.11.6:
|
||||
optionalDependencies:
|
||||
reds "^0.2.5"
|
||||
|
||||
largest-semantic-change@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/largest-semantic-change/-/largest-semantic-change-1.0.0.tgz#25dc538bdaaa8bbdc30276b1ebf902d47a34bf0e"
|
||||
dependencies:
|
||||
check-more-types "2.23.0"
|
||||
lazy-ass "1.5.0"
|
||||
|
||||
latest-version@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
|
||||
dependencies:
|
||||
package-json "^4.0.0"
|
||||
|
||||
lazy-ass@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.3.0.tgz#7d0d14eef3ec9702c6f30c60ea81f1a8d3f900fb"
|
||||
|
||||
lazy-ass@1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.5.0.tgz#ca15be243c7c475b8565cdbfa0f9c2f374f2a01d"
|
||||
|
||||
lazy-ass@1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513"
|
||||
|
||||
lazy-cache@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
|
||||
@@ -5732,10 +5507,6 @@ lodash.keysin@^4.0.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.keysin/-/lodash.keysin-4.2.0.tgz#8cc3fb35c2d94acc443a1863e02fa40799ea6f28"
|
||||
|
||||
lodash.map@^4.5.1:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"
|
||||
|
||||
lodash.memoize@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||
@@ -5796,11 +5567,7 @@ lodash.values@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347"
|
||||
|
||||
lodash@3.10.1, lodash@^3.3.1:
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
|
||||
|
||||
lodash@4.17.4, lodash@^4.0.0, lodash@^4.1.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
|
||||
lodash@^4.0.0, lodash@^4.1.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
|
||||
version "4.17.4"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
|
||||
|
||||
@@ -6115,24 +5882,14 @@ mocha@^3.1.2:
|
||||
mkdirp "0.5.1"
|
||||
supports-color "3.1.2"
|
||||
|
||||
moment-timezone@0.5.13:
|
||||
version "0.5.13"
|
||||
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.13.tgz#99ce5c7d827262eb0f1f702044177f60745d7b90"
|
||||
dependencies:
|
||||
moment ">= 2.9.0"
|
||||
|
||||
moment@2.18.1:
|
||||
version "2.18.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
|
||||
|
||||
moment@2.19.1, moment@^2.10.3:
|
||||
version "2.19.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.1.tgz#56da1a2d1cbf01d38b7e1afc31c10bcfa1929167"
|
||||
|
||||
moment@2.x.x, "moment@>= 2.9.0":
|
||||
moment@2.x.x:
|
||||
version "2.19.4"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.4.tgz#17e5e2c6ead8819c8ecfad83a0acccb312e94682"
|
||||
|
||||
moment@^2.10.3:
|
||||
version "2.19.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.1.tgz#56da1a2d1cbf01d38b7e1afc31c10bcfa1929167"
|
||||
|
||||
moment@^2.18.1:
|
||||
version "2.20.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd"
|
||||
@@ -6232,14 +5989,6 @@ murmurhash-js@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51"
|
||||
|
||||
mute-stream@0.0.4:
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.4.tgz#a9219960a6d5d5d046597aee51252c6655f7177e"
|
||||
|
||||
mute-stream@0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
|
||||
|
||||
mute-stream@0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||
@@ -6678,10 +6427,6 @@ once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0:
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
onetime@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
|
||||
|
||||
onetime@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
|
||||
@@ -6728,6 +6473,10 @@ os-locale@^2.0.0:
|
||||
lcid "^1.0.0"
|
||||
mem "^1.1.0"
|
||||
|
||||
os-shim@^0.1.2:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917"
|
||||
|
||||
os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||
@@ -7038,21 +6787,13 @@ platform@1.3.4:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.4.tgz#6f0fb17edaaa48f21442b3a975c063130f1c3ebd"
|
||||
|
||||
pluralize@6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-6.0.0.tgz#d9b51afad97d3d51075cc1ddba9b132cacccb7ba"
|
||||
|
||||
pluralize@7.0.0, pluralize@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
|
||||
|
||||
pluralize@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
|
||||
|
||||
pop-iterate@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pop-iterate/-/pop-iterate-1.0.1.tgz#ceacfdab4abf353d7a0f2aaa2c1fc7b3f9413ba3"
|
||||
pluralize@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
|
||||
|
||||
popsicle@^6.2.0:
|
||||
version "6.2.2"
|
||||
@@ -7477,24 +7218,13 @@ postcss@^6.0.1:
|
||||
source-map "^0.6.1"
|
||||
supports-color "^4.4.0"
|
||||
|
||||
pre-git@^3.16.0:
|
||||
version "3.16.0"
|
||||
resolved "https://registry.yarnpkg.com/pre-git/-/pre-git-3.16.0.tgz#a7656bc5f277185fd213c78f39f24f2cb603eb61"
|
||||
pre-commit@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/pre-commit/-/pre-commit-1.2.2.tgz#dbcee0ee9de7235e57f79c56d7ce94641a69eec6"
|
||||
dependencies:
|
||||
bluebird "3.5.1"
|
||||
chalk "2.3.0"
|
||||
check-more-types "2.24.0"
|
||||
conventional-commit-message "1.1.0"
|
||||
cz-conventional-changelog "2.1.0"
|
||||
debug "2.6.9"
|
||||
ggit "2.4.0"
|
||||
inquirer "3.3.0"
|
||||
lazy-ass "1.6.0"
|
||||
require-relative "0.8.7"
|
||||
shelljs "0.7.8"
|
||||
simple-commit-message "3.3.2"
|
||||
validate-commit-msg "2.14.0"
|
||||
word-wrap "1.2.3"
|
||||
cross-spawn "^5.0.1"
|
||||
spawn-sync "^1.0.15"
|
||||
which "1.2.x"
|
||||
|
||||
prebuild-install@^2.3.0:
|
||||
version "2.3.0"
|
||||
@@ -7785,18 +7515,10 @@ q@1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e"
|
||||
|
||||
q@1.5.0, q@^1.1.2:
|
||||
q@^1.1.2:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
|
||||
|
||||
q@2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/q/-/q-2.0.3.tgz#75b8db0255a1a5af82f58c3f3aaa1efec7d0d134"
|
||||
dependencies:
|
||||
asap "^2.0.0"
|
||||
pop-iterate "^1.0.1"
|
||||
weak-map "^1.0.5"
|
||||
|
||||
qs@6.5.1, qs@^6.1.0, qs@^6.2.0, qs@~6.5.1:
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
|
||||
@@ -7832,10 +7554,6 @@ querystring@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
|
||||
|
||||
quote@0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/quote/-/quote-0.4.0.tgz#10839217f6c1362b89194044d29b233fd7f32f01"
|
||||
|
||||
raf@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
|
||||
@@ -7846,14 +7564,10 @@ railroad-diagrams@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
|
||||
|
||||
ramda@0.24.1, ramda@^0.24.1:
|
||||
ramda@^0.24.1:
|
||||
version "0.24.1"
|
||||
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.24.1.tgz#c3b7755197f35b8dc3502228262c4c91ddb6b857"
|
||||
|
||||
ramda@0.25.0:
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9"
|
||||
|
||||
randexp@^0.4.2:
|
||||
version "0.4.6"
|
||||
resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
|
||||
@@ -8159,21 +7873,6 @@ readdirp@^2.0.0:
|
||||
readable-stream "^2.0.2"
|
||||
set-immediate-shim "^1.0.1"
|
||||
|
||||
readline2@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/readline2/-/readline2-0.1.1.tgz#99443ba6e83b830ef3051bfd7dc241a82728d568"
|
||||
dependencies:
|
||||
mute-stream "0.0.4"
|
||||
strip-ansi "^2.0.1"
|
||||
|
||||
readline2@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35"
|
||||
dependencies:
|
||||
code-point-at "^1.0.0"
|
||||
is-fullwidth-code-point "^1.0.0"
|
||||
mute-stream "0.0.5"
|
||||
|
||||
rechoir@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
|
||||
@@ -8425,10 +8124,6 @@ require-main-filename@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
|
||||
|
||||
require-relative@0.8.7:
|
||||
version "0.8.7"
|
||||
resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de"
|
||||
|
||||
require-uncached@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
|
||||
@@ -8467,13 +8162,6 @@ resolve@^1.1.7:
|
||||
dependencies:
|
||||
path-parse "^1.0.5"
|
||||
|
||||
restore-cursor@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
|
||||
dependencies:
|
||||
exit-hook "^1.0.0"
|
||||
onetime "^1.0.0"
|
||||
|
||||
restore-cursor@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
|
||||
@@ -8495,10 +8183,6 @@ right-align@^0.1.1:
|
||||
dependencies:
|
||||
align-text "^0.1.1"
|
||||
|
||||
right-pad@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/right-pad/-/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0"
|
||||
|
||||
rimraf@2, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
|
||||
@@ -8525,12 +8209,6 @@ rst-selector-parser@^2.2.3:
|
||||
lodash.flattendeep "^4.4.0"
|
||||
nearley "^2.7.10"
|
||||
|
||||
run-async@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
|
||||
run-async@^2.2.0, run-async@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
|
||||
@@ -8553,14 +8231,6 @@ rx-lite@*, rx-lite@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
|
||||
|
||||
rx-lite@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
|
||||
|
||||
rx@^2.4.3:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/rx/-/rx-2.5.3.tgz#21adc7d80f02002af50dae97fd9dbf248755f566"
|
||||
|
||||
safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
||||
@@ -8640,15 +8310,11 @@ semver-diff@^2.0.0:
|
||||
dependencies:
|
||||
semver "^5.0.3"
|
||||
|
||||
semver-regex@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-1.0.0.tgz#92a4969065f9c70c694753d55248fc68f8f652c9"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.3.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
|
||||
|
||||
semver@5.4.1, semver@^5.0.3, semver@^5.1.0, semver@^5.4.1:
|
||||
semver@^5.0.3, semver@^5.1.0, semver@^5.4.1:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
|
||||
|
||||
@@ -8764,7 +8430,7 @@ shell-quote@^1.6.1:
|
||||
array-reduce "~0.0.0"
|
||||
jsonify "~0.0.0"
|
||||
|
||||
shelljs@0.7.8, shelljs@^0.7.0:
|
||||
shelljs@^0.7.0:
|
||||
version "0.7.8"
|
||||
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3"
|
||||
dependencies:
|
||||
@@ -8780,22 +8446,6 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
|
||||
|
||||
simple-commit-message@3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/simple-commit-message/-/simple-commit-message-3.3.2.tgz#52bdadb7f4f680d8b29c07af1826a6611f7ee783"
|
||||
dependencies:
|
||||
am-i-a-dependency "1.1.2"
|
||||
check-more-types "2.24.0"
|
||||
debug "2.6.9"
|
||||
ggit "1.23.1"
|
||||
hr "0.1.3"
|
||||
inquirer "0.12.0"
|
||||
inquirer-confirm "0.2.2"
|
||||
largest-semantic-change "1.0.0"
|
||||
lazy-ass "1.6.0"
|
||||
semver "5.4.1"
|
||||
word-wrap "1.2.3"
|
||||
|
||||
simple-get@^1.4.2:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-1.4.3.tgz#e9755eda407e96da40c5e5158c9ea37b33becbeb"
|
||||
@@ -8956,6 +8606,13 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
|
||||
spawn-sync@^1.0.15:
|
||||
version "1.0.15"
|
||||
resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476"
|
||||
dependencies:
|
||||
concat-stream "^1.4.7"
|
||||
os-shim "^0.1.2"
|
||||
|
||||
spdx-correct@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
|
||||
@@ -8976,10 +8633,6 @@ split@0.3:
|
||||
dependencies:
|
||||
through "2"
|
||||
|
||||
spots@0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/spots/-/spots-0.5.0.tgz#b7aa0f1ac389a5a6d57c21e98da1d53839405fe1"
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
@@ -9108,12 +8761,6 @@ stringstream@~0.0.4, stringstream@~0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
|
||||
|
||||
strip-ansi@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-2.0.1.tgz#df62c1aa94ed2f114e1d0f21fd1d50482b79a60e"
|
||||
dependencies:
|
||||
ansi-regex "^1.0.0"
|
||||
|
||||
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
|
||||
@@ -9738,15 +9385,6 @@ v8flags@^2.1.1:
|
||||
dependencies:
|
||||
user-home "^1.1.1"
|
||||
|
||||
validate-commit-msg@2.14.0:
|
||||
version "2.14.0"
|
||||
resolved "https://registry.yarnpkg.com/validate-commit-msg/-/validate-commit-msg-2.14.0.tgz#e5383691012cbb270dcc0bc2a4effebe14890eac"
|
||||
dependencies:
|
||||
conventional-commit-types "^2.0.0"
|
||||
find-parent-dir "^0.3.0"
|
||||
findup "0.1.5"
|
||||
semver-regex "1.0.0"
|
||||
|
||||
validate-npm-package-license@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
|
||||
@@ -9807,10 +9445,6 @@ watchpack@^1.4.0:
|
||||
chokidar "^1.7.0"
|
||||
graceful-fs "^4.1.2"
|
||||
|
||||
weak-map@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/weak-map/-/weak-map-1.0.5.tgz#79691584d98607f5070bd3b70a40e6bb22e401eb"
|
||||
|
||||
webidl-conversions@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-2.0.1.tgz#3bf8258f7d318c7443c36f2e169402a1a6703506"
|
||||
@@ -9898,6 +9532,12 @@ which@1, which@^1.2.12, which@^1.2.9:
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
which@1.2.x:
|
||||
version "1.2.14"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5"
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
wide-align@^1.1.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
|
||||
@@ -9940,14 +9580,6 @@ with@^5.0.0:
|
||||
acorn "^3.1.0"
|
||||
acorn-globals "^3.0.0"
|
||||
|
||||
word-wrap@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.1.0.tgz#356153d61d10610d600785c5d701288e0ae764a6"
|
||||
|
||||
word-wrap@1.2.3, word-wrap@^1.0.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||
|
||||
wordwrap@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
|
||||
|
||||
Reference in New Issue
Block a user